feat: 添加主题根据系统切换功能

feat: #387
This commit is contained in:
alger
2025-07-23 22:45:47 +08:00
parent e489ab46b5
commit d5ba218b10
7 changed files with 122 additions and 9 deletions

View File

@@ -15,6 +15,8 @@ export default {
basic: {
themeMode: 'Theme Mode',
themeModeDesc: 'Switch between light/dark theme',
autoTheme: 'Follow System',
manualTheme: 'Manual Switch',
language: 'Language Settings',
languageDesc: 'Change display language',
font: 'Font Settings',

View File

@@ -15,6 +15,8 @@ export default {
basic: {
themeMode: '主题模式',
themeModeDesc: '切换日间/夜间主题',
autoTheme: '跟随系统',
manualTheme: '手动切换',
language: '语言设置',
languageDesc: '切换显示语言',
font: '字体设置',

View File

@@ -15,6 +15,8 @@ export default {
basic: {
themeMode: '主題模式',
themeModeDesc: '切換日間/夜間主題',
autoTheme: '跟隨系統',
manualTheme: '手動切換',
language: '語言設定',
languageDesc: '切換顯示語言',
font: '字體設定',

View File

@@ -25,5 +25,7 @@
"enableMusicUnblock": true,
"enabledMusicSources": ["migu", "kugou", "pyncmd", "bilibili"],
"showTopAction": false,
"contentZoomFactor": 1
"contentZoomFactor": 1,
"autoTheme": false,
"manualTheme": "light"
}

View File

@@ -4,7 +4,7 @@ import { ref } from 'vue';
import setDataDefault from '@/../main/set.json';
import { isElectron } from '@/utils';
import { applyTheme, getCurrentTheme, ThemeType } from '@/utils/theme';
import { applyTheme, getCurrentTheme, getSystemTheme, watchSystemTheme, ThemeType } from '@/utils/theme';
export const useSettingsStore = defineStore('settings', () => {
const theme = ref<ThemeType>(getCurrentTheme());
@@ -18,6 +18,9 @@ export const useSettingsStore = defineStore('settings', () => {
]);
const showDownloadDrawer = ref(false);
// 系统主题监听器清理函数
let systemThemeCleanup: (() => void) | null = null;
// 先声明 setData ref 但不初始化
const setData = ref<any>({});
@@ -56,8 +59,57 @@ export const useSettingsStore = defineStore('settings', () => {
setData.value = getInitialSettings();
const toggleTheme = () => {
theme.value = theme.value === 'dark' ? 'light' : 'dark';
applyTheme(theme.value);
if (setData.value.autoTheme) {
// 如果是自动模式,切换到手动模式并设置相反的主题
const newTheme = theme.value === 'dark' ? 'light' : 'dark';
setSetData({
autoTheme: false,
manualTheme: newTheme
});
theme.value = newTheme;
applyTheme(newTheme);
// 停止监听系统主题
if (systemThemeCleanup) {
systemThemeCleanup();
systemThemeCleanup = null;
}
} else {
// 手动模式下正常切换
const newTheme = theme.value === 'dark' ? 'light' : 'dark';
theme.value = newTheme;
setSetData({ manualTheme: newTheme });
applyTheme(newTheme);
}
};
const setAutoTheme = (auto: boolean) => {
setSetData({ autoTheme: auto });
if (auto) {
// 启用自动模式
const systemTheme = getSystemTheme();
theme.value = systemTheme;
applyTheme(systemTheme);
// 开始监听系统主题变化
systemThemeCleanup = watchSystemTheme((newTheme) => {
if (setData.value.autoTheme) {
theme.value = newTheme;
applyTheme(newTheme);
}
});
} else {
// 切换到手动模式
const manualTheme = setData.value.manualTheme || 'light';
theme.value = manualTheme;
applyTheme(manualTheme);
// 停止监听系统主题
if (systemThemeCleanup) {
systemThemeCleanup();
systemThemeCleanup = null;
}
}
};
const setMiniMode = (value: boolean) => {
@@ -106,7 +158,14 @@ export const useSettingsStore = defineStore('settings', () => {
};
const initializeTheme = () => {
applyTheme(theme.value);
// 根据设置初始化主题
if (setData.value.autoTheme) {
setAutoTheme(true);
} else {
const manualTheme = setData.value.manualTheme || getCurrentTheme();
theme.value = manualTheme;
applyTheme(manualTheme);
}
};
const initializeSystemFonts = async () => {
@@ -133,6 +192,7 @@ export const useSettingsStore = defineStore('settings', () => {
showDownloadDrawer,
setSetData,
toggleTheme,
setAutoTheme,
setMiniMode,
setShowUpdateModal,
setShowArtistDrawer,

View File

@@ -1,5 +1,13 @@
export type ThemeType = 'dark' | 'light';
// 检测系统主题
export const getSystemTheme = (): ThemeType => {
if (typeof window !== 'undefined' && window.matchMedia) {
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}
return 'light';
};
// 应用主题
export const applyTheme = (theme: ThemeType) => {
// 使用 Tailwind 的暗色主题类
@@ -17,3 +25,21 @@ export const applyTheme = (theme: ThemeType) => {
export const getCurrentTheme = (): ThemeType => {
return (localStorage.getItem('theme') as ThemeType) || 'light';
};
// 监听系统主题变化
export const watchSystemTheme = (callback: (theme: ThemeType) => void) => {
if (typeof window !== 'undefined' && window.matchMedia) {
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
const handler = (e: MediaQueryListEvent) => {
callback(e.matches ? 'dark' : 'light');
};
mediaQuery.addEventListener('change', handler);
// 返回清理函数
return () => {
mediaQuery.removeEventListener('change', handler);
};
}
return () => {};
};

View File

@@ -25,10 +25,25 @@
<div class="set-item-title">{{ t('settings.basic.themeMode') }}</div>
<div class="set-item-content">{{ t('settings.basic.themeModeDesc') }}</div>
</div>
<n-switch v-model:value="isDarkTheme">
<template #checked><i class="ri-moon-line"></i></template>
<template #unchecked><i class="ri-sun-line"></i></template>
</n-switch>
<div class="flex items-center gap-3">
<div class="flex items-center gap-2">
<n-switch v-model:value="setData.autoTheme" @update:value="handleAutoThemeChange">
<template #checked><i class="ri-smartphone-line"></i></template>
<template #unchecked><i class="ri-settings-line"></i></template>
</n-switch>
<span class="text-sm text-gray-500">
{{ setData.autoTheme ? t('settings.basic.autoTheme') : t('settings.basic.manualTheme') }}
</span>
</div>
<n-switch
v-model:value="isDarkTheme"
:disabled="setData.autoTheme"
:class="{ 'opacity-50': setData.autoTheme }"
>
<template #checked><i class="ri-moon-line"></i></template>
<template #unchecked><i class="ri-sun-line"></i></template>
</n-switch>
</div>
</div>
<!-- 语言设置 -->
@@ -590,6 +605,10 @@ const isDarkTheme = computed({
set: () => settingsStore.toggleTheme()
});
const handleAutoThemeChange = (value: boolean) => {
settingsStore.setAutoTheme(value);
};
const openAuthor = () => {
window.open(setData.value.authorUrl);
};