新增批量上传功能
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import React, { useEffect, useState, useLayoutEffect, useRef, useCallback } from 'react';
|
||||
import { Button, Empty, Input, Select, Space, Typography, Tag, message } from 'antd';
|
||||
import { Button, Empty, Input, Select, Space, Typography, Tag, message, Upload, Modal, Progress } from 'antd';
|
||||
import {
|
||||
SearchOutlined,
|
||||
FilterOutlined,
|
||||
@@ -10,6 +10,8 @@ import {
|
||||
DownloadOutlined,
|
||||
XOutlined,
|
||||
ClockCircleOutlined,
|
||||
UploadOutlined,
|
||||
PlusOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { gethistory, gethistoryItems } from '../api';
|
||||
|
||||
@@ -31,6 +33,16 @@ const GeneratedRecord: React.FC = () => {
|
||||
const [previewItem, setPreviewItem] = useState<any>(null);
|
||||
const videoRef = React.createRef<HTMLVideoElement>();
|
||||
|
||||
// 批量上传相关状态
|
||||
const [uploadModalVisible, setUploadModalVisible] = useState(false);
|
||||
const [uploadFiles, setUploadFiles] = useState<any[]>([]);
|
||||
const [uploadProgress, setUploadProgress] = useState<{ [key: string]: number }>({});
|
||||
const [uploading, setUploading] = useState(false);
|
||||
|
||||
// 多选相关状态
|
||||
const [isSelectionMode, setIsSelectionMode] = useState(false);
|
||||
const [selectedItems, setSelectedItems] = useState<Set<string>>(new Set());
|
||||
|
||||
// 全局 Intersection Observer 实例(复用,避免创建过多实例)
|
||||
let globalObserver: IntersectionObserver | null = null;
|
||||
const observerCallbacks = new Map<HTMLElement, () => void>();
|
||||
@@ -160,7 +172,10 @@ const GeneratedRecord: React.FC = () => {
|
||||
item: any;
|
||||
mediaType: 'video' | 'image';
|
||||
onClick: () => void;
|
||||
}> = ({ item, mediaType, onClick }) => {
|
||||
isSelected?: boolean;
|
||||
onToggleSelect?: (itemId: string) => void;
|
||||
isSelectionMode?: boolean;
|
||||
}> = ({ item, mediaType, onClick, isSelected = false, onToggleSelect, isSelectionMode = false }) => {
|
||||
const [isLoaded, setIsLoaded] = useState(false);
|
||||
const [isError, setIsError] = useState(false);
|
||||
const [isExpired, setIsExpired] = useState(false);
|
||||
@@ -251,14 +266,18 @@ const GeneratedRecord: React.FC = () => {
|
||||
position: 'relative',
|
||||
backgroundColor: '#f1f5f9',
|
||||
}}
|
||||
onClick={onClick}
|
||||
onClick={!isSelectionMode ? onClick : undefined}
|
||||
onMouseEnter={(e) => {
|
||||
(e.currentTarget as HTMLElement).style.transform = 'scale(1.05)';
|
||||
(e.currentTarget as HTMLElement).style.boxShadow = '0 4px 16px rgba(0,0,0,0.2)';
|
||||
if (!isSelectionMode) {
|
||||
(e.currentTarget as HTMLElement).style.transform = 'scale(1.05)';
|
||||
(e.currentTarget as HTMLElement).style.boxShadow = '0 4px 16px rgba(0,0,0,0.2)';
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
(e.currentTarget as HTMLElement).style.transform = 'scale(1)';
|
||||
(e.currentTarget as HTMLElement).style.boxShadow = '0 2px 8px rgba(0,0,0,0.1)';
|
||||
if (!isSelectionMode) {
|
||||
(e.currentTarget as HTMLElement).style.transform = 'scale(1)';
|
||||
(e.currentTarget as HTMLElement).style.boxShadow = '0 2px 8px rgba(0,0,0,0.1)';
|
||||
}
|
||||
}}
|
||||
>
|
||||
{/* 加载占位符 - 显示渐变背景和加载状态 */}
|
||||
@@ -408,7 +427,7 @@ const GeneratedRecord: React.FC = () => {
|
||||
)}
|
||||
|
||||
{/* 点击提示 */}
|
||||
{!isExpired && (
|
||||
{!isExpired && !isSelectionMode && (
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
@@ -431,6 +450,59 @@ const GeneratedRecord: React.FC = () => {
|
||||
点击预览
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 选择模式下的选择框 */}
|
||||
{isSelectionMode && (
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 8,
|
||||
right: 8,
|
||||
width: 20,
|
||||
height: 20,
|
||||
borderRadius: '50%',
|
||||
backgroundColor: isSelected ? '#10b981' : 'rgba(255,255,255,0.9)',
|
||||
border: isSelected ? '2px solid #10b981' : '2px solid #d1d5db',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
cursor: 'pointer',
|
||||
zIndex: 10,
|
||||
transition: 'all 0.2s',
|
||||
}}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onToggleSelect?.(item.id);
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.transform = 'scale(1.1)';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.transform = 'scale(1)';
|
||||
}}
|
||||
>
|
||||
{isSelected && (
|
||||
<svg width={12} height={12} viewBox="0 0 12 12" fill="none">
|
||||
<path d="M10 3L4.5 8.5L2 6" stroke="white" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 选中状态下的边框高亮 */}
|
||||
{isSelectionMode && isSelected && (
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
border: '3px solid #10b981',
|
||||
borderRadius: 4,
|
||||
pointerEvents: 'none',
|
||||
zIndex: 5,
|
||||
}} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -448,7 +520,6 @@ const GeneratedRecord: React.FC = () => {
|
||||
|
||||
// 预览文件
|
||||
const handlePreview = (item: any) => {
|
||||
|
||||
setPreviewItem(item);
|
||||
setPreviewVisible(true);
|
||||
// 触发事件通知布局组件关闭浮动按钮
|
||||
@@ -471,6 +542,187 @@ const GeneratedRecord: React.FC = () => {
|
||||
setPreviewVisible(false);
|
||||
};
|
||||
|
||||
// 批量上传相关函数
|
||||
const handleUploadChange = (info: any) => {
|
||||
// 过滤文件类型
|
||||
const validFiles = info.fileList.filter((file: any) => {
|
||||
const type = file.type.toLowerCase();
|
||||
return type.startsWith('image/') || type.startsWith('video/');
|
||||
});
|
||||
|
||||
// 检查无效文件并提示
|
||||
const invalidFiles = info.fileList.filter((file: any) => {
|
||||
const type = file.type.toLowerCase();
|
||||
return !type.startsWith('image/') && !type.startsWith('video/');
|
||||
});
|
||||
|
||||
if (invalidFiles.length > 0) {
|
||||
message.warning(`已过滤 ${invalidFiles.length} 个无效文件,仅支持图片和视频`);
|
||||
}
|
||||
|
||||
setUploadFiles(validFiles);
|
||||
};
|
||||
|
||||
const handleRemoveFile = (file: any) => {
|
||||
setUploadFiles(prev => prev.filter(f => f.uid !== file.uid));
|
||||
};
|
||||
|
||||
const handleStartUpload = async () => {
|
||||
if (uploadFiles.length === 0) {
|
||||
message.warning('请先选择要上传的文件');
|
||||
return;
|
||||
}
|
||||
|
||||
setUploading(true);
|
||||
|
||||
// 模拟批量上传过程
|
||||
for (let i = 0; i < uploadFiles.length; i++) {
|
||||
const file = uploadFiles[i];
|
||||
setUploadProgress(prev => ({ ...prev, [file.uid]: 0 }));
|
||||
|
||||
// 模拟上传进度
|
||||
for (let progress = 0; progress <= 100; progress += 10) {
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
setUploadProgress(prev => ({ ...prev, [file.uid]: progress }));
|
||||
}
|
||||
}
|
||||
|
||||
// 上传完成
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
message.success(`成功上传 ${uploadFiles.length} 个文件`);
|
||||
setUploading(false);
|
||||
setUploadFiles([]);
|
||||
setUploadModalVisible(false);
|
||||
// 刷新页面数据
|
||||
setPagebreak(prev => ({ ...prev, page: 1 }));
|
||||
};
|
||||
|
||||
// 多选相关函数
|
||||
const handleToggleSelect = (itemId: string) => {
|
||||
setSelectedItems(prev => {
|
||||
const newSet = new Set(prev);
|
||||
if (newSet.has(itemId)) {
|
||||
newSet.delete(itemId);
|
||||
} else {
|
||||
newSet.add(itemId);
|
||||
}
|
||||
return newSet;
|
||||
});
|
||||
};
|
||||
|
||||
const handleSelectAll = () => {
|
||||
const allItemIds = recordlist.flatMap((group: any) =>
|
||||
group.items.map((item: any) => item.id)
|
||||
);
|
||||
if (selectedItems.size === allItemIds.length) {
|
||||
setSelectedItems(new Set());
|
||||
} else {
|
||||
setSelectedItems(new Set(allItemIds));
|
||||
}
|
||||
};
|
||||
|
||||
const handleBatchUploadSelected = async () => {
|
||||
if (selectedItems.size === 0) {
|
||||
message.warning('请先选择要上传的媒体');
|
||||
return;
|
||||
}
|
||||
|
||||
setUploading(true);
|
||||
const uploadProgressMap: { [key: string]: number } = {};
|
||||
|
||||
try {
|
||||
// 遍历所有选中的项
|
||||
for (const itemId of selectedItems) {
|
||||
let item: any = null;
|
||||
let mediaUrl = '';
|
||||
let mediaType = '';
|
||||
|
||||
// 找到对应的 item
|
||||
for (const group of recordlist) {
|
||||
const found = group.items.find((i: any) => i.id === itemId);
|
||||
if (found) {
|
||||
item = found;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!item) continue;
|
||||
|
||||
// 根据媒体类型获取 URL
|
||||
if (filterMedia === 'video' && item.videoUrl) {
|
||||
mediaUrl = `${import.meta.env.VITE_API_BASE || "http://localhost:8000"}${item.videoUrl}`;
|
||||
mediaType = 'video';
|
||||
} else if (filterMedia === 'image' && item.imageUrl) {
|
||||
mediaUrl = `${import.meta.env.VITE_API_BASE || "http://localhost:8000"}/static${item.imageUrl}`;
|
||||
mediaType = 'image';
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 初始化进度
|
||||
uploadProgressMap[itemId] = 0;
|
||||
setUploadProgress({ ...uploadProgressMap });
|
||||
|
||||
try {
|
||||
// 下载媒体文件
|
||||
const response = await fetch(mediaUrl);
|
||||
if (!response.ok) throw new Error('下载失败');
|
||||
const blob = await response.blob();
|
||||
const file = new File([blob], item.title || `media_${itemId}`, {
|
||||
type: mediaType === 'video' ? 'video/mp4' : 'image/jpeg'
|
||||
});
|
||||
|
||||
// 上传到后台接口
|
||||
const form = new FormData();
|
||||
form.append('file', file);
|
||||
form.append('media_type', mediaType);
|
||||
form.append('original_id', itemId);
|
||||
|
||||
const token = localStorage.getItem('auth_token');
|
||||
const uploadResponse = await fetch(
|
||||
`${import.meta.env.VITE_API_BASE || 'http://localhost:8000'}/api/generation-records/batch-upload`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: token ? { Authorization: `Bearer ${token}` } : {},
|
||||
body: form,
|
||||
}
|
||||
);
|
||||
|
||||
if (!uploadResponse.ok) throw new Error('上传失败');
|
||||
|
||||
// 更新进度
|
||||
uploadProgressMap[itemId] = 100;
|
||||
setUploadProgress({ ...uploadProgressMap });
|
||||
|
||||
} catch (error) {
|
||||
console.error(`上传失败: ${itemId}`, error);
|
||||
uploadProgressMap[itemId] = -1; // 标记失败
|
||||
setUploadProgress({ ...uploadProgressMap });
|
||||
}
|
||||
}
|
||||
|
||||
const successCount = Object.values(uploadProgressMap).filter(p => p === 100).length;
|
||||
const failCount = Object.values(uploadProgressMap).filter(p => p === -1).length;
|
||||
|
||||
if (failCount === 0) {
|
||||
message.success(`成功上传 ${successCount} 个文件`);
|
||||
} else {
|
||||
message.warning(`上传完成:成功 ${successCount} 个,失败 ${failCount} 个`);
|
||||
}
|
||||
|
||||
// 退出选择模式
|
||||
setIsSelectionMode(false);
|
||||
setSelectedItems(new Set());
|
||||
setUploadProgress({});
|
||||
|
||||
} catch (error) {
|
||||
message.error('批量上传失败');
|
||||
console.error(error);
|
||||
} finally {
|
||||
setUploading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
let parameters = '';
|
||||
@@ -508,8 +760,6 @@ const GeneratedRecord: React.FC = () => {
|
||||
...prev,
|
||||
page: prev.page + 1
|
||||
}));
|
||||
|
||||
|
||||
};
|
||||
|
||||
// 分组加载更多
|
||||
@@ -575,7 +825,7 @@ const GeneratedRecord: React.FC = () => {
|
||||
</Typography.Text>
|
||||
</div> */}
|
||||
|
||||
{/* First row filter: 项目记录 / 创作记录 */}
|
||||
{/* 操作栏:筛选 + 上传按钮 */}
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
@@ -585,42 +835,107 @@ const GeneratedRecord: React.FC = () => {
|
||||
borderRadius: 12,
|
||||
background: '#fff',
|
||||
border: '1px solid #f0f0f5',
|
||||
justifyContent: 'space-between',
|
||||
}}>
|
||||
<FilterOutlined style={{ color: '#94a3b8', fontSize: 14 }} />
|
||||
<Space>
|
||||
<Button
|
||||
type={filterType === 'project' ? 'primary' : 'default'}
|
||||
onClick={() => setFilterType('project')}
|
||||
style={{
|
||||
borderRadius: 8,
|
||||
background: filterType === 'project'
|
||||
? 'linear-gradient(135deg, #6366f1, #8b5cf6)'
|
||||
: '#f8f9fc',
|
||||
border: filterType === 'project' ? 'none' : '1px solid #e2e8f0',
|
||||
color: filterType === 'project' ? '#fff' : '#64748b',
|
||||
fontWeight: 600,
|
||||
}}
|
||||
icon={<FolderOpenOutlined />}
|
||||
>
|
||||
项目记录
|
||||
</Button>
|
||||
<Button
|
||||
type={filterType === 'creation' ? 'primary' : 'default'}
|
||||
onClick={() => setFilterType('creation')}
|
||||
style={{
|
||||
borderRadius: 8,
|
||||
background: filterType === 'creation'
|
||||
? 'linear-gradient(135deg, #6366f1, #8b5cf6)'
|
||||
: '#f8f9fc',
|
||||
border: filterType === 'creation' ? 'none' : '1px solid #e2e8f0',
|
||||
color: filterType === 'creation' ? '#fff' : '#64748b',
|
||||
fontWeight: 600,
|
||||
}}
|
||||
icon={<FileTextOutlined />}
|
||||
>
|
||||
创作记录
|
||||
</Button>
|
||||
</Space>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
||||
<FilterOutlined style={{ color: '#94a3b8', fontSize: 14 }} />
|
||||
<Space>
|
||||
<Button
|
||||
type={filterType === 'project' ? 'primary' : 'default'}
|
||||
onClick={() => setFilterType('project')}
|
||||
style={{
|
||||
borderRadius: 8,
|
||||
background: filterType === 'project'
|
||||
? 'linear-gradient(135deg, #6366f1, #8b5cf6)'
|
||||
: '#f8f9fc',
|
||||
border: filterType === 'project' ? 'none' : '1px solid #e2e8f0',
|
||||
color: filterType === 'project' ? '#fff' : '#64748b',
|
||||
fontWeight: 600,
|
||||
}}
|
||||
icon={<FolderOpenOutlined />}
|
||||
>
|
||||
项目记录
|
||||
</Button>
|
||||
<Button
|
||||
type={filterType === 'creation' ? 'primary' : 'default'}
|
||||
onClick={() => setFilterType('creation')}
|
||||
style={{
|
||||
borderRadius: 8,
|
||||
background: filterType === 'creation'
|
||||
? 'linear-gradient(135deg, #6366f1, #8b5cf6)'
|
||||
: '#f8f9fc',
|
||||
border: filterType === 'creation' ? 'none' : '1px solid #e2e8f0',
|
||||
color: filterType === 'creation' ? '#fff' : '#64748b',
|
||||
fontWeight: 600,
|
||||
}}
|
||||
icon={<FileTextOutlined />}
|
||||
>
|
||||
创作记录
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
||||
{/* 多选模式按钮 */}
|
||||
{isSelectionMode ? (
|
||||
<Space>
|
||||
<Button
|
||||
onClick={handleSelectAll}
|
||||
style={{
|
||||
borderRadius: 8,
|
||||
background: '#f8f9fc',
|
||||
border: '1px solid #e2e8f0',
|
||||
color: '#64748b',
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
{selectedItems.size === recordlist.reduce((sum: number, group: any) => sum + group.items.length, 0) ? '取消全选' : '全选'}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setIsSelectionMode(false);
|
||||
setSelectedItems(new Set());
|
||||
}}
|
||||
style={{
|
||||
borderRadius: 8,
|
||||
background: '#f8f9fc',
|
||||
border: '1px solid #e2e8f0',
|
||||
color: '#64748b',
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
取消选择
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={handleBatchUploadSelected}
|
||||
loading={uploading}
|
||||
disabled={uploading || selectedItems.size === 0}
|
||||
style={{
|
||||
borderRadius: 8,
|
||||
background: 'linear-gradient(135deg, #10b981, #059669)',
|
||||
border: 'none',
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
{uploading ? '上传中...' : `上传选中 (${selectedItems.size})`}
|
||||
</Button>
|
||||
</Space>
|
||||
) : (
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<UploadOutlined />}
|
||||
onClick={() => setIsSelectionMode(true)}
|
||||
style={{
|
||||
borderRadius: 8,
|
||||
background: 'linear-gradient(135deg, #10b981, #059669)',
|
||||
border: 'none',
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
批量上传
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Second row filter: 视频 / 图片 */}
|
||||
@@ -703,7 +1018,10 @@ const GeneratedRecord: React.FC = () => {
|
||||
key={item.id}
|
||||
item={item}
|
||||
mediaType={filterMedia}
|
||||
onClick={() => handlePreview(item)}
|
||||
onClick={() => isSelectionMode ? handleToggleSelect(item.id) : handlePreview(item)}
|
||||
isSelected={selectedItems.has(item.id)}
|
||||
onToggleSelect={handleToggleSelect}
|
||||
isSelectionMode={isSelectionMode}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -751,6 +1069,147 @@ const GeneratedRecord: React.FC = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 批量上传弹窗 */}
|
||||
<Modal
|
||||
title="批量上传媒体"
|
||||
open={uploadModalVisible}
|
||||
onCancel={() => {
|
||||
setUploadModalVisible(false);
|
||||
setUploadFiles([]);
|
||||
}}
|
||||
footer={null}
|
||||
width={600}
|
||||
>
|
||||
<div style={{ padding: '16px 0' }}>
|
||||
{/* 上传区域 */}
|
||||
<Upload
|
||||
multiple
|
||||
fileList={uploadFiles}
|
||||
onChange={handleUploadChange}
|
||||
beforeUpload={() => false} // 手动控制上传
|
||||
accept="image/*,video/*"
|
||||
listType="picture-card"
|
||||
onRemove={handleRemoveFile}
|
||||
>
|
||||
<div style={{
|
||||
width: 100,
|
||||
height: 100,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
border: '1px dashed #d9d9d9',
|
||||
borderRadius: 8,
|
||||
cursor: 'pointer',
|
||||
}}>
|
||||
<PlusOutlined style={{ fontSize: 24, color: '#999' }} />
|
||||
<span style={{ marginTop: 8, color: '#999', fontSize: 12 }}>点击上传</span>
|
||||
</div>
|
||||
</Upload>
|
||||
|
||||
{/* 上传列表和进度 */}
|
||||
{uploadFiles.length > 0 && (
|
||||
<div style={{ marginTop: 16 }}>
|
||||
<Typography.Text strong style={{ fontSize: 14, color: '#475569' }}>
|
||||
已选择 {uploadFiles.length} 个文件
|
||||
</Typography.Text>
|
||||
<div style={{ marginTop: 12, maxHeight: 200, overflowY: 'auto' }}>
|
||||
{uploadFiles.map((file) => (
|
||||
<div
|
||||
key={file.uid}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 12,
|
||||
padding: 8,
|
||||
border: '1px solid #e8e8e8',
|
||||
borderRadius: 6,
|
||||
marginBottom: 8,
|
||||
}}
|
||||
>
|
||||
<div style={{
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 4,
|
||||
overflow: 'hidden',
|
||||
backgroundColor: '#f5f5f5',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}>
|
||||
{file.type?.startsWith('image/') ? (
|
||||
<PictureOutlined style={{ color: '#3b82f6', fontSize: 16 }} />
|
||||
) : file.type?.startsWith('video/') ? (
|
||||
<VideoCameraOutlined style={{ color: '#f59e0b', fontSize: 16 }} />
|
||||
) : (
|
||||
<FileTextOutlined style={{ color: '#999', fontSize: 16 }} />
|
||||
)}
|
||||
</div>
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div style={{
|
||||
fontSize: 13,
|
||||
color: '#333',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
}}>
|
||||
{file.name}
|
||||
</div>
|
||||
{uploadProgress[file.uid] !== undefined && (
|
||||
<Progress
|
||||
percent={uploadProgress[file.uid]}
|
||||
size="small"
|
||||
showInfo={false}
|
||||
style={{ marginTop: 4 }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
icon={<XOutlined />}
|
||||
onClick={() => handleRemoveFile(file)}
|
||||
style={{
|
||||
background: 'transparent',
|
||||
border: 'none',
|
||||
color: '#999',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 操作按钮 */}
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
gap: 12,
|
||||
marginTop: 24,
|
||||
paddingTop: 16,
|
||||
borderTop: '1px solid #f0f0f0',
|
||||
justifyContent: 'flex-end',
|
||||
}}>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setUploadModalVisible(false);
|
||||
setUploadFiles([]);
|
||||
}}
|
||||
style={{ borderRadius: 8 }}
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={handleStartUpload}
|
||||
loading={uploading}
|
||||
disabled={uploading || uploadFiles.length === 0}
|
||||
style={{ borderRadius: 8 }}
|
||||
>
|
||||
{uploading ? '上传中...' : '开始上传'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
{/* 预览弹窗 */}
|
||||
{previewVisible && previewItem && (
|
||||
<div
|
||||
|
||||
Reference in New Issue
Block a user