mirror of
https://github.com/ZeroCatDev/Classworks.git
synced 2025-09-03 16:19:22 +00:00
Enhance App.vue, axios.js, MigrationTool.vue, index.vue, settings.vue, and related utilities to support namespace management and improve configuration handling. Add namespace password to axios request headers, load namespace info on App mount, and update settings structure to include namespace settings. Refactor dataProvider to utilize new kvLocalProvider and kvServerProvider for data operations, ensuring better separation of concerns and improved error handling.
This commit is contained in:
parent
6d6e4a27a1
commit
c42c878ac8
14
src/App.vue
14
src/App.vue
@ -17,11 +17,13 @@ import { getSetting } from "@/utils/settings";
|
||||
import { useRouter, useRoute } from "vue-router";
|
||||
import RateLimitModal from "@/components/RateLimitModal.vue";
|
||||
import Clarity from "@microsoft/clarity";
|
||||
import { kvServerProvider } from '@/utils/providers/kvServerProvider';
|
||||
|
||||
const theme = useTheme();
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
|
||||
onMounted(() => {
|
||||
onMounted(async () => {
|
||||
// 应用保存的主题设置
|
||||
const savedTheme = getSetting("theme.mode");
|
||||
theme.global.name.value = savedTheme;
|
||||
@ -29,6 +31,16 @@ onMounted(() => {
|
||||
// 检查存储提供者类型
|
||||
checkProviderType();
|
||||
Clarity.identify(getSetting("device.uuid"), getSetting("server.domain"), getSetting("server.provider"), getSetting("server.classNumber")); // only custom-id is required
|
||||
|
||||
// 如果使用KV服务器,加载命名空间信息
|
||||
const provider = getSetting('server.provider');
|
||||
if (provider === 'kv-server' || provider === 'classworkscloud') {
|
||||
try {
|
||||
await kvServerProvider.loadNamespaceInfo();
|
||||
} catch (error) {
|
||||
console.error('加载命名空间信息失败:', error);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 检查存储提供者类型,如果是已废弃的类型则重定向
|
||||
|
@ -16,6 +16,13 @@ axiosInstance.interceptors.request.use(
|
||||
if (siteKey) {
|
||||
requestConfig.headers["x-site-key"] = siteKey;
|
||||
}
|
||||
|
||||
// 自动添加命名空间密码
|
||||
const namespacePassword = getSetting("namespace.password");
|
||||
if (namespacePassword) {
|
||||
requestConfig.headers["x-namespace-password"] = namespacePassword;
|
||||
}
|
||||
|
||||
return requestConfig;
|
||||
},
|
||||
(error) => {
|
||||
|
@ -681,7 +681,7 @@ export default {
|
||||
|
||||
// 批量导入配置数据
|
||||
const configResponse = await axios.post(
|
||||
`${this.targetServerUrl}/${this.machineId}/import/batch-import`,
|
||||
`${this.targetServerUrl}/${this.machineId}/_batchimport`,
|
||||
batchData,
|
||||
{
|
||||
headers: this.getRequestHeaders(),
|
||||
@ -798,7 +798,7 @@ export default {
|
||||
|
||||
// 批量导入配置数据
|
||||
const configResponse = await axios.post(
|
||||
`${this.targetServerUrl}/${this.machineId}/import/batch-import`,
|
||||
`${this.targetServerUrl}/${this.machineId}/_batchimport`,
|
||||
batchData,
|
||||
{
|
||||
headers: this.getRequestHeaders(),
|
||||
@ -890,7 +890,7 @@ export default {
|
||||
// 发送批量请求
|
||||
if (Object.keys(batchPayload).length > 0) {
|
||||
const response = await axios.post(
|
||||
`${this.targetServerUrl}/${this.machineId}/import/batch-import`,
|
||||
`${this.targetServerUrl}/${this.machineId}/_batchimport`,
|
||||
batchPayload,
|
||||
{
|
||||
headers: this.getRequestHeaders(),
|
||||
|
187
src/components/NamespaceAccess.vue
Normal file
187
src/components/NamespaceAccess.vue
Normal file
@ -0,0 +1,187 @@
|
||||
<template>
|
||||
<div class="namespace-access">
|
||||
<!-- 只读状态显示 -->
|
||||
<v-chip
|
||||
v-if="isReadOnly"
|
||||
color="warning"
|
||||
size="small"
|
||||
class="mr-2"
|
||||
prepend-icon="mdi-lock-outline"
|
||||
>
|
||||
只读
|
||||
</v-chip>
|
||||
<v-btn
|
||||
v-if="isReadOnly"
|
||||
color="primary"
|
||||
size="small"
|
||||
prepend-icon="mdi-lock-open-variant"
|
||||
@click="openPasswordDialog"
|
||||
:disabled="loading"
|
||||
>
|
||||
启用编辑
|
||||
</v-btn>
|
||||
|
||||
<!-- 密码输入对话框 -->
|
||||
<v-dialog v-model="dialog" max-width="400" persistent>
|
||||
<v-card>
|
||||
<v-card-title class="text-h6">输入访问密码</v-card-title>
|
||||
<v-card-text>
|
||||
<v-text-field
|
||||
v-model="password"
|
||||
:type="showPassword ? 'text' : 'password'"
|
||||
label="密码"
|
||||
variant="outlined"
|
||||
:error="!!error"
|
||||
:error-messages="error"
|
||||
@keyup.enter="checkPassword"
|
||||
:append-inner-icon="showPassword ? 'mdi-eye-off' : 'mdi-eye'"
|
||||
@click:append-inner="showPassword = !showPassword"
|
||||
:disabled="loading"
|
||||
autofocus
|
||||
/>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
color="grey"
|
||||
variant="text"
|
||||
@click="dialog = false"
|
||||
:disabled="loading"
|
||||
>
|
||||
取消
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="primary"
|
||||
@click="checkPassword"
|
||||
:loading="loading"
|
||||
:disabled="!password"
|
||||
>
|
||||
确认
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getSetting, setSetting } from "@/utils/settings";
|
||||
import axios from "@/axios/axios";
|
||||
export default {
|
||||
name: "NamespaceAccess",
|
||||
data() {
|
||||
return {
|
||||
dialog: false,
|
||||
password: "",
|
||||
error: "",
|
||||
loading: false,
|
||||
showPassword: false,
|
||||
isReadOnly: false,
|
||||
accessType: "PUBLIC", // 默认为公开访问
|
||||
};
|
||||
},
|
||||
async created() {
|
||||
await this.checkAccess();
|
||||
},
|
||||
methods: {
|
||||
async checkAccess() {
|
||||
try {
|
||||
// 获取命名空间访问类型
|
||||
const response = await axios.get(
|
||||
`${getSetting("server.domain")}/${getSetting("device.uuid")}/_info`
|
||||
);
|
||||
|
||||
if (
|
||||
response.data &&
|
||||
response.data.accessType &&
|
||||
["PRIVATE", "PROTECTED", "PUBLIC"].includes(response.data.accessType)
|
||||
) {
|
||||
this.accessType = response.data.accessType;
|
||||
} else {
|
||||
//this.$router.push("/settings");
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果是私有或受保护的命名空间,检查密码
|
||||
if (this.accessType === "PRIVATE" || this.accessType === "PROTECTED") {
|
||||
const storedPassword = getSetting("namespace.password");
|
||||
if (storedPassword) {
|
||||
await this.verifyPassword(storedPassword);
|
||||
} else if (this.accessType === "PRIVATE") {
|
||||
// 如果是私有且没有密码,立即打开密码对话框
|
||||
this.openPasswordDialog();
|
||||
} else {
|
||||
// 如果是受保护的且没有密码,设置为只读
|
||||
this.setReadOnly(true);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// 处理403错误
|
||||
if (error.response && error.response.status === 403) {
|
||||
this.accessType = "PRIVATE";
|
||||
this.setReadOnly(true);
|
||||
this.openPasswordDialog();
|
||||
} else {
|
||||
console.error("访问检查失败:", error);
|
||||
this.$message?.error("访问检查失败");
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
async verifyPassword(password) {
|
||||
try {
|
||||
const uuid = getSetting("device.uuid");
|
||||
const response = await axios.post(
|
||||
`${getSetting("server.domain")}/${uuid}/_check`,
|
||||
{ password }
|
||||
);
|
||||
|
||||
if (!response.data || response.data.success === false) {
|
||||
throw new Error(response.data?.error?.message || "密码错误");
|
||||
}
|
||||
|
||||
// 密码验证成功
|
||||
setSetting("namespace.password", password);
|
||||
this.setReadOnly(false);
|
||||
this.dialog = false;
|
||||
this.$message?.success("验证成功", "已启用编辑功能");
|
||||
} catch (error) {
|
||||
// 密码验证失败
|
||||
setSetting("namespace.password", "");
|
||||
this.setReadOnly(true);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
openPasswordDialog() {
|
||||
this.password = "";
|
||||
this.error = "";
|
||||
this.dialog = true;
|
||||
},
|
||||
|
||||
async checkPassword() {
|
||||
if (!this.password) {
|
||||
this.error = "请输入密码";
|
||||
return;
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
this.error = "";
|
||||
|
||||
try {
|
||||
await this.verifyPassword(this.password);
|
||||
} catch (error) {
|
||||
console.error("密码验证失败:", error);
|
||||
this.error = "密码验证失败";
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
setReadOnly(value) {
|
||||
this.isReadOnly = value;
|
||||
setSetting("namespace.accessType", value ? "readonly" : "readwrite");
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
427
src/components/settings/cards/NamespaceSettingsCard.vue
Normal file
427
src/components/settings/cards/NamespaceSettingsCard.vue
Normal file
@ -0,0 +1,427 @@
|
||||
<template>
|
||||
<settings-card
|
||||
title="命名空间设置"
|
||||
icon="mdi-database-lock"
|
||||
:loading="loading"
|
||||
>
|
||||
<!-- 命名空间标识符 -->
|
||||
<v-card variant="tonal" class="rounded-lg mb-4">
|
||||
<v-card-item>
|
||||
<template #prepend>
|
||||
<v-icon
|
||||
icon="mdi-identifier"
|
||||
size="large"
|
||||
class="mr-3"
|
||||
color="primary"
|
||||
/>
|
||||
</template>
|
||||
<v-card-title>命名空间标识符</v-card-title>
|
||||
<v-card-subtitle>
|
||||
<div class="d-flex align-center mt-2">
|
||||
<code class="text-body-1">{{ namespaceInfo.uuid }}</code>
|
||||
<v-btn
|
||||
icon="mdi-content-copy"
|
||||
variant="text"
|
||||
size="small"
|
||||
class="ml-2"
|
||||
@click="copyUuid"
|
||||
/>
|
||||
</div>
|
||||
</v-card-subtitle>
|
||||
</v-card-item>
|
||||
</v-card>
|
||||
|
||||
<!-- 命名空间信息表单 -->
|
||||
<v-card variant="tonal" class="rounded-lg mb-4">
|
||||
<v-card-item>
|
||||
<template #prepend>
|
||||
<v-icon
|
||||
icon="mdi-form-textbox"
|
||||
size="large"
|
||||
class="mr-3"
|
||||
color="primary"
|
||||
/>
|
||||
</template>
|
||||
<v-card-title>命名空间信息</v-card-title>
|
||||
</v-card-item>
|
||||
|
||||
<v-card-text>
|
||||
<v-form ref="form" @submit.prevent="saveNamespaceInfo">
|
||||
<v-text-field
|
||||
v-model="namespaceForm.name"
|
||||
label="命名空间名称"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
hide-details="auto"
|
||||
class="mb-4"
|
||||
:loading="loading"
|
||||
:rules="[(v) => !!v || '请输入命名空间名称']"
|
||||
>
|
||||
<template #prepend-inner>
|
||||
<v-icon icon="mdi-tag-text" />
|
||||
</template>
|
||||
</v-text-field>
|
||||
|
||||
<v-select
|
||||
v-model="namespaceForm.accessType"
|
||||
:items="accessTypeOptions"
|
||||
label="访问权限"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
hide-details="auto"
|
||||
class="mb-6"
|
||||
:loading="loading"
|
||||
>
|
||||
<template #prepend-inner>
|
||||
<v-icon icon="mdi-shield-lock" />
|
||||
</template>
|
||||
</v-select>
|
||||
|
||||
<div class="d-flex justify-end">
|
||||
<v-btn
|
||||
color="primary"
|
||||
:loading="loading"
|
||||
:disabled="!isFormChanged"
|
||||
@click="saveNamespaceInfo"
|
||||
>
|
||||
保存更改
|
||||
<template #prepend>
|
||||
<v-icon icon="mdi-content-save" />
|
||||
</template>
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<!-- 访问密码设置 -->
|
||||
<v-card variant="tonal" class="rounded-lg">
|
||||
<v-card-item>
|
||||
<template #prepend>
|
||||
<v-icon icon="mdi-key" size="large" class="mr-3" color="primary" />
|
||||
</template>
|
||||
<v-card-title>访问密码</v-card-title>
|
||||
<v-card-subtitle class="mt-2">
|
||||
设置访问密码以保护数据安全,留空表示无需密码
|
||||
</v-card-subtitle>
|
||||
</v-card-item>
|
||||
<v-card-text>
|
||||
<v-form ref="passwordForm" @submit.prevent="savePassword">
|
||||
<v-text-field
|
||||
v-model="passwordForm.newPassword"
|
||||
label="新密码"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
hide-details="auto"
|
||||
class="mb-4"
|
||||
type="password"
|
||||
:loading="passwordLoading"
|
||||
>
|
||||
<template #prepend-inner>
|
||||
<v-icon icon="mdi-lock-plus" />
|
||||
</template>
|
||||
</v-text-field>
|
||||
|
||||
<v-text-field
|
||||
v-model="passwordForm.confirmPassword"
|
||||
label="确认新密码"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
hide-details="auto"
|
||||
class="mb-6"
|
||||
type="password"
|
||||
:loading="passwordLoading"
|
||||
:rules="[
|
||||
(v) =>
|
||||
!passwordForm.newPassword ||
|
||||
v === passwordForm.newPassword ||
|
||||
'两次输入的密码不一致',
|
||||
]"
|
||||
>
|
||||
<template #prepend-inner>
|
||||
<v-icon icon="mdi-lock-check" />
|
||||
</template>
|
||||
</v-text-field>
|
||||
|
||||
<div class="d-flex justify-space-between align-center">
|
||||
<v-btn
|
||||
v-if="namespaceInfo.hasPassword"
|
||||
color="error"
|
||||
variant="tonal"
|
||||
:loading="passwordLoading"
|
||||
@click="showDeleteConfirm = true"
|
||||
>
|
||||
删除密码
|
||||
<template #prepend>
|
||||
<v-icon icon="mdi-lock-remove" />
|
||||
</template>
|
||||
</v-btn>
|
||||
<v-spacer v-else />
|
||||
|
||||
<v-btn
|
||||
color="primary"
|
||||
:loading="passwordLoading"
|
||||
:disabled="!isPasswordFormValid"
|
||||
@click="savePassword"
|
||||
>
|
||||
保存密码
|
||||
<template #prepend>
|
||||
<v-icon icon="mdi-content-save" />
|
||||
</template>
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-form>
|
||||
<setting-item
|
||||
setting-key="namespace.password"
|
||||
title="访问密码"
|
||||
></setting-item>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<!-- 删除密码确认对话框 -->
|
||||
<v-dialog v-model="showDeleteConfirm" max-width="400">
|
||||
<v-card>
|
||||
<v-card-item>
|
||||
<v-card-title>确认删除密码</v-card-title>
|
||||
<v-card-text class="mt-4">
|
||||
删除密码后,任何人都可以访问和修改此命名空间的数据。确定要继续吗?
|
||||
</v-card-text>
|
||||
</v-card-item>
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
color="grey-darken-1"
|
||||
variant="text"
|
||||
@click="showDeleteConfirm = false"
|
||||
>
|
||||
取消
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="error"
|
||||
variant="text"
|
||||
:loading="passwordLoading"
|
||||
@click="deletePassword"
|
||||
>
|
||||
确认删除
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<v-snackbar
|
||||
v-model="showSnackbar"
|
||||
:timeout="3000"
|
||||
:color="snackbarColor"
|
||||
location="top"
|
||||
>
|
||||
{{ snackbarText }}
|
||||
<template #actions>
|
||||
<v-btn variant="text" @click="showSnackbar = false"> 关闭 </v-btn>
|
||||
</template>
|
||||
</v-snackbar>
|
||||
</settings-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import SettingsCard from "@/components/SettingsCard.vue";
|
||||
import { kvServerProvider } from "@/utils/providers/kvServerProvider";
|
||||
import { getSetting } from "@/utils/settings";
|
||||
import SettingItem from "@/components/settings/SettingItem.vue";
|
||||
|
||||
export default {
|
||||
name: "NamespaceSettingsCard",
|
||||
components: { SettingsCard, SettingItem },
|
||||
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
passwordLoading: false,
|
||||
showSnackbar: false,
|
||||
showDeleteConfirm: false,
|
||||
snackbarText: "",
|
||||
snackbarColor: "success",
|
||||
namespaceInfo: {
|
||||
uuid: "",
|
||||
name: "",
|
||||
accessType: "PUBLIC",
|
||||
hasPassword: false,
|
||||
},
|
||||
namespaceForm: {
|
||||
name: "",
|
||||
accessType: "PUBLIC",
|
||||
},
|
||||
passwordForm: {
|
||||
newPassword: "",
|
||||
confirmPassword: "",
|
||||
},
|
||||
originalForm: {
|
||||
name: "",
|
||||
accessType: "PUBLIC",
|
||||
},
|
||||
accessTypeOptions: [
|
||||
{
|
||||
title: "公开(无需密码)",
|
||||
value: "PUBLIC",
|
||||
icon: "mdi-lock-open",
|
||||
},
|
||||
{
|
||||
title: "受保护(需要密码写入)",
|
||||
value: "PROTECTED",
|
||||
icon: "mdi-lock",
|
||||
},
|
||||
{
|
||||
title: "私有(需要密码读写)",
|
||||
value: "PRIVATE",
|
||||
icon: "mdi-lock-alert",
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
deviceUuid() {
|
||||
return this.namespaceInfo.uuid;
|
||||
},
|
||||
isFormChanged() {
|
||||
return (
|
||||
this.namespaceForm.name !== this.originalForm.name ||
|
||||
this.namespaceForm.accessType !== this.originalForm.accessType
|
||||
);
|
||||
},
|
||||
isPasswordFormValid() {
|
||||
if (!this.passwordForm.newPassword) {
|
||||
return true; // 允许清空密码
|
||||
}
|
||||
return (
|
||||
this.passwordForm.newPassword === this.passwordForm.confirmPassword
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
async created() {
|
||||
await this.loadNamespaceInfo();
|
||||
},
|
||||
|
||||
methods: {
|
||||
async loadNamespaceInfo() {
|
||||
this.loading = true;
|
||||
try {
|
||||
const response = await kvServerProvider.loadNamespaceInfo();
|
||||
if (response.status == 200 && response.data) {
|
||||
this.namespaceInfo = response.data;
|
||||
this.namespaceForm.name = response.data.name;
|
||||
this.namespaceForm.accessType = response.data.accessType;
|
||||
// 保存原始值用于比较
|
||||
this.originalForm = { ...this.namespaceForm };
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("加载命名空间信息失败:", error);
|
||||
this.showError("加载命名空间信息失败");
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
async saveNamespaceInfo() {
|
||||
if (!this.isFormChanged) return;
|
||||
|
||||
this.loading = true;
|
||||
try {
|
||||
const response = await kvServerProvider.updateNamespaceInfo({
|
||||
name: this.namespaceForm.name,
|
||||
accessType: this.namespaceForm.accessType,
|
||||
});
|
||||
console.log(response);
|
||||
if (response.status == 200) {
|
||||
this.originalForm = { ...this.namespaceForm };
|
||||
this.showSuccess("命名空间信息已更新");
|
||||
} else {
|
||||
throw new Error(response.error.message || "保存失败");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("保存命名空间信息失败:", error);
|
||||
this.showError(error.message || "保存命名空间信息失败");
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
async copyUuid() {
|
||||
try {
|
||||
await navigator.clipboard.writeText(this.namespaceInfo.uuid);
|
||||
this.showSuccess("命名空间标识符已复制到剪贴板");
|
||||
} catch (error) {
|
||||
console.error("复制失败:", error);
|
||||
this.showError("复制失败");
|
||||
}
|
||||
},
|
||||
|
||||
async savePassword() {
|
||||
if (!this.isPasswordFormValid) return;
|
||||
|
||||
this.passwordLoading = true;
|
||||
try {
|
||||
const oldPassword = getSetting("namespace.password");
|
||||
const response = await kvServerProvider.updatePassword(
|
||||
this.passwordForm.newPassword || null, // 如果为空字符串则发送null
|
||||
oldPassword
|
||||
);
|
||||
|
||||
if (response.status === 200) {
|
||||
this.namespaceInfo.hasPassword = !!this.passwordForm.newPassword;
|
||||
this.passwordForm = {
|
||||
newPassword: "",
|
||||
confirmPassword: "",
|
||||
};
|
||||
this.showSuccess("密码已更新");
|
||||
this.$router.push("/");
|
||||
} else {
|
||||
console.log(response);
|
||||
throw new Error(response.error.message || "保存失败 #1");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("保存密码失败:", error);
|
||||
this.showError(error.message || "保存密码失败");
|
||||
} finally {
|
||||
this.passwordLoading = false;
|
||||
}
|
||||
},
|
||||
|
||||
async deletePassword() {
|
||||
this.passwordLoading = true;
|
||||
try {
|
||||
const response = await kvServerProvider.deletePassword();
|
||||
|
||||
if (response.status == 200) {
|
||||
this.namespaceInfo.hasPassword = false;
|
||||
this.passwordForm = {
|
||||
newPassword: "",
|
||||
confirmPassword: "",
|
||||
};
|
||||
this.showDeleteConfirm = false;
|
||||
this.showSuccess("密码已删除");
|
||||
} else {
|
||||
throw new Error(response.error.message || "删除失败");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("删除密码失败:", error);
|
||||
this.showError(error.message || "删除密码失败");
|
||||
} finally {
|
||||
this.passwordLoading = false;
|
||||
}
|
||||
},
|
||||
|
||||
showSuccess(message) {
|
||||
this.snackbarColor = "success";
|
||||
this.snackbarText = message;
|
||||
this.showSnackbar = true;
|
||||
},
|
||||
|
||||
showError(message) {
|
||||
this.snackbarColor = "error";
|
||||
this.snackbarText = message;
|
||||
this.showSnackbar = true;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
@ -11,6 +11,7 @@
|
||||
<v-spacer />
|
||||
|
||||
<template #append>
|
||||
<namespace-access />
|
||||
<v-btn
|
||||
icon="mdi-format-font-size-decrease"
|
||||
variant="text"
|
||||
@ -634,6 +635,7 @@
|
||||
<script>
|
||||
import MessageLog from "@/components/MessageLog.vue";
|
||||
import RandomPicker from "@/components/RandomPicker.vue"; // 导入随机点名组件
|
||||
import NamespaceAccess from "@/components/NamespaceAccess.vue";
|
||||
import dataProvider from "@/utils/dataProvider";
|
||||
import {
|
||||
getSetting,
|
||||
@ -653,6 +655,7 @@ export default {
|
||||
components: {
|
||||
MessageLog,
|
||||
RandomPicker, // 注册随机点名组件
|
||||
NamespaceAccess,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
@ -11,52 +11,158 @@
|
||||
<v-app-bar-title class="text-h6">设置</v-app-bar-title>
|
||||
</v-app-bar>
|
||||
|
||||
<v-container class="py-4">
|
||||
<v-row>
|
||||
<v-col cols="12" md="6">
|
||||
<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"
|
||||
/>
|
||||
</v-col>
|
||||
<data-provider-settings-card border class="mt-4" />
|
||||
</v-tabs-window-item>
|
||||
|
||||
<v-col cols="12" md="6">
|
||||
<data-provider-settings-card border />
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" md="6">
|
||||
<edit-settings-card @saved="onSettingsSaved" border/>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" md="6">
|
||||
<refresh-settings-card @saved="onSettingsSaved" border/>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" md="6">
|
||||
<display-settings-card @saved="onSettingsSaved" border/>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" md="6">
|
||||
<theme-settings-card border />
|
||||
</v-col>
|
||||
<v-col cols="12">
|
||||
<settings-link-generator border />
|
||||
</v-col>
|
||||
<!-- 开发者选项卡片 -->
|
||||
<v-col :cols="12" :md="settings.developer.enabled ? 12 : 6">
|
||||
<settings-card
|
||||
<v-tabs-window-item value="namespace">
|
||||
<namespace-settings-card
|
||||
border
|
||||
title="开发者选项"
|
||||
icon="mdi-developer-board"
|
||||
>
|
||||
: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>
|
||||
<v-list-item-subtitle
|
||||
>启用后可以查看和修改开发者设置</v-list-item-subtitle
|
||||
>
|
||||
<template #append>
|
||||
<v-switch
|
||||
v-model="settings.developer.enabled"
|
||||
@ -66,100 +172,33 @@
|
||||
/>
|
||||
</template>
|
||||
</v-list-item>
|
||||
|
||||
<template v-if="settings.developer.enabled">
|
||||
<v-divider class="my-2" />
|
||||
|
||||
<v-list-item>
|
||||
<template #prepend>
|
||||
<v-icon icon="mdi-file-code" 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.showDebugConfig"
|
||||
density="comfortable"
|
||||
hide-details
|
||||
/>
|
||||
</template>
|
||||
</v-list-item>
|
||||
|
||||
<v-expand-transition>
|
||||
<div v-if="settings.developer.showDebugConfig">
|
||||
<v-divider class="my-2" />
|
||||
<v-textarea
|
||||
v-model="debugConfig"
|
||||
label="调试配置"
|
||||
readonly
|
||||
rows="10"
|
||||
class="font-monospace mt-2"
|
||||
/>
|
||||
<div class="d-flex gap-2">
|
||||
<v-btn
|
||||
prepend-icon="mdi-refresh"
|
||||
variant="text"
|
||||
@click="refreshDebugConfig"
|
||||
>
|
||||
刷新
|
||||
</v-btn>
|
||||
<v-btn
|
||||
prepend-icon="mdi-content-copy"
|
||||
variant="text"
|
||||
@click="copyDebugConfig"
|
||||
>
|
||||
复制
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
</v-expand-transition>
|
||||
</template>
|
||||
</v-list>
|
||||
</settings-card>
|
||||
</v-col>
|
||||
|
||||
<!-- 学生列表卡片 -->
|
||||
<v-col cols="12">
|
||||
<student-list-card
|
||||
v-model="studentData"
|
||||
:loading="loading.students"
|
||||
:error="studentsError"
|
||||
:is-mobile="isMobile"
|
||||
:unsaved-changes="hasUnsavedChanges"
|
||||
@save="saveStudents"
|
||||
@reload="loadStudents"
|
||||
@update:modelValue="handleStudentDataChange"
|
||||
<developer-settings-card
|
||||
border
|
||||
:loading="loading.developer"
|
||||
@saved="onSettingsSaved"
|
||||
/>
|
||||
</v-col>
|
||||
<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-col cols="12">
|
||||
<echo-chamber-card border />
|
||||
</v-col>
|
||||
|
||||
<!-- 关于卡片 -->
|
||||
<v-col cols="12">
|
||||
<v-tabs-window-item value="about">
|
||||
<about-card />
|
||||
</v-col>
|
||||
|
||||
<!-- 开发者模式下显示所有设置 -->
|
||||
<v-col v-if="settings.developer.enabled" cols="12">
|
||||
<v-card border>
|
||||
<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>
|
||||
</v-col>
|
||||
|
||||
|
||||
</v-row>
|
||||
<echo-chamber-card border class="mt-4" />
|
||||
</v-tabs-window-item>
|
||||
</v-tabs-window>
|
||||
</v-container>
|
||||
|
||||
<!-- 消息记录组件 -->
|
||||
@ -168,32 +207,32 @@
|
||||
</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 { 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 { kvProvider } from '@/utils/providers/kvProvider';
|
||||
import SettingsExplorer from '@/components/settings/SettingsExplorer.vue';
|
||||
import SettingsLinkGenerator from '@/components/SettingsLinkGenerator.vue';
|
||||
import dataProvider from '@/utils/dataProvider';
|
||||
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',
|
||||
name: "Settings",
|
||||
components: {
|
||||
ServerSettingsCard,
|
||||
EditSettingsCard,
|
||||
@ -207,7 +246,8 @@ export default {
|
||||
ThemeSettingsCard,
|
||||
EchoChamberCard,
|
||||
SettingsExplorer,
|
||||
SettingsLinkGenerator
|
||||
SettingsLinkGenerator,
|
||||
NamespaceSettingsCard,
|
||||
},
|
||||
setup() {
|
||||
const { mobile } = useDisplay();
|
||||
@ -216,91 +256,155 @@ export default {
|
||||
data() {
|
||||
const settings = {
|
||||
server: {
|
||||
domain: getSetting('server.domain'),
|
||||
classNumber: getSetting('server.classNumber'),
|
||||
provider: getSetting('server.provider')
|
||||
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'),
|
||||
auto: getSetting("refresh.auto"),
|
||||
interval: getSetting("refresh.interval"),
|
||||
},
|
||||
font: {
|
||||
size: getSetting('font.size'),
|
||||
size: getSetting("font.size"),
|
||||
},
|
||||
edit: {
|
||||
autoSave: getSetting('edit.autoSave'),
|
||||
blockNonTodayAutoSave: getSetting('edit.blockNonTodayAutoSave'),
|
||||
confirmNonTodaySave: getSetting('edit.confirmNonTodaySave'),
|
||||
refreshBeforeEdit: getSetting('edit.refreshBeforeEdit'),
|
||||
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')
|
||||
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')
|
||||
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')
|
||||
}
|
||||
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'}
|
||||
{ title: "服务器", value: "server" },
|
||||
{ title: "本地数据库", value: "indexedDB" },
|
||||
],
|
||||
studentData: {
|
||||
list: [],
|
||||
text: '',
|
||||
advanced: false
|
||||
text: "",
|
||||
advanced: false,
|
||||
},
|
||||
newStudent: '',
|
||||
newStudent: "",
|
||||
editingIndex: -1,
|
||||
editingName: '',
|
||||
editingName: "",
|
||||
deleteDialog: false,
|
||||
studentToDelete: null,
|
||||
numberDialog: false,
|
||||
newPosition: '',
|
||||
newPosition: "",
|
||||
studentToMove: null,
|
||||
touchStartTime: 0,
|
||||
touchTimeout: null,
|
||||
studentsLoading: false,
|
||||
studentsError: null,
|
||||
debugConfig: '',
|
||||
debugConfig: "",
|
||||
loading: {
|
||||
server: false,
|
||||
students: false
|
||||
students: false,
|
||||
},
|
||||
hasUnsavedChanges: false,
|
||||
lastSavedData: null
|
||||
}
|
||||
lastSavedData: null,
|
||||
settingsTab: "index",
|
||||
settingsTabs: [
|
||||
{
|
||||
title: "首页",
|
||||
icon: "mdi-home",
|
||||
value: "index",
|
||||
},
|
||||
{
|
||||
title: "服务器",
|
||||
icon: "mdi-server",
|
||||
value: "server",
|
||||
},
|
||||
{
|
||||
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': {
|
||||
settings: {
|
||||
handler(newSettings) {
|
||||
this.handleSettingsChange(newSettings);
|
||||
},
|
||||
deep: true
|
||||
deep: true,
|
||||
},
|
||||
'studentData': {
|
||||
studentData: {
|
||||
handler(newData) {
|
||||
// 只检查是否有未保存的更改
|
||||
if (this.lastSavedData) {
|
||||
this.hasUnsavedChanges = JSON.stringify(newData.list) !== JSON.stringify(this.lastSavedData);
|
||||
this.hasUnsavedChanges =
|
||||
JSON.stringify(newData.list) !== JSON.stringify(this.lastSavedData);
|
||||
}
|
||||
// 更新文本显示
|
||||
this.studentData.text = newData.list.join('\n');
|
||||
this.studentData.text = newData.list.join("\n");
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
deep: true,
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
@ -309,13 +413,6 @@ export default {
|
||||
this.loadAllSettings();
|
||||
});
|
||||
this.loadStudents();
|
||||
this.refreshDebugConfig();
|
||||
|
||||
// 检查开发者选项,如果未启用则关闭相关功能
|
||||
if (!this.settings.developer.enabled) {
|
||||
this.settings.developer.showDebugConfig = false;
|
||||
this.handleSettingsChange(this.settings);
|
||||
}
|
||||
},
|
||||
|
||||
beforeUnmount() {
|
||||
@ -326,8 +423,8 @@ export default {
|
||||
|
||||
methods: {
|
||||
loadAllSettings() {
|
||||
Object.keys(this.settings).forEach(section => {
|
||||
Object.keys(this.settings[section]).forEach(key => {
|
||||
Object.keys(this.settings).forEach((section) => {
|
||||
Object.keys(this.settings[section]).forEach((key) => {
|
||||
this.settings[section][key] = getSetting(`${section}.${key}`);
|
||||
});
|
||||
});
|
||||
@ -348,9 +445,9 @@ export default {
|
||||
if (value !== currentValue) {
|
||||
const success = setSetting(settingKey, value);
|
||||
if (success) {
|
||||
this.showMessage('设置已更新', `${settingKey} 已保存`);
|
||||
this.showMessage("设置已更新", `${settingKey} 已保存`);
|
||||
} else {
|
||||
this.showError('保存失败', `${settingKey} 设置失败`);
|
||||
this.showError("保存失败", `${settingKey} 设置失败`);
|
||||
// 回滚到原值
|
||||
this.settings[section][key] = currentValue;
|
||||
}
|
||||
@ -360,11 +457,11 @@ export default {
|
||||
}, 100); // 添加100ms延迟
|
||||
},
|
||||
|
||||
showMessage(title, content = '', type = 'success') {
|
||||
showMessage(title, content = "", type = "success") {
|
||||
this.$message[type](title, content);
|
||||
},
|
||||
|
||||
showError(title, content = '') {
|
||||
showError(title, content = "") {
|
||||
this.$message.error(title, content);
|
||||
},
|
||||
|
||||
@ -372,32 +469,34 @@ export default {
|
||||
this.studentsError = null;
|
||||
try {
|
||||
this.loading.students = true;
|
||||
const classNum = getSetting('server.classNumber');
|
||||
const classNum = getSetting("server.classNumber");
|
||||
|
||||
if (!classNum) {
|
||||
throw new Error('请先设置班号');
|
||||
throw new Error("请先设置班号");
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
// Try to get student list from the dedicated key
|
||||
const response = await dataProvider.loadData('classworks-list-main');
|
||||
const response = await dataProvider.loadData("classworks-list-main");
|
||||
|
||||
if (response.success!=false && Array.isArray(response)) {
|
||||
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.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);
|
||||
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);
|
||||
console.error("加载学生列表失败:", error);
|
||||
this.studentsError = error.message || "加载失败,请检查设置";
|
||||
this.showError("加载失败", this.studentsError);
|
||||
} finally {
|
||||
this.loading.students = false;
|
||||
}
|
||||
@ -405,39 +504,45 @@ export default {
|
||||
|
||||
async saveStudents() {
|
||||
try {
|
||||
const classNum = getSetting('server.classNumber');
|
||||
const classNum = getSetting("server.classNumber");
|
||||
|
||||
if (!classNum) {
|
||||
throw new Error('请先设置班号');
|
||||
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
|
||||
}));
|
||||
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);
|
||||
const response = await dataProvider.saveData(
|
||||
"classworks-list-main",
|
||||
formattedStudentList
|
||||
);
|
||||
|
||||
if (response.success==false) {
|
||||
if (response.success == false) {
|
||||
throw new Error(response.error?.message || "保存失败");
|
||||
}
|
||||
|
||||
// 更新保存状态
|
||||
this.lastSavedData = [...this.studentData.list];
|
||||
this.hasUnsavedChanges = false;
|
||||
this.showMessage('保存成功', '学生列表已更新');
|
||||
this.showMessage("保存成功", "学生列表已更新");
|
||||
} catch (error) {
|
||||
console.error('保存学生列表失败:', error);
|
||||
this.showError('保存失败', error.message || '请重试');
|
||||
console.error("保存学生列表失败:", error);
|
||||
this.showError("保存失败", error.message || "请重试");
|
||||
}
|
||||
},
|
||||
|
||||
handleStudentDataChange(newData) {
|
||||
// 仅在列表实际发生变化时更新
|
||||
if (JSON.stringify(newData.list) !== JSON.stringify(this.studentData.list)) {
|
||||
if (
|
||||
JSON.stringify(newData.list) !== JSON.stringify(this.studentData.list)
|
||||
) {
|
||||
this.studentData = { ...newData };
|
||||
this.hasUnsavedChanges = true;
|
||||
}
|
||||
@ -450,7 +555,7 @@ export default {
|
||||
this.studentData.list[this.editingIndex] = newName;
|
||||
}
|
||||
this.editingIndex = -1;
|
||||
this.editingName = '';
|
||||
this.editingName = "";
|
||||
}
|
||||
},
|
||||
|
||||
@ -462,15 +567,18 @@ export default {
|
||||
confirmDelete(index) {
|
||||
this.studentToDelete = {
|
||||
index,
|
||||
name: this.studentData.list[index]
|
||||
name: this.studentData.list[index],
|
||||
};
|
||||
this.deleteDialog = true;
|
||||
},
|
||||
|
||||
moveStudent(index, direction) {
|
||||
const newIndex = direction === 'up' ? index - 1 : index + 1;
|
||||
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]];
|
||||
[this.studentData.list[index], this.studentData.list[newIndex]] = [
|
||||
this.studentData.list[newIndex],
|
||||
this.studentData.list[index],
|
||||
];
|
||||
}
|
||||
},
|
||||
|
||||
@ -494,7 +602,7 @@ export default {
|
||||
}
|
||||
this.numberDialog = false;
|
||||
this.studentToMove = null;
|
||||
this.newPosition = '';
|
||||
this.newPosition = "";
|
||||
},
|
||||
|
||||
moveToTop(index) {
|
||||
@ -509,7 +617,7 @@ export default {
|
||||
const student = this.newStudent.trim();
|
||||
if (student && !this.studentData.list.includes(student)) {
|
||||
this.studentData.list.push(student);
|
||||
this.newStudent = '';
|
||||
this.newStudent = "";
|
||||
}
|
||||
},
|
||||
|
||||
@ -522,41 +630,19 @@ export default {
|
||||
},
|
||||
|
||||
resetFontSize() {
|
||||
resetSetting('font.size');
|
||||
this.settings.font.size = getSetting('font.size');
|
||||
this.showMessage('字体已重置', '字体大小已恢复默认值');
|
||||
},
|
||||
|
||||
refreshDebugConfig() {
|
||||
const allSettings = {};
|
||||
Object.keys(this.settings).forEach(section => {
|
||||
allSettings[section] = {};
|
||||
Object.keys(this.settings[section]).forEach(key => {
|
||||
allSettings[section][key] = getSetting(`${section}.${key}`);
|
||||
});
|
||||
});
|
||||
this.debugConfig = JSON.stringify(allSettings, null, 2);
|
||||
},
|
||||
|
||||
async copyDebugConfig() {
|
||||
try {
|
||||
await navigator.clipboard.writeText(this.debugConfig);
|
||||
this.showMessage('复制成功', '配置信息已复制到剪贴板');
|
||||
} catch (error) {
|
||||
console.error('复制失败:', error);
|
||||
this.showError('复制失败', '请手动复制');
|
||||
}
|
||||
resetSetting("font.size");
|
||||
this.settings.font.size = getSetting("font.size");
|
||||
this.showMessage("字体已重置", "字体大小已恢复默认值");
|
||||
},
|
||||
|
||||
handleDeveloperChange(enabled) {
|
||||
if (!enabled) {
|
||||
// 关闭开发者选项时重置相关设置
|
||||
this.settings.developer.showDebugConfig = false;
|
||||
this.settings.message = {
|
||||
showSidebar: true,
|
||||
maxActiveMessages: 5,
|
||||
timeout: 5000,
|
||||
saveHistory: true
|
||||
saveHistory: true,
|
||||
};
|
||||
// 不需要手动调用 saveSettings,watch 会自动处理
|
||||
}
|
||||
@ -565,40 +651,39 @@ export default {
|
||||
resetDeveloperSettings() {
|
||||
this.settings.developer = {
|
||||
enabled: false,
|
||||
showDebugConfig: false
|
||||
};
|
||||
this.settings.message = {
|
||||
showSidebar: true,
|
||||
maxActiveMessages: 5,
|
||||
timeout: 5000,
|
||||
saveHistory: true
|
||||
saveHistory: true,
|
||||
};
|
||||
this.handleSettingsChange(this.settings);
|
||||
this.showMessage('已重置', '开发者设置已重置为默认值', 'warning');
|
||||
this.showMessage("已重置", "开发者设置已重置为默认值", "warning");
|
||||
},
|
||||
|
||||
adjustFontSize(direction) {
|
||||
const step = 2;
|
||||
const size = this.settings.font.size;
|
||||
if (direction === 'up' && size < 100) {
|
||||
if (direction === "up" && size < 100) {
|
||||
this.settings.font.size = size + step;
|
||||
} else if (direction === 'down' && size > 16) {
|
||||
} else if (direction === "down" && size > 16) {
|
||||
this.settings.font.size = size - step;
|
||||
}
|
||||
this.handleSettingsChange(this.settings);
|
||||
},
|
||||
|
||||
onSettingsSaved() {
|
||||
this.showMessage('设置已更新', '您的设置已成功保存');
|
||||
this.showMessage("设置已更新", "您的设置已成功保存");
|
||||
// 如果需要,可以在这里重新加载相关数据
|
||||
},
|
||||
|
||||
onSettingUpdate(key, value) {
|
||||
// 处理设置更新
|
||||
this.showMessage('设置已更新', `${key} 已保存为 ${value}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
this.showMessage("设置已更新", `${key} 已保存为 ${value}`);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@ -606,8 +691,7 @@ export default {
|
||||
.v-card {
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 8px rgba(0,0,0,0.1) !important;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,8 @@
|
||||
import { kvProvider } from "./providers/kvProvider";
|
||||
import { kvLocalProvider } from "./providers/kvLocalProvider";
|
||||
import { kvServerProvider } from "./providers/kvServerProvider";
|
||||
import { getSetting } from "./settings";
|
||||
|
||||
export const formatResponse = (data, message = null) => (data);
|
||||
export const formatResponse = (data) => data;
|
||||
|
||||
export const formatError = (message, code = "UNKNOWN_ERROR") => ({
|
||||
success: false,
|
||||
@ -17,9 +18,9 @@ export default {
|
||||
provider === "kv-server" || provider === "classworkscloud";
|
||||
|
||||
if (useServer) {
|
||||
return kvProvider.server.loadData(key);
|
||||
return kvServerProvider.loadData(key);
|
||||
} else {
|
||||
return kvProvider.local.loadData(key);
|
||||
return kvLocalProvider.loadData(key);
|
||||
}
|
||||
},
|
||||
|
||||
@ -29,9 +30,9 @@ export default {
|
||||
provider === "kv-server" || provider === "classworkscloud";
|
||||
|
||||
if (useServer) {
|
||||
return kvProvider.server.saveData(key, data);
|
||||
return kvServerProvider.saveData(key, data);
|
||||
} else {
|
||||
return kvProvider.local.saveData(key, data);
|
||||
return kvLocalProvider.saveData(key, data);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
49
src/utils/providers/kvLocalProvider.js
Normal file
49
src/utils/providers/kvLocalProvider.js
Normal file
@ -0,0 +1,49 @@
|
||||
import { openDB } from "idb";
|
||||
import { formatResponse, formatError } from "../dataProvider";
|
||||
|
||||
// Database initialization for local storage
|
||||
const DB_NAME = "ClassworksDB";
|
||||
const DB_VERSION = 2;
|
||||
|
||||
const initDB = async () => {
|
||||
return openDB(DB_NAME, DB_VERSION, {
|
||||
upgrade(db) {
|
||||
// Create or update stores as needed
|
||||
if (!db.objectStoreNames.contains("kv")) {
|
||||
db.createObjectStore("kv");
|
||||
}
|
||||
|
||||
// Add a system store for machine ID and other system settings
|
||||
if (!db.objectStoreNames.contains("system")) {
|
||||
db.createObjectStore("system");
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const kvLocalProvider = {
|
||||
async loadData(key) {
|
||||
try {
|
||||
const db = await initDB();
|
||||
const data = await db.get("kv", key);
|
||||
|
||||
if (!data) {
|
||||
return formatError("数据不存在", "NOT_FOUND");
|
||||
}
|
||||
|
||||
return formatResponse(JSON.parse(data));
|
||||
} catch (error) {
|
||||
return formatError("读取本地数据失败:" + error);
|
||||
}
|
||||
},
|
||||
|
||||
async saveData(key, data) {
|
||||
try {
|
||||
const db = await initDB();
|
||||
await db.put("kv", JSON.stringify(data), key);
|
||||
return formatResponse(true);
|
||||
} catch (error) {
|
||||
return formatError("保存本地数据失败:" + error);
|
||||
}
|
||||
},
|
||||
};
|
@ -1,112 +0,0 @@
|
||||
import axios from "@/axios/axios";
|
||||
import { formatResponse, formatError } from "../dataProvider";
|
||||
import { openDB } from "idb";
|
||||
import { getSetting } from "../settings";
|
||||
|
||||
// Database initialization for local storage
|
||||
const DB_NAME = "ClassworksDB";
|
||||
const DB_VERSION = 2;
|
||||
|
||||
// Helper function to get request headers with site key if available
|
||||
const getHeaders = () => {
|
||||
const headers = { Accept: "application/json" };
|
||||
const siteKey = getSetting("server.siteKey");
|
||||
|
||||
if (siteKey) {
|
||||
headers["x-site-key"] = siteKey;
|
||||
}
|
||||
|
||||
return headers;
|
||||
};
|
||||
|
||||
// Removed migrateToKvStorage function - now handled by the dedicated migration tool
|
||||
|
||||
const initDB = async () => {
|
||||
return openDB(DB_NAME, DB_VERSION, {
|
||||
upgrade(db) {
|
||||
// Create or update stores as needed
|
||||
if (!db.objectStoreNames.contains("kv")) {
|
||||
db.createObjectStore("kv");
|
||||
}
|
||||
|
||||
// Add a system store for machine ID and other system settings
|
||||
if (!db.objectStoreNames.contains("system")) {
|
||||
db.createObjectStore("system");
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const kvProvider = {
|
||||
// Local storage provider
|
||||
local: {
|
||||
async loadData(key) {
|
||||
try {
|
||||
const db = await initDB();
|
||||
const data = await db.get("kv", key);
|
||||
|
||||
if (!data) {
|
||||
return formatError("数据不存在", "NOT_FOUND");
|
||||
}
|
||||
|
||||
return formatResponse(JSON.parse(data));
|
||||
} catch (error) {
|
||||
return formatError("读取本地数据失败:" + error);
|
||||
}
|
||||
},
|
||||
|
||||
async saveData(data, key) {
|
||||
try {
|
||||
//const formattedDate = formatDateForKey(date);
|
||||
//const key = `${DATA_KEY_PREFIX}${formattedDate}`;
|
||||
|
||||
const db = await initDB();
|
||||
await db.put("kv", JSON.stringify(data), key);
|
||||
return formatResponse(true, "保存成功");
|
||||
} catch (error) {
|
||||
return formatError("保存本地数据失败:" + error);
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
// Server storage provider
|
||||
server: {
|
||||
async loadData(key) {
|
||||
try {
|
||||
const serverUrl = getSetting("server.domain");
|
||||
const machineId = getSetting("device.uuid");
|
||||
|
||||
const res = await axios.get(`${serverUrl}/${machineId}/${key}`, {
|
||||
headers: getHeaders(),
|
||||
});
|
||||
|
||||
return formatResponse(res.data);
|
||||
} catch (error) {
|
||||
if (error.response?.status === 404) {
|
||||
return formatError("数据不存在", "NOT_FOUND");
|
||||
}
|
||||
|
||||
return formatError(
|
||||
error.response?.data?.message || "服务器连接失败",
|
||||
"NETWORK_ERROR"
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
async saveData(key, data) {
|
||||
try {
|
||||
const serverUrl = getSetting("server.domain");
|
||||
const machineId = getSetting("device.uuid");
|
||||
await axios.post(`${serverUrl}/${machineId}/${key}`, data, {
|
||||
headers: getHeaders(),
|
||||
});
|
||||
return formatResponse(true);
|
||||
} catch (error) {
|
||||
return formatError(
|
||||
error.response?.data?.message || "保存失败",
|
||||
"SAVE_ERROR"
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
161
src/utils/providers/kvServerProvider.js
Normal file
161
src/utils/providers/kvServerProvider.js
Normal file
@ -0,0 +1,161 @@
|
||||
import axios from "@/axios/axios";
|
||||
import { formatResponse, formatError } from "../dataProvider";
|
||||
import { getSetting, setSetting } from "../settings";
|
||||
|
||||
// Helper function to get request headers with site key and password if available
|
||||
const getHeaders = () => {
|
||||
const headers = { Accept: "application/json" };
|
||||
const siteKey = getSetting("server.siteKey");
|
||||
const password = getSetting("namespace.password");
|
||||
|
||||
if (siteKey) {
|
||||
headers["x-site-key"] = siteKey;
|
||||
}
|
||||
if (password) {
|
||||
headers["x-namespace-password"] = password;
|
||||
}
|
||||
|
||||
return headers;
|
||||
};
|
||||
|
||||
export const kvServerProvider = {
|
||||
async loadNamespaceInfo() {
|
||||
try {
|
||||
const serverUrl = getSetting("server.domain");
|
||||
const machineId = getSetting("device.uuid");
|
||||
|
||||
const res = await axios.get(`${serverUrl}/${machineId}/_info`, {
|
||||
headers: getHeaders(),
|
||||
});
|
||||
|
||||
const { name, accessType } = res.data;
|
||||
|
||||
// 如果name为null,使用班级号作为名称并更新
|
||||
if (name === null) {
|
||||
const classNumber = getSetting("server.classNumber");
|
||||
await this.updateNamespaceInfo({ name: classNumber });
|
||||
// 重新加载命名空间信息
|
||||
return await this.loadNamespaceInfo();
|
||||
}
|
||||
|
||||
// 更新本地访问权限设置
|
||||
if (accessType) {
|
||||
setSetting("namespace.accessType", accessType);
|
||||
}
|
||||
|
||||
return formatResponse(res);
|
||||
} catch (error) {
|
||||
return formatError(
|
||||
error.response?.data?.message || "获取命名空间信息失败",
|
||||
"NAMESPACE_ERROR"
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
async updateNamespaceInfo(data) {
|
||||
try {
|
||||
const serverUrl = getSetting("server.domain");
|
||||
const machineId = getSetting("device.uuid");
|
||||
|
||||
const res = await axios.put(`${serverUrl}/${machineId}/_info`, data, {
|
||||
headers: getHeaders(),
|
||||
});
|
||||
|
||||
return res;
|
||||
} catch (error) {
|
||||
return formatError(
|
||||
error.response?.data?.message || "更新命名空间信息失败",
|
||||
"NAMESPACE_ERROR"
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
async updatePassword(newPassword, oldPassword) {
|
||||
try {
|
||||
const serverUrl = getSetting("server.domain");
|
||||
const machineId = getSetting("device.uuid");
|
||||
|
||||
const res = await axios.post(
|
||||
`${serverUrl}/${machineId}/_password`,
|
||||
{
|
||||
newPassword,
|
||||
oldPassword,
|
||||
},
|
||||
{
|
||||
headers: getHeaders(),
|
||||
}
|
||||
);
|
||||
|
||||
if (res.status == 200) {
|
||||
setSetting("namespace.password", newPassword);
|
||||
}
|
||||
|
||||
return formatResponse(res);
|
||||
} catch (error) {
|
||||
return formatError(
|
||||
error.response?.data?.message || "更新密码失败",
|
||||
"PASSWORD_ERROR"
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
async deletePassword() {
|
||||
try {
|
||||
const serverUrl = getSetting("server.domain");
|
||||
const machineId = getSetting("device.uuid");
|
||||
|
||||
const res = await axios.delete(`${serverUrl}/${machineId}/_password`, {
|
||||
headers: getHeaders(),
|
||||
});
|
||||
|
||||
if (res.status === 200) {
|
||||
setSetting("namespace.password", null); // 清除本地存储的密码
|
||||
}
|
||||
|
||||
return formatResponse(res);
|
||||
} catch (error) {
|
||||
return formatError(
|
||||
error.response?.data?.message || "删除密码失败",
|
||||
"PASSWORD_ERROR"
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
async loadData(key) {
|
||||
try {
|
||||
const serverUrl = getSetting("server.domain");
|
||||
const machineId = getSetting("device.uuid");
|
||||
|
||||
const res = await axios.get(`${serverUrl}/${machineId}/${key}`, {
|
||||
headers: getHeaders(),
|
||||
});
|
||||
|
||||
return formatResponse(res.data);
|
||||
} catch (error) {
|
||||
if (error.response?.status === 404) {
|
||||
return formatError("数据不存在", "NOT_FOUND");
|
||||
}
|
||||
|
||||
return formatError(
|
||||
error.response?.data?.message || "服务器连接失败",
|
||||
"NETWORK_ERROR"
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
async saveData(key, data) {
|
||||
try {
|
||||
const serverUrl = getSetting("server.domain");
|
||||
const machineId = getSetting("device.uuid");
|
||||
await axios.post(`${serverUrl}/${machineId}/${key}`, data, {
|
||||
headers: getHeaders(),
|
||||
});
|
||||
return formatResponse(true);
|
||||
} catch (error) {
|
||||
return formatError(
|
||||
error.response?.data?.message || "保存失败",
|
||||
"SAVE_ERROR"
|
||||
);
|
||||
}
|
||||
},
|
||||
};
|
@ -37,14 +37,17 @@ async function requestPersistentStorage() {
|
||||
*/
|
||||
async function initializeStorage() {
|
||||
const notificationGranted = await requestNotificationPermission();
|
||||
if (notificationGranted && SettingsManager.getSetting("storage.persistOnLoad")) {
|
||||
if (
|
||||
notificationGranted &&
|
||||
SettingsManager.getSetting("storage.persistOnLoad")
|
||||
) {
|
||||
const persisted = await requestPersistentStorage();
|
||||
console.log(`持久性存储状态: ${persisted ? "已启用" : "未启用"}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 在页面加载时初始化
|
||||
if (typeof window !== 'undefined') {
|
||||
if (typeof window !== "undefined") {
|
||||
window.addEventListener("load", initializeStorage);
|
||||
}
|
||||
|
||||
@ -90,6 +93,21 @@ const settingsDefinitions = {
|
||||
icon: "mdi-identifier",
|
||||
},
|
||||
|
||||
// 命名空间设置
|
||||
"namespace.password": {
|
||||
type: "string",
|
||||
default: "",
|
||||
description: "命名空间访问密码",
|
||||
icon: "mdi-key",
|
||||
},
|
||||
"namespace.accessType": {
|
||||
type: "string",
|
||||
default: "readwrite",
|
||||
description: "访问权限类型",
|
||||
icon: "mdi-shield-lock",
|
||||
validate: (value) => ["readonly", "readwrite"].includes(value),
|
||||
},
|
||||
|
||||
// 存储设置
|
||||
"storage.persistOnLoad": {
|
||||
type: "boolean",
|
||||
@ -191,7 +209,8 @@ const settingsDefinitions = {
|
||||
"server.provider": {
|
||||
type: "string",
|
||||
default: "kv-local",
|
||||
validate: (value) => ["kv-local", "kv-server", "classworkscloud"].includes(value),
|
||||
validate: (value) =>
|
||||
["kv-local", "kv-server", "classworkscloud"].includes(value),
|
||||
description: "数据提供者",
|
||||
icon: "mdi-database",
|
||||
// 选择数据存储方式:使用本地存储或远程服务器
|
||||
@ -388,17 +407,20 @@ class SettingsManagerClass {
|
||||
* @returns {Object} 所有设置的值
|
||||
*/
|
||||
loadSettings() {
|
||||
// Initialize settingsCache as an empty object first
|
||||
this.settingsCache = {};
|
||||
|
||||
try {
|
||||
const stored = typeof localStorage !== 'undefined' ? localStorage.getItem(SETTINGS_STORAGE_KEY) : null;
|
||||
const stored =
|
||||
typeof localStorage !== "undefined"
|
||||
? localStorage.getItem(SETTINGS_STORAGE_KEY)
|
||||
: null;
|
||||
if (stored) {
|
||||
this.settingsCache = JSON.parse(stored);
|
||||
} else {
|
||||
// 首次使用或迁移旧数据
|
||||
this.settingsCache = this.migrateFromLegacy();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("加载设置失败:", error);
|
||||
this.settingsCache = {};
|
||||
// settingsCache is already an empty object, no need to reinitialize
|
||||
}
|
||||
|
||||
// 确保所有设置项都有值(使用默认值填充)
|
||||
@ -411,54 +433,17 @@ class SettingsManagerClass {
|
||||
return this.settingsCache;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从旧版本的localStorage迁移数据
|
||||
*/
|
||||
migrateFromLegacy() {
|
||||
if (typeof localStorage === 'undefined') return {};
|
||||
|
||||
const LEGACY_SETTINGS_KEY = "homeworkpage_settings";
|
||||
const LEGACY_MESSAGE_KEY = "homeworkpage_messages";
|
||||
|
||||
// 尝试从旧版本的设置中迁移
|
||||
const legacySettings = localStorage.getItem(LEGACY_SETTINGS_KEY);
|
||||
if (legacySettings) {
|
||||
try {
|
||||
const settings = JSON.parse(legacySettings);
|
||||
localStorage.setItem(SETTINGS_STORAGE_KEY, JSON.stringify(settings));
|
||||
// 可选:删除旧的设置
|
||||
localStorage.removeItem(LEGACY_SETTINGS_KEY);
|
||||
return settings;
|
||||
} catch (error) {
|
||||
console.error("迁移旧设置失败:", error);
|
||||
}
|
||||
}
|
||||
// 尝试从旧版本的message中迁移
|
||||
const legacyMessages = localStorage.getItem(LEGACY_MESSAGE_KEY);
|
||||
if (legacyMessages) {
|
||||
try {
|
||||
const messages = JSON.parse(legacyMessages);
|
||||
localStorage.setItem(SETTINGS_STORAGE_KEY, JSON.stringify(messages));
|
||||
// 可选:删除旧的message
|
||||
localStorage.removeItem(LEGACY_MESSAGE_KEY);
|
||||
return messages; // 返回迁移后的消息
|
||||
} catch (error) {
|
||||
console.error("迁移旧消息失败:", error);
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有旧设置或迁移失败,返回空对象
|
||||
return {};
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存所有设置到localStorage
|
||||
*/
|
||||
saveSettings() {
|
||||
if (typeof localStorage === 'undefined') return;
|
||||
if (typeof localStorage === "undefined") return;
|
||||
|
||||
try {
|
||||
localStorage.setItem(SETTINGS_STORAGE_KEY, JSON.stringify(this.settingsCache));
|
||||
localStorage.setItem(
|
||||
SETTINGS_STORAGE_KEY,
|
||||
JSON.stringify(this.settingsCache)
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("保存设置失败:", error);
|
||||
}
|
||||
@ -517,7 +502,10 @@ class SettingsManagerClass {
|
||||
}
|
||||
|
||||
// 添加对开发者选项依赖的检查
|
||||
if (definition.requireDeveloper && !this.settingsCache["developer.enabled"]) {
|
||||
if (
|
||||
definition.requireDeveloper &&
|
||||
!this.settingsCache["developer.enabled"]
|
||||
) {
|
||||
console.warn(`设置项 ${key} 需要启用开发者选项`);
|
||||
return false;
|
||||
}
|
||||
@ -546,7 +534,7 @@ class SettingsManagerClass {
|
||||
|
||||
// 为了保持向后兼容,同时更新旧的localStorage键
|
||||
const legacyKey = definition.legacyKey;
|
||||
if (legacyKey && typeof localStorage !== 'undefined') {
|
||||
if (legacyKey && typeof localStorage !== "undefined") {
|
||||
localStorage.setItem(legacyKey, value.toString());
|
||||
}
|
||||
|
||||
@ -610,7 +598,7 @@ class SettingsManagerClass {
|
||||
* @returns {Function} 取消监听的函数
|
||||
*/
|
||||
watchSettings(callback) {
|
||||
if (typeof window === 'undefined') return () => {};
|
||||
if (typeof window === "undefined") return () => {};
|
||||
|
||||
const handler = (event) => {
|
||||
if (event.key === SETTINGS_STORAGE_KEY) {
|
||||
@ -658,7 +646,7 @@ class SettingsManagerClass {
|
||||
const SettingsManager = new SettingsManagerClass();
|
||||
|
||||
// 在服务器端和客户端都能正常工作的初始化
|
||||
if (typeof window !== 'undefined') {
|
||||
if (typeof window !== "undefined") {
|
||||
SettingsManager.init();
|
||||
}
|
||||
|
||||
@ -669,7 +657,8 @@ const resetSetting = (key) => SettingsManager.resetSetting(key);
|
||||
const resetAllSettings = () => SettingsManager.resetAllSettings();
|
||||
const watchSettings = (callback) => SettingsManager.watchSettings(callback);
|
||||
const getSettingDefinition = (key) => SettingsManager.getSettingDefinition(key);
|
||||
const exportSettingsAsKeyValue = () => SettingsManager.exportSettingsAsKeyValue();
|
||||
const exportSettingsAsKeyValue = () =>
|
||||
SettingsManager.exportSettingsAsKeyValue();
|
||||
|
||||
// 导出单例和直接方法
|
||||
export {
|
||||
@ -681,5 +670,5 @@ export {
|
||||
resetAllSettings,
|
||||
watchSettings,
|
||||
getSettingDefinition,
|
||||
exportSettingsAsKeyValue
|
||||
exportSettingsAsKeyValue,
|
||||
};
|
||||
|
Loading…
x
Reference in New Issue
Block a user