新需求更新 同步更新
This commit is contained in:
@@ -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; }
|
||||
|
||||
Reference in New Issue
Block a user