Files
cmshoppe/docs/tasks/T-631.md
T

8.9 KiB
Raw Blame History

id, title, phase, deps, status, created
id title phase deps status created
T-631 商品套图原图自适应展示与批量选择删除 7
T-627
TODO 2026-07-14

问题 / 背景

⑥「商品套图」的商品原图列表当前使用固定 210px 高度的 QListWidget。当原图超过当前可见行数时,列表内部出现纵向滚动;其外层配置面板本身又是 QScrollArea,形成嵌套滚动,用户不容易一次看清最多 16 张原图。

原图管理当前还存在以下限制:

  • 每次只能通过悬停删除按钮、右键菜单或 Delete 键删除当前一张图片,不能全选、反选或批量移除。
  • ProductOriginalList 同时承担单击下载、双击预览和拖拽排序;直接改成 Qt 多行选择会让预览、当前项和拖拽语义混在一起。
  • 后端只有 image_studio.remove_asset_if_unused(asset_id) 单张删除。它会在图片被生成任务或终选引用时拒绝删除,并且故意保留本地文件;若 GUI 简单循环删除多张,可能出现前几张已删除、后一张失败的部分成功状态。
  • 远程蝦皮原图可能仍在后台下载。下载过程中删除对应资产会造成 worker 回写已删除记录或遗留孤立文件的竞态。

顾客需要商品原图区域取消内部上下滚动、按图片数量自然向下展开;每张真实图片增加勾选框,标题右侧增加「全选」「反选」,并能从任意已勾选图片的右键菜单一次删除多张。

方案

1. 商品原图列表按内容自然展开

  • 取消商品原图列表当前固定 210px 高度;关闭列表自身的横向和纵向滚动条。
  • 根据列表当前可用宽度、gridSize、间距和可见 tile 数动态计算列数、行数与内容高度,并更新列表高度。计算须包含现有最少 6 个展示位语义,最多按 16 张真实原图计算。
  • 图片数量变化、窗口或 splitter 宽度变化、缩略图下载完成、拖拽排序及任务切换后都要重新计算高度;不得只在首次创建控件时计算一次。
  • 商品原图列表自然向下撑开后,由左侧外层 suiteConfigScroll 负责整页滚动;不得通过撑大主窗口、恢复列表内部滚动或制造横向滚动来容纳图片。
  • 动态高度必须有明确上限并保持布局稳定,不能因占位图、悬停删除按钮、勾选框或图片下载完成而造成行高跳变。

2. 用勾选状态管理批量选择

  • 只为具有真实 asset_id 的原图项显示复选框;「添加」占位项不得参与全选、反选、批量删除或已选数量。
  • 批量选择以每项 Qt.CheckStateRole 或等价的自定义 delegate 状态为唯一事实源,不把 QListWidget 改成容易影响拖拽的多行选择模式,也不使用会破坏现有 InternalMove 排序的 setItemWidget() 堆叠子控件方案。
  • 单击复选框只切换批量选择;单击缩略图继续执行现有的按需后台下载/当前项行为;双击继续预览;从缩略图区域拖动继续调整原图顺序。复选框点击和拖拽手势不得互相误触。
  • 在「商品原图」标题右侧依次显示 已选 N 张、全选、反选。全选与反选只处理当前项目中可见的真实原图;无真实原图时禁用。
  • 勾选状态是当前界面的临时操作状态,不写 SQLite 或 config.json。普通刷新和拖拽排序时按 asset_id 保留仍存在图片的勾选状态;切换账号、商品或套图任务及删除成功后清空,避免跨项目误删。
  • 生成中继续沿用现有禁止删除边界;选中集合中存在正在后台下载的图片时,批量删除须整体阻断并给中文提示,不能边下载边删除。

3. 右键与键盘删除交互

  • 右键已勾选图片且已选数量大于 1 时,菜单显示 删除选中的 N 张图片…;数量为 1 时显示 删除选中图片…。
  • 右键未勾选图片时,不得静默清空或扩展已有勾选集合;菜单保留单张 删除这张图片…,如同时已有其他勾选图片,可另外显示明确带数量的批量删除动作。
  • Delete/Backspace 键在存在勾选图片时按勾选集合执行批量删除;没有勾选图片时保持删除当前单张的既有语义。所有路径统一进入同一确认与服务层逻辑。
  • 删除确认框必须显示准确数量,并明确:只从当前商品的原图列表移除,不删除蝦皮线上图片,也不删除本地源文件;以后重新拉取蝦皮主图时,线上仍存在的图片可能重新出现。
  • 如果勾选集合包含当前第 1 张主图,确认框额外提示“删除后下一张图片将成为主图”。取消确认不得改变勾选状态或排序。
  • 现有悬停 × 继续作为单张快捷删除入口,但也必须复用相同的确认和后端校验,不另写一套删除逻辑。

