新增 跟随系统主题切换功能,深色模式自动隐藏亚克力效果

This commit is contained in:
2026-04-18 23:12:08 +08:00
parent fa255f78b4
commit 902013f22f
9 changed files with 55 additions and 17 deletions

View File

@@ -97,7 +97,7 @@ npm run test:run
### 主题系统 ### 主题系统
支持种主题:`Light` (浅色) / `Dark` (深色) 支持种主题:`Light` (浅色) / `Dark` (深色) / `System` (跟随系统)
CSS 变量定义在 `src/styles/global.less`,包括: CSS 变量定义在 `src/styles/global.less`,包括:
- `--bg-primary/secondary/elevated` - 背景层级 - `--bg-primary/secondary/elevated` - 背景层级

View File

@@ -9,7 +9,7 @@
- 📝 **API 配置管理** - 支持多环境配置文件切换、创建、编辑、复制和删除 - 📝 **API 配置管理** - 支持多环境配置文件切换、创建、编辑、复制和删除
- 🖥️ **MCP 服务器管理** - 便捷的 Model Context Protocol 服务器配置界面 - 🖥️ **MCP 服务器管理** - 便捷的 Model Context Protocol 服务器配置界面
- 🎨 **Windows 11 设计风格** - 采用 Fluent Design 设计规范 - 🎨 **Windows 11 设计风格** - 采用 Fluent Design 设计规范
- 🌈 **多主题支持** - Light / Dark 种主题 - 🌈 **多主题支持** - Light / Dark / System (跟随系统) 三种主题
- 🌍 **国际化** - 支持简体中文、English、日語 - 🌍 **国际化** - 支持简体中文、English、日語
- 💧 **亚克力效果** - 可调节透明度的现代视觉效果 - 💧 **亚克力效果** - 可调节透明度的现代视觉效果
- 📦 **系统托盘** - 最小化到托盘,快速切换 API 配置 - 📦 **系统托盘** - 最小化到托盘,快速切换 API 配置

View File

