import { computed, ref } from 'vue' import { defineStore } from 'pinia' /** 用户在界面上选择的主题偏好 */ export type ThemePreference = 'light' | 'dark' | 'auto' /** 实际生效的主题(auto 会被解析成二者之一) */ export type ThemeMode = 'light' | 'dark' /** 主题偏好本地存储 key(index.html 内联脚本会读取它做首屏防闪烁) */ export const THEME_STORAGE_KEY = 'tool-theme' const MEDIA_QUERY = '(prefers-color-scheme: dark)' /** 查询系统当前是否为深色(旧浏览器不支持 matchMedia 时按浅色处理) */ function systemPrefersDark(): boolean { if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return false return window.matchMedia(MEDIA_QUERY).matches } /** 读取初始偏好:本地存储优先,其次跟随系统 */ function resolveInitialPreference(): ThemePreference { try { const saved = localStorage.getItem(THEME_STORAGE_KEY) if (saved === 'light' || saved === 'dark' || saved === 'auto') return saved } catch { // 忽略存储不可用的场景 } return 'auto' } /** * 主题状态管理:浅色 / 深色 / 跟随系统 三态切换。 * * 之所以要有「跟随系统」这一态:用户手动选过一次之后, * 系统在日落时自动切深色时页面不会跟着变,体验会断裂。 * 三态让用户既能保留手动控制,也能交回给系统。 * * 实现上把「偏好」与「实际生效主题」分开: * preference → 用户选择,写 localStorage * mode → 真实生效值,写到 html[theme-mode](TDesign 依赖该属性) */ export const useThemeStore = defineStore('theme', () => { const preference = ref(resolveInitialPreference()) const systemDark = ref(systemPrefersDark()) /** 实际生效的主题 */ const mode = computed(() => { if (preference.value === 'auto') return systemDark.value ? 'dark' : 'light' return preference.value }) /** 把主题写到 DOM 并持久化偏好 */ function apply(next: ThemePreference) { preference.value = next // TDesign 通过 html[theme-mode] 切换整套设计变量,必须是 light / dark 具值 document.documentElement.setAttribute('theme-mode', mode.value) try { localStorage.setItem(THEME_STORAGE_KEY, next) } catch { // 忽略存储失败 } } /** * 三态循环切换:浅色 → 深色 → 跟随系统 → 浅色。 * 单按钮即可覆盖三种偏好,避免在顶栏塞三个按钮。 */ function cycle() { const order: ThemePreference[] = ['light', 'dark', 'auto'] const next = order[(order.indexOf(preference.value) + 1) % order.length] apply(next) } /** 直接指定偏好 */ function set(next: ThemePreference) { apply(next) } /** * 初始化:应用一次主题,并在「跟随系统」时监听系统主题变化。 * @returns 取消监听的清理函数(应用卸载时调用,避免监听器泄漏) */ function init(): () => void { apply(preference.value) if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') { return () => {} } const mql = window.matchMedia(MEDIA_QUERY) const onChange = (e: MediaQueryListEvent) => { systemDark.value = e.matches // 仅在跟随系统时同步 DOM;用户手动指定时不受系统影响 if (preference.value === 'auto') { document.documentElement.setAttribute('theme-mode', e.matches ? 'dark' : 'light') } } mql.addEventListener('change', onChange) return () => mql.removeEventListener('change', onChange) } return { preference, mode, apply, set, cycle, init } })