新增 useThemePreference hook 和纯工具函数,支持系统/明亮/黑暗三态主题选择、 matchMedia 系统主题跟随、localStorage 持久化和启动期主题预应用,通过 <html theme-mode> 驱动 TDesign 主题变量切换。 Header 右侧控件重新组织为 .dashboard-header-controls 单行桌面布局,主题 RadioGroup 位于刷新频率 RadioGroup 前。 附带:build.ts import specifier 改为跨平台 sep 转换;config-loader 测试适配 Windows PATH 和 YAML 路径转义;test-utils 类型窄化修复。
74 lines
2.5 KiB
TypeScript
74 lines
2.5 KiB
TypeScript
import { useEffect, useState } from "react";
|
|
|
|
export type EffectiveTheme = "dark" | "light";
|
|
export type ThemePreference = "dark" | "light" | "system";
|
|
|
|
export const THEME_PREFERENCE_STORAGE_KEY = "dial.theme.preference";
|
|
export const THEME_MEDIA_QUERY = "(prefers-color-scheme: dark)";
|
|
|
|
export function applyInitialThemePreference() {
|
|
applyThemeMode(resolveEffectiveTheme(readThemePreference(), getSystemPrefersDark()));
|
|
}
|
|
|
|
export function applyThemeMode(theme: EffectiveTheme, root: HTMLElement = document.documentElement) {
|
|
root.setAttribute("theme-mode", theme);
|
|
}
|
|
|
|
export function getSystemPrefersDark(matchMedia: Window["matchMedia"] = window.matchMedia): boolean {
|
|
try {
|
|
return matchMedia(THEME_MEDIA_QUERY).matches;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
export function parseThemePreference(value: unknown): ThemePreference {
|
|
return value === "dark" || value === "light" || value === "system" ? value : "system";
|
|
}
|
|
|
|
export function readThemePreference(storage: Storage = window.localStorage): ThemePreference {
|
|
try {
|
|
return parseThemePreference(storage.getItem(THEME_PREFERENCE_STORAGE_KEY));
|
|
} catch {
|
|
return "system";
|
|
}
|
|
}
|
|
|
|
export function resolveEffectiveTheme(preference: ThemePreference, systemPrefersDark: boolean): EffectiveTheme {
|
|
if (preference === "dark" || preference === "light") return preference;
|
|
return systemPrefersDark ? "dark" : "light";
|
|
}
|
|
|
|
export function useThemePreference() {
|
|
const [preference, setPreferenceState] = useState<ThemePreference>(() => readThemePreference());
|
|
const [systemPrefersDark, setSystemPrefersDark] = useState(() => getSystemPrefersDark());
|
|
const effectiveTheme = resolveEffectiveTheme(preference, systemPrefersDark);
|
|
|
|
useEffect(() => {
|
|
applyThemeMode(effectiveTheme);
|
|
}, [effectiveTheme]);
|
|
|
|
useEffect(() => {
|
|
const mediaQueryList = window.matchMedia(THEME_MEDIA_QUERY);
|
|
|
|
const handleChange = (event: MediaQueryListEvent) => setSystemPrefersDark(event.matches);
|
|
mediaQueryList.addEventListener("change", handleChange);
|
|
return () => mediaQueryList.removeEventListener("change", handleChange);
|
|
}, []);
|
|
|
|
const setPreference = (nextPreference: ThemePreference) => {
|
|
setPreferenceState(nextPreference);
|
|
writeThemePreference(nextPreference);
|
|
};
|
|
|
|
return { effectiveTheme, preference, setPreference };
|
|
}
|
|
|
|
export function writeThemePreference(preference: ThemePreference, storage: Storage = window.localStorage) {
|
|
try {
|
|
storage.setItem(THEME_PREFERENCE_STORAGE_KEY, preference);
|
|
} catch {
|
|
// 存储不可用时仅使用当前内存状态,避免阻断 Dashboard 渲染。
|
|
}
|
|
}
|