148 lines
6.6 KiB
Vue
148 lines
6.6 KiB
Vue
<template>
|
|
<!-- ═══ 外观设置(主题 / 语言 / AI 语言 / Token 用量)═══ -->
|
|
<section class="panel">
|
|
<div class="panel-header">
|
|
<h2>{{ $t('settings.sectionAppearance') }}</h2>
|
|
</div>
|
|
<div class="general-settings">
|
|
<SettingRow ref="rowTheme" :label="$t('settings.labelTheme')" :desc="$t('settings.descTheme')">
|
|
<!-- 走查 P3-11:不再 @change 即显「已保存」,反馈移到 persist 完成后(防假成功) -->
|
|
<select v-model="settings.theme" class="setting-select">
|
|
<option value="dark">{{ $t('settings.themeDark') }}</option>
|
|
<option value="light">{{ $t('settings.themeLight') }}</option>
|
|
<option value="system">{{ $t('settings.themeSystem') }}</option>
|
|
</select>
|
|
</SettingRow>
|
|
<SettingRow ref="rowLanguage" :label="$t('settings.labelLanguage')" :desc="$t('settings.descLanguage')">
|
|
<select v-model="settings.language" class="setting-select">
|
|
<option value="zh-CN">{{ $t('settings.aiLanguageZh') }}</option>
|
|
<option value="en">{{ $t('settings.aiLanguageEn') }}</option>
|
|
</select>
|
|
</SettingRow>
|
|
<SettingRow ref="rowAiLanguage" :label="$t('settings.labelAiLanguage')" :desc="$t('settings.descAiLanguage')">
|
|
<select v-model="settings.aiLanguage" class="setting-select">
|
|
<option value="auto">{{ $t('settings.aiLanguageAuto') }}</option>
|
|
<option value="zh-CN">{{ $t('settings.aiLanguageZh') }}</option>
|
|
<option value="en">{{ $t('settings.aiLanguageEn') }}</option>
|
|
</select>
|
|
</SettingRow>
|
|
<SettingRow ref="rowToken" :label="$t('settings.labelShowTokenUsage')" :desc="$t('settings.descShowTokenUsage')">
|
|
<label class="toggle">
|
|
<input type="checkbox" v-model="settings.showTokenUsage" />
|
|
<span class="toggle-slider"></span>
|
|
</label>
|
|
</SettingRow>
|
|
<SettingRow ref="rowStreamingMd" :label="$t('settings.labelStreamingMd')" :desc="$t('settings.descStreamingMd')">
|
|
<label class="toggle">
|
|
<input type="checkbox" v-model="settings.streamingMd" />
|
|
<span class="toggle-slider"></span>
|
|
</label>
|
|
</SettingRow>
|
|
</div>
|
|
</section>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { reactive, watch, onMounted, onUnmounted, useTemplateRef } from 'vue'
|
|
import { useI18n } from 'vue-i18n'
|
|
import { useAppSettingsStore } from '@/stores/appSettings'
|
|
import SettingRow from './SettingRow.vue'
|
|
|
|
// ============================================================
|
|
// 外观域 — 本地设置(localStorage):主题 / 界面语言 / AI 语言 / Token 用量显隐
|
|
// 拆自 GeneralPanel,本 Section 仅持有外观相关 key
|
|
// 阶段5:SettingRow 封装行布局,控件变更后调 markSaved 显"已保存"
|
|
// ============================================================
|
|
const appSettings = useAppSettingsStore()
|
|
const { locale } = useI18n()
|
|
|
|
const settings = reactive({
|
|
theme: appSettings.get<string>('df-theme', 'dark'),
|
|
language: appSettings.get<string>('df-language', 'zh-CN'),
|
|
aiLanguage: appSettings.get<string>('df-ai-language', 'auto'),
|
|
showTokenUsage: appSettings.get<boolean>('df-show-token-usage', false),
|
|
// AR-1 流式 Markdown 渲染开关:默认 true(流式过程渲染代码块/列表/标题格式);
|
|
// 关 → 回退纯文本(防掉帧/性能敏感场景降级)
|
|
streamingMd: appSettings.get<boolean>('df-ai-streaming-md', true),
|
|
})
|
|
|
|
function applyTheme(theme: string) {
|
|
if (theme === 'system') {
|
|
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches
|
|
document.documentElement.removeAttribute('data-theme')
|
|
if (!prefersDark) document.documentElement.setAttribute('data-theme', 'light')
|
|
} else if (theme === 'light') {
|
|
document.documentElement.setAttribute('data-theme', 'light')
|
|
} else {
|
|
document.documentElement.removeAttribute('data-theme')
|
|
}
|
|
}
|
|
|
|
// 走查 P2-4:原实现只在挂载/改档时读一次 matchMedia.matches,OS 深浅色切换后应用停在旧色,
|
|
// 需重启或重选档位才跟随。挂 change 监听实时重算;非 system 档是用户手动定色,不应被 OS
|
|
// 切色打断,故回调内先判档。组件卸载必须移除监听(本 Section 随分类切换反复挂卸,不清理会叠监听器)。
|
|
const systemDarkQuery = window.matchMedia('(prefers-color-scheme: dark)')
|
|
function onSystemThemeChange() {
|
|
if (settings.theme === 'system') applyTheme('system')
|
|
}
|
|
|
|
// 即时生效反馈:SettingRow refs,key 映射对应 row
|
|
const rowTheme = useTemplateRef<InstanceType<typeof SettingRow>>('rowTheme')
|
|
const rowLanguage = useTemplateRef<InstanceType<typeof SettingRow>>('rowLanguage')
|
|
const rowAiLanguage = useTemplateRef<InstanceType<typeof SettingRow>>('rowAiLanguage')
|
|
const rowToken = useTemplateRef<InstanceType<typeof SettingRow>>('rowToken')
|
|
const rowStreamingMd = useTemplateRef<InstanceType<typeof SettingRow>>('rowStreamingMd')
|
|
|
|
// 走查 P3-11 假成功族:原 @change 立即 markSaved,先于持久化完成,失败无人知晓。
|
|
// 改为 set 完成后才点亮「已保存」,失败点亮行内红字。注:appSettings.set 当前对落库
|
|
// 失败仅 console 内吞(stores/appSettings.ts 降级内存态设计),catch 实际不会触发;
|
|
// 待 store 层把失败上抛后本处失败通道即自动生效(登记项,见批次报告)。
|
|
async function persist(key: string, value: unknown, row: { value: { markSaved: () => void; markFailed: () => void } | null }) {
|
|
try {
|
|
await appSettings.set(key, value)
|
|
row.value?.markSaved()
|
|
} catch (e) {
|
|
console.error(`保存设置 ${key} 失败:`, e)
|
|
row.value?.markFailed()
|
|
}
|
|
}
|
|
|
|
watch(() => settings.theme, (val) => {
|
|
applyTheme(val)
|
|
void persist('df-theme', val, rowTheme)
|
|
})
|
|
|
|
watch(() => settings.aiLanguage, (val) => {
|
|
void persist('df-ai-language', val, rowAiLanguage)
|
|
})
|
|
|
|
watch(() => settings.showTokenUsage, (val) => {
|
|
void persist('df-show-token-usage', val, rowToken)
|
|
})
|
|
|
|
watch(() => settings.streamingMd, (val) => {
|
|
void persist('df-ai-streaming-md', val, rowStreamingMd)
|
|
})
|
|
|
|
watch(() => settings.language, (val) => {
|
|
// 同步 i18n 实例,让界面立即跟随语言切换
|
|
locale.value = val as 'zh-CN' | 'en'
|
|
void persist('df-language', val, rowLanguage)
|
|
})
|
|
|
|
onMounted(() => {
|
|
applyTheme(settings.theme)
|
|
systemDarkQuery.addEventListener('change', onSystemThemeChange)
|
|
})
|
|
|
|
onUnmounted(() => {
|
|
systemDarkQuery.removeEventListener('change', onSystemThemeChange)
|
|
})
|
|
</script>
|
|
|
|
<style scoped>
|
|
/* 阶段5 UX 重构:复用 settings.css 全局 .panel/.panel-header/.general-settings/
|
|
.setting-select/.toggle/.toggle-slider + SettingRow(封装 .setting-row/.setting-info/
|
|
.setting-label/.setting-desc/.setting-control)。本 Section 无特有 scoped 样式。 */
|
|
</style>
|