fix(前端): 镜像列表修改

master
chenyt 2025-08-14 17:21:32 +08:00
parent 218150f8a0
commit e656e20450
7 changed files with 1000 additions and 90 deletions

View File

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

View File

@ -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' },
{ label: '描述:', key: 'description' },
] as const;

View File

@ -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,6 +22,11 @@ const { Dragger } = Upload;
/**详情弹窗 */
const ModalDetailShow = (props: any) => {
const { detailVisible, setDetailVisible, selectedImage, title } = props;
const longTextFields = ['bt_path', 'description', 'storage_path'];
const getStatusTag = (status: number) => {
const config = STATUS_MAP[status as keyof typeof STATUS_MAP];
return <Tag color={config?.color}>{config.text}</Tag>;
};
return (
<Modal
title={title}
@ -31,18 +37,55 @@ const ModalDetailShow = (props: any) => {
</Button>,
]}
width={600}
width={800} // 增加宽度以容纳两列布局
>
{selectedImage && (
<div className="image-detail">
<div
className="image-detail"
style={{
display: 'grid',
gridTemplateColumns: 'repeat(2, 1fr)', // 两列布局
gap: '16px', // 间距
}}
>
{IMAGE_DETAIL_FIELDS.map((field) => (
<div className="detail-item" key={String(field.key)}>
<label>{field.label}</label>
<span>
<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] || '--'}
</span>
</div>
</div>
))}
</div>
@ -51,7 +94,6 @@ const ModalDetailShow = (props: any) => {
);
};
const ImportModal: React.FC<IMAGES.ImportModalProps> = ({
visible,
onCancel,
@ -88,7 +130,6 @@ const ImportModal: React.FC<IMAGES.ImportModalProps> = ({
e.preventDefault();
e.returnValue = '镜像正在上传中,确定要离开吗?';
return e.returnValue;
}
};

View File

@ -0,0 +1,9 @@
.ant-modal {
.ant-modal-header {
margin-bottom: 20px !important;
.ant-modal-title {
font-weight: 500 !important;
color: #17233d !important;
}
}
}

View File

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

View File

@ -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 {
DeleteOutlined,
@ -21,7 +25,8 @@ import {
} from 'antd';
import dayjs from 'dayjs';
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 './index.less';
@ -140,7 +145,7 @@ const ImageList: React.FC = () => {
key: 'image_name',
title: '镜像名称',
dataIndex: 'image_name',
width: 200,
width: 150,
defaultVisible: true,
alwaysVisible: true,
ellipsis: true,
@ -154,34 +159,51 @@ const ImageList: React.FC = () => {
),
},
{
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] : '--';
},
key: 'image_file_name',
title: '镜像文件',
dataIndex: 'image_file_name',
width: 150,
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,
})),
]}
/>
alwaysVisible: true,
ellipsis: true,
render: (text: string) =>
text ? (
<Tooltip title={text} placement="topLeft">
{text}
</Tooltip>
) : (
'--'
),
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',
title: '模板存放路径',
@ -189,16 +211,30 @@ const ImageList: React.FC = () => {
width: 140,
defaultVisible: 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',
title: 'BT路径',
dataIndex: 'bt_path',
width: 180,
width: 140,
defaultVisible: 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',
@ -230,7 +266,7 @@ const ImageList: React.FC = () => {
},
{
key: 'create_time',
title: '创建时间',
title: '上传时间',
dataIndex: 'create_time',
width: 160,
render: (text: string) =>
@ -324,11 +360,7 @@ const ImageList: React.FC = () => {
}
};
const getStatusTag = (status: number) => {
const statusMap = {
1: { color: 'green', text: '成功' },
2: { color: 'red', text: '失败' },
};
const config = statusMap[status as keyof typeof statusMap];
const config = STATUS_MAP[status as keyof typeof STATUS_MAP];
return <Tag color={config?.color}>{config.text}</Tag>;
};
@ -481,7 +513,7 @@ const ImageList: React.FC = () => {
return (
<div className="image-list">
<div className="search-box">
<Button onClick={() => setImportModalVisible(true)}></Button>
<Button onClick={() => setImportModalVisible(true)}></Button>
<div className="search-input">
<Input.Search
placeholder="镜像名称"

View File

@ -65,6 +65,20 @@ declare namespace IMAGES {
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 =
| ImageDetailFieldWithoutRender
| ImageDetailFieldWithRender;