mirror of
https://github.com/ZeroCatDev/Classworks.git
synced 2025-07-01 16:49:22 +00:00
Add uuid dependency and implement KV storage support in settings and data provider components. Enhance server settings with site key configuration and improve data handling for new KV providers.
This commit is contained in:
parent
00a693aeba
commit
c0e33dcbf9
@ -17,6 +17,7 @@
|
||||
"pinyin-pro": "^3.26.0",
|
||||
"roboto-fontface": "*",
|
||||
"typewriter-effect": "^2.21.0",
|
||||
"uuid": "^9.0.1",
|
||||
"vue": "^3.4.31",
|
||||
"vuetify": "^3.8.0"
|
||||
},
|
||||
|
9
pnpm-lock.yaml
generated
9
pnpm-lock.yaml
generated
@ -29,6 +29,9 @@ importers:
|
||||
typewriter-effect:
|
||||
specifier: ^2.21.0
|
||||
version: 2.21.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
uuid:
|
||||
specifier: ^9.0.1
|
||||
version: 9.0.1
|
||||
vue:
|
||||
specifier: ^3.4.31
|
||||
version: 3.5.13
|
||||
@ -3229,6 +3232,10 @@ packages:
|
||||
util-deprecate@1.0.2:
|
||||
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
||||
|
||||
uuid@9.0.1:
|
||||
resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==}
|
||||
hasBin: true
|
||||
|
||||
varint@6.0.0:
|
||||
resolution: {integrity: sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg==}
|
||||
|
||||
@ -6904,6 +6911,8 @@ snapshots:
|
||||
|
||||
util-deprecate@1.0.2: {}
|
||||
|
||||
uuid@9.0.1: {}
|
||||
|
||||
varint@6.0.0: {}
|
||||
|
||||
vite-plugin-pwa@1.0.0(@vite-pwa/assets-generator@1.0.0)(vite@5.4.17(sass-embedded@1.86.3)(sass@1.86.3)(terser@5.39.0))(workbox-build@7.3.0)(workbox-window@7.3.0):
|
||||
|
26
src/axios/axios.js
Normal file
26
src/axios/axios.js
Normal file
@ -0,0 +1,26 @@
|
||||
import axios from "axios";
|
||||
import { getSetting } from '@/utils/settings';
|
||||
|
||||
// 基本配置
|
||||
const axiosInstance = axios.create({
|
||||
// 可以在这里添加基础配置,例如超时时间等
|
||||
timeout: 10000,
|
||||
});
|
||||
|
||||
// 请求拦截器
|
||||
axiosInstance.interceptors.request.use(
|
||||
(requestConfig) => {
|
||||
// 确保每次请求时都获取最新的 siteKey
|
||||
const siteKey = getSetting('server.siteKey');
|
||||
if (siteKey) {
|
||||
requestConfig.headers["x-site-key"] = siteKey;
|
||||
}
|
||||
return requestConfig;
|
||||
},
|
||||
(error) => {
|
||||
console.log(error);
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
export default axiosInstance;
|
698
src/components/MigrationTool.vue
Normal file
698
src/components/MigrationTool.vue
Normal file
@ -0,0 +1,698 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-card class="mb-6">
|
||||
<v-card-title>迁移设置</v-card-title>
|
||||
<v-card-text>
|
||||
<v-row>
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model="classNumber"
|
||||
label="班级编号"
|
||||
hint="请输入需要迁移的班级编号"
|
||||
persistent-hint
|
||||
prepend-icon="mdi-account-group"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model="machineId"
|
||||
label="设备标识 (UUID)"
|
||||
hint="系统已自动填充设备标识,通常无需修改"
|
||||
persistent-hint
|
||||
prepend-icon="mdi-identifier"
|
||||
readonly
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-radio-group v-model="migrationType" class="mt-2">
|
||||
<v-radio value="local" label="本地数据迁移"></v-radio>
|
||||
<v-radio value="server" label="服务器数据迁移"></v-radio>
|
||||
</v-radio-group>
|
||||
|
||||
<div v-if="migrationType === 'server'" class="mt-4">
|
||||
<v-text-field
|
||||
v-model="serverUrl"
|
||||
label="服务器地址"
|
||||
hint="输入服务器域名,例如:https://example.com"
|
||||
persistent-hint
|
||||
prepend-icon="mdi-server"
|
||||
></v-text-field>
|
||||
|
||||
<v-alert
|
||||
density="compact"
|
||||
type="info"
|
||||
variant="outlined"
|
||||
class="mt-2"
|
||||
>
|
||||
服务器接口格式:<br>
|
||||
- 配置接口:域名/班号/config<br>
|
||||
- 作业数据接口:域名/班号/homework?date=YYYY-MM-DD
|
||||
</v-alert>
|
||||
|
||||
<div class="d-flex align-center mt-4">
|
||||
<v-icon color="warning" class="mr-2">mdi-calendar-range</v-icon>
|
||||
<span class="text-subtitle-1">选择迁移时间范围:</span>
|
||||
</div>
|
||||
|
||||
<v-row class="mt-1">
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model="startDate"
|
||||
label="开始日期"
|
||||
type="date"
|
||||
prepend-icon="mdi-calendar-start"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model="endDate"
|
||||
label="结束日期"
|
||||
type="date"
|
||||
prepend-icon="mdi-calendar-end"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<!-- 统一的数据显示卡片 -->
|
||||
<v-card class="mb-6">
|
||||
<v-card-title class="d-flex align-center">
|
||||
<span>{{ migrationType === 'local' ? '本地数据库内容' : '服务器数据预览' }}</span>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
color="primary"
|
||||
@click="migrationType === 'local' ? scanLocalDatabase() : previewServerData()"
|
||||
:loading="loading || scanning"
|
||||
>
|
||||
{{ migrationType === 'local' ? '扫描数据' : '预览数据' }}
|
||||
</v-btn>
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<v-alert
|
||||
v-if="displayItems.length === 0 && !loading && !scanning"
|
||||
type="info"
|
||||
>
|
||||
{{ migrationType === 'local'
|
||||
? '尚未扫描本地数据或未找到可迁移的数据。点击"扫描数据"按钮开始扫描。'
|
||||
: '尚未预览服务器数据或未找到可迁移的数据。点击"预览数据"按钮开始查询。'
|
||||
}}
|
||||
</v-alert>
|
||||
|
||||
<v-data-table
|
||||
v-if="displayItems.length > 0"
|
||||
:headers="headers"
|
||||
:items="displayItems"
|
||||
:items-per-page="10"
|
||||
item-value="key"
|
||||
class="elevation-1"
|
||||
>
|
||||
<template #[`item.type`]="{ item }">
|
||||
<v-chip :color="getItemType(item) === 'config' ? 'primary' : 'secondary'" size="small">
|
||||
{{ getItemType(item) === 'config' ? '配置' : '作业数据' }}
|
||||
</v-chip>
|
||||
</template>
|
||||
|
||||
<template #[`item.date`]="{ item }">
|
||||
{{ formatDate(getItemDate(item)) }}
|
||||
</template>
|
||||
</v-data-table>
|
||||
|
||||
<v-alert
|
||||
v-if="displayItems.length > 0"
|
||||
type="info"
|
||||
density="compact"
|
||||
class="mt-2"
|
||||
>
|
||||
系统将迁移表格中显示的所有数据项,迁移前请确认数据完整性。
|
||||
</v-alert>
|
||||
|
||||
<v-skeleton-loader v-if="loading || scanning" type="table"></v-skeleton-loader>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<v-card class="mb-6">
|
||||
<v-card-title>迁移目标</v-card-title>
|
||||
<v-card-text>
|
||||
<v-radio-group v-model="targetStorage">
|
||||
<v-radio value="kv-local" label="本地 KV 存储"></v-radio>
|
||||
<v-radio value="kv-server" label="服务器 KV 存储"></v-radio>
|
||||
</v-radio-group>
|
||||
|
||||
<div v-if="targetStorage === 'kv-server'" class="mt-4">
|
||||
<v-text-field
|
||||
v-model="targetServerUrl"
|
||||
label="目标服务器地址"
|
||||
hint="输入KV服务器地址,例如:https://example.com/kv-api"
|
||||
persistent-hint
|
||||
prepend-icon="mdi-server-network"
|
||||
></v-text-field>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<div class="d-flex justify-end mb-6">
|
||||
<v-btn
|
||||
color="success"
|
||||
@click="startMigration"
|
||||
:loading="migrating"
|
||||
:disabled="!canMigrate"
|
||||
>
|
||||
开始迁移
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<v-dialog v-model="showResult" max-width="600">
|
||||
<v-card>
|
||||
<v-card-title class="d-flex align-center">
|
||||
<v-icon
|
||||
:color="migrationSuccess ? 'success' : 'error'"
|
||||
class="mr-2"
|
||||
>
|
||||
{{ migrationSuccess ? 'mdi-check-circle' : 'mdi-alert-circle' }}
|
||||
</v-icon>
|
||||
<span>{{ migrationSuccess ? '迁移成功' : '迁移失败' }}</span>
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<v-alert
|
||||
v-if="migrationError"
|
||||
type="error"
|
||||
class="mb-4"
|
||||
>
|
||||
{{ migrationError }}
|
||||
</v-alert>
|
||||
|
||||
<div v-if="migrationSuccess">
|
||||
<p>成功迁移 {{ migrationStats.success }} 项数据到 {{ targetStorage === 'kv-local' ? '本地' : '服务器' }} KV 存储。</p>
|
||||
<v-divider class="my-4"></v-divider>
|
||||
<v-list>
|
||||
<v-list-subheader>迁移详情</v-list-subheader>
|
||||
<v-list-item v-for="(item, index) in migrationResults" :key="index">
|
||||
<v-list-item-title>
|
||||
{{ item.key }}
|
||||
</v-list-item-title>
|
||||
<v-list-item-subtitle>
|
||||
{{ item.success ? '成功' : '失败' }} {{ item.message }}
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</div>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
color="primary"
|
||||
@click="showResult = false"
|
||||
>
|
||||
关闭
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { openDB } from 'idb';
|
||||
import axios from '@/axios/axios';
|
||||
import { getSetting } from '@/utils/settings';
|
||||
|
||||
export default {
|
||||
name: 'MigrationTool',
|
||||
data() {
|
||||
return {
|
||||
classNumber: 'G2405',
|
||||
machineId: '',
|
||||
migrationType: 'server',
|
||||
serverUrl: 'https://class.wuyuan.dev',
|
||||
targetStorage: 'kv-server',
|
||||
targetServerUrl: 'http://localhost:3030',
|
||||
startDate: this.getDateString(new Date(Date.now() - 30 * 24 * 60 * 60 * 1000)), // 30 days ago
|
||||
endDate: this.getDateString(new Date()),
|
||||
loading: false,
|
||||
scanning: false,
|
||||
migrating: false,
|
||||
showServerPreview: false,
|
||||
showResult: false,
|
||||
migrationSuccess: false,
|
||||
migrationError: null,
|
||||
migrationStats: {
|
||||
total: 0,
|
||||
success: 0,
|
||||
failed: 0
|
||||
},
|
||||
migrationResults: [],
|
||||
localDbItems: [],
|
||||
serverItems: [],
|
||||
selectedItems: [],
|
||||
headers: [
|
||||
{ title: '类型', key: 'type', sortable: true },
|
||||
{ title: '键名', key: 'key', sortable: true },
|
||||
{ title: '日期', key: 'date', sortable: true },
|
||||
{ title: '大小', key: 'size', sortable: true }
|
||||
]
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
displayItems() {
|
||||
return this.migrationType === 'local' ? this.localDbItems : this.serverItems;
|
||||
},
|
||||
canMigrate() {
|
||||
return (
|
||||
this.classNumber &&
|
||||
this.machineId &&
|
||||
this.displayItems.length > 0 &&
|
||||
(this.targetStorage !== 'kv-server' || this.targetServerUrl)
|
||||
);
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
try {
|
||||
await this.initMachineId();
|
||||
} catch (error) {
|
||||
console.error('初始化设备ID失败:', error);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 安全地获取项目类型,处理可能的undefined情况
|
||||
getItemType(item) {
|
||||
if (!item) return '';
|
||||
// 处理直接对象和v-data-table的item对象格式
|
||||
return item.raw ? item.raw.type : item.type;
|
||||
},
|
||||
|
||||
// 安全地获取项目日期,处理可能的undefined情况
|
||||
getItemDate(item) {
|
||||
if (!item) return null;
|
||||
return item.raw ? item.raw.date : item.date;
|
||||
},
|
||||
|
||||
getDateString(date) {
|
||||
return date.toISOString().split('T')[0];
|
||||
},
|
||||
|
||||
// 初始化设备ID
|
||||
async initMachineId() {
|
||||
this.machineId = getSetting("device.uuid");
|
||||
},
|
||||
|
||||
// 获取请求头,包含网站令牌
|
||||
getRequestHeaders() {
|
||||
const headers = { Accept: "application/json" };
|
||||
const siteKey = getSetting("server.siteKey");
|
||||
|
||||
if (siteKey) {
|
||||
headers['x-site-key'] = siteKey;
|
||||
}
|
||||
|
||||
return headers;
|
||||
},
|
||||
|
||||
// 扫描本地数据库
|
||||
async scanLocalDatabase() {
|
||||
if (!this.classNumber) {
|
||||
this.$emit('message', { text: '请先输入班级编号', type: 'error' });
|
||||
return;
|
||||
}
|
||||
|
||||
this.scanning = true;
|
||||
this.localDbItems = [];
|
||||
|
||||
try {
|
||||
// 打开数据库
|
||||
const db = await openDB("ClassworksDB", 2);
|
||||
|
||||
// 检查旧的数据存储
|
||||
if (db.objectStoreNames.contains("homework") && db.objectStoreNames.contains("config")) {
|
||||
// 从旧的作业存储中读取数据
|
||||
const homework = db.transaction("homework", "readonly");
|
||||
const hwStore = homework.objectStore("homework");
|
||||
const hwKeys = await hwStore.getAllKeys();
|
||||
|
||||
// 过滤当前班级的键
|
||||
const classKeys = hwKeys.filter(key => key.startsWith(`homework_${this.classNumber}_`));
|
||||
|
||||
for (const key of classKeys) {
|
||||
const value = await hwStore.get(key);
|
||||
|
||||
// 从键中提取日期: homework_classNumber_YYYY-MM-DD
|
||||
const datePart = key.split('_')[2];
|
||||
let dateObj = null;
|
||||
|
||||
if (datePart) {
|
||||
const [year, month, day] = datePart.split('-');
|
||||
dateObj = new Date(year, month - 1, day);
|
||||
}
|
||||
|
||||
this.localDbItems.push({
|
||||
type: 'homework',
|
||||
key,
|
||||
originalKey: key,
|
||||
date: dateObj,
|
||||
size: this.getDataSize(value) + ' KB',
|
||||
value
|
||||
});
|
||||
}
|
||||
|
||||
// 检查配置
|
||||
const configKey = `config_${this.classNumber}`;
|
||||
const configValue = await db.get("config", configKey);
|
||||
|
||||
if (configValue) {
|
||||
this.localDbItems.push({
|
||||
type: 'config',
|
||||
key: configKey,
|
||||
originalKey: configKey,
|
||||
date: null,
|
||||
size: this.getDataSize(configValue) + ' KB',
|
||||
value: configValue
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 检查新的KV存储中是否已有数据
|
||||
if (db.objectStoreNames.contains("kv")) {
|
||||
const kvTx = db.transaction("kv", "readonly");
|
||||
const kvStore = kvTx.objectStore("kv");
|
||||
const kvKeys = await kvStore.getAllKeys();
|
||||
|
||||
// 过滤当前班级的键
|
||||
const classKvKeys = kvKeys.filter(key => key.startsWith(`${this.classNumber}/`));
|
||||
|
||||
for (const key of classKvKeys) {
|
||||
const value = await kvStore.get(key);
|
||||
|
||||
// 判断是配置还是作业数据
|
||||
const isConfig = key.includes(`/${this.classNumber}/classworks-config`);
|
||||
let dateObj = null;
|
||||
|
||||
if (!isConfig) {
|
||||
// 从键中提取日期: classNumber/classworks-data-YYYYMMDD
|
||||
const match = key.match(/classworks-data-(\d{4})(\d{2})(\d{2})/);
|
||||
if (match) {
|
||||
const [, year, month, day] = match;
|
||||
dateObj = new Date(year, parseInt(month) - 1, day);
|
||||
}
|
||||
}
|
||||
|
||||
this.localDbItems.push({
|
||||
type: isConfig ? 'config' : 'homework',
|
||||
key,
|
||||
originalKey: key,
|
||||
date: dateObj,
|
||||
size: this.getDataSize(value) + ' KB',
|
||||
value,
|
||||
isKv: true
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('扫描本地数据库失败:', error);
|
||||
this.$emit('message', { text: '扫描数据库失败: ' + error.message, type: 'error' });
|
||||
} finally {
|
||||
this.scanning = false;
|
||||
}
|
||||
},
|
||||
|
||||
// 计算数据大小(KB)
|
||||
getDataSize(data) {
|
||||
if (!data) return 0;
|
||||
const str = typeof data === 'string' ? data : JSON.stringify(data);
|
||||
return Math.round((str.length * 2) / 1024 * 100) / 100; // 近似值
|
||||
},
|
||||
|
||||
// 格式化日期显示
|
||||
formatDate(date) {
|
||||
if (!date) return '配置 (无日期)';
|
||||
return date.toLocaleDateString();
|
||||
},
|
||||
|
||||
// 预览服务器数据
|
||||
async previewServerData() {
|
||||
if (!this.serverUrl || !this.classNumber || !this.startDate || !this.endDate) {
|
||||
this.$emit('message', { text: '请填写完整的服务器信息和时间范围', type: 'error' });
|
||||
return;
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
this.serverItems = [];
|
||||
|
||||
try {
|
||||
// 先获取配置信息
|
||||
try {
|
||||
// 构建配置请求URL: 域名/班号/config
|
||||
const configUrl = `${this.serverUrl}/${this.classNumber}/config`;
|
||||
|
||||
const configRes = await axios.get(configUrl, {
|
||||
headers: this.getRequestHeaders()
|
||||
});
|
||||
if (configRes.data) {
|
||||
this.serverItems.push({
|
||||
type: 'config',
|
||||
key: `config_${this.classNumber}`,
|
||||
originalKey: configUrl,
|
||||
date: null,
|
||||
size: this.getDataSize(configRes.data) + ' KB',
|
||||
value: configRes.data
|
||||
});
|
||||
}
|
||||
} catch (configError) {
|
||||
console.warn('无法获取配置:', configError);
|
||||
}
|
||||
|
||||
// 获取日期范围内的所有数据
|
||||
const start = new Date(this.startDate);
|
||||
const end = new Date(this.endDate);
|
||||
const dateArray = this.getDateArray(start, end);
|
||||
|
||||
for (const date of dateArray) {
|
||||
const dateStr = this.formatDateForServer(date);
|
||||
try {
|
||||
// 构建数据请求URL: 域名/班号/homework?date=YYYY-MM-DD
|
||||
const homeworkUrl = `${this.serverUrl}/${this.classNumber}/homework?date=${dateStr}`;
|
||||
|
||||
const res = await axios.get(homeworkUrl, {
|
||||
headers: this.getRequestHeaders()
|
||||
});
|
||||
if (res.data) {
|
||||
this.serverItems.push({
|
||||
type: 'homework',
|
||||
key: `homework_${this.classNumber}_${dateStr}`,
|
||||
originalKey: homeworkUrl,
|
||||
date,
|
||||
size: this.getDataSize(res.data) + ' KB',
|
||||
value: res.data
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.response?.status !== 404) {
|
||||
console.warn(`无法获取 ${dateStr} 的数据:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.showServerPreview = true;
|
||||
} catch (error) {
|
||||
console.error('预览服务器数据失败:', error);
|
||||
this.$emit('message', { text: '预览数据失败: ' + error.message, type: 'error' });
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
// 获取日期范围内的所有日期
|
||||
getDateArray(start, end) {
|
||||
const dateArray = [];
|
||||
const currentDate = new Date(start);
|
||||
|
||||
while (currentDate <= end) {
|
||||
dateArray.push(new Date(currentDate));
|
||||
currentDate.setDate(currentDate.getDate() + 1);
|
||||
}
|
||||
|
||||
return dateArray;
|
||||
},
|
||||
|
||||
// 格式化日期为服务器格式 (YYYY-MM-DD)
|
||||
formatDateForServer(date) {
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
return `${year}-${month}-${day}`;
|
||||
},
|
||||
|
||||
// 格式化日期为KV存储格式 (YYYYMMDD)
|
||||
formatDateForKv(date) {
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
return `${year}${month}${day}`;
|
||||
},
|
||||
|
||||
// 开始迁移
|
||||
async startMigration() {
|
||||
if (!this.canMigrate) {
|
||||
this.$emit('message', { text: '无法开始迁移,请检查配置', type: 'error' });
|
||||
return;
|
||||
}
|
||||
|
||||
this.migrating = true;
|
||||
this.migrationResults = [];
|
||||
this.migrationStats = {
|
||||
total: this.displayItems.length,
|
||||
success: 0,
|
||||
failed: 0
|
||||
};
|
||||
|
||||
try {
|
||||
// 处理所有数据项,但先处理配置,再处理作业数据
|
||||
const configItems = this.displayItems.filter(item => this.getItemType(item) === 'config');
|
||||
const dataItems = this.displayItems.filter(item => this.getItemType(item) === 'homework');
|
||||
|
||||
// 先处理配置项
|
||||
for (const item of configItems) {
|
||||
await this.migrateItem(item);
|
||||
}
|
||||
|
||||
// 再处理数据项
|
||||
for (const item of dataItems) {
|
||||
await this.migrateItem(item);
|
||||
}
|
||||
|
||||
this.migrationSuccess = this.migrationStats.failed === 0;
|
||||
this.showResult = true;
|
||||
} catch (error) {
|
||||
console.error('迁移过程出错:', error);
|
||||
this.migrationSuccess = false;
|
||||
this.migrationError = error.message;
|
||||
this.showResult = true;
|
||||
} finally {
|
||||
this.migrating = false;
|
||||
}
|
||||
},
|
||||
|
||||
// 迁移单个数据项
|
||||
async migrateItem(item) {
|
||||
try {
|
||||
let result;
|
||||
|
||||
if (this.targetStorage === 'kv-local') {
|
||||
result = await this.migrateToLocalKv(item);
|
||||
} else {
|
||||
result = await this.migrateToServerKv(item);
|
||||
}
|
||||
|
||||
this.migrationResults.push({
|
||||
key: item.key,
|
||||
success: result.success,
|
||||
message: result.message
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
this.migrationStats.success++;
|
||||
} else {
|
||||
this.migrationStats.failed++;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`迁移 ${item.key} 失败:`, error);
|
||||
this.migrationResults.push({
|
||||
key: item.key,
|
||||
success: false,
|
||||
message: error.message
|
||||
});
|
||||
this.migrationStats.failed++;
|
||||
}
|
||||
},
|
||||
|
||||
// 迁移到本地KV存储
|
||||
async migrateToLocalKv(item) {
|
||||
try {
|
||||
const db = await openDB("ClassworksDB", 2, {
|
||||
upgrade(db) {
|
||||
if (!db.objectStoreNames.contains("kv")) {
|
||||
db.createObjectStore("kv");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const value = typeof item.value === 'string' ? JSON.parse(item.value) : item.value;
|
||||
const itemType = this.getItemType(item);
|
||||
|
||||
if (itemType === 'config') {
|
||||
// 配置键名: classNumber/classworks-config
|
||||
await db.put("kv", JSON.stringify(value), `${this.classNumber}/classworks-config`);
|
||||
return { success: true, message: '配置已迁移' };
|
||||
} else {
|
||||
// 数据键名: classNumber/classworks-data-YYYYMMDD
|
||||
const itemDate = this.getItemDate(item);
|
||||
let dateStr;
|
||||
|
||||
if (itemDate) {
|
||||
dateStr = this.formatDateForKv(itemDate);
|
||||
} else {
|
||||
// 尝试从键名提取日期
|
||||
const match = item.key.match(/(\d{4})-(\d{2})-(\d{2})/);
|
||||
if (match) {
|
||||
const [, year, month, day] = match;
|
||||
dateStr = `${year}${month}${day}`;
|
||||
} else {
|
||||
return { success: false, message: '无法确定日期格式' };
|
||||
}
|
||||
}
|
||||
|
||||
await db.put("kv", JSON.stringify(value), `${this.classNumber}/classworks-data-${dateStr}`);
|
||||
return { success: true, message: `${dateStr} 数据已迁移` };
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('本地KV迁移失败:', error);
|
||||
return { success: false, message: error.message };
|
||||
}
|
||||
},
|
||||
|
||||
// 迁移到服务器KV存储
|
||||
async migrateToServerKv(item) {
|
||||
try {
|
||||
const value = typeof item.value === 'string' ? JSON.parse(item.value) : item.value;
|
||||
const itemType = this.getItemType(item);
|
||||
|
||||
if (itemType === 'config') {
|
||||
// 配置
|
||||
await axios.post(`${this.targetServerUrl}/${this.machineId}/classworks-config`, value, {
|
||||
headers: this.getRequestHeaders()
|
||||
});
|
||||
return { success: true, message: '配置已迁移到服务器' };
|
||||
} else {
|
||||
// 数据
|
||||
const itemDate = this.getItemDate(item);
|
||||
let dateStr;
|
||||
|
||||
if (itemDate) {
|
||||
dateStr = this.formatDateForKv(itemDate);
|
||||
} else {
|
||||
// 尝试从键名提取日期
|
||||
const match = item.key.match(/(\d{4})-(\d{2})-(\d{2})/);
|
||||
if (match) {
|
||||
const [, year, month, day] = match;
|
||||
dateStr = `${year}${month}${day}`;
|
||||
} else {
|
||||
return { success: false, message: '无法确定日期格式' };
|
||||
}
|
||||
}
|
||||
|
||||
await axios.post(`${this.targetServerUrl}/${this.machineId}/classworks-data-${dateStr}`, value, {
|
||||
headers: this.getRequestHeaders()
|
||||
});
|
||||
return { success: true, message: `${dateStr} 数据已迁移到服务器` };
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('服务器KV迁移失败:', error);
|
||||
return { success: false, message: error.response?.data?.message || error.message };
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
@ -35,7 +35,7 @@
|
||||
@click="adjustValue(stepValue)" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<v-menu location="bottom">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn icon="mdi-dots-vertical" size="small" variant="text" v-bind="props" class="ml-2"
|
||||
@ -170,7 +170,9 @@ export default {
|
||||
},
|
||||
'server.provider': {
|
||||
'server': '远程服务器',
|
||||
'indexedDB': '本地存储'
|
||||
'indexedDB': '本地存储',
|
||||
'kv-local': 'KV本地存储',
|
||||
'kv-server': 'KV远程服务器'
|
||||
}
|
||||
},
|
||||
// 默认图标映射,按设置类型
|
||||
@ -233,12 +235,12 @@ export default {
|
||||
if (this.icon) {
|
||||
return this.icon;
|
||||
}
|
||||
|
||||
|
||||
// 其次使用定义中的图标
|
||||
if (this.definition && this.definition.icon) {
|
||||
return this.definition.icon;
|
||||
}
|
||||
|
||||
|
||||
// 最后使用基于类型的默认图标
|
||||
return this.defaultIcons[this.type] || 'mdi-cog-outline';
|
||||
}
|
||||
|
@ -2,7 +2,7 @@
|
||||
<settings-card title="数据源设置" icon="mdi-database-cog">
|
||||
<v-list>
|
||||
<!-- 服务器模式设置 -->
|
||||
<template v-if="currentProvider === 'server'">
|
||||
<template v-if="currentProvider === 'server' || currentProvider === 'kv-server'">
|
||||
<v-list-item>
|
||||
<template #prepend>
|
||||
<v-icon icon="mdi-lan-connect" class="mr-3" />
|
||||
@ -17,7 +17,7 @@
|
||||
</template>
|
||||
|
||||
<!-- IndexedDB设置 -->
|
||||
<template v-if="currentProvider === 'indexedDB'">
|
||||
<template v-if="currentProvider === 'indexedDB' || currentProvider === 'kv-local'">
|
||||
<v-list-item>
|
||||
<template #prepend>
|
||||
<v-icon icon="mdi-database" class="mr-3" />
|
||||
@ -39,7 +39,32 @@
|
||||
<v-btn variant="tonal" @click="exportData"> 导出 </v-btn>
|
||||
</template>
|
||||
</v-list-item>
|
||||
|
||||
<!-- 显示机器ID,仅对KV本地存储有效 -->
|
||||
<v-list-item v-if="currentProvider === 'kv-local'">
|
||||
<template #prepend>
|
||||
<v-icon icon="mdi-identifier" class="mr-3" />
|
||||
</template>
|
||||
<v-list-item-title>本机唯一标识符</v-list-item-title>
|
||||
<v-list-item-subtitle v-if="machineId">{{ machineId }}</v-list-item-subtitle>
|
||||
<v-list-item-subtitle v-else>正在加载...</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
|
||||
<!-- 数据迁移,仅对KV本地存储有效 -->
|
||||
<v-list-item v-if="currentProvider === 'kv-local'">
|
||||
<template #prepend>
|
||||
<v-icon icon="mdi-database-import" class="mr-3" />
|
||||
</template>
|
||||
<v-list-item-title>迁移旧数据</v-list-item-title>
|
||||
<v-list-item-subtitle>将旧的存储格式数据转移到新的KV存储</v-list-item-subtitle>
|
||||
<template #append>
|
||||
<v-btn :loading="migrateLoading" variant="tonal" @click="migrateData">
|
||||
迁移
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</template>
|
||||
|
||||
<v-list-item>
|
||||
<template #prepend>
|
||||
<v-icon icon="mdi-lan-connect" class="mr-3" />
|
||||
@ -50,7 +75,22 @@
|
||||
查看
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-list-item> <v-list-item>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item>
|
||||
<template #prepend>
|
||||
<v-icon icon="mdi-database-sync" class="mr-3" />
|
||||
</template>
|
||||
<v-list-item-title>高级数据迁移工具</v-list-item-title>
|
||||
<v-list-item-subtitle>更强大的数据迁移工具,支持从本地或服务器迁移到KV存储</v-list-item-subtitle>
|
||||
<template #append>
|
||||
<v-btn variant="tonal" color="primary" to="/datamigration">
|
||||
打开
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item>
|
||||
<template #prepend>
|
||||
<v-icon icon="mdi-lan-connect" class="mr-3" />
|
||||
</template>
|
||||
@ -82,6 +122,8 @@
|
||||
import SettingsCard from "@/components/SettingsCard.vue";
|
||||
import { getSetting } from "@/utils/settings";
|
||||
import axios from "axios";
|
||||
import { getMachineId } from "@/utils/providers/kvProvider";
|
||||
|
||||
export default {
|
||||
name: "DataProviderSettingsCard",
|
||||
components: { SettingsCard },
|
||||
@ -94,6 +136,8 @@ export default {
|
||||
confirmTitle: "",
|
||||
confirmMessage: "",
|
||||
confirmAction: null,
|
||||
machineId: null,
|
||||
migrateLoading: false,
|
||||
};
|
||||
},
|
||||
|
||||
@ -101,6 +145,21 @@ export default {
|
||||
currentProvider() {
|
||||
return getSetting("server.provider");
|
||||
},
|
||||
|
||||
isKvProvider() {
|
||||
return this.currentProvider === 'kv-local' || this.currentProvider === 'kv-server';
|
||||
}
|
||||
},
|
||||
|
||||
async created() {
|
||||
// 如果是KV本地存储,获取机器ID
|
||||
if (this.currentProvider === 'kv-local') {
|
||||
try {
|
||||
this.machineId = await getMachineId();
|
||||
} catch (error) {
|
||||
console.error("获取机器ID失败:", error);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
@ -109,9 +168,17 @@ export default {
|
||||
this.serverchecktime = new Date();
|
||||
try {
|
||||
const domain = getSetting("server.domain");
|
||||
const siteKey = getSetting("server.siteKey");
|
||||
|
||||
// Prepare headers including site key if available
|
||||
const headers = { Accept: "application/json" };
|
||||
if (siteKey) {
|
||||
headers['x-site-key'] = siteKey;
|
||||
}
|
||||
|
||||
const response = await axios.get(`${domain}/api/test`, {
|
||||
method: "GET",
|
||||
headers: { Accept: "application/json" },
|
||||
headers
|
||||
});
|
||||
|
||||
if (response.data.status === "success") {
|
||||
@ -160,6 +227,13 @@ export default {
|
||||
await window.indexedDB.deleteDatabase(DBName);
|
||||
this.$message.success("清除成功", "数据库缓存已清除");
|
||||
this.confirmDialog = false;
|
||||
|
||||
// 如果是KV提供者,需要刷新页面以生成新的UUID
|
||||
if (this.isKvProvider) {
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 1500);
|
||||
}
|
||||
} catch (error) {
|
||||
this.$message.error("清除失败", error.message);
|
||||
}
|
||||
@ -214,6 +288,12 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
async migrateData() {
|
||||
this.migrateLoading = true;
|
||||
this.$router.push('/datamigration');
|
||||
this.migrateLoading = false;
|
||||
},
|
||||
|
||||
handleConfirm() {
|
||||
if (this.confirmAction) {
|
||||
this.confirmAction();
|
||||
|
@ -2,11 +2,32 @@
|
||||
<settings-card title="数据源设置" icon="mdi-database" :loading="loading">
|
||||
<v-form>
|
||||
<setting-item setting-key="server.provider" title="数据提供者" />
|
||||
<v-divider class="my-2" />
|
||||
<setting-item setting-key="server.domain" title="服务器域名" /> <v-divider class="my-2" />
|
||||
|
||||
<v-alert v-if="isKvProvider" type="info" variant="tonal" class="my-2">
|
||||
<v-alert-title>KV 存储系统</v-alert-title>
|
||||
<p>KV存储系统使用本机唯一标识符(UUID)来区分不同设备的数据。</p>
|
||||
<p v-if="currentProvider === 'kv-server'">
|
||||
服务器端点格式: <code>http(s)://服务器域名/</code><br>
|
||||
在服务器域名处仅填写基础URL,不需要任何路径。
|
||||
</p>
|
||||
</v-alert>
|
||||
|
||||
<v-divider class="my-2" />
|
||||
<setting-item setting-key="server.domain" title="服务器域名" />
|
||||
<v-divider class="my-2" />
|
||||
<setting-item setting-key="server.classNumber" title="班号" />
|
||||
|
||||
<v-divider class="my-2" />
|
||||
<setting-item setting-key="server.siteKey" title="网站令牌">
|
||||
<template #description>
|
||||
用于后端验证请求的安全令牌。如需要,请从系统管理员获取。
|
||||
</template>
|
||||
</setting-item>
|
||||
<v-alert v-if="useServer" type="info" variant="tonal" class="my-2">
|
||||
<v-icon icon="mdi-information-outline" class="mr-2"></v-icon>
|
||||
<span>网站令牌将作为 <code>x-site-key</code> 请求头发送给服务器,用于验证请求的合法性。如果您的服务器需要此验证,请在上方输入有效的令牌。</span>
|
||||
</v-alert>
|
||||
<v-divider class="my-2" />
|
||||
<setting-item setting-key="device.uuid" title="设备UUID" />
|
||||
</v-form>
|
||||
</settings-card>
|
||||
</template>
|
||||
@ -14,19 +35,27 @@
|
||||
<script>
|
||||
import SettingsCard from "@/components/SettingsCard.vue";
|
||||
import SettingItem from "@/components/settings/SettingItem.vue";
|
||||
import { getSetting } from "@/utils/settings";
|
||||
|
||||
export default {
|
||||
name: "ServerSettingsCard",
|
||||
components: { SettingsCard },
|
||||
components: { SettingsCard, SettingItem },
|
||||
props: {
|
||||
loading: Boolean,
|
||||
},
|
||||
data() {
|
||||
|
||||
|
||||
return {
|
||||
|
||||
};
|
||||
return {};
|
||||
},
|
||||
|
||||
computed: {
|
||||
currentProvider() {
|
||||
return getSetting("server.provider");
|
||||
},
|
||||
isKvProvider() {
|
||||
return this.currentProvider === 'kv-local' || this.currentProvider === 'kv-server';
|
||||
},
|
||||
useServer() {
|
||||
return this.currentProvider === 'server' || this.currentProvider === 'kv-server';
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
38
src/pages/DataMigration.vue
Normal file
38
src/pages/DataMigration.vue
Normal file
@ -0,0 +1,38 @@
|
||||
<template>
|
||||
<v-container>
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<div class="d-flex align-center mb-6">
|
||||
<v-icon size="x-large" color="primary" class="mr-3">mdi-database-sync</v-icon>
|
||||
<div>
|
||||
<h1 class="text-h4">数据迁移工具</h1>
|
||||
<div class="text-subtitle-1 text-grey">将现有数据迁移至 KV 存储系统</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<v-card class="mb-6" variant="tonal" color="info" density="compact">
|
||||
<v-card-text class="d-flex align-center">
|
||||
<v-icon color="info" class="mr-2">mdi-information-outline</v-icon>
|
||||
<span>使用此工具可以将数据从旧存储系统迁移到新的 KV 存储系统,选择本地或云端迁移,以确保数据不会丢失。</span>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<MigrationTool />
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MigrationTool from '@/components/MigrationTool.vue';
|
||||
|
||||
export default {
|
||||
name: 'DataMigrationPage',
|
||||
components: {
|
||||
MigrationTool
|
||||
},
|
||||
metaInfo: {
|
||||
title: '数据迁移工具'
|
||||
}
|
||||
}
|
||||
</script>
|
@ -1,5 +1,7 @@
|
||||
import { serverProvider } from './providers/server';
|
||||
import { indexedDBProvider } from './providers/indexedDB';
|
||||
import { serverProvider } from "./providers/server";
|
||||
import { indexedDBProvider } from "./providers/indexedDB";
|
||||
import { kvProvider } from "./providers/kvProvider";
|
||||
import { getSetting } from "./settings";
|
||||
|
||||
export const formatResponse = (data, message = null) => ({
|
||||
success: true,
|
||||
@ -12,18 +14,86 @@ export const formatError = (message, code = "UNKNOWN_ERROR") => ({
|
||||
error: { code, message },
|
||||
});
|
||||
|
||||
const providers = {
|
||||
// Legacy providers
|
||||
const legacyProviders = {
|
||||
server: serverProvider,
|
||||
indexedDB: indexedDBProvider,
|
||||
};
|
||||
|
||||
// New KV provider
|
||||
const newProviders = {
|
||||
kv: kvProvider,
|
||||
};
|
||||
|
||||
// Main data provider with support for both legacy and new API
|
||||
export default {
|
||||
loadData: (provider, key, date) => providers[provider]?.loadData(key, date),
|
||||
saveData: (provider, key, data, date) =>
|
||||
providers[provider]?.saveData(key, data, date),
|
||||
loadConfig: (provider, key) => providers[provider]?.loadConfig(key),
|
||||
saveConfig: (provider, key, config) =>
|
||||
providers[provider]?.saveConfig(key, config),
|
||||
// Provider API methods
|
||||
loadData: (provider, key, date) => {
|
||||
if (legacyProviders[provider]) {
|
||||
return legacyProviders[provider]?.loadData(key, date);
|
||||
}
|
||||
|
||||
// If using new KV provider
|
||||
if (provider === "kv-local") {
|
||||
const classNumber = key.split("/").pop();
|
||||
return newProviders.kv.local.loadData(classNumber, date);
|
||||
} else if (provider === "kv-server") {
|
||||
const classNumber = key.split("/").pop();
|
||||
const serverUrl = getSetting("server.domain");
|
||||
return newProviders.kv.server.loadData(serverUrl, classNumber, date);
|
||||
}
|
||||
},
|
||||
|
||||
saveData: (provider, key, data, date) => {
|
||||
if (legacyProviders[provider]) {
|
||||
return legacyProviders[provider]?.saveData(key, data, date);
|
||||
}
|
||||
|
||||
// If using new KV provider
|
||||
if (provider === "kv-local") {
|
||||
const classNumber = key.split("/").pop();
|
||||
return newProviders.kv.local.saveData(classNumber, data, date);
|
||||
} else if (provider === "kv-server") {
|
||||
const classNumber = key.split("/").pop();
|
||||
const serverUrl = getSetting("server.domain");
|
||||
return newProviders.kv.server.saveData(
|
||||
serverUrl,
|
||||
classNumber,
|
||||
data,
|
||||
date
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
loadConfig: (provider, key) => {
|
||||
if (legacyProviders[provider]) {
|
||||
return legacyProviders[provider]?.loadConfig(key);
|
||||
}
|
||||
|
||||
// If using new KV provider
|
||||
if (provider === "kv-local") {
|
||||
const classNumber = key.split("/").pop();
|
||||
return newProviders.kv.local.loadConfig(classNumber);
|
||||
} else if (provider === "kv-server") {
|
||||
const serverUrl = getSetting("server.domain");
|
||||
return newProviders.kv.server.loadConfig(serverUrl);
|
||||
}
|
||||
},
|
||||
|
||||
saveConfig: (provider, key, config) => {
|
||||
if (legacyProviders[provider]) {
|
||||
return legacyProviders[provider]?.saveConfig(key, config);
|
||||
}
|
||||
|
||||
// If using new KV provider
|
||||
if (provider === "kv-local") {
|
||||
const classNumber = key.split("/").pop();
|
||||
return newProviders.kv.local.saveConfig(classNumber, config);
|
||||
} else if (provider === "kv-server") {
|
||||
const serverUrl = getSetting("server.domain");
|
||||
return newProviders.kv.server.saveConfig(serverUrl, config);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export const ErrorCodes = {
|
||||
|
250
src/utils/providers/kvProvider.js
Normal file
250
src/utils/providers/kvProvider.js
Normal file
@ -0,0 +1,250 @@
|
||||
import axios from '@/axios/axios';
|
||||
import { formatResponse, formatError } from '../dataProvider';
|
||||
import { openDB } from 'idb';
|
||||
import { getSetting } from '../settings';
|
||||
|
||||
// Constants for key names
|
||||
const CONFIG_KEY = 'classworks-config';
|
||||
const DATA_KEY_PREFIX = 'classworks-data-';
|
||||
|
||||
// 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;
|
||||
};
|
||||
|
||||
// Get machine UUID from settings
|
||||
const getMachineId = () => {
|
||||
return getSetting("device.uuid");
|
||||
};
|
||||
|
||||
// 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");
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// Format date as YYYYMMDD for keys
|
||||
const formatDateForKey = (date) => {
|
||||
const d = new Date(date);
|
||||
const year = d.getFullYear();
|
||||
const month = String(d.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(d.getDate()).padStart(2, '0');
|
||||
return `${year}${month}${day}`;
|
||||
};
|
||||
|
||||
export const kvProvider = {
|
||||
// Local storage provider
|
||||
local: {
|
||||
async loadData(classNumber, date) {
|
||||
try {
|
||||
if (!classNumber) {
|
||||
return formatError("请先设置班号", "CONFIG_ERROR");
|
||||
}
|
||||
|
||||
const formattedDate = formatDateForKey(date);
|
||||
const key = `${DATA_KEY_PREFIX}${formattedDate}`;
|
||||
|
||||
const db = await initDB();
|
||||
const data = await db.get("kv", `${classNumber}/${key}`);
|
||||
|
||||
if (!data) {
|
||||
const today = new Date().toISOString().split("T")[0];
|
||||
if (date === today) {
|
||||
// Return default data for today
|
||||
return formatResponse({
|
||||
homework: {},
|
||||
attendance: {
|
||||
absent: [],
|
||||
late: []
|
||||
}
|
||||
});
|
||||
}
|
||||
return formatError("数据不存在", "NOT_FOUND");
|
||||
}
|
||||
|
||||
return formatResponse(JSON.parse(data));
|
||||
} catch (error) {
|
||||
return formatError("读取本地数据失败:" + error);
|
||||
}
|
||||
},
|
||||
|
||||
async saveData(classNumber, data, date) {
|
||||
try {
|
||||
if (!classNumber) {
|
||||
return formatError("请先设置班号", "CONFIG_ERROR");
|
||||
}
|
||||
|
||||
const formattedDate = formatDateForKey(date);
|
||||
const key = `${DATA_KEY_PREFIX}${formattedDate}`;
|
||||
|
||||
const db = await initDB();
|
||||
await db.put("kv", JSON.stringify(data), `${classNumber}/${key}`);
|
||||
return formatResponse(null, "保存成功");
|
||||
} catch (error) {
|
||||
return formatError("保存本地数据失败:" + error);
|
||||
}
|
||||
},
|
||||
|
||||
async loadConfig(classNumber) {
|
||||
try {
|
||||
if (!classNumber) {
|
||||
return formatError("请先设置班号", "CONFIG_ERROR");
|
||||
}
|
||||
|
||||
const db = await initDB();
|
||||
const config = await db.get("kv", `${classNumber}/${CONFIG_KEY}`);
|
||||
|
||||
if (!config) {
|
||||
return formatResponse({
|
||||
studentList: [
|
||||
"Classworks可以管理学生列表",
|
||||
'你可以点击设置,在其中找到"学生列表"',
|
||||
"在添加学生处输入学生姓名,点击添加",
|
||||
"或者点击高级编辑,从Excel表格中复制数据并粘贴进来",
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
return formatResponse(JSON.parse(config));
|
||||
} catch (error) {
|
||||
return formatError("读取本地配置失败:" + error);
|
||||
}
|
||||
},
|
||||
|
||||
async saveConfig(classNumber, config) {
|
||||
try {
|
||||
if (!classNumber) {
|
||||
return formatError("请先设置班号", "CONFIG_ERROR");
|
||||
}
|
||||
|
||||
const db = await initDB();
|
||||
await db.put("kv", JSON.stringify(config), `${classNumber}/${CONFIG_KEY}`);
|
||||
return formatResponse(null, "保存成功");
|
||||
} catch (error) {
|
||||
return formatError("保存本地配置失败:" + error);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Server storage provider
|
||||
server: {
|
||||
async loadData(serverUrl, classNumber, date) {
|
||||
try {
|
||||
const machineId = getMachineId();
|
||||
const formattedDate = formatDateForKey(date);
|
||||
const key = `${DATA_KEY_PREFIX}${formattedDate}`;
|
||||
|
||||
const res = await axios.get(`${serverUrl}/${machineId}/${key}`, {
|
||||
headers: getHeaders()
|
||||
});
|
||||
|
||||
return formatResponse(res.data);
|
||||
} catch (error) {
|
||||
if (error.response?.status === 404) {
|
||||
const today = new Date().toISOString().split("T")[0];
|
||||
if (date === today) {
|
||||
// Return default data for today
|
||||
return formatResponse({
|
||||
homework: {},
|
||||
attendance: {
|
||||
absent: [],
|
||||
late: []
|
||||
}
|
||||
});
|
||||
}
|
||||
return formatError("数据不存在", "NOT_FOUND");
|
||||
}
|
||||
|
||||
return formatError(
|
||||
error.response?.data?.message || "服务器连接失败",
|
||||
"NETWORK_ERROR"
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
async saveData(serverUrl, classNumber, data, date) {
|
||||
try {
|
||||
const machineId = getMachineId();
|
||||
const formattedDate = formatDateForKey(date);
|
||||
const key = `${DATA_KEY_PREFIX}${formattedDate}`;
|
||||
|
||||
await axios.post(`${serverUrl}/${machineId}/${key}`, data, {
|
||||
headers: getHeaders()
|
||||
});
|
||||
return formatResponse(null, "保存成功");
|
||||
} catch (error) {
|
||||
return formatError(
|
||||
error.response?.data?.message || "保存失败",
|
||||
"SAVE_ERROR"
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
async loadConfig(serverUrl) {
|
||||
try {
|
||||
const machineId = getMachineId();
|
||||
const res = await axios.get(`${serverUrl}/${machineId}/${CONFIG_KEY}`, {
|
||||
headers: getHeaders()
|
||||
});
|
||||
|
||||
return formatResponse(res.data);
|
||||
} catch (error) {
|
||||
if (error.response?.status === 404) {
|
||||
return formatResponse({
|
||||
studentList: [
|
||||
"Classworks可以管理学生列表",
|
||||
'你可以点击设置,在其中找到"学生列表"',
|
||||
"在添加学生处输入学生姓名,点击添加",
|
||||
"或者点击高级编辑,从Excel表格中复制数据并粘贴进来",
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
return formatError(
|
||||
error.response?.data?.message || "服务器连接失败",
|
||||
"NETWORK_ERROR"
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
async saveConfig(serverUrl, config) {
|
||||
try {
|
||||
const machineId = getMachineId();
|
||||
await axios.post(`${serverUrl}/${machineId}/${CONFIG_KEY}`, config, {
|
||||
headers: getHeaders()
|
||||
});
|
||||
return formatResponse(null, "保存成功");
|
||||
} catch (error) {
|
||||
return formatError(
|
||||
error.response?.data?.message || "保存失败",
|
||||
"SAVE_ERROR"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export { getMachineId };
|
@ -1,10 +1,25 @@
|
||||
import axios from 'axios';
|
||||
import axios from '@/axios/axios';
|
||||
import { formatResponse, formatError } from '../dataProvider';
|
||||
import { getSetting } from '../settings';
|
||||
|
||||
// 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;
|
||||
};
|
||||
|
||||
export const serverProvider = {
|
||||
async loadData(key, date) {
|
||||
try {
|
||||
const res = await axios.get(`${key}/homework?date=${date}`);
|
||||
const res = await axios.get(`${key}/homework?date=${date}`, {
|
||||
headers: getHeaders()
|
||||
});
|
||||
if (res.data?.status === false) {
|
||||
return formatError(res.data.msg || "获取数据失败", "SERVER_ERROR");
|
||||
}
|
||||
@ -21,7 +36,9 @@ export const serverProvider = {
|
||||
try {
|
||||
// 添加date参数到URL
|
||||
const url = date ? `${key}/homework?date=${date}` : `${key}/homework`;
|
||||
await axios.post(url, data);
|
||||
await axios.post(url, data, {
|
||||
headers: getHeaders()
|
||||
});
|
||||
return formatResponse(null, "保存成功");
|
||||
} catch (error) {
|
||||
return formatError(
|
||||
@ -33,7 +50,9 @@ export const serverProvider = {
|
||||
|
||||
async loadConfig(key) {
|
||||
try {
|
||||
const res = await axios.get(`${key}/config`);
|
||||
const res = await axios.get(`${key}/config`, {
|
||||
headers: getHeaders()
|
||||
});
|
||||
if (res.data?.status === false) {
|
||||
return formatError(res.data.msg || "获取配置失败", "SERVER_ERROR");
|
||||
}
|
||||
@ -48,7 +67,9 @@ export const serverProvider = {
|
||||
|
||||
async saveConfig(key, config) {
|
||||
try {
|
||||
const res = await axios.put(`${key}/config`, config);
|
||||
const res = await axios.put(`${key}/config`, config, {
|
||||
headers: getHeaders()
|
||||
});
|
||||
if (res.data?.status === false) {
|
||||
return formatError(res.data.msg || "保存失败", "SAVE_ERROR");
|
||||
}
|
||||
|
@ -36,14 +36,16 @@ async function requestPersistentStorage() {
|
||||
*/
|
||||
async function initializeStorage() {
|
||||
const notificationGranted = await requestNotificationPermission();
|
||||
if (notificationGranted && getSetting("storage.persistOnLoad")) {
|
||||
if (notificationGranted && SettingsManager.getSetting("storage.persistOnLoad")) {
|
||||
const persisted = await requestPersistentStorage();
|
||||
console.log(`持久性存储状态: ${persisted ? "已启用" : "未启用"}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 在页面加载时初始化
|
||||
window.addEventListener("load", initializeStorage);
|
||||
if (typeof window !== 'undefined') {
|
||||
window.addEventListener("load", initializeStorage);
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置项定义
|
||||
@ -60,11 +62,31 @@ window.addEventListener("load", initializeStorage);
|
||||
// 存储所有设置的localStorage键名
|
||||
const SETTINGS_STORAGE_KEY = "Classworks_settings";
|
||||
|
||||
/**
|
||||
* 生成UUID v4
|
||||
* @returns {string} 生成的UUID字符串
|
||||
*/
|
||||
function generateUUID() {
|
||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
||||
const r = Math.random() * 16 | 0;
|
||||
const v = c === 'x' ? r : (r & 0x3 | 0x8);
|
||||
return v.toString(16);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 所有配置项的定义
|
||||
* @type {Object.<string, SettingDefinition>}
|
||||
*/
|
||||
const settingsDefinitions = {
|
||||
// 设备标识
|
||||
"device.uuid": {
|
||||
type: "string",
|
||||
default: generateUUID(),
|
||||
description: "设备唯一标识符",
|
||||
icon: "mdi-identifier",
|
||||
},
|
||||
|
||||
// 存储设置
|
||||
"storage.persistOnLoad": {
|
||||
type: "boolean",
|
||||
@ -151,10 +173,17 @@ const settingsDefinitions = {
|
||||
icon: "mdi-account-group",
|
||||
// 设置班级标识,用于区分不同班级的数据
|
||||
},
|
||||
"server.siteKey": {
|
||||
type: "string",
|
||||
default: "",
|
||||
description: "网站令牌",
|
||||
icon: "mdi-key-chain",
|
||||
// 用于后端验证请求的令牌,将作为请求头 x-site-key 发送
|
||||
},
|
||||
"server.provider": {
|
||||
type: "string",
|
||||
default: "indexedDB",
|
||||
validate: (value) => ["server", "indexedDB"].includes(value),
|
||||
validate: (value) => ["server", "indexedDB", "kv-local", "kv-server"].includes(value),
|
||||
description: "数据提供者",
|
||||
icon: "mdi-database",
|
||||
// 选择数据存储方式:使用本地IndexedDB或远程服务器
|
||||
@ -328,274 +357,309 @@ const settingsDefinitions = {
|
||||
},
|
||||
};
|
||||
|
||||
// 内存中缓存的设置值
|
||||
let settingsCache = null;
|
||||
|
||||
/**
|
||||
* 从localStorage加载所有设置
|
||||
* @returns {Object} 所有设置的值
|
||||
* 设置管理器单例类
|
||||
*/
|
||||
function loadSettings() {
|
||||
try {
|
||||
const stored = localStorage.getItem(SETTINGS_STORAGE_KEY);
|
||||
if (stored) {
|
||||
settingsCache = JSON.parse(stored);
|
||||
} else {
|
||||
// 首次使用或迁移旧数据
|
||||
settingsCache = migrateFromLegacy();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("加载设置失败:", error);
|
||||
settingsCache = {};
|
||||
class SettingsManagerClass {
|
||||
constructor() {
|
||||
this.settingsCache = null;
|
||||
this.isInitialized = false;
|
||||
}
|
||||
|
||||
// 确保所有设置项都有值(使用默认值填充)
|
||||
for (const [key, definition] of Object.entries(settingsDefinitions)) {
|
||||
if (!(key in settingsCache)) {
|
||||
settingsCache[key] = definition.default;
|
||||
}
|
||||
/**
|
||||
* 初始化设置管理器
|
||||
*/
|
||||
init() {
|
||||
if (this.isInitialized) return;
|
||||
this.loadSettings();
|
||||
this.isInitialized = true;
|
||||
}
|
||||
|
||||
return settingsCache;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从旧版本的localStorage迁移数据
|
||||
*/
|
||||
function migrateFromLegacy() {
|
||||
const LEGACY_SETTINGS_KEY = "homeworkpage_settings";
|
||||
const LEGACY_MESSAGE_KEY = "homeworkpage_messages";
|
||||
|
||||
// 尝试从旧版本的设置中迁移
|
||||
const legacySettings = localStorage.getItem(LEGACY_SETTINGS_KEY);
|
||||
if (legacySettings) {
|
||||
/**
|
||||
* 从localStorage加载所有设置
|
||||
* @returns {Object} 所有设置的值
|
||||
*/
|
||||
loadSettings() {
|
||||
try {
|
||||
const settings = JSON.parse(legacySettings);
|
||||
localStorage.setItem(SETTINGS_STORAGE_KEY, JSON.stringify(settings));
|
||||
// 可选:删除旧的设置
|
||||
localStorage.removeItem(LEGACY_SETTINGS_KEY);
|
||||
return settings;
|
||||
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);
|
||||
console.error("加载设置失败:", error);
|
||||
this.settingsCache = {};
|
||||
}
|
||||
|
||||
// 确保所有设置项都有值(使用默认值填充)
|
||||
for (const [key, definition] of Object.entries(settingsDefinitions)) {
|
||||
if (!(key in this.settingsCache)) {
|
||||
this.settingsCache[key] = definition.default;
|
||||
}
|
||||
}
|
||||
|
||||
return this.settingsCache;
|
||||
}
|
||||
// 尝试从旧版本的message中迁移
|
||||
const legacyMessages = localStorage.getItem(LEGACY_MESSAGE_KEY);
|
||||
if (legacyMessages) {
|
||||
|
||||
/**
|
||||
* 从旧版本的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;
|
||||
|
||||
try {
|
||||
const messages = JSON.parse(legacyMessages);
|
||||
localStorage.setItem(SETTINGS_STORAGE_KEY, JSON.stringify(messages));
|
||||
// 可选:删除旧的message
|
||||
localStorage.removeItem(LEGACY_MESSAGE_KEY);
|
||||
return messages; // 返回迁移后的消息
|
||||
localStorage.setItem(SETTINGS_STORAGE_KEY, JSON.stringify(this.settingsCache));
|
||||
} catch (error) {
|
||||
console.error("迁移旧消息失败:", error);
|
||||
console.error("保存设置失败:", error);
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有旧设置或迁移失败,返回空对象
|
||||
return {};
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存所有设置到localStorage
|
||||
*/
|
||||
function saveSettings() {
|
||||
try {
|
||||
localStorage.setItem(SETTINGS_STORAGE_KEY, JSON.stringify(settingsCache));
|
||||
} catch (error) {
|
||||
console.error("保存设置失败:", error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取设置项的值
|
||||
* @param {string} key - 设置项键名
|
||||
* @returns {any} 设置项的值
|
||||
*/
|
||||
function getSetting(key) {
|
||||
if (!settingsCache) {
|
||||
loadSettings();
|
||||
}
|
||||
|
||||
const definition = settingsDefinitions[key];
|
||||
if (!definition) {
|
||||
console.warn(`未定义的设置项: ${key}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
// 确保开发者相关设置正确处理
|
||||
if (definition.requireDeveloper) {
|
||||
const devEnabled = settingsCache["developer.enabled"];
|
||||
if (!devEnabled) {
|
||||
return definition.default;
|
||||
}
|
||||
}
|
||||
|
||||
const value = settingsCache[key];
|
||||
return value !== undefined ? value : definition.default;
|
||||
}
|
||||
|
||||
// 修改 logSettingsChange 函数,优化检查逻辑
|
||||
function logSettingsChange(key, oldValue, newValue) {
|
||||
// 确保设置已加载
|
||||
if (!settingsCache) {
|
||||
loadSettings();
|
||||
}
|
||||
|
||||
const shouldLog =
|
||||
settingsCache["developer.enabled"] &&
|
||||
settingsCache["developer.showDebugConfig"];
|
||||
|
||||
if (shouldLog) {
|
||||
console.log(`[Settings] ${key}:`, {
|
||||
old: oldValue,
|
||||
new: newValue,
|
||||
time: new Date().toLocaleTimeString(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置配置项的值
|
||||
* @param {string} key - 设置项键名
|
||||
* @param {any} value - 要设置的值
|
||||
* @returns {boolean} 是否设置成功
|
||||
*/
|
||||
function setSetting(key, value) {
|
||||
const definition = settingsDefinitions[key];
|
||||
if (!definition) {
|
||||
console.warn(`未定义的设置项: ${key}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 添加对开发者选项依赖的检查
|
||||
if (definition.requireDeveloper && !settingsCache["developer.enabled"]) {
|
||||
console.warn(`设置项 ${key} 需要启用开发者选项`);
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const oldValue = settingsCache[key];
|
||||
// 类型转换
|
||||
if (typeof value !== definition.type) {
|
||||
value =
|
||||
definition.type === "boolean"
|
||||
? Boolean(value)
|
||||
: definition.type === "number"
|
||||
? Number(value)
|
||||
: String(value);
|
||||
/**
|
||||
* 获取设置项的值
|
||||
* @param {string} key - 设置项键名
|
||||
* @returns {any} 设置项的值
|
||||
*/
|
||||
getSetting(key) {
|
||||
if (!this.isInitialized) {
|
||||
this.init();
|
||||
}
|
||||
|
||||
// 验证
|
||||
if (definition.validate && !definition.validate(value)) {
|
||||
console.warn(`设置项 ${key} 的值无效`);
|
||||
const definition = settingsDefinitions[key];
|
||||
if (!definition) {
|
||||
console.warn(`未定义的设置项: ${key}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
// 确保开发者相关设置正确处理
|
||||
if (definition.requireDeveloper) {
|
||||
const devEnabled = this.settingsCache["developer.enabled"];
|
||||
if (!devEnabled) {
|
||||
return definition.default;
|
||||
}
|
||||
}
|
||||
|
||||
const value = this.settingsCache[key];
|
||||
return value !== undefined ? value : definition.default;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置配置项的值
|
||||
* @param {string} key - 设置项键名
|
||||
* @param {any} value - 要设置的值
|
||||
* @returns {boolean} 是否设置成功
|
||||
*/
|
||||
setSetting(key, value) {
|
||||
if (!this.isInitialized) {
|
||||
this.init();
|
||||
}
|
||||
|
||||
const definition = settingsDefinitions[key];
|
||||
if (!definition) {
|
||||
console.warn(`未定义的设置项: ${key}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!settingsCache) {
|
||||
loadSettings();
|
||||
// 添加对开发者选项依赖的检查
|
||||
if (definition.requireDeveloper && !this.settingsCache["developer.enabled"]) {
|
||||
console.warn(`设置项 ${key} 需要启用开发者选项`);
|
||||
return false;
|
||||
}
|
||||
|
||||
settingsCache[key] = value;
|
||||
saveSettings();
|
||||
logSettingsChange(key, oldValue, value);
|
||||
try {
|
||||
const oldValue = this.settingsCache[key];
|
||||
// 类型转换
|
||||
if (typeof value !== definition.type) {
|
||||
value =
|
||||
definition.type === "boolean"
|
||||
? Boolean(value)
|
||||
: definition.type === "number"
|
||||
? Number(value)
|
||||
: String(value);
|
||||
}
|
||||
|
||||
// 为了保持向后兼容,同时更新旧的localStorage键
|
||||
const legacyKey = definition.legacyKey;
|
||||
if (legacyKey) {
|
||||
localStorage.setItem(legacyKey, value.toString());
|
||||
// 验证
|
||||
if (definition.validate && !definition.validate(value)) {
|
||||
console.warn(`设置项 ${key} 的值无效`);
|
||||
return false;
|
||||
}
|
||||
|
||||
this.settingsCache[key] = value;
|
||||
this.saveSettings();
|
||||
this.logSettingsChange(key, oldValue, value);
|
||||
|
||||
// 为了保持向后兼容,同时更新旧的localStorage键
|
||||
const legacyKey = definition.legacyKey;
|
||||
if (legacyKey && typeof localStorage !== 'undefined') {
|
||||
localStorage.setItem(legacyKey, value.toString());
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error(`设置配置项 ${key} 失败:`, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录设置变更
|
||||
*/
|
||||
logSettingsChange(key, oldValue, newValue) {
|
||||
const shouldLog =
|
||||
this.settingsCache["developer.enabled"] &&
|
||||
this.settingsCache["developer.showDebugConfig"];
|
||||
|
||||
if (shouldLog) {
|
||||
console.log(`[Settings] ${key}:`, {
|
||||
old: oldValue,
|
||||
new: newValue,
|
||||
time: new Date().toLocaleTimeString(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置指定设置项到默认值
|
||||
* @param {string} key - 设置项键名
|
||||
*/
|
||||
resetSetting(key) {
|
||||
if (!this.isInitialized) {
|
||||
this.init();
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error(`设置配置项 ${key} 失败:`, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置指定设置项到默认值
|
||||
* @param {string} key - 设置项键名
|
||||
*/
|
||||
function resetSetting(key) {
|
||||
const definition = settingsDefinitions[key];
|
||||
if (!definition) {
|
||||
console.warn(`未定义的设置项: ${key}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!settingsCache) {
|
||||
loadSettings();
|
||||
}
|
||||
|
||||
settingsCache[key] = definition.default;
|
||||
saveSettings();
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置所有设置项到默认值
|
||||
*/
|
||||
function resetAllSettings() {
|
||||
settingsCache = {};
|
||||
for (const [key, definition] of Object.entries(settingsDefinitions)) {
|
||||
settingsCache[key] = definition.default;
|
||||
}
|
||||
saveSettings();
|
||||
}
|
||||
|
||||
/**
|
||||
* 监听设置变化
|
||||
* @param {Function} callback - 当设置改变时调用的回调函数
|
||||
* @returns {Function} 取消监听的函数
|
||||
*/
|
||||
function watchSettings(callback) {
|
||||
const handler = (event) => {
|
||||
if (event.key === SETTINGS_STORAGE_KEY) {
|
||||
settingsCache = JSON.parse(event.newValue);
|
||||
callback(settingsCache);
|
||||
const definition = settingsDefinitions[key];
|
||||
if (!definition) {
|
||||
console.warn(`未定义的设置项: ${key}`);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("storage", handler);
|
||||
return () => window.removeEventListener("storage", handler);
|
||||
}
|
||||
|
||||
// 初始化设置
|
||||
loadSettings();
|
||||
|
||||
/**
|
||||
* 获取设置项的定义
|
||||
* @param {string} key - 设置项键名
|
||||
* @returns {SettingDefinition|null} 设置项的定义或null
|
||||
*/
|
||||
function getSettingDefinition(key) {
|
||||
return settingsDefinitions[key] || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将当前配置导出为简单的键值对对象
|
||||
* @returns {Object} 包含所有设置的键值对对象
|
||||
*/
|
||||
function exportSettingsAsKeyValue() {
|
||||
if (!settingsCache) {
|
||||
loadSettings();
|
||||
this.settingsCache[key] = definition.default;
|
||||
this.saveSettings();
|
||||
}
|
||||
|
||||
// 创建一个新对象,避免直接返回引用
|
||||
const exportedSettings = {};
|
||||
|
||||
// 遍历所有设置项
|
||||
for (const key in settingsDefinitions) {
|
||||
// 获取当前值(确保使用getSetting以应用所有规则,如开发者选项依赖)
|
||||
exportedSettings[key] = getSetting(key);
|
||||
|
||||
/**
|
||||
* 重置所有设置项到默认值
|
||||
*/
|
||||
resetAllSettings() {
|
||||
this.settingsCache = {};
|
||||
for (const [key, definition] of Object.entries(settingsDefinitions)) {
|
||||
this.settingsCache[key] = definition.default;
|
||||
}
|
||||
this.saveSettings();
|
||||
}
|
||||
|
||||
/**
|
||||
* 监听设置变化
|
||||
* @param {Function} callback - 当设置改变时调用的回调函数
|
||||
* @returns {Function} 取消监听的函数
|
||||
*/
|
||||
watchSettings(callback) {
|
||||
if (typeof window === 'undefined') return () => {};
|
||||
|
||||
const handler = (event) => {
|
||||
if (event.key === SETTINGS_STORAGE_KEY) {
|
||||
this.settingsCache = JSON.parse(event.newValue);
|
||||
callback(this.settingsCache);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("storage", handler);
|
||||
return () => window.removeEventListener("storage", handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取设置项的定义
|
||||
* @param {string} key - 设置项键名
|
||||
* @returns {SettingDefinition|null} 设置项的定义或null
|
||||
*/
|
||||
getSettingDefinition(key) {
|
||||
return settingsDefinitions[key] || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将当前配置导出为简单的键值对对象
|
||||
* @returns {Object} 包含所有设置的键值对对象
|
||||
*/
|
||||
exportSettingsAsKeyValue() {
|
||||
if (!this.isInitialized) {
|
||||
this.init();
|
||||
}
|
||||
|
||||
// 创建一个新对象,避免直接返回引用
|
||||
const exportedSettings = {};
|
||||
|
||||
// 遍历所有设置项
|
||||
for (const key in settingsDefinitions) {
|
||||
// 获取当前值(确保使用getSetting以应用所有规则,如开发者选项依赖)
|
||||
exportedSettings[key] = this.getSetting(key);
|
||||
}
|
||||
|
||||
return exportedSettings;
|
||||
}
|
||||
|
||||
return exportedSettings;
|
||||
}
|
||||
|
||||
// 创建单例实例
|
||||
const SettingsManager = new SettingsManagerClass();
|
||||
|
||||
// 在服务器端和客户端都能正常工作的初始化
|
||||
if (typeof window !== 'undefined') {
|
||||
SettingsManager.init();
|
||||
}
|
||||
|
||||
// 为了向后兼容性,提供与原来相同的函数接口
|
||||
const getSetting = (key) => SettingsManager.getSetting(key);
|
||||
const setSetting = (key, value) => SettingsManager.setSetting(key, value);
|
||||
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();
|
||||
|
||||
// 导出单例和直接方法
|
||||
export {
|
||||
settingsDefinitions,
|
||||
SettingsManager,
|
||||
getSetting,
|
||||
setSetting,
|
||||
resetSetting,
|
||||
|
Loading…
x
Reference in New Issue
Block a user