feat: align collect filters with workflow tabs
This commit is contained in:
+109
-6
@@ -2282,6 +2282,16 @@ if QT_IMPORT_ERROR is None:
|
||||
class CollectTab(QWidget):
|
||||
"""Tab 1: import Excel files and list imported tasks."""
|
||||
|
||||
STATUS_FILTERS = [
|
||||
("全部状态", "all"),
|
||||
("待采集", "to_collect"),
|
||||
("已采集", "collected"),
|
||||
("已生成", "generated"),
|
||||
("已更新", "applied"),
|
||||
("失败", "failed"),
|
||||
("略过", "skipped"),
|
||||
]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
parent=None,
|
||||
@@ -2314,6 +2324,15 @@ if QT_IMPORT_ERROR is None:
|
||||
self.stop_collect_button.setEnabled(False)
|
||||
self.batch_filter = QComboBox()
|
||||
self.batch_filter.setObjectName("collectBatchFilter")
|
||||
self.shop_filter = QComboBox()
|
||||
self.shop_filter.setObjectName("collectShopFilter")
|
||||
self.item_filter = QLineEdit()
|
||||
self.item_filter.setObjectName("collectItemFilter")
|
||||
self.item_filter.setPlaceholderText("商品ID")
|
||||
self.status_filter = QComboBox()
|
||||
self.status_filter.setObjectName("collectStatusFilter")
|
||||
for label, value in self.STATUS_FILTERS:
|
||||
self.status_filter.addItem(label, value)
|
||||
self.delete_batch_button = QPushButton("删除批次")
|
||||
self.delete_batch_button.setObjectName("deleteBatchButton")
|
||||
self.delete_batch_button.setStyleSheet(_danger_outline_button_style("deleteBatchButton"))
|
||||
@@ -2322,14 +2341,22 @@ if QT_IMPORT_ERROR is None:
|
||||
toolbar = QHBoxLayout()
|
||||
toolbar.addWidget(self.import_button)
|
||||
toolbar.addWidget(self.refresh_button)
|
||||
toolbar.addWidget(QLabel("批次"))
|
||||
toolbar.addWidget(self.batch_filter, 2)
|
||||
toolbar.addWidget(self.delete_batch_button)
|
||||
toolbar.addWidget(self.collect_button)
|
||||
toolbar.addWidget(self.stop_collect_button)
|
||||
toolbar.addWidget(self.write_back_button)
|
||||
toolbar.addStretch(1)
|
||||
|
||||
filter_layout = QHBoxLayout()
|
||||
filter_layout.addWidget(QLabel("批次"))
|
||||
filter_layout.addWidget(self.batch_filter, 2)
|
||||
filter_layout.addWidget(QLabel("店铺"))
|
||||
filter_layout.addWidget(self.shop_filter, 1)
|
||||
filter_layout.addWidget(QLabel("商品ID"))
|
||||
filter_layout.addWidget(self.item_filter, 1)
|
||||
filter_layout.addWidget(QLabel("状态"))
|
||||
filter_layout.addWidget(self.status_filter, 1)
|
||||
filter_layout.addWidget(self.delete_batch_button)
|
||||
|
||||
self.summary_label = QLabel("未导入任务")
|
||||
self.summary_label.setTextFormat(Qt.RichText)
|
||||
self.batch_progress_label = _build_batch_progress_overview("collectBatchProgressOverview")
|
||||
@@ -2371,6 +2398,7 @@ if QT_IMPORT_ERROR is None:
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setContentsMargins(18, 18, 18, 18)
|
||||
layout.addLayout(toolbar)
|
||||
layout.addLayout(filter_layout)
|
||||
layout.addLayout(summary_layout)
|
||||
layout.addWidget(self.match_detail_label)
|
||||
layout.addWidget(self.batch_progress_label)
|
||||
@@ -2383,6 +2411,9 @@ if QT_IMPORT_ERROR is None:
|
||||
self.import_button.clicked.connect(self.import_excel)
|
||||
self.refresh_button.clicked.connect(self.refresh_tasks)
|
||||
self.batch_filter.currentIndexChanged.connect(self.refresh_tasks)
|
||||
self.shop_filter.currentIndexChanged.connect(self.refresh_tasks)
|
||||
self.item_filter.textChanged.connect(self.refresh_tasks)
|
||||
self.status_filter.currentIndexChanged.connect(self.refresh_tasks)
|
||||
self.delete_batch_button.clicked.connect(self.delete_current_batch)
|
||||
self.collect_button.clicked.connect(self.collect_old_data)
|
||||
self.stop_collect_button.clicked.connect(self.stop_collect)
|
||||
@@ -2576,6 +2607,9 @@ if QT_IMPORT_ERROR is None:
|
||||
db.init_db(self.db_path)
|
||||
batches = db.list_batches(path=self.db_path)
|
||||
selected_batch = self.batch_filter.currentData()
|
||||
selected_shop = self.shop_filter.currentData()
|
||||
selected_status = self.status_filter.currentData() or "all"
|
||||
item_query = self.item_filter.text().strip()
|
||||
if self.current_batch_id and self.batch_filter.findData(self.current_batch_id) < 0:
|
||||
selected_batch = self.current_batch_id
|
||||
self._populate_batch_filter(batches, selected_batch)
|
||||
@@ -2583,6 +2617,14 @@ if QT_IMPORT_ERROR is None:
|
||||
self.current_batch_id = selected_batch
|
||||
task_rows = db.list_tasks(batch_id=selected_batch, path=self.db_path)
|
||||
account_rows = accounts.list_accounts(path=self.db_path, config=self.config)
|
||||
self._populate_shop_filter(task_rows, account_rows, selected_shop)
|
||||
selected_shop = self.shop_filter.currentData()
|
||||
filtered_rows = [
|
||||
task for task in task_rows
|
||||
if self._matches_shop(task, selected_shop)
|
||||
and self._matches_item(task, item_query)
|
||||
and self._matches_status(task, selected_status, account_rows)
|
||||
]
|
||||
except Exception as exc:
|
||||
self.model.set_tasks([], [])
|
||||
self.empty_label.setText("任务读取失败")
|
||||
@@ -2590,7 +2632,7 @@ if QT_IMPORT_ERROR is None:
|
||||
_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.model.set_tasks(filtered_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)
|
||||
@@ -2613,6 +2655,58 @@ if QT_IMPORT_ERROR is None:
|
||||
first_file = os.path.basename(source_files[0]) if source_files else batch.id
|
||||
return f"{batch.created_at} · {first_file}"
|
||||
|
||||
def _populate_shop_filter(self, task_rows, account_rows, selected_shop):
|
||||
aliases = {str(task.alias).strip() for task in task_rows if str(task.alias).strip()}
|
||||
previous = selected_shop if selected_shop in aliases else None
|
||||
account_by_alias = {
|
||||
str(account.alias).strip(): account
|
||||
for account in account_rows
|
||||
if str(account.alias).strip()
|
||||
}
|
||||
self.shop_filter.blockSignals(True)
|
||||
self.shop_filter.clear()
|
||||
self.shop_filter.addItem("全部店铺", None)
|
||||
for alias in sorted(aliases):
|
||||
self.shop_filter.addItem(self._shop_label(alias, account_by_alias), alias)
|
||||
index = self.shop_filter.findData(previous)
|
||||
self.shop_filter.setCurrentIndex(index if index >= 0 else 0)
|
||||
self.shop_filter.blockSignals(False)
|
||||
|
||||
def _shop_label(self, alias, account_by_alias):
|
||||
account = account_by_alias.get(alias)
|
||||
if account is not None:
|
||||
return f"{account.account_name} ({alias})"
|
||||
return alias
|
||||
|
||||
def _matches_shop(self, task, selected_shop):
|
||||
return selected_shop is None or str(task.alias).strip() == selected_shop
|
||||
|
||||
def _matches_item(self, task, item_query):
|
||||
if not item_query:
|
||||
return True
|
||||
return item_query in str(getattr(task, "item_id", ""))
|
||||
|
||||
def _matches_status(self, task, selected_status, account_rows):
|
||||
if selected_status in (None, "all"):
|
||||
return True
|
||||
if selected_status == "to_collect":
|
||||
return task.stage == "imported" and task.status in {"pending", "success"}
|
||||
if selected_status in {"collected", "generated", "applied"}:
|
||||
return task.stage == selected_status
|
||||
if selected_status == "failed":
|
||||
return task.status == "failed"
|
||||
if selected_status == "skipped":
|
||||
return task.status == "skipped" or self._is_unmatched_task(task, account_rows)
|
||||
return True
|
||||
|
||||
def _is_unmatched_task(self, task, account_rows):
|
||||
aliases = {
|
||||
str(account.alias).strip()
|
||||
for account in account_rows
|
||||
if str(account.alias).strip()
|
||||
}
|
||||
return str(task.alias).strip() not in aliases
|
||||
|
||||
def _selected_batch_id(self):
|
||||
return self.batch_filter.currentData()
|
||||
|
||||
@@ -2670,7 +2764,7 @@ if QT_IMPORT_ERROR is None:
|
||||
QMessageBox.information(self, "删除批次", message)
|
||||
|
||||
def collect_old_data(self, checked=False):
|
||||
tasks = list(self.model.all_tasks)
|
||||
tasks = list(self.model.tasks)
|
||||
if not tasks:
|
||||
self._set_status("没有可采集任务")
|
||||
return
|
||||
@@ -2756,6 +2850,9 @@ if QT_IMPORT_ERROR is None:
|
||||
self.write_back_button.setEnabled(not running)
|
||||
self.stop_collect_button.setEnabled(running)
|
||||
self.batch_filter.setEnabled(not running)
|
||||
self.shop_filter.setEnabled(not running)
|
||||
self.item_filter.setEnabled(not running)
|
||||
self.status_filter.setEnabled(not running)
|
||||
self._update_delete_batch_button()
|
||||
|
||||
def _set_write_back_running(self, running):
|
||||
@@ -2764,6 +2861,9 @@ if QT_IMPORT_ERROR is None:
|
||||
self.collect_button.setEnabled(not running)
|
||||
self.write_back_button.setEnabled(not running)
|
||||
self.batch_filter.setEnabled(not running)
|
||||
self.shop_filter.setEnabled(not running)
|
||||
self.item_filter.setEnabled(not running)
|
||||
self.status_filter.setEnabled(not running)
|
||||
self._update_delete_batch_button()
|
||||
|
||||
def _forget_collect_thread(self, thread):
|
||||
@@ -2990,7 +3090,10 @@ if QT_IMPORT_ERROR is None:
|
||||
self.empty_label.setText("暂无任务")
|
||||
return
|
||||
if self.model.rowCount() == 0 and self.model.filter_mode == "unmatched":
|
||||
self.empty_label.setText("没有未匹配任务")
|
||||
self.empty_label.setText("当前筛选没有未匹配任务")
|
||||
return
|
||||
if self.model.rowCount() == 0:
|
||||
self.empty_label.setText("当前筛选没有匹配任务")
|
||||
return
|
||||
unmatched = self.model.unmatched_count()
|
||||
self.empty_label.setText(
|
||||
|
||||
+1
-1
@@ -99,7 +99,7 @@
|
||||
| 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。加轻量批次进度总览:①②③在当前批次/全部批次摘要区下方显示同一组阶段计数,按当前批次筛选聚合总数、导入、已采集、已生成、已更新、失败、略过。计数口径:失败/略过优先按 `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-516 | ①筛选对齐②③ | T-202, T-303b | 依据 `docs/ux-review.md` P2。① 导入采集补齐与②③一致的店铺、商品ID、状态筛选,保持三个列表页筛选心智一致。筛选栏顺序为批次/店铺/商品ID/状态;当前筛选结果用于①表格显示与「采集旧标题/旧封面」作用范围;导入汇总栏、批次进度总览、未匹配数量仍按当前批次全量任务统计,未匹配按钮继续筛出当前筛选结果中的未匹配任务;不改导入、采集、回写、CDP 或 Shopee 更新逻辑 | DONE |
|
||||
| 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
@@ -967,3 +967,10 @@
|
||||
- 测试:`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「①筛选对齐②③」。
|
||||
## 【2026-07-02】T-516 完成 · ①筛选对齐②③
|
||||
|
||||
- 文档先行:`docs/06-tasks.md` 将 T-516 标记为 DOING 后补充验收口径,明确①新增店铺、商品ID、状态筛选;当前筛选结果用于①表格和采集作用范围,导入汇总栏/批次进度/未匹配数量仍按当前批次全量统计。
|
||||
- 代码:`app/gui.py` 的①导入采集新增 `collectShopFilter`、`collectItemFilter`、`collectStatusFilter`,筛选栏顺序对齐②③;刷新时用全量批次任务更新汇总和进度,用筛选后任务更新表格;「采集旧标题/旧封面」改为只处理当前表格可见任务;采集/回写运行中禁用新增筛选控件。
|
||||
- 测试:`tests/test_gui.py` 新增①筛选覆盖,验证店铺、商品ID、状态筛选、空结果提示、未匹配筛出和采集 worker 接收当前筛选任务。
|
||||
- 验证:`python -m py_compile app\gui.py tests\test_gui.py`、`python -m unittest discover -s tests -p "test_gui.py"`(68 tests)、`python -m compileall app main.py`、`python -m unittest discover -s tests`(156 tests)均通过;Qt 仍有本机字体目录提示,不影响测试结果。
|
||||
- 下一步:T-517「⑤设置分区 + 清理兼容字段」。
|
||||
|
||||
@@ -2313,6 +2313,149 @@ class GuiTests(TempDirMixin, unittest.TestCase):
|
||||
|
||||
self.assert_removed(temp_dir)
|
||||
|
||||
def test_collect_tab_filters_by_shop_item_status_and_collect_scope(self):
|
||||
with self.make_temp_dir() as temp_dir:
|
||||
cfg = self.make_config(temp_dir)
|
||||
accounts.create_account("主店", "alias-a", debug_port=9222, config=cfg)
|
||||
accounts.create_account("副店", "alias-b", debug_port=9223, config=cfg)
|
||||
batch_id = db.create_batch(["input.xlsx"], path=cfg["db_path"])
|
||||
db.insert_tasks(
|
||||
batch_id,
|
||||
[
|
||||
{
|
||||
"source_file_abs": os.path.join(temp_dir, "input.xlsx"),
|
||||
"source_sheet": "商品",
|
||||
"source_row": 2,
|
||||
"account_name": "Excel主店",
|
||||
"alias": "alias-a",
|
||||
"item_id": "1001",
|
||||
},
|
||||
{
|
||||
"source_file_abs": os.path.join(temp_dir, "input.xlsx"),
|
||||
"source_sheet": "商品",
|
||||
"source_row": 3,
|
||||
"account_name": "Excel主店",
|
||||
"alias": "alias-a",
|
||||
"item_id": "1002",
|
||||
},
|
||||
{
|
||||
"source_file_abs": os.path.join(temp_dir, "input.xlsx"),
|
||||
"source_sheet": "商品",
|
||||
"source_row": 4,
|
||||
"account_name": "Excel副店",
|
||||
"alias": "alias-b",
|
||||
"item_id": "2001",
|
||||
},
|
||||
{
|
||||
"source_file_abs": os.path.join(temp_dir, "input.xlsx"),
|
||||
"source_sheet": "商品",
|
||||
"source_row": 5,
|
||||
"account_name": "Excel副店",
|
||||
"alias": "alias-b",
|
||||
"item_id": "2002",
|
||||
},
|
||||
{
|
||||
"source_file_abs": os.path.join(temp_dir, "input.xlsx"),
|
||||
"source_sheet": "商品",
|
||||
"source_row": 6,
|
||||
"account_name": "Excel未知",
|
||||
"alias": "missing",
|
||||
"item_id": "3001",
|
||||
},
|
||||
],
|
||||
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["1002"].id, "旧标题", os.path.join(temp_dir, "old.jpg"), path=cfg["db_path"])
|
||||
db.mark_failed(tasks["2002"].id, "collect", "采集失败", path=cfg["db_path"])
|
||||
|
||||
tab = CollectTab(config=cfg)
|
||||
self.addCleanup(tab.close)
|
||||
|
||||
self.assertEqual("collectShopFilter", tab.shop_filter.objectName())
|
||||
self.assertEqual("collectItemFilter", tab.item_filter.objectName())
|
||||
self.assertEqual("collectStatusFilter", tab.status_filter.objectName())
|
||||
self.assertEqual(5, tab.model.rowCount())
|
||||
self.assertIn("5 行", tab.summary_label.text())
|
||||
self.assertEqual("未匹配(1)", tab.show_unmatched_button.text())
|
||||
self.assertIn("总数5", tab.batch_progress_label.text())
|
||||
|
||||
tab.shop_filter.setCurrentIndex(tab.shop_filter.findData("alias-a"))
|
||||
self.assertEqual(2, tab.model.rowCount())
|
||||
self.assertEqual(["1001", "1002"], [tab.model.index(row, 2).data() for row in range(tab.model.rowCount())])
|
||||
self.assertIn("5 行", tab.summary_label.text())
|
||||
self.assertEqual("未匹配(1)", tab.show_unmatched_button.text())
|
||||
|
||||
tab.item_filter.setText("1002")
|
||||
self.assertEqual(1, tab.model.rowCount())
|
||||
self.assertEqual("1002", tab.model.index(0, 2).data())
|
||||
|
||||
tab.status_filter.setCurrentIndex(tab.status_filter.findData("collected"))
|
||||
self.assertEqual(1, tab.model.rowCount())
|
||||
self.assertEqual("已采集", tab.model.index(0, 3).data())
|
||||
|
||||
tab.shop_filter.setCurrentIndex(tab.shop_filter.findData(None))
|
||||
tab.item_filter.clear()
|
||||
tab.status_filter.setCurrentIndex(tab.status_filter.findData("skipped"))
|
||||
self.assertEqual(1, tab.model.rowCount())
|
||||
self.assertEqual("missing", tab.model.index(0, 1).data())
|
||||
self.assertEqual("略过", tab.model.index(0, 3).data())
|
||||
|
||||
tab.item_filter.setText("no-match")
|
||||
self.assertEqual(0, tab.model.rowCount())
|
||||
self.assertIn("当前筛选没有匹配任务", tab.empty_label.text())
|
||||
|
||||
tab.shop_filter.setCurrentIndex(tab.shop_filter.findData("alias-a"))
|
||||
tab.item_filter.setText("1001")
|
||||
tab.status_filter.setCurrentIndex(tab.status_filter.findData("to_collect"))
|
||||
tab.show_all_tasks()
|
||||
self.assertEqual(1, tab.model.rowCount())
|
||||
self.assertEqual("1001", tab.model.index(0, 2).data())
|
||||
|
||||
class FakeSignal:
|
||||
def __init__(self):
|
||||
self.callbacks = []
|
||||
|
||||
def connect(self, callback):
|
||||
self.callbacks.append(callback)
|
||||
|
||||
class FakeWorker:
|
||||
def __init__(self, tasks, **kwargs):
|
||||
captured["tasks"] = tasks
|
||||
captured["kwargs"] = kwargs
|
||||
self.progress = FakeSignal()
|
||||
self.row_updated = FakeSignal()
|
||||
self.log = FakeSignal()
|
||||
self.failed = FakeSignal()
|
||||
self.finished = FakeSignal()
|
||||
self.cancelled = FakeSignal()
|
||||
|
||||
def cancel(self):
|
||||
captured["cancelled"] = True
|
||||
|
||||
class FakeThread:
|
||||
def __init__(self):
|
||||
self.finished = FakeSignal()
|
||||
|
||||
def start(self):
|
||||
captured["started"] = True
|
||||
|
||||
captured = {}
|
||||
with mock.patch("app.gui.CollectWorker", FakeWorker), mock.patch(
|
||||
"app.gui.run_worker",
|
||||
return_value=FakeThread(),
|
||||
):
|
||||
tab.collect_old_data()
|
||||
|
||||
self.assertTrue(captured["started"])
|
||||
self.assertEqual(["1001"], [task.item_id for task in captured["tasks"]])
|
||||
self.assertEqual(cfg["db_path"], captured["kwargs"]["db_path"])
|
||||
self.assertFalse(tab.shop_filter.isEnabled())
|
||||
self.assertFalse(tab.item_filter.isEnabled())
|
||||
self.assertFalse(tab.status_filter.isEnabled())
|
||||
|
||||
self.assert_removed(temp_dir)
|
||||
|
||||
def test_collect_tab_soft_deletes_batch_and_refreshes_workflow_tabs(self):
|
||||
with self.make_temp_dir() as temp_dir:
|
||||
cfg = self.make_config(temp_dir)
|
||||
|
||||
Reference in New Issue
Block a user