feat(admin-web): 批量下载增加进度条显示
- 显示打包进度和下载进度两个阶段 - 进度条实时更新百分比 - 使用 File System Access API 支持用户选择保存位置 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
f97eacdc70
commit
91132ec167
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}>
|
||||
|
|
|
|||
Loading…
Reference in New Issue