fix(前端): 镜像列表修改
parent
218150f8a0
commit
e656e20450
|
@ -19,13 +19,15 @@ export default {
|
||||||
image_name: `Win版 PR 2024 【支持win10、win11】.zip${
|
image_name: `Win版 PR 2024 【支持win10、win11】.zip${
|
||||||
(page_num - 1) * page_size + i
|
(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`,
|
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,
|
||||||
storage_path: '/mock/images',
|
storage_path: '/mock/images',
|
||||||
create_time: +new Date(),
|
create_time: +new Date(),
|
||||||
|
description: `这是一个测试镜像文件,ID: ${i}`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const result = {
|
const result = {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
export const CODE = "200";
|
export const CODE = '200';
|
||||||
|
|
||||||
export const IMAGES_TYPE_MAP = {
|
export const IMAGES_TYPE_MAP = {
|
||||||
1: 'VHD',
|
1: 'VHD',
|
||||||
|
@ -38,6 +38,51 @@ export const MD5_CHUNK_SIZE_CONFIG = [
|
||||||
{ maxSize: Infinity, chunkSize: 8 * 1024 * 1024 }, // >30GB : 8MB
|
{ 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',
|
ERROR: 'error',
|
||||||
} as const;
|
} 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: '秒',
|
SECOND_LABEL: '秒',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 镜像状态映射
|
||||||
|
export const STATUS_MAP = {
|
||||||
|
1: { color: 'green', text: '成功' },
|
||||||
|
2: { color: 'red', text: '失败' },
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 镜像详情字段配置
|
* 镜像详情字段配置
|
||||||
*/
|
*/
|
||||||
export const IMAGE_DETAIL_FIELDS: IMAGES.ImageDetailField[] = [
|
export const IMAGE_DETAIL_FIELDS: IMAGES.ImageDetailField[] = [
|
||||||
{ label: '镜像名称:', key: 'image_name' },
|
{ label: '镜像名称:', key: 'image_name' },
|
||||||
{
|
{ label: '镜像文件:', key: 'image_file_name' },
|
||||||
label: '桌面类型:',
|
// {
|
||||||
key: 'image_type',
|
// label: '桌面类型:',
|
||||||
render: (value: number) =>
|
// key: 'image_type',
|
||||||
IMAGES_TYPE_MAP[value as keyof typeof IMAGES_TYPE_MAP] || '--',
|
// 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: '模板存放路径:', key: 'storage_path' },
|
||||||
{ label: 'BT路径:', key: 'bt_path' },
|
{ label: 'BT路径:', key: 'bt_path' },
|
||||||
{ label: '创建时间:', key: 'create_time' },
|
{ label: '描述:', key: 'description' },
|
||||||
] as const;
|
] as const;
|
||||||
|
|
|
@ -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 {
|
import {
|
||||||
CODE,
|
CODE,
|
||||||
IMAGE_DETAIL_FIELDS,
|
IMAGE_DETAIL_FIELDS,
|
||||||
|
STATUS_MAP,
|
||||||
UPLOAD_CONFIG,
|
UPLOAD_CONFIG,
|
||||||
UPLOAD_STATUS_MAP,
|
UPLOAD_STATUS_MAP,
|
||||||
} from '@/constants/images.constants';
|
} from '@/constants/images.constants';
|
||||||
import {
|
import {
|
||||||
getChunkSize,
|
|
||||||
calculateMD5InChunks,
|
calculateMD5InChunks,
|
||||||
formatTime,
|
formatTime,
|
||||||
|
getChunkSize,
|
||||||
} from '@/pages/images/utils/images';
|
} 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;
|
const { READY, UPLOADING, SUCCESS, ERROR } = UPLOAD_STATUS_MAP;
|
||||||
|
|
||||||
|
@ -21,37 +22,78 @@ const { Dragger } = Upload;
|
||||||
/**详情弹窗 */
|
/**详情弹窗 */
|
||||||
const ModalDetailShow = (props: any) => {
|
const ModalDetailShow = (props: any) => {
|
||||||
const { detailVisible, setDetailVisible, selectedImage, title } = props;
|
const { detailVisible, setDetailVisible, selectedImage, title } = props;
|
||||||
return (
|
const longTextFields = ['bt_path', 'description', 'storage_path'];
|
||||||
<Modal
|
const getStatusTag = (status: number) => {
|
||||||
title={title}
|
const config = STATUS_MAP[status as keyof typeof STATUS_MAP];
|
||||||
open={detailVisible}
|
return <Tag color={config?.color}>{config.text}</Tag>;
|
||||||
onCancel={() => setDetailVisible(false)}
|
};
|
||||||
footer={[
|
return (
|
||||||
<Button key="close" onClick={() => setDetailVisible(false)}>
|
<Modal
|
||||||
关闭
|
title={title}
|
||||||
</Button>,
|
open={detailVisible}
|
||||||
]}
|
onCancel={() => setDetailVisible(false)}
|
||||||
width={600}
|
footer={[
|
||||||
>
|
<Button key="close" onClick={() => setDetailVisible(false)}>
|
||||||
{selectedImage && (
|
关闭
|
||||||
<div className="image-detail">
|
</Button>,
|
||||||
{IMAGE_DETAIL_FIELDS.map((field) => (
|
]}
|
||||||
<div className="detail-item" key={String(field.key)}>
|
width={800} // 增加宽度以容纳两列布局
|
||||||
<label>{field.label}</label>
|
>
|
||||||
<span>
|
{selectedImage && (
|
||||||
{'render' in field && field.render
|
<div
|
||||||
? field.render(selectedImage[field.key])
|
className="image-detail"
|
||||||
: selectedImage[field.key] || '--'}
|
style={{
|
||||||
</span>
|
display: 'grid',
|
||||||
</div>
|
gridTemplateColumns: 'repeat(2, 1fr)', // 两列布局
|
||||||
))}
|
gap: '16px', // 间距
|
||||||
</div>
|
}}
|
||||||
)}
|
>
|
||||||
</Modal>
|
{IMAGE_DETAIL_FIELDS.map((field) => (
|
||||||
);
|
<div
|
||||||
|
key={String(field.key)}
|
||||||
|
style={{
|
||||||
|
gridColumn: longTextFields.includes(String(field.key))
|
||||||
|
? '1 / -1'
|
||||||
|
: 'auto', // 长文本字段占整行
|
||||||
|
display: 'flex',
|
||||||
|
minWidth: 0, // 防止内容溢出
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
style={{
|
||||||
|
fontWeight: 500,
|
||||||
|
color: 'rgba(0, 0, 0, 0.85)',
|
||||||
|
marginBottom: 4,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{field.label}
|
||||||
|
</label>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
wordBreak: 'break-word',
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
display: '-webkit-box',
|
||||||
|
WebkitLineClamp: longTextFields.includes(String(field.key))
|
||||||
|
? 1
|
||||||
|
: 2, // 长文本字段只显示一行
|
||||||
|
WebkitBoxOrient: 'vertical',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{'render' in field && field.render
|
||||||
|
? field.render(selectedImage[field.key])
|
||||||
|
: field.key === 'image_status'
|
||||||
|
? getStatusTag(selectedImage[field.key])
|
||||||
|
: selectedImage[field.key] || '--'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const ImportModal: React.FC<IMAGES.ImportModalProps> = ({
|
const ImportModal: React.FC<IMAGES.ImportModalProps> = ({
|
||||||
visible,
|
visible,
|
||||||
onCancel,
|
onCancel,
|
||||||
|
@ -86,9 +128,8 @@ const ImportModal: React.FC<IMAGES.ImportModalProps> = ({
|
||||||
const handleBeforeUnload = (e: BeforeUnloadEvent) => {
|
const handleBeforeUnload = (e: BeforeUnloadEvent) => {
|
||||||
if (isUploading && !uploadCompletedRef.current) {
|
if (isUploading && !uploadCompletedRef.current) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.returnValue = '镜像正在上传中,确定要离开吗?';
|
e.returnValue = '镜像正在上传中,确定要离开吗?';
|
||||||
return e.returnValue;
|
return e.returnValue;
|
||||||
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -142,7 +183,7 @@ const ImportModal: React.FC<IMAGES.ImportModalProps> = ({
|
||||||
try {
|
try {
|
||||||
// 检查是否已中止
|
// 检查是否已中止
|
||||||
if (abortController.current?.signal.aborted) {
|
if (abortController.current?.signal.aborted) {
|
||||||
return {success:false};
|
return { success: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新状态,提示正在计算MD5
|
// 更新状态,提示正在计算MD5
|
||||||
|
@ -210,7 +251,7 @@ const ImportModal: React.FC<IMAGES.ImportModalProps> = ({
|
||||||
// 检查是否因为中止导致的错误
|
// 检查是否因为中止导致的错误
|
||||||
if (error instanceof Error && error.name === 'AbortError') {
|
if (error instanceof Error && error.name === 'AbortError') {
|
||||||
// console.log(`上传分片 ${index} 被用户取消`);
|
// console.log(`上传分片 ${index} 被用户取消`);
|
||||||
return {success:false};
|
return { success: false };
|
||||||
}
|
}
|
||||||
// console.error(`上传分片 ${index} 失败:`, error);
|
// console.error(`上传分片 ${index} 失败:`, error);
|
||||||
return { success: false };
|
return { success: false };
|
||||||
|
@ -295,7 +336,7 @@ const ImportModal: React.FC<IMAGES.ImportModalProps> = ({
|
||||||
// 2. 开始上传
|
// 2. 开始上传
|
||||||
const startUpload = async (file: File) => {
|
const startUpload = async (file: File) => {
|
||||||
try {
|
try {
|
||||||
isManualCancel.current=false;
|
isManualCancel.current = false;
|
||||||
uploadCompletedRef.current = false;
|
uploadCompletedRef.current = false;
|
||||||
setIsUploading(true);
|
setIsUploading(true);
|
||||||
setUploadStatus(UPLOADING);
|
setUploadStatus(UPLOADING);
|
||||||
|
@ -313,7 +354,7 @@ const ImportModal: React.FC<IMAGES.ImportModalProps> = ({
|
||||||
}
|
}
|
||||||
timerRef.current = setInterval(() => {
|
timerRef.current = setInterval(() => {
|
||||||
// 重新计算开始时间,基于当前时间和已用时间
|
// 重新计算开始时间,基于当前时间和已用时间
|
||||||
setElapsedTime((prev) => prev + 1);
|
setElapsedTime((prev) => prev + 1);
|
||||||
}, 1000);
|
}, 1000);
|
||||||
|
|
||||||
// 初始化状态
|
// 初始化状态
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
.ant-modal {
|
||||||
|
.ant-modal-header {
|
||||||
|
margin-bottom: 20px !important;
|
||||||
|
.ant-modal-title {
|
||||||
|
font-weight: 500 !important;
|
||||||
|
color: #17233d !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<IMAGES.ImportModalProps> = ({
|
||||||
|
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<Array<{ chunk: Blob; index: number }>>([]); // 上传队列
|
||||||
|
const completedChunks = useRef<number>(0); // 已上传的分片数量
|
||||||
|
const totalChunks = useRef<number>(0); // 总分片数量
|
||||||
|
const fileId = useRef<string>(''); // 文件id
|
||||||
|
const fileName = useRef<string>(''); // 文件名
|
||||||
|
const fileSize = useRef<number>(0); // 文件大小
|
||||||
|
const abortController = useRef<AbortController | null>(null); // 用于取消上传
|
||||||
|
// 上传镜像时间相关
|
||||||
|
const [currentFile, setCurrentFile] = useState<File | null>(null);
|
||||||
|
const [elapsedTime, setElapsedTime] = useState<number>(0); // 上传时间
|
||||||
|
const timerRef = useRef<NodeJS.Timeout | null>(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 (
|
||||||
|
<Form.Item
|
||||||
|
key={field.name}
|
||||||
|
name={field.name}
|
||||||
|
label={field.label}
|
||||||
|
rules={field.rules}
|
||||||
|
>
|
||||||
|
<Input {...field.props} />
|
||||||
|
</Form.Item>
|
||||||
|
);
|
||||||
|
case 'select':
|
||||||
|
return (
|
||||||
|
<Form.Item
|
||||||
|
key={field.name}
|
||||||
|
name={field.name}
|
||||||
|
label={field.label}
|
||||||
|
rules={field.rules}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
placeholder={field.props.placeholder || '请选择'}
|
||||||
|
// 添加options存在性检查
|
||||||
|
options={field.props.options || []}
|
||||||
|
{...field.props}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
);
|
||||||
|
case 'textarea':
|
||||||
|
return (
|
||||||
|
<Form.Item
|
||||||
|
key={field.name}
|
||||||
|
name={field.name}
|
||||||
|
label={field.label}
|
||||||
|
rules={field.rules}
|
||||||
|
>
|
||||||
|
<Input.TextArea {...field.props} />
|
||||||
|
</Form.Item>
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 检查表单是否完整
|
||||||
|
const isFormValid = () => {
|
||||||
|
const values = form.getFieldsValue();
|
||||||
|
return (
|
||||||
|
FORM_FIELDS_CONFIG.every((field) => {
|
||||||
|
// 检查必填字段
|
||||||
|
if (field.rules?.some((rule) => rule.required)) {
|
||||||
|
return values[field.name];
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}) && currentFile
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = () => {
|
||||||
|
if (!hasStartedUpload && currentFile) {
|
||||||
|
// 如果还没开始上传且有文件,则开始上传
|
||||||
|
startUpload(currentFile);
|
||||||
|
setHasStartedUpload(true);
|
||||||
|
} else if (uploadStatus === 'success') {
|
||||||
|
// 如果上传已完成,则提交表单
|
||||||
|
form.validateFields().then((values) => {
|
||||||
|
const formData = {
|
||||||
|
...values,
|
||||||
|
file: {
|
||||||
|
name: fileName.current,
|
||||||
|
size: fileSize.current,
|
||||||
|
id: fileId.current,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
console.log('提交数据:', formData);
|
||||||
|
message.success('镜像提交成功');
|
||||||
|
onCancel();
|
||||||
|
});
|
||||||
|
} else if (uploadStatus === 'error') {
|
||||||
|
// 如果是上传失败状态,点击"重新上传"
|
||||||
|
if (currentFile) {
|
||||||
|
resetState(); // 重置上传状态但不重置文件
|
||||||
|
startUpload(currentFile);
|
||||||
|
setHasStartedUpload(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 上传单个分片
|
||||||
|
const uploadChunk = async (
|
||||||
|
chunk: Blob,
|
||||||
|
index: number,
|
||||||
|
): Promise<IMAGES.UploadChunkResult> => {
|
||||||
|
try {
|
||||||
|
if (abortController.current?.signal.aborted) {
|
||||||
|
return { success: false };
|
||||||
|
}
|
||||||
|
// 获取表单中的镜像信息
|
||||||
|
const formValues = form.getFieldsValue([
|
||||||
|
'image_name',
|
||||||
|
'image_version',
|
||||||
|
'os_version',
|
||||||
|
]);
|
||||||
|
|
||||||
|
const chunkMD5 = await calculateMD5InChunks(chunk, fileSize.current);
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file_id', fileId.current);
|
||||||
|
formData.append('file_name', fileName.current);
|
||||||
|
formData.append('file_size', fileSize.current.toString());
|
||||||
|
formData.append('chunk', chunk);
|
||||||
|
formData.append('chunk_md5', chunkMD5);
|
||||||
|
formData.append('chunk_size', chunk.size.toString());
|
||||||
|
formData.append('shard_index', index.toString());
|
||||||
|
formData.append('shard_total', totalChunks.current.toString());
|
||||||
|
|
||||||
|
// 添加镜像信息参数
|
||||||
|
formData.append('image_name', formValues.image_name);
|
||||||
|
formData.append('image_version', formValues.image_version);
|
||||||
|
formData.append('os_version', formValues.os_version);
|
||||||
|
|
||||||
|
const response = await uploadChunkAPI(
|
||||||
|
formData,
|
||||||
|
abortController.current?.signal,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
if (response.status === 'completed') {
|
||||||
|
uploadCompletedRef.current = true; // 设置上传完成状态
|
||||||
|
isManualCancel.current = false; // 重置手动取消状态
|
||||||
|
// 文件上传完成,设置进度为100%
|
||||||
|
setUploadProgress(100); // 这里已经正确设置了100%
|
||||||
|
setIsUploading(false);
|
||||||
|
setUploadStatus(SUCCESS);
|
||||||
|
setUploadMessage('文件上传成功!请稍后查看列表。');
|
||||||
|
message.success('文件上传成功!系统正在处理,请稍后查看列表。');
|
||||||
|
onImportSuccess?.();
|
||||||
|
|
||||||
|
// 停止计时器
|
||||||
|
if (timerRef.current) {
|
||||||
|
clearInterval(timerRef.current);
|
||||||
|
timerRef.current = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
} else {
|
||||||
|
console.error(`上传分片 ${index} 失败:`, response.message);
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error && error.name === 'AbortError') {
|
||||||
|
return { success: false };
|
||||||
|
}
|
||||||
|
return { success: false };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 3. 处理上传队列:每次处理最多 MAX_CONCURRENT 个分片
|
||||||
|
const processUploadQueue = async () => {
|
||||||
|
const promises: Promise<void>[] = [];
|
||||||
|
let hasError = false;
|
||||||
|
|
||||||
|
for (
|
||||||
|
let i = 0;
|
||||||
|
i < Math.min(MAX_CONCURRENT, uploadQueue.current.length);
|
||||||
|
i++
|
||||||
|
) {
|
||||||
|
const processChunk = async () => {
|
||||||
|
while (
|
||||||
|
uploadQueue.current.length > 0 &&
|
||||||
|
!abortController.current?.signal.aborted &&
|
||||||
|
!hasError
|
||||||
|
) {
|
||||||
|
const chunkItem = uploadQueue.current.shift();
|
||||||
|
if (chunkItem) {
|
||||||
|
const { chunk, index } = chunkItem;
|
||||||
|
const result = await uploadChunk(chunk, index);
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
// 只有在没有错误的情况下才更新进度
|
||||||
|
if (!hasError) {
|
||||||
|
completedChunks.current += 1;
|
||||||
|
// 修改进度计算方式,只有在真正完成时才显示100%
|
||||||
|
let progress;
|
||||||
|
if (completedChunks.current === totalChunks.current) {
|
||||||
|
progress = 100; // 当所有分片都完成时,显示100%
|
||||||
|
} else {
|
||||||
|
progress = Math.min(
|
||||||
|
Math.floor(
|
||||||
|
(completedChunks.current / totalChunks.current) * 100,
|
||||||
|
),
|
||||||
|
99, // 最大显示99%,防止在最后一步完成前就显示100%
|
||||||
|
);
|
||||||
|
}
|
||||||
|
setUploadProgress(progress);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!abortController.current?.signal.aborted && !hasError) {
|
||||||
|
hasError = true;
|
||||||
|
setIsUploading(false);
|
||||||
|
setUploadStatus(ERROR);
|
||||||
|
if (!isManualCancel.current) {
|
||||||
|
setUploadMessage(result.message || '文件上传失败');
|
||||||
|
message.error(result.message || '文件上传失败');
|
||||||
|
}
|
||||||
|
if (abortController.current) {
|
||||||
|
abortController.current.abort();
|
||||||
|
}
|
||||||
|
// 停止计时器
|
||||||
|
if (timerRef.current) {
|
||||||
|
clearInterval(timerRef.current);
|
||||||
|
timerRef.current = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
promises.push(processChunk());
|
||||||
|
}
|
||||||
|
await Promise.all(promises);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 2. 开始上传
|
||||||
|
const startUpload = async (file: File) => {
|
||||||
|
try {
|
||||||
|
isManualCancel.current = false;
|
||||||
|
uploadCompletedRef.current = false;
|
||||||
|
setIsUploading(true);
|
||||||
|
setUploadStatus(UPLOADING);
|
||||||
|
setUploadProgress(0);
|
||||||
|
setUploadMessage('正在准备上传...');
|
||||||
|
setCurrentFile(file);
|
||||||
|
|
||||||
|
// 启动计时器
|
||||||
|
// 启动计时器
|
||||||
|
setElapsedTime(0);
|
||||||
|
|
||||||
|
// 立即更新一次时间
|
||||||
|
setElapsedTime(Math.floor((Date.now() - Date.now()) / 1000));
|
||||||
|
|
||||||
|
if (timerRef.current) {
|
||||||
|
clearInterval(timerRef.current);
|
||||||
|
}
|
||||||
|
timerRef.current = setInterval(() => {
|
||||||
|
// 重新计算开始时间,基于当前时间和已用时间
|
||||||
|
setElapsedTime((prev) => prev + 1);
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
// 初始化分片状态
|
||||||
|
completedChunks.current = 0;
|
||||||
|
fileSize.current = file.size;
|
||||||
|
fileName.current = file.name;
|
||||||
|
fileId.current = uuidv4();
|
||||||
|
const chunkSize = getChunkSize(file.size);
|
||||||
|
totalChunks.current = Math.ceil(file.size / chunkSize);
|
||||||
|
uploadQueue.current = [];
|
||||||
|
setUploadMessage(`正在分析文件... `);
|
||||||
|
|
||||||
|
// 切割文件为分片
|
||||||
|
for (let i = 0; i < totalChunks.current; i++) {
|
||||||
|
const start = i * chunkSize;
|
||||||
|
const end = Math.min(start + chunkSize, file.size);
|
||||||
|
const chunk = file.slice(start, end);
|
||||||
|
uploadQueue.current.push({ chunk, index: i + 1 });
|
||||||
|
}
|
||||||
|
|
||||||
|
setUploadMessage(`开始上传文件... `);
|
||||||
|
// 创建新的AbortController
|
||||||
|
abortController.current = new AbortController();
|
||||||
|
|
||||||
|
// 开始处理上传队列
|
||||||
|
await processUploadQueue();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('上传准备失败:', error);
|
||||||
|
setIsUploading(false);
|
||||||
|
setUploadStatus(ERROR);
|
||||||
|
setUploadMessage('上传准备失败,请重试');
|
||||||
|
message.error('上传准备失败');
|
||||||
|
if (timerRef.current) {
|
||||||
|
clearInterval(timerRef.current);
|
||||||
|
timerRef.current = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// 1. 处理文件上传
|
||||||
|
const handleFileUpload: UploadProps['beforeUpload'] = (file) => {
|
||||||
|
// 检查文件扩展名
|
||||||
|
// const allowedExtensions = ['.tar.gz', '.iso', '.qcow2'];
|
||||||
|
// const fileExtension = file.name
|
||||||
|
// .substring(file.name.lastIndexOf('.'))
|
||||||
|
// .toLowerCase();
|
||||||
|
// const isAllowedExtension = allowedExtensions.some((ext) =>
|
||||||
|
// file.name.toLowerCase().endsWith(ext),
|
||||||
|
// );
|
||||||
|
|
||||||
|
// if (!isAllowedExtension) {
|
||||||
|
// message.error(
|
||||||
|
// '不支持的文件类型,请上传 .tar.gz, .iso, .qcow2 格式的文件',
|
||||||
|
// );
|
||||||
|
// return false;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// 检查文件大小
|
||||||
|
if (file.size > UPLOAD_CONFIG.MAX_FILE_SIZE) {
|
||||||
|
// 50GB
|
||||||
|
message.error('文件大小不能超过50GB');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// 设置文件但不立即上传
|
||||||
|
setCurrentFile(file);
|
||||||
|
setHasStartedUpload(false);
|
||||||
|
return false;
|
||||||
|
// // 开始上传流程
|
||||||
|
// startUpload(file);
|
||||||
|
// return false; // 阻止默认上传行为
|
||||||
|
};
|
||||||
|
|
||||||
|
// 取消上传
|
||||||
|
const cancelUpload = async () => {
|
||||||
|
isManualCancel.current = true;
|
||||||
|
uploadCompletedRef.current = true;
|
||||||
|
uploadQueue.current = [];
|
||||||
|
|
||||||
|
const oldController = abortController.current;
|
||||||
|
abortController.current = new AbortController();
|
||||||
|
if (oldController) {
|
||||||
|
oldController.abort();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fileId.current) {
|
||||||
|
try {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
params.append('file_id', fileId.current);
|
||||||
|
await cancelUploadImagesAPI(params);
|
||||||
|
message.success('上传已取消');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('取消上传API调用失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsUploading(false);
|
||||||
|
setUploadStatus(READY);
|
||||||
|
setUploadProgress(0);
|
||||||
|
setHasStartedUpload(false);
|
||||||
|
|
||||||
|
if (timerRef.current) {
|
||||||
|
clearInterval(timerRef.current);
|
||||||
|
timerRef.current = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderFileSize = (file: File | null) => {
|
||||||
|
if (!file) return '0 Bytes';
|
||||||
|
|
||||||
|
const bytes = file.size;
|
||||||
|
if (bytes === 0) return '0 Bytes';
|
||||||
|
|
||||||
|
const k = 1024;
|
||||||
|
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
|
|
||||||
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderUploadInfo = () => (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
border: '1px solid #d9d9d9',
|
||||||
|
borderRadius: 4,
|
||||||
|
padding: 12,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Space>
|
||||||
|
<FileOutlined style={{ fontSize: 24 }} />
|
||||||
|
<div>
|
||||||
|
<div>{currentFile?.name}</div>
|
||||||
|
<div style={{ fontSize: 12, color: '#999' }}>
|
||||||
|
{renderFileSize(currentFile)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Space>
|
||||||
|
|
||||||
|
{hasStartedUpload ? (
|
||||||
|
<Space size="large" align="center">
|
||||||
|
<span style={{ ...MESSAGE_STYLE_MAP[uploadStatus], minWidth: 120 }}>
|
||||||
|
{uploadMessage}
|
||||||
|
</span>
|
||||||
|
{/* 修改Progress组件,添加format显示百分比 */}
|
||||||
|
<Progress
|
||||||
|
percent={uploadProgress}
|
||||||
|
status={uploadStatus === 'error' ? 'exception' : 'normal'}
|
||||||
|
size="small"
|
||||||
|
style={{ width: 120 }}
|
||||||
|
format={(percent) => `${percent}%`}
|
||||||
|
/>
|
||||||
|
<span style={{ position: 'relative', top: 2 }}>
|
||||||
|
{formatTime(elapsedTime)}
|
||||||
|
</span>
|
||||||
|
{isUploading ? (
|
||||||
|
<Button
|
||||||
|
style={{ position: 'relative', top: 2 }}
|
||||||
|
type="text"
|
||||||
|
title="取消上传"
|
||||||
|
onClick={cancelUpload}
|
||||||
|
>
|
||||||
|
取消上传
|
||||||
|
</Button>
|
||||||
|
) : uploadStatus === 'success' ? null : (
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
icon={<CloseOutlined />}
|
||||||
|
onClick={() => {
|
||||||
|
setCurrentFile(null);
|
||||||
|
setHasStartedUpload(false);
|
||||||
|
resetState();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
移除文件
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{/* <Button
|
||||||
|
type="text"
|
||||||
|
icon={<CloseOutlined />}
|
||||||
|
title="删除文件"
|
||||||
|
// onClick={cancelUpload}
|
||||||
|
disabled={isUploading && uploadStatus !== ERROR}
|
||||||
|
/> */}
|
||||||
|
</Space>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
icon={<CloseOutlined />}
|
||||||
|
onClick={() => {
|
||||||
|
setCurrentFile(null);
|
||||||
|
setHasStartedUpload(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
移除文件
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
// 导入弹窗内容
|
||||||
|
const renderStepContent = () => {
|
||||||
|
switch (currentStep) {
|
||||||
|
case 0:
|
||||||
|
return (
|
||||||
|
<Form form={form} labelCol={{ span: 3 }} wrapperCol={{ span: 20 }}>
|
||||||
|
{renderFormFields()}
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
case 1:
|
||||||
|
return (
|
||||||
|
<Form.Item label="上传系统镜像" required>
|
||||||
|
{!currentFile ? (
|
||||||
|
<Dragger
|
||||||
|
beforeUpload={handleFileUpload}
|
||||||
|
disabled={isUploading}
|
||||||
|
multiple={false}
|
||||||
|
showUploadList={false}
|
||||||
|
>
|
||||||
|
<p className="ant-upload-drag-icon">
|
||||||
|
<UploadOutlined style={{ fontSize: 48 }} />
|
||||||
|
</p>
|
||||||
|
<p className="ant-upload-text">点击上传或拖拽文件到此处</p>
|
||||||
|
<p className="ant-upload-hint">
|
||||||
|
支持单个文件上传,大小限制为50G
|
||||||
|
</p>
|
||||||
|
</Dragger>
|
||||||
|
) : (
|
||||||
|
renderUploadInfo()
|
||||||
|
)}
|
||||||
|
</Form.Item>
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderFooter = () => {
|
||||||
|
if (currentStep === 0) {
|
||||||
|
return [
|
||||||
|
// <Button key="cancel" onClick={handleModalCancel}>
|
||||||
|
// 关闭
|
||||||
|
// </Button>,
|
||||||
|
<Button key="next" type="primary" onClick={handleNext}>
|
||||||
|
下一步
|
||||||
|
</Button>,
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
return [
|
||||||
|
<Button
|
||||||
|
key="back"
|
||||||
|
onClick={handlePrev}
|
||||||
|
disabled={
|
||||||
|
isUploading ||
|
||||||
|
uploadStatus === 'uploading' ||
|
||||||
|
uploadStatus === 'success'
|
||||||
|
}
|
||||||
|
icon={<ArrowLeftOutlined />}
|
||||||
|
>
|
||||||
|
上一步
|
||||||
|
</Button>,
|
||||||
|
<Button
|
||||||
|
key="submit"
|
||||||
|
type="primary"
|
||||||
|
onClick={handleSubmit}
|
||||||
|
disabled={
|
||||||
|
!currentFile || // 没有文件时禁用
|
||||||
|
(hasStartedUpload &&
|
||||||
|
(isUploading ||
|
||||||
|
(uploadStatus === 'error' && uploadProgress > 0))) ||
|
||||||
|
uploadStatus === 'success' // 上传失败且已有进度时不禁用
|
||||||
|
}
|
||||||
|
loading={isUploading}
|
||||||
|
>
|
||||||
|
{hasStartedUpload
|
||||||
|
? uploadStatus === 'success'
|
||||||
|
? '上传成功' // 上传成功时显示"上传成功"
|
||||||
|
: uploadStatus === 'error'
|
||||||
|
? '重新上传' // 上传失败时显示"重新上传"
|
||||||
|
: '上传中...'
|
||||||
|
: '开始上传'}
|
||||||
|
</Button>,
|
||||||
|
<Button key="cancel" onClick={handleModalCancel}>
|
||||||
|
关闭
|
||||||
|
</Button>,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleModalCancel = () => {
|
||||||
|
if (isUploading) {
|
||||||
|
cancelUpload().finally(() => {
|
||||||
|
resetState(); // 确保取消完成后再重置
|
||||||
|
onCancel();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
resetState();
|
||||||
|
onCancel();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
title="新建系统镜像"
|
||||||
|
open={visible}
|
||||||
|
onCancel={handleModalCancel}
|
||||||
|
footer={renderFooter()}
|
||||||
|
width={1000}
|
||||||
|
wrapClassName="import-modal"
|
||||||
|
maskClosable={false}
|
||||||
|
closable={!isUploading}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{ display: 'flex', justifyContent: 'center', marginBottom: 16 }}
|
||||||
|
>
|
||||||
|
<Steps current={currentStep} style={{ marginBottom: 24, width: '50%' }}>
|
||||||
|
<Step title="基本信息" />
|
||||||
|
<Step title="上传镜像" />
|
||||||
|
</Steps>
|
||||||
|
</div>
|
||||||
|
{renderStepContent()}
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { ImportModal };
|
|
@ -1,4 +1,8 @@
|
||||||
import { CODE, IMAGES_TYPE_MAP } from '@/constants/images.constants';
|
import {
|
||||||
|
CODE,
|
||||||
|
IMAGES_TYPE_MAP,
|
||||||
|
STATUS_MAP,
|
||||||
|
} from '@/constants/images.constants';
|
||||||
import { delImagesAPI, getImagesList } from '@/services/images';
|
import { delImagesAPI, getImagesList } from '@/services/images';
|
||||||
import {
|
import {
|
||||||
DeleteOutlined,
|
DeleteOutlined,
|
||||||
|
@ -21,7 +25,8 @@ import {
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import { ImportModal, ModalDetailShow } from './components/modalShow/modalShow';
|
import { ModalDetailShow } from './components/modalShow/modalShow';
|
||||||
|
import { ImportModal } from './components/uploadFileModal/uploadFileModal';
|
||||||
import useTableParams from './hook/hook';
|
import useTableParams from './hook/hook';
|
||||||
import './index.less';
|
import './index.less';
|
||||||
|
|
||||||
|
@ -140,7 +145,7 @@ const ImageList: React.FC = () => {
|
||||||
key: 'image_name',
|
key: 'image_name',
|
||||||
title: '镜像名称',
|
title: '镜像名称',
|
||||||
dataIndex: 'image_name',
|
dataIndex: 'image_name',
|
||||||
width: 200,
|
width: 150,
|
||||||
defaultVisible: true,
|
defaultVisible: true,
|
||||||
alwaysVisible: true,
|
alwaysVisible: true,
|
||||||
ellipsis: true,
|
ellipsis: true,
|
||||||
|
@ -154,34 +159,51 @@ const ImageList: React.FC = () => {
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'image_type',
|
key: 'image_file_name',
|
||||||
title: '桌面类型',
|
title: '镜像文件',
|
||||||
dataIndex: 'image_type',
|
dataIndex: 'image_file_name',
|
||||||
width: 120,
|
width: 150,
|
||||||
render: (text: number) => {
|
|
||||||
const key = text as keyof typeof IMAGES_TYPE_MAP;
|
|
||||||
return text ? IMAGES_TYPE_MAP[key] : '--';
|
|
||||||
},
|
|
||||||
defaultVisible: true,
|
defaultVisible: true,
|
||||||
filterDropdown: ({ setSelectedKeys, selectedKeys, confirm }) => (
|
alwaysVisible: true,
|
||||||
<Menu
|
ellipsis: true,
|
||||||
selectedKeys={selectedKeys.length > 0 ? selectedKeys : ['全部']}
|
render: (text: string) =>
|
||||||
onClick={({ key }) => {
|
text ? (
|
||||||
setSelectedKeys(key === '全部' ? [] : [key]);
|
<Tooltip title={text} placement="topLeft">
|
||||||
confirm({ closeDropdown: true }); // 立即触发筛选并关闭下拉菜单
|
{text}
|
||||||
}}
|
</Tooltip>
|
||||||
items={[
|
) : (
|
||||||
{ key: '全部', label: '全部' },
|
'--'
|
||||||
...Object.entries(IMAGES_TYPE_MAP).map(([key, value]) => ({
|
),
|
||||||
key,
|
|
||||||
label: value,
|
|
||||||
})),
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
filterMultiple: false,
|
|
||||||
defaultFilteredValue: ['全部'],
|
|
||||||
},
|
},
|
||||||
|
// {
|
||||||
|
// key: 'image_type',
|
||||||
|
// title: '桌面类型',
|
||||||
|
// dataIndex: 'image_type',
|
||||||
|
// width: 120,
|
||||||
|
// render: (text: number) => {
|
||||||
|
// const key = text as keyof typeof IMAGES_TYPE_MAP;
|
||||||
|
// return text ? IMAGES_TYPE_MAP[key] : '--';
|
||||||
|
// },
|
||||||
|
// defaultVisible: true,
|
||||||
|
// filterDropdown: ({ setSelectedKeys, selectedKeys, confirm }) => (
|
||||||
|
// <Menu
|
||||||
|
// selectedKeys={selectedKeys.length > 0 ? selectedKeys : ['全部']}
|
||||||
|
// onClick={({ key }) => {
|
||||||
|
// setSelectedKeys(key === '全部' ? [] : [key]);
|
||||||
|
// confirm({ closeDropdown: true }); // 立即触发筛选并关闭下拉菜单
|
||||||
|
// }}
|
||||||
|
// items={[
|
||||||
|
// { key: '全部', label: '全部' },
|
||||||
|
// ...Object.entries(IMAGES_TYPE_MAP).map(([key, value]) => ({
|
||||||
|
// key,
|
||||||
|
// label: value,
|
||||||
|
// })),
|
||||||
|
// ]}
|
||||||
|
// />
|
||||||
|
// ),
|
||||||
|
// filterMultiple: false,
|
||||||
|
// defaultFilteredValue: ['全部'],
|
||||||
|
// },
|
||||||
{
|
{
|
||||||
key: 'storage_path',
|
key: 'storage_path',
|
||||||
title: '模板存放路径',
|
title: '模板存放路径',
|
||||||
|
@ -189,16 +211,30 @@ const ImageList: React.FC = () => {
|
||||||
width: 140,
|
width: 140,
|
||||||
defaultVisible: true,
|
defaultVisible: true,
|
||||||
ellipsis: true,
|
ellipsis: true,
|
||||||
render: (text: string) => text ? <Tooltip title={text} placement="topLeft">{text}</Tooltip>: '--'
|
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: 180,
|
width: 140,
|
||||||
defaultVisible: true,
|
defaultVisible: true,
|
||||||
ellipsis: true,
|
ellipsis: true,
|
||||||
render: (text: string) => text ? <Tooltip title={text} placement="topLeft">{text}</Tooltip>:'--'
|
render: (text: string) =>
|
||||||
|
text ? (
|
||||||
|
<Tooltip title={text} placement="topLeft">
|
||||||
|
{text}
|
||||||
|
</Tooltip>
|
||||||
|
) : (
|
||||||
|
'--'
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'image_version',
|
key: 'image_version',
|
||||||
|
@ -230,7 +266,7 @@ const ImageList: React.FC = () => {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'create_time',
|
key: 'create_time',
|
||||||
title: '创建时间',
|
title: '上传时间',
|
||||||
dataIndex: 'create_time',
|
dataIndex: 'create_time',
|
||||||
width: 160,
|
width: 160,
|
||||||
render: (text: string) =>
|
render: (text: string) =>
|
||||||
|
@ -324,11 +360,7 @@ const ImageList: React.FC = () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const getStatusTag = (status: number) => {
|
const getStatusTag = (status: number) => {
|
||||||
const statusMap = {
|
const config = STATUS_MAP[status as keyof typeof STATUS_MAP];
|
||||||
1: { color: 'green', text: '成功' },
|
|
||||||
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>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -481,7 +513,7 @@ const ImageList: React.FC = () => {
|
||||||
return (
|
return (
|
||||||
<div className="image-list">
|
<div className="image-list">
|
||||||
<div className="search-box">
|
<div className="search-box">
|
||||||
<Button onClick={() => setImportModalVisible(true)}>导入</Button>
|
<Button onClick={() => setImportModalVisible(true)}>新建</Button>
|
||||||
<div className="search-input">
|
<div className="search-input">
|
||||||
<Input.Search
|
<Input.Search
|
||||||
placeholder="镜像名称"
|
placeholder="镜像名称"
|
||||||
|
|
|
@ -65,6 +65,20 @@ declare namespace IMAGES {
|
||||||
status?: boolean; // 是否应该停止上传
|
status?: boolean; // 是否应该停止上传
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 镜像新增表单字段配置
|
||||||
|
type FormFieldConfig = {
|
||||||
|
name: string;
|
||||||
|
label: string;
|
||||||
|
type: 'input' | 'select' | 'textarea';
|
||||||
|
rules?: any[];
|
||||||
|
props: {
|
||||||
|
placeholder?: string;
|
||||||
|
maxLength?: number;
|
||||||
|
options?: { value: string; label: string }[]; // 明确options类型
|
||||||
|
[key: string]: any;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export type ImageDetailField =
|
export type ImageDetailField =
|
||||||
| ImageDetailFieldWithoutRender
|
| ImageDetailFieldWithoutRender
|
||||||
| ImageDetailFieldWithRender;
|
| ImageDetailFieldWithRender;
|
||||||
|
|
Loading…
Reference in New Issue