添加考试看板功能

This commit is contained in:
SunWuyuan
2025-08-30 14:55:55 +08:00
parent cd10d0f49a
commit f546621ab9
10 changed files with 2433 additions and 156 deletions
File diff suppressed because it is too large Load Diff
@@ -22,7 +22,8 @@
<v-divider class="my-2" />
<setting-item :setting-key="'display.showAntiScreenBurnCard'" />
<v-divider class="my-2" />
<setting-item :setting-key="'display.showExamScheduleButton'" />
</v-list>
</settings-card>
@@ -82,6 +82,13 @@
@click="editItem(item)"
title="编辑"
/>
<v-btn
icon="mdi-cloud-download"
size="small"
color="primary"
@click="getCloudUrl(item)"
title="获取云端地址"
/>
<v-btn
icon="mdi-delete"
size="small"
@@ -228,6 +235,100 @@
</v-card>
</v-dialog>
<!-- 云端地址对话框 -->
<v-dialog v-model="cloudUrlDialog" max-width="800px">
<v-card>
<v-card-title class="d-flex align-center">
<v-icon icon="mdi-cloud-download" class="mr-2" />
获取云端访问地址
<v-spacer />
<v-btn icon="mdi-close" variant="text" @click="cloudUrlDialog = false" />
</v-card-title>
<v-card-subtitle v-if="selectedCloudItem">
键名: <code>{{ selectedCloudItem.key }}</code>
</v-card-subtitle>
<v-card-text>
<v-alert v-if="cloudUrlError" type="error" variant="tonal" class="mb-4">
{{ cloudUrlError }}
</v-alert>
<v-alert v-if="cloudUrlResult && cloudUrlResult.success" type="success" variant="tonal" class="mb-4">
<v-alert-title>云端地址获取成功</v-alert-title>
<div class="mt-2">
<div v-if="cloudUrlResult.migrated" class="mb-2">
<v-icon icon="mdi-database-arrow-up" class="mr-1" color="success" />
数据已从本地迁移到云端
</div>
<div v-if="cloudUrlResult.configured" class="mb-2">
<v-icon icon="mdi-cog" class="mr-1" color="info" />
云端配置已自动设置
</div>
</div>
</v-alert>
<v-text-field
v-if="cloudUrlResult && cloudUrlResult.url"
:model-value="cloudUrlResult.url"
label="云端访问地址"
variant="outlined"
readonly
class="font-monospace"
append-inner-icon="mdi-content-copy"
@click:append-inner="copyCloudUrl"
/>
<v-expansion-panels v-if="cloudUrlResult && cloudUrlResult.url" class="mt-4">
<v-expansion-panel>
<v-expansion-panel-title>
<v-icon icon="mdi-cog" class="mr-2" />
高级选项
</v-expansion-panel-title>
<v-expansion-panel-text>
<v-checkbox
v-model="cloudUrlOptions.migrateFromLocal"
label="从本地迁移数据到云端"
density="compact"
/>
<v-checkbox
v-model="cloudUrlOptions.autoConfigureCloud"
label="自动配置云端默认设置"
density="compact"
/>
<v-btn
@click="refreshCloudUrl"
variant="tonal"
color="primary"
:loading="gettingCloudUrl"
class="mt-2"
>
<v-icon icon="mdi-refresh" class="mr-1" />
重新获取
</v-btn>
</v-expansion-panel-text>
</v-expansion-panel>
</v-expansion-panels>
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn @click="cloudUrlDialog = false" variant="text">
关闭
</v-btn>
<v-btn
v-if="cloudUrlResult && cloudUrlResult.url"
@click="openCloudUrl"
variant="tonal"
color="primary"
>
<v-icon icon="mdi-open-in-new" class="mr-1" />
在新窗口打开
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<!-- 删除确认对话框 -->
<v-dialog v-model="deleteDialog" max-width="400px">
<v-card>
@@ -289,11 +390,22 @@ export default {
editDialog: false,
deleteDialog: false,
createDialog: false,
cloudUrlDialog: false,
// 选中的项目
selectedItem: null,
editingItem: null,
itemToDelete: null,
selectedCloudItem: null,
// 云端地址相关
gettingCloudUrl: false,
cloudUrlResult: null,
cloudUrlError: null,
cloudUrlOptions: {
migrateFromLocal: true,
autoConfigureCloud: true
},
// 编辑数据
editingData: '',
@@ -597,6 +709,67 @@ export default {
} catch (error) {
this.$message.error('复制失败', error.message);
}
},
async getCloudUrl(item) {
this.selectedCloudItem = item;
this.cloudUrlResult = null;
this.cloudUrlError = null;
this.cloudUrlDialog = true;
await this.fetchCloudUrl();
},
async fetchCloudUrl() {
if (!this.selectedCloudItem) return;
this.gettingCloudUrl = true;
this.cloudUrlError = null;
try {
const result = await dataProvider.getKeyCloudUrl(
this.selectedCloudItem.key,
this.cloudUrlOptions
);
if (result.success) {
this.cloudUrlResult = result;
this.$message.success('云端地址获取成功');
} else {
this.cloudUrlError = result.error?.message || '获取云端地址失败';
this.$message.error('获取失败', this.cloudUrlError);
}
} catch (error) {
this.cloudUrlError = error.message || '获取云端地址时发生错误';
this.$message.error('获取失败', this.cloudUrlError);
} finally {
this.gettingCloudUrl = false;
}
},
async refreshCloudUrl() {
await this.fetchCloudUrl();
},
async copyCloudUrl() {
if (!this.cloudUrlResult?.url) return;
try {
await navigator.clipboard.writeText(this.cloudUrlResult.url);
this.$message.success('云端地址已复制到剪贴板');
} catch (error) {
this.$message.error('复制失败', error.message);
}
},
openCloudUrl() {
if (!this.cloudUrlResult?.url) return;
try {
window.open(this.cloudUrlResult.url, '_blank');
} catch (error) {
this.$message.error('打开链接失败', error.message);
}
}
}
};
+611
View File
@@ -0,0 +1,611 @@
<template>
<v-container class="fill-height">
<v-row>
<v-col cols="12">
<v-card class="elevation-12" border>
<v-card-title class="d-flex align-center primary lighten-1 white--text py-3 px-4">
<v-icon color="white" class="mr-2">mdi-calendar-check</v-icon>
考试看板
</v-card-title>
<v-card-subtitle>
不只是考试看板。
</v-card-subtitle>
<v-card-text>
<!-- 错误提示 -->
<v-alert
v-if="error"
type="error"
class="mb-4 mt-3 mx-2"
variant="tonal"
border="start"
closable
@click:close="error = ''"
>
<div class="d-flex align-center">
<v-icon class="mr-2">mdi-alert-circle</v-icon>
{{ error }}
</div>
</v-alert>
<!-- 成功提示 -->
<v-alert
v-if="success"
type="success"
class="mb-4 mt-3 mx-2"
variant="tonal"
border="start"
closable
@click:close="success = ''"
>
<div class="d-flex align-center">
<v-icon class="mr-2">mdi-check-circle</v-icon>
{{ success }}
</div>
</v-alert>
<!-- 操作按钮 -->
<div class="d-flex justify-space-between align-center mb-4">
<div class="d-flex align-center">
<v-btn
color="primary"
prepend-icon="mdi-plus"
class="mr-2"
@click="createNewConfig"
>
新建配置
</v-btn>
<v-btn
color="info"
prepend-icon="mdi-refresh"
:loading="loading"
@click="loadConfigs"
variant="outlined"
>
刷新
</v-btn>
</div>
<v-chip
v-if="configs.length > 0"
color="primary"
prepend-icon="mdi-format-list-numbered"
>
{{ configs.length }} 个配置
</v-chip>
</div>
<!-- 加载状态 -->
<v-card v-if="loading" class="my-4" outlined>
<v-card-text>
<v-skeleton-loader
type="list-item-avatar-two-line@3"
class="mx-auto"
></v-skeleton-loader>
</v-card-text>
</v-card>
<!-- 配置列表 -->
<v-card v-if="!loading && configs.length > 0" class="my-4" elevation="1">
<v-card-title class="d-flex align-center pa-4 bg-primary-lighten-5">
<v-icon class="mr-2">mdi-format-list-bulleted</v-icon>
<span class="font-weight-bold">配置列表</span>
</v-card-title>
<v-list>
<v-list-item
v-for="(config) in configs"
:key="config.id"
class="border-b"
style="cursor: pointer;"
@click="showEditDialog(config)"
>
<template #prepend>
<v-avatar color="primary" class="mr-2">
<v-icon color="white">mdi-calendar-text</v-icon>
</v-avatar>
</template>
<v-list-item-title class="font-weight-medium">
{{ config.examName || `配置 ${config.id}` }}
</v-list-item-title>
<v-list-item-subtitle class="text-caption mt-1">
<div class="d-flex align-center">
<v-icon size="small" class="mr-1">mdi-information-outline</v-icon>
{{ config.message || '无描述' }}
</div>
<div class="d-flex align-center mt-1">
<v-icon size="small" class="mr-1">mdi-book-multiple</v-icon>
{{ config.examInfos ? config.examInfos.length : 0 }} 堂考试
</div>
</v-list-item-subtitle>
<template #append>
<div class="d-flex align-center">
<v-btn
icon="mdi-pencil"
size="small"
color="primary"
variant="text"
class="mr-1"
@click="showEditDialog(config)"
>
<v-icon>mdi-pencil</v-icon>
</v-btn>
<v-btn
icon="mdi-eye"
size="small"
color="info"
variant="text"
class="mr-1"
@click="showEditDialog(config)"
>
<v-icon>mdi-eye</v-icon>
</v-btn>
</div>
</template>
</v-list-item>
</v-list>
</v-card>
<!-- 空状态 -->
<v-card v-if="!loading && configs.length === 0" class="my-4" elevation="1">
<v-card-text class="text-center py-8">
<v-icon size="64" color="grey-lighten-1" class="mb-4">
mdi-calendar-blank
</v-icon>
<h3 class="text-h6 mb-2 text-grey-darken-1">暂无配置</h3>
<p class="text-body-2 text-grey-darken-1 mb-4">
点击"新建配置"按钮创建您的第一个考试配置
</p>
<v-btn
color="primary"
prepend-icon="mdi-plus"
@click="createNewConfig"
>
新建配置
</v-btn>
</v-card-text>
</v-card>
</v-card-text>
</v-card>
</v-col>
</v-row>
<!-- 重命名对话框 -->
<v-dialog v-model="renameDialog" max-width="500">
<v-card>
<v-card-title class="d-flex align-center">
<v-icon color="primary" class="mr-2">mdi-rename-box</v-icon>
重命名配置
</v-card-title>
<v-card-text>
<v-text-field
v-model="newConfigName"
label="配置名称"
prepend-inner-icon="mdi-calendar-text"
variant="outlined"
:rules="[v => !!v || '配置名称不能为空']"
@keyup.enter="renameConfig"
></v-text-field>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn
color="grey"
variant="text"
@click="renameDialog = false"
>
取消
</v-btn>
<v-btn
color="primary"
variant="outlined"
:loading="renaming"
@click="renameConfig"
:disabled="!newConfigName"
>
确认
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<!-- 编辑配置弹框 -->
<v-dialog v-model="editDialog" max-width="1200" persistent>
<v-card>
<v-card-title class="d-flex align-center primary lighten-1 white--text py-3 px-4">
<v-icon color="white" class="mr-2">mdi-pencil</v-icon>
编辑考试配置
<v-spacer></v-spacer>
<v-chip
v-if="editingConfig"
color="white"
text-color="primary"
size="small"
class="mr-2"
>
ID: {{ editingConfig.id }}
</v-chip>
<v-btn
icon="mdi-close"
color="white"
variant="text"
@click="closeEditDialog"
>
<v-icon>mdi-close</v-icon>
</v-btn>
</v-card-title>
<v-card-text class="pa-4"
style="max-height: 70vh; overflow-y: auto;">
<ExamConfigEditor
v-if="editingConfig"
:config-id="editingConfig.id"
ref="configEditor"
:dialog-mode="true"
@saved="onConfigSaved"
@error="onConfigError"
@opened="onConfigOpened"
@deleted="onConfigDeleted"
/>
</v-card-text>
<v-card-actions class="pa-4">
<v-btn
color="grey"
variant="outlined"
prepend-icon="mdi-close"
@click="closeEditDialog"
>
关闭
</v-btn>
<v-spacer></v-spacer>
<v-btn
color="success"
variant="outlined"
prepend-icon="mdi-content-save"
:loading="saving"
@click="saveConfigInDialog"
>
保存配置
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</v-container>
</template>
<script>
import dataProvider from '@/utils/dataProvider'
import { getSetting } from '@/utils/settings'
import ExamConfigEditor from '@/components/ExamConfigEditor.vue'
export default {
name: 'ExamScheduleManager',
components: {
ExamConfigEditor
},
data() {
return {
configs: [],
loading: false,
error: '',
success: '',
renameDialog: false,
editDialog: false,
configToRename: null,
editingConfig: null,
newConfigName: '',
renaming: false,
saving: false
}
},
async mounted() {
await this.loadConfigs()
},
methods: {
/**
* 初始化示例数据(仅在首次访问时)
*/
async initializeExampleData() {
const exampleConfigs = [
{
id: 'exam_example_001',
examName: '期末考试安排',
message: '请按时参加考试,携带学生证和身份证',
examInfos: [
{
name: '数学',
start: '2025/01/15 09:00',
end: '2025/01/15 11:00'
},
{
name: '英语',
start: '2025/01/16 14:00',
end: '2025/01/16 16:00'
}
]
},
{
id: 'exam_example_002',
examName: '期中考试安排',
message: '考试期间请保持安静',
examInfos: [
{
name: '物理',
start: '2025/01/20 10:00',
end: '2025/01/20 12:00'
},
{
name: '化学',
start: '2025/01/21 14:00',
end: '2025/01/21 16:00'
}
]
},
{
id: 'exam_example_003',
examName: '模拟考试安排',
message: '模拟考试,请认真对待',
examInfos: [
{
name: '语文',
start: '2025/01/25 09:00',
end: '2025/01/25 11:30'
}
]
}
]
// 保存配置列表
const configList = exampleConfigs.map(c => ({ id: c.id }))
await dataProvider.saveData('es_list', configList)
// 保存每个配置的详细信息
for (let config of exampleConfigs) {
const configData = { ...config }
delete configData.id
await dataProvider.saveData(`es_${config.id}`, configData)
}
return exampleConfigs
},
/**
* 加载配置列表
*/
async loadConfigs() {
this.loading = true
this.error = ''
try {
// 读取配置列表
const response = await dataProvider.loadData('es_list')
if (response && response && response.length > 0) {
// 配置列表存在,加载详细信息
this.configs = []
for (let configItem of response) {
try {
const detailResponse = await dataProvider.loadData(`es_${configItem.id}`)
if (detailResponse) {
this.configs.push({
id: configItem.id,
...detailResponse
})
}
} catch (err) {
console.warn(`加载配置 es_${configItem.id} 失败:`, err)
}
}
} else {
// 配置列表不存在或为空,初始化示例数据
this.configs = await this.initializeExampleData()
}
} catch (err) {
this.error = '加载配置列表失败: ' + err.message
this.configs = []
} finally {
this.loading = false
}
},
/**
* 创建新配置
*/
async createNewConfig() {
const newId = Date.now().toString()
const defaultConfig = {
examName: '新考试配置',
message: '请编辑此配置',
room: getSetting('server.classNumber') || '待定',
examInfos: [
{
name: '科目1',
start: '2025/08/29 16:27',
end: '2025/08/29 17:27'
}
]
}
try {
// 保存新配置
const saveResponse = await dataProvider.saveData(`es_${newId}`, defaultConfig)
if (!saveResponse) {
throw new Error(saveResponse.error?.message || '保存失败')
}
// 更新本地配置列表
this.configs.push({
id: newId,
...defaultConfig
})
// 更新存储的配置列表
const currentList = this.configs.map(c => ({ id: c.id }))
const listResponse = await dataProvider.saveData('es_list', currentList)
if (!listResponse) {
throw new Error(listResponse.error?.message || '更新列表失败')
}
this.success = '新配置创建成功'
// 直接打开编辑对话框
const newConfig = this.configs.find(c => c.id === newId)
if (newConfig) {
this.editingConfig = newConfig
this.editDialog = true
}
} catch (err) {
this.error = '创建配置失败: ' + err.message
}
},
/**
* 显示重命名对话框
*/
showRenameDialog(config) {
this.configToRename = config
this.newConfigName = config.examName || `配置 ${config.id}`
this.renameDialog = true
},
/**
* 重命名配置
*/
async renameConfig() {
if (!this.configToRename || !this.newConfigName) return
this.renaming = true
try {
// 准备更新的配置数据(不包含id)
const configData = {
examName: this.newConfigName,
message: this.configToRename.message,
examInfos: this.configToRename.examInfos
}
const saveResponse = await dataProvider.saveData(`es_${this.configToRename.id}`, configData)
if (!saveResponse) {
throw new Error(saveResponse.error?.message || '保存失败')
}
// 更新本地配置列表中的对应项
const configIndex = this.configs.findIndex(c => c.id === this.configToRename.id)
if (configIndex !== -1) {
this.configs[configIndex].examName = this.newConfigName
}
this.success = '配置重命名成功'
this.renameDialog = false
this.configToRename = null
this.newConfigName = ''
} catch (err) {
this.error = '重命名配置失败: ' + err.message
} finally {
this.renaming = false
}
},
/**
* 显示编辑弹框
*/
showEditDialog(config) {
this.editingConfig = config
this.editDialog = true
},
/**
* 关闭编辑弹框
*/
closeEditDialog() {
this.editDialog = false
this.editingConfig = null
this.saving = false
},
/**
* 在弹框中保存配置
*/
async saveConfigInDialog() {
if (this.$refs.configEditor) {
this.saving = true
try {
await this.$refs.configEditor.saveConfig()
} catch (error) {
console.error('保存配置失败:', error)
} finally {
this.saving = false
}
}
},
/**
* 配置保存成功回调
*/
onConfigSaved() {
this.success = '配置保存成功!'
this.loadConfigs() // 重新加载配置列表
setTimeout(() => {
this.success = ''
}, 3000)
},
/**
* 配置保存错误回调
*/
onConfigError(error) {
this.error = error || '保存配置时发生错误'
setTimeout(() => {
this.error = ''
}, 5000)
},
/**
* 配置打开成功回调
*/
onConfigOpened(data) {
this.success = '配置已在新窗口中打开'
setTimeout(() => {
this.success = ''
}, 3000)
},
/**
* 处理配置删除事件
*/
onConfigDeleted(result) {
if (result.success) {
this.success = result.message || "配置删除成功"
// 关闭编辑对话框
this.editDialog = false
// 刷新配置列表
this.loadConfigs()
} else {
this.error = result.message || "删除失败"
}
}
}
}
</script>
<style scoped>
.border-b {
border-bottom: 1px solid rgba(0, 0, 0, 0.12);
}
.border-b:last-child {
border-bottom: none;
}
</style>
+13
View File
@@ -116,6 +116,16 @@
>
随机点名
</v-btn>
<v-btn
v-if="showExamScheduleButton"
color="green"
prepend-icon="mdi-calendar-check"
size="large"
class="ml-2"
@click="$router.push('/examschedule')"
>
考试看板
</v-btn>
<v-btn
v-if="showListCardButton"
color="primary-darken-1"
@@ -839,6 +849,9 @@ export default {
showFullscreenButton() {
return getSetting("display.showFullscreenButton");
},
showExamScheduleButton() {
return getSetting("display.showExamScheduleButton");
},
showAntiScreenBurnCard() {
return getSetting("display.showAntiScreenBurnCard");
},
+187 -5
View File
@@ -1,6 +1,6 @@
import { kvLocalProvider } from "./providers/kvLocalProvider";
import { kvServerProvider } from "./providers/kvServerProvider";
import { getSetting } from "./settings";
import { getSetting, setSetting } from "./settings";
export const formatResponse = (data) => data;
@@ -44,7 +44,7 @@ export default {
* @param {number} options.limit - 每页返回的记录数,默认为 100
* @param {number} options.skip - 跳过的记录数,默认为 0
* @returns {Promise<Object>} 包含键名列表和分页信息的响应对象
*
*
* 使用示例:
* ```javascript
* // 获取前10个键名
@@ -53,14 +53,14 @@ export default {
* console.log('键名列表:', result.keys);
* console.log('总数:', result.total_rows);
* }
*
*
* // 获取第二页数据(跳过前10个)
* const page2 = await dataProvider.loadKeys({ limit: 10, skip: 10 });
*
*
* // 按键名降序排列
* const sorted = await dataProvider.loadKeys({ sortDir: 'desc' });
* ```
*
*
* 返回值格式:
* ```javascript
* {
@@ -86,8 +86,189 @@ export default {
return kvLocalProvider.loadKeys(options);
}
},
/**
* 获取键的云端访问地址,并处理本地到云端的数据迁移
*
* 功能说明:
* 1. 如果用户选择本地存储,则将本地键数据读取并存储到云端
* 2. 如果云端配置为空或错误则自动改成classworksCloudDefaults的配置
* 3. 根据网站验证情况(私有则添加token,公开或受保护则不需要)拼接键的get路径并返回
*
* @param {string} key - 要获取地址的键名
* @param {Object} options - 选项配置
* @param {boolean} options.migrateFromLocal - 是否从本地迁移数据到云端,默认为true
* @param {boolean} options.autoConfigureCloud - 是否自动配置云端默认设置,默认为true
* @returns {Promise<Object>} 包含键访问地址和操作结果的响应对象
*
* 使用示例:
* ```javascript
* import dataProvider from '@/utils/dataProvider';
*
* // 基本用法:获取键的云端地址并自动迁移本地数据
* const result = await dataProvider.getKeyCloudUrl('exam_configs');
* if (result.success) {
* console.log('云端访问地址:', result.url);
* console.log('是否已迁移数据:', result.migrated);
* console.log('是否自动配置:', result.configured);
* } else {
* console.error('获取失败:', result.error.message);
* }
*
* // 仅获取地址,不迁移数据
* const urlOnly = await dataProvider.getKeyCloudUrl('my_data', {
* migrateFromLocal: false
* });
*
* // 不自动配置云端设置
* const noAutoConfig = await dataProvider.getKeyCloudUrl('my_data', {
* autoConfigureCloud: false
* });
* ```
*
* 传入参数示例:
* ```javascript
* // 参数1: key (必需)
* 'exam_configs' // 字符串类型的键名
*
* // 参数2: options (可选)
* {
* migrateFromLocal: true, // 是否迁移本地数据
* autoConfigureCloud: true // 是否自动配置云端
* }
* ```
*
* 返回值格式:
* ```javascript
* // 成功时返回:
* {
* success: true,
* url: "https://kv.wuyuan.dev/device-uuid-123/exam_configs?token=abc123", // 私有访问时包含token
* migrated: true, // 是否成功迁移了本地数据
* configured: false // 是否自动配置了云端设置
* }
*
* // 公开访问时返回:
* {
* success: true,
* url: "https://kv.wuyuan.dev/device-uuid-123/exam_configs", // 公开访问不包含token
* migrated: false,
* configured: true
* }
*
* // 失败时返回:
* {
* success: false,
* error: {
* code: "CLOUD_URL_ERROR",
* message: "获取键云端地址失败"
* }
* }
* ```
*/
async getKeyCloudUrl(key, options = {}) {
const {
migrateFromLocal = true,
autoConfigureCloud = true
} = options;
try {
let serverUrl = getSetting("server.domain");
let siteKey = getSetting("server.siteKey");
const machineId = getSetting("device.uuid");
let configured = false;
// 检查云端配置是否为空或错误,如果是则使用默认配置
if (!serverUrl || !machineId) {
if (autoConfigureCloud) {
// 使用classworksCloudDefaults配置
const classworksCloudDefaults = {
"server.domain": "https://kv.wuyuan.dev",
"server.siteKey": "",
};
if (!serverUrl) {
setSetting("server.domain", classworksCloudDefaults["server.domain"]);
serverUrl = classworksCloudDefaults["server.domain"];
configured = true;
}
if (!siteKey) {
setSetting("server.siteKey", classworksCloudDefaults["server.siteKey"]);
siteKey = classworksCloudDefaults["server.siteKey"];
}
// 设置provider为classworkscloud
setSetting("server.provider", "classworkscloud");
} else {
return formatError("云端配置无效,请检查服务器域名和设备UUID", "CONFIG_ERROR");
}
}
let migrated = false;
// 如果需要迁移本地数据到云端
if (migrateFromLocal) {
try {
// 尝试从本地读取数据
const localData = await kvLocalProvider.loadData(key);
// 如果本地有数据且不是错误响应
if (localData && localData.success !== false) {
// 检查云端是否已有数据
const cloudData = await kvServerProvider.loadData(key);
// 如果云端没有数据,则迁移本地数据
if (cloudData && cloudData.success === false && cloudData.error?.code === "NOT_FOUND") {
const saveResult = await kvServerProvider.saveData(key, localData);
if (saveResult && saveResult.success !== false) {
migrated = true;
console.log(`已成功将键 ${key} 的数据从本地迁移到云端`);
}
}
}
} catch (error) {
console.warn(`迁移键 ${key} 的数据时出错:`, error);
// 迁移失败不影响URL生成,继续执行
}
}
// 构建云端访问URL
let url = `${serverUrl}/${machineId}/${key}`;
// 根据网站验证情况添加token参数
const namespaceInfo = await kvServerProvider.loadNamespaceInfo();
if (namespaceInfo && namespaceInfo.success !== false) {
const { accessType } = namespaceInfo;
// 如果是私有访问,添加token参数
if (accessType === 'private' && siteKey) {
const urlObj = new URL(url);
urlObj.searchParams.set('token', siteKey);
url = urlObj.toString();
}
// 公开或受保护访问不需要token参数
}
return {
success: true,
url,
migrated,
configured
};
} catch (error) {
console.error('获取键云端地址时出错:', error);
return formatError(
error.message || "获取键云端地址失败",
"CLOUD_URL_ERROR"
);
}
},
};
export const ErrorCodes = {
NOT_FOUND: "数据不存在",
NETWORK_ERROR: "网络连接失败",
@@ -96,5 +277,6 @@ export const ErrorCodes = {
CONFIG_ERROR: "配置错误",
PERMISSION_DENIED: "无权限访问",
UNAUTHORIZED: "认证失败",
CLOUD_URL_ERROR: "云端地址获取失败",
UNKNOWN_ERROR: "未知错误",
};
+6 -8
View File
@@ -55,7 +55,7 @@ export const kvLocalProvider = {
* @param {number} options.limit - 每页返回的记录数,默认为 100
* @param {number} options.skip - 跳过的记录数,默认为 0
* @returns {Promise<Object>} 包含键名列表和分页信息的响应对象
*
*
* 返回值示例:
* {
* keys: ["key1", "key2", "key3"],
@@ -73,18 +73,16 @@ export const kvLocalProvider = {
const db = await initDB();
const transaction = db.transaction(["kv"], "readonly");
const store = transaction.objectStore("kv");
// 获取所有键名
const allKeys = await store.getAllKeys();
// 设置默认参数
const {
sortBy = "key",
sortDir = "asc",
limit = 100,
skip = 0
} = options;
// 排序键名(本地存储只支持按键名排序)
const sortedKeys = allKeys.sort((a, b) => {
if (sortDir === "desc") {
@@ -92,11 +90,11 @@ export const kvLocalProvider = {
}
return a.localeCompare(b);
});
// 应用分页
const totalRows = sortedKeys.length;
const paginatedKeys = sortedKeys.slice(skip, skip + limit);
// 构建响应数据
const responseData = {
keys: paginatedKeys,
@@ -108,7 +106,7 @@ export const kvLocalProvider = {
},
load_more: null // 本地存储不需要分页URL
};
return formatResponse(responseData);
} catch (error) {
return formatError("获取本地键名列表失败:" + error.message);
+7
View File
@@ -170,6 +170,13 @@ const settingsDefinitions = {
description: "是否显示列表卡片",
icon: "mdi-list-box",
},
"display.showExamScheduleButton": {
type: "boolean",
default: true,
description: "是否显示考试看板",
icon: "mdi-calendar-check",
// 控制是否在主页显示考试看板按钮,指向考试安排页面
},
// 服务器设置(合并了数据提供者设置)
"server.domain": {
type: "string",