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:
358
apps/web/src/pages/settings/DictionaryManager.tsx
Normal file
358
apps/web/src/pages/settings/DictionaryManager.tsx
Normal file
@@ -0,0 +1,358 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import {
|
||||
Table,
|
||||
Button,
|
||||
Space,
|
||||
Modal,
|
||||
Form,
|
||||
Input,
|
||||
InputNumber,
|
||||
Popconfirm,
|
||||
message,
|
||||
Typography,
|
||||
Tag,
|
||||
} from 'antd';
|
||||
import { PlusOutlined } from '@ant-design/icons';
|
||||
import client from '../../api/client';
|
||||
|
||||
// --- Types ---
|
||||
|
||||
interface DictItem {
|
||||
id: string;
|
||||
label: string;
|
||||
value: string;
|
||||
sort_order: number;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
interface Dictionary {
|
||||
id: string;
|
||||
name: string;
|
||||
code: string;
|
||||
description?: string;
|
||||
items: DictItem[];
|
||||
}
|
||||
|
||||
// --- Component ---
|
||||
|
||||
export default function DictionaryManager() {
|
||||
const [dictionaries, setDictionaries] = useState<Dictionary[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [dictModalOpen, setDictModalOpen] = useState(false);
|
||||
const [editDict, setEditDict] = useState<Dictionary | null>(null);
|
||||
const [itemModalOpen, setItemModalOpen] = useState(false);
|
||||
const [activeDictId, setActiveDictId] = useState<string | null>(null);
|
||||
const [editItem, setEditItem] = useState<DictItem | null>(null);
|
||||
const [dictForm] = Form.useForm();
|
||||
const [itemForm] = Form.useForm();
|
||||
|
||||
const fetchDictionaries = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const { data: resp } = await client.get('/config/dictionaries');
|
||||
setDictionaries(resp.data ?? resp);
|
||||
} catch {
|
||||
message.error('加载字典列表失败');
|
||||
}
|
||||
setLoading(false);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
fetchDictionaries();
|
||||
}, [fetchDictionaries]);
|
||||
|
||||
// --- Dictionary CRUD ---
|
||||
|
||||
const handleDictSubmit = async (values: {
|
||||
name: string;
|
||||
code: string;
|
||||
description?: string;
|
||||
}) => {
|
||||
try {
|
||||
if (editDict) {
|
||||
await client.put(`/config/dictionaries/${editDict.id}`, values);
|
||||
message.success('字典更新成功');
|
||||
} else {
|
||||
await client.post('/config/dictionaries', values);
|
||||
message.success('字典创建成功');
|
||||
}
|
||||
closeDictModal();
|
||||
fetchDictionaries();
|
||||
} catch (err: unknown) {
|
||||
const errorMsg =
|
||||
(err as { response?: { data?: { message?: string } } })?.response?.data
|
||||
?.message || '操作失败';
|
||||
message.error(errorMsg);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteDict = async (id: string) => {
|
||||
try {
|
||||
await client.delete(`/config/dictionaries/${id}`);
|
||||
message.success('字典已删除');
|
||||
fetchDictionaries();
|
||||
} catch {
|
||||
message.error('删除失败');
|
||||
}
|
||||
};
|
||||
|
||||
const openEditDict = (dict: Dictionary) => {
|
||||
setEditDict(dict);
|
||||
dictForm.setFieldsValue({
|
||||
name: dict.name,
|
||||
code: dict.code,
|
||||
description: dict.description,
|
||||
});
|
||||
setDictModalOpen(true);
|
||||
};
|
||||
|
||||
const openCreateDict = () => {
|
||||
setEditDict(null);
|
||||
dictForm.resetFields();
|
||||
setDictModalOpen(true);
|
||||
};
|
||||
|
||||
const closeDictModal = () => {
|
||||
setDictModalOpen(false);
|
||||
setEditDict(null);
|
||||
dictForm.resetFields();
|
||||
};
|
||||
|
||||
// --- Dictionary Item CRUD ---
|
||||
|
||||
const openAddItem = (dictId: string) => {
|
||||
setActiveDictId(dictId);
|
||||
setEditItem(null);
|
||||
itemForm.resetFields();
|
||||
setItemModalOpen(true);
|
||||
};
|
||||
|
||||
const openEditItem = (dictId: string, item: DictItem) => {
|
||||
setActiveDictId(dictId);
|
||||
setEditItem(item);
|
||||
itemForm.setFieldsValue({
|
||||
label: item.label,
|
||||
value: item.value,
|
||||
sort_order: item.sort_order,
|
||||
color: item.color,
|
||||
});
|
||||
setItemModalOpen(true);
|
||||
};
|
||||
|
||||
const handleItemSubmit = async (values: {
|
||||
label: string;
|
||||
value: string;
|
||||
sort_order: number;
|
||||
color?: string;
|
||||
}) => {
|
||||
if (!activeDictId) return;
|
||||
try {
|
||||
if (editItem) {
|
||||
await client.put(
|
||||
`/config/dictionaries/${activeDictId}/items/${editItem.id}`,
|
||||
values,
|
||||
);
|
||||
message.success('字典项更新成功');
|
||||
} else {
|
||||
await client.post(`/config/dictionaries/${activeDictId}/items`, values);
|
||||
message.success('字典项添加成功');
|
||||
}
|
||||
closeItemModal();
|
||||
fetchDictionaries();
|
||||
} catch (err: unknown) {
|
||||
const errorMsg =
|
||||
(err as { response?: { data?: { message?: string } } })?.response?.data
|
||||
?.message || '操作失败';
|
||||
message.error(errorMsg);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteItem = async (dictId: string, itemId: string) => {
|
||||
try {
|
||||
await client.delete(`/config/dictionaries/${dictId}/items/${itemId}`);
|
||||
message.success('字典项已删除');
|
||||
fetchDictionaries();
|
||||
} catch {
|
||||
message.error('删除失败');
|
||||
}
|
||||
};
|
||||
|
||||
const closeItemModal = () => {
|
||||
setItemModalOpen(false);
|
||||
setActiveDictId(null);
|
||||
setEditItem(null);
|
||||
itemForm.resetFields();
|
||||
};
|
||||
|
||||
// --- Columns ---
|
||||
|
||||
const columns = [
|
||||
{ title: '名称', dataIndex: 'name', key: 'name' },
|
||||
{ title: '编码', dataIndex: 'code', key: 'code' },
|
||||
{
|
||||
title: '说明',
|
||||
dataIndex: 'description',
|
||||
key: 'description',
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
render: (_: unknown, record: Dictionary) => (
|
||||
<Space>
|
||||
<Button size="small" onClick={() => openAddItem(record.id)}>
|
||||
添加项
|
||||
</Button>
|
||||
<Button size="small" onClick={() => openEditDict(record)}>
|
||||
编辑
|
||||
</Button>
|
||||
<Popconfirm
|
||||
title="确定删除此字典?"
|
||||
onConfirm={() => handleDeleteDict(record.id)}
|
||||
>
|
||||
<Button size="small" danger>
|
||||
删除
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const itemColumns = (dictId: string) => [
|
||||
{ title: '标签', dataIndex: 'label', key: 'label' },
|
||||
{ title: '值', dataIndex: 'value', key: 'value' },
|
||||
{ title: '排序', dataIndex: 'sort_order', key: 'sort_order', width: 80 },
|
||||
{
|
||||
title: '颜色',
|
||||
dataIndex: 'color',
|
||||
key: 'color',
|
||||
width: 80,
|
||||
render: (color?: string) =>
|
||||
color ? <Tag color={color}>{color}</Tag> : '-',
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
render: (_: unknown, record: DictItem) => (
|
||||
<Space>
|
||||
<Button
|
||||
size="small"
|
||||
onClick={() => openEditItem(dictId, record)}
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
<Popconfirm
|
||||
title="确定删除此字典项?"
|
||||
onConfirm={() => handleDeleteItem(dictId, record.id)}
|
||||
>
|
||||
<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={openCreateDict}>
|
||||
新建字典
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={dictionaries}
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
pagination={{ pageSize: 20 }}
|
||||
expandable={{
|
||||
expandedRowRender: (record) => (
|
||||
<Table
|
||||
columns={itemColumns(record.id)}
|
||||
dataSource={record.items}
|
||||
rowKey="id"
|
||||
size="small"
|
||||
pagination={false}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Dictionary Modal */}
|
||||
<Modal
|
||||
title={editDict ? '编辑字典' : '新建字典'}
|
||||
open={dictModalOpen}
|
||||
onCancel={closeDictModal}
|
||||
onOk={() => dictForm.submit()}
|
||||
>
|
||||
<Form form={dictForm} onFinish={handleDictSubmit} layout="vertical">
|
||||
<Form.Item
|
||||
name="name"
|
||||
label="名称"
|
||||
rules={[{ required: true, message: '请输入字典名称' }]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="code"
|
||||
label="编码"
|
||||
rules={[{ required: true, message: '请输入字典编码' }]}
|
||||
>
|
||||
<Input disabled={!!editDict} />
|
||||
</Form.Item>
|
||||
<Form.Item name="description" label="说明">
|
||||
<Input.TextArea rows={3} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
{/* Dictionary Item Modal */}
|
||||
<Modal
|
||||
title={editItem ? '编辑字典项' : '添加字典项'}
|
||||
open={itemModalOpen}
|
||||
onCancel={closeItemModal}
|
||||
onOk={() => itemForm.submit()}
|
||||
>
|
||||
<Form form={itemForm} onFinish={handleItemSubmit} layout="vertical">
|
||||
<Form.Item
|
||||
name="label"
|
||||
label="标签"
|
||||
rules={[{ required: true, message: '请输入标签' }]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="value"
|
||||
label="值"
|
||||
rules={[{ required: true, message: '请输入值' }]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="sort_order"
|
||||
label="排序"
|
||||
initialValue={0}
|
||||
>
|
||||
<InputNumber min={0} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
<Form.Item name="color" label="颜色">
|
||||
<Input placeholder="如:blue, red, green 或十六进制色值" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user