feat(镜像管理): 待联调
parent
9efc422b8a
commit
65b71d61ac
|
@ -0,0 +1,94 @@
|
|||
export default {
|
||||
'POST /api/v1/images/queryimagesList': (req: any, res: any) => {
|
||||
const { page_size, page_num } = req.body;
|
||||
const data = [];
|
||||
function getRandomFormat() {
|
||||
const random = Math.random(); // 生成 0 ~ 1 的随机数
|
||||
|
||||
if (random < 0.33) {
|
||||
return 1;
|
||||
} else if (random < 0.66) {
|
||||
return 2;
|
||||
} else {
|
||||
return 3;
|
||||
}
|
||||
}
|
||||
for (let i = 1; i <= page_size; i++) {
|
||||
data.push({
|
||||
image_id: i,
|
||||
image_name: `周俊星${(page_num - 1) * page_size + i}`,
|
||||
image_type: getRandomFormat(),
|
||||
bt_path: `/serve/logs`,
|
||||
storage_path: '/mock/images',
|
||||
create_time: +new Date(),
|
||||
});
|
||||
}
|
||||
const result = {
|
||||
error_code: '0000000000',
|
||||
message: '操作成功',
|
||||
data: {
|
||||
paging: {
|
||||
total: 520,
|
||||
total_num: 520,
|
||||
page_num: page_num,
|
||||
page_size: page_size,
|
||||
},
|
||||
data: data,
|
||||
},
|
||||
};
|
||||
setTimeout(() => {
|
||||
res.send(result);
|
||||
}, 500);
|
||||
},
|
||||
'POST /api/v1/images/file/chunk/upload': (req: any, res: any) => {
|
||||
// 打印所有接收到的字段
|
||||
console.log('=== 分片上传信息 ===');
|
||||
console.log('文件ID:', req.body.file_id);
|
||||
console.log('文件名:', req.body.file_name);
|
||||
console.log('文件大小:', req.body.file_size);
|
||||
console.log('分片索引:', req.body.shard_index);
|
||||
console.log('分片总数:', req.body.shard_total);
|
||||
console.log('分片大小:', req.body.chunk_size);
|
||||
console.log('分片MD5:', req.body.chunk_md5);
|
||||
|
||||
// 如果有文件上传,打印文件信息
|
||||
if (req.files && req.files.chunk) {
|
||||
console.log('上传的分片文件:', req.files.chunk);
|
||||
}
|
||||
|
||||
// 模拟上传进度
|
||||
const shardIndex = parseInt(req.body.shard_index);
|
||||
const shardTotal = parseInt(req.body.shard_total);
|
||||
|
||||
console.log(`分片上传进度: ${shardIndex + 1}/${shardTotal}`);
|
||||
|
||||
// 模拟上传完成(最后一个分片)
|
||||
if (shardIndex === shardTotal - 1) {
|
||||
console.log('文件上传完成!');
|
||||
res.send({
|
||||
success: true,
|
||||
error_code: '0000000000',
|
||||
message: '操作成功',
|
||||
data: {
|
||||
file_id: req.body.file_id,
|
||||
message: '文件上传成功',
|
||||
},
|
||||
});
|
||||
} else {
|
||||
// 模拟上传中
|
||||
const progress = Math.round(((shardIndex + 1) / shardTotal) * 100);
|
||||
console.log(`上传进度: ${progress}%`);
|
||||
|
||||
res.send({
|
||||
success: false,
|
||||
error_code: '1221000001',
|
||||
message: `上传中... ${progress}%`,
|
||||
data: {
|
||||
uploaded: shardIndex + 1,
|
||||
total: shardTotal,
|
||||
progress: progress,
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
|
@ -13,11 +13,14 @@
|
|||
"@ant-design/icons": "^5.0.1",
|
||||
"@ant-design/pro-components": "^2.4.4",
|
||||
"@umijs/max": "^4.4.11",
|
||||
"antd": "^5.4.0"
|
||||
"antd": "^5.4.0",
|
||||
"spark-md5": "^3.0.2",
|
||||
"uuid": "^11.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.0.33",
|
||||
"@types/react-dom": "^18.0.11",
|
||||
"@types/spark-md5": "^3.0.5",
|
||||
"lint-staged": "^13.2.0",
|
||||
"prettier": "^2.8.7",
|
||||
"prettier-plugin-organize-imports": "^3.2.2",
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1754450820809" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5341" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M981.448462 133.180788a35.367512 35.367512 0 0 0-35.367512 35.367512v85.103076a505.092283 505.092283 0 0 0-939.449541 221.046951 35.367512 35.367512 0 0 0 32.604425 38.130599 35.367512 35.367512 0 0 0 35.367512-32.604425 434.357258 434.357258 0 0 1 819.53157-165.785213h-93.944954a35.367512 35.367512 0 1 0 0 71.287641h181.2585a35.367512 35.367512 0 0 0 35.367512-35.367512V168.5483a35.367512 35.367512 0 0 0-35.367512-35.367512z m0 379.095521a35.367512 35.367512 0 0 0-38.130599 32.604425 434.357258 434.357258 0 0 1-819.531571 165.785213h100.023746a35.367512 35.367512 0 1 0 0-71.287642H42.551538a35.367512 35.367512 0 0 0-35.367512 35.367513v181.258499a35.367512 35.367512 0 1 0 71.287642 0v-85.655693a505.092283 505.092283 0 0 0 939.449541-221.046951 35.367512 35.367512 0 0 0-34.814895-37.025364z" fill="#5E5C5C" p-id="5342"></path></svg>
|
After Width: | Height: | Size: 1.1 KiB |
|
@ -0,0 +1,7 @@
|
|||
export const ERROR_CODE = '0000000000';
|
||||
|
||||
export const IMAGES_TYPE_MAP = {
|
||||
1: 'VHD',
|
||||
2: 'VHDX',
|
||||
3: 'QCOW2',
|
||||
} as const;
|
|
@ -0,0 +1,425 @@
|
|||
import { IMAGES_TYPE_MAP } from '@/constants/images.constants';
|
||||
import { uploadChunkAPI } from '@/services/images';
|
||||
import { Alert, Button, message, Modal, Progress, Upload } from 'antd';
|
||||
import { UploadProps } from 'antd/lib/upload';
|
||||
import React, { useRef, useState } from 'react';
|
||||
import SparkMD5 from 'spark-md5';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
const { Dragger } = Upload;
|
||||
/**详情弹窗 */
|
||||
const ModalDetailShow = (props: any) => {
|
||||
const { detailVisible, setDetailVisible, selectedImage, title } = props;
|
||||
return (
|
||||
<Modal
|
||||
title={title}
|
||||
open={detailVisible}
|
||||
onCancel={() => setDetailVisible(false)}
|
||||
footer={[
|
||||
<Button key="close" onClick={() => setDetailVisible(false)}>
|
||||
关闭
|
||||
</Button>,
|
||||
]}
|
||||
width={600}
|
||||
>
|
||||
{selectedImage && (
|
||||
<div className="image-detail">
|
||||
<div className="detail-item">
|
||||
<label>镜像名称:</label>
|
||||
<span>{selectedImage.image_name}</span>
|
||||
</div>
|
||||
<div className="detail-item">
|
||||
<label>桌面类型:</label>
|
||||
<span>
|
||||
{IMAGES_TYPE_MAP[
|
||||
selectedImage.image_type as keyof typeof IMAGES_TYPE_MAP
|
||||
] || '--'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="detail-item">
|
||||
<label>模板存放路径:</label>
|
||||
<span>{selectedImage.storage_path}</span>
|
||||
</div>
|
||||
<div className="detail-item">
|
||||
<label>BT路径:</label>
|
||||
<span>{selectedImage.bt_path}</span>
|
||||
</div>
|
||||
{/* <div className="detail-item">
|
||||
<label>状态:</label>
|
||||
<span>{getStatusTag(selectedImage.status)}</span>
|
||||
</div> */}
|
||||
<div className="detail-item">
|
||||
<label>创建时间:</label>
|
||||
<span>{selectedImage.create_time}</span>
|
||||
</div>
|
||||
{/* <div className="detail-item">
|
||||
<label>描述:</label>
|
||||
<p>{selectedImage.description}</p>
|
||||
</div> */}
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
/**上传文件分片 */
|
||||
interface ImportModalProps {
|
||||
visible: boolean;
|
||||
onCancel: () => void;
|
||||
onImportSuccess?: () => void;
|
||||
}
|
||||
|
||||
const ImportModal: React.FC<ImportModalProps> = ({
|
||||
visible,
|
||||
onCancel,
|
||||
onImportSuccess,
|
||||
}) => {
|
||||
const [uploadProgress, setUploadProgress] = useState(0);
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
const [uploadStatus, setUploadStatus] = useState<
|
||||
'ready' | 'uploading' | 'success' | 'error'
|
||||
>('ready');
|
||||
const [uploadMessage, setUploadMessage] = useState('');
|
||||
|
||||
// 分片上传相关
|
||||
const CHUNK_SIZE = 10 * 1024 * 1024; // 50MB/每片 网络传输层面的分片
|
||||
const MAX_CONCURRENT = 5; // 同时上传的分片数量
|
||||
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); // 用于取消上传
|
||||
|
||||
// 5. 分块计算文件MD5
|
||||
const calculateMD5InChunks = (
|
||||
file: Blob,
|
||||
chunkSize: number = file.size > 50 * 1024 * 1024
|
||||
? 2 * 1024 * 1024 // 大文件用2MB块
|
||||
: 1 * 1024 * 1024, // 小文件用1MB块, // 计算MD5时的内存分块大小 内存处理层面的分块 即在计算每个10MB分片的MD5值时,为了避免占用过多内存,将10MB的分片再进一步分成2MB的小块逐步读取计算
|
||||
): Promise<string> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const spark = new SparkMD5.ArrayBuffer();
|
||||
const fileReader = new FileReader();
|
||||
let currentChunk = 0;
|
||||
const chunks = Math.ceil(file.size / chunkSize);
|
||||
|
||||
fileReader.onload = (e) => {
|
||||
try {
|
||||
spark.append(e.target?.result as ArrayBuffer);
|
||||
currentChunk++;
|
||||
|
||||
if (currentChunk < chunks) {
|
||||
loadNext();
|
||||
} else {
|
||||
const hash = spark.end();
|
||||
spark.destroy(); // 释放内存
|
||||
resolve(hash);
|
||||
}
|
||||
} catch (error) {
|
||||
spark.destroy();
|
||||
reject(error);
|
||||
}
|
||||
};
|
||||
|
||||
fileReader.onerror = () => {
|
||||
spark.destroy();
|
||||
reject(new Error('计算MD5失败'));
|
||||
};
|
||||
|
||||
const loadNext = () => {
|
||||
const start = currentChunk * chunkSize;
|
||||
const end = Math.min(start + chunkSize, file.size);
|
||||
fileReader.readAsArrayBuffer(file.slice(start, end));
|
||||
};
|
||||
|
||||
loadNext();
|
||||
});
|
||||
};
|
||||
|
||||
// 4. 上传单个分片
|
||||
const uploadChunk = async (chunk: Blob, index: number): Promise<boolean> => {
|
||||
try {
|
||||
// 检查是否已中止
|
||||
if (abortController.current?.signal.aborted) {
|
||||
return false;
|
||||
}
|
||||
// 使用 spark-md5 计算当前分片的MD5
|
||||
const chunkMD5 = await calculateMD5InChunks(chunk);
|
||||
|
||||
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());
|
||||
|
||||
// 这里应该调用实际的上传API
|
||||
// 示例: await uploadChunkAPI(formData);
|
||||
await uploadChunkAPI(formData, abortController.current?.signal);
|
||||
// 模拟上传过程
|
||||
// await new Promise((resolve) =>
|
||||
// setTimeout(resolve, 300 + Math.random() * 700),
|
||||
// );
|
||||
|
||||
// 模拟8%的失败率用于测试
|
||||
// if (Math.random() < 0.08) {
|
||||
// throw new Error('网络错误');
|
||||
// }
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
// 检查是否因为中止导致的错误
|
||||
if (error instanceof Error && error.name === 'AbortError') {
|
||||
// console.log(`上传分片 ${index} 被用户取消`);
|
||||
return false;
|
||||
}
|
||||
// console.error(`上传分片 ${index} 失败:`, error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// 3. 处理上传队列:每次处理最多 MAX_CONCURRENT 个分片
|
||||
const processUploadQueue = async () => {
|
||||
const promises: Promise<void>[] = [];
|
||||
|
||||
// 同时处理最多 MAX_CONCURRENT 个分片
|
||||
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
|
||||
) {
|
||||
const chunkItem = uploadQueue.current.shift();
|
||||
if (chunkItem) {
|
||||
const { chunk, index } = chunkItem;
|
||||
const success = await uploadChunk(chunk, index);
|
||||
if (success) {
|
||||
completedChunks.current += 1;
|
||||
const progress = Math.round(
|
||||
(completedChunks.current / totalChunks.current) * 100,
|
||||
);
|
||||
setUploadProgress(progress);
|
||||
|
||||
// 所有分片上传完成
|
||||
if (completedChunks.current === totalChunks.current) {
|
||||
setIsUploading(false);
|
||||
setUploadStatus('success');
|
||||
setUploadMessage('文件上传成功!正在处理中,请稍后查看列表。');
|
||||
message.success('文件上传成功!系统正在处理,请稍后查看列表。');
|
||||
onImportSuccess?.();
|
||||
}
|
||||
} else {
|
||||
// 上传失败处理
|
||||
if (!abortController.current?.signal.aborted) {
|
||||
setIsUploading(false);
|
||||
setUploadStatus('error');
|
||||
setUploadMessage('文件上传失败,请重试');
|
||||
message.error('文件上传失败');
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
promises.push(processChunk());
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
};
|
||||
|
||||
// 2. 开始上传
|
||||
const startUpload = async (file: File) => {
|
||||
try {
|
||||
setIsUploading(true);
|
||||
setUploadStatus('uploading');
|
||||
setUploadMessage('正在准备上传...');
|
||||
setUploadProgress(0);
|
||||
|
||||
// 初始化状态
|
||||
completedChunks.current = 0;
|
||||
fileSize.current = file.size;
|
||||
fileName.current = file.name;
|
||||
fileId.current = uuidv4(); // 生成唯一文件ID
|
||||
totalChunks.current = Math.ceil(file.size / CHUNK_SIZE);
|
||||
uploadQueue.current = [];
|
||||
|
||||
setUploadMessage(`正在分析文件... 共 ${totalChunks.current} 个分片`);
|
||||
|
||||
// 创建分片并添加到队列
|
||||
for (let i = 0; i < totalChunks.current; i++) {
|
||||
const start = i * CHUNK_SIZE;
|
||||
const end = Math.min(start + CHUNK_SIZE, file.size);
|
||||
const chunk = file.slice(start, end);
|
||||
uploadQueue.current.push({ chunk, index: i });
|
||||
}
|
||||
|
||||
setUploadMessage(`开始上传文件,共 ${totalChunks.current} 个分片`);
|
||||
|
||||
// 创建新的AbortController
|
||||
abortController.current = new AbortController();
|
||||
|
||||
// 开始处理上传队列
|
||||
await processUploadQueue();
|
||||
} catch (error) {
|
||||
console.error('上传准备失败:', error);
|
||||
setIsUploading(false);
|
||||
setUploadStatus('error');
|
||||
setUploadMessage('上传准备失败,请重试');
|
||||
message.error('上传准备失败');
|
||||
}
|
||||
};
|
||||
|
||||
// 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 > 20 * 1024 * 1024 * 1024) {
|
||||
// 20GB
|
||||
message.error('文件大小不能超过20GB');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 开始上传流程
|
||||
startUpload(file);
|
||||
return false; // 阻止默认上传行为
|
||||
};
|
||||
|
||||
// 取消上传
|
||||
const cancelUpload = () => {
|
||||
if (abortController.current) {
|
||||
abortController.current.abort();
|
||||
}
|
||||
setIsUploading(false);
|
||||
setUploadStatus('ready');
|
||||
setUploadMessage('上传已取消');
|
||||
setUploadProgress(0);
|
||||
message.info('上传已取消');
|
||||
};
|
||||
|
||||
// 导入弹窗内容
|
||||
const importModalContent = (
|
||||
<div>
|
||||
<Alert
|
||||
message="重要提示"
|
||||
description={
|
||||
<div>
|
||||
<div>
|
||||
1.
|
||||
文件上传需要时间,请耐心等待。
|
||||
</div>
|
||||
<div>
|
||||
2. 文件上传中请勿刷新或者离开页面,否则会导致文件上传失败。
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
type="warning"
|
||||
showIcon
|
||||
style={{ marginBottom: 16 }}
|
||||
/>
|
||||
|
||||
<Dragger
|
||||
beforeUpload={handleFileUpload}
|
||||
disabled={isUploading}
|
||||
multiple={false}
|
||||
showUploadList={false}
|
||||
// accept=".tar.gz,.iso,.qcow2"
|
||||
>
|
||||
<p className="ant-upload-drag-icon">
|
||||
<div style={{ fontSize: 48, lineHeight: 1 }}>⇧</div>
|
||||
</p>
|
||||
<p className="ant-upload-text">点击选择文件或拖拽文件到此区域上传</p>
|
||||
<p className="ant-upload-hint">
|
||||
{/* 支持文件扩展名:.tar.gz, .iso, .qcow2,大小限制为20G */}
|
||||
大小限制为20G
|
||||
</p>
|
||||
</Dragger>
|
||||
|
||||
{(isUploading || uploadStatus !== 'ready') && (
|
||||
<div style={{ marginTop: 20 }}>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
marginBottom: 8,
|
||||
}}
|
||||
>
|
||||
<span>上传进度</span>
|
||||
<span>{uploadProgress}%</span>
|
||||
</div>
|
||||
<Progress
|
||||
percent={uploadProgress}
|
||||
status={uploadStatus === 'error' ? 'exception' : 'normal'}
|
||||
/>
|
||||
{uploadMessage && (
|
||||
<div style={{ marginTop: 8, textAlign: 'center' }}>
|
||||
<span>{uploadMessage}</span>
|
||||
</div>
|
||||
)}
|
||||
{/* {isUploading && (
|
||||
<div style={{ marginTop: 12, textAlign: 'center' }}>
|
||||
<Button onClick={cancelUpload}>取消上传</Button>
|
||||
</div>
|
||||
)} */}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="导入镜像"
|
||||
open={visible}
|
||||
onCancel={() => {
|
||||
if (isUploading) {
|
||||
cancelUpload();
|
||||
}
|
||||
onCancel();
|
||||
}}
|
||||
footer={[
|
||||
<Button
|
||||
key="close"
|
||||
onClick={() => {
|
||||
if (isUploading) {
|
||||
cancelUpload();
|
||||
}
|
||||
onCancel();
|
||||
}}
|
||||
>
|
||||
关闭
|
||||
</Button>,
|
||||
]}
|
||||
width={600}
|
||||
maskClosable={false}
|
||||
closable={!isUploading}
|
||||
>
|
||||
{importModalContent}
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export { ImportModal, ModalDetailShow };
|
|
@ -15,6 +15,7 @@
|
|||
|
||||
// 镜像列表样式
|
||||
.image-list {
|
||||
padding: 10px;
|
||||
.image-detail {
|
||||
.detail-item {
|
||||
margin-bottom: 16px;
|
||||
|
|
|
@ -1,151 +1,280 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { Card, Table, Tag, Button, Space, Modal, message, Popconfirm } from 'antd';
|
||||
import {
|
||||
PlusOutlined,
|
||||
EditOutlined,
|
||||
DeleteOutlined,
|
||||
EyeOutlined
|
||||
import { ERROR_CODE, IMAGES_TYPE_MAP } from '@/constants/images.constants';
|
||||
import { getImagesList } from '@/services/images';
|
||||
import {
|
||||
DeleteOutlined,
|
||||
EyeOutlined,
|
||||
SettingOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import type { TableProps } from 'antd';
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
Input,
|
||||
message,
|
||||
Modal,
|
||||
Popconfirm,
|
||||
Popover,
|
||||
Space,
|
||||
Table,
|
||||
Tooltip,
|
||||
} from 'antd';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { ModalDetailShow, ImportModal } from './components/modalShow/modalShow';
|
||||
import './index.less';
|
||||
|
||||
interface ImageItem {
|
||||
id: string;
|
||||
name: string;
|
||||
version: string;
|
||||
size: string;
|
||||
status: 'active' | 'inactive' | 'building';
|
||||
createTime: string;
|
||||
description: string;
|
||||
}
|
||||
import { ReactComponent as RefreshIcon } from '@/assets/icons/refresh.svg';
|
||||
|
||||
const ImageList: React.FC = () => {
|
||||
const [images, setImages] = useState<ImageItem[]>([]);
|
||||
const [images, setImages] = useState<IMAGES.ImageItem[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [selectedImage, setSelectedImage] = useState<ImageItem | null>(null);
|
||||
const [selectedImage, setSelectedImage] = useState<IMAGES.ImageItem | null>(
|
||||
null,
|
||||
);
|
||||
const [searchText, setSearchText] = useState<string>('');
|
||||
const [detailVisible, setDetailVisible] = useState(false);
|
||||
const [importModalVisible, setImportModalVisible] = useState(false);
|
||||
const [tableParams, setTableParams] = useState<IMAGES.TableParams>({
|
||||
pagination: {
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
},
|
||||
});
|
||||
|
||||
// 列显示控制状态
|
||||
const [visibleColumns, setVisibleColumns] = useState<Record<string, boolean>>(
|
||||
{
|
||||
image_type: true,
|
||||
storage_path: true,
|
||||
bt_path: true,
|
||||
create_time: true,
|
||||
action: true,
|
||||
},
|
||||
);
|
||||
const [columnSettingsVisible, setColumnSettingsVisible] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
loadImages();
|
||||
console.log('cyt==>测试push');
|
||||
|
||||
}, []);
|
||||
}, [
|
||||
tableParams.pagination?.current,
|
||||
tableParams.pagination?.pageSize,
|
||||
tableParams?.sortOrder,
|
||||
tableParams?.sortField,
|
||||
JSON.stringify(tableParams.filters),
|
||||
JSON.stringify(tableParams.keywords),
|
||||
]);
|
||||
const getRandomuserParams = (params: IMAGES.TableParams) => {
|
||||
const {
|
||||
pagination,
|
||||
filters,
|
||||
sortField,
|
||||
sortOrder,
|
||||
keywords,
|
||||
...restParams
|
||||
} = params;
|
||||
const result: Record<string, any> = {};
|
||||
|
||||
const loadImages = () => {
|
||||
setLoading(true);
|
||||
// 模拟数据加载
|
||||
setTimeout(() => {
|
||||
const mockData: ImageItem[] = [
|
||||
{
|
||||
id: '1',
|
||||
name: 'Windows 10 专业版',
|
||||
version: 'v1.0.0',
|
||||
size: '15.2 GB',
|
||||
status: 'active',
|
||||
createTime: '2024-01-15 10:30:00',
|
||||
description: 'Windows 10 专业版镜像,包含常用办公软件'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'Ubuntu 22.04 LTS',
|
||||
version: 'v2.1.0',
|
||||
size: '8.5 GB',
|
||||
status: 'active',
|
||||
createTime: '2024-01-10 14:20:00',
|
||||
description: 'Ubuntu 22.04 LTS 服务器版本,适用于开发环境'
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: 'CentOS 8',
|
||||
version: 'v1.5.0',
|
||||
size: '12.1 GB',
|
||||
status: 'building',
|
||||
createTime: '2024-01-20 09:15:00',
|
||||
description: 'CentOS 8 企业级服务器操作系统'
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
name: 'macOS Monterey',
|
||||
version: 'v1.2.0',
|
||||
size: '18.7 GB',
|
||||
status: 'inactive',
|
||||
createTime: '2024-01-05 16:45:00',
|
||||
description: 'macOS Monterey 开发环境镜像'
|
||||
result.page_size = pagination?.pageSize;
|
||||
result.page_num = pagination?.current;
|
||||
|
||||
if (keywords) {
|
||||
result.keywords = keywords;
|
||||
}
|
||||
|
||||
if (filters) {
|
||||
Object.entries(filters).forEach(([key, value]) => {
|
||||
if (value !== undefined && value !== null) {
|
||||
result[key] = value;
|
||||
}
|
||||
];
|
||||
setImages(mockData);
|
||||
});
|
||||
}
|
||||
|
||||
if (sortField) {
|
||||
result.orderby = sortField;
|
||||
result.order = sortOrder === 'ascend' ? 'asc' : 'desc';
|
||||
}
|
||||
|
||||
// 处理其他参数
|
||||
Object.entries(restParams).forEach(([key, value]) => {
|
||||
if (value !== undefined && value !== null) {
|
||||
result[key] = value;
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
};
|
||||
const params = getRandomuserParams(tableParams);
|
||||
|
||||
const loadImages = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const imagesRes = await getImagesList(params);
|
||||
if (imagesRes.error_code === ERROR_CODE) {
|
||||
setImages(imagesRes.data.data || []);
|
||||
setLoading(false);
|
||||
setTableParams((prev) => ({
|
||||
...prev,
|
||||
pagination: {
|
||||
...prev.pagination,
|
||||
total: imagesRes.data.paging.total || 0,
|
||||
},
|
||||
}));
|
||||
} else {
|
||||
message.error(imagesRes.message || '获取镜像列表失败');
|
||||
setLoading(false);
|
||||
}
|
||||
} catch (err) {
|
||||
message.error('获取镜像列表失败');
|
||||
setLoading(false);
|
||||
}, 1000);
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusTag = (status: string) => {
|
||||
const statusMap = {
|
||||
active: { color: 'green', text: '可用' },
|
||||
inactive: { color: 'red', text: '不可用' },
|
||||
building: { color: 'orange', text: '构建中' }
|
||||
};
|
||||
const config = statusMap[status as keyof typeof statusMap];
|
||||
return <Tag color={config.color}>{config.text}</Tag>;
|
||||
};
|
||||
// const getStatusTag = (status: string) => {
|
||||
// const statusMap = {
|
||||
// active: { color: 'green', text: '可用' },
|
||||
// inactive: { color: 'red', text: '不可用' },
|
||||
// building: { color: 'orange', text: '构建中' },
|
||||
// };
|
||||
// const config = statusMap[status as keyof typeof statusMap];
|
||||
// return <Tag color={config.color}>{config.text}</Tag>;
|
||||
// };
|
||||
|
||||
const handleViewDetail = (record: ImageItem) => {
|
||||
const handleViewDetail = (record: IMAGES.ImageItem) => {
|
||||
setSelectedImage(record);
|
||||
setDetailVisible(true);
|
||||
};
|
||||
|
||||
|
||||
|
||||
const handleEdit = (record: ImageItem) => {
|
||||
message.info(`编辑镜像:${record.name}`);
|
||||
};
|
||||
|
||||
const handleDelete = (record: ImageItem) => {
|
||||
const handleDelete = (record: IMAGES.ImageItem) => {
|
||||
Modal.confirm({
|
||||
title: '确认删除',
|
||||
content: `确定要删除镜像 "${record.name}" 吗?`,
|
||||
content: `确定要删除镜像 "${record.image_name}" 吗?`,
|
||||
onOk: () => {
|
||||
setImages(images.filter(img => img.id !== record.id));
|
||||
// TODO: 调用删除接口
|
||||
setImages(images.filter((img) => img.image_id !== record.image_id));
|
||||
message.success('删除成功');
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const columns = [
|
||||
// 列设置相关函数
|
||||
const handleColumnChange = (columnKey: string, checked: boolean) => {
|
||||
setVisibleColumns((prev) => ({
|
||||
...prev,
|
||||
[columnKey]: checked,
|
||||
}));
|
||||
};
|
||||
|
||||
const resetColumns = () => {
|
||||
setVisibleColumns({
|
||||
image_type: true,
|
||||
storage_path: true,
|
||||
bt_path: true,
|
||||
create_time: true,
|
||||
action: true,
|
||||
});
|
||||
};
|
||||
|
||||
// 列设置内容
|
||||
const columnSettingsContent = (
|
||||
<div style={{ padding: '8px 0' }}>
|
||||
<div style={{ padding: '4px 12px' }}>
|
||||
<Checkbox
|
||||
checked={visibleColumns['image_type']}
|
||||
onChange={(e) => handleColumnChange('image_type', e.target.checked)}
|
||||
>
|
||||
桌面类型
|
||||
</Checkbox>
|
||||
</div>
|
||||
<div style={{ padding: '4px 12px' }}>
|
||||
<Checkbox
|
||||
checked={visibleColumns['storage_path']}
|
||||
onChange={(e) => handleColumnChange('storage_path', e.target.checked)}
|
||||
>
|
||||
模板存放路径
|
||||
</Checkbox>
|
||||
</div>
|
||||
<div style={{ padding: '4px 12px' }}>
|
||||
<Checkbox
|
||||
checked={visibleColumns['bt_path']}
|
||||
onChange={(e) => handleColumnChange('bt_path', e.target.checked)}
|
||||
>
|
||||
BT路径
|
||||
</Checkbox>
|
||||
</div>
|
||||
<div style={{ padding: '4px 12px' }}>
|
||||
<Checkbox
|
||||
checked={visibleColumns['create_time']}
|
||||
onChange={(e) => handleColumnChange('create_time', e.target.checked)}
|
||||
>
|
||||
创建时间
|
||||
</Checkbox>
|
||||
</div>
|
||||
<div style={{ padding: '4px 12px' }}>
|
||||
<Checkbox
|
||||
checked={visibleColumns['action']}
|
||||
onChange={(e) => handleColumnChange('action', e.target.checked)}
|
||||
>
|
||||
操作
|
||||
</Checkbox>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
padding: '8px 12px',
|
||||
borderTop: '1px solid #f0f0f0',
|
||||
marginTop: 8,
|
||||
}}
|
||||
>
|
||||
<Button type="link" onClick={resetColumns} style={{ padding: 0 }}>
|
||||
重置
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
// 根据visibleColumns过滤显示的列
|
||||
const filteredColumns = [
|
||||
{
|
||||
title: '镜像名称',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
dataIndex: 'image_name',
|
||||
key: 'image_name',
|
||||
width: 200,
|
||||
},
|
||||
{
|
||||
title: '版本',
|
||||
dataIndex: 'version',
|
||||
key: 'version',
|
||||
width: 100,
|
||||
title: '桌面类型',
|
||||
dataIndex: 'image_type',
|
||||
key: 'image_type',
|
||||
width: 200,
|
||||
render: (text: number) => {
|
||||
const key = text as keyof typeof IMAGES_TYPE_MAP;
|
||||
return <Tooltip>{IMAGES_TYPE_MAP[key] || '--'}</Tooltip>;
|
||||
},
|
||||
...(visibleColumns['image_type'] ? {} : { hidden: true }),
|
||||
},
|
||||
{
|
||||
title: '大小',
|
||||
dataIndex: 'size',
|
||||
key: 'size',
|
||||
width: 100,
|
||||
title: '模板存放路径',
|
||||
dataIndex: 'storage_path',
|
||||
key: 'storage_path',
|
||||
width: 200,
|
||||
...(visibleColumns['storage_path'] ? {} : { hidden: true }),
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
width: 100,
|
||||
render: (status: string) => getStatusTag(status),
|
||||
title: 'BT路径',
|
||||
dataIndex: 'bt_path',
|
||||
key: 'bt_path',
|
||||
width: 200,
|
||||
...(visibleColumns['bt_path'] ? {} : { hidden: true }),
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'createTime',
|
||||
key: 'createTime',
|
||||
dataIndex: 'create_time',
|
||||
key: 'create_time',
|
||||
width: 180,
|
||||
...(visibleColumns['create_time'] ? {} : { hidden: true }),
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 200,
|
||||
render: (_: any, record: ImageItem) => (
|
||||
render: (_: any, record: IMAGES.ImageItem) => (
|
||||
<Space size="small">
|
||||
<Button
|
||||
type="text"
|
||||
|
@ -160,78 +289,127 @@ const ImageList: React.FC = () => {
|
|||
okText="确定"
|
||||
cancelText="取消"
|
||||
>
|
||||
<Button
|
||||
type="text"
|
||||
icon={<DeleteOutlined />}
|
||||
title="删除"
|
||||
danger
|
||||
/>
|
||||
<Button type="text" icon={<DeleteOutlined />} title="删除" danger />
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
),
|
||||
...(visibleColumns['action'] ? {} : { hidden: true }),
|
||||
},
|
||||
];
|
||||
].filter((column) => !column.hidden);
|
||||
|
||||
const handleTableChange: TableProps<IMAGES.ImageItem>['onChange'] = (
|
||||
pagination,
|
||||
filters,
|
||||
sorter,
|
||||
) => {
|
||||
setTableParams({
|
||||
pagination: {
|
||||
current: pagination.current || 1,
|
||||
pageSize: pagination.pageSize || 10,
|
||||
},
|
||||
filters,
|
||||
sortOrder: Array.isArray(sorter) ? undefined : sorter.order,
|
||||
sortField: Array.isArray(sorter)
|
||||
? undefined
|
||||
: (sorter.field as string | number | undefined),
|
||||
});
|
||||
|
||||
if (pagination.pageSize !== tableParams.pagination?.pageSize) {
|
||||
setImages([]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRefresh = () => {
|
||||
loadImages();
|
||||
};
|
||||
|
||||
// 导入成功后的回调
|
||||
const handleImportSuccess = () => {
|
||||
// 可以在这里添加导入成功后的处理逻辑
|
||||
// 例如:刷新列表或显示提示信息
|
||||
setTimeout(() => {
|
||||
loadImages(); // 一段时间后刷新列表查看是否有新导入的镜像
|
||||
}, 5000);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="image-list">
|
||||
<Card>
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={images}
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
pagination={{
|
||||
total: images.length,
|
||||
pageSize: 10,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (total) => `共 ${total} 条记录`,
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Modal
|
||||
title="镜像详情"
|
||||
open={detailVisible}
|
||||
onCancel={() => setDetailVisible(false)}
|
||||
footer={[
|
||||
<Button key="close" onClick={() => setDetailVisible(false)}>
|
||||
关闭
|
||||
</Button>
|
||||
]}
|
||||
width={600}
|
||||
<div
|
||||
style={{
|
||||
marginBottom: 16,
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
}}
|
||||
>
|
||||
{selectedImage && (
|
||||
<div className="image-detail">
|
||||
<div className="detail-item">
|
||||
<label>镜像名称:</label>
|
||||
<span>{selectedImage.name}</span>
|
||||
</div>
|
||||
<div className="detail-item">
|
||||
<label>版本:</label>
|
||||
<span>{selectedImage.version}</span>
|
||||
</div>
|
||||
<div className="detail-item">
|
||||
<label>大小:</label>
|
||||
<span>{selectedImage.size}</span>
|
||||
</div>
|
||||
<div className="detail-item">
|
||||
<label>状态:</label>
|
||||
<span>{getStatusTag(selectedImage.status)}</span>
|
||||
</div>
|
||||
<div className="detail-item">
|
||||
<label>创建时间:</label>
|
||||
<span>{selectedImage.createTime}</span>
|
||||
</div>
|
||||
<div className="detail-item">
|
||||
<label>描述:</label>
|
||||
<p>{selectedImage.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
<div>
|
||||
<Button onClick={() => setImportModalVisible(true)}>导入</Button>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
|
||||
<Input.Search
|
||||
placeholder="镜像名称"
|
||||
value={searchText}
|
||||
onChange={(e) => setSearchText(e.target.value)}
|
||||
style={{ width: 300 }}
|
||||
onSearch={(value) => {
|
||||
setTableParams((prev) => ({
|
||||
...prev,
|
||||
pagination: {
|
||||
...prev.pagination,
|
||||
current: 1,
|
||||
},
|
||||
keywords: value,
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
onClick={handleRefresh}
|
||||
loading={loading}
|
||||
icon={<RefreshIcon style={{ width: 13, height: 13 }} />}
|
||||
></Button>
|
||||
<Popover
|
||||
content={columnSettingsContent}
|
||||
title="列设置"
|
||||
trigger="click"
|
||||
open={columnSettingsVisible}
|
||||
onOpenChange={setColumnSettingsVisible}
|
||||
placement="bottomRight"
|
||||
>
|
||||
<Button icon={<SettingOutlined />}></Button>
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
<Table
|
||||
columns={filteredColumns}
|
||||
dataSource={images}
|
||||
rowKey="image_id"
|
||||
loading={loading}
|
||||
pagination={tableParams.pagination}
|
||||
onChange={handleTableChange}
|
||||
// pagination={{
|
||||
// total: images.length,
|
||||
// pageSize: 10,
|
||||
// showSizeChanger: true,
|
||||
// showQuickJumper: true,
|
||||
// showTotal: (total) => `共 ${total} 条记录`,
|
||||
// }}
|
||||
/>
|
||||
|
||||
{detailVisible ? (
|
||||
<ModalDetailShow
|
||||
title="镜像详情"
|
||||
detailVisible={detailVisible}
|
||||
setDetailVisible={setDetailVisible}
|
||||
selectedImage={selectedImage}
|
||||
/>
|
||||
) : null}
|
||||
{/* 导入弹窗 */}
|
||||
<ImportModal
|
||||
visible={importModalVisible}
|
||||
onCancel={() => setImportModalVisible(false)}
|
||||
onImportSuccess={handleImportSuccess}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ImageList;
|
||||
export default ImageList;
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
import { request } from '@umijs/max';
|
||||
|
||||
const BASE_URL = '/api/v1/images';
|
||||
|
||||
// 根据终端序列号查询镜像列表
|
||||
export async function getImagesList(params:any) {
|
||||
// console.log('镜像列表 params', params);
|
||||
return request<IMAGES.Images_ListInfo>(`${BASE_URL}/queryimagesList`, {
|
||||
method: 'POST',
|
||||
// headers: {
|
||||
// 'Content-Type': 'application/json',
|
||||
// },
|
||||
data: params,
|
||||
});
|
||||
}
|
||||
|
||||
export async function uploadChunkAPI(formData: FormData, signal?: AbortSignal) {
|
||||
return request<any>(`${BASE_URL}/file/chunk/upload`, {
|
||||
method: 'POST',
|
||||
data: formData,
|
||||
signal, // 添加 signal 支持
|
||||
});
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
|
||||
declare namespace IMAGES {
|
||||
interface ImageItem {
|
||||
image_id: number;
|
||||
image_name: string;
|
||||
image_type: number;
|
||||
storage_path: string;
|
||||
bt_path: string;
|
||||
create_time: string;
|
||||
description?: string;
|
||||
desktopType?: string;
|
||||
version?: string;
|
||||
size?: string;
|
||||
status?: 'active' | 'inactive' | 'building';
|
||||
}
|
||||
|
||||
interface TableParams {
|
||||
pagination: {
|
||||
current: number;
|
||||
pageSize: number;
|
||||
};
|
||||
filters?: Record<string, any>;
|
||||
sortOrder?: 'ascend' | 'descend' | null;
|
||||
sortField?: string | number | null;
|
||||
keywords?: string; // 添加关键词字段
|
||||
}
|
||||
|
||||
// interface DeviceParams{
|
||||
// device_id: string;
|
||||
// }
|
||||
|
||||
interface Images_ListInfo {
|
||||
error_code: string;
|
||||
message: string;
|
||||
data: {
|
||||
paging: {
|
||||
total: number;
|
||||
total_num?: number;
|
||||
page_num: number;
|
||||
page_size: number;
|
||||
};
|
||||
data: ImageItem[];
|
||||
};
|
||||
}
|
||||
|
||||
}
|
|
@ -1896,6 +1896,11 @@
|
|||
resolved "https://registry.npmmirror.com/@types/semver/-/semver-7.7.0.tgz#64c441bdae033b378b6eef7d0c3d77c329b9378e"
|
||||
integrity sha512-k107IF4+Xr7UHjwDc7Cfd6PRQfbdkiRabXGRjo07b4WyPahFBZCZ1sE+BNxYIJPPg73UkfOsVOLwqVc/6ETrIA==
|
||||
|
||||
"@types/spark-md5@^3.0.5":
|
||||
version "3.0.5"
|
||||
resolved "http://10.208.10.36:8080/repository/npm/@types/spark-md5/-/spark-md5-3.0.5.tgz#eddec8639217e518c26e9e221ff56bf5f5f5c900"
|
||||
integrity sha512-lWf05dnD42DLVKQJZrDHtWFidcLrHuip01CtnC2/S6AMhX4t9ZlEUj4iuRlAnts0PQk7KESOqKxeGE/b6sIPGg==
|
||||
|
||||
"@types/stylis@^4.0.2":
|
||||
version "4.2.7"
|
||||
resolved "https://registry.npmmirror.com/@types/stylis/-/stylis-4.2.7.tgz#1813190525da9d2a2b6976583bdd4af5301d9fd4"
|
||||
|
@ -9271,6 +9276,11 @@ source-map@^0.7.3, source-map@^0.7.4:
|
|||
resolved "https://registry.npmmirror.com/source-map/-/source-map-0.7.6.tgz#a3658ab87e5b6429c8a1f3ba0083d4c61ca3ef02"
|
||||
integrity sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==
|
||||
|
||||
spark-md5@^3.0.2:
|
||||
version "3.0.2"
|
||||
resolved "http://10.208.10.36:8080/repository/npm/spark-md5/-/spark-md5-3.0.2.tgz#7952c4a30784347abcee73268e473b9c0167e3fc"
|
||||
integrity sha512-wcFzz9cDfbuqe0FZzfi2or1sgyIrsDwmPwfZC4hiNidPdPINjeUwNfv5kldczoEAcjl9Y1L3SM7Uz2PUEQzxQw==
|
||||
|
||||
spdx-correct@^3.0.0:
|
||||
version "3.2.0"
|
||||
resolved "https://registry.npmmirror.com/spdx-correct/-/spdx-correct-3.2.0.tgz#4f5ab0668f0059e34f9c00dce331784a12de4e9c"
|
||||
|
@ -9402,16 +9412,7 @@ string-convert@^0.2.0:
|
|||
resolved "https://registry.npmmirror.com/string-convert/-/string-convert-0.2.1.tgz#6982cc3049fbb4cd85f8b24568b9d9bf39eeff97"
|
||||
integrity sha512-u/1tdPl4yQnPBjnVrmdLo9gtuLvELKsAoRapekWggdiQNvvvum+jYF329d84NAa660KQw7pB2n36KrIKVoXa3A==
|
||||
|
||||
"string-width-cjs@npm:string-width@^4.2.0":
|
||||
version "4.2.3"
|
||||
resolved "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
||||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
||||
dependencies:
|
||||
emoji-regex "^8.0.0"
|
||||
is-fullwidth-code-point "^3.0.0"
|
||||
strip-ansi "^6.0.1"
|
||||
|
||||
string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
|
||||
"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
|
||||
version "4.2.3"
|
||||
resolved "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
||||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
||||
|
@ -9501,14 +9502,7 @@ string_decoder@~1.1.1:
|
|||
dependencies:
|
||||
safe-buffer "~5.1.0"
|
||||
|
||||
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
|
||||
version "6.0.1"
|
||||
resolved "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
||||
dependencies:
|
||||
ansi-regex "^5.0.1"
|
||||
|
||||
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
||||
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
||||
version "6.0.1"
|
||||
resolved "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
||||
|
@ -10106,6 +10100,11 @@ utils-merge@1.0.1:
|
|||
resolved "https://registry.npmmirror.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
|
||||
integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==
|
||||
|
||||
uuid@^11.1.0:
|
||||
version "11.1.0"
|
||||
resolved "http://10.208.10.36:8080/repository/npm/uuid/-/uuid-11.1.0.tgz#9549028be1753bb934fc96e2bca09bb4105ae912"
|
||||
integrity sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==
|
||||
|
||||
v8-compile-cache@^2.3.0:
|
||||
version "2.4.0"
|
||||
resolved "https://registry.npmmirror.com/v8-compile-cache/-/v8-compile-cache-2.4.0.tgz#cdada8bec61e15865f05d097c5f4fd30e94dc128"
|
||||
|
@ -10261,16 +10260,7 @@ word-wrap@^1.2.5:
|
|||
resolved "https://registry.npmmirror.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34"
|
||||
integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==
|
||||
|
||||
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
|
||||
version "7.0.0"
|
||||
resolved "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
|
||||
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
||||
dependencies:
|
||||
ansi-styles "^4.0.0"
|
||||
string-width "^4.1.0"
|
||||
strip-ansi "^6.0.0"
|
||||
|
||||
wrap-ansi@^7.0.0:
|
||||
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
|
||||
version "7.0.0"
|
||||
resolved "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
|
||||
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
||||
|
|
Loading…
Reference in New Issue