feat(ai-studio): refresh workspace layout
This commit is contained in:
+317
-52
@@ -4,7 +4,7 @@ from __future__ import annotations
|
||||
|
||||
import os
|
||||
|
||||
from PySide6.QtCore import QMimeData
|
||||
from PySide6.QtCore import QMimeData, QSize
|
||||
from PySide6.QtWidgets import QListWidget, QListWidgetItem
|
||||
|
||||
from ... import accounts, appconfig, cmhub_models, db, image_studio, image_studio_export, prompts
|
||||
@@ -172,6 +172,8 @@ class ImageStudioSelectionList(QListWidget):
|
||||
if item is None:
|
||||
return
|
||||
asset_id = item.data(Qt.UserRole)
|
||||
if asset_id is None:
|
||||
return
|
||||
self.tab.remove_asset_from_selection(self.selection_type, asset_id)
|
||||
|
||||
def _show_context_menu(self, position):
|
||||
@@ -188,7 +190,8 @@ class ImageStudioSelectionList(QListWidget):
|
||||
def _row_for_asset(self, asset_id):
|
||||
for row in range(self.count()):
|
||||
item = self.item(row)
|
||||
if item is not None and int(item.data(Qt.UserRole)) == int(asset_id):
|
||||
item_asset_id = item.data(Qt.UserRole) if item is not None else None
|
||||
if item_asset_id is not None and int(item_asset_id) == int(asset_id):
|
||||
return row
|
||||
return None
|
||||
|
||||
@@ -289,6 +292,8 @@ class ImageStudioTab(QWidget):
|
||||
root.setContentsMargins(10, 8, 10, 8)
|
||||
root.setSpacing(8)
|
||||
|
||||
root.addWidget(self._build_top_bar(), 0)
|
||||
|
||||
splitter = QSplitter(Qt.Horizontal)
|
||||
splitter.setObjectName("imageStudioMainSplitter")
|
||||
splitter.addWidget(self._build_project_panel())
|
||||
@@ -299,6 +304,49 @@ class ImageStudioTab(QWidget):
|
||||
splitter.setStretchFactor(2, 2)
|
||||
root.addWidget(splitter, 1)
|
||||
root.addWidget(self._build_final_panel(), 0)
|
||||
self._apply_workspace_style()
|
||||
self._update_generation_action_text()
|
||||
|
||||
def _build_top_bar(self):
|
||||
panel = QWidget()
|
||||
panel.setObjectName("imageStudioTopBar")
|
||||
layout = QHBoxLayout(panel)
|
||||
layout.setContentsMargins(8, 6, 8, 6)
|
||||
layout.setSpacing(8)
|
||||
|
||||
self.account_combo = QComboBox()
|
||||
self.account_combo.setObjectName("imageStudioAccountCombo")
|
||||
self.account_combo.setMinimumWidth(180)
|
||||
self.item_id_edit = QLineEdit()
|
||||
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.setObjectName("imageStudioCurrentProjectLabel")
|
||||
self.current_project_label.setMinimumWidth(220)
|
||||
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")
|
||||
|
||||
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.autosave_label)
|
||||
layout.addStretch(1)
|
||||
layout.addWidget(self.pull_images_button)
|
||||
layout.addWidget(self.open_folder_button)
|
||||
return panel
|
||||
|
||||
def _build_project_panel(self):
|
||||
panel = QWidget()
|
||||
@@ -307,28 +355,13 @@ class ImageStudioTab(QWidget):
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.setSpacing(8)
|
||||
|
||||
project_form = QFormLayout()
|
||||
project_form.setLabelAlignment(Qt.AlignRight)
|
||||
self.account_combo = QComboBox()
|
||||
self.account_combo.setObjectName("imageStudioAccountCombo")
|
||||
self.item_id_edit = QLineEdit()
|
||||
self.item_id_edit.setObjectName("imageStudioItemIdEdit")
|
||||
self.item_id_edit.setPlaceholderText("商品ID")
|
||||
project_form.addRow("账号", self.account_combo)
|
||||
project_form.addRow("商品ID", self.item_id_edit)
|
||||
layout.addLayout(project_form)
|
||||
|
||||
toolbar = QHBoxLayout()
|
||||
self.open_project_button = QPushButton("打开项目")
|
||||
self.open_project_button.setObjectName("imageStudioOpenProjectButton")
|
||||
self.pull_images_button = QPushButton("拉取主图")
|
||||
self.pull_images_button.setObjectName("imageStudioPullImagesButton")
|
||||
self.open_folder_button = QPushButton("打开项目文件夹")
|
||||
self.open_folder_button.setObjectName("imageStudioOpenFolderButton")
|
||||
toolbar.addWidget(self.open_project_button)
|
||||
toolbar.addWidget(self.pull_images_button)
|
||||
toolbar.addWidget(self.open_folder_button)
|
||||
layout.addLayout(toolbar)
|
||||
title = QLabel("商品项目")
|
||||
title.setObjectName("imageStudioSectionTitle")
|
||||
hint = QLabel("点击切换项目,项目会自动保存")
|
||||
hint.setObjectName("imageStudioMutedLabel")
|
||||
hint.setWordWrap(True)
|
||||
layout.addWidget(title)
|
||||
layout.addWidget(hint)
|
||||
|
||||
self.project_table = QTableWidget(0, len(self.PROJECT_COLUMNS))
|
||||
self.project_table.setObjectName("imageStudioProjectList")
|
||||
@@ -338,6 +371,7 @@ class ImageStudioTab(QWidget):
|
||||
self.project_table.setEditTriggers(QAbstractItemView.NoEditTriggers)
|
||||
self.project_table.horizontalHeader().setStretchLastSection(True)
|
||||
self.project_table.verticalHeader().setVisible(False)
|
||||
self.project_table.setMinimumWidth(220)
|
||||
layout.addWidget(self.project_table, 1)
|
||||
return panel
|
||||
|
||||
@@ -348,8 +382,15 @@ class ImageStudioTab(QWidget):
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.setSpacing(8)
|
||||
|
||||
self.workspace_empty_label = QLabel("选择账号并输入商品ID,点击「打开商品项目」开始。")
|
||||
self.workspace_empty_label.setObjectName("imageStudioWorkspaceEmptyLabel")
|
||||
self.workspace_empty_label.setWordWrap(True)
|
||||
layout.addWidget(self.workspace_empty_label)
|
||||
|
||||
original_header = QHBoxLayout()
|
||||
original_header.addWidget(QLabel("蝦皮原主图"))
|
||||
original_title = QLabel("蝦皮原主图")
|
||||
original_title.setObjectName("imageStudioSectionTitle")
|
||||
original_header.addWidget(original_title)
|
||||
original_header.addStretch(1)
|
||||
self.original_hint_label = QLabel("单击下载并加入照片池,双击查看大图")
|
||||
self.original_hint_label.setObjectName("imageStudioOriginalHintLabel")
|
||||
@@ -364,10 +405,14 @@ class ImageStudioTab(QWidget):
|
||||
self.original_table.setEditTriggers(QAbstractItemView.NoEditTriggers)
|
||||
self.original_table.horizontalHeader().setStretchLastSection(True)
|
||||
self.original_table.verticalHeader().setVisible(False)
|
||||
self.original_table.setIconSize(QSize(72, 72))
|
||||
self.original_table.setAlternatingRowColors(False)
|
||||
layout.addWidget(self.original_table, 1)
|
||||
|
||||
pool_header = QHBoxLayout()
|
||||
pool_header.addWidget(QLabel("照片池"))
|
||||
pool_title = QLabel("照片池")
|
||||
pool_title.setObjectName("imageStudioSectionTitle")
|
||||
pool_header.addWidget(pool_title)
|
||||
pool_header.addStretch(1)
|
||||
self.source_label = QLabel("源图:未选择")
|
||||
self.source_label.setObjectName("imageStudioSourceLabel")
|
||||
@@ -384,6 +429,8 @@ class ImageStudioTab(QWidget):
|
||||
self.pool_table.setContextMenuPolicy(Qt.CustomContextMenu)
|
||||
self.pool_table.horizontalHeader().setStretchLastSection(True)
|
||||
self.pool_table.verticalHeader().setVisible(False)
|
||||
self.pool_table.setIconSize(QSize(86, 86))
|
||||
self.pool_table.setAlternatingRowColors(False)
|
||||
layout.addWidget(self.pool_table, 2)
|
||||
return panel
|
||||
|
||||
@@ -394,7 +441,24 @@ class ImageStudioTab(QWidget):
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.setSpacing(8)
|
||||
|
||||
title = QLabel("生成设置")
|
||||
title.setObjectName("imageStudioSectionTitle")
|
||||
layout.addWidget(title)
|
||||
|
||||
source_row = QHBoxLayout()
|
||||
self.source_preview_label = QLabel("未选择源图")
|
||||
self.source_preview_label.setObjectName("imageStudioSourcePreview")
|
||||
self.source_preview_label.setAlignment(Qt.AlignCenter)
|
||||
self.source_preview_label.setMinimumSize(86, 86)
|
||||
source_note = QLabel("当前源图\n单击照片池图片设为源图,双击查看大图")
|
||||
source_note.setObjectName("imageStudioMutedLabel")
|
||||
source_note.setWordWrap(True)
|
||||
source_row.addWidget(self.source_preview_label, 0)
|
||||
source_row.addWidget(source_note, 1)
|
||||
layout.addLayout(source_row)
|
||||
|
||||
template_layout = QGridLayout()
|
||||
template_layout.setSpacing(6)
|
||||
self.template_combo = QComboBox()
|
||||
self.template_combo.setObjectName("imageStudioTemplateCombo")
|
||||
self.template_new_button = QPushButton("新建")
|
||||
@@ -405,11 +469,14 @@ class ImageStudioTab(QWidget):
|
||||
self.template_save_button.setObjectName("imageStudioTemplateSaveButton")
|
||||
self.template_delete_button = QPushButton("删除")
|
||||
self.template_delete_button.setObjectName("imageStudioTemplateDeleteButton")
|
||||
template_layout.addWidget(self.template_combo, 0, 0, 1, 2)
|
||||
template_layout.addWidget(self.template_new_button, 0, 2)
|
||||
template_layout.addWidget(self.template_rename_button, 1, 0)
|
||||
template_layout.addWidget(self.template_save_button, 1, 1)
|
||||
template_layout.addWidget(self.template_delete_button, 1, 2)
|
||||
template_label = QLabel("提示词模板")
|
||||
template_label.setObjectName("imageStudioMutedLabel")
|
||||
template_layout.addWidget(template_label, 0, 0, 1, 3)
|
||||
template_layout.addWidget(self.template_combo, 1, 0, 1, 3)
|
||||
template_layout.addWidget(self.template_new_button, 2, 0)
|
||||
template_layout.addWidget(self.template_rename_button, 2, 1)
|
||||
template_layout.addWidget(self.template_save_button, 2, 2)
|
||||
template_layout.addWidget(self.template_delete_button, 3, 2)
|
||||
layout.addLayout(template_layout)
|
||||
|
||||
self.prompt_edit = QPlainTextEdit()
|
||||
@@ -417,6 +484,10 @@ class ImageStudioTab(QWidget):
|
||||
self.prompt_edit.setPlaceholderText("输入完整图片生成提示词")
|
||||
self.prompt_edit.setMinimumHeight(160)
|
||||
layout.addWidget(self.prompt_edit, 2)
|
||||
prompt_hint = QLabel("程序原样提交完整提示词,不自动拆分,也不自动追加动作词。")
|
||||
prompt_hint.setObjectName("imageStudioMutedLabel")
|
||||
prompt_hint.setWordWrap(True)
|
||||
layout.addWidget(prompt_hint)
|
||||
|
||||
form = QFormLayout()
|
||||
self.job_type_combo = QComboBox()
|
||||
@@ -440,9 +511,13 @@ class ImageStudioTab(QWidget):
|
||||
self.billing_label.setObjectName("imageStudioBillingLabel")
|
||||
self.billing_label.setWordWrap(True)
|
||||
layout.addWidget(self.billing_label)
|
||||
self.generation_guard_label = QLabel("不会在失败后静默切换生成来源。")
|
||||
self.generation_guard_label.setObjectName("imageStudioMutedLabel")
|
||||
self.generation_guard_label.setWordWrap(True)
|
||||
layout.addWidget(self.generation_guard_label)
|
||||
|
||||
action_layout = QHBoxLayout()
|
||||
self.start_button = QPushButton("开始生成")
|
||||
self.start_button = QPushButton("生成 4 张主图")
|
||||
self.start_button.setObjectName("imageStudioStartButton")
|
||||
self.resume_button = QPushButton("继续查询任务")
|
||||
self.resume_button.setObjectName("imageStudioResumeButton")
|
||||
@@ -476,10 +551,14 @@ class ImageStudioTab(QWidget):
|
||||
main_panel = QWidget()
|
||||
main_layout = QVBoxLayout(main_panel)
|
||||
main_layout.setContentsMargins(0, 0, 0, 0)
|
||||
final_title = QLabel("终选与排序")
|
||||
final_title.setObjectName("imageStudioSectionTitle")
|
||||
main_layout.addWidget(final_title)
|
||||
self.main_selection_label = QLabel("主图终选 0/9")
|
||||
self.main_selection_label.setObjectName("imageStudioMainSelectionLabel")
|
||||
self.main_selection_list = ImageStudioSelectionList("main", self)
|
||||
self.main_selection_list.setObjectName("imageStudioMainSelectionList")
|
||||
self.main_selection_list.setIconSize(QSize(48, 48))
|
||||
self.main_selection_list.setMinimumHeight(96)
|
||||
main_layout.addWidget(self.main_selection_label)
|
||||
main_layout.addWidget(self.main_selection_list)
|
||||
@@ -487,19 +566,22 @@ class ImageStudioTab(QWidget):
|
||||
detail_panel = QWidget()
|
||||
detail_layout = QVBoxLayout(detail_panel)
|
||||
detail_layout.setContentsMargins(0, 0, 0, 0)
|
||||
detail_layout.addWidget(QLabel(" "))
|
||||
self.detail_selection_label = QLabel("详情图终选 0/12")
|
||||
self.detail_selection_label.setObjectName("imageStudioDetailSelectionLabel")
|
||||
self.detail_selection_list = ImageStudioSelectionList("detail", self)
|
||||
self.detail_selection_list.setObjectName("imageStudioDetailSelectionList")
|
||||
self.detail_selection_list.setIconSize(QSize(42, 42))
|
||||
self.detail_selection_list.setMinimumHeight(96)
|
||||
detail_layout.addWidget(self.detail_selection_label)
|
||||
detail_layout.addWidget(self.detail_selection_list)
|
||||
action_panel = QWidget()
|
||||
action_layout = QVBoxLayout(action_panel)
|
||||
action_layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.export_button = QPushButton("导出终选")
|
||||
action_layout.addWidget(QLabel(" "))
|
||||
self.export_button = QPushButton("导出到文件夹")
|
||||
self.export_button.setObjectName("imageStudioExportButton")
|
||||
self.export_hint_label = QLabel("可部分导出,不要求主图/详情图满额")
|
||||
self.export_hint_label = QLabel("可部分导出;文件名按终选顺序连续,目标目录存在时可覆盖或新建时间目录。")
|
||||
self.export_hint_label.setObjectName("imageStudioExportHintLabel")
|
||||
self.export_hint_label.setWordWrap(True)
|
||||
action_layout.addWidget(self.export_button)
|
||||
@@ -510,6 +592,63 @@ class ImageStudioTab(QWidget):
|
||||
layout.addWidget(action_panel, 0)
|
||||
return panel
|
||||
|
||||
def _apply_workspace_style(self):
|
||||
self.setStyleSheet(
|
||||
"""
|
||||
#imageStudioTopBar,
|
||||
#imageStudioProjectPanel,
|
||||
#imageStudioPoolPanel,
|
||||
#imageStudioGenerationPanel,
|
||||
#imageStudioFinalPanel {
|
||||
background: #ffffff;
|
||||
border: 1px solid #d8dee4;
|
||||
border-radius: 6px;
|
||||
}
|
||||
#imageStudioTopBar {
|
||||
background: #f6f8fa;
|
||||
}
|
||||
#imageStudioSectionTitle {
|
||||
color: #202938;
|
||||
font-weight: 600;
|
||||
}
|
||||
#imageStudioMutedLabel,
|
||||
#imageStudioOriginalHintLabel,
|
||||
#imageStudioAutosaveLabel,
|
||||
#imageStudioWorkspaceEmptyLabel,
|
||||
#imageStudioExportHintLabel {
|
||||
color: #6e7781;
|
||||
}
|
||||
#imageStudioCurrentProjectLabel {
|
||||
color: #24292f;
|
||||
font-weight: 600;
|
||||
}
|
||||
#imageStudioSourcePreview {
|
||||
border: 1px dashed #bfc8d3;
|
||||
border-radius: 6px;
|
||||
background: #fbfcfe;
|
||||
color: #6e7781;
|
||||
}
|
||||
#imageStudioBillingLabel {
|
||||
border: 1px solid #b9d7fa;
|
||||
border-radius: 5px;
|
||||
padding: 6px;
|
||||
background: #eef6ff;
|
||||
color: #24292f;
|
||||
}
|
||||
QTableWidget#imageStudioOriginalTable,
|
||||
QTableWidget#imageStudioPoolTable,
|
||||
QListWidget#imageStudioMainSelectionList,
|
||||
QListWidget#imageStudioDetailSelectionList {
|
||||
border: 1px solid #d8dee4;
|
||||
border-radius: 5px;
|
||||
background: #fbfcfe;
|
||||
gridline-color: #eaeef2;
|
||||
selection-background-color: #ddf4ff;
|
||||
selection-color: #24292f;
|
||||
}
|
||||
"""
|
||||
)
|
||||
|
||||
def _connect_signals(self):
|
||||
self.open_project_button.clicked.connect(self.open_project)
|
||||
self.pull_images_button.clicked.connect(self.pull_main_images)
|
||||
@@ -526,6 +665,8 @@ class ImageStudioTab(QWidget):
|
||||
self.template_save_button.clicked.connect(self.save_template)
|
||||
self.template_delete_button.clicked.connect(self.delete_template)
|
||||
self.prompt_edit.textChanged.connect(self._save_project_prompt)
|
||||
self.job_type_combo.currentIndexChanged.connect(self._update_generation_action_text)
|
||||
self.count_spin.valueChanged.connect(self._update_generation_action_text)
|
||||
self.start_button.clicked.connect(self.start_generation)
|
||||
self.resume_button.clicked.connect(self.resume_generation_jobs)
|
||||
self.stop_button.clicked.connect(self.stop_generation)
|
||||
@@ -558,6 +699,8 @@ class ImageStudioTab(QWidget):
|
||||
self._select_project(self.projects[0].id)
|
||||
elif self.current_project is not None:
|
||||
self._select_project(self.current_project.id, quiet=True)
|
||||
else:
|
||||
self._refresh_project_summary()
|
||||
|
||||
def _fill_project_table(self):
|
||||
self.project_table.setRowCount(len(self.projects))
|
||||
@@ -572,6 +715,7 @@ class ImageStudioTab(QWidget):
|
||||
item = QTableWidgetItem(str(value or ""))
|
||||
item.setData(Qt.UserRole, int(project.id))
|
||||
self.project_table.setItem(row, column, item)
|
||||
self.project_table.setRowHeight(row, 44)
|
||||
self.project_table.resizeColumnsToContents()
|
||||
|
||||
def open_project(self, checked=False):
|
||||
@@ -591,21 +735,21 @@ class ImageStudioTab(QWidget):
|
||||
path=self.db_path,
|
||||
)
|
||||
except Exception as exc:
|
||||
self._message("打开项目失败", str(exc))
|
||||
self._status(f"打开AI工场项目失败:{exc}", "danger")
|
||||
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")
|
||||
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,
|
||||
@@ -616,11 +760,11 @@ class ImageStudioTab(QWidget):
|
||||
worker.log.connect(self._append_log)
|
||||
worker.finished.connect(self._on_pull_finished)
|
||||
worker.failed.connect(self._on_worker_failed)
|
||||
self._start_worker(worker, "AI工场拉取主图")
|
||||
self._append_log("[AI工场] 拉取主图开始")
|
||||
self._start_worker(worker, "AI工场拉取蝦皮主图")
|
||||
self._append_log("[AI工场] 拉取蝦皮主图开始")
|
||||
|
||||
def _on_pull_finished(self, summary):
|
||||
if self._handle_finished_error(summary, "拉取主图失败"):
|
||||
if self._handle_finished_error(summary, "拉取蝦皮主图失败"):
|
||||
return
|
||||
project = summary.get("project")
|
||||
if project is not None:
|
||||
@@ -635,7 +779,7 @@ class ImageStudioTab(QWidget):
|
||||
|
||||
def open_project_folder(self, checked=False):
|
||||
if self.current_project is None:
|
||||
self._message("项目未打开", "请先打开一个AI工场项目。")
|
||||
self._message("商品项目未打开", "请先打开一个AI工场商品项目。")
|
||||
return
|
||||
dirs = image_studio.default_project_image_dirs(self.current_project, self.config)
|
||||
try:
|
||||
@@ -674,8 +818,9 @@ class ImageStudioTab(QWidget):
|
||||
self.selected_source_asset_id = None
|
||||
self.refresh_project_assets()
|
||||
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.account_alias} / {project.item_id}", "muted")
|
||||
|
||||
def _sync_project_selection(self, project_id):
|
||||
for row in range(self.project_table.rowCount()):
|
||||
@@ -694,6 +839,7 @@ class ImageStudioTab(QWidget):
|
||||
self.assets = image_studio.list_assets(self.current_project.id, path=self.db_path)
|
||||
self.jobs = self._list_project_jobs(self.current_project.id)
|
||||
self.selections = image_studio.list_selections(self.current_project.id, path=self.db_path)
|
||||
self._refresh_project_summary()
|
||||
self._fill_original_table()
|
||||
self._fill_pool_table()
|
||||
self._refresh_selection_labels()
|
||||
@@ -703,15 +849,23 @@ class ImageStudioTab(QWidget):
|
||||
originals = [asset for asset in self.assets if asset.kind == image_studio.ASSET_KIND_ORIGINAL]
|
||||
self.original_table.setRowCount(len(originals))
|
||||
for row, asset in enumerate(originals):
|
||||
order = asset.source_order or row + 1
|
||||
values = [
|
||||
str(asset.source_order or row + 1),
|
||||
f"主图 #{order}",
|
||||
_asset_status_text(asset),
|
||||
asset.remote_url or "",
|
||||
]
|
||||
for column, value in enumerate(values):
|
||||
item = QTableWidgetItem(str(value or ""))
|
||||
item.setData(Qt.UserRole, {"type": "asset", "asset_id": int(asset.id)})
|
||||
if column == 0:
|
||||
item.setIcon(_asset_icon(asset, "原"))
|
||||
item.setToolTip("单击下载并加入照片池,双击查看大图")
|
||||
self.original_table.setItem(row, column, item)
|
||||
self.original_table.setRowHeight(row, 84)
|
||||
if originals:
|
||||
self.original_table.setColumnWidth(0, 132)
|
||||
self.original_table.setColumnWidth(1, 96)
|
||||
self.original_table.resizeColumnsToContents()
|
||||
|
||||
def _fill_pool_table(self):
|
||||
@@ -730,7 +884,7 @@ class ImageStudioTab(QWidget):
|
||||
if row_type == "asset":
|
||||
draggable = _asset_is_usable(obj)
|
||||
values = [
|
||||
_asset_badge(obj.kind),
|
||||
f"{_asset_badge(obj.kind)} #{obj.id}",
|
||||
obj.aspect_ratio or "未知",
|
||||
_asset_status_text(obj),
|
||||
_source_text(obj, self.assets),
|
||||
@@ -749,7 +903,24 @@ class ImageStudioTab(QWidget):
|
||||
for column, value in enumerate(values):
|
||||
item = QTableWidgetItem(str(value or ""))
|
||||
item.setData(Qt.UserRole, data)
|
||||
if row_type == "asset" and column == 0:
|
||||
item.setIcon(_asset_icon(obj, _asset_badge(obj.kind)))
|
||||
item.setToolTip("单击设为源图,双击查看大图,可拖入终选槽。")
|
||||
if row_type == "job" and column == 0:
|
||||
item.setIcon(_job_icon(obj.status))
|
||||
item.setToolTip("cmhub 生图任务已保存,可继续查询。")
|
||||
if row_type == "asset" and obj.id == self.selected_source_asset_id:
|
||||
item.setBackground(QColor("#eaf2ff"))
|
||||
if row_type == "job" and obj.status in {"failed", "expired"}:
|
||||
item.setForeground(_qcolor(COLOR_DANGER))
|
||||
elif row_type == "job" and obj.status in {"pending", "submitted", "running"}:
|
||||
item.setForeground(_qcolor(COLOR_WARNING))
|
||||
self.pool_table.setItem(row, column, item)
|
||||
self.pool_table.setRowHeight(row, 104)
|
||||
if rows:
|
||||
self.pool_table.setColumnWidth(0, 150)
|
||||
self.pool_table.setColumnWidth(1, 68)
|
||||
self.pool_table.setColumnWidth(2, 170)
|
||||
self.pool_table.resizeColumnsToContents()
|
||||
|
||||
def _selection_asset_ids(self, selection_type):
|
||||
@@ -888,8 +1059,31 @@ class ImageStudioTab(QWidget):
|
||||
asset = self._asset_by_id(self.selected_source_asset_id)
|
||||
if asset is None:
|
||||
self.source_label.setText("源图:未选择")
|
||||
self.source_preview_label.setPixmap(QPixmap())
|
||||
self.source_preview_label.setText("未选择源图")
|
||||
return
|
||||
self.source_label.setText(f"源图:{_asset_badge(asset.kind)} #{asset.id}")
|
||||
self.source_preview_label.setText("")
|
||||
self.source_preview_label.setPixmap(_asset_pixmap(asset, QSize(86, 86)))
|
||||
|
||||
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,点击「打开商品项目」开始。")
|
||||
return
|
||||
alias = self.current_project.account_alias
|
||||
item_id = self.current_project.item_id
|
||||
self.current_project_label.setText(f"{alias} · {item_id}")
|
||||
self.autosave_label.setText("项目已自动保存")
|
||||
self.workspace_empty_label.setText(
|
||||
f"当前商品:{alias} / {item_id}。按顺序拉取蝦皮主图、选择源图、生成并拖入终选。"
|
||||
)
|
||||
|
||||
def _update_generation_action_text(self, *args):
|
||||
count = self.count_spin.value() if hasattr(self, "count_spin") else 0
|
||||
label = self.job_type_combo.currentText() if hasattr(self, "job_type_combo") else "图片"
|
||||
self.start_button.setText(f"生成 {count} 张{label}")
|
||||
|
||||
def _open_preview(self, asset):
|
||||
dialog = ImageStudioPreviewDialog(asset, self)
|
||||
@@ -1023,7 +1217,7 @@ class ImageStudioTab(QWidget):
|
||||
|
||||
def start_generation(self, checked=False):
|
||||
if self.current_project is None:
|
||||
self._message("项目未打开", "请先打开一个AI工场项目。")
|
||||
self._message("商品项目未打开", "请先打开一个AI工场商品项目。")
|
||||
return
|
||||
source = self._asset_by_id(self.selected_source_asset_id)
|
||||
if source is None:
|
||||
@@ -1066,7 +1260,7 @@ class ImageStudioTab(QWidget):
|
||||
|
||||
def resume_generation_jobs(self, checked=False):
|
||||
if self.current_project is None:
|
||||
self._message("项目未打开", "请先打开一个AI工场项目。")
|
||||
self._message("商品项目未打开", "请先打开一个AI工场商品项目。")
|
||||
return
|
||||
resumable = image_studio.list_resumable_jobs(
|
||||
path=self.db_path,
|
||||
@@ -1095,7 +1289,7 @@ class ImageStudioTab(QWidget):
|
||||
|
||||
def export_selections(self, checked=False):
|
||||
if self.current_project is None:
|
||||
self._message("项目未打开", "请先打开一个AI工场项目。")
|
||||
self._message("商品项目未打开", "请先打开一个AI工场商品项目。")
|
||||
return
|
||||
main_count = len(self._selection_asset_ids("main"))
|
||||
detail_count = len(self._selection_asset_ids("detail"))
|
||||
@@ -1290,10 +1484,8 @@ class ImageStudioTab(QWidget):
|
||||
|
||||
def _fill_selection_list(self, widget, selection_type):
|
||||
widget.clear()
|
||||
for index, selection in enumerate(
|
||||
[item for item in self.selections if item.selection_type == selection_type],
|
||||
start=1,
|
||||
):
|
||||
selected = [item for item in self.selections if item.selection_type == selection_type]
|
||||
for index, selection in enumerate(selected, start=1):
|
||||
asset = self._asset_by_id(selection.asset_id)
|
||||
if asset is None:
|
||||
text = f"{index}. 缺失照片 #{selection.asset_id}"
|
||||
@@ -1305,6 +1497,7 @@ class ImageStudioTab(QWidget):
|
||||
continue
|
||||
text = f"{index}. {_asset_badge(asset.kind)} #{asset.id} · {asset.aspect_ratio or '未知比例'}"
|
||||
item = QListWidgetItem(text)
|
||||
item.setIcon(_asset_icon(asset, str(index)))
|
||||
item.setData(Qt.UserRole, int(asset.id))
|
||||
tooltip = _selection_tooltip(selection_type, asset)
|
||||
item.setToolTip(tooltip)
|
||||
@@ -1315,6 +1508,15 @@ class ImageStudioTab(QWidget):
|
||||
item.setForeground(_qcolor(COLOR_MUTED))
|
||||
item.setToolTip(tooltip + "\n本地文件缺失,不能用于导出。")
|
||||
widget.addItem(item)
|
||||
target = self._selection_target_count(selection_type)
|
||||
for index in range(len(selected) + 1, target + 1):
|
||||
item = QListWidgetItem(f"{index}. 空位")
|
||||
item.setIcon(QIcon(_placeholder_pixmap("空", QSize(48, 48), "#f6f8fa")))
|
||||
item.setData(Qt.UserRole, None)
|
||||
item.setToolTip(f"{_selection_label(selection_type)}空位,可从照片池拖入图片。")
|
||||
item.setForeground(_qcolor(COLOR_MUTED))
|
||||
item.setFlags(Qt.NoItemFlags)
|
||||
widget.addItem(item)
|
||||
|
||||
def _asset_from_table_row(self, table, row):
|
||||
data = self._row_data(table, row)
|
||||
@@ -1405,6 +1607,69 @@ def _asset_is_usable(asset):
|
||||
return bool(local_path and os.path.isfile(local_path))
|
||||
|
||||
|
||||
def _asset_icon(asset, fallback_label):
|
||||
return QIcon(_asset_pixmap(asset, QSize(86, 86), fallback_label=fallback_label))
|
||||
|
||||
|
||||
def _job_icon(status):
|
||||
label = {
|
||||
"pending": "排",
|
||||
"submitted": "提",
|
||||
"running": "生",
|
||||
"failed": "败",
|
||||
"expired": "过",
|
||||
"cancelled": "停",
|
||||
}.get(str(status or ""), "任")
|
||||
color = {
|
||||
"failed": "#ffebe9",
|
||||
"expired": "#ffebe9",
|
||||
"cancelled": "#f6f8fa",
|
||||
"running": "#fff8c5",
|
||||
"submitted": "#fff8c5",
|
||||
"pending": "#f6f8fa",
|
||||
}.get(str(status or ""), "#f6f8fa")
|
||||
return QIcon(_placeholder_pixmap(label, QSize(86, 86), color))
|
||||
|
||||
|
||||
def _asset_pixmap(asset, size, fallback_label=None):
|
||||
path = str(getattr(asset, "local_path", "") or "")
|
||||
if path and os.path.isfile(path):
|
||||
image = QImage(path)
|
||||
if not image.isNull():
|
||||
scaled = QPixmap.fromImage(image).scaled(size, Qt.KeepAspectRatio, Qt.SmoothTransformation)
|
||||
canvas = _placeholder_pixmap("", size, "#f6f8fa")
|
||||
painter = QPainter(canvas)
|
||||
painter.drawPixmap(
|
||||
(size.width() - scaled.width()) // 2,
|
||||
(size.height() - scaled.height()) // 2,
|
||||
scaled,
|
||||
)
|
||||
painter.end()
|
||||
return canvas
|
||||
label = str(fallback_label or _asset_badge(getattr(asset, "kind", "")) or "图")[:2]
|
||||
color = {
|
||||
"original": "#dce9f7",
|
||||
"generated_main": "#ddf4ff",
|
||||
"generated_detail": "#dafbe1",
|
||||
}.get(str(getattr(asset, "kind", "") or ""), "#f6f8fa")
|
||||
if str(getattr(asset, "status", "") or "") == image_studio.ASSET_STATUS_MISSING:
|
||||
color = "#ffebe9"
|
||||
return _placeholder_pixmap(label, size, color)
|
||||
|
||||
|
||||
def _placeholder_pixmap(label, size, color):
|
||||
pixmap = QPixmap(size)
|
||||
pixmap.fill(QColor(color))
|
||||
painter = QPainter(pixmap)
|
||||
painter.setPen(QColor("#d0d7de"))
|
||||
painter.drawRect(0, 0, size.width() - 1, size.height() - 1)
|
||||
if label:
|
||||
painter.setPen(QColor("#57606a"))
|
||||
painter.drawText(pixmap.rect(), Qt.AlignCenter, str(label))
|
||||
painter.end()
|
||||
return pixmap
|
||||
|
||||
|
||||
def _source_text(asset, assets):
|
||||
parent_id = getattr(asset, "parent_asset_id", None)
|
||||
if not parent_id:
|
||||
|
||||
Reference in New Issue
Block a user