feat: add batch progress overview
This commit is contained in:
+73
-1
@@ -194,6 +194,65 @@ if QT_IMPORT_ERROR is None:
|
||||
button.setVisible(active and show_button)
|
||||
|
||||
|
||||
BATCH_PROGRESS_FIELDS = [
|
||||
("total", "总数"),
|
||||
("imported", "导入"),
|
||||
("collected", "已采集"),
|
||||
("generated", "已生成"),
|
||||
("applied", "已更新"),
|
||||
("failed", "失败"),
|
||||
("skipped", "略过"),
|
||||
]
|
||||
|
||||
|
||||
def _summarize_batch_progress(tasks):
|
||||
summary = {key: 0 for key, _label in BATCH_PROGRESS_FIELDS}
|
||||
task_rows = list(tasks or [])
|
||||
summary["total"] = len(task_rows)
|
||||
for task in task_rows:
|
||||
status = getattr(task, "status", None)
|
||||
stage = getattr(task, "stage", None)
|
||||
if status == "failed":
|
||||
summary["failed"] += 1
|
||||
elif status == "skipped":
|
||||
summary["skipped"] += 1
|
||||
elif stage == "applied":
|
||||
summary["applied"] += 1
|
||||
elif stage == "generated":
|
||||
summary["generated"] += 1
|
||||
elif stage == "collected":
|
||||
summary["collected"] += 1
|
||||
else:
|
||||
summary["imported"] += 1
|
||||
return summary
|
||||
|
||||
|
||||
def _format_batch_progress(summary):
|
||||
parts = [f"{label}{summary.get(key, 0)}" for key, label in BATCH_PROGRESS_FIELDS]
|
||||
return "批次进度:" + " · ".join(parts)
|
||||
|
||||
|
||||
def _build_batch_progress_overview(object_name):
|
||||
label = QLabel("")
|
||||
label.setObjectName(object_name)
|
||||
label.setWordWrap(True)
|
||||
label.setStyleSheet(
|
||||
f"QLabel#{object_name} {{ "
|
||||
"background: #f6f8fa; border: 1px solid #d0d7de; "
|
||||
"border-radius: 6px; padding: 8px 10px; color: #24292f; "
|
||||
"}"
|
||||
)
|
||||
label.setVisible(False)
|
||||
return label
|
||||
|
||||
|
||||
def _set_batch_progress_overview(label, tasks):
|
||||
summary = _summarize_batch_progress(tasks)
|
||||
active = summary["total"] > 0
|
||||
label.setVisible(active)
|
||||
label.setText(_format_batch_progress(summary) if active else "")
|
||||
|
||||
|
||||
def _database_path(db_path=None, config=None) -> str:
|
||||
return db_path or appconfig.db_path(config)
|
||||
|
||||
@@ -810,6 +869,7 @@ if QT_IMPORT_ERROR is None:
|
||||
filter_layout.addWidget(self.refresh_button)
|
||||
|
||||
self.summary_label = QLabel("任务 0 条")
|
||||
self.batch_progress_label = _build_batch_progress_overview("generateBatchProgressOverview")
|
||||
(
|
||||
self.empty_state_card,
|
||||
self.empty_state_label,
|
||||
@@ -837,6 +897,7 @@ if QT_IMPORT_ERROR is None:
|
||||
right_layout.setContentsMargins(12, 0, 0, 0)
|
||||
right_layout.addLayout(filter_layout)
|
||||
right_layout.addWidget(self.summary_label)
|
||||
right_layout.addWidget(self.batch_progress_label)
|
||||
right_layout.addWidget(self.empty_state_card)
|
||||
right_layout.addWidget(self.task_table, 1)
|
||||
right_layout.addWidget(QLabel("AI生成运行日志"))
|
||||
@@ -1310,6 +1371,7 @@ if QT_IMPORT_ERROR is None:
|
||||
except Exception as exc:
|
||||
self.model.set_tasks([], [])
|
||||
self.summary_label.setText("任务读取失败")
|
||||
_set_batch_progress_overview(self.batch_progress_label, [])
|
||||
_set_empty_state(self.empty_state_card, self.empty_state_label, self.empty_state_button)
|
||||
self._set_status(f"AI 生成任务读取失败:{exc}")
|
||||
return
|
||||
@@ -1317,6 +1379,7 @@ if QT_IMPORT_ERROR is None:
|
||||
self.summary_label.setText(
|
||||
f"任务 {len(filtered_tasks)}/{len(batch_tasks)} 条"
|
||||
)
|
||||
_set_batch_progress_overview(self.batch_progress_label, batch_tasks)
|
||||
self._update_empty_state(batch_tasks, filtered_tasks, accounts_rows)
|
||||
|
||||
def _update_empty_state(self, batch_tasks, filtered_tasks, account_rows):
|
||||
@@ -1471,6 +1534,7 @@ if QT_IMPORT_ERROR is None:
|
||||
filter_layout.addWidget(self.refresh_button)
|
||||
|
||||
self.summary_label = QLabel("任务 0 条")
|
||||
self.batch_progress_label = _build_batch_progress_overview("applyBatchProgressOverview")
|
||||
self.risk_label = QLabel("可先点击「检查本轮更新」确认当前筛选范围;点击「开始更新」后会再次确认并按批提交线上。")
|
||||
(
|
||||
self.empty_state_card,
|
||||
@@ -1525,6 +1589,7 @@ if QT_IMPORT_ERROR is None:
|
||||
layout.addLayout(filter_layout)
|
||||
layout.addWidget(self.risk_label)
|
||||
layout.addWidget(self.summary_label)
|
||||
layout.addWidget(self.batch_progress_label)
|
||||
layout.addWidget(self.empty_state_card)
|
||||
layout.addWidget(self.task_table, 1)
|
||||
layout.addWidget(QLabel("运行日志"))
|
||||
@@ -1561,8 +1626,9 @@ if QT_IMPORT_ERROR is None:
|
||||
item_query = self.item_filter.text().strip()
|
||||
self._populate_batch_filter(batches, selected_batch)
|
||||
selected_batch = self.batch_filter.currentData()
|
||||
all_batch_tasks = db.list_tasks(batch_id=selected_batch, path=self.db_path)
|
||||
batch_tasks = [
|
||||
task for task in db.list_tasks(batch_id=selected_batch, path=self.db_path)
|
||||
task for task in all_batch_tasks
|
||||
if self._is_update_task(task)
|
||||
]
|
||||
self._populate_shop_filter(batch_tasks, account_rows, selected_shop)
|
||||
@@ -1576,6 +1642,7 @@ if QT_IMPORT_ERROR is None:
|
||||
except Exception as exc:
|
||||
self.model.set_tasks([], [])
|
||||
self.summary_label.setText("任务读取失败")
|
||||
_set_batch_progress_overview(self.batch_progress_label, [])
|
||||
_set_empty_state(self.empty_state_card, self.empty_state_label, self.empty_state_button)
|
||||
self._set_status(f"更新任务读取失败:{exc}")
|
||||
return
|
||||
@@ -1583,6 +1650,7 @@ if QT_IMPORT_ERROR is None:
|
||||
self.summary_label.setText(
|
||||
f"任务 {len(filtered_tasks)}/{len(batch_tasks)} 条"
|
||||
)
|
||||
_set_batch_progress_overview(self.batch_progress_label, all_batch_tasks)
|
||||
self._update_empty_state(batch_tasks, filtered_tasks, account_rows)
|
||||
self._update_write_back_button()
|
||||
|
||||
@@ -2264,6 +2332,7 @@ if QT_IMPORT_ERROR is None:
|
||||
|
||||
self.summary_label = QLabel("未导入任务")
|
||||
self.summary_label.setTextFormat(Qt.RichText)
|
||||
self.batch_progress_label = _build_batch_progress_overview("collectBatchProgressOverview")
|
||||
self.match_detail_label = QLabel("")
|
||||
self.show_all_button = QPushButton("全部")
|
||||
self.show_unmatched_button = QPushButton("未匹配(0)")
|
||||
@@ -2304,6 +2373,7 @@ if QT_IMPORT_ERROR is None:
|
||||
layout.addLayout(toolbar)
|
||||
layout.addLayout(summary_layout)
|
||||
layout.addWidget(self.match_detail_label)
|
||||
layout.addWidget(self.batch_progress_label)
|
||||
layout.addWidget(self.empty_state_card)
|
||||
layout.addWidget(self.table, 1)
|
||||
layout.addWidget(QLabel("采集运行日志"))
|
||||
@@ -2516,11 +2586,13 @@ if QT_IMPORT_ERROR is None:
|
||||
except Exception as exc:
|
||||
self.model.set_tasks([], [])
|
||||
self.empty_label.setText("任务读取失败")
|
||||
_set_batch_progress_overview(self.batch_progress_label, [])
|
||||
_set_empty_state(self.empty_state_card, self.empty_state_label, self.empty_state_button)
|
||||
self._set_status(f"任务读取失败:{exc}")
|
||||
return
|
||||
self.model.set_tasks(task_rows, account_rows)
|
||||
self._update_summary(task_rows, account_rows)
|
||||
_set_batch_progress_overview(self.batch_progress_label, task_rows)
|
||||
self._update_empty_state(task_rows, account_rows)
|
||||
self._update_delete_batch_button()
|
||||
|
||||
|
||||
+1
-1
@@ -98,7 +98,7 @@
|
||||
| T-512 | ③高风险按钮上色 + ①导入校验数字标红 | T-511, T-508, T-202b | ③「开始更新」(写线上)改用 warning `#bc4c00` 系强调(描边或填充;第一版不强制图标,避免 Windows/Qt 字体渲染不稳),与②安全流程 info 蓝主操作区分;「停止」保持中性。①导入汇总栏:未匹配数 >0 时用 danger 且保持现有按钮可点击筛出,=0 保持中性;无效行数 >0 时只标红提示,不承诺在任务表筛出,因为脏行未入库,后续若要点击查看需另做「导入错误明细」。颜色取自 T-511 色板常量 | DONE |
|
||||
| T-513 | 登录点 / ③Tab危险标识 / 破坏性按钮上色 | T-511, T-105, T-206 | ④登录状态 `●` 已登录=success、未登录/未检测=danger 或 muted(保留文字);③ 更新shopee Tab 若做危险标识,不依赖 QSS 硬选第 3 个 Tab,优先用 `QTabWidget.setTabIcon(2, ...)` 设置克制 warning 小圆点图标,或第一版先跳过 Tab 标识;不整条刷红。破坏性按钮「删除批次」「删除账号」文字/描边用 danger,与普通灰按钮区分(二次确认弹窗仍是主防线)。颜色取自 T-511 色板常量 | DONE |
|
||||
| T-514 | ①②③ 首次空状态引导卡片 | T-205 | 依据 `docs/ux-review.md` P1。①②③ 增加轻量空状态引导卡片,不改采集/生成/更新流程和既有预检拦截逻辑。无账号时三页均显示「第一步:前往『④账号管理』配置并登录账号」并提供「前往账号管理」按钮;有账号但无任务时,①提示导入 Excel,②提示先完成①采集,③提示先完成②生成;账号/任务就绪后卡片自动隐藏,老用户无感。卡片只做 UI 引导,不替代执行前账号 Chrome/CDP/登录态检查 | DONE |
|
||||
| T-515 | 批次阶段进度总览 | T-206, T-401 | 依据 `docs/ux-review.md` P1。加轻量批次进度总览:按选中批次聚合各阶段计数(导入/已采集/已生成/已更新/失败/略过),落点可选①顶部进度条或独立概览区。数据由现有 `db.list_tasks(batch_id=...)` 按 `stage/status` 聚合,不新增表;已软删除批次不计入 | TODO |
|
||||
| T-515 | 批次阶段进度总览 | T-206, T-401 | 依据 `docs/ux-review.md` P1。加轻量批次进度总览:①②③在当前批次/全部批次摘要区下方显示同一组阶段计数,按当前批次筛选聚合总数、导入、已采集、已生成、已更新、失败、略过。计数口径:失败/略过优先按 `status=failed/skipped` 统计;已更新按 `stage=applied` 且非失败/略过;已生成按 `stage=generated` 且非失败/略过;已采集按 `stage=collected` 且非失败/略过;导入按其余未进入后续阶段任务统计。数据由现有 `db.list_tasks(batch_id=...)` 聚合,不新增表;已软删除批次不计入;只做 UI 总览,不改采集/生成/更新执行流程 | DONE |
|
||||
| T-516 | ①筛选对齐②③ | T-202, T-303b | 依据 `docs/ux-review.md` P2。① 导入采集补齐与②③一致的店铺/商品ID/状态筛选(至少店铺),保持三个列表页筛选心智一致;不改导入汇总栏与未匹配筛出逻辑,只扩展筛选维度 | TODO |
|
||||
| T-517 | ⑤设置分区 + 清理兼容字段 | T-506, T-507 | 依据 `docs/ux-review.md` P2。⑤ 视觉分区:高频「Shopee 更新安全/执行模式」与低频「基础设施(Chrome路径/端口/DB路径)」分块;彻底隐藏或清理残留的 `test_item_id`、`dry_run` 用户入口(保留内部兼容字段与语义),减少设置页过载与 UI 技术债 | TODO |
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -960,3 +960,10 @@
|
||||
- 测试:`tests/test_gui.py` 新增①②③空状态断言,覆盖无账号时按钮跳④、有账号但无任务时显示对应下一步提示且隐藏跳④按钮。
|
||||
- 验证:`python -m py_compile app\gui.py tests\test_gui.py` 通过;`python -m unittest discover -s tests -p "test_gui.py"` 通过(66 tests);`python -m compileall app main.py` 通过;`python -m unittest discover -s tests` 通过(154 tests)。测试输出仍有 PySide6 字体目录提示,不影响结果。
|
||||
- 下一步:T-515「批次阶段进度总览」。
|
||||
## 【2026-07-02】T-515 完成 · 批次阶段进度总览
|
||||
|
||||
- 文档先行:`docs/06-tasks.md` 将 T-515 标记为 DOING 后补充计数口径,明确①②③同显当前批次/全部批次总数、导入、已采集、已生成、已更新、失败、略过,失败/略过按 `status` 优先统计,不新增表、不改执行流程。
|
||||
- 代码:`app/gui.py` 新增共享批次进度聚合和总览条;①导入采集、②AI生成、③更新shopee在摘要区下方展示同一组计数。③任务列表仍只显示可更新相关记录,但总览使用当前批次全部未软删除任务统计。
|
||||
- 测试:`tests/test_gui.py` 新增①②③批次进度总览覆盖,构造导入/已采集/已生成/已更新/失败/略过各 1 条,并确认软删除批次不出现在筛选和计数中。
|
||||
- 验证:`python -m py_compile app\gui.py tests\test_gui.py`、`python -m unittest discover -s tests -p "test_gui.py"`(67 tests)、`python -m compileall app main.py`、`python -m unittest discover -s tests`(155 tests)均通过;Qt 仍有本机字体目录提示,不影响测试结果。
|
||||
- 下一步:T-516「①筛选对齐②③」。
|
||||
|
||||
@@ -166,6 +166,82 @@ class GuiTests(TempDirMixin, unittest.TestCase):
|
||||
self.assertTrue(tab.empty_state_button.isHidden())
|
||||
|
||||
self.assert_removed(temp_dir)
|
||||
|
||||
def test_workflow_tabs_show_batch_progress_overview(self):
|
||||
with self.make_temp_dir() as temp_dir:
|
||||
cfg = self.make_config(temp_dir)
|
||||
db.init_db(cfg["db_path"])
|
||||
accounts.create_account("主店", "alias", debug_port=9222, config=cfg)
|
||||
batch_id = db.create_batch(["input.xlsx"], path=cfg["db_path"])
|
||||
rows = []
|
||||
for index in range(6):
|
||||
rows.append(
|
||||
{
|
||||
"source_file_abs": os.path.join(temp_dir, "input.xlsx"),
|
||||
"source_sheet": "待处理任务",
|
||||
"source_row": index + 2,
|
||||
"row_key": f"row-{index}",
|
||||
"account_name": "主店",
|
||||
"alias": "alias",
|
||||
"item_id": str(1000 + index),
|
||||
}
|
||||
)
|
||||
db.insert_tasks(batch_id, rows, path=cfg["db_path"])
|
||||
tasks = {task.item_id: task for task in db.list_tasks(batch_id=batch_id, path=cfg["db_path"])}
|
||||
db.set_collected(tasks["1001"].id, "旧标题", os.path.join(temp_dir, "old.jpg"), path=cfg["db_path"])
|
||||
db.set_generated(tasks["1002"].id, "新标题", os.path.join(temp_dir, "new.jpg"), path=cfg["db_path"])
|
||||
db.set_applied(tasks["1003"].id, committed=True, path=cfg["db_path"])
|
||||
db.set_collected(tasks["1004"].id, "旧标题", os.path.join(temp_dir, "old-failed.jpg"), path=cfg["db_path"])
|
||||
db.mark_failed(tasks["1004"].id, "generate", "生成失败", path=cfg["db_path"])
|
||||
db.mark_skipped(tasks["1005"].id, "别名未匹配", path=cfg["db_path"])
|
||||
|
||||
active_other = db.create_batch(["other.xlsx"], path=cfg["db_path"])
|
||||
db.insert_tasks(
|
||||
active_other,
|
||||
[
|
||||
{
|
||||
"source_file_abs": os.path.join(temp_dir, "other.xlsx"),
|
||||
"source_sheet": "待处理任务",
|
||||
"source_row": 2,
|
||||
"row_key": "other-row",
|
||||
"account_name": "主店",
|
||||
"alias": "alias",
|
||||
"item_id": "2000",
|
||||
}
|
||||
],
|
||||
path=cfg["db_path"],
|
||||
)
|
||||
deleted_batch = db.create_batch(["deleted.xlsx"], path=cfg["db_path"])
|
||||
db.insert_tasks(
|
||||
deleted_batch,
|
||||
[
|
||||
{
|
||||
"source_file_abs": os.path.join(temp_dir, "deleted.xlsx"),
|
||||
"source_sheet": "待处理任务",
|
||||
"source_row": 2,
|
||||
"row_key": "deleted-row",
|
||||
"account_name": "主店",
|
||||
"alias": "alias",
|
||||
"item_id": "3000",
|
||||
}
|
||||
],
|
||||
path=cfg["db_path"],
|
||||
)
|
||||
db.delete_batch(deleted_batch, reason="测试软删除", path=cfg["db_path"])
|
||||
|
||||
expected = "批次进度:总数6 · 导入1 · 已采集1 · 已生成1 · 已更新1 · 失败1 · 略过1"
|
||||
for tab_class in (CollectTab, GenerateTab, ApplyTab):
|
||||
tab = tab_class(config=cfg)
|
||||
self.addCleanup(tab.close)
|
||||
self.assertEqual(-1, tab.batch_filter.findData(deleted_batch))
|
||||
tab.batch_filter.setCurrentIndex(tab.batch_filter.findData(batch_id))
|
||||
tab.refresh_tasks()
|
||||
|
||||
self.assertFalse(tab.batch_progress_label.isHidden())
|
||||
self.assertEqual(expected, tab.batch_progress_label.text())
|
||||
|
||||
self.assert_removed(temp_dir)
|
||||
|
||||
def test_settings_tab_loads_ai_models_and_masks_key_field(self):
|
||||
with self.make_temp_dir() as temp_dir:
|
||||
cfg = self.make_config(temp_dir)
|
||||
|
||||
Reference in New Issue
Block a user