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):
"""Sixth tab: project-based AI image studio."""
PROJECT_COLUMNS = ["项目", "账号", "商品ID", "更新时间"]
PROJECT_COLUMNS = ["店铺", "商品ID", "更新时间"]
ORIGINAL_COLUMNS = ["序号", "状态", "远程地址"]
POOL_COLUMNS = ["类型", "比例", "状态", "来源", "本地文件"]
JOB_STATUS_LABELS = {
@@ -277,6 +277,7 @@ class ImageStudioTab(QWidget):
self.selected_source_asset_id = None
self._running_worker = None
self._running_thread = None
self._operation_running = False
self._worker_error_handled = False
self._download_open_after = {}
@@ -321,31 +322,30 @@ class ImageStudioTab(QWidget):
self.item_id_edit.setObjectName("imageStudioItemIdEdit")
self.item_id_edit.setPlaceholderText("商品ID")
self.item_id_edit.setMinimumWidth(160)
self.open_project_button = QPushButton("打开商品项目")
self.open_project_button.setObjectName("imageStudioOpenProjectButton")
self.current_project_label = QLabel("未打开商品项目")
self.current_project_label = QLabel("未选择商品")
self.current_project_label.setObjectName("imageStudioCurrentProjectLabel")
self.current_project_label.setMinimumWidth(220)
self.autosave_label = QLabel("选择账号并输入商品ID后打开")
self.autosave_label = QLabel("选择店铺并输入商品ID后拉取")
self.autosave_label.setObjectName("imageStudioAutosaveLabel")
self.pull_images_button = QPushButton("拉取蝦皮主图")
self.pull_images_button.setObjectName("imageStudioPullImagesButton")
self.open_folder_button = QPushButton("打开项目文件夹")
self.open_folder_button.setObjectName("imageStudioOpenFolderButton")
self.delete_project_button = QPushButton("删除项目")
self.delete_project_button.setObjectName("imageStudioDeleteProjectButton")
layout.addWidget(QLabel("账号"))
layout.addWidget(self.account_combo)
layout.addWidget(QLabel("商品ID"))
layout.addWidget(self.item_id_edit)
layout.addWidget(self.open_project_button)
layout.addSpacing(8)
layout.addWidget(self.current_project_label, 1)
layout.addWidget(self.pull_images_button)
layout.addWidget(self.autosave_label)
layout.addStretch(1)
layout.addWidget(self.pull_images_button)
layout.addWidget(self.open_folder_button)
layout.addWidget(self.delete_project_button)
return panel
def _build_project_panel(self):
@@ -355,7 +355,7 @@ class ImageStudioTab(QWidget):
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(8)
title = QLabel("商品项目")
title = QLabel("商品列表")
title.setObjectName("imageStudioSectionTitle")
hint = QLabel("点击切换项目,项目会自动保存")
hint.setObjectName("imageStudioMutedLabel")
@@ -382,7 +382,7 @@ class ImageStudioTab(QWidget):
layout.setContentsMargins(0, 0, 0, 0)
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.setWordWrap(True)
layout.addWidget(self.workspace_empty_label)
@@ -635,6 +635,9 @@ class ImageStudioTab(QWidget):
background: #eef6ff;
color: #24292f;
}
#imageStudioDeleteProjectButton {
color: #b42318;
}
QTableWidget#imageStudioOriginalTable,
QTableWidget#imageStudioPoolTable,
QListWidget#imageStudioMainSelectionList,
@@ -650,9 +653,9 @@ class ImageStudioTab(QWidget):
)
def _connect_signals(self):
self.open_project_button.clicked.connect(self.open_project)
self.pull_images_button.clicked.connect(self.pull_main_images)
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.original_table.cellClicked.connect(self._on_original_clicked)
self.original_table.cellDoubleClicked.connect(self._on_original_double_clicked)
@@ -695,18 +698,18 @@ class ImageStudioTab(QWidget):
self.projects = []
self._status(f"AI工场项目读取失败:{exc}", "danger")
self._fill_project_table()
if self.current_project is None and self.projects:
self._select_project(self.projects[0].id)
elif self.current_project is not None:
active_ids = {int(project.id) for project in self.projects}
if self.current_project is not None and int(self.current_project.id) in active_ids:
self._select_project(self.current_project.id, quiet=True)
elif self.projects:
self._select_project(self.projects[0].id)
else:
self._refresh_project_summary()
self._clear_current_project()
def _fill_project_table(self):
self.project_table.setRowCount(len(self.projects))
for row, project in enumerate(self.projects):
values = [
f"{project.account_alias} / {project.item_id}",
project.account_name or project.account_alias,
project.item_id,
project.updated_at,
@@ -718,38 +721,11 @@ class ImageStudioTab(QWidget):
self.project_table.setRowHeight(row, 44)
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):
alias = str(self.account_combo.currentData() or "").strip()
item_id = self.item_id_edit.text().strip()
if not alias or not item_id:
self._message("商品项目未打开", "请先选择账号和商品ID并打开商品项目。")
self._message("信息未填写完整", "请先选择店铺并输入商品ID,再拉取蝦皮主图。")
return
worker = ImageStudioPullImagesWorker(
alias,
@@ -779,7 +755,7 @@ class ImageStudioTab(QWidget):
def open_project_folder(self, checked=False):
if self.current_project is None:
self._message("商品项目未打开", "请先打开一个AI工场商品项目。")
self._message("未选择商品", "请先从商品列表选择商品,或拉取蝦皮主图。")
return
dirs = image_studio.default_project_image_dirs(self.current_project, self.config)
try:
@@ -791,6 +767,44 @@ class ImageStudioTab(QWidget):
return
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):
items = self.project_table.selectedItems()
if not items:
@@ -820,7 +834,7 @@ class ImageStudioTab(QWidget):
self._sync_project_selection(project.id)
self._refresh_project_summary()
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):
for row in range(self.project_table.rowCount()):
@@ -1068,17 +1082,42 @@ class ImageStudioTab(QWidget):
def _refresh_project_summary(self):
if self.current_project is None:
self.current_project_label.setText("未打开商品项目")
self.autosave_label.setText("选择账号并输入商品ID后打开")
self.workspace_empty_label.setText("选择账号并输入商品ID,点击「打开商品项目」开始。")
self.current_project_label.setText("未选择商品")
self.current_project_label.setToolTip("")
self.autosave_label.setText("选择店铺并输入商品ID后拉取")
self.workspace_empty_label.setText("选择店铺并输入商品ID后,点击「拉取蝦皮主图」开始。")
self._update_project_action_buttons()
return
alias = self.current_project.account_alias
item_id = self.current_project.item_id
self.current_project_label.setText(f"{alias} · {item_id}")
self.current_project_label.setText(_project_display_name(self.current_project))
self.current_project_label.setToolTip(f"账号别名:{self.current_project.account_alias}")
self.autosave_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):
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):
if self.current_project is None:
self._message("商品项目未打开", "请先打开一个AI工场商品项目。")
self._message("未选择商品", "请先从商品列表选择商品,或拉取蝦皮主图。")
return
source = self._asset_by_id(self.selected_source_asset_id)
if source is None:
@@ -1260,7 +1299,7 @@ class ImageStudioTab(QWidget):
def resume_generation_jobs(self, checked=False):
if self.current_project is None:
self._message("商品项目未打开", "请先打开一个AI工场商品项目。")
self._message("未选择商品", "请先从商品列表选择商品,或拉取蝦皮主图。")
return
resumable = image_studio.list_resumable_jobs(
path=self.db_path,
@@ -1289,7 +1328,7 @@ class ImageStudioTab(QWidget):
def export_selections(self, checked=False):
if self.current_project is None:
self._message("商品项目未打开", "请先打开一个AI工场商品项目。")
self._message("未选择商品", "请先从商品列表选择商品,或拉取蝦皮主图。")
return
main_count = len(self._selection_asset_ids("main"))
detail_count = len(self._selection_asset_ids("detail"))
@@ -1430,9 +1469,8 @@ class ImageStudioTab(QWidget):
self._running_worker = None
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.open_folder_button.setEnabled(not running)
self.project_table.setEnabled(not running)
self.original_table.setEnabled(not running)
self.pool_table.setEnabled(not running)
@@ -1451,6 +1489,7 @@ class ImageStudioTab(QWidget):
self.resume_button.setEnabled(not running)
self.stop_button.setEnabled(running)
self.export_button.setEnabled(not running)
self._update_project_action_buttons()
def _set_account_combo(self, alias):
index = self.account_combo.findData(alias)
@@ -1586,6 +1625,13 @@ def _asset_badge(kind):
}.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):
status = str(getattr(asset, "status", "") or "")
local_path = str(getattr(asset, "local_path", "") or "")