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

609 lines
16 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

/* 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;