feat: add settings schema

This commit is contained in:
QiuSW
2026-07-09 00:12:30 +08:00
parent 5aaac0b166
commit 91debb9ad6
24 changed files with 867 additions and 53 deletions
@@ -6,6 +6,182 @@
// @ts-ignore: Unused imports
import {Create as $Create} from "@wailsio/runtime";
export class AppSettingDefinition {
/**
* Creates a new AppSettingDefinition instance.
* @param {Partial<AppSettingDefinition>} [$$source = {}] - The source object to create the AppSettingDefinition.
*/
constructor($$source = {}) {
if (!("key" in $$source)) {
/**
* @member
* @type {string}
*/
this["key"] = "";
}
if (!("category" in $$source)) {
/**
* @member
* @type {string}
*/
this["category"] = "";
}
if (!("label" in $$source)) {
/**
* @member
* @type {string}
*/
this["label"] = "";
}
if (!("type" in $$source)) {
/**
* @member
* @type {string}
*/
this["type"] = "";
}
if (!("defaultValue" in $$source)) {
/**
* @member
* @type {string}
*/
this["defaultValue"] = "";
}
if (!("value" in $$source)) {
/**
* @member
* @type {string}
*/
this["value"] = "";
}
if (!("description" in $$source)) {
/**
* @member
* @type {string}
*/
this["description"] = "";
}
if (!("options" in $$source)) {
/**
* @member
* @type {AppSettingOption[]}
*/
this["options"] = [];
}
if (!("readOnly" in $$source)) {
/**
* @member
* @type {boolean}
*/
this["readOnly"] = false;
}
Object.assign(this, $$source);
}
/**
* Creates a new AppSettingDefinition instance from a string or object.
* @param {any} [$$source = {}]
* @returns {AppSettingDefinition}
*/
static createFrom($$source = {}) {
const $$createField7_0 = $$createType1;
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
if ("options" in $$parsedSource) {
$$parsedSource["options"] = $$createField7_0($$parsedSource["options"]);
}
return new AppSettingDefinition(/** @type {Partial<AppSettingDefinition>} */($$parsedSource));
}
}
export class AppSettingGroup {
/**
* Creates a new AppSettingGroup instance.
* @param {Partial<AppSettingGroup>} [$$source = {}] - The source object to create the AppSettingGroup.
*/
constructor($$source = {}) {
if (!("category" in $$source)) {
/**
* @member
* @type {string}
*/
this["category"] = "";
}
if (!("label" in $$source)) {
/**
* @member
* @type {string}
*/
this["label"] = "";
}
if (!("description" in $$source)) {
/**
* @member
* @type {string}
*/
this["description"] = "";
}
if (!("items" in $$source)) {
/**
* @member
* @type {AppSettingDefinition[]}
*/
this["items"] = [];
}
Object.assign(this, $$source);
}
/**
* Creates a new AppSettingGroup instance from a string or object.
* @param {any} [$$source = {}]
* @returns {AppSettingGroup}
*/
static createFrom($$source = {}) {
const $$createField3_0 = $$createType3;
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
if ("items" in $$parsedSource) {
$$parsedSource["items"] = $$createField3_0($$parsedSource["items"]);
}
return new AppSettingGroup(/** @type {Partial<AppSettingGroup>} */($$parsedSource));
}
}
export class AppSettingOption {
/**
* Creates a new AppSettingOption instance.
* @param {Partial<AppSettingOption>} [$$source = {}] - The source object to create the AppSettingOption.
*/
constructor($$source = {}) {
if (!("value" in $$source)) {
/**
* @member
* @type {string}
*/
this["value"] = "";
}
if (!("label" in $$source)) {
/**
* @member
* @type {string}
*/
this["label"] = "";
}
Object.assign(this, $$source);
}
/**
* Creates a new AppSettingOption instance from a string or object.
* @param {any} [$$source = {}]
* @returns {AppSettingOption}
*/
static createFrom($$source = {}) {
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
return new AppSettingOption(/** @type {Partial<AppSettingOption>} */($$parsedSource));
}
}
export class AuthProviderOption {
/**
* Creates a new AuthProviderOption instance.
@@ -176,3 +352,9 @@ export class Hotkey {
return new Hotkey(/** @type {Partial<Hotkey>} */($$parsedSource));
}
}
// Private type creation functions
const $$createType0 = AppSettingOption.createFrom;
const $$createType1 = $Create.Array($$createType0);
const $$createType2 = AppSettingDefinition.createFrom;
const $$createType3 = $Create.Array($$createType2);
@@ -6,6 +6,10 @@
// @ts-ignore: Unused imports
import {Call as $Call, Create as $Create} from "@wailsio/runtime";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports
import * as models$0 from "../models/models.js";
/**
* 根据key获取配置项
* @param {string} key
@@ -25,6 +29,18 @@ export function GetLanguage() {
return $resultPromise;
}
/**
* @returns {Promise<models$0.AppSettingGroup[]> & { cancel(): void }}
*/
export function GetSettingsSchema() {
let $resultPromise = /** @type {any} */($Call.ByID(2109450092));
let $typingPromise = /** @type {any} */($resultPromise.then(($result) => {
return $$createType1($result);
}));
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
return $typingPromise;
}
/**
* 设置或更新配置项
* @param {string} key
@@ -45,3 +61,7 @@ export function SetLanguage(lang) {
let $resultPromise = /** @type {any} */($Call.ByID(538150432, lang));
return $resultPromise;
}
// Private type creation functions
const $$createType0 = models$0.AppSettingGroup.createFrom;
const $$createType1 = $Create.Array($$createType0);
@@ -1,18 +1,22 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { setTheme, getCurrentTheme, ThemeMode } from '../../utils/ThemeManager'
import { GetAppConfig, SetAppConfig } from '../../../bindings/cmbone/internal/services/appconfigservice'
import { setTheme, getCurrentTheme, type ThemeMode } from '../../utils/ThemeManager'
const themevalue = ref<ThemeMode>('light')
const themeChange = (val: ThemeMode) => {
const themeChange = async (val: ThemeMode) => {
setTheme(val)
await SetAppConfig('theme.mode', val)
// 通知子窗口也可以加上 BroadcastChannel
const bc = new BroadcastChannel('theme')
bc.postMessage(val)
}
onMounted(() => {
themevalue.value = getCurrentTheme()
onMounted(async () => {
const savedTheme = await GetAppConfig('theme.mode')
themevalue.value = (savedTheme || getCurrentTheme()) as ThemeMode
setTheme(themevalue.value)
})
</script>
@@ -25,6 +29,7 @@ onMounted(() => {
>
<a-select-option value="light">{{$t('components.themesetting.select.opt_light')}}</a-select-option>
<a-select-option value="dark">{{$t('components.themesetting.select.opt_dark')}}</a-select-option>
<a-select-option value="systemdefault">{{$t('components.themesetting.select.opt_system')}}</a-select-option>
</a-select>
</div>
</template>
</template>
+2 -1
View File
@@ -51,7 +51,8 @@
"themesetting": {
"select": {
"opt_dark": "Dark",
"opt_light": "Bright"
"opt_light": "Bright",
"opt_system": "System"
}
},
"about": {
+2 -1
View File
@@ -53,7 +53,8 @@
"themesetting": {
"select": {
"opt_dark": "暗色模式",
"opt_light": "亮色模式"
"opt_light": "亮色模式",
"opt_system": "跟隨系統"
}
},
"about": {
+3 -2
View File
@@ -52,7 +52,8 @@
"themesetting":{
"select":{
"opt_light":"亮色模式",
"opt_dark":"暗色模式"
"opt_dark":"暗色模式",
"opt_system":"跟随系统"
}
},
"about": {
@@ -96,4 +97,4 @@
}
}
}
}
}
+9 -3
View File
@@ -4,16 +4,22 @@ import App from './App.vue'
import './style.css'
import 'ant-design-vue/dist/reset.css';
import {i18n,setupI18n } from './utils/i18n'
import { initTheme } from './utils/ThemeManager'//add
import { initTheme, setTheme, type ThemeMode } from './utils/ThemeManager'//add
import router from './router/index'
import { GetLanguage } from '../bindings/cmbone/internal/services/appconfigservice'
import { GetAppConfig, GetLanguage } from '../bindings/cmbone/internal/services/appconfigservice'
initTheme()
async function bootstrap(){
await setupI18n('zh')//默认语言或从后端读取
createApp(App).use(Antd).use(router).use(i18n).mount('#app')
router.isReady().then(async () => {
const lang = await GetLanguage() //从后端读取语言设置 lang as any|| 'zh'
const [lang, themeMode] = await Promise.all([
GetLanguage(),
GetAppConfig('theme.mode'),
])
if (themeMode) {
setTheme(themeMode as ThemeMode)
}
await setupI18n(lang as any|| 'zh')
})
}
+130 -6
View File
@@ -1,13 +1,71 @@
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue'
import ModulePageShell from '../../components/template/ModulePageShell.vue'
import GeneralSettings from '../../components/General/Index.vue'
import { GetSettingsSchema } from '../../../bindings/cmbone/internal/services/appconfigservice'
const summary = [
{ label: '主题', value: '可配置', tone: 'slate' as const },
{ label: '语言', value: '可配置', tone: 'blue' as const },
{ label: '快捷键', value: '已接入', tone: 'green' as const },
{ label: '窗口行为', value: '待 schema', tone: 'amber' as const },
]
type SettingOption = {
value: string
label: string
}
type SettingItem = {
key: string
category: string
label: string
type: string
defaultValue: string
value: string
description: string
options: SettingOption[]
readOnly: boolean
}
type SettingGroup = {
category: string
label: string
description: string
items: SettingItem[]
}
const groups = ref<SettingGroup[]>([])
const isLoading = ref(true)
const loadError = ref('')
const summary = computed(() => [
{ label: 'Schema 分组', value: String(groups.value.length), tone: 'slate' as const },
{ label: '配置项', value: String(groups.value.reduce((total, group) => total + group.items.length, 0)), tone: 'blue' as const },
{ label: '快捷键', value: findSettingValue('shortcut.open_second_window') || '已接入', tone: 'green' as const },
{ label: '窗口行为', value: findSettingValue('window.start_state') || 'normal', tone: 'amber' as const },
])
onMounted(async () => {
try {
groups.value = await GetSettingsSchema()
} catch (error) {
loadError.value = error instanceof Error ? error.message : String(error)
} finally {
isLoading.value = false
}
})
function findSettingValue(key: string) {
for (const group of groups.value) {
const item = group.items.find((setting) => setting.key === key)
if (item) {
return item.value
}
}
return ''
}
function displayValue(item: SettingItem) {
if (!item.value) {
return item.defaultValue || '-'
}
const option = item.options.find((candidate) => candidate.value === item.value)
return option?.label || item.value
}
</script>
<template>
@@ -21,5 +79,71 @@ const summary = [
<section class="rounded-lg border border-slate-200 bg-white p-4 dark:border-slate-800 dark:bg-slate-900">
<GeneralSettings />
</section>
<section class="rounded-lg border border-slate-200 bg-white dark:border-slate-800 dark:bg-slate-900">
<div class="border-b border-slate-200 p-4 dark:border-slate-800">
<h2 class="mb-1 text-base font-semibold text-slate-950 dark:text-white">
统一设置 Schema
</h2>
<p class="mb-0 text-sm text-slate-500 dark:text-slate-400">
后端初始化默认值,前端和后续服务按 key 使用同一份定义。
</p>
</div>
<div v-if="isLoading" class="p-4 text-sm text-slate-500 dark:text-slate-400">
加载中...
</div>
<div v-else-if="loadError" class="p-4 text-sm text-rose-600 dark:text-rose-300">
{{ loadError }}
</div>
<div v-else class="divide-y divide-slate-200 dark:divide-slate-800">
<div v-for="group in groups" :key="group.category" class="p-4">
<div class="mb-3 flex flex-col gap-1 md:flex-row md:items-end md:justify-between">
<div>
<h3 class="mb-1 text-sm font-semibold text-slate-900 dark:text-white">
{{ group.label }}
</h3>
<p class="mb-0 text-xs text-slate-500 dark:text-slate-400">
{{ group.description }}
</p>
</div>
<span class="text-xs text-slate-400">{{ group.category }}</span>
</div>
<div class="overflow-x-auto">
<table class="min-w-full text-left text-sm">
<thead class="text-xs uppercase text-slate-500 dark:text-slate-400">
<tr>
<th class="w-[220px] py-2 pr-3 font-medium">Key</th>
<th class="w-[160px] py-2 pr-3 font-medium">当前值</th>
<th class="w-[160px] py-2 pr-3 font-medium">默认值</th>
<th class="py-2 pr-3 font-medium">说明</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-100 dark:divide-slate-800">
<tr v-for="item in group.items" :key="item.key">
<td class="py-2 pr-3">
<div class="font-medium text-slate-900 dark:text-white">{{ item.label }}</div>
<div class="text-xs text-slate-500 dark:text-slate-400">{{ item.key }}</div>
</td>
<td class="py-2 pr-3 text-slate-700 dark:text-slate-200">
{{ displayValue(item) }}
</td>
<td class="py-2 pr-3 text-slate-500 dark:text-slate-400">
{{ item.defaultValue || '-' }}
</td>
<td class="py-2 pr-3 text-slate-500 dark:text-slate-400">
<span>{{ item.description }}</span>
<span v-if="item.readOnly" class="ml-2 rounded-md bg-slate-100 px-2 py-0.5 text-xs text-slate-600 dark:bg-slate-800 dark:text-slate-300">
只读
</span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</section>
</ModulePageShell>
</template>