From 1c9ee36488c803f6d99ec6921a89b6deb41c34a9 Mon Sep 17 00:00:00 2001 From: ila Date: Mon, 22 Jun 2026 15:09:56 +0800 Subject: [PATCH] =?UTF-8?q?fix(ai-outfit):=20preview=20sample=20rows=20ign?= =?UTF-8?q?ore=20Excel=20status=20(=C2=A719.9)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add excel_service.read_all_rows (every valid data row, status-independent) and use it in _reload_sample_rows for the preview dropdown, so previewing still works after the whole sheet is 完成. Generation still uses load_outfit_tasks. +1 excel test (read_all_rows includes 完成/失败, skips incomplete); offscreen all-完成 sheet now yields sample rows; full suite (12) green. Co-Authored-By: Claude Opus 4.8 --- src/app/widgets/ai_outfit_panel.py | 15 +++++++------ src/services/excel_service.py | 34 ++++++++++++++++++++++++++++++ tasks.md | 6 +++--- tests/test_excel_service.py | 12 +++++++++++ 4 files changed, 58 insertions(+), 9 deletions(-) diff --git a/src/app/widgets/ai_outfit_panel.py b/src/app/widgets/ai_outfit_panel.py index ea40ddf..3760b70 100644 --- a/src/app/widgets/ai_outfit_panel.py +++ b/src/app/widgets/ai_outfit_panel.py @@ -636,17 +636,20 @@ class AiOutfitPanel(QWidget): self._refresh_preview() def _reload_sample_rows(self): - """Best-effort: read the chosen Excel to fill the sample dropdown.""" + """Best-effort: read the chosen Excel to fill the sample dropdown. + + Uses read_all_rows (status-independent) so preview still works after the + whole sheet is 完成 (docs/11 §10.3); generation still uses load_outfit_tasks. + """ excel = self._excel_edit.text().strip() - tasks = [] + rows = [] if excel: try: - from services.excel_service import load_outfit_tasks - tasks = load_outfit_tasks( - excel, retry_failed=self._retry_failed_chk.isChecked()) + from services.excel_service import read_all_rows + rows = read_all_rows(excel) except Exception as exc: # noqa: BLE001 - silent for preview logger.info("Sample rows unavailable: %s", exc) - self._fill_sample_combo(tasks) + self._fill_sample_combo(rows) def _refresh_preview(self): if not hasattr(self, "_preview_view"): diff --git a/src/services/excel_service.py b/src/services/excel_service.py index bd4d0b5..095b6d0 100644 --- a/src/services/excel_service.py +++ b/src/services/excel_service.py @@ -113,6 +113,40 @@ def load_outfit_tasks(excel_path, retry_failed=False): workbook.close() +def read_all_rows(excel_path): + """Return an OutfitTask for every valid data row, ignoring E-column status. + + Unlike load_outfit_tasks (which filters for *processable* rows), this returns + all rows whose title/product/garment are filled — including 「完成」/「失败」 — + for preview/sample purposes only (docs/11 §10.3). Rows missing any of those + fields are skipped. + """ + path = Path(excel_path) + workbook = load_workbook(str(path)) + try: + sheet = workbook.worksheets[0] + rows = [] + for row_index in range(2, sheet.max_row + 1): + title = sheet.cell(row_index, COL_TITLE).value + product_id = sheet.cell(row_index, COL_PRODUCT_ID).value + garment_path = sheet.cell(row_index, COL_GARMENT_PATH).value + if _is_empty(title) or _is_empty(product_id) or _is_empty(garment_path): + continue + raw_status = sheet.cell(row_index, COL_STATUS).value + rows.append( + OutfitTask( + row_index=row_index, + title=_cell_text(title), + product_id=_cell_text(product_id), + garment_path=_cell_text(garment_path), + status=_cell_text(raw_status) or STATUS_PENDING, + ) + ) + return rows + finally: + workbook.close() + + def write_outfit_result(excel_path, result): """Write one outfit result to columns D/E/F and save immediately.""" if not isinstance(result, OutfitResult): diff --git a/tasks.md b/tasks.md index a0e93b0..d825df1 100644 --- a/tasks.md +++ b/tasks.md @@ -1121,6 +1121,6 @@ 问题:预览样本行用 `load_outfit_tasks()`(给生成用、跳过「完成」行)填,整表生成成功后返回空 → 下拉只剩「(选 Excel 后显示替换效果)」。预览应不看状态。 -- [ ] `services/excel_service.py`:新增 `read_all_rows(excel)`,返回每一有效数据行(标题/货号/衣服图齐全)的 `OutfitTask`,**忽略 E 列状态** -- [ ] `ai_outfit_panel._reload_sample_rows()` 改调 `read_all_rows`(预览专用);生成仍走 `load_outfit_tasks`,`_on_tasks_loaded` 仍用实际任务 -- [ ] 单测 `tests/test_excel_service.py`:`read_all_rows` 含「完成」行也返回、空字段行跳过 +- [x] `services/excel_service.py`:新增 `read_all_rows(excel)`,返回每一有效数据行(标题/货号/衣服图齐全)的 `OutfitTask`,**忽略 E 列状态** +- [x] `ai_outfit_panel._reload_sample_rows()` 改调 `read_all_rows`(预览专用);生成仍走 `load_outfit_tasks`,`_on_tasks_loaded` 仍用实际任务 +- [x] 单测 `tests/test_excel_service.py`:`read_all_rows` 含「完成」「失败」行也返回、空字段行跳过(8 用例);离屏验证全表完成的 Excel 预览下拉有样本行;全套 12 文件绿 diff --git a/tests/test_excel_service.py b/tests/test_excel_service.py index 0c54c87..b9392bc 100644 --- a/tests/test_excel_service.py +++ b/tests/test_excel_service.py @@ -96,6 +96,18 @@ class TestOutfitExcelService(unittest.TestCase): self.assertEqual(self._read_row(2), (None, "失败", "衣服图打不开")) + def test_read_all_rows_includes_done_failed_skips_incomplete(self): + from services.excel_service import read_all_rows + + rows = read_all_rows(self.excel_path) + + # rows 2(待处理) 3(完成) 4(失败) 6(待处理) — row 5 (空衣服图) skipped + self.assertEqual([r.row_index for r in rows], [2, 3, 4, 6]) + by_idx = {r.row_index: r for r in rows} + self.assertEqual(by_idx[3].status, "完成") + self.assertEqual(by_idx[4].status, "失败") + self.assertEqual(by_idx[6].title, " 保留空格 ") # raw text preserved + def test_check_excel_writable_true_for_existing_workbook(self): from services.excel_service import check_excel_writable