pwa,设置项,自动刷新
This commit is contained in:
@@ -0,0 +1,523 @@
|
||||
<template>
|
||||
<v-dialog v-model="dialog" max-width="600" fullscreen-breakpoint="sm">
|
||||
<v-card class="random-picker-card">
|
||||
<v-card-title class="text-h5 d-flex align-center">
|
||||
<v-icon icon="mdi-account-question" class="mr-2" />
|
||||
随机点名
|
||||
<v-spacer />
|
||||
<v-btn icon="mdi-close" variant="text" @click="dialog = false" />
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text v-if="!isPickingStarted" class="text-center py-6">
|
||||
<div class="text-h6 mb-4">请选择抽取人数</div>
|
||||
|
||||
<div class="d-flex justify-center align-center counter-container">
|
||||
<v-btn size="x-large" icon="mdi-minus" variant="tonal" color="primary" :disabled="count <= 1"
|
||||
@click="decrementCount" class="counter-btn" />
|
||||
|
||||
<div class="count-display mx-8">
|
||||
<span class="text-h2 font-weight-bold">{{ count }}</span>
|
||||
<span class="text-subtitle-1 ml-2">人</span>
|
||||
</div>
|
||||
|
||||
<v-btn size="x-large" icon="mdi-plus" variant="tonal" color="primary" :disabled="count >= maxAllowedCount"
|
||||
@click="incrementCount" class="counter-btn" />
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="mt-4">
|
||||
<v-btn size="x-large" color="primary" prepend-icon="mdi-dice-multiple" @click="startPicking"
|
||||
:disabled="filteredStudents.length === 0" class="start-btn">
|
||||
开始抽取
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<div v-if="filteredStudents.length === 0" class="mt-4 text-error">
|
||||
没有可抽取的学生,请调整过滤选项
|
||||
</div>
|
||||
|
||||
<div class="mt-4 text-caption">
|
||||
当前可抽取学生: {{ filteredStudents.length }}人
|
||||
<v-tooltip location="bottom">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-icon v-bind="props" icon="mdi-information-outline" size="small" class="ml-1" />
|
||||
</template>
|
||||
<div class="pa-2">
|
||||
<div v-if="tempFilters.excludeAbsent">• 已排除请假学生 ({{ absentCount }}人)</div>
|
||||
<div v-if="tempFilters.excludeLate">• 已排除迟到学生 ({{ lateCount }}人)</div>
|
||||
<div v-if="tempFilters.excludeExcluded">• 已排除不参与学生 ({{ excludedCount }}人)</div>
|
||||
</div>
|
||||
</v-tooltip><!-- 添加临时过滤选项 -->
|
||||
|
||||
<div class="d-flex flex-wrap justify-center gap-2 mt-4">
|
||||
<v-chip :color="tempFilters.excludeLate ? 'warning' : 'default'"
|
||||
:variant="tempFilters.excludeLate ? 'elevated' : 'text'"
|
||||
@click="tempFilters.excludeLate = !tempFilters.excludeLate" prepend-icon="mdi-clock-alert"
|
||||
class="filter-chip">
|
||||
{{ tempFilters.excludeLate ? '排除' : '包含' }}迟到学生
|
||||
</v-chip>
|
||||
<v-chip :color="tempFilters.excludeAbsent ? 'error' : 'default'"
|
||||
:variant="tempFilters.excludeAbsent ? 'elevated' : 'text'"
|
||||
@click="tempFilters.excludeAbsent = !tempFilters.excludeAbsent" prepend-icon="mdi-account-off"
|
||||
class="filter-chip">
|
||||
{{ tempFilters.excludeAbsent ? '排除' : '包含' }}请假学生
|
||||
</v-chip>
|
||||
|
||||
|
||||
|
||||
<v-chip :color="tempFilters.excludeExcluded ? 'grey' : 'default'"
|
||||
:variant="tempFilters.excludeExcluded ? 'elevated' : 'text'"
|
||||
@click="tempFilters.excludeExcluded = !tempFilters.excludeExcluded" prepend-icon="mdi-account-cancel"
|
||||
class="filter-chip">
|
||||
{{ tempFilters.excludeExcluded ? '排除' : '包含' }}不参与学生
|
||||
</v-chip>
|
||||
</div>
|
||||
</div>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-text v-else class="text-center py-6">
|
||||
<div v-if="isAnimating" class="animation-container">
|
||||
<div class="animation-wrapper">
|
||||
<transition-group name="shuffle" tag="div" class="shuffle-container">
|
||||
<div v-for="(student, index) in animationStudents" :key="student.id" class="student-item"
|
||||
:class="{ 'highlighted': highlightedIndices.includes(index) }">
|
||||
{{ student.name }}
|
||||
</div>
|
||||
</transition-group>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="result-container">
|
||||
<div class="text-h6 mb-4">抽取结果</div>
|
||||
<v-card v-for="(student, index) in pickedStudents" :key="index" variant="outlined" color="primary"
|
||||
class="mb-2 result-card">
|
||||
<v-card-text class="text-h4 text-center py-4 d-flex align-center justify-center">
|
||||
{{ student }}
|
||||
<v-btn icon="mdi-refresh" variant="text" size="small" class="ml-2 refresh-btn"
|
||||
@click="refreshSingleStudent(index)" :disabled="remainingStudents.length === 0"
|
||||
:title="remainingStudents.length === 0 ? '没有更多可用学生' : '重新抽取此学生'" />
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<div class="mt-8 d-flex justify-center">
|
||||
<v-btn color="primary" prepend-icon="mdi-refresh" @click="resetPicker" size="large" class="mx-2">
|
||||
重新抽取
|
||||
</v-btn>
|
||||
<v-btn color="grey" variant="outlined" @click="dialog = false" size="large" class="mx-2">
|
||||
关闭
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getSetting } from '@/utils/settings';
|
||||
|
||||
export default {
|
||||
name: 'RandomPicker',
|
||||
props: {
|
||||
studentList: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
attendance: {
|
||||
type: Object,
|
||||
required: true,
|
||||
default: () => ({ absent: [], late: [], exclude: [] })
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
dialog: false,
|
||||
count: getSetting('randomPicker.defaultCount'),
|
||||
isPickingStarted: false,
|
||||
isAnimating: false,
|
||||
pickedStudents: [],
|
||||
animationStudents: [],
|
||||
highlightedIndices: [],
|
||||
animationTimer: null,
|
||||
getSetting,
|
||||
// 添加临时过滤选项
|
||||
tempFilters: {
|
||||
excludeAbsent: getSetting('randomPicker.excludeAbsent'),
|
||||
excludeLate: getSetting('randomPicker.excludeLate'),
|
||||
excludeExcluded: getSetting('randomPicker.excludeExcluded')
|
||||
}
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
// 计算请假、迟到、不参与的学生数量
|
||||
absentCount() {
|
||||
return this.attendance.absent ? this.attendance.absent.length : 0;
|
||||
},
|
||||
lateCount() {
|
||||
return this.attendance.late ? this.attendance.late.length : 0;
|
||||
},
|
||||
excludedCount() {
|
||||
return this.attendance.exclude ? this.attendance.exclude.length : 0;
|
||||
},
|
||||
|
||||
// 使用临时过滤选项过滤学生
|
||||
filteredStudents() {
|
||||
if (!this.studentList || !this.studentList.length) return [];
|
||||
|
||||
return this.studentList.filter(student => {
|
||||
// 根据临时过滤选项过滤学生
|
||||
if (this.tempFilters.excludeAbsent && this.attendance.absent.includes(student)) {
|
||||
return false;
|
||||
}
|
||||
if (this.tempFilters.excludeLate && this.attendance.late.includes(student)) {
|
||||
return false;
|
||||
}
|
||||
if (this.tempFilters.excludeExcluded && this.attendance.exclude.includes(student)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
},
|
||||
|
||||
// 兼容性:保留原有的 availableStudents 计算属性,但使用新的过滤逻辑
|
||||
availableStudents() {
|
||||
return this.filteredStudents;
|
||||
},
|
||||
|
||||
maxAllowedCount() {
|
||||
return Math.min(10, this.filteredStudents.length);
|
||||
},
|
||||
|
||||
// 计算剩余可用学生(排除已抽取的学生)
|
||||
remainingStudents() {
|
||||
return this.filteredStudents.filter(student => !this.pickedStudents.includes(student));
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
dialog(newVal) {
|
||||
if (newVal) {
|
||||
// 打开对话框时重置状态
|
||||
this.count = getSetting('randomPicker.defaultCount');
|
||||
this.isPickingStarted = false;
|
||||
this.isAnimating = false;
|
||||
this.pickedStudents = [];
|
||||
|
||||
// 重置临时过滤选项为设置中的值
|
||||
this.tempFilters = {
|
||||
excludeAbsent: getSetting('randomPicker.excludeAbsent'),
|
||||
excludeLate: getSetting('randomPicker.excludeLate'),
|
||||
excludeExcluded: getSetting('randomPicker.excludeExcluded')
|
||||
};
|
||||
} else {
|
||||
// 关闭对话框时清除计时器
|
||||
if (this.animationTimer) {
|
||||
clearTimeout(this.animationTimer);
|
||||
this.animationTimer = null;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 监听过滤选项变化,确保count不超过可用学生数
|
||||
'tempFilters': {
|
||||
handler() {
|
||||
if (this.count > this.maxAllowedCount) {
|
||||
this.count = Math.max(1, this.maxAllowedCount);
|
||||
}
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
open() {
|
||||
this.dialog = true;
|
||||
},
|
||||
incrementCount() {
|
||||
if (this.count < this.maxAllowedCount) {
|
||||
this.count++;
|
||||
}
|
||||
},
|
||||
decrementCount() {
|
||||
if (this.count > 1) {
|
||||
this.count--;
|
||||
}
|
||||
},
|
||||
startPicking() {
|
||||
if (this.filteredStudents.length === 0) return;
|
||||
|
||||
this.isPickingStarted = true;
|
||||
|
||||
if (getSetting('randomPicker.animation')) {
|
||||
this.startAnimation();
|
||||
} else {
|
||||
this.finishPicking();
|
||||
}
|
||||
},
|
||||
startAnimation() {
|
||||
this.isAnimating = true;
|
||||
|
||||
// 创建动画用的学生列表(添加ID以便于动画)
|
||||
this.animationStudents = this.filteredStudents.map((name, index) => ({
|
||||
id: `student-${index}`,
|
||||
name
|
||||
}));
|
||||
|
||||
// 随机高亮显示
|
||||
this.animateHighlight();
|
||||
},
|
||||
animateHighlight() {
|
||||
const totalSteps = 5; // 动画总步数
|
||||
let currentStep = 0;
|
||||
const intervalTime = 50; // 初始间隔时间
|
||||
|
||||
const animate = () => {
|
||||
// 清除之前的高亮
|
||||
this.highlightedIndices = [];
|
||||
|
||||
// 随机选择要高亮的索引
|
||||
const indices = [];
|
||||
for (let i = 0; i < this.count; i++) {
|
||||
let randomIndex;
|
||||
do {
|
||||
randomIndex = Math.floor(Math.random() * this.animationStudents.length);
|
||||
} while (indices.includes(randomIndex));
|
||||
indices.push(randomIndex);
|
||||
}
|
||||
|
||||
this.highlightedIndices = indices;
|
||||
|
||||
currentStep++;
|
||||
|
||||
// 逐渐增加间隔时间,使动画变慢
|
||||
const nextInterval = intervalTime + (currentStep * 20);
|
||||
|
||||
if (currentStep < totalSteps) {
|
||||
this.animationTimer = setTimeout(animate, nextInterval);
|
||||
} else {
|
||||
// 动画结束,显示最终结果
|
||||
setTimeout(() => {
|
||||
this.finishPicking();
|
||||
}, 500);
|
||||
}
|
||||
};
|
||||
|
||||
// 开始动画
|
||||
animate();
|
||||
},
|
||||
finishPicking() {
|
||||
this.isAnimating = false;
|
||||
|
||||
// 随机选择学生
|
||||
const shuffled = [...this.filteredStudents].sort(() => 0.5 - Math.random());
|
||||
this.pickedStudents = shuffled.slice(0, this.count);
|
||||
},
|
||||
resetPicker() {
|
||||
this.isPickingStarted = false;
|
||||
this.isAnimating = false;
|
||||
this.pickedStudents = [];
|
||||
if (this.animationTimer) {
|
||||
clearTimeout(this.animationTimer);
|
||||
this.animationTimer = null;
|
||||
}
|
||||
},
|
||||
// 刷新单个学生
|
||||
refreshSingleStudent(index) {
|
||||
if (this.remainingStudents.length === 0) return;
|
||||
|
||||
// 从剩余学生中随机选择一个
|
||||
const randomIndex = Math.floor(Math.random() * this.remainingStudents.length);
|
||||
const newStudent = this.remainingStudents[randomIndex];
|
||||
|
||||
// 替换指定位置的学生
|
||||
this.pickedStudents[index] = newStudent;
|
||||
|
||||
// 添加动画效果
|
||||
const resultCards = document.querySelectorAll('.result-card');
|
||||
if (resultCards[index]) {
|
||||
resultCards[index].classList.add('refresh-animation');
|
||||
setTimeout(() => {
|
||||
resultCards[index].classList.remove('refresh-animation');
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.random-picker-card {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.counter-container {
|
||||
margin: 2rem 0;
|
||||
}
|
||||
|
||||
.counter-btn {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.count-display {
|
||||
min-width: 100px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.start-btn {
|
||||
min-width: 200px;
|
||||
height: 64px;
|
||||
border-radius: 32px;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
// 过滤选项卡片样式
|
||||
.filter-options-card {
|
||||
max-width: 450px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.filter-chip {
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
}
|
||||
|
||||
// 学生列表提示框样式
|
||||
.student-list-tooltip {
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
margin-top: 5px;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.animation-container {
|
||||
min-height: 300px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.animation-wrapper {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.shuffle-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.student-item {
|
||||
padding: 10px 15px;
|
||||
background-color: rgba(var(--v-theme-surface-variant), 0.7);
|
||||
border-radius: 8px;
|
||||
transition: all 0.3s ease;
|
||||
font-size: 1.2rem;
|
||||
|
||||
&.highlighted {
|
||||
background-color: rgb(var(--v-theme-primary));
|
||||
color: rgb(var(--v-theme-on-primary));
|
||||
transform: scale(1.1);
|
||||
font-weight: bold;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
.result-container {
|
||||
min-height: 300px;
|
||||
}
|
||||
|
||||
.result-card {
|
||||
max-width: 400px;
|
||||
margin: 0 auto;
|
||||
transition: transform 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-4px);
|
||||
|
||||
.refresh-btn {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.refresh-btn {
|
||||
opacity: 0.7;
|
||||
transition: opacity 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新动画
|
||||
@keyframes refresh-pulse {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
box-shadow: 0 0 0 rgba(var(--v-theme-primary), 0.5);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 0 15px rgba(var(--v-theme-primary), 0.7);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: scale(1);
|
||||
box-shadow: 0 0 0 rgba(var(--v-theme-primary), 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
.refresh-animation {
|
||||
animation: refresh-pulse 0.5s ease;
|
||||
}
|
||||
|
||||
// 动画效果
|
||||
.shuffle-enter-active,
|
||||
.shuffle-leave-active {
|
||||
transition: all 0.5s ease;
|
||||
}
|
||||
|
||||
.shuffle-enter-from,
|
||||
.shuffle-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
}
|
||||
|
||||
.shuffle-move {
|
||||
transition: transform 0.5s ease;
|
||||
}
|
||||
|
||||
// 触摸屏优化
|
||||
@media (hover: none) {
|
||||
|
||||
.counter-btn,
|
||||
.start-btn {
|
||||
min-height: 72px;
|
||||
}
|
||||
|
||||
.student-item {
|
||||
padding: 12px 20px;
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
|
||||
.refresh-btn {
|
||||
opacity: 1;
|
||||
min-width: 36px;
|
||||
min-height: 36px;
|
||||
}
|
||||
|
||||
.filter-chip {
|
||||
min-height: 40px;
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,80 @@
|
||||
<template>
|
||||
<v-card :border="border" class="setting-group">
|
||||
<v-card-title v-if="title" class="d-flex align-center">
|
||||
<v-icon v-if="icon" :icon="icon" class="mr-2" />
|
||||
{{ title }}
|
||||
</v-card-title>
|
||||
|
||||
<v-card-subtitle v-if="description">
|
||||
{{ description }}
|
||||
</v-card-subtitle>
|
||||
|
||||
<v-card-text>
|
||||
<v-list>
|
||||
<slot>
|
||||
<!-- 默认插槽用于放置 SettingItem 组件 -->
|
||||
</slot>
|
||||
</v-list>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions v-if="$slots.actions">
|
||||
<slot name="actions"></slot>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'SettingGroup',
|
||||
|
||||
props: {
|
||||
/**
|
||||
* 设置组的标题
|
||||
*/
|
||||
title: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
|
||||
/**
|
||||
* 设置组的描述
|
||||
*/
|
||||
description: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
|
||||
/**
|
||||
* 设置组的图标
|
||||
*/
|
||||
icon: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
|
||||
/**
|
||||
* 是否显示边框
|
||||
*/
|
||||
border: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
onSettingUpdate(key, value) {
|
||||
this.$emit('update', key, value);
|
||||
},
|
||||
|
||||
onSettingError(key) {
|
||||
this.$emit('error', key);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.setting-group {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,457 @@
|
||||
<template>
|
||||
<v-list-item class="setting-item" :disabled="disabled">
|
||||
<template #prepend>
|
||||
<v-icon :icon="settingIcon" />
|
||||
</template>
|
||||
|
||||
<v-list-item-title class="text-wrap">
|
||||
{{ displayTitle }}
|
||||
</v-list-item-title>
|
||||
|
||||
<v-list-item-subtitle class="d-flex align-center text-wrap">
|
||||
<span class="text-caption text-grey-darken-1">{{ settingKey }}</span>
|
||||
</v-list-item-subtitle>
|
||||
|
||||
<template #append >
|
||||
<div class="d-flex flex-column flex-sm-row align-center">
|
||||
<div v-if="type !== 'string' || hasOptions" class="me-2">
|
||||
<!-- 根据设置类型渲染不同的控件 -->
|
||||
<v-switch v-if="type === 'boolean'" v-model="localValue" density="comfortable" hide-details :disabled="disabled"
|
||||
@update:model-value="updateSetting" />
|
||||
|
||||
<v-select v-else-if="type === 'string' && hasOptions" v-model="localValue" :items="selectOptions"
|
||||
density="compact" hide-details :disabled="disabled" class="setting-select" variant="outlined" bg-color="surface"
|
||||
@update:model-value="updateSetting" item-title="title" item-value="value" />
|
||||
|
||||
<div v-else-if="type === 'number'" class="d-flex align-center">
|
||||
<v-btn icon="mdi-minus" size="small" variant="text" :disabled="disabled || localValue <= minValue"
|
||||
@click="adjustValue(-stepValue)" />
|
||||
|
||||
<v-text-field v-model.number="localValue" type="number" density="compact" hide-details :min="minValue"
|
||||
:max="maxValue" :step="stepValue" :disabled="disabled" class="mx-2 setting-number-field" style="width: 80px"
|
||||
variant="outlined" bg-color="surface" @update:model-value="updateSetting" />
|
||||
|
||||
<v-btn icon="mdi-plus" size="small" variant="text" :disabled="disabled || localValue >= maxValue"
|
||||
@click="adjustValue(stepValue)" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<v-menu location="bottom">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn icon="mdi-dots-vertical" size="small" variant="text" v-bind="props" class="ml-2"
|
||||
:disabled="disabled" />
|
||||
</template>
|
||||
<v-list density="compact">
|
||||
<v-list-item @click="copySettingId">
|
||||
<template v-slot:prepend>
|
||||
<v-icon icon="mdi-key" size="small" />
|
||||
</template>
|
||||
<v-list-item-title>复制设置ID</v-list-item-title>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item @click="copySettingValue">
|
||||
<template v-slot:prepend>
|
||||
<v-icon icon="mdi-content-copy" size="small" />
|
||||
</template>
|
||||
<v-list-item-title>复制设置值</v-list-item-title>
|
||||
</v-list-item>
|
||||
|
||||
<v-divider></v-divider>
|
||||
|
||||
<v-list-item @click="resetToDefault" :disabled="isDefaultValue">
|
||||
<template v-slot:prepend>
|
||||
<v-icon icon="mdi-restore" size="small" />
|
||||
</template>
|
||||
<v-list-item-title>重置为默认值</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</div>
|
||||
</template>
|
||||
</v-list-item>
|
||||
|
||||
<!-- 文本框显示在下方 -->
|
||||
<div v-if="type === 'string' && !hasOptions" class="px-4 pb-2 pt-0">
|
||||
<v-text-field v-model="localValue" density="compact" hide-details :disabled="disabled"
|
||||
class="setting-text-field mt-1" variant="outlined" bg-color="surface" @update:model-value="updateSetting" />
|
||||
</div>
|
||||
|
||||
<!-- 消息提示 -->
|
||||
<v-snackbar v-model="showSnackbar" :timeout="2000" color="success" location="top">
|
||||
{{ snackbarText }}
|
||||
</v-snackbar>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getSetting, setSetting, getSettingDefinition, resetSetting } from '@/utils/settings';
|
||||
|
||||
export default {
|
||||
name: 'SettingItem',
|
||||
|
||||
props: {
|
||||
/**
|
||||
* 设置项的键名,例如 'display.cardHoverEffect'
|
||||
*/
|
||||
settingKey: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
|
||||
/**
|
||||
* 可选的自定义图标,如果不提供则使用默认图标
|
||||
*/
|
||||
icon: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
|
||||
/**
|
||||
* 是否禁用此设置项
|
||||
*/
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
|
||||
/**
|
||||
* 可选的自定义标题,如果不提供则使用定义中的描述或键名
|
||||
*/
|
||||
title: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
|
||||
/**
|
||||
* 可选的自定义描述,如果不提供则不显示描述
|
||||
*/
|
||||
description: {
|
||||
type: String,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
localValue: null,
|
||||
definition: null,
|
||||
type: null,
|
||||
selectOptions: [],
|
||||
hasOptions: false,
|
||||
minValue: 0,
|
||||
maxValue: 100,
|
||||
stepValue: 1,
|
||||
showSnackbar: false,
|
||||
snackbarText: '',
|
||||
fontFamilies: [
|
||||
{ title: 'Arial', value: 'Arial, sans-serif' },
|
||||
{ title: 'Calibri', value: 'Calibri, sans-serif' },
|
||||
{ title: 'Cambria', value: 'Cambria, serif' },
|
||||
{ title: 'Consolas', value: 'Consolas, monospace' },
|
||||
{ title: 'Courier New', value: 'Courier New, monospace' },
|
||||
{ title: 'Georgia', value: 'Georgia, serif' },
|
||||
{ title: 'Helvetica', value: 'Helvetica, sans-serif' },
|
||||
{ title: 'Segoe UI', value: 'Segoe UI, sans-serif' },
|
||||
{ title: 'Times New Roman', value: 'Times New Roman, serif' },
|
||||
{ title: 'Trebuchet MS', value: 'Trebuchet MS, sans-serif' },
|
||||
{ title: 'Verdana', value: 'Verdana, sans-serif' },
|
||||
{ title: 'Monospace', value: 'monospace' },
|
||||
{ title: 'Sans-serif', value: 'sans-serif' },
|
||||
{ title: 'Serif', value: 'serif' }
|
||||
],
|
||||
// 设置项的显示名称映射
|
||||
displayValueMappings: {
|
||||
'display.emptySubjectDisplay': {
|
||||
'card': '卡片',
|
||||
'button': '按钮'
|
||||
},
|
||||
'theme.mode': {
|
||||
'light': '浅色',
|
||||
'dark': '深色'
|
||||
},
|
||||
'server.provider': {
|
||||
'server': '远程服务器',
|
||||
'indexedDB': '本地存储'
|
||||
}
|
||||
},
|
||||
// 默认图标映射,按设置类型
|
||||
defaultIcons: {
|
||||
'boolean': 'mdi-toggle-switch-outline',
|
||||
'number': 'mdi-numeric',
|
||||
'string': 'mdi-form-textbox'
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
displayTitle() {
|
||||
// 优先使用自定义标题
|
||||
if (this.title) {
|
||||
return this.title;
|
||||
}
|
||||
|
||||
// 其次使用定义中的描述
|
||||
if (this.definition && this.definition.description) {
|
||||
return this.definition.description;
|
||||
}
|
||||
|
||||
// 最后使用键名的最后一部分
|
||||
const parts = this.settingKey.split('.');
|
||||
return parts[parts.length - 1];
|
||||
},
|
||||
|
||||
displayDescription() {
|
||||
// 优先使用自定义描述
|
||||
if (this.description) {
|
||||
return this.description;
|
||||
}
|
||||
|
||||
// 否则使用设置键名
|
||||
return this.settingKey;
|
||||
},
|
||||
|
||||
// 判断是否为字体系列设置
|
||||
isFontFamily() {
|
||||
return this.settingKey.toLowerCase().includes('fontfamily') ||
|
||||
this.settingKey.toLowerCase().includes('font.family');
|
||||
},
|
||||
|
||||
// 判断当前值是否为默认值
|
||||
isDefaultValue() {
|
||||
if (!this.definition) return true;
|
||||
|
||||
// 对于对象或数组,需要进行深度比较
|
||||
if (typeof this.localValue === 'object' && this.localValue !== null) {
|
||||
return JSON.stringify(this.localValue) === JSON.stringify(this.definition.default);
|
||||
}
|
||||
|
||||
return this.localValue === this.definition.default;
|
||||
},
|
||||
|
||||
// 获取设置项的图标
|
||||
settingIcon() {
|
||||
// 优先使用props中传入的图标
|
||||
if (this.icon) {
|
||||
return this.icon;
|
||||
}
|
||||
|
||||
// 其次使用定义中的图标
|
||||
if (this.definition && this.definition.icon) {
|
||||
return this.definition.icon;
|
||||
}
|
||||
|
||||
// 最后使用基于类型的默认图标
|
||||
return this.defaultIcons[this.type] || 'mdi-cog-outline';
|
||||
}
|
||||
},
|
||||
|
||||
created() {
|
||||
this.loadSetting();
|
||||
},
|
||||
|
||||
methods: {
|
||||
loadSetting() {
|
||||
// 获取设置定义
|
||||
this.definition = getSettingDefinition(this.settingKey);
|
||||
|
||||
if (!this.definition) {
|
||||
console.error(`未找到设置项定义: ${this.settingKey}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 设置类型
|
||||
this.type = this.definition.type;
|
||||
|
||||
// 加载设置值
|
||||
this.localValue = getSetting(this.settingKey);
|
||||
|
||||
// 处理选项(对于字符串类型的设置)
|
||||
if (this.type === 'string') {
|
||||
// 检查是否有字体设置
|
||||
if (this.isFontFamily) {
|
||||
this.selectOptions = this.fontFamilies;
|
||||
this.hasOptions = true;
|
||||
}
|
||||
// 检查是否有显示值映射
|
||||
else if (this.settingKey in this.displayValueMappings) {
|
||||
const mapping = this.displayValueMappings[this.settingKey];
|
||||
this.selectOptions = Object.entries(mapping).map(([value, title]) => ({
|
||||
title,
|
||||
value
|
||||
}));
|
||||
this.hasOptions = true;
|
||||
}
|
||||
// 检查是否有验证函数中的选项
|
||||
else if (this.definition.validate) {
|
||||
const validateStr = this.definition.validate.toString();
|
||||
const match = validateStr.match(/\[(.*?)\]/);
|
||||
|
||||
if (match) {
|
||||
const optionsStr = match[1];
|
||||
const options = optionsStr.split(',').map(opt => {
|
||||
const cleaned = opt.trim().replace(/['"]/g, '');
|
||||
// 检查是否有显示值映射
|
||||
const displayValue = this.getDisplayValue(cleaned);
|
||||
return {
|
||||
title: displayValue || cleaned,
|
||||
value: cleaned
|
||||
};
|
||||
});
|
||||
|
||||
if (options.length > 0) {
|
||||
this.selectOptions = options;
|
||||
this.hasOptions = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 处理数字类型的设置
|
||||
if (this.type === 'number' && this.definition.validate) {
|
||||
const validateStr = this.definition.validate.toString();
|
||||
|
||||
// 尝试提取最小值
|
||||
const minMatch = validateStr.match(/value\s*>=\s*(\d+)/);
|
||||
if (minMatch) {
|
||||
this.minValue = Number(minMatch[1]);
|
||||
}
|
||||
|
||||
// 尝试提取最大值
|
||||
const maxMatch = validateStr.match(/value\s*<=\s*(\d+)/);
|
||||
if (maxMatch) {
|
||||
this.maxValue = Number(maxMatch[1]);
|
||||
}
|
||||
|
||||
// 设置合理的步长
|
||||
const range = this.maxValue - this.minValue;
|
||||
if (range > 100) {
|
||||
this.stepValue = 10;
|
||||
} else if (range > 20) {
|
||||
this.stepValue = 5;
|
||||
} else if (range > 10) {
|
||||
this.stepValue = 2;
|
||||
} else {
|
||||
this.stepValue = 1;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 获取设置值的显示名称
|
||||
getDisplayValue(value) {
|
||||
if (this.settingKey in this.displayValueMappings) {
|
||||
const mapping = this.displayValueMappings[this.settingKey];
|
||||
return mapping[value] || value;
|
||||
}
|
||||
return value;
|
||||
},
|
||||
|
||||
updateSetting(value) {
|
||||
// 确保值的类型正确
|
||||
let typedValue = value;
|
||||
|
||||
if (this.type === 'boolean') {
|
||||
typedValue = Boolean(value);
|
||||
} else if (this.type === 'number') {
|
||||
typedValue = Number(value);
|
||||
|
||||
// 确保值在范围内
|
||||
if (typedValue < this.minValue) typedValue = this.minValue;
|
||||
if (typedValue > this.maxValue) typedValue = this.maxValue;
|
||||
}
|
||||
|
||||
// 保存设置
|
||||
const success = setSetting(this.settingKey, typedValue);
|
||||
|
||||
if (success) {
|
||||
this.$emit('update', this.settingKey, typedValue);
|
||||
} else {
|
||||
// 如果保存失败,恢复原值
|
||||
this.localValue = getSetting(this.settingKey);
|
||||
this.$emit('error', this.settingKey);
|
||||
}
|
||||
},
|
||||
|
||||
adjustValue(amount) {
|
||||
if (this.type !== 'number') return;
|
||||
|
||||
const newValue = this.localValue + amount;
|
||||
|
||||
if (newValue >= this.minValue && newValue <= this.maxValue) {
|
||||
this.localValue = newValue;
|
||||
this.updateSetting(newValue);
|
||||
}
|
||||
},
|
||||
|
||||
// 复制设置ID到剪贴板
|
||||
copySettingId() {
|
||||
navigator.clipboard.writeText(this.settingKey)
|
||||
.then(() => {
|
||||
this.showSnackbarMessage('设置ID已复制到剪贴板');
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('复制失败:', err);
|
||||
});
|
||||
},
|
||||
|
||||
// 复制设置值到剪贴板
|
||||
copySettingValue() {
|
||||
let valueText = '';
|
||||
|
||||
if (typeof this.localValue === 'object' && this.localValue !== null) {
|
||||
valueText = JSON.stringify(this.localValue);
|
||||
} else {
|
||||
valueText = String(this.localValue);
|
||||
}
|
||||
|
||||
navigator.clipboard.writeText(valueText)
|
||||
.then(() => {
|
||||
this.showSnackbarMessage('设置值已复制到剪贴板');
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('复制失败:', err);
|
||||
});
|
||||
},
|
||||
|
||||
// 重置为默认值
|
||||
resetToDefault() {
|
||||
if (!this.definition) return;
|
||||
|
||||
resetSetting(this.settingKey);
|
||||
this.localValue = getSetting(this.settingKey);
|
||||
this.showSnackbarMessage('已重置为默认值');
|
||||
this.$emit('update', this.settingKey, this.localValue);
|
||||
},
|
||||
|
||||
// 显示消息提示
|
||||
showSnackbarMessage(message) {
|
||||
this.snackbarText = message;
|
||||
this.showSnackbar = true;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.setting-item {
|
||||
border-radius: 8px;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.setting-text-field,
|
||||
.setting-select,
|
||||
.setting-number-field {
|
||||
min-width: 180px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.text-wrap {
|
||||
white-space: normal;
|
||||
overflow-wrap: break-word;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.setting-item {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,148 @@
|
||||
<template>
|
||||
<div class="settings-explorer">
|
||||
|
||||
|
||||
<div >
|
||||
<v-text-field v-model="searchQuery" label="搜索设置" prepend-inner-icon="mdi-magnify" clearable variant="outlined"
|
||||
density="comfortable" class="mb-4" />
|
||||
|
||||
|
||||
|
||||
<v-list>
|
||||
<div v-for="setting in allSettings" :key="setting.key">
|
||||
<setting-item :key="setting.key" :setting-key="setting.key"
|
||||
:disabled="setting.requireDeveloper && !isDeveloperMode" @update="onSettingUpdate" @error="onSettingError" />
|
||||
<v-divider class="my-2" />
|
||||
</div>
|
||||
</v-list><v-card border>
|
||||
<v-card-title class="text-subtitle-1">当前配置</v-card-title>
|
||||
<v-card-text>
|
||||
<pre class="settings-json">{{ formattedSettings }}</pre>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn @click="copySettingsToClipboard">
|
||||
复制到剪贴板
|
||||
<v-icon right>mdi-content-copy</v-icon>
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getSetting, settingsDefinitions, exportSettingsAsKeyValue, watchSettings } from '@/utils/settings';
|
||||
import SettingItem from './SettingItem.vue';
|
||||
|
||||
export default {
|
||||
name: 'SettingsExplorer',
|
||||
|
||||
components: {
|
||||
SettingItem
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
searchQuery: '',
|
||||
currentSettings: {},
|
||||
unwatchFunction: null,
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
isDeveloperMode() {
|
||||
return getSetting('developer.enabled');
|
||||
},
|
||||
|
||||
allSettings() {
|
||||
const settings = [];
|
||||
|
||||
for (const [key, definition] of Object.entries(settingsDefinitions)) {
|
||||
// 如果有搜索查询,过滤结果
|
||||
if (this.searchQuery && !key.toLowerCase().includes(this.searchQuery.toLowerCase()) &&
|
||||
!definition.description?.toLowerCase().includes(this.searchQuery.toLowerCase())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
settings.push({
|
||||
key,
|
||||
...definition
|
||||
});
|
||||
}
|
||||
|
||||
return settings;
|
||||
},
|
||||
|
||||
formattedSettings() {
|
||||
return JSON.stringify(this.currentSettings, null, 2);
|
||||
}
|
||||
},
|
||||
|
||||
created() {
|
||||
// 初始化当前设置
|
||||
this.updateCurrentSettings();
|
||||
|
||||
// 监听设置变化
|
||||
this.unwatchFunction = watchSettings(() => {
|
||||
this.updateCurrentSettings();
|
||||
});
|
||||
},
|
||||
|
||||
beforeUnmount() {
|
||||
// 组件销毁前取消监听
|
||||
if (this.unwatchFunction) {
|
||||
this.unwatchFunction();
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
updateCurrentSettings() {
|
||||
this.currentSettings = exportSettingsAsKeyValue();
|
||||
},
|
||||
|
||||
onSettingUpdate(key, value) {
|
||||
this.$emit('update', key, value);
|
||||
// 设置更新后立即更新当前设置显示
|
||||
this.updateCurrentSettings();
|
||||
},
|
||||
|
||||
onSettingError(key) {
|
||||
this.$emit('error', key);
|
||||
},
|
||||
|
||||
copySettingsToClipboard() {
|
||||
navigator.clipboard.writeText(JSON.stringify(this.currentSettings))
|
||||
.then(() => {
|
||||
// 可以添加一个提示,表示复制成功
|
||||
this.$emit('message', { type: 'success', text: '设置已复制到剪贴板' });
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('复制到剪贴板失败:', err);
|
||||
this.$emit('message', { type: 'error', text: '复制到剪贴板失败' });
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.settings-explorer {
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.settings-json {
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
padding: 12px;
|
||||
border-radius: 4px;
|
||||
overflow-x: auto;
|
||||
font-family: monospace;
|
||||
white-space: pre-wrap;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.v-theme--dark .settings-json {
|
||||
background-color: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
</style>
|
||||
@@ -144,7 +144,7 @@ export default {
|
||||
|
||||
async clearIndexedDB() {
|
||||
try {
|
||||
const DBName = "HomeworkDB";
|
||||
const DBName = "ClassworksDB";
|
||||
// 删除整个数据库
|
||||
await window.indexedDB.deleteDatabase(DBName);
|
||||
this.$message.success("清除成功", "数据库缓存已清除");
|
||||
@@ -156,7 +156,7 @@ export default {
|
||||
|
||||
async exportData() {
|
||||
try {
|
||||
const DBName = "HomeworkDB";
|
||||
const DBName = "ClassworksDB";
|
||||
const data = { indexedDB: {} };
|
||||
|
||||
// 打开数据库
|
||||
|
||||
@@ -1,260 +1,46 @@
|
||||
<template>
|
||||
<settings-card
|
||||
title="显示设置"
|
||||
icon="mdi-monitor"
|
||||
border
|
||||
>
|
||||
<v-form v-model="isValid" @submit.prevent="save">
|
||||
<settings-card title="显示设置" icon="mdi-monitor" border>
|
||||
<v-list>
|
||||
<v-list-item>
|
||||
<template #prepend>
|
||||
<v-icon icon="mdi-card-outline" class="mr-3" />
|
||||
</template>
|
||||
<v-list-item-title>空科目显示方式</v-list-item-title>
|
||||
<v-list-item-subtitle>选择空科目的显示方式</v-list-item-subtitle>
|
||||
<template #append>
|
||||
<v-select
|
||||
v-model="emptySubjectDisplay"
|
||||
:items="displayOptions"
|
||||
density="compact"
|
||||
hide-details
|
||||
variant="outlined"
|
||||
style="max-width: 150px"
|
||||
/>
|
||||
</template>
|
||||
</v-list-item>
|
||||
<setting-item :setting-key="'display.emptySubjectDisplay'" />
|
||||
|
||||
|
||||
<v-divider class="my-2" />
|
||||
|
||||
<v-list-item>
|
||||
<template #prepend>
|
||||
<v-icon icon="mdi-sort-variant" class="mr-3" />
|
||||
</template>
|
||||
<v-list-item-title>动态排序</v-list-item-title>
|
||||
<v-list-item-subtitle>优化卡片布局以提高显示效果</v-list-item-subtitle>
|
||||
<template #append>
|
||||
<v-switch
|
||||
v-model="dynamicSort"
|
||||
density="comfortable"
|
||||
hide-details
|
||||
/>
|
||||
</template>
|
||||
</v-list-item>
|
||||
<setting-item :setting-key="'display.dynamicSort'" />
|
||||
|
||||
<v-divider class="my-2" />
|
||||
|
||||
<v-list-item>
|
||||
<template #prepend>
|
||||
<v-icon icon="mdi-dice-multiple" class="mr-3" />
|
||||
</template>
|
||||
<v-list-item-title>显示随机按钮</v-list-item-title>
|
||||
<v-list-item-subtitle>在主页显示随机点名按钮</v-list-item-subtitle>
|
||||
<template #append>
|
||||
<v-switch
|
||||
v-model="showRandomButton"
|
||||
density="comfortable"
|
||||
hide-details
|
||||
/>
|
||||
</template>
|
||||
</v-list-item>
|
||||
<setting-item :setting-key="'display.showRandomButton'" />
|
||||
|
||||
<v-divider class="my-2" />
|
||||
|
||||
<v-list-item>
|
||||
<template #prepend>
|
||||
<v-icon icon="mdi-fullscreen" class="mr-3" />
|
||||
</template>
|
||||
<v-list-item-title>显示全屏按钮</v-list-item-title>
|
||||
<v-list-item-subtitle>在主页显示全屏切换按钮</v-list-item-subtitle>
|
||||
<template #append>
|
||||
<v-switch
|
||||
v-model="showFullscreenButton"
|
||||
density="comfortable"
|
||||
hide-details
|
||||
/>
|
||||
</template>
|
||||
</v-list-item>
|
||||
<setting-item :setting-key="'display.showFullscreenButton'" />
|
||||
|
||||
<v-divider class="my-2" />
|
||||
|
||||
<v-list-item>
|
||||
<template #prepend>
|
||||
<v-icon icon="mdi-cards-outline" class="mr-3" />
|
||||
</template>
|
||||
<v-list-item-title>卡片悬浮效果</v-list-item-title>
|
||||
<v-list-item-subtitle>启用卡片悬停时的动画效果</v-list-item-subtitle>
|
||||
<template #append>
|
||||
<v-switch
|
||||
v-model="cardHoverEffect"
|
||||
density="comfortable"
|
||||
hide-details
|
||||
/>
|
||||
</template>
|
||||
</v-list-item>
|
||||
<setting-item :setting-key="'display.cardHoverEffect'" />
|
||||
|
||||
<v-divider class="my-2" />
|
||||
|
||||
<v-list-item>
|
||||
<template #prepend>
|
||||
<v-icon icon="mdi-gesture-tap" class="mr-3" />
|
||||
</template>
|
||||
<v-list-item-title>增强触摸模式</v-list-item-title>
|
||||
<v-list-item-subtitle>优化触摸屏操作体验</v-list-item-subtitle>
|
||||
<template #append>
|
||||
<v-switch
|
||||
v-model="enhancedTouchMode"
|
||||
density="comfortable"
|
||||
hide-details
|
||||
/>
|
||||
</template>
|
||||
</v-list-item>
|
||||
<setting-item :setting-key="'display.enhancedTouchMode'" />
|
||||
|
||||
<v-divider class="my-2" />
|
||||
<setting-item :setting-key="'display.showAntiScreenBurnCard'" />
|
||||
|
||||
|
||||
<v-list-item>
|
||||
<template #prepend>
|
||||
<v-icon icon="mdi-shield-check" class="mr-3" />
|
||||
</template>
|
||||
<v-list-item-title>显示防烧屏提示</v-list-item-title>
|
||||
<v-list-item-subtitle>显示防烧屏技术提示卡片</v-list-item-subtitle>
|
||||
<template #append>
|
||||
<v-switch
|
||||
v-model="showAntiScreenBurnCard"
|
||||
density="comfortable"
|
||||
hide-details
|
||||
/>
|
||||
</template>
|
||||
</v-list-item>
|
||||
|
||||
<div class="d-flex gap-2 mt-4">
|
||||
<v-btn
|
||||
color="primary"
|
||||
type="submit"
|
||||
:disabled="!hasChanges || !isValid"
|
||||
prepend-icon="mdi-content-save"
|
||||
>
|
||||
保存更改
|
||||
</v-btn>
|
||||
<v-btn
|
||||
variant="outlined"
|
||||
@click="reset"
|
||||
:disabled="!hasChanges"
|
||||
>
|
||||
重置
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-list>
|
||||
</v-form>
|
||||
</settings-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import SettingsCard from '@/components/SettingsCard.vue';
|
||||
import { getSetting, setSetting } from '@/utils/settings';
|
||||
|
||||
import SettingItem from '@/components/settings/SettingItem.vue';
|
||||
export default {
|
||||
name: 'DisplaySettingsCard',
|
||||
components: { SettingsCard },
|
||||
components: { SettingsCard, SettingItem },
|
||||
data() {
|
||||
const settings = {
|
||||
emptySubjectDisplay: getSetting('display.emptySubjectDisplay'),
|
||||
dynamicSort: getSetting('display.dynamicSort'),
|
||||
showRandomButton: getSetting('display.showRandomButton'),
|
||||
showFullscreenButton: getSetting('display.showFullscreenButton'),
|
||||
cardHoverEffect: getSetting('display.cardHoverEffect'),
|
||||
enhancedTouchMode: getSetting('display.enhancedTouchMode'),
|
||||
showAntiScreenBurnCard: getSetting('display.showAntiScreenBurnCard')
|
||||
};
|
||||
|
||||
|
||||
return {
|
||||
localSettings: { ...settings },
|
||||
originalSettings: settings,
|
||||
isValid: true,
|
||||
displayOptions: [
|
||||
{ title: '卡片', value: 'card' },
|
||||
{ title: '按钮', value: 'button' }
|
||||
]
|
||||
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
hasChanges() {
|
||||
return JSON.stringify(this.localSettings) !== JSON.stringify(this.originalSettings);
|
||||
},
|
||||
emptySubjectDisplay: {
|
||||
get() {
|
||||
return this.localSettings.emptySubjectDisplay;
|
||||
},
|
||||
set(value) {
|
||||
this.localSettings.emptySubjectDisplay = value;
|
||||
this.$emit('saved');
|
||||
}
|
||||
},
|
||||
dynamicSort: {
|
||||
get() {
|
||||
return this.localSettings.dynamicSort;
|
||||
},
|
||||
set(value) {
|
||||
this.localSettings.dynamicSort = value;
|
||||
this.$emit('saved');
|
||||
}
|
||||
},
|
||||
showRandomButton: {
|
||||
get() {
|
||||
return this.localSettings.showRandomButton;
|
||||
},
|
||||
set(value) {
|
||||
this.localSettings.showRandomButton = value;
|
||||
this.$emit('saved');
|
||||
}
|
||||
},
|
||||
showFullscreenButton: {
|
||||
get() {
|
||||
return this.localSettings.showFullscreenButton;
|
||||
},
|
||||
set(value) {
|
||||
this.localSettings.showFullscreenButton = value;
|
||||
this.$emit('saved');
|
||||
}
|
||||
},
|
||||
cardHoverEffect: {
|
||||
get() {
|
||||
return this.localSettings.cardHoverEffect;
|
||||
},
|
||||
set(value) {
|
||||
this.localSettings.cardHoverEffect = value;
|
||||
this.$emit('saved');
|
||||
}
|
||||
},
|
||||
enhancedTouchMode: {
|
||||
get() {
|
||||
return this.localSettings.enhancedTouchMode;
|
||||
},
|
||||
set(value) {
|
||||
this.localSettings.enhancedTouchMode = value;
|
||||
this.$emit('saved');
|
||||
}
|
||||
},
|
||||
showAntiScreenBurnCard: {
|
||||
get() {
|
||||
return this.localSettings.showAntiScreenBurnCard;
|
||||
},
|
||||
set(value) {
|
||||
this.localSettings.showAntiScreenBurnCard = value;
|
||||
this.$emit('saved');
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
save() {
|
||||
Object.entries(this.localSettings).forEach(([key, value]) => {
|
||||
setSetting(`display.${key}`, value);
|
||||
});
|
||||
this.originalSettings = { ...this.localSettings };
|
||||
this.$emit('saved');
|
||||
},
|
||||
reset() {
|
||||
this.localSettings = { ...this.originalSettings };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,135 +1,27 @@
|
||||
<template>
|
||||
<settings-card
|
||||
title="编辑设置"
|
||||
icon="mdi-pencil-cog"
|
||||
>
|
||||
<v-form v-model="isValid" @submit.prevent="save">
|
||||
<v-list>
|
||||
<v-list-item>
|
||||
<template #prepend>
|
||||
<v-icon icon="mdi-content-save" class="mr-3" />
|
||||
</template>
|
||||
<v-list-item-title>自动保存</v-list-item-title>
|
||||
<v-list-item-subtitle>在编辑完成后自动保存到服务器</v-list-item-subtitle>
|
||||
<template #append>
|
||||
<v-switch
|
||||
v-model="localSettings.autoSave"
|
||||
density="comfortable"
|
||||
hide-details
|
||||
/>
|
||||
</template>
|
||||
</v-list-item>
|
||||
<settings-card title="编辑设置" icon="mdi-cog">
|
||||
<v-list>
|
||||
<setting-item :setting-key="'edit.autoSave'" />
|
||||
|
||||
<v-divider v-if="localSettings.autoSave" class="my-2" />
|
||||
|
||||
<v-list-item v-if="localSettings.autoSave">
|
||||
<template #prepend>
|
||||
<v-icon icon="mdi-calendar-lock" class="mr-3" />
|
||||
</template>
|
||||
<v-list-item-title>禁止自动保存非当天数据</v-list-item-title>
|
||||
<v-list-item-subtitle>仅允许自动保存当天的数据,避免误修改历史记录</v-list-item-subtitle>
|
||||
<template #append>
|
||||
<v-switch
|
||||
v-model="localSettings.blockNonTodayAutoSave"
|
||||
density="comfortable"
|
||||
hide-details
|
||||
/>
|
||||
</template>
|
||||
</v-list-item>
|
||||
<v-divider class="my-2" />
|
||||
<setting-item :setting-key="'edit.blockNonTodayAutoSave'" />
|
||||
|
||||
<v-divider class="my-2" />
|
||||
|
||||
<v-list-item>
|
||||
<template #prepend>
|
||||
<v-icon icon="mdi-calendar-alert" class="mr-3" />
|
||||
</template>
|
||||
<v-list-item-title>确认保存历史数据</v-list-item-title>
|
||||
<v-list-item-subtitle>保存非当天数据时显示确认对话框</v-list-item-subtitle>
|
||||
<template #append>
|
||||
<v-switch
|
||||
v-model="localSettings.confirmNonTodaySave"
|
||||
density="comfortable"
|
||||
hide-details
|
||||
/>
|
||||
</template>
|
||||
</v-list-item>
|
||||
<v-divider class="my-2" />
|
||||
<setting-item :setting-key="'edit.confirmNonTodaySave'" />
|
||||
|
||||
<v-divider class="my-2" />
|
||||
|
||||
<v-list-item>
|
||||
<template #prepend>
|
||||
<v-icon icon="mdi-refresh" class="mr-3" />
|
||||
</template>
|
||||
<v-list-item-title>编辑前刷新</v-list-item-title>
|
||||
<v-list-item-subtitle>在打开编辑框前从服务器获取最新数据</v-list-item-subtitle>
|
||||
<template #append>
|
||||
<v-switch
|
||||
v-model="localSettings.refreshBeforeEdit"
|
||||
density="comfortable"
|
||||
hide-details
|
||||
/>
|
||||
</template>
|
||||
</v-list-item>
|
||||
|
||||
<div class="d-flex gap-2 mt-4">
|
||||
<v-btn
|
||||
color="primary"
|
||||
type="submit"
|
||||
:disabled="!hasChanges || !isValid"
|
||||
prepend-icon="mdi-content-save"
|
||||
>
|
||||
保存更改
|
||||
</v-btn>
|
||||
<v-btn
|
||||
variant="outlined"
|
||||
@click="reset"
|
||||
:disabled="!hasChanges"
|
||||
>
|
||||
重置
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-list>
|
||||
</v-form>
|
||||
<v-divider class="my-2" />
|
||||
<setting-item :setting-key="'edit.refreshBeforeEdit'" />
|
||||
|
||||
</v-list>
|
||||
</settings-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import SettingsCard from '@/components/SettingsCard.vue';
|
||||
import { getSetting, setSetting } from '@/utils/settings';
|
||||
import SettingItem from '../SettingItem.vue';
|
||||
|
||||
export default {
|
||||
name: 'EditSettingsCard',
|
||||
components: { SettingsCard },
|
||||
data() {
|
||||
const settings = {
|
||||
autoSave: getSetting('edit.autoSave'),
|
||||
blockNonTodayAutoSave: getSetting('edit.blockNonTodayAutoSave'),
|
||||
confirmNonTodaySave: getSetting('edit.confirmNonTodaySave'),
|
||||
refreshBeforeEdit: getSetting('edit.refreshBeforeEdit')
|
||||
};
|
||||
|
||||
return {
|
||||
localSettings: { ...settings },
|
||||
originalSettings: settings,
|
||||
isValid: true
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
hasChanges() {
|
||||
return JSON.stringify(this.localSettings) !== JSON.stringify(this.originalSettings);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
save() {
|
||||
Object.entries(this.localSettings).forEach(([key, value]) => {
|
||||
setSetting(`edit.${key}`, value);
|
||||
});
|
||||
this.originalSettings = { ...this.localSettings };
|
||||
this.$emit('saved');
|
||||
},
|
||||
reset() {
|
||||
this.localSettings = { ...this.originalSettings };
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
<template>
|
||||
<v-card border class="mb-4">
|
||||
<v-card-title class="d-flex align-center">
|
||||
<v-icon icon="mdi-account-question" class="mr-2" />
|
||||
随机点名设置
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text>
|
||||
<v-switch
|
||||
v-model="randomPickerEnabled"
|
||||
label="启用随机点名功能"
|
||||
color="primary"
|
||||
hide-details
|
||||
class="mb-4"
|
||||
/>
|
||||
|
||||
<v-switch
|
||||
v-model="randomPickerAnimation"
|
||||
label="启用随机点名动画效果"
|
||||
color="primary"
|
||||
hide-details
|
||||
class="mb-4"
|
||||
:disabled="!randomPickerEnabled"
|
||||
/>
|
||||
|
||||
<v-slider
|
||||
v-model="defaultCount"
|
||||
label="默认抽取人数"
|
||||
min="1"
|
||||
max="10"
|
||||
step="1"
|
||||
thumb-label
|
||||
:disabled="!randomPickerEnabled"
|
||||
class="mb-4"
|
||||
/>
|
||||
|
||||
<v-divider class="my-4" />
|
||||
|
||||
<div class="text-subtitle-1 mb-2">学生过滤设置</div>
|
||||
|
||||
<v-switch
|
||||
v-model="excludeAbsent"
|
||||
label="排除请假学生"
|
||||
color="primary"
|
||||
hide-details
|
||||
class="mb-2"
|
||||
:disabled="!randomPickerEnabled"
|
||||
/>
|
||||
|
||||
<v-switch
|
||||
v-model="excludeLate"
|
||||
label="排除迟到学生"
|
||||
color="primary"
|
||||
hide-details
|
||||
class="mb-2"
|
||||
:disabled="!randomPickerEnabled"
|
||||
/>
|
||||
|
||||
<v-switch
|
||||
v-model="excludeExcluded"
|
||||
label="排除不参与学生"
|
||||
color="primary"
|
||||
hide-details
|
||||
class="mb-2"
|
||||
:disabled="!randomPickerEnabled"
|
||||
/>
|
||||
|
||||
<v-alert
|
||||
v-if="randomPickerEnabled"
|
||||
type="info"
|
||||
variant="tonal"
|
||||
class="mt-4"
|
||||
density="compact"
|
||||
>
|
||||
随机点名功能将在主页显示一个按钮,点击后可以随机抽取学生。
|
||||
</v-alert>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getSetting, setSetting } from '@/utils/settings';
|
||||
|
||||
export default {
|
||||
name: 'RandomPickerSettingsCard',
|
||||
data() {
|
||||
return {
|
||||
randomPickerEnabled: getSetting('randomPicker.enabled'),
|
||||
randomPickerAnimation: getSetting('randomPicker.animation'),
|
||||
defaultCount: getSetting('randomPicker.defaultCount'),
|
||||
excludeAbsent: getSetting('randomPicker.excludeAbsent'),
|
||||
excludeLate: getSetting('randomPicker.excludeLate'),
|
||||
excludeExcluded: getSetting('randomPicker.excludeExcluded')
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
randomPickerEnabled(newValue) {
|
||||
setSetting('randomPicker.enabled', newValue);
|
||||
},
|
||||
randomPickerAnimation(newValue) {
|
||||
setSetting('randomPicker.animation', newValue);
|
||||
},
|
||||
defaultCount(newValue) {
|
||||
setSetting('randomPicker.defaultCount', newValue);
|
||||
},
|
||||
excludeAbsent(newValue) {
|
||||
setSetting('randomPicker.excludeAbsent', newValue);
|
||||
},
|
||||
excludeLate(newValue) {
|
||||
setSetting('randomPicker.excludeLate', newValue);
|
||||
},
|
||||
excludeExcluded(newValue) {
|
||||
setSetting('randomPicker.excludeExcluded', newValue);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -1,102 +1,28 @@
|
||||
<template>
|
||||
<settings-card
|
||||
title="刷新设置"
|
||||
icon="mdi-refresh-circle"
|
||||
>
|
||||
<v-form v-model="isValid" @submit.prevent="save">
|
||||
<settings-card title="刷新设置" icon="mdi-refresh-circle">
|
||||
<v-form>
|
||||
<v-list>
|
||||
<v-list-item>
|
||||
<template #prepend>
|
||||
<v-icon icon="mdi-refresh" class="mr-3" />
|
||||
</template>
|
||||
<v-list-item-title>自动刷新</v-list-item-title>
|
||||
<v-list-item-subtitle>在后台自动刷新数据</v-list-item-subtitle>
|
||||
<template #append>
|
||||
<v-switch
|
||||
v-model="localSettings.auto"
|
||||
density="comfortable"
|
||||
hide-details
|
||||
/>
|
||||
</template>
|
||||
</v-list-item>
|
||||
<setting-item setting-key="refresh.auto" title="自动刷新" /> <v-divider class="my-2" />
|
||||
|
||||
<v-divider class="my-2" />
|
||||
|
||||
<v-list-item>
|
||||
<template #prepend>
|
||||
<v-icon icon="mdi-timer" class="mr-3" />
|
||||
</template>
|
||||
<v-list-item-title>刷新间隔</v-list-item-title>
|
||||
<v-list-item-subtitle>设置自动刷新的时间间隔(分钟)</v-list-item-subtitle>
|
||||
<template #append>
|
||||
<v-text-field
|
||||
v-model="localSettings.interval"
|
||||
type="number"
|
||||
min="1"
|
||||
max="60"
|
||||
density="comfortable"
|
||||
hide-details
|
||||
/>
|
||||
</template>
|
||||
</v-list-item>
|
||||
|
||||
<div class="d-flex gap-2 mt-4">
|
||||
<v-btn
|
||||
color="primary"
|
||||
type="submit"
|
||||
:disabled="!hasChanges || !isValid"
|
||||
prepend-icon="mdi-content-save"
|
||||
>
|
||||
保存更改
|
||||
</v-btn>
|
||||
<v-btn
|
||||
variant="outlined"
|
||||
@click="reset"
|
||||
:disabled="!hasChanges"
|
||||
>
|
||||
重置
|
||||
</v-btn>
|
||||
</div>
|
||||
<setting-item setting-key="refresh.interval" title="刷新间隔" />
|
||||
</v-list>
|
||||
|
||||
</v-form>
|
||||
</settings-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import SettingsCard from '@/components/SettingsCard.vue';
|
||||
import { getSetting, setSetting } from '@/utils/settings';
|
||||
|
||||
import SettingItem from '@/components/settings/SettingItem.vue';
|
||||
export default {
|
||||
name: 'RefreshSettingsCard',
|
||||
components: { SettingsCard },
|
||||
data() {
|
||||
const settings = {
|
||||
auto: getSetting('refresh.auto'),
|
||||
interval: getSetting('refresh.interval')
|
||||
};
|
||||
|
||||
|
||||
return {
|
||||
localSettings: { ...settings },
|
||||
originalSettings: settings,
|
||||
isValid: true
|
||||
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
hasChanges() {
|
||||
return JSON.stringify(this.localSettings) !== JSON.stringify(this.originalSettings);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
save() {
|
||||
Object.entries(this.localSettings).forEach(([key, value]) => {
|
||||
setSetting(`refresh.${key}`, value);
|
||||
});
|
||||
this.originalSettings = { ...this.localSettings };
|
||||
this.$emit('saved');
|
||||
},
|
||||
reset() {
|
||||
this.localSettings = { ...this.originalSettings };
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,52 +1,19 @@
|
||||
<template>
|
||||
<settings-card title="数据源设置" icon="mdi-database" :loading="loading">
|
||||
<v-form v-model="isValid" @submit.prevent="save">
|
||||
<v-select
|
||||
v-model="localSettings.provider"
|
||||
:items="dataProviders"
|
||||
label="数据提供者"
|
||||
class="mb-4"
|
||||
/>
|
||||
<v-expand-transition>
|
||||
<div v-if="localSettings.provider === 'server'">
|
||||
<v-text-field
|
||||
v-model="localSettings.domain"
|
||||
label="服务器域名"
|
||||
placeholder="例如: http://example.com"
|
||||
prepend-inner-icon="mdi-web"
|
||||
/>
|
||||
</div>
|
||||
</v-expand-transition>
|
||||
<v-form>
|
||||
<setting-item setting-key="server.provider" title="数据提供者" />
|
||||
<v-divider class="my-2" />
|
||||
<setting-item setting-key="server.domain" title="服务器域名" /> <v-divider class="my-2" />
|
||||
|
||||
<v-text-field
|
||||
v-model="localSettings.classNumber"
|
||||
label="班号"
|
||||
placeholder="例如: 1 或 A"
|
||||
prepend-inner-icon="mdi-account-group"
|
||||
persistent-hint
|
||||
/>
|
||||
|
||||
<div class="d-flex gap-2 mt-4">
|
||||
<v-btn
|
||||
color="primary"
|
||||
type="submit"
|
||||
:disabled="!hasChanges || !isValid"
|
||||
prepend-icon="mdi-content-save"
|
||||
>
|
||||
保存更改
|
||||
</v-btn>
|
||||
<v-btn variant="outlined" @click="reset" :disabled="!hasChanges">
|
||||
重置
|
||||
</v-btn>
|
||||
</div>
|
||||
<setting-item setting-key="server.classNumber" title="班号" />
|
||||
|
||||
</v-form>
|
||||
</settings-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import SettingsCard from "@/components/SettingsCard.vue";
|
||||
import { getSetting, setSetting } from "@/utils/settings";
|
||||
|
||||
import SettingItem from "@/components/settings/SettingItem.vue";
|
||||
export default {
|
||||
name: "ServerSettingsCard",
|
||||
components: { SettingsCard },
|
||||
@@ -54,68 +21,12 @@ export default {
|
||||
loading: Boolean,
|
||||
},
|
||||
data() {
|
||||
const settings = {
|
||||
provider: getSetting("server.provider"),
|
||||
domain: getSetting("server.domain"),
|
||||
classNumber: getSetting("server.classNumber"),
|
||||
};
|
||||
|
||||
|
||||
return {
|
||||
localSettings: { ...settings },
|
||||
originalSettings: settings,
|
||||
isValid: true,
|
||||
dataProviders: [
|
||||
{ title: "服务器", value: "server" },
|
||||
{ title: "本地数据库", value: "indexedDB" },
|
||||
],
|
||||
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
hasChanges() {
|
||||
return (
|
||||
JSON.stringify(this.localSettings) !==
|
||||
JSON.stringify(this.originalSettings)
|
||||
);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async save() {
|
||||
try {
|
||||
// 先保存设置
|
||||
Object.entries(this.localSettings).forEach(([key, value]) => {
|
||||
const success = setSetting(`server.${key}`, value);
|
||||
if (!success) {
|
||||
throw new Error(`保存设置 ${key} 失败`);
|
||||
}
|
||||
});
|
||||
|
||||
// 如果切换到服务器模式,验证服务器连接
|
||||
if (this.localSettings.provider === 'server' && this.localSettings.domain) {
|
||||
const testUrl = `${this.localSettings.domain.replace(/\/$/, '')}/api/test`;
|
||||
try {
|
||||
const response = await fetch(testUrl);
|
||||
if (!response.ok) {
|
||||
throw new Error('服务器连接测试失败');
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error('无法连接到服务器,请检查域名设置');
|
||||
}
|
||||
}
|
||||
|
||||
this.originalSettings = { ...this.localSettings };
|
||||
this.$emit('saved');
|
||||
|
||||
// 延迟刷新页面,让用户看到保存成功的提示
|
||||
setTimeout(() => {
|
||||
//window.location.reload();
|
||||
}, 1000);
|
||||
} catch (error) {
|
||||
this.$message?.error('保存失败', error.message);
|
||||
}
|
||||
},
|
||||
reset() {
|
||||
this.localSettings = { ...this.originalSettings };
|
||||
},
|
||||
},
|
||||
|
||||
};
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user