update: fix-02

This commit is contained in:
2026-03-13 09:57:04 +08:00
parent 7cd2a18364
commit e0a40ceff0
10 changed files with 843 additions and 133 deletions

View File

@@ -1,4 +1,4 @@
import React, { useMemo, useState } from 'react';
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import {
@@ -16,6 +16,7 @@ import {
Descriptions,
Progress,
Alert,
Empty,
} from 'antd';
import {
CloudUploadOutlined,
@@ -34,12 +35,39 @@ import type { EvidenceImage } from '../../types';
const { Dragger } = Upload;
type UploadBatchItem = {
localId: string;
fileName: string;
fileSize: number;
status: 'uploading' | 'success' | 'error';
uploadedImageId?: string;
previewUrl?: string;
};
const formatFileSize = (size: number): string => {
if (!Number.isFinite(size) || size <= 0) return '-';
if (size < 1024) return `${size} B`;
if (size < 1024 * 1024) return `${(size / 1024).toFixed(1)} KB`;
return `${(size / (1024 * 1024)).toFixed(1)} MB`;
};
const Workspace: React.FC = () => {
const { id = '1' } = useParams();
const navigate = useNavigate();
const queryClient = useQueryClient();
const { message } = App.useApp();
const [uploadingCount, setUploadingCount] = useState(0);
const [uploadBatchItems, setUploadBatchItems] = useState<UploadBatchItem[]>([]);
const batchCounterRef = useRef<{ success: number; failed: number }>({ success: 0, failed: 0 });
const batchActiveRef = useRef(false);
useEffect(() => {
return () => {
uploadBatchItems.forEach((item) => {
if (item.previewUrl) URL.revokeObjectURL(item.previewUrl);
});
};
}, [uploadBatchItems]);
const { data: currentCase } = useQuery({ queryKey: ['case', id], queryFn: () => fetchCase(id) });
const { data: imagesData } = useQuery({ queryKey: ['images', id], queryFn: () => fetchImages(id) });
@@ -206,29 +234,80 @@ const Workspace: React.FC = () => {
accept="image/*"
showUploadList={false}
beforeUpload={(file) => {
setUploadingCount((c) => c + 1);
message.open({
key: 'img-upload',
type: 'loading',
content: `正在上传截图(队列中 ${uploadingCount + 1} 张)...`,
duration: 0,
if (!batchActiveRef.current) {
batchActiveRef.current = true;
batchCounterRef.current = { success: 0, failed: 0 };
setUploadBatchItems((prev) => {
prev.forEach((item) => {
if (item.previewUrl) URL.revokeObjectURL(item.previewUrl);
});
return [];
});
}
const localId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
const previewUrl = URL.createObjectURL(file as File);
setUploadingCount((c) => {
const next = c + 1;
message.open({
key: 'img-upload',
type: 'loading',
content: `正在上传截图(队列中 ${next} 张):${file.name}`,
duration: 0,
});
return next;
});
setUploadBatchItems((prev) => [
...prev,
{
localId,
fileName: file.name,
fileSize: file.size,
status: 'uploading',
previewUrl,
},
]);
uploadImages(id, [file as File])
.then(() => {
message.success('截图上传成功');
.then((uploaded) => {
const uploadedImage = uploaded?.[0];
setUploadBatchItems((prev) =>
prev.map((item) =>
item.localId === localId
? {
...item,
status: 'success',
uploadedImageId: uploadedImage?.id,
}
: item,
),
);
batchCounterRef.current.success += 1;
})
.then(() => {
queryClient.invalidateQueries({ queryKey: ['images', id] });
queryClient.invalidateQueries({ queryKey: ['case', id] });
})
.catch(() => {
message.error('上传失败');
setUploadBatchItems((prev) =>
prev.map((item) =>
item.localId === localId
? { ...item, status: 'error' }
: item,
),
);
batchCounterRef.current.failed += 1;
})
.finally(() => {
setUploadingCount((c) => {
const next = Math.max(0, c - 1);
if (next === 0) {
batchActiveRef.current = false;
message.destroy('img-upload');
const summary = {
success: batchCounterRef.current.success,
failed: batchCounterRef.current.failed,
};
message.success(`本次上传完成:成功 ${summary.success} 张,失败 ${summary.failed}`);
} else {
message.open({
key: 'img-upload',
@@ -259,6 +338,75 @@ const Workspace: React.FC = () => {
{uploadingCount} ...
</Typography.Text>
)}
<Card
size="small"
title="本次上传清单"
style={{ marginTop: 12 }}
extra={
uploadBatchItems.length > 0 ? (
<Button
size="small"
type="link"
onClick={() => navigate(`/cases/${id}/screenshots`)}
>
</Button>
) : null
}
>
{uploadBatchItems.length === 0 ? (
<Empty description="暂无上传记录,请先拖拽截图上传" image={Empty.PRESENTED_IMAGE_SIMPLE} />
) : (
<>
<Typography.Text type="secondary">
{uploadBatchItems.length}
{uploadBatchItems.filter((x) => x.status === 'success').length}
{uploadBatchItems.filter((x) => x.status === 'error').length}
{uploadBatchItems.filter((x) => x.status === 'uploading').length}
</Typography.Text>
<Space direction="vertical" style={{ width: '100%', marginTop: 10 }} size={8}>
{uploadBatchItems.map((item, index) => (
<Row key={item.localId} justify="space-between" align="middle">
<Col span={18}>
<Space size={8}>
<Typography.Text type="secondary">#{index + 1}</Typography.Text>
<div
style={{
width: 40,
height: 40,
borderRadius: 4,
overflow: 'hidden',
border: '1px solid #f0f0f0',
background: '#fafafa',
}}
>
{item.previewUrl ? (
<img
src={item.previewUrl}
alt={item.fileName}
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
/>
) : null}
</div>
<Typography.Text ellipsis style={{ maxWidth: 260 }}>
{item.fileName}
</Typography.Text>
<Typography.Text type="secondary">
{formatFileSize(item.fileSize)}
</Typography.Text>
</Space>
</Col>
<Col>
{item.status === 'uploading' && <Tag color="processing"></Tag>}
{item.status === 'success' && <Tag color="success"></Tag>}
{item.status === 'error' && <Tag color="error"></Tag>}
</Col>
</Row>
))}
</Space>
</>
)}
</Card>
</Card>
<Card title="处理进度">