diff --git a/web-fe/mock/images.ts b/web-fe/mock/images.ts
new file mode 100644
index 0000000..6698b2e
--- /dev/null
+++ b/web-fe/mock/images.ts
@@ -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,
+ },
+ });
+ }
+ },
+};
diff --git a/web-fe/package.json b/web-fe/package.json
index 51ce9f5..6829372 100644
--- a/web-fe/package.json
+++ b/web-fe/package.json
@@ -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",
diff --git a/web-fe/src/assets/icons/refresh.svg b/web-fe/src/assets/icons/refresh.svg
new file mode 100644
index 0000000..ffbfbc8
--- /dev/null
+++ b/web-fe/src/assets/icons/refresh.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/web-fe/src/constants/images.constants.ts b/web-fe/src/constants/images.constants.ts
new file mode 100644
index 0000000..9654a10
--- /dev/null
+++ b/web-fe/src/constants/images.constants.ts
@@ -0,0 +1,7 @@
+export const ERROR_CODE = '0000000000';
+
+export const IMAGES_TYPE_MAP = {
+ 1: 'VHD',
+ 2: 'VHDX',
+ 3: 'QCOW2',
+} as const;
\ No newline at end of file
diff --git a/web-fe/src/pages/images/components/modalShow/modalShow.tsx b/web-fe/src/pages/images/components/modalShow/modalShow.tsx
new file mode 100644
index 0000000..0ddbb3f
--- /dev/null
+++ b/web-fe/src/pages/images/components/modalShow/modalShow.tsx
@@ -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 (
+ setDetailVisible(false)}
+ footer={[
+ ,
+ ]}
+ width={600}
+ >
+ {selectedImage && (
+
+
+
+ {selectedImage.image_name}
+
+
+
+
+ {IMAGES_TYPE_MAP[
+ selectedImage.image_type as keyof typeof IMAGES_TYPE_MAP
+ ] || '--'}
+
+
+
+
+ {selectedImage.storage_path}
+
+
+
+ {selectedImage.bt_path}
+
+ {/*
+
+ {getStatusTag(selectedImage.status)}
+
*/}
+
+
+ {selectedImage.create_time}
+
+ {/*
+
+
{selectedImage.description}
+
*/}
+
+ )}
+
+ );
+};
+
+/**上传文件分片 */
+interface ImportModalProps {
+ visible: boolean;
+ onCancel: () => void;
+ onImportSuccess?: () => void;
+}
+
+const ImportModal: React.FC = ({
+ 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>([]); // 上传队列
+ const completedChunks = useRef(0); // 已上传的分片数量
+ const totalChunks = useRef(0); // 总分片数量
+ const fileId = useRef(''); // 文件id
+ const fileName = useRef(''); // 文件名
+ const fileSize = useRef(0); // 文件大小
+ const abortController = useRef(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 => {
+ 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 => {
+ 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[] = [];
+
+ // 同时处理最多 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 = (
+
+
+
+ 1.
+ 文件上传需要时间,请耐心等待。
+
+
+ 2. 文件上传中请勿刷新或者离开页面,否则会导致文件上传失败。
+
+
+ }
+ type="warning"
+ showIcon
+ style={{ marginBottom: 16 }}
+ />
+
+
+
+
⇧
+
+ 点击选择文件或拖拽文件到此区域上传
+
+ {/* 支持文件扩展名:.tar.gz, .iso, .qcow2,大小限制为20G */}
+ 大小限制为20G
+
+
+
+ {(isUploading || uploadStatus !== 'ready') && (
+
+
+ 上传进度
+ {uploadProgress}%
+
+
+ {uploadMessage && (
+
+ {uploadMessage}
+
+ )}
+ {/* {isUploading && (
+
+
+
+ )} */}
+
+ )}
+
+ );
+
+ return (
+ {
+ if (isUploading) {
+ cancelUpload();
+ }
+ onCancel();
+ }}
+ footer={[
+ ,
+ ]}
+ width={600}
+ maskClosable={false}
+ closable={!isUploading}
+ >
+ {importModalContent}
+
+ );
+};
+
+export { ImportModal, ModalDetailShow };
diff --git a/web-fe/src/pages/images/index.less b/web-fe/src/pages/images/index.less
index 5ef9d15..54cdc1b 100644
--- a/web-fe/src/pages/images/index.less
+++ b/web-fe/src/pages/images/index.less
@@ -15,6 +15,7 @@
// 镜像列表样式
.image-list {
+ padding: 10px;
.image-detail {
.detail-item {
margin-bottom: 16px;
diff --git a/web-fe/src/pages/images/index.tsx b/web-fe/src/pages/images/index.tsx
index d7cbc43..0c81cba 100644
--- a/web-fe/src/pages/images/index.tsx
+++ b/web-fe/src/pages/images/index.tsx
@@ -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([]);
+ const [images, setImages] = useState([]);
const [loading, setLoading] = useState(false);
- const [selectedImage, setSelectedImage] = useState(null);
+ const [selectedImage, setSelectedImage] = useState(
+ null,
+ );
+ const [searchText, setSearchText] = useState('');
const [detailVisible, setDetailVisible] = useState(false);
+ const [importModalVisible, setImportModalVisible] = useState(false);
+ const [tableParams, setTableParams] = useState({
+ pagination: {
+ current: 1,
+ pageSize: 10,
+ },
+ });
+
+ // 列显示控制状态
+ const [visibleColumns, setVisibleColumns] = useState>(
+ {
+ 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 = {};
- 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 {config.text};
- };
+ // 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 {config.text};
+ // };
- 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 = (
+
+
+ handleColumnChange('image_type', e.target.checked)}
+ >
+ 桌面类型
+
+
+
+ handleColumnChange('storage_path', e.target.checked)}
+ >
+ 模板存放路径
+
+
+
+ handleColumnChange('bt_path', e.target.checked)}
+ >
+ BT路径
+
+
+
+ handleColumnChange('create_time', e.target.checked)}
+ >
+ 创建时间
+
+
+
+ handleColumnChange('action', e.target.checked)}
+ >
+ 操作
+
+
+
+
+
+
+ );
+
+ // 根据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 {IMAGES_TYPE_MAP[key] || '--'};
+ },
+ ...(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) => (
),
+ ...(visibleColumns['action'] ? {} : { hidden: true }),
},
- ];
+ ].filter((column) => !column.hidden);
+
+ const handleTableChange: TableProps['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 (
-
- `共 ${total} 条记录`,
- }}
- />
-
-
- setDetailVisible(false)}
- footer={[
- setDetailVisible(false)}>
- 关闭
-
- ]}
- width={600}
+
- {selectedImage && (
-
-
-
- {selectedImage.name}
-
-
-
- {selectedImage.version}
-
-
-
- {selectedImage.size}
-
-
-
- {getStatusTag(selectedImage.status)}
-
-
-
- {selectedImage.createTime}
-
-
-
-
{selectedImage.description}
-
-
- )}
-
+
+ setImportModalVisible(true)}>导入
+
+
+
setSearchText(e.target.value)}
+ style={{ width: 300 }}
+ onSearch={(value) => {
+ setTableParams((prev) => ({
+ ...prev,
+ pagination: {
+ ...prev.pagination,
+ current: 1,
+ },
+ keywords: value,
+ }));
+ }}
+ />
+ }
+ >
+
+ }>
+
+
+
+ `共 ${total} 条记录`,
+ // }}
+ />
+
+ {detailVisible ? (
+
+ ) : null}
+ {/* 导入弹窗 */}
+ setImportModalVisible(false)}
+ onImportSuccess={handleImportSuccess}
+ />
);
};
-export default ImageList;
\ No newline at end of file
+export default ImageList;
diff --git a/web-fe/src/services/images.ts b/web-fe/src/services/images.ts
new file mode 100644
index 0000000..f3dfbed
--- /dev/null
+++ b/web-fe/src/services/images.ts
@@ -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(`${BASE_URL}/queryimagesList`, {
+ method: 'POST',
+ // headers: {
+ // 'Content-Type': 'application/json',
+ // },
+ data: params,
+ });
+}
+
+export async function uploadChunkAPI(formData: FormData, signal?: AbortSignal) {
+ return request(`${BASE_URL}/file/chunk/upload`, {
+ method: 'POST',
+ data: formData,
+ signal, // 添加 signal 支持
+ });
+}
\ No newline at end of file
diff --git a/web-fe/src/types/images.d.ts b/web-fe/src/types/images.d.ts
new file mode 100644
index 0000000..dbe77d3
--- /dev/null
+++ b/web-fe/src/types/images.d.ts
@@ -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;
+ 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[];
+ };
+ }
+
+}
diff --git a/web-fe/yarn.lock b/web-fe/yarn.lock
index 50bf7d8..acf0dc9 100644
--- a/web-fe/yarn.lock
+++ b/web-fe/yarn.lock
@@ -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==