fix(镜像管理): 国际化配置+layout布局适应+修改逻辑+样式

master
chenyt 2025-08-07 11:28:17 +08:00
parent c6361112fa
commit 4348cfffea
7 changed files with 289 additions and 173 deletions

View File

@ -8,6 +8,11 @@ export default defineConfig({
request: {},
layout: false,
outputPath: 'serve/dist',
locale: {
default: 'zh-CN',
antd: true, // 启用 antd 国际化
baseNavigator: true,
},
// 路由配置
routes: [
{
@ -31,9 +36,9 @@ export default defineConfig({
component: '@/pages/userList',
},
{
path:"/terminal",
component: '@/pages/terminal',
}
path: '/terminal',
component: '@/pages/terminal',
},
],
},
],

View File

@ -14,6 +14,7 @@
"@ant-design/pro-components": "^2.4.4",
"@umijs/max": "^4.4.11",
"antd": "^5.4.0",
"dayjs": "^1.11.13",
"spark-md5": "^3.0.2",
"uuid": "^11.1.0"
},

View File

@ -117,7 +117,10 @@ const MainLayout: React.FC = () => {
</div>
</Header>
<Content className="main-content">
<Content
className="main-content"
style={{ height: 'calc(100vh - 64px)', overflow: 'auto' }}
>
<Outlet />
</Content>
</Layout>

View File

@ -2,7 +2,7 @@ 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 React, { useRef, useState, useEffect } from 'react';
import SparkMD5 from 'spark-md5';
import { v4 as uuidv4 } from 'uuid';
@ -92,6 +92,33 @@ const ImportModal: React.FC<ImportModalProps> = ({
const fileSize = useRef<number>(0); // 文件大小
const abortController = useRef<AbortController | null>(null); // 用于取消上传
// 添加重置状态函数
const resetState = () => {
setUploadProgress(0);
setIsUploading(false);
setUploadStatus('ready');
setUploadMessage('');
completedChunks.current = 0;
totalChunks.current = 0;
fileId.current = '';
fileName.current = '';
fileSize.current = 0;
uploadQueue.current = [];
// 如果有正在进行的上传,先中止
if (abortController.current) {
abortController.current.abort();
abortController.current = null;
}
};
// 当弹窗关闭时重置状态
useEffect(() => {
if (!visible) {
resetState();
}
}, [visible]);
// 5. 分块计算文件MD5
const calculateMD5InChunks = (
file: Blob,
@ -158,8 +185,6 @@ const ImportModal: React.FC<ImportModalProps> = ({
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) =>
@ -186,6 +211,7 @@ const ImportModal: React.FC<ImportModalProps> = ({
// 3. 处理上传队列:每次处理最多 MAX_CONCURRENT 个分片
const processUploadQueue = async () => {
const promises: Promise<void>[] = [];
let hasError = false; // 添加错误标记
// 同时处理最多 MAX_CONCURRENT 个分片
for (
@ -196,34 +222,49 @@ const ImportModal: React.FC<ImportModalProps> = ({
const processChunk = async () => {
while (
uploadQueue.current.length > 0 &&
!abortController.current?.signal.aborted
!abortController.current?.signal.aborted &&
!hasError // 添加错误检查,出错时停止处理新分片
) {
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?.();
if (success) {
// 只有在没有错误的情况下才更新进度
if (!hasError) {
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) {
if (!abortController.current?.signal.aborted && !hasError) {
hasError = true; // 设置错误标记
setIsUploading(false);
setUploadStatus('error');
setUploadMessage('文件上传失败,请重试');
message.error('文件上传失败');
// 中止其他正在进行的上传
if (abortController.current) {
abortController.current.abort();
}
}
return;
}
@ -253,7 +294,8 @@ const ImportModal: React.FC<ImportModalProps> = ({
totalChunks.current = Math.ceil(file.size / CHUNK_SIZE);
uploadQueue.current = [];
setUploadMessage(`正在分析文件... 共 ${totalChunks.current} 个分片`);
setUploadMessage(`正在分析文件... `);
// setUploadMessage(`正在分析文件... 共 ${totalChunks.current} 个分片`);
// 创建分片并添加到队列
for (let i = 0; i < totalChunks.current; i++) {
@ -263,7 +305,8 @@ const ImportModal: React.FC<ImportModalProps> = ({
uploadQueue.current.push({ chunk, index: i });
}
setUploadMessage(`开始上传文件,共 ${totalChunks.current} 个分片`);
setUploadMessage(`开始上传文件... `);
// setUploadMessage(`开始上传文件,共 ${totalChunks.current} 个分片`);
// 创建新的AbortController
abortController.current = new AbortController();
@ -328,10 +371,7 @@ const ImportModal: React.FC<ImportModalProps> = ({
message="重要提示"
description={
<div>
<div>
1.
</div>
<div>1. </div>
<div>
2.
</div>
@ -368,23 +408,32 @@ const ImportModal: React.FC<ImportModalProps> = ({
marginBottom: 8,
}}
>
<span></span>
<span>{uploadProgress}%</span>
<span style={{ width: '70px' }}></span>
<div style={{ flex: 1, marginRight: 10 }}>
<Progress
percent={uploadProgress}
status={
uploadStatus === 'error'
? 'exception'
: uploadStatus === 'success'
? 'success'
: 'normal'
}
format={(percent) => `${percent}%`}
/>
</div>
</div>
<Progress
percent={uploadProgress}
status={uploadStatus === 'error' ? 'exception' : 'normal'}
/>
{uploadMessage && (
<div style={{ marginTop: 8, textAlign: 'center' }}>
<span>{uploadMessage}</span>
</div>
)}
{/* {isUploading && (
{isUploading && (
<div style={{ marginTop: 12, textAlign: 'center' }}>
<Button onClick={cancelUpload}></Button>
</div>
)} */}
)}
</div>
)}
</div>
@ -397,6 +446,9 @@ const ImportModal: React.FC<ImportModalProps> = ({
onCancel={() => {
if (isUploading) {
cancelUpload();
} else {
// 如果不是上传状态,直接重置状态
resetState();
}
onCancel();
}}
@ -406,6 +458,8 @@ const ImportModal: React.FC<ImportModalProps> = ({
onClick={() => {
if (isUploading) {
cancelUpload();
} else {
resetState();
}
onCancel();
}}

View File

@ -1,94 +1,163 @@
// 页面头部样式
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
h2 {
margin: 0;
font-size: 24px;
font-weight: 600;
color: #333;
}
}
// 镜像列表样式
.image-list {
height: 100%;
display: flex;
flex-direction: column;
padding: 16px;
box-sizing: border-box;
.search-box {
margin-bottom: 16px;
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
h2 {
margin: 0;
font-size: 24px;
font-weight: 600;
color: #333;
.search-input {
display: flex;
gap: 8px;
align-items: center;
}
}
// 镜像列表样式
.image-list {
padding: 10px;
.image-detail {
.detail-item {
margin-bottom: 16px;
label {
.images-list-container {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
.images-list-table {
display: flex;
flex: 1;
overflow: hidden;
}
}
// 表格适应样式
.ant-table-wrapper {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
.ant-spin-nested-loading {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
.ant-spin-container {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
.ant-table {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
.ant-table-container {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
.ant-table-body {
flex: 1;
overflow-y: auto !important;
table {
height: 100%;
}
}
}
}
}
}
}
.image-detail {
.detail-item {
margin-bottom: 16px;
label {
font-weight: 600;
color: #333;
display: inline-block;
width: 100px;
}
span {
color: #666;
}
p {
margin: 8px 0 0 100px;
color: #666;
line-height: 1.6;
}
}
}
}
// 个人资料样式
.profile-page {
.profile-content {
.profile-header {
display: flex;
align-items: center;
gap: 16px;
margin-bottom: 16px;
.profile-info {
h3 {
margin: 0 0 4px 0;
font-size: 20px;
font-weight: 600;
color: #333;
display: inline-block;
width: 100px;
}
span {
color: #666;
}
p {
margin: 8px 0 0 100px;
margin: 0;
color: #666;
line-height: 1.6;
font-size: 14px;
}
}
}
}
// 个人资料样式
.profile-page {
.profile-content {
.profile-header {
display: flex;
align-items: center;
gap: 16px;
margin-bottom: 16px;
.profile-info {
h3 {
margin: 0 0 4px 0;
font-size: 20px;
font-weight: 600;
color: #333;
}
p {
margin: 0;
color: #666;
font-size: 14px;
}
}
}
.quick-actions {
display: flex;
gap: 12px;
flex-wrap: wrap;
}
}
}
// 响应式设计
@media (max-width: 768px) {
.page-header {
flex-direction: column;
align-items: flex-start;
.quick-actions {
display: flex;
gap: 12px;
flex-wrap: wrap;
}
.profile-content {
.profile-header {
flex-direction: column;
text-align: center;
}
.quick-actions {
justify-content: center;
}
}
}
// 响应式设计
@media (max-width: 768px) {
.page-header {
flex-direction: column;
align-items: flex-start;
gap: 12px;
}
.profile-content {
.profile-header {
flex-direction: column;
text-align: center;
}
}
.quick-actions {
justify-content: center;
}
}
}

View File

@ -6,6 +6,7 @@ import {
SettingOutlined,
} from '@ant-design/icons';
import type { TableProps } from 'antd';
import dayjs from 'dayjs';
import {
Button,
Checkbox,
@ -19,7 +20,7 @@ import {
Tooltip,
} from 'antd';
import React, { useEffect, useState } from 'react';
import { ModalDetailShow, ImportModal } from './components/modalShow/modalShow';
import { ImportModal, ModalDetailShow } from './components/modalShow/modalShow';
import './index.less';
import { ReactComponent as RefreshIcon } from '@/assets/icons/refresh.svg';
@ -62,6 +63,12 @@ const ImageList: React.FC = () => {
JSON.stringify(tableParams.filters),
JSON.stringify(tableParams.keywords),
]);
/**
*
* @param params -
* @returns
*/
const getRandomuserParams = (params: IMAGES.TableParams) => {
const {
pagination,
@ -111,11 +118,14 @@ const ImageList: React.FC = () => {
if (imagesRes.error_code === ERROR_CODE) {
setImages(imagesRes.data.data || []);
setLoading(false);
// 直接使用后端返回的分页信息
setTableParams((prev) => ({
...prev,
pagination: {
...prev.pagination,
current: imagesRes.data.paging.page_num || 1,
total: imagesRes.data.paging.total || 0,
pageSize: imagesRes.data.paging.page_size || 10,
},
}));
} else {
@ -268,6 +278,7 @@ const ImageList: React.FC = () => {
dataIndex: 'create_time',
key: 'create_time',
width: 180,
render:(text:string)=><Tooltip>{dayjs(text).format('YYYY-MM-DD HH:mm:ss')}</Tooltip>,
...(visibleColumns['create_time'] ? {} : { hidden: true }),
},
{
@ -297,6 +308,7 @@ const ImageList: React.FC = () => {
},
].filter((column) => !column.hidden);
// 处理表格分页、过滤和排序变化
const handleTableChange: TableProps<IMAGES.ImageItem>['onChange'] = (
pagination,
filters,
@ -323,28 +335,27 @@ const ImageList: React.FC = () => {
loadImages();
};
// 导入成功后的回调
// 导入镜像成功后的回调
const handleImportSuccess = () => {
// 可以在这里添加导入成功后的处理逻辑
// 例如:刷新列表或显示提示信息
setTimeout(() => {
loadImages(); // 一段时间后刷新列表查看是否有新导入的镜像
loadImages();
}, 5000);
};
// 自定义分页配置
const paginationConfig = {
...tableParams.pagination,
showTotal: (total: number) => `${total} 条记录`,
showSizeChanger: true,
showQuickJumper: true,
pageSizeOptions: ['10', '20', '50', '100'],
};
return (
<div className="image-list">
<div
style={{
marginBottom: 16,
display: 'flex',
justifyContent: 'space-between',
}}
>
<div>
<Button onClick={() => setImportModalVisible(true)}></Button>
</div>
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
<div className="search-box">
<Button onClick={() => setImportModalVisible(true)}></Button>
<div className="search-input">
<Input.Search
placeholder="镜像名称"
value={searchText}
@ -378,21 +389,19 @@ const ImageList: React.FC = () => {
</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} 条记录`,
// }}
/>
<div className="images-list-container">
<div className="images-list-table">
<Table
columns={filteredColumns}
dataSource={images}
rowKey="image_id"
loading={loading}
pagination={paginationConfig}
onChange={handleTableChange}
scroll={{ y: '100%' }}
/>
</div>
</div>
{detailVisible ? (
<ModalDetailShow

View File

@ -3827,7 +3827,7 @@ date-fns@2.x:
dependencies:
"@babel/runtime" "^7.21.0"
dayjs@1.x, dayjs@^1.11.10, dayjs@^1.11.11, dayjs@^1.11.7:
dayjs@1.x, dayjs@^1.11.10, dayjs@^1.11.11, dayjs@^1.11.13, dayjs@^1.11.7:
version "1.11.13"
resolved "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.13.tgz#92430b0139055c3ebb60150aa13e860a4b5a366c"
integrity sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==
@ -9412,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==
@ -9511,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==
@ -10276,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==