mirror of
https://github.com/ZeroCatDev/Classworks.git
synced 2025-07-02 17:29:23 +00:00
1
This commit is contained in:
parent
ec46f6aca2
commit
10b7f3784f
268
src/components/MessageLog.vue
Normal file
268
src/components/MessageLog.vue
Normal file
@ -0,0 +1,268 @@
|
||||
<template>
|
||||
<!-- 侧边栏 -->
|
||||
<v-navigation-drawer
|
||||
v-model="drawer"
|
||||
location="right"
|
||||
temporary
|
||||
width="400"
|
||||
>
|
||||
<v-toolbar color="primary">
|
||||
<v-toolbar-title class="text-white">
|
||||
消息记录
|
||||
<v-chip
|
||||
v-if="unreadCount"
|
||||
color="error"
|
||||
size="small"
|
||||
class="ml-2"
|
||||
>
|
||||
{{ unreadCount }}
|
||||
</v-chip>
|
||||
</v-toolbar-title>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
icon="mdi-delete"
|
||||
variant="text"
|
||||
color="white"
|
||||
@click="clearMessages"
|
||||
/>
|
||||
</v-toolbar>
|
||||
|
||||
<v-list class="message-list">
|
||||
<template v-for="(msg, index) in visibleMessages" :key="msg.id">
|
||||
<v-list-item
|
||||
:class="{ 'unread': !msg.read }"
|
||||
class="message-item mb-2"
|
||||
@click="markAsRead(msg.id)"
|
||||
>
|
||||
<template #prepend>
|
||||
<v-icon
|
||||
:icon="getIcon(msg.type)"
|
||||
:color="getColor(msg.type)"
|
||||
size="small"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<v-list-item-title>{{ msg.title }}</v-list-item-title>
|
||||
<v-list-item-subtitle v-if="msg.content">
|
||||
{{ msg.content }}
|
||||
</v-list-item-subtitle>
|
||||
<v-list-item-subtitle class="text-caption">
|
||||
{{ formatTime(msg.timestamp) }}
|
||||
</v-list-item-subtitle>
|
||||
|
||||
<template #append>
|
||||
<v-btn
|
||||
icon="mdi-delete"
|
||||
variant="text"
|
||||
size="small"
|
||||
@click.stop="deleteMessage(msg.id)"
|
||||
/>
|
||||
</template>
|
||||
</v-list-item>
|
||||
<v-divider v-if="index < visibleMessages.length - 1" />
|
||||
</template>
|
||||
|
||||
<v-btn
|
||||
v-if="hasMoreMessages"
|
||||
block
|
||||
variant="text"
|
||||
@click="loadMoreMessages"
|
||||
>
|
||||
加载更多
|
||||
</v-btn>
|
||||
</v-list>
|
||||
</v-navigation-drawer>
|
||||
|
||||
<!-- 消息提示组 -->
|
||||
<div class="message-container">
|
||||
<TransitionGroup name="message">
|
||||
<v-alert
|
||||
v-for="msg in activeMessages"
|
||||
:key="msg.id"
|
||||
:type="msg.type"
|
||||
variant="tonal"
|
||||
closable
|
||||
class="message-alert mb-2"
|
||||
@click:close="removeActiveMessage(msg.id)"
|
||||
>
|
||||
<div class="d-flex align-center">
|
||||
<span class="font-weight-medium">{{ msg.title }}</span>
|
||||
<v-spacer />
|
||||
<span class="text-caption">{{ formatTime(msg.timestamp) }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="msg.content"
|
||||
class="message-content mt-1"
|
||||
>
|
||||
{{ msg.content }}
|
||||
</div>
|
||||
</v-alert>
|
||||
</TransitionGroup>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import messageService from '@/utils/message';
|
||||
import { getSetting } from '@/utils/settings';
|
||||
import { debounce } from '@/utils/debounce';
|
||||
|
||||
export default {
|
||||
name: 'MessageLog',
|
||||
data: () => ({
|
||||
drawer: false,
|
||||
messages: [],
|
||||
activeMessages: [],
|
||||
unreadCount: 0,
|
||||
maxActiveMessages: getSetting('message.maxActiveMessages'),
|
||||
messageTimeout: getSetting('message.timeout'),
|
||||
showSidebar: getSetting('message.showSidebar'),
|
||||
saveHistory: getSetting('message.saveHistory'),
|
||||
pageSize: 20,
|
||||
currentPage: 1,
|
||||
}),
|
||||
|
||||
computed: {
|
||||
visibleMessages() {
|
||||
return this.messages.slice(0, this.currentPage * this.pageSize);
|
||||
},
|
||||
hasMoreMessages() {
|
||||
return this.visibleMessages.length < this.messages.length;
|
||||
}
|
||||
},
|
||||
|
||||
created() {
|
||||
this.debouncedUpdateMessages = debounce(this.updateMessages, 300);
|
||||
},
|
||||
|
||||
mounted() {
|
||||
messageService.initialize();
|
||||
messageService.onSnackbar(this.showMessage);
|
||||
messageService.onLog(this.debouncedUpdateMessages);
|
||||
},
|
||||
|
||||
methods: {
|
||||
loadMoreMessages() {
|
||||
this.currentPage++;
|
||||
},
|
||||
|
||||
showMessage(message) {
|
||||
if (!this.showSidebar) return;
|
||||
|
||||
this.activeMessages.unshift(message);
|
||||
if (this.activeMessages.length > this.maxActiveMessages) {
|
||||
this.activeMessages.pop();
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
this.removeActiveMessage(message.id);
|
||||
}, this.messageTimeout);
|
||||
},
|
||||
|
||||
removeActiveMessage(id) {
|
||||
const index = this.activeMessages.findIndex(m => m.id === id);
|
||||
if (index !== -1) {
|
||||
this.activeMessages.splice(index, 1);
|
||||
}
|
||||
},
|
||||
|
||||
updateMessages(messages) {
|
||||
if (this.saveHistory) {
|
||||
this.messages = [...messages].reverse();
|
||||
this.unreadCount = messageService.getUnreadCount();
|
||||
}
|
||||
},
|
||||
|
||||
markAsRead(id) {
|
||||
messageService.markAsRead(id);
|
||||
},
|
||||
|
||||
deleteMessage(id) {
|
||||
messageService.deleteMessage(id);
|
||||
},
|
||||
|
||||
clearMessages() {
|
||||
messageService.clearMessages();
|
||||
this.messages = [];
|
||||
this.activeMessages = [];
|
||||
},
|
||||
|
||||
getIcon(type) {
|
||||
const icons = {
|
||||
success: 'mdi-check-circle',
|
||||
error: 'mdi-alert-circle',
|
||||
warning: 'mdi-alert',
|
||||
info: 'mdi-information'
|
||||
};
|
||||
return icons[type] || icons.info;
|
||||
},
|
||||
|
||||
getColor(type) {
|
||||
const colors = {
|
||||
success: 'success',
|
||||
error: 'error',
|
||||
warning: 'warning',
|
||||
info: 'primary'
|
||||
};
|
||||
return colors[type] || colors.info;
|
||||
},
|
||||
|
||||
formatTime: (() => {
|
||||
const cache = new Map();
|
||||
return (timestamp) => {
|
||||
if (cache.has(timestamp)) {
|
||||
return cache.get(timestamp);
|
||||
}
|
||||
const formatted = new Date(timestamp).toLocaleTimeString();
|
||||
cache.set(timestamp, formatted);
|
||||
return formatted;
|
||||
};
|
||||
})()
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.message-container {
|
||||
position: fixed;
|
||||
right: 16px;
|
||||
bottom: 16px;
|
||||
z-index: 1000;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.message-alert {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.message-list {
|
||||
height: calc(100vh - 64px);
|
||||
overflow-y: auto;
|
||||
scroll-behavior: smooth;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.message-item {
|
||||
border-left: 3px solid transparent;
|
||||
contain: content;
|
||||
}
|
||||
|
||||
.message-item.unread {
|
||||
background-color: rgba(var(--v-theme-primary), 0.05);
|
||||
}
|
||||
|
||||
/* 消息动画 */
|
||||
.message-enter-active,
|
||||
.message-leave-active {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.message-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateX(30px);
|
||||
}
|
||||
|
||||
.message-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
}
|
||||
</style>
|
54
src/components/SettingsCard.vue
Normal file
54
src/components/SettingsCard.vue
Normal file
@ -0,0 +1,54 @@
|
||||
<template>
|
||||
<v-card elevation="2" class="settings-card rounded-lg">
|
||||
<v-card-item>
|
||||
<template #prepend>
|
||||
<v-icon
|
||||
:icon="icon"
|
||||
size="large"
|
||||
class="mr-2"
|
||||
/>
|
||||
</template>
|
||||
<v-card-title class="text-h6">{{ title }}</v-card-title>
|
||||
</v-card-item>
|
||||
|
||||
<v-card-text>
|
||||
<v-progress-linear
|
||||
v-if="loading"
|
||||
indeterminate
|
||||
color="primary"
|
||||
class="mb-4"
|
||||
/>
|
||||
<slot />
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions v-if="$slots.actions" class="pa-4">
|
||||
<slot name="actions" />
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'SettingsCard',
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.settings-card {
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
72
src/components/settings/AboutCard.vue
Normal file
72
src/components/settings/AboutCard.vue
Normal file
@ -0,0 +1,72 @@
|
||||
<template>
|
||||
<v-card border>
|
||||
<v-card-item>
|
||||
<template #prepend>
|
||||
<v-icon icon="mdi-information" size="large" class="mr-2" />
|
||||
</template>
|
||||
<v-card-title class="text-h6">关于</v-card-title>
|
||||
</v-card-item>
|
||||
|
||||
<v-card-text>
|
||||
<v-row justify="center" align="center">
|
||||
<v-col cols="12" md="8" class="text-center">
|
||||
<v-avatar size="120" class="mb-4">
|
||||
<v-img
|
||||
src="https://avatars.githubusercontent.com/u/88357633?v=4"
|
||||
alt="作者头像"
|
||||
/>
|
||||
</v-avatar>
|
||||
|
||||
<h2 class="text-h5 mb-2">HomeworkPage</h2>
|
||||
<p class="text-body-1 mb-4">
|
||||
由 <a
|
||||
href="https://github.com/sunwuyuan"
|
||||
target="_blank"
|
||||
class="text-decoration-none font-weight-medium"
|
||||
>Sunwuyuan</a> 开发
|
||||
</p>
|
||||
|
||||
<div class="d-flex justify-center gap-2 flex-wrap">
|
||||
<v-btn
|
||||
color="primary"
|
||||
variant="outlined"
|
||||
href="https://github.com/SunWuyuan/homeworkpage-frontend"
|
||||
target="_blank"
|
||||
prepend-icon="mdi-github"
|
||||
>
|
||||
前端 GitHub
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="primary"
|
||||
variant="outlined"
|
||||
href="https://github.com/SunWuyuan/homeworkpage-backend"
|
||||
target="_blank"
|
||||
prepend-icon="mdi-github"
|
||||
>
|
||||
后端 GitHub
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="primary"
|
||||
variant="outlined"
|
||||
href="https://github.com/SunWuyuan/homeworkpage-backend/issues"
|
||||
target="_blank"
|
||||
prepend-icon="mdi-bug"
|
||||
>
|
||||
报告问题
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<p class="mt-4 text-caption text-medium-emphasis">
|
||||
GPL License © 2024
|
||||
</p>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'AboutCard'
|
||||
}
|
||||
</script>
|
344
src/components/settings/StudentListCard.vue
Normal file
344
src/components/settings/StudentListCard.vue
Normal file
@ -0,0 +1,344 @@
|
||||
<template>
|
||||
<v-card border>
|
||||
<v-card-item>
|
||||
<template #prepend>
|
||||
<v-icon icon="mdi-account-group" size="large" class="mr-2" />
|
||||
</template>
|
||||
<v-card-title class="text-h6">学生列表</v-card-title>
|
||||
<template #append>
|
||||
<v-btn
|
||||
:color="modelValue.advanced ? 'primary' : undefined"
|
||||
variant="text"
|
||||
prepend-icon="mdi-code-braces"
|
||||
@click="toggleAdvanced"
|
||||
>
|
||||
{{ modelValue.advanced ? '返回基础编辑' : '高级编辑' }}
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-card-item>
|
||||
|
||||
<v-card-text>
|
||||
<v-progress-linear
|
||||
v-if="loading"
|
||||
indeterminate
|
||||
color="primary"
|
||||
class="mb-4"
|
||||
/>
|
||||
|
||||
<v-alert
|
||||
v-if="error"
|
||||
type="error"
|
||||
variant="tonal"
|
||||
closable
|
||||
class="mb-4"
|
||||
>
|
||||
{{ error }}
|
||||
</v-alert>
|
||||
|
||||
<v-expand-transition>
|
||||
<!-- 普通编辑模式 -->
|
||||
<div v-if="!modelValue.advanced">
|
||||
<v-row class="mb-6">
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
v-model="newStudent"
|
||||
label="添加学生"
|
||||
placeholder="输入学生姓名后回车添加"
|
||||
prepend-inner-icon="mdi-account-plus"
|
||||
variant="outlined"
|
||||
hide-details
|
||||
class="mb-4"
|
||||
@keyup.enter="addStudent"
|
||||
>
|
||||
<template #append>
|
||||
<v-btn
|
||||
icon="mdi-plus"
|
||||
variant="text"
|
||||
color="primary"
|
||||
:disabled="!newStudent.trim()"
|
||||
@click="addStudent"
|
||||
/>
|
||||
</template>
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col
|
||||
v-for="(student, index) in modelValue.list"
|
||||
:key="index"
|
||||
cols="12"
|
||||
sm="6"
|
||||
md="4"
|
||||
lg="3"
|
||||
>
|
||||
<v-hover v-slot="{ isHovering, props }">
|
||||
<v-card
|
||||
v-bind="props"
|
||||
:elevation="isMobile ? 1 : (isHovering ? 4 : 1)"
|
||||
class="student-card"
|
||||
border
|
||||
>
|
||||
<v-card-text class="d-flex align-center pa-3">
|
||||
<v-menu location="bottom" :open-on-hover="!isMobile">
|
||||
<template #activator="{ props: menuProps }">
|
||||
<v-btn
|
||||
variant="tonal"
|
||||
size="small"
|
||||
class="mr-3 font-weight-medium"
|
||||
v-bind="menuProps"
|
||||
>
|
||||
{{ index + 1 }}
|
||||
</v-btn>
|
||||
</template>
|
||||
|
||||
<v-list density="compact" nav>
|
||||
<v-list-item
|
||||
prepend-icon="mdi-arrow-up-bold"
|
||||
:disabled="index === 0"
|
||||
@click="moveStudent(index, 'top')"
|
||||
>
|
||||
置顶
|
||||
</v-list-item>
|
||||
<v-divider />
|
||||
<v-list-item
|
||||
prepend-icon="mdi-arrow-up"
|
||||
:disabled="index === 0"
|
||||
@click="moveStudent(index, 'up')"
|
||||
>
|
||||
上移
|
||||
</v-list-item>
|
||||
<v-list-item
|
||||
prepend-icon="mdi-arrow-down"
|
||||
:disabled="index === modelValue.list.length - 1"
|
||||
@click="moveStudent(index, 'down')"
|
||||
>
|
||||
下移
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
|
||||
<v-text-field
|
||||
v-if="editingIndex === index"
|
||||
v-model="editingName"
|
||||
density="compact"
|
||||
variant="underlined"
|
||||
hide-details
|
||||
class="flex-grow-1"
|
||||
autofocus
|
||||
@keyup.enter="saveEdit"
|
||||
@blur="saveEdit"
|
||||
/>
|
||||
<span
|
||||
v-else
|
||||
class="text-body-1 flex-grow-1"
|
||||
@click="handleClick(index, student)"
|
||||
>
|
||||
{{ student }}
|
||||
</span>
|
||||
|
||||
<div class="d-flex gap-1 action-buttons" :class="{ 'opacity-100': isHovering || isMobile }">
|
||||
<v-btn
|
||||
icon="mdi-pencil"
|
||||
variant="text"
|
||||
color="primary"
|
||||
size="small"
|
||||
@click="startEdit(index, student)"
|
||||
/>
|
||||
<v-btn
|
||||
icon="mdi-delete"
|
||||
variant="text"
|
||||
color="error"
|
||||
size="small"
|
||||
@click="removeStudent(index)"
|
||||
/>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-hover>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
|
||||
<!-- 高级编辑模式 -->
|
||||
<div v-else class="pt-2">
|
||||
<v-textarea
|
||||
v-model="modelValue.text"
|
||||
label="批量编辑学生列表"
|
||||
placeholder="每行输入一个学生姓名"
|
||||
hint="使用文本编辑模式批量编辑学生名单,保存时会自动去除空行"
|
||||
persistent-hint
|
||||
variant="outlined"
|
||||
rows="10"
|
||||
@input="handleTextInput"
|
||||
/>
|
||||
</div>
|
||||
</v-expand-transition>
|
||||
|
||||
<v-row class="mt-6">
|
||||
<v-col cols="12" class="d-flex gap-2">
|
||||
<v-btn
|
||||
color="primary"
|
||||
prepend-icon="mdi-content-save"
|
||||
size="large"
|
||||
:loading="loading"
|
||||
:disabled="loading"
|
||||
@click="$emit('save')"
|
||||
>
|
||||
保存名单
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="error"
|
||||
variant="outlined"
|
||||
prepend-icon="mdi-refresh"
|
||||
size="large"
|
||||
:loading="loading"
|
||||
:disabled="loading"
|
||||
@click="$emit('reload')"
|
||||
>
|
||||
重载名单
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'StudentListCard',
|
||||
props: {
|
||||
modelValue: {
|
||||
type: Object,
|
||||
required: true,
|
||||
default: () => ({
|
||||
list: [],
|
||||
text: '',
|
||||
advanced: false
|
||||
})
|
||||
},
|
||||
loading: Boolean,
|
||||
error: String,
|
||||
isMobile: Boolean
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
newStudent: '',
|
||||
editingIndex: -1,
|
||||
editingName: ''
|
||||
}
|
||||
},
|
||||
|
||||
emits: ['update:modelValue', 'save', 'reload'],
|
||||
|
||||
methods: {
|
||||
toggleAdvanced() {
|
||||
const advanced = !this.modelValue.advanced;
|
||||
const text = advanced ? this.modelValue.list.join('\n') : this.modelValue.text;
|
||||
|
||||
this.$emit('update:modelValue', {
|
||||
...this.modelValue,
|
||||
advanced,
|
||||
text
|
||||
});
|
||||
},
|
||||
|
||||
handleTextInput(value) {
|
||||
const list = value
|
||||
.split('\n')
|
||||
.map(s => s.trim())
|
||||
.filter(s => s);
|
||||
|
||||
this.$emit('update:modelValue', {
|
||||
...this.modelValue,
|
||||
text: value,
|
||||
list
|
||||
});
|
||||
},
|
||||
|
||||
addStudent() {
|
||||
const name = this.newStudent.trim();
|
||||
if (name && !this.modelValue.list.includes(name)) {
|
||||
const newList = [...this.modelValue.list, name];
|
||||
this.$emit('update:modelValue', {
|
||||
...this.modelValue,
|
||||
list: newList,
|
||||
text: newList.join('\n')
|
||||
});
|
||||
this.newStudent = '';
|
||||
}
|
||||
},
|
||||
|
||||
removeStudent(index) {
|
||||
const newList = this.modelValue.list.filter((_, i) => i !== index);
|
||||
this.$emit('update:modelValue', {
|
||||
...this.modelValue,
|
||||
list: newList,
|
||||
text: newList.join('\n')
|
||||
});
|
||||
},
|
||||
|
||||
moveStudent(index, direction) {
|
||||
const newList = [...this.modelValue.list];
|
||||
let targetIndex;
|
||||
|
||||
if (direction === 'top') {
|
||||
targetIndex = 0;
|
||||
} else if (direction === 'up') {
|
||||
targetIndex = index - 1;
|
||||
} else {
|
||||
targetIndex = index + 1;
|
||||
}
|
||||
|
||||
if (targetIndex >= 0 && targetIndex < newList.length) {
|
||||
const [student] = newList.splice(index, 1);
|
||||
newList.splice(targetIndex, 0, student);
|
||||
|
||||
this.$emit('update:modelValue', {
|
||||
...this.modelValue,
|
||||
list: newList,
|
||||
text: newList.join('\n')
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
startEdit(index, name) {
|
||||
this.editingIndex = index;
|
||||
this.editingName = name;
|
||||
},
|
||||
|
||||
saveEdit() {
|
||||
if (this.editingIndex !== -1 && this.editingName.trim()) {
|
||||
const newList = [...this.modelValue.list];
|
||||
newList[this.editingIndex] = this.editingName.trim();
|
||||
|
||||
this.$emit('update:modelValue', {
|
||||
...this.modelValue,
|
||||
list: newList,
|
||||
text: newList.join('\n')
|
||||
});
|
||||
}
|
||||
this.editingIndex = -1;
|
||||
this.editingName = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.student-card {
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.action-buttons {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -13,8 +13,12 @@ import App from './App.vue'
|
||||
// Composables
|
||||
import { createApp } from 'vue'
|
||||
|
||||
import messageService from './utils/message';
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
registerPlugins(app)
|
||||
|
||||
app.use(messageService);
|
||||
|
||||
app.mount('#app')
|
||||
|
1043
src/pages/index.vue
1043
src/pages/index.vue
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
176
src/styles/index.scss
Normal file
176
src/styles/index.scss
Normal file
@ -0,0 +1,176 @@
|
||||
|
||||
.grid-masonry {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 16px;
|
||||
padding: 8px;
|
||||
grid-auto-flow: dense;
|
||||
}
|
||||
|
||||
.grid-item {
|
||||
width: 100%;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.empty-card {
|
||||
transform: scale(0.9);
|
||||
opacity: 0.8;
|
||||
grid-row-end: span 1 !important;
|
||||
}
|
||||
|
||||
.empty-card:hover {
|
||||
transform: scale(0.95);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.empty-subjects-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
@media (max-width: 1199px) {
|
||||
.grid-masonry {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 799px) {
|
||||
.grid-masonry {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.empty-card {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
}
|
||||
|
||||
/* 确保容器高度不超过视口 */
|
||||
.main-window {
|
||||
max-height: calc(100vh - 180px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* 优化滚动条样式 */
|
||||
.main-window::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.main-window::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.main-window::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(0, 0, 0, 0.2);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.main-window::-webkit-scrollbar-thumb:hover {
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.no-data-message {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 200px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.attendance-drawer {
|
||||
border-left: 1px solid rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.attendance-drawer :deep(.v-navigation-drawer__content) {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* 优化滚动条样式 */
|
||||
.attendance-drawer :deep(.v-navigation-drawer__content::-webkit-scrollbar) {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.attendance-drawer
|
||||
:deep(.v-navigation-drawer__content::-webkit-scrollbar-track) {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.attendance-drawer
|
||||
:deep(.v-navigation-drawer__content::-webkit-scrollbar-thumb) {
|
||||
background-color: rgba(0, 0, 0, 0.2);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.attendance-drawer
|
||||
:deep(.v-navigation-drawer__content::-webkit-scrollbar-thumb:hover) {
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
/* 响应式调整 */
|
||||
@media (max-width: 960px) {
|
||||
.attendance-drawer {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.text-success {
|
||||
color: rgb(var(--v-theme-success));
|
||||
}
|
||||
|
||||
.text-error {
|
||||
color: rgb(var(--v-theme-error));
|
||||
}
|
||||
|
||||
.text-warning {
|
||||
color: rgb(var(--v-theme-warning));
|
||||
}
|
||||
|
||||
.attendance-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.attendance-numbers {
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.total-number {
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.12);
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
.status-number {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.text-h2,
|
||||
.text-h3 {
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.empty-subjects-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
gap: 16px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.empty-subject-card {
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.empty-subject-card:hover {
|
||||
transform: scale(1.02);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.empty-subjects {
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.12);
|
||||
padding-top: 1rem;
|
||||
}
|
||||
|
||||
.empty-subject-card:not(:disabled):hover {
|
||||
opacity: 1;
|
||||
transform: scale(1.02);
|
||||
}
|
@ -8,3 +8,74 @@
|
||||
// @use 'vuetify/settings' with (
|
||||
// $color-pack: false
|
||||
// );
|
||||
|
||||
.student-card {
|
||||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.bg-primary-subtle {
|
||||
background-color: rgb(var(--v-theme-primary), 0.05);
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
transition: opacity 0.2s ease;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.gap-1 {
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.gap-2 {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.student-card .v-text-field {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.v-container {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.v-col {
|
||||
padding: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.student-card.mobile {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.student-card.mobile .v-btn {
|
||||
min-width: 40px;
|
||||
min-height: 40px;
|
||||
}
|
||||
|
||||
.student-card.mobile .v-text-field {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.v-col {
|
||||
padding: 6px !important;
|
||||
}
|
||||
|
||||
.student-card {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.student-card {
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
.student-card:active {
|
||||
background-color: rgb(var(--v-theme-primary), 0.05);
|
||||
}
|
177
src/utils/dataProvider.js
Normal file
177
src/utils/dataProvider.js
Normal file
@ -0,0 +1,177 @@
|
||||
import axios from 'axios';
|
||||
|
||||
const formatResponse = (data, message = null) => ({
|
||||
success: true,
|
||||
data,
|
||||
message
|
||||
});
|
||||
|
||||
const formatError = (message, code = 'UNKNOWN_ERROR') => ({
|
||||
success: false,
|
||||
error: { code, message }
|
||||
});
|
||||
|
||||
const providers = {
|
||||
localStorage: {
|
||||
async loadData(key, date) {
|
||||
try {
|
||||
// 检查是否设置了班号
|
||||
const classNumber = key.split('/').pop();
|
||||
if (!classNumber) {
|
||||
return formatError('请先设置班号', 'CONFIG_ERROR');
|
||||
}
|
||||
|
||||
// 使用班号作为本地存储的前缀
|
||||
const storageKey = `homework_${classNumber}_${date}`;
|
||||
const rawData = localStorage.getItem(storageKey);
|
||||
|
||||
if (!rawData) {
|
||||
// 如果是今天的数据且没有找到,返回空结构而不是null
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
if (date === today) {
|
||||
return formatResponse({
|
||||
homework: {},
|
||||
attendance: { absent: [], late: [] }
|
||||
});
|
||||
}
|
||||
return formatError('数据不存在', 'NOT_FOUND');
|
||||
}
|
||||
|
||||
return formatResponse(JSON.parse(rawData));
|
||||
} catch (error) {
|
||||
|
||||
return formatError('读取本地数据失败');
|
||||
}
|
||||
},
|
||||
|
||||
async saveData(key, data, date) {
|
||||
try {
|
||||
// 检查是否设置了班号
|
||||
const classNumber = key.split('/').pop();
|
||||
if (!classNumber) {
|
||||
return formatError('请先设置班号', 'CONFIG_ERROR');
|
||||
}
|
||||
|
||||
// 使用班号作为本地存储的前缀
|
||||
const storageKey = `homework_${classNumber}_${date}`;
|
||||
localStorage.setItem(storageKey, JSON.stringify(data));
|
||||
return formatResponse(null, '保存成功');
|
||||
} catch (error) {
|
||||
return formatError('保存本地数据失败');
|
||||
}
|
||||
},
|
||||
|
||||
async loadConfig(key) {
|
||||
try {
|
||||
const classNumber = key.split('/').pop();
|
||||
if (!classNumber) {
|
||||
return formatError('请先设置班号', 'CONFIG_ERROR');
|
||||
}
|
||||
|
||||
const storageKey = `config_${classNumber}`;
|
||||
const rawData = localStorage.getItem(storageKey);
|
||||
|
||||
if (!rawData) {
|
||||
return formatResponse({
|
||||
studentList: [],
|
||||
displayOptions: {}
|
||||
});
|
||||
}
|
||||
|
||||
return formatResponse(JSON.parse(rawData));
|
||||
} catch (error) {
|
||||
return formatError('读取本地配置失败');
|
||||
}
|
||||
},
|
||||
|
||||
async saveConfig(key, config) {
|
||||
try {
|
||||
const classNumber = key.split('/').pop();
|
||||
if (!classNumber) {
|
||||
return formatError('请先设置班号', 'CONFIG_ERROR');
|
||||
}
|
||||
|
||||
const storageKey = `config_${classNumber}`;
|
||||
localStorage.setItem(storageKey, JSON.stringify(config));
|
||||
return formatResponse(null, '保存成功');
|
||||
} catch (error) {
|
||||
return formatError('保存本地配置失败');
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
server: {
|
||||
async loadData(key, date) {
|
||||
try {
|
||||
const res = await axios.get(`${key}/homework?date=${date}`);
|
||||
if (res.data?.status === false) {
|
||||
return formatError(res.data.msg || '获取数据失败', 'SERVER_ERROR');
|
||||
}
|
||||
return formatResponse(res.data);
|
||||
} catch (error) {
|
||||
return formatError(
|
||||
error.response?.data?.message || '服务器连接失败',
|
||||
'NETWORK_ERROR'
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
async saveData(key, data) {
|
||||
try {
|
||||
await axios.post(`${key}/homework`, data);
|
||||
return formatResponse(null, '保存成功');
|
||||
} catch (error) {
|
||||
return formatError(
|
||||
error.response?.data?.message || '保存失败',
|
||||
'SAVE_ERROR'
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
async loadConfig(key) {
|
||||
try {
|
||||
const res = await axios.get(`${key}/config`);
|
||||
if (res.data?.status === false) {
|
||||
return formatError(res.data.msg || '获取配置失败', 'SERVER_ERROR');
|
||||
}
|
||||
return formatResponse(res.data);
|
||||
} catch (error) {
|
||||
return formatError(
|
||||
error.response?.data?.message || '服务器连接失败',
|
||||
'NETWORK_ERROR'
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
async saveConfig(key, config) {
|
||||
try {
|
||||
const res = await axios.post(`${key}/config`, config);
|
||||
if (res.data?.status === false) {
|
||||
return formatError(res.data.msg || '保存失败', 'SAVE_ERROR');
|
||||
}
|
||||
return formatResponse(null, '保存成功');
|
||||
} catch (error) {
|
||||
return formatError(
|
||||
error.response?.data?.message || '保存失败',
|
||||
'SAVE_ERROR'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default {
|
||||
loadData: (provider, key, date) => providers[provider]?.loadData(key, date),
|
||||
saveData: (provider, key, data, date) => providers[provider]?.saveData(key, data, date),
|
||||
loadConfig: (provider, key) => providers[provider]?.loadConfig(key),
|
||||
saveConfig: (provider, key, config) => providers[provider]?.saveConfig(key, config)
|
||||
};
|
||||
|
||||
export const ErrorCodes = {
|
||||
NOT_FOUND: '数据不存在',
|
||||
NETWORK_ERROR: '网络连接失败',
|
||||
SERVER_ERROR: '服务器错误',
|
||||
SAVE_ERROR: '保存失败',
|
||||
CONFIG_ERROR: '配置错误',
|
||||
UNKNOWN_ERROR: '未知错误'
|
||||
};
|
27
src/utils/debounce.js
Normal file
27
src/utils/debounce.js
Normal file
@ -0,0 +1,27 @@
|
||||
export function debounce(fn, delay) {
|
||||
let timer = null;
|
||||
return function (...args) {
|
||||
if (timer) clearTimeout(timer);
|
||||
timer = setTimeout(() => {
|
||||
fn.apply(this, args);
|
||||
}, delay);
|
||||
};
|
||||
}
|
||||
|
||||
export function throttle(fn, delay) {
|
||||
let timer = null;
|
||||
let last = 0;
|
||||
return function (...args) {
|
||||
const now = Date.now();
|
||||
if (now - last < delay) {
|
||||
if (timer) clearTimeout(timer);
|
||||
timer = setTimeout(() => {
|
||||
last = now;
|
||||
fn.apply(this, args);
|
||||
}, delay);
|
||||
} else {
|
||||
last = now;
|
||||
fn.apply(this, args);
|
||||
}
|
||||
};
|
||||
}
|
153
src/utils/message.js
Normal file
153
src/utils/message.js
Normal file
@ -0,0 +1,153 @@
|
||||
const messages = [];
|
||||
let snackbarCallback = null;
|
||||
let logCallback = null;
|
||||
|
||||
const MessageType = {
|
||||
SUCCESS: 'success',
|
||||
ERROR: 'error',
|
||||
INFO: 'info',
|
||||
WARNING: 'warning'
|
||||
};
|
||||
|
||||
const defaultOptions = {
|
||||
timeout: 3000,
|
||||
showSnackbar: true,
|
||||
addToLog: true
|
||||
};
|
||||
|
||||
const STORAGE_KEY = 'homeworkpage_messages';
|
||||
const MAX_MESSAGES = 100; // 最大消息数量
|
||||
const MAX_STORAGE_SIZE = 1024 * 1024; // 1MB 存储限制
|
||||
|
||||
// 加载保存的消息
|
||||
function loadStoredMessages() {
|
||||
try {
|
||||
const stored = localStorage.getItem(STORAGE_KEY);
|
||||
if (stored) {
|
||||
messages.push(...JSON.parse(stored));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载消息历史失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 清理旧消息
|
||||
function cleanOldMessages() {
|
||||
if (messages.length > MAX_MESSAGES) {
|
||||
messages.splice(MAX_MESSAGES);
|
||||
}
|
||||
}
|
||||
|
||||
// 检查存储大小
|
||||
function checkStorageSize(data) {
|
||||
try {
|
||||
const size = new Blob([data]).size;
|
||||
return size <= MAX_STORAGE_SIZE;
|
||||
} catch (error) {
|
||||
console.error('检查存储大小失败:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 保存消息到localStorage
|
||||
function saveMessages() {
|
||||
try {
|
||||
cleanOldMessages();
|
||||
const data = JSON.stringify(messages);
|
||||
|
||||
if (!checkStorageSize(data)) {
|
||||
// 如果数据太大,删除一半的旧消息
|
||||
messages.splice(Math.floor(messages.length / 2));
|
||||
return saveMessages();
|
||||
}
|
||||
|
||||
localStorage.setItem(STORAGE_KEY, data);
|
||||
} catch (error) {
|
||||
if (error.name === 'QuotaExceededError') {
|
||||
// 如果存储空间不足,清理一些旧消息再试
|
||||
messages.splice(Math.floor(messages.length / 2));
|
||||
return saveMessages();
|
||||
}
|
||||
console.error('保存消息历史失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function createMessage(type, title, content = '', options = {}) {
|
||||
const msgOptions = { ...defaultOptions, ...options };
|
||||
const message = {
|
||||
id: Date.now() + Math.random(),
|
||||
type,
|
||||
title,
|
||||
content: content.substring(0, 500), // 限制内容长度
|
||||
timestamp: new Date(),
|
||||
read: false
|
||||
};
|
||||
|
||||
if (msgOptions.addToLog) {
|
||||
messages.unshift(message); // 新消息添加到开头
|
||||
cleanOldMessages();
|
||||
saveMessages();
|
||||
logCallback?.(messages);
|
||||
}
|
||||
|
||||
if (msgOptions.showSnackbar) {
|
||||
snackbarCallback?.(message);
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
// 添加防抖函数实现
|
||||
function debounce(fn, delay) {
|
||||
let timer = null;
|
||||
return function (...args) {
|
||||
if (timer) clearTimeout(timer);
|
||||
timer = setTimeout(() => {
|
||||
fn.apply(this, args);
|
||||
}, delay);
|
||||
};
|
||||
}
|
||||
|
||||
export default {
|
||||
install: (app) => {
|
||||
app.config.globalProperties.$message = {
|
||||
success: (title, content, options) => createMessage(MessageType.SUCCESS, title, content, options),
|
||||
error: (title, content, options) => createMessage(MessageType.ERROR, title, content, options),
|
||||
info: (title, content, options) => createMessage(MessageType.INFO, title, content, options),
|
||||
warning: (title, content, options) => createMessage(MessageType.WARNING, title, content, options),
|
||||
};
|
||||
},
|
||||
onSnackbar: (callback) => { snackbarCallback = callback; },
|
||||
onLog: (callback) => { logCallback = callback; },
|
||||
getMessages: () => [...messages],
|
||||
clearMessages: () => {
|
||||
messages.length = 0;
|
||||
try {
|
||||
localStorage.removeItem(STORAGE_KEY);
|
||||
} catch (error) {
|
||||
console.error('清除消息历史失败:', error);
|
||||
}
|
||||
},
|
||||
MessageType,
|
||||
markAsRead: (messageId) => {
|
||||
const message = messages.find(m => m.id === messageId);
|
||||
if (message) {
|
||||
message.read = true;
|
||||
saveMessages();
|
||||
logCallback?.(messages);
|
||||
}
|
||||
},
|
||||
deleteMessage: (messageId) => {
|
||||
const index = messages.findIndex(m => m.id === messageId);
|
||||
if (index !== -1) {
|
||||
messages.splice(index, 1);
|
||||
saveMessages();
|
||||
logCallback?.(messages);
|
||||
}
|
||||
},
|
||||
getUnreadCount: () => messages.filter(m => !m.read).length,
|
||||
initialize: () => {
|
||||
loadStoredMessages();
|
||||
},
|
||||
debounce, // 导出防抖函数
|
||||
};
|
@ -6,6 +6,7 @@
|
||||
* @property {Function} [validate] - 可选的验证函数
|
||||
* @property {string} [description] - 配置项描述
|
||||
* @property {string} [legacyKey] - 旧版本localStorage键名(用于迁移)
|
||||
* @property {boolean} [requireDeveloper] - 是否需要开发者选项启用
|
||||
*/
|
||||
|
||||
// 存储所有设置的localStorage键名
|
||||
@ -17,14 +18,9 @@ const SETTINGS_STORAGE_KEY = 'homeworkpage_settings';
|
||||
*/
|
||||
const settingsDefinitions = {
|
||||
// 显示设置
|
||||
// 'display.showEmptySubjects': {
|
||||
// type: 'boolean',
|
||||
// default: true,
|
||||
// description: '是否在主界面显示没有作业内容的科目'
|
||||
// },
|
||||
'display.emptySubjectDisplay': {
|
||||
type: 'string',
|
||||
default: 'card',
|
||||
default: 'button', // 修改默认值为 'button'
|
||||
validate: value => ['card', 'button'].includes(value),
|
||||
description: '空科目的显示方式:卡片或按钮'
|
||||
},
|
||||
@ -34,7 +30,7 @@ const settingsDefinitions = {
|
||||
description: '是否启用动态排序以优化显示效果'
|
||||
},
|
||||
|
||||
// 服务器设置
|
||||
// 服务器设置(合并了数据提供者设置)
|
||||
'server.domain': {
|
||||
type: 'string',
|
||||
default: '',
|
||||
@ -45,7 +41,13 @@ const settingsDefinitions = {
|
||||
type: 'string',
|
||||
default: '',
|
||||
validate: value => /^[A-Za-z0-9]*$/.test(value),
|
||||
description: '班级编号'
|
||||
description: '班级编号(无论使用哪种存储方式都需要设置)'
|
||||
},
|
||||
'server.provider': { // 新增项
|
||||
type: 'string',
|
||||
default: 'server',
|
||||
validate: value => ['server', 'localStorage'].includes(value),
|
||||
description: '数据提供者,用于决定数据存储方式'
|
||||
},
|
||||
|
||||
// 刷新设置
|
||||
@ -79,6 +81,46 @@ const settingsDefinitions = {
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
description: '编辑前是否自动刷新'
|
||||
},
|
||||
|
||||
// 开发者选项
|
||||
'developer.enabled': {
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: '是否启用开发者选项'
|
||||
},
|
||||
'developer.showDebugConfig': {
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: '是否显示调试配置'
|
||||
},
|
||||
|
||||
// 消息设置
|
||||
'message.showSidebar': {
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
description: '是否显示消息记录侧栏',
|
||||
requireDeveloper: true // 添加标记
|
||||
},
|
||||
'message.maxActiveMessages': {
|
||||
type: 'number',
|
||||
default: 5,
|
||||
validate: value => value >= 1 && value <= 10,
|
||||
description: '同时显示的最大消息数量',
|
||||
requireDeveloper: true
|
||||
},
|
||||
'message.timeout': {
|
||||
type: 'number',
|
||||
default: 5000,
|
||||
validate: value => value >= 1000 && value <= 30000,
|
||||
description: '消息自动关闭时间(毫秒)',
|
||||
requireDeveloper: true
|
||||
},
|
||||
'message.saveHistory': {
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
description: '是否保存消息历史记录',
|
||||
requireDeveloper: true
|
||||
}
|
||||
};
|
||||
|
||||
@ -182,10 +224,26 @@ function getSetting(key) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 添加对开发者选项依赖的检查
|
||||
if (definition.requireDeveloper && !settingsCache['developer.enabled']) {
|
||||
return definition.default;
|
||||
}
|
||||
|
||||
const value = settingsCache[key];
|
||||
return value !== undefined ? value : definition.default;
|
||||
}
|
||||
|
||||
// 添加设置变更日志函数
|
||||
function logSettingsChange(key, oldValue, newValue) {
|
||||
if (settingsCache['developer.enabled'] && settingsCache['developer.showDebugConfig']) {
|
||||
console.log(`[Settings] ${key}:`, {
|
||||
old: oldValue,
|
||||
new: newValue,
|
||||
time: new Date().toLocaleTimeString()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置配置项的值
|
||||
* @param {string} key - 设置项键名
|
||||
@ -199,7 +257,14 @@ function setSetting(key, value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 添加对开发者选项依赖的检查
|
||||
if (definition.requireDeveloper && !settingsCache['developer.enabled']) {
|
||||
console.warn(`设置项 ${key} 需要启用开发者选项`);
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const oldValue = settingsCache[key];
|
||||
// 类型转换
|
||||
if (typeof value !== definition.type) {
|
||||
value = definition.type === 'boolean' ? Boolean(value) :
|
||||
@ -218,6 +283,7 @@ function setSetting(key, value) {
|
||||
|
||||
settingsCache[key] = value;
|
||||
saveSettings();
|
||||
logSettingsChange(key, oldValue, value);
|
||||
|
||||
// 为了保持向后兼容,同时更新旧的localStorage键
|
||||
const legacyKey = definition.legacyKey;
|
||||
|
Loading…
x
Reference in New Issue
Block a user