imetting/frontend/src/pages/admin/DictManagement.jsx

404 lines
13 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

import React, { useState, useEffect } from 'react';
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';
import apiClient from '../../utils/apiClient';
import { buildApiUrl, API_ENDPOINTS } from '../../config/api';
const { Option } = Select;
const { TextArea } = Input;
const { Title, Text } = Typography;
const DictManagement = () => {
const { message } = App.useApp();
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') {
const types = (response.data.types || []).filter((type) => type !== 'system_config');
setDictTypes(types);
if (!types.includes(selectedDictType)) {
setSelectedDictType(types[0] || '');
}
}
} catch (error) {
message.error('获取字典类型失败');
}
};
// 获取指定类型的字典数据
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) {
message.error('获取字典数据失败');
} finally {
setLoading(false);
}
};
// 将树形数据转换为 antd Tree 组件格式
const buildAntdTreeData = (tree) => {
return tree.map(node => ({
title: (
<Space>
{node.parent_code === 'ROOT' ? <FolderOpenOutlined /> : <FileOutlined />}
<span>{node.label_cn}</span>
<Text type="secondary" size="small">({node.dict_code})</Text>
</Space>
),
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) {
message.error('扩展属性 JSON 格式错误');
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') {
message.success('更新成功');
fetchDictData(selectedDictType);
}
} else {
// 新增
const response = await apiClient.post(
buildApiUrl(API_ENDPOINTS.DICT_DATA.CREATE),
values
);
if (response.code === '200') {
message.success('创建成功');
fetchDictData(selectedDictType);
}
}
} catch (error) {
if (!error.errorFields) {
message.error(error.response?.data?.message || '操作失败');
}
}
};
// 删除
const handleDelete = async () => {
if (!selectedNode) return;
try {
await apiClient.delete(buildApiUrl(API_ENDPOINTS.DICT_DATA.DELETE(selectedNode.id)));
message.success('删除成功');
setSelectedNode(null);
setIsEditing(false);
form.resetFields();
fetchDictData(selectedDictType);
} catch (error) {
message.error('删除失败');
}
};
// 取消编辑
const handleCancel = () => {
setIsEditing(false);
setSelectedNode(null);
form.resetFields();
};
// 获取父级选项
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">
<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>
</div>
<Row gutter={24}>
<Col span={8}>
<Card
title={
<Space>
<FolderOpenOutlined />
<span>字典树</span>
</Space>
}
extra={
<Button
type="primary"
size="small"
icon={<PlusOutlined />}
onClick={handleAddNode}
>
新增
</Button>
}
variant="borderless"
>
<div style={{ marginBottom: 16 }}>
<Select
value={selectedDictType}
onChange={setSelectedDictType}
style={{ width: '100%' }}
placeholder="选择字典类型"
>
{dictTypes.map(type => (
<Option key={type} value={type}>{type}</Option>
))}
</Select>
</div>
<div style={{ maxHeight: '60vh', overflowY: 'auto' }}>
{treeData.length > 0 ? (
<Tree
showLine
treeData={treeData}
onSelect={handleSelectNode}
selectedKeys={selectedNode ? [selectedNode.dict_code] : []}
defaultExpandAll
/>
) : (
<Empty description="暂无数据" image={Empty.PRESENTED_IMAGE_SIMPLE} />
)}
</div>
</Card>
</Col>
<Col span={16}>
<Card
title={
<Space>
<FileOutlined />
<span>{selectedNode ? '编辑字典项' : isEditing ? '新增字典项' : '字典详情'}</span>
</Space>
}
extra={
isEditing && (
<Space>
{selectedNode && (
<Popconfirm
title="确定要删除此项吗?"
onConfirm={handleDelete}
okText="确定"
cancelText="取消"
>
<Button size="small" icon={<DeleteOutlined />} className="btn-soft-red">删除</Button>
</Popconfirm>
)}
<Button size="small" icon={<CloseOutlined />} onClick={handleCancel}>取消</Button>
<Button
type="primary"
size="small"
icon={<SaveOutlined />}
onClick={handleSave}
>
保存
</Button>
</Space>
)
}
variant="borderless"
>
<Form
form={form}
layout="vertical"
>
{isEditing ? (
<>
<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>
<Form.Item label="扩展属性JSON格式" name="extension_attr">
<TextArea
rows={4}
placeholder='如: {"suffix": ".exe"}'
/>
</Form.Item>
<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>
</>
) : (
<Empty
description="请从左侧树中选择一个节点进行编辑,或点击新增按钮创建新节点"
image={Empty.PRESENTED_IMAGE_SIMPLE}
/>
)}
</Form>
</Card>
</Col>
</Row>
</div>
);
};
export default DictManagement;