pwa,设置项,自动刷新
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
<template>
|
||||
<v-card :border="border" class="setting-group">
|
||||
<v-card-title v-if="title" class="d-flex align-center">
|
||||
<v-icon v-if="icon" :icon="icon" class="mr-2" />
|
||||
{{ title }}
|
||||
</v-card-title>
|
||||
|
||||
<v-card-subtitle v-if="description">
|
||||
{{ description }}
|
||||
</v-card-subtitle>
|
||||
|
||||
<v-card-text>
|
||||
<v-list>
|
||||
<slot>
|
||||
<!-- 默认插槽用于放置 SettingItem 组件 -->
|
||||
</slot>
|
||||
</v-list>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions v-if="$slots.actions">
|
||||
<slot name="actions"></slot>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'SettingGroup',
|
||||
|
||||
props: {
|
||||
/**
|
||||
* 设置组的标题
|
||||
*/
|
||||
title: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
|
||||
/**
|
||||
* 设置组的描述
|
||||
*/
|
||||
description: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
|
||||
/**
|
||||
* 设置组的图标
|
||||
*/
|
||||
icon: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
|
||||
/**
|
||||
* 是否显示边框
|
||||
*/
|
||||
border: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
onSettingUpdate(key, value) {
|
||||
this.$emit('update', key, value);
|
||||
},
|
||||
|
||||
onSettingError(key) {
|
||||
this.$emit('error', key);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.setting-group {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,457 @@
|
||||
<template>
|
||||
<v-list-item class="setting-item" :disabled="disabled">
|
||||
<template #prepend>
|
||||
<v-icon :icon="settingIcon" />
|
||||
</template>
|
||||
|
||||
<v-list-item-title class="text-wrap">
|
||||
{{ displayTitle }}
|
||||
</v-list-item-title>
|
||||
|
||||
<v-list-item-subtitle class="d-flex align-center text-wrap">
|
||||
<span class="text-caption text-grey-darken-1">{{ settingKey }}</span>
|
||||
</v-list-item-subtitle>
|
||||
|
||||
<template #append >
|
||||
<div class="d-flex flex-column flex-sm-row align-center">
|
||||
<div v-if="type !== 'string' || hasOptions" class="me-2">
|
||||
<!-- 根据设置类型渲染不同的控件 -->
|
||||
<v-switch v-if="type === 'boolean'" v-model="localValue" density="comfortable" hide-details :disabled="disabled"
|
||||
@update:model-value="updateSetting" />
|
||||
|
||||
<v-select v-else-if="type === 'string' && hasOptions" v-model="localValue" :items="selectOptions"
|
||||
density="compact" hide-details :disabled="disabled" class="setting-select" variant="outlined" bg-color="surface"
|
||||
@update:model-value="updateSetting" item-title="title" item-value="value" />
|
||||
|
||||
<div v-else-if="type === 'number'" class="d-flex align-center">
|
||||
<v-btn icon="mdi-minus" size="small" variant="text" :disabled="disabled || localValue <= minValue"
|
||||
@click="adjustValue(-stepValue)" />
|
||||
|
||||
<v-text-field v-model.number="localValue" type="number" density="compact" hide-details :min="minValue"
|
||||
:max="maxValue" :step="stepValue" :disabled="disabled" class="mx-2 setting-number-field" style="width: 80px"
|
||||
variant="outlined" bg-color="surface" @update:model-value="updateSetting" />
|
||||
|
||||
<v-btn icon="mdi-plus" size="small" variant="text" :disabled="disabled || localValue >= maxValue"
|
||||
@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"
|
||||
:disabled="disabled" />
|
||||
</template>
|
||||
<v-list density="compact">
|
||||
<v-list-item @click="copySettingId">
|
||||
<template v-slot:prepend>
|
||||
<v-icon icon="mdi-key" size="small" />
|
||||
</template>
|
||||
<v-list-item-title>复制设置ID</v-list-item-title>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item @click="copySettingValue">
|
||||
<template v-slot:prepend>
|
||||
<v-icon icon="mdi-content-copy" size="small" />
|
||||
</template>
|
||||
<v-list-item-title>复制设置值</v-list-item-title>
|
||||
</v-list-item>
|
||||
|
||||
<v-divider></v-divider>
|
||||
|
||||
<v-list-item @click="resetToDefault" :disabled="isDefaultValue">
|
||||
<template v-slot:prepend>
|
||||
<v-icon icon="mdi-restore" size="small" />
|
||||
</template>
|
||||
<v-list-item-title>重置为默认值</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</div>
|
||||
</template>
|
||||
</v-list-item>
|
||||
|
||||
<!-- 文本框显示在下方 -->
|
||||
<div v-if="type === 'string' && !hasOptions" class="px-4 pb-2 pt-0">
|
||||
<v-text-field v-model="localValue" density="compact" hide-details :disabled="disabled"
|
||||
class="setting-text-field mt-1" variant="outlined" bg-color="surface" @update:model-value="updateSetting" />
|
||||
</div>
|
||||
|
||||
<!-- 消息提示 -->
|
||||
<v-snackbar v-model="showSnackbar" :timeout="2000" color="success" location="top">
|
||||
{{ snackbarText }}
|
||||
</v-snackbar>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getSetting, setSetting, getSettingDefinition, resetSetting } from '@/utils/settings';
|
||||
|
||||
export default {
|
||||
name: 'SettingItem',
|
||||
|
||||
props: {
|
||||
/**
|
||||
* 设置项的键名,例如 'display.cardHoverEffect'
|
||||
*/
|
||||
settingKey: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
|
||||
/**
|
||||
* 可选的自定义图标,如果不提供则使用默认图标
|
||||
*/
|
||||
icon: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
|
||||
/**
|
||||
* 是否禁用此设置项
|
||||
*/
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
|
||||
/**
|
||||
* 可选的自定义标题,如果不提供则使用定义中的描述或键名
|
||||
*/
|
||||
title: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
|
||||
/**
|
||||
* 可选的自定义描述,如果不提供则不显示描述
|
||||
*/
|
||||
description: {
|
||||
type: String,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
localValue: null,
|
||||
definition: null,
|
||||
type: null,
|
||||
selectOptions: [],
|
||||
hasOptions: false,
|
||||
minValue: 0,
|
||||
maxValue: 100,
|
||||
stepValue: 1,
|
||||
showSnackbar: false,
|
||||
snackbarText: '',
|
||||
fontFamilies: [
|
||||
{ title: 'Arial', value: 'Arial, sans-serif' },
|
||||
{ title: 'Calibri', value: 'Calibri, sans-serif' },
|
||||
{ title: 'Cambria', value: 'Cambria, serif' },
|
||||
{ title: 'Consolas', value: 'Consolas, monospace' },
|
||||
{ title: 'Courier New', value: 'Courier New, monospace' },
|
||||
{ title: 'Georgia', value: 'Georgia, serif' },
|
||||
{ title: 'Helvetica', value: 'Helvetica, sans-serif' },
|
||||
{ title: 'Segoe UI', value: 'Segoe UI, sans-serif' },
|
||||
{ title: 'Times New Roman', value: 'Times New Roman, serif' },
|
||||
{ title: 'Trebuchet MS', value: 'Trebuchet MS, sans-serif' },
|
||||
{ title: 'Verdana', value: 'Verdana, sans-serif' },
|
||||
{ title: 'Monospace', value: 'monospace' },
|
||||
{ title: 'Sans-serif', value: 'sans-serif' },
|
||||
{ title: 'Serif', value: 'serif' }
|
||||
],
|
||||
// 设置项的显示名称映射
|
||||
displayValueMappings: {
|
||||
'display.emptySubjectDisplay': {
|
||||
'card': '卡片',
|
||||
'button': '按钮'
|
||||
},
|
||||
'theme.mode': {
|
||||
'light': '浅色',
|
||||
'dark': '深色'
|
||||
},
|
||||
'server.provider': {
|
||||
'server': '远程服务器',
|
||||
'indexedDB': '本地存储'
|
||||
}
|
||||
},
|
||||
// 默认图标映射,按设置类型
|
||||
defaultIcons: {
|
||||
'boolean': 'mdi-toggle-switch-outline',
|
||||
'number': 'mdi-numeric',
|
||||
'string': 'mdi-form-textbox'
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
displayTitle() {
|
||||
// 优先使用自定义标题
|
||||
if (this.title) {
|
||||
return this.title;
|
||||
}
|
||||
|
||||
// 其次使用定义中的描述
|
||||
if (this.definition && this.definition.description) {
|
||||
return this.definition.description;
|
||||
}
|
||||
|
||||
// 最后使用键名的最后一部分
|
||||
const parts = this.settingKey.split('.');
|
||||
return parts[parts.length - 1];
|
||||
},
|
||||
|
||||
displayDescription() {
|
||||
// 优先使用自定义描述
|
||||
if (this.description) {
|
||||
return this.description;
|
||||
}
|
||||
|
||||
// 否则使用设置键名
|
||||
return this.settingKey;
|
||||
},
|
||||
|
||||
// 判断是否为字体系列设置
|
||||
isFontFamily() {
|
||||
return this.settingKey.toLowerCase().includes('fontfamily') ||
|
||||
this.settingKey.toLowerCase().includes('font.family');
|
||||
},
|
||||
|
||||
// 判断当前值是否为默认值
|
||||
isDefaultValue() {
|
||||
if (!this.definition) return true;
|
||||
|
||||
// 对于对象或数组,需要进行深度比较
|
||||
if (typeof this.localValue === 'object' && this.localValue !== null) {
|
||||
return JSON.stringify(this.localValue) === JSON.stringify(this.definition.default);
|
||||
}
|
||||
|
||||
return this.localValue === this.definition.default;
|
||||
},
|
||||
|
||||
// 获取设置项的图标
|
||||
settingIcon() {
|
||||
// 优先使用props中传入的图标
|
||||
if (this.icon) {
|
||||
return this.icon;
|
||||
}
|
||||
|
||||
// 其次使用定义中的图标
|
||||
if (this.definition && this.definition.icon) {
|
||||
return this.definition.icon;
|
||||
}
|
||||
|
||||
// 最后使用基于类型的默认图标
|
||||
return this.defaultIcons[this.type] || 'mdi-cog-outline';
|
||||
}
|
||||
},
|
||||
|
||||
created() {
|
||||
this.loadSetting();
|
||||
},
|
||||
|
||||
methods: {
|
||||
loadSetting() {
|
||||
// 获取设置定义
|
||||
this.definition = getSettingDefinition(this.settingKey);
|
||||
|
||||
if (!this.definition) {
|
||||
console.error(`未找到设置项定义: ${this.settingKey}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 设置类型
|
||||
this.type = this.definition.type;
|
||||
|
||||
// 加载设置值
|
||||
this.localValue = getSetting(this.settingKey);
|
||||
|
||||
// 处理选项(对于字符串类型的设置)
|
||||
if (this.type === 'string') {
|
||||
// 检查是否有字体设置
|
||||
if (this.isFontFamily) {
|
||||
this.selectOptions = this.fontFamilies;
|
||||
this.hasOptions = true;
|
||||
}
|
||||
// 检查是否有显示值映射
|
||||
else if (this.settingKey in this.displayValueMappings) {
|
||||
const mapping = this.displayValueMappings[this.settingKey];
|
||||
this.selectOptions = Object.entries(mapping).map(([value, title]) => ({
|
||||
title,
|
||||
value
|
||||
}));
|
||||
this.hasOptions = true;
|
||||
}
|
||||
// 检查是否有验证函数中的选项
|
||||
else if (this.definition.validate) {
|
||||
const validateStr = this.definition.validate.toString();
|
||||
const match = validateStr.match(/\[(.*?)\]/);
|
||||
|
||||
if (match) {
|
||||
const optionsStr = match[1];
|
||||
const options = optionsStr.split(',').map(opt => {
|
||||
const cleaned = opt.trim().replace(/['"]/g, '');
|
||||
// 检查是否有显示值映射
|
||||
const displayValue = this.getDisplayValue(cleaned);
|
||||
return {
|
||||
title: displayValue || cleaned,
|
||||
value: cleaned
|
||||
};
|
||||
});
|
||||
|
||||
if (options.length > 0) {
|
||||
this.selectOptions = options;
|
||||
this.hasOptions = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 处理数字类型的设置
|
||||
if (this.type === 'number' && this.definition.validate) {
|
||||
const validateStr = this.definition.validate.toString();
|
||||
|
||||
// 尝试提取最小值
|
||||
const minMatch = validateStr.match(/value\s*>=\s*(\d+)/);
|
||||
if (minMatch) {
|
||||
this.minValue = Number(minMatch[1]);
|
||||
}
|
||||
|
||||
// 尝试提取最大值
|
||||
const maxMatch = validateStr.match(/value\s*<=\s*(\d+)/);
|
||||
if (maxMatch) {
|
||||
this.maxValue = Number(maxMatch[1]);
|
||||
}
|
||||
|
||||
// 设置合理的步长
|
||||
const range = this.maxValue - this.minValue;
|
||||
if (range > 100) {
|
||||
this.stepValue = 10;
|
||||
} else if (range > 20) {
|
||||
this.stepValue = 5;
|
||||
} else if (range > 10) {
|
||||
this.stepValue = 2;
|
||||
} else {
|
||||
this.stepValue = 1;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 获取设置值的显示名称
|
||||
getDisplayValue(value) {
|
||||
if (this.settingKey in this.displayValueMappings) {
|
||||
const mapping = this.displayValueMappings[this.settingKey];
|
||||
return mapping[value] || value;
|
||||
}
|
||||
return value;
|
||||
},
|
||||
|
||||
updateSetting(value) {
|
||||
// 确保值的类型正确
|
||||
let typedValue = value;
|
||||
|
||||
if (this.type === 'boolean') {
|
||||
typedValue = Boolean(value);
|
||||
} else if (this.type === 'number') {
|
||||
typedValue = Number(value);
|
||||
|
||||
// 确保值在范围内
|
||||
if (typedValue < this.minValue) typedValue = this.minValue;
|
||||
if (typedValue > this.maxValue) typedValue = this.maxValue;
|
||||
}
|
||||
|
||||
// 保存设置
|
||||
const success = setSetting(this.settingKey, typedValue);
|
||||
|
||||
if (success) {
|
||||
this.$emit('update', this.settingKey, typedValue);
|
||||
} else {
|
||||
// 如果保存失败,恢复原值
|
||||
this.localValue = getSetting(this.settingKey);
|
||||
this.$emit('error', this.settingKey);
|
||||
}
|
||||
},
|
||||
|
||||
adjustValue(amount) {
|
||||
if (this.type !== 'number') return;
|
||||
|
||||
const newValue = this.localValue + amount;
|
||||
|
||||
if (newValue >= this.minValue && newValue <= this.maxValue) {
|
||||
this.localValue = newValue;
|
||||
this.updateSetting(newValue);
|
||||
}
|
||||
},
|
||||
|
||||
// 复制设置ID到剪贴板
|
||||
copySettingId() {
|
||||
navigator.clipboard.writeText(this.settingKey)
|
||||
.then(() => {
|
||||
this.showSnackbarMessage('设置ID已复制到剪贴板');
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('复制失败:', err);
|
||||
});
|
||||
},
|
||||
|
||||
// 复制设置值到剪贴板
|
||||
copySettingValue() {
|
||||
let valueText = '';
|
||||
|
||||
if (typeof this.localValue === 'object' && this.localValue !== null) {
|
||||
valueText = JSON.stringify(this.localValue);
|
||||
} else {
|
||||
valueText = String(this.localValue);
|
||||
}
|
||||
|
||||
navigator.clipboard.writeText(valueText)
|
||||
.then(() => {
|
||||
this.showSnackbarMessage('设置值已复制到剪贴板');
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('复制失败:', err);
|
||||
});
|
||||
},
|
||||
|
||||
// 重置为默认值
|
||||
resetToDefault() {
|
||||
if (!this.definition) return;
|
||||
|
||||
resetSetting(this.settingKey);
|
||||
this.localValue = getSetting(this.settingKey);
|
||||
this.showSnackbarMessage('已重置为默认值');
|
||||
this.$emit('update', this.settingKey, this.localValue);
|
||||
},
|
||||
|
||||
// 显示消息提示
|
||||
showSnackbarMessage(message) {
|
||||
this.snackbarText = message;
|
||||
this.showSnackbar = true;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.setting-item {
|
||||
border-radius: 8px;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.setting-text-field,
|
||||
.setting-select,
|
||||
.setting-number-field {
|
||||
min-width: 180px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.text-wrap {
|
||||
white-space: normal;
|
||||
overflow-wrap: break-word;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.setting-item {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,148 @@
|
||||
<template>
|
||||
<div class="settings-explorer">
|
||||
|
||||
|
||||
<div >
|
||||
<v-text-field v-model="searchQuery" label="搜索设置" prepend-inner-icon="mdi-magnify" clearable variant="outlined"
|
||||
density="comfortable" class="mb-4" />
|
||||
|
||||
|
||||
|
||||
<v-list>
|
||||
<div v-for="setting in allSettings" :key="setting.key">
|
||||
<setting-item :key="setting.key" :setting-key="setting.key"
|
||||
:disabled="setting.requireDeveloper && !isDeveloperMode" @update="onSettingUpdate" @error="onSettingError" />
|
||||
<v-divider class="my-2" />
|
||||
</div>
|
||||
</v-list><v-card border>
|
||||
<v-card-title class="text-subtitle-1">当前配置</v-card-title>
|
||||
<v-card-text>
|
||||
<pre class="settings-json">{{ formattedSettings }}</pre>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn @click="copySettingsToClipboard">
|
||||
复制到剪贴板
|
||||
<v-icon right>mdi-content-copy</v-icon>
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getSetting, settingsDefinitions, exportSettingsAsKeyValue, watchSettings } from '@/utils/settings';
|
||||
import SettingItem from './SettingItem.vue';
|
||||
|
||||
export default {
|
||||
name: 'SettingsExplorer',
|
||||
|
||||
components: {
|
||||
SettingItem
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
searchQuery: '',
|
||||
currentSettings: {},
|
||||
unwatchFunction: null,
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
isDeveloperMode() {
|
||||
return getSetting('developer.enabled');
|
||||
},
|
||||
|
||||
allSettings() {
|
||||
const settings = [];
|
||||
|
||||
for (const [key, definition] of Object.entries(settingsDefinitions)) {
|
||||
// 如果有搜索查询,过滤结果
|
||||
if (this.searchQuery && !key.toLowerCase().includes(this.searchQuery.toLowerCase()) &&
|
||||
!definition.description?.toLowerCase().includes(this.searchQuery.toLowerCase())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
settings.push({
|
||||
key,
|
||||
...definition
|
||||
});
|
||||
}
|
||||
|
||||
return settings;
|
||||
},
|
||||
|
||||
formattedSettings() {
|
||||
return JSON.stringify(this.currentSettings, null, 2);
|
||||
}
|
||||
},
|
||||
|
||||
created() {
|
||||
// 初始化当前设置
|
||||
this.updateCurrentSettings();
|
||||
|
||||
// 监听设置变化
|
||||
this.unwatchFunction = watchSettings(() => {
|
||||
this.updateCurrentSettings();
|
||||
});
|
||||
},
|
||||
|
||||
beforeUnmount() {
|
||||
// 组件销毁前取消监听
|
||||
if (this.unwatchFunction) {
|
||||
this.unwatchFunction();
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
updateCurrentSettings() {
|
||||
this.currentSettings = exportSettingsAsKeyValue();
|
||||
},
|
||||
|
||||
onSettingUpdate(key, value) {
|
||||
this.$emit('update', key, value);
|
||||
// 设置更新后立即更新当前设置显示
|
||||
this.updateCurrentSettings();
|
||||
},
|
||||
|
||||
onSettingError(key) {
|
||||
this.$emit('error', key);
|
||||
},
|
||||
|
||||
copySettingsToClipboard() {
|
||||
navigator.clipboard.writeText(JSON.stringify(this.currentSettings))
|
||||
.then(() => {
|
||||
// 可以添加一个提示,表示复制成功
|
||||
this.$emit('message', { type: 'success', text: '设置已复制到剪贴板' });
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('复制到剪贴板失败:', err);
|
||||
this.$emit('message', { type: 'error', text: '复制到剪贴板失败' });
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.settings-explorer {
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.settings-json {
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
padding: 12px;
|
||||
border-radius: 4px;
|
||||
overflow-x: auto;
|
||||
font-family: monospace;
|
||||
white-space: pre-wrap;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.v-theme--dark .settings-json {
|
||||
background-color: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
</style>
|
||||
@@ -144,7 +144,7 @@ export default {
|
||||
|
||||
async clearIndexedDB() {
|
||||
try {
|
||||
const DBName = "HomeworkDB";
|
||||
const DBName = "ClassworksDB";
|
||||
// 删除整个数据库
|
||||
await window.indexedDB.deleteDatabase(DBName);
|
||||
this.$message.success("清除成功", "数据库缓存已清除");
|
||||
@@ -156,7 +156,7 @@ export default {
|
||||
|
||||
async exportData() {
|
||||
try {
|
||||
const DBName = "HomeworkDB";
|
||||
const DBName = "ClassworksDB";
|
||||
const data = { indexedDB: {} };
|
||||
|
||||
// 打开数据库
|
||||
|
||||
@@ -1,260 +1,46 @@
|
||||
<template>
|
||||
<settings-card
|
||||
title="显示设置"
|
||||
icon="mdi-monitor"
|
||||
border
|
||||
>
|
||||
<v-form v-model="isValid" @submit.prevent="save">
|
||||
<settings-card title="显示设置" icon="mdi-monitor" border>
|
||||
<v-list>
|
||||
<v-list-item>
|
||||
<template #prepend>
|
||||
<v-icon icon="mdi-card-outline" class="mr-3" />
|
||||
</template>
|
||||
<v-list-item-title>空科目显示方式</v-list-item-title>
|
||||
<v-list-item-subtitle>选择空科目的显示方式</v-list-item-subtitle>
|
||||
<template #append>
|
||||
<v-select
|
||||
v-model="emptySubjectDisplay"
|
||||
:items="displayOptions"
|
||||
density="compact"
|
||||
hide-details
|
||||
variant="outlined"
|
||||
style="max-width: 150px"
|
||||
/>
|
||||
</template>
|
||||
</v-list-item>
|
||||
<setting-item :setting-key="'display.emptySubjectDisplay'" />
|
||||
|
||||
|
||||
<v-divider class="my-2" />
|
||||
|
||||
<v-list-item>
|
||||
<template #prepend>
|
||||
<v-icon icon="mdi-sort-variant" class="mr-3" />
|
||||
</template>
|
||||
<v-list-item-title>动态排序</v-list-item-title>
|
||||
<v-list-item-subtitle>优化卡片布局以提高显示效果</v-list-item-subtitle>
|
||||
<template #append>
|
||||
<v-switch
|
||||
v-model="dynamicSort"
|
||||
density="comfortable"
|
||||
hide-details
|
||||
/>
|
||||
</template>
|
||||
</v-list-item>
|
||||
<setting-item :setting-key="'display.dynamicSort'" />
|
||||
|
||||
<v-divider class="my-2" />
|
||||
|
||||
<v-list-item>
|
||||
<template #prepend>
|
||||
<v-icon icon="mdi-dice-multiple" class="mr-3" />
|
||||
</template>
|
||||
<v-list-item-title>显示随机按钮</v-list-item-title>
|
||||
<v-list-item-subtitle>在主页显示随机点名按钮</v-list-item-subtitle>
|
||||
<template #append>
|
||||
<v-switch
|
||||
v-model="showRandomButton"
|
||||
density="comfortable"
|
||||
hide-details
|
||||
/>
|
||||
</template>
|
||||
</v-list-item>
|
||||
<setting-item :setting-key="'display.showRandomButton'" />
|
||||
|
||||
<v-divider class="my-2" />
|
||||
|
||||
<v-list-item>
|
||||
<template #prepend>
|
||||
<v-icon icon="mdi-fullscreen" class="mr-3" />
|
||||
</template>
|
||||
<v-list-item-title>显示全屏按钮</v-list-item-title>
|
||||
<v-list-item-subtitle>在主页显示全屏切换按钮</v-list-item-subtitle>
|
||||
<template #append>
|
||||
<v-switch
|
||||
v-model="showFullscreenButton"
|
||||
density="comfortable"
|
||||
hide-details
|
||||
/>
|
||||
</template>
|
||||
</v-list-item>
|
||||
<setting-item :setting-key="'display.showFullscreenButton'" />
|
||||
|
||||
<v-divider class="my-2" />
|
||||
|
||||
<v-list-item>
|
||||
<template #prepend>
|
||||
<v-icon icon="mdi-cards-outline" class="mr-3" />
|
||||
</template>
|
||||
<v-list-item-title>卡片悬浮效果</v-list-item-title>
|
||||
<v-list-item-subtitle>启用卡片悬停时的动画效果</v-list-item-subtitle>
|
||||
<template #append>
|
||||
<v-switch
|
||||
v-model="cardHoverEffect"
|
||||
density="comfortable"
|
||||
hide-details
|
||||
/>
|
||||
</template>
|
||||
</v-list-item>
|
||||
<setting-item :setting-key="'display.cardHoverEffect'" />
|
||||
|
||||
<v-divider class="my-2" />
|
||||
|
||||
<v-list-item>
|
||||
<template #prepend>
|
||||
<v-icon icon="mdi-gesture-tap" class="mr-3" />
|
||||
</template>
|
||||
<v-list-item-title>增强触摸模式</v-list-item-title>
|
||||
<v-list-item-subtitle>优化触摸屏操作体验</v-list-item-subtitle>
|
||||
<template #append>
|
||||
<v-switch
|
||||
v-model="enhancedTouchMode"
|
||||
density="comfortable"
|
||||
hide-details
|
||||
/>
|
||||
</template>
|
||||
</v-list-item>
|
||||
<setting-item :setting-key="'display.enhancedTouchMode'" />
|
||||
|
||||
<v-divider class="my-2" />
|
||||
<setting-item :setting-key="'display.showAntiScreenBurnCard'" />
|
||||
|
||||
|
||||
<v-list-item>
|
||||
<template #prepend>
|
||||
<v-icon icon="mdi-shield-check" class="mr-3" />
|
||||
</template>
|
||||
<v-list-item-title>显示防烧屏提示</v-list-item-title>
|
||||
<v-list-item-subtitle>显示防烧屏技术提示卡片</v-list-item-subtitle>
|
||||
<template #append>
|
||||
<v-switch
|
||||
v-model="showAntiScreenBurnCard"
|
||||
density="comfortable"
|
||||
hide-details
|
||||
/>
|
||||
</template>
|
||||
</v-list-item>
|
||||
|
||||
<div class="d-flex gap-2 mt-4">
|
||||
<v-btn
|
||||
color="primary"
|
||||
type="submit"
|
||||
:disabled="!hasChanges || !isValid"
|
||||
prepend-icon="mdi-content-save"
|
||||
>
|
||||
保存更改
|
||||
</v-btn>
|
||||
<v-btn
|
||||
variant="outlined"
|
||||
@click="reset"
|
||||
:disabled="!hasChanges"
|
||||
>
|
||||
重置
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-list>
|
||||
</v-form>
|
||||
</settings-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import SettingsCard from '@/components/SettingsCard.vue';
|
||||
import { getSetting, setSetting } from '@/utils/settings';
|
||||
|
||||
import SettingItem from '@/components/settings/SettingItem.vue';
|
||||
export default {
|
||||
name: 'DisplaySettingsCard',
|
||||
components: { SettingsCard },
|
||||
components: { SettingsCard, SettingItem },
|
||||
data() {
|
||||
const settings = {
|
||||
emptySubjectDisplay: getSetting('display.emptySubjectDisplay'),
|
||||
dynamicSort: getSetting('display.dynamicSort'),
|
||||
showRandomButton: getSetting('display.showRandomButton'),
|
||||
showFullscreenButton: getSetting('display.showFullscreenButton'),
|
||||
cardHoverEffect: getSetting('display.cardHoverEffect'),
|
||||
enhancedTouchMode: getSetting('display.enhancedTouchMode'),
|
||||
showAntiScreenBurnCard: getSetting('display.showAntiScreenBurnCard')
|
||||
};
|
||||
|
||||
|
||||
return {
|
||||
localSettings: { ...settings },
|
||||
originalSettings: settings,
|
||||
isValid: true,
|
||||
displayOptions: [
|
||||
{ title: '卡片', value: 'card' },
|
||||
{ title: '按钮', value: 'button' }
|
||||
]
|
||||
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
hasChanges() {
|
||||
return JSON.stringify(this.localSettings) !== JSON.stringify(this.originalSettings);
|
||||
},
|
||||
emptySubjectDisplay: {
|
||||
get() {
|
||||
return this.localSettings.emptySubjectDisplay;
|
||||
},
|
||||
set(value) {
|
||||
this.localSettings.emptySubjectDisplay = value;
|
||||
this.$emit('saved');
|
||||
}
|
||||
},
|
||||
dynamicSort: {
|
||||
get() {
|
||||
return this.localSettings.dynamicSort;
|
||||
},
|
||||
set(value) {
|
||||
this.localSettings.dynamicSort = value;
|
||||
this.$emit('saved');
|
||||
}
|
||||
},
|
||||
showRandomButton: {
|
||||
get() {
|
||||
return this.localSettings.showRandomButton;
|
||||
},
|
||||
set(value) {
|
||||
this.localSettings.showRandomButton = value;
|
||||
this.$emit('saved');
|
||||
}
|
||||
},
|
||||
showFullscreenButton: {
|
||||
get() {
|
||||
return this.localSettings.showFullscreenButton;
|
||||
},
|
||||
set(value) {
|
||||
this.localSettings.showFullscreenButton = value;
|
||||
this.$emit('saved');
|
||||
}
|
||||
},
|
||||
cardHoverEffect: {
|
||||
get() {
|
||||
return this.localSettings.cardHoverEffect;
|
||||
},
|
||||
set(value) {
|
||||
this.localSettings.cardHoverEffect = value;
|
||||
this.$emit('saved');
|
||||
}
|
||||
},
|
||||
enhancedTouchMode: {
|
||||
get() {
|
||||
return this.localSettings.enhancedTouchMode;
|
||||
},
|
||||
set(value) {
|
||||
this.localSettings.enhancedTouchMode = value;
|
||||
this.$emit('saved');
|
||||
}
|
||||
},
|
||||
showAntiScreenBurnCard: {
|
||||
get() {
|
||||
return this.localSettings.showAntiScreenBurnCard;
|
||||
},
|
||||
set(value) {
|
||||
this.localSettings.showAntiScreenBurnCard = value;
|
||||
this.$emit('saved');
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
save() {
|
||||
Object.entries(this.localSettings).forEach(([key, value]) => {
|
||||
setSetting(`display.${key}`, value);
|
||||
});
|
||||
this.originalSettings = { ...this.localSettings };
|
||||
this.$emit('saved');
|
||||
},
|
||||
reset() {
|
||||
this.localSettings = { ...this.originalSettings };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,135 +1,27 @@
|
||||
<template>
|
||||
<settings-card
|
||||
title="编辑设置"
|
||||
icon="mdi-pencil-cog"
|
||||
>
|
||||
<v-form v-model="isValid" @submit.prevent="save">
|
||||
<v-list>
|
||||
<v-list-item>
|
||||
<template #prepend>
|
||||
<v-icon icon="mdi-content-save" class="mr-3" />
|
||||
</template>
|
||||
<v-list-item-title>自动保存</v-list-item-title>
|
||||
<v-list-item-subtitle>在编辑完成后自动保存到服务器</v-list-item-subtitle>
|
||||
<template #append>
|
||||
<v-switch
|
||||
v-model="localSettings.autoSave"
|
||||
density="comfortable"
|
||||
hide-details
|
||||
/>
|
||||
</template>
|
||||
</v-list-item>
|
||||
<settings-card title="编辑设置" icon="mdi-cog">
|
||||
<v-list>
|
||||
<setting-item :setting-key="'edit.autoSave'" />
|
||||
|
||||
<v-divider v-if="localSettings.autoSave" class="my-2" />
|
||||
|
||||
<v-list-item v-if="localSettings.autoSave">
|
||||
<template #prepend>
|
||||
<v-icon icon="mdi-calendar-lock" class="mr-3" />
|
||||
</template>
|
||||
<v-list-item-title>禁止自动保存非当天数据</v-list-item-title>
|
||||
<v-list-item-subtitle>仅允许自动保存当天的数据,避免误修改历史记录</v-list-item-subtitle>
|
||||
<template #append>
|
||||
<v-switch
|
||||
v-model="localSettings.blockNonTodayAutoSave"
|
||||
density="comfortable"
|
||||
hide-details
|
||||
/>
|
||||
</template>
|
||||
</v-list-item>
|
||||
<v-divider class="my-2" />
|
||||
<setting-item :setting-key="'edit.blockNonTodayAutoSave'" />
|
||||
|
||||
<v-divider class="my-2" />
|
||||
|
||||
<v-list-item>
|
||||
<template #prepend>
|
||||
<v-icon icon="mdi-calendar-alert" class="mr-3" />
|
||||
</template>
|
||||
<v-list-item-title>确认保存历史数据</v-list-item-title>
|
||||
<v-list-item-subtitle>保存非当天数据时显示确认对话框</v-list-item-subtitle>
|
||||
<template #append>
|
||||
<v-switch
|
||||
v-model="localSettings.confirmNonTodaySave"
|
||||
density="comfortable"
|
||||
hide-details
|
||||
/>
|
||||
</template>
|
||||
</v-list-item>
|
||||
<v-divider class="my-2" />
|
||||
<setting-item :setting-key="'edit.confirmNonTodaySave'" />
|
||||
|
||||
<v-divider class="my-2" />
|
||||
|
||||
<v-list-item>
|
||||
<template #prepend>
|
||||
<v-icon icon="mdi-refresh" class="mr-3" />
|
||||
</template>
|
||||
<v-list-item-title>编辑前刷新</v-list-item-title>
|
||||
<v-list-item-subtitle>在打开编辑框前从服务器获取最新数据</v-list-item-subtitle>
|
||||
<template #append>
|
||||
<v-switch
|
||||
v-model="localSettings.refreshBeforeEdit"
|
||||
density="comfortable"
|
||||
hide-details
|
||||
/>
|
||||
</template>
|
||||
</v-list-item>
|
||||
|
||||
<div class="d-flex gap-2 mt-4">
|
||||
<v-btn
|
||||
color="primary"
|
||||
type="submit"
|
||||
:disabled="!hasChanges || !isValid"
|
||||
prepend-icon="mdi-content-save"
|
||||
>
|
||||
保存更改
|
||||
</v-btn>
|
||||
<v-btn
|
||||
variant="outlined"
|
||||
@click="reset"
|
||||
:disabled="!hasChanges"
|
||||
>
|
||||
重置
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-list>
|
||||
</v-form>
|
||||
<v-divider class="my-2" />
|
||||
<setting-item :setting-key="'edit.refreshBeforeEdit'" />
|
||||
|
||||
</v-list>
|
||||
</settings-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import SettingsCard from '@/components/SettingsCard.vue';
|
||||
import { getSetting, setSetting } from '@/utils/settings';
|
||||
import SettingItem from '../SettingItem.vue';
|
||||
|
||||
export default {
|
||||
name: 'EditSettingsCard',
|
||||
components: { SettingsCard },
|
||||
data() {
|
||||
const settings = {
|
||||
autoSave: getSetting('edit.autoSave'),
|
||||
blockNonTodayAutoSave: getSetting('edit.blockNonTodayAutoSave'),
|
||||
confirmNonTodaySave: getSetting('edit.confirmNonTodaySave'),
|
||||
refreshBeforeEdit: getSetting('edit.refreshBeforeEdit')
|
||||
};
|
||||
|
||||
return {
|
||||
localSettings: { ...settings },
|
||||
originalSettings: settings,
|
||||
isValid: true
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
hasChanges() {
|
||||
return JSON.stringify(this.localSettings) !== JSON.stringify(this.originalSettings);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
save() {
|
||||
Object.entries(this.localSettings).forEach(([key, value]) => {
|
||||
setSetting(`edit.${key}`, value);
|
||||
});
|
||||
this.originalSettings = { ...this.localSettings };
|
||||
this.$emit('saved');
|
||||
},
|
||||
reset() {
|
||||
this.localSettings = { ...this.originalSettings };
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
<template>
|
||||
<v-card border class="mb-4">
|
||||
<v-card-title class="d-flex align-center">
|
||||
<v-icon icon="mdi-account-question" class="mr-2" />
|
||||
随机点名设置
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text>
|
||||
<v-switch
|
||||
v-model="randomPickerEnabled"
|
||||
label="启用随机点名功能"
|
||||
color="primary"
|
||||
hide-details
|
||||
class="mb-4"
|
||||
/>
|
||||
|
||||
<v-switch
|
||||
v-model="randomPickerAnimation"
|
||||
label="启用随机点名动画效果"
|
||||
color="primary"
|
||||
hide-details
|
||||
class="mb-4"
|
||||
:disabled="!randomPickerEnabled"
|
||||
/>
|
||||
|
||||
<v-slider
|
||||
v-model="defaultCount"
|
||||
label="默认抽取人数"
|
||||
min="1"
|
||||
max="10"
|
||||
step="1"
|
||||
thumb-label
|
||||
:disabled="!randomPickerEnabled"
|
||||
class="mb-4"
|
||||
/>
|
||||
|
||||
<v-divider class="my-4" />
|
||||
|
||||
<div class="text-subtitle-1 mb-2">学生过滤设置</div>
|
||||
|
||||
<v-switch
|
||||
v-model="excludeAbsent"
|
||||
label="排除请假学生"
|
||||
color="primary"
|
||||
hide-details
|
||||
class="mb-2"
|
||||
:disabled="!randomPickerEnabled"
|
||||
/>
|
||||
|
||||
<v-switch
|
||||
v-model="excludeLate"
|
||||
label="排除迟到学生"
|
||||
color="primary"
|
||||
hide-details
|
||||
class="mb-2"
|
||||
:disabled="!randomPickerEnabled"
|
||||
/>
|
||||
|
||||
<v-switch
|
||||
v-model="excludeExcluded"
|
||||
label="排除不参与学生"
|
||||
color="primary"
|
||||
hide-details
|
||||
class="mb-2"
|
||||
:disabled="!randomPickerEnabled"
|
||||
/>
|
||||
|
||||
<v-alert
|
||||
v-if="randomPickerEnabled"
|
||||
type="info"
|
||||
variant="tonal"
|
||||
class="mt-4"
|
||||
density="compact"
|
||||
>
|
||||
随机点名功能将在主页显示一个按钮,点击后可以随机抽取学生。
|
||||
</v-alert>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getSetting, setSetting } from '@/utils/settings';
|
||||
|
||||
export default {
|
||||
name: 'RandomPickerSettingsCard',
|
||||
data() {
|
||||
return {
|
||||
randomPickerEnabled: getSetting('randomPicker.enabled'),
|
||||
randomPickerAnimation: getSetting('randomPicker.animation'),
|
||||
defaultCount: getSetting('randomPicker.defaultCount'),
|
||||
excludeAbsent: getSetting('randomPicker.excludeAbsent'),
|
||||
excludeLate: getSetting('randomPicker.excludeLate'),
|
||||
excludeExcluded: getSetting('randomPicker.excludeExcluded')
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
randomPickerEnabled(newValue) {
|
||||
setSetting('randomPicker.enabled', newValue);
|
||||
},
|
||||
randomPickerAnimation(newValue) {
|
||||
setSetting('randomPicker.animation', newValue);
|
||||
},
|
||||
defaultCount(newValue) {
|
||||
setSetting('randomPicker.defaultCount', newValue);
|
||||
},
|
||||
excludeAbsent(newValue) {
|
||||
setSetting('randomPicker.excludeAbsent', newValue);
|
||||
},
|
||||
excludeLate(newValue) {
|
||||
setSetting('randomPicker.excludeLate', newValue);
|
||||
},
|
||||
excludeExcluded(newValue) {
|
||||
setSetting('randomPicker.excludeExcluded', newValue);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -1,102 +1,28 @@
|
||||
<template>
|
||||
<settings-card
|
||||
title="刷新设置"
|
||||
icon="mdi-refresh-circle"
|
||||
>
|
||||
<v-form v-model="isValid" @submit.prevent="save">
|
||||
<settings-card title="刷新设置" icon="mdi-refresh-circle">
|
||||
<v-form>
|
||||
<v-list>
|
||||
<v-list-item>
|
||||
<template #prepend>
|
||||
<v-icon icon="mdi-refresh" class="mr-3" />
|
||||
</template>
|
||||
<v-list-item-title>自动刷新</v-list-item-title>
|
||||
<v-list-item-subtitle>在后台自动刷新数据</v-list-item-subtitle>
|
||||
<template #append>
|
||||
<v-switch
|
||||
v-model="localSettings.auto"
|
||||
density="comfortable"
|
||||
hide-details
|
||||
/>
|
||||
</template>
|
||||
</v-list-item>
|
||||
<setting-item setting-key="refresh.auto" title="自动刷新" /> <v-divider class="my-2" />
|
||||
|
||||
<v-divider class="my-2" />
|
||||
|
||||
<v-list-item>
|
||||
<template #prepend>
|
||||
<v-icon icon="mdi-timer" class="mr-3" />
|
||||
</template>
|
||||
<v-list-item-title>刷新间隔</v-list-item-title>
|
||||
<v-list-item-subtitle>设置自动刷新的时间间隔(分钟)</v-list-item-subtitle>
|
||||
<template #append>
|
||||
<v-text-field
|
||||
v-model="localSettings.interval"
|
||||
type="number"
|
||||
min="1"
|
||||
max="60"
|
||||
density="comfortable"
|
||||
hide-details
|
||||
/>
|
||||
</template>
|
||||
</v-list-item>
|
||||
|
||||
<div class="d-flex gap-2 mt-4">
|
||||
<v-btn
|
||||
color="primary"
|
||||
type="submit"
|
||||
:disabled="!hasChanges || !isValid"
|
||||
prepend-icon="mdi-content-save"
|
||||
>
|
||||
保存更改
|
||||
</v-btn>
|
||||
<v-btn
|
||||
variant="outlined"
|
||||
@click="reset"
|
||||
:disabled="!hasChanges"
|
||||
>
|
||||
重置
|
||||
</v-btn>
|
||||
</div>
|
||||
<setting-item setting-key="refresh.interval" title="刷新间隔" />
|
||||
</v-list>
|
||||
|
||||
</v-form>
|
||||
</settings-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import SettingsCard from '@/components/SettingsCard.vue';
|
||||
import { getSetting, setSetting } from '@/utils/settings';
|
||||
|
||||
import SettingItem from '@/components/settings/SettingItem.vue';
|
||||
export default {
|
||||
name: 'RefreshSettingsCard',
|
||||
components: { SettingsCard },
|
||||
data() {
|
||||
const settings = {
|
||||
auto: getSetting('refresh.auto'),
|
||||
interval: getSetting('refresh.interval')
|
||||
};
|
||||
|
||||
|
||||
return {
|
||||
localSettings: { ...settings },
|
||||
originalSettings: settings,
|
||||
isValid: true
|
||||
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
hasChanges() {
|
||||
return JSON.stringify(this.localSettings) !== JSON.stringify(this.originalSettings);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
save() {
|
||||
Object.entries(this.localSettings).forEach(([key, value]) => {
|
||||
setSetting(`refresh.${key}`, value);
|
||||
});
|
||||
this.originalSettings = { ...this.localSettings };
|
||||
this.$emit('saved');
|
||||
},
|
||||
reset() {
|
||||
this.localSettings = { ...this.originalSettings };
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,52 +1,19 @@
|
||||
<template>
|
||||
<settings-card title="数据源设置" icon="mdi-database" :loading="loading">
|
||||
<v-form v-model="isValid" @submit.prevent="save">
|
||||
<v-select
|
||||
v-model="localSettings.provider"
|
||||
:items="dataProviders"
|
||||
label="数据提供者"
|
||||
class="mb-4"
|
||||
/>
|
||||
<v-expand-transition>
|
||||
<div v-if="localSettings.provider === 'server'">
|
||||
<v-text-field
|
||||
v-model="localSettings.domain"
|
||||
label="服务器域名"
|
||||
placeholder="例如: http://example.com"
|
||||
prepend-inner-icon="mdi-web"
|
||||
/>
|
||||
</div>
|
||||
</v-expand-transition>
|
||||
<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-text-field
|
||||
v-model="localSettings.classNumber"
|
||||
label="班号"
|
||||
placeholder="例如: 1 或 A"
|
||||
prepend-inner-icon="mdi-account-group"
|
||||
persistent-hint
|
||||
/>
|
||||
|
||||
<div class="d-flex gap-2 mt-4">
|
||||
<v-btn
|
||||
color="primary"
|
||||
type="submit"
|
||||
:disabled="!hasChanges || !isValid"
|
||||
prepend-icon="mdi-content-save"
|
||||
>
|
||||
保存更改
|
||||
</v-btn>
|
||||
<v-btn variant="outlined" @click="reset" :disabled="!hasChanges">
|
||||
重置
|
||||
</v-btn>
|
||||
</div>
|
||||
<setting-item setting-key="server.classNumber" title="班号" />
|
||||
|
||||
</v-form>
|
||||
</settings-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import SettingsCard from "@/components/SettingsCard.vue";
|
||||
import { getSetting, setSetting } from "@/utils/settings";
|
||||
|
||||
import SettingItem from "@/components/settings/SettingItem.vue";
|
||||
export default {
|
||||
name: "ServerSettingsCard",
|
||||
components: { SettingsCard },
|
||||
@@ -54,68 +21,12 @@ export default {
|
||||
loading: Boolean,
|
||||
},
|
||||
data() {
|
||||
const settings = {
|
||||
provider: getSetting("server.provider"),
|
||||
domain: getSetting("server.domain"),
|
||||
classNumber: getSetting("server.classNumber"),
|
||||
};
|
||||
|
||||
|
||||
return {
|
||||
localSettings: { ...settings },
|
||||
originalSettings: settings,
|
||||
isValid: true,
|
||||
dataProviders: [
|
||||
{ title: "服务器", value: "server" },
|
||||
{ title: "本地数据库", value: "indexedDB" },
|
||||
],
|
||||
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
hasChanges() {
|
||||
return (
|
||||
JSON.stringify(this.localSettings) !==
|
||||
JSON.stringify(this.originalSettings)
|
||||
);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async save() {
|
||||
try {
|
||||
// 先保存设置
|
||||
Object.entries(this.localSettings).forEach(([key, value]) => {
|
||||
const success = setSetting(`server.${key}`, value);
|
||||
if (!success) {
|
||||
throw new Error(`保存设置 ${key} 失败`);
|
||||
}
|
||||
});
|
||||
|
||||
// 如果切换到服务器模式,验证服务器连接
|
||||
if (this.localSettings.provider === 'server' && this.localSettings.domain) {
|
||||
const testUrl = `${this.localSettings.domain.replace(/\/$/, '')}/api/test`;
|
||||
try {
|
||||
const response = await fetch(testUrl);
|
||||
if (!response.ok) {
|
||||
throw new Error('服务器连接测试失败');
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error('无法连接到服务器,请检查域名设置');
|
||||
}
|
||||
}
|
||||
|
||||
this.originalSettings = { ...this.localSettings };
|
||||
this.$emit('saved');
|
||||
|
||||
// 延迟刷新页面,让用户看到保存成功的提示
|
||||
setTimeout(() => {
|
||||
//window.location.reload();
|
||||
}, 1000);
|
||||
} catch (error) {
|
||||
this.$message?.error('保存失败', error.message);
|
||||
}
|
||||
},
|
||||
reset() {
|
||||
this.localSettings = { ...this.originalSettings };
|
||||
},
|
||||
},
|
||||
|
||||
};
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user