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}`)