From b2bace168708cadbbe9d4032e1b9628b70a159b7 Mon Sep 17 00:00:00 2001 From: hailin Date: Fri, 6 Feb 2026 00:20:13 -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=E6=94=B9=E4=B8=BA=E9=80=90=E4=B8=AA=E4=B8=8B=E8=BD=BD?= =?UTF-8?q?=E5=88=B0=E7=94=A8=E6=88=B7=E9=80=89=E6=8B=A9=E7=9A=84=E7=9B=AE?= =?UTF-8?q?=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 先弹出目录选择对话框让用户指定保存位置 - 逐个下载合同 PDF 到指定目录 - 显示下载进度(已下载数/总数) Co-Authored-By: Claude Opus 4.5 --- .../src/app/(dashboard)/contracts/page.tsx | 315 +++++++++--------- 1 file changed, 162 insertions(+), 153 deletions(-) diff --git a/frontend/admin-web/src/app/(dashboard)/contracts/page.tsx b/frontend/admin-web/src/app/(dashboard)/contracts/page.tsx index 0636ba32..48eb043d 100644 --- a/frontend/admin-web/src/app/(dashboard)/contracts/page.tsx +++ b/frontend/admin-web/src/app/(dashboard)/contracts/page.tsx @@ -141,8 +141,6 @@ 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); // 轮询任务状态直到完成 @@ -151,7 +149,7 @@ export default function ContractsPage() { for (let i = 0; i < maxAttempts; i++) { try { const status = await contractService.getBatchDownloadStatus(taskNo); - setBatchProgress(status.progress); + // 打包进度(未使用) if (status.status === 'COMPLETED' && status.resultFileUrl) { return status.resultFileUrl; @@ -169,124 +167,106 @@ export default function ContractsPage() { throw new Error('任务处理超时'); }; - // 使用 File System Access API 让用户选择保存位置(Chrome/Edge 支持) - const saveFileWithPicker = async (url: string, suggestedName: string): Promise => { - try { - // 检查浏览器是否支持 File System Access API - if ('showSaveFilePicker' in window) { - // 弹出保存对话框让用户选择位置(先选择再下载) - const fileHandle = await (window as unknown as { showSaveFilePicker: (options: unknown) => Promise }).showSaveFilePicker({ - suggestedName, - types: [{ - description: 'ZIP 压缩文件', - accept: { 'application/zip': ['.zip'] }, - }], - }); + // 下载进度详情 + const [downloadedCount, setDownloadedCount] = useState(0); + const [totalCount, setTotalCount] = useState(0); - // 用户选择了保存位置后,开始下载并显示进度 - 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: ArrayBuffer[] = []; - let loaded = 0; - - while (true) { - const { done, value } = await reader.read(); - if (done) break; - - chunks.push(value.buffer as ArrayBuffer); - loaded += value.length; - - if (total > 0) { - const progress = Math.round((loaded / total) * 100); - setDownloadProgress(progress); - } - } - - // 合并所有 chunks 为 Blob - const blob = new Blob(chunks, { type: 'application/zip' }); - - // 写入文件 - const writable = await fileHandle.createWritable(); - await writable.write(blob); - await writable.close(); - - setDownloadProgress(100); - return true; - } - } catch (error) { - // 用户取消或浏览器不支持,回退到普通下载 - console.log('File System Access API 不可用或用户取消,使用普通下载'); - } - return false; - }; - - // 批量下载(创建下载任务并等待完成后自动下载) + // 批量下载(先选择保存目录,再逐个下载合同) const handleBatchDownload = async () => { + // 检查浏览器是否支持选择目录 + if (!('showDirectoryPicker' in window)) { + toast.error('当前浏览器不支持选择保存目录,请使用 Chrome 或 Edge 浏览器'); + return; + } + + // 第一步:让用户选择保存目录 + let dirHandle: FileSystemDirectoryHandle; + try { + dirHandle = await (window as unknown as { showDirectoryPicker: () => Promise }).showDirectoryPicker(); + } catch (error: unknown) { + const err = error as Error; + if (err.name === 'AbortError') { + toast.info('已取消'); + } else { + toast.error('选择保存目录失败'); + } + return; + } + + // 第二步:获取已签署的合同列表 setBatchDownloading(true); - setBatchProgress(0); - setDownloadStage('packaging'); setDownloadProgress(0); + setDownloadedCount(0); + setTotalCount(0); try { - toast.info('正在准备批量下载,请稍候...'); + toast.info('正在获取合同列表...'); - const result = await contractService.createBatchDownload({ - filters: { - signedAfter: filters.signedAfter || undefined, - signedBefore: filters.signedBefore || undefined, - provinceCode: filters.provinceCode || undefined, - cityCode: filters.cityCode || undefined, - }, + // 获取所有已签署的合同 + const result = await contractService.getContracts({ + signedAfter: filters.signedAfter || undefined, + signedBefore: filters.signedBefore || undefined, + provinceCode: filters.provinceCode || undefined, + cityCode: filters.cityCode || undefined, + status: 'SIGNED', + page: 1, + pageSize: 10000, // 获取所有 }); - // 轮询等待任务完成 - const downloadUrl = await pollTaskUntilComplete(result.taskNo); + const contracts = result.items.filter(c => c.signedPdfUrl); + const total = contracts.length; + setTotalCount(total); - if (downloadUrl) { - // 生成文件名 - const today = new Date().toISOString().split('T')[0].replace(/-/g, ''); - const suggestedName = `contracts_${today}.zip`; - - // 优先使用 File System Access API 让用户选择保存位置 - toast.info('请选择保存位置...'); - const saved = await saveFileWithPicker(downloadUrl, suggestedName); - - if (saved) { - toast.success('文件保存成功!'); - } else { - // 浏览器不支持或用户取消,使用备用方法 - const link = document.createElement('a'); - link.href = downloadUrl; - link.download = suggestedName; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - toast.success('下载已开始'); - } - - // 保存当前时间作为最后下载时间 - const now = new Date().toISOString(); - localStorage.setItem('contract_last_download_time', now); - setLastDownloadTime(now); + if (total === 0) { + toast.warning('没有可下载的已签署合同'); + return; } + + toast.info(`开始下载 ${total} 份合同...`); + + // 第三步:逐个下载合同到用户选择的目录 + let downloaded = 0; + for (const contract of contracts) { + try { + // 生成文件名 + const fileName = `${contract.contractNo}_${contract.userRealName || '未实名'}_${contract.treeCount}棵.pdf`; + + // 下载 PDF + const downloadUrl = contractService.getDownloadUrl(contract.orderNo); + const response = await fetch(downloadUrl); + if (!response.ok) { + console.error(`下载失败: ${contract.orderNo}`); + continue; + } + + const blob = await response.blob(); + + // 保存到用户选择的目录 + const fileHandle = await dirHandle.getFileHandle(fileName, { create: true }); + const writable = await fileHandle.createWritable(); + await writable.write(blob); + await writable.close(); + + downloaded++; + setDownloadedCount(downloaded); + setDownloadProgress(Math.round((downloaded / total) * 100)); + } catch (err) { + console.error(`下载合同失败: ${contract.orderNo}`, err); + } + } + + toast.success(`下载完成!成功 ${downloaded}/${total} 份`); + + // 保存当前时间作为最后下载时间 + const now = new Date().toISOString(); + localStorage.setItem('contract_last_download_time', now); + setLastDownloadTime(now); } catch (error) { console.error('批量下载失败:', error); toast.error('批量下载失败,请重试'); } finally { setBatchDownloading(false); - setBatchProgress(0); - setDownloadStage('idle'); - setDownloadProgress(0); + setDownloadProgress(0); } }; @@ -297,60 +277,91 @@ export default function ContractsPage() { return; } + // 检查浏览器是否支持选择目录 + if (!('showDirectoryPicker' in window)) { + toast.error('当前浏览器不支持选择保存目录,请使用 Chrome 或 Edge 浏览器'); + return; + } + + // 第一步:让用户选择保存目录 + let dirHandle: FileSystemDirectoryHandle; + try { + dirHandle = await (window as unknown as { showDirectoryPicker: () => Promise }).showDirectoryPicker(); + } catch (error: unknown) { + const err = error as Error; + if (err.name === 'AbortError') { + toast.info('已取消'); + } else { + toast.error('选择保存目录失败'); + } + return; + } + + // 第二步:获取增量合同列表 setBatchDownloading(true); - setBatchProgress(0); - setDownloadStage('packaging'); - setDownloadProgress(0); + setDownloadProgress(0); + setDownloadedCount(0); + setTotalCount(0); try { - toast.info('正在准备增量下载,请稍候...'); + toast.info('正在获取新增合同列表...'); - const result = await contractService.createBatchDownload({ - filters: { - signedAfter: lastDownloadTime, - provinceCode: filters.provinceCode || undefined, - cityCode: filters.cityCode || undefined, - }, + // 获取上次下载后新签署的合同 + const result = await contractService.getContracts({ + signedAfter: lastDownloadTime, + provinceCode: filters.provinceCode || undefined, + cityCode: filters.cityCode || undefined, + status: 'SIGNED', + page: 1, + pageSize: 10000, }); - // 轮询等待任务完成 - const downloadUrl = await pollTaskUntilComplete(result.taskNo); + const contracts = result.items.filter(c => c.signedPdfUrl); + const total = contracts.length; + setTotalCount(total); - if (downloadUrl) { - // 生成文件名 - const today = new Date().toISOString().split('T')[0].replace(/-/g, ''); - const suggestedName = `contracts_incremental_${today}.zip`; - - // 优先使用 File System Access API 让用户选择保存位置 - toast.info('请选择保存位置...'); - const saved = await saveFileWithPicker(downloadUrl, suggestedName); - - if (saved) { - toast.success('文件保存成功!'); - } else { - // 浏览器不支持或用户取消,使用备用方法 - const link = document.createElement('a'); - link.href = downloadUrl; - link.download = suggestedName; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - toast.success('下载已开始'); - } - - // 更新最后下载时间 - const now = new Date().toISOString(); - localStorage.setItem('contract_last_download_time', now); - setLastDownloadTime(now); + if (total === 0) { + toast.info('没有新增的合同需要下载'); + return; } + + toast.info(`开始下载 ${total} 份新增合同...`); + + // 第三步:逐个下载合同 + let downloaded = 0; + for (const contract of contracts) { + try { + const fileName = `${contract.contractNo}_${contract.userRealName || '未实名'}_${contract.treeCount}棵.pdf`; + const downloadUrl = contractService.getDownloadUrl(contract.orderNo); + const response = await fetch(downloadUrl); + if (!response.ok) continue; + + const blob = await response.blob(); + const fileHandle = await dirHandle.getFileHandle(fileName, { create: true }); + const writable = await fileHandle.createWritable(); + await writable.write(blob); + await writable.close(); + + downloaded++; + setDownloadedCount(downloaded); + setDownloadProgress(Math.round((downloaded / total) * 100)); + } catch (err) { + console.error(`下载合同失败: ${contract.orderNo}`, err); + } + } + + toast.success(`下载完成!成功 ${downloaded}/${total} 份`); + + // 更新最后下载时间 + const now = new Date().toISOString(); + localStorage.setItem('contract_last_download_time', now); + setLastDownloadTime(now); } catch (error) { console.error('增量下载失败:', error); toast.error('增量下载失败,请重试'); } finally { setBatchDownloading(false); - setBatchProgress(0); - setDownloadStage('idle'); - setDownloadProgress(0); + setDownloadProgress(0); } }; @@ -460,10 +471,8 @@ export default function ContractsPage() {
{lastDownloadTime && (