feat(brand): 品牌检测任务统一轮询驱动并展示行级实时进度
任务面板状态改为独立轮询(getTaskPollIntervalMs 节奏),刷新/提交/取消/删除后 按是否有运行中任务自动续轮询或停表;运行中任务通过详情接口拉取 Redis 行级进度, 进度按"已处理品牌行/总行(含查询失败)"推进并展示 crawling/assembling/uploading 阶段文案
This commit is contained in:
@@ -68,7 +68,7 @@
|
||||
|
||||
<section class="right-panel">
|
||||
<div class="panel-actions-row">
|
||||
<button type="button" class="btn-refresh" @click="loadTasks">刷新任务状态</button>
|
||||
<button type="button" class="btn-refresh" @click="syncTasksAndResumePolling">刷新任务状态</button>
|
||||
</div>
|
||||
<TaskCenterPanel
|
||||
:on-batch-delete="batchDeleteHistory"
|
||||
@@ -113,6 +113,7 @@ import { getPywebviewApi } from '@/shared/bridges/pywebview'
|
||||
import {
|
||||
cancelBrandTask,
|
||||
deleteBrandTask,
|
||||
getBrandTask,
|
||||
getBrandTaskDownloadUrl,
|
||||
getBrandTasks,
|
||||
runBrandNow,
|
||||
@@ -120,6 +121,8 @@ import {
|
||||
} from '@/shared/api/brand'
|
||||
import { checkSelectedFiles, EXCEL_EXTENSIONS } from '@/shared/dispatch-guard.ts'
|
||||
import { passGuard } from '@/shared/dispatch-guard-ui'
|
||||
import { getTaskPollIntervalMs } from '@/shared/task-progress-config'
|
||||
import { createCategorizedTimers } from '@/shared/utils/categorized-timers'
|
||||
import type { UploadFileVo } from '@/shared/api/upload.ts'
|
||||
import type { UploadedFileRef } from '@/shared/api/types/upload.ts'
|
||||
import type { BrandExpandFolderItem } from '@/shared/api/types/modules/brand'
|
||||
@@ -147,8 +150,79 @@ function baseName(path: string) {
|
||||
return path.split(/[\\/]/).pop() || path
|
||||
}
|
||||
|
||||
const timers = createCategorizedTimers('brand-brand-task')
|
||||
let pollTimer: number | null = null
|
||||
/** taskId -> 行级进度快照(按“已处理品牌行 / 总品牌行”推进,成功或查询失败都计入完成数) */
|
||||
const lineProgressMap = ref<Record<string, { current: number; total: number; phase?: string }>>({})
|
||||
|
||||
const BRAND_PHASE_TEXT: Record<string, string> = {
|
||||
crawling: '品牌查询中',
|
||||
assembling: '结果生成中',
|
||||
uploading: '结果上传中',
|
||||
failed: '处理失败',
|
||||
}
|
||||
|
||||
function hasBusyBrandTask() {
|
||||
return tasks.value.some(isBusyBrandTask)
|
||||
}
|
||||
|
||||
function stopPolling() {
|
||||
if (pollTimer != null) {
|
||||
timers.clearTimer('task-poll', pollTimer)
|
||||
pollTimer = null
|
||||
}
|
||||
}
|
||||
|
||||
function scheduleNextPoll() {
|
||||
if (pollTimer != null) return
|
||||
pollTimer = timers.setTimeout('task-poll', () => {
|
||||
pollTimer = null
|
||||
void pollOnce()
|
||||
}, getTaskPollIntervalMs())
|
||||
}
|
||||
|
||||
async function pollOnce() {
|
||||
await loadTasks()
|
||||
const busy = tasks.value.filter(isBusyBrandTask)
|
||||
if (!busy.length) {
|
||||
stopPolling()
|
||||
return
|
||||
}
|
||||
// 清理已不在运行集合的任务旧快照,并拉取运行中任务的实时行级进度
|
||||
for (const key of Object.keys(lineProgressMap.value)) {
|
||||
if (!busy.some((b) => String(b.id) === key)) delete lineProgressMap.value[key]
|
||||
}
|
||||
for (const item of busy) {
|
||||
const id = Number(item.id)
|
||||
if (!id) continue
|
||||
try {
|
||||
const res = await getBrandTask(id)
|
||||
const lp = res?.line_progress
|
||||
if (lp?.has_progress && lp.info) {
|
||||
lineProgressMap.value[String(id)] = {
|
||||
current: Number(lp.info.current_line) || 0,
|
||||
total: Number(lp.info.total_lines) || 0,
|
||||
phase: lp.info.phase,
|
||||
}
|
||||
} else {
|
||||
delete lineProgressMap.value[String(id)]
|
||||
}
|
||||
} catch {
|
||||
// 单条详情失败不中断整轮轮询
|
||||
}
|
||||
}
|
||||
scheduleNextPoll()
|
||||
}
|
||||
|
||||
/** 拉一次列表;若仍有运行/排队任务则保持自动轮询,否则停表 */
|
||||
async function syncTasksAndResumePolling() {
|
||||
await loadTasks()
|
||||
if (hasBusyBrandTask()) {
|
||||
scheduleNextPoll()
|
||||
} else {
|
||||
stopPolling()
|
||||
}
|
||||
}
|
||||
|
||||
function isRunning(item: BrandTaskItem) {
|
||||
return (item.status || '').toLowerCase() === 'running'
|
||||
@@ -227,8 +301,33 @@ function itemSource(item: TaskItemView): BrandTaskItem {
|
||||
}
|
||||
|
||||
function toBrandTaskView(item: BrandTaskItem): TaskItemView {
|
||||
const total = Number(item.progress_total) || 0
|
||||
const current = Number(item.progress_current) || 0
|
||||
const totalFiles = Number(item.progress_total) || 0
|
||||
const doneFiles = Number(item.progress_current) || 0
|
||||
const lp = isRunning(item) ? lineProgressMap.value[String(item.id)] : undefined
|
||||
const lineTotal = lp?.total || 0
|
||||
const lineCurrent = lp?.current || 0
|
||||
const extraLines = [
|
||||
...(item.file_paths?.length ? [`文件:${item.file_paths.length} 个`] : []),
|
||||
...(lp && lineTotal > 0 ? [`已处理 ${lineCurrent} 行 / 共 ${lineTotal} 行(含查询失败)`] : []),
|
||||
...(errorMessage(item) ? [`错误:${errorMessage(item)}`] : []),
|
||||
]
|
||||
// 行级进度优先:完成数 = 已处理品牌行(成功或查询失败都计入),进度 = 完成数 / 总数 * 100
|
||||
let progress: TaskItemView['progress'] = null
|
||||
if (isRunning(item)) {
|
||||
if (lp && lineTotal > 0) {
|
||||
progress = {
|
||||
percent: Math.max(0, Math.min(100, Math.round((lineCurrent / lineTotal) * 100))),
|
||||
stage: (lp.phase && BRAND_PHASE_TEXT[lp.phase]) || '品牌检测中',
|
||||
countLabel: `${lineCurrent}/${lineTotal}`,
|
||||
}
|
||||
} else {
|
||||
progress = {
|
||||
percent: taskProgress(item),
|
||||
stage: '任务进度',
|
||||
countLabel: totalFiles > 0 ? `${doneFiles}/${totalFiles}` : undefined,
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
key: `brand-${item.id}`,
|
||||
title: (item.desc || '').trim() || '品牌检测',
|
||||
@@ -237,17 +336,8 @@ function toBrandTaskView(item: BrandTaskItem): TaskItemView {
|
||||
finishedAt: isTerminal(item) ? formatDateTime(item.updated_at) : '',
|
||||
statusText: statusText(item.status),
|
||||
statusClass: statusClass(item.status),
|
||||
extraLines: [
|
||||
...(item.file_paths?.length ? [`文件:${item.file_paths.length} 个`] : []),
|
||||
...(errorMessage(item) ? [`错误:${errorMessage(item)}`] : []),
|
||||
],
|
||||
progress: isRunning(item)
|
||||
? {
|
||||
percent: taskProgress(item),
|
||||
stage: '任务进度',
|
||||
countLabel: total > 0 ? `${current}/${total}` : undefined,
|
||||
}
|
||||
: null,
|
||||
extraLines,
|
||||
progress,
|
||||
source: item,
|
||||
}
|
||||
}
|
||||
@@ -360,7 +450,7 @@ async function submitRun() {
|
||||
throw new Error((res as { error?: string }).error || '运行失败')
|
||||
}
|
||||
uploadedFiles.value = []
|
||||
await loadTasks()
|
||||
await syncTasksAndResumePolling()
|
||||
} catch (error) {
|
||||
ElMessage.error(error instanceof Error ? error.message : '提交失败')
|
||||
} finally {
|
||||
@@ -392,7 +482,7 @@ async function cancelTask(item: BrandTaskItem) {
|
||||
const res = await cancelBrandTask(item.id)
|
||||
if (!res.success) throw new Error((res as { error?: string }).error || '取消失败')
|
||||
ElMessage.success('已取消')
|
||||
await loadTasks()
|
||||
await syncTasksAndResumePolling()
|
||||
} catch (error) {
|
||||
ElMessage.error(error instanceof Error ? error.message : '取消失败')
|
||||
}
|
||||
@@ -408,7 +498,7 @@ async function deleteTask(item: BrandTaskItem) {
|
||||
const res = await deleteBrandTask(item.id)
|
||||
if (!res.success) throw new Error((res as { error?: string }).error || '删除失败')
|
||||
ElMessage.success('已删除')
|
||||
await loadTasks()
|
||||
await syncTasksAndResumePolling()
|
||||
} catch (error) {
|
||||
ElMessage.error(error instanceof Error ? error.message : '删除失败')
|
||||
}
|
||||
@@ -461,14 +551,11 @@ async function downloadTemplate(kind: 'xlsx' | 'zip') {
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
void loadTasks()
|
||||
void syncTasksAndResumePolling()
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (pollTimer) {
|
||||
window.clearTimeout(pollTimer)
|
||||
pollTimer = null
|
||||
}
|
||||
stopPolling()
|
||||
})
|
||||
|
||||
/**
|
||||
|
||||
@@ -45,9 +45,28 @@ export interface BrandTaskListResponse {
|
||||
error?: string
|
||||
}
|
||||
|
||||
/** Java 详情接口返回的行级进度(Redis 实时进度,每提交一个分片推进 current_line) */
|
||||
export interface BrandLineProgressInfo {
|
||||
file_index?: number
|
||||
file_total?: number
|
||||
file_name?: string
|
||||
/** 已处理行/品牌数 */
|
||||
current_line?: number
|
||||
/** 总行/品牌数 */
|
||||
total_lines?: number
|
||||
/** crawling / assembling / uploading / failed */
|
||||
phase?: string
|
||||
}
|
||||
|
||||
export interface BrandLineProgress {
|
||||
has_progress?: boolean
|
||||
info?: BrandLineProgressInfo
|
||||
}
|
||||
|
||||
export interface BrandTaskDetailResponse {
|
||||
success: boolean
|
||||
task?: BrandTaskItem
|
||||
line_progress?: BrandLineProgress
|
||||
error?: string
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user