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

444 lines
14 KiB
React
Raw Normal View History

import React, { useState, useEffect } from 'react';
import { Tree, Button, Form, Input, InputNumber, Select, Switch, Space, Card, Empty, Popconfirm } from 'antd';
import { BookText, Plus, Save, Trash2, FolderTree, FileText, X } from 'lucide-react';
import apiClient from '../../utils/apiClient';
import { buildApiUrl, API_ENDPOINTS } from '../../config/api';
import Toast from '../../components/Toast';
import './DictManagement.css';
const { Option } = Select;
const { TextArea } = Input;
const DictManagement = () => {
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 [toasts, setToasts] = useState([]);
const [form] = Form.useForm();
// Toast helper functions
const showToast = (message, type = 'info') => {
const id = Date.now();
setToasts(prev => [...prev, { id, message, type }]);
};
const removeToast = (id) => {
setToasts(prev => prev.filter(toast => toast.id !== id));
};
// 获取所有字典类型
const fetchDictTypes = async () => {
try {
const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.DICT_DATA.TYPES));
if (response.code === '200') {
setDictTypes(response.data.types);
}
} catch (error) {
showToast('获取字典类型失败', '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) {
showToast('获取字典数据失败', 'error');
} finally {
setLoading(false);
}
};
// 将树形数据转换为 antd Tree 组件格式
const buildAntdTreeData = (tree) => {
return tree.map(node => ({
title: (
<div className="tree-node-title">
{node.parent_code === 'ROOT' ? <FolderTree size={14} /> : <FileText size={14} />}
<span>{node.label_cn}</span>
<span className="tree-node-code">({node.dict_code})</span>
</div>
),
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) {
showToast('扩展属性 JSON 格式错误无法解析JSON', 'error');
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') {
showToast('更新成功', 'success');
// 重新加载数据
fetchDictData(selectedDictType);
} else {
showToast(response.message || '更新失败', 'error');
}
} else {
// 新增
const response = await apiClient.post(
buildApiUrl(API_ENDPOINTS.DICT_DATA.CREATE),
values
);
if (response.code === '200') {
showToast('创建成功', 'success');
// 重新加载数据
fetchDictData(selectedDictType);
} else {
showToast(response.message || '创建失败', 'error');
}
}
} catch (error) {
if (error.errorFields) {
// 表单验证错误
return;
}
// 显示后端返回的具体错误信息
const errorMsg = error.response?.data?.message || error.message || '操作失败';
showToast(errorMsg, 'error');
}
};
// 删除
const handleDelete = async () => {
if (!selectedNode) return;
try {
await apiClient.delete(buildApiUrl(API_ENDPOINTS.DICT_DATA.DELETE(selectedNode.id)));
showToast('删除成功', 'success');
// 重新加载数据
setSelectedNode(null);
setIsEditing(false);
form.resetFields();
fetchDictData(selectedDictType);
} catch (error) {
showToast('删除失败:' + (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 className="dict-header">
<div className="dict-header-left">
<BookText size={24} />
<div>
<h2>字典管理</h2>
<p>管理系统中的码表数据树形结构</p>
</div>
</div>
</div>
<div className="dict-main-layout">
{/* 左侧面板 */}
<div className="dict-left-panel">
<Card
title={
<div className="panel-header">
<FolderTree size={18} />
<span>字典树</span>
</div>
}
extra={
<Button
type="primary"
size="small"
icon={<Plus size={14} />}
onClick={handleAddNode}
>
新增
</Button>
}
bordered={false}
className="dict-tree-card"
>
<div className="dict-type-selector">
<label>字典类型</label>
<Select
value={selectedDictType}
onChange={setSelectedDictType}
style={{ flex: 1 }}
placeholder="选择字典类型"
>
{dictTypes.map(type => (
<Option key={type} value={type}>{type}</Option>
))}
</Select>
</div>
<div className="dict-tree-container">
{treeData.length > 0 ? (
<Tree
showLine
showIcon={false}
treeData={treeData}
onSelect={handleSelectNode}
selectedKeys={selectedNode ? [selectedNode.dict_code] : []}
defaultExpandAll
/>
) : (
<Empty
description="暂无数据"
image={Empty.PRESENTED_IMAGE_SIMPLE}
/>
)}
</div>
</Card>
</div>
{/* 右侧面板 */}
<div className="dict-right-panel">
<Card
title={
<div className="panel-header">
<FileText size={18} />
<span>{selectedNode ? '编辑字典项' : isEditing ? '新增字典项' : '字典详情'}</span>
</div>
}
extra={
isEditing && (
<Space>
{selectedNode && (
<Popconfirm
title="确定要删除此项吗?"
description="删除后将无法恢复"
onConfirm={handleDelete}
okText="确定"
cancelText="取消"
>
<Button
danger
size="small"
icon={<Trash2 size={14} />}
>
删除
</Button>
</Popconfirm>
)}
<Button size="small" icon={<X size={14} />} onClick={handleCancel}>
取消
</Button>
<Button
type="primary"
size="small"
icon={<Save size={14} />}
onClick={handleSave}
>
保存
</Button>
</Space>
)
}
bordered={false}
className="dict-form-card"
>
{isEditing ? (
<Form
form={form}
layout="vertical"
initialValues={{
dict_type: selectedDictType,
parent_code: 'ROOT',
sort_order: 0,
status: 1,
is_default: false
}}
>
<Form.Item
label="字典类型"
name="dict_type"
rules={[{ required: true, message: '请输入字典类型' }]}
>
<Input disabled={!!selectedNode} placeholder="如: client_platform" />
</Form.Item>
<Form.Item
label="编码"
name="dict_code"
rules={[{ required: true, message: '请输入编码' }]}
>
<Input disabled={!!selectedNode} placeholder="如: WIN, MAC, ANDROID" />
</Form.Item>
<Form.Item
label="中文名称"
name="label_cn"
rules={[{ required: true, message: '请输入中文名称' }]}
>
<Input placeholder="如: Windows" />
</Form.Item>
<Form.Item label="英文名称" name="label_en">
<Input placeholder="如: Windows" />
</Form.Item>
<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>
<Form.Item label="排序" name="sort_order">
<InputNumber min={0} style={{ width: '100%' }} />
</Form.Item>
<Form.Item label="扩展属性JSON格式" name="extension_attr">
<TextArea
rows={4}
placeholder='如: {"suffix": ".exe", "arch_support": ["x86", "x64"]}'
/>
</Form.Item>
<div className="form-inline-group">
<div className="form-inline-item">
<label>是否默认</label>
<Form.Item name="is_default" valuePropName="checked" noStyle>
<Switch checkedChildren="是" unCheckedChildren="否" />
</Form.Item>
</div>
<div className="form-inline-item">
<label>状态</label>
<Form.Item name="status" noStyle>
<Select style={{ width: 120 }}>
<Option value={1}>正常</Option>
<Option value={0}>停用</Option>
</Select>
</Form.Item>
</div>
</div>
</Form>
) : (
<Empty
description="请从左侧树中选择一个节点进行编辑,或点击新增按钮创建新节点"
image={Empty.PRESENTED_IMAGE_SIMPLE}
/>
)}
</Card>
</div>
</div>
{/* Toast notifications */}
{toasts.map(toast => (
<Toast
key={toast.id}
message={toast.message}
type={toast.type}
onClose={() => removeToast(toast.id)}
/>
))}
</div>
);
};
export default DictManagement;