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 (page_num - 1) * page_size + i
}`, }`,
image_type: getRandomFormat(), 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', 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,

View File

@ -4,6 +4,7 @@ import { UploadProps } from 'antd/lib/upload';
import React, { useEffect, useRef, useState } from 'react'; import React, { useEffect, useRef, useState } from 'react';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import { import {
CODE,
IMAGE_DETAIL_FIELDS, IMAGE_DETAIL_FIELDS,
UPLOAD_CONFIG, UPLOAD_CONFIG,
UPLOAD_STATUS_MAP, UPLOAD_STATUS_MAP,
@ -64,8 +65,7 @@ const ImportModal: React.FC<IMAGES.ImportModalProps> = ({
const [uploadMessage, setUploadMessage] = useState(''); 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 uploadQueue = useRef<Array<{ chunk: Blob; index: number }>>([]); // 上传队列
const completedChunks = useRef<number>(0); // 已上传的分片数量 const completedChunks = useRef<number>(0); // 已上传的分片数量
const totalChunks = 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 [elapsedTime, setElapsedTime] = useState<number>(0); // 上传时间
const timerRef = useRef<NodeJS.Timeout | null>(null); 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 = () => { const resetState = () => {
// setUploadCompleted(false); // 重置上传完成状态
setUploadProgress(0); setUploadProgress(0);
setIsUploading(false); setIsUploading(false);
setUploadStatus(READY); setUploadStatus(READY);
@ -112,21 +170,21 @@ const ImportModal: React.FC<IMAGES.ImportModalProps> = ({
}, [visible]); }, [visible]);
// 4. 上传单个分片 // 4. 上传单个分片
const uploadChunk = async (chunk: Blob, index: number): Promise<boolean> => { const uploadChunk = async (
chunk: Blob,
index: number,
): Promise<IMAGES.UploadChunkResult> => {
try { try {
// 检查是否已中止 // 检查是否已中止
if (abortController.current?.signal.aborted) { if (abortController.current?.signal.aborted) {
return false; return {success:false};
} }
// 更新状态提示正在计算MD5 // 更新状态提示正在计算MD5
// setUploadMessage(`正在计算第${index}个分片的MD5...`); // setUploadMessage(`正在计算第${index}个分片的MD5...`);
// 使用 spark-md5 计算当前分片的MD5 // 使用 spark-md5 计算当前分片的MD5
const chunkMD5 = await calculateMD5InChunks( const chunkMD5 = await calculateMD5InChunks(chunk, fileSize.current);
chunk,
fileSize.current,
);
// 更新状态,提示正在上传 // 更新状态,提示正在上传
// setUploadMessage(`正在上传第${index}个分片...`); // setUploadMessage(`正在上传第${index}个分片...`);
@ -149,6 +207,7 @@ const ImportModal: React.FC<IMAGES.ImportModalProps> = ({
if (response.success) { if (response.success) {
if (response.status === 'completed') { if (response.status === 'completed') {
// 文件上传完成设置进度为100% // 文件上传完成设置进度为100%
// setUploadCompleted(true); // 标记上传完成
setUploadProgress(100); // 这里已经正确设置了100% setUploadProgress(100); // 这里已经正确设置了100%
setIsUploading(false); setIsUploading(false);
setUploadStatus(SUCCESS); setUploadStatus(SUCCESS);
@ -163,44 +222,32 @@ const ImportModal: React.FC<IMAGES.ImportModalProps> = ({
} }
// 文件上传完成 // 文件上传完成
return true; return response;
} else if (response.status === 'uploading') { } else if (response.status === 'uploading') {
// 分片上传成功,继续上传 // 分片上传成功,继续上传
return true; return response;
} else if (response.status === 'error') { } else if (response.status === 'error') {
// 后端返回错误状态 // 后端返回错误状态
console.error(`上传分片 ${index} 失败:`, response.message); console.error(`上传分片 ${index} 失败:`, response.message);
return false; return response;
} else { } else {
// 未知状态 // 未知状态
console.error(`上传分片 ${index} 返回未知状态:`, response.status); console.error(`上传分片 ${index} 返回未知状态:`, response.status);
return false; return response;
} }
} else { } else {
// success 为 false // success 为 false
console.error(`上传分片 ${index} 失败:`, response.message); 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) { } catch (error) {
// 检查是否因为中止导致的错误 // 检查是否因为中止导致的错误
if (error instanceof Error && error.name === 'AbortError') { if (error instanceof Error && error.name === 'AbortError') {
// console.log(`上传分片 ${index} 被用户取消`); // console.log(`上传分片 ${index} 被用户取消`);
return false; return {success:false};
} }
// console.error(`上传分片 ${index} 失败:`, error); // console.error(`上传分片 ${index} 失败:`, error);
return false; return { success: false };
} }
}; };
@ -224,9 +271,9 @@ const ImportModal: React.FC<IMAGES.ImportModalProps> = ({
const chunkItem = uploadQueue.current.shift(); const chunkItem = uploadQueue.current.shift();
if (chunkItem) { if (chunkItem) {
const { chunk, index } = chunkItem; const { chunk, index } = chunkItem;
const success = await uploadChunk(chunk, index); const result = await uploadChunk(chunk, index);
if (success) { if (result.success) {
// 只有在没有错误的情况下才更新进度 // 只有在没有错误的情况下才更新进度
if (!hasError) { if (!hasError) {
completedChunks.current += 1; completedChunks.current += 1;
@ -243,25 +290,6 @@ const ImportModal: React.FC<IMAGES.ImportModalProps> = ({
); );
} }
setUploadProgress(progress); 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 { } else {
// 上传失败处理 // 上传失败处理
@ -269,8 +297,8 @@ const ImportModal: React.FC<IMAGES.ImportModalProps> = ({
hasError = true; // 设置错误标记 hasError = true; // 设置错误标记
setIsUploading(false); setIsUploading(false);
setUploadStatus(ERROR); setUploadStatus(ERROR);
setUploadMessage('文件上传失败,请重试'); setUploadMessage(result.message || '文件上传失败,请重试');
message.error('文件上传失败'); message.error(result.message ||'文件上传失败');
// 中止其他正在进行的上传 // 中止其他正在进行的上传
if (abortController.current) { 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) { if (abortController.current) {
abortController.current.abort(); abortController.current.abort();
abortController.current = null;
} }
// 如果有正在上传的文件调用后端取消上传API // 如果有正在上传的文件调用后端取消上传API
if (fileId.current) { if (fileId.current) {
try { try {
const params = new URLSearchParams(); const params = new URLSearchParams();
params.append('file_id', fileId.current); params.append('file_id', fileId.current);
await cancelUploadImagesAPI(params); const res = await cancelUploadImagesAPI(params);
if (res.code === CODE) {
message.success('上传已取消');
}
} catch (error) { } catch (error) {
console.error('取消上传API调用失败:', error); console.error('取消上传API调用失败:', error);
} }
@ -410,8 +445,6 @@ const ImportModal: React.FC<IMAGES.ImportModalProps> = ({
setUploadStatus(READY); setUploadStatus(READY);
setUploadMessage('上传已取消'); setUploadMessage('上传已取消');
setUploadProgress(0); setUploadProgress(0);
message.info('上传已取消');
// 停止计时器 // 停止计时器
if (timerRef.current) { if (timerRef.current) {
clearInterval(timerRef.current); clearInterval(timerRef.current);
@ -425,8 +458,8 @@ const ImportModal: React.FC<IMAGES.ImportModalProps> = ({
<Alert <Alert
message="重要提示" message="重要提示"
description={ description={
<div> <div style={{ color: "rgb(237, 41, 31)" }}>
<div>1. </div> <div>1. </div>
<div> <div>
2. 2.
</div> </div>

View File

@ -13,8 +13,6 @@ const useTableParams = (
useState<IMAGES.TableParams>(initialParams); useState<IMAGES.TableParams>(initialParams);
const getApiParams = useCallback(() => { const getApiParams = useCallback(() => {
console.log('getApiParams', tableParams);
const { pagination, filters, sort, search, ...rest } = tableParams; const { pagination, filters, sort, search, ...rest } = tableParams;
const apiParams: Record<string, any> = { const apiParams: Record<string, any> = {
page_size: pagination?.pageSize, page_size: pagination?.pageSize,
@ -51,7 +49,7 @@ const useTableParams = (
newParams: Partial<IMAGES.TableParams>, newParams: Partial<IMAGES.TableParams>,
options?: { resetPage?: boolean }, options?: { resetPage?: boolean },
) => { ) => {
console.log('updateParams', newParams); // console.log('updateParams', newParams);
setTableParams((prev) => { setTableParams((prev) => {
// 如果是搜索或过滤相关的更新,重置到第一页 // 如果是搜索或过滤相关的更新,重置到第一页
@ -86,7 +84,7 @@ const useTableParams = (
NonNullable<TableProps<IMAGES.ImageItem>['onChange']> NonNullable<TableProps<IMAGES.ImageItem>['onChange']>
>( >(
(pagination, filters, sorter, extra) => { (pagination, filters, sorter, extra) => {
console.log('handleTableChange',pagination,filters,sorter,extra); // console.log('handleTableChange',pagination,filters,sorter,extra);
// 过滤掉空值的filters // 过滤掉空值的filters
const filteredFilters: Record<string, any> = {}; const filteredFilters: Record<string, any> = {};
@ -122,7 +120,7 @@ const useTableParams = (
: undefined, : undefined,
}; };
} }
console.log('handleTableChange', newParams); // console.log('handleTableChange', newParams);
updateParams(newParams); updateParams(newParams);
}, },

View File

@ -37,6 +37,7 @@ type ColumnConfig = {
fixed?: 'left' | 'right'; fixed?: 'left' | 'right';
defaultVisible: boolean; // 默认是否显示 defaultVisible: boolean; // 默认是否显示
alwaysVisible?: boolean; // 始终显示的列 alwaysVisible?: boolean; // 始终显示的列
ellipsis?: boolean; // 是否启用省略号
filters?: { text: string; value: string }[]; filters?: { text: string; value: string }[];
filterMultiple?: boolean; // 是否多选过滤 filterMultiple?: boolean; // 是否多选过滤
filterDropdown?: (props: any) => React.ReactNode; filterDropdown?: (props: any) => React.ReactNode;
@ -142,30 +143,24 @@ const ImageList: React.FC = () => {
width: 200, width: 200,
defaultVisible: true, defaultVisible: true,
alwaysVisible: true, alwaysVisible: true,
render: (text: string) => ( ellipsis: true,
<Tooltip title={text}> render: (text: string) =>
<span text ? (
style={{ <Tooltip title={text} placement="topLeft">
overflow: 'hidden', {text}
textOverflow: 'ellipsis', </Tooltip>
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: 100, width: 120,
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 text ? IMAGES_TYPE_MAP[key] : '--';
}, },
defaultVisible: true, defaultVisible: true,
filterDropdown: ({ setSelectedKeys, selectedKeys, confirm }) => ( filterDropdown: ({ setSelectedKeys, selectedKeys, confirm }) => (
@ -193,13 +188,17 @@ const ImageList: React.FC = () => {
dataIndex: 'storage_path', dataIndex: 'storage_path',
width: 140, width: 140,
defaultVisible: true, defaultVisible: true,
ellipsis: true,
render: (text: string) => text ? <Tooltip title={text} placement="topLeft">{text}</Tooltip>: '--'
}, },
{ {
key: 'bt_path', key: 'bt_path',
title: 'BT路径', title: 'BT路径',
dataIndex: 'bt_path', dataIndex: 'bt_path',
width: 140, width: 250,
defaultVisible: true, defaultVisible: true,
ellipsis: true,
render: (text: string) => text ? <Tooltip title={text} placement="topLeft">{text}</Tooltip>:'--'
}, },
{ {
key: 'image_version', key: 'image_version',
@ -207,6 +206,9 @@ const ImageList: React.FC = () => {
dataIndex: 'image_version', dataIndex: 'image_version',
width: 100, width: 100,
defaultVisible: true, defaultVisible: true,
ellipsis: true,
render: (text: string) =>
text ? <Tooltip title={text}>{text}</Tooltip> : '--',
}, },
{ {
key: 'os_version', key: 'os_version',
@ -214,13 +216,16 @@ const ImageList: React.FC = () => {
dataIndex: 'os_version', dataIndex: 'os_version',
width: 100, width: 100,
defaultVisible: true, defaultVisible: true,
ellipsis: true,
render: (text: string) =>
text ? <Tooltip title={text}>{text}</Tooltip> : '--',
}, },
{ {
key: 'image_status', key: 'image_status',
title: '镜像状态', title: '镜像状态',
dataIndex: 'image_status', dataIndex: 'image_status',
width: 80, width: 80,
render: (text: number) => <Tooltip>{getStatusTag(text)}</Tooltip>, render: (text: number) => (text ? getStatusTag(text) : '--'),
defaultVisible: true, defaultVisible: true,
}, },
{ {
@ -228,10 +233,16 @@ const ImageList: React.FC = () => {
title: '创建时间', title: '创建时间',
dataIndex: 'create_time', dataIndex: 'create_time',
width: 180, width: 180,
render: (text: string) => ( render: (text: string) =>
<Tooltip>{dayjs(text).format('YYYY-MM-DD HH:mm:ss')}</Tooltip> 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, defaultVisible: true,
ellipsis: true,
}, },
{ {
key: 'action', key: 'action',
@ -288,7 +299,6 @@ const ImageList: React.FC = () => {
const apiParams = { const apiParams = {
...getApiParams(), ...getApiParams(),
}; };
console.log('apiParams', apiParams);
const imagesRes = await getImagesList(apiParams); const imagesRes = await getImagesList(apiParams);
if (imagesRes.code == CODE) { if (imagesRes.code == CODE) {
@ -301,7 +311,7 @@ const ImageList: React.FC = () => {
...tableParams.pagination, ...tableParams.pagination,
current: imagesRes.data?.page_num || 1, current: imagesRes.data?.page_num || 1,
total: imagesRes.data?.total || 0, total: imagesRes.data?.total || 0,
pageSize: tableParams.pagination.pageSize || 10, pageSize: tableParams.pagination?.pageSize || 10,
}, },
}); });
} else { } else {
@ -319,7 +329,7 @@ const ImageList: React.FC = () => {
2: { color: 'red', text: '失败' }, 2: { color: 'red', text: '失败' },
}; };
const config = statusMap[status as keyof typeof statusMap]; 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) => { const handleViewDetail = (record: IMAGES.ImageItem) => {
@ -328,8 +338,6 @@ const ImageList: React.FC = () => {
}; };
const handleDelete = (record: IMAGES.ImageItem) => { const handleDelete = (record: IMAGES.ImageItem) => {
console.log(record);
Modal.confirm({ Modal.confirm({
title: '确认删除', title: '确认删除',
content: `确定要删除镜像 "${record.image_name}" 吗?`, content: `确定要删除镜像 "${record.image_name}" 吗?`,
@ -424,12 +432,6 @@ const ImageList: React.FC = () => {
const handleSearch = useCallback( const handleSearch = useCallback(
(searchValue: string) => { (searchValue: string) => {
// 只有当搜索值变化时才发送请求
// if (searchInputRef.current === searchValue) return;
console.log('实际触发搜索:', searchValue);
// searchInputRef.current = searchValue;
const currentTableParams = tableParamsRef.current; const currentTableParams = tableParamsRef.current;
updateParams({ updateParams({
search: { search: {
@ -455,7 +457,6 @@ const ImageList: React.FC = () => {
// 取消所有未执行的防抖请求 // 取消所有未执行的防抖请求
debouncedSearch.cancel(); debouncedSearch.cancel();
immediateSearch.cancel(); immediateSearch.cancel();
console.log('输入变化:', value);
// 清空时立即触发搜索 // 清空时立即触发搜索
if (value === '') { if (value === '') {
@ -469,8 +470,6 @@ const ImageList: React.FC = () => {
// 修改回车搜索处理 // 修改回车搜索处理
const handleEnterSearch = (value: string) => { const handleEnterSearch = (value: string) => {
console.log('回车搜索:', value);
// 回车搜索时取消未执行的防抖 // 回车搜索时取消未执行的防抖
debouncedSearch.cancel(); debouncedSearch.cancel();
immediateSearch.cancel(); immediateSearch.cancel();

View File

@ -94,6 +94,7 @@ export const calculateMD5InChunks = (
* @returns * @returns
*/ */
export const formatTime = (seconds: number): string => { export const formatTime = (seconds: number): string => {
const { HOUR_LABEL, MINUTE_LABEL, SECOND_LABEL } = TIME_FORMAT_CONFIG; const { HOUR_LABEL, MINUTE_LABEL, SECOND_LABEL } = TIME_FORMAT_CONFIG;
const h = Math.floor(seconds / 3600); const h = Math.floor(seconds / 3600);

View File

@ -59,6 +59,12 @@ declare namespace IMAGES {
render: (value: any) => React.ReactNode; render: (value: any) => React.ReactNode;
}; };
type UploadChunkResult = {
success: boolean;
message?: string;
status?: boolean; // 是否应该停止上传
};
export type ImageDetailField = export type ImageDetailField =
| ImageDetailFieldWithoutRender | ImageDetailFieldWithoutRender
| ImageDetailFieldWithRender; | ImageDetailFieldWithRender;