feat(web): 多主题系统 — 4 套主题 + CSS 变量 + Ant Design 动态主题
- CSS 变量层: :root 默认 blue, [data-theme] 覆盖 warm/dark/emerald - Ant Design: ConfigProvider 按 ThemeName 切换 token + algorithm - ThemeSwitcher: 下拉面板含 4 主题色块预览 + localStorage 持久化 - useThemeMode: 从 store 读取主题名替代色值比对(修复 33 页面暗色失效) - index.html: 添加 Noto Serif SC 字体(warm 主题衬线标题)
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useEffect, lazy, Suspense } from 'react';
|
||||
import { useEffect, lazy, Suspense, useMemo } from 'react';
|
||||
import { HashRouter, Routes, Route, Navigate } from 'react-router-dom';
|
||||
import { ConfigProvider, theme as antdTheme, Spin } from 'antd';
|
||||
import zhCN from 'antd/locale/zh_CN';
|
||||
@@ -7,6 +7,7 @@ import Login from './pages/Login';
|
||||
import { ErrorBoundary } from './components/ErrorBoundary';
|
||||
import { useAuthStore } from './stores/auth';
|
||||
import { useAppStore } from './stores/app';
|
||||
import type { ThemeName } from './stores/app';
|
||||
|
||||
const Home = lazy(() => import('./pages/Home'));
|
||||
const Users = lazy(() => import('./pages/Users'));
|
||||
@@ -68,93 +69,129 @@ function PrivateRoute({ children }: { children: React.ReactNode }) {
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
const themeConfig = {
|
||||
token: {
|
||||
colorPrimary: '#2563eb',
|
||||
colorSuccess: '#059669',
|
||||
colorWarning: '#d97706',
|
||||
colorError: '#dc2626',
|
||||
colorInfo: '#0284c7',
|
||||
colorBgLayout: '#f8fafc',
|
||||
colorBgContainer: '#ffffff',
|
||||
colorBgElevated: '#ffffff',
|
||||
colorBorder: '#e2e8f0',
|
||||
colorBorderSecondary: '#f1f5f9',
|
||||
borderRadius: 10,
|
||||
borderRadiusLG: 12,
|
||||
borderRadiusSM: 6,
|
||||
fontFamily: "'Noto Sans SC', -apple-system, system-ui, 'Segoe UI', Roboto, 'PingFang SC', 'Microsoft YaHei', Helvetica, Arial, sans-serif",
|
||||
fontSize: 14,
|
||||
fontSizeHeading4: 20,
|
||||
controlHeight: 40,
|
||||
controlHeightLG: 44,
|
||||
controlHeightSM: 32,
|
||||
boxShadow: 'none',
|
||||
boxShadowSecondary: '0 1px 3px rgba(0,0,0,0.06), 0 1px 2px rgba(0,0,0,0.04)',
|
||||
},
|
||||
components: {
|
||||
Button: {
|
||||
primaryShadow: 'none',
|
||||
fontWeight: 500,
|
||||
},
|
||||
Card: {
|
||||
paddingLG: 20,
|
||||
},
|
||||
Table: {
|
||||
headerBg: '#f1f5f9',
|
||||
headerColor: '#475569',
|
||||
rowHoverBg: '#f1f5f9',
|
||||
fontSize: 14,
|
||||
},
|
||||
Menu: {
|
||||
itemBorderRadius: 10,
|
||||
itemMarginInline: 8,
|
||||
itemHeight: 40,
|
||||
},
|
||||
Modal: {
|
||||
borderRadiusLG: 16,
|
||||
},
|
||||
Tag: {
|
||||
borderRadiusSM: 6,
|
||||
},
|
||||
},
|
||||
const baseToken = {
|
||||
borderRadius: 10,
|
||||
borderRadiusLG: 12,
|
||||
borderRadiusSM: 6,
|
||||
fontFamily: "'Noto Sans SC', -apple-system, system-ui, 'Segoe UI', Roboto, 'PingFang SC', 'Microsoft YaHei', Helvetica, Arial, sans-serif",
|
||||
fontSize: 14,
|
||||
fontSizeHeading4: 20,
|
||||
controlHeight: 40,
|
||||
controlHeightLG: 44,
|
||||
controlHeightSM: 32,
|
||||
boxShadow: 'none',
|
||||
boxShadowSecondary: '0 1px 3px rgba(0,0,0,0.06), 0 1px 2px rgba(0,0,0,0.04)',
|
||||
};
|
||||
|
||||
const darkThemeConfig = {
|
||||
...themeConfig,
|
||||
token: {
|
||||
...themeConfig.token,
|
||||
colorBgLayout: '#0f172a',
|
||||
colorBgContainer: '#1e293b',
|
||||
colorBgElevated: '#334155',
|
||||
colorBorder: '#334155',
|
||||
colorBorderSecondary: 'rgba(255, 255, 255, 0.06)',
|
||||
boxShadow: 'none',
|
||||
boxShadowSecondary: '0 2px 8px rgba(0,0,0,0.3), 0 1px 3px rgba(0,0,0,0.2)',
|
||||
const baseComponents = {
|
||||
Button: { primaryShadow: 'none', fontWeight: 500 },
|
||||
Card: { paddingLG: 20 },
|
||||
Menu: { itemBorderRadius: 10, itemMarginInline: 8, itemHeight: 40 },
|
||||
Modal: { borderRadiusLG: 16 },
|
||||
Tag: { borderRadiusSM: 6 },
|
||||
};
|
||||
|
||||
const themeConfigs: Record<ThemeName, { token: Record<string, unknown>; components: Record<string, Record<string, unknown>> }> = {
|
||||
blue: {
|
||||
token: {
|
||||
...baseToken,
|
||||
colorPrimary: '#2563eb',
|
||||
colorSuccess: '#059669',
|
||||
colorWarning: '#d97706',
|
||||
colorError: '#dc2626',
|
||||
colorInfo: '#0284c7',
|
||||
colorBgLayout: '#f8fafc',
|
||||
colorBgContainer: '#ffffff',
|
||||
colorBgElevated: '#ffffff',
|
||||
colorBorder: '#e2e8f0',
|
||||
colorBorderSecondary: '#f1f5f9',
|
||||
},
|
||||
components: {
|
||||
...baseComponents,
|
||||
Table: { headerBg: '#f1f5f9', headerColor: '#475569', rowHoverBg: '#f1f5f9', fontSize: 14 },
|
||||
},
|
||||
},
|
||||
components: {
|
||||
...themeConfig.components,
|
||||
Table: {
|
||||
headerBg: '#1e293b',
|
||||
headerColor: '#94a3b8',
|
||||
rowHoverBg: '#1e293b',
|
||||
warm: {
|
||||
token: {
|
||||
...baseToken,
|
||||
borderRadius: 12,
|
||||
borderRadiusLG: 14,
|
||||
borderRadiusSM: 8,
|
||||
colorPrimary: '#C4623A',
|
||||
colorSuccess: '#5B7A5E',
|
||||
colorWarning: '#C4873A',
|
||||
colorError: '#B54A4A',
|
||||
colorInfo: '#8B7A5E',
|
||||
colorBgLayout: '#F5F0EB',
|
||||
colorBgContainer: '#ffffff',
|
||||
colorBgElevated: '#ffffff',
|
||||
colorBorder: '#E8E2DC',
|
||||
colorBorderSecondary: '#F0EBE5',
|
||||
},
|
||||
components: {
|
||||
...baseComponents,
|
||||
Table: { headerBg: '#EDE8E2', headerColor: '#7A756E', rowHoverBg: '#F5F0EB', fontSize: 14 },
|
||||
},
|
||||
},
|
||||
dark: {
|
||||
token: {
|
||||
...baseToken,
|
||||
colorPrimary: '#60A5FA',
|
||||
colorSuccess: '#34D399',
|
||||
colorWarning: '#FBBF24',
|
||||
colorError: '#F87171',
|
||||
colorInfo: '#38BDF8',
|
||||
colorBgLayout: '#0F172A',
|
||||
colorBgContainer: '#1E293B',
|
||||
colorBgElevated: '#334155',
|
||||
colorBorder: '#334155',
|
||||
colorBorderSecondary: 'rgba(255,255,255,0.06)',
|
||||
boxShadow: 'none',
|
||||
boxShadowSecondary: '0 2px 8px rgba(0,0,0,0.3), 0 1px 3px rgba(0,0,0,0.2)',
|
||||
},
|
||||
components: {
|
||||
...baseComponents,
|
||||
Table: { headerBg: '#1E293B', headerColor: '#94A3B8', rowHoverBg: '#1E293B', fontSize: 14 },
|
||||
},
|
||||
},
|
||||
emerald: {
|
||||
token: {
|
||||
...baseToken,
|
||||
borderRadius: 10,
|
||||
borderRadiusLG: 14,
|
||||
borderRadiusSM: 8,
|
||||
colorPrimary: '#5B7A5E',
|
||||
colorSuccess: '#3D7A42',
|
||||
colorWarning: '#B8863A',
|
||||
colorError: '#A54A4A',
|
||||
colorInfo: '#4A7A8B',
|
||||
colorBgLayout: '#F4F7F4',
|
||||
colorBgContainer: '#ffffff',
|
||||
colorBgElevated: '#ffffff',
|
||||
colorBorder: '#D5DED5',
|
||||
colorBorderSecondary: '#E5ECE5',
|
||||
},
|
||||
components: {
|
||||
...baseComponents,
|
||||
Table: { headerBg: '#EDF2ED', headerColor: '#5A6E5A', rowHoverBg: '#F4F7F4', fontSize: 14 },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default function App() {
|
||||
const loadFromStorage = useAuthStore((s) => s.loadFromStorage);
|
||||
const themeMode = useAppStore((s) => s.theme);
|
||||
const themeName = useAppStore((s) => s.theme);
|
||||
|
||||
useEffect(() => {
|
||||
loadFromStorage();
|
||||
}, [loadFromStorage]);
|
||||
|
||||
useEffect(() => {
|
||||
document.documentElement.setAttribute('data-theme', themeMode);
|
||||
}, [themeMode]);
|
||||
document.documentElement.setAttribute('data-theme', themeName);
|
||||
}, [themeName]);
|
||||
|
||||
const isDark = themeMode === 'dark';
|
||||
const isDark = themeName === 'dark';
|
||||
const antTheme = useMemo(() => themeConfigs[themeName] ?? themeConfigs.blue, [themeName]);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -162,7 +199,7 @@ export default function App() {
|
||||
<ConfigProvider
|
||||
locale={zhCN}
|
||||
theme={{
|
||||
...isDark ? darkThemeConfig : themeConfig,
|
||||
...antTheme,
|
||||
algorithm: isDark ? antdTheme.darkAlgorithm : antdTheme.defaultAlgorithm,
|
||||
}}
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user