feat(前端): 联调

master
chenyt 2025-08-13 09:22:10 +08:00
parent 3e61c26a6b
commit 8dc26428d7
6 changed files with 135 additions and 98 deletions

View File

@ -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,

View File

@ -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>

View File

@ -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);
},

View File

@ -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();

View File

@ -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);

View File

@ -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;