diff --git a/web-fe/mock/images.ts b/web-fe/mock/images.ts index 727f6f5..f80a9a0 100644 --- a/web-fe/mock/images.ts +++ b/web-fe/mock/images.ts @@ -19,13 +19,15 @@ export default { image_name: `Win版 PR 2024 【支持win10、win11】.zip${ (page_num - 1) * page_size + i }`, - image_type: getRandomFormat(), + image_file_name: `Win版 PR 2024 【支持win10、win11】.zip`, + // image_type: getRandomFormat(), 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, storage_path: '/mock/images', create_time: +new Date(), + description: `这是一个测试镜像文件,ID: ${i}`, }); } const result = { diff --git a/web-fe/src/constants/images.constants.ts b/web-fe/src/constants/images.constants.ts index c5e99e6..ae38e3b 100644 --- a/web-fe/src/constants/images.constants.ts +++ b/web-fe/src/constants/images.constants.ts @@ -1,4 +1,4 @@ -export const CODE = "200"; +export const CODE = '200'; export const IMAGES_TYPE_MAP = { 1: 'VHD', @@ -38,6 +38,51 @@ export const MD5_CHUNK_SIZE_CONFIG = [ { maxSize: Infinity, chunkSize: 8 * 1024 * 1024 }, // >30GB : 8MB ]; +// 表单字段配置 +export const FORM_FIELDS_CONFIG: IMAGES.FormFieldConfig[] = [ + { + name: 'image_name', + label: '镜像名称', + type: 'input', + rules: [{ required: true, message: '请输入镜像名称' }], + props: { + placeholder: '请输入镜像名称', + maxLength: 50, + }, + }, + { + name: 'image_version', + label: '版本', + type: 'input', + rules: [{ required: true, message: '请输入版本号' }], + props: { + placeholder: '请输入版本号', + maxLength: 100, + }, + }, + { + name: 'os_version', + label: '操作系统', + type: 'select', + rules: [{ required: true, message: '请选择操作系统' }], + props: { + placeholder: '请选择操作系统', + options: [ + { value: 'windows', label: 'Windows' }, + { value: 'linux', label: 'Linux' }, + ], + }, + }, + { + name: 'description', + label: '描述', + type: 'textarea', + props: { + placeholder: '请输入描述', + }, + }, +]; + /** * 上传配置 */ @@ -57,6 +102,14 @@ export const UPLOAD_STATUS_MAP = { ERROR: 'error', } as const; +// 上传状态message信息显示样式映射 +export const MESSAGE_STYLE_MAP = { + ready: { color: '#666' }, + uploading: { color: '#1890ff' }, + success: { color: '#52c41a' }, + error: { color: '#f5222d' }, +}; + /** * 时间格式化配置 */ @@ -66,18 +119,29 @@ export const TIME_FORMAT_CONFIG = { SECOND_LABEL: '秒', }; +// 镜像状态映射 +export const STATUS_MAP = { + 1: { color: 'green', text: '成功' }, + 2: { color: 'red', text: '失败' }, +}; + /** * 镜像详情字段配置 */ export const IMAGE_DETAIL_FIELDS: IMAGES.ImageDetailField[] = [ { label: '镜像名称:', key: 'image_name' }, - { - label: '桌面类型:', - key: 'image_type', - render: (value: number) => - IMAGES_TYPE_MAP[value as keyof typeof IMAGES_TYPE_MAP] || '--', - }, + { label: '镜像文件:', key: 'image_file_name' }, + // { + // label: '桌面类型:', + // key: 'image_type', + // render: (value: number) => + // IMAGES_TYPE_MAP[value as keyof typeof IMAGES_TYPE_MAP] || '--', + // }, + { label: '镜像版本:', key: 'image_version' }, + { label: '操作系统:', key: 'os_version' }, + { label: '镜像状态:', key: 'image_status' }, + { label: '上传时间:', key: 'create_time' }, { label: '模板存放路径:', key: 'storage_path' }, { label: 'BT路径:', key: 'bt_path' }, - { label: '创建时间:', key: 'create_time' }, -] as const; \ No newline at end of file + { label: '描述:', key: 'description' }, +] as const; diff --git a/web-fe/src/pages/images/components/modalShow/modalShow.tsx b/web-fe/src/pages/images/components/modalShow/modalShow.tsx index 0372e91..bb677b6 100644 --- a/web-fe/src/pages/images/components/modalShow/modalShow.tsx +++ b/web-fe/src/pages/images/components/modalShow/modalShow.tsx @@ -1,19 +1,20 @@ -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 { v4 as uuidv4 } from 'uuid'; import { CODE, IMAGE_DETAIL_FIELDS, + STATUS_MAP, UPLOAD_CONFIG, UPLOAD_STATUS_MAP, } from '@/constants/images.constants'; import { - getChunkSize, calculateMD5InChunks, formatTime, + getChunkSize, } from '@/pages/images/utils/images'; +import { cancelUploadImagesAPI, uploadChunkAPI } from '@/services/images'; +import { Alert, Button, message, Modal, Progress, Tag, Upload } from 'antd'; +import { UploadProps } from 'antd/lib/upload'; +import React, { useEffect, useRef, useState } from 'react'; +import { v4 as uuidv4 } from 'uuid'; const { READY, UPLOADING, SUCCESS, ERROR } = UPLOAD_STATUS_MAP; @@ -21,37 +22,78 @@ const { Dragger } = Upload; /**详情弹窗 */ const ModalDetailShow = (props: any) => { const { detailVisible, setDetailVisible, selectedImage, title } = props; - return ( - setDetailVisible(false)} - footer={[ - , - ]} - width={600} - > - {selectedImage && ( -
- {IMAGE_DETAIL_FIELDS.map((field) => ( -
- - - {'render' in field && field.render - ? field.render(selectedImage[field.key]) - : selectedImage[field.key] || '--'} - -
- ))} -
- )} -
- ); + const longTextFields = ['bt_path', 'description', 'storage_path']; + const getStatusTag = (status: number) => { + const config = STATUS_MAP[status as keyof typeof STATUS_MAP]; + return {config.text}; + }; + return ( + setDetailVisible(false)} + footer={[ + , + ]} + width={800} // 增加宽度以容纳两列布局 + > + {selectedImage && ( +
+ {IMAGE_DETAIL_FIELDS.map((field) => ( +
+ +
+ {'render' in field && field.render + ? field.render(selectedImage[field.key]) + : field.key === 'image_status' + ? getStatusTag(selectedImage[field.key]) + : selectedImage[field.key] || '--'} +
+
+ ))} +
+ )} +
+ ); }; - const ImportModal: React.FC = ({ visible, onCancel, @@ -86,9 +128,8 @@ const ImportModal: React.FC = ({ const handleBeforeUnload = (e: BeforeUnloadEvent) => { if (isUploading && !uploadCompletedRef.current) { e.preventDefault(); - e.returnValue = '镜像正在上传中,确定要离开吗?'; + e.returnValue = '镜像正在上传中,确定要离开吗?'; return e.returnValue; - } }; @@ -142,7 +183,7 @@ const ImportModal: React.FC = ({ try { // 检查是否已中止 if (abortController.current?.signal.aborted) { - return {success:false}; + return { success: false }; } // 更新状态,提示正在计算MD5 @@ -210,7 +251,7 @@ const ImportModal: React.FC = ({ // 检查是否因为中止导致的错误 if (error instanceof Error && error.name === 'AbortError') { // console.log(`上传分片 ${index} 被用户取消`); - return {success:false}; + return { success: false }; } // console.error(`上传分片 ${index} 失败:`, error); return { success: false }; @@ -295,7 +336,7 @@ const ImportModal: React.FC = ({ // 2. 开始上传 const startUpload = async (file: File) => { try { - isManualCancel.current=false; + isManualCancel.current = false; uploadCompletedRef.current = false; setIsUploading(true); setUploadStatus(UPLOADING); @@ -313,7 +354,7 @@ const ImportModal: React.FC = ({ } timerRef.current = setInterval(() => { // 重新计算开始时间,基于当前时间和已用时间 - setElapsedTime((prev) => prev + 1); + setElapsedTime((prev) => prev + 1); }, 1000); // 初始化状态 diff --git a/web-fe/src/pages/images/components/uploadFileModal/uploadFileModal.less b/web-fe/src/pages/images/components/uploadFileModal/uploadFileModal.less new file mode 100644 index 0000000..40aebcb --- /dev/null +++ b/web-fe/src/pages/images/components/uploadFileModal/uploadFileModal.less @@ -0,0 +1,9 @@ +.ant-modal { + .ant-modal-header { + margin-bottom: 20px !important; + .ant-modal-title { + font-weight: 500 !important; + color: #17233d !important; + } + } +} diff --git a/web-fe/src/pages/images/components/uploadFileModal/uploadFileModal.tsx b/web-fe/src/pages/images/components/uploadFileModal/uploadFileModal.tsx new file mode 100644 index 0000000..af6ff9f --- /dev/null +++ b/web-fe/src/pages/images/components/uploadFileModal/uploadFileModal.tsx @@ -0,0 +1,748 @@ +import { + FORM_FIELDS_CONFIG, + MESSAGE_STYLE_MAP, + UPLOAD_CONFIG, + UPLOAD_STATUS_MAP, +} from '@/constants/images.constants'; +import { + calculateMD5InChunks, + formatTime, + getChunkSize, +} from '@/pages/images/utils/images'; +import { cancelUploadImagesAPI, uploadChunkAPI } from '@/services/images'; +import { + ArrowLeftOutlined, + CloseOutlined, + FileOutlined, + UploadOutlined, +} from '@ant-design/icons'; +import { + Button, + Form, + Input, + message, + Modal, + Progress, + Select, + Space, + Steps, + Upload, +} from 'antd'; +import { UploadProps } from 'antd/lib/upload'; +import React, { useEffect, useRef, useState } from 'react'; +import { v4 as uuidv4 } from 'uuid'; +import './uploadFileModal.less'; + +const { Step } = Steps; +const { READY, UPLOADING, SUCCESS, ERROR } = UPLOAD_STATUS_MAP; + +const { Dragger } = Upload; + +const ImportModal: React.FC = ({ + visible, + onCancel, + onImportSuccess, +}) => { + const [form] = Form.useForm(); + const [currentStep, setCurrentStep] = useState(0); + // 上传相关状态 + const [uploadProgress, setUploadProgress] = useState(0); + const [isUploading, setIsUploading] = useState(false); + const [uploadStatus, setUploadStatus] = useState< + 'ready' | 'uploading' | 'success' | 'error' + >('ready'); + const [uploadMessage, setUploadMessage] = useState(''); + + // 分片上传相关 + const MAX_CONCURRENT = UPLOAD_CONFIG.MAX_CONCURRENT; // 同时上传的分片数量 + const uploadQueue = useRef>([]); // 上传队列 + const completedChunks = useRef(0); // 已上传的分片数量 + const totalChunks = useRef(0); // 总分片数量 + const fileId = useRef(''); // 文件id + const fileName = useRef(''); // 文件名 + const fileSize = useRef(0); // 文件大小 + const abortController = useRef(null); // 用于取消上传 + // 上传镜像时间相关 + const [currentFile, setCurrentFile] = useState(null); + const [elapsedTime, setElapsedTime] = useState(0); // 上传时间 + const timerRef = useRef(null); + // 上传完成状态 + const uploadCompletedRef = useRef(false); + // 手动取消状态 + const isManualCancel = useRef(false); // 是否手动取消上传 + // 是否已开始上传 + const [hasStartedUpload, setHasStartedUpload] = useState(false); + + // 处理页面刷新/关闭 + useEffect(() => { + const handleBeforeUnload = (e: BeforeUnloadEvent) => { + if (isUploading && !uploadCompletedRef.current) { + e.preventDefault(); + e.returnValue = '镜像正在上传中,确定要离开吗?'; + return e.returnValue; + } + }; + + window.addEventListener('beforeunload', handleBeforeUnload); + return () => window.removeEventListener('beforeunload', handleBeforeUnload); + }, [isUploading]); + + // 计时器清理(仅组件卸载时执行) + useEffect(() => { + return () => { + if (timerRef.current) { + clearInterval(timerRef.current); + } + }; + }, []); + + useEffect(() => { + if (visible) { + resetOtherStates(); // 每次打开弹窗时重置状态 + } + }, [visible]); + + const resetOtherStates = () => { + setCurrentFile(null); + setCurrentStep(0); + form.resetFields(); + } + + // 添加重置状态函数 + const resetState = () => { + setUploadProgress(0); + setIsUploading(false); + setUploadStatus(READY); + setUploadMessage(''); + setElapsedTime(0); + setHasStartedUpload(false); + + uploadCompletedRef.current = false; + isManualCancel.current = false; + completedChunks.current = 0; + totalChunks.current = 0; + fileId.current = ''; + fileName.current = ''; + fileSize.current = 0; + uploadQueue.current = []; + + if (timerRef.current) { + clearInterval(timerRef.current); + timerRef.current = null; + } + + if (abortController.current) { + abortController.current.abort(); + abortController.current = null; + } + }; + + const validateStepOne = async () => { + try { + const values = await form.validateFields(); + return FORM_FIELDS_CONFIG.every((field) => { + if (field.rules?.some((rule) => rule.required)) { + return values[field.name]; + } + return true; + }); + } catch { + return false; + } + }; + + // 修改下一步按钮处理 + const handleNext = async () => { + const isValid = await validateStepOne(); + if (isValid) { + setCurrentStep(1); + } else { + message.error('请填写完整信息'); + } + }; + + // 修改上一步按钮处理 + const handlePrev = () => { + if (!isUploading) { + setCurrentStep(0); + } + }; + // 根据配置渲染表单字段 + const renderFormFields = () => { + return FORM_FIELDS_CONFIG.map((field) => { + switch (field.type) { + case 'input': + return ( + + + + ); + case 'select': + return ( + +