diff --git a/web-fe/mock/images.ts b/web-fe/mock/images.ts index 8a9b056..727f6f5 100644 --- a/web-fe/mock/images.ts +++ b/web-fe/mock/images.ts @@ -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, diff --git a/web-fe/src/pages/images/components/modalShow/modalShow.tsx b/web-fe/src/pages/images/components/modalShow/modalShow.tsx index ca2e8f4..3818805 100644 --- a/web-fe/src/pages/images/components/modalShow/modalShow.tsx +++ b/web-fe/src/pages/images/components/modalShow/modalShow.tsx @@ -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 = ({ 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>([]); // 上传队列 const completedChunks = useRef(0); // 已上传的分片数量 const totalChunks = useRef(0); // 总分片数量 @@ -77,8 +77,66 @@ const ImportModal: React.FC = ({ const [elapsedTime, setElapsedTime] = useState(0); // 上传时间 const timerRef = useRef(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 = ({ }, [visible]); // 4. 上传单个分片 - const uploadChunk = async (chunk: Blob, index: number): Promise => { + const uploadChunk = async ( + chunk: Blob, + index: number, + ): Promise => { 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 = ({ 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 = ({ } // 文件上传完成 - 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 = ({ 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 = ({ ); } 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 = ({ 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 = ({ }; // 取消上传 - 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 = ({ setUploadStatus(READY); setUploadMessage('上传已取消'); setUploadProgress(0); - message.info('上传已取消'); - // 停止计时器 if (timerRef.current) { clearInterval(timerRef.current); @@ -425,8 +458,8 @@ const ImportModal: React.FC = ({ -
1. 文件上传需要时间,请耐心等待。
+
+
1. 文件上传后需要组装,需要时间,请耐心等待。
2. 文件上传中请勿刷新或者离开页面,否则会导致文件上传失败。
diff --git a/web-fe/src/pages/images/hook/hook.ts b/web-fe/src/pages/images/hook/hook.ts index 352b84b..ff833f4 100644 --- a/web-fe/src/pages/images/hook/hook.ts +++ b/web-fe/src/pages/images/hook/hook.ts @@ -13,8 +13,6 @@ const useTableParams = ( useState(initialParams); const getApiParams = useCallback(() => { - console.log('getApiParams', tableParams); - const { pagination, filters, sort, search, ...rest } = tableParams; const apiParams: Record = { page_size: pagination?.pageSize, @@ -51,7 +49,7 @@ const useTableParams = ( newParams: Partial, options?: { resetPage?: boolean }, ) => { - console.log('updateParams', newParams); + // console.log('updateParams', newParams); setTableParams((prev) => { // 如果是搜索或过滤相关的更新,重置到第一页 @@ -86,7 +84,7 @@ const useTableParams = ( NonNullable['onChange']> >( (pagination, filters, sorter, extra) => { - console.log('handleTableChange',pagination,filters,sorter,extra); + // console.log('handleTableChange',pagination,filters,sorter,extra); // 过滤掉空值的filters const filteredFilters: Record = {}; @@ -122,7 +120,7 @@ const useTableParams = ( : undefined, }; } - console.log('handleTableChange', newParams); + // console.log('handleTableChange', newParams); updateParams(newParams); }, diff --git a/web-fe/src/pages/images/index.tsx b/web-fe/src/pages/images/index.tsx index 2adef88..e8b59dd 100644 --- a/web-fe/src/pages/images/index.tsx +++ b/web-fe/src/pages/images/index.tsx @@ -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) => ( - - - {text || '--'} - - - ), + ellipsis: true, + render: (text: string) => + text ? ( + + {text} + + ) : ( + '--' + ), }, { key: 'image_type', title: '桌面类型', dataIndex: 'image_type', - width: 100, + width: 120, render: (text: number) => { const key = text as keyof typeof IMAGES_TYPE_MAP; - return {IMAGES_TYPE_MAP[key] || '--'}; + 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 ? {text}: '--' }, { key: 'bt_path', title: 'BT路径', dataIndex: 'bt_path', - width: 140, + width: 250, defaultVisible: true, + ellipsis: true, + render: (text: string) => text ? {text}:'--' }, { key: 'image_version', @@ -207,6 +206,9 @@ const ImageList: React.FC = () => { dataIndex: 'image_version', width: 100, defaultVisible: true, + ellipsis: true, + render: (text: string) => + text ? {text} : '--', }, { key: 'os_version', @@ -214,13 +216,16 @@ const ImageList: React.FC = () => { dataIndex: 'os_version', width: 100, defaultVisible: true, + ellipsis: true, + render: (text: string) => + text ? {text} : '--', }, { key: 'image_status', title: '镜像状态', dataIndex: 'image_status', width: 80, - render: (text: number) => {getStatusTag(text)}, + 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) => ( - {dayjs(text).format('YYYY-MM-DD HH:mm:ss')} - ), + render: (text: string) => + text ? ( + + {text ? dayjs(text).format('YYYY-MM-DD HH:mm:ss') : '--'} + + ) : ( + '--' + ), 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 {config.text}; + return {config.text}; }; 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(); diff --git a/web-fe/src/pages/images/utils/images.ts b/web-fe/src/pages/images/utils/images.ts index b269dd3..81a793d 100644 --- a/web-fe/src/pages/images/utils/images.ts +++ b/web-fe/src/pages/images/utils/images.ts @@ -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); diff --git a/web-fe/src/types/images.d.ts b/web-fe/src/types/images.d.ts index 523e51e..0d0e1c1 100644 --- a/web-fe/src/types/images.d.ts +++ b/web-fe/src/types/images.d.ts @@ -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;