609 lines
16 KiB
TypeScript
609 lines
16 KiB
TypeScript
/* eslint-disable @typescript-eslint/no-use-before-define */
|
||
import { CODE, STATUS_MAP } from '@/constants/images.constants';
|
||
import { delImagesAPI, getImagesList } from '@/services/images';
|
||
import {
|
||
DeleteOutlined,
|
||
EyeOutlined,
|
||
SettingOutlined,
|
||
PlusOutlined
|
||
} from '@ant-design/icons';
|
||
import {
|
||
Button,
|
||
Checkbox,
|
||
Input,
|
||
message,
|
||
Modal,
|
||
Popconfirm,
|
||
Popover,
|
||
Space,
|
||
Table,
|
||
Tag,
|
||
Tooltip,
|
||
} from 'antd';
|
||
import dayjs from 'dayjs';
|
||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||
import { ModalDetailShow } from './components/modalShow/modalShow';
|
||
import { ImportModal } from './components/uploadFileModal/uploadFileModal';
|
||
import useTableParams from './hook/hook';
|
||
import './index.less';
|
||
|
||
import { ReactComponent as RefreshIcon } from '@/assets/icons/refresh.svg';
|
||
|
||
// interface ImagesProps {
|
||
// activeTabKey?: string;
|
||
// }
|
||
|
||
// 列配置定义
|
||
type ColumnConfig = {
|
||
key: string;
|
||
title: string;
|
||
dataIndex?: string;
|
||
width: number;
|
||
render?: (text: any, record: any, index: number) => React.ReactNode;
|
||
fixed?: 'left' | 'right';
|
||
align?: 'left' | 'center' | 'right';
|
||
defaultVisible: boolean; // 默认是否显示
|
||
alwaysVisible?: boolean; // 始终显示的列
|
||
ellipsis?: boolean; // 是否启用省略号
|
||
filters?: { text: string; value: string }[];
|
||
filterMultiple?: boolean; // 是否多选过滤
|
||
filterDropdown?: (props: any) => React.ReactNode;
|
||
defaultFilteredValue?: string[]; // 默认过滤值
|
||
onFilter?: (value: string, record: any) => boolean;
|
||
};
|
||
|
||
type TableColumn = {
|
||
title: string;
|
||
dataIndex?: string;
|
||
key: string;
|
||
width: number;
|
||
render?: any;
|
||
fixed?: 'left' | 'right';
|
||
hidden?: boolean;
|
||
};
|
||
|
||
// 在组件顶部添加防抖函数(支持取消)
|
||
// 增强版防抖函数
|
||
const debounce = (func: Function, delay: number, immediate = false) => {
|
||
let timer: NodeJS.Timeout | null = null;
|
||
const debounced = (...args: any[]) => {
|
||
if (timer) clearTimeout(timer);
|
||
|
||
if (immediate && !timer) {
|
||
func(...args);
|
||
}
|
||
|
||
timer = setTimeout(() => {
|
||
if (!immediate) {
|
||
func(...args);
|
||
}
|
||
timer = null;
|
||
}, delay);
|
||
};
|
||
|
||
debounced.cancel = () => {
|
||
if (timer) {
|
||
clearTimeout(timer);
|
||
timer = null;
|
||
}
|
||
};
|
||
|
||
return debounced;
|
||
};
|
||
|
||
const ImageList: React.FC<DESK.ImagesProps> = (props) => {
|
||
const { activeTabKey } = props;
|
||
const [images, setImages] = useState<IMAGES.ImageItem[]>([]);
|
||
const [loading, setLoading] = useState(false);
|
||
const [selectedImage, setSelectedImage] = useState<IMAGES.ImageItem | null>(
|
||
null,
|
||
);
|
||
const [detailVisible, setDetailVisible] = useState(false);
|
||
const [importModalVisible, setImportModalVisible] = useState(false);
|
||
const [searchText, setSearchText] = useState<string>(''); // 添加本地搜索状态
|
||
const searchInputRef = useRef<string>(''); // 保存已发送请求的搜索文本
|
||
|
||
const { tableParams, getApiParams, updateParams, handleTableChange } =
|
||
useTableParams({
|
||
pagination: {
|
||
current: 1,
|
||
pageSize: 10,
|
||
},
|
||
search: {}, // 初始化搜索参数
|
||
});
|
||
|
||
// 在组件顶部添加一个 ref 来保存最新的 tableParams
|
||
const tableParamsRef = useRef(tableParams);
|
||
tableParamsRef.current = tableParams; // 每次渲染时更新 ref 的值
|
||
|
||
const [columnSettingsVisible, setColumnSettingsVisible] = useState(false);
|
||
|
||
const loadImages = async () => {
|
||
setLoading(true);
|
||
try {
|
||
// 将搜索文本合并到API参数中
|
||
const apiParams = {
|
||
...getApiParams(),
|
||
};
|
||
|
||
const imagesRes = await getImagesList(apiParams);
|
||
if (imagesRes.code == CODE) {
|
||
setImages(imagesRes.data?.data || []);
|
||
setLoading(false);
|
||
|
||
// 正确处理后端返回的分页信息
|
||
updateParams({
|
||
pagination: {
|
||
...tableParams.pagination,
|
||
current: imagesRes.data?.page_num || 1,
|
||
total: imagesRes.data?.total || 0,
|
||
pageSize: tableParams.pagination?.pageSize || 10,
|
||
},
|
||
});
|
||
} else {
|
||
message.error(imagesRes.message || '获取镜像列表失败');
|
||
setLoading(false);
|
||
}
|
||
} catch (err) {
|
||
message.error('获取镜像列表失败');
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
// 表格参数变化 获取镜像列表
|
||
useEffect(() => {
|
||
if (activeTabKey === '1') {
|
||
loadImages();
|
||
}
|
||
}, [
|
||
tableParams.pagination?.current,
|
||
tableParams.pagination?.pageSize,
|
||
tableParams?.sortOrder,
|
||
tableParams?.sortField,
|
||
JSON.stringify(tableParams.filters), // 表格搜索参数
|
||
JSON.stringify(tableParams.search), // 搜索参数依赖
|
||
activeTabKey,
|
||
]);
|
||
|
||
// 定义所有列的配置
|
||
const columnConfigs: ColumnConfig[] = [
|
||
{
|
||
key: 'index',
|
||
title: '序号',
|
||
width: 60,
|
||
render: (text: any, row: any, index: number) =>
|
||
(tableParams.pagination?.current - 1) *
|
||
tableParams.pagination?.pageSize +
|
||
index +
|
||
1,
|
||
defaultVisible: true,
|
||
alwaysVisible: true,
|
||
},
|
||
{
|
||
key: 'image_name',
|
||
// title: '镜像名称',
|
||
title: '名称',
|
||
dataIndex: 'image_name',
|
||
width: 150,
|
||
defaultVisible: true,
|
||
alwaysVisible: true,
|
||
ellipsis: true,
|
||
render: (text: string) =>
|
||
text ? (
|
||
<Tooltip title={text} placement="topLeft">
|
||
{text}
|
||
</Tooltip>
|
||
) : (
|
||
'--'
|
||
),
|
||
},
|
||
{
|
||
key: 'os_version',
|
||
title: '操作系统',
|
||
dataIndex: 'os_version',
|
||
width: 100,
|
||
defaultVisible: true,
|
||
ellipsis: true,
|
||
render: (text: string) =>
|
||
text ? <Tooltip title={text}>{text}</Tooltip> : '--',
|
||
},
|
||
{
|
||
key: 'image_version',
|
||
// title: '镜像版本',
|
||
title: '版本',
|
||
dataIndex: 'image_version',
|
||
width: 100,
|
||
defaultVisible: true,
|
||
ellipsis: true,
|
||
render: (text: string) =>
|
||
text ? <Tooltip title={text}>{text}</Tooltip> : '--',
|
||
},
|
||
{
|
||
key: 'storage_path',
|
||
// title: '模板存放路径',
|
||
title: '存储位置',
|
||
dataIndex: 'storage_path',
|
||
width: 140,
|
||
defaultVisible: true,
|
||
ellipsis: true,
|
||
render: (text: string) =>
|
||
text ? (
|
||
<Tooltip title={text} placement="topLeft">
|
||
{text}
|
||
</Tooltip>
|
||
) : (
|
||
'--'
|
||
),
|
||
},
|
||
// {
|
||
// key: 'image_file_name',
|
||
// title: '镜像文件',
|
||
// dataIndex: 'image_file_name',
|
||
// width: 150,
|
||
// defaultVisible: true,
|
||
// alwaysVisible: true,
|
||
// ellipsis: true,
|
||
// render: (text: string) =>
|
||
// text ? (
|
||
// <Tooltip title={text} placement="topLeft">
|
||
// {text}
|
||
// </Tooltip>
|
||
// ) : (
|
||
// '--'
|
||
// ),
|
||
// },
|
||
// {
|
||
// key: 'image_type',
|
||
// title: '桌面类型',
|
||
// dataIndex: 'image_type',
|
||
// width: 120,
|
||
// render: (text: number) => {
|
||
// const key = text as keyof typeof IMAGES_TYPE_MAP;
|
||
// return text ? IMAGES_TYPE_MAP[key] : '--';
|
||
// },
|
||
// defaultVisible: true,
|
||
// filterDropdown: ({ setSelectedKeys, selectedKeys, confirm }) => (
|
||
// <Menu
|
||
// selectedKeys={selectedKeys.length > 0 ? selectedKeys : ['全部']}
|
||
// onClick={({ key }) => {
|
||
// setSelectedKeys(key === '全部' ? [] : [key]);
|
||
// confirm({ closeDropdown: true }); // 立即触发筛选并关闭下拉菜单
|
||
// }}
|
||
// items={[
|
||
// { key: '全部', label: '全部' },
|
||
// ...Object.entries(IMAGES_TYPE_MAP).map(([key, value]) => ({
|
||
// key,
|
||
// label: value,
|
||
// })),
|
||
// ]}
|
||
// />
|
||
// ),
|
||
// filterMultiple: false,
|
||
// defaultFilteredValue: ['全部'],
|
||
// },
|
||
|
||
// {
|
||
// key: 'bt_path',
|
||
// title: 'BT路径',
|
||
// dataIndex: 'bt_path',
|
||
// width: 140,
|
||
// defaultVisible: true,
|
||
// ellipsis: true,
|
||
// render: (text: string) =>
|
||
// text ? (
|
||
// <Tooltip title={text} placement="topLeft">
|
||
// {text}
|
||
// </Tooltip>
|
||
// ) : (
|
||
// '--'
|
||
// ),
|
||
// },
|
||
// {
|
||
// key: 'image_status',
|
||
// title: '镜像状态',
|
||
// dataIndex: 'image_status',
|
||
// width: 90,
|
||
// render: (text: number) => (text ? getStatusTag(text) : '--'),
|
||
// defaultVisible: true,
|
||
// },
|
||
{
|
||
key: 'create_time',
|
||
title: '上传时间',
|
||
dataIndex: 'create_time',
|
||
width: 160,
|
||
render: (text: string) =>
|
||
text ? (
|
||
<Tooltip title={dayjs(text).format('YYYY-MM-DD HH:mm:ss')}>
|
||
{text ? dayjs(text).format('YYYY-MM-DD HH:mm:ss') : '--'}
|
||
</Tooltip>
|
||
) : (
|
||
'--'
|
||
),
|
||
defaultVisible: true,
|
||
ellipsis: true,
|
||
},
|
||
{
|
||
key: 'action',
|
||
title: '操作',
|
||
width: 90,
|
||
align: 'center',
|
||
fixed: 'right' as 'right',
|
||
render: (_: any, record: IMAGES.ImageItem) => (
|
||
<Space size="small">
|
||
{/* <Button
|
||
type="text"
|
||
icon={<EyeOutlined />}
|
||
onClick={() => handleViewDetail(record)}
|
||
title="查看详情"
|
||
/> */}
|
||
<Button
|
||
size="small"
|
||
type="link"
|
||
title="编辑"
|
||
onClick={() => handleViewDetail(record)}
|
||
>
|
||
详情
|
||
</Button>
|
||
<Popconfirm
|
||
title="确定要删除这个镜像吗?"
|
||
description="删除后无法恢复,请谨慎操作。"
|
||
onConfirm={() => handleDelete(record)}
|
||
okText="确定"
|
||
cancelText="取消"
|
||
>
|
||
{/* <Button type="text" icon={<DeleteOutlined />} title="删除" danger /> */}
|
||
<Button
|
||
size="small"
|
||
type="link"
|
||
title="删除"
|
||
>
|
||
删除
|
||
</Button>
|
||
</Popconfirm>
|
||
</Space>
|
||
),
|
||
defaultVisible: true,
|
||
},
|
||
];
|
||
|
||
// 初始化 visibleColumns 状态
|
||
const initialVisibleColumns = columnConfigs.reduce<Record<string, boolean>>(
|
||
(acc, column) => {
|
||
if (!column.alwaysVisible) {
|
||
acc[column.key] = column.defaultVisible;
|
||
}
|
||
return acc;
|
||
},
|
||
{},
|
||
);
|
||
|
||
const [visibleColumns, setVisibleColumns] = useState<Record<string, boolean>>(
|
||
initialVisibleColumns,
|
||
);
|
||
|
||
// 重置列设置
|
||
const resetColumns = () => {
|
||
setVisibleColumns(initialVisibleColumns);
|
||
};
|
||
|
||
const getStatusTag = (status: number) => {
|
||
const config = STATUS_MAP[status as keyof typeof STATUS_MAP];
|
||
return <Tag color={config?.color}>{config.text}</Tag>;
|
||
};
|
||
|
||
const handleViewDetail = (record: IMAGES.ImageItem) => {
|
||
setSelectedImage(record);
|
||
setDetailVisible(true);
|
||
};
|
||
|
||
const handleDelete = (record: IMAGES.ImageItem) => {
|
||
Modal.confirm({
|
||
title: '确认删除',
|
||
content: `确定要删除镜像 "${record.image_name}" 吗?`,
|
||
onOk: () => {
|
||
delImagesAPI({ id: record.id }).then((res) => {
|
||
if (res.code == CODE) {
|
||
message.success('删除成功');
|
||
loadImages();
|
||
} else {
|
||
message.error(res.message || '删除失败');
|
||
}
|
||
});
|
||
},
|
||
});
|
||
};
|
||
|
||
// 列设置相关函数
|
||
const handleColumnChange = (columnKey: string, checked: boolean) => {
|
||
setVisibleColumns((prev) => ({
|
||
...prev,
|
||
[columnKey]: checked,
|
||
}));
|
||
};
|
||
|
||
// 列设置内容
|
||
const columnSettingsContent = (
|
||
<div style={{ padding: '8px 0' }}>
|
||
{columnConfigs
|
||
.filter((config) => !config.alwaysVisible) // 只显示可控制的列
|
||
.map((config) => (
|
||
<div key={config.key} style={{ padding: '4px 12px' }}>
|
||
<Checkbox
|
||
checked={visibleColumns[config.key]}
|
||
onChange={(e) => handleColumnChange(config.key, e.target.checked)}
|
||
>
|
||
{config.title}
|
||
</Checkbox>
|
||
</div>
|
||
))}
|
||
<div
|
||
style={{
|
||
padding: '8px 12px',
|
||
borderTop: '1px solid #f0f0f0',
|
||
marginTop: 8,
|
||
}}
|
||
>
|
||
<Button type="link" onClick={resetColumns} style={{ padding: 0 }}>
|
||
重置
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
);
|
||
|
||
// 根据visibleColumns过滤显示的列
|
||
const filteredColumns = columnConfigs
|
||
.map((config) => {
|
||
// 对于始终显示的列
|
||
if (config.alwaysVisible) {
|
||
return {
|
||
...config,
|
||
hidden: undefined,
|
||
};
|
||
}
|
||
|
||
// 对于可控制显示/隐藏的列
|
||
return {
|
||
...config,
|
||
...(visibleColumns[config.key] ? {} : { hidden: true }),
|
||
};
|
||
})
|
||
.filter((column) => !column.hidden) as TableColumn[];
|
||
|
||
const handleRefresh = () => {
|
||
loadImages();
|
||
};
|
||
|
||
// 导入镜像成功后的回调
|
||
const handleImportSuccess = () => {
|
||
setTimeout(() => {
|
||
loadImages();
|
||
}, 5000);
|
||
};
|
||
|
||
// 自定义分页配置
|
||
const paginationConfig = {
|
||
...tableParams.pagination,
|
||
showTotal: (total: number) => `共 ${total} 条记录`,
|
||
showSizeChanger: true,
|
||
showQuickJumper: true,
|
||
pageSizeOptions: ['10', '20', '50', '100'],
|
||
};
|
||
|
||
const handleSearch = useCallback(
|
||
(searchValue: string) => {
|
||
const currentTableParams = tableParamsRef.current;
|
||
updateParams({
|
||
search: {
|
||
image_name: searchValue,
|
||
},
|
||
pagination: {
|
||
current: 1,
|
||
pageSize: currentTableParams.pagination?.pageSize || 10,
|
||
},
|
||
});
|
||
},
|
||
[updateParams],
|
||
);
|
||
|
||
// 防抖版本(500ms延迟,不立即执行)
|
||
const debouncedSearch = useRef(debounce(handleSearch, 500)).current;
|
||
// 立即执行版本(用于清空时立即搜索)
|
||
const immediateSearch = useRef(debounce(handleSearch, 0, true)).current;
|
||
|
||
const handleSearchChange = (value: string) => {
|
||
setSearchText(value);
|
||
|
||
// 取消所有未执行的防抖请求
|
||
debouncedSearch.cancel();
|
||
immediateSearch.cancel();
|
||
|
||
// 清空时立即触发搜索
|
||
if (value === '') {
|
||
immediateSearch('');
|
||
return;
|
||
}
|
||
|
||
// 正常输入时使用防抖
|
||
debouncedSearch(value);
|
||
};
|
||
|
||
// 修改回车搜索处理
|
||
const handleEnterSearch = (value: string) => {
|
||
// 回车搜索时取消未执行的防抖
|
||
debouncedSearch.cancel();
|
||
immediateSearch.cancel();
|
||
|
||
// 直接执行搜索
|
||
handleSearch(value);
|
||
};
|
||
|
||
return (
|
||
<div className="image-list">
|
||
<div className="search-box">
|
||
<Button type="primary" icon={<PlusOutlined />} onClick={() => setImportModalVisible(true)}>新建</Button>
|
||
<div className="search-input">
|
||
<Input.Search
|
||
placeholder="镜像名称"
|
||
value={searchText}
|
||
onChange={(e) => handleSearchChange(e.target.value)}
|
||
style={{ width: 300 }}
|
||
onSearch={handleEnterSearch}
|
||
/>
|
||
<Button
|
||
onClick={handleRefresh}
|
||
loading={loading}
|
||
icon={<RefreshIcon style={{ width: 13, height: 13 }} />}
|
||
></Button>
|
||
<Popover
|
||
content={columnSettingsContent}
|
||
title="列设置"
|
||
trigger="click"
|
||
open={columnSettingsVisible}
|
||
onOpenChange={setColumnSettingsVisible}
|
||
placement="bottomRight"
|
||
>
|
||
<Button icon={<SettingOutlined />}></Button>
|
||
</Popover>
|
||
</div>
|
||
</div>
|
||
<div className="images-list-container">
|
||
<div className="images-list-table">
|
||
<Table
|
||
columns={filteredColumns}
|
||
dataSource={images}
|
||
rowKey="id"
|
||
loading={loading}
|
||
pagination={paginationConfig}
|
||
onChange={handleTableChange}
|
||
scroll={{
|
||
y: 'max-content', // 关键:允许内容决定高度
|
||
}}
|
||
style={{
|
||
height: '100%',
|
||
display: 'flex',
|
||
flexDirection: 'column',
|
||
}}
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
{detailVisible ? (
|
||
<ModalDetailShow
|
||
title="镜像详情"
|
||
detailVisible={detailVisible}
|
||
setDetailVisible={setDetailVisible}
|
||
selectedImage={selectedImage}
|
||
/>
|
||
) : null}
|
||
{/* 导入弹窗 */}
|
||
<ImportModal
|
||
visible={importModalVisible}
|
||
onCancel={() => setImportModalVisible(false)}
|
||
onImportSuccess={handleImportSuccess}
|
||
/>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default ImageList;
|