feat(collect): show per-item collection activity timer

This commit is contained in:
chengma
2026-07-14 11:10:36 +08:00
parent da1da13fdf
commit 0abd7ffdff
4 changed files with 494 additions and 9 deletions
+218 -1
View File
@@ -20,6 +20,35 @@ def CollectWorker(*args, **kwargs):
def WriteBackWorker(*args, **kwargs):
return _call_package_attr("WriteBackWorker", _RealWriteBackWorker, *args, **kwargs)
COLLECT_ACTIVITY_STEP_LABELS = {
"preflight": "检查账号",
"match_account": "匹配账号",
"check_login": "检查账号登录",
"prepare_task": "准备采集",
"open_product": "打开商品页",
"wait_ready": "等待商品页加载",
"read_title": "读取标题",
"read_cover": "读取封面",
"download_cover": "下载封面",
"save_result": "保存采集结果",
"db_write": "保存采集结果",
}
def _collect_activity_step_label(step):
return COLLECT_ACTIVITY_STEP_LABELS.get(str(step or ""), "处理当前商品")
def _format_collect_elapsed(seconds):
total = max(0, int(seconds or 0))
hours, remainder = divmod(total, 3600)
minutes, seconds = divmod(remainder, 60)
if hours:
return f"{hours}:{minutes:02d}:{seconds:02d}"
return f"{minutes:02d}:{seconds:02d}"
class CollectTab(QWidget):
"""Tab 1: import Excel files and list imported tasks."""
@@ -56,6 +85,15 @@ class CollectTab(QWidget):
self.write_back_worker = None
self.write_back_thread = None
self.last_collect_run_id = None
self._collect_run_started_at = None
self._collect_task_started_at = None
self._collect_task_elapsed_seconds = 0
self._collect_activity_payload = {}
self._collect_terminal_text = ""
self._collect_stop_requested = False
self._collect_elapsed_timer = QTimer(self)
self._collect_elapsed_timer.setInterval(1000)
self._collect_elapsed_timer.timeout.connect(self._refresh_collect_activity)
self.import_button = QPushButton("导入 Excel...")
self.refresh_button = QPushButton("刷新")
@@ -101,6 +139,14 @@ class CollectTab(QWidget):
self.summary_label = QLabel("未导入任务")
self.summary_label.setTextFormat(Qt.RichText)
self.batch_progress_label = _build_batch_progress_overview("collectBatchProgressOverview")
self.collect_activity_label = QLabel("")
self.collect_activity_label.setObjectName("collectActivityLabel")
self.collect_activity_label.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
activity_sample = "正在采集 999/999 · 等待商品页加载 · 本条 99:59"
activity_width = self.collect_activity_label.fontMetrics().horizontalAdvance(activity_sample) + 24
self.collect_activity_label.setFixedWidth(activity_width)
self.collect_activity_label.setVisible(False)
self._set_collect_activity_style("muted")
self.match_detail_label = QLabel("")
self.show_all_button = QPushButton("全部")
self.show_unmatched_button = QPushButton("未匹配(0)")
@@ -112,6 +158,12 @@ class CollectTab(QWidget):
summary_layout.addWidget(self.show_all_button)
summary_layout.addWidget(self.show_unmatched_button)
batch_progress_layout = QHBoxLayout()
batch_progress_layout.setContentsMargins(0, 0, 0, 0)
batch_progress_layout.setSpacing(8)
batch_progress_layout.addWidget(self.batch_progress_label, 1)
batch_progress_layout.addWidget(self.collect_activity_label)
self.table = QTableView()
self.model = TaskTableModel(self.table)
self.table.setModel(self.model)
@@ -142,7 +194,7 @@ class CollectTab(QWidget):
layout.addLayout(filter_layout)
layout.addLayout(summary_layout)
layout.addWidget(self.match_detail_label)
layout.addWidget(self.batch_progress_label)
layout.addLayout(batch_progress_layout)
layout.addWidget(self.empty_state_card)
layout.addWidget(self.table, 1)
layout.addWidget(QLabel("采集运行日志"))
@@ -168,6 +220,156 @@ class CollectTab(QWidget):
def _set_status(self, message, level=None):
_emit_status(self.status_callback, message, level=level)
def _set_collect_activity_style(self, level):
color = _status_level_color(level)
self.collect_activity_label.setStyleSheet(
"QLabel#collectActivityLabel { "
"background: #f6f8fa; border: 1px solid #d0d7de; "
"border-radius: 6px; padding: 8px 10px; "
f"color: {color}; font-weight: 600; "
"}"
)
def _start_collect_activity(self):
now = time.monotonic()
self._collect_run_started_at = now
self._collect_task_started_at = None
self._collect_task_elapsed_seconds = 0
self._collect_activity_payload = {
"state": "preflight_started",
"total": 0,
"step": "preflight",
}
self._collect_terminal_text = ""
self._collect_stop_requested = False
self._refresh_collect_activity(now=now)
if not self._collect_elapsed_timer.isActive():
self._collect_elapsed_timer.start()
def _on_collect_activity(self, payload):
event = dict(payload or {})
state = str(event.get("state") or "")
now = time.monotonic()
if self._collect_run_started_at is None:
self._collect_run_started_at = now
self._collect_terminal_text = ""
if not self._collect_elapsed_timer.isActive():
self._collect_elapsed_timer.start()
if state == "preflight_started":
self._collect_activity_payload = event
elif state == "task_started":
self._collect_task_started_at = now
self._collect_task_elapsed_seconds = 0
self._collect_activity_payload = event
elif state in {"task_step", "task_finished"}:
current = dict(self._collect_activity_payload)
current.update(event)
self._collect_activity_payload = current
if state == "task_finished" and self._collect_task_started_at is not None:
self._collect_task_elapsed_seconds = max(
0,
int(now - self._collect_task_started_at),
)
self._collect_task_started_at = None
self._refresh_collect_activity(now=now)
def _refresh_collect_activity(self, now=None):
if self._collect_terminal_text:
self.collect_activity_label.setVisible(True)
self.collect_activity_label.setText(self._collect_terminal_text)
return
if self._collect_run_started_at is None:
self.collect_activity_label.setVisible(False)
return
current_time = time.monotonic() if now is None else now
run_elapsed = max(0, int(current_time - self._collect_run_started_at))
if self._collect_task_started_at is not None:
task_elapsed = max(0, int(current_time - self._collect_task_started_at))
else:
task_elapsed = self._collect_task_elapsed_seconds
event = self._collect_activity_payload
state = str(event.get("state") or "")
index = int(event.get("index") or 0)
total = int(event.get("total") or 0)
progress = f"{index}/{total}" if index and total else str(index or total or "")
step_label = _collect_activity_step_label(event.get("step"))
if self._collect_stop_requested:
if index:
text = f"正在停止 · 本条 {_format_collect_elapsed(task_elapsed)}"
else:
text = f"正在停止 · 已等待 {_format_collect_elapsed(run_elapsed)}"
level = "warning"
elif state in {"task_started", "task_step"}:
text = (
f"正在采集 {progress} · {step_label} · "
f"本条 {_format_collect_elapsed(task_elapsed)}"
)
level = "info"
elif state == "task_finished":
result_label = {
"success": "已完成",
"failed": "失败",
"skipped": "略过",
}.get(str(event.get("result") or ""), "已结束")
text = (
f"第 {progress} 条 · {result_label} · "
f"本条 {_format_collect_elapsed(task_elapsed)}"
)
level = "danger" if event.get("result") == "failed" else "muted"
else:
text = f"正在检查账号 · {_format_collect_elapsed(run_elapsed)}"
level = "info"
tooltip_parts = []
if event.get("item_id"):
tooltip_parts.append(f"商品ID:{event.get('item_id')}")
if event.get("alias"):
tooltip_parts.append(f"账号别名:{event.get('alias')}")
if state in {"task_started", "task_step"}:
tooltip_parts.append(f"当前阶段:{step_label}")
self.collect_activity_label.setToolTip("\n".join(tooltip_parts))
self._set_collect_activity_style(level)
self.collect_activity_label.setText(text)
self.collect_activity_label.setVisible(True)
def _finish_collect_activity(self, outcome, payload=None):
now = time.monotonic()
if self._collect_run_started_at is None:
total_elapsed = 0
else:
total_elapsed = max(0, int(now - self._collect_run_started_at))
self._collect_elapsed_timer.stop()
self._collect_run_started_at = None
self._collect_task_started_at = None
self._collect_task_elapsed_seconds = 0
self._collect_activity_payload = {}
self._collect_stop_requested = False
if outcome == "blocked":
text = "采集未开始 · 检查未通过"
level = "warning"
tooltip = "采集前检查未通过,请按弹窗提示处理"
elif outcome == "cancelled":
text = f"采集已停止 · 总用时 {_format_collect_elapsed(total_elapsed)}"
level = "warning"
tooltip = "本轮采集已停止"
elif outcome == "error":
text = "采集已结束 · 请查看运行日志"
level = "danger"
tooltip = "采集异常结束,请查看下方采集运行日志"
else:
text = f"采集完成 · 总用时 {_format_collect_elapsed(total_elapsed)}"
level = "success"
tooltip = "本轮采集已经完成"
self._collect_terminal_text = text
self.collect_activity_label.setToolTip(tooltip)
self._set_collect_activity_style(level)
self.collect_activity_label.setText(text)
self.collect_activity_label.setVisible(True)
def _on_collect_log(self, message):
self._append_collect_log(message)
self._set_status(message)
@@ -522,6 +724,9 @@ class CollectTab(QWidget):
config=self.config,
diagnostic_log_dir=diagnostics.DEFAULT_LOG_DIR,
)
activity_signal = getattr(worker, "activity", None)
if activity_signal is not None:
activity_signal.connect(self._on_collect_activity)
worker.progress.connect(self._on_collect_progress)
worker.row_updated.connect(self._on_collect_row_updated)
worker.log.connect(self._on_collect_log)
@@ -534,11 +739,14 @@ class CollectTab(QWidget):
self.collect_worker = worker
self.collect_thread = thread
self._set_collect_running(True)
self._start_collect_activity()
thread.start()
def stop_collect(self, checked=False):
if self.collect_worker is not None:
self.collect_worker.cancel()
self._collect_stop_requested = True
self._refresh_collect_activity()
self._set_status("正在停止采集...")
def write_back_old_data(self, checked=False):
@@ -616,6 +824,8 @@ class CollectTab(QWidget):
def _forget_collect_thread(self, thread):
if self.collect_thread is thread:
if self._collect_elapsed_timer.isActive():
self._finish_collect_activity("error")
self.collect_thread = None
self.collect_worker = None
@@ -643,6 +853,12 @@ class CollectTab(QWidget):
def _on_collect_finished(self, payload):
self._set_collect_running(False)
if payload.get("blocked"):
self._finish_collect_activity("blocked", payload)
elif payload.get("error"):
self._finish_collect_activity("error", payload)
else:
self._finish_collect_activity("finished", payload)
self.last_collect_run_id = payload.get("run_id") or self.last_collect_run_id
self.refresh_tasks()
self._load_latest_collect_run_log()
@@ -747,6 +963,7 @@ class CollectTab(QWidget):
def _on_collect_cancelled(self, payload):
self._set_collect_running(False)
self._finish_collect_activity("cancelled", payload)
self.refresh_tasks()
self._set_status(
"采集已停止:完成{done}/{total}".format(