vdi/web-fe/src/pages/imagePage/tool/index.tsx

524 lines
14 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 } from '@/constants/images.constants';
import { deleteTool, getToollList } from '@/services/imagePage';
import { SettingOutlined,PlusOutlined } from '@ant-design/icons';
import {
Button,
Checkbox,
Input,
message,
Modal,
Popconfirm,
Popover,
Space,
Table,
Tooltip,
} from 'antd';
import dayjs from 'dayjs';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import useTableParams from './hook/hook';
import './index.less';
import UploadFileModal from './uploadFileModal';
import { ReactComponent as RefreshIcon } from '@/assets/icons/refresh.svg';
// 列配置定义
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 = <T extends (...args: any[]) => any>(
func: T,
delay: number,
immediate = false,
) => {
let timer: NodeJS.Timeout | null = null;
const debounced = (...args: Parameters<T>) => {
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 Index: React.FC<DESK.ImagesProps> = (props) => {
const { activeTabKey } = props;
const [dataSource, setDataSource] = useState<TOOL.ToolItem[]>([]);
const [loading, setLoading] = useState(false);
const [selectedImage, setSelectedImage] = useState<any>(null);
const [importModalVisible, setImportModalVisible] = useState(false);
const [searchText, setSearchText] = useState<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);
// 表格参数变化 获取镜像列表
useEffect(() => {
if (activeTabKey === '4') {
loadDataSource();
}
}, [
activeTabKey,
tableParams.pagination?.current,
tableParams.pagination?.pageSize,
tableParams?.sortOrder,
tableParams?.sortField,
JSON.stringify(tableParams.filters), // 表格搜索参数
JSON.stringify(tableParams.search), // 搜索参数依赖
]);
// 定义所有列的配置
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: 'tool_name',
// title: '镜像名称',
title: '文件名',
dataIndex: 'tool_name',
width: 150,
defaultVisible: true,
alwaysVisible: true,
ellipsis: true,
render: (text: string) =>
text ? (
<Tooltip title={text} placement="topLeft">
{text}
</Tooltip>
) : (
'--'
),
},
{
key: 'tool_type',
title: '文件类型',
dataIndex: 'tool_type',
width: 100,
defaultVisible: true,
ellipsis: true,
render: (text: string) =>
text ? <Tooltip title={text}>{text}</Tooltip> : '--',
},
{
key: 'file_size',
// title: '镜像版本',
title: '文件大小',
dataIndex: 'file_size',
width: 100,
defaultVisible: true,
ellipsis: true,
render: (text: string) =>
text ? <Tooltip title={text}>{text}</Tooltip> : '--',
},
{
key: 'version',
// title: '模板存放路径',
title: '版本',
dataIndex: 'version',
width: 140,
defaultVisible: true,
ellipsis: true,
render: (text: string) =>
text ? (
<Tooltip title={text} placement="topLeft">
{text}
</Tooltip>
) : (
'--'
),
},
{
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: 'description',
title: '描述',
dataIndex: 'description',
width: 140,
defaultVisible: true,
ellipsis: true,
render: (text: string) =>
text ? (
<Tooltip title={text} placement="topLeft">
{text}
</Tooltip>
) : (
'--'
),
},
{
key: 'action',
title: '操作',
width: 90,
align: 'center',
fixed: 'right' as 'right',
render: (_: any, record: TOOL.ToolItem) => (
<Space size="small">
<Button
size="small"
type="link"
title="编辑"
onClick={() => handleViewDetail(record)}
>
</Button>
<Popconfirm
title="确定要删除这个镜像吗?"
description="删除后无法恢复,请谨慎操作。"
onConfirm={() => handleDelete(record)}
okText="确定"
cancelText="取消"
>
<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 loadDataSource = async () => {
setLoading(true);
try {
// 将搜索文本合并到API参数中
const apiParams = {
...getApiParams(),
};
const imagesRes = await getToollList(apiParams);
if (imagesRes.code === CODE) {
setDataSource(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);
}
};
const handleViewDetail = (record: TOOL.ToolItem) => {
setSelectedImage(record);
setImportModalVisible(true);
};
const handleDelete = (record: TOOL.ToolItem) => {
Modal.confirm({
title: '确认删除',
content: `确定要删除镜像 "${record.image_name}" 吗?`,
onOk: () => {
deleteTool({ id: record.id }).then((res) => {
if (res.code === CODE) {
message.success('删除成功');
loadDataSource();
} 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 = () => {
loadDataSource();
};
// 自定义分页配置
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);
};
const onSubmitBack = () => {
setImportModalVisible(false);
setSelectedImage(null);
handleRefresh();
};
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={dataSource}
rowKey="id"
loading={loading}
pagination={paginationConfig}
onChange={handleTableChange}
scroll={{
y: 'max-content', // 关键:允许内容决定高度
}}
style={{
height: '100%',
display: 'flex',
flexDirection: 'column',
}}
/>
</div>
</div>
{importModalVisible && (
<UploadFileModal
visible={importModalVisible}
onCancel={() => {
setImportModalVisible(false);
setSelectedImage(null);
}}
onSubmit={onSubmitBack}
isEditing={selectedImage ? true : false}
initialValues={selectedImage}
/>
)}
</div>
);
};
export default Index;