4. 后端事务级批量移除

  • 在 app/image_studio.py 增加面向商品原图的批量服务,例如 remove_original_assets_if_unused(project_id, asset_ids, ...);GUI 不得直接循环调用单张删除函数。
  • 服务入口先归一化并去重 ID,然后一次性校验:项目存在且未删除、所有 ID 均属于该项目、资产类型均为 original、所有资产均未被 job 或 selection 引用。不得删除其他项目或生成结果资产。
  • 任意一张校验失败时,整批不删除,并返回可供 GUI 中文说明的阻断原因;禁止部分成功。
  • 全部校验通过后,在同一个 SQLite 事务中删除对应资产行,并按剩余顺序连续更新 source_order。本地图片文件继续保留,遵守现有 remove_asset_if_unused() 的非破坏性文件语义。
  • 单张删除入口可复用批量服务的单元素路径,或保留兼容包装;不能让单张与批量删除形成不同的引用校验和文件处理规则。
  • UI 层在调用服务前检查当前任务生成状态和所选资产下载状态;服务层负责项目归属、资产类型、引用和事务完整性,两层职责不得混淆。

5. 测试与界面验证

  • tests/test_product_suite_gui.py 覆盖:
    • 原图列表横向/纵向滚动条关闭,0、6、7、16 张及宽度变化后高度和行数重新计算;
    • 只有真实图片有勾选状态,全选、反选和 已选 N 张 结果正确,空列表按钮禁用;
    • 普通刷新和排序保留有效 asset ID 的勾选,项目/任务切换及删除成功清空;
    • 复选框、单击下载、双击预览和拖拽排序语义互不覆盖;
    • 右键已选/未选图片及 Delete 键产生正确的单张或批量动作,确认文案包含数量和主图变化提示;
    • 生成中或选中图片下载中时批量删除被整体阻断。
  • tests/test_image_studio.py 覆盖:
    • 多张未引用原图在单事务中删除,剩余 source_order 连续;
    • 选择中混入其他项目 ID、非原图 ID、不存在 ID 或已被 job/selection 引用的图片时全部不删除;
    • 删除资产行后本地文件仍存在;单张兼容入口与批量入口校验语义一致。
  • 在最小支持窗口、常规窗口和 Windows 100% / 125% / 150% 显示缩放下人工检查 6、9、16 张原图:列表不出现内部滚动条,外层滚动自然,复选框不遮挡缩略图或文字,标题栏操作不重叠。

验收要点

  • 商品原图列表不再固定高度或显示内部上下滚动条,最多 16 张图片按当前宽度自然换行并向下撑开,左侧配置区仍可正常滚动。
  • 每张真实原图都有可操作的复选框,标题右侧可全选、反选并显示准确已选数量;占位项不参与选择。
  • 预览、按需下载、拖拽排序、悬停单张删除与新增批量选择互不回归。
  • 从已勾选图片右键或按 Delete 可一次删除选中集合;右键未勾选图片不会暗中改变批量范围。
  • 批量删除只有全部校验通过才生效,引用中、跨项目、下载中或生成中的集合不会出现部分删除。
  • 删除后下一张正确成为主图、剩余顺序连续;本地文件和蝦皮线上图片不被删除,重新拉取线上原图的语义有明确提示。
  • 自动验证:
    • py -3.10 -m unittest tests.test_product_suite_gui
    • py -3.10 -m unittest tests.test_image_studio
    • py -3.10 -m unittest discover -s tests
    • python -m ruff check app tests main.py
    • py -3.10 -m compileall app main.py
    • git diff --check

边界(不改什么)

  • 不删除本地图片文件,不删除或修改蝦皮线上商品图片;本任务的“删除”只表示从当前本地商品项目的原图资产列表移除。
  • 不修改拉取蝦皮主图的 CDP 选择器、Chrome 前后台策略、登录检测和 Shopee 页面操作流程。
  • 不修改 cmhub 生图提交、轮询、下载、并发、重试或计费逻辑,不修改生成结果/历史结果删除语义。
  • 不把原图勾选状态持久化,不新增 SQLite 字段;只扩展现有图片资产服务的事务级批量操作。
  • 不修改①~⑤模块 UI,不修改主窗口全局样式。