feat(前端): 联调
parent
3e61c26a6b
commit
8dc26428d7
|
@ -20,7 +20,7 @@ export default {
|
|||
(page_num - 1) * page_size + i
|
||||
}`,
|
||||
image_type: getRandomFormat(),
|
||||
bt_path: `/serve/logs`,
|
||||
bt_path: `https://releases.ubuntu.com/20.04.6/ubuntu-20.04.6-desktop-amd64.iso.torrent`,
|
||||
image_version: '1.0.0',
|
||||
os_version: 'Ubuntu 20.04',
|
||||
image_status: Math.random() > 0.5 ? 1 : 2,
|
||||
|
|
|
@ -4,6 +4,7 @@ import { UploadProps } from 'antd/lib/upload';
|
|||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import {
|
||||
CODE,
|
||||
IMAGE_DETAIL_FIELDS,
|
||||
UPLOAD_CONFIG,
|
||||
UPLOAD_STATUS_MAP,
|
||||
|
@ -64,8 +65,7 @@ const ImportModal: React.FC<IMAGES.ImportModalProps> = ({
|
|||
const [uploadMessage, setUploadMessage] = useState('');
|
||||
|
||||
// 分片上传相关
|
||||
// const CHUNK_SIZE = 10 * 1024 * 1024; // 50MB/每片 网络传输层面的分片
|
||||
const MAX_CONCURRENT = UPLOAD_CONFIG.MAX_CONCURRENT;; // 同时上传的分片数量
|
||||
const MAX_CONCURRENT = UPLOAD_CONFIG.MAX_CONCURRENT; // 同时上传的分片数量
|
||||
const uploadQueue = useRef<Array<{ chunk: Blob; index: number }>>([]); // 上传队列
|
||||
const completedChunks = useRef<number>(0); // 已上传的分片数量
|
||||
const totalChunks = useRef<number>(0); // 总分片数量
|
||||
|
@ -77,8 +77,66 @@ const ImportModal: React.FC<IMAGES.ImportModalProps> = ({
|
|||
const [elapsedTime, setElapsedTime] = useState<number>(0); // 上传时间
|
||||
const timerRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
// const [uploadCompleted, _setUploadCompleted] = useState(false);
|
||||
// const uploadCompletedRef = useRef(false);
|
||||
|
||||
// const setUploadCompleted = (value: boolean) => {
|
||||
// uploadCompletedRef.current = value;
|
||||
// _setUploadCompleted(value);
|
||||
// };
|
||||
|
||||
// 处理页面刷新/关闭
|
||||
// useEffect(() => {
|
||||
// const handleBeforeUnload = (e: BeforeUnloadEvent) => {
|
||||
// if (isUploading && !uploadCompletedRef.current) {
|
||||
// e.preventDefault();
|
||||
// e.returnValue = '镜像正在上传中,确定要离开吗?';
|
||||
|
||||
// // 使用 sendBeacon 发送取消请求
|
||||
// const params = new URLSearchParams();
|
||||
// params.append('file_id', fileId.current);
|
||||
// const blob = new Blob([params.toString()], {
|
||||
// type: 'application/x-www-form-urlencoded',
|
||||
// });
|
||||
// navigator.sendBeacon('/api/cancel-upload', blob);
|
||||
|
||||
// return e.returnValue;
|
||||
// }
|
||||
// };
|
||||
|
||||
// window.addEventListener('beforeunload', handleBeforeUnload);
|
||||
// return () => window.removeEventListener('beforeunload', handleBeforeUnload);
|
||||
// }, [isUploading]);
|
||||
|
||||
// 计时器清理(仅组件卸载时执行)
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (timerRef.current) {
|
||||
clearInterval(timerRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
// // 上传取消逻辑(依赖 isUploading 和 uploadCompletedRef)
|
||||
// useEffect(() => {
|
||||
// return () => {
|
||||
// if (isUploading && !uploadCompletedRef.current && fileId.current) {
|
||||
// const params = new URLSearchParams();
|
||||
// params.append('file_id', fileId.current);
|
||||
// cancelUploadImagesAPI(params).then((res) => {
|
||||
// if (res.code === CODE) {
|
||||
// message.success('上传已取消');
|
||||
// } else {
|
||||
// message.error('取消上传失败');
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
// };
|
||||
// }, [isUploading]); // 保留原有依赖
|
||||
|
||||
// 添加重置状态函数
|
||||
const resetState = () => {
|
||||
// setUploadCompleted(false); // 重置上传完成状态
|
||||
setUploadProgress(0);
|
||||
setIsUploading(false);
|
||||
setUploadStatus(READY);
|
||||
|
@ -112,21 +170,21 @@ const ImportModal: React.FC<IMAGES.ImportModalProps> = ({
|
|||
}, [visible]);
|
||||
|
||||
// 4. 上传单个分片
|
||||
const uploadChunk = async (chunk: Blob, index: number): Promise<boolean> => {
|
||||
const uploadChunk = async (
|
||||
chunk: Blob,
|
||||
index: number,
|
||||
): Promise<IMAGES.UploadChunkResult> => {
|
||||
try {
|
||||
// 检查是否已中止
|
||||
if (abortController.current?.signal.aborted) {
|
||||
return false;
|
||||
return {success:false};
|
||||
}
|
||||
|
||||
// 更新状态,提示正在计算MD5
|
||||
// setUploadMessage(`正在计算第${index}个分片的MD5...`);
|
||||
|
||||
// 使用 spark-md5 计算当前分片的MD5
|
||||
const chunkMD5 = await calculateMD5InChunks(
|
||||
chunk,
|
||||
fileSize.current,
|
||||
);
|
||||
const chunkMD5 = await calculateMD5InChunks(chunk, fileSize.current);
|
||||
// 更新状态,提示正在上传
|
||||
// setUploadMessage(`正在上传第${index}个分片...`);
|
||||
|
||||
|
@ -149,6 +207,7 @@ const ImportModal: React.FC<IMAGES.ImportModalProps> = ({
|
|||
if (response.success) {
|
||||
if (response.status === 'completed') {
|
||||
// 文件上传完成,设置进度为100%
|
||||
// setUploadCompleted(true); // 标记上传完成
|
||||
setUploadProgress(100); // 这里已经正确设置了100%
|
||||
setIsUploading(false);
|
||||
setUploadStatus(SUCCESS);
|
||||
|
@ -163,44 +222,32 @@ const ImportModal: React.FC<IMAGES.ImportModalProps> = ({
|
|||
}
|
||||
|
||||
// 文件上传完成
|
||||
return true;
|
||||
return response;
|
||||
} else if (response.status === 'uploading') {
|
||||
// 分片上传成功,继续上传
|
||||
return true;
|
||||
return response;
|
||||
} else if (response.status === 'error') {
|
||||
// 后端返回错误状态
|
||||
console.error(`上传分片 ${index} 失败:`, response.message);
|
||||
return false;
|
||||
return response;
|
||||
} else {
|
||||
// 未知状态
|
||||
console.error(`上传分片 ${index} 返回未知状态:`, response.status);
|
||||
return false;
|
||||
return response;
|
||||
}
|
||||
} else {
|
||||
// success 为 false
|
||||
console.error(`上传分片 ${index} 失败:`, response.message);
|
||||
return false;
|
||||
return response;
|
||||
}
|
||||
|
||||
// await uploadChunkAPI(formData, abortController.current?.signal);
|
||||
// return true;
|
||||
// 模拟上传过程
|
||||
// await new Promise((resolve) =>
|
||||
// setTimeout(resolve, 300 + Math.random() * 700),
|
||||
// );
|
||||
|
||||
// 模拟8%的失败率用于测试
|
||||
// if (Math.random() < 0.08) {
|
||||
// throw new Error('网络错误');
|
||||
// }
|
||||
} catch (error) {
|
||||
// 检查是否因为中止导致的错误
|
||||
if (error instanceof Error && error.name === 'AbortError') {
|
||||
// console.log(`上传分片 ${index} 被用户取消`);
|
||||
return false;
|
||||
return {success:false};
|
||||
}
|
||||
// console.error(`上传分片 ${index} 失败:`, error);
|
||||
return false;
|
||||
return { success: false };
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -224,9 +271,9 @@ const ImportModal: React.FC<IMAGES.ImportModalProps> = ({
|
|||
const chunkItem = uploadQueue.current.shift();
|
||||
if (chunkItem) {
|
||||
const { chunk, index } = chunkItem;
|
||||
const success = await uploadChunk(chunk, index);
|
||||
const result = await uploadChunk(chunk, index);
|
||||
|
||||
if (success) {
|
||||
if (result.success) {
|
||||
// 只有在没有错误的情况下才更新进度
|
||||
if (!hasError) {
|
||||
completedChunks.current += 1;
|
||||
|
@ -243,25 +290,6 @@ const ImportModal: React.FC<IMAGES.ImportModalProps> = ({
|
|||
);
|
||||
}
|
||||
setUploadProgress(progress);
|
||||
|
||||
// 所有分片上传完成
|
||||
// if (completedChunks.current === totalChunks.current) {
|
||||
// setIsUploading(false);
|
||||
// setUploadStatus(SUCCESS);
|
||||
// setUploadMessage(
|
||||
// '文件上传成功!正在处理中,请稍后查看列表。',
|
||||
// );
|
||||
// message.success(
|
||||
// '文件上传成功!系统正在处理,请稍后查看列表。',
|
||||
// );
|
||||
// onImportSuccess?.();
|
||||
|
||||
// // 停止计时器
|
||||
// if (timerRef.current) {
|
||||
// clearInterval(timerRef.current);
|
||||
// timerRef.current = null;
|
||||
// }
|
||||
// }
|
||||
}
|
||||
} else {
|
||||
// 上传失败处理
|
||||
|
@ -269,8 +297,8 @@ const ImportModal: React.FC<IMAGES.ImportModalProps> = ({
|
|||
hasError = true; // 设置错误标记
|
||||
setIsUploading(false);
|
||||
setUploadStatus(ERROR);
|
||||
setUploadMessage('文件上传失败,请重试');
|
||||
message.error('文件上传失败');
|
||||
setUploadMessage(result.message || '文件上传失败,请重试');
|
||||
message.error(result.message ||'文件上传失败');
|
||||
|
||||
// 中止其他正在进行的上传
|
||||
if (abortController.current) {
|
||||
|
@ -392,16 +420,23 @@ const ImportModal: React.FC<IMAGES.ImportModalProps> = ({
|
|||
};
|
||||
|
||||
// 取消上传
|
||||
const cancelUpload = async() => {
|
||||
const cancelUpload = async () => {
|
||||
// 先更新 ref 再更新 state
|
||||
// uploadCompletedRef.current = true;
|
||||
// _setUploadCompleted(true);
|
||||
if (abortController.current) {
|
||||
abortController.current.abort();
|
||||
abortController.current = null;
|
||||
}
|
||||
// 如果有正在上传的文件,调用后端取消上传API
|
||||
if (fileId.current) {
|
||||
try {
|
||||
const params = new URLSearchParams();
|
||||
params.append('file_id', fileId.current);
|
||||
await cancelUploadImagesAPI(params);
|
||||
const res = await cancelUploadImagesAPI(params);
|
||||
if (res.code === CODE) {
|
||||
message.success('上传已取消');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('取消上传API调用失败:', error);
|
||||
}
|
||||
|
@ -410,8 +445,6 @@ const ImportModal: React.FC<IMAGES.ImportModalProps> = ({
|
|||
setUploadStatus(READY);
|
||||
setUploadMessage('上传已取消');
|
||||
setUploadProgress(0);
|
||||
message.info('上传已取消');
|
||||
|
||||
// 停止计时器
|
||||
if (timerRef.current) {
|
||||
clearInterval(timerRef.current);
|
||||
|
@ -425,8 +458,8 @@ const ImportModal: React.FC<IMAGES.ImportModalProps> = ({
|
|||
<Alert
|
||||
message="重要提示"
|
||||
description={
|
||||
<div>
|
||||
<div>1. 文件上传需要时间,请耐心等待。</div>
|
||||
<div style={{ color: "rgb(237, 41, 31)" }}>
|
||||
<div>1. 文件上传后需要组装,需要时间,请耐心等待。</div>
|
||||
<div>
|
||||
2. 文件上传中请勿刷新或者离开页面,否则会导致文件上传失败。
|
||||
</div>
|
||||
|
|
|
@ -13,8 +13,6 @@ const useTableParams = (
|
|||
useState<IMAGES.TableParams>(initialParams);
|
||||
|
||||
const getApiParams = useCallback(() => {
|
||||
console.log('getApiParams', tableParams);
|
||||
|
||||
const { pagination, filters, sort, search, ...rest } = tableParams;
|
||||
const apiParams: Record<string, any> = {
|
||||
page_size: pagination?.pageSize,
|
||||
|
@ -51,7 +49,7 @@ const useTableParams = (
|
|||
newParams: Partial<IMAGES.TableParams>,
|
||||
options?: { resetPage?: boolean },
|
||||
) => {
|
||||
console.log('updateParams', newParams);
|
||||
// console.log('updateParams', newParams);
|
||||
|
||||
setTableParams((prev) => {
|
||||
// 如果是搜索或过滤相关的更新,重置到第一页
|
||||
|
@ -86,7 +84,7 @@ const useTableParams = (
|
|||
NonNullable<TableProps<IMAGES.ImageItem>['onChange']>
|
||||
>(
|
||||
(pagination, filters, sorter, extra) => {
|
||||
console.log('handleTableChange',pagination,filters,sorter,extra);
|
||||
// console.log('handleTableChange',pagination,filters,sorter,extra);
|
||||
|
||||
// 过滤掉空值的filters
|
||||
const filteredFilters: Record<string, any> = {};
|
||||
|
@ -122,7 +120,7 @@ const useTableParams = (
|
|||
: undefined,
|
||||
};
|
||||
}
|
||||
console.log('handleTableChange', newParams);
|
||||
// console.log('handleTableChange', newParams);
|
||||
|
||||
updateParams(newParams);
|
||||
},
|
||||
|
|
|
@ -37,6 +37,7 @@ type ColumnConfig = {
|
|||
fixed?: 'left' | 'right';
|
||||
defaultVisible: boolean; // 默认是否显示
|
||||
alwaysVisible?: boolean; // 始终显示的列
|
||||
ellipsis?: boolean; // 是否启用省略号
|
||||
filters?: { text: string; value: string }[];
|
||||
filterMultiple?: boolean; // 是否多选过滤
|
||||
filterDropdown?: (props: any) => React.ReactNode;
|
||||
|
@ -142,30 +143,24 @@ const ImageList: React.FC = () => {
|
|||
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>
|
||||
),
|
||||
ellipsis: true,
|
||||
render: (text: string) =>
|
||||
text ? (
|
||||
<Tooltip title={text} placement="topLeft">
|
||||
{text}
|
||||
</Tooltip>
|
||||
) : (
|
||||
'--'
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'image_type',
|
||||
title: '桌面类型',
|
||||
dataIndex: 'image_type',
|
||||
width: 100,
|
||||
width: 120,
|
||||
render: (text: number) => {
|
||||
const key = text as keyof typeof IMAGES_TYPE_MAP;
|
||||
return <Tooltip>{IMAGES_TYPE_MAP[key] || '--'}</Tooltip>;
|
||||
return text ? IMAGES_TYPE_MAP[key] : '--';
|
||||
},
|
||||
defaultVisible: true,
|
||||
filterDropdown: ({ setSelectedKeys, selectedKeys, confirm }) => (
|
||||
|
@ -193,13 +188,17 @@ const ImageList: React.FC = () => {
|
|||
dataIndex: 'storage_path',
|
||||
width: 140,
|
||||
defaultVisible: true,
|
||||
ellipsis: true,
|
||||
render: (text: string) => text ? <Tooltip title={text} placement="topLeft">{text}</Tooltip>: '--'
|
||||
},
|
||||
{
|
||||
key: 'bt_path',
|
||||
title: 'BT路径',
|
||||
dataIndex: 'bt_path',
|
||||
width: 140,
|
||||
width: 250,
|
||||
defaultVisible: true,
|
||||
ellipsis: true,
|
||||
render: (text: string) => text ? <Tooltip title={text} placement="topLeft">{text}</Tooltip>:'--'
|
||||
},
|
||||
{
|
||||
key: 'image_version',
|
||||
|
@ -207,6 +206,9 @@ const ImageList: React.FC = () => {
|
|||
dataIndex: 'image_version',
|
||||
width: 100,
|
||||
defaultVisible: true,
|
||||
ellipsis: true,
|
||||
render: (text: string) =>
|
||||
text ? <Tooltip title={text}>{text}</Tooltip> : '--',
|
||||
},
|
||||
{
|
||||
key: 'os_version',
|
||||
|
@ -214,13 +216,16 @@ const ImageList: React.FC = () => {
|
|||
dataIndex: 'os_version',
|
||||
width: 100,
|
||||
defaultVisible: true,
|
||||
ellipsis: true,
|
||||
render: (text: string) =>
|
||||
text ? <Tooltip title={text}>{text}</Tooltip> : '--',
|
||||
},
|
||||
{
|
||||
key: 'image_status',
|
||||
title: '镜像状态',
|
||||
dataIndex: 'image_status',
|
||||
width: 80,
|
||||
render: (text: number) => <Tooltip>{getStatusTag(text)}</Tooltip>,
|
||||
render: (text: number) => (text ? getStatusTag(text) : '--'),
|
||||
defaultVisible: true,
|
||||
},
|
||||
{
|
||||
|
@ -228,10 +233,16 @@ const ImageList: React.FC = () => {
|
|||
title: '创建时间',
|
||||
dataIndex: 'create_time',
|
||||
width: 180,
|
||||
render: (text: string) => (
|
||||
<Tooltip>{dayjs(text).format('YYYY-MM-DD HH:mm:ss')}</Tooltip>
|
||||
),
|
||||
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',
|
||||
|
@ -288,7 +299,6 @@ const ImageList: React.FC = () => {
|
|||
const apiParams = {
|
||||
...getApiParams(),
|
||||
};
|
||||
console.log('apiParams', apiParams);
|
||||
|
||||
const imagesRes = await getImagesList(apiParams);
|
||||
if (imagesRes.code == CODE) {
|
||||
|
@ -301,7 +311,7 @@ const ImageList: React.FC = () => {
|
|||
...tableParams.pagination,
|
||||
current: imagesRes.data?.page_num || 1,
|
||||
total: imagesRes.data?.total || 0,
|
||||
pageSize: tableParams.pagination.pageSize || 10,
|
||||
pageSize: tableParams.pagination?.pageSize || 10,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
|
@ -319,7 +329,7 @@ const ImageList: React.FC = () => {
|
|||
2: { color: 'red', text: '失败' },
|
||||
};
|
||||
const config = statusMap[status as keyof typeof statusMap];
|
||||
return <Tag color={config.color}>{config.text}</Tag>;
|
||||
return <Tag color={config?.color}>{config.text}</Tag>;
|
||||
};
|
||||
|
||||
const handleViewDetail = (record: IMAGES.ImageItem) => {
|
||||
|
@ -328,8 +338,6 @@ const ImageList: React.FC = () => {
|
|||
};
|
||||
|
||||
const handleDelete = (record: IMAGES.ImageItem) => {
|
||||
console.log(record);
|
||||
|
||||
Modal.confirm({
|
||||
title: '确认删除',
|
||||
content: `确定要删除镜像 "${record.image_name}" 吗?`,
|
||||
|
@ -424,12 +432,6 @@ const ImageList: React.FC = () => {
|
|||
|
||||
const handleSearch = useCallback(
|
||||
(searchValue: string) => {
|
||||
// 只有当搜索值变化时才发送请求
|
||||
// if (searchInputRef.current === searchValue) return;
|
||||
|
||||
console.log('实际触发搜索:', searchValue);
|
||||
// searchInputRef.current = searchValue;
|
||||
|
||||
const currentTableParams = tableParamsRef.current;
|
||||
updateParams({
|
||||
search: {
|
||||
|
@ -455,7 +457,6 @@ const ImageList: React.FC = () => {
|
|||
// 取消所有未执行的防抖请求
|
||||
debouncedSearch.cancel();
|
||||
immediateSearch.cancel();
|
||||
console.log('输入变化:', value);
|
||||
|
||||
// 清空时立即触发搜索
|
||||
if (value === '') {
|
||||
|
@ -469,8 +470,6 @@ const ImageList: React.FC = () => {
|
|||
|
||||
// 修改回车搜索处理
|
||||
const handleEnterSearch = (value: string) => {
|
||||
console.log('回车搜索:', value);
|
||||
|
||||
// 回车搜索时取消未执行的防抖
|
||||
debouncedSearch.cancel();
|
||||
immediateSearch.cancel();
|
||||
|
|
|
@ -94,6 +94,7 @@ export const calculateMD5InChunks = (
|
|||
* @returns 格式化后的时间字符串
|
||||
*/
|
||||
export const formatTime = (seconds: number): string => {
|
||||
|
||||
const { HOUR_LABEL, MINUTE_LABEL, SECOND_LABEL } = TIME_FORMAT_CONFIG;
|
||||
|
||||
const h = Math.floor(seconds / 3600);
|
||||
|
|
|
@ -59,6 +59,12 @@ declare namespace IMAGES {
|
|||
render: (value: any) => React.ReactNode;
|
||||
};
|
||||
|
||||
type UploadChunkResult = {
|
||||
success: boolean;
|
||||
message?: string;
|
||||
status?: boolean; // 是否应该停止上传
|
||||
};
|
||||
|
||||
export type ImageDetailField =
|
||||
| ImageDetailFieldWithoutRender
|
||||
| ImageDetailFieldWithRender;
|
||||
|
|
Loading…
Reference in New Issue