feat(后台/店铺数据): 结果下载增加进度弹窗
批量打包下载与单店下载接入 axios onDownloadProgress,progress 弹窗显示 百分比;后端未回 Content-Length 时退化为 indeterminate 流动条,避免用户 在大 zip 下载期间以为页面无响应。
This commit is contained in:
@@ -56,6 +56,25 @@ const pagedPermissionItems = computed(() =>
|
|||||||
const busyKey = ref('')
|
const busyKey = ref('')
|
||||||
const deletingKey = 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 状态徽章)。 */
|
/** 旧版状态 label 映射(对齐 admin.js 状态徽章)。 */
|
||||||
function statusLabel(status: string): string {
|
function statusLabel(status: string): string {
|
||||||
const map: Record<string, string> = {
|
const map: Record<string, string> = {
|
||||||
@@ -185,9 +204,11 @@ async function downloadRow(row: ShopDataResultRow) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
busyKey.value = row.resultId
|
busyKey.value = row.resultId
|
||||||
|
startDownloadProgress(`正在下载「${row.shopName || '-'}」的数据文件`)
|
||||||
try {
|
try {
|
||||||
const { blob, filename } = await fetchShopDataResultDownload(row.resultId)
|
const { blob, filename } = await fetchShopDataResultDownload(row.resultId, updateDownloadProgress)
|
||||||
saveBlob(blob, filename)
|
saveBlob(blob, filename)
|
||||||
|
downloadProgress.visible = false
|
||||||
ElMessage.success('下载已开始')
|
ElMessage.success('下载已开始')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ElMessage.error(error instanceof Error ? error.message : '下载失败')
|
ElMessage.error(error instanceof Error ? error.message : '下载失败')
|
||||||
@@ -203,9 +224,11 @@ async function downloadBatch() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
busyKey.value = 'batch'
|
busyKey.value = 'batch'
|
||||||
|
startDownloadProgress(`正在打包下载 ${resultIds.length} 个结果文件`)
|
||||||
try {
|
try {
|
||||||
const zip = await requestShopDataZipDownload(resultIds)
|
const zip = await requestShopDataZipDownload(resultIds, updateDownloadProgress)
|
||||||
saveBlob(zip.blob, `店铺数据批量下载_${resultIds.length}.zip`)
|
saveBlob(zip.blob, `店铺数据批量下载_${resultIds.length}.zip`)
|
||||||
|
downloadProgress.visible = false
|
||||||
const note = zip.errorCount ? `,${zip.errorCount} 个失败(详见包内错误清单)` : ''
|
const note = zip.errorCount ? `,${zip.errorCount} 个失败(详见包内错误清单)` : ''
|
||||||
ElMessage.success(`已下载 ${zip.fileCount ?? resultIds.length} 个文件${note}`)
|
ElMessage.success(`已下载 ${zip.fileCount ?? resultIds.length} 个文件${note}`)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -401,6 +424,16 @@ onMounted(load)
|
|||||||
<el-button type="primary" :loading="permissionSaving" @click="savePermission">保存</el-button>
|
<el-button type="primary" :loading="permissionSaving" @click="savePermission">保存</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -712,6 +745,12 @@ h3 {
|
|||||||
color: #8293a5;
|
color: #8293a5;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
.sd-progress-hint {
|
||||||
|
margin: 10px 0 0;
|
||||||
|
text-align: center;
|
||||||
|
color: #5b6f83;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
.empty-tip {
|
.empty-tip {
|
||||||
grid-column: 1 / -1;
|
grid-column: 1 / -1;
|
||||||
padding: 44px 24px;
|
padding: 44px 24px;
|
||||||
|
|||||||
@@ -27,10 +27,15 @@ export interface ShopDataZipDownloadResult {
|
|||||||
errorCount?: number
|
errorCount?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 批量下载选中结果文件为 zip:POST /download-zip(blob);从响应头读成功/失败计数。 */
|
/** 批量下载选中结果文件为 zip:POST /download-zip(blob);从响应头读成功/失败计数。
|
||||||
export async function requestShopDataZipDownload(resultIds: readonly (number | string)[]): Promise<ShopDataZipDownloadResult> {
|
* 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), {
|
const { data, headers } = await http.post<Blob>(`${SHOP_DATA_CRAWL_TASKS_ENDPOINT}/download-zip`, toShopDataZipRequest(resultIds), {
|
||||||
responseType: 'blob',
|
responseType: 'blob',
|
||||||
|
...(onProgress ? { onDownloadProgress: toAxiosProgress(onProgress) } : {}),
|
||||||
})
|
})
|
||||||
const counts = downloadZipHeaderCounts(headers as Record<string, string>)
|
const counts = downloadZipHeaderCounts(headers as Record<string, string>)
|
||||||
return { blob: data, fileCount: counts.fileCount, errorCount: counts.errorCount }
|
return { blob: data, fileCount: counts.fileCount, errorCount: counts.errorCount }
|
||||||
@@ -41,15 +46,31 @@ export interface ShopDataResultDownload {
|
|||||||
filename: string
|
filename: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 下载单店最新采集文件(按每日累计档 id):GET /api/admin/shop-data-crawl-tasks/daily-files/{id}/download。 */
|
/** 下载单店最新采集文件(按每日累计档 id):GET /api/admin/shop-data-crawl-tasks/daily-files/{id}/download。
|
||||||
export async function fetchShopDataResultDownload(resultId: number | string): Promise<ShopDataResultDownload> {
|
* 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`, {
|
const { data, headers } = await http.get<Blob>(`${SHOP_DATA_CRAWL_TASKS_ENDPOINT}/daily-files/${resultId}/download`, {
|
||||||
responseType: 'blob',
|
responseType: 'blob',
|
||||||
|
...(onProgress ? { onDownloadProgress: toAxiosProgress(onProgress) } : {}),
|
||||||
})
|
})
|
||||||
const disposition = (headers as Record<string, string> | undefined)?.['content-disposition']
|
const disposition = (headers as Record<string, string> | undefined)?.['content-disposition']
|
||||||
return { blob: data, filename: shopDataFilenameFromDisposition(disposition, `shop-data-task-${resultId}.xlsx`) }
|
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}。 */
|
/** 删除该店这条店铺数据记录(按每日累计档 id,管理端显式操作):DELETE .../daily-files/{id}。 */
|
||||||
export async function deleteShopDataResultHistory(resultId: number | string): Promise<void> {
|
export async function deleteShopDataResultHistory(resultId: number | string): Promise<void> {
|
||||||
await http.delete<unknown>(`${SHOP_DATA_CRAWL_TASKS_ENDPOINT}/daily-files/${resultId}`)
|
await http.delete<unknown>(`${SHOP_DATA_CRAWL_TASKS_ENDPOINT}/daily-files/${resultId}`)
|
||||||
|
|||||||
Reference in New Issue
Block a user