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 {
|
&__statusGray {
|
||||||
color: #4b5563 !important;
|
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 [batchDownloading, setBatchDownloading] = useState(false);
|
||||||
const [batchProgress, setBatchProgress] = useState(0);
|
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> => {
|
const pollTaskUntilComplete = async (taskNo: string): Promise<string | null> => {
|
||||||
|
|
@ -172,11 +174,7 @@ export default function ContractsPage() {
|
||||||
try {
|
try {
|
||||||
// 检查浏览器是否支持 File System Access API
|
// 检查浏览器是否支持 File System Access API
|
||||||
if ('showSaveFilePicker' in window) {
|
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({
|
const fileHandle = await (window as unknown as { showSaveFilePicker: (options: unknown) => Promise<FileSystemFileHandle> }).showSaveFilePicker({
|
||||||
suggestedName,
|
suggestedName,
|
||||||
types: [{
|
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();
|
const writable = await fileHandle.createWritable();
|
||||||
await writable.write(blob);
|
await writable.write(blob);
|
||||||
await writable.close();
|
await writable.close();
|
||||||
|
|
||||||
|
setDownloadProgress(100);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -203,6 +234,8 @@ export default function ContractsPage() {
|
||||||
const handleBatchDownload = async () => {
|
const handleBatchDownload = async () => {
|
||||||
setBatchDownloading(true);
|
setBatchDownloading(true);
|
||||||
setBatchProgress(0);
|
setBatchProgress(0);
|
||||||
|
setDownloadStage('packaging');
|
||||||
|
setDownloadProgress(0);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
toast.info('正在准备批量下载,请稍候...');
|
toast.info('正在准备批量下载,请稍候...');
|
||||||
|
|
@ -252,6 +285,8 @@ export default function ContractsPage() {
|
||||||
} finally {
|
} finally {
|
||||||
setBatchDownloading(false);
|
setBatchDownloading(false);
|
||||||
setBatchProgress(0);
|
setBatchProgress(0);
|
||||||
|
setDownloadStage('idle');
|
||||||
|
setDownloadProgress(0);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -264,6 +299,8 @@ export default function ContractsPage() {
|
||||||
|
|
||||||
setBatchDownloading(true);
|
setBatchDownloading(true);
|
||||||
setBatchProgress(0);
|
setBatchProgress(0);
|
||||||
|
setDownloadStage('packaging');
|
||||||
|
setDownloadProgress(0);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
toast.info('正在准备增量下载,请稍候...');
|
toast.info('正在准备增量下载,请稍候...');
|
||||||
|
|
@ -312,6 +349,8 @@ export default function ContractsPage() {
|
||||||
} finally {
|
} finally {
|
||||||
setBatchDownloading(false);
|
setBatchDownloading(false);
|
||||||
setBatchProgress(0);
|
setBatchProgress(0);
|
||||||
|
setDownloadStage('idle');
|
||||||
|
setDownloadProgress(0);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -420,7 +459,11 @@ export default function ContractsPage() {
|
||||||
|
|
||||||
<div className={styles.contracts__actions}>
|
<div className={styles.contracts__actions}>
|
||||||
<Button variant="primary" onClick={handleBatchDownload} disabled={batchDownloading}>
|
<Button variant="primary" onClick={handleBatchDownload} disabled={batchDownloading}>
|
||||||
{batchDownloading ? `打包中 ${batchProgress}%...` : '批量下载 ZIP'}
|
{batchDownloading
|
||||||
|
? downloadStage === 'packaging'
|
||||||
|
? `打包中 ${batchProgress}%`
|
||||||
|
: `下载中 ${downloadProgress}%`
|
||||||
|
: '批量下载 ZIP'}
|
||||||
</Button>
|
</Button>
|
||||||
{lastDownloadTime && (
|
{lastDownloadTime && (
|
||||||
<Button variant="outline" onClick={handleIncrementalDownload} disabled={batchDownloading}>
|
<Button variant="outline" onClick={handleIncrementalDownload} disabled={batchDownloading}>
|
||||||
|
|
@ -430,6 +473,24 @@ export default function ContractsPage() {
|
||||||
</div>
|
</div>
|
||||||
</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__table}>
|
||||||
<div className={styles.contracts__tableHeader}>
|
<div className={styles.contracts__tableHeader}>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue