vdi/web-fe/src/pages/images/index.tsx

609 lines
16 KiB
TypeScript
Raw Normal View History

2025-08-29 09:51:17 +00:00
/* eslint-disable @typescript-eslint/no-use-before-define */
import { CODE, STATUS_MAP } from '@/constants/images.constants';
2025-08-12 01:44:46 +00:00
import { delImagesAPI, getImagesList } from '@/services/images';
2025-08-06 09:43:46 +00:00
import {
DeleteOutlined,
EyeOutlined,
SettingOutlined,
2025-08-29 09:51:17 +00:00
PlusOutlined
2025-08-05 01:57:49 +00:00
} from '@ant-design/icons';
2025-08-06 09:43:46 +00:00
import {
Button,
Checkbox,
Input,
message,
Modal,
Popconfirm,
Popover,
Space,
Table,
Tag,
2025-08-12 01:44:46 +00:00
Tooltip,
2025-08-06 09:43:46 +00:00
} from 'antd';
2025-08-12 01:44:46 +00:00
import dayjs from 'dayjs';
import React, { useCallback, useEffect, useRef, useState } from 'react';
2025-08-14 09:21:32 +00:00
import { ModalDetailShow } from './components/modalShow/modalShow';
import { ImportModal } from './components/uploadFileModal/uploadFileModal';
2025-08-12 01:44:46 +00:00
import useTableParams from './hook/hook';
2025-08-05 01:57:49 +00:00
import './index.less';
2025-08-06 09:43:46 +00:00
import { ReactComponent as RefreshIcon } from '@/assets/icons/refresh.svg';
2025-08-05 01:57:49 +00:00
2025-08-29 09:51:17 +00:00
// 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';
2025-08-29 09:51:17 +00:00
align?: 'left' | 'center' | 'right';
defaultVisible: boolean; // 默认是否显示
alwaysVisible?: boolean; // 始终显示的列
2025-08-13 01:22:10 +00:00
ellipsis?: boolean; // 是否启用省略号
2025-08-12 01:44:46 +00:00
filters?: { text: string; value: string }[];
filterMultiple?: boolean; // 是否多选过滤
filterDropdown?: (props: any) => React.ReactNode;
2025-08-12 01:44:46 +00:00
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);
2025-08-29 09:51:17 +00:00
if (immediate && !timer) {
func(...args);
}
2025-08-29 09:51:17 +00:00
timer = setTimeout(() => {
if (!immediate) {
func(...args);
}
timer = null;
}, delay);
};
2025-08-29 09:51:17 +00:00
debounced.cancel = () => {
if (timer) {
clearTimeout(timer);
timer = null;
}
2025-08-12 01:44:46 +00:00
};
2025-08-29 09:51:17 +00:00
return debounced;
2025-08-12 01:44:46 +00:00
};
2025-08-29 09:51:17 +00:00
const ImageList: React.FC<DESK.ImagesProps> = (props) => {
const { activeTabKey } = props;
2025-08-06 09:43:46 +00:00
const [images, setImages] = useState<IMAGES.ImageItem[]>([]);
2025-08-05 01:57:49 +00:00
const [loading, setLoading] = useState(false);
2025-08-06 09:43:46 +00:00
const [selectedImage, setSelectedImage] = useState<IMAGES.ImageItem | null>(
null,
);
2025-08-05 01:57:49 +00:00
const [detailVisible, setDetailVisible] = useState(false);
2025-08-06 09:43:46 +00:00
const [importModalVisible, setImportModalVisible] = useState(false);
2025-08-12 01:44:46 +00:00
const [searchText, setSearchText] = useState<string>(''); // 添加本地搜索状态
const searchInputRef = useRef<string>(''); // 保存已发送请求的搜索文本
const { tableParams, getApiParams, updateParams, handleTableChange } =
useTableParams({
pagination: {
current: 1,
pageSize: 10,
},
search: {}, // 初始化搜索参数
2025-08-12 01:44:46 +00:00
});
// 在组件顶部添加一个 ref 来保存最新的 tableParams
const tableParamsRef = useRef(tableParams);
tableParamsRef.current = tableParams; // 每次渲染时更新 ref 的值
2025-08-06 09:43:46 +00:00
const [columnSettingsVisible, setColumnSettingsVisible] = useState(false);
2025-08-05 01:57:49 +00:00
2025-08-29 09:51:17 +00:00
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);
}
};
// 表格参数变化 获取镜像列表
2025-08-05 01:57:49 +00:00
useEffect(() => {
2025-08-29 09:51:17 +00:00
if (activeTabKey === '1') {
loadImages();
}
2025-08-06 09:43:46 +00:00
}, [
tableParams.pagination?.current,
tableParams.pagination?.pageSize,
tableParams?.sortOrder,
tableParams?.sortField,
JSON.stringify(tableParams.filters), // 表格搜索参数
JSON.stringify(tableParams.search), // 搜索参数依赖
2025-08-29 09:51:17 +00:00
activeTabKey,
2025-08-06 09:43:46 +00:00
]);
// 定义所有列的配置
const columnConfigs: ColumnConfig[] = [
{
key: 'index',
title: '序号',
2025-08-12 01:44:46 +00:00
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',
2025-08-29 09:51:17 +00:00
// title: '镜像名称',
title: '名称',
dataIndex: 'image_name',
2025-08-14 09:21:32 +00:00
width: 150,
defaultVisible: true,
alwaysVisible: true,
2025-08-13 01:22:10 +00:00
ellipsis: true,
render: (text: string) =>
text ? (
<Tooltip title={text} placement="topLeft">
{text}
</Tooltip>
) : (
'--'
),
},
{
2025-08-29 09:51:17 +00:00
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,
2025-08-14 09:21:32 +00:00
ellipsis: true,
render: (text: string) =>
text ? (
<Tooltip title={text} placement="topLeft">
{text}
</Tooltip>
) : (
'--'
),
},
2025-08-14 09:21:32 +00:00
// {
2025-08-29 09:51:17 +00:00
// 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>
// ) : (
// '--'
// ),
// },
// {
2025-08-14 09:21:32 +00:00
// 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: ['全部'],
// },
2025-08-29 09:51:17 +00:00
// {
// 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',
2025-08-14 09:21:32 +00:00
title: '上传时间',
dataIndex: 'create_time',
2025-08-13 08:34:32 +00:00
width: 160,
2025-08-13 01:22:10 +00:00
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,
2025-08-13 01:22:10 +00:00
ellipsis: true,
},
{
key: 'action',
title: '操作',
2025-08-13 08:34:32 +00:00
width: 90,
2025-08-29 09:51:17 +00:00
align: 'center',
fixed: 'right' as 'right',
render: (_: any, record: IMAGES.ImageItem) => (
<Space size="small">
2025-08-29 09:51:17 +00:00
{/* <Button
type="text"
icon={<EyeOutlined />}
onClick={() => handleViewDetail(record)}
title="查看详情"
2025-08-29 09:51:17 +00:00
/> */}
<Button
size="small"
type="link"
title="编辑"
onClick={() => handleViewDetail(record)}
>
</Button>
<Popconfirm
title="确定要删除这个镜像吗?"
description="删除后无法恢复,请谨慎操作。"
onConfirm={() => handleDelete(record)}
okText="确定"
cancelText="取消"
>
2025-08-29 09:51:17 +00:00
{/* <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) => {
2025-08-14 09:21:32 +00:00
const config = STATUS_MAP[status as keyof typeof STATUS_MAP];
2025-08-13 01:22:10 +00:00
return <Tag color={config?.color}>{config.text}</Tag>;
};
2025-08-05 01:57:49 +00:00
2025-08-06 09:43:46 +00:00
const handleViewDetail = (record: IMAGES.ImageItem) => {
setSelectedImage(record);
setDetailVisible(true);
2025-08-05 01:57:49 +00:00
};
2025-08-06 09:43:46 +00:00
const handleDelete = (record: IMAGES.ImageItem) => {
2025-08-05 01:57:49 +00:00
Modal.confirm({
title: '确认删除',
2025-08-06 09:43:46 +00:00
content: `确定要删除镜像 "${record.image_name}" 吗?`,
2025-08-05 01:57:49 +00:00
onOk: () => {
2025-08-29 09:51:17 +00:00
delImagesAPI({ id: record.id }).then((res) => {
2025-08-12 01:44:46 +00:00
if (res.code == CODE) {
message.success('删除成功');
loadImages();
} else {
message.error(res.message || '删除失败');
}
});
2025-08-06 09:43:46 +00:00
},
2025-08-05 01:57:49 +00:00
});
};
2025-08-06 09:43:46 +00:00
// 列设置相关函数
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>
))}
2025-08-06 09:43:46 +00:00
<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 {
2025-08-12 01:44:46 +00:00
...config,
hidden: undefined,
};
}
// 对于可控制显示/隐藏的列
return {
2025-08-12 01:44:46 +00:00
...config,
...(visibleColumns[config.key] ? {} : { hidden: true }),
};
})
.filter((column) => !column.hidden) as TableColumn[];
2025-08-06 09:43:46 +00:00
const handleRefresh = () => {
loadImages();
};
// 导入镜像成功后的回调
2025-08-06 09:43:46 +00:00
const handleImportSuccess = () => {
setTimeout(() => {
loadImages();
2025-08-06 09:43:46 +00:00
}, 5000);
};
2025-08-05 01:57:49 +00:00
// 自定义分页配置
const paginationConfig = {
...tableParams.pagination,
showTotal: (total: number) => `${total} 条记录`,
showSizeChanger: true,
showQuickJumper: true,
pageSizeOptions: ['10', '20', '50', '100'],
};
2025-08-12 01:44:46 +00:00
const handleSearch = useCallback(
(searchValue: string) => {
const currentTableParams = tableParamsRef.current;
updateParams({
search: {
image_name: searchValue,
},
pagination: {
current: 1,
pageSize: currentTableParams.pagination?.pageSize || 10,
},
});
2025-08-12 01:44:46 +00:00
},
[updateParams],
);
// 防抖版本500ms延迟不立即执行
2025-08-12 01:44:46 +00:00
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);
};
2025-08-12 01:44:46 +00:00
2025-08-05 01:57:49 +00:00
return (
<div className="image-list">
<div className="search-box">
2025-08-29 09:51:17 +00:00
<Button type="primary" icon={<PlusOutlined />} onClick={() => setImportModalVisible(true)}></Button>
<div className="search-input">
2025-08-06 09:43:46 +00:00
<Input.Search
placeholder="镜像名称"
value={searchText}
onChange={(e) => handleSearchChange(e.target.value)}
2025-08-12 01:44:46 +00:00
style={{ width: 300 }}
onSearch={handleEnterSearch}
2025-08-06 09:43:46 +00:00
/>
<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}
2025-08-12 01:44:46 +00:00
rowKey="id"
loading={loading}
pagination={paginationConfig}
onChange={handleTableChange}
2025-08-12 01:44:46 +00:00
scroll={{
y: 'max-content', // 关键:允许内容决定高度
}}
style={{
height: '100%',
display: 'flex',
flexDirection: 'column',
}}
/>
</div>
</div>
2025-08-06 09:43:46 +00:00
{detailVisible ? (
<ModalDetailShow
title="镜像详情"
detailVisible={detailVisible}
setDetailVisible={setDetailVisible}
selectedImage={selectedImage}
/>
) : null}
{/* 导入弹窗 */}
<ImportModal
visible={importModalVisible}
onCancel={() => setImportModalVisible(false)}
onImportSuccess={handleImportSuccess}
/>
2025-08-05 01:57:49 +00:00
</div>
);
};
2025-08-06 09:43:46 +00:00
export default ImageList;