feat(config): add system configuration module (Phase 3)
Implement the complete erp-config crate with: - Data dictionaries (CRUD + items management) - Dynamic menus (tree structure with role filtering) - System settings (hierarchical: platform > tenant > org > user) - Numbering rules (concurrency-safe via PostgreSQL advisory_lock) - Theme and language configuration (via settings store) - 6 database migrations (dictionaries, menus, settings, numbering_rules) - Frontend Settings page with 5 tabs (dictionary, menu, numbering, settings, theme) Refactor: move RBAC functions (require_permission) from erp-auth to erp-core to avoid cross-module dependencies. Add 20 new seed permissions for config module operations.
This commit is contained in:
104
apps/web/src/pages/settings/ThemeSettings.tsx
Normal file
104
apps/web/src/pages/settings/ThemeSettings.tsx
Normal file
@@ -0,0 +1,104 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { Form, Input, Select, Button, ColorPicker, message, Typography } from 'antd';
|
||||
import client from '../../api/client';
|
||||
|
||||
// --- Types ---
|
||||
|
||||
interface ThemeConfig {
|
||||
primary_color?: string;
|
||||
logo_url?: string;
|
||||
sidebar_style?: 'light' | 'dark';
|
||||
}
|
||||
|
||||
// --- Component ---
|
||||
|
||||
export default function ThemeSettings() {
|
||||
const [form] = Form.useForm();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
const fetchTheme = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const { data: resp } = await client.get('/config/themes');
|
||||
const theme: ThemeConfig = resp.data ?? resp;
|
||||
form.setFieldsValue({
|
||||
primary_color: theme.primary_color || '#1677ff',
|
||||
logo_url: theme.logo_url || '',
|
||||
sidebar_style: theme.sidebar_style || 'light',
|
||||
});
|
||||
} catch {
|
||||
// Theme may not exist yet; use defaults
|
||||
form.setFieldsValue({
|
||||
primary_color: '#1677ff',
|
||||
logo_url: '',
|
||||
sidebar_style: 'light',
|
||||
});
|
||||
}
|
||||
setLoading(false);
|
||||
}, [form]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchTheme();
|
||||
}, [fetchTheme]);
|
||||
|
||||
const handleSave = async (values: {
|
||||
primary_color: string;
|
||||
logo_url: string;
|
||||
sidebar_style: 'light' | 'dark';
|
||||
}) => {
|
||||
setSaving(true);
|
||||
try {
|
||||
await client.put('/config/themes', {
|
||||
primary_color:
|
||||
typeof values.primary_color === 'string'
|
||||
? values.primary_color
|
||||
: (values.primary_color as { toHexString?: () => string }).toHexString?.() ?? String(values.primary_color),
|
||||
logo_url: values.logo_url,
|
||||
sidebar_style: values.sidebar_style,
|
||||
});
|
||||
message.success('主题设置已保存');
|
||||
} catch (err: unknown) {
|
||||
const errorMsg =
|
||||
(err as { response?: { data?: { message?: string } } })?.response?.data
|
||||
?.message || '保存失败';
|
||||
message.error(errorMsg);
|
||||
}
|
||||
setSaving(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Typography.Title level={5} style={{ marginBottom: 16 }}>
|
||||
主题设置
|
||||
</Typography.Title>
|
||||
|
||||
<Form
|
||||
form={form}
|
||||
onFinish={handleSave}
|
||||
layout="vertical"
|
||||
style={{ maxWidth: 480 }}
|
||||
>
|
||||
<Form.Item name="primary_color" label="主色调">
|
||||
<ColorPicker format="hex" />
|
||||
</Form.Item>
|
||||
<Form.Item name="logo_url" label="Logo URL">
|
||||
<Input placeholder="https://example.com/logo.png" />
|
||||
</Form.Item>
|
||||
<Form.Item name="sidebar_style" label="侧边栏风格">
|
||||
<Select
|
||||
options={[
|
||||
{ label: '亮色', value: 'light' },
|
||||
{ label: '暗色', value: 'dark' },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Button type="primary" htmlType="submit" loading={saving}>
|
||||
保存
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user