feat(admin-web): 批量下载改为逐个下载到用户选择的目录
- 先弹出目录选择对话框让用户指定保存位置 - 逐个下载合同 PDF 到指定目录 - 显示下载进度(已下载数/总数) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
9771a3d69d
commit
b2bace1687
|
|
@ -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<boolean> => {
|
||||
try {
|
||||
// 检查浏览器是否支持 File System Access API
|
||||
if ('showSaveFilePicker' in window) {
|
||||
// 弹出保存对话框让用户选择位置(先选择再下载)
|
||||
const fileHandle = await (window as unknown as { showSaveFilePicker: (options: unknown) => Promise<FileSystemFileHandle> }).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<FileSystemDirectoryHandle> }).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<FileSystemDirectoryHandle> }).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() {
|
|||
<div className={styles.contracts__actions}>
|
||||
<Button variant="primary" onClick={handleBatchDownload} disabled={batchDownloading}>
|
||||
{batchDownloading
|
||||
? downloadStage === 'packaging'
|
||||
? `打包中 ${batchProgress}%`
|
||||
: `下载中 ${downloadProgress}%`
|
||||
: '批量下载 ZIP'}
|
||||
? `下载中 ${downloadedCount}/${totalCount}`
|
||||
: '批量下载'}
|
||||
</Button>
|
||||
{lastDownloadTime && (
|
||||
<Button variant="outline" onClick={handleIncrementalDownload} disabled={batchDownloading}>
|
||||
|
|
@ -477,16 +486,16 @@ export default function ContractsPage() {
|
|||
{batchDownloading && (
|
||||
<div className={styles.contracts__progressContainer}>
|
||||
<div className={styles.contracts__progressLabel}>
|
||||
{downloadStage === 'packaging' ? '正在打包合同文件...' : '正在下载到本地...'}
|
||||
正在下载合同文件... ({downloadedCount}/{totalCount})
|
||||
</div>
|
||||
<div className={styles.contracts__progressBar}>
|
||||
<div
|
||||
className={styles.contracts__progressFill}
|
||||
style={{ width: `${downloadStage === 'packaging' ? batchProgress : downloadProgress}%` }}
|
||||
style={{ width: `${downloadProgress}%` }}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.contracts__progressText}>
|
||||
{downloadStage === 'packaging' ? batchProgress : downloadProgress}%
|
||||
{downloadProgress}%
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
Loading…
Reference in New Issue