feat(admin-web): 批量下载增加进度条显示

- 显示打包进度和下载进度两个阶段
- 进度条实时更新百分比
- 使用 File System Access API 支持用户选择保存位置

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
hailin 2026-02-06 00:04:02 -08:00
parent f97eacdc70
commit 91132ec167
2 changed files with 103 additions and 6 deletions

View File

@ -172,4 +172,40 @@
&__statusGray {
color: #4b5563 !important;
}
// 进度条样式
&__progressContainer {
background: var(--bg-secondary, #f8f9fa);
border-radius: 8px;
padding: 1rem 1.5rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
&__progressLabel {
font-size: 0.875rem;
color: var(--text-primary, #212529);
font-weight: 500;
}
&__progressBar {
height: 12px;
background: var(--border-color, #dee2e6);
border-radius: 6px;
overflow: hidden;
}
&__progressFill {
height: 100%;
background: linear-gradient(90deg, #0d6efd, #0dcaf0);
border-radius: 6px;
transition: width 0.3s ease;
}
&__progressText {
font-size: 0.875rem;
color: var(--text-secondary, #6c757d);
text-align: right;
}
}

View File

@ -142,6 +142,8 @@ export default function ContractsPage() {
// 批量下载状态
const [batchDownloading, setBatchDownloading] = useState(false);
const [batchProgress, setBatchProgress] = useState(0);
const [downloadStage, setDownloadStage] = useState<'idle' | 'packaging' | 'downloading'>('idle');
const [downloadProgress, setDownloadProgress] = useState(0);
// 轮询任务状态直到完成
const pollTaskUntilComplete = async (taskNo: string): Promise<string | null> => {
@ -172,11 +174,7 @@ export default function ContractsPage() {
try {
// 检查浏览器是否支持 File System Access API
if ('showSaveFilePicker' in window) {
// 先下载文件内容
const response = await fetch(url);
const blob = await response.blob();
// 弹出保存对话框让用户选择位置
// 弹出保存对话框让用户选择位置(先选择再下载)
const fileHandle = await (window as unknown as { showSaveFilePicker: (options: unknown) => Promise<FileSystemFileHandle> }).showSaveFilePicker({
suggestedName,
types: [{
@ -185,11 +183,44 @@ export default function ContractsPage() {
}],
});
// 用户选择了保存位置后,开始下载并显示进度
setDownloadStage('downloading');
setDownloadProgress(0);
const response = await fetch(url);
const contentLength = response.headers.get('content-length');
const total = contentLength ? parseInt(contentLength, 10) : 0;
if (!response.body) {
throw new Error('无法读取响应流');
}
const reader = response.body.getReader();
const chunks: Uint8Array[] = [];
let loaded = 0;
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
loaded += value.length;
if (total > 0) {
const progress = Math.round((loaded / total) * 100);
setDownloadProgress(progress);
}
}
// 合并所有 chunks 为 Blob
const blob = new Blob(chunks);
// 写入文件
const writable = await fileHandle.createWritable();
await writable.write(blob);
await writable.close();
setDownloadProgress(100);
return true;
}
} catch (error) {
@ -203,6 +234,8 @@ export default function ContractsPage() {
const handleBatchDownload = async () => {
setBatchDownloading(true);
setBatchProgress(0);
setDownloadStage('packaging');
setDownloadProgress(0);
try {
toast.info('正在准备批量下载,请稍候...');
@ -252,6 +285,8 @@ export default function ContractsPage() {
} finally {
setBatchDownloading(false);
setBatchProgress(0);
setDownloadStage('idle');
setDownloadProgress(0);
}
};
@ -264,6 +299,8 @@ export default function ContractsPage() {
setBatchDownloading(true);
setBatchProgress(0);
setDownloadStage('packaging');
setDownloadProgress(0);
try {
toast.info('正在准备增量下载,请稍候...');
@ -312,6 +349,8 @@ export default function ContractsPage() {
} finally {
setBatchDownloading(false);
setBatchProgress(0);
setDownloadStage('idle');
setDownloadProgress(0);
}
};
@ -420,7 +459,11 @@ export default function ContractsPage() {
<div className={styles.contracts__actions}>
<Button variant="primary" onClick={handleBatchDownload} disabled={batchDownloading}>
{batchDownloading ? `打包中 ${batchProgress}%...` : '批量下载 ZIP'}
{batchDownloading
? downloadStage === 'packaging'
? `打包中 ${batchProgress}%`
: `下载中 ${downloadProgress}%`
: '批量下载 ZIP'}
</Button>
{lastDownloadTime && (
<Button variant="outline" onClick={handleIncrementalDownload} disabled={batchDownloading}>
@ -430,6 +473,24 @@ export default function ContractsPage() {
</div>
</div>
{/* 下载进度条 */}
{batchDownloading && (
<div className={styles.contracts__progressContainer}>
<div className={styles.contracts__progressLabel}>
{downloadStage === 'packaging' ? '正在打包合同文件...' : '正在下载到本地...'}
</div>
<div className={styles.contracts__progressBar}>
<div
className={styles.contracts__progressFill}
style={{ width: `${downloadStage === 'packaging' ? batchProgress : downloadProgress}%` }}
/>
</div>
<div className={styles.contracts__progressText}>
{downloadStage === 'packaging' ? batchProgress : downloadProgress}%
</div>
</div>
)}
{/* 合同列表 */}
<div className={styles.contracts__table}>
<div className={styles.contracts__tableHeader}>