diff --git a/src/containers/AppContainer/AppContainer.tsx b/src/containers/AppContainer/AppContainer.tsx index a4aef5df..5bc28c5d 100644 --- a/src/containers/AppContainer/AppContainer.tsx +++ b/src/containers/AppContainer/AppContainer.tsx @@ -1,24 +1,21 @@ -import { memo, ReactElement, ReactNode, useMemo } from 'react'; +import { memo, ReactElement, ReactNode } from 'react'; -import { ThemeModeContext } from '@/context'; -import { AntdStylish, AntdToken, DisplayTheme, FullToken, ThemeMode } from '@/types'; +import { ThemeAppearance, ThemeMode } from '@/types'; -import { useAntdTheme } from '@/hooks/useAntdTheme'; -import { ThemeProvider } from '../ThemeProvider'; -import { AntdProvider, type AntdProviderProps } from './AntdProvider'; +import { type AntdProviderProps } from './AntdProvider'; +import ThemeContent, { ThemeContentProps } from './ThemeContent'; +import ThemeSwitcher from './ThemeSwitcher'; -type GetCustomToken = ({ token }: { token: AntdToken }) => T; - -type GetCustomStylish = ({ token, stylish }: { token: FullToken; stylish: AntdStylish }) => S; - -export interface AppContainerProps> extends AntdProviderProps { +export interface AppContainerProps> + extends AntdProviderProps, + ThemeContentProps { /** * 应用的展示外观主题,只存在亮色和暗色两种 * @default light */ - appearance?: DisplayTheme; - defaultAppearance?: DisplayTheme; - onAppearanceChange?: (mode: DisplayTheme) => void; + appearance?: ThemeAppearance; + defaultAppearance?: ThemeAppearance; + onAppearanceChange?: (mode: ThemeAppearance) => void; /** * 主题的展示模式,有三种配置:跟随系统、亮色、暗色 * 默认不开启自动模式,需要手动进行配置 @@ -27,68 +24,31 @@ export interface AppContainerProps> extends AntdPr themeMode?: ThemeMode; children: ReactNode; - /** - * 自定义 Token - */ - customToken?: T | GetCustomToken; - /** - * 自定义 Stylish - */ - customStylish?: S | GetCustomStylish; + className?: string; prefixCls?: string; } -const Content: ( - props: Pick, 'customStylish' | 'customToken'> & - AntdProviderProps & { children: ReactNode }, -) => ReactElement | null = ({ - children, - customToken: customTokenOrFn, - customStylish: stylishOrGetStylish, - ...props -}) => { - const { stylish: antdStylish, ...token } = useAntdTheme(); - - // 获取 自定义 token - const customToken = useMemo(() => { - if (typeof customTokenOrFn === 'function') { - // @ts-ignore - return customTokenOrFn({ token }); - } - - return customTokenOrFn; - }, [token, customTokenOrFn]); - - // 获取 stylish - const customStylish = useMemo(() => { - if (typeof stylishOrGetStylish === 'function') { - // @ts-ignore - return stylishOrGetStylish({ token: { ...token, ...customToken }, stylish: antdStylish }); - } - return stylishOrGetStylish; - }, []); - - return ( - - - {children} - - - ); -}; - export const AppContainer: (props: AppContainerProps) => ReactElement | null = memo( - ({ children, appearance, themeMode, customToken, customStylish, ...props }) => ( - ( + - + {children} - - + + ), ); diff --git a/src/containers/AppContainer/ThemeContent.tsx b/src/containers/AppContainer/ThemeContent.tsx new file mode 100644 index 00000000..b2cb5b9b --- /dev/null +++ b/src/containers/AppContainer/ThemeContent.tsx @@ -0,0 +1,90 @@ +import { ReactElement, ReactNode, useMemo } from 'react'; + +import { AntdStylish, AntdToken, FullToken, ThemeAppearance } from '@/types'; + +import { useThemeMode } from '@/hooks'; +import { useAntdTheme } from '@/hooks/useAntdTheme'; +import { theme } from 'antd'; +import { ThemeConfig } from 'antd/es/config-provider/context'; +import { ThemeProvider } from '../ThemeProvider'; +import { AntdProvider, type AntdProviderProps } from './AntdProvider'; + +export type GetCustomToken = (theme: { token: AntdToken; appearance: ThemeAppearance }) => T; + +export type GetCustomStylish = (theme: { + token: FullToken; + stylish: AntdStylish; + appearance: ThemeAppearance; +}) => S; + +export interface ThemeContentProps> extends AntdProviderProps { + children: ReactNode; + /** + * 自定义 Token + */ + customToken?: T | GetCustomToken; + /** + * 自定义 Stylish + */ + customStylish?: S | GetCustomStylish; +} + +const ThemeContent: (props: ThemeContentProps) => ReactElement | null = ({ + children, + customToken: customTokenOrFn, + customStylish: stylishOrGetStylish, + theme: themeProp, + ...props +}) => { + const { appearance, isDarkMode } = useThemeMode(); + const { stylish: antdStylish, ...token } = useAntdTheme(); + + // 获取 自定义 token + const customToken = useMemo(() => { + if (typeof customTokenOrFn === 'function') { + // @ts-ignore + return customTokenOrFn({ token, appearance }); + } + + return customTokenOrFn; + }, [customTokenOrFn, token, appearance]); + + // 获取 stylish + const customStylish = useMemo(() => { + if (typeof stylishOrGetStylish === 'function') { + // @ts-ignore + return stylishOrGetStylish({ token: { ...token, ...customToken }, stylish: antdStylish }); + } + return stylishOrGetStylish; + }, [stylishOrGetStylish, token, customToken, antdStylish, appearance]); + + const antdTheme = useMemo(() => { + const baseAlgorithm = isDarkMode ? theme.darkAlgorithm : theme.defaultAlgorithm; + + if (!themeProp) { + return { algorithm: baseAlgorithm }; + } + + // 如果有 themeProp 说明是外部传入的 theme,需要对算法做一个合并处理,因此先把 themeProp 的算法规整为一个数组 + const algoProp = !themeProp.algorithm + ? [] + : themeProp.algorithm instanceof Array + ? themeProp.algorithm + : [themeProp.algorithm]; + + return { + ...themeProp, + algorithm: !themeProp.algorithm ? baseAlgorithm : [baseAlgorithm, ...algoProp], + }; + }, [themeProp, isDarkMode]); + + return ( + + + {children} + + + ); +}; + +export default ThemeContent; diff --git a/src/containers/AppContainer/ThemeSwitcher.tsx b/src/containers/AppContainer/ThemeSwitcher.tsx new file mode 100644 index 00000000..fa858256 --- /dev/null +++ b/src/containers/AppContainer/ThemeSwitcher.tsx @@ -0,0 +1,90 @@ +import { FC, memo, ReactNode, useCallback, useLayoutEffect } from 'react'; +import useControlledState from 'use-merge-value'; + +import { ThemeModeContext } from '@/context'; +import { ThemeAppearance, ThemeMode } from '@/types'; + +let darkThemeMatch: MediaQueryList; + +const matchThemeMode = (mode: ThemeAppearance) => matchMedia(`(prefers-color-scheme: ${mode})`); + +export interface ThemeSwitcherProps { + /** + * 应用的展示外观主题,只存在亮色和暗色两种 + * @default light + */ + appearance?: ThemeAppearance; + defaultAppearance?: ThemeAppearance; + onAppearanceChange?: (mode: ThemeAppearance) => void; + /** + * 主题的展示模式,有三种配置:跟随系统、亮色、暗色 + * 默认不开启自动模式,需要手动进行配置 + * @default light + */ + themeMode?: ThemeMode; + + children: ReactNode; +} + +const ThemeSwitcher: FC = memo( + ({ + children, + appearance: appearanceProp, + defaultAppearance, + onAppearanceChange, + themeMode = 'light', + }) => { + const [appearance, setAppearance] = useControlledState('light', { + value: appearanceProp, + defaultValue: defaultAppearance, + onChange: onAppearanceChange, + }); + + const matchBrowserTheme = useCallback(() => { + if (matchThemeMode('dark').matches) { + setAppearance('dark'); + } else { + setAppearance('light'); + } + }, [setAppearance]); + + useLayoutEffect(() => { + // 如果是自动的话,则去做一次匹配 + if (themeMode === 'auto') matchBrowserTheme(); + // 否则就明确设定亮暗色 + else setAppearance(themeMode); + }, [themeMode]); + + useLayoutEffect(() => { + if (!darkThemeMatch) { + darkThemeMatch = matchThemeMode('dark'); + } + + if (!themeMode || themeMode === 'auto') { + setTimeout(() => { + matchBrowserTheme(); + }, 100); + } + + darkThemeMatch.addEventListener('change', matchBrowserTheme); + + return () => { + darkThemeMatch.removeEventListener('change', matchBrowserTheme); + }; + }, []); + + return ( + + {children} + + ); + }, +); + +export default ThemeSwitcher; diff --git a/src/types/appearance.ts b/src/types/appearance.ts index 2d749873..7c0159e8 100644 --- a/src/types/appearance.ts +++ b/src/types/appearance.ts @@ -1,3 +1,3 @@ -export type DisplayTheme = 'dark' | 'light'; +export type ThemeAppearance = 'dark' | 'light'; export type ThemeMode = 'auto' | 'dark' | 'light';