2026-01-19 11:03:08 +00:00
|
|
|
|
import React, { useState, useEffect } from 'react';
|
2026-03-26 06:55:12 +00:00
|
|
|
|
import { Tree, Button, Form, Input, InputNumber, Select, Switch, Space, Card, Empty, Popconfirm, App, Row, Col, Typography, Checkbox } from 'antd';
|
|
|
|
|
|
import {
|
|
|
|
|
|
BookOutlined,
|
|
|
|
|
|
PlusOutlined,
|
|
|
|
|
|
SaveOutlined,
|
|
|
|
|
|
DeleteOutlined,
|
|
|
|
|
|
FolderOpenOutlined,
|
|
|
|
|
|
FileOutlined,
|
|
|
|
|
|
CloseOutlined
|
|
|
|
|
|
} from '@ant-design/icons';
|
2026-01-19 11:03:08 +00:00
|
|
|
|
import apiClient from '../../utils/apiClient';
|
|
|
|
|
|
import { buildApiUrl, API_ENDPOINTS } from '../../config/api';
|
|
|
|
|
|
|
|
|
|
|
|
const { Option } = Select;
|
|
|
|
|
|
const { TextArea } = Input;
|
2026-03-26 06:55:12 +00:00
|
|
|
|
const { Title, Text } = Typography;
|
2026-01-19 11:03:08 +00:00
|
|
|
|
|
|
|
|
|
|
const DictManagement = () => {
|
2026-03-26 06:55:12 +00:00
|
|
|
|
const { message } = App.useApp();
|
2026-01-19 11:03:08 +00:00
|
|
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
|
|
const [dictTypes, setDictTypes] = useState([]); // 字典类型列表
|
|
|
|
|
|
const [selectedDictType, setSelectedDictType] = useState('client_platform'); // 当前选中的字典类型
|
|
|
|
|
|
const [dictData, setDictData] = useState([]); // 当前字典类型的数据
|
|
|
|
|
|
const [treeData, setTreeData] = useState([]); // 树形结构数据
|
|
|
|
|
|
const [selectedNode, setSelectedNode] = useState(null); // 当前选中的节点
|
|
|
|
|
|
const [isEditing, setIsEditing] = useState(false); // 是否处于编辑状态
|
|
|
|
|
|
const [form] = Form.useForm();
|
|
|
|
|
|
|
|
|
|
|
|
// 获取所有字典类型
|
|
|
|
|
|
const fetchDictTypes = async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.DICT_DATA.TYPES));
|
|
|
|
|
|
if (response.code === '200') {
|
2026-03-26 06:55:12 +00:00
|
|
|
|
const types = (response.data.types || []).filter((type) => type !== 'system_config');
|
|
|
|
|
|
setDictTypes(types);
|
|
|
|
|
|
if (!types.includes(selectedDictType)) {
|
|
|
|
|
|
setSelectedDictType(types[0] || '');
|
|
|
|
|
|
}
|
2026-01-19 11:03:08 +00:00
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
2026-03-26 06:55:12 +00:00
|
|
|
|
message.error('获取字典类型失败');
|
2026-01-19 11:03:08 +00:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 获取指定类型的字典数据
|
|
|
|
|
|
const fetchDictData = async (dictType) => {
|
|
|
|
|
|
setLoading(true);
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.DICT_DATA.BY_TYPE(dictType)));
|
|
|
|
|
|
if (response.code === '200') {
|
|
|
|
|
|
setDictData(response.data.items);
|
|
|
|
|
|
|
|
|
|
|
|
// 转换为 antd Tree 需要的格式
|
|
|
|
|
|
const antdTreeData = buildAntdTreeData(response.data.tree);
|
|
|
|
|
|
setTreeData(antdTreeData);
|
|
|
|
|
|
|
|
|
|
|
|
// 清空选中节点
|
|
|
|
|
|
setSelectedNode(null);
|
|
|
|
|
|
setIsEditing(false);
|
|
|
|
|
|
form.resetFields();
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
2026-03-26 06:55:12 +00:00
|
|
|
|
message.error('获取字典数据失败');
|
2026-01-19 11:03:08 +00:00
|
|
|
|
} finally {
|
|
|
|
|
|
setLoading(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 将树形数据转换为 antd Tree 组件格式
|
|
|
|
|
|
const buildAntdTreeData = (tree) => {
|
|
|
|
|
|
return tree.map(node => ({
|
|
|
|
|
|
title: (
|
2026-03-26 06:55:12 +00:00
|
|
|
|
<Space>
|
|
|
|
|
|
{node.parent_code === 'ROOT' ? <FolderOpenOutlined /> : <FileOutlined />}
|
2026-01-19 11:03:08 +00:00
|
|
|
|
<span>{node.label_cn}</span>
|
2026-03-26 06:55:12 +00:00
|
|
|
|
<Text type="secondary" size="small">({node.dict_code})</Text>
|
|
|
|
|
|
</Space>
|
2026-01-19 11:03:08 +00:00
|
|
|
|
),
|
|
|
|
|
|
key: node.dict_code,
|
|
|
|
|
|
data: node,
|
|
|
|
|
|
children: node.children && node.children.length > 0 ? buildAntdTreeData(node.children) : []
|
|
|
|
|
|
}));
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
fetchDictTypes();
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (selectedDictType) {
|
|
|
|
|
|
fetchDictData(selectedDictType);
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [selectedDictType]);
|
|
|
|
|
|
|
|
|
|
|
|
// 选中树节点
|
|
|
|
|
|
const handleSelectNode = (selectedKeys, info) => {
|
|
|
|
|
|
if (selectedKeys.length > 0) {
|
|
|
|
|
|
const nodeData = info.node.data;
|
|
|
|
|
|
setSelectedNode(nodeData);
|
|
|
|
|
|
setIsEditing(true);
|
|
|
|
|
|
|
|
|
|
|
|
// 填充表单
|
|
|
|
|
|
form.setFieldsValue({
|
|
|
|
|
|
dict_type: nodeData.dict_type,
|
|
|
|
|
|
dict_code: nodeData.dict_code,
|
|
|
|
|
|
parent_code: nodeData.parent_code,
|
|
|
|
|
|
label_cn: nodeData.label_cn,
|
|
|
|
|
|
label_en: nodeData.label_en,
|
|
|
|
|
|
sort_order: nodeData.sort_order,
|
|
|
|
|
|
extension_attr: nodeData.extension_attr ? JSON.stringify(nodeData.extension_attr, null, 2) : '',
|
|
|
|
|
|
is_default: nodeData.is_default === 1,
|
|
|
|
|
|
status: nodeData.status
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 新增节点
|
|
|
|
|
|
const handleAddNode = () => {
|
|
|
|
|
|
setSelectedNode(null);
|
|
|
|
|
|
setIsEditing(true);
|
|
|
|
|
|
form.resetFields();
|
|
|
|
|
|
form.setFieldsValue({
|
|
|
|
|
|
dict_type: selectedDictType,
|
|
|
|
|
|
parent_code: 'ROOT',
|
|
|
|
|
|
sort_order: 0,
|
|
|
|
|
|
status: 1,
|
|
|
|
|
|
is_default: false
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 保存
|
|
|
|
|
|
const handleSave = async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const values = await form.validateFields();
|
|
|
|
|
|
|
|
|
|
|
|
// 解析 extension_attr JSON
|
|
|
|
|
|
if (values.extension_attr) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
values.extension_attr = JSON.parse(values.extension_attr);
|
|
|
|
|
|
} catch (e) {
|
2026-03-26 06:55:12 +00:00
|
|
|
|
message.error('扩展属性 JSON 格式错误');
|
2026-01-19 11:03:08 +00:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 转换 is_default 为数字
|
|
|
|
|
|
values.is_default = values.is_default ? 1 : 0;
|
|
|
|
|
|
|
|
|
|
|
|
if (selectedNode) {
|
|
|
|
|
|
// 更新
|
|
|
|
|
|
const response = await apiClient.put(
|
|
|
|
|
|
buildApiUrl(API_ENDPOINTS.DICT_DATA.UPDATE(selectedNode.id)),
|
|
|
|
|
|
values
|
|
|
|
|
|
);
|
|
|
|
|
|
if (response.code === '200') {
|
2026-03-26 06:55:12 +00:00
|
|
|
|
message.success('更新成功');
|
2026-01-19 11:03:08 +00:00
|
|
|
|
fetchDictData(selectedDictType);
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 新增
|
|
|
|
|
|
const response = await apiClient.post(
|
|
|
|
|
|
buildApiUrl(API_ENDPOINTS.DICT_DATA.CREATE),
|
|
|
|
|
|
values
|
|
|
|
|
|
);
|
|
|
|
|
|
if (response.code === '200') {
|
2026-03-26 06:55:12 +00:00
|
|
|
|
message.success('创建成功');
|
2026-01-19 11:03:08 +00:00
|
|
|
|
fetchDictData(selectedDictType);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
2026-03-26 06:55:12 +00:00
|
|
|
|
if (!error.errorFields) {
|
|
|
|
|
|
message.error(error.response?.data?.message || '操作失败');
|
2026-01-19 11:03:08 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 删除
|
|
|
|
|
|
const handleDelete = async () => {
|
|
|
|
|
|
if (!selectedNode) return;
|
|
|
|
|
|
try {
|
|
|
|
|
|
await apiClient.delete(buildApiUrl(API_ENDPOINTS.DICT_DATA.DELETE(selectedNode.id)));
|
2026-03-26 06:55:12 +00:00
|
|
|
|
message.success('删除成功');
|
2026-01-19 11:03:08 +00:00
|
|
|
|
setSelectedNode(null);
|
|
|
|
|
|
setIsEditing(false);
|
|
|
|
|
|
form.resetFields();
|
|
|
|
|
|
fetchDictData(selectedDictType);
|
|
|
|
|
|
} catch (error) {
|
2026-03-26 06:55:12 +00:00
|
|
|
|
message.error('删除失败');
|
2026-01-19 11:03:08 +00:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 取消编辑
|
|
|
|
|
|
const handleCancel = () => {
|
|
|
|
|
|
setIsEditing(false);
|
|
|
|
|
|
setSelectedNode(null);
|
|
|
|
|
|
form.resetFields();
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-26 06:55:12 +00:00
|
|
|
|
// 获取父级选项
|
2026-01-19 11:03:08 +00:00
|
|
|
|
const getParentOptions = () => {
|
|
|
|
|
|
const options = [{ label: 'ROOT(顶级)', value: 'ROOT' }];
|
|
|
|
|
|
dictData.forEach(item => {
|
|
|
|
|
|
if (item.parent_code === 'ROOT') {
|
|
|
|
|
|
options.push({ label: `${item.label_cn} (${item.dict_code})`, value: item.dict_code });
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
return options;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="dict-management">
|
2026-03-26 06:55:12 +00:00
|
|
|
|
<div style={{ marginBottom: 24, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
|
|
|
|
|
<Space size="middle">
|
|
|
|
|
|
<BookOutlined style={{ fontSize: 24, color: '#1677ff' }} />
|
|
|
|
|
|
<Title level={4} style={{ margin: 0 }}>字典管理</Title>
|
|
|
|
|
|
</Space>
|
2026-01-19 11:03:08 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-03-26 06:55:12 +00:00
|
|
|
|
<Row gutter={24}>
|
|
|
|
|
|
<Col span={8}>
|
2026-01-19 11:03:08 +00:00
|
|
|
|
<Card
|
|
|
|
|
|
title={
|
2026-03-26 06:55:12 +00:00
|
|
|
|
<Space>
|
|
|
|
|
|
<FolderOpenOutlined />
|
2026-01-19 11:03:08 +00:00
|
|
|
|
<span>字典树</span>
|
2026-03-26 06:55:12 +00:00
|
|
|
|
</Space>
|
2026-01-19 11:03:08 +00:00
|
|
|
|
}
|
|
|
|
|
|
extra={
|
|
|
|
|
|
<Button
|
|
|
|
|
|
type="primary"
|
|
|
|
|
|
size="small"
|
2026-03-26 06:55:12 +00:00
|
|
|
|
icon={<PlusOutlined />}
|
2026-01-19 11:03:08 +00:00
|
|
|
|
onClick={handleAddNode}
|
|
|
|
|
|
>
|
|
|
|
|
|
新增
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
}
|
|
|
|
|
|
bordered={false}
|
|
|
|
|
|
>
|
2026-03-26 06:55:12 +00:00
|
|
|
|
<div style={{ marginBottom: 16 }}>
|
2026-01-19 11:03:08 +00:00
|
|
|
|
<Select
|
|
|
|
|
|
value={selectedDictType}
|
|
|
|
|
|
onChange={setSelectedDictType}
|
2026-03-26 06:55:12 +00:00
|
|
|
|
style={{ width: '100%' }}
|
2026-01-19 11:03:08 +00:00
|
|
|
|
placeholder="选择字典类型"
|
|
|
|
|
|
>
|
|
|
|
|
|
{dictTypes.map(type => (
|
|
|
|
|
|
<Option key={type} value={type}>{type}</Option>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</Select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-03-26 06:55:12 +00:00
|
|
|
|
<div style={{ maxHeight: '60vh', overflowY: 'auto' }}>
|
2026-01-19 11:03:08 +00:00
|
|
|
|
{treeData.length > 0 ? (
|
|
|
|
|
|
<Tree
|
|
|
|
|
|
showLine
|
|
|
|
|
|
treeData={treeData}
|
|
|
|
|
|
onSelect={handleSelectNode}
|
|
|
|
|
|
selectedKeys={selectedNode ? [selectedNode.dict_code] : []}
|
|
|
|
|
|
defaultExpandAll
|
|
|
|
|
|
/>
|
|
|
|
|
|
) : (
|
2026-03-26 06:55:12 +00:00
|
|
|
|
<Empty description="暂无数据" image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
2026-01-19 11:03:08 +00:00
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Card>
|
2026-03-26 06:55:12 +00:00
|
|
|
|
</Col>
|
2026-01-19 11:03:08 +00:00
|
|
|
|
|
2026-03-26 06:55:12 +00:00
|
|
|
|
<Col span={16}>
|
2026-01-19 11:03:08 +00:00
|
|
|
|
<Card
|
|
|
|
|
|
title={
|
2026-03-26 06:55:12 +00:00
|
|
|
|
<Space>
|
|
|
|
|
|
<FileOutlined />
|
2026-01-19 11:03:08 +00:00
|
|
|
|
<span>{selectedNode ? '编辑字典项' : isEditing ? '新增字典项' : '字典详情'}</span>
|
2026-03-26 06:55:12 +00:00
|
|
|
|
</Space>
|
2026-01-19 11:03:08 +00:00
|
|
|
|
}
|
|
|
|
|
|
extra={
|
|
|
|
|
|
isEditing && (
|
|
|
|
|
|
<Space>
|
|
|
|
|
|
{selectedNode && (
|
|
|
|
|
|
<Popconfirm
|
|
|
|
|
|
title="确定要删除此项吗?"
|
|
|
|
|
|
onConfirm={handleDelete}
|
|
|
|
|
|
okText="确定"
|
|
|
|
|
|
cancelText="取消"
|
|
|
|
|
|
>
|
2026-03-26 06:55:12 +00:00
|
|
|
|
<Button danger size="small" icon={<DeleteOutlined />}>删除</Button>
|
2026-01-19 11:03:08 +00:00
|
|
|
|
</Popconfirm>
|
|
|
|
|
|
)}
|
2026-03-26 06:55:12 +00:00
|
|
|
|
<Button size="small" icon={<CloseOutlined />} onClick={handleCancel}>取消</Button>
|
2026-01-19 11:03:08 +00:00
|
|
|
|
<Button
|
|
|
|
|
|
type="primary"
|
|
|
|
|
|
size="small"
|
2026-03-26 06:55:12 +00:00
|
|
|
|
icon={<SaveOutlined />}
|
2026-01-19 11:03:08 +00:00
|
|
|
|
onClick={handleSave}
|
|
|
|
|
|
>
|
|
|
|
|
|
保存
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
bordered={false}
|
|
|
|
|
|
>
|
|
|
|
|
|
{isEditing ? (
|
|
|
|
|
|
<Form
|
|
|
|
|
|
form={form}
|
|
|
|
|
|
layout="vertical"
|
|
|
|
|
|
>
|
2026-03-26 06:55:12 +00:00
|
|
|
|
<Row gutter={16}>
|
|
|
|
|
|
<Col span={12}>
|
|
|
|
|
|
<Form.Item
|
|
|
|
|
|
label="字典类型"
|
|
|
|
|
|
name="dict_type"
|
|
|
|
|
|
rules={[{ required: true, message: '请输入字典类型' }]}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Input disabled={!!selectedNode} placeholder="如: client_platform" />
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
</Col>
|
|
|
|
|
|
<Col span={12}>
|
|
|
|
|
|
<Form.Item
|
|
|
|
|
|
label="编码"
|
|
|
|
|
|
name="dict_code"
|
|
|
|
|
|
rules={[{ required: true, message: '请输入编码' }]}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Input disabled={!!selectedNode} placeholder="如: WIN, MAC" />
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
</Col>
|
|
|
|
|
|
</Row>
|
|
|
|
|
|
|
|
|
|
|
|
<Row gutter={16}>
|
|
|
|
|
|
<Col span={12}>
|
|
|
|
|
|
<Form.Item
|
|
|
|
|
|
label="中文名称"
|
|
|
|
|
|
name="label_cn"
|
|
|
|
|
|
rules={[{ required: true, message: '请输入中文名称' }]}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Input placeholder="如: Windows" />
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
</Col>
|
|
|
|
|
|
<Col span={12}>
|
|
|
|
|
|
<Form.Item label="英文名称" name="label_en">
|
|
|
|
|
|
<Input placeholder="如: Windows" />
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
</Col>
|
|
|
|
|
|
</Row>
|
|
|
|
|
|
|
|
|
|
|
|
<Row gutter={16}>
|
|
|
|
|
|
<Col span={12}>
|
|
|
|
|
|
<Form.Item
|
|
|
|
|
|
label="父级编码"
|
|
|
|
|
|
name="parent_code"
|
|
|
|
|
|
rules={[{ required: true, message: '请选择父级' }]}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Select placeholder="选择父级">
|
|
|
|
|
|
{getParentOptions().map(opt => (
|
|
|
|
|
|
<Option key={opt.value} value={opt.value}>{opt.label}</Option>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</Select>
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
</Col>
|
|
|
|
|
|
<Col span={12}>
|
|
|
|
|
|
<Form.Item label="排序" name="sort_order">
|
|
|
|
|
|
<InputNumber min={0} style={{ width: '100%' }} />
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
</Col>
|
|
|
|
|
|
</Row>
|
2026-01-19 11:03:08 +00:00
|
|
|
|
|
|
|
|
|
|
<Form.Item label="扩展属性(JSON格式)" name="extension_attr">
|
|
|
|
|
|
<TextArea
|
|
|
|
|
|
rows={4}
|
2026-03-26 06:55:12 +00:00
|
|
|
|
placeholder='如: {"suffix": ".exe"}'
|
2026-01-19 11:03:08 +00:00
|
|
|
|
/>
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
|
2026-03-26 06:55:12 +00:00
|
|
|
|
<Space size="large">
|
|
|
|
|
|
<Form.Item name="is_default" valuePropName="checked" style={{ marginBottom: 0 }}>
|
|
|
|
|
|
<Checkbox>设为默认</Checkbox>
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
|
|
|
|
|
|
<Form.Item name="status" label="状态">
|
|
|
|
|
|
<Select style={{ width: 120 }}>
|
|
|
|
|
|
<Option value={1}>正常</Option>
|
|
|
|
|
|
<Option value={0}>停用</Option>
|
|
|
|
|
|
</Select>
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
</Space>
|
2026-01-19 11:03:08 +00:00
|
|
|
|
</Form>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<Empty
|
|
|
|
|
|
description="请从左侧树中选择一个节点进行编辑,或点击新增按钮创建新节点"
|
|
|
|
|
|
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
|
|
|
|
|
/>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</Card>
|
2026-03-26 06:55:12 +00:00
|
|
|
|
</Col>
|
|
|
|
|
|
</Row>
|
2026-01-19 11:03:08 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
export default DictManagement;
|