feat(collect): show per-item collection activity timer
This commit is contained in:
+218
-1
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user