From 441917bfce51ccf026d4b2bd519bb2da30ad77eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E8=87=AA=E8=BE=BE?= <980324341@qq.com> Date: Sat, 12 Sep 2026 11:42:33 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E5=90=8E=E5=8F=B0/=E5=BA=97=E9=93=BA?= =?UTF-8?q?=E6=95=B0=E6=8D=AE):=20=E7=BB=93=E6=9E=9C=E4=B8=8B=E8=BD=BD?= =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E8=BF=9B=E5=BA=A6=E5=BC=B9=E7=AA=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 批量打包下载与单店下载接入 axios onDownloadProgress,progress 弹窗显示 百分比;后端未回 Content-Length 时退化为 indeterminate 流动条,避免用户 在大 zip 下载期间以为页面无响应。 --- .../src/pages/tasks/ShopDataTasksPage.vue | 43 ++++++++++++++++++- .../src/pages/tasks/shop-data-api.ts | 29 +++++++++++-- 2 files changed, 66 insertions(+), 6 deletions(-) diff --git a/admin-frontend-vue/src/pages/tasks/ShopDataTasksPage.vue b/admin-frontend-vue/src/pages/tasks/ShopDataTasksPage.vue index ed529e42..341cd90b 100644 --- a/admin-frontend-vue/src/pages/tasks/ShopDataTasksPage.vue +++ b/admin-frontend-vue/src/pages/tasks/ShopDataTasksPage.vue @@ -56,6 +56,25 @@ const pagedPermissionItems = computed(() => const busyKey = ref('') const deletingKey = ref('') +/** 下载进度弹窗:percent=0~100;indeterminate=true 表示后端未回 Content-Length,只显示流动条。 */ +const downloadProgress = reactive({ visible: false, percent: 0, indeterminate: false, label: '' }) + +function startDownloadProgress(label: string) { + downloadProgress.label = label + downloadProgress.percent = 0 + downloadProgress.indeterminate = false + downloadProgress.visible = true +} + +function updateDownloadProgress(percent: number) { + if (percent < 0) { + downloadProgress.indeterminate = true + return + } + downloadProgress.indeterminate = false + downloadProgress.percent = percent +} + /** 旧版状态 label 映射(对齐 admin.js 状态徽章)。 */ function statusLabel(status: string): string { const map: Record = { @@ -185,9 +204,11 @@ async function downloadRow(row: ShopDataResultRow) { return } busyKey.value = row.resultId + startDownloadProgress(`正在下载「${row.shopName || '-'}」的数据文件`) try { - const { blob, filename } = await fetchShopDataResultDownload(row.resultId) + const { blob, filename } = await fetchShopDataResultDownload(row.resultId, updateDownloadProgress) saveBlob(blob, filename) + downloadProgress.visible = false ElMessage.success('下载已开始') } catch (error) { ElMessage.error(error instanceof Error ? error.message : '下载失败') @@ -203,9 +224,11 @@ async function downloadBatch() { return } busyKey.value = 'batch' + startDownloadProgress(`正在打包下载 ${resultIds.length} 个结果文件`) try { - const zip = await requestShopDataZipDownload(resultIds) + const zip = await requestShopDataZipDownload(resultIds, updateDownloadProgress) saveBlob(zip.blob, `店铺数据批量下载_${resultIds.length}.zip`) + downloadProgress.visible = false const note = zip.errorCount ? `,${zip.errorCount} 个失败(详见包内错误清单)` : '' ElMessage.success(`已下载 ${zip.fileCount ?? resultIds.length} 个文件${note}`) } catch (error) { @@ -401,6 +424,16 @@ onMounted(load) 保存 + + + +

{{ downloadProgress.indeterminate ? '正在接收数据,大小未知,请稍候…' : `已完成 ${downloadProgress.percent}%` }}

+
@@ -712,6 +745,12 @@ h3 { color: #8293a5; font-size: 12px; } +.sd-progress-hint { + margin: 10px 0 0; + text-align: center; + color: #5b6f83; + font-size: 13px; +} .empty-tip { grid-column: 1 / -1; padding: 44px 24px; diff --git a/admin-frontend-vue/src/pages/tasks/shop-data-api.ts b/admin-frontend-vue/src/pages/tasks/shop-data-api.ts index 2a062c47..6d8f1494 100644 --- a/admin-frontend-vue/src/pages/tasks/shop-data-api.ts +++ b/admin-frontend-vue/src/pages/tasks/shop-data-api.ts @@ -27,10 +27,15 @@ export interface ShopDataZipDownloadResult { errorCount?: number } -/** 批量下载选中结果文件为 zip:POST /download-zip(blob);从响应头读成功/失败计数。 */ -export async function requestShopDataZipDownload(resultIds: readonly (number | string)[]): Promise { +/** 批量下载选中结果文件为 zip:POST /download-zip(blob);从响应头读成功/失败计数。 + * onProgress: 可选下载进度回调(0-100,后端未回 Content-Length 时退化为不定进度)。 */ +export async function requestShopDataZipDownload( + resultIds: readonly (number | string)[], + onProgress?: (percent: number) => void, +): Promise { const { data, headers } = await http.post(`${SHOP_DATA_CRAWL_TASKS_ENDPOINT}/download-zip`, toShopDataZipRequest(resultIds), { responseType: 'blob', + ...(onProgress ? { onDownloadProgress: toAxiosProgress(onProgress) } : {}), }) const counts = downloadZipHeaderCounts(headers as Record) return { blob: data, fileCount: counts.fileCount, errorCount: counts.errorCount } @@ -41,15 +46,31 @@ export interface ShopDataResultDownload { filename: string } -/** 下载单店最新采集文件(按每日累计档 id):GET /api/admin/shop-data-crawl-tasks/daily-files/{id}/download。 */ -export async function fetchShopDataResultDownload(resultId: number | string): Promise { +/** 下载单店最新采集文件(按每日累计档 id):GET /api/admin/shop-data-crawl-tasks/daily-files/{id}/download。 + * onProgress: 可选下载进度回调(0-100)。 */ +export async function fetchShopDataResultDownload( + resultId: number | string, + onProgress?: (percent: number) => void, +): Promise { const { data, headers } = await http.get(`${SHOP_DATA_CRAWL_TASKS_ENDPOINT}/daily-files/${resultId}/download`, { responseType: 'blob', + ...(onProgress ? { onDownloadProgress: toAxiosProgress(onProgress) } : {}), }) const disposition = (headers as Record | undefined)?.['content-disposition'] return { blob: data, filename: shopDataFilenameFromDisposition(disposition, `shop-data-task-${resultId}.xlsx`) } } +/** 将 axios 进度事件转为 0-100 百分比;响应未回 Content-Length 时报 -1,前端显示不定进度。 */ +function toAxiosProgress(onProgress: (percent: number) => void) { + return (event: { loaded: number; total?: number }) => { + if (event.total) { + onProgress(Math.min(100, Math.round((event.loaded / event.total) * 100))) + } else { + onProgress(-1) + } + } +} + /** 删除该店这条店铺数据记录(按每日累计档 id,管理端显式操作):DELETE .../daily-files/{id}。 */ export async function deleteShopDataResultHistory(resultId: number | string): Promise { await http.delete(`${SHOP_DATA_CRAWL_TASKS_ENDPOINT}/daily-files/${resultId}`)