Implement data migration tool and enhance KV storage support. Add automatic redirection for legacy data providers, improve user interface for migration settings, and update data handling for Classworks cloud storage. Refactor components for better integration with new storage options.

This commit is contained in:
SunWuyuan
2025-05-10 20:51:56 +08:00
parent c0e33dcbf9
commit a542e9b91f
11 changed files with 355 additions and 204 deletions
+180 -11
View File
@@ -3,36 +3,205 @@
<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>
<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 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>
<span
>使用此工具可以将数据从旧存储系统迁移到新的 KV
存储系统,选择本地或云端迁移,以确保数据不会丢失。</span
>
</v-card-text>
</v-card>
<MigrationTool />
<MigrationTool ref="migrationTool" />
</v-col>
</v-row>
<!-- 一键迁移对话框 -->
<v-dialog v-model="showMigrationDialog" max-width="500" persistent>
<v-card>
<v-card-title class="text-h5 d-flex align-center">
<v-icon color="primary" size="large" class="mr-3"
>mdi-database-sync</v-icon
>
一键数据迁移
</v-card-title>
<v-card-text class="mt-4">
<p>
系统将自动读取您的配置,并将过去半年的数据迁移至Classworks
KV数据库中
</p>
<v-alert
color="info"
variant="outlined"
density="compact"
class="mt-4"
icon="mdi-information-outline"
>
<ul class="ml-3 mt-1">
<li>数据源: {{ dataSourceText }}</li>
<li>班级: {{ classNumber }}</li>
<li>服务器: {{ serverDomain || "本地存储" }}</li>
<li>
迁移范围: {{ formatDate(sixMonthsAgo) }} 至
{{ formatDate(today) }}
</li>
</ul>
</v-alert>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn
color="grey-darken-1"
variant="text"
@click="showMigrationDialog = false"
>
稍后再说
</v-btn>
<v-btn
color="primary"
size="large"
variant="elevated"
@click="startAutoMigration"
:loading="isAutoMigrating"
:disabled="isAutoMigrating"
>
<v-icon left class="mr-2">mdi-database-export</v-icon>
开始一键迁移
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</v-container>
</template>
<script>
import MigrationTool from '@/components/MigrationTool.vue';
import MigrationTool from "@/components/MigrationTool.vue";
import { getSetting, setSetting } from "@/utils/settings";
export default {
name: 'DataMigrationPage',
name: "DataMigrationPage",
components: {
MigrationTool
MigrationTool,
},
data() {
const today = new Date();
const sixMonthsAgo = new Date();
sixMonthsAgo.setMonth(today.getMonth() - 3);
return {
showMigrationDialog: false,
isAutoMigrating: false,
today,
sixMonthsAgo,
classNumber: "",
serverDomain: "",
dataProvider: "",
};
},
computed: {
dataSourceText() {
switch (this.dataProvider) {
case "server":
return "服务器";
case "indexeddb":
return "本地数据库";
case "kv-local":
return "本地 KV 存储";
case "kv-server":
return "远程 KV 存储";
case "classworkscloud":
return "Classworks 云";
default:
return "未知来源";
}
},
},
async mounted() {
this.loadSettings();
if (this.serverDomain == "https://class.wuyuan.dev") {
await this.startAutoMigration();
this.$router.push("/");
}
},
methods: {
loadSettings() {
this.classNumber = getSetting("server.classNumber");
this.serverDomain = getSetting("server.domain");
this.dataProvider = getSetting("server.provider");
this.showMigrationDialog =
this.dataProvider === "server" || this.dataProvider === "indexeddb";
},
formatDate(date) {
return date.toLocaleDateString();
},
async startAutoMigration() {
if (!this.$refs.migrationTool) {
console.error("MigrationTool组件引用不可用");
return;
}
this.isAutoMigrating = true;
try {
// 设置迁移工具的参数
const migrationTool = this.$refs.migrationTool;
migrationTool.classNumber = this.classNumber;
migrationTool.migrationType =
this.dataProvider === "server" ? "server" : "local";
migrationTool.serverUrl = this.serverDomain;
migrationTool.targetStorage = "kv-server";
//migrationTool.targetServerUrl = this.serverDomain;
// 设置半年的日期范围
migrationTool.startDate = this.formatDateString(this.sixMonthsAgo);
migrationTool.endDate = this.formatDateString(this.today);
// 根据数据源类型进行相应操作
if (this.dataProvider === "server") {
// 预览服务器数据
await migrationTool.previewServerData();
} else {
// 扫描本地数据库
await migrationTool.scanLocalDatabase();
}
// 开始迁移
if (migrationTool.displayItems.length > 0) {
await migrationTool.startMigration();
} else {
console.warn("没有找到可迁移的数据");
}
setSetting("server.provider", "classworkscloud");
} catch (error) {
console.error("自动迁移失败:", error);
} finally {
this.isAutoMigrating = false;
this.showMigrationDialog = false;
}
},
formatDateString(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}`;
},
},
metaInfo: {
title: '数据迁移工具'
}
}
</script>
title: "数据迁移工具",
},
};
</script>
+13 -8
View File
@@ -625,6 +625,7 @@
import MessageLog from "@/components/MessageLog.vue";
import RandomPicker from "@/components/RandomPicker.vue"; // 导入随机点名组件
import dataProvider from "@/utils/dataProvider";
import { kvProvider } from "@/utils/providers/kvProvider";
import {
getSetting,
watchSettings,
@@ -1086,7 +1087,6 @@ export default {
try {
this.loading.download = true;
const response = await dataProvider.loadData(
this.provider,
this.dataKey,
this.state.dateString
);
@@ -1192,10 +1192,9 @@ export default {
try {
this.loading.upload = true;
const response = await dataProvider.saveData(
this.provider,
this.dataKey,
this.state.boardData,
this.state.dateString // 添加dateString参数
this.state.dateString
);
if (!response.success) {
@@ -1211,10 +1210,16 @@ export default {
async loadConfig() {
try {
const response = await dataProvider.loadConfig(
this.provider,
this.dataKey
);
// 使用新的kvProvider直接加载配置
const provider = getSetting("server.provider");
const useServer = provider === "kv-server" || provider === "classworkscloud";
let response;
if (useServer) {
response = await kvProvider.server.loadConfig();
} else {
response = await kvProvider.local.loadConfig();
}
if (!response.success) {
throw new Error(response.error.message);
@@ -1325,7 +1330,7 @@ export default {
const classNum = getSetting("server.classNumber");
this.provider = provider;
this.dataKey = provider === "server" ? `${domain}/${classNum}` : classNum;
this.dataKey = provider === "server" || provider === "classworkscloud" ? `${domain}/${classNum}` : classNum;
this.state.classNumber = classNum;
},
+35 -24
View File
@@ -127,7 +127,7 @@
:is-mobile="isMobile"
:unsaved-changes="hasUnsavedChanges"
@save="saveStudents"
@reload="loadStudentList"
@reload="loadStudents"
@update:modelValue="handleStudentDataChange"
/>
</v-col>
@@ -187,7 +187,7 @@ 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 dataProvider from '@/utils/dataProvider';
import { kvProvider } from '@/utils/providers/kvProvider';
import SettingsExplorer from '@/components/settings/SettingsExplorer.vue';
import SettingsLinkGenerator from '@/components/SettingsLinkGenerator.vue';
export default {
@@ -306,7 +306,7 @@ export default {
this.unwatchSettings = watchSettings(() => {
this.loadAllSettings();
});
this.loadStudentList();
this.loadStudents();
this.refreshDebugConfig();
// 检查开发者选项,如果未启用则关闭相关功能
@@ -366,30 +366,34 @@ export default {
this.$message.error(title, content);
},
async loadStudentList() {
async loadStudents() {
this.studentsError = null;
try {
this.loading.students = true;
this.studentsError = null;
const domain = getSetting('server.domain');
const classNum = getSetting('server.classNumber');
const provider = getSetting('server.provider');
if (!classNum) {
throw new Error('请先设置班号');
}
const key = provider === 'server' ? `${domain}/${classNum}` : classNum;
const res = await dataProvider.loadConfig(provider, key);
const provider = getSetting('server.provider');
const useServer = provider === 'kv-server' || provider === 'classworkscloud';
let response;
if (!res.success) {
throw new Error(res.error.message);
if (useServer) {
response = await kvProvider.server.loadConfig();
} else {
response = await kvProvider.local.loadConfig();
}
if (res.data && Array.isArray(res.data.studentList)) {
this.studentData.list = res.data.studentList;
this.studentData.text = res.data.studentList.join('\n');
this.lastSavedData = [...res.data.studentList];
if (!response.success) {
throw new Error(response.error.message);
}
if (response.data && Array.isArray(response.data.studentList)) {
this.studentData.list = response.data.studentList;
this.studentData.text = response.data.studentList.join('\n');
this.lastSavedData = [...response.data.studentList];
this.hasUnsavedChanges = false;
}
} catch (error) {
@@ -403,21 +407,28 @@ export default {
async saveStudents() {
try {
const domain = getSetting('server.domain');
const classNum = getSetting('server.classNumber');
const provider = getSetting('server.provider');
if (!classNum) {
throw new Error('请先设置班号');
}
const key = provider === 'server' ? `${domain}/${classNum}` : classNum;
const res = await dataProvider.saveConfig(provider, key, {
studentList: this.studentData.list,
});
const provider = getSetting('server.provider');
const useServer = provider === 'kv-server' || provider === 'classworkscloud';
let response;
if (!res.success) {
throw new Error(res.error.message);
if (useServer) {
response = await kvProvider.server.saveConfig({
studentList: this.studentData.list,
});
} else {
response = await kvProvider.local.saveConfig({
studentList: this.studentData.list,
});
}
if (!response.success) {
throw new Error(response.error.message);
}
// 更新保存状态