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:
225
apps/web/src/pages/settings/SystemSettings.tsx
Normal file
225
apps/web/src/pages/settings/SystemSettings.tsx
Normal file
@@ -0,0 +1,225 @@
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
Button,
|
||||
Form,
|
||||
Input,
|
||||
Space,
|
||||
Popconfirm,
|
||||
message,
|
||||
Typography,
|
||||
Table,
|
||||
Modal,
|
||||
} from 'antd';
|
||||
import { PlusOutlined, SearchOutlined } from '@ant-design/icons';
|
||||
import client from '../../api/client';
|
||||
|
||||
// --- Types ---
|
||||
|
||||
interface SettingEntry {
|
||||
key: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
// --- Component ---
|
||||
|
||||
export default function SystemSettings() {
|
||||
const [entries, setEntries] = useState<SettingEntry[]>([]);
|
||||
const [searchKey, setSearchKey] = useState('');
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [editEntry, setEditEntry] = useState<SettingEntry | null>(null);
|
||||
const [form] = Form.useForm();
|
||||
|
||||
const handleSearch = async () => {
|
||||
if (!searchKey.trim()) {
|
||||
message.warning('请输入设置键名');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const { data: resp } = await client.get(
|
||||
`/config/settings/${encodeURIComponent(searchKey.trim())}`,
|
||||
);
|
||||
const value = resp.data?.setting_value ?? resp.data?.value ?? resp.setting_value ?? resp.value ?? '';
|
||||
|
||||
// Check if already in local list
|
||||
setEntries((prev) => {
|
||||
const exists = prev.findIndex((e) => e.key === searchKey.trim());
|
||||
if (exists >= 0) {
|
||||
const updated = [...prev];
|
||||
updated[exists] = { ...updated[exists], value: String(value) };
|
||||
return updated;
|
||||
}
|
||||
return [...prev, { key: searchKey.trim(), value: String(value) }];
|
||||
});
|
||||
message.success('查询成功');
|
||||
} catch (err: unknown) {
|
||||
const status = (err as { response?: { status?: number } })?.response?.status;
|
||||
if (status === 404) {
|
||||
message.info('该设置键不存在,可点击"添加设置"创建');
|
||||
} else {
|
||||
message.error('查询失败');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = async (values: { setting_key: string; setting_value: string }) => {
|
||||
const key = values.setting_key.trim();
|
||||
const value = values.setting_value;
|
||||
try {
|
||||
// Validate JSON
|
||||
try {
|
||||
JSON.parse(value);
|
||||
} catch {
|
||||
message.error('设置值必须是有效的 JSON 格式');
|
||||
return;
|
||||
}
|
||||
|
||||
await client.put(`/config/settings/${encodeURIComponent(key)}`, {
|
||||
setting_value: value,
|
||||
});
|
||||
|
||||
setEntries((prev) => {
|
||||
const exists = prev.findIndex((e) => e.key === key);
|
||||
if (exists >= 0) {
|
||||
const updated = [...prev];
|
||||
updated[exists] = { key, value };
|
||||
return updated;
|
||||
}
|
||||
return [...prev, { key, value }];
|
||||
});
|
||||
|
||||
message.success('设置已保存');
|
||||
closeModal();
|
||||
} catch (err: unknown) {
|
||||
const errorMsg =
|
||||
(err as { response?: { data?: { message?: string } } })?.response?.data
|
||||
?.message || '保存失败';
|
||||
message.error(errorMsg);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (key: string) => {
|
||||
try {
|
||||
await client.delete(`/config/settings/${encodeURIComponent(key)}`);
|
||||
setEntries((prev) => prev.filter((e) => e.key !== key));
|
||||
message.success('设置已删除');
|
||||
} catch {
|
||||
message.error('删除失败');
|
||||
}
|
||||
};
|
||||
|
||||
const openCreate = () => {
|
||||
setEditEntry(null);
|
||||
form.resetFields();
|
||||
setModalOpen(true);
|
||||
};
|
||||
|
||||
const openEdit = (entry: SettingEntry) => {
|
||||
setEditEntry(entry);
|
||||
form.setFieldsValue({
|
||||
setting_key: entry.key,
|
||||
setting_value: entry.value,
|
||||
});
|
||||
setModalOpen(true);
|
||||
};
|
||||
|
||||
const closeModal = () => {
|
||||
setModalOpen(false);
|
||||
setEditEntry(null);
|
||||
form.resetFields();
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{ title: '键', dataIndex: 'key', key: 'key', width: 250 },
|
||||
{
|
||||
title: '值 (JSON)',
|
||||
dataIndex: 'value',
|
||||
key: 'value',
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
width: 180,
|
||||
render: (_: unknown, record: SettingEntry) => (
|
||||
<Space>
|
||||
<Button size="small" onClick={() => openEdit(record)}>
|
||||
编辑
|
||||
</Button>
|
||||
<Popconfirm
|
||||
title="确定删除此设置?"
|
||||
onConfirm={() => handleDelete(record.key)}
|
||||
>
|
||||
<Button size="small" danger>
|
||||
删除
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
marginBottom: 16,
|
||||
}}
|
||||
>
|
||||
<Typography.Title level={5} style={{ margin: 0 }}>
|
||||
系统参数
|
||||
</Typography.Title>
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={openCreate}>
|
||||
添加设置
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Space style={{ marginBottom: 16 }} size="middle">
|
||||
<Input
|
||||
placeholder="输入设置键名查询"
|
||||
value={searchKey}
|
||||
onChange={(e) => setSearchKey(e.target.value)}
|
||||
onPressEnter={handleSearch}
|
||||
style={{ width: 300 }}
|
||||
/>
|
||||
<Button icon={<SearchOutlined />} onClick={handleSearch}>
|
||||
查询
|
||||
</Button>
|
||||
</Space>
|
||||
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={entries}
|
||||
rowKey="key"
|
||||
size="small"
|
||||
pagination={false}
|
||||
/>
|
||||
|
||||
<Modal
|
||||
title={editEntry ? '编辑设置' : '添加设置'}
|
||||
open={modalOpen}
|
||||
onCancel={closeModal}
|
||||
onOk={() => form.submit()}
|
||||
width={560}
|
||||
>
|
||||
<Form form={form} onFinish={handleSave} layout="vertical">
|
||||
<Form.Item
|
||||
name="setting_key"
|
||||
label="键名"
|
||||
rules={[{ required: true, message: '请输入设置键名' }]}
|
||||
>
|
||||
<Input disabled={!!editEntry} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="setting_value"
|
||||
label="值 (JSON)"
|
||||
rules={[{ required: true, message: '请输入设置值' }]}
|
||||
>
|
||||
<Input.TextArea rows={6} placeholder='{"key": "value"}' />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user