添加考试看板功能
This commit is contained in:
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
@@ -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
@@ -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: "未知错误",
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user