feat(后台/店铺数据): 结果下载增加进度弹窗
批量打包下载与单店下载接入 axios onDownloadProgress,progress 弹窗显示 百分比;后端未回 Content-Length 时退化为 indeterminate 流动条,避免用户 在大 zip 下载期间以为页面无响应。
This commit is contained in:
@@ -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<string, string> = {
|
||||
@@ -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)
|
||||
<el-button type="primary" :loading="permissionSaving" @click="savePermission">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="downloadProgress.visible" :title="downloadProgress.label" width="440px" :close-on-click-modal="false" :close-on-press-escape="false" :show-close="false">
|
||||
<el-progress
|
||||
:percentage="downloadProgress.indeterminate ? 50 : downloadProgress.percent"
|
||||
:indeterminate="downloadProgress.indeterminate"
|
||||
:duration="downloadProgress.indeterminate ? 3 : undefined"
|
||||
:stroke-width="14"
|
||||
/>
|
||||
<p class="sd-progress-hint">{{ downloadProgress.indeterminate ? '正在接收数据,大小未知,请稍候…' : `已完成 ${downloadProgress.percent}%` }}</p>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -27,10 +27,15 @@ export interface ShopDataZipDownloadResult {
|
||||
errorCount?: number
|
||||
}
|
||||
|
||||
/** 批量下载选中结果文件为 zip:POST /download-zip(blob);从响应头读成功/失败计数。 */
|
||||
export async function requestShopDataZipDownload(resultIds: readonly (number | string)[]): Promise<ShopDataZipDownloadResult> {
|
||||
/** 批量下载选中结果文件为 zip:POST /download-zip(blob);从响应头读成功/失败计数。
|
||||
* onProgress: 可选下载进度回调(0-100,后端未回 Content-Length 时退化为不定进度)。 */
|
||||
export async function requestShopDataZipDownload(
|
||||
resultIds: readonly (number | string)[],
|
||||
onProgress?: (percent: number) => void,
|
||||
): Promise<ShopDataZipDownloadResult> {
|
||||
const { data, headers } = await http.post<Blob>(`${SHOP_DATA_CRAWL_TASKS_ENDPOINT}/download-zip`, toShopDataZipRequest(resultIds), {
|
||||
responseType: 'blob',
|
||||
...(onProgress ? { onDownloadProgress: toAxiosProgress(onProgress) } : {}),
|
||||
})
|
||||
const counts = downloadZipHeaderCounts(headers as Record<string, string>)
|
||||
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<ShopDataResultDownload> {
|
||||
/** 下载单店最新采集文件(按每日累计档 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<ShopDataResultDownload> {
|
||||
const { data, headers } = await http.get<Blob>(`${SHOP_DATA_CRAWL_TASKS_ENDPOINT}/daily-files/${resultId}/download`, {
|
||||
responseType: 'blob',
|
||||
...(onProgress ? { onDownloadProgress: toAxiosProgress(onProgress) } : {}),
|
||||
})
|
||||
const disposition = (headers as Record<string, string> | 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<void> {
|
||||
await http.delete<unknown>(`${SHOP_DATA_CRAWL_TASKS_ENDPOINT}/daily-files/${resultId}`)
|
||||
|
||||
Reference in New Issue
Block a user