新需求更新 同步更新

This commit is contained in:
supernijia
2026-08-06 01:11:54 +08:00
parent 9048bbb7f8
commit 28e7fce11c
112 changed files with 7739 additions and 637 deletions
@@ -3,7 +3,7 @@
<BrandTopBar active="collect-data" />
<div class="main-content">
<aside class="left-panel">
<aside class="left-panel left-panel--with-template">
<div class="section-title">上传文件</div>
<div class="upload-zone">
<div class="hint">选择需要采集的 Excel 文件或文件夹默认根目录采集前数据.xlsx后续将按下方筛选条件交给 Python 端进行数据采集</div>
@@ -112,6 +112,8 @@
<div class="queue-debug-line">{{ queuePushResult }}</div>
<pre v-if="queuePayloadText" class="queue-debug-payload">{{ queuePayloadText }}</pre>
</div>
<ModuleTemplateDownload module-code="collect-data" filename="数据采集 文档格式.xlsx" />
</aside>
<section class="right-panel">
@@ -151,6 +153,12 @@
<div class="files">行数{{ item.rowCount ?? '-' }}</div>
<div class="files">总数据去重过滤{{ item.dedupeFilteredCount ?? 0 }}</div>
<div class="files">不符合 ASIN 过滤{{ item.invalidFilteredCount ?? 0 }}</div>
<div v-if="taskKeywordProgress(item)" class="files">{{ taskKeywordProgress(item) }}</div>
<div v-if="taskStageProgress(item)" class="files">{{ taskStageProgress(item) }}</div>
<div class="task-progress">
<div class="task-progress-meta"><span>任务进度</span><span>{{ taskProgressPercent(item) }}%</span></div>
<div class="task-progress-track"><div class="task-progress-fill" :style="{ width: `${taskProgressPercent(item)}%` }"></div></div>
</div>
</div>
<div class="task-right">
<span class="status running">{{ statusText(item) }}</span>
@@ -176,6 +184,10 @@
<div class="files">不符合 ASIN 过滤{{ item.invalidFilteredCount ?? 0 }}</div>
<div v-if="item.resultFilename" class="files">{{ item.resultFilename }}</div>
<div v-if="item.error" class="files">错误{{ item.error }}</div>
<div class="task-progress">
<div class="task-progress-meta"><span>任务进度</span><span>{{ taskProgressPercent(item) }}%</span></div>
<div class="task-progress-track"><div class="task-progress-fill" :style="{ width: `${taskProgressPercent(item)}%` }"></div></div>
</div>
</div>
<div class="task-right">
<span class="status" :class="statusClass(item)">{{ statusText(item) }}</span>
@@ -195,6 +207,7 @@
import { computed, onBeforeUnmount, onMounted, reactive, ref } from 'vue'
import { ElMessage } from 'element-plus'
import BrandTopBar from '@/pages/brand/components/BrandTopBar.vue'
import ModuleTemplateDownload from '@/shared/components/ModuleTemplateDownload.vue'
import { expandBrandFolderRecursive, type BrandExpandFolderItem } from '@/shared/api/brand'
import { getPywebviewApi } from '@/shared/bridges/pywebview'
import { useTaskProgressLoop } from '@/shared/composables/useTaskProgressLoop'
@@ -210,6 +223,7 @@ import {
getCollectDataTaskProgressBatch,
deleteCollectDataTask,
deleteCollectDataHistory,
failCollectDataTask,
type CollectDataDashboardVo,
type CollectDataHistoryItem,
type CollectDataParseVo,
@@ -284,6 +298,10 @@ const progressLoop = useTaskProgressLoop<CollectDataTaskDetailVo>({
? { ...prev, task: { ...(prev.task || {}), ...(detail.task || {}) }, items: detail.items ?? prev.items }
: detail,
}
const latestItem = detail.items?.[0]
currentItems.value = currentItems.value.map((item) => item.taskId === taskId
? { ...item, ...(latestItem || {}), taskStatus: detail.task?.status ?? latestItem?.taskStatus ?? item.taskStatus }
: item)
},
onTerminal: async (taskId) => {
if (taskSnapshots.value[taskId]) {
@@ -451,9 +469,6 @@ async function submitCollect() {
})
lastParseVo.value = vo
lastTaskId.value = vo.taskId ?? null
if (vo.taskId) {
progressLoop.add(vo.taskId)
}
ElMessage.success(`提交成功:任务 ${vo.taskNo || vo.taskId},落库 ${vo.acceptedRows ?? 0}`)
await Promise.all([loadDashboard(), loadHistory()])
} catch (error) {
@@ -474,8 +489,10 @@ async function pushToPythonQueue() {
return
}
pushing.value = true
let activated = false
try {
await activateCollectDataTask(lastTaskId.value)
activated = true
const payload = {
type: 'collect-data-run',
ts: Date.now(),
@@ -500,7 +517,15 @@ async function pushToPythonQueue() {
ElMessage.success('已推送到 Python 队列')
await loadDashboard()
} catch (error) {
if (activated && lastTaskId.value) {
try {
await failCollectDataTask(lastTaskId.value, error instanceof Error ? error.message : 'Python queue dispatch failed')
} catch {
/* 保留原始入队错误 */
}
}
ElMessage.error(error instanceof Error ? error.message : '推送失败')
await Promise.all([loadDashboard(), loadHistory()])
} finally {
pushing.value = false
}
@@ -540,6 +565,30 @@ function statusClass(item: CollectDataHistoryItem) {
return 'pending'
}
function taskProgressPercent(item: CollectDataHistoryItem) {
const value = Number(item.progressPercent)
if (Number.isFinite(value)) return Math.max(0, Math.min(100, Math.round(value)))
const status = normalizeTaskStatus(item)
return status === 'SUCCESS' || item.success ? 100 : 0
}
function taskStageProgress(item: CollectDataHistoryItem) {
if (item.collectStage === 'search') {
return `搜索页面:${item.searchCurrentPage ?? 0}/${item.searchTotalPages ?? '-'}`
}
if (item.collectStage === 'detail') {
return `商品详情采集:${item.detailProcessedAsins ?? 0}/${item.detailTotalAsins ?? 0} 个商品`
}
return ''
}
function taskKeywordProgress(item: CollectDataHistoryItem) {
const total = Number(item.totalRows)
if (!Number.isFinite(total) || total <= 0) return ''
const completed = Math.max(0, Number(item.processedRows) || 0)
return `关键词进度:第 ${Math.min(total, completed + 1)}/${total}`
}
function canDownload(item: CollectDataHistoryItem) {
const status = normalizeTaskStatus(item)
return Boolean(item.downloadUrl && (item.success || status === 'SUCCESS'))
@@ -573,15 +622,16 @@ async function loadHistory() {
try {
const vo = await getCollectDataHistory(50)
const items = vo.items || []
const running = items.filter((it) => {
const currentTasks = items.filter((it) => {
const s = (it.taskStatus || '').toUpperCase()
return s === 'PENDING' || s === 'RUNNING'
})
const running = currentTasks.filter((it) => (it.taskStatus || '').toUpperCase() === 'RUNNING')
historyItems.value = items.filter((it) => {
const s = (it.taskStatus || '').toUpperCase()
return s === 'SUCCESS' || s === 'FAILED'
})
// 历史接口里运行中的任务自动加入轮询;若返回里已没有该 taskId,则停掉对应轮询
// 仅轮询已入 Python 队列的 RUNNING 任务;PENDING 任务等待用户主动推送。
const seenTaskIds = new Set<number>()
for (const item of running) {
if (item.taskId) {
@@ -590,14 +640,14 @@ async function loadHistory() {
}
}
for (const taskId of [...progressLoop.taskIds.value]) {
const onScreen = items.some((it) => it.taskId === taskId)
const onScreen = running.some((it) => it.taskId === taskId)
if (!onScreen) {
progressLoop.remove(taskId)
}
}
// 合并:历史里 RUNNING/PENDING 的条目 + 轮询缓存里有快照但尚未在历史返回中的占位
const seen = new Set<number>(running.map((it) => it.taskId).filter((id): id is number => typeof id === 'number'))
const merged: CollectDataHistoryItem[] = [...running]
const seen = new Set<number>(currentTasks.map((it) => it.taskId).filter((id): id is number => typeof id === 'number'))
const merged: CollectDataHistoryItem[] = [...currentTasks]
for (const taskId of progressLoop.taskIds.value) {
if (seen.has(taskId)) continue
const detail = taskSnapshots.value[taskId]
@@ -746,6 +796,10 @@ onBeforeUnmount(() => {
.status.pending { background: rgba(149, 165, 166, .18); color: #bdc3c7; }
.download { padding: 6px 10px; color: #d6ecff; background: rgba(52, 152, 219, .18); }
.btn-delete { padding: 6px 10px; color: #ff8f8f; background: rgba(231, 76, 60, .12); }
.task-progress { margin-top: 8px; max-width: 520px; }
.task-progress-meta { display: flex; justify-content: space-between; color: #888; font-size: 11px; margin-bottom: 4px; }
.task-progress-track { height: 5px; border-radius: 3px; background: #333; overflow: hidden; }
.task-progress-fill { height: 100%; border-radius: inherit; background: #3498db; transition: width .25s ease; }
@media (max-width: 1100px) {
.main-content { flex-direction: column; height: auto; }
.left-panel { width: 100%; border-right: none; border-bottom: 1px solid #2a2a2a; }