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:
64
apps/web/src/components/ThemeSwitcher.tsx
Normal file
64
apps/web/src/components/ThemeSwitcher.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
import { Dropdown } from 'antd';
|
||||
import { BgColorsOutlined } from '@ant-design/icons';
|
||||
import { useAppStore, THEME_OPTIONS } from '../stores/app';
|
||||
|
||||
export default function ThemeSwitcher() {
|
||||
const theme = useAppStore((s) => s.theme);
|
||||
const setTheme = useAppStore((s) => s.setTheme);
|
||||
|
||||
const content = (
|
||||
<div style={{
|
||||
padding: 8,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 6,
|
||||
minWidth: 220,
|
||||
background: 'var(--erp-bg-container)',
|
||||
borderRadius: 12,
|
||||
boxShadow: 'var(--erp-shadow-lg)',
|
||||
}}>
|
||||
{THEME_OPTIONS.map((opt) => {
|
||||
const active = theme === opt.key;
|
||||
return (
|
||||
<div
|
||||
key={opt.key}
|
||||
onClick={() => setTheme(opt.key)}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 12,
|
||||
padding: '10px 12px',
|
||||
borderRadius: 8,
|
||||
cursor: 'pointer',
|
||||
border: `2px solid ${active ? opt.preview.primary : 'transparent'}`,
|
||||
background: active ? `${opt.preview.primary}08` : 'transparent',
|
||||
transition: 'all 0.15s ease',
|
||||
}}
|
||||
>
|
||||
{/* 色块预览 */}
|
||||
<div style={{ display: 'flex', gap: 3, flexShrink: 0 }}>
|
||||
<div style={{ width: 20, height: 20, borderRadius: 4, background: opt.preview.primary }} />
|
||||
<div style={{ width: 20, height: 20, borderRadius: 4, background: opt.preview.bg, border: '1px solid #e0e0e0' }} />
|
||||
<div style={{ width: 20, height: 20, borderRadius: 4, background: opt.preview.surface, border: '1px solid #e0e0e0' }} />
|
||||
</div>
|
||||
<div style={{ flex: 1 }}>
|
||||
<div style={{ fontSize: 13, fontWeight: 600, color: active ? opt.preview.primary : '#333' }}>{opt.label}</div>
|
||||
<div style={{ fontSize: 11, color: '#999', marginTop: 1 }}>{opt.desc}</div>
|
||||
</div>
|
||||
{active && (
|
||||
<div style={{ width: 8, height: 8, borderRadius: 4, background: opt.preview.primary, flexShrink: 0 }} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<Dropdown dropdownRender={() => content} trigger={['click']} placement="bottomRight">
|
||||
<div className="erp-header-btn" title="切换主题">
|
||||
<BgColorsOutlined style={{ fontSize: 16 }} />
|
||||
</div>
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user