2025-08-06 09:43:46 +00:00
|
|
|
import { ERROR_CODE, IMAGES_TYPE_MAP } from '@/constants/images.constants';
|
|
|
|
|
import { getImagesList } from '@/services/images';
|
|
|
|
|
import {
|
|
|
|
|
DeleteOutlined,
|
|
|
|
|
EyeOutlined,
|
|
|
|
|
SettingOutlined,
|
2025-08-05 01:57:49 +00:00
|
|
|
} from '@ant-design/icons';
|
2025-08-06 09:43:46 +00:00
|
|
|
import type { TableProps } from 'antd';
|
|
|
|
|
import {
|
|
|
|
|
Button,
|
|
|
|
|
Checkbox,
|
|
|
|
|
Input,
|
|
|
|
|
message,
|
|
|
|
|
Modal,
|
|
|
|
|
Popconfirm,
|
|
|
|
|
Popover,
|
|
|
|
|
Space,
|
|
|
|
|
Table,
|
|
|
|
|
Tooltip,
|
|
|
|
|
} from 'antd';
|
|
|
|
|
import React, { useEffect, useState } from 'react';
|
|
|
|
|
import { ModalDetailShow, ImportModal } from './components/modalShow/modalShow';
|
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
|
|
|
|
|
|
|
|
const ImageList: React.FC = () => {
|
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,
|
|
|
|
|
);
|
|
|
|
|
const [searchText, setSearchText] = useState<string>('');
|
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);
|
|
|
|
|
const [tableParams, setTableParams] = useState<IMAGES.TableParams>({
|
|
|
|
|
pagination: {
|
|
|
|
|
current: 1,
|
|
|
|
|
pageSize: 10,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 列显示控制状态
|
|
|
|
|
const [visibleColumns, setVisibleColumns] = useState<Record<string, boolean>>(
|
|
|
|
|
{
|
|
|
|
|
image_type: true,
|
|
|
|
|
storage_path: true,
|
|
|
|
|
bt_path: true,
|
|
|
|
|
create_time: true,
|
|
|
|
|
action: true,
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
const [columnSettingsVisible, setColumnSettingsVisible] = useState(false);
|
2025-08-05 01:57:49 +00:00
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
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.keywords),
|
|
|
|
|
]);
|
|
|
|
|
const getRandomuserParams = (params: IMAGES.TableParams) => {
|
|
|
|
|
const {
|
|
|
|
|
pagination,
|
|
|
|
|
filters,
|
|
|
|
|
sortField,
|
|
|
|
|
sortOrder,
|
|
|
|
|
keywords,
|
|
|
|
|
...restParams
|
|
|
|
|
} = params;
|
|
|
|
|
const result: Record<string, any> = {};
|
2025-08-05 01:57:49 +00:00
|
|
|
|
2025-08-06 09:43:46 +00:00
|
|
|
result.page_size = pagination?.pageSize;
|
|
|
|
|
result.page_num = pagination?.current;
|
|
|
|
|
|
|
|
|
|
if (keywords) {
|
|
|
|
|
result.keywords = keywords;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (filters) {
|
|
|
|
|
Object.entries(filters).forEach(([key, value]) => {
|
|
|
|
|
if (value !== undefined && value !== null) {
|
|
|
|
|
result[key] = value;
|
2025-08-05 01:57:49 +00:00
|
|
|
}
|
2025-08-06 09:43:46 +00:00
|
|
|
});
|
|
|
|
|
}
|
2025-08-05 01:57:49 +00:00
|
|
|
|
2025-08-06 09:43:46 +00:00
|
|
|
if (sortField) {
|
|
|
|
|
result.orderby = sortField;
|
|
|
|
|
result.order = sortOrder === 'ascend' ? 'asc' : 'desc';
|
|
|
|
|
}
|
2025-08-05 01:57:49 +00:00
|
|
|
|
2025-08-06 09:43:46 +00:00
|
|
|
// 处理其他参数
|
|
|
|
|
Object.entries(restParams).forEach(([key, value]) => {
|
|
|
|
|
if (value !== undefined && value !== null) {
|
|
|
|
|
result[key] = value;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return result;
|
2025-08-05 01:57:49 +00:00
|
|
|
};
|
2025-08-06 09:43:46 +00:00
|
|
|
const params = getRandomuserParams(tableParams);
|
2025-08-05 01:57:49 +00:00
|
|
|
|
2025-08-06 09:43:46 +00:00
|
|
|
const loadImages = async () => {
|
|
|
|
|
setLoading(true);
|
|
|
|
|
try {
|
|
|
|
|
const imagesRes = await getImagesList(params);
|
|
|
|
|
if (imagesRes.error_code === ERROR_CODE) {
|
|
|
|
|
setImages(imagesRes.data.data || []);
|
|
|
|
|
setLoading(false);
|
|
|
|
|
setTableParams((prev) => ({
|
|
|
|
|
...prev,
|
|
|
|
|
pagination: {
|
|
|
|
|
...prev.pagination,
|
|
|
|
|
total: imagesRes.data.paging.total || 0,
|
|
|
|
|
},
|
|
|
|
|
}));
|
|
|
|
|
} else {
|
|
|
|
|
message.error(imagesRes.message || '获取镜像列表失败');
|
|
|
|
|
setLoading(false);
|
|
|
|
|
}
|
|
|
|
|
} catch (err) {
|
|
|
|
|
message.error('获取镜像列表失败');
|
|
|
|
|
setLoading(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
2025-08-05 01:57:49 +00:00
|
|
|
|
2025-08-06 09:43:46 +00:00
|
|
|
// const getStatusTag = (status: string) => {
|
|
|
|
|
// const statusMap = {
|
|
|
|
|
// active: { color: 'green', text: '可用' },
|
|
|
|
|
// inactive: { color: 'red', text: '不可用' },
|
|
|
|
|
// building: { color: 'orange', text: '构建中' },
|
|
|
|
|
// };
|
|
|
|
|
// const config = statusMap[status as keyof typeof statusMap];
|
|
|
|
|
// 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-06 09:43:46 +00:00
|
|
|
// TODO: 调用删除接口
|
|
|
|
|
setImages(images.filter((img) => img.image_id !== record.image_id));
|
2025-08-05 01:57:49 +00:00
|
|
|
message.success('删除成功');
|
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 resetColumns = () => {
|
|
|
|
|
setVisibleColumns({
|
|
|
|
|
image_type: true,
|
|
|
|
|
storage_path: true,
|
|
|
|
|
bt_path: true,
|
|
|
|
|
create_time: true,
|
|
|
|
|
action: true,
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 列设置内容
|
|
|
|
|
const columnSettingsContent = (
|
|
|
|
|
<div style={{ padding: '8px 0' }}>
|
|
|
|
|
<div style={{ padding: '4px 12px' }}>
|
|
|
|
|
<Checkbox
|
|
|
|
|
checked={visibleColumns['image_type']}
|
|
|
|
|
onChange={(e) => handleColumnChange('image_type', e.target.checked)}
|
|
|
|
|
>
|
|
|
|
|
桌面类型
|
|
|
|
|
</Checkbox>
|
|
|
|
|
</div>
|
|
|
|
|
<div style={{ padding: '4px 12px' }}>
|
|
|
|
|
<Checkbox
|
|
|
|
|
checked={visibleColumns['storage_path']}
|
|
|
|
|
onChange={(e) => handleColumnChange('storage_path', e.target.checked)}
|
|
|
|
|
>
|
|
|
|
|
模板存放路径
|
|
|
|
|
</Checkbox>
|
|
|
|
|
</div>
|
|
|
|
|
<div style={{ padding: '4px 12px' }}>
|
|
|
|
|
<Checkbox
|
|
|
|
|
checked={visibleColumns['bt_path']}
|
|
|
|
|
onChange={(e) => handleColumnChange('bt_path', e.target.checked)}
|
|
|
|
|
>
|
|
|
|
|
BT路径
|
|
|
|
|
</Checkbox>
|
|
|
|
|
</div>
|
|
|
|
|
<div style={{ padding: '4px 12px' }}>
|
|
|
|
|
<Checkbox
|
|
|
|
|
checked={visibleColumns['create_time']}
|
|
|
|
|
onChange={(e) => handleColumnChange('create_time', e.target.checked)}
|
|
|
|
|
>
|
|
|
|
|
创建时间
|
|
|
|
|
</Checkbox>
|
|
|
|
|
</div>
|
|
|
|
|
<div style={{ padding: '4px 12px' }}>
|
|
|
|
|
<Checkbox
|
|
|
|
|
checked={visibleColumns['action']}
|
|
|
|
|
onChange={(e) => handleColumnChange('action', e.target.checked)}
|
|
|
|
|
>
|
|
|
|
|
操作
|
|
|
|
|
</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 = [
|
2025-08-05 01:57:49 +00:00
|
|
|
{
|
|
|
|
|
title: '镜像名称',
|
2025-08-06 09:43:46 +00:00
|
|
|
dataIndex: 'image_name',
|
|
|
|
|
key: 'image_name',
|
2025-08-05 01:57:49 +00:00
|
|
|
width: 200,
|
|
|
|
|
},
|
|
|
|
|
{
|
2025-08-06 09:43:46 +00:00
|
|
|
title: '桌面类型',
|
|
|
|
|
dataIndex: 'image_type',
|
|
|
|
|
key: 'image_type',
|
|
|
|
|
width: 200,
|
|
|
|
|
render: (text: number) => {
|
|
|
|
|
const key = text as keyof typeof IMAGES_TYPE_MAP;
|
|
|
|
|
return <Tooltip>{IMAGES_TYPE_MAP[key] || '--'}</Tooltip>;
|
|
|
|
|
},
|
|
|
|
|
...(visibleColumns['image_type'] ? {} : { hidden: true }),
|
2025-08-05 01:57:49 +00:00
|
|
|
},
|
|
|
|
|
{
|
2025-08-06 09:43:46 +00:00
|
|
|
title: '模板存放路径',
|
|
|
|
|
dataIndex: 'storage_path',
|
|
|
|
|
key: 'storage_path',
|
|
|
|
|
width: 200,
|
|
|
|
|
...(visibleColumns['storage_path'] ? {} : { hidden: true }),
|
2025-08-05 01:57:49 +00:00
|
|
|
},
|
|
|
|
|
{
|
2025-08-06 09:43:46 +00:00
|
|
|
title: 'BT路径',
|
|
|
|
|
dataIndex: 'bt_path',
|
|
|
|
|
key: 'bt_path',
|
|
|
|
|
width: 200,
|
|
|
|
|
...(visibleColumns['bt_path'] ? {} : { hidden: true }),
|
2025-08-05 01:57:49 +00:00
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
title: '创建时间',
|
2025-08-06 09:43:46 +00:00
|
|
|
dataIndex: 'create_time',
|
|
|
|
|
key: 'create_time',
|
2025-08-05 01:57:49 +00:00
|
|
|
width: 180,
|
2025-08-06 09:43:46 +00:00
|
|
|
...(visibleColumns['create_time'] ? {} : { hidden: true }),
|
2025-08-05 01:57:49 +00:00
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
title: '操作',
|
|
|
|
|
key: 'action',
|
|
|
|
|
width: 200,
|
2025-08-06 09:43:46 +00:00
|
|
|
render: (_: any, record: IMAGES.ImageItem) => (
|
2025-08-05 01:57:49 +00:00
|
|
|
<Space size="small">
|
|
|
|
|
<Button
|
|
|
|
|
type="text"
|
|
|
|
|
icon={<EyeOutlined />}
|
|
|
|
|
onClick={() => handleViewDetail(record)}
|
|
|
|
|
title="查看详情"
|
|
|
|
|
/>
|
|
|
|
|
<Popconfirm
|
|
|
|
|
title="确定要删除这个镜像吗?"
|
|
|
|
|
description="删除后无法恢复,请谨慎操作。"
|
|
|
|
|
onConfirm={() => handleDelete(record)}
|
|
|
|
|
okText="确定"
|
|
|
|
|
cancelText="取消"
|
|
|
|
|
>
|
2025-08-06 09:43:46 +00:00
|
|
|
<Button type="text" icon={<DeleteOutlined />} title="删除" danger />
|
2025-08-05 01:57:49 +00:00
|
|
|
</Popconfirm>
|
|
|
|
|
</Space>
|
|
|
|
|
),
|
2025-08-06 09:43:46 +00:00
|
|
|
...(visibleColumns['action'] ? {} : { hidden: true }),
|
2025-08-05 01:57:49 +00:00
|
|
|
},
|
2025-08-06 09:43:46 +00:00
|
|
|
].filter((column) => !column.hidden);
|
|
|
|
|
|
|
|
|
|
const handleTableChange: TableProps<IMAGES.ImageItem>['onChange'] = (
|
|
|
|
|
pagination,
|
|
|
|
|
filters,
|
|
|
|
|
sorter,
|
|
|
|
|
) => {
|
|
|
|
|
setTableParams({
|
|
|
|
|
pagination: {
|
|
|
|
|
current: pagination.current || 1,
|
|
|
|
|
pageSize: pagination.pageSize || 10,
|
|
|
|
|
},
|
|
|
|
|
filters,
|
|
|
|
|
sortOrder: Array.isArray(sorter) ? undefined : sorter.order,
|
|
|
|
|
sortField: Array.isArray(sorter)
|
|
|
|
|
? undefined
|
|
|
|
|
: (sorter.field as string | number | undefined),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (pagination.pageSize !== tableParams.pagination?.pageSize) {
|
|
|
|
|
setImages([]);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleRefresh = () => {
|
|
|
|
|
loadImages();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 导入成功后的回调
|
|
|
|
|
const handleImportSuccess = () => {
|
|
|
|
|
// 可以在这里添加导入成功后的处理逻辑
|
|
|
|
|
// 例如:刷新列表或显示提示信息
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
loadImages(); // 一段时间后刷新列表查看是否有新导入的镜像
|
|
|
|
|
}, 5000);
|
|
|
|
|
};
|
2025-08-05 01:57:49 +00:00
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="image-list">
|
2025-08-06 09:43:46 +00:00
|
|
|
<div
|
|
|
|
|
style={{
|
|
|
|
|
marginBottom: 16,
|
|
|
|
|
display: 'flex',
|
|
|
|
|
justifyContent: 'space-between',
|
|
|
|
|
}}
|
2025-08-05 01:57:49 +00:00
|
|
|
>
|
2025-08-06 09:43:46 +00:00
|
|
|
<div>
|
|
|
|
|
<Button onClick={() => setImportModalVisible(true)}>导入</Button>
|
|
|
|
|
</div>
|
|
|
|
|
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
|
|
|
|
|
<Input.Search
|
|
|
|
|
placeholder="镜像名称"
|
|
|
|
|
value={searchText}
|
|
|
|
|
onChange={(e) => setSearchText(e.target.value)}
|
|
|
|
|
style={{ width: 300 }}
|
|
|
|
|
onSearch={(value) => {
|
|
|
|
|
setTableParams((prev) => ({
|
|
|
|
|
...prev,
|
|
|
|
|
pagination: {
|
|
|
|
|
...prev.pagination,
|
|
|
|
|
current: 1,
|
|
|
|
|
},
|
|
|
|
|
keywords: value,
|
|
|
|
|
}));
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
<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>
|
|
|
|
|
<Table
|
|
|
|
|
columns={filteredColumns}
|
|
|
|
|
dataSource={images}
|
|
|
|
|
rowKey="image_id"
|
|
|
|
|
loading={loading}
|
|
|
|
|
pagination={tableParams.pagination}
|
|
|
|
|
onChange={handleTableChange}
|
|
|
|
|
// pagination={{
|
|
|
|
|
// total: images.length,
|
|
|
|
|
// pageSize: 10,
|
|
|
|
|
// showSizeChanger: true,
|
|
|
|
|
// showQuickJumper: true,
|
|
|
|
|
// showTotal: (total) => `共 ${total} 条记录`,
|
|
|
|
|
// }}
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
{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;
|