449 lines
14 KiB
TypeScript
449 lines
14 KiB
TypeScript
|
|
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;
|