From 91132ec167c0e07afeabe293a682cbdedcd638b8 Mon Sep 17 00:00:00 2001 From: hailin Date: Fri, 6 Feb 2026 00:04:02 -0800 Subject: [PATCH] =?UTF-8?q?feat(admin-web):=20=E6=89=B9=E9=87=8F=E4=B8=8B?= =?UTF-8?q?=E8=BD=BD=E5=A2=9E=E5=8A=A0=E8=BF=9B=E5=BA=A6=E6=9D=A1=E6=98=BE?= =?UTF-8?q?=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 显示打包进度和下载进度两个阶段 - 进度条实时更新百分比 - 使用 File System Access API 支持用户选择保存位置 Co-Authored-By: Claude Opus 4.5 --- .../contracts/contracts.module.scss | 36 +++++++++ .../src/app/(dashboard)/contracts/page.tsx | 73 +++++++++++++++++-- 2 files changed, 103 insertions(+), 6 deletions(-) diff --git a/frontend/admin-web/src/app/(dashboard)/contracts/contracts.module.scss b/frontend/admin-web/src/app/(dashboard)/contracts/contracts.module.scss index 401ee846..f082aab6 100644 --- a/frontend/admin-web/src/app/(dashboard)/contracts/contracts.module.scss +++ b/frontend/admin-web/src/app/(dashboard)/contracts/contracts.module.scss @@ -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; + } } diff --git a/frontend/admin-web/src/app/(dashboard)/contracts/page.tsx b/frontend/admin-web/src/app/(dashboard)/contracts/page.tsx index cbe864d1..0cfc4488 100644 --- a/frontend/admin-web/src/app/(dashboard)/contracts/page.tsx +++ b/frontend/admin-web/src/app/(dashboard)/contracts/page.tsx @@ -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 => { @@ -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 }).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() {
{lastDownloadTime && (
+ {/* 下载进度条 */} + {batchDownloading && ( +
+
+ {downloadStage === 'packaging' ? '正在打包合同文件...' : '正在下载到本地...'} +
+
+
+
+
+ {downloadStage === 'packaging' ? batchProgress : downloadProgress}% +
+
+ )} + {/* 合同列表 */}