mirror of
https://github.com/ZeroCatDev/Classworks.git
synced 2025-07-04 10:29:23 +00:00
1
This commit is contained in:
parent
367954cfa6
commit
c6b68ed3a0
@ -53,114 +53,199 @@
|
|||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</v-app-bar>
|
</v-app-bar>
|
||||||
<v-container
|
<div class="d-flex">
|
||||||
class="main-window"
|
<!-- 主要内容区域 -->
|
||||||
fluid
|
<v-container
|
||||||
>
|
class="main-window flex-grow-1"
|
||||||
<v-row>
|
fluid
|
||||||
<v-col :cols="attendanceVisible ? 11 : 12">
|
>
|
||||||
<div class="grid-masonry" ref="gridContainer">
|
<div v-if="showNoDataMessage && !isToday" class="no-data-message">
|
||||||
<div
|
<v-card class="text-center pa-4" variant="outlined">
|
||||||
v-for="item in sortedItems"
|
<v-card-title class="text-h6">{{ noDataMessage }}</v-card-title>
|
||||||
:key="item.key"
|
<v-card-text>
|
||||||
class="grid-item"
|
<div class="text-body-1">该日期未上传作业数据</div>
|
||||||
:class="{
|
</v-card-text>
|
||||||
'empty-card': !item.content,
|
</v-card>
|
||||||
[`grid-row-${item.rowSpan}`]: true
|
</div>
|
||||||
}"
|
|
||||||
:style="{
|
<template v-else>
|
||||||
'grid-row-end': `span ${item.rowSpan}`,
|
<div v-if="showNoDataMessage && isToday" class="no-data-message">
|
||||||
order: item.order
|
<v-card class="text-center pa-4" variant="outlined">
|
||||||
}"
|
<v-card-title class="text-h6">开始今天的作业</v-card-title>
|
||||||
@click="openDialog(item.key)"
|
<v-card-text>
|
||||||
>
|
<div class="text-body-1 mb-4">今天还没有上传作业哦~</div>
|
||||||
<v-card border height="100%">
|
<v-btn
|
||||||
<v-card-title :class="{ 'text-subtitle-1': !item.content }">
|
color="primary"
|
||||||
{{ item.name }}
|
@click="initializeAndOpenDialog"
|
||||||
|
>
|
||||||
|
开始添加作业
|
||||||
|
</v-btn>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<div class="grid-masonry" ref="gridContainer">
|
||||||
|
<div
|
||||||
|
v-for="item in sortedItems"
|
||||||
|
:key="item.key"
|
||||||
|
class="grid-item"
|
||||||
|
:class="{
|
||||||
|
'empty-card': !item.content,
|
||||||
|
[`grid-row-${item.rowSpan}`]: true
|
||||||
|
}"
|
||||||
|
:style="{
|
||||||
|
'grid-row-end': `span ${item.rowSpan}`,
|
||||||
|
order: item.order,
|
||||||
|
cursor: uploadLoading || downloadLoading ? 'not-allowed' : 'pointer',
|
||||||
|
opacity: uploadLoading || downloadLoading ? '0.7' : '1'
|
||||||
|
}"
|
||||||
|
@click="!uploadLoading && !downloadLoading && openDialog(item.key)"
|
||||||
|
>
|
||||||
|
<v-card border height="100%">
|
||||||
|
<v-card-title :class="{ 'text-subtitle-1': !item.content }">
|
||||||
|
{{ item.name }}
|
||||||
|
</v-card-title>
|
||||||
|
<v-card-text :style="item.content ? contentStyle : null">
|
||||||
|
<template v-if="item.content">
|
||||||
|
<v-list>
|
||||||
|
<v-list-item
|
||||||
|
v-for="text in splitPoint(item.content)"
|
||||||
|
:key="text"
|
||||||
|
>
|
||||||
|
{{ text }}
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<div class="text-center pa-2">
|
||||||
|
<v-icon size="small" color="grey">mdi-plus</v-icon>
|
||||||
|
<div class="text-caption text-grey">点击添加作业</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 空科目按钮组 -->
|
||||||
|
<div v-if="emptySubjectDisplay === 'button'" class="empty-subjects-container">
|
||||||
|
<v-btn
|
||||||
|
v-for="subject in emptySubjects"
|
||||||
|
:key="subject.key"
|
||||||
|
variant="outlined"
|
||||||
|
color="primary"
|
||||||
|
class="empty-subject-btn"
|
||||||
|
@click="!uploadLoading && !downloadLoading && openDialog(subject.key)"
|
||||||
|
:disabled="uploadLoading || downloadLoading"
|
||||||
|
>
|
||||||
|
<v-icon start>mdi-plus</v-icon>
|
||||||
|
{{ subject.name }}
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</v-container>
|
||||||
|
|
||||||
|
<!-- 出勤统计区域 -->
|
||||||
|
<v-navigation-drawer
|
||||||
|
v-if="studentList.length > 1"
|
||||||
|
location="right"
|
||||||
|
permanent
|
||||||
|
width="300"
|
||||||
|
class="attendance-drawer"
|
||||||
|
>
|
||||||
|
<v-card
|
||||||
|
class="h-100"
|
||||||
|
flat
|
||||||
|
>
|
||||||
|
<v-card-item>
|
||||||
|
<v-card-title class="text-h6">
|
||||||
|
<v-icon icon="mdi-account-multiple" class="mr-2" />
|
||||||
|
出勤统计
|
||||||
|
</v-card-title>
|
||||||
|
</v-card-item>
|
||||||
|
|
||||||
|
<v-card-text>
|
||||||
|
<div class="attendance-stats mb-4">
|
||||||
|
<div class="d-flex justify-space-between align-center mb-2">
|
||||||
|
<span class="text-subtitle-1">应到人数</span>
|
||||||
|
<span class="text-h6">{{ studentList.length }}人</span>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex justify-space-between align-center mb-2">
|
||||||
|
<span class="text-subtitle-1">实到人数</span>
|
||||||
|
<span class="text-h6 text-success">
|
||||||
|
{{ studentList.length - selectedSet.size - lateSet.size }}人
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<v-divider class="my-2" />
|
||||||
|
<div class="d-flex justify-space-between align-center mb-2">
|
||||||
|
<span class="text-subtitle-1">请假人数</span>
|
||||||
|
<span class="text-h6 text-error">{{ selectedSet.size }}人</span>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex justify-space-between align-center">
|
||||||
|
<span class="text-subtitle-1">迟到人数</span>
|
||||||
|
<span class="text-h6 text-warning">{{ lateSet.size }}人</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template v-if="selectedSet.size > 0">
|
||||||
|
<v-card
|
||||||
|
variant="outlined"
|
||||||
|
class="mb-4"
|
||||||
|
>
|
||||||
|
<v-card-title class="text-subtitle-1">
|
||||||
|
<v-icon icon="mdi-account-off" class="mr-2" color="error" />
|
||||||
|
请假名单
|
||||||
</v-card-title>
|
</v-card-title>
|
||||||
<v-card-text :style="item.content ? contentStyle : null">
|
<v-card-text>
|
||||||
<template v-if="item.content">
|
<v-list density="compact" nav>
|
||||||
<v-list>
|
<v-list-item
|
||||||
<v-list-item
|
v-for="i in Array.from(selectedSet)"
|
||||||
v-for="text in splitPoint(item.content)"
|
:key="'absent-' + i"
|
||||||
:key="text"
|
:title="`${i + 1}. ${studentList[i]}`"
|
||||||
>
|
prepend-icon="mdi-account"
|
||||||
{{ text }}
|
/>
|
||||||
</v-list-item>
|
</v-list>
|
||||||
</v-list>
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
<div class="text-center pa-2">
|
|
||||||
<v-icon size="small" color="grey">mdi-plus</v-icon>
|
|
||||||
<div class="text-caption text-grey">点击添加作业</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
</v-card>
|
</v-card>
|
||||||
</div>
|
</template>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 空科目按钮组 -->
|
<template v-if="lateSet.size > 0">
|
||||||
<div v-if="emptySubjectDisplay === 'button'" class="empty-subjects-container">
|
<v-card
|
||||||
|
variant="outlined"
|
||||||
|
>
|
||||||
|
<v-card-title class="text-subtitle-1">
|
||||||
|
<v-icon icon="mdi-clock-alert" class="mr-2" color="warning" />
|
||||||
|
迟到名单
|
||||||
|
</v-card-title>
|
||||||
|
<v-card-text>
|
||||||
|
<v-list density="compact" nav>
|
||||||
|
<v-list-item
|
||||||
|
v-for="i in Array.from(lateSet)"
|
||||||
|
:key="'late-' + i"
|
||||||
|
:title="`${i + 1}. ${studentList[i]}`"
|
||||||
|
prepend-icon="mdi-account-clock"
|
||||||
|
/>
|
||||||
|
</v-list>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</template>
|
||||||
|
</v-card-text>
|
||||||
|
|
||||||
|
<v-card-actions class="pa-4">
|
||||||
<v-btn
|
<v-btn
|
||||||
v-for="subject in emptySubjects"
|
|
||||||
:key="subject.key"
|
|
||||||
variant="outlined"
|
|
||||||
color="primary"
|
|
||||||
class="empty-subject-btn"
|
|
||||||
@click="openDialog(subject.key)"
|
|
||||||
>
|
|
||||||
<v-icon start>mdi-plus</v-icon>
|
|
||||||
{{ subject.name }}
|
|
||||||
</v-btn>
|
|
||||||
</div>
|
|
||||||
</v-col>
|
|
||||||
|
|
||||||
<v-col
|
|
||||||
v-if="studentList.length"
|
|
||||||
class="attendance-area"
|
|
||||||
:cols="1"
|
|
||||||
@click="setAttendanceArea"
|
|
||||||
>
|
|
||||||
<h1>出勤</h1>
|
|
||||||
<h2>应到:{{ studentList.length }}人</h2>
|
|
||||||
<h2>
|
|
||||||
实到:{{ studentList.length - selectedSet.size }}人
|
|
||||||
</h2>
|
|
||||||
<h2>请假:{{ selectedSet.size }} 人</h2>
|
|
||||||
<h3
|
|
||||||
v-for="(i, index) in selectedSet"
|
|
||||||
:key="'absent-' + index"
|
|
||||||
>
|
|
||||||
{{ `${index + 1}. ${studentList[i]}` }}
|
|
||||||
</h3>
|
|
||||||
<h2>迟到:{{ lateSet.size }} 人</h2>
|
|
||||||
|
|
||||||
<h3
|
|
||||||
v-for="(i, index) in lateSet"
|
|
||||||
:key="'late-' + index"
|
|
||||||
>
|
|
||||||
{{ `${index + 1}. ${studentList[i]}` }}
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
<!-- 空科目按钮显示区域 -->
|
|
||||||
<template v-if="showEmptySubjects && emptySubjectDisplay === 'button'">
|
|
||||||
<v-divider class="my-4" />
|
|
||||||
<h2>未填写作业</h2>
|
|
||||||
<v-btn
|
|
||||||
v-for="subject in emptySubjects"
|
|
||||||
:key="subject.key"
|
|
||||||
block
|
block
|
||||||
variant="outlined"
|
color="primary"
|
||||||
class="mb-2"
|
prepend-icon="mdi-pencil"
|
||||||
@click.stop="openDialog(subject.key)"
|
@click="setAttendanceArea"
|
||||||
>
|
>
|
||||||
{{ subject.name }}
|
编辑出勤状态
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</template>
|
</v-card-actions>
|
||||||
</v-col>
|
</v-card>
|
||||||
</v-row>
|
</v-navigation-drawer>
|
||||||
</v-container>
|
</div>
|
||||||
<v-container fluid>
|
<v-container fluid>
|
||||||
<v-btn
|
<v-btn
|
||||||
v-if="!synced"
|
v-if="!synced"
|
||||||
@ -331,11 +416,65 @@
|
|||||||
.main-window::-webkit-scrollbar-thumb:hover {
|
.main-window::-webkit-scrollbar-thumb:hover {
|
||||||
background-color: rgba(0, 0, 0, 0.3);
|
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));
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { useDisplay } from "vuetify";
|
import { useDisplay } from "vuetify";
|
||||||
|
import { getSetting, watchSettings } from '@/utils/settings';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "HomeworkBoard",
|
name: "HomeworkBoard",
|
||||||
@ -353,25 +492,24 @@ export default {
|
|||||||
dateString: "",
|
dateString: "",
|
||||||
synced: false,
|
synced: false,
|
||||||
attendDialogVisible: false,
|
attendDialogVisible: false,
|
||||||
contentStyle: { "font-size": "28px" },
|
contentStyle: { "font-size": `${getSetting('font.size')}px` },
|
||||||
uploadLoading: false,
|
uploadLoading: false,
|
||||||
downloadLoading: false,
|
downloadLoading: false,
|
||||||
homeworkData: {},
|
homeworkData: {},
|
||||||
homeworkArrange: [[], []],
|
homeworkArrange: [[], []],
|
||||||
snackbar: false,
|
snackbar: false,
|
||||||
snackbarText: "",
|
snackbarText: "",
|
||||||
fontSize: parseInt(localStorage.getItem('fontSize')) || 28,
|
fontSize: getSetting('font.size'),
|
||||||
datePickerDialog: false,
|
datePickerDialog: false,
|
||||||
selectedDate: null,
|
selectedDate: null,
|
||||||
refreshInterval: null,
|
refreshInterval: null,
|
||||||
autoSave: false,
|
|
||||||
refreshBeforeEdit: false,
|
|
||||||
showEmptySubjects: localStorage.getItem('showEmptySubjects') === 'true',
|
|
||||||
emptySubjectDisplay: localStorage.getItem('emptySubjectDisplay') || 'card',
|
|
||||||
subjectOrder: [
|
subjectOrder: [
|
||||||
"语文", "数学", "英语", "物理", "化学",
|
"语文", "数学", "英语", "物理", "化学",
|
||||||
"生物", "政治", "历史", "地理", "其他"
|
"生物", "政治", "历史", "地理", "其他"
|
||||||
],
|
],
|
||||||
|
showNoDataMessage: false,
|
||||||
|
noDataMessage: '',
|
||||||
|
isToday: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -405,7 +543,6 @@ export default {
|
|||||||
name: value.name,
|
name: value.name,
|
||||||
content: value.content,
|
content: value.content,
|
||||||
order: this.subjectOrder.indexOf(key),
|
order: this.subjectOrder.indexOf(key),
|
||||||
// 计算每个卡片的行数
|
|
||||||
rowSpan: value.content ?
|
rowSpan: value.content ?
|
||||||
Math.ceil((value.content.split('\n').filter(line => line.trim()).length + 1) * 0.8) : 1
|
Math.ceil((value.content.split('\n').filter(line => line.trim()).length + 1) * 0.8) : 1
|
||||||
}))
|
}))
|
||||||
@ -416,8 +553,12 @@ export default {
|
|||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
// 对项目进行排序和优化布局
|
// 根据排序模式选择不同的排序方法
|
||||||
return this.optimizeGridLayout(items);
|
if (this.dynamicSort) {
|
||||||
|
return this.optimizeGridLayout(items);
|
||||||
|
} else {
|
||||||
|
return this.fixedGridLayout(items);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
attendanceVisible() {
|
attendanceVisible() {
|
||||||
return this.studentList.length > 0;
|
return this.studentList.length > 0;
|
||||||
@ -435,6 +576,18 @@ export default {
|
|||||||
.filter(subject => !subject.content)
|
.filter(subject => !subject.content)
|
||||||
.sort((a, b) => a.order - b.order);
|
.sort((a, b) => a.order - b.order);
|
||||||
},
|
},
|
||||||
|
autoSave() {
|
||||||
|
return getSetting('edit.autoSave');
|
||||||
|
},
|
||||||
|
refreshBeforeEdit() {
|
||||||
|
return getSetting('edit.refreshBeforeEdit');
|
||||||
|
},
|
||||||
|
emptySubjectDisplay() {
|
||||||
|
return getSetting('display.emptySubjectDisplay');
|
||||||
|
},
|
||||||
|
dynamicSort() {
|
||||||
|
return getSetting('display.dynamicSort');
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
async mounted() {
|
async mounted() {
|
||||||
@ -442,14 +595,26 @@ export default {
|
|||||||
this.updateBackendUrl();
|
this.updateBackendUrl();
|
||||||
await this.initializeData();
|
await this.initializeData();
|
||||||
this.setupAutoRefresh();
|
this.setupAutoRefresh();
|
||||||
this.autoSave = localStorage.getItem('autoSave') === 'true';
|
|
||||||
this.refreshBeforeEdit = localStorage.getItem('refreshBeforeEdit') === 'true';
|
// 监听设置变化
|
||||||
|
this.unwatchSettings = watchSettings(() => {
|
||||||
|
this.updateSettings();
|
||||||
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("初始化失败:", err);
|
console.error("初始化失败:", err);
|
||||||
this.showError("初始化失败,请刷新页面重试");
|
this.showError("初始化失败,请刷新页面重试");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
beforeUnmount() {
|
||||||
|
if (this.unwatchSettings) {
|
||||||
|
this.unwatchSettings();
|
||||||
|
}
|
||||||
|
if (this.refreshInterval) {
|
||||||
|
clearInterval(this.refreshInterval);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
async initializeData() {
|
async initializeData() {
|
||||||
const res = await axios.get(`${this.backurl}/config`);
|
const res = await axios.get(`${this.backurl}/config`);
|
||||||
@ -593,7 +758,7 @@ export default {
|
|||||||
this.contentStyle = {
|
this.contentStyle = {
|
||||||
"font-size": `${this.fontSize}px`,
|
"font-size": `${this.fontSize}px`,
|
||||||
};
|
};
|
||||||
localStorage.setItem('fontSize', this.fontSize.toString());
|
setSetting('font.size', this.fontSize);
|
||||||
},
|
},
|
||||||
|
|
||||||
async uploadData() {
|
async uploadData() {
|
||||||
@ -630,64 +795,42 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
async downloadDataDirectly() {
|
async downloadDataDirectly() {
|
||||||
const formattedDate = new Date(this.dateString).toISOString().split('T')[0];
|
try {
|
||||||
const res = await axios.get(
|
const formattedDate = new Date(this.dateString).toISOString().split('T')[0];
|
||||||
`${this.backurl}/homework?date=${formattedDate}`
|
const res = await axios.get(
|
||||||
);
|
`${this.backurl}/homework?date=${formattedDate}`
|
||||||
this.homeworkData = res.data.data || this.homeworkData;
|
);
|
||||||
|
|
||||||
const subjectOrder = [
|
// 检查是否是今天的日期
|
||||||
"语文",
|
const today = new Date();
|
||||||
"数学",
|
const isToday = today.toISOString().split('T')[0] === formattedDate;
|
||||||
"英语",
|
this.isToday = isToday;
|
||||||
"物理",
|
|
||||||
"化学",
|
|
||||||
"生物",
|
|
||||||
"政治",
|
|
||||||
"历史",
|
|
||||||
"地理",
|
|
||||||
"其他",
|
|
||||||
];
|
|
||||||
// 1. 将对象转换成数组,方便排序
|
|
||||||
let sortedSubjects = Object.keys(this.homeworkData).map((key) => ({
|
|
||||||
key,
|
|
||||||
...this.homeworkData[key],
|
|
||||||
}));
|
|
||||||
|
|
||||||
// 2. 按照指定的顺序排序
|
if (!res.data.status) {
|
||||||
sortedSubjects.sort((a, b) => {
|
this.showNoDataMessage = true;
|
||||||
const indexA = subjectOrder.indexOf(a.key);
|
this.noDataMessage = res.data.msg || '未找到数据';
|
||||||
const indexB = subjectOrder.indexOf(b.key);
|
// 不再初始化作业数据,保持为空
|
||||||
|
this.homeworkData = {};
|
||||||
// 优先根据学科顺序排序
|
this.selectedSet.clear();
|
||||||
if (indexA !== indexB) {
|
this.lateSet.clear();
|
||||||
return indexA - indexB;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
return 0;
|
// 有数据的情况
|
||||||
});
|
this.showNoDataMessage = false;
|
||||||
|
this.homeworkData = res.data.data || {};
|
||||||
// 3. 进一步排序:将 content 为空的项移动到最后
|
this.selectedSet = new Set(res.data.attendance || []);
|
||||||
sortedSubjects = [
|
this.lateSet = new Set(res.data.late || []);
|
||||||
// 先放置有 content 的项
|
this.synced = true;
|
||||||
...sortedSubjects.filter((item) => item.content != ""),
|
} catch (error) {
|
||||||
// 再放置 content 为空的项
|
console.error('下载数据失败:', error);
|
||||||
...sortedSubjects.filter((item) => item.content == ""),
|
this.showError('下载数据失败,请重试');
|
||||||
];
|
}
|
||||||
|
|
||||||
// 4. 将排序后的数组转换回对象
|
|
||||||
this.homeworkData = sortedSubjects.reduce((acc, curr) => {
|
|
||||||
acc[curr.key] = { name: curr.name, content: curr.content };
|
|
||||||
return acc;
|
|
||||||
}, {});
|
|
||||||
this.selectedSet = new Set(res.data.attendance || []);
|
|
||||||
this.lateSet = new Set(res.data.late || []); // Initialize late set
|
|
||||||
this.synced = true;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
updateBackendUrl() {
|
updateBackendUrl() {
|
||||||
const domain = localStorage.getItem('backendServerDomain');
|
const domain = getSetting('server.domain');
|
||||||
const classNum = localStorage.getItem('classNumber');
|
const classNum = getSetting('server.classNumber');
|
||||||
|
|
||||||
if (domain && classNum) {
|
if (domain && classNum) {
|
||||||
this.backurl = `${domain}/${classNum}`;
|
this.backurl = `${domain}/${classNum}`;
|
||||||
@ -696,8 +839,12 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
setupAutoRefresh() {
|
setupAutoRefresh() {
|
||||||
const autoRefresh = localStorage.getItem('autoRefresh') === 'true';
|
const autoRefresh = getSetting('refresh.auto');
|
||||||
const interval = parseInt(localStorage.getItem('refreshInterval')) || 300;
|
const interval = getSetting('refresh.interval');
|
||||||
|
|
||||||
|
if (this.refreshInterval) {
|
||||||
|
clearInterval(this.refreshInterval);
|
||||||
|
}
|
||||||
|
|
||||||
if (autoRefresh) {
|
if (autoRefresh) {
|
||||||
this.refreshInterval = setInterval(() => {
|
this.refreshInterval = setInterval(() => {
|
||||||
@ -706,6 +853,18 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
updateSettings() {
|
||||||
|
// 更新字体大小
|
||||||
|
this.fontSize = getSetting('font.size');
|
||||||
|
this.contentStyle = { "font-size": `${this.fontSize}px` };
|
||||||
|
|
||||||
|
// 更新自动刷新
|
||||||
|
this.setupAutoRefresh();
|
||||||
|
|
||||||
|
// 更新服务器设置
|
||||||
|
this.updateBackendUrl();
|
||||||
|
},
|
||||||
|
|
||||||
handleDateSelect(newDate) {
|
handleDateSelect(newDate) {
|
||||||
if (newDate) {
|
if (newDate) {
|
||||||
// 使用本地时区处理日期,避免时区偏移
|
// 使用本地时区处理日期,避免时区偏移
|
||||||
@ -721,12 +880,6 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
beforeDestroy() {
|
|
||||||
if (this.refreshInterval) {
|
|
||||||
clearInterval(this.refreshInterval);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
optimizeGridLayout(items) {
|
optimizeGridLayout(items) {
|
||||||
// 首先按内容长度和科目顺序排序
|
// 首先按内容长度和科目顺序排序
|
||||||
const sortedItems = items.sort((a, b) => {
|
const sortedItems = items.sort((a, b) => {
|
||||||
@ -760,7 +913,73 @@ export default {
|
|||||||
...item,
|
...item,
|
||||||
order: index
|
order: index
|
||||||
}));
|
}));
|
||||||
}
|
},
|
||||||
|
|
||||||
|
fixedGridLayout(items) {
|
||||||
|
// 定义每行的科目顺序
|
||||||
|
const rowSubjects = [
|
||||||
|
["语文", "数学", "英语"],
|
||||||
|
["物理", "化学", "生物"],
|
||||||
|
["政治", "历史", "地理", "其他"]
|
||||||
|
];
|
||||||
|
|
||||||
|
// 按照固定布局排序
|
||||||
|
return items.sort((a, b) => {
|
||||||
|
// 找出科目所在的行号
|
||||||
|
const getRowIndex = (subject) => {
|
||||||
|
for (let i = 0; i < rowSubjects.length; i++) {
|
||||||
|
if (rowSubjects[i].includes(subject)) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rowSubjects.length; // 其他科目放最后
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取科目在行内的位置
|
||||||
|
const getColumnIndex = (subject) => {
|
||||||
|
for (const row of rowSubjects) {
|
||||||
|
const index = row.indexOf(subject);
|
||||||
|
if (index !== -1) return index;
|
||||||
|
}
|
||||||
|
return 999; // 未知科目放最后
|
||||||
|
};
|
||||||
|
|
||||||
|
const rowA = getRowIndex(a.key);
|
||||||
|
const rowB = getRowIndex(b.key);
|
||||||
|
|
||||||
|
if (rowA !== rowB) {
|
||||||
|
return rowA - rowB;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 同一行内按照预定义顺序排序
|
||||||
|
const colA = getColumnIndex(a.key);
|
||||||
|
const colB = getColumnIndex(b.key);
|
||||||
|
return colA - colB;
|
||||||
|
}).map((item, index) => ({
|
||||||
|
...item,
|
||||||
|
order: index,
|
||||||
|
// 固定布局时每个卡片占用相同的行高
|
||||||
|
rowSpan: item.content ? 2 : 1
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
|
||||||
|
initializeAndOpenDialog() {
|
||||||
|
// 初始化作业数据
|
||||||
|
this.initializeHomeworkData();
|
||||||
|
this.showNoDataMessage = false;
|
||||||
|
this.synced = false; // 设置为未同步状态
|
||||||
|
|
||||||
|
// 如果启用了自动保存,初始化后自动上传
|
||||||
|
if (this.autoSave) {
|
||||||
|
this.uploadData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 打开第一个科目的对话框
|
||||||
|
const firstSubject = Object.keys(this.homeworkData)[0];
|
||||||
|
if (firstSubject) {
|
||||||
|
this.openDialog(firstSubject);
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
|
@ -24,7 +24,7 @@
|
|||||||
|
|
||||||
<v-card-text>
|
<v-card-text>
|
||||||
<v-text-field
|
<v-text-field
|
||||||
v-model="serverDomain"
|
v-model="settings.server.domain"
|
||||||
label="服务器域名"
|
label="服务器域名"
|
||||||
placeholder="例如: http://example.com"
|
placeholder="例如: http://example.com"
|
||||||
prepend-inner-icon="mdi-web"
|
prepend-inner-icon="mdi-web"
|
||||||
@ -34,7 +34,7 @@
|
|||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
<v-text-field
|
<v-text-field
|
||||||
v-model="classNumber"
|
v-model="settings.server.classNumber"
|
||||||
label="班号"
|
label="班号"
|
||||||
placeholder="例如: 1 或 A"
|
placeholder="例如: 1 或 A"
|
||||||
prepend-inner-icon="mdi-account-group"
|
prepend-inner-icon="mdi-account-group"
|
||||||
@ -50,7 +50,7 @@
|
|||||||
color="primary"
|
color="primary"
|
||||||
prepend-icon="mdi-content-save"
|
prepend-icon="mdi-content-save"
|
||||||
block
|
block
|
||||||
@click="saveServerSettings"
|
@click="saveSettings('server')"
|
||||||
>
|
>
|
||||||
保存设置
|
保存设置
|
||||||
</v-btn>
|
</v-btn>
|
||||||
@ -71,18 +71,18 @@
|
|||||||
|
|
||||||
<v-card-text>
|
<v-card-text>
|
||||||
<v-switch
|
<v-switch
|
||||||
v-model="autoRefresh"
|
v-model="settings.refresh.auto"
|
||||||
label="启用自动刷新"
|
label="启用自动刷新"
|
||||||
color="primary"
|
color="primary"
|
||||||
hide-details
|
hide-details
|
||||||
class="mb-4"
|
class="mb-4"
|
||||||
/>
|
/>
|
||||||
<v-text-field
|
<v-text-field
|
||||||
v-model="refreshInterval"
|
v-model="settings.refresh.interval"
|
||||||
type="number"
|
type="number"
|
||||||
label="刷新间隔"
|
label="刷新间隔"
|
||||||
suffix="秒"
|
suffix="秒"
|
||||||
:disabled="!autoRefresh"
|
:disabled="!settings.refresh.auto"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
density="comfortable"
|
density="comfortable"
|
||||||
:rules="[
|
:rules="[
|
||||||
@ -97,7 +97,7 @@
|
|||||||
color="primary"
|
color="primary"
|
||||||
prepend-icon="mdi-content-save"
|
prepend-icon="mdi-content-save"
|
||||||
block
|
block
|
||||||
@click="saveRefreshSettings"
|
@click="saveSettings('refresh')"
|
||||||
>
|
>
|
||||||
保存设置
|
保存设置
|
||||||
</v-btn>
|
</v-btn>
|
||||||
@ -116,7 +116,7 @@
|
|||||||
|
|
||||||
<v-card-text>
|
<v-card-text>
|
||||||
<v-text-field
|
<v-text-field
|
||||||
v-model="fontSize"
|
v-model="settings.font.size"
|
||||||
type="number"
|
type="number"
|
||||||
label="字体大小"
|
label="字体大小"
|
||||||
suffix="px"
|
suffix="px"
|
||||||
@ -144,7 +144,7 @@
|
|||||||
color="primary"
|
color="primary"
|
||||||
prepend-icon="mdi-content-save"
|
prepend-icon="mdi-content-save"
|
||||||
class="flex-grow-1"
|
class="flex-grow-1"
|
||||||
@click="saveFontSize"
|
@click="saveSettings('font')"
|
||||||
>
|
>
|
||||||
保存设置
|
保存设置
|
||||||
</v-btn>
|
</v-btn>
|
||||||
@ -163,7 +163,7 @@
|
|||||||
|
|
||||||
<v-card-text>
|
<v-card-text>
|
||||||
<v-switch
|
<v-switch
|
||||||
v-model="autoSave"
|
v-model="settings.edit.autoSave"
|
||||||
label="启用自动保存"
|
label="启用自动保存"
|
||||||
color="primary"
|
color="primary"
|
||||||
hide-details
|
hide-details
|
||||||
@ -185,7 +185,7 @@
|
|||||||
</v-switch>
|
</v-switch>
|
||||||
|
|
||||||
<v-switch
|
<v-switch
|
||||||
v-model="refreshBeforeEdit"
|
v-model="settings.edit.refreshBeforeEdit"
|
||||||
label="编辑前自动刷新"
|
label="编辑前自动刷新"
|
||||||
color="primary"
|
color="primary"
|
||||||
hide-details
|
hide-details
|
||||||
@ -211,13 +211,113 @@
|
|||||||
color="primary"
|
color="primary"
|
||||||
prepend-icon="mdi-content-save"
|
prepend-icon="mdi-content-save"
|
||||||
block
|
block
|
||||||
@click="saveEditSettings"
|
@click="saveSettings('edit')"
|
||||||
>
|
>
|
||||||
保存设置
|
保存设置
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</v-card-actions>
|
</v-card-actions>
|
||||||
</v-card>
|
</v-card>
|
||||||
</v-col>
|
</v-col> <v-col cols="12" md="6">
|
||||||
|
<v-card elevation="2" class="rounded-lg">
|
||||||
|
<v-card-item>
|
||||||
|
<template v-slot:prepend>
|
||||||
|
<v-icon icon="mdi-card-outline" size="large" class="mr-2" />
|
||||||
|
</template>
|
||||||
|
<v-card-title class="text-h6">显示设置</v-card-title>
|
||||||
|
</v-card-item>
|
||||||
|
|
||||||
|
<v-card-text>
|
||||||
|
<v-switch
|
||||||
|
v-model="settings.display.dynamicSort"
|
||||||
|
label="启用动态排序"
|
||||||
|
hint="动态排序会根据内容长度自动调整卡片位置以优化显示效果"
|
||||||
|
persistent-hint
|
||||||
|
class="mb-4"
|
||||||
|
@change="saveSettings('display')"
|
||||||
|
>
|
||||||
|
<template v-slot:append>
|
||||||
|
<v-tooltip location="right">
|
||||||
|
<template v-slot:activator="{ props }">
|
||||||
|
<v-icon
|
||||||
|
v-bind="props"
|
||||||
|
icon="mdi-help-circle-outline"
|
||||||
|
size="small"
|
||||||
|
class="ml-2"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<div>
|
||||||
|
<p>启用:根据内容长度动态调整位置</p>
|
||||||
|
<p>关闭:按语数英/物化生/政史地固定排列</p>
|
||||||
|
</div>
|
||||||
|
</v-tooltip>
|
||||||
|
</template>
|
||||||
|
</v-switch>
|
||||||
|
|
||||||
|
<v-divider class="my-4" />
|
||||||
|
|
||||||
|
<v-radio-group
|
||||||
|
v-model="settings.display.emptySubjectDisplay"
|
||||||
|
label="空作业显示方式"
|
||||||
|
class="mt-4"
|
||||||
|
@change="saveSettings('display')"
|
||||||
|
>
|
||||||
|
<v-radio
|
||||||
|
value="card"
|
||||||
|
label="显示为空卡片"
|
||||||
|
>
|
||||||
|
<template v-slot:label>
|
||||||
|
<div class="d-flex align-center">
|
||||||
|
显示为空卡片
|
||||||
|
<v-tooltip location="right">
|
||||||
|
<template v-slot:activator="{ props }">
|
||||||
|
<v-icon
|
||||||
|
v-bind="props"
|
||||||
|
icon="mdi-help-circle-outline"
|
||||||
|
size="small"
|
||||||
|
class="ml-2"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
在主界面中显示为可点击的空白卡片
|
||||||
|
</v-tooltip>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</v-radio>
|
||||||
|
<v-radio
|
||||||
|
value="button"
|
||||||
|
label="显示为按钮组"
|
||||||
|
>
|
||||||
|
<template v-slot:label>
|
||||||
|
<div class="d-flex align-center">
|
||||||
|
显示为按钮组
|
||||||
|
<v-tooltip location="right">
|
||||||
|
<template v-slot:activator="{ props }">
|
||||||
|
<v-icon
|
||||||
|
v-bind="props"
|
||||||
|
icon="mdi-help-circle-outline"
|
||||||
|
size="small"
|
||||||
|
class="ml-2"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
在主界面底部显示为一组添加按钮
|
||||||
|
</v-tooltip>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</v-radio>
|
||||||
|
</v-radio-group>
|
||||||
|
</v-card-text>
|
||||||
|
|
||||||
|
<v-card-actions class="px-4 pb-4">
|
||||||
|
<v-btn
|
||||||
|
color="primary"
|
||||||
|
prepend-icon="mdi-content-save"
|
||||||
|
block
|
||||||
|
@click="saveSettings('display')"
|
||||||
|
>
|
||||||
|
保存设置
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
<v-col cols="12">
|
<v-col cols="12">
|
||||||
<v-card elevation="2" class="rounded-lg">
|
<v-card elevation="2" class="rounded-lg">
|
||||||
<v-card-item>
|
<v-card-item>
|
||||||
@ -238,6 +338,23 @@
|
|||||||
</v-card-item>
|
</v-card-item>
|
||||||
|
|
||||||
<v-card-text>
|
<v-card-text>
|
||||||
|
<v-progress-linear
|
||||||
|
v-if="studentsLoading"
|
||||||
|
indeterminate
|
||||||
|
color="primary"
|
||||||
|
class="mb-4"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<v-alert
|
||||||
|
v-if="studentsError"
|
||||||
|
type="error"
|
||||||
|
variant="tonal"
|
||||||
|
closable
|
||||||
|
class="mb-4"
|
||||||
|
>
|
||||||
|
{{ studentsError }}
|
||||||
|
</v-alert>
|
||||||
|
|
||||||
<v-expand-transition>
|
<v-expand-transition>
|
||||||
<div v-if="!showAdvancedEdit">
|
<div v-if="!showAdvancedEdit">
|
||||||
<v-row class="mb-6">
|
<v-row class="mb-6">
|
||||||
@ -402,6 +519,8 @@
|
|||||||
color="primary"
|
color="primary"
|
||||||
prepend-icon="mdi-content-save"
|
prepend-icon="mdi-content-save"
|
||||||
size="large"
|
size="large"
|
||||||
|
:loading="studentsLoading"
|
||||||
|
:disabled="studentsLoading"
|
||||||
@click="saveStudents"
|
@click="saveStudents"
|
||||||
>
|
>
|
||||||
保存学生列表
|
保存学生列表
|
||||||
@ -411,6 +530,8 @@
|
|||||||
variant="outlined"
|
variant="outlined"
|
||||||
prepend-icon="mdi-refresh"
|
prepend-icon="mdi-refresh"
|
||||||
size="large"
|
size="large"
|
||||||
|
:loading="studentsLoading"
|
||||||
|
:disabled="studentsLoading"
|
||||||
@click="reloadStudentList"
|
@click="reloadStudentList"
|
||||||
>
|
>
|
||||||
重置列表
|
重置列表
|
||||||
@ -484,86 +605,6 @@
|
|||||||
</v-card>
|
</v-card>
|
||||||
</v-col>
|
</v-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
|
|
||||||
<v-switch
|
|
||||||
v-model="showEmptySubjects"
|
|
||||||
label="显示空作业科目"
|
|
||||||
hint="是否在主界面显示没有作业内容的科目"
|
|
||||||
persistent-hint
|
|
||||||
@change="saveSettings"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<v-col cols="12" md="6">
|
|
||||||
<v-card elevation="2" class="rounded-lg">
|
|
||||||
<v-card-item>
|
|
||||||
<template v-slot:prepend>
|
|
||||||
<v-icon icon="mdi-card-outline" size="large" class="mr-2" />
|
|
||||||
</template>
|
|
||||||
<v-card-title class="text-h6">空作业显示设置</v-card-title>
|
|
||||||
</v-card-item>
|
|
||||||
|
|
||||||
<v-card-text>
|
|
||||||
<v-radio-group
|
|
||||||
v-model="emptySubjectDisplay"
|
|
||||||
label="空作业显示方式"
|
|
||||||
>
|
|
||||||
<v-radio
|
|
||||||
value="card"
|
|
||||||
label="显示为空卡片"
|
|
||||||
>
|
|
||||||
<template v-slot:label>
|
|
||||||
<div class="d-flex align-center">
|
|
||||||
显示为空卡片
|
|
||||||
<v-tooltip location="right">
|
|
||||||
<template v-slot:activator="{ props }">
|
|
||||||
<v-icon
|
|
||||||
v-bind="props"
|
|
||||||
icon="mdi-help-circle-outline"
|
|
||||||
size="small"
|
|
||||||
class="ml-2"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
在主界面中显示为可点击的空白卡片
|
|
||||||
</v-tooltip>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</v-radio>
|
|
||||||
<v-radio
|
|
||||||
value="button"
|
|
||||||
label="显示为按钮组"
|
|
||||||
>
|
|
||||||
<template v-slot:label>
|
|
||||||
<div class="d-flex align-center">
|
|
||||||
显示为按钮组
|
|
||||||
<v-tooltip location="right">
|
|
||||||
<template v-slot:activator="{ props }">
|
|
||||||
<v-icon
|
|
||||||
v-bind="props"
|
|
||||||
icon="mdi-help-circle-outline"
|
|
||||||
size="small"
|
|
||||||
class="ml-2"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
在主界面底部显示为一组添加按钮
|
|
||||||
</v-tooltip>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</v-radio>
|
|
||||||
</v-radio-group>
|
|
||||||
</v-card-text>
|
|
||||||
|
|
||||||
<v-card-actions class="px-4 pb-4">
|
|
||||||
<v-btn
|
|
||||||
color="primary"
|
|
||||||
prepend-icon="mdi-content-save"
|
|
||||||
block
|
|
||||||
@click="saveEmptySubjectSettings"
|
|
||||||
>
|
|
||||||
保存设置
|
|
||||||
</v-btn>
|
|
||||||
</v-card-actions>
|
|
||||||
</v-card>
|
|
||||||
</v-col>
|
|
||||||
</v-container>
|
</v-container>
|
||||||
|
|
||||||
<v-snackbar v-model="snackbar">
|
<v-snackbar v-model="snackbar">
|
||||||
@ -624,6 +665,12 @@
|
|||||||
<script>
|
<script>
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { useDisplay } from 'vuetify';
|
import { useDisplay } from 'vuetify';
|
||||||
|
import {
|
||||||
|
getSetting,
|
||||||
|
setSetting,
|
||||||
|
resetSetting,
|
||||||
|
watchSettings
|
||||||
|
} from '@/utils/settings';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
setup() {
|
setup() {
|
||||||
@ -633,17 +680,31 @@ export default {
|
|||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
serverDomain: '',
|
settings: {
|
||||||
classNumber: '',
|
server: {
|
||||||
|
domain: getSetting('server.domain'),
|
||||||
|
classNumber: getSetting('server.classNumber'),
|
||||||
|
},
|
||||||
|
refresh: {
|
||||||
|
auto: getSetting('refresh.auto'),
|
||||||
|
interval: getSetting('refresh.interval'),
|
||||||
|
},
|
||||||
|
font: {
|
||||||
|
size: getSetting('font.size'),
|
||||||
|
},
|
||||||
|
edit: {
|
||||||
|
autoSave: getSetting('edit.autoSave'),
|
||||||
|
refreshBeforeEdit: getSetting('edit.refreshBeforeEdit'),
|
||||||
|
},
|
||||||
|
display: {
|
||||||
|
emptySubjectDisplay: getSetting('display.emptySubjectDisplay'),
|
||||||
|
dynamicSort: getSetting('display.dynamicSort'),
|
||||||
|
}
|
||||||
|
},
|
||||||
students: '',
|
students: '',
|
||||||
studentsList: [],
|
studentsList: [],
|
||||||
snackbar: false,
|
snackbar: false,
|
||||||
snackbarText: '',
|
snackbarText: '',
|
||||||
autoRefresh: false,
|
|
||||||
refreshInterval: 300,
|
|
||||||
fontSize: '28',
|
|
||||||
autoSave: false,
|
|
||||||
refreshBeforeEdit: false,
|
|
||||||
showAdvancedEdit: false,
|
showAdvancedEdit: false,
|
||||||
newStudent: '',
|
newStudent: '',
|
||||||
editingIndex: -1,
|
editingIndex: -1,
|
||||||
@ -655,16 +716,58 @@ export default {
|
|||||||
studentToMove: null,
|
studentToMove: null,
|
||||||
touchStartTime: 0,
|
touchStartTime: 0,
|
||||||
touchTimeout: null,
|
touchTimeout: null,
|
||||||
showEmptySubjects: localStorage.getItem('showEmptySubjects') === 'true',
|
studentsLoading: false,
|
||||||
emptySubjectDisplay: localStorage.getItem('emptySubjectDisplay') || 'card',
|
studentsError: null,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
this.loadSettings();
|
this.loadAllSettings();
|
||||||
|
this.unwatchSettings = watchSettings(() => {
|
||||||
|
this.loadAllSettings();
|
||||||
|
});
|
||||||
|
this.loadStudentList();
|
||||||
|
},
|
||||||
|
|
||||||
|
beforeUnmount() {
|
||||||
|
if (this.unwatchSettings) {
|
||||||
|
this.unwatchSettings();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
|
'settings.server': {
|
||||||
|
handler(newVal, oldVal) {
|
||||||
|
if (newVal.domain !== oldVal?.domain || newVal.classNumber !== oldVal?.classNumber) {
|
||||||
|
this.loadStudentList();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
deep: true
|
||||||
|
},
|
||||||
|
'settings.refresh': {
|
||||||
|
handler() {
|
||||||
|
this.saveSettings('refresh');
|
||||||
|
},
|
||||||
|
deep: true
|
||||||
|
},
|
||||||
|
'settings.font': {
|
||||||
|
handler() {
|
||||||
|
this.saveSettings('font');
|
||||||
|
},
|
||||||
|
deep: true
|
||||||
|
},
|
||||||
|
'settings.edit': {
|
||||||
|
handler() {
|
||||||
|
this.saveSettings('edit');
|
||||||
|
},
|
||||||
|
deep: true
|
||||||
|
},
|
||||||
|
'settings.display': {
|
||||||
|
handler() {
|
||||||
|
this.saveSettings('display');
|
||||||
|
},
|
||||||
|
deep: true
|
||||||
|
},
|
||||||
students: {
|
students: {
|
||||||
handler(newVal) {
|
handler(newVal) {
|
||||||
this.studentsList = newVal.split('\n').filter(s => s.trim());
|
this.studentsList = newVal.split('\n').filter(s => s.trim());
|
||||||
@ -680,64 +783,58 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
loadSettings() {
|
loadAllSettings() {
|
||||||
const savedDomain = localStorage.getItem('backendServerDomain');
|
Object.keys(this.settings).forEach(section => {
|
||||||
const savedClass = localStorage.getItem('classNumber');
|
Object.keys(this.settings[section]).forEach(key => {
|
||||||
|
this.settings[section][key] = getSetting(`${section}.${key}`);
|
||||||
if (savedDomain) {
|
});
|
||||||
this.serverDomain = savedDomain;
|
});
|
||||||
}
|
|
||||||
if (savedClass) {
|
|
||||||
this.classNumber = savedClass;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (localStorage.getItem('studentList')) {
|
|
||||||
this.students = localStorage.getItem('studentList').replace(/,/g, '\n');
|
|
||||||
this.studentsList = this.students.split('\n').filter(s => s.trim());
|
|
||||||
}
|
|
||||||
|
|
||||||
this.autoRefresh = localStorage.getItem('autoRefresh') === 'true';
|
|
||||||
this.refreshInterval = parseInt(localStorage.getItem('refreshInterval')) || 300;
|
|
||||||
|
|
||||||
const savedFontSize = localStorage.getItem('fontSize');
|
|
||||||
if (savedFontSize) {
|
|
||||||
this.fontSize = savedFontSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.autoSave = localStorage.getItem('autoSave') === 'true';
|
|
||||||
this.refreshBeforeEdit = localStorage.getItem('refreshBeforeEdit') === 'true';
|
|
||||||
},
|
},
|
||||||
|
|
||||||
saveServerSettings() {
|
saveSettings(section) {
|
||||||
try {
|
try {
|
||||||
if (this.serverDomain === '') {
|
Object.keys(this.settings[section]).forEach(key => {
|
||||||
localStorage.removeItem('backendServerDomain');
|
setSetting(`${section}.${key}`, this.settings[section][key]);
|
||||||
localStorage.removeItem('classNumber');
|
});
|
||||||
this.showMessage('删除成功');
|
this.showMessage('设置已保存');
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
new URL(this.serverDomain);
|
|
||||||
|
|
||||||
const cleanDomain = this.serverDomain.replace(/\/+$/, '');
|
|
||||||
|
|
||||||
if (!this.classNumber || !/^[A-Za-z0-9]+$/.test(this.classNumber)) {
|
|
||||||
throw new Error('Invalid class number');
|
|
||||||
}
|
|
||||||
|
|
||||||
localStorage.setItem('backendServerDomain', cleanDomain);
|
|
||||||
localStorage.setItem('classNumber', this.classNumber);
|
|
||||||
this.showMessage('保存成功');
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error('保存设置失败:', error);
|
||||||
this.showMessage('保存失败,请检查服务器域名和班号');
|
this.showMessage('保存设置失败,请检查输入');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async loadStudentList() {
|
||||||
|
try {
|
||||||
|
this.studentsLoading = true;
|
||||||
|
this.studentsError = null;
|
||||||
|
|
||||||
|
const domain = getSetting('server.domain');
|
||||||
|
const classNum = getSetting('server.classNumber');
|
||||||
|
|
||||||
|
if (!domain || !classNum) {
|
||||||
|
throw new Error('请先设置服务器域名和班号');
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await axios.get(`${domain}/${classNum}/config`);
|
||||||
|
if (res.data && Array.isArray(res.data.studentList)) {
|
||||||
|
this.studentsList = res.data.studentList;
|
||||||
|
this.students = this.studentsList.join('\n');
|
||||||
|
} else {
|
||||||
|
throw new Error('获取学生列表失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载学生列表失败:', error);
|
||||||
|
this.studentsError = error.message || '加载失败,请检查服务器设置';
|
||||||
|
this.showMessage(this.studentsError);
|
||||||
|
} finally {
|
||||||
|
this.studentsLoading = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async saveStudents() {
|
async saveStudents() {
|
||||||
try {
|
try {
|
||||||
const domain = localStorage.getItem('backendServerDomain');
|
const domain = getSetting('server.domain');
|
||||||
const classNum = localStorage.getItem('classNumber');
|
const classNum = getSetting('server.classNumber');
|
||||||
|
|
||||||
if (!domain || !classNum) {
|
if (!domain || !classNum) {
|
||||||
throw new Error('请先设置服务器域名和班号');
|
throw new Error('请先设置服务器域名和班号');
|
||||||
@ -747,78 +844,21 @@ export default {
|
|||||||
studentList: this.studentsList,
|
studentList: this.studentsList,
|
||||||
id: 1,
|
id: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
localStorage.setItem('studentList', this.studentsList.join(','));
|
localStorage.setItem('studentList', this.studentsList.join(','));
|
||||||
this.showMessage('保存成功');
|
this.showMessage('保存成功');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error('保存学生列表失败:', error);
|
||||||
this.showMessage(error.message || '保存失败,请检查服务器设置和学生列表');
|
this.showMessage(error.message || '保存失败,请检查服务器设置和学生列表');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
saveRefreshSettings() {
|
async reloadStudentList() {
|
||||||
localStorage.setItem('autoRefresh', this.autoRefresh);
|
|
||||||
localStorage.setItem('refreshInterval', this.refreshInterval);
|
|
||||||
this.showMessage('保存成功');
|
|
||||||
},
|
|
||||||
|
|
||||||
saveFontSize() {
|
|
||||||
try {
|
try {
|
||||||
const size = parseInt(this.fontSize);
|
await this.loadStudentList();
|
||||||
if (size >= 16 && size <= 100) {
|
this.showMessage('已重新加载学生列表');
|
||||||
localStorage.setItem('fontSize', size.toString());
|
|
||||||
this.showMessage('字体大小保存成功');
|
|
||||||
} else {
|
|
||||||
throw new Error('Invalid font size');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.showMessage('保存失败,字体大小必须在16-100之间');
|
this.showMessage('重新加载失败');
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
resetFontSize() {
|
|
||||||
localStorage.removeItem('fontSize');
|
|
||||||
this.fontSize = '28';
|
|
||||||
this.showMessage('字体大小已重置为默认值');
|
|
||||||
},
|
|
||||||
|
|
||||||
saveEditSettings() {
|
|
||||||
localStorage.setItem('autoSave', this.autoSave);
|
|
||||||
localStorage.setItem('refreshBeforeEdit', this.refreshBeforeEdit);
|
|
||||||
this.showMessage(
|
|
||||||
this.autoSave
|
|
||||||
? '已启用自动保存'
|
|
||||||
: this.refreshBeforeEdit
|
|
||||||
? '已启用编辑前刷新'
|
|
||||||
: '已更新编辑设置'
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
addStudent() {
|
|
||||||
const student = this.newStudent.trim();
|
|
||||||
if (student && !this.studentsList.includes(student)) {
|
|
||||||
this.studentsList.push(student);
|
|
||||||
this.newStudent = '';
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
removeStudent(index) {
|
|
||||||
if (index !== undefined) {
|
|
||||||
this.studentsList.splice(index, 1);
|
|
||||||
this.deleteDialog = false;
|
|
||||||
this.studentToDelete = null;
|
|
||||||
this.synced = false;
|
|
||||||
if (this.autoSave) this.saveStudents();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
reloadStudentList() {
|
|
||||||
const savedList = localStorage.getItem('studentList');
|
|
||||||
if (savedList) {
|
|
||||||
this.students = savedList.replace(/,/g, '\n');
|
|
||||||
} else {
|
|
||||||
this.students = '';
|
|
||||||
this.studentsList = [];
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -841,7 +881,7 @@ export default {
|
|||||||
const newName = this.editingName.trim();
|
const newName = this.editingName.trim();
|
||||||
if (newName && newName !== this.studentsList[this.editingIndex]) {
|
if (newName && newName !== this.studentsList[this.editingIndex]) {
|
||||||
this.studentsList[this.editingIndex] = newName;
|
this.studentsList[this.editingIndex] = newName;
|
||||||
if (this.autoSave) {
|
if (this.settings.edit.autoSave) {
|
||||||
this.saveStudents();
|
this.saveStudents();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -864,7 +904,7 @@ export default {
|
|||||||
[this.studentsList[index], this.studentsList[newIndex]] =
|
[this.studentsList[index], this.studentsList[newIndex]] =
|
||||||
[this.studentsList[newIndex], this.studentsList[index]];
|
[this.studentsList[newIndex], this.studentsList[index]];
|
||||||
|
|
||||||
if (this.autoSave) {
|
if (this.settings.edit.autoSave) {
|
||||||
this.saveStudents();
|
this.saveStudents();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -887,8 +927,7 @@ export default {
|
|||||||
const student = this.studentsList[this.studentToMove];
|
const student = this.studentsList[this.studentToMove];
|
||||||
this.studentsList.splice(this.studentToMove, 1);
|
this.studentsList.splice(this.studentToMove, 1);
|
||||||
this.studentsList.splice(newPos, 0, student);
|
this.studentsList.splice(newPos, 0, student);
|
||||||
this.synced = false;
|
if (this.settings.edit.autoSave) this.saveStudents();
|
||||||
if (this.autoSave) this.saveStudents();
|
|
||||||
}
|
}
|
||||||
this.numberDialog = false;
|
this.numberDialog = false;
|
||||||
this.studentToMove = null;
|
this.studentToMove = null;
|
||||||
@ -901,21 +940,37 @@ export default {
|
|||||||
this.studentsList.splice(index, 1);
|
this.studentsList.splice(index, 1);
|
||||||
this.studentsList.unshift(student);
|
this.studentsList.unshift(student);
|
||||||
|
|
||||||
if (this.autoSave) {
|
if (this.settings.edit.autoSave) {
|
||||||
this.saveStudents();
|
this.saveStudents();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
saveSettings() {
|
addStudent() {
|
||||||
localStorage.setItem('showEmptySubjects', this.showEmptySubjects.toString());
|
const student = this.newStudent.trim();
|
||||||
localStorage.setItem('emptySubjectDisplay', this.emptySubjectDisplay);
|
if (student && !this.studentsList.includes(student)) {
|
||||||
|
this.studentsList.push(student);
|
||||||
|
this.newStudent = '';
|
||||||
|
if (this.settings.edit.autoSave) {
|
||||||
|
this.saveStudents();
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
saveEmptySubjectSettings() {
|
removeStudent(index) {
|
||||||
this.saveSettings();
|
if (index !== undefined) {
|
||||||
this.showMessage('保存成功');
|
this.studentsList.splice(index, 1);
|
||||||
}
|
this.deleteDialog = false;
|
||||||
|
this.studentToDelete = null;
|
||||||
|
if (this.settings.edit.autoSave) this.saveStudents();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
resetFontSize() {
|
||||||
|
resetSetting('font.size');
|
||||||
|
this.settings.font.size = getSetting('font.size');
|
||||||
|
this.showMessage('字体大小已重置为默认值');
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
292
src/utils/settings.js
Normal file
292
src/utils/settings.js
Normal file
@ -0,0 +1,292 @@
|
|||||||
|
/**
|
||||||
|
* 配置项定义
|
||||||
|
* @typedef {Object} SettingDefinition
|
||||||
|
* @property {string} type - 配置项类型 ('boolean' | 'number' | 'string')
|
||||||
|
* @property {any} default - 默认值
|
||||||
|
* @property {Function} [validate] - 可选的验证函数
|
||||||
|
* @property {string} [description] - 配置项描述
|
||||||
|
* @property {string} [legacyKey] - 旧版本localStorage键名(用于迁移)
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 存储所有设置的localStorage键名
|
||||||
|
const SETTINGS_STORAGE_KEY = 'homeworkpage_settings';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 所有配置项的定义
|
||||||
|
* @type {Object.<string, SettingDefinition>}
|
||||||
|
*/
|
||||||
|
const settingsDefinitions = {
|
||||||
|
// 显示设置
|
||||||
|
// 'display.showEmptySubjects': {
|
||||||
|
// type: 'boolean',
|
||||||
|
// default: true,
|
||||||
|
// description: '是否在主界面显示没有作业内容的科目'
|
||||||
|
// },
|
||||||
|
'display.emptySubjectDisplay': {
|
||||||
|
type: 'string',
|
||||||
|
default: 'card',
|
||||||
|
validate: value => ['card', 'button'].includes(value),
|
||||||
|
description: '空科目的显示方式:卡片或按钮'
|
||||||
|
},
|
||||||
|
'display.dynamicSort': {
|
||||||
|
type: 'boolean',
|
||||||
|
default: true,
|
||||||
|
description: '是否启用动态排序以优化显示效果'
|
||||||
|
},
|
||||||
|
|
||||||
|
// 服务器设置
|
||||||
|
'server.domain': {
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
validate: value => !value || /^https?:\/\//.test(value),
|
||||||
|
description: '后端服务器域名'
|
||||||
|
},
|
||||||
|
'server.classNumber': {
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
validate: value => /^[A-Za-z0-9]*$/.test(value),
|
||||||
|
description: '班级编号'
|
||||||
|
},
|
||||||
|
|
||||||
|
// 刷新设置
|
||||||
|
'refresh.auto': {
|
||||||
|
type: 'boolean',
|
||||||
|
default: false,
|
||||||
|
description: '是否启用自动刷新'
|
||||||
|
},
|
||||||
|
'refresh.interval': {
|
||||||
|
type: 'number',
|
||||||
|
default: 300,
|
||||||
|
validate: value => value >= 10 && value <= 3600,
|
||||||
|
description: '自动刷新间隔(秒)'
|
||||||
|
},
|
||||||
|
|
||||||
|
// 字体设置
|
||||||
|
'font.size': {
|
||||||
|
type: 'number',
|
||||||
|
default: 28,
|
||||||
|
validate: value => value >= 16 && value <= 100,
|
||||||
|
description: '字体大小(像素)'
|
||||||
|
},
|
||||||
|
|
||||||
|
// 编辑设置
|
||||||
|
'edit.autoSave': {
|
||||||
|
type: 'boolean',
|
||||||
|
default: false,
|
||||||
|
description: '是否启用自动保存'
|
||||||
|
},
|
||||||
|
'edit.refreshBeforeEdit': {
|
||||||
|
type: 'boolean',
|
||||||
|
default: false,
|
||||||
|
description: '编辑前是否自动刷新'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 内存中缓存的设置值
|
||||||
|
let settingsCache = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从localStorage加载所有设置
|
||||||
|
* @returns {Object} 所有设置的值
|
||||||
|
*/
|
||||||
|
function loadSettings() {
|
||||||
|
try {
|
||||||
|
const stored = localStorage.getItem(SETTINGS_STORAGE_KEY);
|
||||||
|
if (stored) {
|
||||||
|
settingsCache = JSON.parse(stored);
|
||||||
|
} else {
|
||||||
|
// 首次使用或迁移旧数据
|
||||||
|
settingsCache = migrateFromLegacy();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载设置失败:', error);
|
||||||
|
settingsCache = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保所有设置项都有值(使用默认值填充)
|
||||||
|
for (const [key, definition] of Object.entries(settingsDefinitions)) {
|
||||||
|
if (!(key in settingsCache)) {
|
||||||
|
settingsCache[key] = definition.default;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return settingsCache;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从旧版本的localStorage迁移数据
|
||||||
|
*/
|
||||||
|
function migrateFromLegacy() {
|
||||||
|
const settings = {};
|
||||||
|
const legacyKeyMap = {
|
||||||
|
'server.domain': 'backendServerDomain',
|
||||||
|
'server.classNumber': 'classNumber',
|
||||||
|
'refresh.auto': 'autoRefresh',
|
||||||
|
'refresh.interval': 'refreshInterval',
|
||||||
|
'font.size': 'fontSize',
|
||||||
|
'edit.autoSave': 'autoSave',
|
||||||
|
'edit.refreshBeforeEdit': 'refreshBeforeEdit',
|
||||||
|
'display.emptySubjectDisplay': 'emptySubjectDisplay',
|
||||||
|
'display.dynamicSort': 'dynamicSort'
|
||||||
|
};
|
||||||
|
|
||||||
|
// 迁移旧数据
|
||||||
|
for (const [newKey, oldKey] of Object.entries(legacyKeyMap)) {
|
||||||
|
const oldValue = localStorage.getItem(oldKey);
|
||||||
|
if (oldValue !== null) {
|
||||||
|
const definition = settingsDefinitions[newKey];
|
||||||
|
switch (definition.type) {
|
||||||
|
case 'boolean':
|
||||||
|
settings[newKey] = oldValue === 'true';
|
||||||
|
break;
|
||||||
|
case 'number':
|
||||||
|
settings[newKey] = Number(oldValue);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
settings[newKey] = oldValue;
|
||||||
|
}
|
||||||
|
// 可选:删除旧的localStorage项
|
||||||
|
// localStorage.removeItem(oldKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存迁移后的数据
|
||||||
|
localStorage.setItem(SETTINGS_STORAGE_KEY, JSON.stringify(settings));
|
||||||
|
return settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存所有设置到localStorage
|
||||||
|
*/
|
||||||
|
function saveSettings() {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(SETTINGS_STORAGE_KEY, JSON.stringify(settingsCache));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('保存设置失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取设置项的值
|
||||||
|
* @param {string} key - 设置项键名
|
||||||
|
* @returns {any} 设置项的值
|
||||||
|
*/
|
||||||
|
function getSetting(key) {
|
||||||
|
if (!settingsCache) {
|
||||||
|
loadSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
const definition = settingsDefinitions[key];
|
||||||
|
if (!definition) {
|
||||||
|
console.warn(`未定义的设置项: ${key}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const value = settingsCache[key];
|
||||||
|
return value !== undefined ? value : definition.default;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置配置项的值
|
||||||
|
* @param {string} key - 设置项键名
|
||||||
|
* @param {any} value - 要设置的值
|
||||||
|
* @returns {boolean} 是否设置成功
|
||||||
|
*/
|
||||||
|
function setSetting(key, value) {
|
||||||
|
const definition = settingsDefinitions[key];
|
||||||
|
if (!definition) {
|
||||||
|
console.warn(`未定义的设置项: ${key}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 类型转换
|
||||||
|
if (typeof value !== definition.type) {
|
||||||
|
value = definition.type === 'boolean' ? Boolean(value) :
|
||||||
|
definition.type === 'number' ? Number(value) : String(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证
|
||||||
|
if (definition.validate && !definition.validate(value)) {
|
||||||
|
console.warn(`设置项 ${key} 的值无效`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!settingsCache) {
|
||||||
|
loadSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
settingsCache[key] = value;
|
||||||
|
saveSettings();
|
||||||
|
|
||||||
|
// 为了保持向后兼容,同时更新旧的localStorage键
|
||||||
|
const legacyKey = definition.legacyKey;
|
||||||
|
if (legacyKey) {
|
||||||
|
localStorage.setItem(legacyKey, value.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`设置配置项 ${key} 失败:`, error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重置指定设置项到默认值
|
||||||
|
* @param {string} key - 设置项键名
|
||||||
|
*/
|
||||||
|
function resetSetting(key) {
|
||||||
|
const definition = settingsDefinitions[key];
|
||||||
|
if (!definition) {
|
||||||
|
console.warn(`未定义的设置项: ${key}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!settingsCache) {
|
||||||
|
loadSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
settingsCache[key] = definition.default;
|
||||||
|
saveSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重置所有设置项到默认值
|
||||||
|
*/
|
||||||
|
function resetAllSettings() {
|
||||||
|
settingsCache = {};
|
||||||
|
for (const [key, definition] of Object.entries(settingsDefinitions)) {
|
||||||
|
settingsCache[key] = definition.default;
|
||||||
|
}
|
||||||
|
saveSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 监听设置变化
|
||||||
|
* @param {Function} callback - 当设置改变时调用的回调函数
|
||||||
|
* @returns {Function} 取消监听的函数
|
||||||
|
*/
|
||||||
|
function watchSettings(callback) {
|
||||||
|
const handler = (event) => {
|
||||||
|
if (event.key === SETTINGS_STORAGE_KEY) {
|
||||||
|
settingsCache = JSON.parse(event.newValue);
|
||||||
|
callback(settingsCache);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('storage', handler);
|
||||||
|
return () => window.removeEventListener('storage', handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化设置
|
||||||
|
loadSettings();
|
||||||
|
|
||||||
|
export {
|
||||||
|
settingsDefinitions,
|
||||||
|
getSetting,
|
||||||
|
setSetting,
|
||||||
|
resetSetting,
|
||||||
|
resetAllSettings,
|
||||||
|
watchSettings
|
||||||
|
};
|
Loading…
x
Reference in New Issue
Block a user