fix(镜像列表): 联调+参数管理
parent
725932dfc2
commit
60e6deaa17
|
@ -1,5 +1,5 @@
|
|||
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 data = [];
|
||||
function getRandomFormat() {
|
||||
|
@ -15,13 +15,15 @@ export default {
|
|||
}
|
||||
for (let i = 1; i <= page_size; i++) {
|
||||
data.push({
|
||||
image_id: i,
|
||||
image_name: `周俊星${(page_num - 1) * page_size + i}`,
|
||||
id: i,
|
||||
image_name: `Win版 PR 2024 【支持win10、win11】.zip${
|
||||
(page_num - 1) * page_size + i
|
||||
}`,
|
||||
image_type: getRandomFormat(),
|
||||
bt_path: `/serve/logs`,
|
||||
image_version: '1.0.0',
|
||||
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',
|
||||
create_time: +new Date(),
|
||||
});
|
||||
|
@ -30,11 +32,9 @@ export default {
|
|||
code: '200',
|
||||
message: '操作成功',
|
||||
data: {
|
||||
paging: {
|
||||
total: 520,
|
||||
page_num: page_num,
|
||||
page_size: page_size,
|
||||
},
|
||||
data: data,
|
||||
},
|
||||
};
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
import { IMAGES_TYPE_MAP } from '@/constants/images.constants';
|
||||
import { uploadChunkAPI } from '@/services/images';
|
||||
import { uploadChunkAPI, cancelUploadImagesAPI } from '@/services/images';
|
||||
import { Alert, Button, message, Modal, Progress, Upload } from 'antd';
|
||||
import { UploadProps } from 'antd/lib/upload';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import SparkMD5 from 'spark-md5';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import {
|
||||
IMAGE_DETAIL_FIELDS,
|
||||
|
@ -13,10 +11,10 @@ import {
|
|||
import {
|
||||
getChunkSize,
|
||||
calculateMD5InChunks,
|
||||
calculateMD5InChunksWorkers,
|
||||
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 { Dragger } = Upload;
|
||||
|
@ -77,8 +75,7 @@ const ImportModal: React.FC<IMAGES.ImportModalProps> = ({
|
|||
const fileSize = useRef<number>(0); // 文件大小
|
||||
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);
|
||||
|
||||
// 添加重置状态函数
|
||||
|
@ -95,7 +92,6 @@ const ImportModal: React.FC<IMAGES.ImportModalProps> = ({
|
|||
uploadQueue.current = [];
|
||||
|
||||
// 重置计时器
|
||||
setUploadStartTime(null);
|
||||
setElapsedTime(0);
|
||||
if (timerRef.current) {
|
||||
clearInterval(timerRef.current);
|
||||
|
@ -123,8 +119,17 @@ const ImportModal: React.FC<IMAGES.ImportModalProps> = ({
|
|||
if (abortController.current?.signal.aborted) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 更新状态,提示正在计算MD5
|
||||
// setUploadMessage(`正在计算第${index}个分片的MD5...`);
|
||||
|
||||
// 使用 spark-md5 计算当前分片的MD5
|
||||
const chunkMD5 = await calculateMD5InChunks(chunk, fileSize.current);
|
||||
const chunkMD5 = await calculateMD5InChunks(
|
||||
chunk,
|
||||
fileSize.current,
|
||||
);
|
||||
// 更新状态,提示正在上传
|
||||
// setUploadMessage(`正在上传第${index}个分片...`);
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('file_id', fileId.current);
|
||||
|
@ -300,18 +305,17 @@ const ImportModal: React.FC<IMAGES.ImportModalProps> = ({
|
|||
setUploadProgress(0);
|
||||
|
||||
// 启动计时器
|
||||
const startTime = Date.now();
|
||||
setUploadStartTime(startTime);
|
||||
setElapsedTime(0);
|
||||
|
||||
// 立即更新一次时间
|
||||
setElapsedTime(Math.floor((Date.now() - startTime) / 1000));
|
||||
setElapsedTime(Math.floor((Date.now() - Date.now()) / 1000));
|
||||
|
||||
if (timerRef.current) {
|
||||
clearInterval(timerRef.current);
|
||||
}
|
||||
timerRef.current = setInterval(() => {
|
||||
setElapsedTime(Math.floor((Date.now() - startTime) / 1000));
|
||||
// 重新计算开始时间,基于当前时间和已用时间
|
||||
setElapsedTime((prev) => prev + 1);
|
||||
}, 1000);
|
||||
|
||||
// 初始化状态
|
||||
|
@ -389,10 +393,18 @@ const ImportModal: React.FC<IMAGES.ImportModalProps> = ({
|
|||
};
|
||||
|
||||
// 取消上传
|
||||
const cancelUpload = () => {
|
||||
const cancelUpload = async() => {
|
||||
if (abortController.current) {
|
||||
abortController.current.abort();
|
||||
}
|
||||
// 如果有正在上传的文件,调用后端取消上传API
|
||||
if (fileId.current) {
|
||||
try {
|
||||
await cancelUploadImagesAPI({ file_id: fileId.current });
|
||||
} catch (error) {
|
||||
console.error('取消上传API调用失败:', error);
|
||||
}
|
||||
}
|
||||
setIsUploading(false);
|
||||
setUploadStatus(READY);
|
||||
setUploadMessage('上传已取消');
|
||||
|
@ -432,7 +444,7 @@ const ImportModal: React.FC<IMAGES.ImportModalProps> = ({
|
|||
// accept=".tar.gz,.iso,.qcow2"
|
||||
>
|
||||
<p className="ant-upload-drag-icon">
|
||||
<div style={{ fontSize: 48, lineHeight: 1 }}>⇧</div>
|
||||
<span style={{ fontSize: 48, lineHeight: 1 }}>⇧</span>
|
||||
</p>
|
||||
<p className="ant-upload-text">点击选择文件或拖拽文件到此区域上传</p>
|
||||
<p className="ant-upload-hint">
|
||||
|
|
|
@ -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;
|
|
@ -36,17 +36,16 @@
|
|||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
.images-list-table {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
// 表格适应样式
|
||||
.ant-table-wrapper {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
// 表格适应样式
|
||||
.ant-table-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
|
||||
.ant-spin-nested-loading {
|
||||
flex: 1;
|
||||
|
@ -61,23 +60,31 @@
|
|||
overflow: hidden;
|
||||
|
||||
.ant-table {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
|
||||
.ant-table-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
|
||||
.ant-table-header {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.ant-table-body {
|
||||
flex: 1;
|
||||
overflow-y: auto !important;
|
||||
overflow: auto !important;
|
||||
|
||||
table {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
// 确保分页器在底部正确显示
|
||||
.ant-table-pagination {
|
||||
flex-shrink: 0;
|
||||
// 确保分页器始终可见
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -85,6 +92,7 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.image-detail {
|
||||
.detail-item {
|
||||
margin-bottom: 16px;
|
||||
|
|
|
@ -1,12 +1,10 @@
|
|||
import { CODE, IMAGES_TYPE_MAP } from '@/constants/images.constants';
|
||||
import { getImagesList } from '@/services/images';
|
||||
import { delImagesAPI, getImagesList } from '@/services/images';
|
||||
import {
|
||||
DeleteOutlined,
|
||||
EyeOutlined,
|
||||
SettingOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import type { TableProps } from 'antd';
|
||||
import dayjs from 'dayjs';
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
|
@ -17,11 +15,13 @@ import {
|
|||
Popover,
|
||||
Space,
|
||||
Table,
|
||||
Tooltip,
|
||||
Tag,
|
||||
Tooltip,
|
||||
} 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 useTableParams from './hook/hook';
|
||||
import './index.less';
|
||||
|
||||
import { ReactComponent as RefreshIcon } from '@/assets/icons/refresh.svg';
|
||||
|
@ -36,6 +36,10 @@ type ColumnConfig = {
|
|||
fixed?: 'left' | 'right';
|
||||
defaultVisible: boolean; // 默认是否显示
|
||||
alwaysVisible?: boolean; // 始终显示的列
|
||||
filters?: { text: string; value: string }[];
|
||||
filterMultiple?: boolean; // 是否多选过滤
|
||||
defaultFilteredValue?: string[]; // 默认过滤值
|
||||
onFilter?: (value: string, record: any) => boolean;
|
||||
};
|
||||
|
||||
type TableColumn = {
|
||||
|
@ -48,22 +52,38 @@ type TableColumn = {
|
|||
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 [images, setImages] = useState<IMAGES.ImageItem[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [selectedImage, setSelectedImage] = useState<IMAGES.ImageItem | null>(
|
||||
null,
|
||||
);
|
||||
const [searchText, setSearchText] = useState<string>('');
|
||||
const [detailVisible, setDetailVisible] = useState(false);
|
||||
const [importModalVisible, setImportModalVisible] = useState(false);
|
||||
const [tableParams, setTableParams] = useState<IMAGES.TableParams>({
|
||||
const [searchText, setSearchText] = useState<string>(''); // 添加本地搜索状态
|
||||
const searchInputRef = useRef<string>(''); // 保存已发送请求的搜索文本
|
||||
|
||||
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);
|
||||
|
||||
// 表格参数变化 获取镜像列表
|
||||
|
@ -75,7 +95,6 @@ const ImageList: React.FC = () => {
|
|||
tableParams?.sortOrder,
|
||||
tableParams?.sortField,
|
||||
JSON.stringify(tableParams.filters),
|
||||
JSON.stringify(tableParams.image_name),
|
||||
]);
|
||||
|
||||
// 定义所有列的配置
|
||||
|
@ -83,7 +102,7 @@ const ImageList: React.FC = () => {
|
|||
{
|
||||
key: 'index',
|
||||
title: '序号',
|
||||
width: 120,
|
||||
width: 60,
|
||||
render: (text: any, row: any, index: number) =>
|
||||
(tableParams.pagination?.current - 1) *
|
||||
tableParams.pagination?.pageSize +
|
||||
|
@ -96,20 +115,52 @@ const ImageList: React.FC = () => {
|
|||
key: 'image_name',
|
||||
title: '镜像名称',
|
||||
dataIndex: 'image_name',
|
||||
width: 120,
|
||||
width: 200,
|
||||
defaultVisible: 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',
|
||||
title: '桌面类型',
|
||||
dataIndex: 'image_type',
|
||||
width: 80,
|
||||
width: 100,
|
||||
render: (text: number) => {
|
||||
const key = text as keyof typeof IMAGES_TYPE_MAP;
|
||||
return <Tooltip>{IMAGES_TYPE_MAP[key] || '--'}</Tooltip>;
|
||||
},
|
||||
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',
|
||||
|
@ -205,70 +256,31 @@ const ImageList: React.FC = () => {
|
|||
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 () => {
|
||||
setLoading(true);
|
||||
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) {
|
||||
setImages(imagesRes.data.data || []);
|
||||
setImages(imagesRes.data?.data || []);
|
||||
setLoading(false);
|
||||
// 直接使用后端返回的分页信息
|
||||
setTableParams((prev) => ({
|
||||
...prev,
|
||||
console.log('imagesRes', imagesRes);
|
||||
|
||||
// 正确处理后端返回的分页信息
|
||||
updateParams({
|
||||
pagination: {
|
||||
...prev.pagination,
|
||||
current: imagesRes.data.paging.page_num || 1,
|
||||
total: imagesRes.data.paging.total || 0,
|
||||
pageSize: imagesRes.data.paging.page_size || 10,
|
||||
...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);
|
||||
|
@ -297,9 +309,14 @@ const ImageList: React.FC = () => {
|
|||
title: '确认删除',
|
||||
content: `确定要删除镜像 "${record.image_name}" 吗?`,
|
||||
onOk: () => {
|
||||
// TODO: 调用删除接口
|
||||
setImages(images.filter((img) => img.image_id !== record.image_id));
|
||||
delImagesAPI(record.id).then((res) => {
|
||||
if (res.code == CODE) {
|
||||
message.success('删除成功');
|
||||
loadImages();
|
||||
} else {
|
||||
message.error(res.message || '删除失败');
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
@ -347,52 +364,19 @@ const ImageList: React.FC = () => {
|
|||
// 对于始终显示的列
|
||||
if (config.alwaysVisible) {
|
||||
return {
|
||||
title: config.title,
|
||||
dataIndex: config.dataIndex,
|
||||
key: config.key,
|
||||
width: config.width,
|
||||
render: config.render,
|
||||
fixed: config.fixed,
|
||||
...config,
|
||||
hidden: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
// 对于可控制显示/隐藏的列
|
||||
return {
|
||||
title: config.title,
|
||||
dataIndex: config.dataIndex,
|
||||
key: config.key,
|
||||
width: config.width,
|
||||
render: config.render,
|
||||
fixed: config.fixed,
|
||||
...config,
|
||||
...(visibleColumns[config.key] ? {} : { hidden: true }),
|
||||
};
|
||||
})
|
||||
.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 = () => {
|
||||
loadImages();
|
||||
};
|
||||
|
@ -413,6 +397,33 @@ const ImageList: React.FC = () => {
|
|||
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 (
|
||||
<div className="image-list">
|
||||
<div className="search-box">
|
||||
|
@ -421,18 +432,18 @@ const ImageList: React.FC = () => {
|
|||
<Input.Search
|
||||
placeholder="镜像名称"
|
||||
value={searchText}
|
||||
onChange={(e) => setSearchText(e.target.value)}
|
||||
style={{ width: 300 }}
|
||||
onSearch={(value) => {
|
||||
setTableParams((prev) => ({
|
||||
...prev,
|
||||
pagination: {
|
||||
...prev.pagination,
|
||||
current: 1,
|
||||
},
|
||||
image_name: value,
|
||||
}));
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
setSearchText(value);
|
||||
// 只有当输入为空时立即触发搜索,否则使用防抖
|
||||
if (value === '') {
|
||||
handleSearch('');
|
||||
} else {
|
||||
debouncedSearch(value);
|
||||
}
|
||||
}}
|
||||
style={{ width: 300 }}
|
||||
onSearch={handleSearch}
|
||||
/>
|
||||
<Button
|
||||
onClick={handleRefresh}
|
||||
|
@ -456,11 +467,18 @@ const ImageList: React.FC = () => {
|
|||
<Table
|
||||
columns={filteredColumns}
|
||||
dataSource={images}
|
||||
rowKey="image_id"
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
pagination={paginationConfig}
|
||||
onChange={handleTableChange}
|
||||
scroll={{ y: '100%' }}
|
||||
scroll={{
|
||||
y: 'max-content', // 关键:允许内容决定高度
|
||||
}}
|
||||
style={{
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -43,21 +43,28 @@ export const getMD5ChunkSize = (fileSize: number): number => {
|
|||
export const calculateMD5InChunks = (
|
||||
file: Blob,
|
||||
fileSize: number,
|
||||
chunkSize: number = getMD5ChunkSize(fileSize)
|
||||
chunkSize: number = getMD5ChunkSize(fileSize),
|
||||
): Promise<string> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const spark = new SparkMD5.ArrayBuffer();
|
||||
const fileReader = new FileReader();
|
||||
let currentChunk = 0;
|
||||
const chunks = Math.ceil(file.size / chunkSize);
|
||||
|
||||
fileReader.onload = (e) => {
|
||||
const loadNext = () => {
|
||||
const start = currentChunk * chunkSize;
|
||||
const end = Math.min(start + chunkSize, file.size);
|
||||
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) {
|
||||
loadNext();
|
||||
// 使用 setTimeout 让出主线程,避免长时间阻塞
|
||||
setTimeout(loadNext, 1);
|
||||
} else {
|
||||
const hash = spark.end();
|
||||
spark.destroy(); // 释放内存
|
||||
|
@ -69,15 +76,12 @@ export const calculateMD5InChunks = (
|
|||
}
|
||||
};
|
||||
|
||||
fileReader.onerror = () => {
|
||||
reader.onerror = () => {
|
||||
spark.destroy();
|
||||
reject(new Error('计算MD5失败'));
|
||||
};
|
||||
|
||||
const loadNext = () => {
|
||||
const start = currentChunk * chunkSize;
|
||||
const end = Math.min(start + chunkSize, file.size);
|
||||
fileReader.readAsArrayBuffer(file.slice(start, end));
|
||||
reader.readAsArrayBuffer(chunk);
|
||||
};
|
||||
|
||||
loadNext();
|
|
@ -1,26 +1,38 @@
|
|||
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) {
|
||||
// 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',
|
||||
// headers: {
|
||||
// 'Content-Type': 'application/json',
|
||||
// },
|
||||
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`, {
|
||||
method: 'POST',
|
||||
data: params,
|
||||
});
|
||||
}
|
||||
|
||||
// return request<any>(`/api/v1/images/file/chunk/upload`, {
|
||||
return request<any>(`${BASE_URL}/upload-chunk`, {
|
||||
// 上传镜像
|
||||
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,
|
||||
});
|
||||
}
|
|
@ -1,7 +1,6 @@
|
|||
|
||||
declare namespace IMAGES {
|
||||
interface ImageItem {
|
||||
image_id: number;
|
||||
id: number;
|
||||
image_name: string;
|
||||
image_type: number;
|
||||
storage_path: string;
|
||||
|
@ -21,11 +20,17 @@ declare namespace IMAGES {
|
|||
pagination: {
|
||||
current: number;
|
||||
pageSize: number;
|
||||
total?: number;
|
||||
};
|
||||
image_name?: string;
|
||||
image_type?: number | string; // 修改为联合类型
|
||||
filters?: Record<string, any>;
|
||||
sort?: {
|
||||
field?: string;
|
||||
order?: 'ascend' | 'descend';
|
||||
};
|
||||
sortOrder?: 'ascend' | 'descend' | null;
|
||||
sortField?: string | number | null;
|
||||
image_name?: string; // 添加关键词字段
|
||||
image_status?: string;
|
||||
}
|
||||
|
||||
|
@ -33,11 +38,9 @@ declare namespace IMAGES {
|
|||
code: string;
|
||||
message: string;
|
||||
data: {
|
||||
paging: {
|
||||
total: number;
|
||||
page_num: number;
|
||||
page_size: number;
|
||||
};
|
||||
data: ImageItem[];
|
||||
};
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue