diff --git a/dashboard/src/config.ts b/dashboard/src/config.ts
index f52812ad8e..99d5982765 100644
--- a/dashboard/src/config.ts
+++ b/dashboard/src/config.ts
@@ -1,28 +1,55 @@
+export type ThemeMode = 'light' | 'dark' | 'system';
+
export type ConfigProps = {
Sidebar_drawer: boolean;
Customizer_drawer: boolean;
mini_sidebar: boolean;
fontTheme: string;
uiTheme: string;
+ themeMode: ThemeMode;
inputBg: boolean;
};
-function checkUITheme() {
- /* 检查localStorage有无记忆的主题选项,如有则使用,否则使用默认值 */
- const theme = localStorage.getItem("uiTheme");
- if (!theme || !(['PurpleTheme', 'PurpleThemeDark'].includes(theme))) {
- localStorage.setItem("uiTheme", "PurpleTheme"); // todo: 这部分可以根据vuetify.ts的默认主题动态调整
- return 'PurpleTheme';
- } else return theme;
+function checkThemeMode(): ThemeMode {
+ const mode = localStorage.getItem('themeMode') as ThemeMode | null;
+ if (mode === 'light' || mode === 'dark' || mode === 'system') return mode;
+
+ const legacyTheme = localStorage.getItem('uiTheme');
+ if (legacyTheme === 'PurpleThemeDark') {
+ localStorage.setItem('themeMode', 'dark');
+ return 'dark';
+ }
+ if (legacyTheme === 'PurpleTheme') {
+ localStorage.setItem('themeMode', 'light');
+ return 'light';
+ }
+
+ localStorage.setItem('themeMode', 'system');
+ return 'system';
}
+export function resolveUiTheme(mode: ThemeMode): string {
+ if (mode === 'dark') return 'PurpleThemeDark';
+ if (mode === 'light') return 'PurpleTheme';
+ const prefersDark =
+ typeof window !== 'undefined' &&
+ window.matchMedia('(prefers-color-scheme: dark)').matches;
+ return prefersDark ? 'PurpleThemeDark' : 'PurpleTheme';
+}
+
+const themeMode = checkThemeMode();
+const uiTheme = resolveUiTheme(themeMode);
+
+localStorage.setItem('uiTheme', uiTheme);
+
const config: ConfigProps = {
Sidebar_drawer: true,
Customizer_drawer: false,
mini_sidebar: false,
fontTheme: 'Roboto',
- uiTheme: checkUITheme(),
- inputBg: false
+ uiTheme,
+ themeMode,
+ inputBg: false,
};
export default config;
diff --git a/dashboard/src/i18n/locales/en-US/core/header.json b/dashboard/src/i18n/locales/en-US/core/header.json
index d83f06152f..082bdd7e1f 100644
--- a/dashboard/src/i18n/locales/en-US/core/header.json
+++ b/dashboard/src/i18n/locales/en-US/core/header.json
@@ -8,8 +8,10 @@
"update": "Update",
"account": "Account",
"theme": {
+ "title": "Theme",
"light": "Light Mode",
- "dark": "Dark Mode"
+ "dark": "Dark Mode",
+ "system": "Follow System"
}
},
"updateDialog": {
diff --git a/dashboard/src/i18n/locales/en-US/features/auth.json b/dashboard/src/i18n/locales/en-US/features/auth.json
index 526c080be0..f85cc81e96 100644
--- a/dashboard/src/i18n/locales/en-US/features/auth.json
+++ b/dashboard/src/i18n/locales/en-US/features/auth.json
@@ -57,7 +57,11 @@
"subtitle": "Welcome"
},
"theme": {
+ "light": "Light Mode",
+ "dark": "Dark Mode",
+ "system": "Follow System",
"switchToDark": "Switch to Dark Theme",
- "switchToLight": "Switch to Light Theme"
+ "switchToLight": "Switch to Light Theme",
+ "title": "Theme"
}
-}
+}
diff --git a/dashboard/src/i18n/locales/ru-RU/core/header.json b/dashboard/src/i18n/locales/ru-RU/core/header.json
index 3124cd9719..a851897df7 100644
--- a/dashboard/src/i18n/locales/ru-RU/core/header.json
+++ b/dashboard/src/i18n/locales/ru-RU/core/header.json
@@ -8,8 +8,10 @@
"update": "Обновить",
"account": "Аккаунт",
"theme": {
+ "title": "Тема",
"light": "Светлая тема",
- "dark": "Темная тема"
+ "dark": "Темная тема",
+ "system": "Как в системе"
}
},
"updateDialog": {
diff --git a/dashboard/src/i18n/locales/ru-RU/features/auth.json b/dashboard/src/i18n/locales/ru-RU/features/auth.json
index 97f2a71f74..9c5f9f638f 100644
--- a/dashboard/src/i18n/locales/ru-RU/features/auth.json
+++ b/dashboard/src/i18n/locales/ru-RU/features/auth.json
@@ -57,7 +57,11 @@
"subtitle": "Добро пожаловать"
},
"theme": {
+ "light": "Светлая тема",
+ "dark": "Темная тема",
+ "system": "Как в системе",
"switchToDark": "Перейти на темную тему",
- "switchToLight": "Перейти на светлую тему"
+ "switchToLight": "Перейти на светлую тему",
+ "title": "Тема"
}
}
diff --git a/dashboard/src/i18n/locales/zh-CN/core/header.json b/dashboard/src/i18n/locales/zh-CN/core/header.json
index 8897ad0af5..611213b782 100644
--- a/dashboard/src/i18n/locales/zh-CN/core/header.json
+++ b/dashboard/src/i18n/locales/zh-CN/core/header.json
@@ -8,8 +8,10 @@
"update": "更新",
"account": "账户",
"theme": {
+ "title": "主题",
"light": "浅色模式",
- "dark": "深色模式"
+ "dark": "深色模式",
+ "system": "跟随系统"
}
},
"updateDialog": {
diff --git a/dashboard/src/i18n/locales/zh-CN/features/auth.json b/dashboard/src/i18n/locales/zh-CN/features/auth.json
index ad886f105b..1631437134 100644
--- a/dashboard/src/i18n/locales/zh-CN/features/auth.json
+++ b/dashboard/src/i18n/locales/zh-CN/features/auth.json
@@ -57,7 +57,11 @@
"subtitle": "欢迎使用"
},
"theme": {
+ "light": "浅色模式",
+ "dark": "深色模式",
+ "system": "跟随系统",
"switchToDark": "切换到深色主题",
- "switchToLight": "切换到浅色主题"
+ "switchToLight": "切换到浅色主题",
+ "title": "主题"
}
}
diff --git a/dashboard/src/layouts/full/vertical-header/VerticalHeader.vue b/dashboard/src/layouts/full/vertical-header/VerticalHeader.vue
index c41b566e70..c34f2c4266 100644
--- a/dashboard/src/layouts/full/vertical-header/VerticalHeader.vue
+++ b/dashboard/src/layouts/full/vertical-header/VerticalHeader.vue
@@ -725,13 +725,16 @@ function updateDashboard() {
});
}
-function toggleDarkMode() {
- const newTheme =
- customizer.uiTheme === "PurpleThemeDark"
- ? "PurpleTheme"
- : "PurpleThemeDark";
- customizer.SET_UI_THEME(newTheme);
- theme.global.name.value = newTheme;
+// 主题选项配置
+const themeOptions = [
+ { mode: 'light' as const, icon: 'mdi-white-balance-sunny', labelKey: 'core.header.buttons.theme.light' },
+ { mode: 'dark' as const, icon: 'mdi-weather-night', labelKey: 'core.header.buttons.theme.dark' },
+ { mode: 'system' as const, icon: 'mdi-sync', labelKey: 'core.header.buttons.theme.system' },
+] as const;
+
+function setThemeMode(mode: 'light' | 'dark' | 'system') {
+ customizer.SET_THEME_MODE(mode);
+ theme.global.name.value = customizer.uiTheme;
}
function openReleaseNotesDialog(body: string, tag: string) {
@@ -1077,29 +1080,68 @@ onMounted(async () => {
-
-
+
+
+
{
min-width: 180px;
}
+.theme-group-trigger :deep(.v-list-item__append) {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+}
+
+.theme-group-current {
+ display: flex;
+ align-items: center;
+ opacity: 0.75;
+}
+
+.theme-option-icon {
+ margin-right: 8px;
+ opacity: 0.85;
+}
+
.mobile-mode-toggle-wrapper {
display: flex;
justify-content: center;
diff --git a/dashboard/src/main.ts b/dashboard/src/main.ts
index ce5514207c..021e972146 100644
--- a/dashboard/src/main.ts
+++ b/dashboard/src/main.ts
@@ -47,25 +47,31 @@ import { waitForRouterReadyInBackground } from './utils/routerReadiness.mjs';
},
};
-// 初始化新的i18n系统,等待完成后再挂载应用
-setupI18n().then(async () => {
- console.log('🌍 新i18n系统初始化完成');
-
- const app = createApp(App);
- const pinia = createPinia();
- app.use(pinia);
- app.use(router);
- app.use(print);
- app.use(VueApexCharts);
- app.use(vuetify);
- app.use(confirmPlugin);
- await router.isReady();
- app.mount('#app');
-
- // 挂载后同步 Vuetify 主题
+/**
+ * 挂载后初始化主题并注册全局系统主题监听器。
+ * 职责:
+ * - 同步 Vuetify theme 名称与 store 中的 uiTheme
+ * - 当 themeMode === 'system' 时,监听系统色彩模式变化,实时更新两者
+ * - 应用自定义 primary/secondary 色
+ * 注意:VerticalHeader.vue / ThemeSwitcher.vue 不再自行注册 matchMedia 监听器,
+ * 避免与此处产生竞态。
+ */
+function setupThemeSync(pinia: ReturnType) {
import('./stores/customizer').then(({ useCustomizerStore }) => {
const customizer = useCustomizerStore(pinia);
+
+ // 1. 若当前是 system 模式,重新用 matchMedia 计算,防止 SSR / 构建时偏差
+ if (customizer.themeMode === 'system') {
+ const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
+ const uiTheme = prefersDark ? 'PurpleThemeDark' : 'PurpleTheme';
+ customizer.uiTheme = uiTheme;
+ localStorage.setItem('uiTheme', uiTheme);
+ }
+
+ // 2. 将 Vuetify 主题对齐到 store
vuetify.theme.global.name.value = customizer.uiTheme;
+
+ // 3. 应用用户自定义色
const storedPrimary = localStorage.getItem('themePrimary');
const storedSecondary = localStorage.getItem('themeSecondary');
if (storedPrimary || storedSecondary) {
@@ -79,10 +85,38 @@ setupI18n().then(async () => {
if (storedSecondary && theme.colors.darksecondary) theme.colors.darksecondary = storedSecondary;
});
}
+
+ // 4. 全局唯一 matchMedia 监听器:仅在 system 模式下响应系统切换
+ const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
+ mediaQuery.addEventListener('change', (e) => {
+ if (customizer.themeMode !== 'system') return;
+ const uiTheme = e.matches ? 'PurpleThemeDark' : 'PurpleTheme';
+ customizer.uiTheme = uiTheme;
+ localStorage.setItem('uiTheme', uiTheme);
+ vuetify.theme.global.name.value = uiTheme;
+ });
});
+}
+
+// 初始化新的i18n系统,等待完成后再挂载应用
+setupI18n().then(async () => {
+ console.log('🌍 新i18n系统初始化完成');
+
+ const app = createApp(App);
+ const pinia = createPinia();
+ app.use(pinia);
+ app.use(router);
+ app.use(print);
+ app.use(VueApexCharts);
+ app.use(vuetify);
+ app.use(confirmPlugin);
+ await router.isReady();
+ app.mount('#app');
+
+ setupThemeSync(pinia);
}).catch(error => {
console.error('❌ 新i18n系统初始化失败:', error);
-
+
// 即使i18n初始化失败,也要挂载应用(使用回退机制)
const app = createApp(App);
const pinia = createPinia();
@@ -94,25 +128,8 @@ setupI18n().then(async () => {
app.use(confirmPlugin);
app.mount('#app');
waitForRouterReadyInBackground(router);
-
- // 挂载后同步 Vuetify 主题
- import('./stores/customizer').then(({ useCustomizerStore }) => {
- const customizer = useCustomizerStore(pinia);
- vuetify.theme.global.name.value = customizer.uiTheme;
- const storedPrimary = localStorage.getItem('themePrimary');
- const storedSecondary = localStorage.getItem('themeSecondary');
- if (storedPrimary || storedSecondary) {
- const themes = vuetify.theme.themes.value;
- ['PurpleTheme', 'PurpleThemeDark'].forEach((name) => {
- const theme = themes[name];
- if (!theme?.colors) return;
- if (storedPrimary) theme.colors.primary = storedPrimary;
- if (storedSecondary) theme.colors.secondary = storedSecondary;
- if (storedPrimary && theme.colors.darkprimary) theme.colors.darkprimary = storedPrimary;
- if (storedSecondary && theme.colors.darksecondary) theme.colors.darksecondary = storedSecondary;
- });
- }
- });
+
+ setupThemeSync(pinia);
});
diff --git a/dashboard/src/stores/customizer.ts b/dashboard/src/stores/customizer.ts
index a78a9f5cd2..568e814849 100644
--- a/dashboard/src/stores/customizer.ts
+++ b/dashboard/src/stores/customizer.ts
@@ -1,22 +1,25 @@
import { defineStore } from 'pinia';
-import config from '@/config';
+import config, { type ThemeMode, resolveUiTheme } from '@/config';
const DARK_THEMES: ReadonlySet = new Set(['PurpleThemeDark']);
-export const useCustomizerStore = defineStore("customizer", {
+
+export const useCustomizerStore = defineStore('customizer', {
state: () => ({
Sidebar_drawer: config.Sidebar_drawer,
Customizer_drawer: config.Customizer_drawer,
mini_sidebar: config.mini_sidebar,
- fontTheme: "Noto Sans SC",
+ fontTheme: 'Noto Sans SC',
uiTheme: config.uiTheme,
+ themeMode: config.themeMode as ThemeMode,
inputBg: config.inputBg,
- chatSidebarOpen: false // chat mode mobile sidebar state
+ chatSidebarOpen: false, // chat mode mobile sidebar state
}),
getters: {
isDark: (state) => state.uiTheme ? DARK_THEMES.has(state.uiTheme) : false,
},
+
actions: {
SET_SIDEBAR_DRAWER() {
this.Sidebar_drawer = !this.Sidebar_drawer;
@@ -27,9 +30,21 @@ export const useCustomizerStore = defineStore("customizer", {
SET_FONT(payload: string) {
this.fontTheme = payload;
},
+
SET_UI_THEME(payload: string) {
this.uiTheme = payload;
- localStorage.setItem("uiTheme", payload);
+ localStorage.setItem('uiTheme', payload);
+ const mode: ThemeMode = payload === 'PurpleThemeDark' ? 'dark' : 'light';
+ this.themeMode = mode;
+ localStorage.setItem('themeMode', mode);
+ },
+
+ SET_THEME_MODE(mode: ThemeMode) {
+ this.themeMode = mode;
+ localStorage.setItem('themeMode', mode);
+ const uiTheme = resolveUiTheme(mode);
+ this.uiTheme = uiTheme;
+ localStorage.setItem('uiTheme', uiTheme);
},
TOGGLE_CHAT_SIDEBAR() {
@@ -38,5 +53,5 @@ export const useCustomizerStore = defineStore("customizer", {
SET_CHAT_SIDEBAR(payload: boolean) {
this.chatSidebarOpen = payload;
},
- }
+ },
});
diff --git a/dashboard/src/views/authentication/auth/LoginPage.vue b/dashboard/src/views/authentication/auth/LoginPage.vue
index 282761eafe..d6a8a77317 100644
--- a/dashboard/src/views/authentication/auth/LoginPage.vue
+++ b/dashboard/src/views/authentication/auth/LoginPage.vue
@@ -24,13 +24,23 @@ const logoTitle = computed(() => {
return t('logo.title');
});
-// 主题切换函数
-function toggleTheme() {
- const newTheme = customizer.uiTheme === 'PurpleThemeDark' ? 'PurpleTheme' : 'PurpleThemeDark';
- customizer.SET_UI_THEME(newTheme);
- theme.global.name.value = newTheme;
+const themeOptions = [
+ { mode: 'light' as const, icon: 'mdi-white-balance-sunny', labelKey: 'theme.light' },
+ { mode: 'dark' as const, icon: 'mdi-weather-night', labelKey: 'theme.dark' },
+ { mode: 'system' as const, icon: 'mdi-sync', labelKey: 'theme.system' },
+] as const;
+
+function setThemeMode(mode: 'light' | 'dark' | 'system') {
+ customizer.SET_THEME_MODE(mode);
+ theme.global.name.value = customizer.uiTheme;
}
+const currentThemeIcon = computed(() => {
+ if (customizer.themeMode === 'dark') return 'mdi-weather-night';
+ if (customizer.themeMode === 'system') return 'mdi-sync';
+ return 'mdi-white-balance-sunny';
+});
+
onMounted(async () => {
// 检查用户是否已登录,如果已登录则重定向
if (authStore.has_token()) {
@@ -73,14 +83,55 @@ onMounted(async () => {
-
-
- {{ customizer.uiTheme === 'PurpleThemeDark' ? 'mdi-white-balance-sunny' : 'mdi-weather-night' }}
-
-
- {{ customizer.uiTheme === 'PurpleThemeDark' ? t('theme.switchToLight') : t('theme.switchToDark') }}
-
-
+
+
+
+
+
+
+ {{ currentThemeIcon }}
+
+
+ {{ t('theme.title') }}
+
+
+
+
+
+
{{ logoTitle }}
diff --git a/dashboard/src/views/authentication/auth/SetupPage.vue b/dashboard/src/views/authentication/auth/SetupPage.vue
index ae8f11d03b..1e769d9e6a 100644
--- a/dashboard/src/views/authentication/auth/SetupPage.vue
+++ b/dashboard/src/views/authentication/auth/SetupPage.vue
@@ -1,7 +1,7 @@