fix(镜像列表): 联调+参数管理

master
chenyt 2025-08-12 09:44:46 +08:00
parent 725932dfc2
commit 60e6deaa17
8 changed files with 390 additions and 221 deletions

View File

@ -1,5 +1,5 @@
export default { export default {
'POST /api/files/queryimagesList': (req: any, res: any) => { 'POST /api/nex/v1/queryimagesList': (req: any, res: any) => {
const { page_size, page_num } = req.body; const { page_size, page_num } = req.body;
const data = []; const data = [];
function getRandomFormat() { function getRandomFormat() {
@ -15,13 +15,15 @@ export default {
} }
for (let i = 1; i <= page_size; i++) { for (let i = 1; i <= page_size; i++) {
data.push({ data.push({
image_id: i, id: i,
image_name: `周俊星${(page_num - 1) * page_size + i}`, image_name: `Win版 PR 2024 【支持win10、win11】.zip${
(page_num - 1) * page_size + i
}`,
image_type: getRandomFormat(), image_type: getRandomFormat(),
bt_path: `/serve/logs`, bt_path: `/serve/logs`,
image_version: '1.0.0', image_version: '1.0.0',
os_version: 'Ubuntu 20.04', os_version: 'Ubuntu 20.04',
image_status:Math.random() > 0.5 ? 1 : 2, image_status: Math.random() > 0.5 ? 1 : 2,
storage_path: '/mock/images', storage_path: '/mock/images',
create_time: +new Date(), create_time: +new Date(),
}); });
@ -30,11 +32,9 @@ export default {
code: '200', code: '200',
message: '操作成功', message: '操作成功',
data: { data: {
paging: { total: 520,
total: 520, page_num: page_num,
page_num: page_num, page_size: page_size,
page_size: page_size,
},
data: data, data: data,
}, },
}; };

View File

@ -1,9 +1,7 @@
import { IMAGES_TYPE_MAP } from '@/constants/images.constants'; import { uploadChunkAPI, cancelUploadImagesAPI } from '@/services/images';
import { uploadChunkAPI } from '@/services/images';
import { Alert, Button, message, Modal, Progress, Upload } from 'antd'; import { Alert, Button, message, Modal, Progress, Upload } from 'antd';
import { UploadProps } from 'antd/lib/upload'; import { UploadProps } from 'antd/lib/upload';
import React, { useEffect, useRef, useState } from 'react'; import React, { useEffect, useRef, useState } from 'react';
import SparkMD5 from 'spark-md5';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import { import {
IMAGE_DETAIL_FIELDS, IMAGE_DETAIL_FIELDS,
@ -13,10 +11,10 @@ import {
import { import {
getChunkSize, getChunkSize,
calculateMD5InChunks, calculateMD5InChunks,
calculateMD5InChunksWorkers,
formatTime, formatTime,
} from '@/utils/images'; } from '@/pages/images/utils/images';
const { MAX_CONCURRENT, MAX_FILE_SIZE, ALLOWED_EXTENSIONS } = UPLOAD_CONFIG;
const { READY, UPLOADING, SUCCESS, ERROR } = UPLOAD_STATUS_MAP; const { READY, UPLOADING, SUCCESS, ERROR } = UPLOAD_STATUS_MAP;
const { Dragger } = Upload; const { Dragger } = Upload;
@ -77,8 +75,7 @@ const ImportModal: React.FC<IMAGES.ImportModalProps> = ({
const fileSize = useRef<number>(0); // 文件大小 const fileSize = useRef<number>(0); // 文件大小
const abortController = useRef<AbortController | null>(null); // 用于取消上传 const abortController = useRef<AbortController | null>(null); // 用于取消上传
// 上传镜像时间相关 // 上传镜像时间相关
const [uploadStartTime, setUploadStartTime] = useState<number | null>(null); const [elapsedTime, setElapsedTime] = useState<number>(0); // 上传时间
const [elapsedTime, setElapsedTime] = useState<number>(0);
const timerRef = useRef<NodeJS.Timeout | null>(null); const timerRef = useRef<NodeJS.Timeout | null>(null);
// 添加重置状态函数 // 添加重置状态函数
@ -95,7 +92,6 @@ const ImportModal: React.FC<IMAGES.ImportModalProps> = ({
uploadQueue.current = []; uploadQueue.current = [];
// 重置计时器 // 重置计时器
setUploadStartTime(null);
setElapsedTime(0); setElapsedTime(0);
if (timerRef.current) { if (timerRef.current) {
clearInterval(timerRef.current); clearInterval(timerRef.current);
@ -123,8 +119,17 @@ const ImportModal: React.FC<IMAGES.ImportModalProps> = ({
if (abortController.current?.signal.aborted) { if (abortController.current?.signal.aborted) {
return false; return false;
} }
// 更新状态提示正在计算MD5
// setUploadMessage(`正在计算第${index}个分片的MD5...`);
// 使用 spark-md5 计算当前分片的MD5 // 使用 spark-md5 计算当前分片的MD5
const chunkMD5 = await calculateMD5InChunks(chunk, fileSize.current); const chunkMD5 = await calculateMD5InChunks(
chunk,
fileSize.current,
);
// 更新状态,提示正在上传
// setUploadMessage(`正在上传第${index}个分片...`);
const formData = new FormData(); const formData = new FormData();
formData.append('file_id', fileId.current); formData.append('file_id', fileId.current);
@ -300,18 +305,17 @@ const ImportModal: React.FC<IMAGES.ImportModalProps> = ({
setUploadProgress(0); setUploadProgress(0);
// 启动计时器 // 启动计时器
const startTime = Date.now();
setUploadStartTime(startTime);
setElapsedTime(0); setElapsedTime(0);
// 立即更新一次时间 // 立即更新一次时间
setElapsedTime(Math.floor((Date.now() - startTime) / 1000)); setElapsedTime(Math.floor((Date.now() - Date.now()) / 1000));
if (timerRef.current) { if (timerRef.current) {
clearInterval(timerRef.current); clearInterval(timerRef.current);
} }
timerRef.current = setInterval(() => { timerRef.current = setInterval(() => {
setElapsedTime(Math.floor((Date.now() - startTime) / 1000)); // 重新计算开始时间,基于当前时间和已用时间
setElapsedTime((prev) => prev + 1);
}, 1000); }, 1000);
// 初始化状态 // 初始化状态
@ -389,10 +393,18 @@ const ImportModal: React.FC<IMAGES.ImportModalProps> = ({
}; };
// 取消上传 // 取消上传
const cancelUpload = () => { const cancelUpload = async() => {
if (abortController.current) { if (abortController.current) {
abortController.current.abort(); abortController.current.abort();
} }
// 如果有正在上传的文件调用后端取消上传API
if (fileId.current) {
try {
await cancelUploadImagesAPI({ file_id: fileId.current });
} catch (error) {
console.error('取消上传API调用失败:', error);
}
}
setIsUploading(false); setIsUploading(false);
setUploadStatus(READY); setUploadStatus(READY);
setUploadMessage('上传已取消'); setUploadMessage('上传已取消');
@ -432,7 +444,7 @@ const ImportModal: React.FC<IMAGES.ImportModalProps> = ({
// accept=".tar.gz,.iso,.qcow2" // accept=".tar.gz,.iso,.qcow2"
> >
<p className="ant-upload-drag-icon"> <p className="ant-upload-drag-icon">
<div style={{ fontSize: 48, lineHeight: 1 }}></div> <span style={{ fontSize: 48, lineHeight: 1 }}></span>
</p> </p>
<p className="ant-upload-text"></p> <p className="ant-upload-text"></p>
<p className="ant-upload-hint"> <p className="ant-upload-hint">

View File

@ -0,0 +1,112 @@
import type { TableProps } from 'antd';
import { useCallback, useState } from 'react';
const useTableParams = (
initialParams: IMAGES.TableParams = {
pagination: { current: 1, pageSize: 10 },
filters: {},
sort: {},
},
) => {
const [tableParams, setTableParams] =
useState<IMAGES.TableParams>(initialParams);
const getApiParams = useCallback(() => {
console.log('getApiParams', tableParams);
const { pagination, filters, sort, ...rest } = tableParams;
const apiParams: Record<string, any> = {
page_size: pagination?.pageSize,
page_num: pagination?.current,
...rest,
};
if (sort?.field) {
apiParams.orderby = sort.field;
apiParams.order = sort.order === 'ascend' ? 'asc' : 'desc';
}
Object.entries(filters || {}).forEach(([key, value]) => {
if (value !== undefined && value !== null) {
apiParams[key] = value;
}
});
console.log('getApiParams apiParams', apiParams);
return apiParams;
}, [tableParams]);
const updateParams = useCallback((newParams: Partial<IMAGES.TableParams>) => {
console.log('updateParams', newParams);
setTableParams((prev) => ({
...prev,
...newParams,
pagination: {
...prev.pagination,
...newParams.pagination,
},
}));
}, []);
/**
*
* @param pagination
* @param filters filters
* @param sorter
* @param extra
* @returns void
* */
const handleTableChange = useCallback<
NonNullable<TableProps<IMAGES.ImageItem>['onChange']>
>(
(pagination, filters, sorter, extra) => {
// 过滤掉空值的filters
const filteredFilters: Record<string, any> = {};
Object.entries(filters || {}).forEach(([key, value]) => {
if (key === 'image_type') {
if (Array.isArray(value) && value.length > 0 && value[0] !== '全部') {
filteredFilters[key] = Number(value[0]);
}
} else {
if (Array.isArray(value) && value.length > 0) {
filteredFilters[key] = value[0];
} else if (value !== undefined && value !== null) {
if (!Array.isArray(value) && value !== '') {
filteredFilters[key] = value;
}
}
}
});
const newParams: Partial<IMAGES.TableParams> = {
pagination: {
current: pagination.current || 1,
pageSize: pagination.pageSize || 10,
},
filters: filteredFilters,
};
if (!Array.isArray(sorter)) {
newParams.sort = {
field: sorter.field as string,
order:
sorter.order === 'ascend' || sorter.order === 'descend'
? sorter.order
: undefined,
};
}
console.log('handleTableChange', newParams);
updateParams(newParams);
},
[updateParams],
);
return {
tableParams,
getApiParams,
updateParams,
handleTableChange,
};
};
export default useTableParams;

View File

@ -36,48 +36,55 @@
flex-direction: column; flex-direction: column;
overflow: hidden; overflow: hidden;
.images-list-table { .images-list-table {
display: flex;
flex: 1;
overflow: hidden;
}
}
// 表格适应样式
.ant-table-wrapper {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
.ant-spin-nested-loading {
flex: 1; flex: 1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
overflow: hidden; overflow: hidden;
// 表格适应样式
.ant-spin-container { .ant-table-wrapper {
flex: 1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
overflow: hidden; flex: 1;
overflow: hidden;
.ant-table { .ant-spin-nested-loading {
flex: 1; flex: 1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
overflow: hidden; overflow: hidden;
.ant-table-container { .ant-spin-container {
flex: 1; flex: 1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
overflow: hidden; overflow: hidden;
.ant-table-body { .ant-table {
display: flex;
flex-direction: column;
flex: 1; flex: 1;
overflow-y: auto !important; overflow: hidden;
table { .ant-table-container {
height: 100%; display: flex;
flex-direction: column;
flex: 1;
overflow: hidden;
.ant-table-header {
flex-shrink: 0;
}
.ant-table-body {
flex: 1;
overflow: auto !important;
}
}
// 确保分页器在底部正确显示
.ant-table-pagination {
flex-shrink: 0;
// 确保分页器始终可见
position: relative;
z-index: 1;
} }
} }
} }
@ -85,6 +92,7 @@
} }
} }
} }
.image-detail { .image-detail {
.detail-item { .detail-item {
margin-bottom: 16px; margin-bottom: 16px;

View File

@ -1,12 +1,10 @@
import { CODE, IMAGES_TYPE_MAP } from '@/constants/images.constants'; import { CODE, IMAGES_TYPE_MAP } from '@/constants/images.constants';
import { getImagesList } from '@/services/images'; import { delImagesAPI, getImagesList } from '@/services/images';
import { import {
DeleteOutlined, DeleteOutlined,
EyeOutlined, EyeOutlined,
SettingOutlined, SettingOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import type { TableProps } from 'antd';
import dayjs from 'dayjs';
import { import {
Button, Button,
Checkbox, Checkbox,
@ -17,11 +15,13 @@ import {
Popover, Popover,
Space, Space,
Table, Table,
Tooltip,
Tag, Tag,
Tooltip,
} from 'antd'; } from 'antd';
import React, { useEffect, useState } from 'react'; import dayjs from 'dayjs';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { ImportModal, ModalDetailShow } from './components/modalShow/modalShow'; import { ImportModal, ModalDetailShow } from './components/modalShow/modalShow';
import useTableParams from './hook/hook';
import './index.less'; import './index.less';
import { ReactComponent as RefreshIcon } from '@/assets/icons/refresh.svg'; import { ReactComponent as RefreshIcon } from '@/assets/icons/refresh.svg';
@ -36,6 +36,10 @@ type ColumnConfig = {
fixed?: 'left' | 'right'; fixed?: 'left' | 'right';
defaultVisible: boolean; // 默认是否显示 defaultVisible: boolean; // 默认是否显示
alwaysVisible?: boolean; // 始终显示的列 alwaysVisible?: boolean; // 始终显示的列
filters?: { text: string; value: string }[];
filterMultiple?: boolean; // 是否多选过滤
defaultFilteredValue?: string[]; // 默认过滤值
onFilter?: (value: string, record: any) => boolean;
}; };
type TableColumn = { type TableColumn = {
@ -48,21 +52,37 @@ type TableColumn = {
hidden?: boolean; hidden?: boolean;
}; };
// 在组件顶部添加防抖函数
const debounce = (func: Function, delay: number) => {
let timer: NodeJS.Timeout;
return (...args: any[]) => {
clearTimeout(timer);
timer = setTimeout(() => func(...args), delay);
};
};
const ImageList: React.FC = () => { const ImageList: React.FC = () => {
const [images, setImages] = useState<IMAGES.ImageItem[]>([]); const [images, setImages] = useState<IMAGES.ImageItem[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [selectedImage, setSelectedImage] = useState<IMAGES.ImageItem | null>( const [selectedImage, setSelectedImage] = useState<IMAGES.ImageItem | null>(
null, null,
); );
const [searchText, setSearchText] = useState<string>('');
const [detailVisible, setDetailVisible] = useState(false); const [detailVisible, setDetailVisible] = useState(false);
const [importModalVisible, setImportModalVisible] = useState(false); const [importModalVisible, setImportModalVisible] = useState(false);
const [tableParams, setTableParams] = useState<IMAGES.TableParams>({ const [searchText, setSearchText] = useState<string>(''); // 添加本地搜索状态
pagination: { const searchInputRef = useRef<string>(''); // 保存已发送请求的搜索文本
current: 1,
pageSize: 10, const { tableParams, getApiParams, updateParams, handleTableChange } =
}, useTableParams({
}); pagination: {
current: 1,
pageSize: 10,
},
});
// 在组件顶部添加一个 ref 来保存最新的 tableParams
const tableParamsRef = useRef(tableParams);
tableParamsRef.current = tableParams; // 每次渲染时更新 ref 的值
const [columnSettingsVisible, setColumnSettingsVisible] = useState(false); const [columnSettingsVisible, setColumnSettingsVisible] = useState(false);
@ -75,7 +95,6 @@ const ImageList: React.FC = () => {
tableParams?.sortOrder, tableParams?.sortOrder,
tableParams?.sortField, tableParams?.sortField,
JSON.stringify(tableParams.filters), JSON.stringify(tableParams.filters),
JSON.stringify(tableParams.image_name),
]); ]);
// 定义所有列的配置 // 定义所有列的配置
@ -83,7 +102,7 @@ const ImageList: React.FC = () => {
{ {
key: 'index', key: 'index',
title: '序号', title: '序号',
width: 120, width: 60,
render: (text: any, row: any, index: number) => render: (text: any, row: any, index: number) =>
(tableParams.pagination?.current - 1) * (tableParams.pagination?.current - 1) *
tableParams.pagination?.pageSize + tableParams.pagination?.pageSize +
@ -96,20 +115,52 @@ const ImageList: React.FC = () => {
key: 'image_name', key: 'image_name',
title: '镜像名称', title: '镜像名称',
dataIndex: 'image_name', dataIndex: 'image_name',
width: 120, width: 200,
defaultVisible: true, defaultVisible: true,
alwaysVisible: true, alwaysVisible: true,
render: (text: string) => (
<Tooltip title={text}>
<span
style={{
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
display: 'inline-block',
width: '100%',
}}
>
{text || '--'}
</span>
</Tooltip>
),
}, },
{ {
key: 'image_type', key: 'image_type',
title: '桌面类型', title: '桌面类型',
dataIndex: 'image_type', dataIndex: 'image_type',
width: 80, width: 100,
render: (text: number) => { render: (text: number) => {
const key = text as keyof typeof IMAGES_TYPE_MAP; const key = text as keyof typeof IMAGES_TYPE_MAP;
return <Tooltip>{IMAGES_TYPE_MAP[key] || '--'}</Tooltip>; return <Tooltip>{IMAGES_TYPE_MAP[key] || '--'}</Tooltip>;
}, },
defaultVisible: true, defaultVisible: true,
filters: [
{ text: '全部', value: '全部' },
...Object.entries(IMAGES_TYPE_MAP).map(([key, value]) => ({
text: value,
value: key, // 保持 key 为字符串
})),
],
filterMultiple: false,
defaultFilteredValue: ['全部'],
// onFilter: (value, record) => {
// // 当选择"全部"时显示所有记录
// if (value === '全部') {
// return true;
// }
// // 比较时将字符串 value 转换为数字
// return record.image_type === Number(value);
// },
}, },
{ {
key: 'storage_path', key: 'storage_path',
@ -205,70 +256,31 @@ const ImageList: React.FC = () => {
setVisibleColumns(initialVisibleColumns); setVisibleColumns(initialVisibleColumns);
}; };
/**
*
* @param params -
* @returns
*/
const getRandomuserParams = (params: IMAGES.TableParams) => {
const {
pagination,
filters,
sortField,
sortOrder,
image_name,
...restParams
} = params;
const result: Record<string, any> = {};
result.page_size = pagination?.pageSize;
result.page_num = pagination?.current;
if (image_name) {
result.image_name = image_name;
}
if (filters) {
Object.entries(filters).forEach(([key, value]) => {
if (value !== undefined && value !== null) {
result[key] = value;
}
});
}
if (sortField) {
result.orderby = sortField;
result.order = sortOrder === 'ascend' ? 'asc' : 'desc';
}
// 处理其他参数
Object.entries(restParams).forEach(([key, value]) => {
if (value !== undefined && value !== null) {
result[key] = value;
}
});
return result;
};
const params = getRandomuserParams(tableParams);
const loadImages = async () => { const loadImages = async () => {
setLoading(true); setLoading(true);
try { try {
const imagesRes = await getImagesList(params); // 将搜索文本合并到API参数中
const apiParams = {
...getApiParams(),
};
if (searchInputRef.current) {
apiParams.image_name = searchInputRef.current;
}
const imagesRes = await getImagesList(apiParams);
if (imagesRes.code == CODE) { if (imagesRes.code == CODE) {
setImages(imagesRes.data.data || []); setImages(imagesRes.data?.data || []);
setLoading(false); setLoading(false);
// 直接使用后端返回的分页信息 console.log('imagesRes', imagesRes);
setTableParams((prev) => ({
...prev, // 正确处理后端返回的分页信息
updateParams({
pagination: { pagination: {
...prev.pagination, ...tableParams.pagination,
current: imagesRes.data.paging.page_num || 1, current: imagesRes.data?.page_num || 1,
total: imagesRes.data.paging.total || 0, total: imagesRes.data?.total || 0,
pageSize: imagesRes.data.paging.page_size || 10, pageSize: tableParams.pagination.pageSize|| 10,
}, },
})); });
} else { } else {
message.error(imagesRes.message || '获取镜像列表失败'); message.error(imagesRes.message || '获取镜像列表失败');
setLoading(false); setLoading(false);
@ -297,9 +309,14 @@ const ImageList: React.FC = () => {
title: '确认删除', title: '确认删除',
content: `确定要删除镜像 "${record.image_name}" 吗?`, content: `确定要删除镜像 "${record.image_name}" 吗?`,
onOk: () => { onOk: () => {
// TODO: 调用删除接口 delImagesAPI(record.id).then((res) => {
setImages(images.filter((img) => img.image_id !== record.image_id)); if (res.code == CODE) {
message.success('删除成功'); message.success('删除成功');
loadImages();
} else {
message.error(res.message || '删除失败');
}
});
}, },
}); });
}; };
@ -347,52 +364,19 @@ const ImageList: React.FC = () => {
// 对于始终显示的列 // 对于始终显示的列
if (config.alwaysVisible) { if (config.alwaysVisible) {
return { return {
title: config.title, ...config,
dataIndex: config.dataIndex,
key: config.key,
width: config.width,
render: config.render,
fixed: config.fixed,
hidden: undefined, hidden: undefined,
}; };
} }
// 对于可控制显示/隐藏的列 // 对于可控制显示/隐藏的列
return { return {
title: config.title, ...config,
dataIndex: config.dataIndex,
key: config.key,
width: config.width,
render: config.render,
fixed: config.fixed,
...(visibleColumns[config.key] ? {} : { hidden: true }), ...(visibleColumns[config.key] ? {} : { hidden: true }),
}; };
}) })
.filter((column) => !column.hidden) as TableColumn[]; .filter((column) => !column.hidden) as TableColumn[];
// 处理表格分页、过滤和排序变化
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 = () => { const handleRefresh = () => {
loadImages(); loadImages();
}; };
@ -413,6 +397,33 @@ const ImageList: React.FC = () => {
pageSizeOptions: ['10', '20', '50', '100'], pageSizeOptions: ['10', '20', '50', '100'],
}; };
const handleSearch = useCallback(
(searchValue: string) => {
console.log('handleSearch', searchValue, tableParams);
// 使用 ref 中的最新值
const currentTableParams = tableParamsRef.current;
// 只有当搜索值变化时才更新参数和触发请求
if (searchInputRef.current !== searchValue) {
searchInputRef.current = searchValue;
updateParams({
pagination: {
current: 1,
pageSize: currentTableParams.pagination?.pageSize || 10,
},
filters: {
...currentTableParams.filters,
image_name: searchValue ? searchValue : undefined,
},
});
}
},
[updateParams],
);
const debouncedSearch = useRef(debounce(handleSearch, 500)).current;
return ( return (
<div className="image-list"> <div className="image-list">
<div className="search-box"> <div className="search-box">
@ -421,18 +432,18 @@ const ImageList: React.FC = () => {
<Input.Search <Input.Search
placeholder="镜像名称" placeholder="镜像名称"
value={searchText} value={searchText}
onChange={(e) => setSearchText(e.target.value)} onChange={(e) => {
style={{ width: 300 }} const value = e.target.value;
onSearch={(value) => { setSearchText(value);
setTableParams((prev) => ({ // 只有当输入为空时立即触发搜索,否则使用防抖
...prev, if (value === '') {
pagination: { handleSearch('');
...prev.pagination, } else {
current: 1, debouncedSearch(value);
}, }
image_name: value,
}));
}} }}
style={{ width: 300 }}
onSearch={handleSearch}
/> />
<Button <Button
onClick={handleRefresh} onClick={handleRefresh}
@ -456,11 +467,18 @@ const ImageList: React.FC = () => {
<Table <Table
columns={filteredColumns} columns={filteredColumns}
dataSource={images} dataSource={images}
rowKey="image_id" rowKey="id"
loading={loading} loading={loading}
pagination={paginationConfig} pagination={paginationConfig}
onChange={handleTableChange} onChange={handleTableChange}
scroll={{ y: '100%' }} scroll={{
y: 'max-content', // 关键:允许内容决定高度
}}
style={{
height: '100%',
display: 'flex',
flexDirection: 'column',
}}
/> />
</div> </div>
</div> </div>

View File

@ -43,41 +43,45 @@ export const getMD5ChunkSize = (fileSize: number): number => {
export const calculateMD5InChunks = ( export const calculateMD5InChunks = (
file: Blob, file: Blob,
fileSize: number, fileSize: number,
chunkSize: number = getMD5ChunkSize(fileSize) chunkSize: number = getMD5ChunkSize(fileSize),
): Promise<string> => { ): Promise<string> => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const spark = new SparkMD5.ArrayBuffer(); const spark = new SparkMD5.ArrayBuffer();
const fileReader = new FileReader();
let currentChunk = 0; let currentChunk = 0;
const chunks = Math.ceil(file.size / chunkSize); const chunks = Math.ceil(file.size / chunkSize);
fileReader.onload = (e) => {
try {
spark.append(e.target?.result as ArrayBuffer);
currentChunk++;
if (currentChunk < chunks) {
loadNext();
} else {
const hash = spark.end();
spark.destroy(); // 释放内存
resolve(hash);
}
} catch (error) {
spark.destroy();
reject(error);
}
};
fileReader.onerror = () => {
spark.destroy();
reject(new Error('计算MD5失败'));
};
const loadNext = () => { const loadNext = () => {
const start = currentChunk * chunkSize; const start = currentChunk * chunkSize;
const end = Math.min(start + chunkSize, file.size); const end = Math.min(start + chunkSize, file.size);
fileReader.readAsArrayBuffer(file.slice(start, end)); const chunk = file.slice(start, end);
const reader = new FileReader();
reader.onload = (e) => {
try {
spark.append(e.target?.result as ArrayBuffer);
currentChunk++;
if (currentChunk < chunks) {
// 使用 setTimeout 让出主线程,避免长时间阻塞
setTimeout(loadNext, 1);
} else {
const hash = spark.end();
spark.destroy(); // 释放内存
resolve(hash);
}
} catch (error) {
spark.destroy();
reject(error);
}
};
reader.onerror = () => {
spark.destroy();
reject(new Error('计算MD5失败'));
};
reader.readAsArrayBuffer(chunk);
}; };
loadNext(); loadNext();

View File

@ -1,26 +1,38 @@
import { request } from '@umijs/max'; import { request } from '@umijs/max';
const BASE_URL = '/api/files'; const BASE_URL_FILE = '/api/files';
const BASE_URL = '/api/nex/v1';
// 根据终端序列号查询镜像列表 // 查询镜像列表
export async function getImagesList(params:any) { export async function getImagesList(params:any) {
// console.log('镜像列表 params', params); // return request<IMAGES.Images_ListInfo>(`${BASE_URL}/queryimagesList`, {
return request<IMAGES.Images_ListInfo>(`${BASE_URL}/queryimagesList`, { return request<IMAGES.Images_ListInfo>(`${BASE_URL}/image/select/page`, {
method: 'POST', method: 'POST',
// headers: {
// 'Content-Type': 'application/json',
// },
data: params, data: params,
}); });
} }
export async function uploadChunkAPI(formData: FormData, signal?: AbortSignal) { // 删除镜像
console.log('formData', formData); export async function delImagesAPI(params:any) {
return request<any>(`${BASE_URL}/image/delete`, {
// return request<any>(`/api/v1/images/file/chunk/upload`, {
return request<any>(`${BASE_URL}/upload-chunk`, {
method: 'POST', method: 'POST',
data: formData, data: params,
signal, // 添加 signal 支持 });
}
// 上传镜像
export async function uploadChunkAPI(formData: FormData, signal?: AbortSignal) {
return request<any>(`${BASE_URL_FILE}/upload-chunk`, {
method: 'POST',
data: formData,
signal, // 添加 signal 支持
});
}
// 取消上传:调用后端,删除已上传的分片
export async function cancelUploadImagesAPI(params:any) {
return request<any>(`${BASE_URL_FILE}/cancel/upload`, {
method: 'POST',
data: params,
}); });
} }

View File

@ -1,7 +1,6 @@
declare namespace IMAGES { declare namespace IMAGES {
interface ImageItem { interface ImageItem {
image_id: number; id: number;
image_name: string; image_name: string;
image_type: number; image_type: number;
storage_path: string; storage_path: string;
@ -21,11 +20,17 @@ declare namespace IMAGES {
pagination: { pagination: {
current: number; current: number;
pageSize: number; pageSize: number;
total?: number;
}; };
image_name?: string;
image_type?: number | string; // 修改为联合类型
filters?: Record<string, any>; filters?: Record<string, any>;
sort?: {
field?: string;
order?: 'ascend' | 'descend';
};
sortOrder?: 'ascend' | 'descend' | null; sortOrder?: 'ascend' | 'descend' | null;
sortField?: string | number | null; sortField?: string | number | null;
image_name?: string; // 添加关键词字段
image_status?: string; image_status?: string;
} }
@ -33,11 +38,9 @@ declare namespace IMAGES {
code: string; code: string;
message: string; message: string;
data: { data: {
paging: { total: number;
total: number; page_num: number;
page_num: number; page_size: number;
page_size: number;
};
data: ImageItem[]; data: ImageItem[];
}; };
} }