pms-front-react/src/pages/system/DictDataPage.tsx

449 lines
14 KiB
TypeScript
Raw Normal View History

2026-04-07 08:44:06 +00:00
import { useCallback, useEffect, useMemo, useState } from 'react';
import {
App,
Button,
Form,
Input,
InputNumber,
Modal,
Popconfirm,
Radio,
Select,
Space,
Table,
Tag,
} from 'antd';
import type { TableColumnsType } from 'antd';
import {
DeleteOutlined,
DownloadOutlined,
EditOutlined,
PlusOutlined,
ReloadOutlined,
SearchOutlined,
} from '@ant-design/icons';
import { useParams } from 'react-router-dom';
import { saveAs } from 'file-saver';
import dayjs from 'dayjs';
import {
addDictData,
delDictData,
getDictData,
getDictType,
listDictData,
updateDictData,
} from '@/api/system/dict';
import PageBackButton from '@/components/PageBackButton';
import Permission from '@/components/Permission';
import ReadonlyAction from '@/components/Permission/ReadonlyAction';
import { parseTime } from '@/utils/ruoyi';
import './system-admin.css';
interface DictDataRecord {
dictCode?: number | string;
dictLabel?: string;
dictValue?: string;
dictType?: string;
dictSort?: number | string;
cssClass?: string;
listClass?: string;
isDefault?: string;
status?: string;
remark?: string;
createTime?: string | number | Date;
[key: string]: unknown;
}
const statusOptions = [
{ value: '0', label: '正常' },
{ value: '1', label: '停用' },
];
const defaultQueryParams = {
pageNum: 1,
pageSize: 10,
dictLabel: undefined as string | undefined,
status: undefined as string | undefined,
};
const DictDataPage = () => {
const { message } = App.useApp();
const { dictId = '' } = useParams();
const [queryForm] = Form.useForm();
const [dictDataForm] = Form.useForm();
const [loading, setLoading] = useState(false);
const [modalVisible, setModalVisible] = useState(false);
const [modalTitle, setModalTitle] = useState('');
const [dictTypeName, setDictTypeName] = useState('');
const [dictType, setDictType] = useState('');
const [dictDataList, setDictDataList] = useState<DictDataRecord[]>([]);
const [total, setTotal] = useState(0);
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
const [currentRecord, setCurrentRecord] = useState<DictDataRecord>({});
const [queryParams, setQueryParams] = useState(defaultQueryParams);
const currentDictId = useMemo(() => Number(dictId) || dictId, [dictId]);
const loadDictType = useCallback(async () => {
if (!currentDictId) {
return;
}
try {
const response = await getDictType(currentDictId);
const detail = (response && typeof response === 'object' && 'data' in response
? response.data
: response) as { dictType?: string; dictName?: string } | undefined;
setDictType(detail?.dictType ?? '');
setDictTypeName(detail?.dictName ?? '');
} catch (error) {
console.error('Failed to load dict type detail:', error);
message.error('获取字典类型信息失败');
}
}, [currentDictId, message]);
const loadList = useCallback(async () => {
if (!dictType) {
return;
}
setLoading(true);
try {
const response = await listDictData({
...queryParams,
dictType,
}) as { rows?: DictDataRecord[]; total?: number };
setDictDataList(response.rows ?? []);
setTotal(Number(response.total ?? 0));
} catch (error) {
console.error('Failed to load dict data list:', error);
message.error('获取字典数据失败');
} finally {
setLoading(false);
}
}, [dictType, message, queryParams]);
useEffect(() => {
void loadDictType();
}, [loadDictType]);
useEffect(() => {
if (dictType) {
void loadList();
}
}, [dictType, loadList]);
const handleAdd = () => {
setCurrentRecord({});
dictDataForm.resetFields();
dictDataForm.setFieldsValue({
dictType,
status: '0',
isDefault: 'N',
dictSort: 0,
});
setModalTitle('添加字典数据');
setModalVisible(true);
};
const handleEdit = async (record?: DictDataRecord) => {
const targetId = record?.dictCode ?? selectedRowKeys[0];
if (targetId === undefined) {
message.warning('请选择要修改的数据');
return;
}
try {
const response = await getDictData(targetId as string | number);
const detail = (response && typeof response === 'object' && 'data' in response
? response.data
: response) as DictDataRecord | undefined;
setCurrentRecord(detail ?? {});
dictDataForm.setFieldsValue({
...detail,
status: String(detail?.status ?? '0'),
isDefault: String(detail?.isDefault ?? 'N'),
});
setModalTitle('修改字典数据');
setModalVisible(true);
} catch (error) {
console.error('Failed to load dict data detail:', error);
message.error('获取字典数据详情失败');
}
};
const handleDelete = async (record?: DictDataRecord) => {
const ids = record?.dictCode !== undefined ? [record.dictCode] : selectedRowKeys;
if (ids.length === 0) {
message.warning('请选择要删除的数据');
return;
}
try {
await delDictData(ids.join(','));
message.success('删除成功');
setSelectedRowKeys([]);
await loadList();
} catch (error) {
console.error('Failed to delete dict data:', error);
message.error('删除失败');
}
};
const handleExport = async () => {
const hide = message.loading('正在导出数据...', 0);
try {
const response = await listDictData({
...queryParams,
dictType,
pageNum: undefined,
pageSize: undefined,
}) as { rows?: DictDataRecord[] };
const rows = response.rows ?? [];
const csvContent = [
['字典编码', '字典标签', '字典键值', '字典排序', '状态', '默认', '创建时间'],
...rows.map((item) => [
item.dictCode,
item.dictLabel,
item.dictValue,
item.dictSort,
String(item.status ?? '') === '0' ? '正常' : '停用',
item.isDefault,
parseTime(item.createTime),
]),
]
.map((row) => row.map((cell) => `"${String(cell ?? '').replace(/"/g, '""')}"`).join(','))
.join('\n');
saveAs(new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }), `dict_data_${dayjs().format('YYYYMMDDHHmmss')}.csv`);
hide();
message.success('导出成功');
} catch (error) {
hide();
console.error('Failed to export dict data:', error);
message.error('导出失败');
}
};
const submitForm = async () => {
try {
const values = await dictDataForm.validateFields();
const payload = {
...currentRecord,
...values,
dictType,
};
if (payload.dictCode !== undefined) {
await updateDictData(payload);
message.success('修改成功');
} else {
await addDictData(payload);
message.success('新增成功');
}
setModalVisible(false);
await loadList();
} catch (error) {
if (error && typeof error === 'object' && 'errorFields' in error) {
return;
}
console.error('Failed to submit dict data form:', error);
message.error('保存失败');
}
};
const columns: TableColumnsType<DictDataRecord> = [
{ title: '字典编码', dataIndex: 'dictCode', align: 'center', width: 120 },
{ title: '字典标签', dataIndex: 'dictLabel', align: 'center' },
{ title: '字典键值', dataIndex: 'dictValue', align: 'center' },
{ title: '排序', dataIndex: 'dictSort', align: 'center', width: 100 },
{
title: '状态',
dataIndex: 'status',
align: 'center',
width: 100,
render: (value) => (
<Tag color={String(value ?? '') === '0' ? 'green' : 'red'}>
{String(value ?? '') === '0' ? '正常' : '停用'}
</Tag>
),
},
{ title: '默认', dataIndex: 'isDefault', align: 'center', width: 100 },
{
title: '创建时间',
dataIndex: 'createTime',
align: 'center',
width: 180,
render: (value) => parseTime(value),
},
{
title: '操作',
key: 'operation',
align: 'center',
width: 180,
render: (_value, record) => (
<Space size="small">
<Permission permissions="system:dict:edit" fallback={<ReadonlyAction icon={<EditOutlined />}></ReadonlyAction>}>
<Button type="link" icon={<EditOutlined />} onClick={() => void handleEdit(record)}>
</Button>
</Permission>
<Permission permissions="system:dict:remove" fallback={<ReadonlyAction icon={<DeleteOutlined />} danger></ReadonlyAction>}>
<Popconfirm title="确认删除该字典数据吗?" onConfirm={() => void handleDelete(record)}>
<Button type="link" danger icon={<DeleteOutlined />}>
</Button>
</Popconfirm>
</Permission>
</Space>
),
},
];
return (
<div className="app-container dict-page-container">
<Space direction="vertical" size={16} style={{ width: '100%' }}>
<Space wrap style={{ justifyContent: 'space-between', width: '100%' }}>
<Space wrap>
<PageBackButton fallbackPath="/system/dict" />
<strong>{dictTypeName || '字典数据'}</strong>
<Tag>{dictType || '-'}</Tag>
</Space>
</Space>
<Form
form={queryForm}
layout="inline"
className="search-form"
onFinish={() =>
setQueryParams((prev) => ({
...prev,
pageNum: 1,
...queryForm.getFieldsValue(),
}))
}
>
<Form.Item label="字典标签" name="dictLabel">
<Input placeholder="请输入字典标签" allowClear />
</Form.Item>
<Form.Item label="状态" name="status">
<Select placeholder="请选择状态" allowClear style={{ width: 160 }}>
{statusOptions.map((item) => (
<Select.Option key={item.value} value={item.value}>
{item.label}
</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item>
<Button type="primary" icon={<SearchOutlined />} htmlType="submit">
</Button>
<Button
icon={<ReloadOutlined />}
onClick={() => {
queryForm.resetFields();
setQueryParams(defaultQueryParams);
}}
style={{ marginLeft: 8 }}
>
</Button>
</Form.Item>
</Form>
<Space className="mb8">
<Permission permissions="system:dict:add">
<Button type="primary" ghost icon={<PlusOutlined />} onClick={handleAdd}>
</Button>
</Permission>
<Permission permissions="system:dict:edit">
<Button type="primary" ghost icon={<EditOutlined />} disabled={selectedRowKeys.length !== 1} onClick={() => void handleEdit()}>
</Button>
</Permission>
<Permission permissions="system:dict:remove">
<Button danger ghost icon={<DeleteOutlined />} disabled={selectedRowKeys.length === 0} onClick={() => void handleDelete()}>
</Button>
</Permission>
<Permission permissions="system:dict:export">
<Button ghost icon={<DownloadOutlined />} onClick={() => void handleExport()}>
</Button>
</Permission>
</Space>
<Table
rowKey="dictCode"
columns={columns}
dataSource={dictDataList}
loading={loading}
rowSelection={{
selectedRowKeys,
onChange: (keys) => setSelectedRowKeys(keys),
}}
pagination={{
current: queryParams.pageNum,
pageSize: queryParams.pageSize,
total,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (count) => `${count}`,
onChange: (page, pageSize) => setQueryParams((prev) => ({ ...prev, pageNum: page, pageSize })),
}}
/>
</Space>
<Modal
className="system-admin-modal"
title={modalTitle}
open={modalVisible}
onOk={() => void submitForm()}
onCancel={() => setModalVisible(false)}
width={560}
forceRender
>
<Form form={dictDataForm} labelCol={{ span: 5 }} wrapperCol={{ span: 16 }}>
<Form.Item name="dictCode" hidden>
<Input />
</Form.Item>
<Form.Item label="字典标签" name="dictLabel" rules={[{ required: true, message: '请输入字典标签' }]}>
<Input placeholder="请输入字典标签" />
</Form.Item>
<Form.Item label="字典键值" name="dictValue" rules={[{ required: true, message: '请输入字典键值' }]}>
<Input placeholder="请输入字典键值" />
</Form.Item>
<Form.Item label="字典排序" name="dictSort" rules={[{ required: true, message: '请输入字典排序' }]}>
<InputNumber min={0} style={{ width: '100%' }} />
</Form.Item>
<Form.Item label="状态" name="status">
<Radio.Group>
{statusOptions.map((item) => (
<Radio key={item.value} value={item.value}>
{item.label}
</Radio>
))}
</Radio.Group>
</Form.Item>
<Form.Item label="默认" name="isDefault">
<Radio.Group>
<Radio value="Y"></Radio>
<Radio value="N"></Radio>
</Radio.Group>
</Form.Item>
<Form.Item label="样式属性" name="cssClass">
<Input placeholder="请输入样式属性" />
</Form.Item>
<Form.Item label="回显样式" name="listClass">
<Input placeholder="请输入回显样式" />
</Form.Item>
<Form.Item label="备注" name="remark">
<Input.TextArea placeholder="请输入备注" />
</Form.Item>
</Form>
</Modal>
</div>
);
};
export default DictDataPage;