Files
ClassHomeworkBoard/src/pages/settings.vue
T

704 lines
22 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="settings-page">
<v-app-bar elevation="1">
<template #prepend>
<v-btn
icon="mdi-arrow-left"
variant="text"
@click="$router.push('/')"
/>
</template>
<v-app-bar-title class="text-h6">设置</v-app-bar-title>
</v-app-bar>
<v-container fluid>
<v-navigation-drawer>
<v-list>
<v-list-item
v-for="tab in settingsTabs"
:key="tab.value"
@click="settingsTab = tab.value"
:active="settingsTab === tab.value"
:prepend-icon="tab.icon"
class="rounded-e-xl"
:color="settingsTab === tab.value ? 'primary' : 'default'"
>
<v-list-item-title>{{ tab.title }}</v-list-item-title>
</v-list-item>
</v-list>
</v-navigation-drawer>
<v-tabs-window
v-model="settingsTab"
style="width: 100%"
direction="vertical"
>
<v-tabs-window-item value="index">
<v-card title="Classworks" subtitle="设置" class="rounded-xl"
border
>
<v-card-text>
<v-alert color="error" variant="tonal" icon="mdi-alert-circle"
class="rounded-xl"
>Classworks
是开源免费的软件,官方没有提供任何形式的付费支持服务,源代码仓库地址在
<a
href="https://github.com/ZeroCatDev/Classworks"
target="_blank"
>https://github.com/ZeroCatDev/Classworks</a
>。如果您通过有偿协助等付费方式取得本应用,在遇到问题时请在与卖家约定的服务框架下,优先向卖家求助。如果卖家没有提供您预期的服务,请退款或通过其它形式积极维护您的合法权益。</v-alert
>
<v-alert
class="mt-4 rounded-xl"
color="info"
variant="tonal"
icon="mdi-information"
>请不要使用浏览器清除缓存功能,否则会导致配置丢失。<del
>恶意的操作可能导致您受到贵校教师的处理</del
></v-alert
>
<v-alert
class="mt-4 rounded-xl"
color="warning"
variant="tonal"
icon="mdi-information"
><p>
请不要使用包括但不限于360极速浏览器、360安全浏览器、夸克浏览器、QQ浏览器等浏览器使用
Classworks
,这些浏览器过时且存在严重的一致性问题。在Windows上,使用新版
Microsoft Edge 浏览器是最推荐的选择。
</p>
<p style="color: #666">
上述浏览器商标为其所属公司所有,Classworks™
与上述浏览器所属公司暂无竞争关系。
</p>
<br /><v-btn
href="https://www.microsoft.com/zh-cn/windows/microsoft-edge"
target="_blank"
color="warning"
variant="tonal"
class="text-none rounded-xl"
append-icon="mdi-open-in-new"
>下载 Microsoft Edge(微软边缘浏览器)</v-btn
></v-alert
>
</v-card-text>
</v-card>
</v-tabs-window-item>
<v-tabs-window-item value="server">
<server-settings-card
border
:loading="loading.server"
@saved="onSettingsSaved"
/>
<data-provider-settings-card border class="mt-4" />
</v-tabs-window-item>
<v-tabs-window-item value="namespace">
<namespace-settings-card
border
:loading="loading.namespace"
@saved="onSettingsSaved"
/>
</v-tabs-window-item>
<v-tabs-window-item value="share">
<settings-link-generator border class="mt-4" />
</v-tabs-window-item>
<v-tabs-window-item value="refresh">
<refresh-settings-card
border
:loading="loading.refresh"
@saved="onSettingsSaved"
/>
</v-tabs-window-item>
<v-tabs-window-item value="edit">
<edit-settings-card
border
:loading="loading.edit"
@saved="onSettingsSaved"
/>
</v-tabs-window-item>
<v-tabs-window-item value="display">
<display-settings-card
border
:loading="loading.display"
@saved="onSettingsSaved"
/>
</v-tabs-window-item>
<v-tabs-window-item value="theme">
<theme-settings-card
border
:loading="loading.theme"
@saved="onSettingsSaved"
/>
</v-tabs-window-item>
<v-tabs-window-item value="student">
<student-list-card
border
:loading="loading.students"
:error="studentsError"
:is-mobile="isMobile"
:unsaved-changes="hasUnsavedChanges"
@save="saveStudents"
@reload="loadStudents"
@update:modelValue="handleStudentDataChange"
/>
</v-tabs-window-item>
<v-tabs-window-item value="developer"
><settings-card border title="开发者选项" icon="mdi-developer-board">
<v-list>
<v-list-item>
<template #prepend>
<v-icon icon="mdi-code-tags" class="mr-3" />
</template>
<v-list-item-title>启用开发者选项</v-list-item-title>
<v-list-item-subtitle
>启用后可以查看和修改开发者设置</v-list-item-subtitle
>
<template #append>
<v-switch
v-model="settings.developer.enabled"
density="comfortable"
hide-details
@change="handleDeveloperChange"
/>
</template>
</v-list-item>
</v-list>
</settings-card>
<developer-settings-card
border
:loading="loading.developer"
@saved="onSettingsSaved"
/>
<template v-if="settings.developer.enabled">
<v-card border class="mt-4 rounded-lg">
<v-card-title class="d-flex align-center">
<v-icon icon="mdi-cog-outline" class="mr-2" />
所有设置
</v-card-title>
<v-card-subtitle> 浏览和修改所有可用设置 </v-card-subtitle>
<v-card-text>
<settings-explorer @update="onSettingUpdate" />
</v-card-text>
</v-card>
</template>
<v-col v-if="settings.developer.enabled" cols="12"> </v-col>
</v-tabs-window-item>
<v-tabs-window-item value="about">
<about-card />
<echo-chamber-card border class="mt-4" />
</v-tabs-window-item>
</v-tabs-window>
</v-container>
<!-- 消息记录组件 -->
<message-log ref="messageLog" />
</div>
</template>
<script>
import { useDisplay } from "vuetify";
import ServerSettingsCard from "@/components/settings/cards/ServerSettingsCard.vue";
import EditSettingsCard from "@/components/settings/cards/EditSettingsCard.vue";
import RefreshSettingsCard from "@/components/settings/cards/RefreshSettingsCard.vue";
import DisplaySettingsCard from "@/components/settings/cards/DisplaySettingsCard.vue";
import DataProviderSettingsCard from "@/components/settings/cards/DataProviderSettingsCard.vue";
import ThemeSettingsCard from "@/components/settings/cards/ThemeSettingsCard.vue";
import EchoChamberCard from "@/components/settings/cards/EchoChamberCard.vue";
import {
getSetting,
setSetting,
resetSetting,
watchSettings,
} from "@/utils/settings";
import MessageLog from "@/components/MessageLog.vue";
import SettingsCard from "@/components/SettingsCard.vue";
import StudentListCard from "@/components/settings/StudentListCard.vue";
import AboutCard from "@/components/settings/AboutCard.vue";
import "../styles/settings.scss";
import SettingsExplorer from "@/components/settings/SettingsExplorer.vue";
import SettingsLinkGenerator from "@/components/SettingsLinkGenerator.vue";
import dataProvider from "@/utils/dataProvider";
import NamespaceSettingsCard from "@/components/settings/cards/NamespaceSettingsCard.vue";
export default {
name: "Settings",
components: {
ServerSettingsCard,
EditSettingsCard,
RefreshSettingsCard,
DisplaySettingsCard,
MessageLog,
SettingsCard,
StudentListCard,
AboutCard,
DataProviderSettingsCard,
ThemeSettingsCard,
EchoChamberCard,
SettingsExplorer,
SettingsLinkGenerator,
NamespaceSettingsCard,
},
setup() {
const { mobile } = useDisplay();
return { isMobile: mobile };
},
data() {
const provider = getSetting("server.provider");
const showNamespaceSettings = provider === "kv-server" || provider === "classworkscloud";
const settings = {
server: {
domain: getSetting("server.domain"),
classNumber: getSetting("server.classNumber"),
provider: getSetting("server.provider"),
},
namespace: {
name: getSetting("namespace.name"),
accessType: getSetting("namespace.accessType"),
password: getSetting("namespace.password"),
},
refresh: {
auto: getSetting("refresh.auto"),
interval: getSetting("refresh.interval"),
},
font: {
size: getSetting("font.size"),
},
edit: {
autoSave: getSetting("edit.autoSave"),
blockNonTodayAutoSave: getSetting("edit.blockNonTodayAutoSave"),
confirmNonTodaySave: getSetting("edit.confirmNonTodaySave"),
refreshBeforeEdit: getSetting("edit.refreshBeforeEdit"),
},
display: {
emptySubjectDisplay: getSetting("display.emptySubjectDisplay"),
dynamicSort: getSetting("display.dynamicSort"),
showRandomButton: getSetting("display.showRandomButton"),
showFullscreenButton: getSetting("display.showFullscreenButton"),
},
developer: {
enabled: getSetting("developer.enabled"),
showDebugConfig: getSetting("developer.showDebugConfig"),
},
message: {
showSidebar: getSetting("message.showSidebar"),
maxActiveMessages: getSetting("message.maxActiveMessages"),
timeout: getSetting("message.timeout"),
saveHistory: getSetting("message.saveHistory"),
},
};
return {
settings,
dataProviders: [
{ title: "服务器", value: "server" },
{ title: "本地数据库", value: "indexedDB" },
],
studentData: {
list: [],
text: "",
advanced: false,
},
newStudent: "",
editingIndex: -1,
editingName: "",
deleteDialog: false,
studentToDelete: null,
numberDialog: false,
newPosition: "",
studentToMove: null,
touchStartTime: 0,
touchTimeout: null,
studentsLoading: false,
studentsError: null,
debugConfig: "",
loading: {
server: false,
students: false,
},
hasUnsavedChanges: false,
lastSavedData: null,
settingsTab: "index",
settingsTabs: [
{
title: "首页",
icon: "mdi-home",
value: "index",
},
{
title: "服务器",
icon: "mdi-server",
value: "server",
},
...(showNamespaceSettings ? [
{
title: "命名空间",
icon: "mdi-database-lock",
value: "namespace",
}
] : []),
{
title: "分享设置",
icon: "mdi-share",
value: "share",
},
{
title: "刷新",
icon: "mdi-refresh",
value: "refresh",
},
{
title: "编辑",
icon: "mdi-pencil",
value: "edit",
},
{
title: "显示",
icon: "mdi-eye",
value: "display",
},
{
title: "主题",
icon: "mdi-theme-light-dark",
value: "theme",
},
{
title: "学生列表",
icon: "mdi-account-group",
value: "student",
},
{
title: "开发者",
icon: "mdi-developer-board",
value: "developer",
},
{
title: "关于",
icon: "mdi-information",
value: "about",
},
],
};
},
watch: {
settings: {
handler(newSettings) {
this.handleSettingsChange(newSettings);
},
deep: true,
},
studentData: {
handler(newData) {
// 只检查是否有未保存的更改
if (this.lastSavedData) {
this.hasUnsavedChanges =
JSON.stringify(newData.list) !== JSON.stringify(this.lastSavedData);
}
// 更新文本显示
this.studentData.text = newData.list.join("\n");
},
deep: true,
},
},
mounted() {
this.loadAllSettings();
this.unwatchSettings = watchSettings(() => {
this.loadAllSettings();
});
this.loadStudents();
},
beforeUnmount() {
if (this.unwatchSettings) {
this.unwatchSettings();
}
},
methods: {
loadAllSettings() {
Object.keys(this.settings).forEach((section) => {
Object.keys(this.settings[section]).forEach((key) => {
this.settings[section][key] = getSetting(`${section}.${key}`);
});
});
},
// 添加统一的设置处理方法
handleSettingsChange(newSettings) {
// 使用防抖来避免过多更新
if (this.settingsChangeTimeout) {
clearTimeout(this.settingsChangeTimeout);
}
this.settingsChangeTimeout = setTimeout(() => {
Object.entries(newSettings).forEach(([section, values]) => {
Object.entries(values).forEach(([key, value]) => {
const settingKey = `${section}.${key}`;
const currentValue = getSetting(settingKey);
if (value !== currentValue) {
const success = setSetting(settingKey, value);
if (success) {
this.showMessage("设置已更新", `${settingKey} 已保存`);
} else {
this.showError("保存失败", `${settingKey} 设置失败`);
// 回滚到原值
this.settings[section][key] = currentValue;
}
}
});
});
}, 100); // 添加100ms延迟
},
showMessage(title, content = "", type = "success") {
this.$message[type](title, content);
},
showError(title, content = "") {
this.$message.error(title, content);
},
async loadStudents() {
this.studentsError = null;
try {
this.loading.students = true;
const classNum = getSetting("server.classNumber");
if (!classNum) {
throw new Error("请先设置班号");
}
try {
// Try to get student list from the dedicated key
const response = await dataProvider.loadData("classworks-list-main");
if (response.success != false && Array.isArray(response)) {
// Transform the data into a simple list of names
this.studentData.list = response.map((student) => student.name);
this.studentData.text = this.studentData.list.join("\n");
this.lastSavedData = [...this.studentData.list];
this.hasUnsavedChanges = false;
return;
}
} catch (error) {
console.warn(
"Failed to load student list from dedicated key, falling back to config",
error
);
}
} catch (error) {
console.error("加载学生列表失败:", error);
this.studentsError = error.message || "加载失败,请检查设置";
this.showError("加载失败", this.studentsError);
} finally {
this.loading.students = false;
}
},
async saveStudents() {
try {
const classNum = getSetting("server.classNumber");
if (!classNum) {
throw new Error("请先设置班号");
}
// Convert the list of names to the new format with IDs
const formattedStudentList = this.studentData.list.map(
(name, index) => ({
id: index + 1,
name,
})
);
// Save the student list to the dedicated key
const response = await dataProvider.saveData(
"classworks-list-main",
formattedStudentList
);
if (response.success == false) {
throw new Error(response.error?.message || "保存失败");
}
// 更新保存状态
this.lastSavedData = [...this.studentData.list];
this.hasUnsavedChanges = false;
this.showMessage("保存成功", "学生列表已更新");
} catch (error) {
console.error("保存学生列表失败:", error);
this.showError("保存失败", error.message || "请重试");
}
},
handleStudentDataChange(newData) {
// 仅在列表实际发生变化时更新
if (
JSON.stringify(newData.list) !== JSON.stringify(this.studentData.list)
) {
this.studentData = { ...newData };
this.hasUnsavedChanges = true;
}
},
saveEdit() {
if (this.editingIndex !== -1) {
const newName = this.editingName.trim();
if (newName && newName !== this.studentData.list[this.editingIndex]) {
this.studentData.list[this.editingIndex] = newName;
}
this.editingIndex = -1;
this.editingName = "";
}
},
startEdit(index, name) {
this.editingIndex = index;
this.editingName = name;
},
confirmDelete(index) {
this.studentToDelete = {
index,
name: this.studentData.list[index],
};
this.deleteDialog = true;
},
moveStudent(index, direction) {
const newIndex = direction === "up" ? index - 1 : index + 1;
if (newIndex >= 0 && newIndex < this.studentData.list.length) {
[this.studentData.list[index], this.studentData.list[newIndex]] = [
this.studentData.list[newIndex],
this.studentData.list[index],
];
}
},
setStudentNumber(index) {
this.studentToMove = index;
this.newPosition = String(index + 1);
this.numberDialog = true;
},
applyNewPosition() {
const newPos = parseInt(this.newPosition) - 1;
if (
this.studentToMove !== null &&
newPos >= 0 &&
newPos < this.studentData.list.length &&
newPos !== this.studentToMove
) {
const student = this.studentData.list[this.studentToMove];
this.studentData.list.splice(this.studentToMove, 1);
this.studentData.list.splice(newPos, 0, student);
}
this.numberDialog = false;
this.studentToMove = null;
this.newPosition = "";
},
moveToTop(index) {
if (index > 0) {
const student = this.studentData.list[index];
this.studentData.list.splice(index, 1);
this.studentData.list.unshift(student);
}
},
addStudent() {
const student = this.newStudent.trim();
if (student && !this.studentData.list.includes(student)) {
this.studentData.list.push(student);
this.newStudent = "";
}
},
removeStudent(index) {
if (index !== undefined) {
this.studentData.list.splice(index, 1);
this.deleteDialog = false;
this.studentToDelete = null;
}
},
resetFontSize() {
resetSetting("font.size");
this.settings.font.size = getSetting("font.size");
this.showMessage("字体已重置", "字体大小已恢复默认值");
},
handleDeveloperChange(enabled) {
if (!enabled) {
// 关闭开发者选项时重置相关设置
this.settings.message = {
showSidebar: true,
maxActiveMessages: 5,
timeout: 5000,
saveHistory: true,
};
// 不需要手动调用 saveSettings,watch 会自动处理
}
},
resetDeveloperSettings() {
this.settings.developer = {
enabled: false,
};
this.settings.message = {
showSidebar: true,
maxActiveMessages: 5,
timeout: 5000,
saveHistory: true,
};
this.handleSettingsChange(this.settings);
this.showMessage("已重置", "开发者设置已重置为默认值", "warning");
},
adjustFontSize(direction) {
const step = 2;
const size = this.settings.font.size;
if (direction === "up" && size < 100) {
this.settings.font.size = size + step;
} else if (direction === "down" && size > 16) {
this.settings.font.size = size - step;
}
this.handleSettingsChange(this.settings);
},
onSettingsSaved() {
this.showMessage("设置已更新", "您的设置已成功保存");
// 如果需要,可以在这里重新加载相关数据
},
onSettingUpdate(key, value) {
// 处理设置更新
this.showMessage("设置已更新", `${key} 已保存为 ${value}`);
},
},
};
</script>
<style lang="scss">
.settings-page {
.v-card {
transition: transform 0.2s, box-shadow 0.2s;
&:hover {
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1) !important;
}
}
}
</style>