首次提交
This commit is contained in:
+8
-2
@@ -2,8 +2,14 @@
|
||||
<v-app>
|
||||
<!-- 正常路由 -->
|
||||
<router-view v-slot="{ Component, route }">
|
||||
<transition mode="out-in" name="md3">
|
||||
<component :is="Component" :key="route.path" />
|
||||
<transition
|
||||
mode="out-in"
|
||||
name="md3"
|
||||
>
|
||||
<component
|
||||
:is="Component"
|
||||
:key="route.path"
|
||||
/>
|
||||
</transition>
|
||||
</router-view>
|
||||
<global-message />
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.8 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 367 B |
Binary file not shown.
|
Before Width: | Height: | Size: 12 KiB |
@@ -1,25 +0,0 @@
|
||||
<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.4 KiB |
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<v-app-bar :elevation="2">
|
||||
<template v-slot:prepend>
|
||||
<v-app-bar-nav-icon></v-app-bar-nav-icon>
|
||||
<template #prepend>
|
||||
<v-app-bar-nav-icon />
|
||||
</template>
|
||||
|
||||
<v-app-bar-title>作业</v-app-bar-title>
|
||||
|
||||
@@ -2,48 +2,85 @@
|
||||
<v-card>
|
||||
<v-card-title class="d-flex align-center">
|
||||
<span>缓存管理</span>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn :loading="loading" color="error" @click="clearAllCaches">
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
:loading="loading"
|
||||
color="error"
|
||||
@click="clearAllCaches"
|
||||
>
|
||||
清除所有缓存
|
||||
</v-btn>
|
||||
<v-btn class="ml-2" icon @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" class="mb-4" type="warning">
|
||||
<v-alert
|
||||
v-if="!serviceWorkerActive"
|
||||
class="mb-4"
|
||||
type="warning"
|
||||
>
|
||||
Service Worker 未激活,缓存管理功能不可用。
|
||||
</v-alert>
|
||||
|
||||
<v-alert v-if="message" :type="messageType" class="mb-4">
|
||||
<v-alert
|
||||
v-if="message"
|
||||
:type="messageType"
|
||||
class="mb-4"
|
||||
>
|
||||
{{ message }}
|
||||
</v-alert>
|
||||
|
||||
<v-expansion-panels v-if="caches.length > 0">
|
||||
<v-expansion-panel v-for="cache in caches" :key="cache.name">
|
||||
<v-expansion-panel
|
||||
v-for="cache in caches"
|
||||
:key="cache.name"
|
||||
>
|
||||
<v-expansion-panel-title>
|
||||
<div class="d-flex align-center">
|
||||
<span>{{ formatCacheName(cache.name) }}</span>
|
||||
<v-chip class="ml-2" size="small">{{ cache.urls.length }} 个文件</v-chip>
|
||||
<v-chip
|
||||
class="ml-2"
|
||||
size="small"
|
||||
>
|
||||
{{ cache.urls.length }} 个文件
|
||||
</v-chip>
|
||||
</div>
|
||||
</v-expansion-panel-title>
|
||||
<v-expansion-panel-text>
|
||||
<div class="d-flex justify-end mb-2">
|
||||
<v-btn :loading="loading" color="error" size="small" @click="clearCache(cache.name)">
|
||||
<v-btn
|
||||
:loading="loading"
|
||||
color="error"
|
||||
size="small"
|
||||
@click="clearCache(cache.name)"
|
||||
>
|
||||
清除此缓存
|
||||
</v-btn>
|
||||
</div>
|
||||
<v-list lines="two">
|
||||
<v-list-item v-for="(url, index) in cache.urls" :key="index">
|
||||
<v-list-item
|
||||
v-for="(url, index) in cache.urls"
|
||||
:key="index"
|
||||
>
|
||||
<v-list-item-title class="text-truncate">
|
||||
{{ getFileName(url) }}
|
||||
</v-list-item-title>
|
||||
<v-list-item-subtitle class="text-truncate">
|
||||
{{ url }}
|
||||
</v-list-item-subtitle>
|
||||
<template v-slot:append>
|
||||
<v-btn color="error" icon size="small" @click="clearUrl(cache.name, url)">
|
||||
<template #append>
|
||||
<v-btn
|
||||
color="error"
|
||||
icon
|
||||
size="small"
|
||||
@click="clearUrl(cache.name, url)"
|
||||
>
|
||||
<v-icon>mdi-delete</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
@@ -53,9 +90,15 @@
|
||||
</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">
|
||||
<v-alert
|
||||
v-else
|
||||
type="info"
|
||||
>
|
||||
没有找到缓存数据。
|
||||
</v-alert>
|
||||
</v-card-text>
|
||||
|
||||
+314
-127
@@ -11,7 +11,9 @@
|
||||
@click:close="error = ''"
|
||||
>
|
||||
<div class="d-flex align-center">
|
||||
<v-icon class="mr-2">mdi-alert-circle</v-icon>
|
||||
<v-icon class="mr-2">
|
||||
mdi-alert-circle
|
||||
</v-icon>
|
||||
{{ error }}
|
||||
</div>
|
||||
</v-alert>
|
||||
@@ -27,7 +29,9 @@
|
||||
@click:close="success = ''"
|
||||
>
|
||||
<div class="d-flex align-center">
|
||||
<v-icon class="mr-2">mdi-check-circle</v-icon>
|
||||
<v-icon class="mr-2">
|
||||
mdi-check-circle
|
||||
</v-icon>
|
||||
{{ success }}
|
||||
</div>
|
||||
</v-alert>
|
||||
@@ -43,29 +47,49 @@
|
||||
<div class="d-flex align-center">
|
||||
<span class="font-weight-bold">配置验证失败,请检查以下问题:</span>
|
||||
</div>
|
||||
<v-list class="bg-transparent" density="compact">
|
||||
<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 color="warning" size="small">mdi-circle-small</v-icon>
|
||||
<template #prepend>
|
||||
<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-title class="text-body-2">
|
||||
{{ error }}
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-alert>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<v-card v-if="loading" class="my-4" outlined>
|
||||
<v-card
|
||||
v-if="loading"
|
||||
class="my-4"
|
||||
outlined
|
||||
>
|
||||
<v-card-text>
|
||||
<v-skeleton-loader class="mx-auto" type="article"></v-skeleton-loader>
|
||||
<v-skeleton-loader
|
||||
class="mx-auto"
|
||||
type="article"
|
||||
/>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<!-- 模式切换按钮和操作按钮 -->
|
||||
<div v-if="!loading" class="d-flex justify-space-between align-center mb-4">
|
||||
<div
|
||||
v-if="!loading"
|
||||
class="d-flex justify-space-between align-center mb-4"
|
||||
>
|
||||
<div class="d-flex align-center gap-2">
|
||||
<v-btn
|
||||
:disabled="!isValidConfig"
|
||||
@@ -140,19 +164,31 @@
|
||||
class="text-error"
|
||||
prepend-icon="mdi-delete"
|
||||
@click="confirmDelete"
|
||||
|
||||
>
|
||||
删除配置
|
||||
</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>
|
||||
|
||||
<!-- 预览模式 -->
|
||||
<div v-if="!loading && !isEditMode">
|
||||
<div class="mb-8">
|
||||
<div class="text-h3 font-weight-bold" style="line-height: 1.2">
|
||||
<div
|
||||
class="text-h3 font-weight-bold"
|
||||
style="line-height: 1.2"
|
||||
>
|
||||
{{ localConfig.examName || "未设置考试名称" }}
|
||||
</div>
|
||||
<div
|
||||
@@ -161,8 +197,14 @@
|
||||
>
|
||||
{{ localConfig.message || "未设置考试提示" }}
|
||||
</div>
|
||||
<v-chip v-if="localConfig.room" class="px-4 py-2" size="large">
|
||||
<v-icon start>mdi-home</v-icon>
|
||||
<v-chip
|
||||
v-if="localConfig.room"
|
||||
class="px-4 py-2"
|
||||
size="large"
|
||||
>
|
||||
<v-icon start>
|
||||
mdi-home
|
||||
</v-icon>
|
||||
考场:{{ localConfig.room }}
|
||||
</v-chip>
|
||||
</div>
|
||||
@@ -178,20 +220,29 @@
|
||||
lg="4"
|
||||
md="6"
|
||||
>
|
||||
<v-card class="h-100" hover variant="tonal">
|
||||
<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>
|
||||
<v-icon class="mr-2">
|
||||
mdi-book-open-page-variant
|
||||
</v-icon>
|
||||
<span class="">{{ examInfo.name || "未设置科目" }}</span>
|
||||
</div>
|
||||
</v-card-title>
|
||||
<v-card-text class="pa-4">
|
||||
<div class="mb-3">
|
||||
<div class="d-flex align-center mb-1">
|
||||
<v-icon class="mr-2" color="success" size="small"
|
||||
>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>
|
||||
<div class="text-h6 font-weight-medium text-success">
|
||||
@@ -200,10 +251,13 @@
|
||||
</div>
|
||||
<div>
|
||||
<div class="d-flex align-center mb-1">
|
||||
<v-icon class="mr-2" color="error" size="small"
|
||||
>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>
|
||||
<div class="text-h6 font-weight-medium text-error">
|
||||
@@ -215,29 +269,50 @@
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
<div v-else class="text-center py-12">
|
||||
<v-icon class="mb-4" color="grey-lighten-2" size="80">
|
||||
<div
|
||||
v-else
|
||||
class="text-center py-12"
|
||||
>
|
||||
<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>
|
||||
<div class="text-h5 text-grey-darken-1 mb-2">
|
||||
暂无考试科目安排
|
||||
</div>
|
||||
<div class="text-body-1 text-grey mb-4">
|
||||
点击上方"添加科目"按钮开始配置考试时间表
|
||||
</div>
|
||||
<v-btn color="primary" variant="outlined" @click="quickEdit">
|
||||
<v-icon start>mdi-plus</v-icon>
|
||||
<v-btn
|
||||
color="primary"
|
||||
variant="outlined"
|
||||
@click="quickEdit"
|
||||
>
|
||||
<v-icon start>
|
||||
mdi-plus
|
||||
</v-icon>
|
||||
立即添加
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<!-- JSON预览 -->
|
||||
<v-card border class="mb-4" elevation="2">
|
||||
<v-card
|
||||
border
|
||||
class="mb-4"
|
||||
elevation="2"
|
||||
>
|
||||
<v-card-title
|
||||
class="d-flex align-center text-white cursor-pointer"
|
||||
@click="showJsonPreview = !showJsonPreview"
|
||||
>
|
||||
<v-icon class="mr-2">mdi-code-json</v-icon>
|
||||
<v-icon class="mr-2">
|
||||
mdi-code-json
|
||||
</v-icon>
|
||||
配置预览
|
||||
<v-spacer></v-spacer>
|
||||
<v-spacer />
|
||||
|
||||
<v-btn
|
||||
color="white"
|
||||
@@ -255,12 +330,17 @@
|
||||
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 class="pa-4" variant="tonal">
|
||||
<v-card-text
|
||||
v-show="showJsonPreview"
|
||||
class="pa-4"
|
||||
>
|
||||
<v-card
|
||||
class="pa-4"
|
||||
variant="tonal"
|
||||
>
|
||||
<pre class="json-preview"><code>{{ formattedStorageJson }}</code></pre>
|
||||
</v-card>
|
||||
</v-card-text>
|
||||
@@ -271,14 +351,23 @@
|
||||
<!-- 编辑模式 -->
|
||||
<div v-if="!loading && isEditMode">
|
||||
<!-- 基本信息 -->
|
||||
<v-card border class="mb-4" elevation="1">
|
||||
<v-card
|
||||
border
|
||||
class="mb-4"
|
||||
elevation="1"
|
||||
>
|
||||
<v-card-title class="d-flex align-center bg-primary-lighten-5 pa-4">
|
||||
<v-icon class="mr-2">mdi-information</v-icon>
|
||||
<v-icon class="mr-2">
|
||||
mdi-information
|
||||
</v-icon>
|
||||
<span class="font-weight-bold">基本信息</span>
|
||||
</v-card-title>
|
||||
<v-card-text class="pa-6">
|
||||
<v-row>
|
||||
<v-col cols="12" md="6">
|
||||
<v-col
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<v-text-field
|
||||
v-model="localConfig.examName"
|
||||
:rules="[
|
||||
@@ -293,9 +382,12 @@
|
||||
prepend-inner-icon="mdi-calendar-text"
|
||||
required
|
||||
variant="outlined"
|
||||
></v-text-field>
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6">
|
||||
<v-col
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<v-text-field
|
||||
v-model="localConfig.room"
|
||||
clearable
|
||||
@@ -304,13 +396,16 @@
|
||||
placeholder="如:一号考场"
|
||||
prepend-inner-icon="mdi-home"
|
||||
variant="outlined"
|
||||
></v-text-field>
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<span class="text-subtitle-2 font-weight-bold d-block mb-2">
|
||||
<v-icon size="small" class="mr-1">mdi-message-text</v-icon>
|
||||
<v-icon
|
||||
size="small"
|
||||
class="mr-1"
|
||||
>mdi-message-text</v-icon>
|
||||
考试提示
|
||||
</span>
|
||||
<v-textarea
|
||||
@@ -326,10 +421,13 @@
|
||||
placeholder="例如:请保持卷面整洁,诚信应考。在听到终考铃时立刻停止作答。"
|
||||
rows="3"
|
||||
variant="outlined"
|
||||
></v-textarea>
|
||||
/>
|
||||
|
||||
<!-- 默认提示选项 -->
|
||||
<div v-if="!localConfig.message || localConfig.message.trim() === ''" class="mt-3">
|
||||
<div
|
||||
v-if="!localConfig.message || localConfig.message.trim() === ''"
|
||||
class="mt-3"
|
||||
>
|
||||
<v-chip-group
|
||||
class="d-flex gap-2"
|
||||
column
|
||||
@@ -343,12 +441,22 @@
|
||||
variant="outlined"
|
||||
@click="selectDefaultTip(tip)"
|
||||
>
|
||||
<v-icon size="small" start>mdi-plus</v-icon>
|
||||
<v-icon
|
||||
size="small"
|
||||
start
|
||||
>
|
||||
mdi-plus
|
||||
</v-icon>
|
||||
{{ tip.substring(0, 20) }}...
|
||||
</v-chip>
|
||||
</v-chip-group>
|
||||
<div class="text-caption text-medium-emphasis mt-2 ml-2">
|
||||
<v-icon class="mr-1" size="x-small">mdi-lightbulb-outline</v-icon>
|
||||
<v-icon
|
||||
class="mr-1"
|
||||
size="x-small"
|
||||
>
|
||||
mdi-lightbulb-outline
|
||||
</v-icon>
|
||||
点击上方选项快速添加常用考试提示
|
||||
</div>
|
||||
</div>
|
||||
@@ -358,11 +466,17 @@
|
||||
</v-card>
|
||||
|
||||
<!-- 考试科目安排 -->
|
||||
<v-card border class="mb-4" elevation="1">
|
||||
<v-card
|
||||
border
|
||||
class="mb-4"
|
||||
elevation="1"
|
||||
>
|
||||
<v-card-title class="d-flex align-center bg-success-lighten-5 pa-4">
|
||||
<v-icon class="mr-2">mdi-format-list-bulleted</v-icon>
|
||||
<v-icon class="mr-2">
|
||||
mdi-format-list-bulleted
|
||||
</v-icon>
|
||||
<span class="font-weight-bold">考试科目安排</span>
|
||||
<v-spacer></v-spacer>
|
||||
<v-spacer />
|
||||
|
||||
<!-- 提醒时间开关 -->
|
||||
<div class="d-flex align-center mr-4">
|
||||
@@ -373,7 +487,7 @@
|
||||
hide-details
|
||||
@change="toggleAlertTimeMode"
|
||||
>
|
||||
<template v-slot:label>
|
||||
<template #label>
|
||||
<span class="text-body-2">自定义提醒时间</span>
|
||||
</template>
|
||||
</v-switch>
|
||||
@@ -408,14 +522,19 @@
|
||||
variant="tonal"
|
||||
class="mr-3"
|
||||
>
|
||||
<v-icon start size="small">mdi-numeric-{{ index + 1 }}-circle</v-icon>
|
||||
<v-icon
|
||||
start
|
||||
size="small"
|
||||
>
|
||||
mdi-numeric-{{ index + 1 }}-circle
|
||||
</v-icon>
|
||||
第 {{ index + 1 }} 科目
|
||||
</v-chip>
|
||||
|
||||
<!-- 考试时长显示 -->
|
||||
|
||||
|
||||
<v-spacer></v-spacer>
|
||||
<v-spacer />
|
||||
<div class="d-flex gap-1">
|
||||
<v-btn
|
||||
v-if="index > 0"
|
||||
@@ -425,7 +544,12 @@
|
||||
variant="text"
|
||||
@click="moveExamInfo(index, -1)"
|
||||
>
|
||||
<v-tooltip activator="parent" location="bottom">上移</v-tooltip>
|
||||
<v-tooltip
|
||||
activator="parent"
|
||||
location="bottom"
|
||||
>
|
||||
上移
|
||||
</v-tooltip>
|
||||
</v-btn>
|
||||
<v-btn
|
||||
v-if="index < localConfig.examInfos.length - 1"
|
||||
@@ -435,7 +559,12 @@
|
||||
variant="text"
|
||||
@click="moveExamInfo(index, 1)"
|
||||
>
|
||||
<v-tooltip activator="parent" location="bottom">下移</v-tooltip>
|
||||
<v-tooltip
|
||||
activator="parent"
|
||||
location="bottom"
|
||||
>
|
||||
下移
|
||||
</v-tooltip>
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="error"
|
||||
@@ -444,13 +573,21 @@
|
||||
variant="text"
|
||||
@click="removeExamInfo(index)"
|
||||
>
|
||||
<v-tooltip activator="parent" location="bottom">删除</v-tooltip>
|
||||
<v-tooltip
|
||||
activator="parent"
|
||||
location="bottom"
|
||||
>
|
||||
删除
|
||||
</v-tooltip>
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<v-row class="align-start">
|
||||
<v-col cols="12" md="4">
|
||||
<v-col
|
||||
cols="12"
|
||||
md="4"
|
||||
>
|
||||
<!-- 科目名称自动完成选择器 -->
|
||||
<v-autocomplete
|
||||
v-model="examInfo.name"
|
||||
@@ -467,20 +604,23 @@
|
||||
prepend-inner-icon="mdi-book"
|
||||
variant="outlined"
|
||||
>
|
||||
<template v-slot:prepend-item>
|
||||
<template #prepend-item>
|
||||
<v-list-item
|
||||
v-if="customSubjectInput"
|
||||
title="自定义:"
|
||||
>
|
||||
<template v-slot:append>
|
||||
<template #append>
|
||||
<span class="text-primary font-weight-bold">{{ customSubjectInput }}</span>
|
||||
</template>
|
||||
</v-list-item>
|
||||
<v-divider v-if="customSubjectInput"></v-divider>
|
||||
<v-divider v-if="customSubjectInput" />
|
||||
</template>
|
||||
</v-autocomplete>
|
||||
</v-col>
|
||||
<v-col cols="12" md="3">
|
||||
<v-col
|
||||
cols="12"
|
||||
md="3"
|
||||
>
|
||||
<v-menu
|
||||
v-model="examInfo.startDateMenu"
|
||||
:close-on-content-click="false"
|
||||
@@ -488,7 +628,7 @@
|
||||
offset-y
|
||||
transition="scale-transition"
|
||||
>
|
||||
<template v-slot:activator="{ props }">
|
||||
<template #activator="{ props }">
|
||||
<v-text-field
|
||||
v-model="examInfo.startFormatted"
|
||||
:rules="[
|
||||
@@ -504,19 +644,27 @@
|
||||
variant="outlined"
|
||||
@blur="updateStartDateTimeFromInput(index)"
|
||||
>
|
||||
<template v-slot:append>
|
||||
<template #append>
|
||||
<v-icon>mdi-calendar-clock</v-icon>
|
||||
</template>
|
||||
</v-text-field>
|
||||
</template>
|
||||
<v-card min-width="500">
|
||||
<v-card-title class="text-center py-3 bg-primary-lighten-5">
|
||||
<v-icon class="mr-2" color="primary">mdi-clock-start</v-icon>
|
||||
<v-icon
|
||||
class="mr-2"
|
||||
color="primary"
|
||||
>
|
||||
mdi-clock-start
|
||||
</v-icon>
|
||||
选择开始时间
|
||||
</v-card-title>
|
||||
<v-card-text class="pa-0">
|
||||
<v-row no-gutters>
|
||||
<v-col class="border-e" cols="6">
|
||||
<v-col
|
||||
class="border-e"
|
||||
cols="6"
|
||||
>
|
||||
<v-date-picker
|
||||
v-model="examInfo.startDate"
|
||||
color="primary"
|
||||
@@ -524,7 +672,7 @@
|
||||
locale="zh-cn"
|
||||
show-adjacent-months
|
||||
@update:model-value="updateStartDateTime(index)"
|
||||
></v-date-picker>
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="6">
|
||||
<v-time-picker
|
||||
@@ -534,12 +682,12 @@
|
||||
format="24hr"
|
||||
scrollable
|
||||
@update:model-value="updateStartDateTime(index)"
|
||||
></v-time-picker>
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
color="grey"
|
||||
variant="text"
|
||||
@@ -551,7 +699,10 @@
|
||||
</v-card>
|
||||
</v-menu>
|
||||
</v-col>
|
||||
<v-col cols="12" md="3">
|
||||
<v-col
|
||||
cols="12"
|
||||
md="3"
|
||||
>
|
||||
<v-menu
|
||||
v-model="examInfo.endDateMenu"
|
||||
:close-on-content-click="false"
|
||||
@@ -559,7 +710,7 @@
|
||||
offset-y
|
||||
transition="scale-transition"
|
||||
>
|
||||
<template v-slot:activator="{ props }">
|
||||
<template #activator="{ props }">
|
||||
<v-text-field
|
||||
v-model="examInfo.endFormatted"
|
||||
:rules="[
|
||||
@@ -576,19 +727,27 @@
|
||||
variant="outlined"
|
||||
@blur="updateEndDateTimeFromInput(index)"
|
||||
>
|
||||
<template v-slot:append>
|
||||
<template #append>
|
||||
<v-icon>mdi-calendar-clock</v-icon>
|
||||
</template>
|
||||
</v-text-field>
|
||||
</template>
|
||||
<v-card min-width="500">
|
||||
<v-card-title class="text-center py-3 bg-error-lighten-5">
|
||||
<v-icon class="mr-2" color="error">mdi-clock-end</v-icon>
|
||||
<v-icon
|
||||
class="mr-2"
|
||||
color="error"
|
||||
>
|
||||
mdi-clock-end
|
||||
</v-icon>
|
||||
选择结束时间
|
||||
</v-card-title>
|
||||
<v-card-text class="pa-0">
|
||||
<v-row no-gutters>
|
||||
<v-col class="border-e" cols="6">
|
||||
<v-col
|
||||
class="border-e"
|
||||
cols="6"
|
||||
>
|
||||
<v-date-picker
|
||||
v-model="examInfo.endDate"
|
||||
color="error"
|
||||
@@ -596,7 +755,7 @@
|
||||
locale="zh-cn"
|
||||
show-adjacent-months
|
||||
@update:model-value="updateEndDateTime(index)"
|
||||
></v-date-picker>
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="6">
|
||||
<v-time-picker
|
||||
@@ -606,12 +765,12 @@
|
||||
format="24hr"
|
||||
scrollable
|
||||
@update:model-value="updateEndDateTime(index)"
|
||||
></v-time-picker>
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
color="grey"
|
||||
variant="text"
|
||||
@@ -623,60 +782,76 @@
|
||||
</v-card>
|
||||
</v-menu>
|
||||
</v-col>
|
||||
<v-col cols="12" md="2">
|
||||
<v-text-field
|
||||
v-model="examInfo.durationMinutes"
|
||||
:rules="[
|
||||
(v) => !!v || '时长不能为空',
|
||||
(v) => !isNaN(v) || '时长必须是数字',
|
||||
(v) => parseInt(v) > 0 || '时长必须大于0',
|
||||
(v) => parseInt(v) <= 1440 || '时长不能超过1440分钟(24小时)'
|
||||
]"
|
||||
type="number"
|
||||
min="1"
|
||||
max="1440"
|
||||
density="comfortable"
|
||||
label="时长(分钟)"
|
||||
placeholder="例如:120"
|
||||
prepend-inner-icon="mdi-timer"
|
||||
variant="outlined"
|
||||
:hint="durationHint(examInfo)"
|
||||
persistent-hint
|
||||
@blur="updateDurationFromInput(index)"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col
|
||||
cols="12"
|
||||
md="2"
|
||||
>
|
||||
<v-text-field
|
||||
v-model="examInfo.durationMinutes"
|
||||
:rules="[
|
||||
(v) => !!v || '时长不能为空',
|
||||
(v) => !isNaN(v) || '时长必须是数字',
|
||||
(v) => parseInt(v) > 0 || '时长必须大于0',
|
||||
(v) => parseInt(v) <= 1440 || '时长不能超过1440分钟(24小时)'
|
||||
]"
|
||||
type="number"
|
||||
min="1"
|
||||
max="1440"
|
||||
density="comfortable"
|
||||
label="时长(分钟)"
|
||||
placeholder="例如:120"
|
||||
prepend-inner-icon="mdi-timer"
|
||||
variant="outlined"
|
||||
:hint="durationHint(examInfo)"
|
||||
persistent-hint
|
||||
@blur="updateDurationFromInput(index)"
|
||||
/>
|
||||
</v-col>
|
||||
|
||||
<!-- 提醒时间输入框(仅在启用自定义时显示) -->
|
||||
<v-col v-if="enableCustomAlertTime" cols="12" md="2">
|
||||
<v-text-field
|
||||
v-model="examInfo.alertTime"
|
||||
:rules="[
|
||||
(v) => !!v || '提醒时间不能为空',
|
||||
(v) => !isNaN(v) || '提醒时间必须是数字',
|
||||
(v) => parseInt(v) >= 0 || '提醒时间不能为负数',
|
||||
(v) => parseInt(v) <= 120 || '提醒时间不能超过120分钟'
|
||||
]"
|
||||
type="number"
|
||||
min="0"
|
||||
max="120"
|
||||
density="comfortable"
|
||||
label="提醒时间(分钟)"
|
||||
placeholder="例如:15"
|
||||
prepend-inner-icon="mdi-bell-ring"
|
||||
variant="outlined"
|
||||
hint="考试结束前提醒"
|
||||
persistent-hint
|
||||
/>
|
||||
</v-col>
|
||||
<!-- 提醒时间输入框(仅在启用自定义时显示) -->
|
||||
<v-col
|
||||
v-if="enableCustomAlertTime"
|
||||
cols="12"
|
||||
md="2"
|
||||
>
|
||||
<v-text-field
|
||||
v-model="examInfo.alertTime"
|
||||
:rules="[
|
||||
(v) => !!v || '提醒时间不能为空',
|
||||
(v) => !isNaN(v) || '提醒时间必须是数字',
|
||||
(v) => parseInt(v) >= 0 || '提醒时间不能为负数',
|
||||
(v) => parseInt(v) <= 120 || '提醒时间不能超过120分钟'
|
||||
]"
|
||||
type="number"
|
||||
min="0"
|
||||
max="120"
|
||||
density="comfortable"
|
||||
label="提醒时间(分钟)"
|
||||
placeholder="例如:15"
|
||||
prepend-inner-icon="mdi-bell-ring"
|
||||
variant="outlined"
|
||||
hint="考试结束前提醒"
|
||||
persistent-hint
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
<div v-else class="text-center py-12">
|
||||
<v-icon class="mb-4" color="grey-lighten-2" size="80">
|
||||
<div
|
||||
v-else
|
||||
class="text-center py-12"
|
||||
>
|
||||
<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>
|
||||
<div class="text-h5 text-grey-darken-1 mb-2">
|
||||
暂无考试科目安排
|
||||
</div>
|
||||
<div class="text-body-1 text-grey mb-4">
|
||||
点击上方"添加科目"按钮开始配置
|
||||
</div>
|
||||
@@ -695,10 +870,18 @@
|
||||
</div>
|
||||
|
||||
<!-- 删除确认对话框 -->
|
||||
<v-dialog v-model="deleteDialog" max-width="400">
|
||||
<v-dialog
|
||||
v-model="deleteDialog"
|
||||
max-width="400"
|
||||
>
|
||||
<v-card>
|
||||
<v-card-title class="d-flex align-center">
|
||||
<v-icon class="mr-2" color="error">mdi-delete-alert</v-icon>
|
||||
<v-icon
|
||||
class="mr-2"
|
||||
color="error"
|
||||
>
|
||||
mdi-delete-alert
|
||||
</v-icon>
|
||||
确认删除配置
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
@@ -706,7 +889,7 @@
|
||||
<br><small class="text-grey">此操作不可撤销,将会删除所有相关数据</small>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
color="grey"
|
||||
variant="text"
|
||||
@@ -945,8 +1128,12 @@ export default {
|
||||
async loadSubjects() {
|
||||
try {
|
||||
const response = await dataProvider.loadData("classworks-config-subject");
|
||||
if (response && Array.isArray(response)) {
|
||||
this.availableSubjects = response
|
||||
|
||||
// 兼容返回数组或包含 value 属性的对象
|
||||
const dataArray = Array.isArray(response) ? response : (response && Array.isArray(response.value) ? response.value : null);
|
||||
|
||||
if (response && response.success !== false && dataArray) {
|
||||
this.availableSubjects = dataArray
|
||||
.sort((a, b) => (a.order ?? 0) - (b.order ?? 0))
|
||||
.map(subject => ({
|
||||
name: subject.name,
|
||||
|
||||
@@ -7,7 +7,10 @@
|
||||
elevation="4"
|
||||
rounded="xl"
|
||||
>
|
||||
<v-btn-group class="toolbar-buttons" variant="text">
|
||||
<v-btn-group
|
||||
class="toolbar-buttons"
|
||||
variant="text"
|
||||
>
|
||||
<v-btn
|
||||
v-ripple
|
||||
:title="'查看昨天'"
|
||||
@@ -32,7 +35,10 @@
|
||||
variant="text"
|
||||
@click="$emit('zoom', 'up')"
|
||||
/>
|
||||
<v-menu :close-on-content-click="false" location="top">
|
||||
<v-menu
|
||||
:close-on-content-click="false"
|
||||
location="top"
|
||||
>
|
||||
<template #activator="{ props }">
|
||||
<v-btn
|
||||
v-ripple
|
||||
@@ -43,7 +49,10 @@
|
||||
variant="text"
|
||||
/>
|
||||
</template>
|
||||
<v-card border class="date-picker-card">
|
||||
<v-card
|
||||
border
|
||||
class="date-picker-card"
|
||||
>
|
||||
<v-date-picker
|
||||
:model-value="selectedDate"
|
||||
color="primary"
|
||||
@@ -52,6 +61,7 @@
|
||||
</v-card>
|
||||
</v-menu>
|
||||
<v-btn
|
||||
:key="loading"
|
||||
v-ripple
|
||||
:loading="loading"
|
||||
:title="'刷新数据'"
|
||||
@@ -76,19 +86,24 @@
|
||||
|
||||
<!-- Side Action Button -->
|
||||
<v-slide-x-reverse-transition>
|
||||
<v-btn
|
||||
<div
|
||||
v-if="!isToday"
|
||||
:loading="copyToTodayLoading"
|
||||
:disabled="copyToTodayLoading"
|
||||
class="side-action-btn"
|
||||
color="primary"
|
||||
elevation="4"
|
||||
prepend-icon="mdi-content-copy"
|
||||
rounded="xl"
|
||||
size="large"
|
||||
text="复制作业内容到今天"
|
||||
@click="$emit('copy-to-today')"
|
||||
>复制到今天</v-btn>
|
||||
class="side-action-container"
|
||||
>
|
||||
<v-btn
|
||||
:loading="copyToTodayLoading"
|
||||
:disabled="copyToTodayLoading"
|
||||
class="side-action-btn"
|
||||
color="primary"
|
||||
elevation="4"
|
||||
prepend-icon="mdi-content-copy"
|
||||
rounded="xl"
|
||||
size="large"
|
||||
@click="$emit('copy-to-today')"
|
||||
>
|
||||
复制到今天
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-slide-x-reverse-transition>
|
||||
</div>
|
||||
</template>
|
||||
@@ -138,9 +153,8 @@ export default {
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 0;
|
||||
overflow: visible;
|
||||
z-index: 100;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@@ -191,12 +205,15 @@ export default {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.side-action-btn {
|
||||
.side-action-container {
|
||||
position: absolute;
|
||||
bottom: 24px;
|
||||
right: 24px;
|
||||
pointer-events: auto;
|
||||
z-index: 101;
|
||||
}
|
||||
|
||||
.side-action-btn {
|
||||
background: rgba(255, 255, 255, 0.9) !important;
|
||||
backdrop-filter: blur(12px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
|
||||
@@ -8,14 +8,28 @@
|
||||
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 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 icon="mdi-close" variant="text" @click="snackbar = false"/>
|
||||
<v-btn
|
||||
icon="mdi-close"
|
||||
variant="text"
|
||||
@click="snackbar = false"
|
||||
/>
|
||||
</template>
|
||||
</v-snackbar>
|
||||
</template>
|
||||
|
||||
@@ -1,164 +0,0 @@
|
||||
<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.png"
|
||||
/>
|
||||
|
||||
<div class="text-center">
|
||||
<div class="text-body-2 font-weight-light mb-n1">Welcome to</div>
|
||||
|
||||
<h1 class="text-h2 font-weight-bold">Vuetify</h1>
|
||||
</div>
|
||||
|
||||
<div class="py-4"/>
|
||||
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<v-card
|
||||
class="py-4"
|
||||
color="surface-variant"
|
||||
image="https://cdn.vuetifyjs.com/docs/images/one/create/feature.png"
|
||||
prepend-icon="mdi-rocket-launch-outline"
|
||||
rounded="lg"
|
||||
variant="outlined"
|
||||
>
|
||||
<template #image>
|
||||
<v-img position="top right"/>
|
||||
</template>
|
||||
|
||||
<template #title>
|
||||
<h2 class="text-h5 font-weight-bold">Get started</h2>
|
||||
</template>
|
||||
|
||||
<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>
|
||||
.
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<v-overlay
|
||||
contained
|
||||
model-value
|
||||
opacity=".12"
|
||||
persistent
|
||||
scrim="primary"
|
||||
/>
|
||||
</v-card>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="6">
|
||||
<v-card
|
||||
append-icon="mdi-open-in-new"
|
||||
class="py-4"
|
||||
color="surface-variant"
|
||||
href="https://vuetifyjs.com/"
|
||||
prepend-icon="mdi-text-box-outline"
|
||||
rel="noopener noreferrer"
|
||||
rounded="lg"
|
||||
subtitle="Learn about all things Vuetify in our documentation."
|
||||
target="_blank"
|
||||
title="Documentation"
|
||||
variant="text"
|
||||
>
|
||||
<v-overlay
|
||||
contained
|
||||
model-value
|
||||
opacity=".06"
|
||||
persistent
|
||||
scrim="primary"
|
||||
/>
|
||||
</v-card>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="6">
|
||||
<v-card
|
||||
append-icon="mdi-open-in-new"
|
||||
class="py-4"
|
||||
color="surface-variant"
|
||||
href="https://vuetifyjs.com/introduction/why-vuetify/#feature-guides"
|
||||
prepend-icon="mdi-star-circle-outline"
|
||||
rel="noopener noreferrer"
|
||||
rounded="lg"
|
||||
subtitle="Explore available framework Features."
|
||||
target="_blank"
|
||||
title="Features"
|
||||
variant="text"
|
||||
>
|
||||
<v-overlay
|
||||
contained
|
||||
model-value
|
||||
opacity=".06"
|
||||
persistent
|
||||
scrim="primary"
|
||||
/>
|
||||
</v-card>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="6">
|
||||
<v-card
|
||||
append-icon="mdi-open-in-new"
|
||||
class="py-4"
|
||||
color="surface-variant"
|
||||
href="https://vuetifyjs.com/components/all"
|
||||
prepend-icon="mdi-widgets-outline"
|
||||
rel="noopener noreferrer"
|
||||
rounded="lg"
|
||||
subtitle="Discover components in the API Explorer."
|
||||
target="_blank"
|
||||
title="Components"
|
||||
variant="text"
|
||||
>
|
||||
<v-overlay
|
||||
contained
|
||||
model-value
|
||||
opacity=".06"
|
||||
persistent
|
||||
scrim="primary"
|
||||
/>
|
||||
</v-card>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="6">
|
||||
<v-card
|
||||
append-icon="mdi-open-in-new"
|
||||
class="py-4"
|
||||
color="surface-variant"
|
||||
href="https://discord.vuetifyjs.com"
|
||||
prepend-icon="mdi-account-group-outline"
|
||||
rel="noopener noreferrer"
|
||||
rounded="lg"
|
||||
subtitle="Connect with Vuetify developers."
|
||||
target="_blank"
|
||||
title="Community"
|
||||
variant="text"
|
||||
>
|
||||
<v-overlay
|
||||
contained
|
||||
model-value
|
||||
opacity=".06"
|
||||
persistent
|
||||
scrim="primary"
|
||||
/>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-responsive>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
//
|
||||
</script>
|
||||
@@ -69,7 +69,7 @@ export default {
|
||||
loading: false,
|
||||
timer: null,
|
||||
unwatch: null,
|
||||
fontSize: 28 // Will be updated in mounted()
|
||||
fontSize: 28
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -111,7 +111,7 @@ export default {
|
||||
loadLocalSettings() {
|
||||
this.enabled = SettingsManager.getSetting('hitokoto.enabled')
|
||||
this.refreshInterval = SettingsManager.getSetting('hitokoto.refreshInterval')
|
||||
this.fontSize = SettingsManager.getSetting('font.size')
|
||||
this.fontSize = 28
|
||||
},
|
||||
async loadKvSettings() {
|
||||
try {
|
||||
|
||||
@@ -1,15 +1,26 @@
|
||||
<template>
|
||||
<div>
|
||||
<setting-group title="一言设置" icon="mdi-comment-quote">
|
||||
<setting-group
|
||||
title="一言设置"
|
||||
icon="mdi-comment-quote"
|
||||
>
|
||||
<setting-item setting-key="hitokoto.enabled" />
|
||||
<setting-item setting-key="hitokoto.refreshInterval" />
|
||||
</setting-group>
|
||||
|
||||
<setting-group title="数据源配置" icon="mdi-cloud-sync" class="mt-4">
|
||||
<div class="text-caption text-grey px-4 pt-2 pb-2">以下配置将同步到云端,对所有连接此班级的设备生效。</div>
|
||||
<setting-group
|
||||
title="数据源配置"
|
||||
icon="mdi-cloud-sync"
|
||||
class="mt-4"
|
||||
>
|
||||
<div class="text-caption text-grey px-4 pt-2 pb-2">
|
||||
以下配置将同步到云端,对所有连接此班级的设备生效。
|
||||
</div>
|
||||
|
||||
<v-list-item>
|
||||
<v-list-item-title class="mb-2">启用数据源</v-list-item-title>
|
||||
<v-list-item-title class="mb-2">
|
||||
启用数据源
|
||||
</v-list-item-title>
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
<v-checkbox
|
||||
v-model="kvConfig.sources"
|
||||
@@ -42,7 +53,13 @@
|
||||
/>
|
||||
</div>
|
||||
<div class="text-caption text-orange mt-2">
|
||||
<v-icon size="x-small" color="orange" class="mr-1">mdi-alert</v-icon>
|
||||
<v-icon
|
||||
size="x-small"
|
||||
color="orange"
|
||||
class="mr-1"
|
||||
>
|
||||
mdi-alert
|
||||
</v-icon>
|
||||
一言(Hitokoto)数据源已收到关于存在负面内容的大量反馈,请用户谨慎启用。
|
||||
</div>
|
||||
</v-list-item>
|
||||
@@ -99,39 +116,62 @@
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-checkbox
|
||||
v-model="enableCloudSensitiveWords"
|
||||
label="启用云端敏感词列表"
|
||||
hide-details
|
||||
v-model="enableCloudSensitiveWords"
|
||||
density="compact"
|
||||
disabled
|
||||
class="mb-2"
|
||||
/>
|
||||
<div class="text-caption text-grey">
|
||||
已启用的数据源将在获取一言时随机尝试,直到成功获取内容为止。<br/>
|
||||
敏感词过滤会将包含任意敏感词的句子过滤掉,避免显示不当内容。<br/>
|
||||
已启用的数据源将在获取一言时随机尝试,直到成功获取内容为止。<br>
|
||||
敏感词过滤会将包含任意敏感词的句子过滤掉,避免显示不当内容。<br>
|
||||
</div>
|
||||
</v-list-item>
|
||||
</v-list-item>
|
||||
|
||||
<div v-if="loading" class="text-center pb-4">
|
||||
<v-progress-circular indeterminate size="24" color="primary" />
|
||||
<div
|
||||
v-if="loading"
|
||||
class="text-center pb-4"
|
||||
>
|
||||
<v-progress-circular
|
||||
indeterminate
|
||||
size="24"
|
||||
color="primary"
|
||||
/>
|
||||
<span class="ml-2 text-caption">正在同步配置...</span>
|
||||
</div>
|
||||
</setting-group>
|
||||
|
||||
<v-dialog v-model="testResultDialog" max-width="600">
|
||||
<v-card v-if="testResultData" class="rounded-lg">
|
||||
|
||||
|
||||
<v-dialog
|
||||
v-model="testResultDialog"
|
||||
max-width="600"
|
||||
>
|
||||
<v-card
|
||||
v-if="testResultData"
|
||||
class="rounded-lg"
|
||||
>
|
||||
<v-card-text class="pa-0">
|
||||
<v-list lines="two" class="py-0">
|
||||
<v-list
|
||||
lines="two"
|
||||
class="py-0"
|
||||
>
|
||||
<v-list-item class="px-4 py-3">
|
||||
<template v-slot:prepend>
|
||||
<v-avatar color="primary" variant="tonal" class="mr-2">
|
||||
<template #prepend>
|
||||
<v-avatar
|
||||
color="primary"
|
||||
variant="tonal"
|
||||
class="mr-2"
|
||||
>
|
||||
<v-icon icon="mdi-key-variant" />
|
||||
</v-avatar>
|
||||
</template>
|
||||
<v-list-item-title class="text-subtitle-2 font-weight-bold mb-1">Token</v-list-item-title>
|
||||
<v-list-item-subtitle class="text-body-2 text-high-emphasis" style="word-break: break-all;">
|
||||
<v-list-item-title class="text-subtitle-2 font-weight-bold mb-1">
|
||||
Token
|
||||
</v-list-item-title>
|
||||
<v-list-item-subtitle
|
||||
class="text-body-2 text-high-emphasis"
|
||||
style="word-break: break-all;"
|
||||
>
|
||||
{{ testResultData.data.token }}
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
@@ -141,20 +181,36 @@
|
||||
<v-row no-gutters>
|
||||
<v-col cols="6">
|
||||
<v-list-item class="px-4 py-2">
|
||||
<template v-slot:prepend>
|
||||
<v-icon icon="mdi-ip-network" color="grey-darken-1" class="mr-2" />
|
||||
<template #prepend>
|
||||
<v-icon
|
||||
icon="mdi-ip-network"
|
||||
color="grey-darken-1"
|
||||
class="mr-2"
|
||||
/>
|
||||
</template>
|
||||
<v-list-item-title class="text-caption text-grey-darken-1">IP 地址</v-list-item-title>
|
||||
<v-list-item-subtitle class="text-body-2">{{ testResultData.data.ip }}</v-list-item-subtitle>
|
||||
<v-list-item-title class="text-caption text-grey-darken-1">
|
||||
IP 地址
|
||||
</v-list-item-title>
|
||||
<v-list-item-subtitle class="text-body-2">
|
||||
{{ testResultData.data.ip }}
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
</v-col>
|
||||
<v-col cols="6">
|
||||
<v-list-item class="px-4 py-2">
|
||||
<template v-slot:prepend>
|
||||
<v-icon icon="mdi-map-marker-radius" color="grey-darken-1" class="mr-2" />
|
||||
<template #prepend>
|
||||
<v-icon
|
||||
icon="mdi-map-marker-radius"
|
||||
color="grey-darken-1"
|
||||
class="mr-2"
|
||||
/>
|
||||
</template>
|
||||
<v-list-item-title class="text-caption text-grey-darken-1">地区</v-list-item-title>
|
||||
<v-list-item-subtitle class="text-body-2">{{ testResultData.data.region }}</v-list-item-subtitle>
|
||||
<v-list-item-title class="text-caption text-grey-darken-1">
|
||||
地区
|
||||
</v-list-item-title>
|
||||
<v-list-item-subtitle class="text-body-2">
|
||||
{{ testResultData.data.region }}
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
</v-col>
|
||||
</v-row>
|
||||
@@ -165,59 +221,123 @@
|
||||
|
||||
<v-container class="px-4 py-3">
|
||||
<v-row dense>
|
||||
<v-col cols="6" sm="4">
|
||||
<v-col
|
||||
cols="6"
|
||||
sm="4"
|
||||
>
|
||||
<div class="d-flex align-center mb-2">
|
||||
<v-icon icon="mdi-thermometer" color="orange" class="mr-2" />
|
||||
<v-icon
|
||||
icon="mdi-thermometer"
|
||||
color="orange"
|
||||
class="mr-2"
|
||||
/>
|
||||
<div>
|
||||
<div class="text-caption text-grey">温度</div>
|
||||
<div class="text-body-1 font-weight-medium">{{ testResultData.data.weatherData.temperature }}°C</div>
|
||||
<div class="text-caption text-grey">
|
||||
温度
|
||||
</div>
|
||||
<div class="text-body-1 font-weight-medium">
|
||||
{{ testResultData.data.weatherData.temperature }}°C
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</v-col>
|
||||
<v-col cols="6" sm="4">
|
||||
<v-col
|
||||
cols="6"
|
||||
sm="4"
|
||||
>
|
||||
<div class="d-flex align-center mb-2">
|
||||
<v-icon icon="mdi-weather-cloudy" color="blue-grey" class="mr-2" />
|
||||
<v-icon
|
||||
icon="mdi-weather-cloudy"
|
||||
color="blue-grey"
|
||||
class="mr-2"
|
||||
/>
|
||||
<div>
|
||||
<div class="text-caption text-grey">天气</div>
|
||||
<div class="text-body-1 font-weight-medium">{{ testResultData.data.weatherData.weather }}</div>
|
||||
<div class="text-caption text-grey">
|
||||
天气
|
||||
</div>
|
||||
<div class="text-body-1 font-weight-medium">
|
||||
{{ testResultData.data.weatherData.weather }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</v-col>
|
||||
<v-col cols="6" sm="4">
|
||||
<v-col
|
||||
cols="6"
|
||||
sm="4"
|
||||
>
|
||||
<div class="d-flex align-center mb-2">
|
||||
<v-icon icon="mdi-water-percent" color="blue" class="mr-2" />
|
||||
<v-icon
|
||||
icon="mdi-water-percent"
|
||||
color="blue"
|
||||
class="mr-2"
|
||||
/>
|
||||
<div>
|
||||
<div class="text-caption text-grey">湿度</div>
|
||||
<div class="text-body-1 font-weight-medium">{{ testResultData.data.weatherData.humidity }}%</div>
|
||||
<div class="text-caption text-grey">
|
||||
湿度
|
||||
</div>
|
||||
<div class="text-body-1 font-weight-medium">
|
||||
{{ testResultData.data.weatherData.humidity }}%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</v-col>
|
||||
<v-col cols="6" sm="4">
|
||||
<v-col
|
||||
cols="6"
|
||||
sm="4"
|
||||
>
|
||||
<div class="d-flex align-center mb-2">
|
||||
<v-icon icon="mdi-weather-windy" color="teal" class="mr-2" />
|
||||
<v-icon
|
||||
icon="mdi-weather-windy"
|
||||
color="teal"
|
||||
class="mr-2"
|
||||
/>
|
||||
<div>
|
||||
<div class="text-caption text-grey">风向/风力</div>
|
||||
<div class="text-caption text-grey">
|
||||
风向/风力
|
||||
</div>
|
||||
<div class="text-body-2 font-weight-medium">
|
||||
{{ testResultData.data.weatherData.windDirection }} {{ testResultData.data.weatherData.windPower }}级
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</v-col>
|
||||
<v-col cols="6" sm="4">
|
||||
<v-col
|
||||
cols="6"
|
||||
sm="4"
|
||||
>
|
||||
<div class="d-flex align-center mb-2">
|
||||
<v-icon icon="mdi-blur" color="grey" class="mr-2" />
|
||||
<v-icon
|
||||
icon="mdi-blur"
|
||||
color="grey"
|
||||
class="mr-2"
|
||||
/>
|
||||
<div>
|
||||
<div class="text-caption text-grey">PM2.5</div>
|
||||
<div class="text-body-1 font-weight-medium">{{ testResultData.data.weatherData.pm25 }}</div>
|
||||
<div class="text-caption text-grey">
|
||||
PM2.5
|
||||
</div>
|
||||
<div class="text-body-1 font-weight-medium">
|
||||
{{ testResultData.data.weatherData.pm25 }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</v-col>
|
||||
<v-col cols="6" sm="4">
|
||||
<v-col
|
||||
cols="6"
|
||||
sm="4"
|
||||
>
|
||||
<div class="d-flex align-center mb-2">
|
||||
<v-icon icon="mdi-eye" color="indigo" class="mr-2" />
|
||||
<v-icon
|
||||
icon="mdi-eye"
|
||||
color="indigo"
|
||||
class="mr-2"
|
||||
/>
|
||||
<div>
|
||||
<div class="text-caption text-grey">能见度</div>
|
||||
<div class="text-body-1 font-weight-medium">{{ testResultData.data.weatherData.visibility }}</div>
|
||||
<div class="text-caption text-grey">
|
||||
能见度
|
||||
</div>
|
||||
<div class="text-body-1 font-weight-medium">
|
||||
{{ testResultData.data.weatherData.visibility }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</v-col>
|
||||
@@ -227,7 +347,9 @@
|
||||
<v-divider />
|
||||
|
||||
<div class="px-4 py-3">
|
||||
<div class="text-caption text-grey mb-2">环境标签</div>
|
||||
<div class="text-caption text-grey mb-2">
|
||||
环境标签
|
||||
</div>
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
<v-chip
|
||||
v-for="tag in testResultData.data.tags"
|
||||
@@ -245,8 +367,12 @@
|
||||
<v-divider />
|
||||
|
||||
<v-list-item class="px-4 py-2">
|
||||
<template v-slot:prepend>
|
||||
<v-icon icon="mdi-clock-outline" size="small" class="mr-2" />
|
||||
<template #prepend>
|
||||
<v-icon
|
||||
icon="mdi-clock-outline"
|
||||
size="small"
|
||||
class="mr-2"
|
||||
/>
|
||||
</template>
|
||||
<v-list-item-title class="text-caption text-grey-darken-1">
|
||||
北京时间: {{ new Date(testResultData.data.beijingTime).toLocaleString() }}
|
||||
@@ -254,8 +380,6 @@
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-card-text>
|
||||
|
||||
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</div>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,33 +1,50 @@
|
||||
<template>
|
||||
<v-navigation-drawer v-if="drawer" v-model="drawer" location="right" temporary width="400">
|
||||
<v-navigation-drawer
|
||||
v-if="drawer"
|
||||
v-model="drawer"
|
||||
location="right"
|
||||
temporary
|
||||
width="400"
|
||||
>
|
||||
<v-toolbar color="primary">
|
||||
<v-toolbar-title>消息记录</v-toolbar-title>
|
||||
|
||||
</v-toolbar>
|
||||
|
||||
<v-list>
|
||||
<v-list-item v-for="msg in messages" :key="msg.id" rounded>
|
||||
<v-list-item
|
||||
v-for="msg in messages"
|
||||
:key="msg.id"
|
||||
rounded
|
||||
>
|
||||
<template #prepend>
|
||||
<v-icon :color="colors[msg.type]" :icon="icons[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-if="msg.content">
|
||||
{{
|
||||
msg.content
|
||||
}}
|
||||
</v-list-item-subtitle>
|
||||
<span class="text-caption text-grey">
|
||||
{{ new Date(msg.timestamp).toLocaleTimeString() }}
|
||||
</span>
|
||||
|
||||
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item v-if="!messages.length">
|
||||
<template #prepend>
|
||||
<v-icon color="grey" icon="mdi-inbox"/>
|
||||
<v-icon
|
||||
color="grey"
|
||||
icon="mdi-inbox"
|
||||
/>
|
||||
</template>
|
||||
<v-list-item-title class="text-grey">暂无消息</v-list-item-title>
|
||||
<v-list-item-title class="text-grey">
|
||||
暂无消息
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-navigation-drawer>
|
||||
|
||||
@@ -16,7 +16,10 @@
|
||||
</v-card-subtitle>
|
||||
|
||||
<v-card-text class="pt-0 pb-1">
|
||||
<v-list density="comfortable" lines="two">
|
||||
<v-list
|
||||
density="comfortable"
|
||||
lines="two"
|
||||
>
|
||||
<v-list-item
|
||||
v-for="item in chipList"
|
||||
:key="item.key"
|
||||
@@ -24,8 +27,12 @@
|
||||
@click="() => handleSingleRequest(item.key)"
|
||||
>
|
||||
<template #prepend>
|
||||
<v-avatar :color="chipColors[item.status]" size="32" variant="tonal">
|
||||
<v-icon :icon="statusIcons[item.status]"></v-icon>
|
||||
<v-avatar
|
||||
:color="chipColors[item.status]"
|
||||
size="32"
|
||||
variant="tonal"
|
||||
>
|
||||
<v-icon :icon="statusIcons[item.status]" />
|
||||
</v-avatar>
|
||||
</template>
|
||||
|
||||
@@ -33,7 +40,12 @@
|
||||
<v-list-item-subtitle>{{ item.description }}</v-list-item-subtitle>
|
||||
|
||||
<template #append>
|
||||
<v-chip :color="chipColors[item.status]" size="small" variant="tonal" class="me-2">
|
||||
<v-chip
|
||||
:color="chipColors[item.status]"
|
||||
size="small"
|
||||
variant="tonal"
|
||||
class="me-2"
|
||||
>
|
||||
{{ statusText[item.status] }}
|
||||
</v-chip>
|
||||
<v-btn
|
||||
@@ -42,7 +54,7 @@
|
||||
size="small"
|
||||
:disabled="isRequesting"
|
||||
@click.stop="() => openHelp(item.key)"
|
||||
></v-btn>
|
||||
/>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
@@ -76,15 +88,25 @@
|
||||
size="100"
|
||||
rounded="0"
|
||||
>
|
||||
<v-icon icon="mdi-monitor-cellphone" size="80"></v-icon>
|
||||
<v-icon
|
||||
icon="mdi-monitor-cellphone"
|
||||
size="80"
|
||||
/>
|
||||
</v-avatar>
|
||||
</div>
|
||||
|
||||
<v-dialog v-model="helpDialog" max-width="520">
|
||||
<v-dialog
|
||||
v-model="helpDialog"
|
||||
max-width="520"
|
||||
>
|
||||
<v-card>
|
||||
<v-card-title class="text-h6">{{ helpContent.title }}</v-card-title>
|
||||
<v-card-title class="text-h6">
|
||||
{{ helpContent.title }}
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<p class="mb-3">{{ helpContent.message }}</p>
|
||||
<p class="mb-3">
|
||||
{{ helpContent.message }}
|
||||
</p>
|
||||
<v-list density="comfortable">
|
||||
<v-list-item
|
||||
v-for="(link, index) in helpContent.links"
|
||||
@@ -99,8 +121,13 @@
|
||||
</v-list>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn variant="text" @click="helpDialog = false">我知道了</v-btn>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
variant="text"
|
||||
@click="helpDialog = false"
|
||||
>
|
||||
我知道了
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
@@ -5,16 +5,32 @@
|
||||
max-width="600"
|
||||
persistent
|
||||
>
|
||||
<v-card border class="random-picker-card" rounded="xl">
|
||||
<v-card
|
||||
border
|
||||
class="random-picker-card"
|
||||
rounded="xl"
|
||||
>
|
||||
<v-card-title class="text-h5 d-flex align-center">
|
||||
<v-icon class="mr-2" icon="mdi-account-question"/>
|
||||
<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">
|
||||
<div class="text-h6 mb-4">请选择抽取人数</div>
|
||||
<v-card-text
|
||||
v-if="!isPickingStarted"
|
||||
class="text-center py-6"
|
||||
>
|
||||
<div class="text-h6 mb-4">
|
||||
请选择抽取人数
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-center align-center counter-container">
|
||||
<v-btn
|
||||
@@ -52,14 +68,29 @@
|
||||
mandatory
|
||||
rounded="pill"
|
||||
>
|
||||
<v-btn prepend-icon="mdi-account" value="name">姓名模式</v-btn>
|
||||
<v-btn prepend-icon="mdi-numeric" value="number">学号模式</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>
|
||||
|
||||
<!-- 学号范围设置 -->
|
||||
<div v-if="pickerMode === 'number'" class="number-range-container mt-4">
|
||||
<div class="text-subtitle-1 mb-2">学号范围设置</div>
|
||||
<div
|
||||
v-if="pickerMode === 'number'"
|
||||
class="number-range-container mt-4"
|
||||
>
|
||||
<div class="text-subtitle-1 mb-2">
|
||||
学号范围设置
|
||||
</div>
|
||||
<div class="d-flex justify-center align-center gap-4">
|
||||
<v-text-field
|
||||
v-model.number="minNumber"
|
||||
@@ -98,7 +129,10 @@
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<div v-if="filteredStudents.length === 0" class="mt-4 text-error">
|
||||
<div
|
||||
v-if="filteredStudents.length === 0"
|
||||
class="mt-4 text-error"
|
||||
>
|
||||
<template v-if="pickerMode === 'name'">
|
||||
没有可抽取的学生,请调整过滤选项
|
||||
</template>
|
||||
@@ -109,8 +143,11 @@
|
||||
|
||||
<div class="mt-4 text-caption">
|
||||
当前可抽取学生: {{ filteredStudents.length }}人
|
||||
<v-tooltip v-if="pickerMode === 'name'" location="bottom">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-tooltip
|
||||
v-if="pickerMode === 'name'"
|
||||
location="bottom"
|
||||
>
|
||||
<template #activator="{ props }">
|
||||
<v-icon
|
||||
class="ml-1"
|
||||
icon="mdi-information-outline"
|
||||
@@ -132,7 +169,10 @@
|
||||
</v-tooltip>
|
||||
|
||||
<!-- 添加临时过滤选项 -->
|
||||
<div v-if="pickerMode === 'name'" class="d-flex flex-wrap justify-center gap-2 mt-4">
|
||||
<div
|
||||
v-if="pickerMode === 'name'"
|
||||
class="d-flex flex-wrap justify-center gap-2 mt-4"
|
||||
>
|
||||
<v-chip
|
||||
:color="tempFilters.excludeLate ? 'warning' : 'default'"
|
||||
:variant="tempFilters.excludeLate ? 'elevated' : 'text'"
|
||||
@@ -165,8 +205,14 @@
|
||||
</div>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-text v-else class="text-center py-6">
|
||||
<div v-if="isAnimating" class="animation-container">
|
||||
<v-card-text
|
||||
v-else
|
||||
class="text-center py-6"
|
||||
>
|
||||
<div
|
||||
v-if="isAnimating"
|
||||
class="animation-container"
|
||||
>
|
||||
<div class="animation-wrapper">
|
||||
<transition-group
|
||||
class="shuffle-container"
|
||||
@@ -185,8 +231,13 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="result-container">
|
||||
<div class="text-h6 mb-4">抽取结果</div>
|
||||
<div
|
||||
v-else
|
||||
class="result-container"
|
||||
>
|
||||
<div class="text-h6 mb-4">
|
||||
抽取结果
|
||||
</div>
|
||||
<v-card
|
||||
v-for="(student, index) in pickedStudents"
|
||||
:key="index"
|
||||
|
||||
@@ -1,22 +1,39 @@
|
||||
<template>
|
||||
<v-dialog v-model="isVisible" max-width="500" persistent>
|
||||
<v-dialog
|
||||
v-model="isVisible"
|
||||
max-width="500"
|
||||
persistent
|
||||
>
|
||||
<v-card class="rate-limit-modal">
|
||||
<v-card-title class="text-center pa-4 bg-error text-white">
|
||||
<v-icon class="mr-2" icon="mdi-clock-alert-outline" size="large"/>
|
||||
<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>
|
||||
<div class="text-body-1 mb-4">
|
||||
您的请求过于频繁,请稍后再试。
|
||||
</div>
|
||||
|
||||
<v-card v-if="activeRequests.length > 0" class="mb-4" flat>
|
||||
<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 color="primary" prepend-icon="mdi-web">
|
||||
<v-list-item
|
||||
color="primary"
|
||||
prepend-icon="mdi-web"
|
||||
>
|
||||
<v-list-item-title>
|
||||
等待时间:
|
||||
<span class="text-primary font-weight-bold">{{
|
||||
@@ -27,12 +44,11 @@
|
||||
{{ request.method }} {{ request.path }}
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
</v-list
|
||||
>
|
||||
</v-list>
|
||||
<v-divider
|
||||
v-if="index < activeRequests.length - 1"
|
||||
class="my-3"
|
||||
></v-divider>
|
||||
/>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
@@ -42,8 +58,14 @@
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions class="pa-4 pt-0">
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="primary" variant="tonal" @click="close"> 我知道了</v-btn>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
color="primary"
|
||||
variant="tonal"
|
||||
@click="close"
|
||||
>
|
||||
我知道了
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
@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">
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
<template>
|
||||
<v-card class="settings-card rounded-lg" elevation="2">
|
||||
<v-card
|
||||
class="settings-card rounded-lg"
|
||||
elevation="2"
|
||||
>
|
||||
<v-card-item>
|
||||
<template #prepend>
|
||||
<v-icon
|
||||
@@ -8,7 +11,9 @@
|
||||
size="large"
|
||||
/>
|
||||
</template>
|
||||
<v-card-title class="text-h6">{{ title }}</v-card-title>
|
||||
<v-card-title class="text-h6">
|
||||
{{ title }}
|
||||
</v-card-title>
|
||||
</v-card-item>
|
||||
|
||||
<v-card-text>
|
||||
@@ -18,11 +23,14 @@
|
||||
color="primary"
|
||||
indeterminate
|
||||
/>
|
||||
<slot/>
|
||||
<slot />
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions v-if="$slots.actions" class="pa-4">
|
||||
<slot name="actions"/>
|
||||
<v-card-actions
|
||||
v-if="$slots.actions"
|
||||
class="pa-4"
|
||||
>
|
||||
<slot name="actions" />
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
<template>
|
||||
<div>
|
||||
<!-- 统一链接生成器卡片 -->
|
||||
<v-card border class="unified-link-generator">
|
||||
<v-card
|
||||
border
|
||||
class="unified-link-generator"
|
||||
>
|
||||
<v-card-title class="text-h6">
|
||||
<v-icon class="mr-2" icon="mdi-link-variant" start/>
|
||||
<v-icon
|
||||
class="mr-2"
|
||||
icon="mdi-link-variant"
|
||||
start
|
||||
/>
|
||||
统一链接生成器
|
||||
</v-card-title>
|
||||
|
||||
@@ -13,15 +20,23 @@
|
||||
</div>
|
||||
|
||||
<!-- 预配置认证信息部分 -->
|
||||
<v-card class="mb-4" variant="tonal">
|
||||
<v-card
|
||||
class="mb-4"
|
||||
variant="tonal"
|
||||
>
|
||||
<v-card-title class="text-subtitle-1">
|
||||
<v-icon start>mdi-account-key</v-icon>
|
||||
<v-icon start>
|
||||
mdi-account-key
|
||||
</v-icon>
|
||||
预配置认证信息
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text>
|
||||
<v-row>
|
||||
<v-col cols="12" md="6">
|
||||
<v-col
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<v-text-field
|
||||
v-model="preconfigForm.namespace"
|
||||
hint="设备的命名空间标识符"
|
||||
@@ -32,7 +47,10 @@
|
||||
variant="outlined"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6">
|
||||
<v-col
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<v-text-field
|
||||
v-model="preconfigForm.authCode"
|
||||
hint="留空则需要用户手动输入"
|
||||
@@ -64,9 +82,19 @@
|
||||
type="info"
|
||||
variant="tonal"
|
||||
>
|
||||
<div class="text-subtitle-2 mb-2">预配置信息:</div>
|
||||
<v-chip class="mr-2 mb-1" size="small">
|
||||
<v-icon size="small" start>mdi-identifier</v-icon>
|
||||
<div class="text-subtitle-2 mb-2">
|
||||
预配置信息:
|
||||
</div>
|
||||
<v-chip
|
||||
class="mr-2 mb-1"
|
||||
size="small"
|
||||
>
|
||||
<v-icon
|
||||
size="small"
|
||||
start
|
||||
>
|
||||
mdi-identifier
|
||||
</v-icon>
|
||||
命名空间: {{ preconfigForm.namespace }}
|
||||
</v-chip>
|
||||
<v-chip
|
||||
@@ -75,12 +103,27 @@
|
||||
color="warning"
|
||||
size="small"
|
||||
>
|
||||
<v-icon size="small" start>mdi-lock</v-icon>
|
||||
<v-icon
|
||||
size="small"
|
||||
start
|
||||
>
|
||||
mdi-lock
|
||||
</v-icon>
|
||||
认证码: {{ preconfigForm.authCode.length > 8 ? preconfigForm.authCode.substring(0, 8) + "..." :
|
||||
preconfigForm.authCode }}
|
||||
preconfigForm.authCode }}
|
||||
</v-chip>
|
||||
<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-else
|
||||
class="mr-2 mb-1"
|
||||
color="grey"
|
||||
size="small"
|
||||
>
|
||||
<v-icon
|
||||
size="small"
|
||||
start
|
||||
>
|
||||
mdi-lock-open
|
||||
</v-icon>
|
||||
无认证码
|
||||
</v-chip>
|
||||
<v-chip
|
||||
@@ -88,8 +131,12 @@
|
||||
class="mr-2 mb-1"
|
||||
size="small"
|
||||
>
|
||||
<v-icon size="small" start>{{
|
||||
preconfigForm.autoExecute ? "mdi-play-circle" : "mdi-hand-back-left"
|
||||
<v-icon
|
||||
size="small"
|
||||
start
|
||||
>
|
||||
{{
|
||||
preconfigForm.autoExecute ? "mdi-play-circle" : "mdi-hand-back-left"
|
||||
}}
|
||||
</v-icon>
|
||||
{{ preconfigForm.autoExecute ? "自动认证" : "手动认证" }}
|
||||
@@ -99,9 +146,14 @@
|
||||
</v-card>
|
||||
|
||||
<!-- 设置分享部分 -->
|
||||
<v-card class="mb-4" variant="tonal">
|
||||
<v-card
|
||||
class="mb-4"
|
||||
variant="tonal"
|
||||
>
|
||||
<v-card-title class="text-subtitle-1">
|
||||
<v-icon start>mdi-cog-transfer</v-icon>
|
||||
<v-icon start>
|
||||
mdi-cog-transfer
|
||||
</v-icon>
|
||||
设置分享(可选)
|
||||
</v-card-title>
|
||||
|
||||
@@ -152,7 +204,10 @@
|
||||
|
||||
<!-- 选择摘要 -->
|
||||
<div class="d-flex align-center mb-3 flex-wrap gap-2">
|
||||
<v-chip class="mr-2" color="primary">
|
||||
<v-chip
|
||||
class="mr-2"
|
||||
color="primary"
|
||||
>
|
||||
已选 {{ selectedItems.length }} 项设置
|
||||
</v-chip>
|
||||
|
||||
@@ -183,7 +238,9 @@
|
||||
<v-expansion-panel-title>
|
||||
<template #default="{ expanded }">
|
||||
<div class="d-flex align-center">
|
||||
<v-icon class="mr-2">{{ expanded ? 'mdi-chevron-up' : 'mdi-chevron-down' }}</v-icon>
|
||||
<v-icon class="mr-2">
|
||||
{{ expanded ? 'mdi-chevron-up' : 'mdi-chevron-down' }}
|
||||
</v-icon>
|
||||
显示设置列表详情
|
||||
</div>
|
||||
</template>
|
||||
@@ -261,9 +318,14 @@
|
||||
</v-card>
|
||||
|
||||
<!-- 链接生成和操作部分 -->
|
||||
<v-card class="mb-4" variant="outlined">
|
||||
<v-card
|
||||
class="mb-4"
|
||||
variant="outlined"
|
||||
>
|
||||
<v-card-title class="text-subtitle-1">
|
||||
<v-icon start>mdi-link</v-icon>
|
||||
<v-icon start>
|
||||
mdi-link
|
||||
</v-icon>
|
||||
生成的统一链接
|
||||
</v-card-title>
|
||||
|
||||
@@ -317,10 +379,20 @@
|
||||
type="success"
|
||||
variant="tonal"
|
||||
>
|
||||
<div class="text-subtitle-2 mb-2">链接包含内容:</div>
|
||||
<div class="text-subtitle-2 mb-2">
|
||||
链接包含内容:
|
||||
</div>
|
||||
<div class="d-flex flex-wrap gap-1">
|
||||
<v-chip color="primary" size="small">
|
||||
<v-icon size="small" start>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
|
||||
@@ -328,11 +400,25 @@
|
||||
color="secondary"
|
||||
size="small"
|
||||
>
|
||||
<v-icon size="small" start>mdi-cog</v-icon>
|
||||
<v-icon
|
||||
size="small"
|
||||
start
|
||||
>
|
||||
mdi-cog
|
||||
</v-icon>
|
||||
{{ selectedItems.length }} 项设置
|
||||
</v-chip>
|
||||
<v-chip v-else color="grey" size="small">
|
||||
<v-icon size="small" start>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>
|
||||
@@ -341,8 +427,13 @@
|
||||
</v-card>
|
||||
|
||||
<!-- 安全提醒 -->
|
||||
<v-alert type="warning" variant="tonal">
|
||||
<div class="text-subtitle-2 mb-2">⚠️ 安全提醒</div>
|
||||
<v-alert
|
||||
type="warning"
|
||||
variant="tonal"
|
||||
>
|
||||
<div class="text-subtitle-2 mb-2">
|
||||
⚠️ 安全提醒
|
||||
</div>
|
||||
<ul class="text-body-2 pl-4">
|
||||
<li>认证码和设置信息会在URL中传输,请谨慎分发</li>
|
||||
<li>建议仅在受信任的网络环境中使用</li>
|
||||
@@ -509,6 +600,45 @@ export default {
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
// 监听选择变化,自动生成统一链接
|
||||
selectedItems: {
|
||||
handler() {
|
||||
if (this.preconfigForm.namespace.trim()) {
|
||||
this.generateUnifiedLink();
|
||||
}
|
||||
},
|
||||
deep: true,
|
||||
},
|
||||
|
||||
// 监听预配置表单变化,自动生成统一链接
|
||||
"preconfigForm.namespace": {
|
||||
handler() {
|
||||
if (this.preconfigForm.namespace.trim()) {
|
||||
this.generateUnifiedLink();
|
||||
} else {
|
||||
this.unifiedLink = "";
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
"preconfigForm.authCode": {
|
||||
handler() {
|
||||
if (this.preconfigForm.namespace.trim()) {
|
||||
this.generateUnifiedLink();
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
"preconfigForm.autoExecute": {
|
||||
handler() {
|
||||
if (this.preconfigForm.namespace.trim()) {
|
||||
this.generateUnifiedLink();
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* 处理表格选择变化
|
||||
@@ -709,7 +839,7 @@ export default {
|
||||
|
||||
// 转换为JSON并进行base64编码
|
||||
const jsonString = JSON.stringify(configObj);
|
||||
const utf8Encoder = new TextEncoder();
|
||||
const utf8Encoder = new window.TextEncoder();
|
||||
const utf8Bytes = utf8Encoder.encode(jsonString);
|
||||
const base64String = btoa(
|
||||
Array.from(utf8Bytes)
|
||||
@@ -782,44 +912,5 @@ export default {
|
||||
this.linkCopied = false;
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
// 监听选择变化,自动生成统一链接
|
||||
selectedItems: {
|
||||
handler() {
|
||||
if (this.preconfigForm.namespace.trim()) {
|
||||
this.generateUnifiedLink();
|
||||
}
|
||||
},
|
||||
deep: true,
|
||||
},
|
||||
|
||||
// 监听预配置表单变化,自动生成统一链接
|
||||
"preconfigForm.namespace": {
|
||||
handler() {
|
||||
if (this.preconfigForm.namespace.trim()) {
|
||||
this.generateUnifiedLink();
|
||||
} else {
|
||||
this.unifiedLink = "";
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
"preconfigForm.authCode": {
|
||||
handler() {
|
||||
if (this.preconfigForm.namespace.trim()) {
|
||||
this.generateUnifiedLink();
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
"preconfigForm.autoExecute": {
|
||||
handler() {
|
||||
if (this.preconfigForm.namespace.trim()) {
|
||||
this.generateUnifiedLink();
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -35,7 +35,6 @@
|
||||
|
||||
<!-- 教师模式 -->
|
||||
<template v-else-if="isTeacherToken">
|
||||
|
||||
<!-- 名称输入框 -->
|
||||
<v-text-field
|
||||
v-model="teacherForm.name"
|
||||
@@ -48,7 +47,6 @@
|
||||
|
||||
class="mt-2 mb-4"
|
||||
>
|
||||
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
<v-chip
|
||||
v-for="teacher in filteredTeacherSuggestions"
|
||||
|
||||
@@ -33,8 +33,6 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { SettingsManager, watchSettings } from '@/utils/settings'
|
||||
|
||||
// 时间字体大小比例(大屏场景)
|
||||
const TIME_FONT_RATIO = 2.0
|
||||
const SECONDS_FONT_RATIO = 0.9
|
||||
@@ -46,7 +44,6 @@ export default {
|
||||
return {
|
||||
now: new Date(),
|
||||
timer: null,
|
||||
unwatch: null,
|
||||
fontSize: 28,
|
||||
}
|
||||
},
|
||||
@@ -106,22 +103,12 @@ export default {
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.loadSettings()
|
||||
this.startTimer()
|
||||
this.unwatch = watchSettings(() => {
|
||||
this.loadSettings()
|
||||
})
|
||||
},
|
||||
beforeUnmount() {
|
||||
this.stopTimer()
|
||||
if (this.unwatch) {
|
||||
this.unwatch()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
loadSettings() {
|
||||
this.fontSize = SettingsManager.getSetting('font.size')
|
||||
},
|
||||
startTimer() {
|
||||
// 每秒更新一次
|
||||
this.timer = setInterval(() => {
|
||||
|
||||
@@ -15,18 +15,28 @@
|
||||
<div class="urgent-title mb-6">
|
||||
{{ currentNotification?.content?.message || "无内容" }}
|
||||
</div>
|
||||
<div class="urgent-subtitle mb-6">
|
||||
<div class="urgent-subtitle mb-6">
|
||||
{{ senderName }} {{ deviceType }} {{ formatTime(currentNotification?.timestamp) }}
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<!-- 多通知导航 -->
|
||||
<div v-if="hasMultipleNotifications" class="navigation-controls mt-6">
|
||||
<v-card variant="flat" color="rgba(255,255,255,0.1)">
|
||||
<div
|
||||
v-if="hasMultipleNotifications"
|
||||
class="navigation-controls mt-6"
|
||||
>
|
||||
<v-card
|
||||
variant="flat"
|
||||
color="rgba(255,255,255,0.1)"
|
||||
>
|
||||
<v-card-text class="text-center">
|
||||
<div class="notification-counter mb-3">
|
||||
<v-chip color="white" variant="flat" size="small">
|
||||
<v-chip
|
||||
color="white"
|
||||
variant="flat"
|
||||
size="small"
|
||||
>
|
||||
{{ notificationCountText }}
|
||||
</v-chip>
|
||||
</div>
|
||||
@@ -59,8 +69,15 @@
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="mt-8">
|
||||
<v-btn color="white" size="large" variant="flat" @click="close">
|
||||
<v-icon left> mdi-check </v-icon>
|
||||
<v-btn
|
||||
color="white"
|
||||
size="large"
|
||||
variant="flat"
|
||||
@click="close"
|
||||
>
|
||||
<v-icon left>
|
||||
mdi-check
|
||||
</v-icon>
|
||||
我知道了
|
||||
</v-btn>
|
||||
</div>
|
||||
@@ -75,7 +92,7 @@
|
||||
<script>
|
||||
import EventSender from "@/components/EventSender.vue";
|
||||
import { getSetting } from "@/utils/settings.js";
|
||||
import { playSound, stopSound } from "@/utils/soundList.js";
|
||||
import { playSoundAsync, stopSound } from "@/utils/soundList.js";
|
||||
|
||||
export default {
|
||||
name: "UrgentNotification",
|
||||
@@ -297,7 +314,7 @@ export default {
|
||||
};
|
||||
return labels[deviceType] || deviceType;
|
||||
},
|
||||
playNotificationSound() {
|
||||
async playNotificationSound() {
|
||||
try {
|
||||
// 停止之前的音频
|
||||
this.stopNotificationSound();
|
||||
@@ -307,15 +324,10 @@ export default {
|
||||
? getSetting('notification.urgentSound')
|
||||
: getSetting('notification.singleSound');
|
||||
|
||||
// 使用配置的铃声文件
|
||||
this.currentAudio = playSound(soundFile, false);
|
||||
|
||||
if (!this.currentAudio) {
|
||||
// 如果播放失败,使用备用蜂鸣音
|
||||
this.playFallbackSound();
|
||||
}
|
||||
this.currentAudio = await playSoundAsync(soundFile, false);
|
||||
} catch (error) {
|
||||
console.warn("无法播放通知音效:", error);
|
||||
this.currentAudio = null;
|
||||
this.playFallbackSound();
|
||||
}
|
||||
},
|
||||
@@ -405,7 +417,7 @@ export default {
|
||||
}
|
||||
},
|
||||
// 启动加急通知的定时音效
|
||||
startUrgentSound() {
|
||||
async startUrgentSound() {
|
||||
this.stopUrgentSound(); // 先清除之前的定时器
|
||||
|
||||
// 停止之前的音频
|
||||
@@ -413,10 +425,10 @@ export default {
|
||||
|
||||
// 播放循环铃声
|
||||
const soundFile = getSetting('notification.urgentSound');
|
||||
this.currentAudio = playSound(soundFile, true); // 循环播放
|
||||
|
||||
if (!this.currentAudio) {
|
||||
// 如果播放失败,使用备用方案:每秒播放一次提示音
|
||||
try {
|
||||
this.currentAudio = await playSoundAsync(soundFile, true);
|
||||
} catch {
|
||||
this.currentAudio = null;
|
||||
this.urgentSoundTimer = setInterval(() => {
|
||||
if (this.visible && this.isUrgent) {
|
||||
this.playFallbackSound();
|
||||
|
||||
@@ -28,8 +28,6 @@
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<v-card>
|
||||
|
||||
|
||||
<v-card-text>
|
||||
<v-form>
|
||||
<v-row>
|
||||
@@ -42,15 +40,14 @@
|
||||
label="强调通知"
|
||||
color="red"
|
||||
inset
|
||||
>
|
||||
</v-switch>
|
||||
/>
|
||||
<v-checkbox
|
||||
v-model="notificationForm.isPersistent"
|
||||
label="常驻展示"
|
||||
color="primary"
|
||||
hide-details
|
||||
class="mt-0"
|
||||
></v-checkbox>
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12">
|
||||
<v-textarea
|
||||
@@ -81,8 +78,6 @@
|
||||
</v-btn>
|
||||
|
||||
<v-spacer />
|
||||
|
||||
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-col>
|
||||
@@ -93,11 +88,16 @@
|
||||
<v-col cols="12">
|
||||
<v-card>
|
||||
<v-card-title>
|
||||
<v-icon class="mr-2">mdi-pin</v-icon>
|
||||
<v-icon class="mr-2">
|
||||
mdi-pin
|
||||
</v-icon>
|
||||
常驻通知管理
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<div v-if="persistentNotifications.length === 0" class="text-center text-grey py-4">
|
||||
<div
|
||||
v-if="persistentNotifications.length === 0"
|
||||
class="text-center text-grey py-4"
|
||||
>
|
||||
暂无常驻通知
|
||||
</div>
|
||||
<v-list v-else>
|
||||
@@ -108,14 +108,25 @@
|
||||
:subtitle="formatTime(item.timestamp)"
|
||||
lines="two"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<template #prepend>
|
||||
<v-icon :color="item.isUrgent ? 'error' : 'primary'">
|
||||
{{ item.isUrgent ? 'mdi-alert-circle' : 'mdi-information' }}
|
||||
</v-icon>
|
||||
</template>
|
||||
<template v-slot:append>
|
||||
<v-btn icon="mdi-pencil" variant="text" size="small" @click="openEditDialog(item)"></v-btn>
|
||||
<v-btn icon="mdi-delete" variant="text" color="error" size="small" @click="deletePersistentNotification(item.id)"></v-btn>
|
||||
<template #append>
|
||||
<v-btn
|
||||
icon="mdi-pencil"
|
||||
variant="text"
|
||||
size="small"
|
||||
@click="openEditDialog(item)"
|
||||
/>
|
||||
<v-btn
|
||||
icon="mdi-delete"
|
||||
variant="text"
|
||||
color="error"
|
||||
size="small"
|
||||
@click="deletePersistentNotification(item.id)"
|
||||
/>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
@@ -201,17 +212,14 @@
|
||||
>
|
||||
<v-card-text class="pa-2">
|
||||
<div class="align-center">
|
||||
|
||||
<span class="text-body-2 font-weight-medium">{{ device.deviceName }} </span>
|
||||
<br/>
|
||||
<br>
|
||||
|
||||
{{ device.deviceType }}
|
||||
|
||||
</div>
|
||||
<div class="text-caption mt-1">
|
||||
已读于 {{ formatDeviceTime(device.timestamp) }}
|
||||
</div>
|
||||
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
@@ -226,7 +234,6 @@
|
||||
>
|
||||
<v-card-text class="pa-2">
|
||||
<div class="align-center">
|
||||
|
||||
<span class="text-body-2 font-weight-medium">{{ device.deviceName }}</span>
|
||||
<v-spacer />
|
||||
<span class="text-caption text-grey">
|
||||
@@ -238,9 +245,9 @@
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
</div>
|
||||
<div v-else> <v-card
|
||||
<div v-else>
|
||||
<v-card
|
||||
|
||||
color="info-lighten-4"
|
||||
variant="outlined"
|
||||
@@ -249,9 +256,10 @@
|
||||
title="无设备在线"
|
||||
>
|
||||
<v-card-text>
|
||||
如果数秒后任然显示这个提示,则可能没有任何设备在线接收通知。
|
||||
如果数秒后任然显示这个提示,则可能没有任何设备在线接收通知。
|
||||
</v-card-text>
|
||||
</v-card></div>
|
||||
</v-card>
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
@@ -266,12 +274,22 @@
|
||||
<EventSender ref="eventSender" />
|
||||
|
||||
<!-- 编辑常驻通知对话框 -->
|
||||
<v-dialog v-model="editDialog" max-width="500" :fullscreen="$vuetify.display.xs">
|
||||
<v-dialog
|
||||
v-model="editDialog"
|
||||
max-width="500"
|
||||
:fullscreen="$vuetify.display.xs"
|
||||
>
|
||||
<v-card>
|
||||
<v-toolbar flat density="compact">
|
||||
<v-toolbar
|
||||
flat
|
||||
density="compact"
|
||||
>
|
||||
<v-toolbar-title>编辑常驻通知</v-toolbar-title>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn icon="mdi-close" @click="editDialog = false"></v-btn>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
icon="mdi-close"
|
||||
@click="editDialog = false"
|
||||
/>
|
||||
</v-toolbar>
|
||||
<v-card-text>
|
||||
<v-form>
|
||||
@@ -280,38 +298,66 @@
|
||||
label="通知内容"
|
||||
rows="3"
|
||||
auto-grow
|
||||
></v-textarea>
|
||||
/>
|
||||
<v-switch
|
||||
v-model="editForm.isUrgent"
|
||||
label="强调通知"
|
||||
color="error"
|
||||
hide-details
|
||||
></v-switch>
|
||||
/>
|
||||
<v-checkbox
|
||||
v-model="editForm.resend"
|
||||
label="保存并重新发送通知"
|
||||
hint="勾选后将作为新通知发送给所有在线设备"
|
||||
persistent-hint
|
||||
></v-checkbox>
|
||||
/>
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn variant="text" @click="editDialog = false">取消</v-btn>
|
||||
<v-btn color="primary" :loading="savingEdit" @click="saveEdit">保存</v-btn>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
variant="text"
|
||||
@click="editDialog = false"
|
||||
>
|
||||
取消
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="primary"
|
||||
:loading="savingEdit"
|
||||
@click="saveEdit"
|
||||
>
|
||||
保存
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<!-- 删除确认对话框 -->
|
||||
<v-dialog v-model="deleteConfirmDialog" max-width="400">
|
||||
<v-dialog
|
||||
v-model="deleteConfirmDialog"
|
||||
max-width="400"
|
||||
>
|
||||
<v-card>
|
||||
<v-card-title class="text-h5">确认删除</v-card-title>
|
||||
<v-card-title class="text-h5">
|
||||
确认删除
|
||||
</v-card-title>
|
||||
<v-card-text>确定要删除这条常驻通知吗?此操作无法撤销。</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="grey-darken-1" variant="text" @click="deleteConfirmDialog = false">取消</v-btn>
|
||||
<v-btn color="error" variant="text" @click="executeDelete">删除</v-btn>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
color="grey-darken-1"
|
||||
variant="text"
|
||||
@click="deleteConfirmDialog = false"
|
||||
>
|
||||
取消
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="error"
|
||||
variant="text"
|
||||
@click="executeDelete"
|
||||
>
|
||||
删除
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
@@ -0,0 +1,230 @@
|
||||
<template>
|
||||
<v-overlay
|
||||
v-model="visible"
|
||||
class="align-center justify-center"
|
||||
persistent
|
||||
opacity="0.9"
|
||||
color="#121212"
|
||||
z-index="9000"
|
||||
>
|
||||
<div
|
||||
class="text-center pa-4"
|
||||
style="max-width: 80vw; max-height: 90vh; width: 600px;"
|
||||
>
|
||||
<v-card
|
||||
color="grey-darken-4"
|
||||
elevation="24"
|
||||
class="rounded-xl border-thin"
|
||||
style="border-color: rgba(255,255,255,0.1) !important;"
|
||||
>
|
||||
<!-- Header -->
|
||||
<v-card-title class="d-flex align-center justify-center py-4 bg-grey-darken-3">
|
||||
<v-icon
|
||||
color="orange"
|
||||
size="large"
|
||||
class="mr-3"
|
||||
>
|
||||
mdi-bullhorn
|
||||
</v-icon>
|
||||
<span class="text-h5 font-weight-bold">通知/公告</span>
|
||||
</v-card-title>
|
||||
|
||||
<v-divider />
|
||||
|
||||
<!-- Content -->
|
||||
<v-card-text class="pa-6">
|
||||
<div
|
||||
class="text-h5 text-white markdown-content text-left"
|
||||
v-html="parsedContent"
|
||||
/>
|
||||
</v-card-text>
|
||||
|
||||
<v-divider v-if="duration <= 0" />
|
||||
|
||||
<!-- Actions -->
|
||||
<v-card-actions class="pa-4 justify-center">
|
||||
<v-btn
|
||||
v-if="duration > 0"
|
||||
color="grey-lighten-1"
|
||||
variant="tonal"
|
||||
size="large"
|
||||
block
|
||||
@click="close"
|
||||
>
|
||||
关闭 ({{ remaining }}s)
|
||||
</v-btn>
|
||||
<v-btn
|
||||
v-else
|
||||
color="primary"
|
||||
variant="flat"
|
||||
size="large"
|
||||
block
|
||||
@click="close"
|
||||
>
|
||||
我知道了
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</div>
|
||||
</v-overlay>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import dataProvider from "@/utils/dataProvider";
|
||||
import { parseMarkdown } from "@/utils/markdown";
|
||||
import { playSoundAsync, stopSound } from "@/utils/soundList.js";
|
||||
|
||||
export default {
|
||||
name: "AnnouncementOverlay",
|
||||
data() {
|
||||
return {
|
||||
visible: false,
|
||||
currentNotification: null,
|
||||
currentAudio: null,
|
||||
settings: {
|
||||
notifications: [],
|
||||
enableSound: true
|
||||
},
|
||||
timer: null,
|
||||
remaining: 0,
|
||||
countdownTimer: null,
|
||||
lastTriggeredTime: null
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
parsedContent() {
|
||||
if (!this.currentNotification?.content) return '';
|
||||
return parseMarkdown(this.currentNotification.content);
|
||||
},
|
||||
duration() {
|
||||
return this.currentNotification?.duration || 0;
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.loadSettings();
|
||||
// Check every second
|
||||
this.timer = setInterval(this.checkTime, 1000);
|
||||
},
|
||||
beforeUnmount() {
|
||||
if (this.timer) clearInterval(this.timer);
|
||||
if (this.countdownTimer) clearInterval(this.countdownTimer);
|
||||
this.stopSound();
|
||||
},
|
||||
methods: {
|
||||
async loadSettings() {
|
||||
try {
|
||||
const res = await dataProvider.loadData('announcement-settings');
|
||||
if (res && res.success !== false) {
|
||||
this.settings = {
|
||||
notifications: res.notifications || [],
|
||||
enableSound: res.enableSound !== false
|
||||
};
|
||||
} else {
|
||||
this.settings = { notifications: [], enableSound: true };
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Load announcement settings failed", e);
|
||||
this.settings = { notifications: [], enableSound: true };
|
||||
}
|
||||
},
|
||||
checkTime() {
|
||||
if (this.visible) return; // Already showing
|
||||
if (!this.settings || !this.settings.notifications) return;
|
||||
|
||||
const now = new Date();
|
||||
const hh = String(now.getHours()).padStart(2, '0');
|
||||
const mm = String(now.getMinutes()).padStart(2, '0');
|
||||
const ss = String(now.getSeconds()).padStart(2, '0');
|
||||
const currentTime = `${hh}:${mm}:${ss}`;
|
||||
|
||||
// Avoid re-triggering same second
|
||||
if (this.lastTriggeredTime === currentTime) return;
|
||||
|
||||
const matched = this.settings.notifications.find(n => {
|
||||
const noteTime = n.time.length === 5 ? `${n.time}:00` : n.time;
|
||||
return noteTime === currentTime;
|
||||
});
|
||||
|
||||
if (matched) {
|
||||
this.show(matched);
|
||||
this.lastTriggeredTime = currentTime;
|
||||
}
|
||||
},
|
||||
show(notification) {
|
||||
this.currentNotification = notification;
|
||||
this.visible = true;
|
||||
this.remaining = notification.duration;
|
||||
|
||||
if (this.settings.enableSound) {
|
||||
this.playSound();
|
||||
}
|
||||
|
||||
if (this.remaining > 0) {
|
||||
// Clear any existing countdown first
|
||||
if (this.countdownTimer) clearInterval(this.countdownTimer);
|
||||
|
||||
this.countdownTimer = setInterval(() => {
|
||||
this.remaining--;
|
||||
if (this.remaining <= 0) {
|
||||
this.close();
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
},
|
||||
close() {
|
||||
this.visible = false;
|
||||
this.currentNotification = null;
|
||||
if (this.countdownTimer) {
|
||||
clearInterval(this.countdownTimer);
|
||||
this.countdownTimer = null;
|
||||
}
|
||||
this.stopSound();
|
||||
},
|
||||
async playSound() {
|
||||
try {
|
||||
this.stopSound();
|
||||
this.currentAudio = await playSoundAsync("universfield-new-notification-024-370048.mp3", false);
|
||||
} catch (e) {
|
||||
this.playFallbackSound();
|
||||
}
|
||||
},
|
||||
playFallbackSound() {
|
||||
try {
|
||||
const ctx = new (window.AudioContext || window.webkitAudioContext)();
|
||||
const osc = ctx.createOscillator();
|
||||
const gain = ctx.createGain();
|
||||
osc.connect(gain);
|
||||
gain.connect(ctx.destination);
|
||||
osc.frequency.value = 440;
|
||||
osc.type = 'sine';
|
||||
gain.gain.value = 0.1;
|
||||
osc.start();
|
||||
setTimeout(() => {
|
||||
osc.stop();
|
||||
ctx.close();
|
||||
}, 500);
|
||||
} catch {
|
||||
}
|
||||
},
|
||||
stopSound() {
|
||||
if (this.currentAudio) {
|
||||
stopSound(this.currentAudio);
|
||||
this.currentAudio = null;
|
||||
}
|
||||
},
|
||||
reload() {
|
||||
this.loadSettings();
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.markdown-content :deep(h1) { font-size: 1.5em; margin-bottom: 0.5em; font-weight: bold; color: white !important; }
|
||||
.markdown-content :deep(h2) { font-size: 1.3em; margin-bottom: 0.5em; font-weight: bold; color: white !important; }
|
||||
.markdown-content :deep(p) { margin-bottom: 0.5em; color: white !important; }
|
||||
.markdown-content :deep(ul) { padding-left: 1.5em; margin-bottom: 0.5em; color: white !important; }
|
||||
.markdown-content :deep(li) { margin-bottom: 0.25em; }
|
||||
.markdown-content :deep(mark) { background-color: yellow; color: black !important; padding: 0 4px; border-radius: 2px; }
|
||||
.markdown-content :deep(hr) { margin: 1em 0; border-color: rgba(255,255,255,0.2); }
|
||||
</style>
|
||||
@@ -0,0 +1,579 @@
|
||||
<template>
|
||||
<v-dialog
|
||||
v-model="dialogVisible"
|
||||
max-width="600"
|
||||
scrollable
|
||||
>
|
||||
<v-card color="#1E1E1E">
|
||||
<v-toolbar
|
||||
color="transparent"
|
||||
density="compact"
|
||||
>
|
||||
<v-toolbar-title class="text-h6 font-weight-bold ml-4">
|
||||
公告板设置
|
||||
</v-toolbar-title>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
icon="mdi-close"
|
||||
variant="text"
|
||||
@click="dialogVisible = false"
|
||||
/>
|
||||
</v-toolbar>
|
||||
|
||||
<v-card-text class="pa-4">
|
||||
<!-- Settings Section -->
|
||||
<v-card
|
||||
variant="outlined"
|
||||
class="mb-4"
|
||||
color="grey-darken-3"
|
||||
>
|
||||
<v-card-text>
|
||||
<div class="d-flex align-center mb-2">
|
||||
<v-icon
|
||||
start
|
||||
icon="mdi-bullhorn"
|
||||
class="mr-2"
|
||||
/>
|
||||
<span class="text-subtitle-1 font-weight-bold">公告板设置</span>
|
||||
</div>
|
||||
<div class="text-caption text-grey-lighten-1 mb-4">
|
||||
设置定时弹出的公告和提示音
|
||||
</div>
|
||||
|
||||
<v-switch
|
||||
v-model="enableSound"
|
||||
color="primary"
|
||||
label="启用提示音"
|
||||
hide-details
|
||||
inset
|
||||
/>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<v-divider class="mb-4" />
|
||||
|
||||
<!-- List Section -->
|
||||
<div class="d-flex align-center mb-2">
|
||||
<span class="text-subtitle-1 font-weight-bold">定时公告列表</span>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
color="primary"
|
||||
variant="text"
|
||||
prepend-icon="mdi-plus"
|
||||
@click="openAddDialog"
|
||||
>
|
||||
添加公告
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="notifications.length === 0"
|
||||
class="text-center text-grey-lighten-1 py-8"
|
||||
>
|
||||
暂无定时公告
|
||||
</div>
|
||||
|
||||
<v-card
|
||||
v-for="note in notifications"
|
||||
:key="note.id"
|
||||
class="mb-2"
|
||||
variant="outlined"
|
||||
color="grey-darken-3"
|
||||
>
|
||||
<v-card-text class="d-flex align-center py-2">
|
||||
<div class="mr-4 text-h6 font-weight-bold">
|
||||
{{ note.time }}
|
||||
</div>
|
||||
<div class="flex-grow-1 text-truncate text-grey-lighten-1">
|
||||
{{ note.content || '无内容' }}
|
||||
</div>
|
||||
<div
|
||||
v-if="note.duration > 0"
|
||||
class="text-caption text-grey-lighten-1 mr-2"
|
||||
>
|
||||
{{ note.duration }}秒
|
||||
</div>
|
||||
<v-btn
|
||||
icon="mdi-pencil"
|
||||
variant="text"
|
||||
color="info"
|
||||
size="small"
|
||||
class="mr-2"
|
||||
@click="editNotification(note)"
|
||||
/>
|
||||
<v-btn
|
||||
icon="mdi-delete"
|
||||
variant="text"
|
||||
color="error"
|
||||
size="small"
|
||||
@click="deleteNotification(note.id)"
|
||||
/>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
color="primary"
|
||||
@click="dialogVisible = false"
|
||||
>
|
||||
关闭
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
|
||||
<!-- Add/Edit Dialog -->
|
||||
<v-dialog
|
||||
v-model="addDialogVisible"
|
||||
max-width="800"
|
||||
persistent
|
||||
>
|
||||
<v-card color="#1E1E1E">
|
||||
<v-toolbar color="transparent">
|
||||
<v-toolbar-title>{{ isEditing ? '编辑公告' : '添加公告' }}</v-toolbar-title>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
color="primary"
|
||||
variant="tonal"
|
||||
prepend-icon="mdi-help-circle-outline"
|
||||
class="mr-2"
|
||||
@click="showHelpDialog = true"
|
||||
>
|
||||
使用帮助
|
||||
</v-btn>
|
||||
</v-toolbar>
|
||||
|
||||
<v-card-text>
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<div class="text-caption text-grey-lighten-1 mb-1">
|
||||
时间 (HH:mm:ss)
|
||||
</div>
|
||||
<v-text-field
|
||||
v-model="newItem.time"
|
||||
type="time"
|
||||
step="1"
|
||||
variant="outlined"
|
||||
prepend-inner-icon="mdi-clock-outline"
|
||||
hide-details
|
||||
bg-color="grey-darken-4"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12">
|
||||
<div class="text-caption text-grey-lighten-1 mb-1">
|
||||
持续时间 (秒, 0为不自动关闭)
|
||||
</div>
|
||||
<v-text-field
|
||||
v-model.number="newItem.duration"
|
||||
type="number"
|
||||
variant="outlined"
|
||||
prepend-inner-icon="mdi-timer-sand"
|
||||
hide-details
|
||||
bg-color="grey-darken-4"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row class="mt-2">
|
||||
<v-col
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<div class="d-flex align-center justify-space-between mb-2 flex-wrap">
|
||||
<span class="text-caption text-grey-lighten-1">编辑内容 (Markdown)</span>
|
||||
<!-- Formatting Toolbar -->
|
||||
<div class="d-flex align-center">
|
||||
<v-btn
|
||||
size="x-small"
|
||||
variant="text"
|
||||
icon="mdi-format-bold"
|
||||
@click="insertFormat('**', '**')"
|
||||
/>
|
||||
<v-btn
|
||||
size="x-small"
|
||||
variant="text"
|
||||
icon="mdi-format-italic"
|
||||
@click="insertFormat('*', '*')"
|
||||
/>
|
||||
<v-btn
|
||||
size="x-small"
|
||||
variant="text"
|
||||
icon="mdi-format-header-1"
|
||||
@click="insertFormat('# ', '')"
|
||||
/>
|
||||
<v-btn
|
||||
size="x-small"
|
||||
variant="text"
|
||||
icon="mdi-format-header-2"
|
||||
@click="insertFormat('## ', '')"
|
||||
/>
|
||||
<v-btn
|
||||
size="x-small"
|
||||
variant="text"
|
||||
icon="mdi-format-list-bulleted"
|
||||
@click="insertFormat('- ', '')"
|
||||
/>
|
||||
<v-btn
|
||||
size="x-small"
|
||||
variant="text"
|
||||
icon="mdi-minus"
|
||||
@click="insertFormat('\n---\n', '')"
|
||||
/>
|
||||
<v-btn
|
||||
size="x-small"
|
||||
variant="text"
|
||||
icon="mdi-marker"
|
||||
color="yellow"
|
||||
@click="insertFormat('<mark>', '</mark>')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<v-textarea
|
||||
ref="editor"
|
||||
v-model="newItem.content"
|
||||
variant="outlined"
|
||||
rows="10"
|
||||
no-resize
|
||||
bg-color="grey-darken-4"
|
||||
hide-details
|
||||
/>
|
||||
</v-col>
|
||||
<v-col
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<div class="text-caption text-grey-lighten-1 mb-2">
|
||||
预览区域
|
||||
</div>
|
||||
<v-card
|
||||
variant="outlined"
|
||||
color="grey-darken-4"
|
||||
height="266"
|
||||
class="pa-4 overflow-y-auto"
|
||||
>
|
||||
<div
|
||||
class="markdown-preview text-white"
|
||||
v-html="renderMarkdown(newItem.content)"
|
||||
/>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
variant="text"
|
||||
@click="addDialogVisible = false"
|
||||
>
|
||||
取消
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="primary"
|
||||
@click="saveNotification"
|
||||
>
|
||||
保存
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<!-- Help Dialog -->
|
||||
<v-dialog
|
||||
v-model="showHelpDialog"
|
||||
max-width="700"
|
||||
>
|
||||
<v-card color="#1E1E1E">
|
||||
<v-toolbar
|
||||
color="blue"
|
||||
density="compact"
|
||||
>
|
||||
<v-icon class="ml-4">
|
||||
mdi-school
|
||||
</v-icon>
|
||||
<v-toolbar-title>如何使用公告板</v-toolbar-title>
|
||||
<v-btn
|
||||
icon="mdi-close"
|
||||
@click="showHelpDialog = false"
|
||||
/>
|
||||
</v-toolbar>
|
||||
|
||||
<v-card-text class="pa-0">
|
||||
<v-row no-gutters>
|
||||
<v-col
|
||||
cols="12"
|
||||
md="6"
|
||||
class="pa-4 border-e border-grey-darken-3"
|
||||
>
|
||||
<h3 class="text-h6 text-blue mb-4">
|
||||
<v-icon
|
||||
color="blue"
|
||||
class="mr-2"
|
||||
>
|
||||
mdi-clock-outline
|
||||
</v-icon>
|
||||
基础设置
|
||||
</h3>
|
||||
|
||||
<div class="mb-4">
|
||||
<div class="text-subtitle-2 font-weight-bold">
|
||||
设定时间
|
||||
</div>
|
||||
<div class="text-caption text-grey">
|
||||
设置公告自动弹出的时间(24小时制)。到达该时间时,全屏会显示此公告。
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<div class="text-subtitle-2 font-weight-bold">
|
||||
持续时间
|
||||
</div>
|
||||
<div class="text-caption text-grey">
|
||||
公告显示的秒数。设为 0 则不会自动关闭,需要手动点击屏幕关闭。
|
||||
</div>
|
||||
</div>
|
||||
</v-col>
|
||||
|
||||
<v-col
|
||||
cols="12"
|
||||
md="6"
|
||||
class="pa-4"
|
||||
>
|
||||
<h3 class="text-h6 text-green mb-4">
|
||||
<v-icon
|
||||
color="green"
|
||||
class="mr-2"
|
||||
>
|
||||
mdi-format-paint
|
||||
</v-icon>
|
||||
排版与美化
|
||||
</h3>
|
||||
|
||||
<div class="mb-2">
|
||||
<div class="text-subtitle-2 font-weight-bold">
|
||||
加粗 / 斜体
|
||||
</div>
|
||||
<div class="text-caption text-grey">
|
||||
选中文字点击按钮,强调重要信息
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-2">
|
||||
<div class="text-subtitle-2 font-weight-bold">
|
||||
大标题 / 小标题
|
||||
</div>
|
||||
<div class="text-caption text-grey">
|
||||
使用 H1/H2 区分不同板块
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-2">
|
||||
<div class="text-subtitle-2 font-weight-bold">
|
||||
高亮标记 (Mark)
|
||||
</div>
|
||||
<div class="text-caption text-grey">
|
||||
先选中文字,再点击颜色按钮,会自动包裹。
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-2">
|
||||
<div class="text-subtitle-2 font-weight-bold">
|
||||
分割线
|
||||
</div>
|
||||
<div class="text-caption text-grey">
|
||||
插入水平线分隔内容
|
||||
</div>
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions class="bg-grey-darken-4 pa-4">
|
||||
<v-alert
|
||||
color="info"
|
||||
variant="tonal"
|
||||
icon="mdi-eye"
|
||||
density="compact"
|
||||
class="flex-grow-1 mr-4"
|
||||
>
|
||||
<div class="text-caption">
|
||||
实时预览:右侧窗口会实时显示最终效果,所见即所得。
|
||||
</div>
|
||||
</v-alert>
|
||||
<v-btn
|
||||
color="primary"
|
||||
min-width="100"
|
||||
@click="showHelpDialog = false"
|
||||
>
|
||||
我学会了
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { parseMarkdown } from "@/utils/markdown";
|
||||
import dataProvider from "@/utils/dataProvider";
|
||||
|
||||
export default {
|
||||
name: "AnnouncementSettingsDialog",
|
||||
props: {
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
emits: ["update:modelValue", "save"],
|
||||
data() {
|
||||
return {
|
||||
enableSound: true,
|
||||
notifications: [],
|
||||
addDialogVisible: false,
|
||||
showHelpDialog: false,
|
||||
isEditing: false,
|
||||
editingId: null,
|
||||
newItem: {
|
||||
time: "12:00:00",
|
||||
duration: 5,
|
||||
content: ""
|
||||
}
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
dialogVisible: {
|
||||
get() {
|
||||
return this.modelValue;
|
||||
},
|
||||
set(value) {
|
||||
this.$emit("update:modelValue", value);
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
modelValue(val) {
|
||||
if (val) {
|
||||
this.loadSettings();
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async loadSettings() {
|
||||
try {
|
||||
const res = await dataProvider.loadData('announcement-settings');
|
||||
if (res && res.success !== false) {
|
||||
this.notifications = res.notifications || [];
|
||||
this.enableSound = res.enableSound !== false;
|
||||
} else {
|
||||
this.notifications = [];
|
||||
this.enableSound = true;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Load announcements failed", e);
|
||||
}
|
||||
},
|
||||
openAddDialog() {
|
||||
this.isEditing = false;
|
||||
this.editingId = null;
|
||||
this.newItem = {
|
||||
time: "12:00:00",
|
||||
duration: 5,
|
||||
content: ""
|
||||
};
|
||||
this.addDialogVisible = true;
|
||||
},
|
||||
editNotification(note) {
|
||||
this.isEditing = true;
|
||||
this.editingId = note.id;
|
||||
this.newItem = { ...note };
|
||||
// Ensure time format includes seconds if missing
|
||||
if (this.newItem.time && this.newItem.time.length === 5) {
|
||||
this.newItem.time = `${this.newItem.time}:00`;
|
||||
}
|
||||
this.addDialogVisible = true;
|
||||
},
|
||||
async saveNotification() {
|
||||
if (!this.newItem.content) return;
|
||||
|
||||
// Ensure time format includes seconds if missing
|
||||
if (this.newItem.time && this.newItem.time.length === 5) {
|
||||
this.newItem.time = `${this.newItem.time}:00`;
|
||||
}
|
||||
|
||||
if (this.isEditing) {
|
||||
const index = this.notifications.findIndex(n => n.id === this.editingId);
|
||||
if (index !== -1) {
|
||||
this.notifications[index] = { ...this.newItem, id: this.editingId };
|
||||
}
|
||||
} else {
|
||||
this.notifications.push({
|
||||
id: Date.now().toString(),
|
||||
...this.newItem
|
||||
});
|
||||
}
|
||||
|
||||
// Sort by time
|
||||
this.notifications.sort((a, b) => a.time.localeCompare(b.time));
|
||||
|
||||
await this.saveAll();
|
||||
this.addDialogVisible = false;
|
||||
},
|
||||
async deleteNotification(id) {
|
||||
this.notifications = this.notifications.filter(n => n.id !== id);
|
||||
await this.saveAll();
|
||||
},
|
||||
async saveAll() {
|
||||
await dataProvider.saveData('announcement-settings', {
|
||||
notifications: this.notifications,
|
||||
enableSound: this.enableSound
|
||||
});
|
||||
this.$emit('save'); // Notify parent to reload overlay settings
|
||||
},
|
||||
renderMarkdown(content) {
|
||||
if (!content) return '';
|
||||
try {
|
||||
return parseMarkdown(content);
|
||||
} catch (e) {
|
||||
return content;
|
||||
}
|
||||
},
|
||||
insertFormat(prefix, suffix) {
|
||||
if (!this.$refs.editor) return;
|
||||
const textarea = this.$refs.editor.$el.querySelector('textarea');
|
||||
if (!textarea) return;
|
||||
|
||||
const start = textarea.selectionStart;
|
||||
const end = textarea.selectionEnd;
|
||||
const text = this.newItem.content || "";
|
||||
|
||||
const before = text.substring(0, start);
|
||||
const selected = text.substring(start, end);
|
||||
const after = text.substring(end);
|
||||
|
||||
this.newItem.content = before + prefix + selected + suffix + after;
|
||||
|
||||
this.$nextTick(() => {
|
||||
textarea.focus();
|
||||
const newCursorPos = start + prefix.length + selected.length + suffix.length;
|
||||
textarea.setSelectionRange(newCursorPos, newCursorPos);
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.gap-2 { gap: 8px; }
|
||||
/* Ensure preview text is visible against dark background */
|
||||
.markdown-preview {
|
||||
color: white !important;
|
||||
}
|
||||
.markdown-preview :deep(h1) { font-size: 1.5em; font-weight: bold; margin-bottom: 0.5em; color: white !important; }
|
||||
.markdown-preview :deep(h2) { font-size: 1.25em; font-weight: bold; margin-bottom: 0.5em; color: white !important; }
|
||||
.markdown-preview :deep(ul) { padding-left: 1.5em; margin-bottom: 0.5em; color: white !important; }
|
||||
.markdown-preview :deep(p) { margin-bottom: 0.5em; color: white !important; }
|
||||
.markdown-preview :deep(mark) { background-color: yellow; color: black !important; padding: 0 2px; border-radius: 2px; }
|
||||
.markdown-preview :deep(hr) { border-color: rgba(255, 255, 255, 0.2); margin: 1em 0; }
|
||||
</style>
|
||||
@@ -0,0 +1,224 @@
|
||||
<template>
|
||||
<v-card
|
||||
class="mb-4"
|
||||
variant="outlined"
|
||||
color="grey-darken-3"
|
||||
>
|
||||
<v-card-title class="d-flex align-center py-2 px-3">
|
||||
<v-icon
|
||||
size="small"
|
||||
class="mr-2"
|
||||
color="orange"
|
||||
>
|
||||
mdi-bullhorn
|
||||
</v-icon>
|
||||
<span class="text-subtitle-2 font-weight-bold">公告板</span>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
icon="mdi-cog"
|
||||
variant="text"
|
||||
size="small"
|
||||
color="grey"
|
||||
@click="$emit('open-settings')"
|
||||
/>
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text class="pa-0">
|
||||
<div v-if="notifications.length > 0">
|
||||
<div
|
||||
v-for="(note, index) in notifications"
|
||||
:key="index"
|
||||
class="px-3 py-2 border-b cursor-pointer hover-bg"
|
||||
style="border-bottom: 1px solid rgba(255,255,255,0.05);"
|
||||
@click="showDetail(note)"
|
||||
>
|
||||
<div class="d-flex align-center justify-space-between mb-1">
|
||||
<div class="d-flex align-center">
|
||||
<v-icon
|
||||
size="x-small"
|
||||
color="primary"
|
||||
class="mr-1"
|
||||
>
|
||||
mdi-clock-outline
|
||||
</v-icon>
|
||||
<span class="text-caption font-weight-bold text-primary">{{ note.time }}</span>
|
||||
</div>
|
||||
<v-chip
|
||||
v-if="isNext(note)"
|
||||
size="x-small"
|
||||
color="success"
|
||||
variant="flat"
|
||||
class="px-1"
|
||||
style="height: 16px; font-size: 10px;"
|
||||
>
|
||||
NEXT
|
||||
</v-chip>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="text-caption text-grey-lighten-1 markdown-preview-mini"
|
||||
v-html="renderMiniMarkdown(note.content)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="pa-4 text-center text-caption text-grey"
|
||||
>
|
||||
暂无公告安排
|
||||
</div>
|
||||
</v-card-text>
|
||||
|
||||
<!-- Detail Dialog -->
|
||||
<v-dialog
|
||||
v-model="detailDialog"
|
||||
max-width="500"
|
||||
>
|
||||
<v-card color="grey-darken-4">
|
||||
<v-card-title class="d-flex align-center">
|
||||
<v-icon
|
||||
color="primary"
|
||||
class="mr-2"
|
||||
>
|
||||
mdi-bullhorn-outline
|
||||
</v-icon>
|
||||
公告详情
|
||||
<v-spacer />
|
||||
<v-chip
|
||||
color="primary"
|
||||
variant="outlined"
|
||||
size="small"
|
||||
>
|
||||
{{ selectedNote?.time }}
|
||||
</v-chip>
|
||||
</v-card-title>
|
||||
<v-card-text class="pa-4">
|
||||
<div
|
||||
class="markdown-preview text-white"
|
||||
v-html="renderFullMarkdown(selectedNote?.content)"
|
||||
/>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
color="primary"
|
||||
variant="text"
|
||||
@click="detailDialog = false"
|
||||
>
|
||||
关闭
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import dataProvider from "@/utils/dataProvider";
|
||||
import { parseMarkdown } from "@/utils/markdown";
|
||||
|
||||
export default {
|
||||
name: "AnnouncementWidget",
|
||||
emits: ["open-settings"],
|
||||
data() {
|
||||
return {
|
||||
notifications: [],
|
||||
timer: null,
|
||||
detailDialog: false,
|
||||
selectedNote: null
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.loadSettings();
|
||||
// Refresh every second to update "NEXT" status
|
||||
this.timer = setInterval(() => {
|
||||
this.$forceUpdate();
|
||||
}, 1000);
|
||||
},
|
||||
beforeUnmount() {
|
||||
if (this.timer) clearInterval(this.timer);
|
||||
},
|
||||
methods: {
|
||||
async loadSettings() {
|
||||
try {
|
||||
const res = await dataProvider.loadData('announcement-settings');
|
||||
if (res && res.success !== false && res.notifications) {
|
||||
this.notifications = res.notifications.sort((a, b) => a.time.localeCompare(b.time));
|
||||
} else {
|
||||
this.notifications = [];
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Load announcements failed", e);
|
||||
}
|
||||
},
|
||||
isNext(note) {
|
||||
if (!this.notifications.length) return false;
|
||||
|
||||
const now = new Date();
|
||||
const hh = String(now.getHours()).padStart(2, '0');
|
||||
const mm = String(now.getMinutes()).padStart(2, '0');
|
||||
const ss = String(now.getSeconds()).padStart(2, '0');
|
||||
const currentTime = `${hh}:${mm}:${ss}`;
|
||||
|
||||
// Find the first notification that is strictly after current time
|
||||
const next = this.notifications.find(n => {
|
||||
const noteTime = n.time.length === 5 ? `${n.time}:00` : n.time;
|
||||
return noteTime > currentTime;
|
||||
});
|
||||
|
||||
if (next) {
|
||||
return note === next;
|
||||
} else {
|
||||
// If all today's times passed, the first one tomorrow is next
|
||||
return note === this.notifications[0];
|
||||
}
|
||||
},
|
||||
renderMiniMarkdown(text) {
|
||||
if (!text) return '';
|
||||
// Render full markdown but stripped of block tags for mini view or just first line?
|
||||
// Better: Render full markdown but restrict height/overflow in CSS
|
||||
// Or: Just render it. The user said "use markdown format"
|
||||
return parseMarkdown(text);
|
||||
},
|
||||
renderFullMarkdown(text) {
|
||||
return parseMarkdown(text);
|
||||
},
|
||||
showDetail(note) {
|
||||
this.selectedNote = note;
|
||||
this.detailDialog = true;
|
||||
},
|
||||
reload() {
|
||||
this.loadSettings();
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.hover-bg:hover {
|
||||
background-color: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
.markdown-preview-mini {
|
||||
max-height: 60px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
/* Add fade out effect at bottom */
|
||||
mask-image: linear-gradient(to bottom, black 60%, transparent 100%);
|
||||
-webkit-mask-image: linear-gradient(to bottom, black 60%, transparent 100%);
|
||||
}
|
||||
|
||||
/* Re-use markdown styles from settings dialog or global */
|
||||
.markdown-preview-mini :deep(h1),
|
||||
.markdown-preview :deep(h1) { font-size: 1.2em; font-weight: bold; margin: 0.2em 0; color: white !important; }
|
||||
|
||||
.markdown-preview-mini :deep(h2),
|
||||
.markdown-preview :deep(h2) { font-size: 1.1em; font-weight: bold; margin: 0.2em 0; color: white !important; }
|
||||
|
||||
.markdown-preview-mini :deep(p),
|
||||
.markdown-preview :deep(p) { margin: 0 0 0.5em 0; color: rgba(255,255,255,0.9) !important; }
|
||||
|
||||
.markdown-preview-mini :deep(ul),
|
||||
.markdown-preview :deep(ul) { padding-left: 1.2em; margin: 0; color: rgba(255,255,255,0.9) !important; }
|
||||
|
||||
.markdown-preview :deep(mark) { background-color: yellow; color: black !important; padding: 0 2px; border-radius: 2px; }
|
||||
</style>
|
||||
@@ -6,25 +6,42 @@
|
||||
max-width="900"
|
||||
@update:model-value="$emit('update:modelValue', $event)"
|
||||
>
|
||||
<v-card>
|
||||
<v-card color="black-lighten-1">
|
||||
<v-card-title class="d-flex align-center">
|
||||
<v-icon class="mr-2" icon="mdi-account-group" />
|
||||
<v-icon
|
||||
class="mr-2"
|
||||
icon="mdi-account-group"
|
||||
/>
|
||||
考勤
|
||||
<v-spacer />
|
||||
<v-chip v-if="!isMobile" class="ml-2" color="primary" size="small">
|
||||
{{ dateString }}
|
||||
</v-chip>
|
||||
<v-btn v-if="isMobile" icon="mdi-close" variant="text" @click="$emit('update:modelValue', false)" />
|
||||
<v-btn
|
||||
color="secondary"
|
||||
variant="tonal"
|
||||
prepend-icon="mdi-format-list-bulleted"
|
||||
class="mr-2"
|
||||
@click="showEditListDialog = true"
|
||||
>
|
||||
修改名单
|
||||
</v-btn>
|
||||
<v-btn
|
||||
v-if="isMobile"
|
||||
icon="mdi-close"
|
||||
variant="text"
|
||||
@click="$emit('update:modelValue', false)"
|
||||
/>
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text>
|
||||
<!-- 批量操作和搜索 -->
|
||||
<v-row class="mb-4">
|
||||
<v-col cols="12" md="12">
|
||||
<v-col
|
||||
cols="12"
|
||||
md="12"
|
||||
>
|
||||
<v-text-field
|
||||
v-model="attendanceSearch"
|
||||
clearable
|
||||
hint="支持筛选姓氏,如输入'孙'可筛选所有姓孙的学生"
|
||||
hint="*支持筛选姓氏"
|
||||
label="搜索学生"
|
||||
prepend-inner-icon="mdi-magnify"
|
||||
variant="outlined"
|
||||
@@ -128,7 +145,10 @@
|
||||
md="6"
|
||||
sm="6"
|
||||
>
|
||||
<v-card border class="student-card">
|
||||
<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">
|
||||
@@ -137,11 +157,13 @@
|
||||
class="mr-2"
|
||||
size="24"
|
||||
>
|
||||
<v-icon size="small"
|
||||
>{{ getStudentStatusIcon(student) }}
|
||||
<v-icon size="small">
|
||||
{{ getStudentStatusIcon(student) }}
|
||||
</v-icon>
|
||||
</v-avatar>
|
||||
<div class="text-subtitle-1">{{ student }}</div>
|
||||
<div class="text-subtitle-1">
|
||||
{{ student }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="attendance-actions">
|
||||
@@ -183,10 +205,19 @@
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" md="12">
|
||||
<v-card class="mb-4" color="primary" variant="tonal">
|
||||
<v-col
|
||||
cols="12"
|
||||
md="12"
|
||||
>
|
||||
<v-card
|
||||
class="mb-4"
|
||||
color="primary"
|
||||
variant="tonal"
|
||||
>
|
||||
<v-card-text>
|
||||
<div class="text-subtitle-2 mb-2">批量操作</div>
|
||||
<div class="text-subtitle-2 mb-2">
|
||||
批量操作
|
||||
</div>
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
<v-btn
|
||||
class="flex-grow-1"
|
||||
@@ -232,18 +263,83 @@
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
|
||||
<v-btn color="primary" @click="$emit('save')">
|
||||
<v-icon start>mdi-content-save</v-icon>
|
||||
<v-btn
|
||||
color="primary"
|
||||
@click="$emit('save')"
|
||||
>
|
||||
<v-icon start>
|
||||
mdi-content-save
|
||||
</v-icon>
|
||||
保存
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<!-- Edit List Dialog -->
|
||||
<v-dialog
|
||||
v-model="showEditListDialog"
|
||||
max-width="500"
|
||||
>
|
||||
<v-card>
|
||||
<v-card-title>修改班级名单</v-card-title>
|
||||
<v-card-text>
|
||||
<v-alert
|
||||
type="info"
|
||||
variant="tonal"
|
||||
class="mb-4"
|
||||
density="compact"
|
||||
>
|
||||
请输入学生姓名,每行一个。
|
||||
</v-alert>
|
||||
<div class="d-flex justify-end mb-2">
|
||||
<v-btn
|
||||
color="success"
|
||||
variant="text"
|
||||
prepend-icon="mdi-microsoft-excel"
|
||||
size="small"
|
||||
@click="triggerFileInput"
|
||||
>
|
||||
从Excel导入
|
||||
</v-btn>
|
||||
<input
|
||||
ref="fileInput"
|
||||
type="file"
|
||||
accept=".xlsx, .xls"
|
||||
style="display: none"
|
||||
@change="handleFileUpload"
|
||||
>
|
||||
</div>
|
||||
<v-textarea
|
||||
v-model="editingStudentList"
|
||||
rows="10"
|
||||
auto-grow
|
||||
placeholder="张三 李四 王五"
|
||||
/>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
variant="text"
|
||||
@click="showEditListDialog = false"
|
||||
>
|
||||
取消
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="primary"
|
||||
@click="saveStudentList"
|
||||
>
|
||||
保存名单
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { useDisplay } from "vuetify";
|
||||
import { getSetting } from "@/utils/settings";
|
||||
import dataProvider from "@/utils/dataProvider";
|
||||
|
||||
export default {
|
||||
name: "AttendanceManagementDialog",
|
||||
@@ -265,7 +361,7 @@ export default {
|
||||
default: "",
|
||||
},
|
||||
},
|
||||
emits: ["update:modelValue", "save", "change"],
|
||||
emits: ["update:modelValue", "save", "change", "list-updated"],
|
||||
setup() {
|
||||
const { mobile } = useDisplay();
|
||||
return { mobile };
|
||||
@@ -274,6 +370,8 @@ export default {
|
||||
return {
|
||||
attendanceSearch: "",
|
||||
attendanceFilter: [],
|
||||
showEditListDialog: false,
|
||||
editingStudentList: "",
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -330,7 +428,111 @@ export default {
|
||||
.sort((a, b) => a.name.localeCompare(b.name, 'zh-CN'));
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
showEditListDialog(val) {
|
||||
if (val) {
|
||||
this.editingStudentList = this.studentList.join('\n');
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
triggerFileInput() {
|
||||
this.$refs.fileInput.click();
|
||||
},
|
||||
async handleFileUpload(event) {
|
||||
const file = event.target.files[0];
|
||||
if (!file) return;
|
||||
|
||||
try {
|
||||
const XLSX = await import("xlsx");
|
||||
const data = await file.arrayBuffer();
|
||||
const workbook = XLSX.read(data);
|
||||
const firstSheetName = workbook.SheetNames[0];
|
||||
const worksheet = workbook.Sheets[firstSheetName];
|
||||
const jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1 });
|
||||
|
||||
if (jsonData.length === 0) {
|
||||
this.$message?.warning("提示", "文件为空");
|
||||
return;
|
||||
}
|
||||
|
||||
// Find "姓名" column index
|
||||
let nameColIndex = -1;
|
||||
let headerRowIndex = 0;
|
||||
|
||||
// Search in first 10 rows
|
||||
for (let i = 0; i < Math.min(jsonData.length, 10); i++) {
|
||||
const row = jsonData[i];
|
||||
if (Array.isArray(row)) {
|
||||
const idx = row.findIndex(
|
||||
(cell) =>
|
||||
cell && typeof cell === "string" && cell.includes("姓名")
|
||||
);
|
||||
if (idx !== -1) {
|
||||
nameColIndex = idx;
|
||||
headerRowIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (nameColIndex === -1) {
|
||||
this.$message?.warning("提示", "未找到“姓名”列,请检查Excel文件");
|
||||
return;
|
||||
}
|
||||
|
||||
const names = [];
|
||||
for (let i = headerRowIndex + 1; i < jsonData.length; i++) {
|
||||
const row = jsonData[i];
|
||||
if (row[nameColIndex]) {
|
||||
names.push(String(row[nameColIndex]).trim());
|
||||
}
|
||||
}
|
||||
|
||||
if (names.length > 0) {
|
||||
if (this.editingStudentList.trim()) {
|
||||
// 直接覆盖而不是弹出 confirm 对话框
|
||||
this.editingStudentList = names.join("\n");
|
||||
} else {
|
||||
this.editingStudentList = names.join("\n");
|
||||
}
|
||||
|
||||
this.$message?.success("导入成功", `成功读取 ${names.length} 个名字,请点击保存名单`);
|
||||
} else {
|
||||
this.$message?.warning("提示", "未找到名字数据");
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Excel import failed", e);
|
||||
this.$message?.error("导入失败", e.message);
|
||||
} finally {
|
||||
event.target.value = ""; // Reset input
|
||||
}
|
||||
},
|
||||
async saveStudentList() {
|
||||
const list = this.editingStudentList
|
||||
.split('\n')
|
||||
.map(s => s.trim())
|
||||
.filter(s => s);
|
||||
|
||||
try {
|
||||
const studentObjects = list.map((name, index) => ({ name, id: index + 1 }));
|
||||
const response = await dataProvider.saveData("classworks-list-main", studentObjects);
|
||||
|
||||
if (response && response.success === false) {
|
||||
throw new Error(response.error?.message || "保存失败");
|
||||
}
|
||||
|
||||
// Update the dialog content to match what was saved
|
||||
this.editingStudentList = list.join('\n');
|
||||
|
||||
this.$emit("list-updated");
|
||||
this.showEditListDialog = false;
|
||||
this.$message?.success("保存成功", "学生列表已更新");
|
||||
} catch (e) {
|
||||
console.error("Failed to save student list", e);
|
||||
this.$message?.error("保存失败", e.message || "无法保存学生列表");
|
||||
}
|
||||
},
|
||||
toggleFilter(filter) {
|
||||
const index = this.attendanceFilter.indexOf(filter);
|
||||
if (index === -1) {
|
||||
|
||||
@@ -1,89 +1,430 @@
|
||||
<template>
|
||||
<v-col
|
||||
v-if="studentList && studentList.length"
|
||||
v-ripple="!isEditingDisabled ? {
|
||||
class: `text-${
|
||||
['primary', 'secondary', 'info', 'success', 'warning', 'error'][
|
||||
Math.floor(Math.random() * 6)
|
||||
]
|
||||
}`,
|
||||
} : false"
|
||||
:class="{ 'cursor-not-allowed': isEditingDisabled }"
|
||||
class="attendance-area no-select"
|
||||
cols="1"
|
||||
@click="handleClick"
|
||||
<v-card
|
||||
v-if="!inDialog"
|
||||
class="attendance-sidebar"
|
||||
:class="compact ? 'mb-2' : 'mb-4'"
|
||||
:style="contentStyle"
|
||||
variant="outlined"
|
||||
color="grey-darken-3"
|
||||
elevation="0"
|
||||
>
|
||||
<h1>出勤</h1>
|
||||
<h2>
|
||||
<span style="white-space: nowrap"> 应到</span>
|
||||
:
|
||||
<span style="white-space: nowrap">
|
||||
{{ studentList.length - attendance.exclude.length }}人
|
||||
</span>
|
||||
</h2>
|
||||
<h2>
|
||||
<span style="white-space: nowrap"> 实到</span>
|
||||
:
|
||||
<span style="white-space: nowrap">
|
||||
{{
|
||||
studentList.length -
|
||||
attendance.absent.length -
|
||||
(!getSetting("display.lateStudentsArePresent")) * attendance.late.length -
|
||||
attendance.exclude.length
|
||||
}}人
|
||||
</span>
|
||||
</h2>
|
||||
<h2>
|
||||
<span style="white-space: nowrap"> 请假</span>
|
||||
:
|
||||
<span style="white-space: nowrap">
|
||||
{{ attendance.absent.length }}人
|
||||
</span>
|
||||
</h2>
|
||||
<h3
|
||||
v-for="(name, index) in attendance.absent"
|
||||
:key="'absent-' + index"
|
||||
class="gray-text"
|
||||
<!-- Header with Animation -->
|
||||
<v-card-title
|
||||
v-if="!compact"
|
||||
class="bg-primary text-white d-flex align-center py-3 px-4"
|
||||
style="min-height: 52px;"
|
||||
>
|
||||
<span v-if="display.lgAndUp.value">{{ `${index + 1}. ` }}</span
|
||||
><span style="white-space: nowrap">{{ name }}</span>
|
||||
</h3>
|
||||
<h2>
|
||||
<span style="white-space: nowrap">迟到</span>
|
||||
:
|
||||
<span style="white-space: nowrap">
|
||||
{{ attendance.late.length }}人
|
||||
</span>
|
||||
</h2>
|
||||
<h3
|
||||
v-for="(name, index) in attendance.late"
|
||||
:key="'late-' + index"
|
||||
class="gray-text"
|
||||
<v-icon
|
||||
start
|
||||
class="animate-pulse"
|
||||
size="small"
|
||||
>
|
||||
mdi-account-group
|
||||
</v-icon>
|
||||
<span
|
||||
class="font-weight-bold text-subtitle-1"
|
||||
style="font-size: 1.25em !important;"
|
||||
>出勤</span>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
icon="mdi-pencil"
|
||||
variant="text"
|
||||
size="small"
|
||||
color="white"
|
||||
@click.stop="$emit('click')"
|
||||
/>
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text class="pa-0">
|
||||
<div
|
||||
v-ripple="!isEditingDisabled"
|
||||
class="attendance-content"
|
||||
:class="[{ 'disabled': isEditingDisabled, 'cursor-pointer': !isEditingDisabled }, compact ? 'pa-1' : 'pa-3']"
|
||||
:style="contentStyle"
|
||||
@click.stop="$emit('click')"
|
||||
>
|
||||
<!-- Statistics Grid -->
|
||||
<v-row
|
||||
dense
|
||||
class="ma-0"
|
||||
:class="compact ? 'mb-0' : 'mb-2'"
|
||||
>
|
||||
<!-- 全勤时显示在左侧的图标 (仅在 compact 模式) -->
|
||||
<v-col
|
||||
v-if="isAllPresent && compact"
|
||||
:cols="isAllPresent && compact ? 4 : 12"
|
||||
class="d-flex align-center justify-center pa-0"
|
||||
>
|
||||
<div class="text-center text-success d-flex flex-row align-center justify-center">
|
||||
<v-icon
|
||||
color="success"
|
||||
size="24"
|
||||
class="mr-1"
|
||||
>
|
||||
mdi-check-circle
|
||||
</v-icon>
|
||||
<div
|
||||
class="font-weight-bold"
|
||||
style="font-size: 0.9em;"
|
||||
>
|
||||
全勤
|
||||
</div>
|
||||
</div>
|
||||
</v-col>
|
||||
|
||||
<v-col
|
||||
:cols="isAllPresent && compact ? 4 : 6"
|
||||
class="pa-0"
|
||||
>
|
||||
<div class="stat-item d-flex flex-column align-center justify-center">
|
||||
<div
|
||||
class="text-caption text-white"
|
||||
:style="{ fontSize: compact ? '0.7em !important' : '0.8em !important', lineHeight: compact ? '1' : '' }"
|
||||
>
|
||||
应到
|
||||
</div>
|
||||
<div
|
||||
class="font-weight-bold text-white"
|
||||
:class="compact ? 'text-subtitle-2' : 'text-h6'"
|
||||
:style="{ fontSize: compact ? '1.1em !important' : '1.5em !important', lineHeight: compact ? '1' : '' }"
|
||||
>
|
||||
{{ stats.expected }}
|
||||
</div>
|
||||
</div>
|
||||
</v-col>
|
||||
|
||||
<v-col
|
||||
:cols="isAllPresent && compact ? 4 : 6"
|
||||
class="pa-0"
|
||||
>
|
||||
<div class="stat-item d-flex flex-column align-center justify-center">
|
||||
<div
|
||||
class="text-caption text-white"
|
||||
:style="{ fontSize: compact ? '0.7em !important' : '0.8em !important', lineHeight: compact ? '1' : '' }"
|
||||
>
|
||||
实到
|
||||
</div>
|
||||
<div
|
||||
class="font-weight-bold text-success"
|
||||
:class="compact ? 'text-subtitle-2' : 'text-h6'"
|
||||
:style="{ fontSize: compact ? '1.1em !important' : '1.5em !important', lineHeight: compact ? '1' : '' }"
|
||||
>
|
||||
{{ stats.actual }}
|
||||
</div>
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-divider
|
||||
v-if="!isAllPresent || !compact"
|
||||
:class="compact ? 'mb-1 mt-0' : 'mb-2'"
|
||||
/>
|
||||
|
||||
<!-- Scrollable Area -->
|
||||
<div
|
||||
v-if="!isAllPresent || !compact"
|
||||
ref="scrollContainer"
|
||||
class="status-lists"
|
||||
:style="{ maxHeight: compact ? '80px' : '300px', overflowY: 'auto', ...contentStyle }"
|
||||
@mouseenter="isHovering = true"
|
||||
@mouseleave="isHovering = false"
|
||||
>
|
||||
<!-- Status Lists -->
|
||||
<div
|
||||
v-if="attendance.absent.length > 0"
|
||||
class="status-group mb-1"
|
||||
>
|
||||
<div
|
||||
class="d-flex align-center mb-0 sticky-header bg-surface"
|
||||
style="position: sticky; top: 0; z-index: 1;"
|
||||
>
|
||||
<v-icon
|
||||
:size="compact ? '12' : 'x-small'"
|
||||
start
|
||||
color="error"
|
||||
:style="compact ? 'margin-right: 4px;' : ''"
|
||||
>
|
||||
mdi-account-off
|
||||
</v-icon>
|
||||
<span
|
||||
class="font-weight-bold text-error"
|
||||
:class="compact ? 'text-caption' : 'text-caption'"
|
||||
:style="{ fontSize: compact ? '0.7em !important' : '0.9em !important', lineHeight: compact ? '1.2' : '' }"
|
||||
>请假 ({{ attendance.absent.length }})</span>
|
||||
</div>
|
||||
<div class="d-flex flex-column gap-0 pl-1 border-s-sm border-error">
|
||||
<div
|
||||
v-for="name in attendance.absent"
|
||||
:key="'absent-' + name"
|
||||
class="font-weight-bold text-white py-0"
|
||||
:class="compact ? 'text-caption' : 'text-caption'"
|
||||
:style="{ fontSize: compact ? '0.8em !important' : '1.1em !important', padding: compact ? '0 !important' : '4px 0 !important', lineHeight: compact ? '1.2' : '' }"
|
||||
>
|
||||
{{ name }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="attendance.late.length > 0"
|
||||
class="status-group mb-1"
|
||||
>
|
||||
<div
|
||||
class="d-flex align-center mb-0 sticky-header bg-surface"
|
||||
style="position: sticky; top: 0; z-index: 1;"
|
||||
>
|
||||
<v-icon
|
||||
:size="compact ? '12' : 'x-small'"
|
||||
start
|
||||
color="warning"
|
||||
:style="compact ? 'margin-right: 4px;' : ''"
|
||||
>
|
||||
mdi-clock-alert
|
||||
</v-icon>
|
||||
<span
|
||||
class="font-weight-bold text-warning"
|
||||
:class="compact ? 'text-caption' : 'text-caption'"
|
||||
:style="{ fontSize: compact ? '0.7em !important' : '0.9em !important', lineHeight: compact ? '1.2' : '' }"
|
||||
>迟到 ({{ attendance.late.length }})</span>
|
||||
</div>
|
||||
<div class="d-flex flex-column gap-0 pl-1 border-s-sm border-warning">
|
||||
<div
|
||||
v-for="name in attendance.late"
|
||||
:key="'late-' + name"
|
||||
class="font-weight-bold text-white py-0"
|
||||
:class="compact ? 'text-caption' : 'text-caption'"
|
||||
:style="{ fontSize: compact ? '0.8em !important' : '1.1em !important', padding: compact ? '0 !important' : '4px 0 !important', lineHeight: compact ? '1.2' : '' }"
|
||||
>
|
||||
{{ name }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="attendance.exclude.length > 0"
|
||||
class="status-group mb-1"
|
||||
>
|
||||
<div
|
||||
class="d-flex align-center mb-0 sticky-header bg-surface"
|
||||
style="position: sticky; top: 0; z-index: 1;"
|
||||
>
|
||||
<v-icon
|
||||
:size="compact ? '12' : 'x-small'"
|
||||
start
|
||||
color="grey"
|
||||
:style="compact ? 'margin-right: 4px;' : ''"
|
||||
>
|
||||
mdi-account-cancel
|
||||
</v-icon>
|
||||
<span
|
||||
class="font-weight-bold text-grey"
|
||||
:class="compact ? 'text-caption' : 'text-caption'"
|
||||
:style="{ fontSize: compact ? '0.7em !important' : '0.9em !important', lineHeight: compact ? '1.2' : '' }"
|
||||
>不参与 ({{ attendance.exclude.length }})</span>
|
||||
</div>
|
||||
<div class="d-flex flex-column gap-0 pl-1 border-s-sm border-grey">
|
||||
<div
|
||||
v-for="name in attendance.exclude"
|
||||
:key="'exclude-' + name"
|
||||
class="text-white py-0"
|
||||
:class="compact ? 'text-caption' : 'text-caption'"
|
||||
:style="{ fontSize: compact ? '0.75em !important' : '0.9em !important', padding: compact ? '0 !important' : '4px 0 !important', lineHeight: compact ? '1.2' : '' }"
|
||||
>
|
||||
{{ name }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="isAllPresent && !compact"
|
||||
class="text-center text-success d-flex flex-column align-center justify-center"
|
||||
:class="compact ? 'py-1' : 'py-6'"
|
||||
>
|
||||
<v-icon
|
||||
color="success"
|
||||
:size="compact ? '28' : '64'"
|
||||
:class="compact ? 'mb-0' : 'mb-2'"
|
||||
>
|
||||
mdi-check-circle
|
||||
</v-icon>
|
||||
<div
|
||||
class="font-weight-bold"
|
||||
:class="compact ? 'text-subtitle-2' : 'text-h5'"
|
||||
>
|
||||
全勤
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
<div
|
||||
v-else
|
||||
:style="contentStyle"
|
||||
>
|
||||
<v-container
|
||||
fluid
|
||||
class="pa-4"
|
||||
>
|
||||
<span v-if="display.lgAndUp.value">{{ `${index + 1}. ` }}</span
|
||||
><span style="white-space: nowrap">{{ name }}</span>
|
||||
</h3>
|
||||
<h2>
|
||||
<span style="white-space: nowrap">不参与</span>
|
||||
:
|
||||
<span style="white-space: nowrap">
|
||||
{{ attendance.exclude.length }}人
|
||||
</span>
|
||||
</h2>
|
||||
<h3
|
||||
v-for="(name, index) in attendance.exclude"
|
||||
:key="'exclude-' + index"
|
||||
class="gray-text"
|
||||
>
|
||||
<span v-if="display.lgAndUp.value">{{ `${index + 1}. ` }}</span
|
||||
><span style="white-space: nowrap">{{ name }}</span>
|
||||
</h3>
|
||||
</v-col>
|
||||
<v-text-field
|
||||
v-model="search"
|
||||
prepend-inner-icon="mdi-magnify"
|
||||
label="搜索学生"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
hide-details
|
||||
class="mb-4"
|
||||
bg-color="surface"
|
||||
/>
|
||||
|
||||
<!-- 统计信息 -->
|
||||
<div class="d-flex justify-space-between mb-4 text-body-2 font-weight-bold px-2">
|
||||
<span
|
||||
v-for="(count, status) in statusCounts"
|
||||
:key="status"
|
||||
>
|
||||
{{ status }} ({{ count }})
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 状态筛选 Chip -->
|
||||
<div class="d-flex flex-wrap gap-2 mb-4">
|
||||
<v-chip
|
||||
v-for="status in ['all', 'present', 'absent', 'late', 'exclude']"
|
||||
:key="status"
|
||||
:color="filterStatus === status ? 'primary' : 'default'"
|
||||
:variant="filterStatus === status ? 'flat' : 'outlined'"
|
||||
class="mr-2"
|
||||
@click="filterStatus = status"
|
||||
>
|
||||
<v-icon
|
||||
start
|
||||
size="small"
|
||||
>
|
||||
{{ getStatusIconByString(status) }}
|
||||
</v-icon>
|
||||
{{ getStatusLabel(status) }}
|
||||
</v-chip>
|
||||
</div>
|
||||
|
||||
<!-- 学生网格 -->
|
||||
<v-row dense>
|
||||
<v-col
|
||||
v-for="student in filteredStudents"
|
||||
:key="student"
|
||||
cols="6"
|
||||
sm="4"
|
||||
md="3"
|
||||
>
|
||||
<v-card
|
||||
v-ripple
|
||||
variant="outlined"
|
||||
class="d-flex align-center pa-2"
|
||||
:color="getStatusColor(student) !== 'grey-lighten-1' ? getStatusColor(student) : 'grey-darken-3'"
|
||||
:style="{ borderColor: getStatusColor(student) === 'grey-lighten-1' ? 'rgba(255,255,255,0.1)' : 'currentColor' }"
|
||||
@click="toggleStatus(student)"
|
||||
>
|
||||
<v-avatar
|
||||
size="32"
|
||||
:color="getStatusColor(student) === 'grey-lighten-1' ? 'grey-darken-4' : getStatusColor(student)"
|
||||
variant="tonal"
|
||||
class="mr-2"
|
||||
>
|
||||
<v-icon
|
||||
size="small"
|
||||
:color="getStatusColor(student) === 'grey-lighten-1' ? 'white' : ''"
|
||||
>
|
||||
{{ getStatusIcon(student) }}
|
||||
</v-icon>
|
||||
</v-avatar>
|
||||
<div class="text-truncate flex-grow-1 font-weight-medium text-white">
|
||||
{{ student }}
|
||||
</div>
|
||||
<v-icon
|
||||
v-if="getStatus(student)"
|
||||
size="small"
|
||||
:color="getStatusColor(student) === 'grey-lighten-1' ? 'transparent' : getStatusColor(student)"
|
||||
>
|
||||
{{ getStatusIcon(student) }}
|
||||
</v-icon>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- 批量操作 -->
|
||||
<v-card
|
||||
class="mt-6 pa-4 bg-surface-variant"
|
||||
variant="flat"
|
||||
>
|
||||
<div class="text-subtitle-2 font-weight-bold mb-3 text-primary">
|
||||
批量操作
|
||||
</div>
|
||||
<v-row dense>
|
||||
<v-col
|
||||
cols="6"
|
||||
sm="3"
|
||||
>
|
||||
<v-btn
|
||||
block
|
||||
color="success"
|
||||
variant="flat"
|
||||
prepend-icon="mdi-account-check"
|
||||
@click="batchSetStatus(null)"
|
||||
>
|
||||
全部到齐
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-col
|
||||
cols="6"
|
||||
sm="3"
|
||||
>
|
||||
<v-btn
|
||||
block
|
||||
color="error"
|
||||
variant="flat"
|
||||
prepend-icon="mdi-account-off"
|
||||
@click="batchSetStatus('absent')"
|
||||
>
|
||||
全部请假
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-col
|
||||
cols="6"
|
||||
sm="3"
|
||||
>
|
||||
<v-btn
|
||||
block
|
||||
color="warning"
|
||||
variant="flat"
|
||||
prepend-icon="mdi-clock-alert"
|
||||
@click="batchSetStatus('late')"
|
||||
>
|
||||
全部迟到
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-col
|
||||
cols="6"
|
||||
sm="3"
|
||||
>
|
||||
<v-btn
|
||||
block
|
||||
color="grey"
|
||||
variant="flat"
|
||||
prepend-icon="mdi-account-cancel"
|
||||
@click="batchSetStatus('exclude')"
|
||||
>
|
||||
全部不参与
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card>
|
||||
</v-container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { useDisplay } from "vuetify";
|
||||
import {getSetting} from "@/utils/settings.js";
|
||||
import { getSetting } from "@/utils/settings.js";
|
||||
|
||||
export default {
|
||||
name: "AttendanceSidebar",
|
||||
@@ -100,14 +441,240 @@ export default {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
contentStyle: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
inDialog: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
compact: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
emits: ["click", "disabled-click"],
|
||||
emits: ["click", "disabled-click", "edit-list", "save"],
|
||||
setup() {
|
||||
const display = useDisplay();
|
||||
return { display };
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
scrollTimer: null,
|
||||
scrollPosition: 0,
|
||||
isScrollingUp: false,
|
||||
isHovering: false,
|
||||
search: "",
|
||||
filterStatus: "all",
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
stats() {
|
||||
const total = this.studentList.length;
|
||||
const excludeCount = this.attendance.exclude.length;
|
||||
const absentCount = this.attendance.absent.length;
|
||||
// 迟到也算实到
|
||||
|
||||
const expected = total - excludeCount;
|
||||
const actual = total - absentCount - excludeCount;
|
||||
|
||||
return {
|
||||
expected: Math.max(0, expected),
|
||||
actual: Math.max(0, actual),
|
||||
};
|
||||
},
|
||||
statusCounts() {
|
||||
const total = this.studentList.length;
|
||||
const absent = this.attendance.absent.length;
|
||||
const late = this.attendance.late.length;
|
||||
const exclude = this.attendance.exclude.length;
|
||||
const present = total - absent - late - exclude;
|
||||
return {
|
||||
'全部': total,
|
||||
'到课': present,
|
||||
'请假': absent,
|
||||
'迟到': late,
|
||||
'不参与': exclude
|
||||
};
|
||||
},
|
||||
isAllPresent() {
|
||||
return (
|
||||
this.attendance.absent.length === 0 &&
|
||||
this.attendance.late.length === 0 &&
|
||||
this.studentList.length > 0
|
||||
);
|
||||
},
|
||||
filteredStudents() {
|
||||
let result = this.studentList;
|
||||
|
||||
if (this.search) {
|
||||
result = result.filter(student =>
|
||||
student.toLowerCase().includes(this.search.toLowerCase())
|
||||
);
|
||||
}
|
||||
|
||||
if (this.filterStatus !== 'all') {
|
||||
result = result.filter(student => {
|
||||
const status = this.getStatus(student);
|
||||
if (this.filterStatus === 'present') return status === null; // null means present (conceptually)
|
||||
return status === this.filterStatus;
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.startAutoScroll();
|
||||
},
|
||||
beforeUnmount() {
|
||||
this.stopAutoScroll();
|
||||
},
|
||||
methods: {
|
||||
getSetting,
|
||||
getStatusLabel(status) {
|
||||
const map = {
|
||||
'all': '全部',
|
||||
'present': '到课',
|
||||
'absent': '请假',
|
||||
'late': '迟到',
|
||||
'exclude': '不参与'
|
||||
};
|
||||
return map[status] || status;
|
||||
},
|
||||
getStatusIconByString(status) {
|
||||
const map = {
|
||||
'all': 'mdi-account-group',
|
||||
'present': 'mdi-account-check',
|
||||
'absent': 'mdi-account-off',
|
||||
'late': 'mdi-clock-alert',
|
||||
'exclude': 'mdi-account-cancel'
|
||||
};
|
||||
return map[status] || 'mdi-circle-small';
|
||||
},
|
||||
batchSetStatus(status) {
|
||||
if (this.isEditingDisabled) {
|
||||
this.$emit('disabled-click');
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear all statuses first
|
||||
this.attendance.absent = [];
|
||||
this.attendance.late = [];
|
||||
this.attendance.exclude = [];
|
||||
|
||||
if (status === null) {
|
||||
// Everyone present, lists already cleared
|
||||
} else if (status === 'absent') {
|
||||
this.attendance.absent = [...this.studentList];
|
||||
} else if (status === 'late') {
|
||||
this.attendance.late = [...this.studentList];
|
||||
} else if (status === 'exclude') {
|
||||
this.attendance.exclude = [...this.studentList];
|
||||
}
|
||||
|
||||
this.$emit('save');
|
||||
},
|
||||
getStatus(student) {
|
||||
if (this.attendance.absent.includes(student)) return 'absent';
|
||||
if (this.attendance.late.includes(student)) return 'late';
|
||||
if (this.attendance.exclude.includes(student)) return 'exclude';
|
||||
return null;
|
||||
},
|
||||
getStatusColor(student) {
|
||||
const status = this.getStatus(student);
|
||||
if (status === 'absent') return 'error';
|
||||
if (status === 'late') return 'warning';
|
||||
if (status === 'exclude') return 'grey';
|
||||
return 'grey-lighten-1';
|
||||
},
|
||||
getStatusIcon(student) {
|
||||
const status = this.getStatus(student);
|
||||
if (status === 'absent') return 'mdi-account-off';
|
||||
if (status === 'late') return 'mdi-clock-alert';
|
||||
if (status === 'exclude') return 'mdi-account-cancel';
|
||||
return 'mdi-account-check';
|
||||
},
|
||||
getStatusText(student) {
|
||||
const status = this.getStatus(student);
|
||||
if (status === 'absent') return '请假';
|
||||
if (status === 'late') return '迟到';
|
||||
if (status === 'exclude') return '不参与';
|
||||
return '';
|
||||
},
|
||||
toggleStatus(student) {
|
||||
if (this.isEditingDisabled) {
|
||||
this.$emit('disabled-click');
|
||||
return;
|
||||
}
|
||||
const currentStatus = this.getStatus(student);
|
||||
let newStatus = null;
|
||||
|
||||
// Cycle: null -> absent -> late -> exclude -> null
|
||||
if (currentStatus === null) newStatus = 'absent';
|
||||
else if (currentStatus === 'absent') newStatus = 'late';
|
||||
else if (currentStatus === 'late') newStatus = 'exclude';
|
||||
else if (currentStatus === 'exclude') newStatus = null;
|
||||
|
||||
// Update attendance object locally (since it's passed by reference, but we should emit)
|
||||
// Actually, since we are modifying the prop object directly (Vue 3 reactive), it might update the parent state.
|
||||
// However, best practice is to emit an event or update the object carefully.
|
||||
// The parent passes `state.boardData.attendance` which is reactive.
|
||||
|
||||
// Remove from all lists first
|
||||
const remove = (arr, val) => {
|
||||
const idx = arr.indexOf(val);
|
||||
if (idx > -1) arr.splice(idx, 1);
|
||||
};
|
||||
|
||||
remove(this.attendance.absent, student);
|
||||
remove(this.attendance.late, student);
|
||||
remove(this.attendance.exclude, student);
|
||||
|
||||
// Add to new list
|
||||
if (newStatus === 'absent') this.attendance.absent.push(student);
|
||||
else if (newStatus === 'late') this.attendance.late.push(student);
|
||||
else if (newStatus === 'exclude') this.attendance.exclude.push(student);
|
||||
|
||||
this.$emit('save');
|
||||
},
|
||||
startAutoScroll() {
|
||||
if (this.scrollTimer) return;
|
||||
this.scrollTimer = setInterval(() => {
|
||||
const el = this.$refs.scrollContainer;
|
||||
if (!el || this.isHovering) return;
|
||||
|
||||
// 如果内容高度没有超过容器高度,不需要滚动
|
||||
if (el.scrollHeight <= el.clientHeight) return;
|
||||
|
||||
if (this.isScrollingUp) {
|
||||
el.scrollTop -= 1;
|
||||
if (el.scrollTop <= 0) {
|
||||
this.isScrollingUp = false;
|
||||
// 触顶后暂停2秒再向下滚
|
||||
clearInterval(this.scrollTimer);
|
||||
this.scrollTimer = null;
|
||||
setTimeout(() => this.startAutoScroll(), 2000);
|
||||
}
|
||||
} else {
|
||||
el.scrollTop += 1;
|
||||
// 检查是否到底部 (允许1px误差)
|
||||
if (el.scrollTop + el.clientHeight >= el.scrollHeight - 1) {
|
||||
this.isScrollingUp = true;
|
||||
// 触底后暂停2秒再向上滚
|
||||
clearInterval(this.scrollTimer);
|
||||
this.scrollTimer = null;
|
||||
setTimeout(() => this.startAutoScroll(), 2000);
|
||||
}
|
||||
}
|
||||
}, 50); // 50ms滚动一次,速度比较平缓
|
||||
},
|
||||
stopAutoScroll() {
|
||||
if (this.scrollTimer) {
|
||||
clearInterval(this.scrollTimer);
|
||||
this.scrollTimer = null;
|
||||
}
|
||||
},
|
||||
handleClick() {
|
||||
if (this.isEditingDisabled) {
|
||||
this.$emit('disabled-click');
|
||||
@@ -120,5 +687,29 @@ export default {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Add any specific styles here if needed, or rely on global styles */
|
||||
.attendance-sidebar {
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
.attendance-sidebar:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
|
||||
}
|
||||
.attendance-content {
|
||||
position: relative;
|
||||
}
|
||||
.attendance-content.disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.7;
|
||||
}
|
||||
.gap-1 {
|
||||
gap: 4px;
|
||||
}
|
||||
@keyframes pulse {
|
||||
0% { transform: scale(1); }
|
||||
50% { transform: scale(1.1); }
|
||||
100% { transform: scale(1); }
|
||||
}
|
||||
.animate-pulse {
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
</v-alert>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer/>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
v-if="showCancel"
|
||||
variant="text"
|
||||
|
||||
@@ -41,9 +41,7 @@
|
||||
label="命名空间"
|
||||
prepend-inner-icon="mdi-identifier"
|
||||
variant="outlined"
|
||||
>
|
||||
|
||||
</v-text-field>
|
||||
/>
|
||||
|
||||
<v-text-field
|
||||
v-model="form.password"
|
||||
@@ -51,9 +49,7 @@
|
||||
prepend-inner-icon="mdi-lock-outline"
|
||||
type="text"
|
||||
variant="outlined"
|
||||
>
|
||||
|
||||
</v-text-field>
|
||||
/>
|
||||
|
||||
<v-alert
|
||||
v-if="error"
|
||||
@@ -77,7 +73,7 @@
|
||||
>
|
||||
取消
|
||||
</v-btn>
|
||||
<v-spacer/>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
:disabled="!form.namespace || authenticating"
|
||||
:loading="authenticating"
|
||||
|
||||
@@ -139,8 +139,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</v-card>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<!-- 步骤 3: 询问使用场景 -->
|
||||
@@ -280,8 +278,8 @@
|
||||
也可以手动前往 Classworks KV 控制台获取认证信息:
|
||||
</div>
|
||||
<v-card
|
||||
:color=" kvserverurl=='https://kv.houlang.cloud'? 'primary' : 'error' "
|
||||
:variant="kvserverurl=='https://kv.houlang.cloud'? 'elevated' : 'outlined'"
|
||||
color="primary"
|
||||
variant="elevated"
|
||||
class="pa-6 mb-6"
|
||||
@click="openKVSite"
|
||||
>
|
||||
@@ -293,13 +291,13 @@
|
||||
</v-icon>
|
||||
|
||||
<h4 class="text-h6 font-weight-bold">
|
||||
请访问 {{ kvserverurl=='https://kv.houlang.cloud'? 'Classworks KV' : '自定义的 Classworks KV 实例 ' }} 控制台
|
||||
请访问 Classworks KV 控制台
|
||||
</h4>
|
||||
<div class="text-h5 mb-6">
|
||||
{{ kvserverurl }}
|
||||
{{ kvserverurl || '未配置' }}
|
||||
</div>
|
||||
<h6 class="text-subtitle-2">
|
||||
{{ kvserverurl=='https://kv.houlang.cloud'? '此实例由 Classworks KV 官方提供' : '此链接由您的实例、预配代码或管理员管理,当前可能不是 Classworks KV 官方的实例地址。' }}
|
||||
此链接由您的实例、预配代码或管理员管理。
|
||||
</h6>
|
||||
</v-card>
|
||||
|
||||
@@ -553,7 +551,7 @@
|
||||
</v-icon>
|
||||
上一步
|
||||
</v-btn>
|
||||
<v-spacer/>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
v-if="currentStep < totalSteps && currentStep !== 4"
|
||||
:disabled="currentStep === 3 && !storageType"
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
variant="tonal"
|
||||
>
|
||||
<template #prepend>
|
||||
<v-icon icon="mdi-information"/>
|
||||
<v-icon icon="mdi-information" />
|
||||
</template>
|
||||
系统将自动为您创建设备并获取访问令牌,无需手动配置
|
||||
</v-alert>
|
||||
@@ -52,7 +52,7 @@
|
||||
variant="tonal"
|
||||
>
|
||||
<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">
|
||||
@@ -83,7 +83,7 @@
|
||||
variant="tonal"
|
||||
>
|
||||
<template #prepend>
|
||||
<v-icon icon="mdi-information"/>
|
||||
<v-icon icon="mdi-information" />
|
||||
</template>
|
||||
您可以点击下方按钮访问云端控制台来设置密码和管理高级功能
|
||||
</v-alert>
|
||||
@@ -97,7 +97,7 @@
|
||||
variant="tonal"
|
||||
>
|
||||
<template #prepend>
|
||||
<v-icon icon="mdi-alert-circle"/>
|
||||
<v-icon icon="mdi-alert-circle" />
|
||||
</template>
|
||||
{{ errorMessage }}
|
||||
</v-alert>
|
||||
@@ -105,7 +105,7 @@
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions>
|
||||
<v-spacer/>
|
||||
<v-spacer />
|
||||
|
||||
<!-- 注册按钮 -->
|
||||
<v-btn
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
</v-alert>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer/>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
v-if="showCancel"
|
||||
variant="text"
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
<template>
|
||||
<div class="async-loading-placeholder d-flex justify-center align-center" :style="{ minHeight: height }">
|
||||
<v-progress-circular indeterminate color="primary" size="28" width="2" />
|
||||
<div
|
||||
class="async-loading-placeholder d-flex justify-center align-center"
|
||||
:style="{ minHeight: height }"
|
||||
>
|
||||
<v-progress-circular
|
||||
indeterminate
|
||||
color="primary"
|
||||
size="28"
|
||||
width="2"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
<template>
|
||||
<!-- 纯 CSS 骨架屏,避免使用 VSkeletonLoader 引起的渲染冲突 -->
|
||||
<v-container class="main-window" fluid>
|
||||
<v-container
|
||||
class="main-window"
|
||||
fluid
|
||||
>
|
||||
<div class="skeleton-grid">
|
||||
<div
|
||||
v-for="n in cardCount"
|
||||
|
||||
@@ -1,15 +1,29 @@
|
||||
<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"/>
|
||||
|
||||
<div class="text-center">
|
||||
<div class="text-body-2 font-weight-light mb-n1">出现了错误</div>
|
||||
|
||||
<h1 class="text-h2 font-weight-bold">404</h1>
|
||||
<v-responsive
|
||||
class="align-centerfill-height mx-auto"
|
||||
max-width="900"
|
||||
>
|
||||
<div class="text-center mb-4">
|
||||
<v-icon
|
||||
size="150"
|
||||
color="primary"
|
||||
>
|
||||
mdi-alert-circle-outline
|
||||
</v-icon>
|
||||
</div>
|
||||
|
||||
<div class="py-4"/>
|
||||
<div class="text-center">
|
||||
<div class="text-body-2 font-weight-light mb-n1">
|
||||
出现了错误
|
||||
</div>
|
||||
|
||||
<h1 class="text-h2 font-weight-bold">
|
||||
404
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div class="py-4" />
|
||||
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
@@ -21,15 +35,19 @@
|
||||
variant="outlined"
|
||||
>
|
||||
<template #image>
|
||||
<v-img position="top right"/>
|
||||
<v-img position="top right" />
|
||||
</template>
|
||||
|
||||
<template #title>
|
||||
<h2 class="text-h5 font-weight-bold">为什么会出现此错误?</h2>
|
||||
<h2 class="text-h5 font-weight-bold">
|
||||
为什么会出现此错误?
|
||||
</h2>
|
||||
</template>
|
||||
|
||||
<template #subtitle>
|
||||
<div class="text-subtitle-1">大概是页面未找到</div>
|
||||
<div class="text-subtitle-1">
|
||||
大概是页面未找到
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<v-overlay
|
||||
@@ -70,7 +88,7 @@
|
||||
rounded="lg"
|
||||
title="返回上一页"
|
||||
variant="text"
|
||||
@click="this.$router.back()"
|
||||
@click="$router.back()"
|
||||
>
|
||||
<v-overlay
|
||||
contained
|
||||
|
||||
@@ -11,13 +11,19 @@
|
||||
<span class="text-truncate">{{ exam?.examName || "加载中..." }}</span>
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text class="flex-grow-1 pa-4 overflow-y-auto" :style="contentStyle">
|
||||
<div v-if="loading" class="d-flex justify-center align-center py-4">
|
||||
<v-card-text
|
||||
class="flex-grow-1 pa-4 overflow-y-auto"
|
||||
:style="contentStyle"
|
||||
>
|
||||
<div
|
||||
v-if="loading"
|
||||
class="d-flex justify-center align-center py-4"
|
||||
>
|
||||
<v-progress-circular
|
||||
indeterminate
|
||||
size="24"
|
||||
color="primary"
|
||||
></v-progress-circular>
|
||||
/>
|
||||
</div>
|
||||
|
||||
<template v-else-if="exam">
|
||||
@@ -26,7 +32,11 @@
|
||||
</div>-->
|
||||
|
||||
<div class="d-flex flex-column">
|
||||
<div v-for="(group, gIndex) in groupedExamInfos" :key="gIndex" class="mb-3">
|
||||
<div
|
||||
v-for="(group, gIndex) in groupedExamInfos"
|
||||
:key="gIndex"
|
||||
class="mb-3"
|
||||
>
|
||||
<div class="text-subtitle-2 font-weight-bold text-primary mb-1">
|
||||
<RelativeTimeDisplay :time="group.date" />
|
||||
</div>
|
||||
@@ -39,7 +49,10 @@
|
||||
'text-grey': isPast(info.end),
|
||||
}"
|
||||
>
|
||||
<div class="font-weight-bold mr-2" style="font-size: 1.1em">
|
||||
<div
|
||||
class="font-weight-bold mr-2"
|
||||
style="font-size: 1.1em"
|
||||
>
|
||||
{{ info.name }}
|
||||
</div>
|
||||
<div
|
||||
@@ -54,7 +67,12 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div v-else class="text-center text-caption text-grey py-2">无法加载</div>
|
||||
<div
|
||||
v-else
|
||||
class="text-center text-caption text-grey py-2"
|
||||
>
|
||||
无法加载
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
@@ -1,15 +1,35 @@
|
||||
<template>
|
||||
<v-card class="fill-height d-flex flex-column rounded-xl" elevation="2">
|
||||
<v-card
|
||||
class="fill-height d-flex flex-column rounded-xl"
|
||||
elevation="2"
|
||||
>
|
||||
<v-card-title class="d-flex align-center py-3 px-4 bg-primary text-white">
|
||||
<v-icon class="mr-2">mdi-calendar-clock</v-icon>
|
||||
<v-icon class="mr-2">
|
||||
mdi-calendar-clock
|
||||
</v-icon>
|
||||
<span class="text-truncate">{{ exam?.examName || '加载中...' }}</span>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn icon="mdi-close" variant="text" density="comfortable" @click="$emit('close')"></v-btn>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
icon="mdi-close"
|
||||
variant="text"
|
||||
density="comfortable"
|
||||
@click="$emit('close')"
|
||||
/>
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text class="flex-grow-1 pa-4 overflow-y-auto" :style="contentStyle">
|
||||
<div v-if="loading" class="d-flex justify-center align-center fill-height" style="min-height: 200px;">
|
||||
<v-progress-circular indeterminate color="primary"></v-progress-circular>
|
||||
<v-card-text
|
||||
class="flex-grow-1 pa-4 overflow-y-auto"
|
||||
:style="contentStyle"
|
||||
>
|
||||
<div
|
||||
v-if="loading"
|
||||
class="d-flex justify-center align-center fill-height"
|
||||
style="min-height: 200px;"
|
||||
>
|
||||
<v-progress-circular
|
||||
indeterminate
|
||||
color="primary"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<template v-else-if="exam">
|
||||
@@ -24,12 +44,24 @@
|
||||
{{ exam.message }}
|
||||
</v-alert>
|
||||
|
||||
<v-list density="comfortable" class="pa-0 bg-transparent">
|
||||
<v-list-item v-for="(info, index) in exam.examInfos" :key="index" class="px-0 mb-3">
|
||||
<v-list
|
||||
density="comfortable"
|
||||
class="pa-0 bg-transparent"
|
||||
>
|
||||
<v-list-item
|
||||
v-for="(info, index) in exam.examInfos"
|
||||
:key="index"
|
||||
class="px-0 mb-3"
|
||||
>
|
||||
<template #prepend>
|
||||
<v-avatar color="primary" variant="tonal" size="large" class="mr-3 font-weight-bold elevation-1">
|
||||
{{ info.name.charAt(0) }}
|
||||
</v-avatar>
|
||||
<v-avatar
|
||||
color="primary"
|
||||
variant="tonal"
|
||||
size="large"
|
||||
class="mr-3 font-weight-bold elevation-1"
|
||||
>
|
||||
{{ info.name.charAt(0) }}
|
||||
</v-avatar>
|
||||
</template>
|
||||
|
||||
<v-list-item-title class="font-weight-bold text-h6 mb-1">
|
||||
@@ -38,20 +70,40 @@
|
||||
|
||||
<v-list-item-subtitle class="text-body-1">
|
||||
<div class="d-flex align-center mb-1">
|
||||
<v-icon size="small" color="success" class="mr-2">mdi-clock-start</v-icon>
|
||||
<span class="font-weight-medium">{{ formatTime(info.start) }}</span>
|
||||
<v-icon
|
||||
size="small"
|
||||
color="success"
|
||||
class="mr-2"
|
||||
>
|
||||
mdi-clock-start
|
||||
</v-icon>
|
||||
<span class="font-weight-medium">{{ formatTime(info.start) }}</span>
|
||||
</div>
|
||||
<div class="d-flex align-center">
|
||||
<v-icon size="small" color="error" class="mr-2">mdi-clock-end</v-icon>
|
||||
<span class="font-weight-medium">{{ formatTime(info.end) }}</span>
|
||||
<v-icon
|
||||
size="small"
|
||||
color="error"
|
||||
class="mr-2"
|
||||
>
|
||||
mdi-clock-end
|
||||
</v-icon>
|
||||
<span class="font-weight-medium">{{ formatTime(info.end) }}</span>
|
||||
</div>
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</template>
|
||||
|
||||
<div v-else class="d-flex flex-column align-center justify-center fill-height text-grey mt-4">
|
||||
<v-icon size="large" class="mb-2">mdi-alert-circle-outline</v-icon>
|
||||
<div
|
||||
v-else
|
||||
class="d-flex flex-column align-center justify-center fill-height text-grey mt-4"
|
||||
>
|
||||
<v-icon
|
||||
size="large"
|
||||
class="mb-2"
|
||||
>
|
||||
mdi-alert-circle-outline
|
||||
</v-icon>
|
||||
无法加载考试信息
|
||||
</div>
|
||||
</v-card-text>
|
||||
|
||||
@@ -10,7 +10,12 @@
|
||||
>
|
||||
上传
|
||||
</v-btn>
|
||||
<v-btn v-else color="success" size="large" @click="$emit('show-sync-message')">
|
||||
<v-btn
|
||||
v-else
|
||||
color="success"
|
||||
size="large"
|
||||
@click="$emit('show-sync-message')"
|
||||
>
|
||||
同步完成
|
||||
</v-btn>
|
||||
<v-btn
|
||||
@@ -86,7 +91,11 @@
|
||||
variant="tonal"
|
||||
>
|
||||
<v-card-title class="text-subtitle-1">
|
||||
<v-icon icon="mdi-shield-check" size="small" start />
|
||||
<v-icon
|
||||
icon="mdi-shield-check"
|
||||
size="small"
|
||||
start
|
||||
/>
|
||||
屏幕保护技术已启用
|
||||
</v-card-title>
|
||||
<v-card-text class="text-body-2">
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
<template>
|
||||
<div ref="gridContainer" class="grid-masonry">
|
||||
<div
|
||||
ref="gridContainer"
|
||||
class="grid-masonry"
|
||||
>
|
||||
<TransitionGroup name="grid">
|
||||
<div
|
||||
v-for="item in sortedItems"
|
||||
@@ -12,98 +15,27 @@
|
||||
class="grid-item"
|
||||
>
|
||||
<!-- 时间卡片 -->
|
||||
<div v-if="item.type === 'time'" style="height: 100%">
|
||||
<div
|
||||
v-if="item.type === 'time'"
|
||||
style="height: 100%"
|
||||
>
|
||||
<time-card />
|
||||
</div>
|
||||
|
||||
<!-- 一言卡片 -->
|
||||
<div v-else-if="item.type === 'hitokoto'" style="height: 100%">
|
||||
<div
|
||||
v-else-if="item.type === 'hitokoto'"
|
||||
style="height: 100%"
|
||||
>
|
||||
<hitokoto-card />
|
||||
</div>
|
||||
|
||||
<!-- 考试卡片 -->
|
||||
<div v-else-if="item.type === 'exam'" style="height: 100%">
|
||||
<concise-exam-card
|
||||
:exam-id="item.data.examId"
|
||||
:content-style="contentStyle"
|
||||
@click="$emit('open-exam-detail', item.data.examId)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 出勤卡片 -->
|
||||
<v-card
|
||||
v-else-if="item.type === 'attendance'"
|
||||
:class="{ 'glow-highlight': highlightedCards[item.key], 'cursor-not-allowed': isEditingDisabled, 'cursor-pointer': !isEditingDisabled }"
|
||||
border
|
||||
class="glow-track"
|
||||
height="100%"
|
||||
@click="handleCardClick('attendance', null)"
|
||||
@mousemove="handleMouseMove"
|
||||
@touchmove="handleTouchMove"
|
||||
>
|
||||
<v-card-title class="d-flex align-center">
|
||||
<v-icon class="mr-2" color="primary" icon="mdi-account-group" />
|
||||
出勤统计
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<div class="d-flex justify-space-between align-center mb-2">
|
||||
<span>应到/实到</span>
|
||||
<span class="text-h6">
|
||||
{{ item.data.total - item.data.exclude.length }}/{{
|
||||
item.data.total -
|
||||
item.data.absent.length -
|
||||
(!getSetting("display.lateStudentsArePresent")) * item.data.late.length -
|
||||
item.data.exclude.length
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
<v-divider class="mb-2" />
|
||||
|
||||
<div v-if="item.data.absent.length > 0" class="mb-2">
|
||||
<div class="text-error text-caption mb-1">请假 ({{ item.data.absent.length }})</div>
|
||||
<div class="d-flex flex-wrap" style="gap: 4px">
|
||||
<v-chip v-for="name in item.data.absent" :key="name" color="error" size="x-small" variant="flat">
|
||||
{{ name }}
|
||||
</v-chip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="item.data.late.length > 0" class="mb-2">
|
||||
<div class="text-warning text-caption mb-1">迟到 ({{ item.data.late.length }})</div>
|
||||
<div class="d-flex flex-wrap" style="gap: 4px">
|
||||
<v-chip v-for="name in item.data.late" :key="name" color="warning" size="x-small" variant="flat">
|
||||
{{ name }}
|
||||
</v-chip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="item.data.exclude.length > 0" class="mb-2">
|
||||
<div class="text-grey text-caption mb-1">不参与 ({{ item.data.exclude.length }})</div>
|
||||
<div class="d-flex flex-wrap" style="gap: 4px">
|
||||
<v-chip v-for="name in item.data.exclude" :key="name" color="grey" size="x-small" variant="flat">
|
||||
{{ name }}
|
||||
</v-chip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="
|
||||
item.data.absent.length === 0 &&
|
||||
item.data.late.length === 0 &&
|
||||
item.data.exclude.length === 0
|
||||
"
|
||||
class="text-success text-center mt-2"
|
||||
>
|
||||
全勤
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<!-- 自定义/测试卡片 -->
|
||||
<v-card
|
||||
v-else-if="item.type === 'custom'"
|
||||
:class="{ 'glow-highlight': highlightedCards[item.key], 'cursor-not-allowed': isEditingDisabled, 'cursor-pointer': !isEditingDisabled }"
|
||||
border
|
||||
variant="outlined"
|
||||
color="grey-darken-3"
|
||||
class="glow-track"
|
||||
height="100%"
|
||||
@click="handleCardClick('dialog', item.key)"
|
||||
@@ -111,7 +43,11 @@
|
||||
@touchmove="handleTouchMove"
|
||||
>
|
||||
<v-card-title class="text-primary">
|
||||
<v-icon class="mr-2" icon="mdi-card-text-outline" size="small" />
|
||||
<v-icon
|
||||
class="mr-2"
|
||||
icon="mdi-card-text-outline"
|
||||
size="small"
|
||||
/>
|
||||
{{ item.name }}
|
||||
</v-card-title>
|
||||
<v-card-text :style="contentStyle">
|
||||
@@ -123,23 +59,96 @@
|
||||
<v-card
|
||||
v-else
|
||||
:class="{ 'glow-highlight': highlightedCards[item.key], 'cursor-not-allowed': isEditingDisabled, 'cursor-pointer': !isEditingDisabled }"
|
||||
border
|
||||
variant="outlined"
|
||||
color="grey-darken-3"
|
||||
class="glow-track"
|
||||
height="100%"
|
||||
style="border-radius: 6px !important"
|
||||
@click="handleCardClick('dialog', item.key)"
|
||||
@mousemove="handleMouseMove"
|
||||
@touchmove="handleTouchMove"
|
||||
>
|
||||
<v-card-title>{{ item.name }}</v-card-title>
|
||||
<v-card-text :style="contentStyle">
|
||||
<v-list>
|
||||
<v-list-item
|
||||
v-for="text in splitPoint(item.content)"
|
||||
:key="text"
|
||||
<v-card-title
|
||||
class="text-white"
|
||||
:style="titleStyle"
|
||||
>
|
||||
{{ item.name }}
|
||||
</v-card-title>
|
||||
<v-card-text
|
||||
:style="contentStyle"
|
||||
class="text-white"
|
||||
>
|
||||
<div
|
||||
v-for="(line, index) in splitPoint(item.content)"
|
||||
:key="index"
|
||||
class="d-flex align-start flex-wrap mb-1"
|
||||
>
|
||||
<span class="mr-2 item-line-text">{{ line.text }}</span>
|
||||
<!-- Submission Time -->
|
||||
<v-chip
|
||||
v-if="line.submissionTime"
|
||||
size="small"
|
||||
color="info"
|
||||
variant="flat"
|
||||
prepend-icon="mdi-clock-outline"
|
||||
class="mr-1"
|
||||
:style="{ fontSize: '0.65em !important', height: '1.6em !important', padding: '0 0.5em !important' }"
|
||||
>
|
||||
{{ text }}
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
{{ formatSubmissionTime(line.submissionTime).replace('上交|', '') }}
|
||||
<span
|
||||
class="text-caption ml-1 font-weight-bold"
|
||||
style="font-size: 0.8em !important; opacity: 0.7;"
|
||||
>上交</span>
|
||||
</v-chip>
|
||||
|
||||
<!-- Submit To -->
|
||||
<v-chip
|
||||
v-if="line.submitTo"
|
||||
size="small"
|
||||
color="primary"
|
||||
variant="flat"
|
||||
prepend-icon="mdi-account"
|
||||
class="mr-1"
|
||||
:style="{ fontSize: '0.65em !important', height: '1.6em !important', padding: '0 0.5em !important' }"
|
||||
>
|
||||
交给: {{ line.submitTo }}
|
||||
</v-chip>
|
||||
|
||||
<v-chip
|
||||
v-if="line.optional"
|
||||
size="small"
|
||||
color="orange"
|
||||
variant="flat"
|
||||
prepend-icon="mdi-star-outline"
|
||||
class="mr-1 text-white"
|
||||
:style="{ fontSize: '0.65em !important', height: '1.6em !important', padding: '0 0.5em !important' }"
|
||||
>
|
||||
选做
|
||||
</v-chip>
|
||||
|
||||
<!-- Not Submit Button -->
|
||||
<v-chip
|
||||
v-if="line.notSubmit"
|
||||
size="small"
|
||||
color="warning"
|
||||
variant="flat"
|
||||
class="mr-1"
|
||||
:style="{ fontSize: '0.65em !important', height: '1.6em !important', padding: '0 0.5em !important' }"
|
||||
>
|
||||
不上交
|
||||
</v-chip>
|
||||
|
||||
<!-- Self Correction -->
|
||||
<v-chip
|
||||
v-if="line.selfCorrection"
|
||||
size="small"
|
||||
color="success"
|
||||
variant="flat"
|
||||
:style="{ fontSize: '0.65em !important', height: '1.6em !important', padding: '0 0.5em !important' }"
|
||||
>
|
||||
自批
|
||||
</v-chip>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</div>
|
||||
@@ -149,7 +158,10 @@
|
||||
<!-- 单独显示空科目 -->
|
||||
<div class="empty-subjects mt-4">
|
||||
<!-- 移动端优化视图 -->
|
||||
<div v-if="isMobile" class="d-flex flex-wrap justify-center">
|
||||
<div
|
||||
v-if="isMobile"
|
||||
class="d-flex flex-wrap justify-center"
|
||||
>
|
||||
<v-chip
|
||||
v-for="subject in unusedSubjects"
|
||||
:key="subject.name"
|
||||
@@ -158,7 +170,10 @@
|
||||
variant="tonal"
|
||||
@click="handleCardClick('dialog', subject.name)"
|
||||
>
|
||||
<v-icon start size="small">
|
||||
<v-icon
|
||||
start
|
||||
size="small"
|
||||
>
|
||||
{{ isReadOnlyToken ? 'mdi-cancel' : 'mdi-plus' }}
|
||||
</v-icon>
|
||||
{{ subject.name }}
|
||||
@@ -166,7 +181,10 @@
|
||||
</div>
|
||||
|
||||
<template v-else-if="emptySubjectDisplay === 'button'">
|
||||
<v-btn-group divided variant="tonal">
|
||||
<v-btn-group
|
||||
divided
|
||||
variant="tonal"
|
||||
>
|
||||
<v-btn
|
||||
v-for="subject in unusedSubjects"
|
||||
:key="subject.name"
|
||||
@@ -179,26 +197,44 @@
|
||||
</v-btn>
|
||||
</v-btn-group>
|
||||
</template>
|
||||
<div v-else class="empty-subjects-grid">
|
||||
<div
|
||||
v-else
|
||||
class="empty-subjects-grid"
|
||||
>
|
||||
<TransitionGroup name="v-list">
|
||||
<v-card
|
||||
v-for="subject in unusedSubjects"
|
||||
:key="subject.name"
|
||||
border
|
||||
style="border-radius: 6px !important"
|
||||
class="empty-subject-card"
|
||||
@click="handleCardClick('dialog', subject.name)"
|
||||
>
|
||||
<v-card-title class="text-subtitle-1">
|
||||
<v-card-title class="text-subtitle-1 text-white">
|
||||
{{ subject.name }}
|
||||
</v-card-title>
|
||||
<v-card-text class="text-center">
|
||||
<template v-if="isReadOnlyToken">
|
||||
<v-icon color="grey" size="small"> mdi-cancel </v-icon>
|
||||
<div class="text-caption text-grey"> 当日无作业 </div>
|
||||
<v-icon
|
||||
color="white"
|
||||
size="small"
|
||||
>
|
||||
mdi-cancel
|
||||
</v-icon>
|
||||
<div class="text-caption text-white">
|
||||
当日无作业
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<v-icon color="grey" size="small"> mdi-plus </v-icon>
|
||||
<div class="text-caption text-grey"> 点击添加作业 </div>
|
||||
<v-icon
|
||||
color="white"
|
||||
size="small"
|
||||
>
|
||||
mdi-plus
|
||||
</v-icon>
|
||||
<div class="text-caption text-white">
|
||||
点击添加作业
|
||||
</div>
|
||||
</template>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
@@ -210,20 +246,13 @@
|
||||
<script>
|
||||
import HitokotoCard from "@/components/HitokotoCard.vue";
|
||||
import TimeCard from "@/components/TimeCard.vue";
|
||||
import ConciseExamCard from "@/components/home/ConciseExamCard.vue";
|
||||
import {getSetting} from "@/utils/settings.js";
|
||||
|
||||
export default {
|
||||
name: "HomeworkGrid",
|
||||
computed: {
|
||||
settings() {
|
||||
return settings
|
||||
}
|
||||
},
|
||||
components: {
|
||||
HitokotoCard,
|
||||
TimeCard,
|
||||
ConciseExamCard,
|
||||
},
|
||||
props: {
|
||||
sortedItems: {
|
||||
@@ -250,23 +279,25 @@ export default {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
titleStyle: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
highlightedCards: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
emits: ["open-dialog", "open-attendance", "disabled-click"],
|
||||
emits: ["open-dialog", "disabled-click"],
|
||||
data() {
|
||||
return {
|
||||
isReadOnlyToken: false,
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
/* eslint-disable no-undef */
|
||||
this.resizeObserver = new ResizeObserver(() => {
|
||||
this.resizeObserver = new window.ResizeObserver(() => {
|
||||
this.resizeAllGridItems();
|
||||
});
|
||||
/* eslint-enable no-undef */
|
||||
|
||||
// Observe the grid container for width changes
|
||||
if (this.$refs.gridContainer) {
|
||||
@@ -309,7 +340,9 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getSetting,
|
||||
getSetting(key) {
|
||||
return getSetting(key);
|
||||
},
|
||||
async checkReadOnlyStatus() {
|
||||
// 尝试获取父组件中的StudentNameManager引用
|
||||
try {
|
||||
@@ -327,8 +360,6 @@ export default {
|
||||
this.isReadOnlyToken = manager.isReadOnly;
|
||||
} else {
|
||||
// 如果无法直接访问manager,尝试通过全局设置获取token信息
|
||||
// 这里需要使用utils/settings中的函数
|
||||
const { getSetting } = await import('@/utils/settings');
|
||||
const token = getSetting('server.kvToken');
|
||||
|
||||
if (token) {
|
||||
@@ -361,8 +392,11 @@ export default {
|
||||
const grid = this.$refs.gridContainer;
|
||||
if (!grid) return;
|
||||
|
||||
const rowHeight = parseInt(window.getComputedStyle(grid).getPropertyValue('grid-auto-rows'));
|
||||
const rowGap = parseInt(window.getComputedStyle(grid).getPropertyValue('gap'));
|
||||
const rowHeightVal = window.getComputedStyle(grid).getPropertyValue('grid-auto-rows');
|
||||
const rowGapVal = window.getComputedStyle(grid).getPropertyValue('gap');
|
||||
|
||||
const rowHeight = parseInt(rowHeightVal) || 1;
|
||||
const rowGap = parseInt(rowGapVal) || 16;
|
||||
|
||||
// Find the content element (v-card or div)
|
||||
const content = item.firstElementChild;
|
||||
@@ -372,11 +406,20 @@ export default {
|
||||
// We use scrollHeight to get the full height of content
|
||||
// Add a small buffer to prevent scrollbars
|
||||
const contentHeight = content.getBoundingClientRect().height;
|
||||
|
||||
// If content height is 0, it might not be rendered yet.
|
||||
// Set a min height to ensure visibility if data is present
|
||||
if (contentHeight === 0 && item.dataset.key) {
|
||||
// Fallback for initial render
|
||||
}
|
||||
|
||||
// Formula: span = ceil((contentHeight + gap) / (rowHeight + gap))
|
||||
const rowSpan = Math.ceil((contentHeight + rowGap) / (rowHeight + rowGap));
|
||||
|
||||
item.style.gridRowEnd = `span ${rowSpan}`;
|
||||
// Ensure minimum span
|
||||
const finalSpan = Math.max(rowSpan, 10);
|
||||
|
||||
item.style.gridRowEnd = `span ${finalSpan}`;
|
||||
},
|
||||
resizeAllGridItems() {
|
||||
const items = this.$refs.items;
|
||||
@@ -390,14 +433,123 @@ export default {
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === 'attendance') {
|
||||
this.$emit('open-attendance');
|
||||
} else if (type === 'dialog') {
|
||||
if (type === 'dialog') {
|
||||
this.$emit('open-dialog', key);
|
||||
}
|
||||
},
|
||||
splitPoint(content) {
|
||||
return content.split("\n").filter((text) => text.trim());
|
||||
if (typeof content !== 'string') return [];
|
||||
|
||||
const normalized = content.replace(/\r\n/g, "\n");
|
||||
const blocks = /\n\s*\n/.test(normalized)
|
||||
? normalized.split(/\n\s*\n+/)
|
||||
: normalized.split("\n");
|
||||
const items = blocks.map((t) => t.trim()).filter(Boolean);
|
||||
const showIndex = items.length > 1;
|
||||
|
||||
return items.map((text, index) => {
|
||||
let processedText = text;
|
||||
let submissionTime = null;
|
||||
let submitTo = null;
|
||||
let selfCorrection = false;
|
||||
let optional = false;
|
||||
let notSubmit = false;
|
||||
|
||||
// Match [YYYY-MM-DD]
|
||||
const timeMatch = processedText.match(/\[(\d{4}-\d{2}-\d{2})\]/);
|
||||
if (timeMatch) {
|
||||
submissionTime = timeMatch[1];
|
||||
processedText = processedText.replace(timeMatch[0], '');
|
||||
}
|
||||
|
||||
// Match [交给:XXX]
|
||||
const submitToMatch = processedText.match(/\[交给:(.*?)\]/);
|
||||
if (submitToMatch) {
|
||||
submitTo = submitToMatch[1];
|
||||
processedText = processedText.replace(submitToMatch[0], '');
|
||||
}
|
||||
|
||||
// Match [自批]
|
||||
const selfCorrectMatch = processedText.match(/\[自批\]/);
|
||||
if (selfCorrectMatch) {
|
||||
selfCorrection = true;
|
||||
processedText = processedText.replace(selfCorrectMatch[0], '');
|
||||
}
|
||||
|
||||
const optionalMatch = processedText.match(/\[选做\]/);
|
||||
if (optionalMatch) {
|
||||
optional = true;
|
||||
processedText = processedText.replace(optionalMatch[0], '');
|
||||
}
|
||||
|
||||
// Match [不上交]
|
||||
const notSubmitMatch = processedText.match(/\[不上交\]/);
|
||||
if (notSubmitMatch) {
|
||||
notSubmit = true;
|
||||
processedText = processedText.replace(notSubmitMatch[0], '');
|
||||
}
|
||||
|
||||
// 添加序号
|
||||
const displayText = showIndex
|
||||
? `${index + 1}. ${processedText.trim()}`
|
||||
: processedText.trim();
|
||||
|
||||
return {
|
||||
text: displayText,
|
||||
submissionTime,
|
||||
submitTo,
|
||||
selfCorrection,
|
||||
optional,
|
||||
notSubmit
|
||||
};
|
||||
});
|
||||
},
|
||||
formatSubmissionTime(timeStr) {
|
||||
if (!timeStr) return '';
|
||||
|
||||
const date = new Date(timeStr);
|
||||
if (isNaN(date.getTime())) return timeStr;
|
||||
|
||||
const today = new Date();
|
||||
// Reset time parts for comparison
|
||||
today.setHours(0,0,0,0);
|
||||
|
||||
const targetDate = new Date(date);
|
||||
targetDate.setHours(0,0,0,0);
|
||||
|
||||
const diffTime = targetDate - today;
|
||||
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
||||
|
||||
if (diffDays === 0) return '上交|今天';
|
||||
if (diffDays === 1) return '上交|明天';
|
||||
|
||||
const weekdays = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
|
||||
const targetDay = weekdays[targetDate.getDay()];
|
||||
|
||||
// Calculate week difference
|
||||
// Get the Monday of the current week
|
||||
const currentMonday = new Date(today);
|
||||
const day = currentMonday.getDay() || 7; // Get current day number, converting Sun (0) to 7
|
||||
if (day !== 1) currentMonday.setHours(-24 * (day - 1));
|
||||
|
||||
// Get the Monday of the target date's week
|
||||
const targetMonday = new Date(targetDate);
|
||||
const tDay = targetMonday.getDay() || 7;
|
||||
if (tDay !== 1) targetMonday.setHours(-24 * (tDay - 1));
|
||||
|
||||
const weekDiff = Math.round((targetMonday - currentMonday) / (7 * 24 * 60 * 60 * 1000));
|
||||
|
||||
if (weekDiff === 0) {
|
||||
return `上交|${targetDay}`;
|
||||
} else if (weekDiff === 1) {
|
||||
return `上交|下${targetDay}`;
|
||||
} else if (weekDiff === 2) {
|
||||
return `上交|下下${targetDay}`;
|
||||
} else {
|
||||
const m = targetDate.getMonth() + 1;
|
||||
const d = targetDate.getDate();
|
||||
return `上交|${m}.${d}`;
|
||||
}
|
||||
},
|
||||
handleMouseMove(e) {
|
||||
const card = e.currentTarget;
|
||||
@@ -434,4 +586,8 @@ export default {
|
||||
.v-card.cursor-not-allowed:hover {
|
||||
transform: none !important;
|
||||
}
|
||||
|
||||
.item-line-text {
|
||||
white-space: pre-line;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,15 +1,29 @@
|
||||
<template>
|
||||
<v-card border hover rounded="xl">
|
||||
<v-card
|
||||
border
|
||||
hover
|
||||
rounded="xl"
|
||||
>
|
||||
<v-card-item>
|
||||
<template #prepend>
|
||||
<v-icon class="mr-2" icon="mdi-information" size="large"/>
|
||||
<v-icon
|
||||
class="mr-2"
|
||||
icon="mdi-information"
|
||||
size="large"
|
||||
/>
|
||||
</template>
|
||||
<v-card-title class="text-h6">关于</v-card-title>
|
||||
<v-card-title class="text-h6">
|
||||
关于
|
||||
</v-card-title>
|
||||
</v-card-item>
|
||||
|
||||
<v-card-text>
|
||||
<v-row>
|
||||
<v-col class="mx-auto" cols="12" md="8">
|
||||
<v-col
|
||||
class="mx-auto"
|
||||
cols="12"
|
||||
md="8"
|
||||
>
|
||||
<!-- 捐赠卡片 -->
|
||||
<v-card
|
||||
border
|
||||
@@ -24,8 +38,9 @@
|
||||
<v-card-item>
|
||||
<div class="card-content">
|
||||
<div>
|
||||
<div class="text-h6 font-weight-bold">请支持我们 Classworks</div>
|
||||
|
||||
<div class="text-h6 font-weight-bold">
|
||||
请支持我们 Classworks
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</v-card-item>
|
||||
@@ -50,15 +65,22 @@
|
||||
</v-card>
|
||||
|
||||
<div class="d-flex flex-column align-start">
|
||||
<v-avatar class="mb-4" size="120">
|
||||
<v-img
|
||||
alt="Classworks"
|
||||
src="../../assets/cslogo.png"
|
||||
/>
|
||||
<v-avatar
|
||||
class="mb-4"
|
||||
color="primary"
|
||||
size="120"
|
||||
>
|
||||
<v-icon size="80">
|
||||
mdi-school
|
||||
</v-icon>
|
||||
</v-avatar>
|
||||
|
||||
<h2 class="text-h5 mb-2">Classworks</h2>
|
||||
<p class="text-body-1 mb-4">适用于班级大屏的作业板小工具</p>
|
||||
<h2 class="text-h5 mb-2">
|
||||
Classworks
|
||||
</h2>
|
||||
<p class="text-body-1 mb-4">
|
||||
适用于班级大屏的作业板小工具
|
||||
</p>
|
||||
|
||||
<div class="d-flex gap-2 flex-wrap mb-6">
|
||||
<v-btn
|
||||
@@ -96,9 +118,11 @@
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<v-divider class="mb-4 w-100"></v-divider>
|
||||
<v-divider class="mb-4 w-100" />
|
||||
|
||||
<h3 class="text-h6 mb-2">备注与致谢</h3>
|
||||
<h3 class="text-h6 mb-2">
|
||||
备注与致谢
|
||||
</h3>
|
||||
<v-list class="mb-4 bg-transparent">
|
||||
<v-list-item
|
||||
append-icon="mdi-link"
|
||||
@@ -113,31 +137,7 @@
|
||||
等人的贡献
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
<v-list-item
|
||||
append-icon="mdi-link"
|
||||
href="https://hlyun.org"
|
||||
target="_blank"
|
||||
>
|
||||
<v-list-item-title>
|
||||
Classworks 由<strong>厚浪云</strong>提供
|
||||
</v-list-item-title>
|
||||
<v-list-item-subtitle>
|
||||
长江后浪推前浪 浮事新人换旧人
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
<v-list-item
|
||||
append-icon="mdi-link"
|
||||
href="https://zerocat.houlangs.com"
|
||||
target="_blank"
|
||||
>
|
||||
<v-list-item-title>
|
||||
感谢 ZeroCat 社区的开发者们
|
||||
</v-list-item-title>
|
||||
<v-list-item-subtitle>
|
||||
新一代,开源,编程社区
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
<v-divider class="ma-1"></v-divider>
|
||||
<v-divider class="ma-1" />
|
||||
<v-list-item
|
||||
append-icon="mdi-link"
|
||||
href="https://github.com/HUSX100/IslandCaller"
|
||||
@@ -180,12 +180,14 @@
|
||||
fullscreen
|
||||
transition="dialog-bottom-transition"
|
||||
>
|
||||
<v-card
|
||||
>
|
||||
<v-card>
|
||||
<v-toolbar>
|
||||
<v-btn icon="mdi-close" @click="showDeps = false"></v-btn>
|
||||
<v-btn
|
||||
icon="mdi-close"
|
||||
@click="showDeps = false"
|
||||
/>
|
||||
<v-toolbar-title>使用的第三方库</v-toolbar-title>
|
||||
<v-spacer></v-spacer>
|
||||
<v-spacer />
|
||||
</v-toolbar>
|
||||
<v-card-text>
|
||||
<v-list>
|
||||
@@ -209,57 +211,108 @@
|
||||
</v-dialog>
|
||||
|
||||
<!-- 报告问题对话框 -->
|
||||
<v-dialog v-model="showReportDialog" max-width="640">
|
||||
<v-dialog
|
||||
v-model="showReportDialog"
|
||||
max-width="640"
|
||||
>
|
||||
<v-card>
|
||||
<v-toolbar density="compact">
|
||||
<v-btn icon="mdi-close" @click="showReportDialog = false"></v-btn>
|
||||
<v-btn
|
||||
icon="mdi-close"
|
||||
@click="showReportDialog = false"
|
||||
/>
|
||||
<v-toolbar-title>报告问题</v-toolbar-title>
|
||||
<v-spacer></v-spacer>
|
||||
<v-spacer />
|
||||
</v-toolbar>
|
||||
<v-card-text>
|
||||
<p class="mb-4">
|
||||
调试ID与下方的浏览器环境信息将帮助我们快速定位问题,请在反馈中一并附上。
|
||||
</p>
|
||||
<v-sheet class="mb-3 pa-3 bg-grey-lighten-4 rounded" style="max-height: 260px; overflow: auto;">
|
||||
<pre class="text-body-2" style="white-space: pre-wrap; margin: 0;">{{ envBoxText }}</pre>
|
||||
<v-sheet
|
||||
class="mb-3 pa-3 bg-grey-lighten-4 rounded"
|
||||
style="max-height: 260px; overflow: auto;"
|
||||
>
|
||||
<pre
|
||||
class="text-body-2"
|
||||
style="white-space: pre-wrap; margin: 0;"
|
||||
>{{ envBoxText }}</pre>
|
||||
</v-sheet>
|
||||
<div class="d-flex gap-2 flex-wrap mb-4">
|
||||
<v-btn size="small" variant="text" prepend-icon="mdi-refresh" @click="reloadVisitorId" :loading="visitorLoading">刷新</v-btn>
|
||||
<v-btn size="small" variant="text" prepend-icon="mdi-content-copy" @click="copyEnvInfo">复制信息</v-btn>
|
||||
<v-btn size="small" variant="text" prepend-icon="mdi-open-in-new" @click="goToDebug">查看 /debug 页面</v-btn>
|
||||
</div>
|
||||
<v-alert v-if="copyOk" type="success" density="compact" class="mb-4">已复制到剪贴板</v-alert>
|
||||
<div class="d-flex gap-2 mb-4">
|
||||
<v-btn
|
||||
size="small"
|
||||
color="primary"
|
||||
variant="elevated"
|
||||
prepend-icon="mdi-message-alert"
|
||||
@click="openFeedback"
|
||||
block
|
||||
variant="text"
|
||||
prepend-icon="mdi-refresh"
|
||||
:loading="visitorLoading"
|
||||
@click="reloadVisitorId"
|
||||
>
|
||||
发送错误反馈到 Sentry
|
||||
刷新
|
||||
</v-btn>
|
||||
<v-btn
|
||||
size="small"
|
||||
variant="text"
|
||||
prepend-icon="mdi-content-copy"
|
||||
@click="copyEnvInfo"
|
||||
>
|
||||
复制信息
|
||||
</v-btn>
|
||||
<v-btn
|
||||
size="small"
|
||||
variant="text"
|
||||
prepend-icon="mdi-open-in-new"
|
||||
@click="goToDebug"
|
||||
>
|
||||
查看 /debug 页面
|
||||
</v-btn>
|
||||
</div>
|
||||
<h4 class="text-subtitle-1 mb-2">反馈渠道</h4>
|
||||
<v-list lines="one" class="bg-transparent">
|
||||
<v-list-item :href="qqGroupLink" target="_blank" prepend-icon="mdi-qqchat">
|
||||
<v-alert
|
||||
v-if="copyOk"
|
||||
type="success"
|
||||
density="compact"
|
||||
class="mb-4"
|
||||
>
|
||||
已复制到剪贴板
|
||||
</v-alert>
|
||||
<h4 class="text-subtitle-1 mb-2">
|
||||
反馈渠道
|
||||
</h4>
|
||||
<v-list
|
||||
lines="one"
|
||||
class="bg-transparent"
|
||||
>
|
||||
<v-list-item
|
||||
:href="qqGroupLink"
|
||||
target="_blank"
|
||||
prepend-icon="mdi-qqchat"
|
||||
>
|
||||
<v-list-item-title>QQ群 ({{ qqGroupNumber }})</v-list-item-title>
|
||||
<v-list-item-subtitle>964979747</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
<v-list-item :href="githubIssueUrl" target="_blank" prepend-icon="mdi-github">
|
||||
<v-list-item
|
||||
:href="githubIssueUrl"
|
||||
target="_blank"
|
||||
prepend-icon="mdi-github"
|
||||
>
|
||||
<v-list-item-title>GitHub Issue</v-list-item-title>
|
||||
<v-list-item-subtitle>ZeroCatDev/Classworks</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
<v-list-item :href="mailtoLink" target="_blank" prepend-icon="mdi-email">
|
||||
<v-list-item
|
||||
:href="mailtoLink"
|
||||
target="_blank"
|
||||
prepend-icon="mdi-email"
|
||||
>
|
||||
<v-list-item-title>邮件</v-list-item-title>
|
||||
<v-list-item-subtitle>sun@wuyuan.dev</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn variant="text" @click="showReportDialog = false">关闭</v-btn>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
variant="text"
|
||||
@click="showReportDialog = false"
|
||||
>
|
||||
关闭
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
@@ -364,17 +417,17 @@ export default {
|
||||
const nav = navigator || {};
|
||||
const intl = (typeof Intl !== 'undefined' && Intl.DateTimeFormat) ? Intl.DateTimeFormat().resolvedOptions() : {};
|
||||
const tz = intl && intl.timeZone ? intl.timeZone : '';
|
||||
const routePath = router.currentRoute?.value?.fullPath || location.pathname;
|
||||
const routePath = router.currentRoute?.value?.fullPath || window.location.pathname;
|
||||
const lines = [
|
||||
`App 版本: v${packageJson?.version || 'unknown'}`,
|
||||
`URL: ${location.href}`,
|
||||
`URL: ${window.location.href}`,
|
||||
`路由: ${routePath}`,
|
||||
`UserAgent: ${nav.userAgent || ''}`,
|
||||
`语言: ${nav.language || ''}`,
|
||||
`时区: ${tz}`,
|
||||
`平台: ${nav.platform || ''}`,
|
||||
`在线: ${String(nav.onLine)}`,
|
||||
`屏幕: ${screen?.width || '-'}x${screen?.height || '-'}`,
|
||||
`屏幕: ${window.screen?.width || '-'}x${window.screen?.height || '-'}`,
|
||||
`视口: ${window.innerWidth || '-'}x${window.innerHeight || '-'}`,
|
||||
];
|
||||
return lines.join('\n');
|
||||
@@ -408,15 +461,6 @@ export default {
|
||||
return `mailto:sun@wuyuan.dev?subject=${subject}&body=${body}`;
|
||||
});
|
||||
|
||||
const openFeedback = () => {
|
||||
// 打开反馈对话框
|
||||
if (typeof window.openSentryFeedback === 'function') {
|
||||
window.openSentryFeedback();
|
||||
} else {
|
||||
console.warn('Sentry Feedback 功能不可用');
|
||||
}
|
||||
};
|
||||
|
||||
const openDonationLink = () => {
|
||||
window.open('https://afdian.com/a/wydev', '_blank');
|
||||
};
|
||||
@@ -438,7 +482,6 @@ export default {
|
||||
reloadVisitorId,
|
||||
openReportDialog,
|
||||
copyEnvInfo,
|
||||
openFeedback,
|
||||
openDonationLink,
|
||||
envBoxText,
|
||||
envInfo,
|
||||
|
||||
@@ -1,17 +1,33 @@
|
||||
<template>
|
||||
<v-dialog v-model="dialog" max-width="600" scrollable>
|
||||
<v-dialog
|
||||
v-model="dialog"
|
||||
max-width="600"
|
||||
scrollable
|
||||
>
|
||||
<v-card>
|
||||
<v-card-title>迁移到云端</v-card-title>
|
||||
<v-card-text style="height: 400px;">
|
||||
<div v-if="loading" class="d-flex justify-center align-center fill-height">
|
||||
<v-progress-circular indeterminate color="primary"></v-progress-circular>
|
||||
<div
|
||||
v-if="loading"
|
||||
class="d-flex justify-center align-center fill-height"
|
||||
>
|
||||
<v-progress-circular
|
||||
indeterminate
|
||||
color="primary"
|
||||
/>
|
||||
</div>
|
||||
<div v-else-if="keys.length === 0" class="d-flex justify-center align-center fill-height">
|
||||
<div
|
||||
v-else-if="keys.length === 0"
|
||||
class="d-flex justify-center align-center fill-height"
|
||||
>
|
||||
没有找到本地数据
|
||||
</div>
|
||||
<div v-else>
|
||||
<!-- Category Selection -->
|
||||
<v-list select-strategy="classic" class="mb-4">
|
||||
<v-list
|
||||
select-strategy="classic"
|
||||
class="mb-4"
|
||||
>
|
||||
<v-list-subheader>选择数据类型</v-list-subheader>
|
||||
|
||||
<v-list-item
|
||||
@@ -19,32 +35,38 @@
|
||||
:key="category.id"
|
||||
@click="toggleCategory(category)"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<template #prepend>
|
||||
<v-checkbox-btn
|
||||
:model-value="getCategoryState(category)"
|
||||
:indeterminate="getCategoryIndeterminate(category)"
|
||||
@click.stop="toggleCategory(category)"
|
||||
></v-checkbox-btn>
|
||||
/>
|
||||
</template>
|
||||
<v-list-item-title>{{ category.label }}</v-list-item-title>
|
||||
<v-list-item-subtitle>{{ category.description }} ({{ getCategoryCount(category) }} 项)</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
|
||||
<v-divider class="mb-4"></v-divider>
|
||||
<v-divider class="mb-4" />
|
||||
|
||||
<!-- Individual Keys Expansion -->
|
||||
<v-expansion-panels>
|
||||
<v-expansion-panel title="详细数据列表">
|
||||
<v-expansion-panel-text>
|
||||
<v-list select-strategy="classic" density="compact">
|
||||
<v-list
|
||||
select-strategy="classic"
|
||||
density="compact"
|
||||
>
|
||||
<v-list-item
|
||||
v-for="key in keys"
|
||||
:key="key"
|
||||
:value="key"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<v-checkbox-btn v-model="selectedKeys" :value="key"></v-checkbox-btn>
|
||||
<template #prepend>
|
||||
<v-checkbox-btn
|
||||
v-model="selectedKeys"
|
||||
:value="key"
|
||||
/>
|
||||
</template>
|
||||
<v-list-item-title>{{ key }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
@@ -54,14 +76,24 @@
|
||||
</v-expansion-panels>
|
||||
</div>
|
||||
</v-card-text>
|
||||
<v-divider></v-divider>
|
||||
<v-divider />
|
||||
<v-card-actions>
|
||||
<div class="text-caption ml-4 text-medium-emphasis">
|
||||
已选择 {{ selectedKeys.length }} 项
|
||||
</div>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn variant="text" @click="dialog = false">取消</v-btn>
|
||||
<v-btn color="primary" @click="migrate" :loading="migrating" :disabled="selectedKeys.length === 0">
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
variant="text"
|
||||
@click="dialog = false"
|
||||
>
|
||||
取消
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="primary"
|
||||
:loading="migrating"
|
||||
:disabled="selectedKeys.length === 0"
|
||||
@click="migrate"
|
||||
>
|
||||
开始迁移
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
@@ -69,7 +101,10 @@
|
||||
</v-dialog>
|
||||
|
||||
<!-- Result Dialog -->
|
||||
<v-dialog v-model="resultDialog" max-width="500">
|
||||
<v-dialog
|
||||
v-model="resultDialog"
|
||||
max-width="500"
|
||||
>
|
||||
<v-card>
|
||||
<v-card-title>迁移结果</v-card-title>
|
||||
<v-card-text>
|
||||
@@ -79,12 +114,19 @@
|
||||
<p>失败: {{ result.summary.failed }}</p>
|
||||
</div>
|
||||
<div v-else-if="error">
|
||||
<p class="text-error">{{ error }}</p>
|
||||
<p class="text-error">
|
||||
{{ error }}
|
||||
</p>
|
||||
</div>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="primary" @click="resultDialog = false">关闭</v-btn>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
color="primary"
|
||||
@click="resultDialog = false"
|
||||
>
|
||||
关闭
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
@@ -15,7 +15,9 @@
|
||||
@click:close="showAutoplayWarning = false"
|
||||
>
|
||||
<div class="d-flex align-center">
|
||||
<v-icon class="mr-2">mdi-information</v-icon>
|
||||
<v-icon class="mr-2">
|
||||
mdi-information
|
||||
</v-icon>
|
||||
<span>首次使用请点击试听按钮测试音频播放是否正常</span>
|
||||
</div>
|
||||
</v-alert>
|
||||
@@ -25,7 +27,9 @@
|
||||
<v-col cols="12">
|
||||
<v-card border>
|
||||
<v-card-title class="text-subtitle-1">
|
||||
<v-icon left>mdi-bell-ring</v-icon>
|
||||
<v-icon left>
|
||||
mdi-bell-ring
|
||||
</v-icon>
|
||||
单次通知铃声
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
@@ -66,13 +70,17 @@
|
||||
variant="tonal"
|
||||
@click="previewSound(singleSound)"
|
||||
>
|
||||
<v-icon left>mdi-play</v-icon>
|
||||
<v-icon left>
|
||||
mdi-play
|
||||
</v-icon>
|
||||
试听
|
||||
</v-btn>
|
||||
<v-btn
|
||||
@click="resetSingleSound"
|
||||
>
|
||||
<v-icon left>mdi-restore</v-icon>
|
||||
<v-icon left>
|
||||
mdi-restore
|
||||
</v-icon>
|
||||
恢复
|
||||
</v-btn>
|
||||
</div>
|
||||
@@ -84,7 +92,12 @@
|
||||
<v-col cols="12">
|
||||
<v-card border>
|
||||
<v-card-title class="text-subtitle-1">
|
||||
<v-icon left color="error">mdi-bell-alert</v-icon>
|
||||
<v-icon
|
||||
left
|
||||
color="error"
|
||||
>
|
||||
mdi-bell-alert
|
||||
</v-icon>
|
||||
紧急通知铃声
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
@@ -125,13 +138,17 @@
|
||||
variant="tonal"
|
||||
@click="previewSound(urgentSound)"
|
||||
>
|
||||
<v-icon left>mdi-play</v-icon>
|
||||
<v-icon left>
|
||||
mdi-play
|
||||
</v-icon>
|
||||
试听
|
||||
</v-btn>
|
||||
<v-btn
|
||||
@click="resetUrgentSound"
|
||||
>
|
||||
<v-icon left>mdi-restore</v-icon>
|
||||
<v-icon left>
|
||||
mdi-restore
|
||||
</v-icon>
|
||||
恢复
|
||||
</v-btn>
|
||||
</div>
|
||||
@@ -145,7 +162,7 @@
|
||||
|
||||
<script>
|
||||
import { getSetting, setSetting } from '@/utils/settings.js';
|
||||
import { soundFiles, stopSound } from '@/utils/soundList.js';
|
||||
import { soundFiles, stopSound, defaultSingleSound, defaultUrgentSound } from '@/utils/soundList.js';
|
||||
|
||||
export default {
|
||||
name: 'NotificationSoundSettings',
|
||||
@@ -175,6 +192,14 @@ export default {
|
||||
loadSettings() {
|
||||
this.singleSound = getSetting('notification.singleSound');
|
||||
this.urgentSound = getSetting('notification.urgentSound');
|
||||
if (!soundFiles.includes(this.singleSound)) {
|
||||
this.singleSound = defaultSingleSound;
|
||||
setSetting('notification.singleSound', this.singleSound);
|
||||
}
|
||||
if (!soundFiles.includes(this.urgentSound)) {
|
||||
this.urgentSound = defaultUrgentSound;
|
||||
setSetting('notification.urgentSound', this.urgentSound);
|
||||
}
|
||||
},
|
||||
onSingleSoundChange(value) {
|
||||
setSetting('notification.singleSound', value);
|
||||
@@ -256,12 +281,12 @@ export default {
|
||||
}
|
||||
},
|
||||
resetSingleSound() {
|
||||
this.singleSound = 'Teams 默认.mp3';
|
||||
this.singleSound = defaultSingleSound;
|
||||
setSetting('notification.singleSound', this.singleSound);
|
||||
this.$message?.success('已恢复单次通知铃声默认设置');
|
||||
},
|
||||
resetUrgentSound() {
|
||||
this.urgentSound = 'Teams 默认通话铃.mp3';
|
||||
this.urgentSound = defaultUrgentSound;
|
||||
setSetting('notification.urgentSound', this.urgentSound);
|
||||
this.$message?.success('已恢复紧急通知铃声默认设置');
|
||||
},
|
||||
|
||||
@@ -1,7 +1,17 @@
|
||||
<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-card
|
||||
:border="border"
|
||||
class="setting-group"
|
||||
>
|
||||
<v-card-title
|
||||
v-if="title"
|
||||
class="d-flex align-center"
|
||||
>
|
||||
<v-icon
|
||||
v-if="icon"
|
||||
:icon="icon"
|
||||
class="mr-2"
|
||||
/>
|
||||
{{ title }}
|
||||
</v-card-title>
|
||||
|
||||
@@ -18,7 +28,7 @@
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions v-if="$slots.actions">
|
||||
<slot name="actions"></slot>
|
||||
<slot name="actions" />
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
<template>
|
||||
<v-list-item :disabled="disabled" class="setting-item">
|
||||
<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">
|
||||
@@ -14,7 +17,10 @@
|
||||
|
||||
<template #append>
|
||||
<div class="d-flex flex-column flex-sm-row align-center">
|
||||
<div v-if="type !== 'string' || hasOptions" class="me-2">
|
||||
<div
|
||||
v-if="type !== 'string' || hasOptions"
|
||||
class="me-2"
|
||||
>
|
||||
<!-- 根据设置类型渲染不同的控件 -->
|
||||
<v-switch
|
||||
v-if="type === 'boolean'"
|
||||
@@ -40,7 +46,10 @@
|
||||
@update:model-value="updateSetting"
|
||||
/>
|
||||
|
||||
<div v-else-if="type === 'number'" class="d-flex align-center">
|
||||
<div
|
||||
v-else-if="type === 'number'"
|
||||
class="d-flex align-center"
|
||||
>
|
||||
<v-btn
|
||||
:disabled="disabled || localValue <= minValue"
|
||||
icon="mdi-minus"
|
||||
@@ -76,7 +85,7 @@
|
||||
</div>
|
||||
|
||||
<v-menu location="bottom">
|
||||
<template v-slot:activator="{ props }">
|
||||
<template #activator="{ props }">
|
||||
<v-btn
|
||||
:disabled="disabled"
|
||||
class="ml-2"
|
||||
@@ -88,24 +97,36 @@
|
||||
</template>
|
||||
<v-list density="compact">
|
||||
<v-list-item @click="copySettingId">
|
||||
<template v-slot:prepend>
|
||||
<v-icon icon="mdi-key" size="small"/>
|
||||
<template #prepend>
|
||||
<v-icon
|
||||
icon="mdi-key"
|
||||
size="small"
|
||||
/>
|
||||
</template>
|
||||
<v-list-item-title>复制设置ID</v-list-item-title>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item @click="copySettingValue">
|
||||
<template v-slot:prepend>
|
||||
<v-icon icon="mdi-content-copy" size="small"/>
|
||||
<template #prepend>
|
||||
<v-icon
|
||||
icon="mdi-content-copy"
|
||||
size="small"
|
||||
/>
|
||||
</template>
|
||||
<v-list-item-title>复制设置值</v-list-item-title>
|
||||
</v-list-item>
|
||||
|
||||
<v-divider></v-divider>
|
||||
<v-divider />
|
||||
|
||||
<v-list-item :disabled="isDefaultValue" @click="resetToDefault">
|
||||
<template v-slot:prepend>
|
||||
<v-icon icon="mdi-restore" size="small"/>
|
||||
<v-list-item
|
||||
:disabled="isDefaultValue"
|
||||
@click="resetToDefault"
|
||||
>
|
||||
<template #prepend>
|
||||
<v-icon
|
||||
icon="mdi-restore"
|
||||
size="small"
|
||||
/>
|
||||
</template>
|
||||
<v-list-item-title>重置为默认值</v-list-item-title>
|
||||
</v-list-item>
|
||||
@@ -116,7 +137,10 @@
|
||||
</v-list-item>
|
||||
|
||||
<!-- 文本框显示在下方 -->
|
||||
<div v-if="type === 'string' && !hasOptions" class="px-4 pb-2 pt-0">
|
||||
<div
|
||||
v-if="type === 'string' && !hasOptions"
|
||||
class="px-4 pb-2 pt-0"
|
||||
>
|
||||
<v-text-field
|
||||
v-model="localValue"
|
||||
:disabled="disabled"
|
||||
|
||||
@@ -1,30 +1,46 @@
|
||||
<template>
|
||||
<div class="settings-explorer">
|
||||
|
||||
|
||||
<div>
|
||||
<v-text-field v-model="searchQuery" class="mb-4" clearable density="comfortable" label="搜索设置"
|
||||
prepend-inner-icon="mdi-magnify" variant="outlined"/>
|
||||
<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" :disabled="setting.requireDeveloper && !isDeveloperMode"
|
||||
:setting-key="setting.key" @error="onSettingError"
|
||||
@update="onSettingUpdate"/>
|
||||
<v-divider class="my-2"/>
|
||||
<div
|
||||
v-for="setting in allSettings"
|
||||
:key="setting.key"
|
||||
>
|
||||
<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-card-title class="text-subtitle-1">当前配置</v-card-title>
|
||||
<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-spacer />
|
||||
<v-btn @click="copySettingsToClipboard">
|
||||
复制到剪贴板
|
||||
<v-icon right>mdi-content-copy</v-icon>
|
||||
<v-icon right>
|
||||
mdi-content-copy
|
||||
</v-icon>
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
|
||||
@@ -6,11 +6,20 @@
|
||||
>
|
||||
<v-card-item>
|
||||
<template #prepend>
|
||||
<v-icon class="mr-2" icon="mdi-account-group" size="large"/>
|
||||
<v-icon
|
||||
class="mr-2"
|
||||
icon="mdi-account-group"
|
||||
size="large"
|
||||
/>
|
||||
</template>
|
||||
<v-card-title class="text-h6">学生列表</v-card-title>
|
||||
<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"
|
||||
@@ -39,7 +48,13 @@
|
||||
indeterminate
|
||||
/>
|
||||
|
||||
<v-alert v-if="error" class="mb-4" closable type="error" variant="tonal">
|
||||
<v-alert
|
||||
v-if="error"
|
||||
class="mb-4"
|
||||
closable
|
||||
type="error"
|
||||
variant="tonal"
|
||||
>
|
||||
{{ error }}
|
||||
</v-alert>
|
||||
|
||||
@@ -47,7 +62,11 @@
|
||||
<!-- 普通编辑模式 -->
|
||||
<div v-if="!modelValue.advanced">
|
||||
<v-row class="mb-6">
|
||||
<v-col cols="12" md="4" sm="6">
|
||||
<v-col
|
||||
cols="12"
|
||||
md="4"
|
||||
sm="6"
|
||||
>
|
||||
<v-text-field
|
||||
v-model="newStudentName"
|
||||
class="mb-4"
|
||||
@@ -88,7 +107,10 @@
|
||||
v-bind="props"
|
||||
>
|
||||
<v-card-text class="d-flex align-center pa-3">
|
||||
<v-menu :open-on-hover="!isMobile" location="bottom">
|
||||
<v-menu
|
||||
:open-on-hover="!isMobile"
|
||||
location="bottom"
|
||||
>
|
||||
<template #activator="{ props: menuProps }">
|
||||
<v-btn
|
||||
class="mr-3 font-weight-medium"
|
||||
@@ -100,7 +122,10 @@
|
||||
</v-btn>
|
||||
</template>
|
||||
|
||||
<v-list density="compact" nav>
|
||||
<v-list
|
||||
density="compact"
|
||||
nav
|
||||
>
|
||||
<v-list-item
|
||||
:disabled="index === 0"
|
||||
prepend-icon="mdi-arrow-up-bold"
|
||||
@@ -108,7 +133,7 @@
|
||||
>
|
||||
置顶
|
||||
</v-list-item>
|
||||
<v-divider/>
|
||||
<v-divider />
|
||||
<v-list-item
|
||||
:disabled="index === 0"
|
||||
prepend-icon="mdi-arrow-up"
|
||||
@@ -172,7 +197,10 @@
|
||||
</div>
|
||||
|
||||
<!-- 高级编辑模式 -->
|
||||
<div v-else class="pt-2">
|
||||
<div
|
||||
v-else
|
||||
class="pt-2"
|
||||
>
|
||||
<v-textarea
|
||||
v-model="modelValue.text"
|
||||
hint="使用文本编辑模式批量编辑学生名单,保存时会自动去除空行"
|
||||
@@ -187,7 +215,10 @@
|
||||
</v-expand-transition>
|
||||
|
||||
<v-row class="mt-6">
|
||||
<v-col class="d-flex gap-2" cols="12">
|
||||
<v-col
|
||||
class="d-flex gap-2"
|
||||
cols="12"
|
||||
>
|
||||
<v-btn
|
||||
:disabled="loading"
|
||||
:loading="loading"
|
||||
@@ -228,8 +259,6 @@ async function loadPinyin() {
|
||||
}
|
||||
return _pinyin;
|
||||
}
|
||||
import {getSetting} from "@/utils/settings";
|
||||
|
||||
export default {
|
||||
name: "StudentListCard",
|
||||
components: {
|
||||
@@ -267,7 +296,6 @@ export default {
|
||||
if (!this.modelValue.advanced) {
|
||||
this.modelValue.text = newData.list
|
||||
.slice()
|
||||
.sort((a, b) => a.id - b.id)
|
||||
.map(s => s.name)
|
||||
.join("\n");
|
||||
}
|
||||
@@ -285,22 +313,20 @@ export default {
|
||||
this.error = null;
|
||||
try {
|
||||
this.loading = true;
|
||||
const classNum = getSetting("server.classNumber");
|
||||
|
||||
if (!classNum) {
|
||||
throw new Error("请先设置班号");
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await dataProvider.loadData("classworks-list-main");
|
||||
console.log("loadStudents response:", response);
|
||||
|
||||
if (response.success != false && Array.isArray(response)) {
|
||||
this.modelValue.list = response.map((item, index) => {
|
||||
// 兼容返回数组或包含 value 属性的对象
|
||||
const dataArray = Array.isArray(response) ? response : (response && Array.isArray(response.value) ? response.value : null);
|
||||
|
||||
if (response.success != false && dataArray) {
|
||||
this.modelValue.list = dataArray.map((item, index) => {
|
||||
if (typeof item === 'string') {
|
||||
return {id: index + 1, name: item};
|
||||
}
|
||||
return {
|
||||
id: item.id || index + 1,
|
||||
id: Number(item.id) || index + 1,
|
||||
name: item.name || item.toString()
|
||||
};
|
||||
});
|
||||
@@ -328,15 +354,9 @@ export default {
|
||||
|
||||
async saveStudents() {
|
||||
try {
|
||||
const classNum = getSetting("server.classNumber");
|
||||
|
||||
if (!classNum) {
|
||||
throw new Error("请先设置班号");
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
const formattedList = this.modelValue.list
|
||||
.slice()
|
||||
.sort((a, b) => a.id - b.id)
|
||||
.map((student, index) => ({
|
||||
id: index + 1,
|
||||
name: student.name
|
||||
@@ -358,6 +378,8 @@ export default {
|
||||
} catch (error) {
|
||||
console.error("保存学生列表失败:", error);
|
||||
this.$message?.error("保存失败", error.message || "请重试");
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -373,7 +395,7 @@ export default {
|
||||
|
||||
// Create a map of existing student names to their IDs
|
||||
const currentIds = new Map(this.modelValue.list.map(s => [s.name, s.id]));
|
||||
let maxId = Math.max(0, ...this.modelValue.list.map(s => s.id));
|
||||
let maxId = Math.max(0, ...this.modelValue.list.map(s => Number(s.id) || 0));
|
||||
|
||||
// Create new list preserving IDs for existing names and generating new IDs for new names
|
||||
const newList = lines.map(name => {
|
||||
@@ -386,18 +408,23 @@ export default {
|
||||
|
||||
// Update the list
|
||||
this.modelValue.list = newList;
|
||||
// Also update the text representation for standard mode
|
||||
if (this.modelValue.advanced) {
|
||||
this.modelValue.text = text;
|
||||
}
|
||||
},
|
||||
|
||||
addStudent() {
|
||||
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));
|
||||
const maxId = Math.max(0, ...this.modelValue.list.map(s => Number(s.id) || 0));
|
||||
this.modelValue.list.push({id: maxId + 1, name});
|
||||
this.newStudentName = "";
|
||||
}
|
||||
},
|
||||
|
||||
startEdit(index, student) {
|
||||
if (this._editTimeout) clearTimeout(this._editTimeout);
|
||||
this.editState.index = index;
|
||||
this.editState.name = student.name;
|
||||
},
|
||||
@@ -408,8 +435,13 @@ export default {
|
||||
if (newName && newName !== this.modelValue.list[this.editState.index].name) {
|
||||
this.modelValue.list[this.editState.index].name = newName;
|
||||
}
|
||||
this.editState.index = -1;
|
||||
this.editState.name = "";
|
||||
|
||||
// 延迟重置状态,防止DOM突变导致丢失按钮点击事件
|
||||
if (this._editTimeout) clearTimeout(this._editTimeout);
|
||||
this._editTimeout = setTimeout(() => {
|
||||
this.editState.index = -1;
|
||||
this.editState.name = "";
|
||||
}, 200);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -6,11 +6,20 @@
|
||||
>
|
||||
<v-card-item>
|
||||
<template #prepend>
|
||||
<v-icon class="mr-2" icon="mdi-account-tie" size="large"/>
|
||||
<v-icon
|
||||
class="mr-2"
|
||||
icon="mdi-account-tie"
|
||||
size="large"
|
||||
/>
|
||||
</template>
|
||||
<v-card-title class="text-h6">教师列表</v-card-title>
|
||||
<v-card-title class="text-h6">
|
||||
教师列表
|
||||
</v-card-title>
|
||||
<template #append>
|
||||
<unsaved-warning :show="unsavedChanges" message="有未保存的更改"/>
|
||||
<unsaved-warning
|
||||
:show="unsavedChanges"
|
||||
message="有未保存的更改"
|
||||
/>
|
||||
<v-btn
|
||||
:color="modelValue.advanced ? 'primary' : undefined"
|
||||
prepend-icon="mdi-code-braces"
|
||||
@@ -30,7 +39,13 @@
|
||||
indeterminate
|
||||
/>
|
||||
|
||||
<v-alert v-if="error" class="mb-4" closable type="error" variant="tonal">
|
||||
<v-alert
|
||||
v-if="error"
|
||||
class="mb-4"
|
||||
closable
|
||||
type="error"
|
||||
variant="tonal"
|
||||
>
|
||||
{{ error }}
|
||||
</v-alert>
|
||||
|
||||
@@ -38,10 +53,16 @@
|
||||
<!-- 普通编辑模式 -->
|
||||
<div v-if="!modelValue.advanced">
|
||||
<!-- 添加教师表单 -->
|
||||
<v-card class="mb-6" variant="outlined">
|
||||
<v-card
|
||||
class="mb-6"
|
||||
variant="outlined"
|
||||
>
|
||||
<v-card-text>
|
||||
<v-row>
|
||||
<v-col cols="12" md="4">
|
||||
<v-col
|
||||
cols="12"
|
||||
md="4"
|
||||
>
|
||||
<v-text-field
|
||||
v-model="newTeacher.name"
|
||||
density="comfortable"
|
||||
@@ -52,7 +73,10 @@
|
||||
variant="outlined"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" md="5">
|
||||
<v-col
|
||||
cols="12"
|
||||
md="5"
|
||||
>
|
||||
<v-combobox
|
||||
v-model="newTeacher.subjects"
|
||||
:items="commonSubjects"
|
||||
@@ -68,7 +92,11 @@
|
||||
variant="outlined"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" md="3" class="d-flex align-center gap-2">
|
||||
<v-col
|
||||
cols="12"
|
||||
md="3"
|
||||
class="d-flex align-center gap-2"
|
||||
>
|
||||
<v-checkbox
|
||||
v-model="newTeacher.isHeadTeacher"
|
||||
density="comfortable"
|
||||
@@ -91,7 +119,10 @@
|
||||
<!-- 教师列表 -->
|
||||
<v-row v-if="modelValue.list.length === 0">
|
||||
<v-col cols="12">
|
||||
<v-alert type="info" variant="tonal">
|
||||
<v-alert
|
||||
type="info"
|
||||
variant="tonal"
|
||||
>
|
||||
暂无教师信息,请添加教师
|
||||
</v-alert>
|
||||
</v-col>
|
||||
@@ -155,7 +186,10 @@
|
||||
</v-chip>
|
||||
</div>
|
||||
|
||||
<div v-if="editState.index === index" class="mt-2">
|
||||
<div
|
||||
v-if="editState.index === index"
|
||||
class="mt-2"
|
||||
>
|
||||
<v-combobox
|
||||
v-model="editState.teacher.subjects"
|
||||
:items="commonSubjects"
|
||||
@@ -175,7 +209,10 @@
|
||||
label="班主任"
|
||||
/>
|
||||
</div>
|
||||
<div v-else class="mt-1">
|
||||
<div
|
||||
v-else
|
||||
class="mt-1"
|
||||
>
|
||||
<v-chip
|
||||
v-for="(subject, sIndex) in teacher.subjects"
|
||||
:key="sIndex"
|
||||
@@ -235,13 +272,16 @@
|
||||
</div>
|
||||
|
||||
<!-- 高级编辑模式 -->
|
||||
<div v-else class="pt-2">
|
||||
<div
|
||||
v-else
|
||||
class="pt-2"
|
||||
>
|
||||
<v-textarea
|
||||
v-model="modelValue.text"
|
||||
hint="JSON 格式编辑教师列表。每个教师需包含 name、subjects(数组)、isHeadTeacher(布尔值)"
|
||||
label="批量编辑教师列表 (JSON)"
|
||||
persistent-hint
|
||||
placeholder='[{"name":"教师姓名","subjects":["语文","数学"],"isHeadTeacher":true}]'
|
||||
placeholder="[{"name":"教师姓名","subjects":["语文","数学"],"isHeadTeacher":true}]"
|
||||
rows="15"
|
||||
variant="outlined"
|
||||
@update:model-value="handleTextInput"
|
||||
@@ -250,7 +290,10 @@
|
||||
</v-expand-transition>
|
||||
|
||||
<v-row class="mt-6">
|
||||
<v-col class="d-flex gap-2" cols="12">
|
||||
<v-col
|
||||
class="d-flex gap-2"
|
||||
cols="12"
|
||||
>
|
||||
<v-btn
|
||||
:disabled="loading"
|
||||
:loading="loading"
|
||||
@@ -282,7 +325,6 @@
|
||||
import UnsavedWarning from "../common/UnsavedWarning.vue";
|
||||
import "@/styles/warnings.scss";
|
||||
import dataProvider from "@/utils/dataProvider";
|
||||
import {getSetting} from "@/utils/settings";
|
||||
|
||||
export default {
|
||||
name: "TeacherListCard",
|
||||
@@ -354,17 +396,15 @@ export default {
|
||||
this.error = null;
|
||||
try {
|
||||
this.loading = true;
|
||||
const classNum = getSetting("server.classNumber");
|
||||
|
||||
if (!classNum) {
|
||||
throw new Error("请先设置班号");
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await dataProvider.loadData("classworks-list-teacher");
|
||||
console.log("loadTeachers response:", response);
|
||||
|
||||
if (response.success !== false && Array.isArray(response)) {
|
||||
this.modelValue.list = response.map((item) => ({
|
||||
// 兼容返回数组或包含 value 属性的对象
|
||||
const dataArray = Array.isArray(response) ? response : (response && Array.isArray(response.value) ? response.value : null);
|
||||
|
||||
if (response.success != false && dataArray) {
|
||||
this.modelValue.list = dataArray.map((item) => ({
|
||||
name: item.name || "",
|
||||
subjects: Array.isArray(item.subjects) ? item.subjects : [],
|
||||
isHeadTeacher: Boolean(item.isHeadTeacher),
|
||||
@@ -391,12 +431,7 @@ export default {
|
||||
|
||||
async saveTeachers() {
|
||||
try {
|
||||
const classNum = getSetting("server.classNumber");
|
||||
|
||||
if (!classNum) {
|
||||
throw new Error("请先设置班号");
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
const formattedList = this.modelValue.list.map((teacher) => ({
|
||||
name: teacher.name,
|
||||
subjects: Array.isArray(teacher.subjects) ? teacher.subjects : [],
|
||||
@@ -419,6 +454,8 @@ export default {
|
||||
} catch (error) {
|
||||
console.error("保存教师列表失败:", error);
|
||||
this.$message?.error("保存失败", error.message || "请重试");
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -476,16 +513,17 @@ export default {
|
||||
},
|
||||
|
||||
startEdit(index, teacher) {
|
||||
if (this._editTimeout) clearTimeout(this._editTimeout);
|
||||
this.editState.index = index;
|
||||
// 深度拷贝以防直接修改原对象
|
||||
this.editState.teacher = {
|
||||
name: teacher.name,
|
||||
subjects: [...teacher.subjects],
|
||||
isHeadTeacher: teacher.isHeadTeacher,
|
||||
...teacher,
|
||||
subjects: [...(teacher.subjects || [])],
|
||||
};
|
||||
},
|
||||
|
||||
saveEdit() {
|
||||
if (this.editState.index !== -1) {
|
||||
if (this.editState.index !== -1 && this.editState.teacher) {
|
||||
const newName = this.editState.teacher.name.trim();
|
||||
if (!newName) {
|
||||
this.$message?.warning("提示", "教师姓名不能为空");
|
||||
@@ -502,8 +540,12 @@ export default {
|
||||
isHeadTeacher: this.editState.teacher.isHeadTeacher,
|
||||
};
|
||||
|
||||
this.editState.index = -1;
|
||||
this.editState.teacher = null;
|
||||
// 延迟重置状态,防止DOM突变导致丢失按钮点击事件
|
||||
if (this._editTimeout) clearTimeout(this._editTimeout);
|
||||
this._editTimeout = setTimeout(() => {
|
||||
this.editState.index = -1;
|
||||
this.editState.teacher = null;
|
||||
}, 200);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
<template>
|
||||
<v-card :disabled="!hasNamespaceInfo" :loading="loading" class="my-4">
|
||||
<v-card
|
||||
:disabled="!hasNamespaceInfo"
|
||||
:loading="loading"
|
||||
class="my-4"
|
||||
>
|
||||
<template #loader>
|
||||
<v-progress-linear v-if="loading" color="primary" indeterminate/>
|
||||
<v-progress-linear
|
||||
v-if="loading"
|
||||
color="primary"
|
||||
indeterminate
|
||||
/>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -140,16 +148,8 @@
|
||||
title="Classworks KV"
|
||||
>
|
||||
<v-card-text>
|
||||
Classworks KV 是厚浪云推出的文档形键值数据库,其是一个开放的云应用平台,为各种应用提供存储服务。此设备正在使用其服务,如果您希望管理设备信息,请前往
|
||||
Classworks KV 是一个文档形键值数据库,其是一个开放的云应用平台,为各种应用提供存储服务。此设备正在使用其服务,如果您希望管理设备信息,请前往
|
||||
Classworks KV 的网站,如果您在服务推出前就在使用 Classworks,您的数据已被自动迁移。
|
||||
<br><br>
|
||||
Classworks KV 的全域管理员是
|
||||
<a
|
||||
href="https://wuyuan.dev"
|
||||
target="_blank"
|
||||
>
|
||||
孙悟元
|
||||
</a>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-btn
|
||||
@@ -175,7 +175,7 @@
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions>
|
||||
<v-spacer/>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
:loading="loading"
|
||||
color="primary"
|
||||
@@ -213,7 +213,7 @@
|
||||
<p>您确定要重新初始化云端存储吗?</p>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer/>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
variant="text"
|
||||
@click="showReinitDialog = false"
|
||||
|
||||
@@ -1,16 +1,22 @@
|
||||
<template>
|
||||
<settings-card icon="mdi-database-cog" title="数据源设置">
|
||||
<settings-card
|
||||
icon="mdi-database-cog"
|
||||
title="数据源设置"
|
||||
>
|
||||
<v-list>
|
||||
<!-- 服务器模式设置 -->
|
||||
<template
|
||||
v-if="
|
||||
currentProvider === 'kv-server' ||
|
||||
currentProvider === 'classworkscloud'
|
||||
currentProvider === 'classworkscloud'
|
||||
"
|
||||
>
|
||||
<v-list-item>
|
||||
<template #prepend>
|
||||
<v-icon class="mr-3" icon="mdi-lan-connect"/>
|
||||
<v-icon
|
||||
class="mr-3"
|
||||
icon="mdi-lan-connect"
|
||||
/>
|
||||
</template>
|
||||
<v-list-item-title>检查服务器连接</v-list-item-title>
|
||||
<template #append>
|
||||
@@ -22,64 +28,94 @@
|
||||
测试连接
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-list-item
|
||||
><!-- 数据迁移,仅对KV本地存储有效 -->
|
||||
</v-list-item><!-- 数据迁移,仅对KV本地存储有效 -->
|
||||
</template>
|
||||
|
||||
<!-- 本地存储设置 -->
|
||||
<template v-if="currentProvider === 'kv-local'">
|
||||
<v-list-item>
|
||||
<template #prepend>
|
||||
<v-icon class="mr-3" icon="mdi-database"/>
|
||||
<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>
|
||||
这将清除所有本地数据库中的数据
|
||||
</v-list-item-subtitle>
|
||||
<template #append>
|
||||
<v-btn color="error" variant="tonal" @click="confirmClearIndexedDB">
|
||||
<v-btn
|
||||
color="error"
|
||||
variant="tonal"
|
||||
@click="confirmClearIndexedDB"
|
||||
>
|
||||
清除
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<template #prepend>
|
||||
<v-icon class="mr-3" icon="mdi-database-export"/>
|
||||
<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 class="mr-3" icon="mdi-lan-connect"/>
|
||||
<v-icon
|
||||
class="mr-3"
|
||||
icon="mdi-lan-connect"
|
||||
/>
|
||||
</template>
|
||||
<v-list-item-title>查看本地缓存</v-list-item-title>
|
||||
<template #append>
|
||||
<v-btn to="/cachemanagement" variant="tonal"> 查看</v-btn>
|
||||
<v-btn
|
||||
to="/cachemanagement"
|
||||
variant="tonal"
|
||||
>
|
||||
查看
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
|
||||
<!-- 确认对话框 -->
|
||||
<v-dialog v-model="confirmDialog" max-width="400">
|
||||
<v-dialog
|
||||
v-model="confirmDialog"
|
||||
max-width="400"
|
||||
>
|
||||
<v-card>
|
||||
<v-card-title>{{ confirmTitle }}</v-card-title>
|
||||
<v-card-text>{{ confirmMessage }}</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="grey" variant="text" @click="confirmDialog = false"
|
||||
>取消
|
||||
</v-btn
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
color="grey"
|
||||
variant="text"
|
||||
@click="confirmDialog = false"
|
||||
>
|
||||
<v-btn color="error" variant="tonal" @click="handleConfirm"
|
||||
>确认
|
||||
</v-btn
|
||||
取消
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="error"
|
||||
variant="tonal"
|
||||
@click="handleConfirm"
|
||||
>
|
||||
确认
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
@@ -1,39 +1,42 @@
|
||||
<template>
|
||||
<settings-card border icon="mdi-monitor" title="显示设置">
|
||||
<settings-card
|
||||
border
|
||||
icon="mdi-monitor"
|
||||
title="显示设置"
|
||||
>
|
||||
<v-list>
|
||||
<setting-item :setting-key="'display.emptySubjectDisplay'"/>
|
||||
<setting-item :setting-key="'display.emptySubjectDisplay'" />
|
||||
|
||||
|
||||
<v-divider class="my-2"/>
|
||||
<setting-item :setting-key="'display.dynamicSort'"/>
|
||||
<v-divider class="my-2" />
|
||||
<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.showRandomButton'" />
|
||||
|
||||
<v-divider class="my-2"/>
|
||||
<setting-item :setting-key="'display.showFullscreenButton'"/>
|
||||
<v-divider class="my-2" />
|
||||
<setting-item :setting-key="'display.showFullscreenButton'" />
|
||||
|
||||
<v-divider class="my-2"/>
|
||||
<setting-item :setting-key="'display.cardHoverEffect'"/>
|
||||
<v-divider class="my-2" />
|
||||
<setting-item :setting-key="'display.cardHoverEffect'" />
|
||||
|
||||
<v-divider class="my-2"/>
|
||||
<setting-item :setting-key="'display.enhancedTouchMode'"/>
|
||||
<v-divider class="my-2" />
|
||||
<setting-item :setting-key="'display.enhancedTouchMode'" />
|
||||
|
||||
<v-divider class="my-2"/>
|
||||
<setting-item :setting-key="'display.showQuickTools'"/>
|
||||
<v-divider class="my-2" />
|
||||
<setting-item :setting-key="'display.showQuickTools'" />
|
||||
|
||||
<v-divider class="my-2"/>
|
||||
<setting-item :setting-key="'display.showAntiScreenBurnCard'"/>
|
||||
<v-divider class="my-2" />
|
||||
<setting-item :setting-key="'display.showAntiScreenBurnCard'" />
|
||||
|
||||
<v-divider class="my-2"/>
|
||||
<setting-item :setting-key="'display.showExamScheduleButton'"/>
|
||||
<v-divider class="my-2" />
|
||||
<setting-item :setting-key="'display.showExamScheduleButton'" />
|
||||
|
||||
<v-divider class="my-2"/>
|
||||
<setting-item :setting-key="'display.forceDesktopMode'"/>
|
||||
|
||||
<v-divider class="my-2"/>
|
||||
<setting-item :setting-key="'display.lateStudentsArePresent'"/>
|
||||
<v-divider class="my-2" />
|
||||
<setting-item :setting-key="'display.forceDesktopMode'" />
|
||||
|
||||
<v-divider class="my-2" />
|
||||
<setting-item :setting-key="'display.lateStudentsArePresent'" />
|
||||
</v-list>
|
||||
</settings-card>
|
||||
</template>
|
||||
|
||||
@@ -6,13 +6,22 @@
|
||||
@click="handleClick"
|
||||
>
|
||||
<v-card-text>
|
||||
<div ref="typewriter" class="typewriter-text"></div>
|
||||
<div ref="sourceWriter" class="source-text"></div>
|
||||
<div
|
||||
ref="typewriter"
|
||||
class="typewriter-text"
|
||||
/>
|
||||
<div
|
||||
ref="sourceWriter"
|
||||
class="source-text"
|
||||
/>
|
||||
</v-card-text>
|
||||
<transition name="fade">
|
||||
<v-chip v-if="currentQuote?.contributor" class="contributor">
|
||||
<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>
|
||||
@@ -57,6 +66,10 @@ export default {
|
||||
this.initTypewriters();
|
||||
},
|
||||
|
||||
beforeUnmount() {
|
||||
[this.typewriter, this.sourceWriter].forEach(writer => writer?.stop());
|
||||
},
|
||||
|
||||
methods: {
|
||||
async initTypewriters() {
|
||||
const Typewriter = await getTypewriter();
|
||||
@@ -102,10 +115,6 @@ export default {
|
||||
console.error("复制失败:", err);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
beforeUnmount() {
|
||||
[this.typewriter, this.sourceWriter].forEach(writer => writer?.stop());
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,28 +1,30 @@
|
||||
<template>
|
||||
<settings-card icon="mdi-cog" title="编辑设置">
|
||||
<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-divider class="my-2"/>
|
||||
<setting-item :setting-key="'edit.autoSavePromptText'"/>
|
||||
<v-divider class="my-2" />
|
||||
<setting-item :setting-key="'edit.autoSavePromptText'" />
|
||||
|
||||
|
||||
<v-divider class="my-2"/>
|
||||
<setting-item :setting-key="'edit.manualSavePromptText'"/>
|
||||
|
||||
<v-divider class="my-2" />
|
||||
<setting-item :setting-key="'edit.manualSavePromptText'" />
|
||||
</v-list>
|
||||
</settings-card>
|
||||
</template>
|
||||
|
||||
@@ -48,8 +48,15 @@
|
||||
</div>
|
||||
|
||||
<v-row>
|
||||
<v-col cols="12" md="6">
|
||||
<setting-group border icon="mdi-book" title="科目配置">
|
||||
<v-col
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<setting-group
|
||||
border
|
||||
icon="mdi-book"
|
||||
title="科目配置"
|
||||
>
|
||||
<v-list>
|
||||
<v-list-item>
|
||||
<v-text-field
|
||||
@@ -63,8 +70,14 @@
|
||||
/>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item v-for="subject in subjectList" :key="subject">
|
||||
<v-card border class="w-100 mb-2">
|
||||
<v-list-item
|
||||
v-for="subject in subjectList"
|
||||
:key="subject"
|
||||
>
|
||||
<v-card
|
||||
border
|
||||
class="w-100 mb-2"
|
||||
>
|
||||
<v-card-title class="d-flex align-center">
|
||||
<v-text-field
|
||||
v-model="editedSubjects[subject]"
|
||||
@@ -74,7 +87,7 @@
|
||||
variant="plain"
|
||||
@blur="updateSubject(subject)"
|
||||
/>
|
||||
<v-spacer/>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
color="error"
|
||||
icon="mdi-delete"
|
||||
@@ -95,17 +108,24 @@
|
||||
@keyup.enter="() => addBookType(subject)"
|
||||
/>
|
||||
|
||||
<v-list border density="compact" rounded>
|
||||
<v-list
|
||||
border
|
||||
density="compact"
|
||||
rounded
|
||||
>
|
||||
<v-list-item
|
||||
v-for="(books, bookType) in config.subjects[subject].books"
|
||||
:key="bookType"
|
||||
:title="bookType"
|
||||
@click="openSubjectBookDialog(subject, bookType, books)"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<v-icon class="mr-2" icon="mdi-book-open-variant"/>
|
||||
<template #prepend>
|
||||
<v-icon
|
||||
class="mr-2"
|
||||
icon="mdi-book-open-variant"
|
||||
/>
|
||||
</template>
|
||||
<template v-slot:append>
|
||||
<template #append>
|
||||
<v-chip
|
||||
class="mr-2"
|
||||
color="info"
|
||||
@@ -130,8 +150,15 @@
|
||||
</setting-group>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" md="6">
|
||||
<setting-group border icon="mdi-cog" title="通用配置">
|
||||
<v-col
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<setting-group
|
||||
border
|
||||
icon="mdi-cog"
|
||||
title="通用配置"
|
||||
>
|
||||
<v-list>
|
||||
<v-list-item>
|
||||
<v-text-field
|
||||
@@ -146,17 +173,24 @@
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item>
|
||||
<v-list border density="compact" rounded>
|
||||
<v-list
|
||||
border
|
||||
density="compact"
|
||||
rounded
|
||||
>
|
||||
<v-list-item
|
||||
v-for="(books, bookType) in config.commonSubject.books"
|
||||
:key="bookType"
|
||||
:title="bookType"
|
||||
@click="openSubjectBookDialog('common', bookType, books)"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<v-icon class="mr-2" icon="mdi-book-multiple"/>
|
||||
<template #prepend>
|
||||
<v-icon
|
||||
class="mr-2"
|
||||
icon="mdi-book-multiple"
|
||||
/>
|
||||
</template>
|
||||
<template v-slot:append>
|
||||
<template #append>
|
||||
<v-chip
|
||||
class="mr-2"
|
||||
color="info"
|
||||
@@ -176,7 +210,7 @@
|
||||
</v-list>
|
||||
</v-list-item>
|
||||
|
||||
<v-divider class="my-2"/>
|
||||
<v-divider class="my-2" />
|
||||
|
||||
<v-list-item>
|
||||
<v-text-field
|
||||
@@ -191,14 +225,18 @@
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item>
|
||||
<v-list border density="compact" rounded>
|
||||
<v-list
|
||||
border
|
||||
density="compact"
|
||||
rounded
|
||||
>
|
||||
<v-list-item
|
||||
v-for="action in config.actions"
|
||||
:key="action"
|
||||
:title="action"
|
||||
@click="openActionDialog(action)"
|
||||
>
|
||||
<template v-slot:append>
|
||||
<template #append>
|
||||
<v-btn
|
||||
color="error"
|
||||
icon="mdi-delete"
|
||||
@@ -216,7 +254,10 @@
|
||||
</v-row>
|
||||
|
||||
<!-- 编辑弹框 -->
|
||||
<v-dialog v-model="dialog.show" max-width="600px">
|
||||
<v-dialog
|
||||
v-model="dialog.show"
|
||||
max-width="600px"
|
||||
>
|
||||
<v-card>
|
||||
<v-card-title class="text-h5 pa-4">
|
||||
{{ dialog.title }}
|
||||
@@ -235,22 +276,43 @@
|
||||
/>
|
||||
</v-col>
|
||||
|
||||
<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-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 v-if="['subjectBook', 'commonBook'].includes(dialog.editedItem.type)" cols="12">
|
||||
<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-title class="text-subtitle-1 py-2">
|
||||
需完成部分
|
||||
</v-card-title>
|
||||
<v-card-text class="pt-0">
|
||||
<v-list border class="mb-2" density="compact" rounded>
|
||||
<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 class="mr-2" icon="mdi-checkbox-blank-circle-outline" size="small"/>
|
||||
<template #prepend>
|
||||
<v-icon
|
||||
class="mr-2"
|
||||
icon="mdi-checkbox-blank-circle-outline"
|
||||
size="small"
|
||||
/>
|
||||
</template>
|
||||
<v-text-field
|
||||
v-model="dialog.editedItem.tasks[index]"
|
||||
@@ -258,7 +320,7 @@
|
||||
hide-details
|
||||
variant="plain"
|
||||
/>
|
||||
<template v-slot:append>
|
||||
<template #append>
|
||||
<v-btn
|
||||
color="error"
|
||||
icon="mdi-delete"
|
||||
@@ -287,7 +349,7 @@
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions class="pa-4">
|
||||
<v-spacer></v-spacer>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
color="primary"
|
||||
variant="elevated"
|
||||
|
||||
@@ -1,40 +1,67 @@
|
||||
<template>
|
||||
<settings-card :loading="loading" icon="mdi-database-edit" title="KV数据库管理">
|
||||
<settings-card
|
||||
:loading="loading"
|
||||
icon="mdi-database-edit"
|
||||
title="KV数据库管理"
|
||||
>
|
||||
<v-list>
|
||||
<!-- 数据库连接状态 -->
|
||||
<v-list-item>
|
||||
<template #prepend>
|
||||
<v-icon :color="connectionColor" :icon="connectionIcon" 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 :loading="loading" variant="tonal" @click="refreshConnection">
|
||||
<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 class="mr-3" icon="mdi-format-list-bulleted"/>
|
||||
<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 :loading="loadingData" @click="loadKvData">
|
||||
<v-btn
|
||||
:loading="loadingData"
|
||||
@click="loadKvData"
|
||||
>
|
||||
加载数据
|
||||
</v-btn>
|
||||
<v-btn :disabled="!isKvProvider" @click="createNewItem">
|
||||
<v-icon class="mr-1" icon="mdi-plus"/>
|
||||
<v-btn
|
||||
:disabled="!isKvProvider"
|
||||
@click="createNewItem"
|
||||
>
|
||||
<v-icon
|
||||
class="mr-1"
|
||||
icon="mdi-plus"
|
||||
/>
|
||||
新建
|
||||
</v-btn>
|
||||
<v-btn @click="showMigrationDialog = true">
|
||||
<v-icon class="mr-1" icon="mdi-cloud-upload"/>
|
||||
<v-icon
|
||||
class="mr-1"
|
||||
icon="mdi-cloud-upload"
|
||||
/>
|
||||
从本地迁移
|
||||
</v-btn>
|
||||
</v-btn-group>
|
||||
@@ -43,11 +70,18 @@
|
||||
</v-list>
|
||||
|
||||
<!-- 数据表格 -->
|
||||
<v-card v-if="kvData.length > 0" class="mt-4" variant="outlined">
|
||||
<v-card
|
||||
v-if="kvData.length > 0"
|
||||
class="mt-4"
|
||||
variant="outlined"
|
||||
>
|
||||
<v-card-title class="d-flex align-center">
|
||||
<v-icon class="mr-2" icon="mdi-table"/>
|
||||
<v-icon
|
||||
class="mr-2"
|
||||
icon="mdi-table"
|
||||
/>
|
||||
KV数据列表
|
||||
<v-spacer/>
|
||||
<v-spacer />
|
||||
<v-text-field
|
||||
v-model="searchQuery"
|
||||
clearable
|
||||
@@ -73,7 +107,10 @@
|
||||
</template>
|
||||
|
||||
<template #[`item.actions`]="{ item }">
|
||||
<v-btn-group density="compact" variant="text">
|
||||
<v-btn-group
|
||||
density="compact"
|
||||
variant="text"
|
||||
>
|
||||
<v-btn
|
||||
icon="mdi-eye"
|
||||
size="small"
|
||||
@@ -106,13 +143,23 @@
|
||||
</v-card>
|
||||
|
||||
<!-- 查看数据对话框 -->
|
||||
<v-dialog v-model="viewDialog" max-width="800px">
|
||||
<v-dialog
|
||||
v-model="viewDialog"
|
||||
max-width="800px"
|
||||
>
|
||||
<v-card>
|
||||
<v-card-title class="d-flex align-center">
|
||||
<v-icon class="mr-2" icon="mdi-eye"/>
|
||||
<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">
|
||||
@@ -132,12 +179,21 @@
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions>
|
||||
<v-spacer/>
|
||||
<v-btn variant="tonal" @click="copyToClipboard(selectedItem?.value)">
|
||||
<v-icon class="mr-1" icon="mdi-content-copy"/>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
variant="tonal"
|
||||
@click="copyToClipboard(selectedItem?.value)"
|
||||
>
|
||||
<v-icon
|
||||
class="mr-1"
|
||||
icon="mdi-content-copy"
|
||||
/>
|
||||
复制数据
|
||||
</v-btn>
|
||||
<v-btn variant="text" @click="viewDialog = false">
|
||||
<v-btn
|
||||
variant="text"
|
||||
@click="viewDialog = false"
|
||||
>
|
||||
关闭
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
@@ -145,13 +201,23 @@
|
||||
</v-dialog>
|
||||
|
||||
<!-- 编辑数据对话框 -->
|
||||
<v-dialog v-model="editDialog" max-width="800px">
|
||||
<v-dialog
|
||||
v-model="editDialog"
|
||||
max-width="800px"
|
||||
>
|
||||
<v-card>
|
||||
<v-card-title class="d-flex align-center">
|
||||
<v-icon class="mr-2" icon="mdi-pencil"/>
|
||||
<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">
|
||||
@@ -171,8 +237,11 @@
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions>
|
||||
<v-spacer/>
|
||||
<v-btn variant="text" @click="closeEditDialog">
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
variant="text"
|
||||
@click="closeEditDialog"
|
||||
>
|
||||
取消
|
||||
</v-btn>
|
||||
<v-btn
|
||||
@@ -189,13 +258,23 @@
|
||||
</v-dialog>
|
||||
|
||||
<!-- 新建数据对话框 -->
|
||||
<v-dialog v-model="createDialog" max-width="800px">
|
||||
<v-dialog
|
||||
v-model="createDialog"
|
||||
max-width="800px"
|
||||
>
|
||||
<v-card>
|
||||
<v-card-title class="d-flex align-center">
|
||||
<v-icon class="mr-2" icon="mdi-plus"/>
|
||||
<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>
|
||||
@@ -215,15 +294,18 @@
|
||||
:error-messages="isValidNewJson ? [] : ['请输入有效的JSON格式']"
|
||||
class="font-monospace"
|
||||
label="数据内容 (JSON格式)"
|
||||
placeholder='请输入JSON数据,如:{"name": "value"}'
|
||||
placeholder="请输入JSON数据,如:{"name": "value"}"
|
||||
rows="15"
|
||||
variant="outlined"
|
||||
/>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions>
|
||||
<v-spacer/>
|
||||
<v-btn variant="text" @click="closeCreateDialog">
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
variant="text"
|
||||
@click="closeCreateDialog"
|
||||
>
|
||||
取消
|
||||
</v-btn>
|
||||
<v-btn
|
||||
@@ -240,13 +322,23 @@
|
||||
</v-dialog>
|
||||
|
||||
<!-- 云端地址对话框 -->
|
||||
<v-dialog v-model="cloudUrlDialog" max-width="800px">
|
||||
<v-dialog
|
||||
v-model="cloudUrlDialog"
|
||||
max-width="800px"
|
||||
>
|
||||
<v-card>
|
||||
<v-card-title class="d-flex align-center">
|
||||
<v-icon class="mr-2" icon="mdi-cloud-download"/>
|
||||
<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">
|
||||
@@ -254,19 +346,43 @@
|
||||
</v-card-subtitle>
|
||||
|
||||
<v-card-text>
|
||||
<v-alert v-if="cloudUrlError" class="mb-4" type="error" variant="tonal">
|
||||
<v-alert
|
||||
v-if="cloudUrlError"
|
||||
class="mb-4"
|
||||
type="error"
|
||||
variant="tonal"
|
||||
>
|
||||
{{ cloudUrlError }}
|
||||
</v-alert>
|
||||
|
||||
<v-alert v-if="cloudUrlResult && cloudUrlResult.success" class="mb-4" type="success" variant="tonal">
|
||||
<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 class="mr-1" color="success" icon="mdi-database-arrow-up"/>
|
||||
<div
|
||||
v-if="cloudUrlResult.migrated"
|
||||
class="mb-2"
|
||||
>
|
||||
<v-icon
|
||||
class="mr-1"
|
||||
color="success"
|
||||
icon="mdi-database-arrow-up"
|
||||
/>
|
||||
数据已从本地迁移到云端
|
||||
</div>
|
||||
<div v-if="cloudUrlResult.configured" class="mb-2">
|
||||
<v-icon class="mr-1" color="info" icon="mdi-cog"/>
|
||||
<div
|
||||
v-if="cloudUrlResult.configured"
|
||||
class="mb-2"
|
||||
>
|
||||
<v-icon
|
||||
class="mr-1"
|
||||
color="info"
|
||||
icon="mdi-cog"
|
||||
/>
|
||||
云端配置已自动设置
|
||||
</div>
|
||||
</div>
|
||||
@@ -283,10 +399,16 @@
|
||||
@click:append-inner="copyCloudUrl"
|
||||
/>
|
||||
|
||||
<v-expansion-panels v-if="cloudUrlResult && cloudUrlResult.url" class="mt-4">
|
||||
<v-expansion-panels
|
||||
v-if="cloudUrlResult && cloudUrlResult.url"
|
||||
class="mt-4"
|
||||
>
|
||||
<v-expansion-panel>
|
||||
<v-expansion-panel-title>
|
||||
<v-icon class="mr-2" icon="mdi-cog"/>
|
||||
<v-icon
|
||||
class="mr-2"
|
||||
icon="mdi-cog"
|
||||
/>
|
||||
高级选项
|
||||
</v-expansion-panel-title>
|
||||
<v-expansion-panel-text>
|
||||
@@ -307,7 +429,10 @@
|
||||
variant="tonal"
|
||||
@click="refreshCloudUrl"
|
||||
>
|
||||
<v-icon class="mr-1" icon="mdi-refresh"/>
|
||||
<v-icon
|
||||
class="mr-1"
|
||||
icon="mdi-refresh"
|
||||
/>
|
||||
重新获取
|
||||
</v-btn>
|
||||
</v-expansion-panel-text>
|
||||
@@ -316,8 +441,11 @@
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions>
|
||||
<v-spacer/>
|
||||
<v-btn variant="text" @click="cloudUrlDialog = false">
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
variant="text"
|
||||
@click="cloudUrlDialog = false"
|
||||
>
|
||||
关闭
|
||||
</v-btn>
|
||||
<v-btn
|
||||
@@ -326,7 +454,10 @@
|
||||
variant="tonal"
|
||||
@click="openCloudUrl"
|
||||
>
|
||||
<v-icon class="mr-1" icon="mdi-open-in-new"/>
|
||||
<v-icon
|
||||
class="mr-1"
|
||||
icon="mdi-open-in-new"
|
||||
/>
|
||||
在新窗口打开
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
@@ -334,24 +465,37 @@
|
||||
</v-dialog>
|
||||
|
||||
<!-- 删除确认对话框 -->
|
||||
<v-dialog v-model="deleteDialog" max-width="400px">
|
||||
<v-dialog
|
||||
v-model="deleteDialog"
|
||||
max-width="400px"
|
||||
>
|
||||
<v-card>
|
||||
<v-card-title class="d-flex align-center text-error">
|
||||
<v-icon class="mr-2" icon="mdi-alert"/>
|
||||
<v-icon
|
||||
class="mr-2"
|
||||
icon="mdi-alert"
|
||||
/>
|
||||
确认删除
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text>
|
||||
确定要删除键名为 <code>{{ itemToDelete?.key }}</code> 的数据吗?
|
||||
<br><br>
|
||||
<v-alert class="mt-2" type="warning" variant="tonal">
|
||||
<v-alert
|
||||
class="mt-2"
|
||||
type="warning"
|
||||
variant="tonal"
|
||||
>
|
||||
此操作不可撤销,请谨慎操作!
|
||||
</v-alert>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions>
|
||||
<v-spacer/>
|
||||
<v-btn variant="text" @click="deleteDialog = false">
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
variant="text"
|
||||
@click="deleteDialog = false"
|
||||
>
|
||||
取消
|
||||
</v-btn>
|
||||
<v-btn
|
||||
|
||||
@@ -1,22 +1,23 @@
|
||||
<template>
|
||||
<settings-card icon="mdi-cog" title="编辑设置">
|
||||
<settings-card
|
||||
icon="mdi-cog"
|
||||
title="编辑设置"
|
||||
>
|
||||
<v-list>
|
||||
<setting-item setting-key="randomPicker.enabled"/>
|
||||
<v-divider class="my-2"/>
|
||||
<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"/>
|
||||
|
||||
|
||||
<v-divider class="my-2"/>
|
||||
<setting-item setting-key="randomPicker.animation"/>
|
||||
<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-list>
|
||||
</settings-card>
|
||||
</template>
|
||||
|
||||
@@ -1,13 +1,21 @@
|
||||
<template>
|
||||
<settings-card icon="mdi-refresh-circle" title="刷新设置">
|
||||
<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>
|
||||
</settings-card>
|
||||
</template>
|
||||
|
||||
@@ -118,6 +118,7 @@
|
||||
import SettingsCard from "@/components/SettingsCard.vue";
|
||||
import CloudNamespaceInfoCard from "./CloudNamespaceInfoCard.vue";
|
||||
import {getSetting, setSetting, watchSettings} from "@/utils/settings";
|
||||
import dataProvider from "@/utils/dataProvider";
|
||||
|
||||
export default {
|
||||
name: "ServerSettingsCard",
|
||||
@@ -132,7 +133,7 @@ export default {
|
||||
serverSettings: {
|
||||
provider: getSetting("server.provider"),
|
||||
domain: getSetting("server.domain"),
|
||||
classNumber: getSetting("server.classNumber"),
|
||||
classNumber: "加载中...",
|
||||
kvToken: getSetting("server.kvToken"),
|
||||
},
|
||||
// 用于监听设置变化时刷新 UI
|
||||
@@ -186,19 +187,41 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
// 从全局设置加载所有设置到本地
|
||||
loadAllSettings() {
|
||||
async loadAllSettings() {
|
||||
this.serverSettings = {
|
||||
provider: getSetting("server.provider"),
|
||||
domain: getSetting("server.domain"),
|
||||
classNumber: getSetting("server.classNumber"),
|
||||
classNumber: "加载中...",
|
||||
kvToken: getSetting("server.kvToken"),
|
||||
};
|
||||
|
||||
try {
|
||||
const res = await dataProvider.loadData("classworks-config-basic");
|
||||
if (res && res.success !== false && res.classNumber) {
|
||||
this.serverSettings.classNumber = res.classNumber;
|
||||
} else {
|
||||
this.serverSettings.classNumber = "我的班级";
|
||||
}
|
||||
} catch (e) {
|
||||
this.serverSettings.classNumber = "我的班级";
|
||||
}
|
||||
},
|
||||
|
||||
// 保存所有本地设置到全局
|
||||
saveAllSettings() {
|
||||
Object.entries(this.serverSettings).forEach(([key, value]) => {
|
||||
const settingKey = `server.${key}`;
|
||||
|
||||
if (key === 'classNumber') {
|
||||
// Sync to server data so it's consistent
|
||||
dataProvider.saveData("classworks-config-basic", { classNumber: value }).then(() => {
|
||||
console.log(`班级名称已同步到服务器: ${value}`);
|
||||
}).catch(e => {
|
||||
console.error("无法保存班级到服务器配置", e);
|
||||
});
|
||||
return; // Skip saving classNumber to localStorage
|
||||
}
|
||||
|
||||
const currentValue = getSetting(settingKey);
|
||||
|
||||
// 只有当值发生变化时才进行设置
|
||||
|
||||
@@ -58,10 +58,16 @@
|
||||
</div>
|
||||
|
||||
<!-- 添加新科目 -->
|
||||
<v-card class="mb-4" variant="outlined">
|
||||
<v-card
|
||||
class="mb-4"
|
||||
variant="outlined"
|
||||
>
|
||||
<v-card-text>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6">
|
||||
<v-col
|
||||
cols="12"
|
||||
sm="6"
|
||||
>
|
||||
<v-text-field
|
||||
v-model="newSubjectName"
|
||||
:rules="[v => !!v || '科目名称不能为空']"
|
||||
@@ -85,7 +91,7 @@
|
||||
v-for="(subject, index) in subjects"
|
||||
:key="subject.order"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<template #prepend>
|
||||
<div class="d-flex flex-column align-center mr-2">
|
||||
<v-btn
|
||||
:disabled="index === 0"
|
||||
@@ -114,7 +120,7 @@
|
||||
/>
|
||||
</v-list-item-title>
|
||||
|
||||
<template v-slot:append>
|
||||
<template #append>
|
||||
<v-btn
|
||||
color="error"
|
||||
icon="mdi-delete"
|
||||
@@ -191,9 +197,13 @@ export default {
|
||||
this.loading = true;
|
||||
try {
|
||||
const response = await dataProvider.loadData("classworks-config-subject");
|
||||
if (response) {
|
||||
|
||||
// 兼容返回数组或包含 value 属性的对象
|
||||
const dataArray = Array.isArray(response) ? response : (response && Array.isArray(response.value) ? response.value : null);
|
||||
|
||||
if (response && response.success !== false && dataArray) {
|
||||
// 数据存在且加载成功
|
||||
this.subjects = response.map((subject, index) => ({
|
||||
this.subjects = dataArray.map((subject, index) => ({
|
||||
name: subject.name,
|
||||
order: subject.order ?? index
|
||||
})).sort((a, b) => a.order - b.order);
|
||||
@@ -203,7 +213,9 @@ export default {
|
||||
// 数据不存在,使用空数组
|
||||
this.subjects = [];
|
||||
this.originalSubjects = [];
|
||||
this.showMessage('使用默认配置', 'info');
|
||||
if (response && response.success === false) {
|
||||
this.showMessage('未找到已有配置,使用默认配置', 'info');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load config:', error);
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
<template>
|
||||
<settings-card icon="mdi-palette" title="主题设置">
|
||||
<settings-card
|
||||
icon="mdi-palette"
|
||||
title="主题设置"
|
||||
>
|
||||
<v-list>
|
||||
<v-list-item>
|
||||
<template #prepend>
|
||||
<v-icon class="mr-3" icon="mdi-theme-light-dark"/>
|
||||
<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>
|
||||
@@ -14,11 +20,17 @@
|
||||
density="comfortable"
|
||||
>
|
||||
<v-btn value="light">
|
||||
<v-icon class="mr-2" icon="mdi-white-balance-sunny"/>
|
||||
<v-icon
|
||||
class="mr-2"
|
||||
icon="mdi-white-balance-sunny"
|
||||
/>
|
||||
明亮
|
||||
</v-btn>
|
||||
<v-btn value="dark">
|
||||
<v-icon class="mr-2" icon="mdi-moon-waning-crescent"/>
|
||||
<v-icon
|
||||
class="mr-2"
|
||||
icon="mdi-moon-waning-crescent"
|
||||
/>
|
||||
暗黑
|
||||
</v-btn>
|
||||
</v-btn-toggle>
|
||||
@@ -37,6 +49,11 @@ export default {
|
||||
name: 'ThemeSettingsCard',
|
||||
components: {SettingsCard},
|
||||
|
||||
setup() {
|
||||
const theme = useTheme();
|
||||
return {theme};
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
localTheme: getSetting('theme.mode')
|
||||
@@ -50,11 +67,6 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
setup() {
|
||||
const theme = useTheme();
|
||||
return {theme};
|
||||
},
|
||||
|
||||
methods: {
|
||||
updateTheme(mode) {
|
||||
this.theme.global.name.value = mode;
|
||||
|
||||
-32
@@ -27,35 +27,3 @@ app.component('GlobalMessage', GlobalMessage)
|
||||
app.mount('#app')
|
||||
|
||||
// ====== 以下全部异步,不阻塞首屏渲染 ======
|
||||
|
||||
// 异步初始化 Sentry(延迟到首帧渲染完成后,防止 errorHandler 与渲染周期冲突)
|
||||
setTimeout(() => {
|
||||
import('./utils/sentry').then(({ initSentry }) => {
|
||||
const router = app.config.globalProperties.$router
|
||||
initSentry(app, router)
|
||||
}).catch((err) => {
|
||||
console.warn('Sentry 初始化失败:', err)
|
||||
})
|
||||
}, 1000)
|
||||
|
||||
// 异步加载 Clarity(在页面完全加载后)
|
||||
const loadClarity = async () => {
|
||||
try {
|
||||
const { getVisitorId } = await import('./utils/visitorId')
|
||||
const Clarity = (await import('@microsoft/clarity')).default
|
||||
Clarity.init('rhp8uqoc3l')
|
||||
|
||||
const visitorId = await getVisitorId()
|
||||
console.log('Visitor ID:', visitorId)
|
||||
Clarity.identify(visitorId)
|
||||
Clarity.setTag('fingerprintjs', visitorId)
|
||||
} catch (error) {
|
||||
console.warn('Clarity 加载或标识设置失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
if (document.readyState === 'complete') {
|
||||
loadClarity()
|
||||
} else {
|
||||
window.addEventListener('load', loadClarity, { once: true })
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<error404/>
|
||||
<error404 />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
|
||||
@@ -3,39 +3,82 @@
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<div class="d-flex align-center mb-6">
|
||||
<v-icon class="mr-3" color="primary" size="x-large">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>
|
||||
<h1 class="text-h4 ">
|
||||
缓存管理
|
||||
</h1>
|
||||
<div class="text-subtitle-1 text-grey">
|
||||
管理应用的本地缓存资源
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<v-card class="mb-6" color="info" density="compact" variant="tonal">
|
||||
<v-card
|
||||
class="mb-6"
|
||||
color="info"
|
||||
density="compact"
|
||||
variant="tonal"
|
||||
>
|
||||
<v-card-text class="d-flex align-center">
|
||||
<v-icon class="mr-2" color="info">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-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 class="mr-2" color="primary">mdi-information</v-icon>
|
||||
<v-icon
|
||||
class="mr-2"
|
||||
color="primary"
|
||||
>
|
||||
mdi-information
|
||||
</v-icon>
|
||||
<span class="text-h6">什么是缓存?</span>
|
||||
</div>
|
||||
<p>
|
||||
缓存是浏览器在本地存储的网站资源副本,如图片、脚本和样式表等。这些缓存可以加快页面加载速度,减少数据使用,并在离线时提供基本功能。</p>
|
||||
缓存是浏览器在本地存储的网站资源副本,如图片、脚本和样式表等。这些缓存可以加快页面加载速度,减少数据使用,并在离线时提供基本功能。
|
||||
</p>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" md="4">
|
||||
<v-card class="mb-4" variant="tonal">
|
||||
<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 class="mr-2" color="warning">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">
|
||||
@@ -48,7 +91,7 @@
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<CacheManager/>
|
||||
<CacheManager />
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
|
||||
+21
-6
@@ -1,7 +1,17 @@
|
||||
<template>
|
||||
<v-container class="fill-height" fluid>
|
||||
<v-row align="center" justify="center">
|
||||
<v-col cols="12" md="6" sm="8">
|
||||
<v-container
|
||||
class="fill-height"
|
||||
fluid
|
||||
>
|
||||
<v-row
|
||||
align="center"
|
||||
justify="center"
|
||||
>
|
||||
<v-col
|
||||
cols="12"
|
||||
md="6"
|
||||
sm="8"
|
||||
>
|
||||
<v-card>
|
||||
<v-card-title class="text-h5">
|
||||
{{ status === 'processing' ? '正在处理授权...' : status === 'success' ? '授权成功' : '授权失败' }}
|
||||
@@ -12,12 +22,17 @@
|
||||
class="mb-4"
|
||||
color="primary"
|
||||
indeterminate
|
||||
></v-progress-linear>
|
||||
/>
|
||||
<p>{{ message }}</p>
|
||||
</v-card-text>
|
||||
<v-card-actions v-if="status !== 'processing'">
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="primary" @click="goToHome">返回首页</v-btn>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
color="primary"
|
||||
@click="goToHome"
|
||||
>
|
||||
返回首页
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-col>
|
||||
|
||||
+376
-143
@@ -2,9 +2,17 @@
|
||||
<v-container class="fill-height">
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<v-card class="elevation-12" border>
|
||||
<v-card
|
||||
class="elevation-12"
|
||||
border
|
||||
>
|
||||
<v-card-title class="d-flex align-center primary lighten-1 white--text py-3 px-4">
|
||||
<v-icon color="white" class="mr-2">mdi-swap-horizontal</v-icon>
|
||||
<v-icon
|
||||
color="white"
|
||||
class="mr-2"
|
||||
>
|
||||
mdi-swap-horizontal
|
||||
</v-icon>
|
||||
课程表转换工具
|
||||
</v-card-title>
|
||||
<v-card-subtitle>
|
||||
@@ -22,7 +30,9 @@
|
||||
@click:close="error = ''"
|
||||
>
|
||||
<div class="d-flex align-center">
|
||||
<v-icon class="mr-2">mdi-alert-circle</v-icon>
|
||||
<v-icon class="mr-2">
|
||||
mdi-alert-circle
|
||||
</v-icon>
|
||||
{{ error }}
|
||||
</div>
|
||||
</v-alert>
|
||||
@@ -38,38 +48,75 @@
|
||||
@click:close="success = ''"
|
||||
>
|
||||
<div class="d-flex align-center">
|
||||
<v-icon class="mr-2">mdi-check-circle</v-icon>
|
||||
<v-icon class="mr-2">
|
||||
mdi-check-circle
|
||||
</v-icon>
|
||||
{{ success }}
|
||||
</div>
|
||||
</v-alert>
|
||||
|
||||
<!-- 输入方式选择 -->
|
||||
<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-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
|
||||
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 value="auto">自动检测</v-btn>
|
||||
<v-btn value="json">JSON</v-btn>
|
||||
<v-btn value="yaml" :disabled="!yamlLibLoaded">
|
||||
<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"
|
||||
>
|
||||
YAML
|
||||
<v-tooltip activator="parent" location="bottom">
|
||||
<v-tooltip
|
||||
activator="parent"
|
||||
location="bottom"
|
||||
>
|
||||
{{ yamlLibLoaded ? 'YAML解析库已加载' : '正在加载YAML解析库...' }}
|
||||
</v-tooltip>
|
||||
</v-btn>
|
||||
</v-btn-toggle>
|
||||
|
||||
<!-- 添加当前检测到的格式提示 -->
|
||||
<div v-if="jsonText && formatMode === 'auto'" class="text-caption mb-2">
|
||||
<div
|
||||
v-if="jsonText && formatMode === 'auto'"
|
||||
class="text-caption mb-2"
|
||||
>
|
||||
检测到的格式: {{ isYaml(jsonText) ? 'YAML' : 'JSON' }}
|
||||
</div>
|
||||
|
||||
@@ -85,7 +132,7 @@
|
||||
rows="6"
|
||||
placeholder="请在此粘贴CSES格式的数据..."
|
||||
@input="handleTextChange"
|
||||
></v-textarea>
|
||||
/>
|
||||
</div>
|
||||
</v-window-item>
|
||||
<v-window-item value="file">
|
||||
@@ -96,13 +143,13 @@
|
||||
prepend-icon="mdi-file-upload"
|
||||
:loading="loading"
|
||||
:disabled="loading"
|
||||
@change="handleFileChange"
|
||||
hint="支持JSON、YAML格式文件"
|
||||
persistent-hint
|
||||
:rules="[
|
||||
v => !v || v.size < 2000000 || '文件大小不能超过 2 MB',
|
||||
]"
|
||||
></v-file-input>
|
||||
@change="handleFileChange"
|
||||
/>
|
||||
|
||||
<v-alert
|
||||
v-if="file && formatMode === 'auto'"
|
||||
@@ -118,17 +165,33 @@
|
||||
|
||||
<!-- 设置面板 -->
|
||||
<v-col cols="12">
|
||||
<v-card flat class="pa-4 rounded-lg" border>
|
||||
<v-card
|
||||
flat
|
||||
class="pa-4 rounded-lg"
|
||||
border
|
||||
>
|
||||
<div class="d-flex align-center mb-3">
|
||||
<v-icon color="primary" class="mr-2">mdi-calendar-multiselect</v-icon>
|
||||
<h3 class="text-subtitle-1 font-weight-medium mr-auto">选择导出天数</h3>
|
||||
<v-icon
|
||||
color="primary"
|
||||
class="mr-2"
|
||||
>
|
||||
mdi-calendar-multiselect
|
||||
</v-icon>
|
||||
<h3 class="text-subtitle-1 font-weight-medium mr-auto">
|
||||
选择导出天数
|
||||
</h3>
|
||||
<v-btn
|
||||
variant="text"
|
||||
color="primary"
|
||||
class="ml-2"
|
||||
@click="selectAllDays"
|
||||
>
|
||||
<v-icon start size="small">mdi-checkbox-multiple-marked</v-icon>
|
||||
<v-icon
|
||||
start
|
||||
size="small"
|
||||
>
|
||||
mdi-checkbox-multiple-marked
|
||||
</v-icon>
|
||||
全选
|
||||
</v-btn>
|
||||
<v-btn
|
||||
@@ -137,19 +200,37 @@
|
||||
class="ml-2"
|
||||
@click="clearSelectedDays"
|
||||
>
|
||||
<v-icon start size="small">mdi-checkbox-multiple-blank-outline</v-icon>
|
||||
<v-icon
|
||||
start
|
||||
size="small"
|
||||
>
|
||||
mdi-checkbox-multiple-blank-outline
|
||||
</v-icon>
|
||||
清除
|
||||
</v-btn>
|
||||
</div>
|
||||
<v-chip-group v-model="selectedDays" multiple class="mb-2" color="primary">
|
||||
<v-chip v-for="day in 7" :key="day" :value="day" filter variant="tonal" class="filter-chip" label>
|
||||
<v-chip-group
|
||||
v-model="selectedDays"
|
||||
multiple
|
||||
class="mb-2"
|
||||
color="primary"
|
||||
>
|
||||
<v-chip
|
||||
v-for="day in 7"
|
||||
:key="day"
|
||||
:value="day"
|
||||
filter
|
||||
variant="tonal"
|
||||
class="filter-chip"
|
||||
label
|
||||
>
|
||||
{{ dayNames[day] }}
|
||||
<v-badge
|
||||
v-if="getDaySchedule(day).length > 0"
|
||||
:content="getDaySchedule(day).length"
|
||||
color="primary"
|
||||
inline
|
||||
></v-badge>
|
||||
/>
|
||||
</v-chip>
|
||||
</v-chip-group>
|
||||
</v-card>
|
||||
@@ -157,31 +238,51 @@
|
||||
|
||||
<!-- 改进设置选项卡,显示为开关组 -->
|
||||
<v-col cols="12">
|
||||
<v-card flat class="pa-4 rounded-lg" border>
|
||||
<v-card
|
||||
flat
|
||||
class="pa-4 rounded-lg"
|
||||
border
|
||||
>
|
||||
<div class="d-flex align-center mb-3">
|
||||
<v-icon color="primary" class="mr-2">mdi-cog</v-icon>
|
||||
<h3 class="text-subtitle-1 font-weight-medium">显示配置</h3>
|
||||
<v-icon
|
||||
color="primary"
|
||||
class="mr-2"
|
||||
>
|
||||
mdi-cog
|
||||
</v-icon>
|
||||
<h3 class="text-subtitle-1 font-weight-medium">
|
||||
显示配置
|
||||
</h3>
|
||||
</div>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6">
|
||||
<v-col
|
||||
cols="12"
|
||||
sm="6"
|
||||
>
|
||||
<v-switch
|
||||
v-model="settings.hideTeacherName"
|
||||
label="不显示教师姓名"
|
||||
color="primary"
|
||||
inset
|
||||
hide-details
|
||||
></v-switch>
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6">
|
||||
<v-col
|
||||
cols="12"
|
||||
sm="6"
|
||||
>
|
||||
<v-switch
|
||||
v-model="settings.hideRoom"
|
||||
label="不显示教室信息"
|
||||
color="primary"
|
||||
inset
|
||||
hide-details
|
||||
></v-switch>
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6">
|
||||
<v-col
|
||||
cols="12"
|
||||
sm="6"
|
||||
>
|
||||
<v-text-field
|
||||
v-model.number="settings.totalWeeks"
|
||||
label="总周数"
|
||||
@@ -193,19 +294,23 @@
|
||||
variant="outlined"
|
||||
prepend-inner-icon="mdi-calendar-week"
|
||||
class="mt-3"
|
||||
></v-text-field>
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card>
|
||||
</v-col>
|
||||
|
||||
<!-- 添加加载状态的骨架屏 -->
|
||||
<v-card v-if="loading" class="my-4" outlined>
|
||||
<v-card
|
||||
v-if="loading"
|
||||
class="my-4"
|
||||
outlined
|
||||
>
|
||||
<v-card-text>
|
||||
<v-skeleton-loader
|
||||
type="table"
|
||||
class="mx-auto"
|
||||
></v-skeleton-loader>
|
||||
/>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
@@ -230,12 +335,31 @@
|
||||
</v-alert>
|
||||
|
||||
<!-- 课程表预览 -->
|
||||
<v-card v-if="processedData" class="my-4" elevation="1">
|
||||
<v-card
|
||||
v-if="processedData"
|
||||
class="my-4"
|
||||
elevation="1"
|
||||
>
|
||||
<v-card-title class="d-flex align-center pa-4 bg-primary-lighten-5">
|
||||
<v-icon color="primary" class="mr-2">mdi-table</v-icon>
|
||||
<v-icon
|
||||
color="primary"
|
||||
class="mr-2"
|
||||
>
|
||||
mdi-table
|
||||
</v-icon>
|
||||
<span class="font-weight-bold">课程表</span>
|
||||
<v-chip color="primary" class="ml-3" size="small" pill>
|
||||
<v-icon start size="x-small">mdi-book-open-variant</v-icon>
|
||||
<v-chip
|
||||
color="primary"
|
||||
class="ml-3"
|
||||
size="small"
|
||||
pill
|
||||
>
|
||||
<v-icon
|
||||
start
|
||||
size="x-small"
|
||||
>
|
||||
mdi-book-open-variant
|
||||
</v-icon>
|
||||
{{ processedData.tableData.length }} 节课程
|
||||
</v-chip>
|
||||
</v-card-title>
|
||||
@@ -263,8 +387,15 @@
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template v-for="day in 7" #[`item.${day}`]="{ item }" :key="day">
|
||||
<div v-if="item[day]" class="course-cell">
|
||||
<template
|
||||
v-for="day in 7"
|
||||
#[`item.${day}`]="{ item }"
|
||||
:key="day"
|
||||
>
|
||||
<div
|
||||
v-if="item[day]"
|
||||
class="course-cell"
|
||||
>
|
||||
<template v-if="Array.isArray(item[day])">
|
||||
<div
|
||||
v-for="(course, index) in item[day]"
|
||||
@@ -275,12 +406,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"
|
||||
@@ -295,12 +426,12 @@
|
||||
<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
|
||||
v-if="item[day].weekType"
|
||||
@@ -316,18 +447,48 @@
|
||||
</v-card>
|
||||
|
||||
<!-- 时间表 -->
|
||||
<v-card v-if="hasExportData" class="my-4" elevation="1">
|
||||
<v-card
|
||||
v-if="hasExportData"
|
||||
class="my-4"
|
||||
elevation="1"
|
||||
>
|
||||
<v-card-title class="d-flex align-center pa-4 bg-primary-lighten-5">
|
||||
<v-icon color="primary" class="mr-2">mdi-timetable</v-icon>
|
||||
<v-icon
|
||||
color="primary"
|
||||
class="mr-2"
|
||||
>
|
||||
mdi-timetable
|
||||
</v-icon>
|
||||
<span class="font-weight-bold">每日课程时间表</span>
|
||||
<v-chip class="ml-3" size="small" color="primary" pill>
|
||||
<v-icon start size="x-small">mdi-clock-outline</v-icon>
|
||||
<v-chip
|
||||
class="ml-3"
|
||||
size="small"
|
||||
color="primary"
|
||||
pill
|
||||
>
|
||||
<v-icon
|
||||
start
|
||||
size="x-small"
|
||||
>
|
||||
mdi-clock-outline
|
||||
</v-icon>
|
||||
{{ totalClassHours }} 课时
|
||||
</v-chip>
|
||||
<v-tooltip v-if="exportPeriods.length > 0">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-chip class="ml-2" size="small" color="info" v-bind="props" pill>
|
||||
<v-icon start size="x-small">mdi-information-outline</v-icon>
|
||||
<template #activator="{ props }">
|
||||
<v-chip
|
||||
class="ml-2"
|
||||
size="small"
|
||||
color="info"
|
||||
v-bind="props"
|
||||
pill
|
||||
>
|
||||
<v-icon
|
||||
start
|
||||
size="x-small"
|
||||
>
|
||||
mdi-information-outline
|
||||
</v-icon>
|
||||
节次已重排
|
||||
</v-chip>
|
||||
</template>
|
||||
@@ -336,93 +497,165 @@
|
||||
</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-tab v-for="day in daysWithSchedule" :key="day" :value="day" class="px-2 font-weight-medium">
|
||||
<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
|
||||
:content="getDaySchedule(day).length"
|
||||
color="primary"
|
||||
inline
|
||||
></v-badge>
|
||||
/>
|
||||
</v-tab>
|
||||
</v-tabs>
|
||||
|
||||
<!-- 当前选中日期的课程表 -->
|
||||
<v-window v-model="activeDay">
|
||||
<v-window-item v-for="day in daysWithSchedule" :key="day" :value="day">
|
||||
<v-table density="compact" class="rounded" :headers-length="6" disable-sort>
|
||||
<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
|
||||
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 #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>
|
||||
原节次: {{ 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 || '-' }}
|
||||
<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>
|
||||
</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>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</v-table>
|
||||
</v-window-item>
|
||||
</v-window>
|
||||
|
||||
<!-- 无数据提示 -->
|
||||
<v-alert v-if="hasExportData && daysWithSchedule.length === 0" type="info" class="mt-3">
|
||||
<v-alert
|
||||
v-if="hasExportData && daysWithSchedule.length === 0"
|
||||
type="info"
|
||||
class="mt-3"
|
||||
>
|
||||
没有找到任何课程数据
|
||||
</v-alert>
|
||||
</v-card-text>
|
||||
@@ -430,24 +663,24 @@
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions class="">
|
||||
<v-spacer></v-spacer>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
color="primary"
|
||||
variant="outlined"
|
||||
:loading="loading"
|
||||
:disabled="(!jsonText && !file) || loading"
|
||||
@click="processInput"
|
||||
prepend-icon="mdi-cog-refresh"
|
||||
@click="processInput"
|
||||
>
|
||||
处理数据
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="info"
|
||||
:disabled="!hasExportData"
|
||||
@click="showExportPreview"
|
||||
class="ml-2"
|
||||
prepend-icon="mdi-eye"
|
||||
border
|
||||
@click="showExportPreview"
|
||||
>
|
||||
刷新
|
||||
</v-btn>
|
||||
@@ -455,10 +688,10 @@
|
||||
color="success"
|
||||
variant="outlined"
|
||||
:disabled="!hasExportData"
|
||||
@click="downloadCSV"
|
||||
class="ml-2"
|
||||
prepend-icon="mdi-download"
|
||||
border
|
||||
@click="downloadCSV"
|
||||
>
|
||||
下载CSV
|
||||
</v-btn>
|
||||
@@ -735,6 +968,22 @@ export default {
|
||||
return days;
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
// 加载YAML解析库
|
||||
try {
|
||||
await loadJsYaml();
|
||||
this.yamlLibLoaded = true;
|
||||
} catch (error) {
|
||||
this.error = error.message;
|
||||
}
|
||||
|
||||
// 监听daysWithSchedule变化,设置默认选中的日期
|
||||
this.$watch('daysWithSchedule', (newVal) => {
|
||||
if (newVal.length > 0 && !this.activeDay) {
|
||||
this.activeDay = newVal[0];
|
||||
}
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
async handleFileChange() {
|
||||
this.resetData();
|
||||
@@ -762,7 +1011,7 @@ export default {
|
||||
|
||||
// 读取文件内容
|
||||
try {
|
||||
const reader = new FileReader();
|
||||
const reader = new window.FileReader();
|
||||
reader.onload = async (fileEvent) => {
|
||||
try {
|
||||
this.jsonText = fileEvent.target.result;
|
||||
@@ -1026,7 +1275,7 @@ export default {
|
||||
|
||||
// 添加BOM头以确保Excel正确识别UTF-8编码
|
||||
const BOM = "\uFEFF";
|
||||
const blob = new Blob([BOM + csvContent], {
|
||||
const blob = new window.Blob([BOM + csvContent], {
|
||||
type: "text/csv;charset=utf-8",
|
||||
});
|
||||
|
||||
@@ -1241,22 +1490,6 @@ export default {
|
||||
clearSelectedDays() {
|
||||
this.selectedDays = [];
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
// 加载YAML解析库
|
||||
try {
|
||||
await loadJsYaml();
|
||||
this.yamlLibLoaded = true;
|
||||
} catch (error) {
|
||||
this.error = error.message;
|
||||
}
|
||||
|
||||
// 监听daysWithSchedule变化,设置默认选中的日期
|
||||
this.$watch('daysWithSchedule', (newVal) => {
|
||||
if (newVal.length > 0 && !this.activeDay) {
|
||||
this.activeDay = newVal[0];
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
label="server.authDomain"
|
||||
/>
|
||||
</v-form>
|
||||
<v-divider class="my-4"/>
|
||||
<v-divider class="my-4" />
|
||||
|
||||
<v-btn
|
||||
class="me-2"
|
||||
|
||||
@@ -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"
|
||||
@@ -83,11 +83,17 @@
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-divider class="my-4"/>
|
||||
<v-divider class="my-4" />
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<v-card border color="primary" variant="tonal">
|
||||
<v-card-title class="text-subtitle-1">聊天室消息</v-card-title>
|
||||
<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"
|
||||
@@ -97,7 +103,7 @@
|
||||
rows="2"
|
||||
/>
|
||||
<div class="d-flex">
|
||||
<v-spacer/>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
:disabled="!canSendChat"
|
||||
color="primary"
|
||||
@@ -178,7 +184,7 @@
|
||||
<v-card border>
|
||||
<v-card-title class="d-flex align-center">
|
||||
事件日志
|
||||
<v-spacer/>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
color="error"
|
||||
size="small"
|
||||
|
||||
+8
-2
@@ -6,13 +6,19 @@
|
||||
请将这个ID复制并私聊给开发者,以便进行问题排查。
|
||||
</v-card-subtitle>
|
||||
<v-card-text>
|
||||
<div class="text-h6 mb-2">访客 ID</div>
|
||||
<div class="text-h6 mb-2">
|
||||
访客 ID
|
||||
</div>
|
||||
<v-code class="d-block pa-2 bg-grey-lighten-4 rounded mb-4">
|
||||
{{ visitorId || '加载中...' }}
|
||||
</v-code>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-btn color="primary" @click="loadData" :loading="loading">
|
||||
<v-btn
|
||||
color="primary"
|
||||
:loading="loading"
|
||||
@click="loadData"
|
||||
>
|
||||
Refresh
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<v-app-bar-title class="text-h6">
|
||||
编辑考试配置
|
||||
</v-app-bar-title>
|
||||
<v-spacer/>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
:loading="saving"
|
||||
color="success"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+2190
-1572
File diff suppressed because it is too large
Load Diff
+119
-35
@@ -7,11 +7,20 @@
|
||||
@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-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
|
||||
border
|
||||
@@ -23,11 +32,19 @@
|
||||
</v-btn>
|
||||
<h1 v-if="list && !isRenaming">
|
||||
{{ list.name }}
|
||||
<v-btn border icon size="small" @click="startRenaming">
|
||||
<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">
|
||||
<div
|
||||
v-else-if="list && isRenaming"
|
||||
class="d-flex align-center"
|
||||
>
|
||||
<v-text-field
|
||||
v-model="newListName"
|
||||
autofocus
|
||||
@@ -37,11 +54,20 @@
|
||||
label="列表名称"
|
||||
style="min-width: 200px;"
|
||||
@keyup.enter="saveListName"
|
||||
></v-text-field>
|
||||
<v-btn class="mr-2" color="primary" size="small" @click="saveListName">
|
||||
/>
|
||||
<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-btn
|
||||
color="error"
|
||||
size="small"
|
||||
@click="cancelRenaming"
|
||||
>
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
@@ -51,10 +77,14 @@
|
||||
</div>
|
||||
|
||||
|
||||
<v-card border class="mb-5" rounded="xl">
|
||||
<v-card
|
||||
border
|
||||
class="mb-5"
|
||||
rounded="xl"
|
||||
>
|
||||
<v-card-title class="d-flex align-center">
|
||||
项目列表
|
||||
<v-spacer/>
|
||||
<v-spacer />
|
||||
<v-btn-toggle
|
||||
v-model="sortType"
|
||||
mandatory
|
||||
@@ -97,7 +127,7 @@
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
<v-card-actions v-if="sortedItems.length > 0">
|
||||
<v-spacer/>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
:disabled="!hasCompletedItems"
|
||||
color="error"
|
||||
@@ -108,7 +138,11 @@
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
<v-card border class="mb-5" rounded="xl">
|
||||
<v-card
|
||||
border
|
||||
class="mb-5"
|
||||
rounded="xl"
|
||||
>
|
||||
<v-card-title>添加新项目</v-card-title>
|
||||
<v-card-text>
|
||||
<v-text-field
|
||||
@@ -126,7 +160,11 @@
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<v-card border class="mb-5" rounded="xl">
|
||||
<v-card
|
||||
border
|
||||
class="mb-5"
|
||||
rounded="xl"
|
||||
>
|
||||
<v-card-title>列表排序</v-card-title>
|
||||
<v-card-text>
|
||||
<v-text-field
|
||||
@@ -153,16 +191,30 @@
|
||||
</v-card>
|
||||
|
||||
<!-- 确认删除对话框 -->
|
||||
<v-dialog v-model="deleteDialog.show" max-width="500">
|
||||
<v-card border rounded="xl">
|
||||
<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-spacer />
|
||||
<v-btn
|
||||
color="primary"
|
||||
variant="text"
|
||||
@click="deleteDialog.show = false"
|
||||
>
|
||||
取消
|
||||
</v-btn>
|
||||
<v-btn color="error" variant="text" @click="confirmDelete">
|
||||
<v-btn
|
||||
color="error"
|
||||
variant="text"
|
||||
@click="confirmDelete"
|
||||
>
|
||||
确认删除
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
@@ -170,8 +222,14 @@
|
||||
</v-dialog>
|
||||
|
||||
<!-- 项目详情对话框 -->
|
||||
<v-dialog v-model="itemDialog.show" max-width="600">
|
||||
<v-card border rounded="xl">
|
||||
<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>
|
||||
@@ -181,13 +239,16 @@
|
||||
<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 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-title class="text-subtitle-1 font-weight-bold">
|
||||
状态
|
||||
</v-list-item-title>
|
||||
<v-list-item-subtitle>
|
||||
<v-chip
|
||||
:color="itemDialog.item.completed ? 'success' : 'warning'"
|
||||
@@ -199,20 +260,24 @@
|
||||
</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-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">
|
||||
<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"
|
||||
@@ -220,7 +285,7 @@
|
||||
label="描述"
|
||||
rows="3"
|
||||
variant="outlined"
|
||||
></v-textarea>
|
||||
/>
|
||||
|
||||
|
||||
<v-switch
|
||||
@@ -228,37 +293,56 @@
|
||||
color="success"
|
||||
hide-details
|
||||
label="已完成"
|
||||
></v-switch>
|
||||
/>
|
||||
</div>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-spacer />
|
||||
|
||||
<template v-if="!itemDialog.isEditing">
|
||||
<v-btn color="primary" variant="text" @click="startEditingItem">
|
||||
<v-btn
|
||||
color="primary"
|
||||
variant="text"
|
||||
@click="startEditingItem"
|
||||
>
|
||||
编辑
|
||||
</v-btn>
|
||||
<v-btn color="error" variant="text" @click="confirmDeleteItem(itemDialog.item?.id)">
|
||||
<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
|
||||
color="secondary"
|
||||
variant="text"
|
||||
@click="itemDialog.show = false"
|
||||
>
|
||||
关闭
|
||||
</v-btn>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<v-btn color="success" variant="text" @click="saveItemChanges">
|
||||
<v-btn
|
||||
color="success"
|
||||
variant="text"
|
||||
@click="saveItemChanges"
|
||||
>
|
||||
保存
|
||||
</v-btn>
|
||||
<v-btn color="secondary" variant="text" @click="cancelEditingItem">
|
||||
<v-btn
|
||||
color="secondary"
|
||||
variant="text"
|
||||
@click="cancelEditingItem"
|
||||
>
|
||||
取消
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
|
||||
+68
-19
@@ -7,12 +7,16 @@
|
||||
@click="$router.push('/')"
|
||||
/>
|
||||
</template>
|
||||
<v-app-bar-title class="text-h6">列表</v-app-bar-title>
|
||||
<v-app-bar-title class="text-h6">
|
||||
列表
|
||||
</v-app-bar-title>
|
||||
</v-app-bar>
|
||||
<v-container>
|
||||
|
||||
|
||||
<v-card border class="mb-5" rounded="xl">
|
||||
<v-card
|
||||
border
|
||||
class="mb-5"
|
||||
rounded="xl"
|
||||
>
|
||||
<v-card-title>现有列表</v-card-title>
|
||||
<v-card-text v-if="lists.length === 0">
|
||||
暂无列表,请创建新列表
|
||||
@@ -27,7 +31,10 @@
|
||||
<div v-if="list.id !== editingListId">
|
||||
<v-list-item-title>{{ list.name }}</v-list-item-title>
|
||||
</div>
|
||||
<div v-else class="d-flex align-center w-100">
|
||||
<div
|
||||
v-else
|
||||
class="d-flex align-center w-100"
|
||||
>
|
||||
<v-text-field
|
||||
v-model="editListName"
|
||||
autofocus
|
||||
@@ -36,21 +43,41 @@
|
||||
hide-details
|
||||
label="列表名称"
|
||||
@keyup.enter="saveListName"
|
||||
></v-text-field>
|
||||
<v-btn border class="mr-2" color="primary" icon @click.stop.prevent="saveListName">
|
||||
/>
|
||||
<v-btn
|
||||
border
|
||||
class="mr-2"
|
||||
color="primary"
|
||||
icon
|
||||
@click.stop.prevent="saveListName"
|
||||
>
|
||||
<v-icon>mdi-check</v-icon>
|
||||
</v-btn>
|
||||
<v-btn border color="error" icon @click.stop.prevent="cancelEditing">
|
||||
<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 border class="mr-2" icon @click.stop.prevent="startEditing(list.id)">
|
||||
<v-btn
|
||||
border
|
||||
class="mr-2"
|
||||
icon
|
||||
@click.stop.prevent="startEditing(list.id)"
|
||||
>
|
||||
<v-icon>mdi-pencil</v-icon>
|
||||
</v-btn>
|
||||
<v-btn border icon @click.stop.prevent="confirmDeleteList(list.id)">
|
||||
<v-btn
|
||||
border
|
||||
icon
|
||||
@click.stop.prevent="confirmDeleteList(list.id)"
|
||||
>
|
||||
<v-icon>mdi-delete</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
@@ -58,30 +85,49 @@
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-card>
|
||||
<v-card border class="mb-5" 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"
|
||||
:rules="[v => !!v || '名称不能为空']"
|
||||
label="列表名称"
|
||||
></v-text-field>
|
||||
<v-btn :disabled="!newListName" color="primary" @click="createNewList">
|
||||
/>
|
||||
<v-btn
|
||||
:disabled="!newListName"
|
||||
color="primary"
|
||||
@click="createNewList"
|
||||
>
|
||||
创建列表
|
||||
</v-btn>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
<!-- 确认删除对话框 -->
|
||||
<v-dialog v-model="deleteDialog.show" max-width="500">
|
||||
<v-dialog
|
||||
v-model="deleteDialog.show"
|
||||
max-width="500"
|
||||
>
|
||||
<v-card border>
|
||||
<v-card-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-spacer />
|
||||
<v-btn
|
||||
color="primary"
|
||||
variant="text"
|
||||
@click="deleteDialog.show = false"
|
||||
>
|
||||
取消
|
||||
</v-btn>
|
||||
<v-btn color="error" variant="text" @click="confirmDelete">
|
||||
<v-btn
|
||||
color="error"
|
||||
variant="text"
|
||||
@click="confirmDelete"
|
||||
>
|
||||
确认删除
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
@@ -134,8 +180,11 @@ export default {
|
||||
try {
|
||||
// 从classworks-list-main加载学生列表数据
|
||||
const response = await dataProvider.loadData("classworks-list-main");
|
||||
if (response && Array.isArray(response)) {
|
||||
this.studentList = response;
|
||||
|
||||
const dataArray = Array.isArray(response) ? response : (response && Array.isArray(response.value) ? response.value : null);
|
||||
|
||||
if (response && response.success !== false && dataArray) {
|
||||
this.studentList = dataArray;
|
||||
} else {
|
||||
this.studentList = [];
|
||||
}
|
||||
|
||||
+161
-57
@@ -11,10 +11,11 @@
|
||||
icon="mdi-menu"
|
||||
variant="text"
|
||||
@click="drawer = !drawer"
|
||||
|
||||
/>
|
||||
</template>
|
||||
<v-app-bar-title class="text-h6">设置</v-app-bar-title>
|
||||
<v-app-bar-title class="text-h6">
|
||||
设置
|
||||
</v-app-bar-title>
|
||||
</v-app-bar>
|
||||
|
||||
<v-container fluid>
|
||||
@@ -43,14 +44,23 @@
|
||||
direction="vertical"
|
||||
style="width: 100%"
|
||||
>
|
||||
<v-tabs-window-item value="index"
|
||||
>
|
||||
<v-card border class="service-card gradient-right clickable mb-4" color="primary" elevation="8" hover
|
||||
rounded="xl" variant="tonal" @click="openClassworksKV">
|
||||
<v-tabs-window-item value="index">
|
||||
<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>
|
||||
<div class="text-h6">在寻找 Classworks KV ?</div>
|
||||
<div class="text-h6">
|
||||
在寻找 Classworks KV ?
|
||||
</div>
|
||||
<div class="text-caption text-medium-emphasis">
|
||||
文档形键值数据库
|
||||
</div>
|
||||
@@ -71,50 +81,53 @@
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
<v-card border class="rounded-xl mb-4" subtitle="设置" title="Classworks">
|
||||
<v-card
|
||||
border
|
||||
class="rounded-xl mb-4"
|
||||
subtitle="设置"
|
||||
title="Classworks"
|
||||
>
|
||||
<v-card-text>
|
||||
<v-alert
|
||||
class="rounded-xl"
|
||||
color="error"
|
||||
icon="mdi-alert-circle"
|
||||
variant="tonal"
|
||||
>Classworks
|
||||
>
|
||||
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"
|
||||
icon="mdi-information"
|
||||
variant="tonal"
|
||||
>请不要使用浏览器清除缓存功能,否则会导致配置丢失。
|
||||
<del
|
||||
>恶意的操作可能导致您受到贵校教师的处理
|
||||
</del
|
||||
>
|
||||
</v-alert
|
||||
>
|
||||
请不要使用浏览器清除缓存功能,否则会导致配置丢失。
|
||||
<del>恶意的操作可能导致您受到贵校教师的处理
|
||||
</del>
|
||||
</v-alert>
|
||||
<v-alert
|
||||
class="mt-4 rounded-xl"
|
||||
color="warning"
|
||||
icon="mdi-information"
|
||||
variant="tonal"
|
||||
><p>
|
||||
请不要使用包括但不限于360极速浏览器、360安全浏览器、夸克浏览器、QQ浏览器等浏览器使用
|
||||
Classworks
|
||||
,这些浏览器过时且存在严重的一致性问题。在Windows上,使用新版
|
||||
Microsoft Edge 浏览器是最推荐的选择。
|
||||
</p>
|
||||
>
|
||||
<p>
|
||||
请不要使用包括但不限于360极速浏览器、360安全浏览器、夸克浏览器、QQ浏览器等浏览器使用
|
||||
Classworks
|
||||
,这些浏览器过时且存在严重的一致性问题。在Windows上,使用新版
|
||||
Microsoft Edge 浏览器是最推荐的选择。
|
||||
</p>
|
||||
<p style="color: #666">
|
||||
上述浏览器商标为其所属公司所有,Classworks™
|
||||
与上述浏览器所属公司无竞争关系。
|
||||
</p>
|
||||
<br/>
|
||||
<br>
|
||||
<v-btn
|
||||
append-icon="mdi-open-in-new"
|
||||
class="text-none rounded-xl"
|
||||
@@ -122,14 +135,13 @@
|
||||
href="https://www.microsoft.com/zh-cn/windows/microsoft-edge"
|
||||
target="_blank"
|
||||
variant="tonal"
|
||||
>下载 Microsoft Edge(微软边缘浏览器)
|
||||
</v-btn
|
||||
>
|
||||
</v-alert
|
||||
>
|
||||
下载 Microsoft Edge(微软边缘浏览器)
|
||||
</v-btn>
|
||||
</v-alert>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
<about-card/>
|
||||
<about-card />
|
||||
</v-tabs-window-item>
|
||||
|
||||
<v-tabs-window-item value="server">
|
||||
@@ -138,16 +150,32 @@
|
||||
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 :is-mobile="isMobile" border/>
|
||||
<teacher-list-card :is-mobile="isMobile" border class="mt-4"/>
|
||||
<student-list-card
|
||||
:is-mobile="isMobile"
|
||||
border
|
||||
/>
|
||||
<teacher-list-card
|
||||
:is-mobile="isMobile"
|
||||
border
|
||||
class="mt-4"
|
||||
/>
|
||||
</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">
|
||||
@@ -191,27 +219,35 @@
|
||||
</v-tabs-window-item>
|
||||
|
||||
<v-tabs-window-item value="randomPicker">
|
||||
<random-picker-card :is-mobile="isMobile" border/>
|
||||
<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 icon="mdi-developer-board" title="开发者选项">
|
||||
<v-tabs-window-item value="developer">
|
||||
<settings-card
|
||||
border
|
||||
icon="mdi-developer-board"
|
||||
title="开发者选项"
|
||||
>
|
||||
<v-list>
|
||||
<v-list-item>
|
||||
<template #prepend>
|
||||
<v-icon class="mr-3" icon="mdi-code-tags"/>
|
||||
<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>
|
||||
启用后可以查看和修改开发者设置
|
||||
</v-list-item-subtitle>
|
||||
<template #append>
|
||||
<v-switch
|
||||
v-model="settings.developer.enabled"
|
||||
@@ -229,29 +265,41 @@
|
||||
@saved="onSettingsSaved"
|
||||
/>
|
||||
<template v-if="settings.developer.enabled">
|
||||
<v-card border class="mt-4 rounded-lg">
|
||||
<v-card
|
||||
border
|
||||
class="mt-4 rounded-lg"
|
||||
>
|
||||
<v-card-title class="d-flex align-center">
|
||||
<v-icon class="mr-2" icon="mdi-cog-outline"/>
|
||||
<v-icon
|
||||
class="mr-2"
|
||||
icon="mdi-cog-outline"
|
||||
/>
|
||||
所有设置
|
||||
</v-card-title>
|
||||
<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-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>
|
||||
|
||||
@@ -319,7 +367,7 @@ export default {
|
||||
const settings = {
|
||||
server: {
|
||||
domain: getSetting("server.domain"),
|
||||
classNumber: getSetting("server.classNumber"),
|
||||
classNumber: "加载中...",
|
||||
provider: getSetting("server.provider"),
|
||||
},
|
||||
refresh: {
|
||||
@@ -488,8 +536,23 @@ export default {
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
async mounted() {
|
||||
this.loadAllSettings();
|
||||
|
||||
try {
|
||||
const dataProvider = (await import("@/utils/dataProvider")).default;
|
||||
const res = await dataProvider.loadData("classworks-config-basic");
|
||||
if (res && res.success !== false && res.classNumber) {
|
||||
this.settings.server.classNumber = res.classNumber;
|
||||
} else {
|
||||
this.settings.server.classNumber = "我的班级";
|
||||
}
|
||||
} catch (e) {
|
||||
this.settings.server.classNumber = "我的班级";
|
||||
}
|
||||
|
||||
this.originalSettings = JSON.parse(JSON.stringify(this.settings));
|
||||
|
||||
this.unwatchSettings = watchSettings(() => {
|
||||
this.loadAllSettings();
|
||||
});
|
||||
@@ -508,8 +571,49 @@ export default {
|
||||
window.open(getSetting("server.authDomain"), "_blank");
|
||||
},
|
||||
loadAllSettings() {
|
||||
// Keep existing classNumber or set to 加载中
|
||||
const currentClassNumber = this.settings ? this.settings.server.classNumber : "加载中...";
|
||||
|
||||
this.settings = {
|
||||
server: {
|
||||
domain: getSetting("server.domain"),
|
||||
classNumber: currentClassNumber,
|
||||
provider: getSetting("server.provider"),
|
||||
},
|
||||
refresh: {
|
||||
auto: getSetting("refresh.auto"),
|
||||
interval: getSetting("refresh.interval"),
|
||||
},
|
||||
font: {
|
||||
size: getSetting("font.size"),
|
||||
},
|
||||
edit: {
|
||||
autoSave: getSetting("edit.autoSave"),
|
||||
blockNonTodayAutoSave: getSetting("edit.blockNonTodayAutoSave"),
|
||||
confirmNonTodaySave: getSetting("edit.confirmNonTodaySave"),
|
||||
refreshBeforeEdit: getSetting("edit.refreshBeforeEdit"),
|
||||
},
|
||||
display: {
|
||||
emptySubjectDisplay: getSetting("display.emptySubjectDisplay"),
|
||||
dynamicSort: getSetting("display.dynamicSort"),
|
||||
showRandomButton: getSetting("display.showRandomButton"),
|
||||
showFullscreenButton: getSetting("display.showFullscreenButton"),
|
||||
},
|
||||
developer: {
|
||||
enabled: getSetting("developer.enabled"),
|
||||
showDebugConfig: getSetting("developer.showDebugConfig"),
|
||||
},
|
||||
message: {
|
||||
showSidebar: getSetting("message.showSidebar"),
|
||||
maxActiveMessages: getSetting("message.maxActiveMessages"),
|
||||
timeout: getSetting("message.timeout"),
|
||||
saveHistory: getSetting("message.saveHistory"),
|
||||
},
|
||||
};
|
||||
|
||||
Object.keys(this.settings).forEach((section) => {
|
||||
Object.keys(this.settings[section]).forEach((key) => {
|
||||
if (section === "server" && key === "classNumber") return;
|
||||
this.settings[section][key] = getSetting(`${section}.${key}`);
|
||||
});
|
||||
});
|
||||
|
||||
+104
-27
@@ -5,10 +5,18 @@
|
||||
<v-col cols="12">
|
||||
<v-card border>
|
||||
<v-card-title class="d-flex align-center">
|
||||
<v-icon class="mr-2" color="primary">mdi-network</v-icon>
|
||||
<v-icon
|
||||
class="mr-2"
|
||||
color="primary"
|
||||
>
|
||||
mdi-network
|
||||
</v-icon>
|
||||
Socket.IO 连接调试器
|
||||
<v-spacer />
|
||||
<v-chip :color="connectionStatus.color" size="small">
|
||||
<v-chip
|
||||
:color="connectionStatus.color"
|
||||
size="small"
|
||||
>
|
||||
{{ connectionStatus.text }}
|
||||
</v-chip>
|
||||
</v-card-title>
|
||||
@@ -16,7 +24,10 @@
|
||||
</v-col>
|
||||
|
||||
<!-- 连接信息卡片 -->
|
||||
<v-col cols="12" md="6">
|
||||
<v-col
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<v-card border>
|
||||
<v-card-title>连接信息</v-card-title>
|
||||
<v-card-text>
|
||||
@@ -43,7 +54,10 @@
|
||||
</template>
|
||||
<v-list-item-title>传输方式</v-list-item-title>
|
||||
<v-list-item-subtitle>
|
||||
<v-chip size="x-small" :color="transportColor">
|
||||
<v-chip
|
||||
size="x-small"
|
||||
:color="transportColor"
|
||||
>
|
||||
{{ transport || '未知' }}
|
||||
</v-chip>
|
||||
</v-list-item-subtitle>
|
||||
@@ -113,40 +127,71 @@
|
||||
</v-col>
|
||||
|
||||
<!-- 统计信息卡片 -->
|
||||
<v-col cols="12" md="6">
|
||||
<v-col
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<v-card border>
|
||||
<v-card-title>统计信息</v-card-title>
|
||||
<v-card-text>
|
||||
<v-row>
|
||||
<v-col cols="6">
|
||||
<v-card variant="tonal" color="success">
|
||||
<v-card
|
||||
variant="tonal"
|
||||
color="success"
|
||||
>
|
||||
<v-card-text class="text-center">
|
||||
<div class="text-h4">{{ stats.eventsReceived }}</div>
|
||||
<div class="text-caption">接收事件</div>
|
||||
<div class="text-h4">
|
||||
{{ stats.eventsReceived }}
|
||||
</div>
|
||||
<div class="text-caption">
|
||||
接收事件
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
<v-col cols="6">
|
||||
<v-card variant="tonal" color="primary">
|
||||
<v-card
|
||||
variant="tonal"
|
||||
color="primary"
|
||||
>
|
||||
<v-card-text class="text-center">
|
||||
<div class="text-h4">{{ stats.eventsSent }}</div>
|
||||
<div class="text-caption">发送事件</div>
|
||||
<div class="text-h4">
|
||||
{{ stats.eventsSent }}
|
||||
</div>
|
||||
<div class="text-caption">
|
||||
发送事件
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
<v-col cols="6">
|
||||
<v-card variant="tonal" color="warning">
|
||||
<v-card
|
||||
variant="tonal"
|
||||
color="warning"
|
||||
>
|
||||
<v-card-text class="text-center">
|
||||
<div class="text-h4">{{ stats.errors }}</div>
|
||||
<div class="text-caption">错误次数</div>
|
||||
<div class="text-h4">
|
||||
{{ stats.errors }}
|
||||
</div>
|
||||
<div class="text-caption">
|
||||
错误次数
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
<v-col cols="6">
|
||||
<v-card variant="tonal" color="info">
|
||||
<v-card
|
||||
variant="tonal"
|
||||
color="info"
|
||||
>
|
||||
<v-card-text class="text-center">
|
||||
<div class="text-h4">{{ stats.reconnects }}</div>
|
||||
<div class="text-caption">重连次数</div>
|
||||
<div class="text-h4">
|
||||
{{ stats.reconnects }}
|
||||
</div>
|
||||
<div class="text-caption">
|
||||
重连次数
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
@@ -175,14 +220,26 @@
|
||||
</v-col>
|
||||
|
||||
<!-- 事件监控 -->
|
||||
<v-col cols="12" md="6">
|
||||
<v-col
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<v-card border>
|
||||
<v-card-title>
|
||||
事件监控
|
||||
<v-chip class="ml-2" size="small">{{ activeListeners.size }} 个监听器</v-chip>
|
||||
<v-chip
|
||||
class="ml-2"
|
||||
size="small"
|
||||
>
|
||||
{{ activeListeners.size }} 个监听器
|
||||
</v-chip>
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<v-list density="compact" max-height="300" style="overflow-y: auto">
|
||||
<v-list
|
||||
density="compact"
|
||||
max-height="300"
|
||||
style="overflow-y: auto"
|
||||
>
|
||||
<v-list-item v-if="activeListeners.size === 0">
|
||||
<v-list-item-title class="text-center text-disabled">
|
||||
暂无活动监听器
|
||||
@@ -193,7 +250,9 @@
|
||||
:key="listener"
|
||||
>
|
||||
<template #prepend>
|
||||
<v-icon size="small">mdi-eye</v-icon>
|
||||
<v-icon size="small">
|
||||
mdi-eye
|
||||
</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>{{ listener }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
@@ -203,7 +262,10 @@
|
||||
</v-col>
|
||||
|
||||
<!-- 自定义事件发送 -->
|
||||
<v-col cols="12" md="6">
|
||||
<v-col
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<v-card border>
|
||||
<v-card-title>发送自定义事件</v-card-title>
|
||||
<v-card-text>
|
||||
@@ -272,9 +334,15 @@
|
||||
<span class="log-time">[{{ log.time }}]</span>
|
||||
<span class="log-type">[{{ log.type.toUpperCase() }}]</span>
|
||||
<span class="log-message">{{ log.message }}</span>
|
||||
<pre v-if="log.data" class="log-data">{{ log.data }}</pre>
|
||||
<pre
|
||||
v-if="log.data"
|
||||
class="log-data"
|
||||
>{{ log.data }}</pre>
|
||||
</div>
|
||||
<div v-if="logs.length === 0" class="text-center text-disabled pa-8">
|
||||
<div
|
||||
v-if="logs.length === 0"
|
||||
class="text-center text-disabled pa-8"
|
||||
>
|
||||
暂无日志
|
||||
</div>
|
||||
</div>
|
||||
@@ -288,7 +356,10 @@
|
||||
<v-card-title>连接诊断</v-card-title>
|
||||
<v-card-text>
|
||||
<v-row>
|
||||
<v-col cols="12" md="4">
|
||||
<v-col
|
||||
cols="12"
|
||||
md="4"
|
||||
>
|
||||
<v-btn
|
||||
block
|
||||
color="info"
|
||||
@@ -298,7 +369,10 @@
|
||||
测试连接
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-col cols="12" md="4">
|
||||
<v-col
|
||||
cols="12"
|
||||
md="4"
|
||||
>
|
||||
<v-btn
|
||||
block
|
||||
color="success"
|
||||
@@ -309,7 +383,10 @@
|
||||
测量延迟
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-col cols="12" md="4">
|
||||
<v-col
|
||||
cols="12"
|
||||
md="4"
|
||||
>
|
||||
<v-btn
|
||||
block
|
||||
color="warning"
|
||||
|
||||
@@ -15,8 +15,10 @@ export const useExamStore = defineStore('exam', {
|
||||
this.loadingList = true
|
||||
try {
|
||||
const response = await dataProvider.loadData('es_list')
|
||||
if (Array.isArray(response)) {
|
||||
this.examList = response
|
||||
const dataArray = Array.isArray(response) ? response : (response && Array.isArray(response.value) ? response.value : null)
|
||||
|
||||
if (response && response.success !== false && dataArray) {
|
||||
this.examList = dataArray
|
||||
} else {
|
||||
this.examList = []
|
||||
}
|
||||
|
||||
@@ -236,3 +236,19 @@ $standard-accelerate: cubic-bezier(0.3, 0.0, 1.0, 1.0);
|
||||
opacity: 0;
|
||||
transform: scale(0.9);
|
||||
}
|
||||
|
||||
// Slide X Transition
|
||||
.slide-x-transition-enter-active,
|
||||
.slide-x-transition-leave-active {
|
||||
transition: transform 0.3s cubic-bezier(0.25, 0.8, 0.5, 1), opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.slide-x-transition-enter-from {
|
||||
transform: translateX(15px);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.slide-x-transition-leave-to {
|
||||
transform: translateX(-15px);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
+8
-243
@@ -1,7 +1,4 @@
|
||||
import {kvLocalProvider} from "./providers/kvLocalProvider";
|
||||
import {kvServerProvider} from "./providers/kvServerProvider";
|
||||
import {getSetting, setSetting} from "./settings";
|
||||
import {getEffectiveServerUrl} from "./serverRotation";
|
||||
import {kvFileProvider} from "./providers/kvFileProvider";
|
||||
|
||||
export const formatResponse = (data) => data;
|
||||
|
||||
@@ -10,264 +7,32 @@ export const formatError = (message, code = "UNKNOWN_ERROR") => ({
|
||||
error: {code, message},
|
||||
});
|
||||
|
||||
// Main data provider with simplified API
|
||||
// Main data provider with simplified API - Local File Storage
|
||||
export default {
|
||||
// Provider API methods
|
||||
loadData: async (key) => {
|
||||
const provider = getSetting("server.provider");
|
||||
const useServer =
|
||||
provider === "kv-server" || provider === "classworkscloud";
|
||||
|
||||
if (useServer) {
|
||||
return kvServerProvider.loadData(key);
|
||||
} else {
|
||||
return kvLocalProvider.loadData(key);
|
||||
}
|
||||
return kvFileProvider.loadData(key);
|
||||
},
|
||||
|
||||
saveData: async (key, data) => {
|
||||
const provider = getSetting("server.provider");
|
||||
const useServer =
|
||||
provider === "kv-server" || provider === "classworkscloud";
|
||||
|
||||
if (useServer) {
|
||||
return kvServerProvider.saveData(key, data);
|
||||
} else {
|
||||
return kvLocalProvider.saveData(key, data);
|
||||
}
|
||||
return kvFileProvider.saveData(key, data);
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取键名列表
|
||||
* @param {Object} options - 查询选项
|
||||
* @param {string} options.sortBy - 排序字段,默认为 "key"
|
||||
* @param {string} options.sortDir - 排序方向,"asc" 或 "desc",默认为 "asc"
|
||||
* @param {number} options.limit - 每页返回的记录数,默认为 100
|
||||
* @param {number} options.skip - 跳过的记录数,默认为 0
|
||||
* @returns {Promise<Object>} 包含键名列表和分页信息的响应对象
|
||||
*
|
||||
* 使用示例:
|
||||
* ```javascript
|
||||
* // 获取前10个键名
|
||||
* const result = await dataProvider.loadKeys({ limit: 10 });
|
||||
* if (result.success !== false) {
|
||||
* console.log('键名列表:', result.keys);
|
||||
* console.log('总数:', result.total_rows);
|
||||
* }
|
||||
*
|
||||
* // 获取第二页数据(跳过前10个)
|
||||
* const page2 = await dataProvider.loadKeys({ limit: 10, skip: 10 });
|
||||
*
|
||||
* // 按键名降序排列
|
||||
* const sorted = await dataProvider.loadKeys({ sortDir: 'desc' });
|
||||
* ```
|
||||
*
|
||||
* 返回值格式:
|
||||
* ```javascript
|
||||
* {
|
||||
* keys: ["key1", "key2", "key3"],
|
||||
* total_rows: 150,
|
||||
* current_page: {
|
||||
* limit: 10,
|
||||
* skip: 0,
|
||||
* count: 10
|
||||
* },
|
||||
* load_more: "/api/kv/namespace/_keys?..." // 仅服务器模式
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
loadKeys: async (options = {}) => {
|
||||
const provider = getSetting("server.provider");
|
||||
const useServer =
|
||||
provider === "kv-server" || provider === "classworkscloud";
|
||||
|
||||
if (useServer) {
|
||||
return kvServerProvider.loadKeys(options);
|
||||
} else {
|
||||
return kvLocalProvider.loadKeys(options);
|
||||
}
|
||||
return kvFileProvider.loadKeys(options);
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取键的云端访问地址,并处理本地到云端的数据迁移
|
||||
*
|
||||
* 功能说明:
|
||||
* 1. 如果用户选择本地存储,则将本地键数据读取并存储到云端
|
||||
* 2. 如果云端配置为空或错误则自动改成classworksCloudDefaults的配置
|
||||
* 3. 根据网站验证情况(私有则添加token,公开或受保护则不需要)拼接键的get路径并返回
|
||||
*
|
||||
* @param {string} key - 要获取地址的键名
|
||||
* @param {Object} options - 选项配置
|
||||
* @param {boolean} options.migrateFromLocal - 是否从本地迁移数据到云端,默认为true
|
||||
* @param {boolean} options.autoConfigureCloud - 是否自动配置云端默认设置,默认为true
|
||||
* @returns {Promise<Object>} 包含键访问地址和操作结果的响应对象
|
||||
*
|
||||
* 使用示例:
|
||||
* ```javascript
|
||||
* import dataProvider from '@/utils/dataProvider';
|
||||
*
|
||||
* // 基本用法:获取键的云端地址并自动迁移本地数据
|
||||
* const result = await dataProvider.getKeyCloudUrl('exam_configs');
|
||||
* if (result.success) {
|
||||
* console.log('云端访问地址:', result.url);
|
||||
* console.log('是否已迁移数据:', result.migrated);
|
||||
* console.log('是否自动配置:', result.configured);
|
||||
* } else {
|
||||
* console.error('获取失败:', result.error.message);
|
||||
* }
|
||||
*
|
||||
* // 仅获取地址,不迁移数据
|
||||
* const urlOnly = await dataProvider.getKeyCloudUrl('my_data', {
|
||||
* migrateFromLocal: false
|
||||
* });
|
||||
*
|
||||
* // 不自动配置云端设置
|
||||
* const noAutoConfig = await dataProvider.getKeyCloudUrl('my_data', {
|
||||
* autoConfigureCloud: false
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* 传入参数示例:
|
||||
* ```javascript
|
||||
* // 参数1: key (必需)
|
||||
* 'exam_configs' // 字符串类型的键名
|
||||
*
|
||||
* // 参数2: options (可选)
|
||||
* {
|
||||
* migrateFromLocal: true, // 是否迁移本地数据
|
||||
* autoConfigureCloud: true // 是否自动配置云端
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* 返回值格式:
|
||||
* ```javascript
|
||||
* // 成功时返回:
|
||||
* {
|
||||
* success: true,
|
||||
* url: "https://kv-service.houlang.cloud/device-uuid-123/exam_configs?token=abc123", // 私有访问时包含token
|
||||
* migrated: true, // 是否成功迁移了本地数据
|
||||
* configured: false // 是否自动配置了云端设置
|
||||
* }
|
||||
*
|
||||
* // 公开访问时返回:
|
||||
* {
|
||||
* success: true,
|
||||
* url: "https://kv-service.houlang.cloud/device-uuid-123/exam_configs", // 公开访问不包含token
|
||||
* migrated: false,
|
||||
* configured: true
|
||||
* }
|
||||
*
|
||||
* // 失败时返回:
|
||||
* {
|
||||
* success: false,
|
||||
* error: {
|
||||
* code: "CLOUD_URL_ERROR",
|
||||
* message: "获取键云端地址失败"
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
* Removed cloud related methods
|
||||
*/
|
||||
async getKeyCloudUrl(key, options = {}) {
|
||||
const {
|
||||
migrateFromLocal = true,
|
||||
autoConfigureCloud = true
|
||||
} = options;
|
||||
|
||||
try {
|
||||
const provider = getSetting("server.provider");
|
||||
let serverUrl;
|
||||
|
||||
// Use effective server URL for classworkscloud provider
|
||||
if (provider === "classworkscloud") {
|
||||
serverUrl = getEffectiveServerUrl();
|
||||
} else {
|
||||
serverUrl = getSetting("server.domain");
|
||||
}
|
||||
|
||||
let siteKey = getSetting("server.siteKey");
|
||||
const machineId = getSetting("device.uuid");
|
||||
let configured = false;
|
||||
|
||||
// 检查云端配置是否为空或错误,如果是则使用默认配置
|
||||
if (!serverUrl || !machineId) {
|
||||
if (autoConfigureCloud) {
|
||||
// 使用classworksCloudDefaults配置
|
||||
const classworksCloudDefaults = {
|
||||
"server.domain": import.meta.env.VITE_DEFAULT_KV_SERVER || "https://kv-service.houlang.cloud",
|
||||
"server.siteKey": "",
|
||||
};
|
||||
|
||||
if (!serverUrl) {
|
||||
setSetting("server.domain", classworksCloudDefaults["server.domain"]);
|
||||
serverUrl = classworksCloudDefaults["server.domain"];
|
||||
configured = true;
|
||||
}
|
||||
|
||||
if (!siteKey) {
|
||||
setSetting("server.siteKey", classworksCloudDefaults["server.siteKey"]);
|
||||
siteKey = classworksCloudDefaults["server.siteKey"];
|
||||
}
|
||||
|
||||
// 设置provider为classworkscloud
|
||||
setSetting("server.provider", "classworkscloud");
|
||||
// Get effective URL after setting provider
|
||||
serverUrl = getEffectiveServerUrl();
|
||||
} else {
|
||||
return formatError("云端配置无效,请检查服务器域名和设备UUID", "CONFIG_ERROR");
|
||||
}
|
||||
}
|
||||
|
||||
let migrated = false;
|
||||
|
||||
// 如果需要迁移本地数据到云端
|
||||
if (migrateFromLocal) {
|
||||
try {
|
||||
// 尝试从本地读取数据
|
||||
const localData = await kvLocalProvider.loadData(key);
|
||||
|
||||
// 如果本地有数据且不是错误响应
|
||||
if (localData && localData.success !== false) {
|
||||
// 检查云端是否已有数据
|
||||
const cloudData = await kvServerProvider.loadData(key);
|
||||
|
||||
// 如果云端没有数据,则迁移本地数据
|
||||
if (cloudData && cloudData.success === false && cloudData.error?.code === "NOT_FOUND") {
|
||||
const saveResult = await kvServerProvider.saveData(key, localData);
|
||||
if (saveResult && saveResult.success !== false) {
|
||||
migrated = true;
|
||||
console.log(`已成功将键 ${key} 的数据从本地迁移到云端`);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`迁移键 ${key} 的数据时出错:`, error);
|
||||
// 迁移失败不影响URL生成,继续执行
|
||||
}
|
||||
}
|
||||
// 获取认证token
|
||||
const authtoken = getSetting("server.kvToken");
|
||||
// 构建云端访问URL
|
||||
let url = `${serverUrl}/kv/${key}?token=${authtoken}`;
|
||||
|
||||
|
||||
return {
|
||||
success: true,
|
||||
url,
|
||||
migrated,
|
||||
configured
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error('获取键云端地址时出错:', error);
|
||||
return formatError(
|
||||
error.message || "获取键云端地址失败",
|
||||
"CLOUD_URL_ERROR"
|
||||
);
|
||||
}
|
||||
getKeyCloudUrl: async () => {
|
||||
return formatError("Cloud features are disabled", "NOT_SUPPORTED");
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
export const ErrorCodes = {
|
||||
NOT_FOUND: "数据不存在",
|
||||
NETWORK_ERROR: "网络连接失败",
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
/*
|
||||
* @Author: error: error: git config user.name & please set dead value or install git && error: git config user.email & please set dead value or install git & please set dead value or install git
|
||||
* @Date: 2026-03-07 23:45:42
|
||||
* @LastEditors: error: error: git config user.name & please set dead value or install git && error: git config user.email & please set dead value or install git & please set dead value or install git
|
||||
* @LastEditTime: 2026-03-20 22:54:42
|
||||
* @FilePath: \ClassHomeworkBoard\src\utils\defaults\defaultData.js
|
||||
* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
|
||||
*/
|
||||
export const defaultConfig = {
|
||||
studentList: [
|
||||
"Classworks可以管理学生列表",
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
export function parseMarkdown(val) {
|
||||
if (val == null) return '';
|
||||
let text = String(val);
|
||||
|
||||
// 1. Normalize newlines
|
||||
text = text.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
||||
|
||||
// 2. Escape HTML
|
||||
text = text
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">");
|
||||
|
||||
// 3. Process Block Elements (Line-based)
|
||||
const lines = text.split('\n');
|
||||
let inList = false;
|
||||
const output = [];
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
let line = lines[i];
|
||||
|
||||
// Headers (H1-H3)
|
||||
// ^# space ...
|
||||
if (line.match(/^#\s+(.+)$/)) {
|
||||
line = line.replace(/^#\s+(.+)$/, '<h1>$1</h1>');
|
||||
} else if (line.match(/^##\s+(.+)$/)) {
|
||||
line = line.replace(/^##\s+(.+)$/, '<h2>$1</h2>');
|
||||
} else if (line.match(/^###\s+(.+)$/)) {
|
||||
line = line.replace(/^###\s+(.+)$/, '<h3>$1</h3>');
|
||||
}
|
||||
// Horizontal Rule
|
||||
else if (line.match(/^---$/)) {
|
||||
line = '<hr>';
|
||||
}
|
||||
|
||||
// Lists (- item)
|
||||
if (line.match(/^\-\s+(.+)$/)) {
|
||||
if (!inList) {
|
||||
output.push('<ul>');
|
||||
inList = true;
|
||||
}
|
||||
line = line.replace(/^\-\s+(.+)$/, '<li>$1</li>');
|
||||
} else {
|
||||
// Not a list item
|
||||
if (inList) {
|
||||
output.push('</ul>');
|
||||
inList = false;
|
||||
}
|
||||
}
|
||||
|
||||
output.push(line);
|
||||
}
|
||||
|
||||
if (inList) {
|
||||
output.push('</ul>');
|
||||
}
|
||||
|
||||
text = output.join('\n');
|
||||
|
||||
// 4. Process Inline Elements (Global)
|
||||
|
||||
// Bold (**text**)
|
||||
text = text.replace(/\*\*(.*?)\*\*/g, '<b>$1</b>');
|
||||
|
||||
// Italic (*text*)
|
||||
text = text.replace(/\*(.*?)\*/g, '<i>$1</i>');
|
||||
|
||||
// Highlight (<mark>text</mark>) -> escaped as <mark>
|
||||
text = text.replace(/<mark>(.*?)<\/mark>/g, '<mark>$1</mark>');
|
||||
|
||||
// 5. Layout & Spacing
|
||||
|
||||
// Double newlines -> Paragraphs
|
||||
text = text.replace(/\n\n/g, '<p>');
|
||||
|
||||
// Cleanup newlines around block tags
|
||||
// Remove newline after block closing tags
|
||||
text = text.replace(/(<\/h[1-3]>|<\/ul>|<\/li>|<hr>)\n/g, '$1');
|
||||
|
||||
// Remaining single newlines -> <br>
|
||||
// But be careful not to break HTML structure we just built.
|
||||
// Actually, standard markdown treats single newline as space, but here we usually want <br>.
|
||||
// Let's replace remaining \n with <br>, but only if not inside tags?
|
||||
// Simple approach:
|
||||
text = text.replace(/\n/g, '<br>');
|
||||
|
||||
return text;
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
import { formatResponse, formatError } from "../dataProvider";
|
||||
|
||||
// Dynamically use the current origin instead of hardcoding localhost:3031
|
||||
// This handles preview modes, different ports, and local network IPs automatically
|
||||
const getApiBase = () => {
|
||||
if (typeof window !== 'undefined') {
|
||||
return `${window.location.origin}/api/local-file`;
|
||||
}
|
||||
return "/api/local-file";
|
||||
};
|
||||
|
||||
export const kvFileProvider = {
|
||||
async loadData(key) {
|
||||
try {
|
||||
const response = await fetch(`${getApiBase()}/load?key=${encodeURIComponent(key)}&_t=${Date.now()}`, {
|
||||
headers: {
|
||||
'Cache-Control': 'no-cache',
|
||||
'Pragma': 'no-cache'
|
||||
}
|
||||
});
|
||||
if (response.status === 404) {
|
||||
return formatError("数据不存在", "NOT_FOUND");
|
||||
}
|
||||
|
||||
const contentType = response.headers.get("content-type");
|
||||
if (!contentType || !contentType.includes("application/json")) {
|
||||
// If we didn't get JSON, it might be the dev server serving index.html
|
||||
// This happens if the plugin isn't intercepting the request properly
|
||||
throw new Error("Server did not return JSON. Is the local file plugin running?");
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
return formatResponse(result.data);
|
||||
} else {
|
||||
return formatError(result.error);
|
||||
}
|
||||
} catch (error) {
|
||||
return formatError("读取本地文件失败:" + error.message);
|
||||
}
|
||||
},
|
||||
|
||||
async saveData(key, data) {
|
||||
try {
|
||||
const response = await fetch(`${getApiBase()}/save`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
Accept: "application/json",
|
||||
},
|
||||
body: JSON.stringify({ key, data }),
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
return formatResponse(true);
|
||||
} else {
|
||||
return formatError(result.error);
|
||||
}
|
||||
} catch (error) {
|
||||
return formatError("保存本地文件失败:" + error.message);
|
||||
}
|
||||
},
|
||||
|
||||
async loadKeys(options = {}) {
|
||||
try {
|
||||
const response = await fetch(`${getApiBase()}/keys`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
const allKeys = result.keys;
|
||||
const {
|
||||
sortDir = "asc",
|
||||
limit = 100,
|
||||
skip = 0
|
||||
} = options;
|
||||
|
||||
const sortedKeys = allKeys.sort((a, b) => {
|
||||
if (sortDir === "desc") {
|
||||
return b.localeCompare(a);
|
||||
}
|
||||
return a.localeCompare(b);
|
||||
});
|
||||
|
||||
const paginatedKeys = sortedKeys.slice(skip, skip + limit);
|
||||
|
||||
return formatResponse({
|
||||
keys: paginatedKeys,
|
||||
total_rows: sortedKeys.length,
|
||||
current_page: {
|
||||
limit,
|
||||
skip,
|
||||
count: paginatedKeys.length
|
||||
},
|
||||
load_more: null
|
||||
});
|
||||
} else {
|
||||
return formatError(result.error);
|
||||
}
|
||||
} catch (error) {
|
||||
return formatError("获取文件列表失败:" + error.message);
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -1,3 +1,6 @@
|
||||
import { on } from '@/utils/socketClient'
|
||||
import { ref, onBeforeUnmount } from 'vue'
|
||||
|
||||
/**
|
||||
* Vue 组件安全事件处理工具
|
||||
* 防止组件卸载时的事件处理错误
|
||||
@@ -63,7 +66,7 @@ export function createSafeEventMixin() {
|
||||
if (this._isDestroying || !this.$el) return
|
||||
|
||||
try {
|
||||
requestAnimationFrame(() => {
|
||||
window.requestAnimationFrame(() => {
|
||||
if (!this._isDestroying && this.$el) {
|
||||
domOperation()
|
||||
}
|
||||
@@ -114,7 +117,6 @@ export const socketEventMixin = {
|
||||
*/
|
||||
$socketOn(event, handler) {
|
||||
return this.$safeOn(() => {
|
||||
const { on } = require('@/utils/socketClient')
|
||||
return on(event, this.$safeHandler(handler))
|
||||
})
|
||||
}
|
||||
@@ -156,8 +158,6 @@ export function withSafeEvents(component) {
|
||||
* Composition API 版本的安全事件处理
|
||||
*/
|
||||
export function useSafeEvents() {
|
||||
const { ref, onBeforeUnmount } = require('vue')
|
||||
|
||||
const isDestroying = ref(false)
|
||||
const cleanupFunctions = ref([])
|
||||
|
||||
@@ -193,7 +193,7 @@ export function useSafeEvents() {
|
||||
if (isDestroying.value) return
|
||||
|
||||
try {
|
||||
requestAnimationFrame(() => {
|
||||
window.requestAnimationFrame(() => {
|
||||
if (!isDestroying.value) {
|
||||
domOperation()
|
||||
}
|
||||
|
||||
@@ -1,128 +0,0 @@
|
||||
/**
|
||||
* Sentry 异步初始化模块
|
||||
*
|
||||
* 从 main.js 中抽离,在 Vue app 挂载后异步加载,
|
||||
* 避免 @sentry/vue (~60KB gzip) 阻塞首屏渲染。
|
||||
*/
|
||||
import * as Sentry from '@sentry/vue'
|
||||
import { getVisitorId } from './visitorId'
|
||||
|
||||
// 保存 feedback integration 实例的引用
|
||||
let feedbackIntegration = null
|
||||
|
||||
/**
|
||||
* 异步初始化 Sentry(在 app mount 后调用)
|
||||
* @param {import('vue').App} app - Vue app 实例
|
||||
* @param {import('vue-router').Router} router - Vue Router 实例
|
||||
*/
|
||||
export function initSentry(app, router) {
|
||||
Sentry.init({
|
||||
app,
|
||||
dsn: 'https://dc34ab47426f49c0925445f0d87b7007@report.houlang.cloud/6',
|
||||
sendDefaultPii: true,
|
||||
integrations: [
|
||||
Sentry.browserTracingIntegration({ router }),
|
||||
Sentry.replayIntegration({
|
||||
maskAllText: false,
|
||||
blockAllMedia: false,
|
||||
}),
|
||||
feedbackIntegration = Sentry.feedbackIntegration({
|
||||
autoInject: false,
|
||||
colorScheme: 'system',
|
||||
showBranding: false,
|
||||
showName: true,
|
||||
showEmail: true,
|
||||
isNameRequired: false,
|
||||
isEmailRequired: false,
|
||||
useSentryUser: {
|
||||
name: 'username',
|
||||
email: 'email',
|
||||
},
|
||||
themeDark: {
|
||||
submitBackground: '#6200EA',
|
||||
submitBackgroundHover: '#7C4DFF',
|
||||
},
|
||||
themeLight: {
|
||||
submitBackground: '#6200EA',
|
||||
submitBackgroundHover: '#7C4DFF',
|
||||
},
|
||||
}),
|
||||
],
|
||||
tracesSampleRate: 1.0,
|
||||
tracePropagationTargets: [
|
||||
'localhost',
|
||||
/^https?:\/\/cs\.(houlang\.cloud|houlangs\.com)/,
|
||||
],
|
||||
replaysSessionSampleRate: 0,
|
||||
replaysOnErrorSampleRate: 0,
|
||||
enableLogs: true,
|
||||
beforeSend(event) {
|
||||
return event
|
||||
},
|
||||
})
|
||||
|
||||
// 异步设置用户 fingerprint
|
||||
getVisitorId()
|
||||
.then((visitorId) => {
|
||||
Sentry.setUser({ id: visitorId, username: visitorId })
|
||||
Sentry.setTag('fingerprint', visitorId)
|
||||
console.log('Sentry 用户标识已设置:', visitorId)
|
||||
})
|
||||
.catch((error) => {
|
||||
console.warn('设置 Sentry 用户标识失败:', error)
|
||||
})
|
||||
|
||||
// 注册全局函数:打开反馈表单
|
||||
window.openSentryFeedback = () => {
|
||||
try {
|
||||
if (!feedbackIntegration) {
|
||||
console.warn('Sentry Feedback integration 未初始化')
|
||||
return false
|
||||
}
|
||||
if (typeof feedbackIntegration.createWidget === 'function') {
|
||||
const widget = feedbackIntegration.createWidget()
|
||||
if (widget && typeof widget.open === 'function') {
|
||||
widget.open()
|
||||
console.log('Sentry Feedback 对话框已打开')
|
||||
return true
|
||||
}
|
||||
}
|
||||
if (typeof feedbackIntegration.openDialog === 'function') {
|
||||
feedbackIntegration.openDialog()
|
||||
console.log('Sentry Feedback 对话框已打开')
|
||||
return true
|
||||
}
|
||||
console.warn('无法找到打开 Feedback 的方法')
|
||||
console.log('可用方法:', Object.keys(feedbackIntegration))
|
||||
return false
|
||||
} catch (error) {
|
||||
console.error('打开 Sentry Feedback 时出错:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 注册全局函数:手动启动录制
|
||||
window.startSentryReplay = () => {
|
||||
try {
|
||||
const client = Sentry.getClient()
|
||||
if (!client) {
|
||||
console.warn('Sentry 客户端未初始化')
|
||||
return false
|
||||
}
|
||||
const integrations = client.getOptions().integrations || []
|
||||
const replayIntegration = integrations.find(
|
||||
(integration) => integration && integration.name === 'Replay'
|
||||
)
|
||||
if (replayIntegration && typeof replayIntegration.start === 'function') {
|
||||
replayIntegration.start()
|
||||
console.log('Sentry Replay 已手动启动')
|
||||
return true
|
||||
}
|
||||
console.warn('无法找到 Sentry Replay integration')
|
||||
return false
|
||||
} catch (error) {
|
||||
console.error('启动 Sentry Replay 时出错:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,8 +7,7 @@ import { getSetting } from "./settings";
|
||||
|
||||
// Server list for classworkscloud provider (in priority order)
|
||||
const CLASSWORKS_CLOUD_SERVERS = [
|
||||
"https://kv-service.houlang.cloud",
|
||||
"https://kv-service.wuyuan.dev",
|
||||
// You can add default servers here
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -90,7 +89,7 @@ export function getEffectiveServerUrl() {
|
||||
const provider = getSetting("server.provider");
|
||||
|
||||
if (provider === "classworkscloud") {
|
||||
return CLASSWORKS_CLOUD_SERVERS[0];
|
||||
return CLASSWORKS_CLOUD_SERVERS[0] || "";
|
||||
}
|
||||
|
||||
return getSetting("server.domain") || "";
|
||||
|
||||
+27
-10
@@ -65,8 +65,7 @@ const SETTINGS_STORAGE_KEY = "Classworks_settings";
|
||||
|
||||
// 新增: Classworks云端存储的默认设置
|
||||
const classworksCloudDefaults = {
|
||||
"server.domain": import.meta.env.VITE_DEFAULT_KV_SERVER || "https://kv-service.houlang.cloud",
|
||||
//"server.domain": "http://localhost:3030",
|
||||
"server.domain": import.meta.env.VITE_DEFAULT_KV_SERVER || "",
|
||||
"server.siteKey": "",
|
||||
};
|
||||
|
||||
@@ -216,7 +215,7 @@ const settingsDefinitions = {
|
||||
},
|
||||
"server.classNumber": {
|
||||
type: "string",
|
||||
default: "高三八班",
|
||||
default: "我的班级",
|
||||
//validate: (value) => /^[A-Za-z0-9]*$/.test(value),
|
||||
validate: (value) => /.*/.test(value),
|
||||
description: "班级编号",
|
||||
@@ -239,7 +238,7 @@ const settingsDefinitions = {
|
||||
},
|
||||
"server.authDomain": {
|
||||
type: "string",
|
||||
default: import.meta.env.VITE_DEFAULT_AUTH_SERVER || "https://kv.houlang.cloud",
|
||||
default: import.meta.env.VITE_DEFAULT_AUTH_SERVER || "",
|
||||
description: "授权服务器域名",
|
||||
icon: "mdi-shield-account",
|
||||
validate: (value) => {
|
||||
@@ -418,14 +417,14 @@ const settingsDefinitions = {
|
||||
// 通知铃声设置
|
||||
"notification.singleSound": {
|
||||
type: "string",
|
||||
default: "Teams 默认.mp3",
|
||||
default: "universfield-new-notification-024-370048.mp3",
|
||||
description: "单次通知铃声",
|
||||
icon: "mdi-bell-ring",
|
||||
// 设置单次通知时播放的音频文件
|
||||
},
|
||||
"notification.urgentSound": {
|
||||
type: "string",
|
||||
default: "Teams 默认通话铃.mp3",
|
||||
default: "universfield-new-notification-024-370048.mp3",
|
||||
description: "持续通知铃声",
|
||||
icon: "mdi-bell-alert",
|
||||
// 设置紧急通知时循环播放的音频文件
|
||||
@@ -556,10 +555,18 @@ class SettingsManagerClass {
|
||||
if (typeof localStorage === "undefined") return;
|
||||
|
||||
try {
|
||||
const newValue = JSON.stringify(this.settingsCache);
|
||||
localStorage.setItem(
|
||||
SETTINGS_STORAGE_KEY,
|
||||
JSON.stringify(this.settingsCache)
|
||||
newValue
|
||||
);
|
||||
|
||||
// 触发同页面的自定义事件,以便当前页面也能监听到设置改变
|
||||
if (typeof window !== "undefined" && typeof window.CustomEvent !== "undefined") {
|
||||
window.dispatchEvent(new window.CustomEvent('classworks-settings-changed', {
|
||||
detail: this.settingsCache
|
||||
}));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("保存设置失败:", error);
|
||||
}
|
||||
@@ -717,15 +724,25 @@ class SettingsManagerClass {
|
||||
if (typeof window === "undefined") return () => {
|
||||
};
|
||||
|
||||
const handler = (event) => {
|
||||
const storageHandler = (event) => {
|
||||
if (event.key === SETTINGS_STORAGE_KEY) {
|
||||
this.settingsCache = JSON.parse(event.newValue);
|
||||
callback(this.settingsCache);
|
||||
}
|
||||
};
|
||||
|
||||
const customEventHandler = (event) => {
|
||||
this.settingsCache = event.detail;
|
||||
callback(this.settingsCache);
|
||||
};
|
||||
|
||||
window.addEventListener("storage", handler);
|
||||
return () => window.removeEventListener("storage", handler);
|
||||
window.addEventListener("storage", storageHandler);
|
||||
window.addEventListener("classworks-settings-changed", customEventHandler);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("storage", storageHandler);
|
||||
window.removeEventListener("classworks-settings-changed", customEventHandler);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+3
-37
@@ -6,48 +6,14 @@
|
||||
|
||||
// 所有可用的音频文件列表
|
||||
export const soundFiles = [
|
||||
"Teams Ping.mp3",
|
||||
"Teams Remix.mp3",
|
||||
"Teams bounce.mp3",
|
||||
"Teams incoming-ringtone-level30.mp3",
|
||||
"Teams incoming-ringtone-level40.mp3",
|
||||
"Teams meetup_ring.mp3",
|
||||
"Teams screenshare_ring.mp3",
|
||||
"Teams teams_meet_up_reminder.mp3",
|
||||
"Teams teams_notification.mp3",
|
||||
"Teams 优先处理.mp3",
|
||||
"Teams 共鸣.mp3",
|
||||
"Teams 召唤.mp3",
|
||||
"Teams 叮铃.mp3",
|
||||
"Teams 增强.mp3",
|
||||
"Teams 尤里卡.mp3",
|
||||
"Teams 弹拨.mp3",
|
||||
"Teams 提醒.mp3",
|
||||
"Teams 摇摆.mp3",
|
||||
"Teams 时空.mp3",
|
||||
"Teams 气泡(大声).mp3",
|
||||
"Teams 气泡.mp3",
|
||||
"Teams 波普.mp3",
|
||||
"Teams 波纹.mp3",
|
||||
"Teams 滴水.mp3",
|
||||
"Teams 点击.mp3",
|
||||
"Teams 蜂鸣声.mp3",
|
||||
"Teams 警报.mp3",
|
||||
"Teams 赋予希望.mp3",
|
||||
"Teams 轻弹.mp3",
|
||||
"Teams 进阶.mp3",
|
||||
"Teams 重复振铃.mp3",
|
||||
"Teams 颤振.mp3",
|
||||
"Teams 高分.mp3",
|
||||
"Teams 默认.mp3",
|
||||
"Teams 默认通话铃.mp3"
|
||||
"universfield-new-notification-024-370048.mp3"
|
||||
];
|
||||
|
||||
// 默认的单次通知铃声
|
||||
export const defaultSingleSound = 'Teams 默认.mp3';
|
||||
export const defaultSingleSound = 'universfield-new-notification-024-370048.mp3';
|
||||
|
||||
// 默认的持续通知铃声
|
||||
export const defaultUrgentSound = 'Teams 默认通话铃.mp3';
|
||||
export const defaultUrgentSound = 'universfield-new-notification-024-370048.mp3';
|
||||
|
||||
// 获取音频文件的完整路径
|
||||
export function getSoundPath(filename) {
|
||||
|
||||
Reference in New Issue
Block a user