feat(ai-studio): streamline project workspace

This commit is contained in:
chengma
2026-07-11 16:23:22 +08:00
parent f28d197c58
commit 82e7e1bf14
3 changed files with 219 additions and 68 deletions
+104 -58
View File
@@ -238,7 +238,7 @@ class ImageStudioPreviewDialog(QDialog):
class ImageStudioTab(QWidget): class ImageStudioTab(QWidget):
"""Sixth tab: project-based AI image studio.""" """Sixth tab: project-based AI image studio."""
PROJECT_COLUMNS = ["项目", "账号", "商品ID", "更新时间"] PROJECT_COLUMNS = ["店铺", "商品ID", "更新时间"]
ORIGINAL_COLUMNS = ["序号", "状态", "远程地址"] ORIGINAL_COLUMNS = ["序号", "状态", "远程地址"]
POOL_COLUMNS = ["类型", "比例", "状态", "来源", "本地文件"] POOL_COLUMNS = ["类型", "比例", "状态", "来源", "本地文件"]
JOB_STATUS_LABELS = { JOB_STATUS_LABELS = {
@@ -277,6 +277,7 @@ class ImageStudioTab(QWidget):
self.selected_source_asset_id = None self.selected_source_asset_id = None
self._running_worker = None self._running_worker = None
self._running_thread = None self._running_thread = None
self._operation_running = False
self._worker_error_handled = False self._worker_error_handled = False
self._download_open_after = {} self._download_open_after = {}
@@ -321,31 +322,30 @@ class ImageStudioTab(QWidget):
self.item_id_edit.setObjectName("imageStudioItemIdEdit") self.item_id_edit.setObjectName("imageStudioItemIdEdit")
self.item_id_edit.setPlaceholderText("商品ID") self.item_id_edit.setPlaceholderText("商品ID")
self.item_id_edit.setMinimumWidth(160) self.item_id_edit.setMinimumWidth(160)
self.open_project_button = QPushButton("打开商品项目") self.current_project_label = QLabel("未选择商品")
self.open_project_button.setObjectName("imageStudioOpenProjectButton")
self.current_project_label = QLabel("未打开商品项目")
self.current_project_label.setObjectName("imageStudioCurrentProjectLabel") self.current_project_label.setObjectName("imageStudioCurrentProjectLabel")
self.current_project_label.setMinimumWidth(220) self.current_project_label.setMinimumWidth(220)
self.autosave_label = QLabel("选择账号并输入商品ID后打开") self.autosave_label = QLabel("选择店铺并输入商品ID后拉取")
self.autosave_label.setObjectName("imageStudioAutosaveLabel") self.autosave_label.setObjectName("imageStudioAutosaveLabel")
self.pull_images_button = QPushButton("拉取蝦皮主图") self.pull_images_button = QPushButton("拉取蝦皮主图")
self.pull_images_button.setObjectName("imageStudioPullImagesButton") self.pull_images_button.setObjectName("imageStudioPullImagesButton")
self.open_folder_button = QPushButton("打开项目文件夹") self.open_folder_button = QPushButton("打开项目文件夹")
self.open_folder_button.setObjectName("imageStudioOpenFolderButton") self.open_folder_button.setObjectName("imageStudioOpenFolderButton")
self.delete_project_button = QPushButton("删除项目")
self.delete_project_button.setObjectName("imageStudioDeleteProjectButton")
layout.addWidget(QLabel("账号")) layout.addWidget(QLabel("账号"))
layout.addWidget(self.account_combo) layout.addWidget(self.account_combo)
layout.addWidget(QLabel("商品ID")) layout.addWidget(QLabel("商品ID"))
layout.addWidget(self.item_id_edit) layout.addWidget(self.item_id_edit)
layout.addWidget(self.open_project_button)
layout.addSpacing(8) layout.addSpacing(8)
layout.addWidget(self.current_project_label, 1) layout.addWidget(self.current_project_label, 1)
layout.addWidget(self.pull_images_button)
layout.addWidget(self.autosave_label) layout.addWidget(self.autosave_label)
layout.addStretch(1) layout.addStretch(1)
layout.addWidget(self.pull_images_button)
layout.addWidget(self.open_folder_button) layout.addWidget(self.open_folder_button)
layout.addWidget(self.delete_project_button)
return panel return panel
def _build_project_panel(self): def _build_project_panel(self):
@@ -355,7 +355,7 @@ class ImageStudioTab(QWidget):
layout.setContentsMargins(0, 0, 0, 0) layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(8) layout.setSpacing(8)
title = QLabel("商品项目") title = QLabel("商品列表")
title.setObjectName("imageStudioSectionTitle") title.setObjectName("imageStudioSectionTitle")
hint = QLabel("点击切换项目,项目会自动保存") hint = QLabel("点击切换项目,项目会自动保存")
hint.setObjectName("imageStudioMutedLabel") hint.setObjectName("imageStudioMutedLabel")
@@ -382,7 +382,7 @@ class ImageStudioTab(QWidget):
layout.setContentsMargins(0, 0, 0, 0) layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(8) layout.setSpacing(8)
self.workspace_empty_label = QLabel("选择账号并输入商品ID,点击「打开商品项目」开始。") self.workspace_empty_label = QLabel("选择店铺并输入商品ID后,点击「拉取蝦皮主图」开始。")
self.workspace_empty_label.setObjectName("imageStudioWorkspaceEmptyLabel") self.workspace_empty_label.setObjectName("imageStudioWorkspaceEmptyLabel")
self.workspace_empty_label.setWordWrap(True) self.workspace_empty_label.setWordWrap(True)
layout.addWidget(self.workspace_empty_label) layout.addWidget(self.workspace_empty_label)
@@ -635,6 +635,9 @@ class ImageStudioTab(QWidget):
background: #eef6ff; background: #eef6ff;
color: #24292f; color: #24292f;
} }
#imageStudioDeleteProjectButton {
color: #b42318;
}
QTableWidget#imageStudioOriginalTable, QTableWidget#imageStudioOriginalTable,
QTableWidget#imageStudioPoolTable, QTableWidget#imageStudioPoolTable,
QListWidget#imageStudioMainSelectionList, QListWidget#imageStudioMainSelectionList,
@@ -650,9 +653,9 @@ class ImageStudioTab(QWidget):
) )
def _connect_signals(self): def _connect_signals(self):
self.open_project_button.clicked.connect(self.open_project)
self.pull_images_button.clicked.connect(self.pull_main_images) self.pull_images_button.clicked.connect(self.pull_main_images)
self.open_folder_button.clicked.connect(self.open_project_folder) self.open_folder_button.clicked.connect(self.open_project_folder)
self.delete_project_button.clicked.connect(self.delete_current_project)
self.project_table.itemSelectionChanged.connect(self._on_project_selection_changed) self.project_table.itemSelectionChanged.connect(self._on_project_selection_changed)
self.original_table.cellClicked.connect(self._on_original_clicked) self.original_table.cellClicked.connect(self._on_original_clicked)
self.original_table.cellDoubleClicked.connect(self._on_original_double_clicked) self.original_table.cellDoubleClicked.connect(self._on_original_double_clicked)
@@ -695,18 +698,18 @@ class ImageStudioTab(QWidget):
self.projects = [] self.projects = []
self._status(f"AI工场项目读取失败:{exc}", "danger") self._status(f"AI工场项目读取失败:{exc}", "danger")
self._fill_project_table() self._fill_project_table()
if self.current_project is None and self.projects: active_ids = {int(project.id) for project in self.projects}
self._select_project(self.projects[0].id) if self.current_project is not None and int(self.current_project.id) in active_ids:
elif self.current_project is not None:
self._select_project(self.current_project.id, quiet=True) self._select_project(self.current_project.id, quiet=True)
elif self.projects:
self._select_project(self.projects[0].id)
else: else:
self._refresh_project_summary() self._clear_current_project()
def _fill_project_table(self): def _fill_project_table(self):
self.project_table.setRowCount(len(self.projects)) self.project_table.setRowCount(len(self.projects))
for row, project in enumerate(self.projects): for row, project in enumerate(self.projects):
values = [ values = [
f"{project.account_alias} / {project.item_id}",
project.account_name or project.account_alias, project.account_name or project.account_alias,
project.item_id, project.item_id,
project.updated_at, project.updated_at,
@@ -718,38 +721,11 @@ class ImageStudioTab(QWidget):
self.project_table.setRowHeight(row, 44) self.project_table.setRowHeight(row, 44)
self.project_table.resizeColumnsToContents() self.project_table.resizeColumnsToContents()
def open_project(self, checked=False):
alias = str(self.account_combo.currentData() or "").strip()
item_id = self.item_id_edit.text().strip()
if not alias:
self._message("账号未选择", "请先在④账号管理添加并选择账号。")
return
if not item_id:
self._message("商品ID不能为空", "请输入要打开的蝦皮商品ID。")
return
account = accounts.get_account(alias, path=self.db_path, config=self.config)
try:
project = image_studio.create_or_get_project(
account,
item_id=item_id,
path=self.db_path,
)
except Exception as exc:
self._message("打开商品项目失败", str(exc))
self._status(f"打开AI工场商品项目失败:{exc}", "danger")
return
self.current_project = project
self.item_id_edit.setText(project.item_id)
self._set_account_combo(project.account_alias)
self.refresh_projects()
self._select_project(project.id)
self._status("AI工场商品项目已打开", "success")
def pull_main_images(self, checked=False): def pull_main_images(self, checked=False):
alias = str(self.account_combo.currentData() or "").strip() alias = str(self.account_combo.currentData() or "").strip()
item_id = self.item_id_edit.text().strip() item_id = self.item_id_edit.text().strip()
if not alias or not item_id: if not alias or not item_id:
self._message("商品项目未打开", "请先选择账号和商品ID并打开商品项目。") self._message("信息未填写完整", "请先选择店铺并输入商品ID,再拉取蝦皮主图。")
return return
worker = ImageStudioPullImagesWorker( worker = ImageStudioPullImagesWorker(
alias, alias,
@@ -779,7 +755,7 @@ class ImageStudioTab(QWidget):
def open_project_folder(self, checked=False): def open_project_folder(self, checked=False):
if self.current_project is None: if self.current_project is None:
self._message("商品项目未打开", "请先打开一个AI工场商品项目。") self._message("未选择商品", "请先从商品列表选择商品,或拉取蝦皮主图。")
return return
dirs = image_studio.default_project_image_dirs(self.current_project, self.config) dirs = image_studio.default_project_image_dirs(self.current_project, self.config)
try: try:
@@ -791,6 +767,44 @@ class ImageStudioTab(QWidget):
return return
self._status(f"已打开项目文件夹:{opened}", "success") self._status(f"已打开项目文件夹:{opened}", "success")
def delete_current_project(self, checked=False):
project = self.current_project
if project is None:
self._message("未选择商品", "请先从商品列表选择要删除的项目。")
return
active_jobs = [
job
for job in self._list_project_jobs(project.id)
if job.status in {"pending", "submitted", "running"}
]
if active_jobs:
self._message(
"暂不能删除项目",
"该商品还有未完成的图片任务。请先等待任务完成、停止本轮或继续查询任务后再删除。",
)
return
display_name = _project_display_name(project)
if not self._confirm(
"删除项目",
f"确定从AI工场商品列表删除“{display_name}”吗?\n"
"不会删除蝦皮商品,也不会删除本地图片。再次拉取同一商品会恢复原项目记录。",
):
return
try:
image_studio.soft_delete_project(
project.id,
reason="用户从AI工场商品列表删除",
path=self.db_path,
)
except Exception as exc:
message = diagnostics.redact_log_text(str(exc or "未知错误"))
self._message("删除项目失败", f"无法从AI工场商品列表删除:{message}")
self._status(f"删除AI工场项目失败:{message}", "danger")
return
self._clear_current_project()
self.refresh_projects()
self._status(f"已从AI工场商品列表删除:{display_name}", "success")
def _on_project_selection_changed(self): def _on_project_selection_changed(self):
items = self.project_table.selectedItems() items = self.project_table.selectedItems()
if not items: if not items:
@@ -820,7 +834,7 @@ class ImageStudioTab(QWidget):
self._sync_project_selection(project.id) self._sync_project_selection(project.id)
self._refresh_project_summary() self._refresh_project_summary()
if not quiet: if not quiet:
self._status(f"当前AI工场商品项目:{project.account_alias} / {project.item_id}", "muted") self._status(f"当前AI工场商品:{_project_display_name(project)}", "muted")
def _sync_project_selection(self, project_id): def _sync_project_selection(self, project_id):
for row in range(self.project_table.rowCount()): for row in range(self.project_table.rowCount()):
@@ -1068,17 +1082,42 @@ class ImageStudioTab(QWidget):
def _refresh_project_summary(self): def _refresh_project_summary(self):
if self.current_project is None: if self.current_project is None:
self.current_project_label.setText("未打开商品项目") self.current_project_label.setText("未选择商品")
self.autosave_label.setText("选择账号并输入商品ID后打开") self.current_project_label.setToolTip("")
self.workspace_empty_label.setText("选择账号并输入商品ID,点击「打开商品项目」开始。") self.autosave_label.setText("选择店铺并输入商品ID后拉取")
self.workspace_empty_label.setText("选择店铺并输入商品ID后,点击「拉取蝦皮主图」开始。")
self._update_project_action_buttons()
return return
alias = self.current_project.account_alias self.current_project_label.setText(_project_display_name(self.current_project))
item_id = self.current_project.item_id self.current_project_label.setToolTip(f"账号别名:{self.current_project.account_alias}")
self.current_project_label.setText(f"{alias} · {item_id}")
self.autosave_label.setText("项目已自动保存") self.autosave_label.setText("项目已自动保存")
self.workspace_empty_label.setText( self.workspace_empty_label.setText(
f"当前商品:{alias} / {item_id}。按顺序拉取蝦皮主图、选择源图、生成并拖入终选。" f"当前商品:{_project_display_name(self.current_project)}。按顺序拉取蝦皮主图、选择源图、生成并拖入终选。"
) )
self._update_project_action_buttons()
def _clear_current_project(self):
self.current_project = None
self.assets = []
self.jobs = []
self.selections = []
self.selected_source_asset_id = None
self.item_id_edit.clear()
self.prompt_edit.blockSignals(True)
try:
self.prompt_edit.clear()
finally:
self.prompt_edit.blockSignals(False)
self._fill_original_table()
self._fill_pool_table()
self._refresh_selection_labels()
self._refresh_source_label()
self._refresh_project_summary()
def _update_project_action_buttons(self):
enabled = self.current_project is not None and not self._operation_running
self.open_folder_button.setEnabled(enabled)
self.delete_project_button.setEnabled(enabled)
def _update_generation_action_text(self, *args): def _update_generation_action_text(self, *args):
count = self.count_spin.value() if hasattr(self, "count_spin") else 0 count = self.count_spin.value() if hasattr(self, "count_spin") else 0
@@ -1217,7 +1256,7 @@ class ImageStudioTab(QWidget):
def start_generation(self, checked=False): def start_generation(self, checked=False):
if self.current_project is None: if self.current_project is None:
self._message("商品项目未打开", "请先打开一个AI工场商品项目。") self._message("未选择商品", "请先从商品列表选择商品,或拉取蝦皮主图。")
return return
source = self._asset_by_id(self.selected_source_asset_id) source = self._asset_by_id(self.selected_source_asset_id)
if source is None: if source is None:
@@ -1260,7 +1299,7 @@ class ImageStudioTab(QWidget):
def resume_generation_jobs(self, checked=False): def resume_generation_jobs(self, checked=False):
if self.current_project is None: if self.current_project is None:
self._message("商品项目未打开", "请先打开一个AI工场商品项目。") self._message("未选择商品", "请先从商品列表选择商品,或拉取蝦皮主图。")
return return
resumable = image_studio.list_resumable_jobs( resumable = image_studio.list_resumable_jobs(
path=self.db_path, path=self.db_path,
@@ -1289,7 +1328,7 @@ class ImageStudioTab(QWidget):
def export_selections(self, checked=False): def export_selections(self, checked=False):
if self.current_project is None: if self.current_project is None:
self._message("商品项目未打开", "请先打开一个AI工场商品项目。") self._message("未选择商品", "请先从商品列表选择商品,或拉取蝦皮主图。")
return return
main_count = len(self._selection_asset_ids("main")) main_count = len(self._selection_asset_ids("main"))
detail_count = len(self._selection_asset_ids("detail")) detail_count = len(self._selection_asset_ids("detail"))
@@ -1430,9 +1469,8 @@ class ImageStudioTab(QWidget):
self._running_worker = None self._running_worker = None
def _set_running(self, running): def _set_running(self, running):
self.open_project_button.setEnabled(not running) self._operation_running = bool(running)
self.pull_images_button.setEnabled(not running) self.pull_images_button.setEnabled(not running)
self.open_folder_button.setEnabled(not running)
self.project_table.setEnabled(not running) self.project_table.setEnabled(not running)
self.original_table.setEnabled(not running) self.original_table.setEnabled(not running)
self.pool_table.setEnabled(not running) self.pool_table.setEnabled(not running)
@@ -1451,6 +1489,7 @@ class ImageStudioTab(QWidget):
self.resume_button.setEnabled(not running) self.resume_button.setEnabled(not running)
self.stop_button.setEnabled(running) self.stop_button.setEnabled(running)
self.export_button.setEnabled(not running) self.export_button.setEnabled(not running)
self._update_project_action_buttons()
def _set_account_combo(self, alias): def _set_account_combo(self, alias):
index = self.account_combo.findData(alias) index = self.account_combo.findData(alias)
@@ -1586,6 +1625,13 @@ def _asset_badge(kind):
}.get(str(kind or ""), str(kind or "图片")) }.get(str(kind or ""), str(kind or "图片"))
def _project_display_name(project):
account_name = str(getattr(project, "account_name", "") or "").strip()
account_alias = str(getattr(project, "account_alias", "") or "").strip()
item_id = str(getattr(project, "item_id", "") or "").strip()
return f"{account_name or account_alias or '未命名店铺'} · {item_id or '未填写商品ID'}"
def _asset_status_text(asset): def _asset_status_text(asset):
status = str(getattr(asset, "status", "") or "") status = str(getattr(asset, "status", "") or "")
local_path = str(getattr(asset, "local_path", "") or "") local_path = str(getattr(asset, "local_path", "") or "")
+21 -2
View File
@@ -3,7 +3,7 @@ id: T-606
title: AI工场项目入口、商品列表与安全删除优化 title: AI工场项目入口、商品列表与安全删除优化
phase: 7 phase: 7
deps: [T-605] deps: [T-605]
status: TODO status: DONE
created: 2026-07-11 created: 2026-07-11
--- ---
@@ -99,4 +99,23 @@ git diff --check
## 执行记录 ## 执行记录
- 待执行。 - 2026-07-11:已完成。
- 代码:
- `app/gui/tabs/image_studio.py`:移除可见的「打开商品项目」入口;顶部改为店铺/商品ID输入、当前「店铺名 · 商品ID」、紧邻的「拉取蝦皮主图」以及「打开项目文件夹」「删除项目」操作组。
- `app/gui/tabs/image_studio.py`:左侧改为「商品列表」,展示店铺、商品ID、更新时间三列;当前项目标题优先显示店铺名,账号别名仅作为 tooltip。
- `app/gui/tabs/image_studio.py`:新增「删除项目」确认和软删除入口,明确不删除蝦皮商品或本地图片;删除前拦截 `pending/submitted/running` 任务,删除后显式清理当前项目 UI 并切换到剩余项目或空状态。
- `app/gui/tabs/image_studio.py`:统一未选择项目时的中文提示,移除旧按钮依赖,并让项目文件夹/删除按钮随当前项目和后台运行状态正确启用或禁用。
- 测试:
- `tests/test_gui.py`:更新 AI工场构建测试,覆盖去除旧入口、三列商品列表、顶部中文空状态和项目操作按钮状态。
- `tests/test_gui.py`:新增确认取消/确认软删除后切换项目测试,以及未完成图片任务阻止删除测试。
- 验证:
- 主工作区定向验证通过:
- `py -3.10 -m unittest tests.test_gui.GuiTests.test_image_studio_tab_builds_project_pool_and_template_controls tests.test_gui.GuiTests.test_image_studio_deletes_selected_project_only_after_safe_confirmation tests.test_gui.GuiTests.test_image_studio_blocks_deletion_with_active_remote_job tests.test_gui.GuiTests.test_image_studio_pull_main_images_uses_current_item_input tests.test_gui.GuiTests.test_image_studio_worker_thread_is_kept_until_thread_finished`
- `python -m ruff check app tests main.py`
- `py -3.10 -m compileall app main.py`
- `git diff --check -- app/gui/tabs/image_studio.py tests/test_gui.py docs/tasks/T-606.md`
- 主工作区存在未提交默认提示词文件改动;为隔离无关改动,在临时干净 worktree `D:\chengma\cmshopee-t606-verify` 应用本任务 diff 后通过:
- `python -m ruff check app tests main.py`
- `py -3.10 -m compileall app main.py`
- `py -3.10 -m unittest discover -s tests`(380 tests)
- `git diff --check`
+94 -8
View File
@@ -494,7 +494,7 @@ class GuiTests(TempDirMixin, unittest.TestCase):
with self.make_temp_dir() as temp_dir: with self.make_temp_dir() as temp_dir:
cfg = self.make_config(temp_dir) cfg = self.make_config(temp_dir)
prompts_dir = os.path.join(temp_dir, "prompts", "image_studio") prompts_dir = os.path.join(temp_dir, "prompts", "image_studio")
accounts.create_account("主店", "alias-a", debug_port=9222, config=cfg) account = accounts.create_account("主店", "alias-a", debug_port=9222, config=cfg)
prompts.save_image_studio_template("工场模板", "完整提示词", prompts_dir) prompts.save_image_studio_template("工场模板", "完整提示词", prompts_dir)
tab = ImageStudioTab( tab = ImageStudioTab(
@@ -512,19 +512,25 @@ class GuiTests(TempDirMixin, unittest.TestCase):
"导入本地图片", "导入本地图片",
" ".join(button.text() for button in tab.findChildren(gui.QPushButton)), " ".join(button.text() for button in tab.findChildren(gui.QPushButton)),
) )
self.assertEqual("打开商品项目", tab.open_project_button.text()) self.assertNotIn(
"打开商品项目",
" ".join(button.text() for button in tab.findChildren(gui.QPushButton)),
)
self.assertEqual("拉取蝦皮主图", tab.pull_images_button.text()) self.assertEqual("拉取蝦皮主图", tab.pull_images_button.text())
self.assertEqual("删除项目", tab.delete_project_button.text())
self.assertEqual("导出到文件夹", tab.export_button.text()) self.assertEqual("导出到文件夹", tab.export_button.text())
self.assertIn("可部分导出", tab.export_hint_label.text()) self.assertIn("可部分导出", tab.export_hint_label.text())
self.assertIn("文件名按终选顺序连续", tab.export_hint_label.text()) self.assertIn("文件名按终选顺序连续", tab.export_hint_label.text())
self.assertEqual("未打开商品项目", tab.current_project_label.text()) self.assertEqual("未选择商品", tab.current_project_label.text())
self.assertIn("打开商品项目", tab.workspace_empty_label.text()) self.assertIn("拉取蝦皮主图", tab.workspace_empty_label.text())
self.assertFalse(tab.open_folder_button.isEnabled())
self.assertFalse(tab.delete_project_button.isEnabled())
self.assertEqual("生成 4 张主图", tab.start_button.text()) self.assertEqual("生成 4 张主图", tab.start_button.text())
tab.count_spin.setValue(3) tab.count_spin.setValue(3)
tab.job_type_combo.setCurrentIndex(tab.job_type_combo.findData("detail")) tab.job_type_combo.setCurrentIndex(tab.job_type_combo.findData("detail"))
self.assertEqual("生成 3 张详情图", tab.start_button.text()) self.assertEqual("生成 3 张详情图", tab.start_button.text())
label_texts = "\n".join(label.text() for label in tab.findChildren(gui.QLabel)) label_texts = "\n".join(label.text() for label in tab.findChildren(gui.QLabel))
self.assertIn("商品项目", label_texts) self.assertIn("商品列表", label_texts)
self.assertIn("蝦皮原主图", label_texts) self.assertIn("蝦皮原主图", label_texts)
self.assertIn("照片池", label_texts) self.assertIn("照片池", label_texts)
self.assertIn("生成设置", label_texts) self.assertIn("生成设置", label_texts)
@@ -536,15 +542,26 @@ class GuiTests(TempDirMixin, unittest.TestCase):
tab.load_selected_template() tab.load_selected_template()
self.assertEqual("完整提示词", tab.prompt_edit.toPlainText()) self.assertEqual("完整提示词", tab.prompt_edit.toPlainText())
tab.item_id_edit.setText("51100639510") project = image_studio.create_or_get_project(
tab.open_project() account,
item_id="51100639510",
path=cfg["db_path"],
)
tab.refresh_projects()
self.assertIsNotNone(tab.current_project) self.assertIsNotNone(tab.current_project)
self.assertEqual("alias-a", tab.current_project.account_alias) self.assertEqual("alias-a", tab.current_project.account_alias)
self.assertEqual("51100639510", tab.current_project.item_id) self.assertEqual("51100639510", tab.current_project.item_id)
self.assertIn("alias-a", tab.current_project_label.text()) self.assertEqual(project.id, tab.current_project.id)
self.assertIn("主店", tab.current_project_label.text())
self.assertIn("51100639510", tab.current_project_label.text()) self.assertIn("51100639510", tab.current_project_label.text())
self.assertEqual("项目已自动保存", tab.autosave_label.text()) self.assertEqual("项目已自动保存", tab.autosave_label.text())
self.assertEqual(1, tab.project_table.rowCount()) self.assertEqual(1, tab.project_table.rowCount())
self.assertEqual(["店铺", "商品ID", "更新时间"], [
tab.project_table.horizontalHeaderItem(column).text()
for column in range(tab.project_table.columnCount())
])
self.assertTrue(tab.open_folder_button.isEnabled())
self.assertTrue(tab.delete_project_button.isEnabled())
original = image_studio.sync_original_asset_urls( original = image_studio.sync_original_asset_urls(
tab.current_project.id, tab.current_project.id,
@@ -565,6 +582,75 @@ class GuiTests(TempDirMixin, unittest.TestCase):
self.assert_removed(temp_dir) self.assert_removed(temp_dir)
def test_image_studio_deletes_selected_project_only_after_safe_confirmation(self):
with self.make_temp_dir() as temp_dir:
cfg = self.make_config(temp_dir)
db.init_db(cfg["db_path"])
first = image_studio.create_or_get_project(
account_alias="alias-a",
account_name="主店",
account_slug="alias_a",
item_id="51100639510",
path=cfg["db_path"],
)
second = image_studio.create_or_get_project(
account_alias="alias-a",
account_name="主店",
account_slug="alias_a",
item_id="51100639511",
path=cfg["db_path"],
)
tab = ImageStudioTab(config=cfg, db_path=cfg["db_path"])
self.addCleanup(tab.close)
tab._select_project(first.id)
confirmations = []
tab._confirm = lambda title, text: confirmations.append((title, text)) or False
tab.delete_current_project()
self.assertIsNotNone(image_studio.get_project(first.id, path=cfg["db_path"]))
self.assertEqual(first.id, tab.current_project.id)
self.assertIn("不会删除蝦皮商品", confirmations[0][1])
self.assertIn("不会删除本地图片", confirmations[0][1])
tab._confirm = lambda title, text: True
tab.delete_current_project()
self.assertIsNone(image_studio.get_project(first.id, path=cfg["db_path"]))
deleted = image_studio.get_project(first.id, path=cfg["db_path"], include_deleted=True)
self.assertIsNotNone(deleted)
self.assertEqual("用户从AI工场商品列表删除", deleted.deleted_reason)
self.assertIsNotNone(tab.current_project)
self.assertEqual(second.id, tab.current_project.id)
self.assertEqual(1, tab.project_table.rowCount())
self.assert_removed(temp_dir)
def test_image_studio_blocks_deletion_with_active_remote_job(self):
with self.make_temp_dir() as temp_dir:
cfg = self.make_config(temp_dir)
db.init_db(cfg["db_path"])
project = image_studio.create_or_get_project(
account_alias="alias-a",
account_name="主店",
account_slug="alias_a",
item_id="51100639510",
path=cfg["db_path"],
)
image_studio.create_job(project.id, path=cfg["db_path"])
tab = ImageStudioTab(config=cfg, db_path=cfg["db_path"])
self.addCleanup(tab.close)
tab._select_project(project.id)
messages = []
tab._message = lambda title, text: messages.append((title, text))
tab._confirm = lambda title, text: self.fail("有未完成任务时不应出现删除确认")
tab.delete_current_project()
self.assertIsNotNone(image_studio.get_project(project.id, path=cfg["db_path"]))
self.assertEqual("暂不能删除项目", messages[0][0])
self.assertIn("未完成的图片任务", messages[0][1])
self.assert_removed(temp_dir)
def test_image_studio_final_selection_order_and_guards(self): def test_image_studio_final_selection_order_and_guards(self):
with self.make_temp_dir() as temp_dir: with self.make_temp_dir() as temp_dir:
cfg = self.make_config(temp_dir) cfg = self.make_config(temp_dir)