feat: Add theme and dark mode

This commit is contained in:
shiyu
2025-09-08 15:20:49 +08:00
parent f40ff4d751
commit d41b72d0ce
22 changed files with 525 additions and 100 deletions

View File

@@ -1,9 +1,11 @@
import { Form, Input, Button, message, Tabs, Space, Card, Select, Modal } from 'antd';
import { Form, Input, Button, message, Tabs, Space, Card, Select, Modal, Radio, InputNumber } from 'antd';
import { useEffect, useState } from 'react';
import PageCard from '../../components/PageCard';
import { getAllConfig, setConfig } from '../../api/config';
import { vectorDBApi } from '../../api/vectorDB';
import { AppstoreOutlined, RobotOutlined, DatabaseOutlined } from '@ant-design/icons';
import { AppstoreOutlined, RobotOutlined, DatabaseOutlined, SkinOutlined } from '@ant-design/icons';
import { useTheme } from '../../contexts/ThemeContext';
import '../../styles/settings-tabs.css';
const APP_CONFIG_KEYS: {key: string, label: string, default?: string}[] = [
{ key: 'APP_NAME', label: '应用名称' },
@@ -26,10 +28,20 @@ const EMBED_CONFIG_KEYS = [
const ALL_AI_KEYS = [...VISION_CONFIG_KEYS, ...EMBED_CONFIG_KEYS];
// Theme related config keys
const THEME_KEYS = {
MODE: 'THEME_MODE',
PRIMARY: 'THEME_PRIMARY_COLOR',
RADIUS: 'THEME_BORDER_RADIUS',
TOKENS: 'THEME_CUSTOM_TOKENS',
CSS: 'THEME_CUSTOM_CSS',
};
export default function SystemSettingsPage() {
const [loading, setLoading] = useState(false);
const [config, setConfigState] = useState<Record<string, string> | null>(null);
const [activeTab, setActiveTab] = useState('app');
const [activeTab, setActiveTab] = useState('appearance');
const { refreshTheme, previewTheme } = useTheme();
useEffect(() => {
getAllConfig().then((data) => setConfigState(data as Record<string, string>));
@@ -43,12 +55,23 @@ export default function SystemSettingsPage() {
}
message.success('保存成功');
setConfigState({ ...config, ...values });
// trigger theme refresh if related keys changed
if (Object.keys(values).some(k => Object.values(THEME_KEYS).includes(k))) {
await refreshTheme();
}
} catch (e: any) {
message.error(e.message || '保存失败');
}
setLoading(false);
};
// 离开“外观设置”时,恢复后端持久化配置(取消未保存的预览)
useEffect(() => {
if (activeTab !== 'appearance') {
refreshTheme();
}
}, [activeTab]);
if (!config) {
return <PageCard title='系统设置'><div>...</div></PageCard>;
}
@@ -59,11 +82,92 @@ export default function SystemSettingsPage() {
>
<Space direction="vertical" style={{ width: '100%' }} size={32}>
<Tabs
className="fx-settings-tabs"
activeKey={activeTab}
onChange={setActiveTab}
centered
tabPosition="left"
items={[
{
key: 'appearance',
label: (
<span>
<SkinOutlined style={{ marginRight: 8 }} />
</span>
),
children: (
<Form
layout="vertical"
initialValues={{
[THEME_KEYS.MODE]: config[THEME_KEYS.MODE] ?? 'light',
[THEME_KEYS.PRIMARY]: config[THEME_KEYS.PRIMARY] ?? '#111111',
[THEME_KEYS.RADIUS]: Number(config[THEME_KEYS.RADIUS] ?? '10'),
[THEME_KEYS.TOKENS]: config[THEME_KEYS.TOKENS] ?? '',
[THEME_KEYS.CSS]: config[THEME_KEYS.CSS] ?? '',
}}
onValuesChange={(_, all) => {
try {
const tokens = all[THEME_KEYS.TOKENS] ? JSON.parse(all[THEME_KEYS.TOKENS]) : undefined;
previewTheme({
mode: all[THEME_KEYS.MODE],
primaryColor: all[THEME_KEYS.PRIMARY],
borderRadius: typeof all[THEME_KEYS.RADIUS] === 'number' ? all[THEME_KEYS.RADIUS] : undefined,
customTokens: tokens,
customCSS: all[THEME_KEYS.CSS],
});
} catch {
// JSON 不合法时忽略 tokens 预览,其他项仍然生效
previewTheme({
mode: all[THEME_KEYS.MODE],
primaryColor: all[THEME_KEYS.PRIMARY],
borderRadius: typeof all[THEME_KEYS.RADIUS] === 'number' ? all[THEME_KEYS.RADIUS] : undefined,
customCSS: all[THEME_KEYS.CSS],
});
}
}}
onFinish={async (vals) => {
// Validate JSON if provided
if (vals[THEME_KEYS.TOKENS]) {
try { JSON.parse(vals[THEME_KEYS.TOKENS]); }
catch { return message.error('高级 Token 需为合法 JSON'); }
}
await handleSave(vals);
}}
style={{ marginTop: 24 }}
key={'appearance-' + JSON.stringify(config)}
>
<Card title="主题">
<Form.Item name={THEME_KEYS.MODE} label="主题模式">
<Radio.Group buttonStyle="solid">
<Radio.Button value="light"></Radio.Button>
<Radio.Button value="dark"></Radio.Button>
<Radio.Button value="system"></Radio.Button>
</Radio.Group>
</Form.Item>
<Form.Item name={THEME_KEYS.PRIMARY} label="主色">
<Input type="color" size="large" />
</Form.Item>
<Form.Item name={THEME_KEYS.RADIUS} label="圆角">
<InputNumber min={0} max={24} style={{ width: '100%' }} />
</Form.Item>
</Card>
<Card title="高级" style={{ marginTop: 24 }}>
<Form.Item name={THEME_KEYS.TOKENS} label="覆盖 AntD TokenJSON" tooltip="例如:{ &quot;colorText&quot;: &quot;#222&quot; }">
<Input.TextArea autoSize={{ minRows: 4 }} placeholder='{ "colorText": "#222" }' />
</Form.Item>
<Form.Item name={THEME_KEYS.CSS} label="自定义 CSS">
<Input.TextArea autoSize={{ minRows: 6 }} placeholder={":root{ }\n/* 支持任意 CSS */"} />
</Form.Item>
</Card>
<Form.Item style={{ marginTop: 24 }}>
<Button type="primary" htmlType="submit" loading={loading} block>
</Button>
</Form.Item>
</Form>
)
},
{
key: 'app',
label: (