mirror of
https://github.com/DrizzleTime/Foxel.git
synced 2026-06-07 16:39:36 +08:00
feat: Add theme and dark mode
This commit is contained in:
@@ -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 Token(JSON)" tooltip="例如:{ "colorText": "#222" }">
|
||||
<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: (
|
||||
|
||||
Reference in New Issue
Block a user