409 lines
12 KiB
TypeScript
409 lines
12 KiB
TypeScript
import * as dictApi from '@/servers/api/dict';
|
||
import {
|
||
ActionType,
|
||
PageContainer,
|
||
ProTable,
|
||
} from '@ant-design/pro-components';
|
||
import { request } from '@umijs/max';
|
||
import { Button, Input, Layout, Space, Table, message } from 'antd';
|
||
import React, { useEffect, useRef, useState } from 'react';
|
||
import DictItemActions from '../../Dict/components/DictItemActions';
|
||
import DictItemModal from '../../Dict/components/DictItemModal';
|
||
|
||
const { Sider, Content } = Layout;
|
||
|
||
import { notAttributes } from './consts';
|
||
|
||
const AttributePage: React.FC = () => {
|
||
// 左侧字典列表状态
|
||
const [dicts, setDicts] = useState<any[]>([]);
|
||
const [loadingDicts, setLoadingDicts] = useState(false);
|
||
const [searchText, setSearchText] = useState('');
|
||
const [selectedDict, setSelectedDict] = useState<any>(null);
|
||
|
||
// 右侧字典项 ProTable 的引用
|
||
const actionRef = useRef<ActionType>();
|
||
|
||
// 字典项模态框状态(由 DictItemModal 组件管理)
|
||
const [isDictItemModalVisible, setIsDictItemModalVisible] = useState(false);
|
||
const [isEditDictItem, setIsEditDictItem] = useState(false);
|
||
const [editingDictItemData, setEditingDictItemData] = useState<any>(null);
|
||
|
||
// 导出字典项数据
|
||
const handleExportDictItems = async () => {
|
||
// 条件判断,确保已选择字典
|
||
if (!selectedDict) {
|
||
message.warning('请先选择字典');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
// 获取当前字典的所有数据
|
||
const response = await request('/dict/items', {
|
||
params: {
|
||
dictId: selectedDict.id,
|
||
},
|
||
});
|
||
|
||
// 确保返回的是数组
|
||
const data = Array.isArray(response) ? response : response?.data || [];
|
||
|
||
// 条件判断,检查是否有数据可导出
|
||
if (data.length === 0) {
|
||
message.warning('当前字典没有数据可导出');
|
||
return;
|
||
}
|
||
|
||
// 将数据转换为CSV格式
|
||
const headers = [
|
||
'name',
|
||
'title',
|
||
'titleCN',
|
||
'value',
|
||
'sort',
|
||
'image',
|
||
'shortName',
|
||
];
|
||
const csvContent = [
|
||
headers.join(','),
|
||
...data.map((item: any) =>
|
||
headers
|
||
.map((header) => {
|
||
const value = item[header] || '';
|
||
// 条件判断,如果值包含逗号或引号,需要转义
|
||
if (
|
||
typeof value === 'string' &&
|
||
(value.includes(',') || value.includes('"'))
|
||
) {
|
||
return `"${value.replace(/"/g, '""')}"`;
|
||
}
|
||
return value;
|
||
})
|
||
.join(','),
|
||
),
|
||
].join('\n');
|
||
|
||
// 创建blob并下载
|
||
const blob = new Blob(['\ufeff' + csvContent], {
|
||
// 添加BOM以支持中文
|
||
type: 'text/csv;charset=utf-8',
|
||
});
|
||
const url = window.URL.createObjectURL(blob);
|
||
const link = document.createElement('a');
|
||
link.href = url;
|
||
link.setAttribute('download', `${selectedDict.name}_dict_items.csv`);
|
||
document.body.appendChild(link);
|
||
link.click();
|
||
link.remove();
|
||
window.URL.revokeObjectURL(url);
|
||
|
||
message.success(`成功导出 ${data.length} 条数据`);
|
||
} catch (error: any) {
|
||
message.error('导出字典项失败:' + (error.message || '未知错误'));
|
||
}
|
||
};
|
||
|
||
const fetchDicts = async (title?: string) => {
|
||
setLoadingDicts(true);
|
||
try {
|
||
const res = await request('/dict/list', { params: { title } });
|
||
// 条件判断,确保res是数组再进行过滤
|
||
const dataList = Array.isArray(res) ? res : res?.data || [];
|
||
const filtered = dataList.filter((d: any) => !notAttributes.has(d?.name));
|
||
setDicts(filtered);
|
||
} catch (error) {
|
||
console.error('获取字典列表失败:', error);
|
||
message.error('获取字典列表失败');
|
||
setDicts([]);
|
||
}
|
||
setLoadingDicts(false);
|
||
};
|
||
|
||
// 组件挂载时初始化数据
|
||
useEffect(() => {
|
||
fetchDicts();
|
||
}, []);
|
||
|
||
// 搜索触发过滤
|
||
const handleSearch = (value: string) => {
|
||
fetchDicts(value);
|
||
};
|
||
|
||
// 打开添加字典项模态框
|
||
const handleAddDictItem = () => {
|
||
setIsEditDictItem(false);
|
||
setEditingDictItemData(null);
|
||
setIsDictItemModalVisible(true);
|
||
};
|
||
|
||
// 打开编辑字典项模态框
|
||
const handleEditDictItem = (item: any) => {
|
||
setIsEditDictItem(true);
|
||
setEditingDictItemData(item);
|
||
setIsDictItemModalVisible(true);
|
||
};
|
||
|
||
// 字典项表单提交(新增或编辑)
|
||
const handleDictItemFormSubmit = async (values: any) => {
|
||
try {
|
||
if (isEditDictItem && editingDictItemData) {
|
||
// 条件判断,存在编辑项则执行更新
|
||
await request(`/dict/item/${editingDictItemData.id}`, {
|
||
method: 'PUT',
|
||
data: values,
|
||
});
|
||
message.success('更新成功');
|
||
} else {
|
||
// 否则执行新增,绑定到当前选择的字典
|
||
await request('/dict/item', {
|
||
method: 'POST',
|
||
data: { ...values, dictId: selectedDict.id },
|
||
});
|
||
message.success('添加成功');
|
||
}
|
||
setIsDictItemModalVisible(false);
|
||
actionRef.current?.reload(); // 刷新 ProTable
|
||
} catch (error) {
|
||
message.error(isEditDictItem ? '更新失败' : '添加失败');
|
||
}
|
||
};
|
||
|
||
// 删除字典项
|
||
const handleDeleteDictItem = async (itemId: number) => {
|
||
try {
|
||
const res = await request(`/dict/item/${itemId}`, { method: 'DELETE' });
|
||
const isOk =
|
||
typeof res === 'boolean'
|
||
? res
|
||
: res && res.code === 0
|
||
? res.data === true || res.data === null
|
||
: false;
|
||
if (!isOk) {
|
||
message.error('删除失败');
|
||
return;
|
||
}
|
||
if (selectedDict?.id) {
|
||
try {
|
||
const list = await request('/dict/items', {
|
||
params: {
|
||
dictId: selectedDict.id,
|
||
},
|
||
});
|
||
// 确保list是数组再进行some操作
|
||
const dataList = Array.isArray(list) ? list : list?.data || [];
|
||
const exists = dataList.some((it: any) => it.id === itemId);
|
||
if (exists) {
|
||
message.error('删除失败');
|
||
} else {
|
||
message.success('删除成功');
|
||
actionRef.current?.reload();
|
||
}
|
||
} catch (error) {
|
||
console.error('验证删除结果失败:', error);
|
||
message.success('删除成功');
|
||
actionRef.current?.reload();
|
||
}
|
||
} else {
|
||
message.success('删除成功');
|
||
actionRef.current?.reload();
|
||
}
|
||
} catch (error) {
|
||
message.error('删除失败');
|
||
}
|
||
};
|
||
|
||
// 左侧字典列表列定义(紧凑样式)
|
||
const dictColumns = [
|
||
{ title: '名称', dataIndex: 'name', key: 'name' },
|
||
{ title: '标题', dataIndex: 'title', key: 'title' },
|
||
];
|
||
|
||
// 右侧字典项列表列定义(紧凑样式)
|
||
const dictItemColumns: any[] = [
|
||
{ title: '名称', dataIndex: 'name', key: 'name', copyable: true },
|
||
{ title: '标题', dataIndex: 'title', key: 'title', copyable: true },
|
||
{ title: '中文标题', dataIndex: 'titleCN', key: 'titleCN', copyable: true },
|
||
{ title: '简称', dataIndex: 'shortName', key: 'shortName', copyable: true },
|
||
{
|
||
title: '图片',
|
||
dataIndex: 'image',
|
||
key: 'image',
|
||
valueType: 'image',
|
||
width: 80,
|
||
},
|
||
{
|
||
title: '操作',
|
||
key: 'action',
|
||
valueType: 'option',
|
||
render: (_: any, record: any) => (
|
||
<Space size="small">
|
||
<Button
|
||
size="small"
|
||
type="link"
|
||
onClick={() => handleEditDictItem(record)}
|
||
>
|
||
编辑
|
||
</Button>
|
||
<Button
|
||
size="small"
|
||
type="link"
|
||
danger
|
||
onClick={() => handleDeleteDictItem(record.id)}
|
||
>
|
||
删除
|
||
</Button>
|
||
</Space>
|
||
),
|
||
},
|
||
];
|
||
|
||
return (
|
||
<PageContainer>
|
||
<Layout style={{ background: '#fff' }}>
|
||
<Sider
|
||
width={240}
|
||
style={{
|
||
background: '#fff',
|
||
padding: '8px',
|
||
borderRight: '1px solid #f0f0f0',
|
||
}}
|
||
>
|
||
<Space direction="vertical" style={{ width: '100%' }} size="small">
|
||
<Input.Search
|
||
placeholder="搜索字典"
|
||
onSearch={handleSearch}
|
||
onChange={(e) => setSearchText(e.target.value)}
|
||
enterButton
|
||
allowClear
|
||
size="small"
|
||
/>
|
||
</Space>
|
||
<div
|
||
style={{
|
||
marginTop: '8px',
|
||
overflowY: 'auto',
|
||
height: 'calc(100vh - 150px)',
|
||
}}
|
||
>
|
||
<Table
|
||
dataSource={dicts}
|
||
columns={dictColumns}
|
||
rowKey="id"
|
||
loading={loadingDicts}
|
||
size="small"
|
||
onRow={(record) => ({
|
||
onClick: () => {
|
||
// 条件判断,重复点击同一行则取消选择
|
||
if (selectedDict?.id === record.id) {
|
||
setSelectedDict(null);
|
||
} else {
|
||
setSelectedDict(record);
|
||
}
|
||
},
|
||
})}
|
||
rowClassName={(record) =>
|
||
selectedDict?.id === record.id ? 'ant-table-row-selected' : ''
|
||
}
|
||
pagination={false}
|
||
/>
|
||
</div>
|
||
</Sider>
|
||
<Content style={{ padding: '8px' }}>
|
||
<ProTable
|
||
columns={dictItemColumns}
|
||
actionRef={actionRef}
|
||
request={async (params) => {
|
||
// 当没有选择字典时,不发起请求
|
||
if (!selectedDict?.id) {
|
||
return {
|
||
data: [],
|
||
success: true,
|
||
};
|
||
}
|
||
const { name, title } = params;
|
||
try {
|
||
const res = await request('/dict/items', {
|
||
params: {
|
||
dictId: selectedDict.id,
|
||
name,
|
||
title,
|
||
},
|
||
});
|
||
// 确保返回的是数组
|
||
const data = Array.isArray(res) ? res : res?.data || [];
|
||
return {
|
||
data: data,
|
||
success: true,
|
||
};
|
||
} catch (error) {
|
||
console.error('获取字典项失败:', error);
|
||
return {
|
||
data: [],
|
||
success: false,
|
||
};
|
||
}
|
||
}}
|
||
rowKey="id"
|
||
search={{
|
||
layout: 'vertical',
|
||
}}
|
||
pagination={false}
|
||
options={{
|
||
reload: true,
|
||
density: false,
|
||
setting: {
|
||
draggable: true,
|
||
checkable: true,
|
||
checkedReset: false,
|
||
},
|
||
search: false,
|
||
fullScreen: false,
|
||
}}
|
||
size="small"
|
||
key={selectedDict?.id}
|
||
headerTitle={
|
||
<DictItemActions
|
||
selectedDict={selectedDict}
|
||
actionRef={actionRef}
|
||
showExport={true}
|
||
onImport={async (file: File, dictId: number) => {
|
||
// 创建 FormData 对象
|
||
const formData = new FormData();
|
||
// 添加文件到 FormData
|
||
formData.append('file', file);
|
||
// 添加字典 ID 到 FormData
|
||
formData.append('dictId', String(dictId));
|
||
// 调用导入字典项的 API
|
||
const response = await dictApi.dictcontrollerImportdictitems(
|
||
formData,
|
||
);
|
||
// 返回 JSON 响应
|
||
return await response.json();
|
||
}}
|
||
onExport={handleExportDictItems}
|
||
onAdd={handleAddDictItem}
|
||
onRefreshDicts={fetchDicts}
|
||
/>
|
||
}
|
||
/>
|
||
</Content>
|
||
</Layout>
|
||
|
||
{/* 字典项 Modal(添加或编辑) */}
|
||
<DictItemModal
|
||
visible={isDictItemModalVisible}
|
||
isEdit={isEditDictItem}
|
||
editingData={editingDictItemData}
|
||
selectedDict={selectedDict}
|
||
onCancel={() => {
|
||
setIsDictItemModalVisible(false);
|
||
setEditingDictItemData(null);
|
||
}}
|
||
onOk={handleDictItemFormSubmit}
|
||
/>
|
||
</PageContainer>
|
||
);
|
||
};
|
||
|
||
export default AttributePage;
|