Files
assetx/landingpage/contexts/ThemeContext.tsx
default 2ee4553b71 init: 初始化 AssetX 项目仓库
包含 webapp(Next.js 用户端)、webapp-back(Go 后端)、
antdesign(管理后台)、landingpage(营销落地页)、
数据库 SQL 和配置文件。
2026-03-27 11:26:43 +00:00

109 lines
2.8 KiB
TypeScript

'use client';
import { createContext, useContext, useState, useEffect, useCallback, useMemo, ReactNode } from 'react';
type Theme = 'light' | 'dark';
interface ThemeContextType {
theme: Theme;
toggleTheme: () => void;
setTheme: (theme: Theme) => void;
isSystemTheme: boolean;
}
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
// 纯函数,放在组件外避免闭包问题
function getSystemTheme(): Theme {
if (typeof window === 'undefined') return 'light';
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}
function isNightTime(): boolean {
if (typeof window === 'undefined') return false;
const hour = new Date().getHours();
return hour >= 18 || hour < 6;
}
function getAutoTheme(): Theme {
if (getSystemTheme() === 'dark') return 'dark';
if (isNightTime()) return 'dark';
return 'light';
}
export function ThemeProvider({ children }: { children: ReactNode }) {
const [theme, setThemeState] = useState<Theme>('light');
const [isSystemTheme, setIsSystemTheme] = useState(true);
const [mounted, setMounted] = useState(false);
// 设置主题(仅当前会话,不持久化)
const setTheme = useCallback((newTheme: Theme) => {
setThemeState(newTheme);
setIsSystemTheme(false);
}, []);
// 切换主题
const toggleTheme = useCallback(() => {
setThemeState(prev => prev === 'light' ? 'dark' : 'light');
setIsSystemTheme(false);
}, []);
// 初始化:始终跟随系统主题 + 时间
useEffect(() => {
setThemeState(getAutoTheme());
setIsSystemTheme(true);
setMounted(true);
}, []);
// 监听系统主题变化
useEffect(() => {
if (!isSystemTheme) return;
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
const handleChange = () => {
setThemeState(getAutoTheme());
};
mediaQuery.addEventListener('change', handleChange);
return () => mediaQuery.removeEventListener('change', handleChange);
}, [isSystemTheme]);
// 每分钟检查时间变化
useEffect(() => {
if (!isSystemTheme) return;
const interval = setInterval(() => {
setThemeState(getAutoTheme());
}, 60000);
return () => clearInterval(interval);
}, [isSystemTheme]);
// 在 DOM 上设置主题属性
useEffect(() => {
if (mounted) {
document.documentElement.setAttribute('data-theme', theme);
}
}, [theme, mounted]);
const value = useMemo(
() => ({ theme, toggleTheme, setTheme, isSystemTheme }),
[theme, toggleTheme, setTheme, isSystemTheme]
);
return (
<ThemeContext.Provider value={value}>
{children}
</ThemeContext.Provider>
);
}
export function useTheme() {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useTheme must be used within a ThemeProvider');
}
return context;
}