@@ -1,6 +1,6 @@
{ {
"name": "iflow-settings-editor", "name": "iflow-settings-editor",
"version": "1.6.0", "version": "1.6.1",
"description": "一个用于编辑 iFlow CLI 配置文件的桌面应用程序。", "description": "一个用于编辑 iFlow CLI 配置文件的桌面应用程序。",
"main": "main.js", "main": "main.js",
"author": "上海潘哆呐科技有限公司", "author": "上海潘哆呐科技有限公司",

View File

@@ -84,6 +84,7 @@ const currentServerName = ref(null)
const isLoading = ref(true) const isLoading = ref(true)
const apiProfiles = ref([]) const apiProfiles = ref([])
const currentApiProfile = ref('default') const currentApiProfile = ref('default')
const systemTheme = ref('Light')
const showInputDialog = ref({ show: false, title: '', placeholder: '', callback: null, isConfirm: false, defaultValue: '' }) const showInputDialog = ref({ show: false, title: '', placeholder: '', callback: null, isConfirm: false, defaultValue: '' })
const showMessageDialog = ref({ show: false, type: 'info', title: '', message: '', callback: null }) const showMessageDialog = ref({ show: false, type: 'info', title: '', message: '', callback: null })
@@ -307,8 +308,14 @@ const showSection = section => {
const serverCount = computed(() => (settings.value.mcpServers ? Object.keys(settings.value.mcpServers).length : 0)) const serverCount = computed(() => (settings.value.mcpServers ? Object.keys(settings.value.mcpServers).length : 0))
const themeClass = computed(() => { const getEffectiveTheme = () => {
const theme = settings.value.uiTheme const theme = settings.value.uiTheme
if (theme === 'System') return systemTheme.value
return theme
}
const themeClass = computed(() => {
const theme = getEffectiveTheme()
if (theme === 'Dark') return 'dark' if (theme === 'Dark') return 'dark'
return '' return ''
}) })
@@ -471,14 +478,10 @@ const closeMessageDialog = () => {
watch( watch(
() => settings.value.uiTheme, () => settings.value.uiTheme,
theme => { () => {
document.body.classList.remove('dark')
const cls = themeClass.value const cls = themeClass.value
if (cls) { if (cls) document.body.classList.add(cls)
document.body.classList.add(cls)
if (cls === 'dark') document.body.classList.remove('solarized-dark')
} else {
document.body.classList.remove('dark', 'solarized-dark')
}
applyAcrylicStyle() applyAcrylicStyle()
}, },
) )
@@ -494,7 +497,7 @@ const applyAcrylicStyle = () => {
const intensity = settings.value.acrylicIntensity const intensity = settings.value.acrylicIntensity
if (intensity === undefined || intensity === null) return if (intensity === undefined || intensity === null) return
const opacity = 1 - intensity / 100 const opacity = 1 - intensity / 100
const isDark = settings.value.uiTheme === 'Dark' const isDark = getEffectiveTheme() === 'Dark'
const root = document.documentElement const root = document.documentElement
if (isDark) { if (isDark) {
@@ -512,10 +515,28 @@ const applyAcrylicStyle = () => {
} }
} }
const updateSystemTheme = () => {
const isDark = window.matchMedia('(prefers-color-scheme: dark)').matches
systemTheme.value = isDark ? 'Dark' : 'Light'
if (settings.value.uiTheme === 'System') {
const cls = themeClass.value
document.body.classList.remove('dark')
if (cls) document.body.classList.add(cls)
applyAcrylicStyle()
}
}
onMounted(async () => { onMounted(async () => {
await loadApiProfiles() await loadApiProfiles()
await loadSettings() await loadSettings()
locale.value = settings.value.language locale.value = settings.value.language
// 初始化系统主题
updateSystemTheme()
// 监听系统主题变化
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', updateSystemTheme)
const cls = themeClass.value const cls = themeClass.value
if (cls) { if (cls) {
document.body.classList.add(cls) document.body.classList.add(cls)

View File

@@ -33,7 +33,8 @@ export default {
}, },
theme: { theme: {
dark: 'Dark', dark: 'Dark',
light: 'Light' light: 'Light',
system: 'System'
}, },
api: { api: {
title: 'API Configuration', title: 'API Configuration',

View File

@@ -33,7 +33,8 @@ export default {
}, },
theme: { theme: {
dark: '深色', dark: '深色',
light: '浅色' light: '浅色',
system: '跟随系统'
}, },
api: { api: {
title: 'API 配置', title: 'API 配置',

View File

@@ -33,7 +33,8 @@ export default {
}, },
theme: { theme: {
dark: 'ダーク', dark: 'ダーク',
light: 'ライト' light: 'ライト',
system: 'システム'
}, },
api: { api: {
title: 'API 設定', title: 'API 設定',

View File

@@ -61,9 +61,10 @@ describe('GeneralSettings.vue', () => {
}); });
const themeOptions = wrapper.findAll('.form-select')[1].findAll('option'); const themeOptions = wrapper.findAll('.form-select')[1].findAll('option');
expect(themeOptions.length).toBe(2); expect(themeOptions.length).toBe(3);
expect(themeOptions[0].attributes('value')).toBe('Light'); expect(themeOptions[0].attributes('value')).toBe('Light');
expect(themeOptions[1].attributes('value')).toBe('Dark'); expect(themeOptions[1].attributes('value')).toBe('Dark');
expect(themeOptions[2].attributes('value')).toBe('System');
}); });
it('reflects current settings in form controls', async () => { it('reflects current settings in form controls', async () => {

View File

@@ -23,6 +23,7 @@
<select class="form-select" v-model="localSettings.uiTheme"> <select class="form-select" v-model="localSettings.uiTheme">
<option value="Light">{{ $t('theme.light') }}</option> <option value="Light">{{ $t('theme.light') }}</option>
<option value="Dark">{{ $t('theme.dark') }}</option> <option value="Dark">{{ $t('theme.dark') }}</option>
<option value="System">{{ $t('theme.system') }}</option>
</select> </select>
</div> </div>
</div> </div>
@@ -90,12 +91,24 @@ const localSettings = computed({
set: val => emit('update:settings', val), set: val => emit('update:settings', val),
}) })
const systemTheme = ref('Light')
const supportsAcrylic = computed(() => { const supportsAcrylic = computed(() => {
return typeof document !== 'undefined' && 'backdropFilter' in document.documentElement.style && props.settings.uiTheme !== 'Dark' if (typeof document === 'undefined' || !('backdropFilter' in document.documentElement.style)) return false
const effectiveTheme = props.settings.uiTheme === 'System' ? systemTheme.value : props.settings.uiTheme
return effectiveTheme !== 'Dark'
}) })
const sliderWrapper = ref(null) const sliderWrapper = ref(null)
onMounted(() => {
const isDark = window.matchMedia('(prefers-color-scheme: dark)').matches
systemTheme.value = isDark ? 'Dark' : 'Light'
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => {
systemTheme.value = e.matches ? 'Dark' : 'Light'
})
})
const updateSliderValue = e => { const updateSliderValue = e => {
const value = Number(e.target.value) const value = Number(e.target.value)
emit('update:settings', { ...props.settings, acrylicIntensity: value }) emit('update:settings', { ...props.settings, acrylicIntensity: value })