mirror of
https://github.com/ZeroCatDev/Classworks.git
synced 2025-08-31 20:29:23 +00:00
添加key查看器
This commit is contained in:
parent
c744f37f39
commit
cd10d0f49a
616
src/components/settings/cards/KvDatabaseCard.vue
Normal file
616
src/components/settings/cards/KvDatabaseCard.vue
Normal file
@ -0,0 +1,616 @@
|
||||
<template>
|
||||
<settings-card title="KV数据库管理" icon="mdi-database-edit" :loading="loading">
|
||||
<v-list>
|
||||
<!-- 数据库连接状态 -->
|
||||
<v-list-item>
|
||||
<template #prepend>
|
||||
<v-icon :icon="connectionIcon" :color="connectionColor" class="mr-3" />
|
||||
</template>
|
||||
<v-list-item-title>数据库状态</v-list-item-title>
|
||||
<v-list-item-subtitle>{{ connectionStatus }}</v-list-item-subtitle>
|
||||
<template #append>
|
||||
<v-btn variant="tonal" @click="refreshConnection" :loading="loading">
|
||||
刷新
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-list-item>
|
||||
|
||||
<v-divider class="my-2" />
|
||||
|
||||
<!-- 数据列表 -->
|
||||
<v-list-item>
|
||||
<template #prepend>
|
||||
<v-icon icon="mdi-format-list-bulleted" class="mr-3" />
|
||||
</template>
|
||||
<v-list-item-title>数据条目</v-list-item-title>
|
||||
<v-list-item-subtitle>共 {{ kvData.length }} 条记录</v-list-item-subtitle>
|
||||
<template #append>
|
||||
<v-btn-group variant="tonal">
|
||||
<v-btn @click="loadKvData" :loading="loadingData">
|
||||
加载数据
|
||||
</v-btn>
|
||||
<v-btn @click="createNewItem" :disabled="!isKvProvider">
|
||||
<v-icon icon="mdi-plus" class="mr-1" />
|
||||
新建
|
||||
</v-btn>
|
||||
</v-btn-group>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
|
||||
<!-- 数据表格 -->
|
||||
<v-card v-if="kvData.length > 0" class="mt-4" variant="outlined">
|
||||
<v-card-title class="d-flex align-center">
|
||||
<v-icon icon="mdi-table" class="mr-2" />
|
||||
KV数据列表
|
||||
<v-spacer />
|
||||
<v-text-field
|
||||
v-model="searchQuery"
|
||||
label="搜索键名"
|
||||
prepend-inner-icon="mdi-magnify"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
hide-details
|
||||
clearable
|
||||
style="max-width: 300px;"
|
||||
/>
|
||||
</v-card-title>
|
||||
|
||||
<v-data-table
|
||||
:headers="tableHeaders"
|
||||
:items="filteredKvData"
|
||||
:loading="loadingData"
|
||||
item-value="key"
|
||||
class="elevation-0"
|
||||
:items-per-page="10"
|
||||
>
|
||||
<template #[`item.key`]="{ item }">
|
||||
<code class="text-primary">{{ item.key }}</code>
|
||||
</template>
|
||||
|
||||
<template #[`item.actions`]="{ item }">
|
||||
<v-btn-group variant="text" density="compact">
|
||||
<v-btn
|
||||
icon="mdi-eye"
|
||||
size="small"
|
||||
@click="viewItem(item)"
|
||||
title="查看"
|
||||
/>
|
||||
<v-btn
|
||||
icon="mdi-pencil"
|
||||
size="small"
|
||||
@click="editItem(item)"
|
||||
title="编辑"
|
||||
/>
|
||||
<v-btn
|
||||
icon="mdi-delete"
|
||||
size="small"
|
||||
color="error"
|
||||
@click="confirmDelete(item)"
|
||||
title="删除"
|
||||
/>
|
||||
</v-btn-group>
|
||||
</template>
|
||||
</v-data-table>
|
||||
</v-card>
|
||||
|
||||
<!-- 查看数据对话框 -->
|
||||
<v-dialog v-model="viewDialog" max-width="800px">
|
||||
<v-card>
|
||||
<v-card-title class="d-flex align-center">
|
||||
<v-icon icon="mdi-eye" class="mr-2" />
|
||||
查看数据
|
||||
<v-spacer />
|
||||
<v-btn icon="mdi-close" variant="text" @click="viewDialog = false" />
|
||||
</v-card-title>
|
||||
|
||||
<v-card-subtitle v-if="selectedItem">
|
||||
键名: <code>{{ selectedItem.key }}</code>
|
||||
</v-card-subtitle>
|
||||
|
||||
<v-card-text>
|
||||
<v-textarea
|
||||
v-if="selectedItem"
|
||||
:model-value="formatJsonData(selectedItem.value)"
|
||||
label="数据内容"
|
||||
variant="outlined"
|
||||
readonly
|
||||
rows="15"
|
||||
class="font-monospace"
|
||||
/>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn @click="copyToClipboard(selectedItem?.value)" variant="tonal">
|
||||
<v-icon icon="mdi-content-copy" class="mr-1" />
|
||||
复制数据
|
||||
</v-btn>
|
||||
<v-btn @click="viewDialog = false" variant="text">
|
||||
关闭
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<!-- 编辑数据对话框 -->
|
||||
<v-dialog v-model="editDialog" max-width="800px">
|
||||
<v-card>
|
||||
<v-card-title class="d-flex align-center">
|
||||
<v-icon icon="mdi-pencil" class="mr-2" />
|
||||
编辑数据
|
||||
<v-spacer />
|
||||
<v-btn icon="mdi-close" variant="text" @click="closeEditDialog" />
|
||||
</v-card-title>
|
||||
|
||||
<v-card-subtitle v-if="editingItem">
|
||||
键名: <code>{{ editingItem.key }}</code>
|
||||
</v-card-subtitle>
|
||||
|
||||
<v-card-text>
|
||||
<v-textarea
|
||||
v-model="editingData"
|
||||
label="数据内容 (JSON格式)"
|
||||
variant="outlined"
|
||||
rows="15"
|
||||
class="font-monospace"
|
||||
:error="!isValidJson"
|
||||
:error-messages="isValidJson ? [] : ['请输入有效的JSON格式']"
|
||||
/>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn @click="closeEditDialog" variant="text">
|
||||
取消
|
||||
</v-btn>
|
||||
<v-btn
|
||||
@click="saveEditedData"
|
||||
variant="tonal"
|
||||
color="primary"
|
||||
:disabled="!isValidJson"
|
||||
:loading="savingData"
|
||||
>
|
||||
保存
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<!-- 新建数据对话框 -->
|
||||
<v-dialog v-model="createDialog" max-width="800px">
|
||||
<v-card>
|
||||
<v-card-title class="d-flex align-center">
|
||||
<v-icon icon="mdi-plus" class="mr-2" />
|
||||
新建数据
|
||||
<v-spacer />
|
||||
<v-btn icon="mdi-close" variant="text" @click="closeCreateDialog" />
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text>
|
||||
<v-text-field
|
||||
v-model="newKey"
|
||||
label="键名"
|
||||
variant="outlined"
|
||||
class="mb-4"
|
||||
:error="!isValidKey"
|
||||
:error-messages="isValidKey ? [] : ['键名不能为空且不能与现有键重复']"
|
||||
placeholder="请输入键名,如:my-config"
|
||||
/>
|
||||
|
||||
<v-textarea
|
||||
v-model="newData"
|
||||
label="数据内容 (JSON格式)"
|
||||
variant="outlined"
|
||||
rows="15"
|
||||
class="font-monospace"
|
||||
:error="!isValidNewJson"
|
||||
:error-messages="isValidNewJson ? [] : ['请输入有效的JSON格式']"
|
||||
placeholder='请输入JSON数据,如:{"name": "value"}'
|
||||
/>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn @click="closeCreateDialog" variant="text">
|
||||
取消
|
||||
</v-btn>
|
||||
<v-btn
|
||||
@click="saveNewData"
|
||||
variant="tonal"
|
||||
color="primary"
|
||||
:disabled="!isValidKey || !isValidNewJson"
|
||||
:loading="savingData"
|
||||
>
|
||||
创建
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<!-- 删除确认对话框 -->
|
||||
<v-dialog v-model="deleteDialog" max-width="400px">
|
||||
<v-card>
|
||||
<v-card-title class="d-flex align-center text-error">
|
||||
<v-icon icon="mdi-alert" class="mr-2" />
|
||||
确认删除
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text>
|
||||
确定要删除键名为 <code>{{ itemToDelete?.key }}</code> 的数据吗?
|
||||
<br><br>
|
||||
<v-alert type="warning" variant="tonal" class="mt-2">
|
||||
此操作不可撤销,请谨慎操作!
|
||||
</v-alert>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn @click="deleteDialog = false" variant="text">
|
||||
取消
|
||||
</v-btn>
|
||||
<v-btn
|
||||
@click="deleteItem"
|
||||
variant="tonal"
|
||||
color="error"
|
||||
:loading="deletingData"
|
||||
>
|
||||
删除
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</settings-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import SettingsCard from '@/components/SettingsCard.vue';
|
||||
import dataProvider from '@/utils/dataProvider';
|
||||
import { getSetting } from '@/utils/settings';
|
||||
import { openDB } from 'idb';
|
||||
|
||||
export default {
|
||||
name: 'KvDatabaseCard',
|
||||
components: {
|
||||
SettingsCard
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
loadingData: false,
|
||||
savingData: false,
|
||||
deletingData: false,
|
||||
kvData: [],
|
||||
searchQuery: '',
|
||||
|
||||
// 对话框状态
|
||||
viewDialog: false,
|
||||
editDialog: false,
|
||||
deleteDialog: false,
|
||||
createDialog: false,
|
||||
|
||||
// 选中的项目
|
||||
selectedItem: null,
|
||||
editingItem: null,
|
||||
itemToDelete: null,
|
||||
|
||||
// 编辑数据
|
||||
editingData: '',
|
||||
newKey: '',
|
||||
newData: '',
|
||||
|
||||
// 表格头部
|
||||
tableHeaders: [
|
||||
{ title: '键名', key: 'key', sortable: true },
|
||||
{ title: '操作', key: 'actions', sortable: false, width: '120px' }
|
||||
]
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
currentProvider() {
|
||||
return getSetting('server.provider');
|
||||
},
|
||||
|
||||
isKvProvider() {
|
||||
return this.currentProvider === 'kv-local' || this.currentProvider === 'kv-server'||this.currentProvider === 'classworkscloud'
|
||||
},
|
||||
|
||||
connectionStatus() {
|
||||
if (!this.isKvProvider) {
|
||||
return '当前数据提供者不支持KV数据库管理';
|
||||
}
|
||||
return this.currentProvider === 'kv-local' ? '本地数据库' : '服务器数据库';
|
||||
},
|
||||
|
||||
connectionIcon() {
|
||||
if (!this.isKvProvider) return 'mdi-database-off';
|
||||
return this.currentProvider === 'kv-local' ? 'mdi-database' : 'mdi-database-sync';
|
||||
},
|
||||
|
||||
connectionColor() {
|
||||
if (!this.isKvProvider) return 'error';
|
||||
return 'success';
|
||||
},
|
||||
|
||||
filteredKvData() {
|
||||
if (!this.searchQuery) return this.kvData;
|
||||
return this.kvData.filter(item =>
|
||||
item.key.toLowerCase().includes(this.searchQuery.toLowerCase())
|
||||
);
|
||||
},
|
||||
|
||||
isValidJson() {
|
||||
if (!this.editingData) return true;
|
||||
try {
|
||||
JSON.parse(this.editingData);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
isValidNewJson() {
|
||||
if (!this.newData) return true;
|
||||
try {
|
||||
JSON.parse(this.newData);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
isValidKey() {
|
||||
if (!this.newKey || this.newKey.trim() === '') return false;
|
||||
// 检查是否与现有键重复
|
||||
return !this.kvData.some(item => item.key === this.newKey.trim());
|
||||
}
|
||||
},
|
||||
|
||||
async mounted() {
|
||||
if (this.isKvProvider) {
|
||||
await this.loadKvData();
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
async refreshConnection() {
|
||||
this.loading = true;
|
||||
try {
|
||||
// 重新检查连接状态
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
this.$message.success('连接状态已刷新');
|
||||
} catch (error) {
|
||||
this.$message.error('刷新失败', error.message);
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
async loadKvData() {
|
||||
if (!this.isKvProvider) {
|
||||
this.$message.warning('当前数据提供者不支持KV数据库管理');
|
||||
return;
|
||||
}
|
||||
|
||||
this.loadingData = true;
|
||||
try {
|
||||
this.kvData = [];
|
||||
|
||||
// 使用新的loadKeys API获取键名列表
|
||||
const result = await dataProvider.loadKeys({
|
||||
sortBy: 'key',
|
||||
sortDir: 'asc',
|
||||
limit: 1000 // 获取更多数据,可根据需要调整
|
||||
});
|
||||
|
||||
if (result.success === false) {
|
||||
throw new Error(result.error?.message || '获取键名列表失败');
|
||||
}
|
||||
|
||||
// 只保存键名,不读取内容
|
||||
this.kvData = result.keys.map(key => ({
|
||||
key,
|
||||
value: null, // 不预加载内容
|
||||
loaded: false // 标记是否已加载内容
|
||||
}));
|
||||
|
||||
this.$message.success('键名加载完成', `共找到 ${this.kvData.length} 个键,总计 ${result.total_rows} 个键`);
|
||||
} catch (error) {
|
||||
this.$message.error('加载数据失败', error.message);
|
||||
} finally {
|
||||
this.loadingData = false;
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
async viewItem(item) {
|
||||
this.selectedItem = item;
|
||||
this.viewDialog = true;
|
||||
|
||||
// 如果数据未加载,则加载数据
|
||||
if (!item.loaded || item.value === null) {
|
||||
await this.loadItemData(item);
|
||||
}
|
||||
},
|
||||
|
||||
async editItem(item) {
|
||||
this.editingItem = item;
|
||||
|
||||
// 如果数据未加载,则加载数据
|
||||
if (!item.loaded || item.value === null) {
|
||||
await this.loadItemData(item);
|
||||
}
|
||||
|
||||
this.editingData = this.formatJsonData(item.value);
|
||||
this.editDialog = true;
|
||||
},
|
||||
|
||||
async loadItemData(item) {
|
||||
try {
|
||||
const data = await dataProvider.loadData(item.key);
|
||||
if (data && data.success !== false) {
|
||||
item.value = data;
|
||||
item.loaded = true;
|
||||
} else {
|
||||
throw new Error('数据加载失败');
|
||||
}
|
||||
} catch (error) {
|
||||
this.$message.error('加载数据失败', error.message);
|
||||
item.value = null;
|
||||
item.loaded = false;
|
||||
}
|
||||
},
|
||||
|
||||
closeEditDialog() {
|
||||
this.editDialog = false;
|
||||
this.editingItem = null;
|
||||
this.editingData = '';
|
||||
},
|
||||
|
||||
createNewItem() {
|
||||
this.newKey = '';
|
||||
this.newData = '{\n "example": "value"\n}';
|
||||
this.createDialog = true;
|
||||
},
|
||||
|
||||
closeCreateDialog() {
|
||||
this.createDialog = false;
|
||||
this.newKey = '';
|
||||
this.newData = '';
|
||||
},
|
||||
|
||||
async saveNewData() {
|
||||
if (!this.isValidKey || !this.isValidNewJson) return;
|
||||
|
||||
this.savingData = true;
|
||||
try {
|
||||
const parsedData = JSON.parse(this.newData);
|
||||
const key = this.newKey.trim();
|
||||
const result = await dataProvider.saveData(key, parsedData);
|
||||
|
||||
if (result && !result.error) {
|
||||
// 添加到本地数据列表
|
||||
this.kvData.push({
|
||||
key,
|
||||
value: parsedData,
|
||||
loaded: true
|
||||
});
|
||||
|
||||
this.$message.success('数据创建成功');
|
||||
this.closeCreateDialog();
|
||||
} else {
|
||||
throw new Error(result.error?.message || '创建失败');
|
||||
}
|
||||
} catch (error) {
|
||||
this.$message.error('创建失败', error.message);
|
||||
} finally {
|
||||
this.savingData = false;
|
||||
}
|
||||
},
|
||||
|
||||
async saveEditedData() {
|
||||
if (!this.isValidJson || !this.editingItem) return;
|
||||
|
||||
this.savingData = true;
|
||||
try {
|
||||
const parsedData = JSON.parse(this.editingData);
|
||||
const result = await dataProvider.saveData(this.editingItem.key, parsedData);
|
||||
|
||||
if (result && !result.error) {
|
||||
// 更新本地数据
|
||||
const index = this.kvData.findIndex(item => item.key === this.editingItem.key);
|
||||
if (index !== -1) {
|
||||
this.kvData[index].value = parsedData;
|
||||
this.kvData[index].loaded = true;
|
||||
}
|
||||
|
||||
this.$message.success('数据保存成功');
|
||||
this.closeEditDialog();
|
||||
} else {
|
||||
throw new Error(result.error?.message || '保存失败');
|
||||
}
|
||||
} catch (error) {
|
||||
this.$message.error('保存失败', error.message);
|
||||
} finally {
|
||||
this.savingData = false;
|
||||
}
|
||||
},
|
||||
|
||||
confirmDelete(item) {
|
||||
this.itemToDelete = item;
|
||||
this.deleteDialog = true;
|
||||
},
|
||||
|
||||
async deleteItem() {
|
||||
if (!this.itemToDelete) return;
|
||||
|
||||
this.deletingData = true;
|
||||
try {
|
||||
// 对于本地存储,直接删除
|
||||
if (this.currentProvider === 'kv-local') {
|
||||
const db = await openDB('ClassworksDB', 2);
|
||||
const tx = db.transaction('kv', 'readwrite');
|
||||
const store = tx.objectStore('kv');
|
||||
await store.delete(this.itemToDelete.key);
|
||||
} else {
|
||||
// 对于服务器存储,这里需要实现删除API
|
||||
// 注意:大多数KV服务器不提供删除功能,可能需要设置为null
|
||||
await dataProvider.saveData(this.itemToDelete.key, null);
|
||||
}
|
||||
|
||||
// 从本地列表中移除
|
||||
const index = this.kvData.findIndex(item => item.key === this.itemToDelete.key);
|
||||
if (index !== -1) {
|
||||
this.kvData.splice(index, 1);
|
||||
}
|
||||
|
||||
this.$message.success('数据删除成功');
|
||||
this.deleteDialog = false;
|
||||
this.itemToDelete = null;
|
||||
} catch (error) {
|
||||
this.$message.error('删除失败', error.message);
|
||||
} finally {
|
||||
this.deletingData = false;
|
||||
}
|
||||
},
|
||||
|
||||
formatJsonData(data) {
|
||||
try {
|
||||
return JSON.stringify(data, null, 2);
|
||||
} catch {
|
||||
return String(data);
|
||||
}
|
||||
},
|
||||
|
||||
async copyToClipboard(data) {
|
||||
try {
|
||||
const text = this.formatJsonData(data);
|
||||
await navigator.clipboard.writeText(text);
|
||||
this.$message.success('数据已复制到剪贴板');
|
||||
} catch (error) {
|
||||
this.$message.error('复制失败', error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.font-monospace {
|
||||
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
|
||||
}
|
||||
|
||||
code {
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
padding: 2px 4px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.875em;
|
||||
}
|
||||
</style>
|
@ -1194,23 +1194,26 @@ export default {
|
||||
);
|
||||
}
|
||||
|
||||
// 加载科目配置
|
||||
try {
|
||||
const subjectsResponse = await dataProvider.loadData("classworks-config-subject");
|
||||
if (subjectsResponse && Array.isArray(subjectsResponse)) {
|
||||
// 更新科目列表
|
||||
this.state.availableSubjects = subjectsResponse;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("Failed to load subject configuration:", error);
|
||||
// 保持默认科目列表
|
||||
}
|
||||
await this.loadSubjects();
|
||||
} catch (error) {
|
||||
console.error("加载配置失败:", error);
|
||||
this.$message.error("加载配置失败", error.message);
|
||||
}
|
||||
},
|
||||
|
||||
async loadSubjects() {
|
||||
try {
|
||||
const subjectsResponse = await dataProvider.loadData("classworks-config-subject");
|
||||
if (subjectsResponse && Array.isArray(subjectsResponse)) {
|
||||
// 更新科目列表
|
||||
this.state.availableSubjects = subjectsResponse;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("Failed to load subject configuration:", error);
|
||||
// 保持默认科目列表
|
||||
}
|
||||
},
|
||||
|
||||
showSyncMessage() {
|
||||
this.$message.success("数据已同步", "数据已完成与服务器同步");
|
||||
},
|
||||
@ -1358,7 +1361,7 @@ export default {
|
||||
this.updateBackendUrl();
|
||||
},
|
||||
|
||||
handleDateSelect(newDate) {
|
||||
async handleDateSelect(newDate) {
|
||||
if (!newDate) return;
|
||||
|
||||
try {
|
||||
@ -1377,7 +1380,12 @@ export default {
|
||||
query: { date: formattedDate },
|
||||
})
|
||||
.catch(() => {});
|
||||
this.downloadData();
|
||||
|
||||
// Load both data and subjects in parallel
|
||||
await Promise.all([
|
||||
this.downloadData(),
|
||||
this.loadSubjects()
|
||||
]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Date processing error:", error);
|
||||
|
@ -104,6 +104,7 @@
|
||||
@saved="onSettingsSaved"
|
||||
/>
|
||||
<data-provider-settings-card border class="mt-4" />
|
||||
<kv-database-card border class="mt-4" />
|
||||
</v-tabs-window-item>
|
||||
|
||||
<v-tabs-window-item value="namespace">
|
||||
@ -241,6 +242,7 @@ import NamespaceSettingsCard from "@/components/settings/cards/NamespaceSettings
|
||||
import RandomPickerCard from "@/components/settings/cards/RandomPickerCard.vue";
|
||||
import HomeworkTemplateCard from "@/components/settings/cards/HomeworkTemplateCard.vue";
|
||||
import SubjectManagementCard from "@/components/settings/cards/SubjectManagementCard.vue";
|
||||
import KvDatabaseCard from "@/components/settings/cards/KvDatabaseCard.vue";
|
||||
export default {
|
||||
name: "Settings",
|
||||
components: {
|
||||
@ -261,6 +263,7 @@ export default {
|
||||
RandomPickerCard,
|
||||
HomeworkTemplateCard,
|
||||
SubjectManagementCard,
|
||||
KvDatabaseCard,
|
||||
},
|
||||
setup() {
|
||||
const { mobile } = useDisplay();
|
||||
|
@ -35,6 +35,57 @@ export default {
|
||||
return kvLocalProvider.saveData(key, data);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取键名列表
|
||||
* @param {Object} options - 查询选项
|
||||
* @param {string} options.sortBy - 排序字段,默认为 "key"
|
||||
* @param {string} options.sortDir - 排序方向,"asc" 或 "desc",默认为 "asc"
|
||||
* @param {number} options.limit - 每页返回的记录数,默认为 100
|
||||
* @param {number} options.skip - 跳过的记录数,默认为 0
|
||||
* @returns {Promise<Object>} 包含键名列表和分页信息的响应对象
|
||||
*
|
||||
* 使用示例:
|
||||
* ```javascript
|
||||
* // 获取前10个键名
|
||||
* const result = await dataProvider.loadKeys({ limit: 10 });
|
||||
* if (result.success !== false) {
|
||||
* 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
|
||||
* {
|
||||
* keys: ["key1", "key2", "key3"],
|
||||
* total_rows: 150,
|
||||
* current_page: {
|
||||
* limit: 10,
|
||||
* skip: 0,
|
||||
* count: 10
|
||||
* },
|
||||
* load_more: "/api/kv/namespace/_keys?..." // 仅服务器模式
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
loadKeys: async (options = {}) => {
|
||||
const provider = getSetting("server.provider");
|
||||
const useServer =
|
||||
provider === "kv-server" || provider === "classworkscloud";
|
||||
|
||||
if (useServer) {
|
||||
return kvServerProvider.loadKeys(options);
|
||||
} else {
|
||||
return kvLocalProvider.loadKeys(options);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export const ErrorCodes = {
|
||||
@ -43,5 +94,7 @@ export const ErrorCodes = {
|
||||
SERVER_ERROR: "服务器错误",
|
||||
SAVE_ERROR: "保存失败",
|
||||
CONFIG_ERROR: "配置错误",
|
||||
PERMISSION_DENIED: "无权限访问",
|
||||
UNAUTHORIZED: "认证失败",
|
||||
UNKNOWN_ERROR: "未知错误",
|
||||
};
|
||||
|
@ -46,4 +46,72 @@ export const kvLocalProvider = {
|
||||
return formatError("保存本地数据失败:" + error);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取本地存储的键名列表
|
||||
* @param {Object} options - 查询选项
|
||||
* @param {string} options.sortBy - 排序字段,默认为 "key"
|
||||
* @param {string} options.sortDir - 排序方向,"asc" 或 "desc",默认为 "asc"
|
||||
* @param {number} options.limit - 每页返回的记录数,默认为 100
|
||||
* @param {number} options.skip - 跳过的记录数,默认为 0
|
||||
* @returns {Promise<Object>} 包含键名列表和分页信息的响应对象
|
||||
*
|
||||
* 返回值示例:
|
||||
* {
|
||||
* keys: ["key1", "key2", "key3"],
|
||||
* total_rows: 150,
|
||||
* current_page: {
|
||||
* limit: 10,
|
||||
* skip: 0,
|
||||
* count: 10
|
||||
* },
|
||||
* load_more: null // 本地存储不需要分页URL
|
||||
* }
|
||||
*/
|
||||
async loadKeys(options = {}) {
|
||||
try {
|
||||
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") {
|
||||
return b.localeCompare(a);
|
||||
}
|
||||
return a.localeCompare(b);
|
||||
});
|
||||
|
||||
// 应用分页
|
||||
const totalRows = sortedKeys.length;
|
||||
const paginatedKeys = sortedKeys.slice(skip, skip + limit);
|
||||
|
||||
// 构建响应数据
|
||||
const responseData = {
|
||||
keys: paginatedKeys,
|
||||
total_rows: totalRows,
|
||||
current_page: {
|
||||
limit,
|
||||
skip,
|
||||
count: paginatedKeys.length
|
||||
},
|
||||
load_more: null // 本地存储不需要分页URL
|
||||
};
|
||||
|
||||
return formatResponse(responseData);
|
||||
} catch (error) {
|
||||
return formatError("获取本地键名列表失败:" + error.message);
|
||||
}
|
||||
},
|
||||
};
|
@ -157,4 +157,69 @@ export const kvServerProvider = {
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取键名列表
|
||||
* @param {Object} options - 查询选项
|
||||
* @param {string} options.sortBy - 排序字段,默认为 "key"
|
||||
* @param {string} options.sortDir - 排序方向,"asc" 或 "desc",默认为 "asc"
|
||||
* @param {number} options.limit - 每页返回的记录数,默认为 100
|
||||
* @param {number} options.skip - 跳过的记录数,默认为 0
|
||||
* @returns {Promise<Object>} 包含键名列表和分页信息的响应对象
|
||||
*
|
||||
* 返回值示例:
|
||||
* {
|
||||
* keys: ["key1", "key2", "key3"],
|
||||
* total_rows: 150,
|
||||
* current_page: {
|
||||
* limit: 10,
|
||||
* skip: 0,
|
||||
* count: 10
|
||||
* },
|
||||
* load_more: "/api/kv/namespace/_keys?sortBy=key&sortDir=asc&limit=10&skip=10"
|
||||
* }
|
||||
*/
|
||||
async loadKeys(options = {}) {
|
||||
try {
|
||||
const serverUrl = getSetting("server.domain");
|
||||
const machineId = getSetting("device.uuid");
|
||||
|
||||
// 设置默认参数
|
||||
const {
|
||||
sortBy = "key",
|
||||
sortDir = "asc",
|
||||
limit = 100,
|
||||
skip = 0
|
||||
} = options;
|
||||
|
||||
// 构建查询参数
|
||||
const params = new URLSearchParams({
|
||||
sortBy,
|
||||
sortDir,
|
||||
limit: limit.toString(),
|
||||
skip: skip.toString()
|
||||
});
|
||||
|
||||
const res = await axios.get(`${serverUrl}/${machineId}/_keys?${params}`, {
|
||||
headers: getHeaders(),
|
||||
});
|
||||
|
||||
return formatResponse(res.data);
|
||||
} catch (error) {
|
||||
if (error.response?.status === 404) {
|
||||
return formatError("命名空间不存在", "NOT_FOUND");
|
||||
}
|
||||
if (error.response?.status === 403) {
|
||||
return formatError("无权限访问此命名空间", "PERMISSION_DENIED");
|
||||
}
|
||||
if (error.response?.status === 401) {
|
||||
return formatError("认证失败", "UNAUTHORIZED");
|
||||
}
|
||||
console.log(error);
|
||||
return formatError(
|
||||
error.response?.data?.message || "获取键名列表失败",
|
||||
"NETWORK_ERROR"
|
||||
);
|
||||
}
|
||||
},
|
||||
};
|
Loading…
x
Reference in New Issue
Block a user