704 lines
22 KiB
Vue
704 lines
22 KiB
Vue
<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>
|