Skip to content
45 changes: 36 additions & 9 deletions dashboard/src/config.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,55 @@
export type ThemeMode = 'light' | 'dark' | 'system';
Comment thread
sourcery-ai[bot] marked this conversation as resolved.

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;
4 changes: 3 additions & 1 deletion dashboard/src/i18n/locales/en-US/core/header.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@
"update": "Update",
"account": "Account",
"theme": {
"title": "Theme",
"light": "Light Mode",
"dark": "Dark Mode"
"dark": "Dark Mode",
"system": "Follow System"
}
},
"updateDialog": {
Expand Down
8 changes: 6 additions & 2 deletions dashboard/src/i18n/locales/en-US/features/auth.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
}
4 changes: 3 additions & 1 deletion dashboard/src/i18n/locales/ru-RU/core/header.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@
"update": "Обновить",
"account": "Аккаунт",
"theme": {
"title": "Тема",
"light": "Светлая тема",
"dark": "Темная тема"
"dark": "Темная тема",
"system": "Как в системе"
}
},
"updateDialog": {
Expand Down
6 changes: 5 additions & 1 deletion dashboard/src/i18n/locales/ru-RU/features/auth.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,11 @@
"subtitle": "Добро пожаловать"
},
"theme": {
"light": "Светлая тема",
"dark": "Темная тема",
"system": "Как в системе",
"switchToDark": "Перейти на темную тему",
"switchToLight": "Перейти на светлую тему"
"switchToLight": "Перейти на светлую тему",
"title": "Тема"
}
}
4 changes: 3 additions & 1 deletion dashboard/src/i18n/locales/zh-CN/core/header.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@
"update": "更新",
"account": "账户",
"theme": {
"title": "主题",
"light": "浅色模式",
"dark": "深色模式"
"dark": "深色模式",
"system": "跟随系统"
}
},
"updateDialog": {
Expand Down
6 changes: 5 additions & 1 deletion dashboard/src/i18n/locales/zh-CN/features/auth.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,11 @@
"subtitle": "欢迎使用"
},
"theme": {
"light": "浅色模式",
"dark": "深色模式",
"system": "跟随系统",
"switchToDark": "切换到深色主题",
"switchToLight": "切换到浅色主题"
"switchToLight": "切换到浅色主题",
"title": "主题"
}
}
115 changes: 87 additions & 28 deletions dashboard/src/layouts/full/vertical-header/VerticalHeader.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Comment thread
lingyun14beta marked this conversation as resolved.

function setThemeMode(mode: 'light' | 'dark' | 'system') {
customizer.SET_THEME_MODE(mode);
theme.global.name.value = customizer.uiTheme;
}

function openReleaseNotesDialog(body: string, tag: string) {
Expand Down Expand Up @@ -1077,29 +1080,68 @@ onMounted(async () => {
</v-card>
</v-menu>

<!-- 主题切换 -->
<v-list-item
@click="toggleDarkMode()"
class="styled-menu-item"
rounded="md"
<!-- 主题切换分组 -->
<v-menu
open-on-click
:open-on-hover="!$vuetify.display.xs"
:open-delay="!$vuetify.display.xs ? 60 : 0"
:close-delay="!$vuetify.display.xs ? 120 : 0"
:location="$vuetify.display.xs ? 'bottom' : 'start center'"
offset="8"
>
<template v-slot:prepend>
<v-icon>
{{
useCustomizerStore().uiTheme === "PurpleThemeDark"
? "mdi-weather-night"
: "mdi-white-balance-sunny"
}}
</v-icon>
<template v-slot:activator="{ props: themeMenuProps }">
<v-list-item
v-bind="themeMenuProps"
@click.stop
class="styled-menu-item theme-group-trigger"
rounded="md"
>
<template v-slot:prepend>
<v-icon>mdi-brightness-6</v-icon>
</template>
<v-list-item-title>{{
t("core.header.buttons.theme.title")
}}</v-list-item-title>
<template v-slot:append>
<span class="theme-group-current">
<v-icon size="16">{{
customizer.themeMode === 'dark'
? 'mdi-weather-night'
: customizer.themeMode === 'system'
? 'mdi-theme-light-dark'
: 'mdi-white-balance-sunny'
}}</v-icon>
</span>
<v-icon size="18" class="language-group-arrow">mdi-chevron-right</v-icon>
</template>
</v-list-item>
</template>
<v-list-item-title>
{{
useCustomizerStore().uiTheme === "PurpleThemeDark"
? t("core.header.buttons.theme.light")
: t("core.header.buttons.theme.dark")
}}
</v-list-item-title>
</v-list-item>

<v-card
class="styled-menu-card"
style="min-width: 170px"
elevation="8"
rounded="lg"
>
<v-list density="compact" class="styled-menu-list pa-1">
<v-list-item
v-for="option in themeOptions"
:key="option.mode"
@click="setThemeMode(option.mode)"
:class="{
'styled-menu-item-active': customizer.themeMode === option.mode,
}"
class="styled-menu-item"
rounded="md"
>
<template v-slot:prepend>
<v-icon size="18" class="theme-option-icon">{{ option.icon }}</v-icon>
</template>
<v-list-item-title>{{ t(option.labelKey) }}</v-list-item-title>
</v-list-item>
</v-list>
</v-card>
</v-menu>

<!-- 更新按钮 -->
<v-list-item
Expand Down Expand Up @@ -1822,6 +1864,23 @@ 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;
Expand Down
89 changes: 53 additions & 36 deletions dashboard/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof createPinia>) {
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);
}
Comment thread
lingyun14beta marked this conversation as resolved.

// 2. 将 Vuetify 主题对齐到 store
vuetify.theme.global.name.value = customizer.uiTheme;

// 3. 应用用户自定义色
const storedPrimary = localStorage.getItem('themePrimary');
const storedSecondary = localStorage.getItem('themeSecondary');
if (storedPrimary || storedSecondary) {
Expand All @@ -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;
});
Comment thread
lingyun14beta marked this conversation as resolved.
});
}

// 初始化新的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();
Expand All @@ -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);
});


Expand Down
Loading
Loading