Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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