mirror of
https://github.com/ZeroCatDev/Classworks.git
synced 2025-12-07 13:03:59 +00:00
规范代码格式
This commit is contained in:
parent
0af2c4cc63
commit
76c2dba502
15
src/App.vue
15
src/App.vue
@ -3,8 +3,8 @@
|
||||
<!-- 正常路由 -->
|
||||
<router-view v-slot="{ Component, route }">
|
||||
<transition
|
||||
name="md3"
|
||||
mode="out-in"
|
||||
name="md3"
|
||||
>
|
||||
<component
|
||||
:is="Component"
|
||||
@ -12,17 +12,18 @@
|
||||
/>
|
||||
</transition>
|
||||
</router-view>
|
||||
<global-message />
|
||||
<rate-limit-modal />
|
||||
<global-message/>
|
||||
<rate-limit-modal/>
|
||||
</v-app>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted } from "vue";
|
||||
import { useTheme } from "vuetify";
|
||||
import { getSetting } from "@/utils/settings";
|
||||
import {onMounted} from "vue";
|
||||
import {useTheme} from "vuetify";
|
||||
import {getSetting} from "@/utils/settings";
|
||||
import RateLimitModal from "@/components/RateLimitModal.vue";
|
||||
import Clarity from "@microsoft/clarity";
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
onMounted(() => {
|
||||
@ -44,7 +45,7 @@ onMounted(() => {
|
||||
.md3-enter-active,
|
||||
.md3-leave-active {
|
||||
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 {
|
||||
|
||||
@ -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">
|
||||
<g clip-path="url(#clip-path-74_1)">
|
||||
<path fill="#FFFFFF" d="M0 256L256 256L256 0L0 0L0 256Z">
|
||||
</path>
|
||||
<rect x="0" y="0" width="256" height="128" fill="#D8C4A0" >
|
||||
</rect>
|
||||
<rect x="0" y="128" width="256" height="128" fill="#F5E0BB" >
|
||||
</rect>
|
||||
<path d="M28 228L128 128L228 128L128 228L28 228Z" fill-rule="evenodd" fill="#241A04" >
|
||||
</path>
|
||||
<path d="M28 128L128 28L228 28L128 128L28 128Z" fill-rule="evenodd" fill="#52452A" >
|
||||
</path>
|
||||
<g >
|
||||
<path fill="#000000" d="M-3049.01 2467.94L-3043.48 2467.94L-3043.48 2466.99L-3045.92 2466.99C-3046.36 2466.99 -3046.9 2467.04 -3047.36 2467.08C-3045.29 2465.12 -3043.9 2463.33 -3043.9 2461.57C-3043.9 2460.01 -3044.9 2458.99 -3046.47 2458.99C-3047.58 2458.99 -3048.35 2459.49 -3049.06 2460.27L-3048.43 2460.9C-3047.93 2460.31 -3047.32 2459.88 -3046.6 2459.88C-3045.51 2459.88 -3044.98 2460.61 -3044.98 2461.62C-3044.98 2463.13 -3046.25 2464.88 -3049.01 2467.29L-3049.01 2467.94ZM-3039.27 2468.1C-3037.9 2468.1 -3036.74 2466.95 -3036.74 2465.24C-3036.74 2463.39 -3037.7 2462.48 -3039.19 2462.48C-3039.87 2462.48 -3040.64 2462.88 -3041.18 2463.54C-3041.13 2460.81 -3040.13 2459.89 -3038.91 2459.89C-3038.38 2459.89 -3037.85 2460.15 -3037.52 2460.56L-3036.89 2459.89C-3037.39 2459.36 -3038.04 2458.99 -3038.96 2458.99C-3040.66 2458.99 -3042.21 2460.3 -3042.21 2463.74C-3042.21 2466.65 -3040.95 2468.1 -3039.27 2468.1ZM-3041.15 2464.41C-3040.58 2463.6 -3039.91 2463.3 -3039.36 2463.3C-3038.3 2463.3 -3037.78 2464.05 -3037.78 2465.24C-3037.78 2466.44 -3038.43 2467.23 -3039.27 2467.23C-3040.37 2467.23 -3041.03 2466.24 -3041.15 2464.41ZM-3035.17 2467.94L-3030.34 2467.94L-3030.34 2467.03L-3032.1 2467.03L-3032.1 2459.15L-3032.95 2459.15C-3033.43 2459.42 -3033.99 2459.62 -3034.77 2459.77L-3034.77 2460.47L-3033.2 2460.47L-3033.2 2467.03L-3035.17 2467.03L-3035.17 2467.94ZM-3029.51 2467.94L-3028.4 2467.94L-3027.54 2465.25L-3024.33 2465.25L-3023.49 2467.94L-3022.31 2467.94L-3025.3 2459.15L-3026.54 2459.15L-3029.51 2467.94ZM-3027.27 2464.38L-3026.84 2463.02C-3026.52 2462.02 -3026.24 2461.08 -3025.96 2460.04L-3025.91 2460.04C-3025.62 2461.06 -3025.35 2462.02 -3025.02 2463.02L-3024.6 2464.38L-3027.27 2464.38ZM-3018.93 2468.1C-3017.26 2468.1 -3016.19 2466.58 -3016.19 2463.51C-3016.19 2460.47 -3017.26 2458.99 -3018.93 2458.99C-3020.61 2458.99 -3021.67 2460.47 -3021.67 2463.51C-3021.67 2466.58 -3020.61 2468.1 -3018.93 2468.1ZM-3018.93 2467.21C-3019.93 2467.21 -3020.61 2466.09 -3020.61 2463.51C-3020.61 2460.95 -3019.93 2459.85 -3018.93 2459.85C-3017.93 2459.85 -3017.25 2460.95 -3017.25 2463.51C-3017.25 2466.09 -3017.93 2467.21 -3018.93 2467.21ZM-3012.27 2468.1C-3010.6 2468.1 -3009.53 2466.58 -3009.53 2463.51C-3009.53 2460.47 -3010.6 2458.99 -3012.27 2458.99C-3013.95 2458.99 -3015.01 2460.47 -3015.01 2463.51C-3015.01 2466.58 -3013.95 2468.1 -3012.27 2468.1ZM-3012.27 2467.21C-3013.27 2467.21 -3013.95 2466.09 -3013.95 2463.51C-3013.95 2460.95 -3013.27 2459.85 -3012.27 2459.85C-3011.27 2459.85 -3010.59 2460.95 -3010.59 2463.51C-3010.59 2466.09 -3011.27 2467.21 -3012.27 2467.21Z">
|
||||
</path>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip-path-74_1">
|
||||
<path d="M0 256L256 256L256 0L0 0L0 256Z" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
<svg xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" width="256" height="256"
|
||||
viewBox="0 0 256 256" fill="none">
|
||||
<g clip-path="url(#clip-path-74_1)">
|
||||
<path fill="#FFFFFF" d="M0 256L256 256L256 0L0 0L0 256Z">
|
||||
</path>
|
||||
<rect x="0" y="0" width="256" height="128" fill="#D8C4A0">
|
||||
</rect>
|
||||
<rect x="0" y="128" width="256" height="128" fill="#F5E0BB">
|
||||
</rect>
|
||||
<path d="M28 228L128 128L228 128L128 228L28 228Z" fill-rule="evenodd" fill="#241A04">
|
||||
</path>
|
||||
<path d="M28 128L128 28L228 28L128 128L28 128Z" fill-rule="evenodd" fill="#52452A">
|
||||
</path>
|
||||
<g>
|
||||
<path fill="#000000"
|
||||
d="M-3049.01 2467.94L-3043.48 2467.94L-3043.48 2466.99L-3045.92 2466.99C-3046.36 2466.99 -3046.9 2467.04 -3047.36 2467.08C-3045.29 2465.12 -3043.9 2463.33 -3043.9 2461.57C-3043.9 2460.01 -3044.9 2458.99 -3046.47 2458.99C-3047.58 2458.99 -3048.35 2459.49 -3049.06 2460.27L-3048.43 2460.9C-3047.93 2460.31 -3047.32 2459.88 -3046.6 2459.88C-3045.51 2459.88 -3044.98 2460.61 -3044.98 2461.62C-3044.98 2463.13 -3046.25 2464.88 -3049.01 2467.29L-3049.01 2467.94ZM-3039.27 2468.1C-3037.9 2468.1 -3036.74 2466.95 -3036.74 2465.24C-3036.74 2463.39 -3037.7 2462.48 -3039.19 2462.48C-3039.87 2462.48 -3040.64 2462.88 -3041.18 2463.54C-3041.13 2460.81 -3040.13 2459.89 -3038.91 2459.89C-3038.38 2459.89 -3037.85 2460.15 -3037.52 2460.56L-3036.89 2459.89C-3037.39 2459.36 -3038.04 2458.99 -3038.96 2458.99C-3040.66 2458.99 -3042.21 2460.3 -3042.21 2463.74C-3042.21 2466.65 -3040.95 2468.1 -3039.27 2468.1ZM-3041.15 2464.41C-3040.58 2463.6 -3039.91 2463.3 -3039.36 2463.3C-3038.3 2463.3 -3037.78 2464.05 -3037.78 2465.24C-3037.78 2466.44 -3038.43 2467.23 -3039.27 2467.23C-3040.37 2467.23 -3041.03 2466.24 -3041.15 2464.41ZM-3035.17 2467.94L-3030.34 2467.94L-3030.34 2467.03L-3032.1 2467.03L-3032.1 2459.15L-3032.95 2459.15C-3033.43 2459.42 -3033.99 2459.62 -3034.77 2459.77L-3034.77 2460.47L-3033.2 2460.47L-3033.2 2467.03L-3035.17 2467.03L-3035.17 2467.94ZM-3029.51 2467.94L-3028.4 2467.94L-3027.54 2465.25L-3024.33 2465.25L-3023.49 2467.94L-3022.31 2467.94L-3025.3 2459.15L-3026.54 2459.15L-3029.51 2467.94ZM-3027.27 2464.38L-3026.84 2463.02C-3026.52 2462.02 -3026.24 2461.08 -3025.96 2460.04L-3025.91 2460.04C-3025.62 2461.06 -3025.35 2462.02 -3025.02 2463.02L-3024.6 2464.38L-3027.27 2464.38ZM-3018.93 2468.1C-3017.26 2468.1 -3016.19 2466.58 -3016.19 2463.51C-3016.19 2460.47 -3017.26 2458.99 -3018.93 2458.99C-3020.61 2458.99 -3021.67 2460.47 -3021.67 2463.51C-3021.67 2466.58 -3020.61 2468.1 -3018.93 2468.1ZM-3018.93 2467.21C-3019.93 2467.21 -3020.61 2466.09 -3020.61 2463.51C-3020.61 2460.95 -3019.93 2459.85 -3018.93 2459.85C-3017.93 2459.85 -3017.25 2460.95 -3017.25 2463.51C-3017.25 2466.09 -3017.93 2467.21 -3018.93 2467.21ZM-3012.27 2468.1C-3010.6 2468.1 -3009.53 2466.58 -3009.53 2463.51C-3009.53 2460.47 -3010.6 2458.99 -3012.27 2458.99C-3013.95 2458.99 -3015.01 2460.47 -3015.01 2463.51C-3015.01 2466.58 -3013.95 2468.1 -3012.27 2468.1ZM-3012.27 2467.21C-3013.27 2467.21 -3013.95 2466.09 -3013.95 2463.51C-3013.95 2460.95 -3013.27 2459.85 -3012.27 2459.85C-3011.27 2459.85 -3010.59 2460.95 -3010.59 2463.51C-3010.59 2466.09 -3011.27 2467.21 -3012.27 2467.21Z">
|
||||
</path>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip-path-74_1">
|
||||
<path d="M0 256L256 256L256 0L0 0L0 256Z" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 3.4 KiB |
@ -1,8 +1,8 @@
|
||||
import axios from "axios";
|
||||
import { getSetting } from "@/utils/settings";
|
||||
import { parseRateLimit } from "ratelimit-header-parser";
|
||||
import {getSetting} from "@/utils/settings";
|
||||
import {parseRateLimit} from "ratelimit-header-parser";
|
||||
import RateLimitModal from "@/components/RateLimitModal.vue";
|
||||
import { Base64 } from "js-base64";
|
||||
import {Base64} from "js-base64";
|
||||
|
||||
// 基本配置
|
||||
const axiosInstance = axios.create({
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
<template>
|
||||
<v-app-bar :elevation="2">
|
||||
<template v-slot:prepend>
|
||||
<v-app-bar-nav-icon></v-app-bar-nav-icon>
|
||||
</template>
|
||||
<v-app-bar :elevation="2">
|
||||
<template v-slot:prepend>
|
||||
<v-app-bar-nav-icon></v-app-bar-nav-icon>
|
||||
</template>
|
||||
|
||||
<v-app-bar-title>作业</v-app-bar-title>
|
||||
</v-app-bar>
|
||||
<v-app-bar-title>作业</v-app-bar-title>
|
||||
</v-app-bar>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
@ -3,16 +3,16 @@
|
||||
<v-card-title class="d-flex align-center">
|
||||
<span>缓存管理</span>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="error" @click="clearAllCaches" :loading="loading">
|
||||
<v-btn :loading="loading" color="error" @click="clearAllCaches">
|
||||
清除所有缓存
|
||||
</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-btn>
|
||||
</v-card-title>
|
||||
|
||||
<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 未激活,缓存管理功能不可用。
|
||||
</v-alert>
|
||||
|
||||
@ -30,7 +30,7 @@
|
||||
</v-expansion-panel-title>
|
||||
<v-expansion-panel-text>
|
||||
<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>
|
||||
</div>
|
||||
@ -43,7 +43,7 @@
|
||||
{{ url }}
|
||||
</v-list-item-subtitle>
|
||||
<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-btn>
|
||||
</template>
|
||||
@ -53,7 +53,7 @@
|
||||
</v-expansion-panel>
|
||||
</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">
|
||||
没有找到缓存数据。
|
||||
@ -99,7 +99,7 @@ export default {
|
||||
|
||||
try {
|
||||
// 获取所有缓存名称
|
||||
const cacheNames = await this.sendMessageToSW({ type: 'CACHE_KEYS' });
|
||||
const cacheNames = await this.sendMessageToSW({type: 'CACHE_KEYS'});
|
||||
|
||||
// 获取每个缓存的内容
|
||||
for (const cacheName of cacheNames.cacheNames) {
|
||||
@ -167,7 +167,7 @@ export default {
|
||||
|
||||
this.loading = true;
|
||||
try {
|
||||
const result = await this.sendMessageToSW({ type: 'CLEAR_ALL_CACHES' });
|
||||
const result = await this.sendMessageToSW({type: 'CLEAR_ALL_CACHES'});
|
||||
|
||||
if (result.success) {
|
||||
this.showMessage('已清除所有缓存', 'success');
|
||||
|
||||
@ -2,12 +2,12 @@
|
||||
<!-- Floating toggle button -->
|
||||
<div
|
||||
v-if="showToggleButton"
|
||||
class="chat-toggle"
|
||||
:style="toggleStyle"
|
||||
class="chat-toggle"
|
||||
>
|
||||
<v-btn
|
||||
icon
|
||||
color="primary"
|
||||
icon
|
||||
variant="flat"
|
||||
@click="open()"
|
||||
>
|
||||
@ -27,26 +27,26 @@
|
||||
<!-- Chat panel -->
|
||||
<div
|
||||
v-show="visible"
|
||||
class="chat-panel"
|
||||
:style="panelStyle"
|
||||
class="chat-panel"
|
||||
>
|
||||
<v-card
|
||||
border
|
||||
elevation="8"
|
||||
class="chat-card"
|
||||
elevation="8"
|
||||
>
|
||||
<v-card-title class="d-flex align-center">
|
||||
<v-icon class="mr-2">
|
||||
mdi-chat-processing
|
||||
</v-icon>
|
||||
<span class="text-subtitle-1">设备聊天室</span>
|
||||
<v-spacer />
|
||||
<v-spacer/>
|
||||
<v-tooltip location="top">
|
||||
<template #activator="{ props }">
|
||||
<v-chip
|
||||
v-bind="props"
|
||||
size="x-small"
|
||||
:color="connected ? 'success' : 'grey'"
|
||||
size="x-small"
|
||||
v-bind="props"
|
||||
variant="tonal"
|
||||
>
|
||||
{{ connected ? '已连接' : '未连接' }}
|
||||
@ -63,7 +63,7 @@
|
||||
</v-btn>
|
||||
</v-card-title>
|
||||
|
||||
<v-divider />
|
||||
<v-divider/>
|
||||
|
||||
<v-card-text class="chat-body">
|
||||
<div
|
||||
@ -78,21 +78,21 @@
|
||||
v-if="msg._type === 'divider'"
|
||||
class="divider-row"
|
||||
>
|
||||
<v-divider class="my-2" />
|
||||
<v-divider class="my-2"/>
|
||||
<div class="divider-text">
|
||||
今天 - 上次访问
|
||||
</div>
|
||||
<v-divider class="my-2" />
|
||||
<v-divider class="my-2"/>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="message-row"
|
||||
:class="{ self: msg.self }"
|
||||
class="message-row"
|
||||
>
|
||||
<div class="avatar">
|
||||
<v-avatar
|
||||
size="24"
|
||||
:color="msg.self ? 'primary' : 'grey'"
|
||||
size="24"
|
||||
>
|
||||
<v-icon size="small">
|
||||
{{ msg.self ? 'mdi-account' : 'mdi-account-outline' }}
|
||||
@ -112,13 +112,13 @@
|
||||
</div>
|
||||
</v-card-text>
|
||||
|
||||
<v-divider />
|
||||
<v-divider/>
|
||||
|
||||
<v-card-actions class="chat-input">
|
||||
<v-btn
|
||||
class="mr-1"
|
||||
icon
|
||||
variant="text"
|
||||
class="mr-1"
|
||||
@click="insertEmoji('😄')"
|
||||
>
|
||||
<v-icon>mdi-emoticon-outline</v-icon>
|
||||
@ -126,19 +126,19 @@
|
||||
<v-textarea
|
||||
ref="inputRef"
|
||||
v-model="text"
|
||||
class="flex-grow-1"
|
||||
rows="1"
|
||||
auto-grow
|
||||
variant="solo"
|
||||
class="flex-grow-1"
|
||||
hide-details
|
||||
placeholder="输入消息"
|
||||
rows="1"
|
||||
variant="solo"
|
||||
@keydown.enter.prevent="handleEnter"
|
||||
@keydown.shift.enter.stop
|
||||
/>
|
||||
<v-btn
|
||||
color="primary"
|
||||
:disabled="!canSend"
|
||||
class="ml-2"
|
||||
color="primary"
|
||||
@click="send"
|
||||
>
|
||||
<v-icon start>
|
||||
@ -152,8 +152,8 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getSetting } from '@/utils/settings'
|
||||
import { getSocket, joinToken, on as socketOn } from '@/utils/socketClient'
|
||||
import {getSetting} from '@/utils/settings'
|
||||
import {getSocket, joinToken, on as socketOn} from '@/utils/socketClient'
|
||||
|
||||
export default {
|
||||
name: 'ChatWidget',
|
||||
@ -222,7 +222,7 @@ export default {
|
||||
const after = this.messages.slice(idx)
|
||||
return [
|
||||
...before,
|
||||
{ _id: 'divider', _type: 'divider' },
|
||||
{_id: 'divider', _type: 'divider'},
|
||||
...after,
|
||||
]
|
||||
},
|
||||
@ -239,7 +239,9 @@ export default {
|
||||
try {
|
||||
const stored = localStorage.getItem('chat.lastVisit')
|
||||
if (stored) this.lastVisit = stored
|
||||
} catch (e) { void e }
|
||||
} catch (e) {
|
||||
void e
|
||||
}
|
||||
|
||||
// Prepare socket
|
||||
const s = getSocket()
|
||||
@ -280,7 +282,9 @@ export default {
|
||||
this.$emit('update:modelValue', false)
|
||||
try {
|
||||
localStorage.setItem('chat.lastVisit', new Date().toISOString())
|
||||
} catch (e) { void e }
|
||||
} catch (e) {
|
||||
void e
|
||||
}
|
||||
this.unreadCount = 0
|
||||
},
|
||||
onOpen() {
|
||||
@ -340,7 +344,9 @@ export default {
|
||||
if (!el) return
|
||||
try {
|
||||
el.scrollTop = el.scrollHeight
|
||||
} catch (e) { void e }
|
||||
} catch (e) {
|
||||
void e
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
@ -351,46 +357,80 @@ export default {
|
||||
position: fixed;
|
||||
z-index: 1100;
|
||||
}
|
||||
|
||||
.chat-panel {
|
||||
position: fixed;
|
||||
z-index: 1101;
|
||||
}
|
||||
|
||||
.chat-card {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.chat-body {
|
||||
padding: 8px 12px;
|
||||
height: calc(100% - 120px);
|
||||
}
|
||||
|
||||
.messages {
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.message-row {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
.message-row.self {
|
||||
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 {
|
||||
max-width: 70%;
|
||||
background: rgba(255,255,255,0.06);
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
border-radius: 10px;
|
||||
padding: 6px 10px;
|
||||
margin: 0 8px;
|
||||
}
|
||||
|
||||
.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>
|
||||
|
||||
@ -3,11 +3,11 @@
|
||||
<!-- 错误提示 -->
|
||||
<v-alert
|
||||
v-if="error"
|
||||
type="error"
|
||||
class="mb-4 mt-3 mx-2"
|
||||
variant="tonal"
|
||||
border="start"
|
||||
class="mb-4 mt-3 mx-2"
|
||||
closable
|
||||
type="error"
|
||||
variant="tonal"
|
||||
@click:close="error = ''"
|
||||
>
|
||||
<div class="d-flex align-center">
|
||||
@ -19,11 +19,11 @@
|
||||
<!-- 成功提示 -->
|
||||
<v-alert
|
||||
v-if="success"
|
||||
type="success"
|
||||
class="mb-4 mt-3 mx-2"
|
||||
variant="tonal"
|
||||
border="start"
|
||||
class="mb-4 mt-3 mx-2"
|
||||
closable
|
||||
type="success"
|
||||
variant="tonal"
|
||||
@click:close="success = ''"
|
||||
>
|
||||
<div class="d-flex align-center">
|
||||
@ -35,22 +35,22 @@
|
||||
<!-- 验证错误提示 -->
|
||||
<v-alert
|
||||
v-if="hasValidationErrors && !loading"
|
||||
type="warning"
|
||||
class="mb-4 mt-3 mx-2"
|
||||
variant="tonal"
|
||||
border="start"
|
||||
class="mb-4 mt-3 mx-2"
|
||||
type="warning"
|
||||
variant="tonal"
|
||||
>
|
||||
<div class="d-flex align-center">
|
||||
<span class="font-weight-bold">配置验证失败,请检查以下问题:</span>
|
||||
</div>
|
||||
<v-list density="compact" class="bg-transparent">
|
||||
<v-list class="bg-transparent" density="compact">
|
||||
<v-list-item
|
||||
v-for="(error, index) in validationErrors"
|
||||
:key="index"
|
||||
class="px-0 py-0"
|
||||
>
|
||||
<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>
|
||||
<v-list-item-title class="text-body-2">{{ error }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
@ -60,7 +60,7 @@
|
||||
<!-- 加载状态 -->
|
||||
<v-card v-if="loading" class="my-4" outlined>
|
||||
<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>
|
||||
|
||||
@ -68,12 +68,12 @@
|
||||
<div v-if="!loading" class="d-flex justify-space-between align-center mb-4">
|
||||
<div class="d-flex align-center gap-2">
|
||||
<v-btn
|
||||
color="success"
|
||||
variant="elevated"
|
||||
prepend-icon="mdi-open-in-new"
|
||||
@click="openConfig"
|
||||
class="mr-2 text-none"
|
||||
:disabled="!isValidConfig"
|
||||
class="mr-2 text-none"
|
||||
color="success"
|
||||
prepend-icon="mdi-open-in-new"
|
||||
variant="elevated"
|
||||
@click="openConfig"
|
||||
>
|
||||
打开 ExamSchedule
|
||||
</v-btn>
|
||||
@ -89,9 +89,10 @@
|
||||
<v-btn-toggle
|
||||
v-model="isEditMode"
|
||||
color="primary"
|
||||
variant="outlined"
|
||||
divided
|
||||
> <v-btn
|
||||
variant="outlined"
|
||||
>
|
||||
<v-btn
|
||||
class="text-error"
|
||||
prepend-icon="mdi-delete"
|
||||
@click="confirmDelete"
|
||||
@ -99,8 +100,8 @@
|
||||
>
|
||||
删除配置
|
||||
</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="false" prepend-icon="mdi-eye"> 预览</v-btn>
|
||||
<v-btn :value="true" prepend-icon="mdi-pencil"> 编辑</v-btn>
|
||||
</v-btn-toggle>
|
||||
</div>
|
||||
|
||||
@ -116,7 +117,7 @@
|
||||
>
|
||||
{{ localConfig.message || "未设置考试提示" }}
|
||||
</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>
|
||||
考场:{{ localConfig.room }}
|
||||
</v-chip>
|
||||
@ -130,10 +131,10 @@
|
||||
v-for="(examInfo, index) in localConfig.examInfos"
|
||||
:key="index"
|
||||
cols="12"
|
||||
md="6"
|
||||
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">
|
||||
<div class="d-flex align-center">
|
||||
<v-icon class="mr-2">mdi-book-open-page-variant</v-icon>
|
||||
@ -143,8 +144,9 @@
|
||||
<v-card-text class="pa-4">
|
||||
<div class="mb-3">
|
||||
<div class="d-flex align-center mb-1">
|
||||
<v-icon size="small" class="mr-2" color="success"
|
||||
>mdi-clock-start</v-icon
|
||||
<v-icon class="mr-2" color="success" size="small"
|
||||
>mdi-clock-start
|
||||
</v-icon
|
||||
>
|
||||
<span class="text-body-2 text-grey-darken-1">开始时间</span>
|
||||
</div>
|
||||
@ -154,8 +156,9 @@
|
||||
</div>
|
||||
<div>
|
||||
<div class="d-flex align-center mb-1">
|
||||
<v-icon size="small" class="mr-2" color="error"
|
||||
>mdi-clock-end</v-icon
|
||||
<v-icon class="mr-2" color="error" size="small"
|
||||
>mdi-clock-end
|
||||
</v-icon
|
||||
>
|
||||
<span class="text-body-2 text-grey-darken-1">结束时间</span>
|
||||
</div>
|
||||
@ -169,7 +172,7 @@
|
||||
</v-row>
|
||||
</div>
|
||||
<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
|
||||
</v-icon>
|
||||
<div class="text-h5 text-grey-darken-1 mb-2">暂无考试科目安排</div>
|
||||
@ -183,7 +186,7 @@
|
||||
</div>
|
||||
|
||||
<!-- JSON预览 -->
|
||||
<v-card class="mb-4" elevation="2" border>
|
||||
<v-card border class="mb-4" elevation="2">
|
||||
<v-card-title
|
||||
class="d-flex align-center text-white cursor-pointer"
|
||||
@click="showJsonPreview = !showJsonPreview"
|
||||
@ -193,25 +196,25 @@
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
color="white"
|
||||
variant="outlined"
|
||||
prepend-icon="mdi-content-copy"
|
||||
size="small"
|
||||
variant="outlined"
|
||||
@click.stop="copyToClipboard"
|
||||
>
|
||||
复制
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="white"
|
||||
variant="text"
|
||||
size="small"
|
||||
:icon="showJsonPreview ? 'mdi-chevron-up' : 'mdi-chevron-down'"
|
||||
class="ml-2"
|
||||
color="white"
|
||||
size="small"
|
||||
variant="text"
|
||||
>
|
||||
</v-btn>
|
||||
</v-card-title>
|
||||
<v-expand-transition>
|
||||
<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>
|
||||
</v-card>
|
||||
</v-card-text>
|
||||
@ -222,7 +225,7 @@
|
||||
<!-- 编辑模式 -->
|
||||
<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-icon class="mr-2">mdi-information</v-icon>
|
||||
基本信息
|
||||
@ -232,11 +235,11 @@
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model="localConfig.examName"
|
||||
:rules="[(v) => !!v || '考试名称不能为空']"
|
||||
label="考试名称"
|
||||
prepend-inner-icon="mdi-calendar-text"
|
||||
variant="outlined"
|
||||
:rules="[(v) => !!v || '考试名称不能为空']"
|
||||
required
|
||||
variant="outlined"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6">
|
||||
@ -251,10 +254,10 @@
|
||||
<v-textarea
|
||||
v-model="localConfig.message"
|
||||
label="考试提示"
|
||||
prepend-inner-icon="mdi-message-text"
|
||||
variant="outlined"
|
||||
rows="3"
|
||||
placeholder="输入考试相关的提示信息..."
|
||||
prepend-inner-icon="mdi-message-text"
|
||||
rows="3"
|
||||
variant="outlined"
|
||||
></v-textarea>
|
||||
|
||||
<!-- 默认提示选项 -->
|
||||
@ -266,25 +269,25 @@
|
||||
<v-chip
|
||||
v-for="(tip, index) in defaultExamTips"
|
||||
:key="index"
|
||||
color="primary"
|
||||
variant="outlined"
|
||||
size="small"
|
||||
@click="selectDefaultTip(tip)"
|
||||
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 }}
|
||||
</v-chip>
|
||||
</v-chip-group>
|
||||
<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>
|
||||
</v-card-text>
|
||||
</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-icon class="mr-2">mdi-format-list-bulleted</v-icon>
|
||||
考试科目安排
|
||||
@ -312,31 +315,31 @@
|
||||
<v-col cols="12" md="4">
|
||||
<v-text-field
|
||||
v-model="examInfo.name"
|
||||
:rules="[(v) => !!v || '科目名称不能为空']"
|
||||
density="comfortable"
|
||||
label="科目名称"
|
||||
prepend-inner-icon="mdi-book"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
:rules="[(v) => !!v || '科目名称不能为空']"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" md="3">
|
||||
<v-menu
|
||||
v-model="examInfo.startDateMenu"
|
||||
:close-on-content-click="false"
|
||||
transition="scale-transition"
|
||||
offset-y
|
||||
min-width="auto"
|
||||
offset-y
|
||||
transition="scale-transition"
|
||||
>
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-text-field
|
||||
v-model="examInfo.startFormatted"
|
||||
:rules="[(v) => !!v || '开始时间不能为空']"
|
||||
density="comfortable"
|
||||
label="开始时间"
|
||||
prepend-inner-icon="mdi-clock-start"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
readonly
|
||||
v-bind="props"
|
||||
:rules="[(v) => !!v || '开始时间不能为空']"
|
||||
variant="outlined"
|
||||
></v-text-field>
|
||||
</template>
|
||||
<v-card min-width="600">
|
||||
@ -345,24 +348,24 @@
|
||||
</v-card-title>
|
||||
<v-card-text class="pa-0">
|
||||
<v-row no-gutters>
|
||||
<v-col cols="6" class="border-e">
|
||||
<v-col class="border-e" cols="6">
|
||||
<v-date-picker
|
||||
v-model="examInfo.startDate"
|
||||
@update:model-value="updateStartDateTime(index)"
|
||||
color="primary"
|
||||
elevation="0"
|
||||
locale="zh-cn"
|
||||
show-adjacent-months
|
||||
elevation="0"
|
||||
@update:model-value="updateStartDateTime(index)"
|
||||
></v-date-picker>
|
||||
</v-col>
|
||||
<v-col cols="6">
|
||||
<v-time-picker
|
||||
v-model="examInfo.startTime"
|
||||
@update:model-value="updateStartDateTime(index)"
|
||||
color="primary"
|
||||
elevation="0"
|
||||
format="24hr"
|
||||
scrollable
|
||||
elevation="0"
|
||||
@update:model-value="updateStartDateTime(index)"
|
||||
></v-time-picker>
|
||||
</v-col>
|
||||
</v-row>
|
||||
@ -391,20 +394,20 @@
|
||||
<v-menu
|
||||
v-model="examInfo.endDateMenu"
|
||||
:close-on-content-click="false"
|
||||
transition="scale-transition"
|
||||
offset-y
|
||||
min-width="auto"
|
||||
offset-y
|
||||
transition="scale-transition"
|
||||
>
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-text-field
|
||||
v-model="examInfo.endFormatted"
|
||||
:rules="[(v) => !!v || '结束时间不能为空']"
|
||||
density="comfortable"
|
||||
label="结束时间"
|
||||
prepend-inner-icon="mdi-clock-end"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
readonly
|
||||
v-bind="props"
|
||||
:rules="[(v) => !!v || '结束时间不能为空']"
|
||||
variant="outlined"
|
||||
></v-text-field>
|
||||
</template>
|
||||
<v-card min-width="600">
|
||||
@ -413,24 +416,24 @@
|
||||
</v-card-title>
|
||||
<v-card-text class="pa-0">
|
||||
<v-row no-gutters>
|
||||
<v-col cols="6" class="border-e">
|
||||
<v-col class="border-e" cols="6">
|
||||
<v-date-picker
|
||||
v-model="examInfo.endDate"
|
||||
@update:model-value="updateEndDateTime(index)"
|
||||
color="primary"
|
||||
elevation="0"
|
||||
locale="zh-cn"
|
||||
show-adjacent-months
|
||||
elevation="0"
|
||||
@update:model-value="updateEndDateTime(index)"
|
||||
></v-date-picker>
|
||||
</v-col>
|
||||
<v-col cols="6">
|
||||
<v-time-picker
|
||||
v-model="examInfo.endTime"
|
||||
@update:model-value="updateEndDateTime(index)"
|
||||
color="primary"
|
||||
elevation="0"
|
||||
format="24hr"
|
||||
scrollable
|
||||
elevation="0"
|
||||
@update:model-value="updateEndDateTime(index)"
|
||||
></v-time-picker>
|
||||
</v-col>
|
||||
</v-row>
|
||||
@ -455,32 +458,32 @@
|
||||
</v-card>
|
||||
</v-menu>
|
||||
</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
|
||||
icon="mdi-delete"
|
||||
color="error"
|
||||
variant="text"
|
||||
icon="mdi-delete"
|
||||
size="small"
|
||||
variant="text"
|
||||
@click="removeExamInfo(index)"
|
||||
>
|
||||
<v-icon>mdi-delete</v-icon>
|
||||
</v-btn>
|
||||
<v-btn
|
||||
v-if="index > 0"
|
||||
icon="mdi-arrow-up"
|
||||
color="primary"
|
||||
variant="text"
|
||||
icon="mdi-arrow-up"
|
||||
size="small"
|
||||
variant="text"
|
||||
@click="moveExamInfo(index, -1)"
|
||||
>
|
||||
<v-icon>mdi-arrow-up</v-icon>
|
||||
</v-btn>
|
||||
<v-btn
|
||||
v-if="index < localConfig.examInfos.length - 1"
|
||||
icon="mdi-arrow-down"
|
||||
color="primary"
|
||||
variant="text"
|
||||
icon="mdi-arrow-down"
|
||||
size="small"
|
||||
variant="text"
|
||||
@click="moveExamInfo(index, 1)"
|
||||
>
|
||||
<v-icon>mdi-arrow-down</v-icon>
|
||||
@ -491,7 +494,7 @@
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
<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
|
||||
</v-icon>
|
||||
<p class="text-body-2 text-grey-darken-1 mb-4">
|
||||
@ -509,7 +512,7 @@
|
||||
<v-dialog v-model="deleteDialog" max-width="400">
|
||||
<v-card>
|
||||
<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-text>
|
||||
@ -526,10 +529,10 @@
|
||||
取消
|
||||
</v-btn>
|
||||
<v-btn
|
||||
:loading="deleting"
|
||||
color="error"
|
||||
variant="outlined"
|
||||
@click="deleteConfig"
|
||||
:loading="deleting"
|
||||
>
|
||||
删除
|
||||
</v-btn>
|
||||
@ -1063,7 +1066,7 @@ export default {
|
||||
window.open(examUrl, '_blank');
|
||||
|
||||
this.success = '配置已在新窗口中打开';
|
||||
this.$emit('opened', { configId: this.configId, url: result.url });
|
||||
this.$emit('opened', {configId: this.configId, url: result.url});
|
||||
} else {
|
||||
throw new Error(result.error || '获取云端地址失败');
|
||||
}
|
||||
@ -1074,7 +1077,6 @@ export default {
|
||||
},
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 确认删除配置
|
||||
*/
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
<template>
|
||||
<a
|
||||
aria-label="浙ICP备2024068645号"
|
||||
class="floating-icp-link"
|
||||
href="https://beian.miit.gov.cn/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="浙ICP备2024068645号"
|
||||
target="_blank"
|
||||
>
|
||||
浙ICP备2024068645号
|
||||
</a>
|
||||
|
||||
@ -1,47 +1,47 @@
|
||||
<template>
|
||||
<v-slide-y-transition>
|
||||
<v-card
|
||||
:class="{ 'toolbar-expanded': isExpanded }"
|
||||
class="floating-toolbar"
|
||||
elevation="4"
|
||||
rounded="xl"
|
||||
:class="{ 'toolbar-expanded': isExpanded }"
|
||||
>
|
||||
|
||||
|
||||
<v-btn-group variant="text" class="toolbar-buttons">
|
||||
<v-btn-group class="toolbar-buttons" variant="text">
|
||||
<v-btn
|
||||
icon="mdi-chevron-left"
|
||||
variant="text"
|
||||
@click="$emit('prev-day')"
|
||||
:title="'查看昨天'"
|
||||
class="toolbar-btn"
|
||||
v-ripple
|
||||
/>
|
||||
v-ripple
|
||||
:title="'查看昨天'"
|
||||
class="toolbar-btn"
|
||||
icon="mdi-chevron-left"
|
||||
variant="text"
|
||||
@click="$emit('prev-day')"
|
||||
/>
|
||||
<v-btn
|
||||
v-ripple
|
||||
:title="'缩小字体'"
|
||||
class="toolbar-btn"
|
||||
icon="mdi-format-font-size-decrease"
|
||||
variant="text"
|
||||
@click="$emit('zoom', 'out')"
|
||||
:title="'缩小字体'"
|
||||
class="toolbar-btn"
|
||||
v-ripple
|
||||
/>
|
||||
<v-btn
|
||||
v-ripple
|
||||
:title="'放大字体'"
|
||||
class="toolbar-btn"
|
||||
icon="mdi-format-font-size-increase"
|
||||
variant="text"
|
||||
@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 }">
|
||||
<v-btn
|
||||
icon="mdi-calendar"
|
||||
variant="text"
|
||||
v-bind="props"
|
||||
v-ripple
|
||||
:title="'选择日期'"
|
||||
class="toolbar-btn"
|
||||
v-ripple
|
||||
icon="mdi-calendar"
|
||||
v-bind="props"
|
||||
variant="text"
|
||||
/>
|
||||
</template>
|
||||
<v-card border class="date-picker-card">
|
||||
@ -53,24 +53,24 @@
|
||||
</v-card>
|
||||
</v-menu>
|
||||
<v-btn
|
||||
icon="mdi-refresh"
|
||||
variant="text"
|
||||
v-ripple
|
||||
:loading="loading"
|
||||
@click="$emit('refresh')"
|
||||
:title="'刷新数据'"
|
||||
class="toolbar-btn"
|
||||
v-ripple
|
||||
icon="mdi-refresh"
|
||||
variant="text"
|
||||
@click="$emit('refresh')"
|
||||
/>
|
||||
|
||||
<v-btn
|
||||
v-if="!isToday"
|
||||
icon="mdi-chevron-right"
|
||||
variant="text"
|
||||
@click="$emit('next-day')"
|
||||
:title="'查看明天'"
|
||||
class="toolbar-btn"
|
||||
v-ripple
|
||||
/>
|
||||
<v-btn
|
||||
v-if="!isToday"
|
||||
v-ripple
|
||||
:title="'查看明天'"
|
||||
class="toolbar-btn"
|
||||
icon="mdi-chevron-right"
|
||||
variant="text"
|
||||
@click="$emit('next-day')"
|
||||
/>
|
||||
</v-btn-group>
|
||||
|
||||
|
||||
|
||||
@ -8,20 +8,20 @@
|
||||
variant="tonal"
|
||||
>
|
||||
<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 class="text-subtitle-2 font-weight-medium">{{ message?.title }}</div>
|
||||
<div v-if="message?.content" class="text-body-2">{{ message?.content }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<template #actions>
|
||||
<v-btn variant="text" icon="mdi-close" @click="snackbar = false" />
|
||||
<v-btn icon="mdi-close" variant="text" @click="snackbar = false"/>
|
||||
</template>
|
||||
</v-snackbar>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent, ref, onBeforeUnmount, nextTick } from 'vue';
|
||||
import {defineComponent, ref, onBeforeUnmount, nextTick} from 'vue';
|
||||
import messageService from '@/utils/message';
|
||||
|
||||
export default defineComponent({
|
||||
@ -56,7 +56,7 @@ export default defineComponent({
|
||||
|
||||
onBeforeUnmount(() => unsubscribe?.());
|
||||
|
||||
return { snackbar, message, icons, colors };
|
||||
return {snackbar, message, icons, colors};
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@ -16,7 +16,7 @@
|
||||
<h1 class="text-h2 font-weight-bold">Vuetify</h1>
|
||||
</div>
|
||||
|
||||
<div class="py-4" />
|
||||
<div class="py-4"/>
|
||||
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
@ -29,7 +29,7 @@
|
||||
variant="outlined"
|
||||
>
|
||||
<template #image>
|
||||
<v-img position="top right" />
|
||||
<v-img position="top right"/>
|
||||
</template>
|
||||
|
||||
<template #title>
|
||||
@ -38,16 +38,23 @@
|
||||
|
||||
<template #subtitle>
|
||||
<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>
|
||||
</template>
|
||||
|
||||
<v-overlay
|
||||
opacity=".12"
|
||||
scrim="primary"
|
||||
contained
|
||||
model-value
|
||||
opacity=".12"
|
||||
persistent
|
||||
scrim="primary"
|
||||
/>
|
||||
</v-card>
|
||||
</v-col>
|
||||
@ -67,11 +74,11 @@
|
||||
variant="text"
|
||||
>
|
||||
<v-overlay
|
||||
opacity=".06"
|
||||
scrim="primary"
|
||||
contained
|
||||
model-value
|
||||
opacity=".06"
|
||||
persistent
|
||||
scrim="primary"
|
||||
/>
|
||||
</v-card>
|
||||
</v-col>
|
||||
@ -91,11 +98,11 @@
|
||||
variant="text"
|
||||
>
|
||||
<v-overlay
|
||||
opacity=".06"
|
||||
scrim="primary"
|
||||
contained
|
||||
model-value
|
||||
opacity=".06"
|
||||
persistent
|
||||
scrim="primary"
|
||||
/>
|
||||
</v-card>
|
||||
</v-col>
|
||||
@ -115,11 +122,11 @@
|
||||
variant="text"
|
||||
>
|
||||
<v-overlay
|
||||
opacity=".06"
|
||||
scrim="primary"
|
||||
contained
|
||||
model-value
|
||||
opacity=".06"
|
||||
persistent
|
||||
scrim="primary"
|
||||
/>
|
||||
</v-card>
|
||||
</v-col>
|
||||
@ -139,11 +146,11 @@
|
||||
variant="text"
|
||||
>
|
||||
<v-overlay
|
||||
opacity=".06"
|
||||
scrim="primary"
|
||||
contained
|
||||
model-value
|
||||
opacity=".06"
|
||||
persistent
|
||||
scrim="primary"
|
||||
/>
|
||||
</v-card>
|
||||
</v-col>
|
||||
@ -153,5 +160,5 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
//
|
||||
//
|
||||
</script>
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
# 创建新的作业编辑对话框组件
|
||||
<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-title>{{ title }}</v-card-title>
|
||||
<v-card-subtitle>
|
||||
@ -15,9 +15,9 @@
|
||||
auto-grow
|
||||
placeholder="使用换行表示分条"
|
||||
rows="5"
|
||||
width="480"
|
||||
@click="updateCurrentLine"
|
||||
@keyup="updateCurrentLine"
|
||||
width="480"
|
||||
/>
|
||||
|
||||
<!-- Template Buttons Section -->
|
||||
@ -29,9 +29,9 @@
|
||||
<template v-if="subjectBooks">
|
||||
<div v-for="(pages, book) in subjectBooks" :key="book" class="button-group">
|
||||
<v-chip
|
||||
class="ma-1 book-chip"
|
||||
:color="isBookSelected(book) ? 'success' : 'default'"
|
||||
:variant="isBookSelected(book) ? 'elevated' : 'flat'"
|
||||
class="ma-1 book-chip"
|
||||
@click="handleBookClick(book)"
|
||||
>
|
||||
{{ book }}
|
||||
@ -42,9 +42,9 @@
|
||||
<v-chip
|
||||
v-for="page in pages"
|
||||
:key="page"
|
||||
class="ma-1"
|
||||
:color="isPageSelected(book, page) ? 'info' : 'default'"
|
||||
:variant="isPageSelected(book, page) ? 'elevated' : 'flat'"
|
||||
class="ma-1"
|
||||
@click="handlePageClick(book, page)"
|
||||
>
|
||||
{{ page }}
|
||||
@ -57,9 +57,9 @@
|
||||
<template v-if="commonBooks">
|
||||
<div v-for="(pages, book) in commonBooks" :key="book" class="button-group">
|
||||
<v-chip
|
||||
class="ma-1 book-chip"
|
||||
:color="isBookSelected(book) ? 'success' : 'default'"
|
||||
:variant="isBookSelected(book) ? 'elevated' : 'flat'"
|
||||
class="ma-1 book-chip"
|
||||
@click="handleBookClick(book)"
|
||||
>
|
||||
{{ book }}
|
||||
@ -70,9 +70,9 @@
|
||||
<v-chip
|
||||
v-for="page in pages"
|
||||
:key="page"
|
||||
class="ma-1"
|
||||
:color="isPageSelected(book, page) ? 'info' : 'default'"
|
||||
:variant="isPageSelected(book, page) ? 'elevated' : 'flat'"
|
||||
class="ma-1"
|
||||
@click="handlePageClick(book, page)"
|
||||
>
|
||||
{{ page }}
|
||||
@ -102,16 +102,16 @@
|
||||
</div>
|
||||
|
||||
<!-- 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 -->
|
||||
<div class="numeric-keypad mb-4">
|
||||
<div class="keypad-row">
|
||||
<v-btn
|
||||
v-for="n in 3"
|
||||
:key="n"
|
||||
class="keypad-btn"
|
||||
size="small"
|
||||
variant="tonal"
|
||||
class="keypad-btn"
|
||||
@click="insertAtCursor(String(n))"
|
||||
>
|
||||
{{ n }}
|
||||
@ -121,9 +121,9 @@
|
||||
<v-btn
|
||||
v-for="n in 3"
|
||||
:key="n"
|
||||
class="keypad-btn"
|
||||
size="small"
|
||||
variant="tonal"
|
||||
class="keypad-btn"
|
||||
@click="insertAtCursor(String(n + 3))"
|
||||
>
|
||||
{{ n + 3 }}
|
||||
@ -133,9 +133,9 @@
|
||||
<v-btn
|
||||
v-for="n in 3"
|
||||
:key="n"
|
||||
class="keypad-btn"
|
||||
size="small"
|
||||
variant="tonal"
|
||||
class="keypad-btn"
|
||||
@click="insertAtCursor(String(n + 6))"
|
||||
>
|
||||
{{ n + 6 }}
|
||||
@ -143,26 +143,26 @@
|
||||
</div>
|
||||
<div class="keypad-row">
|
||||
<v-btn
|
||||
class="keypad-btn"
|
||||
size="small"
|
||||
variant="tonal"
|
||||
class="keypad-btn"
|
||||
@click="insertAtCursor('-')"
|
||||
>
|
||||
-
|
||||
</v-btn>
|
||||
<v-btn
|
||||
class="keypad-btn"
|
||||
size="small"
|
||||
variant="tonal"
|
||||
class="keypad-btn"
|
||||
@click="insertAtCursor('0')"
|
||||
>
|
||||
0
|
||||
</v-btn>
|
||||
<v-btn
|
||||
size="small"
|
||||
variant="tonal"
|
||||
class="keypad-btn"
|
||||
color="error"
|
||||
size="small"
|
||||
variant="tonal"
|
||||
@click="deleteLastChar"
|
||||
>
|
||||
←
|
||||
@ -170,16 +170,17 @@
|
||||
</div>
|
||||
<div class="keypad-row">
|
||||
<v-btn
|
||||
class="keypad-btn space-btn"
|
||||
size="small"
|
||||
variant="tonal"
|
||||
class="keypad-btn space-btn"
|
||||
@click="insertAtCursor(' ')"
|
||||
>
|
||||
空格
|
||||
</v-btn><v-btn
|
||||
</v-btn>
|
||||
<v-btn
|
||||
class="keypad-btn space-btn"
|
||||
size="small"
|
||||
variant="tonal"
|
||||
class="keypad-btn space-btn"
|
||||
@click="insertAtCursor('\n')"
|
||||
>
|
||||
换行
|
||||
@ -212,7 +213,7 @@
|
||||
|
||||
<script>
|
||||
import dataProvider from "@/utils/dataProvider";
|
||||
import { getSetting } from "@/utils/settings";
|
||||
import {getSetting} from "@/utils/settings";
|
||||
|
||||
export default {
|
||||
name: "HomeworkEditDialog",
|
||||
@ -242,7 +243,7 @@ export default {
|
||||
currentLine: "",
|
||||
currentLineStart: 0,
|
||||
currentLineEnd: 0,
|
||||
quickTexts: ["课", "题", "例","变","T", "P"]
|
||||
quickTexts: ["课", "题", "例", "变", "T", "P"]
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@ -391,8 +392,8 @@ export default {
|
||||
currentLineContent.slice(0, lastIndex) +
|
||||
currentLineContent.slice(lastIndex + page.length);
|
||||
this.content = this.content.slice(0, start) +
|
||||
newLineContent.trim() +
|
||||
this.content.slice(end);
|
||||
newLineContent.trim() +
|
||||
this.content.slice(end);
|
||||
}
|
||||
} else {
|
||||
// 在当前行末尾插入
|
||||
@ -400,10 +401,10 @@ export default {
|
||||
const end = this.currentLineEnd;
|
||||
const currentLineContent = this.content.slice(start, end);
|
||||
this.content = this.content.slice(0, start) +
|
||||
currentLineContent.trim() +
|
||||
(currentLineContent.trim().length > 0 ? ' ' : '') +
|
||||
page +
|
||||
this.content.slice(end);
|
||||
currentLineContent.trim() +
|
||||
(currentLineContent.trim().length > 0 ? ' ' : '') +
|
||||
page +
|
||||
this.content.slice(end);
|
||||
}
|
||||
this.$nextTick(() => {
|
||||
const textarea = this.$refs.inputRef.$el.querySelector('textarea');
|
||||
@ -492,7 +493,6 @@ export default {
|
||||
}
|
||||
|
||||
|
||||
|
||||
.book-chip {
|
||||
align-self: flex-start;
|
||||
}
|
||||
@ -547,4 +547,4 @@ export default {
|
||||
.space-btn {
|
||||
width: 100% !important;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@ -25,8 +25,8 @@
|
||||
<div class="card-horizontal-layout">
|
||||
<div class="card-icon-wrapper">
|
||||
<v-icon
|
||||
size="48"
|
||||
color="primary"
|
||||
size="48"
|
||||
>
|
||||
mdi-new-box
|
||||
</v-icon>
|
||||
@ -53,8 +53,8 @@
|
||||
<div class="card-horizontal-layout">
|
||||
<div class="card-icon-wrapper">
|
||||
<v-icon
|
||||
size="48"
|
||||
color="success"
|
||||
size="48"
|
||||
>
|
||||
mdi-account-check
|
||||
</v-icon>
|
||||
@ -81,8 +81,8 @@
|
||||
<div class="card-horizontal-layout">
|
||||
<div class="card-icon-wrapper">
|
||||
<v-icon
|
||||
size="48"
|
||||
color="info"
|
||||
size="48"
|
||||
>
|
||||
mdi-database-cog
|
||||
</v-icon>
|
||||
@ -102,33 +102,33 @@
|
||||
|
||||
<div class="options-buttons">
|
||||
<v-btn
|
||||
variant="tonal"
|
||||
prepend-icon="mdi-laptop"
|
||||
size="small"
|
||||
variant="tonal"
|
||||
@click="useLocalMode"
|
||||
>
|
||||
使用本地模式
|
||||
</v-btn>
|
||||
<v-btn
|
||||
variant="tonal"
|
||||
prepend-icon="mdi-flash"
|
||||
size="small"
|
||||
variant="tonal"
|
||||
@click="handleAutoAuthorize"
|
||||
>
|
||||
授权码式授权(弃用)
|
||||
</v-btn>
|
||||
<v-btn
|
||||
variant="tonal"
|
||||
prepend-icon="mdi-key"
|
||||
size="small"
|
||||
variant="tonal"
|
||||
@click="showTokenDialog = true"
|
||||
>
|
||||
输入 Token
|
||||
</v-btn>
|
||||
<v-btn
|
||||
variant="tonal"
|
||||
prepend-icon="mdi-code-tags"
|
||||
size="small"
|
||||
variant="tonal"
|
||||
@click="showAlternativeCodeDialog = true"
|
||||
>
|
||||
输入替代代码
|
||||
@ -158,10 +158,10 @@
|
||||
>
|
||||
<DeviceAuthDialog
|
||||
ref="deviceAuthDialog"
|
||||
:show-cancel="true"
|
||||
:preconfig="deviceAuthPreconfig"
|
||||
@success="handleAuthSuccess"
|
||||
:show-cancel="true"
|
||||
@cancel="showDeviceAuthDialog = false"
|
||||
@success="handleAuthSuccess"
|
||||
/>
|
||||
</v-dialog>
|
||||
|
||||
@ -171,8 +171,8 @@
|
||||
>
|
||||
<TokenInputDialog
|
||||
:show-cancel="true"
|
||||
@success="handleTokenSuccess"
|
||||
@cancel="showTokenDialog = false"
|
||||
@success="handleTokenSuccess"
|
||||
/>
|
||||
</v-dialog>
|
||||
|
||||
@ -182,16 +182,16 @@
|
||||
>
|
||||
<AlternativeCodeDialog
|
||||
:show-cancel="true"
|
||||
@submit="handleAlternativeCodeSubmit"
|
||||
@cancel="showAlternativeCodeDialog = false"
|
||||
@submit="handleAlternativeCodeSubmit"
|
||||
/>
|
||||
</v-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, watch } from 'vue'
|
||||
import { getSetting, setSetting } from '@/utils/settings'
|
||||
import {ref, computed, onMounted, watch} from 'vue'
|
||||
import {getSetting, setSetting} from '@/utils/settings'
|
||||
import DeviceAuthDialog from './auth/DeviceAuthDialog.vue'
|
||||
import TokenInputDialog from './auth/TokenInputDialog.vue'
|
||||
import AlternativeCodeDialog from './auth/AlternativeCodeDialog.vue'
|
||||
@ -258,7 +258,7 @@ watch(
|
||||
}, 500)
|
||||
}
|
||||
},
|
||||
{ immediate: true, deep: true }
|
||||
{immediate: true, deep: true}
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
@ -339,7 +339,7 @@ const openClassworksKV = () => {
|
||||
.init-header .title {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
text-align:left;
|
||||
text-align: left;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
@ -364,7 +364,7 @@ const openClassworksKV = () => {
|
||||
}
|
||||
|
||||
.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 {
|
||||
@ -387,18 +387,18 @@ const openClassworksKV = () => {
|
||||
}
|
||||
|
||||
.gradient-new {
|
||||
background: linear-gradient(135deg, rgba(33,150,243,.12), rgba(103,80,164,0.08) 60%);
|
||||
border: 2px solid rgba(33,150,243,.2);
|
||||
background: linear-gradient(135deg, rgba(33, 150, 243, .12), rgba(103, 80, 164, 0.08) 60%);
|
||||
border: 2px solid rgba(33, 150, 243, .2);
|
||||
}
|
||||
|
||||
.gradient-registered {
|
||||
background: linear-gradient(135deg, rgba(76,175,80,.12), rgba(0,184,212,0.08) 60%);
|
||||
border: 2px solid rgba(76,175,80,.2);
|
||||
background: linear-gradient(135deg, rgba(76, 175, 80, .12), rgba(0, 184, 212, 0.08) 60%);
|
||||
border: 2px solid rgba(76, 175, 80, .2);
|
||||
}
|
||||
|
||||
.gradient-kv {
|
||||
background: linear-gradient(135deg, rgba(0,184,212,.12), rgba(33,150,243,0.08) 60%);
|
||||
border: 2px solid rgba(0,184,212,.2);
|
||||
background: linear-gradient(135deg, rgba(0, 184, 212, .12), rgba(33, 150, 243, 0.08) 60%);
|
||||
border: 2px solid rgba(0, 184, 212, .2);
|
||||
}
|
||||
|
||||
/* 其他选项 */
|
||||
|
||||
@ -8,22 +8,22 @@
|
||||
<v-card
|
||||
class="kvinit-card"
|
||||
elevation="8"
|
||||
title="初始化云端存储授权"
|
||||
subtitle="请完成授权以启用云端存储功能"
|
||||
prepend-icon="mdi-cloud-lock"
|
||||
subtitle="请完成授权以启用云端存储功能"
|
||||
title="初始化云端存储授权"
|
||||
>
|
||||
<v-card-actions class="justify-end">
|
||||
<v-btn
|
||||
text
|
||||
class="me-3"
|
||||
text
|
||||
@click="useLocalMode"
|
||||
>
|
||||
使用本地模式
|
||||
</v-btn>
|
||||
<v-btn
|
||||
:loading="loading"
|
||||
color="primary"
|
||||
variant="flat"
|
||||
:loading="loading"
|
||||
@click="goToAuthorize"
|
||||
>
|
||||
前往授权
|
||||
@ -36,10 +36,10 @@
|
||||
class="d-flex align-center"
|
||||
>
|
||||
<v-progress-circular
|
||||
class="me-2"
|
||||
indeterminate
|
||||
size="20"
|
||||
width="2"
|
||||
class="me-2"
|
||||
/>
|
||||
<span class="body-2"> 正在检查授权状态… </span>
|
||||
</div>
|
||||
@ -57,10 +57,10 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onBeforeUnmount } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
import { getSetting,setSetting } from "@/utils/settings";
|
||||
import { kvServerProvider } from "@/utils/providers/kvServerProvider";
|
||||
import {ref, onMounted, onBeforeUnmount} from "vue";
|
||||
import {useRoute} from "vue-router";
|
||||
import {getSetting, setSetting} from "@/utils/settings";
|
||||
import {kvServerProvider} from "@/utils/providers/kvServerProvider";
|
||||
|
||||
const visible = ref(false);
|
||||
const loading = ref(false);
|
||||
@ -103,7 +103,7 @@ const goToAuthorize = () => {
|
||||
|
||||
// set a short-lived guard to prevent immediate re-redirect
|
||||
try {
|
||||
const guardObj = { ts: Date.now() };
|
||||
const guardObj = {ts: Date.now()};
|
||||
sessionStorage.setItem(REDIRECT_GUARD_KEY, JSON.stringify(guardObj));
|
||||
} catch (err) {
|
||||
// sessionStorage may be unavailable in some environments
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<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-title>消息记录</v-toolbar-title>
|
||||
|
||||
@ -8,13 +8,14 @@
|
||||
<v-list>
|
||||
<v-list-item v-for="msg in messages" :key="msg.id" rounded>
|
||||
<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>
|
||||
|
||||
<v-list-item-title>{{ msg.title }}</v-list-item-title>
|
||||
<v-list-item-subtitle v-if="msg.content">{{
|
||||
msg.content
|
||||
}}</v-list-item-subtitle>
|
||||
}}
|
||||
</v-list-item-subtitle>
|
||||
<span class="text-caption text-grey">
|
||||
{{ new Date(msg.timestamp).toLocaleTimeString() }}
|
||||
</span>
|
||||
@ -24,7 +25,7 @@
|
||||
|
||||
<v-list-item v-if="!messages.length">
|
||||
<template #prepend>
|
||||
<v-icon icon="mdi-inbox" color="grey" />
|
||||
<v-icon color="grey" icon="mdi-inbox"/>
|
||||
</template>
|
||||
<v-list-item-title class="text-grey">暂无消息</v-list-item-title>
|
||||
</v-list-item>
|
||||
@ -33,7 +34,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent, ref } from "vue";
|
||||
import {defineComponent, ref} from "vue";
|
||||
import messageService from "@/utils/message";
|
||||
|
||||
export default defineComponent({
|
||||
|
||||
@ -7,8 +7,8 @@
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model="classNumber"
|
||||
label="班级编号"
|
||||
hint="请输入需要迁移的班级编号"
|
||||
label="班级编号"
|
||||
persistent-hint
|
||||
prepend-icon="mdi-account-group"
|
||||
></v-text-field>
|
||||
@ -17,8 +17,8 @@
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model="machineId"
|
||||
label="设备标识 (UUID)"
|
||||
hint="系统已自动填充设备标识,通常无需修改"
|
||||
label="设备标识 (UUID)"
|
||||
persistent-hint
|
||||
prepend-icon="mdi-identifier"
|
||||
readonly
|
||||
@ -27,32 +27,32 @@
|
||||
</v-row>
|
||||
|
||||
<v-radio-group v-model="migrationType" class="mt-2">
|
||||
<v-radio value="local" label="本地数据迁移"></v-radio>
|
||||
<v-radio value="server" label="服务器数据迁移"></v-radio>
|
||||
<v-radio label="本地数据迁移" value="local"></v-radio>
|
||||
<v-radio label="服务器数据迁移" value="server"></v-radio>
|
||||
</v-radio-group>
|
||||
|
||||
<div v-if="migrationType === 'server'" class="mt-4">
|
||||
<v-text-field
|
||||
v-model="serverUrl"
|
||||
label="服务器地址"
|
||||
hint="输入服务器域名,例如:https://example.com"
|
||||
label="服务器地址"
|
||||
persistent-hint
|
||||
prepend-icon="mdi-server"
|
||||
></v-text-field>
|
||||
|
||||
<v-alert
|
||||
class="mt-2"
|
||||
density="compact"
|
||||
type="info"
|
||||
variant="outlined"
|
||||
class="mt-2"
|
||||
>
|
||||
服务器接口格式:<br />
|
||||
- 配置接口:域名/班号/config<br />
|
||||
服务器接口格式:<br/>
|
||||
- 配置接口:域名/班号/config<br/>
|
||||
- 作业数据接口:域名/班号/homework?date=YYYY-MM-DD
|
||||
</v-alert>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
|
||||
@ -61,8 +61,8 @@
|
||||
<v-text-field
|
||||
v-model="startDate"
|
||||
label="开始日期"
|
||||
type="date"
|
||||
prepend-icon="mdi-calendar-start"
|
||||
type="date"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
|
||||
@ -70,8 +70,8 @@
|
||||
<v-text-field
|
||||
v-model="endDate"
|
||||
label="结束日期"
|
||||
type="date"
|
||||
prepend-icon="mdi-calendar-end"
|
||||
type="date"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
@ -87,13 +87,13 @@
|
||||
}}</span>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
:loading="loading || scanning"
|
||||
color="primary"
|
||||
@click="
|
||||
migrationType === 'local'
|
||||
? scanLocalDatabase()
|
||||
: previewServerData()
|
||||
"
|
||||
:loading="loading || scanning"
|
||||
>
|
||||
{{ migrationType === "local" ? "扫描数据" : "加载数据" }}
|
||||
</v-btn>
|
||||
@ -104,9 +104,9 @@
|
||||
type="info"
|
||||
>
|
||||
{{
|
||||
migrationType === "local"
|
||||
? '尚未扫描本地数据或未找到可迁移的数据。点击"扫描数据"按钮开始扫描。'
|
||||
: '尚未预览服务器数据或未找到可迁移的数据。点击"加载数据"按钮开始查询。'
|
||||
migrationType === "local"
|
||||
? '尚未扫描本地数据或未找到可迁移的数据。点击"扫描数据"按钮开始扫描。'
|
||||
: '尚未预览服务器数据或未找到可迁移的数据。点击"加载数据"按钮开始查询。'
|
||||
}}
|
||||
</v-alert>
|
||||
|
||||
@ -115,8 +115,8 @@
|
||||
:headers="headers"
|
||||
:items="displayItems"
|
||||
:items-per-page="10"
|
||||
item-value="key"
|
||||
class="elevation-1"
|
||||
item-value="key"
|
||||
>
|
||||
<template #[`item.type`]="{ item }">
|
||||
<v-chip
|
||||
@ -134,9 +134,9 @@
|
||||
|
||||
<v-alert
|
||||
v-if="displayItems.length > 0"
|
||||
type="info"
|
||||
density="compact"
|
||||
class="mt-2"
|
||||
density="compact"
|
||||
type="info"
|
||||
>
|
||||
系统将迁移表格中显示的所有数据项,迁移前请确认数据完整性。
|
||||
</v-alert>
|
||||
@ -152,15 +152,15 @@
|
||||
<v-card-title>迁移目标</v-card-title>
|
||||
<v-card-text>
|
||||
<v-radio-group v-model="targetStorage">
|
||||
<v-radio value="kv-local" label="本地 KV 存储"></v-radio>
|
||||
<v-radio value="kv-server" label="服务器 KV 存储"></v-radio>
|
||||
<v-radio label="本地 KV 存储" value="kv-local"></v-radio>
|
||||
<v-radio label="服务器 KV 存储" value="kv-server"></v-radio>
|
||||
</v-radio-group>
|
||||
|
||||
<div v-if="targetStorage === 'kv-server'" class="mt-4">
|
||||
<v-text-field
|
||||
v-model="targetServerUrl"
|
||||
label="目标服务器地址"
|
||||
hint="输入KV服务器地址,例如:https://example.com/kv-api"
|
||||
label="目标服务器地址"
|
||||
persistent-hint
|
||||
prepend-icon="mdi-server-network"
|
||||
></v-text-field>
|
||||
@ -170,10 +170,10 @@
|
||||
|
||||
<div class="d-flex justify-end mb-6">
|
||||
<v-btn
|
||||
:disabled="!canMigrate"
|
||||
:loading="migrating"
|
||||
color="success"
|
||||
@click="startMigration"
|
||||
:loading="migrating"
|
||||
:disabled="!canMigrate"
|
||||
>
|
||||
开始迁移
|
||||
</v-btn>
|
||||
@ -188,7 +188,7 @@
|
||||
<span>{{ migrationSuccess ? "迁移成功" : "迁移失败" }}</span>
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<v-alert v-if="migrationError" type="error" class="mb-4">
|
||||
<v-alert v-if="migrationError" class="mb-4" type="error">
|
||||
{{ migrationError }}
|
||||
</v-alert>
|
||||
|
||||
@ -216,7 +216,7 @@
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<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>
|
||||
</v-dialog>
|
||||
@ -224,9 +224,9 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { openDB } from "idb";
|
||||
import {openDB} from "idb";
|
||||
import axios from "@/axios/axios";
|
||||
import { getSetting, setSetting } from "@/utils/settings";
|
||||
import {getSetting, setSetting} from "@/utils/settings";
|
||||
|
||||
export default {
|
||||
name: "MigrationTool",
|
||||
@ -259,10 +259,10 @@ export default {
|
||||
serverItems: [],
|
||||
selectedItems: [],
|
||||
headers: [
|
||||
{ title: "类型", key: "type", sortable: true },
|
||||
{ title: "键名", key: "key", sortable: true },
|
||||
{ title: "日期", key: "date", sortable: true },
|
||||
{ title: "大小", key: "size", sortable: true },
|
||||
{title: "类型", key: "type", sortable: true},
|
||||
{title: "键名", key: "key", sortable: true},
|
||||
{title: "日期", key: "date", sortable: true},
|
||||
{title: "大小", key: "size", sortable: true},
|
||||
],
|
||||
};
|
||||
},
|
||||
@ -317,7 +317,7 @@ export default {
|
||||
|
||||
// 获取请求头,包含网站令牌
|
||||
getRequestHeaders() {
|
||||
const headers = { Accept: "application/json" };
|
||||
const headers = {Accept: "application/json"};
|
||||
const siteKey = getSetting("server.siteKey");
|
||||
|
||||
if (siteKey) {
|
||||
@ -330,7 +330,7 @@ export default {
|
||||
// 扫描本地数据库
|
||||
async scanLocalDatabase() {
|
||||
if (!this.classNumber) {
|
||||
this.$emit("message", { text: "请先输入班级编号", type: "error" });
|
||||
this.$emit("message", {text: "请先输入班级编号", type: "error"});
|
||||
return;
|
||||
}
|
||||
|
||||
@ -606,7 +606,7 @@ export default {
|
||||
);
|
||||
|
||||
// Remove studentList from config
|
||||
const configWithoutStudentList = { ...value };
|
||||
const configWithoutStudentList = {...value};
|
||||
delete configWithoutStudentList.studentList;
|
||||
|
||||
// Save the modified config
|
||||
@ -619,7 +619,7 @@ export default {
|
||||
// Just store the config as is
|
||||
await db.put("kv", JSON.stringify(value), `classworks-config`);
|
||||
}
|
||||
return { success: true, message: "配置已迁移" };
|
||||
return {success: true, message: "配置已迁移"};
|
||||
} else {
|
||||
// 数据键名: classNumber/classworks-data-YYYYMMDD
|
||||
const itemDate = this.getItemDate(item);
|
||||
@ -634,7 +634,7 @@ export default {
|
||||
const [, year, month, day] = match;
|
||||
dateStr = `${year}${month}${day}`;
|
||||
} else {
|
||||
return { success: false, message: "无法确定日期格式" };
|
||||
return {success: false, message: "无法确定日期格式"};
|
||||
}
|
||||
}
|
||||
|
||||
@ -643,11 +643,11 @@ export default {
|
||||
JSON.stringify(value),
|
||||
`classworks-data-${dateStr}`
|
||||
);
|
||||
return { success: true, message: `${dateStr} 数据已迁移` };
|
||||
return {success: true, message: `${dateStr} 数据已迁移`};
|
||||
}
|
||||
} catch (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;
|
||||
|
||||
// 准备批量导入数据
|
||||
@ -721,7 +721,7 @@ export default {
|
||||
}
|
||||
);
|
||||
}
|
||||
return { success: true, message: "配置已迁移到服务器" };
|
||||
return {success: true, message: "配置已迁移到服务器"};
|
||||
} else {
|
||||
// 数据
|
||||
const itemDate = this.getItemDate(item);
|
||||
@ -736,7 +736,7 @@ export default {
|
||||
const [, year, month, day] = match;
|
||||
dateStr = `${year}${month}${day}`;
|
||||
} else {
|
||||
return { success: false, message: "无法确定日期格式" };
|
||||
return {success: false, message: "无法确定日期格式"};
|
||||
}
|
||||
}
|
||||
|
||||
@ -747,7 +747,7 @@ export default {
|
||||
headers: this.getRequestHeaders(),
|
||||
}
|
||||
);
|
||||
return { success: true, message: `${dateStr} 数据已迁移到服务器` };
|
||||
return {success: true, message: `${dateStr} 数据已迁移到服务器`};
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("服务器KV迁移失败:", error);
|
||||
@ -787,7 +787,7 @@ export default {
|
||||
);
|
||||
|
||||
// 移除学生列表
|
||||
const configWithoutStudentList = { ...value };
|
||||
const configWithoutStudentList = {...value};
|
||||
delete configWithoutStudentList.studentList;
|
||||
|
||||
// 准备批量导入数据
|
||||
@ -923,7 +923,7 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
return {success: true};
|
||||
} catch (error) {
|
||||
console.error("批量迁移到服务器失败:", error);
|
||||
return {
|
||||
|
||||
@ -4,7 +4,9 @@ Vue template files in this folder are automatically imported.
|
||||
|
||||
## 🚀 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`:
|
||||
|
||||
|
||||
@ -1,16 +1,16 @@
|
||||
<template>
|
||||
<v-dialog
|
||||
v-model="dialog"
|
||||
max-width="600"
|
||||
fullscreen-breakpoint="sm"
|
||||
max-width="600"
|
||||
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-icon icon="mdi-account-question" class="mr-2" />
|
||||
<v-icon class="mr-2" icon="mdi-account-question"/>
|
||||
随机点名
|
||||
<v-spacer />
|
||||
<v-btn icon="mdi-close" variant="text" @click="dialog = false" />
|
||||
<v-spacer/>
|
||||
<v-btn icon="mdi-close" variant="text" @click="dialog = false"/>
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text v-if="!isPickingStarted" class="text-center py-6">
|
||||
@ -18,13 +18,13 @@
|
||||
|
||||
<div class="d-flex justify-center align-center counter-container">
|
||||
<v-btn
|
||||
size="x-large"
|
||||
icon="mdi-minus"
|
||||
variant="tonal"
|
||||
color="primary"
|
||||
:disabled="count <= 1"
|
||||
@click="decrementCount"
|
||||
class="counter-btn"
|
||||
color="primary"
|
||||
icon="mdi-minus"
|
||||
size="x-large"
|
||||
variant="tonal"
|
||||
@click="decrementCount"
|
||||
/>
|
||||
|
||||
<div class="count-display mx-8">
|
||||
@ -33,13 +33,13 @@
|
||||
</div>
|
||||
|
||||
<v-btn
|
||||
size="x-large"
|
||||
icon="mdi-plus"
|
||||
variant="tonal"
|
||||
color="primary"
|
||||
:disabled="count >= maxAllowedCount"
|
||||
@click="incrementCount"
|
||||
class="counter-btn"
|
||||
color="primary"
|
||||
icon="mdi-plus"
|
||||
size="x-large"
|
||||
variant="tonal"
|
||||
@click="incrementCount"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -47,13 +47,13 @@
|
||||
<div class="mode-switch-container mt-6">
|
||||
<v-btn-toggle
|
||||
v-model="pickerMode"
|
||||
color="primary"
|
||||
rounded="pill"
|
||||
mandatory
|
||||
class="mode-toggle"
|
||||
color="primary"
|
||||
mandatory
|
||||
rounded="pill"
|
||||
>
|
||||
<v-btn value="name" prepend-icon="mdi-account">姓名模式</v-btn>
|
||||
<v-btn value="number" prepend-icon="mdi-numeric">学号模式</v-btn>
|
||||
<v-btn prepend-icon="mdi-account" value="name">姓名模式</v-btn>
|
||||
<v-btn prepend-icon="mdi-numeric" value="number">学号模式</v-btn>
|
||||
</v-btn-toggle>
|
||||
</div>
|
||||
|
||||
@ -63,36 +63,36 @@
|
||||
<div class="d-flex justify-center align-center gap-4">
|
||||
<v-text-field
|
||||
v-model.number="minNumber"
|
||||
label="最小值"
|
||||
type="number"
|
||||
min="1"
|
||||
max="100"
|
||||
hide-details
|
||||
class="number-input"
|
||||
density="compact"
|
||||
hide-details
|
||||
label="最小值"
|
||||
max="100"
|
||||
min="1"
|
||||
type="number"
|
||||
/>
|
||||
<span class="mx-2">至</span>
|
||||
<v-text-field
|
||||
v-model.number="maxNumber"
|
||||
label="最大值"
|
||||
type="number"
|
||||
min="1"
|
||||
max="100"
|
||||
hide-details
|
||||
class="number-input"
|
||||
density="compact"
|
||||
hide-details
|
||||
label="最大值"
|
||||
max="100"
|
||||
min="1"
|
||||
type="number"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<v-btn
|
||||
size="x-large"
|
||||
color="primary"
|
||||
prepend-icon="mdi-dice-multiple"
|
||||
@click="startPicking"
|
||||
:disabled="filteredStudents.length === 0"
|
||||
class="start-btn"
|
||||
color="primary"
|
||||
prepend-icon="mdi-dice-multiple"
|
||||
size="x-large"
|
||||
@click="startPicking"
|
||||
>
|
||||
开始抽取
|
||||
</v-btn>
|
||||
@ -112,10 +112,10 @@
|
||||
<v-tooltip v-if="pickerMode === 'name'" location="bottom">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-icon
|
||||
v-bind="props"
|
||||
class="ml-1"
|
||||
icon="mdi-information-outline"
|
||||
size="small"
|
||||
class="ml-1"
|
||||
v-bind="props"
|
||||
/>
|
||||
</template>
|
||||
<div class="pa-2">
|
||||
@ -136,18 +136,18 @@
|
||||
<v-chip
|
||||
:color="tempFilters.excludeLate ? 'warning' : 'default'"
|
||||
:variant="tempFilters.excludeLate ? 'elevated' : 'text'"
|
||||
@click="tempFilters.excludeLate = !tempFilters.excludeLate"
|
||||
prepend-icon="mdi-clock-alert"
|
||||
class="filter-chip"
|
||||
prepend-icon="mdi-clock-alert"
|
||||
@click="tempFilters.excludeLate = !tempFilters.excludeLate"
|
||||
>
|
||||
{{ tempFilters.excludeLate ? "排除" : "包含" }}迟到学生
|
||||
</v-chip>
|
||||
<v-chip
|
||||
:color="tempFilters.excludeAbsent ? 'error' : 'default'"
|
||||
:variant="tempFilters.excludeAbsent ? 'elevated' : 'text'"
|
||||
@click="tempFilters.excludeAbsent = !tempFilters.excludeAbsent"
|
||||
prepend-icon="mdi-account-off"
|
||||
class="filter-chip"
|
||||
prepend-icon="mdi-account-off"
|
||||
@click="tempFilters.excludeAbsent = !tempFilters.excludeAbsent"
|
||||
>
|
||||
{{ tempFilters.excludeAbsent ? "排除" : "包含" }}请假学生
|
||||
</v-chip>
|
||||
@ -155,9 +155,9 @@
|
||||
<v-chip
|
||||
:color="tempFilters.excludeExcluded ? 'grey' : 'default'"
|
||||
:variant="tempFilters.excludeExcluded ? 'elevated' : 'text'"
|
||||
@click="tempFilters.excludeExcluded = !tempFilters.excludeExcluded"
|
||||
prepend-icon="mdi-account-cancel"
|
||||
class="filter-chip"
|
||||
prepend-icon="mdi-account-cancel"
|
||||
@click="tempFilters.excludeExcluded = !tempFilters.excludeExcluded"
|
||||
>
|
||||
{{ tempFilters.excludeExcluded ? "排除" : "包含" }}不参与学生
|
||||
</v-chip>
|
||||
@ -169,15 +169,15 @@
|
||||
<div v-if="isAnimating" class="animation-container">
|
||||
<div class="animation-wrapper">
|
||||
<transition-group
|
||||
class="shuffle-container"
|
||||
name="shuffle"
|
||||
tag="div"
|
||||
class="shuffle-container"
|
||||
>
|
||||
<div
|
||||
v-for="(student, index) in animationStudents"
|
||||
:key="student.id"
|
||||
class="student-item"
|
||||
:class="{ highlighted: highlightedIndices.includes(index) }"
|
||||
class="student-item"
|
||||
>
|
||||
{{ student.name }}
|
||||
</div>
|
||||
@ -190,46 +190,46 @@
|
||||
<v-card
|
||||
v-for="(student, index) in pickedStudents"
|
||||
:key="index"
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
class="mb-2 result-card"
|
||||
color="primary"
|
||||
variant="outlined"
|
||||
>
|
||||
<v-card-text
|
||||
class="text-h4 text-center py-4 d-flex align-center justify-center"
|
||||
>
|
||||
{{ student }}
|
||||
<v-btn
|
||||
icon="mdi-refresh"
|
||||
variant="text"
|
||||
size="small"
|
||||
class="ml-2 refresh-btn"
|
||||
@click="refreshSingleStudent(index)"
|
||||
:disabled="remainingStudents.length === 0"
|
||||
:title="
|
||||
remainingStudents.length === 0
|
||||
? '没有更多可用学生'
|
||||
: '重新抽取此学生'
|
||||
"
|
||||
class="ml-2 refresh-btn"
|
||||
icon="mdi-refresh"
|
||||
size="small"
|
||||
variant="text"
|
||||
@click="refreshSingleStudent(index)"
|
||||
/>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<div class="mt-8 d-flex justify-center">
|
||||
<v-btn
|
||||
class="mx-2"
|
||||
color="primary"
|
||||
prepend-icon="mdi-refresh"
|
||||
@click="resetPicker"
|
||||
size="large"
|
||||
class="mx-2"
|
||||
@click="resetPicker"
|
||||
>
|
||||
重新抽取
|
||||
</v-btn>
|
||||
<v-btn
|
||||
class="mx-2"
|
||||
color="grey"
|
||||
size="large"
|
||||
variant="outlined"
|
||||
@click="dialog = false"
|
||||
size="large"
|
||||
class="mx-2"
|
||||
>
|
||||
关闭
|
||||
</v-btn>
|
||||
@ -241,7 +241,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getSetting, setSetting } from "@/utils/settings";
|
||||
import {getSetting, setSetting} from "@/utils/settings";
|
||||
|
||||
export default {
|
||||
name: "RandomPicker",
|
||||
@ -253,7 +253,7 @@ export default {
|
||||
attendance: {
|
||||
type: Object,
|
||||
required: true,
|
||||
default: () => ({ absent: [], late: [], exclude: [] }),
|
||||
default: () => ({absent: [], late: [], exclude: []}),
|
||||
},
|
||||
},
|
||||
data() {
|
||||
|
||||
@ -2,20 +2,21 @@
|
||||
<v-dialog v-model="isVisible" max-width="500" persistent>
|
||||
<v-card class="rate-limit-modal">
|
||||
<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-text class="pa-6">
|
||||
<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-list
|
||||
v-for="(request, index) in activeRequests"
|
||||
:key="index"
|
||||
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>
|
||||
等待时间:
|
||||
<span class="text-primary font-weight-bold">{{
|
||||
@ -25,7 +26,8 @@
|
||||
<v-list-item-subtitle>
|
||||
{{ request.method }} {{ request.path }}
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item></v-list
|
||||
</v-list-item>
|
||||
</v-list
|
||||
>
|
||||
<v-divider
|
||||
v-if="index < activeRequests.length - 1"
|
||||
@ -41,7 +43,7 @@
|
||||
|
||||
<v-card-actions class="pa-4 pt-0">
|
||||
<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>
|
||||
</v-dialog>
|
||||
|
||||
@ -1,15 +1,15 @@
|
||||
<template>
|
||||
<v-alert
|
||||
v-if="isReadOnly"
|
||||
class="readonly-warning"
|
||||
closable
|
||||
prominent
|
||||
type="warning"
|
||||
variant="tonal"
|
||||
prominent
|
||||
closable
|
||||
class="readonly-warning"
|
||||
@click:close="dismissed = true"
|
||||
>
|
||||
<template #prepend>
|
||||
<v-icon icon="mdi-lock-alert" />
|
||||
<v-icon icon="mdi-lock-alert"/>
|
||||
</template>
|
||||
<v-alert-title>当前使用只读 Token</v-alert-title>
|
||||
<div class="text-body-2">
|
||||
@ -32,8 +32,8 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, watch } from 'vue'
|
||||
import { getSetting } from '@/utils/settings'
|
||||
import {ref, computed, onMounted, watch} from 'vue'
|
||||
import {getSetting} from '@/utils/settings'
|
||||
import axios from '@/axios/axios'
|
||||
|
||||
const props = defineProps({
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
<template>
|
||||
<v-card elevation="2" class="settings-card rounded-lg">
|
||||
<v-card class="settings-card rounded-lg" elevation="2">
|
||||
<v-card-item>
|
||||
<template #prepend>
|
||||
<v-icon
|
||||
:icon="icon"
|
||||
size="large"
|
||||
class="mr-2"
|
||||
size="large"
|
||||
/>
|
||||
</template>
|
||||
<v-card-title class="text-h6">{{ title }}</v-card-title>
|
||||
@ -14,15 +14,15 @@
|
||||
<v-card-text>
|
||||
<v-progress-linear
|
||||
v-if="loading"
|
||||
indeterminate
|
||||
color="primary"
|
||||
class="mb-4"
|
||||
color="primary"
|
||||
indeterminate
|
||||
/>
|
||||
<slot />
|
||||
<slot/>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions v-if="$slots.actions" class="pa-4">
|
||||
<slot name="actions" />
|
||||
<slot name="actions"/>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
<!-- 统一链接生成器卡片 -->
|
||||
<v-card border class="unified-link-generator">
|
||||
<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>
|
||||
|
||||
@ -13,7 +13,7 @@
|
||||
</div>
|
||||
|
||||
<!-- 预配置认证信息部分 -->
|
||||
<v-card variant="tonal" class="mb-4">
|
||||
<v-card class="mb-4" variant="tonal">
|
||||
<v-card-title class="text-subtitle-1">
|
||||
<v-icon start>mdi-account-key</v-icon>
|
||||
预配置认证信息
|
||||
@ -24,23 +24,23 @@
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model="preconfigForm.namespace"
|
||||
label="命名空间"
|
||||
variant="outlined"
|
||||
prepend-inner-icon="mdi-identifier"
|
||||
placeholder="例如: classroom-001"
|
||||
hint="设备的命名空间标识符"
|
||||
label="命名空间"
|
||||
persistent-hint
|
||||
placeholder="例如: classroom-001"
|
||||
prepend-inner-icon="mdi-identifier"
|
||||
variant="outlined"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model="preconfigForm.authCode"
|
||||
label="认证码"
|
||||
variant="outlined"
|
||||
prepend-inner-icon="mdi-lock-outline"
|
||||
placeholder="设备认证码(可选)"
|
||||
hint="留空则需要用户手动输入"
|
||||
label="认证码"
|
||||
persistent-hint
|
||||
placeholder="设备认证码(可选)"
|
||||
prepend-inner-icon="mdi-lock-outline"
|
||||
variant="outlined"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
@ -49,10 +49,10 @@
|
||||
<v-col cols="12">
|
||||
<v-checkbox
|
||||
v-model="preconfigForm.autoExecute"
|
||||
label="自动执行认证"
|
||||
hint="启用后会自动尝试认证,即使没有认证码也会尝试"
|
||||
persistent-hint
|
||||
density="compact"
|
||||
hint="启用后会自动尝试认证,即使没有认证码也会尝试"
|
||||
label="自动执行认证"
|
||||
persistent-hint
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
@ -60,36 +60,38 @@
|
||||
<!-- 预配置信息预览 -->
|
||||
<v-alert
|
||||
v-if="preconfigForm.namespace"
|
||||
class="mt-3"
|
||||
type="info"
|
||||
variant="tonal"
|
||||
class="mt-3"
|
||||
>
|
||||
<div class="text-subtitle-2 mb-2">预配置信息:</div>
|
||||
<v-chip size="small" class="mr-2 mb-1">
|
||||
<v-icon start size="small">mdi-identifier</v-icon>
|
||||
<v-chip class="mr-2 mb-1" size="small">
|
||||
<v-icon size="small" start>mdi-identifier</v-icon>
|
||||
命名空间: {{ preconfigForm.namespace }}
|
||||
</v-chip>
|
||||
<v-chip
|
||||
v-if="preconfigForm.authCode"
|
||||
size="small"
|
||||
class="mr-2 mb-1"
|
||||
color="warning"
|
||||
size="small"
|
||||
>
|
||||
<v-icon start size="small">mdi-lock</v-icon>
|
||||
认证码: {{ preconfigForm.authCode.length > 8 ? preconfigForm.authCode.substring(0, 8) + "..." : preconfigForm.authCode }}
|
||||
<v-icon size="small" start>mdi-lock</v-icon>
|
||||
认证码: {{ preconfigForm.authCode.length > 8 ? preconfigForm.authCode.substring(0, 8) + "..." :
|
||||
preconfigForm.authCode }}
|
||||
</v-chip>
|
||||
<v-chip v-else size="small" class="mr-2 mb-1" color="grey">
|
||||
<v-icon start size="small">mdi-lock-open</v-icon>
|
||||
<v-chip v-else class="mr-2 mb-1" color="grey" size="small">
|
||||
<v-icon size="small" start>mdi-lock-open</v-icon>
|
||||
无认证码
|
||||
</v-chip>
|
||||
<v-chip
|
||||
size="small"
|
||||
class="mr-2 mb-1"
|
||||
: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"
|
||||
}}</v-icon>
|
||||
}}
|
||||
</v-icon>
|
||||
{{ preconfigForm.autoExecute ? "自动认证" : "手动认证" }}
|
||||
</v-chip>
|
||||
</v-alert>
|
||||
@ -97,7 +99,7 @@
|
||||
</v-card>
|
||||
|
||||
<!-- 设置分享部分 -->
|
||||
<v-card variant="tonal" class="mb-4">
|
||||
<v-card class="mb-4" variant="tonal">
|
||||
<v-card-title class="text-subtitle-1">
|
||||
<v-icon start>mdi-cog-transfer</v-icon>
|
||||
设置分享(可选)
|
||||
@ -111,37 +113,37 @@
|
||||
<!-- 设置快速选择按钮 -->
|
||||
<div class="d-flex mb-3 gap-2 flex-wrap">
|
||||
<v-btn
|
||||
size="small"
|
||||
variant="tonal"
|
||||
color="primary"
|
||||
prepend-icon="mdi-server-network"
|
||||
size="small"
|
||||
variant="tonal"
|
||||
@click="selectDataSourceSettings"
|
||||
>
|
||||
数据源设置
|
||||
</v-btn>
|
||||
<v-btn
|
||||
size="small"
|
||||
variant="tonal"
|
||||
color="primary"
|
||||
prepend-icon="mdi-compare"
|
||||
size="small"
|
||||
variant="tonal"
|
||||
@click="selectChangedSettings"
|
||||
>
|
||||
已变更设置
|
||||
</v-btn>
|
||||
<v-btn
|
||||
size="small"
|
||||
variant="tonal"
|
||||
color="success"
|
||||
prepend-icon="mdi-select-all"
|
||||
size="small"
|
||||
variant="tonal"
|
||||
@click="selectAll"
|
||||
>
|
||||
全选
|
||||
</v-btn>
|
||||
<v-btn
|
||||
size="small"
|
||||
variant="tonal"
|
||||
color="error"
|
||||
prepend-icon="mdi-select-remove"
|
||||
size="small"
|
||||
variant="tonal"
|
||||
@click="resetSelection"
|
||||
>
|
||||
清除选择
|
||||
@ -150,7 +152,7 @@
|
||||
|
||||
<!-- 选择摘要 -->
|
||||
<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 }} 项设置
|
||||
</v-chip>
|
||||
|
||||
@ -158,17 +160,17 @@
|
||||
<v-chip
|
||||
v-for="item in selectedItems.slice(0, 3)"
|
||||
:key="item"
|
||||
size="small"
|
||||
class="mr-1"
|
||||
size="small"
|
||||
variant="text"
|
||||
>
|
||||
{{ getSettingDescription(item) }}
|
||||
</v-chip>
|
||||
<v-chip
|
||||
v-if="selectedItems.length > 3"
|
||||
color="grey"
|
||||
size="small"
|
||||
variant="text"
|
||||
color="grey"
|
||||
>
|
||||
+{{ selectedItems.length - 3 }} 更多
|
||||
</v-chip>
|
||||
@ -190,39 +192,39 @@
|
||||
<v-expansion-panel-text>
|
||||
<v-text-field
|
||||
v-model="search"
|
||||
class="mb-4"
|
||||
clearable
|
||||
hide-details
|
||||
label="搜索设置"
|
||||
prepend-inner-icon="mdi-magnify"
|
||||
single-line
|
||||
hide-details
|
||||
class="mb-4"
|
||||
clearable
|
||||
/>
|
||||
|
||||
<v-data-table
|
||||
:items-per-page="settingItems.length"
|
||||
v-model="selectedItems"
|
||||
:headers="headers"
|
||||
:items="filteredItems"
|
||||
item-value="key"
|
||||
v-model="selectedItems"
|
||||
show-select
|
||||
density="compact"
|
||||
class="rounded setting-table"
|
||||
@update:selected="handleSelectionChange"
|
||||
:items-per-page="settingItems.length"
|
||||
:sort-by="[{ key: 'isChanged', order: 'desc' }]"
|
||||
class="rounded setting-table"
|
||||
density="compact"
|
||||
item-value="key"
|
||||
show-select
|
||||
@update:selected="handleSelectionChange"
|
||||
>
|
||||
<template #[`item.description`]="{ item }">
|
||||
<div class="d-flex align-center">
|
||||
<v-icon
|
||||
size="small"
|
||||
:icon="item.icon"
|
||||
class="mr-2"
|
||||
size="small"
|
||||
/>
|
||||
{{ item.description }}
|
||||
<v-chip
|
||||
v-if="item.key === 'server.kvToken'"
|
||||
size="x-small"
|
||||
color="error"
|
||||
class="ml-2"
|
||||
color="error"
|
||||
size="x-small"
|
||||
>
|
||||
敏感
|
||||
</v-chip>
|
||||
@ -245,10 +247,10 @@
|
||||
|
||||
<template #[`item.isChanged`]="{ item }">
|
||||
<v-chip
|
||||
size="x-small"
|
||||
:color="item.isChanged ? 'warning' : 'success'"
|
||||
:text="item.isChanged ? '已修改' : '默认'"
|
||||
density="compact"
|
||||
size="x-small"
|
||||
/>
|
||||
</template>
|
||||
</v-data-table>
|
||||
@ -259,7 +261,7 @@
|
||||
</v-card>
|
||||
|
||||
<!-- 链接生成和操作部分 -->
|
||||
<v-card variant="outlined" class="mb-4">
|
||||
<v-card class="mb-4" variant="outlined">
|
||||
<v-card-title class="text-subtitle-1">
|
||||
<v-icon start>mdi-link</v-icon>
|
||||
生成的统一链接
|
||||
@ -269,27 +271,27 @@
|
||||
<!-- 操作按钮 -->
|
||||
<div class="d-flex mb-3 gap-2 flex-wrap">
|
||||
<v-btn
|
||||
variant="flat"
|
||||
:disabled="!preconfigForm.namespace.trim()"
|
||||
color="primary"
|
||||
prepend-icon="mdi-auto-fix"
|
||||
variant="flat"
|
||||
@click="generateUnifiedLink"
|
||||
:disabled="!preconfigForm.namespace.trim()"
|
||||
>
|
||||
生成统一链接
|
||||
</v-btn>
|
||||
<v-btn
|
||||
variant="tonal"
|
||||
:disabled="!unifiedLink"
|
||||
color="success"
|
||||
prepend-icon="mdi-test-tube"
|
||||
variant="tonal"
|
||||
@click="openTestLink"
|
||||
:disabled="!unifiedLink"
|
||||
>
|
||||
测试链接
|
||||
</v-btn>
|
||||
<v-btn
|
||||
variant="tonal"
|
||||
color="error"
|
||||
prepend-icon="mdi-delete"
|
||||
variant="tonal"
|
||||
@click="clearAll"
|
||||
>
|
||||
清空所有
|
||||
@ -299,38 +301,38 @@
|
||||
<!-- 生成的链接 -->
|
||||
<v-text-field
|
||||
v-model="unifiedLink"
|
||||
:append-inner-icon="linkCopied ? 'mdi-check' : 'mdi-content-copy'"
|
||||
:placeholder="preconfigForm.namespace ? '点击「生成统一链接」按钮' : '请先输入命名空间'"
|
||||
class="mb-3"
|
||||
label="统一链接"
|
||||
readonly
|
||||
variant="outlined"
|
||||
class="mb-3"
|
||||
:append-inner-icon="linkCopied ? 'mdi-check' : 'mdi-content-copy'"
|
||||
@click:append-inner="copyUnifiedLink"
|
||||
:placeholder="preconfigForm.namespace ? '点击「生成统一链接」按钮' : '请先输入命名空间'"
|
||||
/>
|
||||
|
||||
<!-- 链接内容预览 -->
|
||||
<v-alert
|
||||
v-if="unifiedLink"
|
||||
class="mb-3"
|
||||
type="success"
|
||||
variant="tonal"
|
||||
class="mb-3"
|
||||
>
|
||||
<div class="text-subtitle-2 mb-2">链接包含内容:</div>
|
||||
<div class="d-flex flex-wrap gap-1">
|
||||
<v-chip size="small" color="primary">
|
||||
<v-icon start size="small">mdi-account-key</v-icon>
|
||||
<v-chip color="primary" size="small">
|
||||
<v-icon size="small" start>mdi-account-key</v-icon>
|
||||
预配置认证
|
||||
</v-chip>
|
||||
<v-chip
|
||||
v-if="selectedItems.length > 0"
|
||||
size="small"
|
||||
color="secondary"
|
||||
size="small"
|
||||
>
|
||||
<v-icon start size="small">mdi-cog</v-icon>
|
||||
<v-icon size="small" start>mdi-cog</v-icon>
|
||||
{{ selectedItems.length }} 项设置
|
||||
</v-chip>
|
||||
<v-chip v-else size="small" color="grey">
|
||||
<v-icon start size="small">mdi-cog-off</v-icon>
|
||||
<v-chip v-else color="grey" size="small">
|
||||
<v-icon size="small" start>mdi-cog-off</v-icon>
|
||||
无额外设置
|
||||
</v-chip>
|
||||
</div>
|
||||
@ -389,16 +391,16 @@ export default {
|
||||
unifiedLink: "",
|
||||
|
||||
headers: [
|
||||
{ title: "", key: "data-table-select" },
|
||||
{ title: "设置项", key: "description", sortable: true },
|
||||
{ title: "当前值", key: "value", sortable: true },
|
||||
{title: "", key: "data-table-select"},
|
||||
{title: "设置项", key: "description", sortable: true},
|
||||
{title: "当前值", key: "value", sortable: true},
|
||||
{
|
||||
title: "键名",
|
||||
key: "key",
|
||||
class: "d-none d-sm-table-cell",
|
||||
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中存在
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
|
||||
@ -14,12 +14,12 @@
|
||||
<v-autocomplete
|
||||
v-model="selectedName"
|
||||
:items="studentList"
|
||||
clearable
|
||||
hide-details
|
||||
item-title="name"
|
||||
item-value="name"
|
||||
label="学生姓名"
|
||||
placeholder="选择您的姓名"
|
||||
clearable
|
||||
hide-details
|
||||
/>
|
||||
<div
|
||||
v-if="studentList.length > 0"
|
||||
@ -29,9 +29,9 @@
|
||||
</div>
|
||||
<v-alert
|
||||
v-if="error"
|
||||
class="mt-3"
|
||||
type="error"
|
||||
variant="tonal"
|
||||
class="mt-3"
|
||||
>
|
||||
{{ error }}
|
||||
</v-alert>
|
||||
@ -43,7 +43,7 @@
|
||||
>
|
||||
稍后设置
|
||||
</v-btn>
|
||||
<v-spacer />
|
||||
<v-spacer/>
|
||||
<v-btn
|
||||
:disabled="!selectedName || saving"
|
||||
:loading="saving"
|
||||
@ -58,18 +58,19 @@
|
||||
|
||||
<!-- 顶栏学生姓名显示(通过插槽暴露给父组件) -->
|
||||
<slot
|
||||
name="header-display"
|
||||
:student-name="currentStudentName"
|
||||
:is-student="isStudentToken"
|
||||
:open-dialog="openDialog"
|
||||
:student-name="currentStudentName"
|
||||
name="header-display"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch, onMounted } from 'vue'
|
||||
import { getSetting, watchSettings } from '@/utils/settings'
|
||||
import {ref, computed, watch, onMounted} from 'vue'
|
||||
import {getSetting, watchSettings} from '@/utils/settings'
|
||||
import axios from '@/axios/axios'
|
||||
import dataProvider from '@/utils/dataProvider'
|
||||
|
||||
const emit = defineEmits(['token-info-updated'])
|
||||
|
||||
const showDialog = ref(false)
|
||||
@ -236,7 +237,7 @@ watchSettings(() => {
|
||||
// 监听 tokenInfo 变化,通知父组件
|
||||
watch(tokenInfo, () => {
|
||||
emit('token-info-updated')
|
||||
}, { deep: true })
|
||||
}, {deep: true})
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
|
||||
@ -4,23 +4,23 @@
|
||||
<v-card-text>
|
||||
<v-textarea
|
||||
v-model="code"
|
||||
density="comfortable"
|
||||
hide-details="auto"
|
||||
label="替代代码"
|
||||
placeholder="请输入替代代码"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
rows="5"
|
||||
hide-details="auto"
|
||||
variant="outlined"
|
||||
/>
|
||||
<v-alert
|
||||
class="mt-3"
|
||||
type="info"
|
||||
variant="tonal"
|
||||
class="mt-3"
|
||||
>
|
||||
替代代码功能暂未实现,敬请期待
|
||||
</v-alert>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-spacer/>
|
||||
<v-btn
|
||||
v-if="showCancel"
|
||||
variant="text"
|
||||
@ -40,7 +40,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import {ref} from 'vue'
|
||||
|
||||
defineProps({
|
||||
showCancel: {
|
||||
|
||||
@ -3,9 +3,9 @@
|
||||
<v-card-text class="pa-8">
|
||||
<div class="text-center mb-6">
|
||||
<v-icon
|
||||
size="80"
|
||||
color="success"
|
||||
class="mb-4"
|
||||
color="success"
|
||||
size="80"
|
||||
>
|
||||
mdi-account-key
|
||||
</v-icon>
|
||||
@ -18,29 +18,29 @@
|
||||
</div>
|
||||
|
||||
<v-card
|
||||
variant="tonal"
|
||||
color="info"
|
||||
class="pa-4 mb-6"
|
||||
color="info"
|
||||
variant="tonal"
|
||||
>
|
||||
<div class="text-body-2">
|
||||
<v-icon
|
||||
size="20"
|
||||
class="mr-2"
|
||||
size="20"
|
||||
>
|
||||
mdi-information
|
||||
</v-icon>
|
||||
对于已有UUID的用户,您应当使用UUID与您的密码登录。
|
||||
对于已有UUID的用户,您应当使用UUID与您的密码登录。
|
||||
</div>
|
||||
</v-card>
|
||||
|
||||
<div class="form-section">
|
||||
<v-text-field
|
||||
v-model="form.namespace"
|
||||
label="命名空间"
|
||||
class="mb-4"
|
||||
variant="outlined"
|
||||
hide-details="auto"
|
||||
label="命名空间"
|
||||
prepend-inner-icon="mdi-identifier"
|
||||
variant="outlined"
|
||||
>
|
||||
|
||||
</v-text-field>
|
||||
@ -48,19 +48,19 @@
|
||||
<v-text-field
|
||||
v-model="form.password"
|
||||
label="认证码"
|
||||
prepend-inner-icon="mdi-lock-outline"
|
||||
type="text"
|
||||
variant="outlined"
|
||||
prepend-inner-icon="mdi-lock-outline"
|
||||
>
|
||||
|
||||
</v-text-field>
|
||||
|
||||
<v-alert
|
||||
v-if="error"
|
||||
type="error"
|
||||
variant="tonal"
|
||||
class="mt-4"
|
||||
closable
|
||||
type="error"
|
||||
variant="tonal"
|
||||
@click:close="error = ''"
|
||||
>
|
||||
{{ error }}
|
||||
@ -77,19 +77,19 @@
|
||||
>
|
||||
取消
|
||||
</v-btn>
|
||||
<v-spacer />
|
||||
<v-spacer/>
|
||||
<v-btn
|
||||
:disabled="!form.namespace || authenticating"
|
||||
:loading="authenticating"
|
||||
size="x-large"
|
||||
color="primary"
|
||||
variant="elevated"
|
||||
class="px-8"
|
||||
color="primary"
|
||||
size="x-large"
|
||||
variant="elevated"
|
||||
@click="authenticate"
|
||||
>
|
||||
<v-icon
|
||||
start
|
||||
size="24"
|
||||
start
|
||||
>
|
||||
mdi-login
|
||||
</v-icon>
|
||||
@ -100,8 +100,8 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue'
|
||||
import { getSetting, setSetting } from '@/utils/settings'
|
||||
import {ref, watch} from 'vue'
|
||||
import {getSetting, setSetting} from '@/utils/settings'
|
||||
import axios from '@/axios/axios'
|
||||
|
||||
const props = defineProps({
|
||||
@ -145,7 +145,7 @@ watch(
|
||||
}
|
||||
}
|
||||
},
|
||||
{ immediate: true, deep: true }
|
||||
{immediate: true, deep: true}
|
||||
)
|
||||
|
||||
const authenticate = async () => {
|
||||
@ -197,7 +197,7 @@ const authenticate = async () => {
|
||||
// 暴露清空表单的方法
|
||||
defineExpose({
|
||||
reset: () => {
|
||||
form.value = { namespace: '', password: '' }
|
||||
form.value = {namespace: '', password: ''}
|
||||
error.value = ''
|
||||
}
|
||||
})
|
||||
|
||||
@ -15,9 +15,9 @@
|
||||
>
|
||||
<div class="text-center mb-6">
|
||||
<v-icon
|
||||
size="80"
|
||||
color="primary"
|
||||
class="mb-4"
|
||||
color="primary"
|
||||
size="80"
|
||||
>
|
||||
mdi-hand-wave
|
||||
</v-icon>
|
||||
@ -40,22 +40,22 @@
|
||||
</h3>
|
||||
|
||||
<v-card
|
||||
variant="tonal"
|
||||
color="primary"
|
||||
class="pa-6 mb-6"
|
||||
color="primary"
|
||||
variant="tonal"
|
||||
>
|
||||
<div class="relationship-diagram">
|
||||
<!-- Classworks 应用 -->
|
||||
<div class="diagram-item">
|
||||
<v-card
|
||||
elevation="8"
|
||||
color="blue-darken-1"
|
||||
class="pa-4"
|
||||
color="blue-darken-1"
|
||||
elevation="8"
|
||||
>
|
||||
<div class="text-center">
|
||||
<v-icon
|
||||
size="60"
|
||||
color="white"
|
||||
size="60"
|
||||
>
|
||||
mdi-laptop
|
||||
</v-icon>
|
||||
@ -70,10 +70,10 @@
|
||||
|
||||
<div class="diagram-description mt-3">
|
||||
<v-chip
|
||||
color="blue"
|
||||
variant="flat"
|
||||
size="small"
|
||||
class="mb-2"
|
||||
color="blue"
|
||||
size="small"
|
||||
variant="flat"
|
||||
>
|
||||
前端应用
|
||||
</v-chip>
|
||||
@ -88,8 +88,8 @@
|
||||
<!-- 连接线 -->
|
||||
<div class="diagram-connector">
|
||||
<v-icon
|
||||
size="40"
|
||||
color="primary"
|
||||
size="40"
|
||||
>
|
||||
mdi-swap-horizontal
|
||||
</v-icon>
|
||||
@ -101,14 +101,14 @@
|
||||
<!-- Classworks KV -->
|
||||
<div class="diagram-item">
|
||||
<v-card
|
||||
elevation="8"
|
||||
color="green-darken-1"
|
||||
class="pa-4"
|
||||
color="green-darken-1"
|
||||
elevation="8"
|
||||
>
|
||||
<div class="text-center">
|
||||
<v-icon
|
||||
size="60"
|
||||
color="white"
|
||||
size="60"
|
||||
>
|
||||
mdi-cloud-sync
|
||||
</v-icon>
|
||||
@ -123,10 +123,10 @@
|
||||
|
||||
<div class="diagram-description mt-3">
|
||||
<v-chip
|
||||
color="green"
|
||||
variant="flat"
|
||||
size="small"
|
||||
class="mb-2"
|
||||
color="green"
|
||||
size="small"
|
||||
variant="flat"
|
||||
>
|
||||
后端服务
|
||||
</v-chip>
|
||||
@ -153,9 +153,9 @@
|
||||
</h3>
|
||||
|
||||
<v-card
|
||||
variant="tonal"
|
||||
color="info"
|
||||
class="mb-6 pa-4"
|
||||
color="info"
|
||||
variant="tonal"
|
||||
>
|
||||
<div class="text-body-2">
|
||||
比如:在家里电脑、手机上查看,或者多个教室设备共享数据
|
||||
@ -164,17 +164,17 @@
|
||||
|
||||
<div class="button-group">
|
||||
<v-btn
|
||||
size="x-large"
|
||||
block
|
||||
variant="elevated"
|
||||
color="primary"
|
||||
class="mb-4 py-6"
|
||||
color="primary"
|
||||
size="x-large"
|
||||
variant="elevated"
|
||||
@click="selectStorageType('cloud')"
|
||||
>
|
||||
<div class="d-flex flex-column align-center py-2">
|
||||
<v-icon
|
||||
size="40"
|
||||
class="mb-2"
|
||||
size="40"
|
||||
>
|
||||
mdi-cloud-check
|
||||
</v-icon>
|
||||
@ -184,16 +184,16 @@
|
||||
</v-btn>
|
||||
|
||||
<v-btn
|
||||
size="x-large"
|
||||
block
|
||||
variant="outlined"
|
||||
class="py-6"
|
||||
size="x-large"
|
||||
variant="outlined"
|
||||
@click="selectStorageType('local')"
|
||||
>
|
||||
<div class="d-flex flex-column align-center py-2">
|
||||
<v-icon
|
||||
size="40"
|
||||
class="mb-2"
|
||||
size="40"
|
||||
>
|
||||
mdi-laptop
|
||||
</v-icon>
|
||||
@ -211,9 +211,9 @@
|
||||
>
|
||||
<div class="text-center mb-6">
|
||||
<v-icon
|
||||
size="80"
|
||||
color="success"
|
||||
class="mb-4"
|
||||
color="success"
|
||||
size="80"
|
||||
>
|
||||
mdi-check-circle
|
||||
</v-icon>
|
||||
@ -221,8 +221,8 @@
|
||||
您可以使用本地模式
|
||||
</h3>
|
||||
<v-card
|
||||
variant="tonal"
|
||||
class="pa-4 text-left"
|
||||
variant="tonal"
|
||||
>
|
||||
<div class="text-body-1 mb-2">
|
||||
此数据将存储在您的浏览器中,如果您的浏览器不支持IndexedDB,可能会出现问题。如果您经常清除浏览器数据,请谨慎使用本地模式。
|
||||
@ -241,9 +241,9 @@
|
||||
>
|
||||
<div class="text-center mb-6">
|
||||
<v-icon
|
||||
size="80"
|
||||
color="primary"
|
||||
class="mb-4"
|
||||
color="primary"
|
||||
size="80"
|
||||
>
|
||||
mdi-cloud-cog
|
||||
</v-icon>
|
||||
@ -253,8 +253,8 @@
|
||||
</div>
|
||||
|
||||
<v-card
|
||||
variant="tonal"
|
||||
class="pa-6 mb-6"
|
||||
variant="tonal"
|
||||
>
|
||||
<div class="d-flex flex-column flex-sm-row align-center">
|
||||
<div class="flex-grow-1">
|
||||
@ -266,9 +266,9 @@
|
||||
</p>
|
||||
<v-btn
|
||||
color="primary"
|
||||
prepend-icon="mdi-flash"
|
||||
size="large"
|
||||
variant="elevated"
|
||||
prepend-icon="mdi-flash"
|
||||
@click="goToProgressiveStep"
|
||||
>
|
||||
自动注册
|
||||
@ -277,16 +277,17 @@
|
||||
</div>
|
||||
</v-card>
|
||||
<div class="mb-6">
|
||||
也可以手动前往 Classworks KV 控制台获取认证信息:</div>
|
||||
也可以手动前往 Classworks KV 控制台获取认证信息:
|
||||
</div>
|
||||
<v-card
|
||||
:variant="kvserverurl=='https://kv.houlang.cloud'? 'elevated' : 'outlined'"
|
||||
:color=" kvserverurl=='https://kv.houlang.cloud'? 'primary' : 'error' "
|
||||
:variant="kvserverurl=='https://kv.houlang.cloud'? 'elevated' : 'outlined'"
|
||||
class="pa-6 mb-6"
|
||||
@click="openKVSite"
|
||||
>
|
||||
<v-icon
|
||||
size="48"
|
||||
class="mb-3"
|
||||
size="48"
|
||||
>
|
||||
mdi-open-in-new
|
||||
</v-icon>
|
||||
@ -322,12 +323,13 @@
|
||||
</v-expansion-panel-title>
|
||||
<v-expansion-panel-text>
|
||||
<v-card
|
||||
variant="tonal"
|
||||
color="success"
|
||||
class="pa-4"
|
||||
color="success"
|
||||
variant="tonal"
|
||||
>
|
||||
<div class="text-body-2 mb-2">
|
||||
如果您之前已经使用过 Classworks KV,可以直接使用您的 <strong>UUID(命名空间)</strong> 和 <strong>设置的密码</strong> 进行认证。
|
||||
如果您之前已经使用过 Classworks KV,可以直接使用您的 <strong>UUID(命名空间)</strong> 和
|
||||
<strong>设置的密码</strong> 进行认证。
|
||||
</div>
|
||||
<div class="text-body-2">
|
||||
返回上一页,点击"已注册"按钮,输入您的认证信息即可登录。
|
||||
@ -350,9 +352,9 @@
|
||||
</v-expansion-panel-title>
|
||||
<v-expansion-panel-text>
|
||||
<v-card
|
||||
variant="tonal"
|
||||
color="info"
|
||||
class="pa-4"
|
||||
color="info"
|
||||
variant="tonal"
|
||||
>
|
||||
<div class="text-body-2 mb-2">
|
||||
不同的密码对应不同的设备类型,这将由 <strong>管理员管理</strong>。
|
||||
@ -385,10 +387,10 @@
|
||||
>
|
||||
<div class="text-center mb-6">
|
||||
<v-avatar
|
||||
size="80"
|
||||
color="primary"
|
||||
variant="tonal"
|
||||
class="mb-4"
|
||||
color="primary"
|
||||
size="80"
|
||||
variant="tonal"
|
||||
>
|
||||
<v-icon size="48">
|
||||
mdi-rocket-launch
|
||||
@ -404,21 +406,20 @@
|
||||
|
||||
<v-progress-linear
|
||||
:model-value="progressValue"
|
||||
height="8"
|
||||
color="primary"
|
||||
rounded
|
||||
class="mb-6"
|
||||
color="primary"
|
||||
height="8"
|
||||
rounded
|
||||
/>
|
||||
|
||||
|
||||
|
||||
<v-row>
|
||||
<v-col
|
||||
cols="12"
|
||||
>
|
||||
<v-card
|
||||
variant="tonal"
|
||||
:color="statusColor"
|
||||
variant="tonal"
|
||||
>
|
||||
<v-card-item>
|
||||
<div class="d-flex align-center mb-3">
|
||||
@ -487,8 +488,8 @@
|
||||
<v-btn
|
||||
v-if="progressiveStatus === 'idle'"
|
||||
color="primary"
|
||||
size="large"
|
||||
prepend-icon="mdi-play"
|
||||
size="large"
|
||||
@click="startProgressiveRegister"
|
||||
>
|
||||
开始创建
|
||||
@ -497,8 +498,8 @@
|
||||
<v-btn
|
||||
v-if="progressiveStatus === 'error'"
|
||||
color="error"
|
||||
variant="outlined"
|
||||
prepend-icon="mdi-refresh"
|
||||
variant="outlined"
|
||||
@click="retryProgressiveRegister"
|
||||
>
|
||||
重试
|
||||
@ -506,10 +507,10 @@
|
||||
|
||||
<v-btn
|
||||
v-if="progressiveStatus === 'registering'"
|
||||
color="primary"
|
||||
variant="tonal"
|
||||
:loading="true"
|
||||
color="primary"
|
||||
prepend-icon="mdi-progress-clock"
|
||||
variant="tonal"
|
||||
>
|
||||
正在执行…
|
||||
</v-btn>
|
||||
@ -517,9 +518,9 @@
|
||||
<v-btn
|
||||
v-if="progressiveStatus === 'success'"
|
||||
color="success"
|
||||
prepend-icon="mdi-check-circle"
|
||||
size="large"
|
||||
variant="elevated"
|
||||
prepend-icon="mdi-check-circle"
|
||||
@click="applyTokenAndClose"
|
||||
>
|
||||
应用令牌并关闭
|
||||
@ -528,9 +529,9 @@
|
||||
<v-btn
|
||||
v-if="progressiveStatus === 'success'"
|
||||
color="primary"
|
||||
prepend-icon="mdi-open-in-new"
|
||||
size="large"
|
||||
variant="outlined"
|
||||
prepend-icon="mdi-open-in-new"
|
||||
@click="openAuthPage"
|
||||
>
|
||||
前往绑定账户
|
||||
@ -552,12 +553,12 @@
|
||||
</v-icon>
|
||||
上一步
|
||||
</v-btn>
|
||||
<v-spacer />
|
||||
<v-spacer/>
|
||||
<v-btn
|
||||
v-if="currentStep < totalSteps && currentStep !== 4"
|
||||
:disabled="currentStep === 3 && !storageType"
|
||||
size="large"
|
||||
color="primary"
|
||||
size="large"
|
||||
variant="elevated"
|
||||
@click="nextStep"
|
||||
>
|
||||
@ -568,8 +569,8 @@
|
||||
</v-btn>
|
||||
<v-btn
|
||||
v-if="currentStep === totalSteps || currentStep === 4"
|
||||
size="large"
|
||||
color="primary"
|
||||
size="large"
|
||||
variant="elevated"
|
||||
@click="finish"
|
||||
>
|
||||
@ -580,10 +581,11 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import { getSetting, setSetting } from '@/utils/settings'
|
||||
import {ref, computed} from 'vue'
|
||||
import {getSetting, setSetting} from '@/utils/settings'
|
||||
import axios from '@/axios/axios'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import {v4 as uuidv4} from 'uuid'
|
||||
|
||||
const emit = defineEmits(['close', 'success'])
|
||||
const kvserverurl = getSetting('server.authDomain')
|
||||
const currentStep = ref(1)
|
||||
@ -596,7 +598,7 @@ const progressiveError = ref('')
|
||||
const deviceInfo = ref(null)
|
||||
const tokenData = ref(null) // 保存获取到的 token 数据
|
||||
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 = () => {
|
||||
if (currentStep.value < totalSteps) {
|
||||
@ -638,28 +640,28 @@ const statusColor = computed(() => {
|
||||
return progressiveStatus.value === 'success'
|
||||
? 'success'
|
||||
: progressiveStatus.value === 'error'
|
||||
? 'error'
|
||||
: 'primary'
|
||||
? 'error'
|
||||
: 'primary'
|
||||
})
|
||||
|
||||
const statusIcon = computed(() => {
|
||||
return progressiveStatus.value === 'success'
|
||||
? 'mdi-check-circle'
|
||||
: progressiveStatus.value === 'error'
|
||||
? 'mdi-alert-circle'
|
||||
: progressiveStatus.value === 'registering'
|
||||
? 'mdi-progress-clock'
|
||||
: 'mdi-rocket-launch'
|
||||
? 'mdi-alert-circle'
|
||||
: progressiveStatus.value === 'registering'
|
||||
? 'mdi-progress-clock'
|
||||
: 'mdi-rocket-launch'
|
||||
})
|
||||
|
||||
const statusTitle = computed(() => {
|
||||
return progressiveStatus.value === 'success'
|
||||
? '完成!设备已创建'
|
||||
: progressiveStatus.value === 'error'
|
||||
? '创建失败'
|
||||
: progressiveStatus.value === 'registering'
|
||||
? '正在执行…'
|
||||
: '准备开始'
|
||||
? '创建失败'
|
||||
: progressiveStatus.value === 'registering'
|
||||
? '正在执行…'
|
||||
: '准备开始'
|
||||
})
|
||||
|
||||
const addLog = (message) => {
|
||||
@ -667,7 +669,7 @@ const addLog = (message) => {
|
||||
const hh = String(now.getHours()).padStart(2, '0')
|
||||
const mm = String(now.getMinutes()).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'
|
||||
progressiveError.value = ''
|
||||
logs.value = []
|
||||
stepStates.value = { 1: false, 2: false, 3: false, 4: false }
|
||||
stepStates.value = {1: false, 2: false, 3: false, 4: false}
|
||||
|
||||
try {
|
||||
addLog('正在生成设备信息…')
|
||||
@ -692,12 +694,12 @@ const startProgressiveRegister = async () => {
|
||||
stepStates.value[1] = true
|
||||
|
||||
addLog('向服务器注册设备…')
|
||||
const response = await axios.post(`${serverUrl}/devices`, { uuid, deviceName })
|
||||
const response = await axios.post(`${serverUrl}/devices`, {uuid, deviceName})
|
||||
void response
|
||||
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))
|
||||
|
||||
addLog('获取访问令牌…')
|
||||
@ -742,7 +744,7 @@ const retryProgressiveRegister = () => {
|
||||
progressiveStatus.value = 'idle'
|
||||
progressiveError.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 = () => {
|
||||
@ -853,7 +855,7 @@ const applyTokenAndClose = () => {
|
||||
}
|
||||
|
||||
.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 {
|
||||
@ -865,7 +867,7 @@ const applyTokenAndClose = () => {
|
||||
}
|
||||
|
||||
.progressive-register-card code {
|
||||
background: rgba(0,0,0,0.1);
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
|
||||
|
||||
@ -2,8 +2,8 @@
|
||||
<v-card>
|
||||
<v-card-title class="d-flex align-center">
|
||||
<v-icon
|
||||
icon="mdi-account-plus"
|
||||
class="mr-2"
|
||||
icon="mdi-account-plus"
|
||||
/>
|
||||
渐进式注册
|
||||
</v-card-title>
|
||||
@ -15,12 +15,12 @@
|
||||
</p>
|
||||
|
||||
<v-alert
|
||||
class="mb-4"
|
||||
type="info"
|
||||
variant="tonal"
|
||||
class="mb-4"
|
||||
>
|
||||
<template #prepend>
|
||||
<v-icon icon="mdi-information" />
|
||||
<v-icon icon="mdi-information"/>
|
||||
</template>
|
||||
系统将自动为您创建设备并获取访问令牌,无需手动配置
|
||||
</v-alert>
|
||||
@ -30,10 +30,10 @@
|
||||
<div v-else-if="isRegistering">
|
||||
<div class="text-center py-4">
|
||||
<v-progress-circular
|
||||
class="mb-4"
|
||||
color="primary"
|
||||
indeterminate
|
||||
size="48"
|
||||
color="primary"
|
||||
class="mb-4"
|
||||
/>
|
||||
<p class="text-h6 mb-2">
|
||||
正在注册设备...
|
||||
@ -47,12 +47,12 @@
|
||||
<!-- 注册成功 -->
|
||||
<div v-else-if="isRegistered && deviceInfo">
|
||||
<v-alert
|
||||
class="mb-4"
|
||||
type="success"
|
||||
variant="tonal"
|
||||
class="mb-4"
|
||||
>
|
||||
<template #prepend>
|
||||
<v-icon icon="mdi-check-circle" />
|
||||
<v-icon icon="mdi-check-circle"/>
|
||||
</template>
|
||||
设备注册成功!已自动获取访问令牌
|
||||
</v-alert>
|
||||
@ -60,7 +60,7 @@
|
||||
<v-list>
|
||||
<v-list-item>
|
||||
<template #prepend>
|
||||
<v-icon icon="mdi-identifier" />
|
||||
<v-icon icon="mdi-identifier"/>
|
||||
</template>
|
||||
<v-list-item-title>设备名称</v-list-item-title>
|
||||
<v-list-item-subtitle>{{ deviceInfo.deviceName }}</v-list-item-subtitle>
|
||||
@ -68,7 +68,7 @@
|
||||
|
||||
<v-list-item>
|
||||
<template #prepend>
|
||||
<v-icon icon="mdi-key" />
|
||||
<v-icon icon="mdi-key"/>
|
||||
</template>
|
||||
<v-list-item-title>设备 UUID</v-list-item-title>
|
||||
<v-list-item-subtitle class="font-mono text-caption">
|
||||
@ -78,12 +78,12 @@
|
||||
</v-list>
|
||||
|
||||
<v-alert
|
||||
class="mt-4"
|
||||
type="info"
|
||||
variant="tonal"
|
||||
class="mt-4"
|
||||
>
|
||||
<template #prepend>
|
||||
<v-icon icon="mdi-information" />
|
||||
<v-icon icon="mdi-information"/>
|
||||
</template>
|
||||
您可以点击下方按钮访问云端控制台来设置密码和管理高级功能
|
||||
</v-alert>
|
||||
@ -92,12 +92,12 @@
|
||||
<!-- 错误状态 -->
|
||||
<div v-else-if="errorMessage">
|
||||
<v-alert
|
||||
class="mb-4"
|
||||
type="error"
|
||||
variant="tonal"
|
||||
class="mb-4"
|
||||
>
|
||||
<template #prepend>
|
||||
<v-icon icon="mdi-alert-circle" />
|
||||
<v-icon icon="mdi-alert-circle"/>
|
||||
</template>
|
||||
{{ errorMessage }}
|
||||
</v-alert>
|
||||
@ -105,14 +105,14 @@
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-spacer/>
|
||||
|
||||
<!-- 注册按钮 -->
|
||||
<v-btn
|
||||
v-if="!isRegistered && !isRegistering"
|
||||
:loading="isRegistering"
|
||||
color="primary"
|
||||
prepend-icon="mdi-plus"
|
||||
:loading="isRegistering"
|
||||
@click="registerDevice"
|
||||
>
|
||||
注册设备
|
||||
@ -149,8 +149,8 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { getSetting, setSetting } from '@/utils/settings'
|
||||
import {ref} from 'vue'
|
||||
import {getSetting, setSetting} from '@/utils/settings'
|
||||
import axios from '@/axios/axios'
|
||||
|
||||
// 事件定义
|
||||
@ -168,7 +168,7 @@ const registrationStep = ref('')
|
||||
|
||||
// 生成 UUID
|
||||
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 v = c === 'x' ? r : (r & 0x3 | 0x8)
|
||||
return v.toString(16)
|
||||
@ -211,7 +211,7 @@ const registerDevice = async () => {
|
||||
const serverUrl = getSetting('server.domain')
|
||||
|
||||
registrationStep.value = '正在注册设备到服务器...'
|
||||
console.log('开始注册设备:', { uuid, deviceName, serverUrl })
|
||||
console.log('开始注册设备:', {uuid, deviceName, serverUrl})
|
||||
|
||||
// 调用设备注册接口
|
||||
const response = await axios.post(`${serverUrl}/devices`, {
|
||||
|
||||
@ -5,19 +5,24 @@
|
||||
## 组件列表
|
||||
|
||||
### DeviceAuthDialog.vue
|
||||
|
||||
设备认证对话框,用于通过 namespace 和密码进行设备认证。
|
||||
|
||||
**Props:**
|
||||
|
||||
- `showCancel` (Boolean): 是否显示取消按钮,默认为 `false`
|
||||
|
||||
**Events:**
|
||||
|
||||
- `@success`: 认证成功时触发,传递认证数据
|
||||
- `@cancel`: 点击取消按钮时触发
|
||||
|
||||
**暴露的方法:**
|
||||
|
||||
- `reset()`: 清空表单和错误信息
|
||||
|
||||
**使用示例:**
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<v-dialog v-model="dialog">
|
||||
@ -37,19 +42,24 @@ import DeviceAuthDialog from '@/components/auth/DeviceAuthDialog.vue'
|
||||
---
|
||||
|
||||
### TokenInputDialog.vue
|
||||
|
||||
Token 输入对话框,用于手动输入 KV 授权 Token。
|
||||
|
||||
**Props:**
|
||||
|
||||
- `showCancel` (Boolean): 是否显示取消按钮,默认为 `false`
|
||||
|
||||
**Events:**
|
||||
|
||||
- `@success`: Token 验证成功时触发
|
||||
- `@cancel`: 点击取消按钮时触发
|
||||
|
||||
**暴露的方法:**
|
||||
|
||||
- `reset()`: 清空表单和错误信息
|
||||
|
||||
**使用示例:**
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<v-dialog v-model="dialog">
|
||||
@ -69,19 +79,24 @@ import TokenInputDialog from '@/components/auth/TokenInputDialog.vue'
|
||||
---
|
||||
|
||||
### AlternativeCodeDialog.vue
|
||||
|
||||
替代代码输入对话框(功能暂未实现)。
|
||||
|
||||
**Props:**
|
||||
|
||||
- `showCancel` (Boolean): 是否显示取消按钮,默认为 `false`
|
||||
|
||||
**Events:**
|
||||
|
||||
- `@submit`: 提交代码时触发,传递代码内容
|
||||
- `@cancel`: 点击取消按钮时触发
|
||||
|
||||
**暴露的方法:**
|
||||
|
||||
- `reset()`: 清空表单
|
||||
|
||||
**使用示例:**
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<v-dialog v-model="dialog">
|
||||
@ -101,12 +116,15 @@ import AlternativeCodeDialog from '@/components/auth/AlternativeCodeDialog.vue'
|
||||
---
|
||||
|
||||
### FirstTimeGuide.vue
|
||||
|
||||
初次使用指南,介绍 Classworks KV 的功能和使用方式。
|
||||
|
||||
**Events:**
|
||||
|
||||
- `@close`: 关闭指南时触发
|
||||
|
||||
**使用示例:**
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<v-dialog v-model="dialog">
|
||||
|
||||
@ -4,24 +4,24 @@
|
||||
<v-card-text>
|
||||
<v-text-field
|
||||
v-model="token"
|
||||
clearable
|
||||
density="comfortable"
|
||||
hide-details="auto"
|
||||
label="KV 授权 Token"
|
||||
placeholder="粘贴从授权页面获取的 Token"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
hide-details="auto"
|
||||
clearable
|
||||
/>
|
||||
<v-alert
|
||||
v-if="error"
|
||||
class="mt-3"
|
||||
type="error"
|
||||
variant="tonal"
|
||||
class="mt-3"
|
||||
>
|
||||
{{ error }}
|
||||
</v-alert>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-spacer/>
|
||||
<v-btn
|
||||
v-if="showCancel"
|
||||
variant="text"
|
||||
@ -42,8 +42,8 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { getSetting, setSetting } from '@/utils/settings'
|
||||
import {ref} from 'vue'
|
||||
import {getSetting, setSetting} from '@/utils/settings'
|
||||
import axios from '@/axios/axios'
|
||||
|
||||
defineProps({
|
||||
|
||||
@ -2,9 +2,9 @@
|
||||
<div class="warning-container">
|
||||
<v-chip
|
||||
v-if="show"
|
||||
class="warning-chip"
|
||||
color="warning"
|
||||
size="small"
|
||||
class="warning-chip"
|
||||
>
|
||||
{{ message }}
|
||||
</v-chip>
|
||||
@ -35,7 +35,13 @@ export default {
|
||||
}
|
||||
|
||||
@keyframes fade-in {
|
||||
from { opacity: 0; transform: translateY(-10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<v-container class="fill-height">
|
||||
<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-body-2 font-weight-light mb-n1">出现了错误</div>
|
||||
@ -9,7 +9,7 @@
|
||||
<h1 class="text-h2 font-weight-bold">404</h1>
|
||||
</div>
|
||||
|
||||
<div class="py-4" />
|
||||
<div class="py-4"/>
|
||||
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
@ -21,7 +21,7 @@
|
||||
variant="outlined"
|
||||
>
|
||||
<template #image>
|
||||
<v-img position="top right" />
|
||||
<v-img position="top right"/>
|
||||
</template>
|
||||
|
||||
<template #title>
|
||||
@ -33,11 +33,11 @@
|
||||
</template>
|
||||
|
||||
<v-overlay
|
||||
opacity=".12"
|
||||
scrim="primary"
|
||||
contained
|
||||
model-value
|
||||
opacity=".12"
|
||||
persistent
|
||||
scrim="primary"
|
||||
/>
|
||||
</v-card>
|
||||
</v-col>
|
||||
@ -46,18 +46,18 @@
|
||||
<v-card
|
||||
class="py-4"
|
||||
color="surface-variant"
|
||||
to="/"
|
||||
prepend-icon="mdi-home"
|
||||
rounded="lg"
|
||||
title="返回首页"
|
||||
to="/"
|
||||
variant="text"
|
||||
>
|
||||
<v-overlay
|
||||
opacity=".06"
|
||||
scrim="primary"
|
||||
contained
|
||||
model-value
|
||||
opacity=".06"
|
||||
persistent
|
||||
scrim="primary"
|
||||
/>
|
||||
</v-card>
|
||||
</v-col>
|
||||
@ -66,18 +66,18 @@
|
||||
<v-card
|
||||
class="py-4"
|
||||
color="surface-variant"
|
||||
@click="this.$router.back()"
|
||||
prepend-icon="mdi-arrow-left-drop-circle"
|
||||
rounded="lg"
|
||||
title="返回上一页"
|
||||
variant="text"
|
||||
@click="this.$router.back()"
|
||||
>
|
||||
<v-overlay
|
||||
opacity=".06"
|
||||
scrim="primary"
|
||||
contained
|
||||
model-value
|
||||
opacity=".06"
|
||||
persistent
|
||||
scrim="primary"
|
||||
/>
|
||||
</v-card>
|
||||
</v-col>
|
||||
@ -88,4 +88,4 @@
|
||||
|
||||
<script setup>
|
||||
//
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@ -1,20 +1,20 @@
|
||||
<template>
|
||||
<v-card border rounded="xl" hover>
|
||||
<v-card border hover rounded="xl">
|
||||
<v-card-item>
|
||||
<template #prepend>
|
||||
<v-icon icon="mdi-information" size="large" class="mr-2" />
|
||||
<v-icon class="mr-2" icon="mdi-information" size="large"/>
|
||||
</template>
|
||||
<v-card-title class="text-h6">关于</v-card-title>
|
||||
</v-card-item>
|
||||
|
||||
<v-card-text>
|
||||
<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">
|
||||
<v-avatar size="120" class="mb-4">
|
||||
<v-avatar class="mb-4" size="120">
|
||||
<v-img
|
||||
src="../../assets/cslogo.png"
|
||||
alt="Classworks"
|
||||
src="../../assets/cslogo.png"
|
||||
/>
|
||||
</v-avatar>
|
||||
|
||||
@ -24,35 +24,35 @@
|
||||
<div class="d-flex gap-2 flex-wrap mb-6">
|
||||
<v-btn
|
||||
color="red"
|
||||
variant="tonal"
|
||||
href="https://github.com/ClassworksDev/Classworks/issues"
|
||||
target="_blank"
|
||||
prepend-icon="mdi-bug"
|
||||
target="_blank"
|
||||
variant="tonal"
|
||||
>
|
||||
报告问题
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="primary"
|
||||
variant="tonal"
|
||||
href="https://qm.qq.com/q/qNBX4ZZVeg"
|
||||
target="_blank"
|
||||
prepend-icon="mdi-qqchat"
|
||||
target="_blank"
|
||||
variant="tonal"
|
||||
>
|
||||
QQ 群
|
||||
</v-btn>
|
||||
<v-btn
|
||||
variant="text"
|
||||
href="https://github.com/ClassworksDev/Classworks"
|
||||
target="_blank"
|
||||
prepend-icon="mdi-github"
|
||||
target="_blank"
|
||||
variant="text"
|
||||
>
|
||||
前端
|
||||
</v-btn>
|
||||
<v-btn
|
||||
variant="text"
|
||||
href="https://github.com/ClassworksDev/ClassworksServer"
|
||||
target="_blank"
|
||||
prepend-icon="mdi-github"
|
||||
target="_blank"
|
||||
variant="text"
|
||||
>
|
||||
后端
|
||||
</v-btn>
|
||||
@ -63,9 +63,9 @@
|
||||
<h3 class="text-h6 mb-2">备注与致谢</h3>
|
||||
<v-list class="mb-4 bg-transparent">
|
||||
<v-list-item
|
||||
append-icon="mdi-link"
|
||||
href="https://github.com/EnderWolf006/HomeworkBoard"
|
||||
target="_blank"
|
||||
append-icon="mdi-link"
|
||||
>
|
||||
<v-list-item-title>
|
||||
本项目受到 HomeworkBoard 的启发而开发
|
||||
@ -76,9 +76,9 @@
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
<v-list-item
|
||||
append-icon="mdi-link"
|
||||
href="https://hlyun.org"
|
||||
target="_blank"
|
||||
append-icon="mdi-link"
|
||||
>
|
||||
<v-list-item-title>
|
||||
Classworks 由<strong>厚浪云</strong>提供
|
||||
@ -88,9 +88,9 @@
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
<v-list-item
|
||||
append-icon="mdi-link"
|
||||
href="https://zerocat.houlangs.com"
|
||||
target="_blank"
|
||||
append-icon="mdi-link"
|
||||
>
|
||||
<v-list-item-title>
|
||||
感谢 ZeroCat 社区的开发者们
|
||||
@ -101,9 +101,9 @@
|
||||
</v-list-item>
|
||||
<v-divider class="ma-1"></v-divider>
|
||||
<v-list-item
|
||||
append-icon="mdi-link"
|
||||
href="https://github.com/HUSX100/IslandCaller"
|
||||
target="_blank"
|
||||
append-icon="mdi-link"
|
||||
>
|
||||
<v-list-item-title>
|
||||
本项目与 IslandCaller 没有从属关系
|
||||
@ -114,9 +114,9 @@
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
<v-list-item
|
||||
append-icon="mdi-link"
|
||||
href="https://classisland.tech"
|
||||
target="_blank"
|
||||
append-icon="mdi-link"
|
||||
>
|
||||
<v-list-item-title>
|
||||
本项目与 ClassIsland 没有从属关系
|
||||
@ -129,9 +129,9 @@
|
||||
</v-list>
|
||||
|
||||
<v-btn
|
||||
variant="text"
|
||||
class="mb-4"
|
||||
prepend-icon="mdi-package-variant"
|
||||
variant="text"
|
||||
@click="showDeps = true"
|
||||
>
|
||||
查看使用的第三方库
|
||||
@ -139,11 +139,12 @@
|
||||
|
||||
<v-dialog
|
||||
v-model="showDeps"
|
||||
transition="dialog-bottom-transition"
|
||||
fullscreen
|
||||
transition="dialog-bottom-transition"
|
||||
>
|
||||
<v-card
|
||||
><v-toolbar>
|
||||
>
|
||||
<v-toolbar>
|
||||
<v-btn icon="mdi-close" @click="showDeps = false"></v-btn>
|
||||
<v-toolbar-title>使用的第三方库</v-toolbar-title>
|
||||
<v-spacer></v-spacer>
|
||||
@ -154,8 +155,8 @@
|
||||
v-for="dep in Dependencies"
|
||||
:key="dep.name"
|
||||
:href="'https://www.npmjs.com/package/' + dep.name"
|
||||
target="_blank"
|
||||
append-icon="mdi-link"
|
||||
target="_blank"
|
||||
>
|
||||
<v-list-item-title>
|
||||
{{ dep.name }}
|
||||
@ -180,7 +181,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, onMounted } from "vue";
|
||||
import {ref, onMounted} from "vue";
|
||||
import packageJson from "../../../package.json";
|
||||
|
||||
export default {
|
||||
|
||||
@ -1,14 +1,14 @@
|
||||
<template>
|
||||
<v-card :border="border" class="setting-group">
|
||||
<v-card-title v-if="title" class="d-flex align-center">
|
||||
<v-icon v-if="icon" :icon="icon" class="mr-2" />
|
||||
<v-icon v-if="icon" :icon="icon" class="mr-2"/>
|
||||
{{ title }}
|
||||
</v-card-title>
|
||||
|
||||
|
||||
<v-card-subtitle v-if="description">
|
||||
{{ description }}
|
||||
</v-card-subtitle>
|
||||
|
||||
|
||||
<v-card-text>
|
||||
<v-list>
|
||||
<slot>
|
||||
@ -16,7 +16,7 @@
|
||||
</slot>
|
||||
</v-list>
|
||||
</v-card-text>
|
||||
|
||||
|
||||
<v-card-actions v-if="$slots.actions">
|
||||
<slot name="actions"></slot>
|
||||
</v-card-actions>
|
||||
@ -26,7 +26,7 @@
|
||||
<script>
|
||||
export default {
|
||||
name: 'SettingGroup',
|
||||
|
||||
|
||||
props: {
|
||||
/**
|
||||
* 设置组的标题
|
||||
@ -35,7 +35,7 @@ export default {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* 设置组的描述
|
||||
*/
|
||||
@ -43,7 +43,7 @@ export default {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* 设置组的图标
|
||||
*/
|
||||
@ -51,7 +51,7 @@ export default {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* 是否显示边框
|
||||
*/
|
||||
@ -60,12 +60,12 @@ export default {
|
||||
default: false
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
methods: {
|
||||
onSettingUpdate(key, value) {
|
||||
this.$emit('update', key, value);
|
||||
},
|
||||
|
||||
|
||||
onSettingError(key) {
|
||||
this.$emit('error', key);
|
||||
}
|
||||
@ -77,4 +77,4 @@ export default {
|
||||
.setting-group {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<v-list-item class="setting-item" :disabled="disabled">
|
||||
<v-list-item :disabled="disabled" class="setting-item">
|
||||
<template #prepend>
|
||||
<v-icon :icon="settingIcon" />
|
||||
<v-icon :icon="settingIcon"/>
|
||||
</template>
|
||||
|
||||
<v-list-item-title class="text-wrap">
|
||||
@ -19,57 +19,57 @@
|
||||
<v-switch
|
||||
v-if="type === 'boolean'"
|
||||
v-model="localValue"
|
||||
:disabled="disabled"
|
||||
density="comfortable"
|
||||
hide-details
|
||||
:disabled="disabled"
|
||||
@update:model-value="updateSetting"
|
||||
/>
|
||||
|
||||
<v-select
|
||||
v-else-if="type === 'string' && hasOptions"
|
||||
v-model="localValue"
|
||||
:disabled="disabled"
|
||||
:items="selectOptions"
|
||||
bg-color="surface"
|
||||
class="setting-select"
|
||||
density="compact"
|
||||
hide-details
|
||||
:disabled="disabled"
|
||||
class="setting-select"
|
||||
variant="outlined"
|
||||
bg-color="surface"
|
||||
@update:model-value="updateSetting"
|
||||
item-title="title"
|
||||
item-value="value"
|
||||
variant="outlined"
|
||||
@update:model-value="updateSetting"
|
||||
/>
|
||||
|
||||
<div v-else-if="type === 'number'" class="d-flex align-center">
|
||||
<v-btn
|
||||
:disabled="disabled || localValue <= minValue"
|
||||
icon="mdi-minus"
|
||||
size="small"
|
||||
variant="text"
|
||||
:disabled="disabled || localValue <= minValue"
|
||||
@click="adjustValue(-stepValue)"
|
||||
/>
|
||||
|
||||
<v-text-field
|
||||
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"
|
||||
hide-details
|
||||
:min="minValue"
|
||||
:max="maxValue"
|
||||
:step="stepValue"
|
||||
:disabled="disabled"
|
||||
class="mx-2 setting-number-field"
|
||||
style="width: 80px"
|
||||
type="number"
|
||||
variant="outlined"
|
||||
bg-color="surface"
|
||||
@update:model-value="updateSetting"
|
||||
/>
|
||||
|
||||
<v-btn
|
||||
:disabled="disabled || localValue >= maxValue"
|
||||
icon="mdi-plus"
|
||||
size="small"
|
||||
variant="text"
|
||||
:disabled="disabled || localValue >= maxValue"
|
||||
@click="adjustValue(stepValue)"
|
||||
/>
|
||||
</div>
|
||||
@ -78,34 +78,34 @@
|
||||
<v-menu location="bottom">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn
|
||||
:disabled="disabled"
|
||||
class="ml-2"
|
||||
icon="mdi-dots-vertical"
|
||||
size="small"
|
||||
variant="text"
|
||||
v-bind="props"
|
||||
class="ml-2"
|
||||
:disabled="disabled"
|
||||
variant="text"
|
||||
/>
|
||||
</template>
|
||||
<v-list density="compact">
|
||||
<v-list-item @click="copySettingId">
|
||||
<template v-slot:prepend>
|
||||
<v-icon icon="mdi-key" size="small" />
|
||||
<v-icon icon="mdi-key" size="small"/>
|
||||
</template>
|
||||
<v-list-item-title>复制设置ID</v-list-item-title>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item @click="copySettingValue">
|
||||
<template v-slot:prepend>
|
||||
<v-icon icon="mdi-content-copy" size="small" />
|
||||
<v-icon icon="mdi-content-copy" size="small"/>
|
||||
</template>
|
||||
<v-list-item-title>复制设置值</v-list-item-title>
|
||||
</v-list-item>
|
||||
|
||||
<v-divider></v-divider>
|
||||
|
||||
<v-list-item @click="resetToDefault" :disabled="isDefaultValue">
|
||||
<v-list-item :disabled="isDefaultValue" @click="resetToDefault">
|
||||
<template v-slot:prepend>
|
||||
<v-icon icon="mdi-restore" size="small" />
|
||||
<v-icon icon="mdi-restore" size="small"/>
|
||||
</template>
|
||||
<v-list-item-title>重置为默认值</v-list-item-title>
|
||||
</v-list-item>
|
||||
@ -119,12 +119,12 @@
|
||||
<div v-if="type === 'string' && !hasOptions" class="px-4 pb-2 pt-0">
|
||||
<v-text-field
|
||||
v-model="localValue"
|
||||
:disabled="disabled"
|
||||
bg-color="surface"
|
||||
class="setting-text-field mt-1"
|
||||
density="compact"
|
||||
hide-details
|
||||
:disabled="disabled"
|
||||
class="setting-text-field mt-1"
|
||||
variant="outlined"
|
||||
bg-color="surface"
|
||||
@update:model-value="updateSetting"
|
||||
/>
|
||||
</div>
|
||||
@ -206,20 +206,20 @@ export default {
|
||||
showSnackbar: false,
|
||||
snackbarText: "",
|
||||
fontFamilies: [
|
||||
{ title: "Arial", value: "Arial, sans-serif" },
|
||||
{ title: "Calibri", value: "Calibri, sans-serif" },
|
||||
{ title: "Cambria", value: "Cambria, serif" },
|
||||
{ title: "Consolas", value: "Consolas, monospace" },
|
||||
{ title: "Courier New", value: "Courier New, monospace" },
|
||||
{ title: "Georgia", value: "Georgia, serif" },
|
||||
{ title: "Helvetica", value: "Helvetica, sans-serif" },
|
||||
{ title: "Segoe UI", value: "Segoe UI, sans-serif" },
|
||||
{ title: "Times New Roman", value: "Times New Roman, serif" },
|
||||
{ title: "Trebuchet MS", value: "Trebuchet MS, sans-serif" },
|
||||
{ title: "Verdana", value: "Verdana, sans-serif" },
|
||||
{ title: "Monospace", value: "monospace" },
|
||||
{ title: "Sans-serif", value: "sans-serif" },
|
||||
{ title: "Serif", value: "serif" },
|
||||
{title: "Arial", value: "Arial, sans-serif"},
|
||||
{title: "Calibri", value: "Calibri, sans-serif"},
|
||||
{title: "Cambria", value: "Cambria, serif"},
|
||||
{title: "Consolas", value: "Consolas, monospace"},
|
||||
{title: "Courier New", value: "Courier New, monospace"},
|
||||
{title: "Georgia", value: "Georgia, serif"},
|
||||
{title: "Helvetica", value: "Helvetica, sans-serif"},
|
||||
{title: "Segoe UI", value: "Segoe UI, sans-serif"},
|
||||
{title: "Times New Roman", value: "Times New Roman, serif"},
|
||||
{title: "Trebuchet MS", value: "Trebuchet MS, sans-serif"},
|
||||
{title: "Verdana", value: "Verdana, sans-serif"},
|
||||
{title: "Monospace", value: "monospace"},
|
||||
{title: "Sans-serif", value: "sans-serif"},
|
||||
{title: "Serif", value: "serif"},
|
||||
],
|
||||
// 设置项的显示名称映射
|
||||
displayValueMappings: {
|
||||
|
||||
@ -1,27 +1,28 @@
|
||||
<template>
|
||||
<div class="settings-explorer">
|
||||
|
||||
|
||||
<div >
|
||||
<v-text-field v-model="searchQuery" label="搜索设置" prepend-inner-icon="mdi-magnify" clearable variant="outlined"
|
||||
density="comfortable" class="mb-4" />
|
||||
|
||||
|
||||
<div>
|
||||
<v-text-field v-model="searchQuery" class="mb-4" clearable density="comfortable" label="搜索设置"
|
||||
prepend-inner-icon="mdi-magnify" variant="outlined"/>
|
||||
|
||||
|
||||
<v-list>
|
||||
<div v-for="setting in allSettings" :key="setting.key">
|
||||
<setting-item :key="setting.key" :setting-key="setting.key"
|
||||
:disabled="setting.requireDeveloper && !isDeveloperMode" @update="onSettingUpdate" @error="onSettingError" />
|
||||
<v-divider class="my-2" />
|
||||
<setting-item :key="setting.key" :disabled="setting.requireDeveloper && !isDeveloperMode"
|
||||
:setting-key="setting.key" @error="onSettingError"
|
||||
@update="onSettingUpdate"/>
|
||||
<v-divider class="my-2"/>
|
||||
</div>
|
||||
</v-list><v-card border>
|
||||
</v-list>
|
||||
<v-card border>
|
||||
<v-card-title class="text-subtitle-1">当前配置</v-card-title>
|
||||
<v-card-text>
|
||||
<pre class="settings-json">{{ formattedSettings }}</pre>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn @click="copySettingsToClipboard">
|
||||
<v-btn @click="copySettingsToClipboard">
|
||||
复制到剪贴板
|
||||
<v-icon right>mdi-content-copy</v-icon>
|
||||
</v-btn>
|
||||
@ -32,7 +33,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getSetting, settingsDefinitions, exportSettingsAsKeyValue, watchSettings } from '@/utils/settings';
|
||||
import {getSetting, settingsDefinitions, exportSettingsAsKeyValue, watchSettings} from '@/utils/settings';
|
||||
import SettingItem from './SettingItem.vue';
|
||||
|
||||
export default {
|
||||
@ -82,7 +83,7 @@ export default {
|
||||
created() {
|
||||
// 初始化当前设置
|
||||
this.updateCurrentSettings();
|
||||
|
||||
|
||||
// 监听设置变化
|
||||
this.unwatchFunction = watchSettings(() => {
|
||||
this.updateCurrentSettings();
|
||||
@ -115,11 +116,11 @@ export default {
|
||||
navigator.clipboard.writeText(JSON.stringify(this.currentSettings))
|
||||
.then(() => {
|
||||
// 可以添加一个提示,表示复制成功
|
||||
this.$emit('message', { type: 'success', text: '设置已复制到剪贴板' });
|
||||
this.$emit('message', {type: 'success', text: '设置已复制到剪贴板'});
|
||||
})
|
||||
.catch(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 {
|
||||
background-color: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@ -1,29 +1,29 @@
|
||||
<template>
|
||||
<v-card
|
||||
border
|
||||
:color="unsavedChanges ? 'warning-subtle' : undefined"
|
||||
:class="{ 'unsaved-changes': unsavedChanges }"
|
||||
:color="unsavedChanges ? 'warning-subtle' : undefined"
|
||||
border
|
||||
>
|
||||
<v-card-item>
|
||||
<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>
|
||||
<v-card-title class="text-h6">学生列表</v-card-title>
|
||||
<template #append>
|
||||
<unsaved-warning :show="unsavedChanges" message="有未保存的更改" />
|
||||
<unsaved-warning :show="unsavedChanges" message="有未保存的更改"/>
|
||||
<v-btn
|
||||
:disabled="modelValue.list.length === 0"
|
||||
class="mr-2"
|
||||
prepend-icon="mdi-sort-alphabetical-variant"
|
||||
variant="text"
|
||||
class="mr-2"
|
||||
@click="sortStudentsByPinyin"
|
||||
:disabled="modelValue.list.length === 0"
|
||||
>
|
||||
按姓名首字母排序
|
||||
</v-btn>
|
||||
<v-btn
|
||||
:color="modelValue.advanced ? 'primary' : undefined"
|
||||
variant="text"
|
||||
prepend-icon="mdi-code-braces"
|
||||
variant="text"
|
||||
@click="toggleAdvanced"
|
||||
>
|
||||
{{ modelValue.advanced ? "返回基础编辑" : "高级编辑" }}
|
||||
@ -34,12 +34,12 @@
|
||||
<v-card-text>
|
||||
<v-progress-linear
|
||||
v-if="loading"
|
||||
indeterminate
|
||||
color="primary"
|
||||
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 }}
|
||||
</v-alert>
|
||||
|
||||
@ -47,23 +47,23 @@
|
||||
<!-- 普通编辑模式 -->
|
||||
<div v-if="!modelValue.advanced">
|
||||
<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-model="newStudentName"
|
||||
class="mb-4"
|
||||
hide-details
|
||||
label="添加学生"
|
||||
placeholder="输入学生姓名后回车添加"
|
||||
prepend-inner-icon="mdi-account-plus"
|
||||
variant="outlined"
|
||||
hide-details
|
||||
class="mb-4"
|
||||
@keyup.enter="addStudent"
|
||||
>
|
||||
<template #append>
|
||||
<v-btn
|
||||
:disabled="!newStudentName.trim()"
|
||||
color="primary"
|
||||
icon="mdi-plus"
|
||||
variant="text"
|
||||
color="primary"
|
||||
:disabled="!newStudentName.trim()"
|
||||
@click="addStudent"
|
||||
/>
|
||||
</template>
|
||||
@ -76,25 +76,25 @@
|
||||
v-for="(student, index) in modelValue.list"
|
||||
:key="index"
|
||||
cols="12"
|
||||
sm="6"
|
||||
md="4"
|
||||
lg="3"
|
||||
md="4"
|
||||
sm="6"
|
||||
>
|
||||
<v-hover v-slot="{ isHovering, props }">
|
||||
<v-card
|
||||
v-bind="props"
|
||||
:elevation="isMobile ? 1 : isHovering ? 4 : 1"
|
||||
class="student-card"
|
||||
border
|
||||
class="student-card"
|
||||
v-bind="props"
|
||||
>
|
||||
<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 }">
|
||||
<v-btn
|
||||
variant="tonal"
|
||||
size="small"
|
||||
class="mr-3 font-weight-medium"
|
||||
size="small"
|
||||
v-bind="menuProps"
|
||||
variant="tonal"
|
||||
>
|
||||
{{ index + 1 }}
|
||||
</v-btn>
|
||||
@ -102,23 +102,23 @@
|
||||
|
||||
<v-list density="compact" nav>
|
||||
<v-list-item
|
||||
prepend-icon="mdi-arrow-up-bold"
|
||||
:disabled="index === 0"
|
||||
prepend-icon="mdi-arrow-up-bold"
|
||||
@click="moveStudent(index, 'top')"
|
||||
>
|
||||
置顶
|
||||
</v-list-item>
|
||||
<v-divider />
|
||||
<v-divider/>
|
||||
<v-list-item
|
||||
prepend-icon="mdi-arrow-up"
|
||||
:disabled="index === 0"
|
||||
prepend-icon="mdi-arrow-up"
|
||||
@click="moveStudent(index, 'up')"
|
||||
>
|
||||
上移
|
||||
</v-list-item>
|
||||
<v-list-item
|
||||
prepend-icon="mdi-arrow-down"
|
||||
:disabled="index === modelValue.list.length - 1"
|
||||
prepend-icon="mdi-arrow-down"
|
||||
@click="moveStudent(index, 'down')"
|
||||
>
|
||||
下移
|
||||
@ -129,13 +129,13 @@
|
||||
<v-text-field
|
||||
v-if="editState.index === index"
|
||||
v-model="editState.name"
|
||||
density="compact"
|
||||
variant="underlined"
|
||||
hide-details
|
||||
class="flex-grow-1"
|
||||
autofocus
|
||||
@keyup.enter="saveEdit"
|
||||
class="flex-grow-1"
|
||||
density="compact"
|
||||
hide-details
|
||||
variant="underlined"
|
||||
@blur="saveEdit"
|
||||
@keyup.enter="saveEdit"
|
||||
/>
|
||||
<span
|
||||
v-else
|
||||
@ -146,21 +146,21 @@
|
||||
</span>
|
||||
|
||||
<div
|
||||
class="d-flex gap-1 action-buttons"
|
||||
:class="{ 'opacity-100': isHovering || isMobile }"
|
||||
class="d-flex gap-1 action-buttons"
|
||||
>
|
||||
<v-btn
|
||||
icon="mdi-pencil"
|
||||
variant="text"
|
||||
color="primary"
|
||||
icon="mdi-pencil"
|
||||
size="small"
|
||||
variant="text"
|
||||
@click="startEdit(index, student)"
|
||||
/>
|
||||
<v-btn
|
||||
icon="mdi-delete"
|
||||
variant="text"
|
||||
color="error"
|
||||
icon="mdi-delete"
|
||||
size="small"
|
||||
variant="text"
|
||||
@click="removeStudent(index)"
|
||||
/>
|
||||
</div>
|
||||
@ -175,36 +175,36 @@
|
||||
<div v-else class="pt-2">
|
||||
<v-textarea
|
||||
v-model="modelValue.text"
|
||||
label="批量编辑学生列表"
|
||||
placeholder="每行输入一个学生姓名"
|
||||
hint="使用文本编辑模式批量编辑学生名单,保存时会自动去除空行"
|
||||
label="批量编辑学生列表"
|
||||
persistent-hint
|
||||
variant="outlined"
|
||||
placeholder="每行输入一个学生姓名"
|
||||
rows="10"
|
||||
variant="outlined"
|
||||
@update:model-value="handleTextInput"
|
||||
/>
|
||||
</div>
|
||||
</v-expand-transition>
|
||||
|
||||
<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
|
||||
:disabled="loading"
|
||||
:loading="loading"
|
||||
color="primary"
|
||||
prepend-icon="mdi-content-save"
|
||||
size="large"
|
||||
:loading="loading"
|
||||
:disabled="loading"
|
||||
@click="saveStudents"
|
||||
>
|
||||
保存名单
|
||||
</v-btn>
|
||||
<v-btn
|
||||
:disabled="loading"
|
||||
:loading="loading"
|
||||
color="error"
|
||||
variant="outlined"
|
||||
prepend-icon="mdi-refresh"
|
||||
size="large"
|
||||
:loading="loading"
|
||||
:disabled="loading"
|
||||
variant="outlined"
|
||||
@click="loadStudents"
|
||||
>
|
||||
重载名单
|
||||
@ -218,9 +218,9 @@
|
||||
<script>
|
||||
import UnsavedWarning from "../common/UnsavedWarning.vue";
|
||||
import "@/styles/warnings.scss";
|
||||
import { pinyin } from "pinyin-pro";
|
||||
import {pinyin} from "pinyin-pro";
|
||||
import dataProvider from "@/utils/dataProvider";
|
||||
import { getSetting } from "@/utils/settings";
|
||||
import {getSetting} from "@/utils/settings";
|
||||
|
||||
export default {
|
||||
name: "StudentListCard",
|
||||
@ -289,7 +289,7 @@ export default {
|
||||
if (response.success != false && Array.isArray(response)) {
|
||||
this.modelValue.list = response.map((item, index) => {
|
||||
if (typeof item === 'string') {
|
||||
return { id: index + 1, name: item };
|
||||
return {id: index + 1, name: item};
|
||||
}
|
||||
return {
|
||||
id: item.id || index + 1,
|
||||
@ -301,7 +301,7 @@ export default {
|
||||
this.modelValue.text = this.modelValue.list.map(s => s.name).join("\n");
|
||||
this.lastSavedData = JSON.parse(JSON.stringify(this.modelValue.list));
|
||||
this.unsavedChanges = false;
|
||||
return;
|
||||
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
@ -371,9 +371,9 @@ export default {
|
||||
const newList = lines.map(name => {
|
||||
name = name.trim();
|
||||
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
|
||||
@ -384,7 +384,7 @@ export default {
|
||||
const name = this.newStudentName.trim();
|
||||
if (name && !this.modelValue.list.some(s => s.name === name)) {
|
||||
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 = "";
|
||||
}
|
||||
},
|
||||
@ -442,8 +442,8 @@ export default {
|
||||
|
||||
sortStudentsByPinyin() {
|
||||
const sorted = [...this.modelValue.list].sort((a, b) => {
|
||||
const pinyinA = pinyin(a.name, { toneType: "none" });
|
||||
const pinyinB = pinyin(b.name, { toneType: "none" });
|
||||
const pinyinA = pinyin(a.name, {toneType: "none"});
|
||||
const pinyinB = pinyin(b.name, {toneType: "none"});
|
||||
return pinyinA.localeCompare(pinyinB);
|
||||
});
|
||||
sorted.forEach((s, i) => s.id = i + 1);
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<v-card class="my-4" :loading="loading" :disabled="!hasNamespaceInfo">
|
||||
<v-card :disabled="!hasNamespaceInfo" :loading="loading" class="my-4">
|
||||
<template #loader>
|
||||
<v-progress-linear v-if="loading" indeterminate color="primary" />
|
||||
<v-progress-linear v-if="loading" color="primary" indeterminate/>
|
||||
</template>
|
||||
|
||||
|
||||
@ -21,18 +21,18 @@
|
||||
class="mb-4"
|
||||
>
|
||||
<v-alert
|
||||
border
|
||||
type="warning"
|
||||
variant="tonal"
|
||||
border
|
||||
>
|
||||
<v-alert-title>设备未绑定账号</v-alert-title>
|
||||
<div>当前设备尚未绑定账号,部分功能可能受限。请前往绑定账号以获得完整体验。</div>
|
||||
<v-btn
|
||||
class="mt-3"
|
||||
variant="outlined"
|
||||
:href="getBindAccountUrl()"
|
||||
append-icon="mdi-open-in-new"
|
||||
class="mt-3"
|
||||
target="_blank"
|
||||
variant="outlined"
|
||||
>
|
||||
前往绑定账号
|
||||
</v-btn>
|
||||
@ -45,13 +45,13 @@
|
||||
class="d-flex align-center mb-4"
|
||||
>
|
||||
<v-card
|
||||
border
|
||||
hover
|
||||
class="w-100"
|
||||
variant="tonal"
|
||||
:prepend-avatar="namespaceInfo.account.avatarUrl"
|
||||
:title="namespaceInfo.account.name || '未命名用户'"
|
||||
:subtitle="'此设备由贵校管理 管理员账号 ID: ' + namespaceInfo.account.id"
|
||||
:title="namespaceInfo.account.name || '未命名用户'"
|
||||
border
|
||||
class="w-100"
|
||||
hover
|
||||
variant="tonal"
|
||||
>
|
||||
<v-card-text>
|
||||
此设备由贵校或贵单位管理,该管理员系此空间所有者,如有疑问请咨询他,对于恶意绑定、滥用行为请反馈。
|
||||
@ -62,10 +62,10 @@
|
||||
<!-- 设备信息卡片 -->
|
||||
<v-card
|
||||
v-if="namespaceInfo.device"
|
||||
variant="tonal"
|
||||
class="mb-4"
|
||||
border
|
||||
class="mb-4"
|
||||
hover
|
||||
variant="tonal"
|
||||
>
|
||||
<v-card-title class="pb-1">
|
||||
设备信息
|
||||
@ -74,8 +74,8 @@
|
||||
<div class="d-flex flex-column gap-1">
|
||||
<div class="d-flex align-center">
|
||||
<v-icon
|
||||
size="small"
|
||||
class="me-2"
|
||||
size="small"
|
||||
>
|
||||
mdi-tag
|
||||
</v-icon>
|
||||
@ -84,8 +84,8 @@
|
||||
</div>
|
||||
<div class="d-flex align-center">
|
||||
<v-icon
|
||||
size="small"
|
||||
class="me-2"
|
||||
size="small"
|
||||
>
|
||||
mdi-identifier
|
||||
</v-icon>
|
||||
@ -98,8 +98,8 @@
|
||||
class="d-flex align-center"
|
||||
>
|
||||
<v-icon
|
||||
size="small"
|
||||
class="me-2"
|
||||
size="small"
|
||||
>
|
||||
mdi-uuid
|
||||
</v-icon>
|
||||
@ -108,8 +108,8 @@
|
||||
</div>
|
||||
<div class="d-flex align-center">
|
||||
<v-icon
|
||||
size="small"
|
||||
class="me-2"
|
||||
size="small"
|
||||
>
|
||||
mdi-calendar
|
||||
</v-icon>
|
||||
@ -121,8 +121,8 @@
|
||||
class="d-flex align-center"
|
||||
>
|
||||
<v-icon
|
||||
size="small"
|
||||
class="me-2"
|
||||
size="small"
|
||||
>
|
||||
mdi-calendar-clock
|
||||
</v-icon>
|
||||
@ -134,13 +134,14 @@
|
||||
</v-card>
|
||||
|
||||
<v-card
|
||||
title="Classworks KV"
|
||||
subtitle="文档形键值数据库"
|
||||
border
|
||||
hover
|
||||
subtitle="文档形键值数据库"
|
||||
title="Classworks KV"
|
||||
>
|
||||
<v-card-text>
|
||||
Classworks KV 是厚浪云推出的文档形键值数据库,其是一个开放的云应用平台,为各种应用提供存储服务。此设备正在使用其服务,如果您希望管理设备信息,请前往 Classworks KV 的网站,如果您在服务推出前就在使用 Classworks,您的数据已被自动迁移。
|
||||
Classworks KV 是厚浪云推出的文档形键值数据库,其是一个开放的云应用平台,为各种应用提供存储服务。此设备正在使用其服务,如果您希望管理设备信息,请前往
|
||||
Classworks KV 的网站,如果您在服务推出前就在使用 Classworks,您的数据已被自动迁移。
|
||||
<br><br>
|
||||
Classworks KV 的全域管理员是
|
||||
<a
|
||||
@ -153,8 +154,8 @@
|
||||
<v-card-actions>
|
||||
<v-btn
|
||||
:href="defaultAuthServer"
|
||||
class="text-none"
|
||||
append-icon="mdi-open-in-new"
|
||||
class="text-none"
|
||||
target="_blank"
|
||||
>
|
||||
前往 Classworks KV
|
||||
@ -174,11 +175,11 @@
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-spacer/>
|
||||
<v-btn
|
||||
:loading="loading"
|
||||
color="primary"
|
||||
variant="outlined"
|
||||
:loading="loading"
|
||||
@click="reloadInfo"
|
||||
>
|
||||
刷新设备信息
|
||||
@ -202,9 +203,9 @@
|
||||
<v-card-title>确认重新初始化</v-card-title>
|
||||
<v-card-text>
|
||||
<v-alert
|
||||
class="mb-3"
|
||||
type="warning"
|
||||
variant="tonal"
|
||||
class="mb-3"
|
||||
>
|
||||
<v-alert-title>警告</v-alert-title>
|
||||
此操作将清除当前的云端存储配置(包括 Token),您需要重新进行授权。
|
||||
@ -212,7 +213,7 @@
|
||||
<p>您确定要重新初始化云端存储吗?</p>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-spacer/>
|
||||
<v-btn
|
||||
variant="text"
|
||||
@click="showReinitDialog = false"
|
||||
@ -232,8 +233,8 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { kvServerProvider } from "@/utils/providers/kvServerProvider";
|
||||
import { setSetting, getSetting } from "@/utils/settings";
|
||||
import {kvServerProvider} from "@/utils/providers/kvServerProvider";
|
||||
import {setSetting, getSetting} from "@/utils/settings";
|
||||
|
||||
export default {
|
||||
name: "CloudNamespaceInfoCard",
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<settings-card title="数据源设置" icon="mdi-database-cog">
|
||||
<settings-card icon="mdi-database-cog" title="数据源设置">
|
||||
<v-list>
|
||||
<!-- 服务器模式设置 -->
|
||||
<template
|
||||
@ -10,7 +10,7 @@
|
||||
>
|
||||
<v-list-item>
|
||||
<template #prepend>
|
||||
<v-icon icon="mdi-lan-connect" class="mr-3" />
|
||||
<v-icon class="mr-3" icon="mdi-lan-connect"/>
|
||||
</template>
|
||||
<v-list-item-title>检查服务器连接</v-list-item-title>
|
||||
<template #append>
|
||||
@ -21,7 +21,8 @@
|
||||
>
|
||||
测试连接
|
||||
</v-btn>
|
||||
</template> </v-list-item
|
||||
</template>
|
||||
</v-list-item
|
||||
><!-- 数据迁移,仅对KV本地存储有效 -->
|
||||
</template>
|
||||
|
||||
@ -29,11 +30,12 @@
|
||||
<template v-if="currentProvider === 'kv-local'">
|
||||
<v-list-item>
|
||||
<template #prepend>
|
||||
<v-icon icon="mdi-database" class="mr-3" />
|
||||
<v-icon class="mr-3" icon="mdi-database"/>
|
||||
</template>
|
||||
<v-list-item-title>清除数据库缓存</v-list-item-title>
|
||||
<v-list-item-subtitle
|
||||
>这将清除所有本地数据库中的数据</v-list-item-subtitle
|
||||
>这将清除所有本地数据库中的数据
|
||||
</v-list-item-subtitle
|
||||
>
|
||||
<template #append>
|
||||
<v-btn color="error" variant="tonal" @click="confirmClearIndexedDB">
|
||||
@ -43,45 +45,48 @@
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<template #prepend>
|
||||
<v-icon icon="mdi-database-export" class="mr-3" />
|
||||
<v-icon class="mr-3" icon="mdi-database-export"/>
|
||||
</template>
|
||||
<v-list-item-title>导出数据库</v-list-item-title>
|
||||
<template #append>
|
||||
<v-btn variant="tonal" @click="exportData"> 导出 </v-btn>
|
||||
<v-btn variant="tonal" @click="exportData"> 导出</v-btn>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</template>
|
||||
<v-list-item>
|
||||
<template #prepend>
|
||||
<v-icon icon="mdi-database-import" class="mr-3" />
|
||||
<v-icon class="mr-3" icon="mdi-database-import"/>
|
||||
</template>
|
||||
<v-list-item-title>迁移旧数据</v-list-item-title>
|
||||
<v-list-item-subtitle
|
||||
>将旧的存储格式数据转移到新的KV存储</v-list-item-subtitle
|
||||
>将旧的存储格式数据转移到新的KV存储
|
||||
</v-list-item-subtitle
|
||||
>
|
||||
<template #append>
|
||||
<v-btn :loading="migrateLoading" variant="tonal" @click="migrateData">
|
||||
迁移
|
||||
</v-btn>
|
||||
</template> </v-list-item
|
||||
</template>
|
||||
</v-list-item
|
||||
><!-- 显示机器ID -->
|
||||
<v-list-item>
|
||||
<template #prepend>
|
||||
<v-icon icon="mdi-identifier" class="mr-3" />
|
||||
<v-icon class="mr-3" icon="mdi-identifier"/>
|
||||
</template>
|
||||
<v-list-item-title>本机唯一标识符</v-list-item-title>
|
||||
<v-list-item-subtitle v-if="machineId">{{
|
||||
machineId
|
||||
}}</v-list-item-subtitle>
|
||||
}}
|
||||
</v-list-item-subtitle>
|
||||
<v-list-item-subtitle v-else>正在加载...</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<template #prepend>
|
||||
<v-icon icon="mdi-lan-connect" class="mr-3" />
|
||||
<v-icon class="mr-3" icon="mdi-lan-connect"/>
|
||||
</template>
|
||||
<v-list-item-title>查看本地缓存</v-list-item-title>
|
||||
<template #append>
|
||||
<v-btn variant="tonal" to="/cachemanagement"> 查看 </v-btn>
|
||||
<v-btn to="/cachemanagement" variant="tonal"> 查看</v-btn>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
@ -94,10 +99,12 @@
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="grey" variant="text" @click="confirmDialog = false"
|
||||
>取消</v-btn
|
||||
>取消
|
||||
</v-btn
|
||||
>
|
||||
<v-btn color="error" variant="tonal" @click="handleConfirm"
|
||||
>确认</v-btn
|
||||
>确认
|
||||
</v-btn
|
||||
>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
@ -107,12 +114,12 @@
|
||||
|
||||
<script>
|
||||
import SettingsCard from "@/components/SettingsCard.vue";
|
||||
import { getSetting } from "@/utils/settings";
|
||||
import {getSetting} from "@/utils/settings";
|
||||
import axios from "axios";
|
||||
|
||||
export default {
|
||||
name: "DataProviderSettingsCard",
|
||||
components: { SettingsCard },
|
||||
components: {SettingsCard},
|
||||
|
||||
data() {
|
||||
return {
|
||||
@ -156,7 +163,7 @@ export default {
|
||||
const siteKey = getSetting("server.siteKey");
|
||||
|
||||
// Prepare headers including site key if available
|
||||
const headers = { Accept: "application/json" };
|
||||
const headers = {Accept: "application/json"};
|
||||
if (siteKey) {
|
||||
headers["x-site-key"] = siteKey;
|
||||
}
|
||||
@ -227,7 +234,7 @@ export default {
|
||||
async exportData() {
|
||||
try {
|
||||
const DBName = "ClassworksDB";
|
||||
const data = { indexedDB: {} };
|
||||
const data = {indexedDB: {}};
|
||||
|
||||
// 打开数据库
|
||||
const db = await new Promise((resolve, reject) => {
|
||||
|
||||
@ -1,50 +1,49 @@
|
||||
<template>
|
||||
<settings-card title="显示设置" icon="mdi-monitor" border>
|
||||
<v-list>
|
||||
<setting-item :setting-key="'display.emptySubjectDisplay'" />
|
||||
|
||||
<settings-card border icon="mdi-monitor" title="显示设置">
|
||||
<v-list>
|
||||
<setting-item :setting-key="'display.emptySubjectDisplay'"/>
|
||||
|
||||
<v-divider class="my-2" />
|
||||
<setting-item :setting-key="'display.dynamicSort'" />
|
||||
|
||||
<v-divider class="my-2" />
|
||||
<setting-item :setting-key="'display.showRandomButton'" />
|
||||
<v-divider class="my-2"/>
|
||||
<setting-item :setting-key="'display.dynamicSort'"/>
|
||||
|
||||
<v-divider class="my-2" />
|
||||
<setting-item :setting-key="'display.showFullscreenButton'" />
|
||||
<v-divider class="my-2"/>
|
||||
<setting-item :setting-key="'display.showRandomButton'"/>
|
||||
|
||||
<v-divider class="my-2" />
|
||||
<setting-item :setting-key="'display.cardHoverEffect'" />
|
||||
<v-divider class="my-2"/>
|
||||
<setting-item :setting-key="'display.showFullscreenButton'"/>
|
||||
|
||||
<v-divider class="my-2" />
|
||||
<setting-item :setting-key="'display.enhancedTouchMode'" />
|
||||
<v-divider class="my-2"/>
|
||||
<setting-item :setting-key="'display.cardHoverEffect'"/>
|
||||
|
||||
<v-divider class="my-2" />
|
||||
<setting-item :setting-key="'display.showQuickTools'" />
|
||||
<v-divider class="my-2"/>
|
||||
<setting-item :setting-key="'display.enhancedTouchMode'"/>
|
||||
|
||||
<v-divider class="my-2" />
|
||||
<setting-item :setting-key="'display.showAntiScreenBurnCard'" />
|
||||
<v-divider class="my-2"/>
|
||||
<setting-item :setting-key="'display.showQuickTools'"/>
|
||||
|
||||
<v-divider class="my-2" />
|
||||
<setting-item :setting-key="'display.showExamScheduleButton'" />
|
||||
<v-divider class="my-2"/>
|
||||
<setting-item :setting-key="'display.showAntiScreenBurnCard'"/>
|
||||
|
||||
</v-list>
|
||||
<v-divider class="my-2"/>
|
||||
<setting-item :setting-key="'display.showExamScheduleButton'"/>
|
||||
|
||||
</v-list>
|
||||
</settings-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import SettingsCard from '@/components/SettingsCard.vue';
|
||||
import SettingItem from '@/components/settings/SettingItem.vue';
|
||||
|
||||
export default {
|
||||
name: 'DisplaySettingsCard',
|
||||
components: { SettingsCard, SettingItem },
|
||||
components: {SettingsCard, SettingItem},
|
||||
data() {
|
||||
|
||||
return {
|
||||
|
||||
};
|
||||
|
||||
return {};
|
||||
},
|
||||
|
||||
|
||||
|
||||
|
||||
};
|
||||
</script>
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<settings-card
|
||||
border
|
||||
title="回声洞"
|
||||
icon="mdi-thought-bubble"
|
||||
title="回声洞"
|
||||
@click="handleClick"
|
||||
>
|
||||
<v-card-text>
|
||||
@ -12,7 +12,7 @@
|
||||
<transition name="fade">
|
||||
<v-chip v-if="currentQuote?.contributor" class="contributor">
|
||||
<v-avatar start>
|
||||
<v-img :src="`https://github.com/${currentQuote.contributor}.png`" />
|
||||
<v-img :src="`https://github.com/${currentQuote.contributor}.png`"/>
|
||||
</v-avatar>
|
||||
{{ currentQuote.contributor }}
|
||||
</v-chip>
|
||||
@ -31,13 +31,13 @@ const INITIAL_STATE = {
|
||||
};
|
||||
|
||||
const TYPEWRITER_CONFIG = {
|
||||
main: { delay: 50, deleteSpeed: 100 },
|
||||
source: { delay: 10, deleteSpeed: 10, cursor: "" }
|
||||
main: {delay: 50, deleteSpeed: 100},
|
||||
source: {delay: 10, deleteSpeed: 10, cursor: ""}
|
||||
};
|
||||
|
||||
export default {
|
||||
name: "EchoChamberCard",
|
||||
components: { SettingsCard },
|
||||
components: {SettingsCard},
|
||||
data: () => ({
|
||||
typewriter: null,
|
||||
sourceWriter: null,
|
||||
@ -79,7 +79,7 @@ export default {
|
||||
async copyToClipboard() {
|
||||
if (!this.currentQuote) return;
|
||||
|
||||
const { text, author, contributor, link } = this.currentQuote;
|
||||
const {text, author, contributor, link} = this.currentQuote;
|
||||
const parts = [
|
||||
text,
|
||||
author && `作者:${author}`,
|
||||
@ -107,6 +107,11 @@ export default {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.fade-enter-active, .fade-leave-active { transition: opacity 0.3s ease; }
|
||||
.fade-enter-from, .fade-leave-to { opacity: 0; }
|
||||
.fade-enter-active, .fade-leave-active {
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.fade-enter-from, .fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -1,20 +1,19 @@
|
||||
<template>
|
||||
<settings-card title="编辑设置" icon="mdi-cog">
|
||||
<settings-card icon="mdi-cog" title="编辑设置">
|
||||
<v-list>
|
||||
<setting-item :setting-key="'edit.autoSave'" />
|
||||
<setting-item :setting-key="'edit.autoSave'"/>
|
||||
|
||||
|
||||
<v-divider class="my-2" />
|
||||
<setting-item :setting-key="'edit.blockNonTodayAutoSave'" />
|
||||
<v-divider class="my-2"/>
|
||||
<setting-item :setting-key="'edit.blockNonTodayAutoSave'"/>
|
||||
|
||||
|
||||
<v-divider class="my-2" />
|
||||
<setting-item :setting-key="'edit.confirmNonTodaySave'" />
|
||||
<v-divider class="my-2"/>
|
||||
<setting-item :setting-key="'edit.confirmNonTodaySave'"/>
|
||||
|
||||
|
||||
|
||||
<v-divider class="my-2" />
|
||||
<setting-item :setting-key="'edit.refreshBeforeEdit'" />
|
||||
<v-divider class="my-2"/>
|
||||
<setting-item :setting-key="'edit.refreshBeforeEdit'"/>
|
||||
|
||||
</v-list>
|
||||
</settings-card>
|
||||
|
||||
@ -1,17 +1,17 @@
|
||||
<template>
|
||||
<settings-card
|
||||
title="作业模板配置"
|
||||
icon="mdi-book-edit"
|
||||
:loading="loading"
|
||||
border
|
||||
icon="mdi-book-edit"
|
||||
title="作业模板配置"
|
||||
>
|
||||
<!-- 顶部操作按钮 -->
|
||||
<v-alert
|
||||
v-if="error"
|
||||
class="mb-4"
|
||||
closable
|
||||
type="error"
|
||||
variant="tonal"
|
||||
closable
|
||||
class="mb-4"
|
||||
>
|
||||
{{ error }}
|
||||
</v-alert>
|
||||
@ -19,20 +19,20 @@
|
||||
<div class="d-flex justify-space-between align-center mb-6">
|
||||
<div>
|
||||
<v-btn
|
||||
color="primary"
|
||||
size="large"
|
||||
prepend-icon="mdi-refresh"
|
||||
:loading="loading"
|
||||
@click="loadConfig"
|
||||
class="mr-2"
|
||||
color="primary"
|
||||
prepend-icon="mdi-refresh"
|
||||
size="large"
|
||||
@click="loadConfig"
|
||||
>
|
||||
重新加载配置
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="success"
|
||||
size="large"
|
||||
prepend-icon="mdi-content-save"
|
||||
:loading="loading"
|
||||
color="success"
|
||||
prepend-icon="mdi-content-save"
|
||||
size="large"
|
||||
@click="saveConfig"
|
||||
>
|
||||
保存所有更改
|
||||
@ -49,15 +49,15 @@
|
||||
|
||||
<v-row>
|
||||
<v-col cols="12" md="6">
|
||||
<setting-group title="科目配置" icon="mdi-book" border>
|
||||
<setting-group border icon="mdi-book" title="科目配置">
|
||||
<v-list>
|
||||
<v-list-item>
|
||||
<v-text-field
|
||||
v-model="newSubject"
|
||||
append-inner-icon="mdi-plus"
|
||||
density="comfortable"
|
||||
label="添加新科目"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
append-inner-icon="mdi-plus"
|
||||
@click:append-inner="addSubject"
|
||||
@keyup.enter="addSubject"
|
||||
/>
|
||||
@ -70,32 +70,32 @@
|
||||
v-model="editedSubjects[subject]"
|
||||
:placeholder="subject"
|
||||
density="comfortable"
|
||||
variant="plain"
|
||||
hide-details
|
||||
variant="plain"
|
||||
@blur="updateSubject(subject)"
|
||||
/>
|
||||
<v-spacer />
|
||||
<v-spacer/>
|
||||
<v-btn
|
||||
icon="mdi-delete"
|
||||
variant="text"
|
||||
color="error"
|
||||
icon="mdi-delete"
|
||||
size="small"
|
||||
variant="text"
|
||||
@click="deleteSubject(subject)"
|
||||
/>
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<v-text-field
|
||||
v-model="newBookTypes[subject]"
|
||||
append-inner-icon="mdi-plus"
|
||||
class="mb-2"
|
||||
density="comfortable"
|
||||
label="添加作业本名称"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
class="mb-2"
|
||||
append-inner-icon="mdi-plus"
|
||||
@click:append-inner="() => addBookType(subject)"
|
||||
@keyup.enter="() => addBookType(subject)"
|
||||
/>
|
||||
|
||||
<v-list density="compact" border rounded>
|
||||
<v-list border density="compact" rounded>
|
||||
<v-list-item
|
||||
v-for="(books, bookType) in config.subjects[subject].books"
|
||||
:key="bookType"
|
||||
@ -103,21 +103,21 @@
|
||||
@click="openSubjectBookDialog(subject, bookType, books)"
|
||||
>
|
||||
<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 v-slot:append>
|
||||
<v-chip
|
||||
size="small"
|
||||
class="mr-2"
|
||||
color="info"
|
||||
size="small"
|
||||
>
|
||||
{{ books.length }}个部分
|
||||
</v-chip>
|
||||
<v-btn
|
||||
icon="mdi-delete"
|
||||
variant="text"
|
||||
color="error"
|
||||
icon="mdi-delete"
|
||||
size="small"
|
||||
variant="text"
|
||||
@click.stop="() => deleteBookType(subject, bookType)"
|
||||
/>
|
||||
</template>
|
||||
@ -131,22 +131,22 @@
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" md="6">
|
||||
<setting-group title="通用配置" icon="mdi-cog" border>
|
||||
<setting-group border icon="mdi-cog" title="通用配置">
|
||||
<v-list>
|
||||
<v-list-item>
|
||||
<v-text-field
|
||||
v-model="newCommonBook"
|
||||
append-inner-icon="mdi-plus"
|
||||
density="comfortable"
|
||||
label="添加作业本名称"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
append-inner-icon="mdi-plus"
|
||||
@click:append-inner="addCommonBook"
|
||||
@keyup.enter="addCommonBook"
|
||||
/>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item>
|
||||
<v-list density="compact" border rounded>
|
||||
<v-list border density="compact" rounded>
|
||||
<v-list-item
|
||||
v-for="(books, bookType) in config.commonSubject.books"
|
||||
:key="bookType"
|
||||
@ -154,21 +154,21 @@
|
||||
@click="openSubjectBookDialog('common', bookType, books)"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<v-icon icon="mdi-book-multiple" class="mr-2" />
|
||||
<v-icon class="mr-2" icon="mdi-book-multiple"/>
|
||||
</template>
|
||||
<template v-slot:append>
|
||||
<v-chip
|
||||
size="small"
|
||||
class="mr-2"
|
||||
color="info"
|
||||
size="small"
|
||||
>
|
||||
{{ books.length }}个部分
|
||||
</v-chip>
|
||||
<v-btn
|
||||
icon="mdi-delete"
|
||||
variant="text"
|
||||
color="error"
|
||||
icon="mdi-delete"
|
||||
size="small"
|
||||
variant="text"
|
||||
@click.stop="() => deleteBookType('common', bookType)"
|
||||
/>
|
||||
</template>
|
||||
@ -176,22 +176,22 @@
|
||||
</v-list>
|
||||
</v-list-item>
|
||||
|
||||
<v-divider class="my-2" />
|
||||
<v-divider class="my-2"/>
|
||||
|
||||
<v-list-item>
|
||||
<v-text-field
|
||||
v-model="newAction"
|
||||
append-inner-icon="mdi-plus"
|
||||
density="comfortable"
|
||||
label="添加操作"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
append-inner-icon="mdi-plus"
|
||||
@click:append-inner="addAction"
|
||||
@keyup.enter="addAction"
|
||||
/>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item>
|
||||
<v-list density="compact" border rounded>
|
||||
<v-list border density="compact" rounded>
|
||||
<v-list-item
|
||||
v-for="action in config.actions"
|
||||
:key="action"
|
||||
@ -200,10 +200,10 @@
|
||||
>
|
||||
<template v-slot:append>
|
||||
<v-btn
|
||||
icon="mdi-delete"
|
||||
variant="text"
|
||||
color="error"
|
||||
icon="mdi-delete"
|
||||
size="small"
|
||||
variant="text"
|
||||
@click.stop="removeAction(action)"
|
||||
/>
|
||||
</template>
|
||||
@ -229,41 +229,41 @@
|
||||
<v-text-field
|
||||
v-model="dialog.editedItem.name"
|
||||
:label="dialog.nameLabel"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
:rules="[v => !!v || '名称不能为空']"
|
||||
density="comfortable"
|
||||
variant="outlined"
|
||||
/>
|
||||
</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>
|
||||
<v-chip color="primary">{{ dialog.editedItem.subject }}</v-chip>
|
||||
</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-title class="text-subtitle-1 py-2">需完成部分</v-card-title>
|
||||
<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-for="(task, index) in dialog.editedItem.tasks"
|
||||
:key="index"
|
||||
>
|
||||
<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>
|
||||
<v-text-field
|
||||
v-model="dialog.editedItem.tasks[index]"
|
||||
variant="plain"
|
||||
density="compact"
|
||||
hide-details
|
||||
variant="plain"
|
||||
/>
|
||||
<template v-slot:append>
|
||||
<v-btn
|
||||
icon="mdi-delete"
|
||||
variant="text"
|
||||
color="error"
|
||||
icon="mdi-delete"
|
||||
size="small"
|
||||
variant="text"
|
||||
@click="removeTask(index)"
|
||||
/>
|
||||
</template>
|
||||
@ -271,13 +271,13 @@
|
||||
</v-list>
|
||||
<v-text-field
|
||||
v-model="newTask"
|
||||
append-inner-icon="mdi-plus"
|
||||
class="mt-2"
|
||||
density="comfortable"
|
||||
label="添加需完成部分"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
append-inner-icon="mdi-plus"
|
||||
@click:append-inner="addTask"
|
||||
@keyup.enter="addTask"
|
||||
class="mt-2"
|
||||
/>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
@ -318,7 +318,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { reactive } from 'vue';
|
||||
import {reactive} from 'vue';
|
||||
import SettingsCard from '@/components/SettingsCard.vue';
|
||||
import SettingGroup from '@/components/settings/SettingGroup.vue';
|
||||
import dataProvider from "@/utils/dataProvider.js";
|
||||
@ -466,7 +466,7 @@ export default {
|
||||
addSubject() {
|
||||
if (!this.newSubject) return;
|
||||
if (!this.config.subjects[this.newSubject]) {
|
||||
this.config.subjects[this.newSubject] = { books: {} };
|
||||
this.config.subjects[this.newSubject] = {books: {}};
|
||||
}
|
||||
this.newSubject = '';
|
||||
},
|
||||
@ -627,7 +627,7 @@ export default {
|
||||
},
|
||||
|
||||
saveDialog() {
|
||||
const { type, name, subject, originalName, tasks } = this.dialog.editedItem;
|
||||
const {type, name, subject, originalName, tasks} = this.dialog.editedItem;
|
||||
|
||||
if (!name) {
|
||||
this.showMessage('名称不能为空', 'error');
|
||||
@ -687,4 +687,4 @@ export default {
|
||||
.v-card-text {
|
||||
padding-top: 0;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@ -1,36 +1,36 @@
|
||||
<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-item>
|
||||
<template #prepend>
|
||||
<v-icon :icon="connectionIcon" :color="connectionColor" class="mr-3" />
|
||||
<v-icon :color="connectionColor" :icon="connectionIcon" class="mr-3"/>
|
||||
</template>
|
||||
<v-list-item-title>数据库状态</v-list-item-title>
|
||||
<v-list-item-subtitle>{{ connectionStatus }}</v-list-item-subtitle>
|
||||
<template #append>
|
||||
<v-btn variant="tonal" @click="refreshConnection" :loading="loading">
|
||||
<v-btn :loading="loading" variant="tonal" @click="refreshConnection">
|
||||
刷新
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-list-item>
|
||||
|
||||
<v-divider class="my-2" />
|
||||
<v-divider class="my-2"/>
|
||||
|
||||
<!-- 数据列表 -->
|
||||
<v-list-item>
|
||||
<template #prepend>
|
||||
<v-icon icon="mdi-format-list-bulleted" class="mr-3" />
|
||||
<v-icon class="mr-3" icon="mdi-format-list-bulleted"/>
|
||||
</template>
|
||||
<v-list-item-title>数据条目</v-list-item-title>
|
||||
<v-list-item-subtitle>共 {{ kvData.length }} 条记录</v-list-item-subtitle>
|
||||
<template #append>
|
||||
<v-btn-group variant="tonal">
|
||||
<v-btn @click="loadKvData" :loading="loadingData">
|
||||
<v-btn :loading="loadingData" @click="loadKvData">
|
||||
加载数据
|
||||
</v-btn>
|
||||
<v-btn @click="createNewItem" :disabled="!isKvProvider">
|
||||
<v-icon icon="mdi-plus" class="mr-1" />
|
||||
<v-btn :disabled="!isKvProvider" @click="createNewItem">
|
||||
<v-icon class="mr-1" icon="mdi-plus"/>
|
||||
新建
|
||||
</v-btn>
|
||||
</v-btn-group>
|
||||
@ -41,60 +41,60 @@
|
||||
<!-- 数据表格 -->
|
||||
<v-card v-if="kvData.length > 0" class="mt-4" variant="outlined">
|
||||
<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数据列表
|
||||
<v-spacer />
|
||||
<v-spacer/>
|
||||
<v-text-field
|
||||
v-model="searchQuery"
|
||||
label="搜索键名"
|
||||
prepend-inner-icon="mdi-magnify"
|
||||
variant="outlined"
|
||||
clearable
|
||||
density="compact"
|
||||
hide-details
|
||||
clearable
|
||||
label="搜索键名"
|
||||
prepend-inner-icon="mdi-magnify"
|
||||
style="max-width: 300px;"
|
||||
variant="outlined"
|
||||
/>
|
||||
</v-card-title>
|
||||
|
||||
<v-data-table
|
||||
:headers="tableHeaders"
|
||||
:items="filteredKvData"
|
||||
:loading="loadingData"
|
||||
item-value="key"
|
||||
class="elevation-0"
|
||||
:items-per-page="10"
|
||||
:loading="loadingData"
|
||||
class="elevation-0"
|
||||
item-value="key"
|
||||
>
|
||||
<template #[`item.key`]="{ item }">
|
||||
<code class="text-primary">{{ item.key }}</code>
|
||||
</template>
|
||||
|
||||
<template #[`item.actions`]="{ item }">
|
||||
<v-btn-group variant="text" density="compact">
|
||||
<v-btn-group density="compact" variant="text">
|
||||
<v-btn
|
||||
icon="mdi-eye"
|
||||
size="small"
|
||||
@click="viewItem(item)"
|
||||
title="查看"
|
||||
@click="viewItem(item)"
|
||||
/>
|
||||
<v-btn
|
||||
icon="mdi-pencil"
|
||||
size="small"
|
||||
@click="editItem(item)"
|
||||
title="编辑"
|
||||
@click="editItem(item)"
|
||||
/>
|
||||
<v-btn
|
||||
color="primary"
|
||||
icon="mdi-cloud-download"
|
||||
size="small"
|
||||
color="primary"
|
||||
@click="getCloudUrl(item)"
|
||||
title="获取云端地址"
|
||||
@click="getCloudUrl(item)"
|
||||
/>
|
||||
<v-btn
|
||||
color="error"
|
||||
icon="mdi-delete"
|
||||
size="small"
|
||||
color="error"
|
||||
@click="confirmDelete(item)"
|
||||
title="删除"
|
||||
@click="confirmDelete(item)"
|
||||
/>
|
||||
</v-btn-group>
|
||||
</template>
|
||||
@ -105,10 +105,10 @@
|
||||
<v-dialog v-model="viewDialog" max-width="800px">
|
||||
<v-card>
|
||||
<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-btn icon="mdi-close" variant="text" @click="viewDialog = false" />
|
||||
<v-spacer/>
|
||||
<v-btn icon="mdi-close" variant="text" @click="viewDialog = false"/>
|
||||
</v-card-title>
|
||||
|
||||
<v-card-subtitle v-if="selectedItem">
|
||||
@ -119,21 +119,21 @@
|
||||
<v-textarea
|
||||
v-if="selectedItem"
|
||||
:model-value="formatJsonData(selectedItem.value)"
|
||||
class="font-monospace"
|
||||
label="数据内容"
|
||||
variant="outlined"
|
||||
readonly
|
||||
rows="15"
|
||||
class="font-monospace"
|
||||
variant="outlined"
|
||||
/>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn @click="copyToClipboard(selectedItem?.value)" variant="tonal">
|
||||
<v-icon icon="mdi-content-copy" class="mr-1" />
|
||||
<v-spacer/>
|
||||
<v-btn variant="tonal" @click="copyToClipboard(selectedItem?.value)">
|
||||
<v-icon class="mr-1" icon="mdi-content-copy"/>
|
||||
复制数据
|
||||
</v-btn>
|
||||
<v-btn @click="viewDialog = false" variant="text">
|
||||
<v-btn variant="text" @click="viewDialog = false">
|
||||
关闭
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
@ -144,10 +144,10 @@
|
||||
<v-dialog v-model="editDialog" max-width="800px">
|
||||
<v-card>
|
||||
<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-btn icon="mdi-close" variant="text" @click="closeEditDialog" />
|
||||
<v-spacer/>
|
||||
<v-btn icon="mdi-close" variant="text" @click="closeEditDialog"/>
|
||||
</v-card-title>
|
||||
|
||||
<v-card-subtitle v-if="editingItem">
|
||||
@ -157,26 +157,26 @@
|
||||
<v-card-text>
|
||||
<v-textarea
|
||||
v-model="editingData"
|
||||
label="数据内容 (JSON格式)"
|
||||
variant="outlined"
|
||||
rows="15"
|
||||
class="font-monospace"
|
||||
:error="!isValidJson"
|
||||
:error-messages="isValidJson ? [] : ['请输入有效的JSON格式']"
|
||||
class="font-monospace"
|
||||
label="数据内容 (JSON格式)"
|
||||
rows="15"
|
||||
variant="outlined"
|
||||
/>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn @click="closeEditDialog" variant="text">
|
||||
<v-spacer/>
|
||||
<v-btn variant="text" @click="closeEditDialog">
|
||||
取消
|
||||
</v-btn>
|
||||
<v-btn
|
||||
@click="saveEditedData"
|
||||
variant="tonal"
|
||||
color="primary"
|
||||
:disabled="!isValidJson"
|
||||
:loading="savingData"
|
||||
color="primary"
|
||||
variant="tonal"
|
||||
@click="saveEditedData"
|
||||
>
|
||||
保存
|
||||
</v-btn>
|
||||
@ -188,46 +188,46 @@
|
||||
<v-dialog v-model="createDialog" max-width="800px">
|
||||
<v-card>
|
||||
<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-btn icon="mdi-close" variant="text" @click="closeCreateDialog" />
|
||||
<v-spacer/>
|
||||
<v-btn icon="mdi-close" variant="text" @click="closeCreateDialog"/>
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text>
|
||||
<v-text-field
|
||||
v-model="newKey"
|
||||
label="键名"
|
||||
variant="outlined"
|
||||
class="mb-4"
|
||||
:error="!isValidKey"
|
||||
:error-messages="isValidKey ? [] : ['键名不能为空且不能与现有键重复']"
|
||||
class="mb-4"
|
||||
label="键名"
|
||||
placeholder="请输入键名,如:my-config"
|
||||
variant="outlined"
|
||||
/>
|
||||
|
||||
<v-textarea
|
||||
v-model="newData"
|
||||
label="数据内容 (JSON格式)"
|
||||
variant="outlined"
|
||||
rows="15"
|
||||
class="font-monospace"
|
||||
:error="!isValidNewJson"
|
||||
:error-messages="isValidNewJson ? [] : ['请输入有效的JSON格式']"
|
||||
class="font-monospace"
|
||||
label="数据内容 (JSON格式)"
|
||||
placeholder='请输入JSON数据,如:{"name": "value"}'
|
||||
rows="15"
|
||||
variant="outlined"
|
||||
/>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn @click="closeCreateDialog" variant="text">
|
||||
<v-spacer/>
|
||||
<v-btn variant="text" @click="closeCreateDialog">
|
||||
取消
|
||||
</v-btn>
|
||||
<v-btn
|
||||
@click="saveNewData"
|
||||
variant="tonal"
|
||||
color="primary"
|
||||
:disabled="!isValidKey || !isValidNewJson"
|
||||
:loading="savingData"
|
||||
color="primary"
|
||||
variant="tonal"
|
||||
@click="saveNewData"
|
||||
>
|
||||
创建
|
||||
</v-btn>
|
||||
@ -239,10 +239,10 @@
|
||||
<v-dialog v-model="cloudUrlDialog" max-width="800px">
|
||||
<v-card>
|
||||
<v-card-title class="d-flex align-center">
|
||||
<v-icon icon="mdi-cloud-download" class="mr-2" />
|
||||
<v-icon class="mr-2" icon="mdi-cloud-download"/>
|
||||
获取云端访问地址
|
||||
<v-spacer />
|
||||
<v-btn icon="mdi-close" variant="text" @click="cloudUrlDialog = false" />
|
||||
<v-spacer/>
|
||||
<v-btn icon="mdi-close" variant="text" @click="cloudUrlDialog = false"/>
|
||||
</v-card-title>
|
||||
|
||||
<v-card-subtitle v-if="selectedCloudItem">
|
||||
@ -250,19 +250,19 @@
|
||||
</v-card-subtitle>
|
||||
|
||||
<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 }}
|
||||
</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>
|
||||
<div class="mt-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 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>
|
||||
@ -271,39 +271,39 @@
|
||||
<v-text-field
|
||||
v-if="cloudUrlResult && cloudUrlResult.url"
|
||||
:model-value="cloudUrlResult.url"
|
||||
label="云端访问地址"
|
||||
variant="outlined"
|
||||
readonly
|
||||
class="font-monospace"
|
||||
append-inner-icon="mdi-content-copy"
|
||||
class="font-monospace"
|
||||
label="云端访问地址"
|
||||
readonly
|
||||
variant="outlined"
|
||||
@click:append-inner="copyCloudUrl"
|
||||
/>
|
||||
|
||||
<v-expansion-panels v-if="cloudUrlResult && cloudUrlResult.url" class="mt-4">
|
||||
<v-expansion-panel>
|
||||
<v-expansion-panel-title>
|
||||
<v-icon icon="mdi-cog" class="mr-2" />
|
||||
<v-icon class="mr-2" icon="mdi-cog"/>
|
||||
高级选项
|
||||
</v-expansion-panel-title>
|
||||
<v-expansion-panel-text>
|
||||
<v-checkbox
|
||||
v-model="cloudUrlOptions.migrateFromLocal"
|
||||
label="从本地迁移数据到云端"
|
||||
density="compact"
|
||||
label="从本地迁移数据到云端"
|
||||
/>
|
||||
<v-checkbox
|
||||
v-model="cloudUrlOptions.autoConfigureCloud"
|
||||
label="自动配置云端默认设置"
|
||||
density="compact"
|
||||
label="自动配置云端默认设置"
|
||||
/>
|
||||
<v-btn
|
||||
@click="refreshCloudUrl"
|
||||
variant="tonal"
|
||||
color="primary"
|
||||
:loading="gettingCloudUrl"
|
||||
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-expansion-panel-text>
|
||||
@ -312,17 +312,17 @@
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn @click="cloudUrlDialog = false" variant="text">
|
||||
<v-spacer/>
|
||||
<v-btn variant="text" @click="cloudUrlDialog = false">
|
||||
关闭
|
||||
</v-btn>
|
||||
<v-btn
|
||||
v-if="cloudUrlResult && cloudUrlResult.url"
|
||||
@click="openCloudUrl"
|
||||
variant="tonal"
|
||||
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-card-actions>
|
||||
@ -333,28 +333,28 @@
|
||||
<v-dialog v-model="deleteDialog" max-width="400px">
|
||||
<v-card>
|
||||
<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-text>
|
||||
确定要删除键名为 <code>{{ itemToDelete?.key }}</code> 的数据吗?
|
||||
<br><br>
|
||||
<v-alert type="warning" variant="tonal" class="mt-2">
|
||||
<v-alert class="mt-2" type="warning" variant="tonal">
|
||||
此操作不可撤销,请谨慎操作!
|
||||
</v-alert>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn @click="deleteDialog = false" variant="text">
|
||||
<v-spacer/>
|
||||
<v-btn variant="text" @click="deleteDialog = false">
|
||||
取消
|
||||
</v-btn>
|
||||
<v-btn
|
||||
@click="deleteItem"
|
||||
variant="tonal"
|
||||
color="error"
|
||||
:loading="deletingData"
|
||||
color="error"
|
||||
variant="tonal"
|
||||
@click="deleteItem"
|
||||
>
|
||||
删除
|
||||
</v-btn>
|
||||
@ -367,8 +367,8 @@
|
||||
<script>
|
||||
import SettingsCard from '@/components/SettingsCard.vue';
|
||||
import dataProvider from '@/utils/dataProvider';
|
||||
import { getSetting } from '@/utils/settings';
|
||||
import { openDB } from 'idb';
|
||||
import {getSetting} from '@/utils/settings';
|
||||
import {openDB} from 'idb';
|
||||
|
||||
export default {
|
||||
name: 'KvDatabaseCard',
|
||||
@ -414,8 +414,8 @@ export default {
|
||||
|
||||
// 表格头部
|
||||
tableHeaders: [
|
||||
{ title: '键名', key: 'key', sortable: true },
|
||||
{ title: '操作', key: 'actions', sortable: false, width: '120px' }
|
||||
{title: '键名', key: 'key', sortable: true},
|
||||
{title: '操作', key: 'actions', sortable: false, width: '120px'}
|
||||
]
|
||||
};
|
||||
},
|
||||
@ -426,7 +426,7 @@ export default {
|
||||
},
|
||||
|
||||
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() {
|
||||
@ -537,15 +537,10 @@ export default {
|
||||
},
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
async viewItem(item) {
|
||||
this.selectedItem = item;
|
||||
this.viewDialog = true;
|
||||
|
||||
|
||||
// 如果数据未加载,则加载数据
|
||||
if (!item.loaded || item.value === null) {
|
||||
await this.loadItemData(item);
|
||||
@ -554,12 +549,12 @@ export default {
|
||||
|
||||
async editItem(item) {
|
||||
this.editingItem = item;
|
||||
|
||||
|
||||
// 如果数据未加载,则加载数据
|
||||
if (!item.loaded || item.value === null) {
|
||||
await this.loadItemData(item);
|
||||
}
|
||||
|
||||
|
||||
this.editingData = this.formatJsonData(item.value);
|
||||
this.editDialog = true;
|
||||
},
|
||||
@ -716,22 +711,22 @@ export default {
|
||||
this.cloudUrlResult = null;
|
||||
this.cloudUrlError = null;
|
||||
this.cloudUrlDialog = true;
|
||||
|
||||
|
||||
await this.fetchCloudUrl();
|
||||
},
|
||||
|
||||
async fetchCloudUrl() {
|
||||
if (!this.selectedCloudItem) return;
|
||||
|
||||
|
||||
this.gettingCloudUrl = true;
|
||||
this.cloudUrlError = null;
|
||||
|
||||
|
||||
try {
|
||||
const result = await dataProvider.getKeyCloudUrl(
|
||||
this.selectedCloudItem.key,
|
||||
this.cloudUrlOptions
|
||||
);
|
||||
|
||||
|
||||
if (result.success) {
|
||||
this.cloudUrlResult = result;
|
||||
this.$message.success('云端地址获取成功');
|
||||
@ -753,7 +748,7 @@ export default {
|
||||
|
||||
async copyCloudUrl() {
|
||||
if (!this.cloudUrlResult?.url) return;
|
||||
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(this.cloudUrlResult.url);
|
||||
this.$message.success('云端地址已复制到剪贴板');
|
||||
@ -764,7 +759,7 @@ export default {
|
||||
|
||||
openCloudUrl() {
|
||||
if (!this.cloudUrlResult?.url) return;
|
||||
|
||||
|
||||
try {
|
||||
window.open(this.cloudUrlResult.url, '_blank');
|
||||
} catch (error) {
|
||||
|
||||
@ -1,30 +1,28 @@
|
||||
<template>
|
||||
<settings-card title="编辑设置" icon="mdi-cog">
|
||||
<v-list>
|
||||
<setting-item setting-key="randomPicker.enabled" />
|
||||
<v-divider class="my-2" />
|
||||
<settings-card icon="mdi-cog" title="编辑设置">
|
||||
<v-list>
|
||||
<setting-item setting-key="randomPicker.enabled"/>
|
||||
<v-divider class="my-2"/>
|
||||
|
||||
<setting-item setting-key="randomPicker.mode" />
|
||||
<v-divider class="my-2" />
|
||||
<setting-item setting-key="randomPicker.minNumber" />
|
||||
<v-divider class="my-2" />
|
||||
<setting-item setting-key="randomPicker.maxNumber" />
|
||||
<v-divider class="my-2" />
|
||||
<setting-item setting-key="randomPicker.defaultCount" />
|
||||
<setting-item setting-key="randomPicker.mode"/>
|
||||
<v-divider class="my-2"/>
|
||||
<setting-item setting-key="randomPicker.minNumber"/>
|
||||
<v-divider class="my-2"/>
|
||||
<setting-item setting-key="randomPicker.maxNumber"/>
|
||||
<v-divider class="my-2"/>
|
||||
<setting-item setting-key="randomPicker.defaultCount"/>
|
||||
|
||||
|
||||
<v-divider class="my-2" />
|
||||
<setting-item setting-key="randomPicker.animation" />
|
||||
<v-divider class="my-2"/>
|
||||
<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>
|
||||
</settings-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import SettingsCard from '@/components/SettingsCard.vue';
|
||||
import SettingItem from '../SettingItem.vue';
|
||||
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@ -1,10 +1,11 @@
|
||||
<template>
|
||||
<settings-card title="刷新设置" icon="mdi-refresh-circle">
|
||||
<settings-card icon="mdi-refresh-circle" title="刷新设置">
|
||||
<v-form>
|
||||
<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-form>
|
||||
@ -14,15 +15,14 @@
|
||||
<script>
|
||||
import SettingsCard from '@/components/SettingsCard.vue';
|
||||
import SettingItem from '@/components/settings/SettingItem.vue';
|
||||
|
||||
export default {
|
||||
name: 'RefreshSettingsCard',
|
||||
components: { SettingsCard },
|
||||
components: {SettingsCard},
|
||||
data() {
|
||||
|
||||
|
||||
return {
|
||||
|
||||
};
|
||||
return {};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<settings-card
|
||||
title="数据源设置"
|
||||
icon="mdi-database"
|
||||
:loading="loading"
|
||||
icon="mdi-database"
|
||||
title="数据源设置"
|
||||
>
|
||||
<v-form>
|
||||
<!-- 使用双向绑定来替代 setting-key -->
|
||||
@ -13,20 +13,20 @@
|
||||
{ title: 'KV本地存储', value: 'kv-local' },
|
||||
{ title: 'KV远程服务器', value: 'kv-server' }
|
||||
]"
|
||||
label="数据提供者"
|
||||
variant="outlined"
|
||||
class="mb-3"
|
||||
density="comfortable"
|
||||
item-title="title"
|
||||
item-value="value"
|
||||
label="数据提供者"
|
||||
prepend-icon="mdi-database"
|
||||
class="mb-3"
|
||||
variant="outlined"
|
||||
/>
|
||||
|
||||
<v-alert
|
||||
v-if="isKvProvider"
|
||||
class="my-2"
|
||||
type="info"
|
||||
variant="tonal"
|
||||
class="my-2"
|
||||
>
|
||||
<v-alert-title>KV 存储系统</v-alert-title>
|
||||
<p>KV存储系统使用本机唯一标识符(UUID)来区分不同设备的数据。</p>
|
||||
@ -38,10 +38,10 @@
|
||||
|
||||
<v-alert
|
||||
v-if="isClassworksCloud"
|
||||
type="info"
|
||||
color="success"
|
||||
variant="tonal"
|
||||
class="my-2"
|
||||
color="success"
|
||||
type="info"
|
||||
variant="tonal"
|
||||
>
|
||||
<v-alert-title>Classworks云端存储</v-alert-title>
|
||||
<p>Classworks云端存储是官方提供的存储解决方案,自动配置了最优的访问设置。</p>
|
||||
@ -56,17 +56,16 @@
|
||||
<div v-if="isClassworksCloud">
|
||||
<v-text-field
|
||||
v-model="serverSettings.kvToken"
|
||||
label="KV 授权令牌"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
prepend-icon="mdi-shield-key"
|
||||
class="mb-2"
|
||||
density="comfortable"
|
||||
hint="令牌用于云端存储授权"
|
||||
label="KV 授权令牌"
|
||||
persistent-hint
|
||||
prepend-icon="mdi-shield-key"
|
||||
variant="outlined"
|
||||
/>
|
||||
|
||||
|
||||
|
||||
<cloud-namespace-info-card
|
||||
:visible="isClassworksCloud"
|
||||
class="mt-4"
|
||||
@ -77,24 +76,24 @@
|
||||
<div v-else-if="currentProvider === 'kv-server'">
|
||||
<v-text-field
|
||||
v-model="serverSettings.domain"
|
||||
label="服务器域名"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
prepend-icon="mdi-web"
|
||||
class="mb-2"
|
||||
density="comfortable"
|
||||
hint="例如: https://example.com (不需要路径)"
|
||||
label="服务器域名"
|
||||
persistent-hint
|
||||
prepend-icon="mdi-web"
|
||||
variant="outlined"
|
||||
/>
|
||||
|
||||
<v-text-field
|
||||
v-model="serverSettings.kvToken"
|
||||
label="KV 授权令牌"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
prepend-icon="mdi-shield-key"
|
||||
class="mb-2"
|
||||
density="comfortable"
|
||||
hint="令牌用于服务器验证"
|
||||
label="KV 授权令牌"
|
||||
persistent-hint
|
||||
prepend-icon="mdi-shield-key"
|
||||
variant="outlined"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -102,13 +101,13 @@
|
||||
<div v-else-if="currentProvider === 'kv-local'">
|
||||
<v-text-field
|
||||
v-model="serverSettings.classNumber"
|
||||
label="班级编号"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
prepend-icon="mdi-account-group"
|
||||
class="mb-2"
|
||||
density="comfortable"
|
||||
hint="例如: 高三八班"
|
||||
label="班级编号"
|
||||
persistent-hint
|
||||
prepend-icon="mdi-account-group"
|
||||
variant="outlined"
|
||||
/>
|
||||
</div>
|
||||
</v-form>
|
||||
@ -118,11 +117,11 @@
|
||||
<script>
|
||||
import SettingsCard from "@/components/SettingsCard.vue";
|
||||
import CloudNamespaceInfoCard from "./CloudNamespaceInfoCard.vue";
|
||||
import { getSetting, setSetting, watchSettings } from "@/utils/settings";
|
||||
import {getSetting, setSetting, watchSettings} from "@/utils/settings";
|
||||
|
||||
export default {
|
||||
name: "ServerSettingsCard",
|
||||
components: { SettingsCard, CloudNamespaceInfoCard },
|
||||
components: {SettingsCard, CloudNamespaceInfoCard},
|
||||
props: {
|
||||
loading: Boolean,
|
||||
},
|
||||
|
||||
@ -1,16 +1,16 @@
|
||||
<template>
|
||||
<settings-card
|
||||
title="科目管理"
|
||||
icon="mdi-book-multiple"
|
||||
:loading="loading"
|
||||
border
|
||||
icon="mdi-book-multiple"
|
||||
title="科目管理"
|
||||
>
|
||||
<v-alert
|
||||
v-if="error"
|
||||
class="mb-4"
|
||||
closable
|
||||
type="error"
|
||||
variant="tonal"
|
||||
closable
|
||||
class="mb-4"
|
||||
>
|
||||
{{ error }}
|
||||
</v-alert>
|
||||
@ -18,32 +18,32 @@
|
||||
<div class="d-flex justify-space-between align-center mb-6">
|
||||
<div>
|
||||
<v-btn
|
||||
variant="text"
|
||||
color="primary"
|
||||
size="large"
|
||||
prepend-icon="mdi-refresh"
|
||||
:loading="loading"
|
||||
@click="loadConfig"
|
||||
class="mr-2"
|
||||
color="primary"
|
||||
prepend-icon="mdi-refresh"
|
||||
size="large"
|
||||
variant="text"
|
||||
@click="loadConfig"
|
||||
>
|
||||
重新加载
|
||||
</v-btn>
|
||||
|
||||
<v-btn
|
||||
color="success"
|
||||
size="large"
|
||||
prepend-icon="mdi-content-save"
|
||||
:loading="loading"
|
||||
color="success"
|
||||
prepend-icon="mdi-content-save"
|
||||
size="large"
|
||||
@click="saveConfig"
|
||||
>
|
||||
保存
|
||||
</v-btn>
|
||||
<v-btn
|
||||
variant="text"
|
||||
prepend-icon="mdi-restore"
|
||||
<v-btn
|
||||
:loading="loading"
|
||||
@click="resetToDefault"
|
||||
class="mr-2"
|
||||
prepend-icon="mdi-restore"
|
||||
variant="text"
|
||||
@click="resetToDefault"
|
||||
>
|
||||
重置为默认
|
||||
</v-btn>
|
||||
@ -64,12 +64,12 @@
|
||||
<v-col cols="12" sm="6">
|
||||
<v-text-field
|
||||
v-model="newSubjectName"
|
||||
:rules="[v => !!v || '科目名称不能为空']"
|
||||
append-inner-icon="mdi-plus"
|
||||
density="comfortable"
|
||||
label="科目名称"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
:rules="[v => !!v || '科目名称不能为空']"
|
||||
@keyup.enter="addSubject"
|
||||
append-inner-icon="mdi-plus"
|
||||
@click:append-inner="addSubject"
|
||||
/>
|
||||
</v-col>
|
||||
@ -88,17 +88,17 @@
|
||||
<template v-slot:prepend>
|
||||
<div class="d-flex flex-column align-center mr-2">
|
||||
<v-btn
|
||||
icon="mdi-chevron-up"
|
||||
variant="text"
|
||||
size="small"
|
||||
:disabled="index === 0"
|
||||
icon="mdi-chevron-up"
|
||||
size="small"
|
||||
variant="text"
|
||||
@click="moveSubject(index, -1)"
|
||||
/>
|
||||
<v-btn
|
||||
icon="mdi-chevron-down"
|
||||
variant="text"
|
||||
size="small"
|
||||
:disabled="index === subjects.length - 1"
|
||||
icon="mdi-chevron-down"
|
||||
size="small"
|
||||
variant="text"
|
||||
@click="moveSubject(index, 1)"
|
||||
/>
|
||||
</div>
|
||||
@ -107,19 +107,19 @@
|
||||
<v-list-item-title>
|
||||
<v-text-field
|
||||
v-model="subject.name"
|
||||
variant="plain"
|
||||
density="compact"
|
||||
hide-details
|
||||
variant="plain"
|
||||
@blur="updateSubject(subject)"
|
||||
/>
|
||||
</v-list-item-title>
|
||||
|
||||
<template v-slot:append>
|
||||
<v-btn
|
||||
icon="mdi-delete"
|
||||
variant="text"
|
||||
color="error"
|
||||
icon="mdi-delete"
|
||||
size="small"
|
||||
variant="text"
|
||||
@click="deleteSubject(subject)"
|
||||
/>
|
||||
</template>
|
||||
@ -161,16 +161,16 @@ export default {
|
||||
snackbarText: '',
|
||||
snackbarColor: 'success',
|
||||
defaultSubjects: [
|
||||
{ name: '语文', order: 0 },
|
||||
{ name: '数学', order: 1 },
|
||||
{ name: '英语', order: 2 },
|
||||
{ name: '物理', order: 3 },
|
||||
{ name: '化学', order: 4 },
|
||||
{ name: '生物', order: 5 },
|
||||
{ name: '政治', order: 6 },
|
||||
{ name: '历史', order: 7 },
|
||||
{ name: '地理', order: 8 },
|
||||
{ name: '其他', order: 9 }
|
||||
{name: '语文', order: 0},
|
||||
{name: '数学', order: 1},
|
||||
{name: '英语', order: 2},
|
||||
{name: '物理', order: 3},
|
||||
{name: '化学', order: 4},
|
||||
{name: '生物', order: 5},
|
||||
{name: '政治', order: 6},
|
||||
{name: '历史', order: 7},
|
||||
{name: '地理', order: 8},
|
||||
{name: '其他', order: 9}
|
||||
]
|
||||
};
|
||||
},
|
||||
@ -250,7 +250,7 @@ export default {
|
||||
updateSubject(subject) {
|
||||
const index = this.subjects.findIndex(s => s.order === subject.order);
|
||||
if (index > -1) {
|
||||
this.subjects[index] = { ...subject };
|
||||
this.subjects[index] = {...subject};
|
||||
}
|
||||
},
|
||||
|
||||
@ -291,7 +291,8 @@ export default {
|
||||
.v-list-item {
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.v-list-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@ -1,24 +1,24 @@
|
||||
<template>
|
||||
<settings-card title="主题设置" icon="mdi-palette">
|
||||
<settings-card icon="mdi-palette" title="主题设置">
|
||||
<v-list>
|
||||
<v-list-item>
|
||||
<template #prepend>
|
||||
<v-icon icon="mdi-theme-light-dark" class="mr-3" />
|
||||
<v-icon class="mr-3" icon="mdi-theme-light-dark"/>
|
||||
</template>
|
||||
<v-list-item-title>主题模式</v-list-item-title>
|
||||
<v-list-item-subtitle>选择明亮或暗黑主题</v-list-item-subtitle>
|
||||
<template #append>
|
||||
<v-btn-toggle
|
||||
v-model="localTheme"
|
||||
density="comfortable"
|
||||
color="primary"
|
||||
density="comfortable"
|
||||
>
|
||||
<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 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-toggle>
|
||||
@ -30,12 +30,12 @@
|
||||
|
||||
<script>
|
||||
import SettingsCard from '@/components/SettingsCard.vue';
|
||||
import { getSetting, setSetting } from '@/utils/settings';
|
||||
import { useTheme } from 'vuetify';
|
||||
import {getSetting, setSetting} from '@/utils/settings';
|
||||
import {useTheme} from 'vuetify';
|
||||
|
||||
export default {
|
||||
name: 'ThemeSettingsCard',
|
||||
components: { SettingsCard },
|
||||
components: {SettingsCard},
|
||||
|
||||
data() {
|
||||
return {
|
||||
@ -52,7 +52,7 @@ export default {
|
||||
|
||||
setup() {
|
||||
const theme = useTheme();
|
||||
return { theme };
|
||||
return {theme};
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,5 +1,7 @@
|
||||
# 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.
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
<template>
|
||||
<v-app>
|
||||
<v-main>
|
||||
<router-view />
|
||||
<router-view/>
|
||||
</v-main>
|
||||
</v-app>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
//
|
||||
//
|
||||
</script>
|
||||
|
||||
34
src/main.js
34
src/main.js
@ -5,8 +5,9 @@
|
||||
*/
|
||||
|
||||
// Plugins
|
||||
import { registerPlugins } from '@/plugins'
|
||||
import { createPinia } from 'pinia'
|
||||
import {registerPlugins} from '@/plugins'
|
||||
import {createPinia} from 'pinia'
|
||||
|
||||
const pinia = createPinia()
|
||||
|
||||
// Components
|
||||
@ -14,8 +15,9 @@ import App from './App.vue'
|
||||
import GlobalMessage from '@/components/GlobalMessage.vue'
|
||||
|
||||
// Composables
|
||||
import { createApp } from 'vue'
|
||||
import {createApp} from 'vue'
|
||||
import Clarity from '@microsoft/clarity';
|
||||
|
||||
const projectId = "rhp8uqoc3l"
|
||||
//import TDesign from 'tdesign-vue-next'
|
||||
//import 'tdesign-vue-next/es/style/index.css'
|
||||
@ -37,18 +39,18 @@ app.mount('#app')
|
||||
|
||||
// 移除首屏 CSS 加载覆盖层(在 Vue 挂载完成后)
|
||||
try {
|
||||
const removeLoader = () => {
|
||||
document.body.classList.add('app-loaded');
|
||||
const el = document.getElementById('app-loader');
|
||||
if (!el) return;
|
||||
// 与 CSS 过渡对齐,稍等再移除节点,避免闪烁
|
||||
setTimeout(() => el.remove(), 220);
|
||||
};
|
||||
if (document.readyState === 'complete' || document.readyState === 'interactive') {
|
||||
removeLoader();
|
||||
} else {
|
||||
window.addEventListener('DOMContentLoaded', removeLoader, { once: true });
|
||||
}
|
||||
const removeLoader = () => {
|
||||
document.body.classList.add('app-loaded');
|
||||
const el = document.getElementById('app-loader');
|
||||
if (!el) return;
|
||||
// 与 CSS 过渡对齐,稍等再移除节点,避免闪烁
|
||||
setTimeout(() => el.remove(), 220);
|
||||
};
|
||||
if (document.readyState === 'complete' || document.readyState === 'interactive') {
|
||||
removeLoader();
|
||||
} else {
|
||||
window.addEventListener('DOMContentLoaded', removeLoader, {once: true});
|
||||
}
|
||||
} catch {
|
||||
// 安全失败:即便移除失败也不影响应用
|
||||
// 安全失败:即便移除失败也不影响应用
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<error404 />
|
||||
<error404/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import error404 from "@/components/error/404.vue";
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@ -3,38 +3,39 @@
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<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>
|
||||
<h1 class="text-h4 ">缓存管理</h1>
|
||||
<div class="text-subtitle-1 text-grey">管理应用的本地缓存资源</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-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>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
|
||||
<v-row>
|
||||
<v-col cols="12" md="8">
|
||||
<v-card class="mb-4" variant="tonal">
|
||||
<v-card-text>
|
||||
<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>
|
||||
</div>
|
||||
<p>缓存是浏览器在本地存储的网站资源副本,如图片、脚本和样式表等。这些缓存可以加快页面加载速度,减少数据使用,并在离线时提供基本功能。</p>
|
||||
<p>
|
||||
缓存是浏览器在本地存储的网站资源副本,如图片、脚本和样式表等。这些缓存可以加快页面加载速度,减少数据使用,并在离线时提供基本功能。</p>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
|
||||
|
||||
<v-col cols="12" md="4">
|
||||
<v-card class="mb-4" variant="tonal">
|
||||
<v-card-text>
|
||||
<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>
|
||||
</div>
|
||||
<ul class="pl-4">
|
||||
@ -46,8 +47,8 @@
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<CacheManager />
|
||||
|
||||
<CacheManager/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
@ -65,4 +66,4 @@ export default {
|
||||
title: '缓存管理'
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@ -4,9 +4,9 @@
|
||||
<v-col cols="12">
|
||||
<div class="d-flex align-center mb-6">
|
||||
<v-icon
|
||||
size="x-large"
|
||||
color="primary"
|
||||
class="mr-3"
|
||||
color="primary"
|
||||
size="x-large"
|
||||
>
|
||||
mdi-database-sync
|
||||
</v-icon>
|
||||
@ -22,13 +22,13 @@
|
||||
|
||||
<v-card
|
||||
class="mb-6"
|
||||
variant="tonal"
|
||||
color="info"
|
||||
variant="tonal"
|
||||
>
|
||||
<v-card-text class="d-flex align-center">
|
||||
<v-icon
|
||||
color="info"
|
||||
class="mr-2"
|
||||
color="info"
|
||||
>
|
||||
mdi-information-outline
|
||||
</v-icon>
|
||||
@ -38,7 +38,7 @@
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<MigrationTool ref="migrationTool" />
|
||||
<MigrationTool ref="migrationTool"/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
@ -51,9 +51,9 @@
|
||||
<v-card>
|
||||
<v-card-title class="text-h5 d-flex align-center">
|
||||
<v-icon
|
||||
class="mr-3"
|
||||
color="primary"
|
||||
size="large"
|
||||
class="mr-3"
|
||||
>
|
||||
mdi-database-sync
|
||||
</v-icon>
|
||||
@ -66,10 +66,10 @@
|
||||
</p>
|
||||
|
||||
<v-alert
|
||||
color="info"
|
||||
variant="tonal"
|
||||
class="mt-4"
|
||||
color="info"
|
||||
icon="mdi-information-outline"
|
||||
variant="tonal"
|
||||
>
|
||||
<ul class="ml-3 mt-1">
|
||||
<li>数据源: {{ dataSourceText }}</li>
|
||||
@ -83,7 +83,7 @@
|
||||
</v-alert>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-spacer/>
|
||||
<v-btn
|
||||
color="grey-darken-1"
|
||||
variant="text"
|
||||
@ -92,16 +92,16 @@
|
||||
稍后再说
|
||||
</v-btn>
|
||||
<v-btn
|
||||
:disabled="isAutoMigrating"
|
||||
:loading="isAutoMigrating"
|
||||
color="primary"
|
||||
size="large"
|
||||
variant="elevated"
|
||||
:loading="isAutoMigrating"
|
||||
:disabled="isAutoMigrating"
|
||||
@click="startAutoMigration"
|
||||
>
|
||||
<v-icon
|
||||
left
|
||||
class="mr-2"
|
||||
left
|
||||
>
|
||||
mdi-database-export
|
||||
</v-icon>
|
||||
@ -115,7 +115,7 @@
|
||||
|
||||
<script>
|
||||
import MigrationTool from "@/components/MigrationTool.vue";
|
||||
import { getSetting, setSetting } from "@/utils/settings";
|
||||
import {getSetting, setSetting} from "@/utils/settings";
|
||||
|
||||
export default {
|
||||
name: "DataMigrationPage",
|
||||
|
||||
@ -2,4 +2,5 @@
|
||||
|
||||
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.
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<v-container class="fill-height" fluid>
|
||||
<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-title class="text-h5">
|
||||
{{ status === 'processing' ? '正在处理授权...' : status === 'success' ? '授权成功' : '授权失败' }}
|
||||
@ -9,9 +9,9 @@
|
||||
<v-card-text>
|
||||
<v-progress-linear
|
||||
v-if="status === 'processing'"
|
||||
indeterminate
|
||||
color="primary"
|
||||
class="mb-4"
|
||||
color="primary"
|
||||
indeterminate
|
||||
></v-progress-linear>
|
||||
<p>{{ message }}</p>
|
||||
</v-card-text>
|
||||
@ -26,9 +26,9 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { getSetting, setSetting } from '@/utils/settings';
|
||||
import {ref, onMounted} from 'vue';
|
||||
import {useRoute, useRouter} from 'vue-router';
|
||||
import {getSetting, setSetting} from '@/utils/settings';
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
@ -59,7 +59,7 @@ onMounted(async () => {
|
||||
}
|
||||
|
||||
status.value = 'success';
|
||||
router.push('/');
|
||||
router.push('/');
|
||||
|
||||
} catch (error) {
|
||||
console.error('授权处理失败:', error);
|
||||
|
||||
@ -45,12 +45,19 @@
|
||||
|
||||
<!-- 输入方式选择 -->
|
||||
<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="file" class="px-5"><v-icon start>mdi-file-upload</v-icon> 文件上传</v-tab>
|
||||
<v-tab value="text" class="px-5">
|
||||
<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-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="json">JSON</v-btn>
|
||||
<v-btn value="yaml" :disabled="!yamlLibLoaded">
|
||||
@ -268,12 +275,12 @@
|
||||
<span
|
||||
v-if="!settings.hideTeacherName && course.teacher"
|
||||
>
|
||||
<br />{{ course.teacher }}
|
||||
<br/>{{ course.teacher }}
|
||||
</span>
|
||||
<span
|
||||
v-if="!settings.hideRoom && course.room"
|
||||
>
|
||||
<br />{{ course.room }}
|
||||
<br/>{{ course.room }}
|
||||
</span>
|
||||
<span
|
||||
v-if="course.weekType"
|
||||
@ -288,17 +295,17 @@
|
||||
<span
|
||||
v-if="!settings.hideTeacherName && item[day].teacher"
|
||||
>
|
||||
<br />{{ item[day].teacher }}
|
||||
<br/>{{ item[day].teacher }}
|
||||
</span>
|
||||
<span
|
||||
v-if="!settings.hideRoom && item[day].room"
|
||||
>
|
||||
<br />{{ item[day].room }}
|
||||
<br/>{{ item[day].room }}
|
||||
</span>
|
||||
<span
|
||||
<span
|
||||
v-if="item[day].weekType"
|
||||
class="week-type"
|
||||
>
|
||||
class="week-type"
|
||||
>
|
||||
{{ item[day].weekType }}周
|
||||
</span>
|
||||
</template>
|
||||
@ -329,7 +336,8 @@
|
||||
</v-card-title>
|
||||
<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">
|
||||
{{ dayNames[day] }}
|
||||
<v-badge
|
||||
@ -345,67 +353,69 @@
|
||||
<v-window-item v-for="day in daysWithSchedule" :key="day" :value="day">
|
||||
<v-table density="compact" class="rounded" :headers-length="6" disable-sort>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-center">节次</th>
|
||||
<th>课程</th>
|
||||
<th>时间</th>
|
||||
<th>教师</th>
|
||||
<th>教室</th>
|
||||
<th>周次</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th class="text-center">节次</th>
|
||||
<th>课程</th>
|
||||
<th>时间</th>
|
||||
<th>教师</th>
|
||||
<th>教室</th>
|
||||
<th>周次</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<template v-for="(group, index) in groupByPeriod(getDaySchedule(day))" :key="index">
|
||||
<tr>
|
||||
<td class="text-center font-weight-bold">
|
||||
{{ group.period }}
|
||||
<v-tooltip v-if="group.originalPeriod !== group.period">
|
||||
<template v-slot:activator="{ props }">
|
||||
<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 v-for="(group, index) in groupByPeriod(getDaySchedule(day))" :key="index">
|
||||
<tr>
|
||||
<td class="text-center font-weight-bold">
|
||||
{{ group.period }}
|
||||
<v-tooltip v-if="group.originalPeriod !== group.period">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-icon size="x-small" v-bind="props" color="info" class="ml-1">mdi-sync</v-icon>
|
||||
</template>
|
||||
<template v-else>-</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>
|
||||
原节次: {{ 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.weeks }}
|
||||
{{ item.teacher || '-' }}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</template>
|
||||
<template v-else>-</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>
|
||||
</v-table>
|
||||
</v-window-item>
|
||||
@ -600,25 +610,25 @@ export default {
|
||||
totalWeeks: 30
|
||||
},
|
||||
tableHeaders: [
|
||||
{ title: "", key: "data-table-select" },
|
||||
{ title: "节次", key: "period" },
|
||||
{ title: "周一", key: "1" },
|
||||
{ title: "周二", key: "2" },
|
||||
{ title: "周三", key: "3" },
|
||||
{ title: "周四", key: "4" },
|
||||
{ title: "周五", key: "5" },
|
||||
{ title: "周六", key: "6" },
|
||||
{ title: "周日", key: "7" },
|
||||
{title: "", key: "data-table-select"},
|
||||
{title: "节次", key: "period"},
|
||||
{title: "周一", key: "1"},
|
||||
{title: "周二", key: "2"},
|
||||
{title: "周三", key: "3"},
|
||||
{title: "周四", key: "4"},
|
||||
{title: "周五", key: "5"},
|
||||
{title: "周六", key: "6"},
|
||||
{title: "周日", key: "7"},
|
||||
],
|
||||
timeTableHeaders: [
|
||||
{ title: "节次", key: "period" },
|
||||
{ title: "课程", key: "subject" },
|
||||
{ title: "星期", key: "day" },
|
||||
{ title: "开始时间", key: "startTime" },
|
||||
{ title: "结束时间", key: "endTime" },
|
||||
{ title: "教师", key: "teacher" },
|
||||
{ title: "教室", key: "room" },
|
||||
{ title: "周次", key: "weeks" },
|
||||
{title: "节次", key: "period"},
|
||||
{title: "课程", key: "subject"},
|
||||
{title: "星期", key: "day"},
|
||||
{title: "开始时间", key: "startTime"},
|
||||
{title: "结束时间", key: "endTime"},
|
||||
{title: "教师", key: "teacher"},
|
||||
{title: "教室", key: "room"},
|
||||
{title: "周次", key: "weeks"},
|
||||
],
|
||||
dayNames: {
|
||||
1: "周一",
|
||||
@ -700,7 +710,7 @@ export default {
|
||||
// 先按节次排序
|
||||
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];
|
||||
});
|
||||
},
|
||||
@ -845,8 +855,8 @@ export default {
|
||||
// 如果数据是通过CSESParser解析的
|
||||
if (data instanceof CSESParser) {
|
||||
return data.version === 1 &&
|
||||
Array.isArray(data.subjects) &&
|
||||
Array.isArray(data.schedules);
|
||||
Array.isArray(data.subjects) &&
|
||||
Array.isArray(data.schedules);
|
||||
}
|
||||
|
||||
// 基本验证
|
||||
@ -865,7 +875,7 @@ export default {
|
||||
},
|
||||
|
||||
processCsesData(data) {
|
||||
const { schedules, subjects } = data;
|
||||
const {schedules, subjects} = data;
|
||||
|
||||
// 使用对象引用优化内存使用
|
||||
const subjectMap = Object.fromEntries(
|
||||
@ -1004,7 +1014,7 @@ export default {
|
||||
for (const group of periodGroups) {
|
||||
// 遍历该节次的每个课程
|
||||
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 room = this.settings.hideRoom ? "" : (item.room || "");
|
||||
|
||||
@ -1072,7 +1082,7 @@ export default {
|
||||
}
|
||||
// 检查是否含有YAML典型的格式如键值对 key: value
|
||||
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) {
|
||||
@ -1094,7 +1104,7 @@ export default {
|
||||
// 获取原始数据并过滤
|
||||
const allData = this.getUnfilteredTimeTableData();
|
||||
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;
|
||||
});
|
||||
},
|
||||
@ -1165,7 +1175,7 @@ export default {
|
||||
// 先按节次排序
|
||||
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];
|
||||
});
|
||||
},
|
||||
|
||||
@ -26,18 +26,18 @@
|
||||
label="server.authDomain"
|
||||
/>
|
||||
</v-form>
|
||||
<v-divider class="my-4" />
|
||||
<v-divider class="my-4"/>
|
||||
|
||||
<v-btn
|
||||
color="primary"
|
||||
class="me-2"
|
||||
color="primary"
|
||||
@click="applySettings"
|
||||
>
|
||||
应用设置
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="secondary"
|
||||
class="me-2"
|
||||
color="secondary"
|
||||
@click="clearGuard"
|
||||
>
|
||||
清除重定向守卫
|
||||
@ -83,9 +83,9 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import { getSetting, setSetting } from '@/utils/settings'
|
||||
import { kvServerProvider } from '@/utils/providers/kvServerProvider'
|
||||
import {ref, computed} from 'vue'
|
||||
import {getSetting, setSetting} from '@/utils/settings'
|
||||
import {kvServerProvider} from '@/utils/providers/kvServerProvider'
|
||||
|
||||
const REDIRECT_GUARD_KEY = 'kvinit.redirecting'
|
||||
|
||||
@ -104,7 +104,11 @@ const applySettings = () => {
|
||||
}
|
||||
|
||||
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 = () => {
|
||||
@ -117,7 +121,11 @@ const simulateLoadError = () => {
|
||||
}
|
||||
|
||||
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(() => {
|
||||
|
||||
@ -25,8 +25,8 @@
|
||||
<v-list-item-subtitle>
|
||||
<v-chip
|
||||
:color="connected ? 'success' : 'error'"
|
||||
size="small"
|
||||
class="mr-2"
|
||||
size="small"
|
||||
>
|
||||
{{ connected ? 'connected' : 'disconnected' }}
|
||||
</v-chip>
|
||||
@ -42,7 +42,7 @@
|
||||
<v-list-item-subtitle>{{ currentDataKey }}</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
<v-divider class="my-4" />
|
||||
<v-divider class="my-4"/>
|
||||
<v-row>
|
||||
<v-col
|
||||
cols="12"
|
||||
@ -50,26 +50,26 @@
|
||||
>
|
||||
<v-text-field
|
||||
v-model="manualToken"
|
||||
label="手动加入 Token (留空使用配置的 Token)"
|
||||
clearable
|
||||
label="手动加入 Token (留空使用配置的 Token)"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col
|
||||
class="d-flex align-center"
|
||||
cols="12"
|
||||
md="4"
|
||||
class="d-flex align-center"
|
||||
>
|
||||
<v-btn
|
||||
color="primary"
|
||||
class="mr-2"
|
||||
color="primary"
|
||||
@click="handleJoinToken(manualToken || currentToken)"
|
||||
>
|
||||
加入
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="warning"
|
||||
class="mr-2"
|
||||
:disabled="!joinedToken"
|
||||
class="mr-2"
|
||||
color="warning"
|
||||
@click="handleLeaveToken(joinedToken)"
|
||||
>
|
||||
离开当前
|
||||
@ -83,24 +83,24 @@
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-divider class="my-4" />
|
||||
<v-divider class="my-4"/>
|
||||
<v-row>
|
||||
<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-text>
|
||||
<v-textarea
|
||||
v-model="chatInput"
|
||||
label="发送到当前已加入的设备频道"
|
||||
rows="2"
|
||||
auto-grow
|
||||
clearable
|
||||
label="发送到当前已加入的设备频道"
|
||||
rows="2"
|
||||
/>
|
||||
<div class="d-flex">
|
||||
<v-spacer />
|
||||
<v-spacer/>
|
||||
<v-btn
|
||||
color="primary"
|
||||
:disabled="!canSendChat"
|
||||
color="primary"
|
||||
@click="sendChat"
|
||||
>
|
||||
发送聊天
|
||||
@ -128,8 +128,8 @@
|
||||
<v-card-title>在线设备</v-card-title>
|
||||
<v-card-text>
|
||||
<v-btn
|
||||
color="primary"
|
||||
class="mb-3"
|
||||
color="primary"
|
||||
@click="fetchOnline"
|
||||
>
|
||||
刷新在线列表
|
||||
@ -178,11 +178,11 @@
|
||||
<v-card border>
|
||||
<v-card-title class="d-flex align-center">
|
||||
事件日志
|
||||
<v-spacer />
|
||||
<v-spacer/>
|
||||
<v-btn
|
||||
color="error"
|
||||
size="small"
|
||||
variant="text"
|
||||
color="error"
|
||||
@click="clearLogs"
|
||||
>
|
||||
清空
|
||||
@ -214,8 +214,8 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onBeforeUnmount, computed } from 'vue'
|
||||
import { getSetting } from '@/utils/settings'
|
||||
import {ref, onMounted, onBeforeUnmount, computed} from 'vue'
|
||||
import {getSetting} from '@/utils/settings'
|
||||
import {
|
||||
getSocket,
|
||||
on as socketOn,
|
||||
@ -262,17 +262,17 @@ function wireSocketBaseEvents() {
|
||||
s.on('connect', () => {
|
||||
connected.value = true
|
||||
socketId.value = s.id || ''
|
||||
pushLog('connect', { id: s.id })
|
||||
pushLog('connect', {id: s.id})
|
||||
// re-join with token if set
|
||||
if (joinedToken.value) joinToken(joinedToken.value)
|
||||
})
|
||||
s.on('disconnect', (reason) => {
|
||||
connected.value = false
|
||||
pushLog('disconnect', { reason })
|
||||
pushLog('disconnect', {reason})
|
||||
})
|
||||
s.on('connect_error', (err) => pushLog('connect_error', { message: err?.message }))
|
||||
s.on('reconnect_attempt', (n) => pushLog('reconnect_attempt', { attempt: n }))
|
||||
s.on('reconnect', (n) => pushLog('reconnect', { attempt: n }))
|
||||
s.on('connect_error', (err) => pushLog('connect_error', {message: err?.message}))
|
||||
s.on('reconnect_attempt', (n) => pushLog('reconnect_attempt', {attempt: n}))
|
||||
s.on('reconnect', (n) => pushLog('reconnect', {attempt: n}))
|
||||
}
|
||||
|
||||
function wireBusinessEvents() {
|
||||
@ -306,7 +306,7 @@ function handleJoinToken(token) {
|
||||
}
|
||||
joinToken(token)
|
||||
joinedToken.value = token
|
||||
pushLog('join-token', { token })
|
||||
pushLog('join-token', {token})
|
||||
} catch (e) {
|
||||
pushLog('join-token-error', String(e))
|
||||
}
|
||||
@ -316,7 +316,7 @@ function handleLeaveToken(token) {
|
||||
try {
|
||||
leaveToken(token)
|
||||
if (joinedToken.value === token) joinedToken.value = ''
|
||||
pushLog('leave-token', { token })
|
||||
pushLog('leave-token', {token})
|
||||
} catch (e) {
|
||||
pushLog('leave-token-error', String(e))
|
||||
}
|
||||
@ -353,7 +353,7 @@ function sendChat() {
|
||||
const s = getSocket()
|
||||
// send as plain string per server contract
|
||||
s.emit('chat:send', text)
|
||||
pushLog('chat:send', { text })
|
||||
pushLog('chat:send', {text})
|
||||
chatInput.value = ''
|
||||
} catch (e) {
|
||||
pushLog('chat:error', String(e))
|
||||
@ -373,7 +373,7 @@ async function fetchOnline() {
|
||||
const resp = await fetch(`${serverUrl.value}/devices/online`)
|
||||
const data = await resp.json()
|
||||
onlineDevices.value = Array.isArray(data?.devices) ? data.devices : []
|
||||
pushLog('fetch-online', { count: onlineDevices.value.length })
|
||||
pushLog('fetch-online', {count: onlineDevices.value.length})
|
||||
} catch (e) {
|
||||
pushLog('fetch-online-error', String(e))
|
||||
}
|
||||
|
||||
@ -11,12 +11,12 @@
|
||||
<v-app-bar-title class="text-h6">
|
||||
编辑考试配置
|
||||
</v-app-bar-title>
|
||||
<v-spacer />
|
||||
<v-spacer/>
|
||||
<v-btn
|
||||
color="success"
|
||||
variant="outlined"
|
||||
prepend-icon="mdi-content-save"
|
||||
:loading="saving"
|
||||
color="success"
|
||||
prepend-icon="mdi-content-save"
|
||||
variant="outlined"
|
||||
@click="save"
|
||||
>
|
||||
保存
|
||||
@ -31,8 +31,8 @@
|
||||
v-if="id"
|
||||
ref="editor"
|
||||
:config-id="id"
|
||||
@saved="onSaved"
|
||||
@error="onError"
|
||||
@saved="onSaved"
|
||||
/>
|
||||
</v-container>
|
||||
</v-container>
|
||||
@ -43,7 +43,7 @@ import ExamConfigEditor from '@/components/ExamConfigEditor.vue'
|
||||
|
||||
export default {
|
||||
name: 'ExamEditorPage',
|
||||
components: { ExamConfigEditor },
|
||||
components: {ExamConfigEditor},
|
||||
data() {
|
||||
return {
|
||||
id: this.$route.params.id,
|
||||
|
||||
@ -1,30 +1,30 @@
|
||||
<template>
|
||||
<v-alert
|
||||
v-if="error"
|
||||
type="error"
|
||||
variant="tonal"
|
||||
border="start"
|
||||
class="mb-4"
|
||||
closable
|
||||
type="error"
|
||||
variant="tonal"
|
||||
@click:close="error = ''"
|
||||
>
|
||||
{{ error }}
|
||||
</v-alert>
|
||||
|
||||
<v-skeleton-loader v-if="loading" type="article" />
|
||||
<v-skeleton-loader v-if="loading" type="article"/>
|
||||
|
||||
<div v-else-if="!config">
|
||||
<v-alert type="warning" variant="tonal" border="start">
|
||||
<v-alert border="start" type="warning" variant="tonal">
|
||||
缺少配置,请通过 URL 参数 id 或 url 传入配置。
|
||||
</v-alert>
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<div class="player" ref="playerRef">
|
||||
<div ref="playerRef" class="player">
|
||||
<ExamPlayer
|
||||
v-model:room-number="roomNumberLocal"
|
||||
:exam-config="config"
|
||||
:config="playerConfigObj"
|
||||
:exam-config="config"
|
||||
:show-action-bar="true"
|
||||
:time-sync-status="'电脑时间'"
|
||||
@exit="exit()"
|
||||
@ -34,14 +34,14 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, computed, onMounted, watch } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import {ref, computed, onMounted, watch} from "vue";
|
||||
import {useRoute, useRouter} from "vue-router";
|
||||
import dataProvider from "@/utils/dataProvider";
|
||||
import { ExamPlayer } from "@examaware-cs/player";
|
||||
import {ExamPlayer} from "@examaware-cs/player";
|
||||
|
||||
export default {
|
||||
name: "ExamPlayerPage",
|
||||
components: { ExamPlayer },
|
||||
components: {ExamPlayer},
|
||||
setup() {
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
@ -103,11 +103,11 @@ export default {
|
||||
// ExamAware 需要 examInfos: { name, start, end, alertTime? }
|
||||
examInfos: Array.isArray(raw?.examInfos)
|
||||
? raw.examInfos.map((i) => ({
|
||||
name: i?.name || "未命名科目",
|
||||
start: i?.start || "",
|
||||
end: i?.end || "",
|
||||
alertTime: typeof i?.alertTime === "number" ? i.alertTime : 15,
|
||||
}))
|
||||
name: i?.name || "未命名科目",
|
||||
start: i?.start || "",
|
||||
end: i?.end || "",
|
||||
alertTime: typeof i?.alertTime === "number" ? i.alertTime : 15,
|
||||
}))
|
||||
: [],
|
||||
};
|
||||
}
|
||||
@ -143,7 +143,7 @@ export default {
|
||||
@font-face {
|
||||
font-family: "TCloudNumber";
|
||||
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-style: normal;
|
||||
font-display: swap;
|
||||
@ -156,8 +156,8 @@ body,
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: "MiSans", MiSans, -apple-system, BlinkMacSystemFont, "Segoe UI",
|
||||
Roboto, "Helvetica Neue", Arial, "Noto Sans", "Liberation Sans",
|
||||
"PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
|
||||
Roboto, "Helvetica Neue", Arial, "Noto Sans", "Liberation Sans",
|
||||
"PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
|
||||
}
|
||||
|
||||
/* 设置主题为深色 */
|
||||
|
||||
@ -2,9 +2,9 @@
|
||||
<v-container class="fill-height">
|
||||
<v-row>
|
||||
<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-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-subtitle>
|
||||
@ -14,11 +14,11 @@
|
||||
<!-- 错误提示 -->
|
||||
<v-alert
|
||||
v-if="error"
|
||||
type="error"
|
||||
class="mb-4 mt-3 mx-2"
|
||||
variant="tonal"
|
||||
border="start"
|
||||
class="mb-4 mt-3 mx-2"
|
||||
closable
|
||||
type="error"
|
||||
variant="tonal"
|
||||
@click:close="error = ''"
|
||||
>
|
||||
<div class="d-flex align-center">
|
||||
@ -30,11 +30,11 @@
|
||||
<!-- 成功提示 -->
|
||||
<v-alert
|
||||
v-if="success"
|
||||
type="success"
|
||||
class="mb-4 mt-3 mx-2"
|
||||
variant="tonal"
|
||||
border="start"
|
||||
class="mb-4 mt-3 mx-2"
|
||||
closable
|
||||
type="success"
|
||||
variant="tonal"
|
||||
@click:close="success = ''"
|
||||
>
|
||||
<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 align-center">
|
||||
<v-btn
|
||||
class="mr-2"
|
||||
color="primary"
|
||||
prepend-icon="mdi-plus"
|
||||
class="mr-2"
|
||||
@click="createNewConfig"
|
||||
>
|
||||
新建配置
|
||||
</v-btn>
|
||||
<v-btn
|
||||
:loading="loading"
|
||||
color="info"
|
||||
prepend-icon="mdi-refresh"
|
||||
:loading="loading"
|
||||
@click="loadConfigs"
|
||||
variant="outlined"
|
||||
@click="loadConfigs"
|
||||
>
|
||||
刷新
|
||||
</v-btn>
|
||||
@ -77,8 +77,8 @@
|
||||
<v-card v-if="loading" class="my-4" outlined>
|
||||
<v-card-text>
|
||||
<v-skeleton-loader
|
||||
type="list-item-avatar-two-line@3"
|
||||
class="mx-auto"
|
||||
type="list-item-avatar-two-line@3"
|
||||
></v-skeleton-loader>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
@ -98,7 +98,7 @@
|
||||
@click="showEditDialog(config)"
|
||||
>
|
||||
<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-avatar>
|
||||
</template>
|
||||
@ -108,11 +108,11 @@
|
||||
</v-list-item-title>
|
||||
<v-list-item-subtitle class="text-caption mt-1">
|
||||
<div class="d-flex align-center">
|
||||
<v-icon size="small" class="mr-1">mdi-information-outline</v-icon>
|
||||
<v-icon class="mr-1" size="small">mdi-information-outline</v-icon>
|
||||
{{ config.message || '无描述' }}
|
||||
</div>
|
||||
<div class="d-flex align-center mt-1">
|
||||
<v-icon size="small" class="mr-1">mdi-book-multiple</v-icon>
|
||||
<v-icon class="mr-1" size="small">mdi-book-multiple</v-icon>
|
||||
{{ config.examInfos ? config.examInfos.length : 0 }} 堂考试
|
||||
</div>
|
||||
</v-list-item-subtitle>
|
||||
@ -120,22 +120,22 @@
|
||||
<template #append>
|
||||
<div class="d-flex align-center">
|
||||
<v-btn
|
||||
class="mr-1"
|
||||
color="primary"
|
||||
icon="mdi-pencil"
|
||||
size="small"
|
||||
color="primary"
|
||||
variant="text"
|
||||
class="mr-1"
|
||||
@click="showEditDialog(config)"
|
||||
>
|
||||
<v-icon>mdi-pencil</v-icon>
|
||||
</v-btn>
|
||||
|
||||
<v-btn
|
||||
class="mr-1"
|
||||
color="info"
|
||||
icon="mdi-eye"
|
||||
size="small"
|
||||
color="info"
|
||||
variant="text"
|
||||
class="mr-1"
|
||||
@click="showEditDialog(config)"
|
||||
>
|
||||
<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-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
|
||||
</v-icon>
|
||||
<h3 class="text-h6 mb-2 text-grey-darken-1">暂无配置</h3>
|
||||
@ -176,16 +176,16 @@
|
||||
<v-dialog v-model="renameDialog" max-width="500">
|
||||
<v-card>
|
||||
<v-card-title class="d-flex align-center">
|
||||
<v-icon color="primary" class="mr-2">mdi-rename-box</v-icon>
|
||||
<v-icon class="mr-2" color="primary">mdi-rename-box</v-icon>
|
||||
重命名配置
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<v-text-field
|
||||
v-model="newConfigName"
|
||||
:rules="[v => !!v || '配置名称不能为空']"
|
||||
label="配置名称"
|
||||
prepend-inner-icon="mdi-calendar-text"
|
||||
variant="outlined"
|
||||
:rules="[v => !!v || '配置名称不能为空']"
|
||||
@keyup.enter="renameConfig"
|
||||
></v-text-field>
|
||||
</v-card-text>
|
||||
@ -199,11 +199,11 @@
|
||||
取消
|
||||
</v-btn>
|
||||
<v-btn
|
||||
:disabled="!newConfigName"
|
||||
:loading="renaming"
|
||||
color="primary"
|
||||
variant="outlined"
|
||||
:loading="renaming"
|
||||
@click="renameConfig"
|
||||
:disabled="!newConfigName"
|
||||
>
|
||||
确认
|
||||
</v-btn>
|
||||
@ -215,22 +215,22 @@
|
||||
<v-dialog v-model="editDialog" max-width="1200" persistent>
|
||||
<v-card>
|
||||
<v-card-title class="d-flex align-center primary lighten-1 white--text py-3 px-4">
|
||||
<v-icon color="white" class="mr-2">mdi-pencil</v-icon>
|
||||
<v-icon class="mr-2" color="white">mdi-pencil</v-icon>
|
||||
编辑考试配置
|
||||
<v-spacer></v-spacer>
|
||||
<v-chip
|
||||
v-if="editingConfig"
|
||||
color="white"
|
||||
text-color="primary"
|
||||
size="small"
|
||||
class="mr-2"
|
||||
color="white"
|
||||
size="small"
|
||||
text-color="primary"
|
||||
>
|
||||
ID: {{ editingConfig.id }}
|
||||
</v-chip>
|
||||
|
||||
<v-btn
|
||||
icon="mdi-close"
|
||||
color="white"
|
||||
icon="mdi-close"
|
||||
variant="text"
|
||||
@click="closeEditDialog"
|
||||
>
|
||||
@ -238,33 +238,33 @@
|
||||
</v-btn>
|
||||
</v-card-title>
|
||||
<v-card-text class="pa-4"
|
||||
style="max-height: 70vh; overflow-y: auto;">
|
||||
style="max-height: 70vh; overflow-y: auto;">
|
||||
<ExamConfigEditor
|
||||
v-if="editingConfig"
|
||||
:config-id="editingConfig.id"
|
||||
ref="configEditor"
|
||||
:config-id="editingConfig.id"
|
||||
:dialog-mode="true"
|
||||
@saved="onConfigSaved"
|
||||
@deleted="onConfigDeleted"
|
||||
@error="onConfigError"
|
||||
@opened="onConfigOpened"
|
||||
@deleted="onConfigDeleted"
|
||||
@saved="onConfigSaved"
|
||||
/>
|
||||
</v-card-text>
|
||||
<v-card-actions class="pa-4">
|
||||
<v-btn
|
||||
color="grey"
|
||||
variant="outlined"
|
||||
prepend-icon="mdi-close"
|
||||
variant="outlined"
|
||||
@click="closeEditDialog"
|
||||
>
|
||||
关闭
|
||||
</v-btn>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
color="success"
|
||||
variant="outlined"
|
||||
prepend-icon="mdi-content-save"
|
||||
:loading="saving"
|
||||
color="success"
|
||||
prepend-icon="mdi-content-save"
|
||||
variant="outlined"
|
||||
@click="saveConfigInDialog"
|
||||
>
|
||||
保存配置
|
||||
@ -277,7 +277,7 @@ style="max-height: 70vh; overflow-y: auto;">
|
||||
|
||||
<script>
|
||||
import dataProvider from '@/utils/dataProvider'
|
||||
import { getSetting } from '@/utils/settings'
|
||||
import {getSetting} from '@/utils/settings'
|
||||
import ExamConfigEditor from '@/components/ExamConfigEditor.vue'
|
||||
|
||||
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)
|
||||
|
||||
// 保存每个配置的详细信息
|
||||
for (let config of exampleConfigs) {
|
||||
const configData = { ...config }
|
||||
const configData = {...config}
|
||||
delete configData.id
|
||||
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)
|
||||
if (!listResponse) {
|
||||
throw new Error(listResponse.error?.message || '更新列表失败')
|
||||
@ -464,11 +464,6 @@ export default {
|
||||
},
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 显示重命名对话框
|
||||
*/
|
||||
@ -534,7 +529,6 @@ export default {
|
||||
},
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 在弹框中保存配置
|
||||
*/
|
||||
|
||||
@ -4,16 +4,16 @@
|
||||
{{ titleText }}
|
||||
</v-app-bar-title>
|
||||
|
||||
<v-spacer />
|
||||
<v-spacer/>
|
||||
|
||||
<template #append>
|
||||
<!-- 只读 Token 警告 -->
|
||||
<v-chip
|
||||
v-if="tokenDisplayInfo.readonly"
|
||||
color="warning"
|
||||
variant="tonal"
|
||||
class="mx-2"
|
||||
color="warning"
|
||||
prepend-icon="mdi-lock-alert"
|
||||
variant="tonal"
|
||||
>
|
||||
只读
|
||||
</v-chip>
|
||||
@ -21,25 +21,25 @@
|
||||
<!-- 学生名称显示 chip(始终蓝色) -->
|
||||
<v-chip
|
||||
v-if="tokenDisplayInfo.show"
|
||||
color="primary"
|
||||
variant="tonal"
|
||||
class="mx-2"
|
||||
prepend-icon="mdi-account"
|
||||
:style="{ cursor: tokenDisplayInfo.disabled ? 'default' : 'pointer' }"
|
||||
class="mx-2"
|
||||
color="primary"
|
||||
prepend-icon="mdi-account"
|
||||
variant="tonal"
|
||||
@click="handleTokenChipClick"
|
||||
>
|
||||
{{ tokenDisplayInfo.text }}
|
||||
</v-chip>
|
||||
|
||||
<v-btn icon="mdi-chat" variant="text" @click="isChatOpen = true" />
|
||||
<v-btn icon="mdi-chat" variant="text" @click="isChatOpen = true"/>
|
||||
<v-btn
|
||||
icon="mdi-bell"
|
||||
variant="text"
|
||||
:badge="unreadCount || undefined"
|
||||
:badge-color="unreadCount ? 'error' : undefined"
|
||||
icon="mdi-bell"
|
||||
variant="text"
|
||||
@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>
|
||||
</v-app-bar>
|
||||
<!-- 初始化选择卡片,仅在首页且需要授权时显示;不影响顶栏 -->
|
||||
@ -65,17 +65,17 @@
|
||||
<div
|
||||
v-for="item in sortedItems"
|
||||
:key="item.key"
|
||||
class="grid-item"
|
||||
:style="{
|
||||
'grid-row-end': `span ${item.rowSpan}`,
|
||||
order: item.order,
|
||||
}"
|
||||
class="grid-item"
|
||||
>
|
||||
<v-card
|
||||
border
|
||||
height="100%"
|
||||
class="glow-track"
|
||||
:class="{ 'glow-highlight': highlightedCards[item.key] }"
|
||||
border
|
||||
class="glow-track"
|
||||
height="100%"
|
||||
@click="!isEditingDisabled && openDialog(item.key)"
|
||||
@mousemove="handleMouseMove"
|
||||
@touchmove="handleTouchMove"
|
||||
@ -106,7 +106,7 @@
|
||||
:disabled="isEditingDisabled"
|
||||
@click="openDialog(subject.name)"
|
||||
>
|
||||
<v-icon start> mdi-plus </v-icon>
|
||||
<v-icon start> mdi-plus</v-icon>
|
||||
{{ subject.name }}
|
||||
</v-btn>
|
||||
</v-btn-group>
|
||||
@ -116,16 +116,16 @@
|
||||
<v-card
|
||||
v-for="subject in unusedSubjects"
|
||||
:key="subject.name"
|
||||
:disabled="isEditingDisabled"
|
||||
border
|
||||
class="empty-subject-card"
|
||||
:disabled="isEditingDisabled"
|
||||
@click="openDialog(subject.name)"
|
||||
>
|
||||
<v-card-title class="text-subtitle-1">
|
||||
{{ subject.name }}
|
||||
</v-card-title>
|
||||
<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>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
@ -134,10 +134,10 @@
|
||||
</div>
|
||||
<v-btn
|
||||
v-if="!state.synced"
|
||||
color="error"
|
||||
size="large"
|
||||
:loading="loading.upload"
|
||||
class="ml-2"
|
||||
color="error"
|
||||
size="large"
|
||||
@click="manualUpload"
|
||||
>
|
||||
上传
|
||||
@ -147,31 +147,31 @@
|
||||
</v-btn>
|
||||
<v-btn
|
||||
v-if="showRandomPickerButton"
|
||||
append-icon="mdi-dice-multiple"
|
||||
class="ml-2"
|
||||
color="amber"
|
||||
prepend-icon="mdi-account-question"
|
||||
append-icon="mdi-dice-multiple"
|
||||
size="large"
|
||||
class="ml-2"
|
||||
@click="openRandomPicker"
|
||||
>
|
||||
随机点名
|
||||
</v-btn>
|
||||
<v-btn
|
||||
v-if="showExamScheduleButton"
|
||||
class="ml-2"
|
||||
color="green"
|
||||
prepend-icon="mdi-calendar-check"
|
||||
size="large"
|
||||
class="ml-2"
|
||||
@click="$router.push('/examschedule')"
|
||||
>
|
||||
考试看板
|
||||
</v-btn>
|
||||
<v-btn
|
||||
v-if="showListCardButton"
|
||||
class="ml-2"
|
||||
color="primary-darken-1"
|
||||
prepend-icon="mdi-list-box"
|
||||
size="large"
|
||||
class="ml-2"
|
||||
@click="$router.push('/list')"
|
||||
>
|
||||
列表
|
||||
@ -182,8 +182,8 @@
|
||||
:prepend-icon="
|
||||
state.isFullscreen ? 'mdi-fullscreen-exit' : 'mdi-fullscreen'
|
||||
"
|
||||
size="large"
|
||||
class="ml-2"
|
||||
size="large"
|
||||
@click="toggleFullscreen"
|
||||
>
|
||||
{{ state.isFullscreen ? "退出全屏" : "全屏显示"
|
||||
@ -197,7 +197,7 @@
|
||||
variant="tonal"
|
||||
>
|
||||
<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-text class="text-body-2">
|
||||
@ -206,10 +206,10 @@
|
||||
</p>
|
||||
<p class="text-caption text-grey">
|
||||
*研究显示动态像素偏移技术可以修复屏幕坏点,起到保护屏幕的作用,数据来自实验室。<a
|
||||
href="https://patentscope.wipo.int/search/zh/detail.jsf?docId=CN232281523&_cid=P20-M8L0YX-67061-1"
|
||||
target="_blank"
|
||||
>专利号CN108648692
|
||||
</a>
|
||||
href="https://patentscope.wipo.int/search/zh/detail.jsf?docId=CN232281523&_cid=P20-M8L0YX-67061-1"
|
||||
target="_blank"
|
||||
>专利号CN108648692
|
||||
</a>
|
||||
</p>
|
||||
<p class="text-caption text-grey">
|
||||
*技术已自动适配您的设备,无需手动调整
|
||||
@ -220,71 +220,76 @@
|
||||
|
||||
<!-- 出勤统计区域 -->
|
||||
<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-ripple="{ class: `text-${['primary','secondary','info','success','warning','error'][Math.floor(Math.random()*6)]}` }"
|
||||
class="attendance-area no-select"
|
||||
cols="1"
|
||||
@click="setAttendanceArea()"
|
||||
>
|
||||
<h1>出勤</h1>
|
||||
<h2>
|
||||
<snap style="white-space: nowrap"> 应到 </snap>:
|
||||
<snap style="white-space: nowrap"> 应到</snap>
|
||||
:
|
||||
<snap style="white-space: nowrap">
|
||||
{{
|
||||
state.studentList.length -
|
||||
state.boardData.attendance.exclude.length
|
||||
state.studentList.length -
|
||||
state.boardData.attendance.exclude.length
|
||||
}}人
|
||||
</snap>
|
||||
</h2>
|
||||
<h2>
|
||||
<snap style="white-space: nowrap"> 实到 </snap>:
|
||||
<snap style="white-space: nowrap"> 实到</snap>
|
||||
:
|
||||
<snap style="white-space: nowrap">
|
||||
{{
|
||||
state.studentList.length -
|
||||
state.boardData.attendance.absent.length -
|
||||
state.boardData.attendance.late.length -
|
||||
state.boardData.attendance.exclude.length
|
||||
state.studentList.length -
|
||||
state.boardData.attendance.absent.length -
|
||||
state.boardData.attendance.late.length -
|
||||
state.boardData.attendance.exclude.length
|
||||
}}人
|
||||
</snap>
|
||||
</h2>
|
||||
<h2>
|
||||
<snap style="white-space: nowrap"> 请假 </snap>:
|
||||
<snap style="white-space: nowrap"> 请假</snap>
|
||||
:
|
||||
<snap style="white-space: nowrap">
|
||||
{{ state.boardData.attendance.absent.length }}人
|
||||
</snap>
|
||||
</h2>
|
||||
<h3
|
||||
class="gray-text"
|
||||
v-for="(name, index) in state.boardData.attendance.absent"
|
||||
:key="'absent-' + index"
|
||||
class="gray-text"
|
||||
>
|
||||
<span v-if="useDisplay().lgAndUp.value">{{ `${index + 1}. ` }}</span
|
||||
><span style="white-space: nowrap">{{ name }}</span>
|
||||
</h3>
|
||||
<h2>
|
||||
<snap style="white-space: nowrap">迟到</snap>:
|
||||
<snap style="white-space: nowrap">迟到</snap>
|
||||
:
|
||||
<snap style="white-space: nowrap">
|
||||
{{ state.boardData.attendance.late.length }}人
|
||||
</snap>
|
||||
</h2>
|
||||
<h3
|
||||
class="gray-text"
|
||||
v-for="(name, index) in state.boardData.attendance.late"
|
||||
:key="'late-' + index"
|
||||
class="gray-text"
|
||||
>
|
||||
<span v-if="useDisplay().lgAndUp.value">{{ `${index + 1}. ` }}</span
|
||||
><span style="white-space: nowrap">{{ name }}</span>
|
||||
</h3>
|
||||
<h2>
|
||||
<snap style="white-space: nowrap">不参与</snap>:
|
||||
<snap style="white-space: nowrap">不参与</snap>
|
||||
:
|
||||
<snap style="white-space: nowrap">
|
||||
{{ state.boardData.attendance.exclude.length }}人
|
||||
</snap>
|
||||
</h2>
|
||||
<h3
|
||||
class="gray-text"
|
||||
v-for="(name, index) in state.boardData.attendance.exclude"
|
||||
:key="'exclude-' + index"
|
||||
class="gray-text"
|
||||
>
|
||||
<span v-if="useDisplay().lgAndUp.value">{{ `${index + 1}. ` }}</span
|
||||
><span style="white-space: nowrap">{{ name }}</span>
|
||||
@ -294,9 +299,9 @@
|
||||
|
||||
<homework-edit-dialog
|
||||
v-model="state.dialogVisible"
|
||||
:title="state.dialogTitle"
|
||||
:initial-content="state.textarea"
|
||||
:auto-save="autoSave"
|
||||
:initial-content="state.textarea"
|
||||
:title="state.dialogTitle"
|
||||
@save="handleHomeworkSave"
|
||||
/>
|
||||
|
||||
@ -306,16 +311,16 @@
|
||||
|
||||
<v-dialog
|
||||
v-model="state.attendanceDialog"
|
||||
max-width="900"
|
||||
fullscreen-breakpoint="sm"
|
||||
max-width="900"
|
||||
@update:model-value="handleAttendanceDialogClose"
|
||||
>
|
||||
<v-card>
|
||||
<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-chip color="primary" size="small" class="ml-2">
|
||||
<v-spacer/>
|
||||
<v-chip class="ml-2" color="primary" size="small">
|
||||
{{ state.dateString }}
|
||||
</v-chip>
|
||||
</v-card-title>
|
||||
@ -326,11 +331,11 @@
|
||||
<v-col cols="12" md="12">
|
||||
<v-text-field
|
||||
v-model="attendanceSearch"
|
||||
prepend-inner-icon="mdi-magnify"
|
||||
label="搜索学生"
|
||||
hint="支持筛选姓氏,如输入'孙'可筛选所有姓孙的学生"
|
||||
variant="outlined"
|
||||
clearable
|
||||
hint="支持筛选姓氏,如输入'孙'可筛选所有姓孙的学生"
|
||||
label="搜索学生"
|
||||
prepend-inner-icon="mdi-magnify"
|
||||
variant="outlined"
|
||||
@update:model-value="handleSearchChange"
|
||||
/>
|
||||
|
||||
@ -339,10 +344,10 @@
|
||||
<v-btn
|
||||
v-for="surname in extractedSurnames"
|
||||
:key="surname.name"
|
||||
:color="attendanceSearch === surname.name ? 'primary' : ''"
|
||||
:variant="
|
||||
attendanceSearch === surname.name ? 'elevated' : 'text'
|
||||
"
|
||||
:color="attendanceSearch === surname.name ? 'primary' : ''"
|
||||
@click="
|
||||
attendanceSearch =
|
||||
attendanceSearch === surname.name ? '' : surname.name
|
||||
@ -359,63 +364,63 @@
|
||||
<div class="d-flex flex-wrap mb-4 gap-2">
|
||||
<div>
|
||||
<v-chip
|
||||
value="present"
|
||||
:append-icon="
|
||||
attendanceFilter.includes('present') ? 'mdi-check' : ''
|
||||
"
|
||||
:color="attendanceFilter.includes('present') ? 'success' : ''"
|
||||
:variant="
|
||||
attendanceFilter.includes('present') ? 'elevated' : 'tonal'
|
||||
"
|
||||
class="px-2 filter-chip"
|
||||
@click="toggleFilter('present')"
|
||||
prepend-icon="mdi-account-check"
|
||||
:append-icon="
|
||||
attendanceFilter.includes('present') ? 'mdi-check' : ''
|
||||
"
|
||||
value="present"
|
||||
@click="toggleFilter('present')"
|
||||
>
|
||||
到课
|
||||
</v-chip>
|
||||
|
||||
<v-chip
|
||||
value="absent"
|
||||
:append-icon="
|
||||
attendanceFilter.includes('absent') ? 'mdi-check' : ''
|
||||
"
|
||||
:color="attendanceFilter.includes('absent') ? 'error' : ''"
|
||||
:variant="
|
||||
attendanceFilter.includes('absent') ? 'elevated' : 'tonal'
|
||||
"
|
||||
class="px-2 filter-chip"
|
||||
@click="toggleFilter('absent')"
|
||||
prepend-icon="mdi-account-off"
|
||||
:append-icon="
|
||||
attendanceFilter.includes('absent') ? 'mdi-check' : ''
|
||||
"
|
||||
value="absent"
|
||||
@click="toggleFilter('absent')"
|
||||
>
|
||||
请假
|
||||
</v-chip>
|
||||
<v-chip
|
||||
value="late"
|
||||
:append-icon="
|
||||
attendanceFilter.includes('late') ? 'mdi-check' : ''
|
||||
"
|
||||
:color="attendanceFilter.includes('late') ? 'warning' : ''"
|
||||
:variant="
|
||||
attendanceFilter.includes('late') ? 'elevated' : 'tonal'
|
||||
"
|
||||
class="px-2 filter-chip"
|
||||
@click="toggleFilter('late')"
|
||||
prepend-icon="mdi-clock-alert"
|
||||
:append-icon="
|
||||
attendanceFilter.includes('late') ? 'mdi-check' : ''
|
||||
"
|
||||
value="late"
|
||||
@click="toggleFilter('late')"
|
||||
>
|
||||
迟到
|
||||
</v-chip>
|
||||
<v-chip
|
||||
value="exclude"
|
||||
:append-icon="
|
||||
attendanceFilter.includes('exclude') ? 'mdi-check' : ''
|
||||
"
|
||||
:color="attendanceFilter.includes('exclude') ? 'grey' : ''"
|
||||
:variant="
|
||||
attendanceFilter.includes('exclude') ? 'elevated' : 'tonal'
|
||||
"
|
||||
class="px-2 filter-chip"
|
||||
@click="toggleFilter('exclude')"
|
||||
prepend-icon="mdi-account-cancel"
|
||||
:append-icon="
|
||||
attendanceFilter.includes('exclude') ? 'mdi-check' : ''
|
||||
"
|
||||
value="exclude"
|
||||
@click="toggleFilter('exclude')"
|
||||
>
|
||||
不参与
|
||||
</v-chip>
|
||||
@ -428,11 +433,11 @@
|
||||
v-for="student in filteredStudents"
|
||||
:key="student"
|
||||
cols="12"
|
||||
sm="6"
|
||||
md="6"
|
||||
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">
|
||||
<div class="flex-grow-1">
|
||||
<div class="d-flex align-center">
|
||||
@ -442,12 +447,13 @@
|
||||
state.studentList.indexOf(student)
|
||||
)
|
||||
"
|
||||
size="24"
|
||||
class="mr-2"
|
||||
size="24"
|
||||
>
|
||||
<v-icon size="small">{{
|
||||
getStudentStatusIcon(state.studentList.indexOf(student))
|
||||
}}</v-icon>
|
||||
}}
|
||||
</v-icon>
|
||||
</v-avatar>
|
||||
<div class="text-subtitle-1">{{ student }}</div>
|
||||
</div>
|
||||
@ -459,11 +465,11 @@
|
||||
? 'success'
|
||||
: ''
|
||||
"
|
||||
:title="'设为到课'"
|
||||
icon="mdi-account-check"
|
||||
size="small"
|
||||
variant="text"
|
||||
@click="setPresent(state.studentList.indexOf(student))"
|
||||
:title="'设为到课'"
|
||||
/>
|
||||
<v-btn
|
||||
:color="
|
||||
@ -471,11 +477,11 @@
|
||||
? 'error'
|
||||
: ''
|
||||
"
|
||||
:title="'设为请假'"
|
||||
icon="mdi-account-off"
|
||||
size="small"
|
||||
variant="text"
|
||||
@click="setAbsent(state.studentList.indexOf(student))"
|
||||
:title="'设为请假'"
|
||||
/>
|
||||
<v-btn
|
||||
:color="
|
||||
@ -483,11 +489,11 @@
|
||||
? 'warning'
|
||||
: ''
|
||||
"
|
||||
:title="'设为迟到'"
|
||||
icon="mdi-clock-alert"
|
||||
size="small"
|
||||
variant="text"
|
||||
@click="setLate(state.studentList.indexOf(student))"
|
||||
:title="'设为迟到'"
|
||||
/>
|
||||
<v-btn
|
||||
:color="
|
||||
@ -495,11 +501,11 @@
|
||||
? 'grey'
|
||||
: ''
|
||||
"
|
||||
:title="'设为不参与'"
|
||||
icon="mdi-account-cancel"
|
||||
size="small"
|
||||
variant="text"
|
||||
@click="setExclude(state.studentList.indexOf(student))"
|
||||
:title="'设为不参与'"
|
||||
/>
|
||||
</div>
|
||||
</v-card-text>
|
||||
@ -508,7 +514,7 @@
|
||||
</v-row>
|
||||
<v-row>
|
||||
<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>
|
||||
<div class="text-subtitle-2 mb-2">批量操作</div>
|
||||
<v-btn-group>
|
||||
@ -545,14 +551,15 @@
|
||||
</v-btn-group>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col></v-row
|
||||
</v-col>
|
||||
</v-row
|
||||
>
|
||||
</v-card-text>
|
||||
|
||||
<v-divider />
|
||||
<v-divider/>
|
||||
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-spacer/>
|
||||
|
||||
<v-btn color="primary" @click="saveAttendance">
|
||||
<v-icon start>mdi-content-save</v-icon>
|
||||
@ -562,16 +569,16 @@
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<message-log ref="messageLog" />
|
||||
<message-log ref="messageLog"/>
|
||||
|
||||
<!-- 添加悬浮工具栏 -->
|
||||
<floating-toolbar
|
||||
:loading="loading.download"
|
||||
:unread-count="unreadCount"
|
||||
:selected-date="state.selectedDateObj"
|
||||
:is-today="isToday"
|
||||
@zoom="zoom"
|
||||
:loading="loading.download"
|
||||
:selected-date="state.selectedDateObj"
|
||||
:unread-count="unreadCount"
|
||||
@refresh="downloadData"
|
||||
@zoom="zoom"
|
||||
@open-messages="$refs.messageLog.drawer = true"
|
||||
@open-settings="$router.push('/settings')"
|
||||
@date-select="handleDateSelect"
|
||||
@ -580,24 +587,24 @@
|
||||
/>
|
||||
|
||||
<!-- 添加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-card>
|
||||
<v-card-title class="text-h6"> 确认保存 </v-card-title>
|
||||
<v-card-title class="text-h6"> 确认保存</v-card-title>
|
||||
<v-card-text>
|
||||
您正在修改 {{ state.dateString }} 的数据,确定要保存吗?
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-spacer/>
|
||||
<v-btn color="grey" variant="text" @click="confirmDialog.reject">
|
||||
取消
|
||||
</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>
|
||||
</v-dialog>
|
||||
@ -605,14 +612,14 @@
|
||||
<!-- 添加随机点名组件 -->
|
||||
<random-picker
|
||||
ref="randomPicker"
|
||||
:student-list="state.studentList"
|
||||
:attendance="state.boardData.attendance"
|
||||
:student-list="state.studentList"
|
||||
/>
|
||||
|
||||
<!-- 添加URL配置确认对话框 -->
|
||||
<v-dialog v-model="urlConfigDialog.show" max-width="500">
|
||||
<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>
|
||||
<p>以下配置将应用于当前班级:</p>
|
||||
<v-list density="compact">
|
||||
@ -621,17 +628,18 @@
|
||||
:key="change.key"
|
||||
>
|
||||
<template #prepend>
|
||||
<v-icon :icon="change.icon" size="small" class="mr-2" />
|
||||
<v-icon :icon="change.icon" class="mr-2" size="small"/>
|
||||
</template>
|
||||
<v-list-item-title class="d-flex align-center">
|
||||
<span class="text-subtitle-1">{{ change.name }}</span>
|
||||
<v-tooltip activator="parent" location="top">{{
|
||||
change.description || change.key
|
||||
}}</v-tooltip>
|
||||
}}
|
||||
</v-tooltip>
|
||||
</v-list-item-title>
|
||||
<v-list-item-subtitle>
|
||||
<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">{{
|
||||
change.newValue
|
||||
}}</span>
|
||||
@ -640,7 +648,7 @@
|
||||
</v-list>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-spacer/>
|
||||
<v-btn
|
||||
color="grey"
|
||||
variant="text"
|
||||
@ -652,8 +660,10 @@
|
||||
确认应用
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card> </v-dialog
|
||||
><br /><br /><br />
|
||||
</v-card>
|
||||
</v-dialog
|
||||
>
|
||||
<br/><br/><br/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@ -672,14 +682,14 @@ import {
|
||||
setSetting,
|
||||
settingsDefinitions,
|
||||
} from "@/utils/settings";
|
||||
import { kvServerProvider } from "@/utils/providers/kvServerProvider";
|
||||
import { useDisplay } from "vuetify";
|
||||
import {kvServerProvider} from "@/utils/providers/kvServerProvider";
|
||||
import {useDisplay} from "vuetify";
|
||||
import "../styles/index.scss";
|
||||
import "../styles/transitions.scss";
|
||||
import "../styles/global.scss";
|
||||
import { pinyin } from "pinyin-pro";
|
||||
import { debounce, throttle } from "@/utils/debounce";
|
||||
import { Base64 } from "js-base64";
|
||||
import {pinyin} from "pinyin-pro";
|
||||
import {debounce, throttle} from "@/utils/debounce";
|
||||
import {Base64} from "js-base64";
|
||||
import {
|
||||
getSocket,
|
||||
on as socketOn,
|
||||
@ -702,16 +712,16 @@ export default {
|
||||
},
|
||||
data() {
|
||||
const defaultSubjects = [
|
||||
{ name: "语文", order: 0 },
|
||||
{ name: "数学", order: 1 },
|
||||
{ name: "英语", order: 2 },
|
||||
{ name: "物理", order: 3 },
|
||||
{ name: "化学", order: 4 },
|
||||
{ name: "生物", order: 5 },
|
||||
{ name: "政治", order: 6 },
|
||||
{ name: "历史", order: 7 },
|
||||
{ name: "地理", order: 8 },
|
||||
{ name: "其他", order: 9 },
|
||||
{name: "语文", order: 0},
|
||||
{name: "数学", order: 1},
|
||||
{name: "英语", order: 2},
|
||||
{name: "物理", order: 3},
|
||||
{name: "化学", order: 4},
|
||||
{name: "生物", order: 5},
|
||||
{name: "政治", order: 6},
|
||||
{name: "历史", order: 7},
|
||||
{name: "地理", order: 8},
|
||||
{name: "其他", order: 9},
|
||||
];
|
||||
|
||||
return {
|
||||
@ -738,7 +748,7 @@ export default {
|
||||
dateString: "",
|
||||
synced: false,
|
||||
attendDialogVisible: false,
|
||||
contentStyle: { "font-size": `${getSetting("font.size")}px` },
|
||||
contentStyle: {"font-size": `${getSetting("font.size")}px`},
|
||||
uploadLoading: false,
|
||||
downloadLoading: false,
|
||||
snackbar: false,
|
||||
@ -822,7 +832,7 @@ export default {
|
||||
// 优先展示当前设备名称(如果已从云端获取)
|
||||
const deviceName =
|
||||
this.state.namespaceInfo?.device?.name ||
|
||||
this.state.classNumber ||
|
||||
this.state.classNumber ||
|
||||
"高三八班";
|
||||
|
||||
const today = this.getToday();
|
||||
@ -834,7 +844,7 @@ export default {
|
||||
const yesterdayStr = this.formatDate(yesterday);
|
||||
|
||||
if (currentDateStr === todayStr) {
|
||||
return deviceName +" - 今天的作业";
|
||||
return deviceName + " - 今天的作业";
|
||||
} else if (currentDateStr === yesterdayStr) {
|
||||
return deviceName + " - 昨天的作业";
|
||||
} else {
|
||||
@ -861,7 +871,7 @@ export default {
|
||||
rowSpan: Math.ceil(
|
||||
(value.content.split("\n").filter((line) => line.trim()).length +
|
||||
1) *
|
||||
0.8
|
||||
0.8
|
||||
),
|
||||
}));
|
||||
|
||||
@ -1011,10 +1021,10 @@ export default {
|
||||
});
|
||||
|
||||
return Array.from(surnameMap.entries())
|
||||
.map(([name, count]) => ({ name, count }))
|
||||
.map(([name, count]) => ({name, count}))
|
||||
.sort((a, b) => {
|
||||
const pinyinA = pinyin(a.name, { toneType: "none", mode: "surname" });
|
||||
const pinyinB = pinyin(b.name, { toneType: "none", mode: "surname" });
|
||||
const pinyinA = pinyin(a.name, {toneType: "none", mode: "surname"});
|
||||
const pinyinB = pinyin(b.name, {toneType: "none", mode: "surname"});
|
||||
return pinyinA.localeCompare(pinyinB);
|
||||
});
|
||||
},
|
||||
@ -1300,7 +1310,7 @@ export default {
|
||||
if (forceClear || !this.state.boardData || (!this.state.boardData.homework && !this.state.boardData.attendance)) {
|
||||
this.state.boardData = {
|
||||
homework: {},
|
||||
attendance: { absent: [], late: [], exclude: [] },
|
||||
attendance: {absent: [], late: [], exclude: []},
|
||||
};
|
||||
}
|
||||
} else {
|
||||
@ -1327,7 +1337,7 @@ export default {
|
||||
if (forceClear || !this.state.boardData || (!this.state.boardData.homework && !this.state.boardData.attendance)) {
|
||||
this.state.boardData = {
|
||||
homework: {},
|
||||
attendance: { absent: [], late: [], exclude: [] },
|
||||
attendance: {absent: [], late: [], exclude: []},
|
||||
};
|
||||
}
|
||||
} finally {
|
||||
@ -1586,7 +1596,7 @@ export default {
|
||||
|
||||
updateSettings() {
|
||||
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.updateBackendUrl();
|
||||
// 设置更新时尝试刷新设备名称(例如 Token 或域名变更)
|
||||
@ -1611,9 +1621,10 @@ export default {
|
||||
|
||||
this.$router
|
||||
.replace({
|
||||
query: { date: formattedDate },
|
||||
query: {date: formattedDate},
|
||||
})
|
||||
.catch(() => {});
|
||||
.catch(() => {
|
||||
});
|
||||
|
||||
// Load both data and subjects in parallel, force clear data when switching dates
|
||||
await Promise.all([this.downloadData(true), this.loadSubjects()]);
|
||||
@ -1628,7 +1639,7 @@ export default {
|
||||
const maxColumns = Math.min(3, Math.floor(window.innerWidth / 300));
|
||||
if (maxColumns <= 1) return items;
|
||||
|
||||
const columns = Array.from({ length: maxColumns }, () => ({
|
||||
const columns = Array.from({length: maxColumns}, () => ({
|
||||
height: 0,
|
||||
items: [],
|
||||
}));
|
||||
@ -1756,7 +1767,7 @@ export default {
|
||||
|
||||
isPresent(index) {
|
||||
const student = this.state.studentList[index];
|
||||
const { absent, late, exclude } = this.state.boardData.attendance;
|
||||
const {absent, late, exclude} = this.state.boardData.attendance;
|
||||
return (
|
||||
!absent.includes(student) &&
|
||||
!late.includes(student) &&
|
||||
|
||||
@ -1,260 +1,263 @@
|
||||
<template><v-app-bar elevation="1">
|
||||
<template #prepend>
|
||||
<v-btn
|
||||
icon="mdi-arrow-left"
|
||||
variant="text"
|
||||
@click="$router.push('/')"
|
||||
/>
|
||||
</template>
|
||||
<v-app-bar-title class="text-h6" v-if="list && !isRenaming">{{ list.name }}</v-app-bar-title>
|
||||
<v-app-bar-title class="text-h6" v-else>列表</v-app-bar-title>
|
||||
</v-app-bar>
|
||||
<template>
|
||||
<v-app-bar elevation="1">
|
||||
<template #prepend>
|
||||
<v-btn
|
||||
icon="mdi-arrow-left"
|
||||
variant="text"
|
||||
@click="$router.push('/')"
|
||||
/>
|
||||
</template>
|
||||
<v-app-bar-title v-if="list && !isRenaming" class="text-h6">{{ list.name }}</v-app-bar-title>
|
||||
<v-app-bar-title v-else class="text-h6">列表</v-app-bar-title>
|
||||
</v-app-bar>
|
||||
<v-container>
|
||||
|
||||
<div class="d-flex align-center mb-4">
|
||||
<v-btn
|
||||
icon
|
||||
class="mr-2"
|
||||
to="/list"
|
||||
border
|
||||
>
|
||||
<v-icon>mdi-arrow-left</v-icon>
|
||||
<div class="d-flex align-center mb-4">
|
||||
<v-btn
|
||||
border
|
||||
class="mr-2"
|
||||
icon
|
||||
to="/list"
|
||||
>
|
||||
<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>
|
||||
<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>
|
||||
<h1 v-else>
|
||||
加载中...
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
|
||||
<v-card class="mb-5" border rounded="xl">
|
||||
<v-card-title class="d-flex align-center">
|
||||
项目列表
|
||||
<v-spacer />
|
||||
<v-btn-toggle
|
||||
v-model="sortType"
|
||||
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-card border class="mb-5" rounded="xl">
|
||||
<v-card-title class="d-flex align-center">
|
||||
项目列表
|
||||
<v-spacer/>
|
||||
<v-btn-toggle
|
||||
v-model="sortType"
|
||||
mandatory
|
||||
>
|
||||
<v-list-item
|
||||
v-for="(item, index) in sortedItems"
|
||||
:key="item.id"
|
||||
:class="{ 'text-decoration-line-through': item.completed }"
|
||||
@click="openItemDetails(item)"
|
||||
>
|
||||
<template #prepend>
|
||||
<v-list-item-action start>
|
||||
<v-checkbox-btn
|
||||
:model-value="item.completed"
|
||||
@update:model-value="updateItemStatus(item.id, $event)"
|
||||
@click.stop
|
||||
/>
|
||||
</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
|
||||
color="error"
|
||||
prepend-icon="mdi-delete-sweep"
|
||||
@click="confirmDeleteCompleted"
|
||||
:disabled="!hasCompletedItems"
|
||||
>
|
||||
删除已完成项目
|
||||
<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-for="(item, index) in sortedItems"
|
||||
:key="item.id"
|
||||
:class="{ 'text-decoration-line-through': item.completed }"
|
||||
@click="openItemDetails(item)"
|
||||
>
|
||||
<template #prepend>
|
||||
<v-list-item-action start>
|
||||
<v-checkbox-btn
|
||||
:model-value="item.completed"
|
||||
@update:model-value="updateItemStatus(item.id, $event)"
|
||||
@click.stop
|
||||
/>
|
||||
</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-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-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-text-field
|
||||
v-model="sortSeed"
|
||||
label="排序种子 (任意数字或文本)"
|
||||
hint="输入相同的种子值可以得到相同的排序结果"
|
||||
persistent-hint
|
||||
class="mb-3"
|
||||
/>
|
||||
<v-btn
|
||||
color="primary"
|
||||
class="mr-2"
|
||||
@click="randomSort"
|
||||
>
|
||||
随机排序
|
||||
</v-btn>
|
||||
<v-btn
|
||||
variant="text"
|
||||
@click="resetSort"
|
||||
>
|
||||
撤销
|
||||
</v-btn>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
<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-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-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"
|
||||
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 color="error" variant="text" @click="confirmDelete">
|
||||
确认删除
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</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>
|
||||
</template>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
</v-container>
|
||||
</template>
|
||||
@ -544,7 +547,7 @@ export default {
|
||||
|
||||
// 移除临时的randomValue属性并更新items
|
||||
this.items = itemsWithRandom.map((item) => {
|
||||
const newItem = { ...item };
|
||||
const newItem = {...item};
|
||||
delete newItem.randomValue;
|
||||
return newItem;
|
||||
});
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
<template><v-app-bar elevation="1">
|
||||
<template>
|
||||
<v-app-bar elevation="1">
|
||||
<template #prepend>
|
||||
<v-btn
|
||||
icon="mdi-arrow-left"
|
||||
@ -7,9 +8,8 @@
|
||||
/>
|
||||
</template>
|
||||
<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">
|
||||
@ -21,8 +21,8 @@
|
||||
<v-list-item
|
||||
v-for="list in lists"
|
||||
:key="list.id"
|
||||
:to="list.id !== editingListId ? `/list/${list.id}` : undefined"
|
||||
:active="list.id === editingListId"
|
||||
:to="list.id !== editingListId ? `/list/${list.id}` : undefined"
|
||||
>
|
||||
<div v-if="list.id !== editingListId">
|
||||
<v-list-item-title>{{ list.name }}</v-list-item-title>
|
||||
@ -30,27 +30,27 @@
|
||||
<div v-else class="d-flex align-center w-100">
|
||||
<v-text-field
|
||||
v-model="editListName"
|
||||
label="列表名称"
|
||||
hide-details
|
||||
density="compact"
|
||||
class="mr-2"
|
||||
autofocus
|
||||
class="mr-2"
|
||||
density="compact"
|
||||
hide-details
|
||||
label="列表名称"
|
||||
@keyup.enter="saveListName"
|
||||
></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-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-btn>
|
||||
</div>
|
||||
|
||||
<template #append>
|
||||
<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-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-btn>
|
||||
</div>
|
||||
@ -58,15 +58,15 @@
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</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-text>
|
||||
<v-text-field
|
||||
v-model="newListName"
|
||||
label="列表名称"
|
||||
:rules="[v => !!v || '名称不能为空']"
|
||||
label="列表名称"
|
||||
></v-text-field>
|
||||
<v-btn color="primary" @click="createNewList" :disabled="!newListName">
|
||||
<v-btn :disabled="!newListName" color="primary" @click="createNewList">
|
||||
创建列表
|
||||
</v-btn>
|
||||
</v-card-text>
|
||||
@ -87,11 +87,12 @@
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</v-container>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import dataProvider from "@/utils/dataProvider.js";
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
|
||||
@ -27,11 +27,11 @@
|
||||
<v-list-item
|
||||
v-for="tab in settingsTabs"
|
||||
:key="tab.value"
|
||||
@click="settingsTab = tab.value"
|
||||
:active="settingsTab === tab.value"
|
||||
:color="settingsTab === tab.value ? 'primary' : 'default'"
|
||||
:prepend-icon="tab.icon"
|
||||
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>
|
||||
@ -40,11 +40,13 @@
|
||||
|
||||
<v-tabs-window
|
||||
v-model="settingsTab"
|
||||
style="width: 100%"
|
||||
direction="vertical"
|
||||
style="width: 100%"
|
||||
>
|
||||
<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>
|
||||
<div class="card-title">
|
||||
<div>
|
||||
@ -58,10 +60,10 @@
|
||||
<v-card-text>
|
||||
<div class="mt-4">
|
||||
<v-btn
|
||||
variant="text"
|
||||
class="text-none"
|
||||
append-icon="mdi-arrow-right"
|
||||
class="text-none"
|
||||
rounded="xl"
|
||||
variant="text"
|
||||
@click="openClassworksKV"
|
||||
>
|
||||
打开 Classworks KV
|
||||
@ -69,126 +71,137 @@
|
||||
</div>
|
||||
</v-card-text>
|
||||
</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-alert
|
||||
color="error"
|
||||
variant="tonal"
|
||||
icon="mdi-alert-circle"
|
||||
class="rounded-xl"
|
||||
>Classworks
|
||||
color="error"
|
||||
icon="mdi-alert-circle"
|
||||
variant="tonal"
|
||||
>Classworks
|
||||
是开源免费的软件,官方没有提供任何形式的付费支持服务,源代码仓库地址在
|
||||
<a
|
||||
href="https://github.com/ZeroCatDev/Classworks"
|
||||
target="_blank"
|
||||
>https://github.com/ZeroCatDev/Classworks</a
|
||||
>。如果您通过有偿协助等付费方式取得本应用,在遇到问题时请在与卖家约定的服务框架下,优先向卖家求助。如果卖家没有提供您预期的服务,请退款或通过其它形式积极维护您的合法权益。</v-alert
|
||||
>https://github.com/ZeroCatDev/Classworks</a
|
||||
>。如果您通过有偿协助等付费方式取得本应用,在遇到问题时请在与卖家约定的服务框架下,优先向卖家求助。如果卖家没有提供您预期的服务,请退款或通过其它形式积极维护您的合法权益。
|
||||
</v-alert
|
||||
>
|
||||
<v-alert
|
||||
class="mt-4 rounded-xl"
|
||||
color="info"
|
||||
variant="tonal"
|
||||
icon="mdi-information"
|
||||
>请不要使用浏览器清除缓存功能,否则会导致配置丢失。<del
|
||||
>恶意的操作可能导致您受到贵校教师的处理</del
|
||||
></v-alert
|
||||
variant="tonal"
|
||||
>请不要使用浏览器清除缓存功能,否则会导致配置丢失。
|
||||
<del
|
||||
>恶意的操作可能导致您受到贵校教师的处理
|
||||
</del
|
||||
>
|
||||
</v-alert
|
||||
>
|
||||
<v-alert
|
||||
class="mt-4 rounded-xl"
|
||||
color="warning"
|
||||
variant="tonal"
|
||||
icon="mdi-information"
|
||||
><p>
|
||||
请不要使用包括但不限于360极速浏览器、360安全浏览器、夸克浏览器、QQ浏览器等浏览器使用
|
||||
Classworks
|
||||
,这些浏览器过时且存在严重的一致性问题。在Windows上,使用新版
|
||||
Microsoft Edge 浏览器是最推荐的选择。
|
||||
</p>
|
||||
variant="tonal"
|
||||
><p>
|
||||
请不要使用包括但不限于360极速浏览器、360安全浏览器、夸克浏览器、QQ浏览器等浏览器使用
|
||||
Classworks
|
||||
,这些浏览器过时且存在严重的一致性问题。在Windows上,使用新版
|
||||
Microsoft Edge 浏览器是最推荐的选择。
|
||||
</p>
|
||||
<p style="color: #666">
|
||||
上述浏览器商标为其所属公司所有,Classworks™
|
||||
与上述浏览器所属公司无竞争关系。
|
||||
</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"
|
||||
target="_blank"
|
||||
color="warning"
|
||||
variant="tonal"
|
||||
class="text-none rounded-xl"
|
||||
append-icon="mdi-open-in-new"
|
||||
>下载 Microsoft Edge(微软边缘浏览器)</v-btn
|
||||
></v-alert
|
||||
>下载 Microsoft Edge(微软边缘浏览器)
|
||||
</v-btn
|
||||
>
|
||||
</v-alert
|
||||
>
|
||||
</v-card-text>
|
||||
</v-card><about-card />
|
||||
</v-card>
|
||||
<about-card/>
|
||||
</v-tabs-window-item>
|
||||
|
||||
<v-tabs-window-item value="server">
|
||||
<server-settings-card
|
||||
border
|
||||
:loading="loading.server"
|
||||
border
|
||||
@saved="onSettingsSaved"
|
||||
/>
|
||||
<data-provider-settings-card border class="mt-4" />
|
||||
<kv-database-card border class="mt-4" />
|
||||
<data-provider-settings-card border class="mt-4"/>
|
||||
<kv-database-card border class="mt-4"/>
|
||||
</v-tabs-window-item>
|
||||
|
||||
<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 value="share">
|
||||
<settings-link-generator border class="mt-4" />
|
||||
<settings-link-generator border class="mt-4"/>
|
||||
</v-tabs-window-item>
|
||||
|
||||
<v-tabs-window-item value="refresh">
|
||||
<refresh-settings-card
|
||||
border
|
||||
:loading="loading.refresh"
|
||||
border
|
||||
@saved="onSettingsSaved"
|
||||
/>
|
||||
</v-tabs-window-item>
|
||||
|
||||
<v-tabs-window-item value="edit">
|
||||
<edit-settings-card
|
||||
border
|
||||
:loading="loading.edit"
|
||||
border
|
||||
@saved="onSettingsSaved"
|
||||
/>
|
||||
</v-tabs-window-item>
|
||||
|
||||
<v-tabs-window-item value="display">
|
||||
<display-settings-card
|
||||
border
|
||||
:loading="loading.display"
|
||||
border
|
||||
@saved="onSettingsSaved"
|
||||
/>
|
||||
</v-tabs-window-item>
|
||||
|
||||
<v-tabs-window-item value="theme">
|
||||
<theme-settings-card
|
||||
border
|
||||
:loading="loading.theme"
|
||||
border
|
||||
@saved="onSettingsSaved"
|
||||
/>
|
||||
</v-tabs-window-item>
|
||||
|
||||
<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 value="subject">
|
||||
<subject-management-card border /> <br />
|
||||
<homework-template-card border />
|
||||
<subject-management-card border/>
|
||||
<br/>
|
||||
<homework-template-card border/>
|
||||
</v-tabs-window-item>
|
||||
|
||||
<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-item>
|
||||
<template #prepend>
|
||||
<v-icon icon="mdi-code-tags" class="mr-3" />
|
||||
<v-icon class="mr-3" icon="mdi-code-tags"/>
|
||||
</template>
|
||||
<v-list-item-title>启用开发者选项</v-list-item-title>
|
||||
<v-list-item-subtitle
|
||||
>启用后可以查看和修改开发者设置</v-list-item-subtitle
|
||||
>启用后可以查看和修改开发者设置
|
||||
</v-list-item-subtitle
|
||||
>
|
||||
<template #append>
|
||||
<v-switch
|
||||
@ -202,39 +215,39 @@
|
||||
</v-list>
|
||||
</settings-card>
|
||||
<developer-settings-card
|
||||
border
|
||||
:loading="loading.developer"
|
||||
border
|
||||
@saved="onSettingsSaved"
|
||||
/>
|
||||
<template v-if="settings.developer.enabled">
|
||||
<v-card border class="mt-4 rounded-lg">
|
||||
<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-subtitle> 浏览和修改所有可用设置 </v-card-subtitle>
|
||||
<v-card-subtitle> 浏览和修改所有可用设置</v-card-subtitle>
|
||||
<v-card-text>
|
||||
<settings-explorer @update="onSettingUpdate" />
|
||||
<settings-explorer @update="onSettingUpdate"/>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</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 value="about">
|
||||
<about-card />
|
||||
<echo-chamber-card border class="mt-4" />
|
||||
<about-card/>
|
||||
<echo-chamber-card border class="mt-4"/>
|
||||
</v-tabs-window-item>
|
||||
</v-tabs-window>
|
||||
</v-container>
|
||||
|
||||
<!-- 消息记录组件 -->
|
||||
<message-log ref="messageLog" />
|
||||
<message-log ref="messageLog"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { useDisplay } from "vuetify";
|
||||
import {useDisplay} from "vuetify";
|
||||
import ServerSettingsCard from "@/components/settings/cards/ServerSettingsCard.vue";
|
||||
import EditSettingsCard from "@/components/settings/cards/EditSettingsCard.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 SubjectManagementCard from "@/components/settings/cards/SubjectManagementCard.vue";
|
||||
import KvDatabaseCard from "@/components/settings/cards/KvDatabaseCard.vue";
|
||||
|
||||
export default {
|
||||
name: "Settings",
|
||||
components: {
|
||||
@ -281,8 +295,8 @@ export default {
|
||||
KvDatabaseCard,
|
||||
},
|
||||
setup() {
|
||||
const { mobile } = useDisplay();
|
||||
return { isMobile: mobile };
|
||||
const {mobile} = useDisplay();
|
||||
return {isMobile: mobile};
|
||||
},
|
||||
data() {
|
||||
const provider = getSetting("server.provider");
|
||||
@ -326,8 +340,8 @@ export default {
|
||||
return {
|
||||
settings,
|
||||
dataProviders: [
|
||||
{ title: "服务器", value: "server" },
|
||||
{ title: "本地数据库", value: "indexedDB" },
|
||||
{title: "服务器", value: "server"},
|
||||
{title: "本地数据库", value: "indexedDB"},
|
||||
],
|
||||
studentData: {
|
||||
list: [],
|
||||
@ -642,6 +656,7 @@ export default {
|
||||
.settings-page {
|
||||
.v-card {
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1) !important;
|
||||
}
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
# 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.
|
||||
|
||||
@ -9,7 +9,7 @@ import vuetify from './vuetify'
|
||||
import pinia from '@/stores'
|
||||
import router from '@/router'
|
||||
|
||||
export function registerPlugins (app) {
|
||||
export function registerPlugins(app) {
|
||||
app
|
||||
.use(vuetify)
|
||||
.use(router)
|
||||
|
||||
@ -9,7 +9,7 @@ import '@mdi/font/css/materialdesignicons.css'
|
||||
import 'vuetify/styles'
|
||||
|
||||
// Composables
|
||||
import { createVuetify } from 'vuetify'
|
||||
import {createVuetify} from 'vuetify'
|
||||
|
||||
// https://vuetifyjs.com/en/introduction/why-vuetify/#feature-guides
|
||||
export default createVuetify({
|
||||
|
||||
@ -5,9 +5,9 @@
|
||||
*/
|
||||
|
||||
// Composables
|
||||
import { createRouter, createWebHistory } from 'vue-router/auto'
|
||||
import { setupLayouts } from 'virtual:generated-layouts'
|
||||
import { routes } from 'vue-router/auto-routes'
|
||||
import {createRouter, createWebHistory} from 'vue-router/auto'
|
||||
import {setupLayouts} from 'virtual:generated-layouts'
|
||||
import {routes} from 'vue-router/auto-routes'
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
// Utilities
|
||||
import { defineStore } from 'pinia'
|
||||
import {defineStore} from 'pinia'
|
||||
|
||||
export const useAppStore = defineStore('app', {
|
||||
state: () => ({
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Utilities
|
||||
import { createPinia } from 'pinia'
|
||||
import {createPinia} from 'pinia'
|
||||
|
||||
export default createPinia()
|
||||
|
||||
@ -4,20 +4,20 @@
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||
|
||||
|
||||
.v-card-title {
|
||||
font-size: 1.25rem;
|
||||
padding: 16px 20px;
|
||||
}
|
||||
|
||||
|
||||
.v-card-text {
|
||||
padding: 16px 20px;
|
||||
}
|
||||
|
||||
|
||||
.v-card-actions {
|
||||
padding: 12px 20px;
|
||||
}
|
||||
|
||||
|
||||
&:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
@ -27,7 +27,7 @@
|
||||
.glow-card {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
@ -35,14 +35,14 @@
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: radial-gradient(circle at var(--x, 50%) var(--y, 50%),
|
||||
rgba(255, 255, 255, 0.2) 0%,
|
||||
rgba(255, 255, 255, 0) 60%);
|
||||
background: radial-gradient(circle at var(--x, 50%) var(--y, 50%),
|
||||
rgba(255, 255, 255, 0.2) 0%,
|
||||
rgba(255, 255, 255, 0) 60%);
|
||||
opacity: 0;
|
||||
transition: opacity 0.5s;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
|
||||
&:hover::after {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@ -42,8 +42,8 @@
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: radial-gradient(circle at var(--x, 50%) var(--y, 50%),
|
||||
rgba(255, 255, 255, 0.2) 0%,
|
||||
rgba(255, 255, 255, 0) 60%);
|
||||
rgba(255, 255, 255, 0.2) 0%,
|
||||
rgba(255, 255, 255, 0) 60%);
|
||||
opacity: 0;
|
||||
transition: opacity 0.5s;
|
||||
pointer-events: none;
|
||||
|
||||
@ -1,6 +1,3 @@
|
||||
|
||||
|
||||
|
||||
// 添加卡片发光效果
|
||||
.glow-track {
|
||||
position: relative;
|
||||
@ -15,8 +12,8 @@
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: radial-gradient(circle at var(--x, 50%) var(--y, 50%),
|
||||
rgba(255, 255, 255, 0.15) 0%,
|
||||
rgba(255, 255, 255, 0) 70%);
|
||||
rgba(255, 255, 255, 0.15) 0%,
|
||||
rgba(255, 255, 255, 0) 70%);
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s;
|
||||
pointer-events: none;
|
||||
@ -32,20 +29,20 @@
|
||||
.glow-highlight {
|
||||
animation: glow-pulse 3s ease-in-out;
|
||||
box-shadow: 0 0 20px rgba(33, 150, 243, 0.6),
|
||||
0 0 40px rgba(33, 150, 243, 0.4),
|
||||
0 0 60px rgba(33, 150, 243, 0.2) !important;
|
||||
0 0 40px rgba(33, 150, 243, 0.4),
|
||||
0 0 60px rgba(33, 150, 243, 0.2) !important;
|
||||
}
|
||||
|
||||
@keyframes glow-pulse {
|
||||
0%, 100% {
|
||||
box-shadow: 0 0 20px rgba(33, 150, 243, 0.6),
|
||||
0 0 40px rgba(33, 150, 243, 0.4),
|
||||
0 0 60px rgba(33, 150, 243, 0.2);
|
||||
0 0 40px rgba(33, 150, 243, 0.4),
|
||||
0 0 60px rgba(33, 150, 243, 0.2);
|
||||
}
|
||||
50% {
|
||||
box-shadow: 0 0 30px rgba(33, 150, 243, 0.8),
|
||||
0 0 60px rgba(33, 150, 243, 0.6),
|
||||
0 0 90px rgba(33, 150, 243, 0.4);
|
||||
0 0 60px rgba(33, 150, 243, 0.6),
|
||||
0 0 90px rgba(33, 150, 243, 0.4);
|
||||
}
|
||||
}
|
||||
|
||||
@ -140,175 +137,176 @@
|
||||
transform: scale(0.95);
|
||||
}
|
||||
}
|
||||
.grid-masonry {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 16px;
|
||||
padding: 8px;
|
||||
grid-auto-flow: dense;
|
||||
}
|
||||
|
||||
.grid-item {
|
||||
width: 100%;
|
||||
transition: all 0.2s ease;
|
||||
.grid-masonry {
|
||||
display: grid;
|
||||
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 {
|
||||
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;
|
||||
}
|
||||
/* 优化滚动条样式 */
|
||||
.main-window::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
@media (max-width: 1199px) {
|
||||
.grid-masonry {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
.main-window::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
@media (max-width: 799px) {
|
||||
.grid-masonry {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.main-window::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(0, 0, 0, 0.2);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.empty-card {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
}
|
||||
.main-window::-webkit-scrollbar-thumb:hover {
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
/* 优化滚动条样式 */
|
||||
.main-window::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
.no-data-message {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 200px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.main-window::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
.attendance-drawer {
|
||||
border-left: 1px solid rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.main-window::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(0, 0, 0, 0.2);
|
||||
border-radius: 4px;
|
||||
}
|
||||
.attendance-drawer :deep(.v-navigation-drawer__content) {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.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 {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 200px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
.attendance-drawer
|
||||
:deep(.v-navigation-drawer__content::-webkit-scrollbar-track) {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.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 {
|
||||
border-left: 1px solid rgba(0, 0, 0, 0.12);
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.attendance-drawer :deep(.v-navigation-drawer__content) {
|
||||
overflow-y: auto;
|
||||
}
|
||||
.text-success {
|
||||
color: rgb(var(--v-theme-success));
|
||||
}
|
||||
|
||||
/* 优化滚动条样式 */
|
||||
.attendance-drawer :deep(.v-navigation-drawer__content::-webkit-scrollbar) {
|
||||
width: 8px;
|
||||
}
|
||||
.text-error {
|
||||
color: rgb(var(--v-theme-error));
|
||||
}
|
||||
|
||||
.attendance-drawer
|
||||
:deep(.v-navigation-drawer__content::-webkit-scrollbar-track) {
|
||||
background: transparent;
|
||||
}
|
||||
.text-warning {
|
||||
color: rgb(var(--v-theme-warning));
|
||||
}
|
||||
|
||||
.attendance-drawer
|
||||
:deep(.v-navigation-drawer__content::-webkit-scrollbar-thumb) {
|
||||
background-color: rgba(0, 0, 0, 0.2);
|
||||
border-radius: 4px;
|
||||
}
|
||||
.attendance-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.attendance-drawer
|
||||
:deep(.v-navigation-drawer__content::-webkit-scrollbar-thumb:hover) {
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
.attendance-numbers {
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
/* 响应式调整 */
|
||||
@media (max-width: 960px) {
|
||||
.attendance-drawer {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
.total-number {
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.12);
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
.text-success {
|
||||
color: rgb(var(--v-theme-success));
|
||||
}
|
||||
.status-number {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.text-error {
|
||||
color: rgb(var(--v-theme-error));
|
||||
}
|
||||
.text-h2,
|
||||
.text-h3 {
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.text-warning {
|
||||
color: rgb(var(--v-theme-warning));
|
||||
}
|
||||
.empty-subjects-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
gap: 16px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.attendance-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.empty-subject-card {
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.attendance-numbers {
|
||||
padding: 20px 0;
|
||||
}
|
||||
.empty-subject-card:hover {
|
||||
transform: scale(1.02);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.total-number {
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.12);
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
.empty-subjects {
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.12);
|
||||
padding-top: 1rem;
|
||||
}
|
||||
|
||||
.status-number {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
.empty-subject-card:not(:disabled):hover {
|
||||
opacity: 1;
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
// 出勤管理对话框样式
|
||||
.attendance-stat {
|
||||
|
||||
@ -10,72 +10,72 @@
|
||||
// );
|
||||
|
||||
.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 {
|
||||
background-color: rgb(var(--v-theme-primary), 0.05);
|
||||
.v-col {
|
||||
padding: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
transition: opacity 0.2s ease;
|
||||
opacity: 0;
|
||||
}
|
||||
.student-card.mobile {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.gap-1 {
|
||||
gap: 4px;
|
||||
}
|
||||
.student-card.mobile .v-btn {
|
||||
min-width: 40px;
|
||||
min-height: 40px;
|
||||
}
|
||||
|
||||
.gap-2 {
|
||||
gap: 8px;
|
||||
}
|
||||
.student-card.mobile .v-text-field {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.student-card .v-text-field {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
@media (max-width: 600px) {
|
||||
.v-col {
|
||||
padding: 6px !important;
|
||||
}
|
||||
|
||||
.student-card {
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.student-card:active {
|
||||
background-color: rgb(var(--v-theme-primary), 0.05);
|
||||
}
|
||||
.action-buttons {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.student-card {
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
.student-card:active {
|
||||
background-color: rgb(var(--v-theme-primary), 0.05);
|
||||
}
|
||||
@ -8,13 +8,13 @@ $standard-accelerate: cubic-bezier(0.3, 0.0, 1.0, 1.0);
|
||||
// 网格项目的进入和离开动画
|
||||
.grid-item {
|
||||
transition: transform 400ms $emphasized-decelerate,
|
||||
opacity 200ms $standard-easing;
|
||||
opacity 200ms $standard-easing;
|
||||
will-change: transform, opacity;
|
||||
backface-visibility: hidden;
|
||||
|
||||
&.v-enter-active {
|
||||
transition: transform 400ms $emphasized-decelerate,
|
||||
opacity 250ms $standard-easing;
|
||||
opacity 250ms $standard-easing;
|
||||
}
|
||||
|
||||
&.v-move {
|
||||
@ -25,7 +25,7 @@ $standard-accelerate: cubic-bezier(0.3, 0.0, 1.0, 1.0);
|
||||
&.v-leave-active {
|
||||
position: absolute !important;
|
||||
transition: transform 300ms $emphasized-accelerate,
|
||||
opacity 200ms $standard-accelerate;
|
||||
opacity 200ms $standard-accelerate;
|
||||
}
|
||||
|
||||
&.v-enter-from,
|
||||
@ -122,20 +122,20 @@ $standard-accelerate: cubic-bezier(0.3, 0.0, 1.0, 1.0);
|
||||
// 保持对话框本身的过渡动画
|
||||
.v-dialog-transition-enter-active {
|
||||
transition: transform 400ms $emphasized-decelerate,
|
||||
opacity 300ms $standard-easing;
|
||||
opacity 300ms $standard-easing;
|
||||
}
|
||||
|
||||
.v-dialog-transition-leave-active {
|
||||
transition: transform 250ms $emphasized-accelerate,
|
||||
opacity 200ms $standard-accelerate;
|
||||
opacity 200ms $standard-accelerate;
|
||||
}
|
||||
|
||||
// 按钮状态变化动画
|
||||
.v-btn {
|
||||
transition: background-color 250ms $standard-easing,
|
||||
transform 150ms $emphasized-decelerate;
|
||||
transform 150ms $emphasized-decelerate;
|
||||
touch-action: manipulation;
|
||||
min-height: 40px; // 确保触摸目标足够大
|
||||
min-height: 40px; // 确保触摸目标足够大
|
||||
min-width: 40px;
|
||||
|
||||
&:active {
|
||||
|
||||
@ -16,11 +16,19 @@
|
||||
}
|
||||
|
||||
@keyframes pulse-warning {
|
||||
0%, 100% { transform: scale(1); }
|
||||
50% { transform: scale(1.002); }
|
||||
0%, 100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.002);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse-border {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
0%, 100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
24
src/sw.js
24
src/sw.js
@ -1,8 +1,8 @@
|
||||
import { precacheAndRoute, cleanupOutdatedCaches } from 'workbox-precaching'
|
||||
import { registerRoute, setCatchHandler } from 'workbox-routing'
|
||||
import { CacheFirst, NetworkFirst, StaleWhileRevalidate } from 'workbox-strategies'
|
||||
import { ExpirationPlugin } from 'workbox-expiration'
|
||||
import { CacheableResponsePlugin } from 'workbox-cacheable-response'
|
||||
import {precacheAndRoute, cleanupOutdatedCaches} from 'workbox-precaching'
|
||||
import {registerRoute, setCatchHandler} from 'workbox-routing'
|
||||
import {CacheFirst, NetworkFirst, StaleWhileRevalidate} from 'workbox-strategies'
|
||||
import {ExpirationPlugin} from 'workbox-expiration'
|
||||
import {CacheableResponsePlugin} from 'workbox-cacheable-response'
|
||||
|
||||
// 使用 self.__WB_MANIFEST 是 workbox 的一个特殊变量,会被实际的预缓存清单替换
|
||||
precacheAndRoute(self.__WB_MANIFEST)
|
||||
@ -81,7 +81,7 @@ registerRoute(
|
||||
|
||||
// 外部资源缓存
|
||||
registerRoute(
|
||||
({ url }) => url.origin !== self.location.origin,
|
||||
({url}) => url.origin !== self.location.origin,
|
||||
new NetworkFirst({
|
||||
cacheName: 'external-resources',
|
||||
plugins: [
|
||||
@ -102,7 +102,7 @@ self.addEventListener('message', (event) => {
|
||||
if (event.data && event.data.type === 'CACHE_KEYS') {
|
||||
// 获取所有缓存键
|
||||
caches.keys().then((cacheNames) => {
|
||||
event.ports[0].postMessage({ cacheNames });
|
||||
event.ports[0].postMessage({cacheNames});
|
||||
});
|
||||
} else if (event.data && event.data.type === 'CACHE_CONTENT') {
|
||||
// 获取特定缓存的内容
|
||||
@ -110,14 +110,14 @@ self.addEventListener('message', (event) => {
|
||||
caches.open(cacheName).then((cache) => {
|
||||
cache.keys().then((requests) => {
|
||||
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') {
|
||||
// 清除特定缓存
|
||||
const cacheName = event.data.cacheName;
|
||||
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') {
|
||||
// 清除特定URL的缓存
|
||||
@ -125,7 +125,7 @@ self.addEventListener('message', (event) => {
|
||||
const url = event.data.url;
|
||||
caches.open(cacheName).then((cache) => {
|
||||
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') {
|
||||
@ -134,8 +134,8 @@ self.addEventListener('message', (event) => {
|
||||
Promise.all(
|
||||
cacheNames.map(name => caches.delete(name))
|
||||
).then(() => {
|
||||
event.ports[0].postMessage({ success: true });
|
||||
event.ports[0].postMessage({success: true});
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
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
|
||||
const isValidProvider = () => {
|
||||
@ -9,7 +9,7 @@ const isValidProvider = () => {
|
||||
|
||||
// Helper function to get request headers with kvtoken
|
||||
const getHeaders = () => {
|
||||
const headers = { Accept: "application/json" };
|
||||
const headers = {Accept: "application/json"};
|
||||
const kvToken = getSetting("server.kvToken");
|
||||
const siteKey = getSetting("server.siteKey");
|
||||
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
import { kvLocalProvider } from "./providers/kvLocalProvider";
|
||||
import { kvServerProvider } from "./providers/kvServerProvider";
|
||||
import { getSetting, setSetting } from "./settings";
|
||||
import {kvLocalProvider} from "./providers/kvLocalProvider";
|
||||
import {kvServerProvider} from "./providers/kvServerProvider";
|
||||
import {getSetting, setSetting} from "./settings";
|
||||
|
||||
export const formatResponse = (data) => data;
|
||||
|
||||
export const formatError = (message, code = "UNKNOWN_ERROR") => ({
|
||||
success: false,
|
||||
error: { code, message },
|
||||
error: {code, message},
|
||||
});
|
||||
|
||||
// Main data provider with simplified API
|
||||
@ -256,7 +256,6 @@ export default {
|
||||
};
|
||||
|
||||
|
||||
|
||||
export const ErrorCodes = {
|
||||
NOT_FOUND: "数据不存在",
|
||||
NETWORK_ERROR: "网络连接失败",
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { getSetting } from './settings';
|
||||
import {getSetting} from './settings';
|
||||
|
||||
class LogDB {
|
||||
constructor() {
|
||||
@ -39,7 +39,7 @@ const defaultOptions = {
|
||||
};
|
||||
|
||||
async function createMessage(type, title, content = '', options = {}) {
|
||||
const msgOptions = { ...defaultOptions, ...options };
|
||||
const msgOptions = {...defaultOptions, ...options};
|
||||
const message = {
|
||||
id: Date.now() + Math.random(),
|
||||
type,
|
||||
@ -87,8 +87,12 @@ export default {
|
||||
warning: (title, content, options) => createMessage(MessageType.WARNING, title, content, options),
|
||||
};
|
||||
},
|
||||
onSnackbar: (callback) => { snackbarCallback = callback; },
|
||||
onLog: (callback) => { logCallback = callback; },
|
||||
onSnackbar: (callback) => {
|
||||
snackbarCallback = callback;
|
||||
},
|
||||
onLog: (callback) => {
|
||||
logCallback = callback;
|
||||
},
|
||||
getMessages: async () => {
|
||||
try {
|
||||
return await logDB.getLogs();
|
||||
@ -107,7 +111,8 @@ export default {
|
||||
}
|
||||
},
|
||||
MessageType,
|
||||
markAsRead: () => {}, // 移除标记已读功能
|
||||
markAsRead: () => {
|
||||
}, // 移除标记已读功能
|
||||
deleteMessage: async (messageId) => {
|
||||
try {
|
||||
await logDB.deleteLog(messageId);
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { openDB } from "idb";
|
||||
import { formatResponse, formatError } from "../dataProvider";
|
||||
import {openDB} from "idb";
|
||||
import {formatResponse, formatError} from "../dataProvider";
|
||||
|
||||
// Database initialization for local storage
|
||||
const DB_NAME = "ClassworksDB";
|
||||
@ -112,4 +112,4 @@ export const kvLocalProvider = {
|
||||
return formatError("获取本地键名列表失败:" + error.message);
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import axios from "@/axios/axios";
|
||||
import { formatResponse, formatError } from "../dataProvider";
|
||||
import { getSetting } from "../settings";
|
||||
import {formatResponse, formatError} from "../dataProvider";
|
||||
import {getSetting} from "../settings";
|
||||
|
||||
// Helper function to get request headers with kvtoken
|
||||
const getHeaders = () => {
|
||||
const headers = { Accept: "application/json" };
|
||||
const headers = {Accept: "application/json"};
|
||||
const kvToken = getSetting("server.kvToken");
|
||||
const siteKey = getSetting("server.siteKey");
|
||||
|
||||
|
||||
@ -556,8 +556,8 @@ class SettingsManagerClass {
|
||||
definition.type === "boolean"
|
||||
? Boolean(value)
|
||||
: definition.type === "number"
|
||||
? Number(value)
|
||||
: String(value);
|
||||
? Number(value)
|
||||
: String(value);
|
||||
}
|
||||
|
||||
// 验证
|
||||
@ -636,7 +636,8 @@ class SettingsManagerClass {
|
||||
* @returns {Function} 取消监听的函数
|
||||
*/
|
||||
watchSettings(callback) {
|
||||
if (typeof window === "undefined") return () => {};
|
||||
if (typeof window === "undefined") return () => {
|
||||
};
|
||||
|
||||
const handler = (event) => {
|
||||
if (event.key === SETTINGS_STORAGE_KEY) {
|
||||
|
||||
@ -2,8 +2,8 @@
|
||||
// - Uses server domain from settings when available
|
||||
// - Exposes join/leave helpers and event on/off wrappers
|
||||
|
||||
import { io } from 'socket.io-client';
|
||||
import { getSetting } from '@/utils/settings';
|
||||
import {io} from 'socket.io-client';
|
||||
import {getSetting} from '@/utils/settings';
|
||||
|
||||
let socket = null;
|
||||
let connectedDomain = null;
|
||||
@ -20,16 +20,18 @@ export function getSocket() {
|
||||
const serverUrl = getServerUrl();
|
||||
if (!socket || connectedDomain !== serverUrl) {
|
||||
if (socket) {
|
||||
try { socket.disconnect(); } catch (e) {
|
||||
try {
|
||||
socket.disconnect();
|
||||
} catch (e) {
|
||||
void e; // ignore
|
||||
}
|
||||
socket = null;
|
||||
}
|
||||
connectedDomain = serverUrl;
|
||||
socket = io(serverUrl, { transports: ['websocket'] });
|
||||
socket = io(serverUrl, {transports: ['websocket']});
|
||||
|
||||
// Re-attach previously registered event handlers on new socket instance
|
||||
listeners.forEach(({ event, handler }) => {
|
||||
listeners.forEach(({event, handler}) => {
|
||||
socket.on(event, handler);
|
||||
});
|
||||
}
|
||||
@ -39,7 +41,7 @@ export function getSocket() {
|
||||
export function on(event, handler) {
|
||||
const s = getSocket();
|
||||
s.on(event, handler);
|
||||
listeners.add({ event, handler });
|
||||
listeners.add({event, handler});
|
||||
return () => off(event, handler);
|
||||
}
|
||||
|
||||
@ -57,12 +59,12 @@ export function off(event, handler) {
|
||||
export function joinToken(token) {
|
||||
const s = getSocket();
|
||||
if (!token) return;
|
||||
s.emit('join-token', { token });
|
||||
s.emit('join-token', {token});
|
||||
}
|
||||
|
||||
export function leaveToken(token) {
|
||||
if (!socket) return;
|
||||
socket.emit('leave-token', { token });
|
||||
socket.emit('leave-token', {token});
|
||||
}
|
||||
|
||||
export function leaveAll() {
|
||||
@ -78,7 +80,9 @@ export function onConnect(handler) {
|
||||
|
||||
export function disconnect() {
|
||||
if (!socket) return;
|
||||
try { socket.disconnect(); } catch (e) {
|
||||
try {
|
||||
socket.disconnect();
|
||||
} catch (e) {
|
||||
void e; // ignore
|
||||
}
|
||||
socket = null;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user