diff --git a/frontend-vue/src/pages/brand/components/BrandDeleteBrandTab.vue b/frontend-vue/src/pages/brand/components/BrandDeleteBrandTab.vue index 535fea2..10a659d 100644 --- a/frontend-vue/src/pages/brand/components/BrandDeleteBrandTab.vue +++ b/frontend-vue/src/pages/brand/components/BrandDeleteBrandTab.vue @@ -224,9 +224,14 @@ import { } from '@/shared/api/java-modules' import { getPywebviewApi, type UploadedJavaFile } from '@/shared/bridges/pywebview' +interface SessionDeleteBrandItem extends DeleteBrandResultItem { + _pushed?: boolean + _completed?: boolean +} + interface StoredCurrentTask { taskId: number - items: DeleteBrandResultItem[] + items: SessionDeleteBrandItem[] createdAt: number queuePushResult?: string queuePayloadText?: string @@ -318,6 +323,84 @@ const historySectionItems = computed(() => ); const hasVisibleItems = computed(() => currentSectionItems.value.length > 0 || historySectionItems.value.length > 0) +function persistSessionTasks() { + if (typeof window !== 'undefined') { + window.localStorage.setItem(getStorageKey(), JSON.stringify(sessionTasks.value)) + } +} + +function isMultiFileTask(taskId?: number) { + if (!taskId) return false + const sessionTask = sessionTasks.value.find(t => t.taskId === taskId) + return (sessionTask?.items.length || 0) > 1 +} + +function findSessionTask(taskId?: number) { + if (!taskId) return null + return sessionTasks.value.find(t => t.taskId === taskId) || null +} + +function isSameDeleteBrandItem(left: Pick | null | undefined, + right: Pick | null | undefined) { + if (!left || !right) return false + if (left.resultId != null && right.resultId != null) { + return left.resultId === right.resultId + } + if (left.fileKey && right.fileKey) { + return left.fileKey === right.fileKey + } + return left.sourceFilename === right.sourceFilename +} + +function findSessionItem(taskId: number | undefined, item: DeleteBrandResultItem) { + const sessionTask = findSessionTask(taskId) + if (!sessionTask) return null + return sessionTask.items.find(i => isSameDeleteBrandItem(i, item)) || null +} + +function updateCompletedItemsFromProgress(taskId: number | undefined) { + if (!taskId) return false + + const sessionTask = findSessionTask(taskId) + if (!sessionTask) return false + + let changed = false + const info = taskDetails.value[taskId]?.line_progress?.info + + // 多文件任务优先依据后端 finished_files 同步已完成文件数量。 + // 规则:按 sessionTask.items 的顺序,标记「已推送」中的前 N 个为 completed。 + if (isMultiFileTask(taskId) && info?.finished_files != null && info.finished_files >= 0) { + const pushedItems = sessionTask.items.filter(i => i._pushed) + const shouldCompleted = Math.max(0, Math.min(info.finished_files, pushedItems.length)) + + for (let i = 0; i < shouldCompleted; i += 1) { + if (!pushedItems[i]._completed) { + pushedItems[i]._completed = true + changed = true + } + } + } + + // 兼容单文件/兜底:当当前文件行进度到达末行时,标记活动文件完成。 + if (info?.file_name && info.current_line != null && info.total_lines != null && info.total_lines > 0) { + if (info.current_line >= info.total_lines) { + const activeItem = currentSectionItems.value.find(item => getItemKey(item) === activeItemKey.value) + const sessionItem = activeItem && activeItem.taskId === taskId + ? findSessionItem(taskId, activeItem) + : sessionTask.items.find(i => i.sourceFilename === info.file_name) + if (sessionItem && !sessionItem._completed) { + sessionItem._completed = true + changed = true + } + } + } + + if (changed) { + persistSessionTasks() + } + return changed +} + function getTaskStatus(taskId?: number) { if (!taskId) return '' return taskDetails.value[taskId]?.task?.status || '' @@ -342,14 +425,20 @@ function shouldShowProgress(item: DeleteBrandResultItem) { function normalizeDeleteBrandItems(items: DeleteBrandResultItem[]) { return items.map((item) => { if (item.matchStatus === 'MATCHED') { - return item + return { + ...item, + _pushed: false, + _completed: false, + } as SessionDeleteBrandItem } return { ...item, matched: false, openStoreUrl: undefined, error: item.error || undefined, - } + _pushed: false, + _completed: false, + } as SessionDeleteBrandItem }) } @@ -404,11 +493,14 @@ function formatProgress(taskId: number) { if (info.file_index && info.file_total) parts.push(`文件:${info.file_index}/${info.file_total}`) if (info.file_name) parts.push(`当前文件:${info.file_name}`) if (info.current_line != null && info.total_lines != null) { - parts.push(`进度:${info.current_line}/${info.total_lines}`) + parts.push(`${isMultiFileTask(taskId) ? '当前文件进度' : '进度'}:${info.current_line}/${info.total_lines}`) } if (info.current_country) parts.push(`国家:${info.current_country}`) if (info.current_asin) parts.push(`ASIN:${info.current_asin}`) - if (info.finished_files != null) parts.push(`已完成文件:${info.finished_files}`) + if (info.finished_files != null) { + const total = info.file_total && info.file_total > 0 ? `/${info.file_total}` : '' + parts.push(`已完成文件:${info.finished_files}${total}`) + } return parts.join('|') } @@ -446,34 +538,41 @@ function getPollingTaskIds() { function getQueueStatus(item: DeleteBrandResultItem) { const taskId = item.taskId if (!taskId) return '' - const sessionTask = sessionTasks.value.find(t => t.taskId === taskId) + const sessionTask = findSessionTask(taskId) if (!sessionTask) return '' + const sessionItem = findSessionItem(taskId, item) const status = getTaskStatus(taskId) - if (status === 'SUCCESS' || status === 'FAILED' || item.taskStatus === 'SUCCESS' || item.taskStatus === 'FAILED') { + const info = taskDetails.value[taskId]?.line_progress + const multiFileTask = isMultiFileTask(taskId) + + if (!multiFileTask && (status === 'SUCCESS' || status === 'FAILED' || item.taskStatus === 'SUCCESS' || item.taskStatus === 'FAILED')) { return '已被全局判定为终态' } - const sessionItem = sessionTask.items.find(i => i.resultId === item.resultId || i.sourceFilename === item.sourceFilename) - - const info = taskDetails.value[taskId]?.line_progress - - // 此时这个特定文件是否正好被后端报了进度名 if (status === 'RUNNING' && info?.has_progress && info?.info?.file_name === item.sourceFilename) { return '处理中' } - // 查查是否已经完结了这个特定文件 - const detail = taskDetails.value[taskId] - if (detail?.items) { - // 后端返回的 items 是 `resultJson` 解析来的,里面只含有已经做完的! - const finished = detail.items.find((i: DeleteBrandResultItem) => i.sourceFilename === item.sourceFilename) - if (finished) { - return '本文件解析完毕' + if (sessionItem?._completed) { + return '本文件解析完毕' + } + + if (multiFileTask && status === 'SUCCESS') { + return '本文件解析完毕' + } + + if (!multiFileTask) { + const detail = taskDetails.value[taskId] + if (detail?.items) { + const finished = detail.items.find((i: DeleteBrandResultItem) => isSameDeleteBrandItem(i, item)) + if (finished && status === 'SUCCESS') { + return '本文件解析完毕' + } } } - if (sessionItem && (sessionItem as any)._pushed) { + if (sessionItem?._pushed) { return '已入队' } @@ -497,12 +596,14 @@ function mergeTaskDetailItemsIntoSession(detail: DeleteBrandTaskDetailVo) { let changed = false const mergedItems = sessionTask.items.map((item) => { - const latest = detail.items?.find((candidate) => candidate.resultId === item.resultId || candidate.sourceFilename === item.sourceFilename) + const latest = detail.items?.find((candidate) => isSameDeleteBrandItem(candidate, item)) if (!latest) return item const merged = { ...item, ...latest, success: item.success ?? latest.success, + _pushed: item._pushed, + _completed: item._completed, } if (JSON.stringify(merged) !== JSON.stringify(item)) { changed = true @@ -516,8 +617,8 @@ function mergeTaskDetailItemsIntoSession(detail: DeleteBrandTaskDetailVo) { changed = true } - if (changed && typeof window !== 'undefined') { - window.localStorage.setItem(getStorageKey(), JSON.stringify(sessionTasks.value)) + if (changed) { + persistSessionTasks() } return changed } @@ -536,6 +637,9 @@ async function refreshTaskDetails(taskIds?: number[]) { if (mergeTaskDetailItemsIntoSession(detail)) { changed = true } + if (updateCompletedItemsFromProgress(id)) { + changed = true + } } } if (changed) { @@ -833,6 +937,8 @@ function findNextAutoRunnableItem() { return currentSectionItems.value.find((item) => { if (!item.openStoreUrl) return false if (getItemKey(item) === activeItemKey.value) return false + const sessionItem = findSessionItem(item.taskId, item) + if (sessionItem?._completed) return false return getQueueStatus(item) === '未入队' }) } @@ -870,9 +976,10 @@ async function runItem(item: DeleteBrandResultItem, options?: { auto?: boolean } if (pushResult?.success) { qpr = `已推送文件 ${item.sourceFilename},当前队列长度:${pushResult.queue_size ?? '-'}` - const sessionItem = sessionTask.items.find(i => i.resultId === item.resultId || i.sourceFilename === item.sourceFilename) + const sessionItem = findSessionItem(taskId, item) if (sessionItem) { - (sessionItem as any)._pushed = true + sessionItem._pushed = true + sessionItem._completed = false } activeItemKey.value = getItemKey(item) saveSessionTask(taskId, sessionTask.items, qpr, queuePayloadText.value) @@ -892,10 +999,21 @@ async function maybeAutoAdvance() { if (currentKey) { const currentItem = currentSectionItems.value.find(item => getItemKey(item) === currentKey) if (currentItem) { + const taskId = currentItem.taskId + const multiFileTask = isMultiFileTask(taskId) + if (multiFileTask) { + updateCompletedItemsFromProgress(taskId) + } const status = getQueueStatus(currentItem) if (status === '已入队' || status === '处理中') { return } + if (multiFileTask) { + const sessionItem = findSessionItem(taskId, currentItem) + if (sessionItem && !sessionItem._completed) { + return + } + } } }