feat(ai-studio): refresh workspace layout
This commit is contained in:
+317
-52
@@ -4,7 +4,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from PySide6.QtCore import QMimeData
|
from PySide6.QtCore import QMimeData, QSize
|
||||||
from PySide6.QtWidgets import QListWidget, QListWidgetItem
|
from PySide6.QtWidgets import QListWidget, QListWidgetItem
|
||||||
|
|
||||||
from ... import accounts, appconfig, cmhub_models, db, image_studio, image_studio_export, prompts
|
from ... import accounts, appconfig, cmhub_models, db, image_studio, image_studio_export, prompts
|
||||||
@@ -172,6 +172,8 @@ class ImageStudioSelectionList(QListWidget):
|
|||||||
if item is None:
|
if item is None:
|
||||||
return
|
return
|
||||||
asset_id = item.data(Qt.UserRole)
|
asset_id = item.data(Qt.UserRole)
|
||||||
|
if asset_id is None:
|
||||||
|
return
|
||||||
self.tab.remove_asset_from_selection(self.selection_type, asset_id)
|
self.tab.remove_asset_from_selection(self.selection_type, asset_id)
|
||||||
|
|
||||||
def _show_context_menu(self, position):
|
def _show_context_menu(self, position):
|
||||||
@@ -188,7 +190,8 @@ class ImageStudioSelectionList(QListWidget):
|
|||||||
def _row_for_asset(self, asset_id):
|
def _row_for_asset(self, asset_id):
|
||||||
for row in range(self.count()):
|
for row in range(self.count()):
|
||||||
item = self.item(row)
|
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 row
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -289,6 +292,8 @@ class ImageStudioTab(QWidget):
|
|||||||
root.setContentsMargins(10, 8, 10, 8)
|
root.setContentsMargins(10, 8, 10, 8)
|
||||||
root.setSpacing(8)
|
root.setSpacing(8)
|
||||||
|
|
||||||
|
root.addWidget(self._build_top_bar(), 0)
|
||||||
|
|
||||||
splitter = QSplitter(Qt.Horizontal)
|
splitter = QSplitter(Qt.Horizontal)
|
||||||
splitter.setObjectName("imageStudioMainSplitter")
|
splitter.setObjectName("imageStudioMainSplitter")
|
||||||
splitter.addWidget(self._build_project_panel())
|
splitter.addWidget(self._build_project_panel())
|
||||||
@@ -299,6 +304,49 @@ class ImageStudioTab(QWidget):
|
|||||||
splitter.setStretchFactor(2, 2)
|
splitter.setStretchFactor(2, 2)
|
||||||
root.addWidget(splitter, 1)
|
root.addWidget(splitter, 1)
|
||||||
root.addWidget(self._build_final_panel(), 0)
|
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):
|
def _build_project_panel(self):
|
||||||
panel = QWidget()
|
panel = QWidget()
|
||||||
@@ -307,28 +355,13 @@ class ImageStudioTab(QWidget):
|
|||||||
layout.setContentsMargins(0, 0, 0, 0)
|
layout.setContentsMargins(0, 0, 0, 0)
|
||||||
layout.setSpacing(8)
|
layout.setSpacing(8)
|
||||||
|
|
||||||
project_form = QFormLayout()
|
title = QLabel("商品项目")
|
||||||
project_form.setLabelAlignment(Qt.AlignRight)
|
title.setObjectName("imageStudioSectionTitle")
|
||||||
self.account_combo = QComboBox()
|
hint = QLabel("点击切换项目,项目会自动保存")
|
||||||
self.account_combo.setObjectName("imageStudioAccountCombo")
|
hint.setObjectName("imageStudioMutedLabel")
|
||||||
self.item_id_edit = QLineEdit()
|
hint.setWordWrap(True)
|
||||||
self.item_id_edit.setObjectName("imageStudioItemIdEdit")
|
layout.addWidget(title)
|
||||||
self.item_id_edit.setPlaceholderText("商品ID")
|
layout.addWidget(hint)
|
||||||
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)
|
|
||||||
|
|
||||||
self.project_table = QTableWidget(0, len(self.PROJECT_COLUMNS))
|
self.project_table = QTableWidget(0, len(self.PROJECT_COLUMNS))
|
||||||
self.project_table.setObjectName("imageStudioProjectList")
|
self.project_table.setObjectName("imageStudioProjectList")
|
||||||
@@ -338,6 +371,7 @@ class ImageStudioTab(QWidget):
|
|||||||
self.project_table.setEditTriggers(QAbstractItemView.NoEditTriggers)
|
self.project_table.setEditTriggers(QAbstractItemView.NoEditTriggers)
|
||||||
self.project_table.horizontalHeader().setStretchLastSection(True)
|
self.project_table.horizontalHeader().setStretchLastSection(True)
|
||||||
self.project_table.verticalHeader().setVisible(False)
|
self.project_table.verticalHeader().setVisible(False)
|
||||||
|
self.project_table.setMinimumWidth(220)
|
||||||
layout.addWidget(self.project_table, 1)
|
layout.addWidget(self.project_table, 1)
|
||||||
return panel
|
return panel
|
||||||
|
|
||||||
@@ -348,8 +382,15 @@ 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.setObjectName("imageStudioWorkspaceEmptyLabel")
|
||||||
|
self.workspace_empty_label.setWordWrap(True)
|
||||||
|
layout.addWidget(self.workspace_empty_label)
|
||||||
|
|
||||||
original_header = QHBoxLayout()
|
original_header = QHBoxLayout()
|
||||||
original_header.addWidget(QLabel("蝦皮原主图"))
|
original_title = QLabel("蝦皮原主图")
|
||||||
|
original_title.setObjectName("imageStudioSectionTitle")
|
||||||
|
original_header.addWidget(original_title)
|
||||||
original_header.addStretch(1)
|
original_header.addStretch(1)
|
||||||
self.original_hint_label = QLabel("单击下载并加入照片池,双击查看大图")
|
self.original_hint_label = QLabel("单击下载并加入照片池,双击查看大图")
|
||||||
self.original_hint_label.setObjectName("imageStudioOriginalHintLabel")
|
self.original_hint_label.setObjectName("imageStudioOriginalHintLabel")
|
||||||
@@ -364,10 +405,14 @@ class ImageStudioTab(QWidget):
|
|||||||
self.original_table.setEditTriggers(QAbstractItemView.NoEditTriggers)
|
self.original_table.setEditTriggers(QAbstractItemView.NoEditTriggers)
|
||||||
self.original_table.horizontalHeader().setStretchLastSection(True)
|
self.original_table.horizontalHeader().setStretchLastSection(True)
|
||||||
self.original_table.verticalHeader().setVisible(False)
|
self.original_table.verticalHeader().setVisible(False)
|
||||||
|
self.original_table.setIconSize(QSize(72, 72))
|
||||||
|
self.original_table.setAlternatingRowColors(False)
|
||||||
layout.addWidget(self.original_table, 1)
|
layout.addWidget(self.original_table, 1)
|
||||||
|
|
||||||
pool_header = QHBoxLayout()
|
pool_header = QHBoxLayout()
|
||||||
pool_header.addWidget(QLabel("照片池"))
|
pool_title = QLabel("照片池")
|
||||||
|
pool_title.setObjectName("imageStudioSectionTitle")
|
||||||
|
pool_header.addWidget(pool_title)
|
||||||
pool_header.addStretch(1)
|
pool_header.addStretch(1)
|
||||||
self.source_label = QLabel("源图:未选择")
|
self.source_label = QLabel("源图:未选择")
|
||||||
self.source_label.setObjectName("imageStudioSourceLabel")
|
self.source_label.setObjectName("imageStudioSourceLabel")
|
||||||
@@ -384,6 +429,8 @@ class ImageStudioTab(QWidget):
|
|||||||
self.pool_table.setContextMenuPolicy(Qt.CustomContextMenu)
|
self.pool_table.setContextMenuPolicy(Qt.CustomContextMenu)
|
||||||
self.pool_table.horizontalHeader().setStretchLastSection(True)
|
self.pool_table.horizontalHeader().setStretchLastSection(True)
|
||||||
self.pool_table.verticalHeader().setVisible(False)
|
self.pool_table.verticalHeader().setVisible(False)
|
||||||
|
self.pool_table.setIconSize(QSize(86, 86))
|
||||||
|
self.pool_table.setAlternatingRowColors(False)
|
||||||
layout.addWidget(self.pool_table, 2)
|
layout.addWidget(self.pool_table, 2)
|
||||||
return panel
|
return panel
|
||||||
|
|
||||||
@@ -394,7 +441,24 @@ class ImageStudioTab(QWidget):
|
|||||||
layout.setContentsMargins(0, 0, 0, 0)
|
layout.setContentsMargins(0, 0, 0, 0)
|
||||||
layout.setSpacing(8)
|
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 = QGridLayout()
|
||||||
|
template_layout.setSpacing(6)
|
||||||
self.template_combo = QComboBox()
|
self.template_combo = QComboBox()
|
||||||
self.template_combo.setObjectName("imageStudioTemplateCombo")
|
self.template_combo.setObjectName("imageStudioTemplateCombo")
|
||||||
self.template_new_button = QPushButton("新建")
|
self.template_new_button = QPushButton("新建")
|
||||||
@@ -405,11 +469,14 @@ class ImageStudioTab(QWidget):
|
|||||||
self.template_save_button.setObjectName("imageStudioTemplateSaveButton")
|
self.template_save_button.setObjectName("imageStudioTemplateSaveButton")
|
||||||
self.template_delete_button = QPushButton("删除")
|
self.template_delete_button = QPushButton("删除")
|
||||||
self.template_delete_button.setObjectName("imageStudioTemplateDeleteButton")
|
self.template_delete_button.setObjectName("imageStudioTemplateDeleteButton")
|
||||||
template_layout.addWidget(self.template_combo, 0, 0, 1, 2)
|
template_label = QLabel("提示词模板")
|
||||||
template_layout.addWidget(self.template_new_button, 0, 2)
|
template_label.setObjectName("imageStudioMutedLabel")
|
||||||
template_layout.addWidget(self.template_rename_button, 1, 0)
|
template_layout.addWidget(template_label, 0, 0, 1, 3)
|
||||||
template_layout.addWidget(self.template_save_button, 1, 1)
|
template_layout.addWidget(self.template_combo, 1, 0, 1, 3)
|
||||||
template_layout.addWidget(self.template_delete_button, 1, 2)
|
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)
|
layout.addLayout(template_layout)
|
||||||
|
|
||||||
self.prompt_edit = QPlainTextEdit()
|
self.prompt_edit = QPlainTextEdit()
|
||||||
@@ -417,6 +484,10 @@ class ImageStudioTab(QWidget):
|
|||||||
self.prompt_edit.setPlaceholderText("输入完整图片生成提示词")
|
self.prompt_edit.setPlaceholderText("输入完整图片生成提示词")
|
||||||
self.prompt_edit.setMinimumHeight(160)
|
self.prompt_edit.setMinimumHeight(160)
|
||||||
layout.addWidget(self.prompt_edit, 2)
|
layout.addWidget(self.prompt_edit, 2)
|
||||||
|
prompt_hint = QLabel("程序原样提交完整提示词,不自动拆分,也不自动追加动作词。")
|
||||||
|
prompt_hint.setObjectName("imageStudioMutedLabel")
|
||||||
|
prompt_hint.setWordWrap(True)
|
||||||
|
layout.addWidget(prompt_hint)
|
||||||
|
|
||||||
form = QFormLayout()
|
form = QFormLayout()
|
||||||
self.job_type_combo = QComboBox()
|
self.job_type_combo = QComboBox()
|
||||||
@@ -440,9 +511,13 @@ class ImageStudioTab(QWidget):
|
|||||||
self.billing_label.setObjectName("imageStudioBillingLabel")
|
self.billing_label.setObjectName("imageStudioBillingLabel")
|
||||||
self.billing_label.setWordWrap(True)
|
self.billing_label.setWordWrap(True)
|
||||||
layout.addWidget(self.billing_label)
|
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()
|
action_layout = QHBoxLayout()
|
||||||
self.start_button = QPushButton("开始生成")
|
self.start_button = QPushButton("生成 4 张主图")
|
||||||
self.start_button.setObjectName("imageStudioStartButton")
|
self.start_button.setObjectName("imageStudioStartButton")
|
||||||
self.resume_button = QPushButton("继续查询任务")
|
self.resume_button = QPushButton("继续查询任务")
|
||||||
self.resume_button.setObjectName("imageStudioResumeButton")
|
self.resume_button.setObjectName("imageStudioResumeButton")
|
||||||
@@ -476,10 +551,14 @@ class ImageStudioTab(QWidget):
|
|||||||
main_panel = QWidget()
|
main_panel = QWidget()
|
||||||
main_layout = QVBoxLayout(main_panel)
|
main_layout = QVBoxLayout(main_panel)
|
||||||
main_layout.setContentsMargins(0, 0, 0, 0)
|
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 = QLabel("主图终选 0/9")
|
||||||
self.main_selection_label.setObjectName("imageStudioMainSelectionLabel")
|
self.main_selection_label.setObjectName("imageStudioMainSelectionLabel")
|
||||||
self.main_selection_list = ImageStudioSelectionList("main", self)
|
self.main_selection_list = ImageStudioSelectionList("main", self)
|
||||||
self.main_selection_list.setObjectName("imageStudioMainSelectionList")
|
self.main_selection_list.setObjectName("imageStudioMainSelectionList")
|
||||||
|
self.main_selection_list.setIconSize(QSize(48, 48))
|
||||||
self.main_selection_list.setMinimumHeight(96)
|
self.main_selection_list.setMinimumHeight(96)
|
||||||
main_layout.addWidget(self.main_selection_label)
|
main_layout.addWidget(self.main_selection_label)
|
||||||
main_layout.addWidget(self.main_selection_list)
|
main_layout.addWidget(self.main_selection_list)
|
||||||
@@ -487,19 +566,22 @@ class ImageStudioTab(QWidget):
|
|||||||
detail_panel = QWidget()
|
detail_panel = QWidget()
|
||||||
detail_layout = QVBoxLayout(detail_panel)
|
detail_layout = QVBoxLayout(detail_panel)
|
||||||
detail_layout.setContentsMargins(0, 0, 0, 0)
|
detail_layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
detail_layout.addWidget(QLabel(" "))
|
||||||
self.detail_selection_label = QLabel("详情图终选 0/12")
|
self.detail_selection_label = QLabel("详情图终选 0/12")
|
||||||
self.detail_selection_label.setObjectName("imageStudioDetailSelectionLabel")
|
self.detail_selection_label.setObjectName("imageStudioDetailSelectionLabel")
|
||||||
self.detail_selection_list = ImageStudioSelectionList("detail", self)
|
self.detail_selection_list = ImageStudioSelectionList("detail", self)
|
||||||
self.detail_selection_list.setObjectName("imageStudioDetailSelectionList")
|
self.detail_selection_list.setObjectName("imageStudioDetailSelectionList")
|
||||||
|
self.detail_selection_list.setIconSize(QSize(42, 42))
|
||||||
self.detail_selection_list.setMinimumHeight(96)
|
self.detail_selection_list.setMinimumHeight(96)
|
||||||
detail_layout.addWidget(self.detail_selection_label)
|
detail_layout.addWidget(self.detail_selection_label)
|
||||||
detail_layout.addWidget(self.detail_selection_list)
|
detail_layout.addWidget(self.detail_selection_list)
|
||||||
action_panel = QWidget()
|
action_panel = QWidget()
|
||||||
action_layout = QVBoxLayout(action_panel)
|
action_layout = QVBoxLayout(action_panel)
|
||||||
action_layout.setContentsMargins(0, 0, 0, 0)
|
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_button.setObjectName("imageStudioExportButton")
|
||||||
self.export_hint_label = QLabel("可部分导出,不要求主图/详情图满额")
|
self.export_hint_label = QLabel("可部分导出;文件名按终选顺序连续,目标目录存在时可覆盖或新建时间目录。")
|
||||||
self.export_hint_label.setObjectName("imageStudioExportHintLabel")
|
self.export_hint_label.setObjectName("imageStudioExportHintLabel")
|
||||||
self.export_hint_label.setWordWrap(True)
|
self.export_hint_label.setWordWrap(True)
|
||||||
action_layout.addWidget(self.export_button)
|
action_layout.addWidget(self.export_button)
|
||||||
@@ -510,6 +592,63 @@ class ImageStudioTab(QWidget):
|
|||||||
layout.addWidget(action_panel, 0)
|
layout.addWidget(action_panel, 0)
|
||||||
return panel
|
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):
|
def _connect_signals(self):
|
||||||
self.open_project_button.clicked.connect(self.open_project)
|
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)
|
||||||
@@ -526,6 +665,8 @@ class ImageStudioTab(QWidget):
|
|||||||
self.template_save_button.clicked.connect(self.save_template)
|
self.template_save_button.clicked.connect(self.save_template)
|
||||||
self.template_delete_button.clicked.connect(self.delete_template)
|
self.template_delete_button.clicked.connect(self.delete_template)
|
||||||
self.prompt_edit.textChanged.connect(self._save_project_prompt)
|
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.start_button.clicked.connect(self.start_generation)
|
||||||
self.resume_button.clicked.connect(self.resume_generation_jobs)
|
self.resume_button.clicked.connect(self.resume_generation_jobs)
|
||||||
self.stop_button.clicked.connect(self.stop_generation)
|
self.stop_button.clicked.connect(self.stop_generation)
|
||||||
@@ -558,6 +699,8 @@ class ImageStudioTab(QWidget):
|
|||||||
self._select_project(self.projects[0].id)
|
self._select_project(self.projects[0].id)
|
||||||
elif self.current_project is not None:
|
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)
|
||||||
|
else:
|
||||||
|
self._refresh_project_summary()
|
||||||
|
|
||||||
def _fill_project_table(self):
|
def _fill_project_table(self):
|
||||||
self.project_table.setRowCount(len(self.projects))
|
self.project_table.setRowCount(len(self.projects))
|
||||||
@@ -572,6 +715,7 @@ class ImageStudioTab(QWidget):
|
|||||||
item = QTableWidgetItem(str(value or ""))
|
item = QTableWidgetItem(str(value or ""))
|
||||||
item.setData(Qt.UserRole, int(project.id))
|
item.setData(Qt.UserRole, int(project.id))
|
||||||
self.project_table.setItem(row, column, item)
|
self.project_table.setItem(row, column, item)
|
||||||
|
self.project_table.setRowHeight(row, 44)
|
||||||
self.project_table.resizeColumnsToContents()
|
self.project_table.resizeColumnsToContents()
|
||||||
|
|
||||||
def open_project(self, checked=False):
|
def open_project(self, checked=False):
|
||||||
@@ -591,21 +735,21 @@ class ImageStudioTab(QWidget):
|
|||||||
path=self.db_path,
|
path=self.db_path,
|
||||||
)
|
)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
self._message("打开项目失败", str(exc))
|
self._message("打开商品项目失败", str(exc))
|
||||||
self._status(f"打开AI工场项目失败:{exc}", "danger")
|
self._status(f"打开AI工场商品项目失败:{exc}", "danger")
|
||||||
return
|
return
|
||||||
self.current_project = project
|
self.current_project = project
|
||||||
self.item_id_edit.setText(project.item_id)
|
self.item_id_edit.setText(project.item_id)
|
||||||
self._set_account_combo(project.account_alias)
|
self._set_account_combo(project.account_alias)
|
||||||
self.refresh_projects()
|
self.refresh_projects()
|
||||||
self._select_project(project.id)
|
self._select_project(project.id)
|
||||||
self._status("AI工场项目已打开", "success")
|
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,
|
||||||
@@ -616,11 +760,11 @@ class ImageStudioTab(QWidget):
|
|||||||
worker.log.connect(self._append_log)
|
worker.log.connect(self._append_log)
|
||||||
worker.finished.connect(self._on_pull_finished)
|
worker.finished.connect(self._on_pull_finished)
|
||||||
worker.failed.connect(self._on_worker_failed)
|
worker.failed.connect(self._on_worker_failed)
|
||||||
self._start_worker(worker, "AI工场拉取主图")
|
self._start_worker(worker, "AI工场拉取蝦皮主图")
|
||||||
self._append_log("[AI工场] 拉取主图开始")
|
self._append_log("[AI工场] 拉取蝦皮主图开始")
|
||||||
|
|
||||||
def _on_pull_finished(self, summary):
|
def _on_pull_finished(self, summary):
|
||||||
if self._handle_finished_error(summary, "拉取主图失败"):
|
if self._handle_finished_error(summary, "拉取蝦皮主图失败"):
|
||||||
return
|
return
|
||||||
project = summary.get("project")
|
project = summary.get("project")
|
||||||
if project is not None:
|
if project is not None:
|
||||||
@@ -635,7 +779,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("商品项目未打开", "请先打开一个AI工场商品项目。")
|
||||||
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:
|
||||||
@@ -674,8 +818,9 @@ class ImageStudioTab(QWidget):
|
|||||||
self.selected_source_asset_id = None
|
self.selected_source_asset_id = None
|
||||||
self.refresh_project_assets()
|
self.refresh_project_assets()
|
||||||
self._sync_project_selection(project.id)
|
self._sync_project_selection(project.id)
|
||||||
|
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.account_alias} / {project.item_id}", "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()):
|
||||||
@@ -694,6 +839,7 @@ class ImageStudioTab(QWidget):
|
|||||||
self.assets = image_studio.list_assets(self.current_project.id, path=self.db_path)
|
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.jobs = self._list_project_jobs(self.current_project.id)
|
||||||
self.selections = image_studio.list_selections(self.current_project.id, path=self.db_path)
|
self.selections = image_studio.list_selections(self.current_project.id, path=self.db_path)
|
||||||
|
self._refresh_project_summary()
|
||||||
self._fill_original_table()
|
self._fill_original_table()
|
||||||
self._fill_pool_table()
|
self._fill_pool_table()
|
||||||
self._refresh_selection_labels()
|
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]
|
originals = [asset for asset in self.assets if asset.kind == image_studio.ASSET_KIND_ORIGINAL]
|
||||||
self.original_table.setRowCount(len(originals))
|
self.original_table.setRowCount(len(originals))
|
||||||
for row, asset in enumerate(originals):
|
for row, asset in enumerate(originals):
|
||||||
|
order = asset.source_order or row + 1
|
||||||
values = [
|
values = [
|
||||||
str(asset.source_order or row + 1),
|
f"主图 #{order}",
|
||||||
_asset_status_text(asset),
|
_asset_status_text(asset),
|
||||||
asset.remote_url or "",
|
asset.remote_url or "",
|
||||||
]
|
]
|
||||||
for column, value in enumerate(values):
|
for column, value in enumerate(values):
|
||||||
item = QTableWidgetItem(str(value or ""))
|
item = QTableWidgetItem(str(value or ""))
|
||||||
item.setData(Qt.UserRole, {"type": "asset", "asset_id": int(asset.id)})
|
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.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()
|
self.original_table.resizeColumnsToContents()
|
||||||
|
|
||||||
def _fill_pool_table(self):
|
def _fill_pool_table(self):
|
||||||
@@ -730,7 +884,7 @@ class ImageStudioTab(QWidget):
|
|||||||
if row_type == "asset":
|
if row_type == "asset":
|
||||||
draggable = _asset_is_usable(obj)
|
draggable = _asset_is_usable(obj)
|
||||||
values = [
|
values = [
|
||||||
_asset_badge(obj.kind),
|
f"{_asset_badge(obj.kind)} #{obj.id}",
|
||||||
obj.aspect_ratio or "未知",
|
obj.aspect_ratio or "未知",
|
||||||
_asset_status_text(obj),
|
_asset_status_text(obj),
|
||||||
_source_text(obj, self.assets),
|
_source_text(obj, self.assets),
|
||||||
@@ -749,7 +903,24 @@ class ImageStudioTab(QWidget):
|
|||||||
for column, value in enumerate(values):
|
for column, value in enumerate(values):
|
||||||
item = QTableWidgetItem(str(value or ""))
|
item = QTableWidgetItem(str(value or ""))
|
||||||
item.setData(Qt.UserRole, data)
|
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.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()
|
self.pool_table.resizeColumnsToContents()
|
||||||
|
|
||||||
def _selection_asset_ids(self, selection_type):
|
def _selection_asset_ids(self, selection_type):
|
||||||
@@ -888,8 +1059,31 @@ class ImageStudioTab(QWidget):
|
|||||||
asset = self._asset_by_id(self.selected_source_asset_id)
|
asset = self._asset_by_id(self.selected_source_asset_id)
|
||||||
if asset is None:
|
if asset is None:
|
||||||
self.source_label.setText("源图:未选择")
|
self.source_label.setText("源图:未选择")
|
||||||
|
self.source_preview_label.setPixmap(QPixmap())
|
||||||
|
self.source_preview_label.setText("未选择源图")
|
||||||
return
|
return
|
||||||
self.source_label.setText(f"源图:{_asset_badge(asset.kind)} #{asset.id}")
|
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):
|
def _open_preview(self, asset):
|
||||||
dialog = ImageStudioPreviewDialog(asset, self)
|
dialog = ImageStudioPreviewDialog(asset, self)
|
||||||
@@ -1023,7 +1217,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("商品项目未打开", "请先打开一个AI工场商品项目。")
|
||||||
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:
|
||||||
@@ -1066,7 +1260,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("商品项目未打开", "请先打开一个AI工场商品项目。")
|
||||||
return
|
return
|
||||||
resumable = image_studio.list_resumable_jobs(
|
resumable = image_studio.list_resumable_jobs(
|
||||||
path=self.db_path,
|
path=self.db_path,
|
||||||
@@ -1095,7 +1289,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("商品项目未打开", "请先打开一个AI工场商品项目。")
|
||||||
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"))
|
||||||
@@ -1290,10 +1484,8 @@ class ImageStudioTab(QWidget):
|
|||||||
|
|
||||||
def _fill_selection_list(self, widget, selection_type):
|
def _fill_selection_list(self, widget, selection_type):
|
||||||
widget.clear()
|
widget.clear()
|
||||||
for index, selection in enumerate(
|
selected = [item for item in self.selections if item.selection_type == selection_type]
|
||||||
[item for item in self.selections if item.selection_type == selection_type],
|
for index, selection in enumerate(selected, start=1):
|
||||||
start=1,
|
|
||||||
):
|
|
||||||
asset = self._asset_by_id(selection.asset_id)
|
asset = self._asset_by_id(selection.asset_id)
|
||||||
if asset is None:
|
if asset is None:
|
||||||
text = f"{index}. 缺失照片 #{selection.asset_id}"
|
text = f"{index}. 缺失照片 #{selection.asset_id}"
|
||||||
@@ -1305,6 +1497,7 @@ class ImageStudioTab(QWidget):
|
|||||||
continue
|
continue
|
||||||
text = f"{index}. {_asset_badge(asset.kind)} #{asset.id} · {asset.aspect_ratio or '未知比例'}"
|
text = f"{index}. {_asset_badge(asset.kind)} #{asset.id} · {asset.aspect_ratio or '未知比例'}"
|
||||||
item = QListWidgetItem(text)
|
item = QListWidgetItem(text)
|
||||||
|
item.setIcon(_asset_icon(asset, str(index)))
|
||||||
item.setData(Qt.UserRole, int(asset.id))
|
item.setData(Qt.UserRole, int(asset.id))
|
||||||
tooltip = _selection_tooltip(selection_type, asset)
|
tooltip = _selection_tooltip(selection_type, asset)
|
||||||
item.setToolTip(tooltip)
|
item.setToolTip(tooltip)
|
||||||
@@ -1315,6 +1508,15 @@ class ImageStudioTab(QWidget):
|
|||||||
item.setForeground(_qcolor(COLOR_MUTED))
|
item.setForeground(_qcolor(COLOR_MUTED))
|
||||||
item.setToolTip(tooltip + "\n本地文件缺失,不能用于导出。")
|
item.setToolTip(tooltip + "\n本地文件缺失,不能用于导出。")
|
||||||
widget.addItem(item)
|
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):
|
def _asset_from_table_row(self, table, row):
|
||||||
data = self._row_data(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))
|
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):
|
def _source_text(asset, assets):
|
||||||
parent_id = getattr(asset, "parent_asset_id", None)
|
parent_id = getattr(asset, "parent_asset_id", None)
|
||||||
if not parent_id:
|
if not parent_id:
|
||||||
|
|||||||
+25
-2
@@ -3,7 +3,7 @@ id: T-605
|
|||||||
title: AI工场参考 v3 效果图优化信息架构与界面展示
|
title: AI工场参考 v3 效果图优化信息架构与界面展示
|
||||||
phase: 7
|
phase: 7
|
||||||
deps: [T-591, T-592, T-593, T-594, T-604]
|
deps: [T-591, T-592, T-593, T-594, T-604]
|
||||||
status: TODO
|
status: DONE
|
||||||
created: 2026-07-11
|
created: 2026-07-11
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -192,4 +192,27 @@ git diff --check
|
|||||||
|
|
||||||
## 执行记录
|
## 执行记录
|
||||||
|
|
||||||
待执行。
|
- 2026-07-11:已完成。
|
||||||
|
- 代码:
|
||||||
|
- `app/gui/tabs/image_studio.py`:⑥AI工场改为顶部操作条 + 左侧商品项目 + 中央蝦皮原主图/照片池 + 右侧生成设置 + 底部终选与排序的工作台结构。
|
||||||
|
- 顶部入口文案改为「打开商品项目」「拉取蝦皮主图」,并新增当前商品、自动保存状态和未打开项目空状态。
|
||||||
|
- 原主图和照片池增加稳定缩略图/占位图、图片类型与任务状态图标、源图高亮,保留单击下载/选源图、双击预览、右键移除和拖入终选能力。
|
||||||
|
- 生成设置区新增源图预览、提示词提交说明、生成来源/扣点提示和「不会在失败后静默切换生成来源」说明;开始按钮文案会随类型和数量变化,如「生成 3 张详情图」。
|
||||||
|
- 底部终选区改为固定槽位显示:主图 9 个、详情图 12 个;空位只做视觉占位,真实保存顺序仍只来自 SQLite 终选记录。
|
||||||
|
- 不改 `app/image_studio.py`、`app/editor.py`、cmhub 生图/续查/导出业务逻辑。
|
||||||
|
- 测试:
|
||||||
|
- `tests/test_gui.py`:更新 AI工场构建测试,覆盖 v3 关键中文文案、顶部状态、拉取蝦皮主图、导出到文件夹、生成按钮动态文案、缩略图图标和不出现「导入本地图片」。
|
||||||
|
- `tests/test_gui.py`:更新终选排序测试,覆盖主图 9 槽、详情图 12 槽和空位占位不影响真实终选顺序。
|
||||||
|
- `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_final_selection_order_and_guards tests.test_gui.GuiTests.test_image_studio_tab_shows_resume_and_job_billing_status tests.test_gui.GuiTests.test_image_studio_generation_log_uses_cmhub_tier_summary tests.test_gui.GuiTests.test_image_studio_worker_thread_is_kept_until_thread_finished tests.test_gui.GuiTests.test_image_studio_worker_failure_is_handled_once tests.test_gui.GuiTests.test_image_studio_pull_main_images_uses_current_item_input`
|
||||||
|
- `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-605.md`
|
||||||
|
- 主工作区全量 `py -3.10 -m unittest discover -s tests` 仍因既有未提交默认提示词改动失败 3 个用例,失败内容为 `papa1` 与乱码 `默认` 文件名不一致,不属于 T-605。
|
||||||
|
- 为满足提交前全绿验证,已在临时干净 worktree `D:\chengma\cmshopee-t605-verify` 只应用 T-605 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`(378 tests)
|
||||||
|
- `git diff --check -- app\gui\tabs\image_studio.py tests\test_gui.py docs\tasks\T-605.md`
|
||||||
|
|||||||
+31
-3
@@ -512,8 +512,23 @@ 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.export_button.text())
|
self.assertEqual("打开商品项目", tab.open_project_button.text())
|
||||||
|
self.assertEqual("拉取蝦皮主图", tab.pull_images_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.assertEqual("未打开商品项目", tab.current_project_label.text())
|
||||||
|
self.assertIn("打开商品项目", tab.workspace_empty_label.text())
|
||||||
|
self.assertEqual("生成 4 张主图", tab.start_button.text())
|
||||||
|
tab.count_spin.setValue(3)
|
||||||
|
tab.job_type_combo.setCurrentIndex(tab.job_type_combo.findData("detail"))
|
||||||
|
self.assertEqual("生成 3 张详情图", tab.start_button.text())
|
||||||
|
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.assertEqual("完整提示词", prompts.load_image_studio_template("工场模板", prompts_dir))
|
self.assertEqual("完整提示词", prompts.load_image_studio_template("工场模板", prompts_dir))
|
||||||
template_index = tab.template_combo.findData("工场模板")
|
template_index = tab.template_combo.findData("工场模板")
|
||||||
self.assertGreaterEqual(template_index, 0)
|
self.assertGreaterEqual(template_index, 0)
|
||||||
@@ -526,6 +541,9 @@ class GuiTests(TempDirMixin, unittest.TestCase):
|
|||||||
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.assertIn("51100639510", tab.current_project_label.text())
|
||||||
|
self.assertEqual("项目已自动保存", tab.autosave_label.text())
|
||||||
self.assertEqual(1, tab.project_table.rowCount())
|
self.assertEqual(1, tab.project_table.rowCount())
|
||||||
|
|
||||||
original = image_studio.sync_original_asset_urls(
|
original = image_studio.sync_original_asset_urls(
|
||||||
@@ -536,10 +554,12 @@ class GuiTests(TempDirMixin, unittest.TestCase):
|
|||||||
tab.refresh_project_assets()
|
tab.refresh_project_assets()
|
||||||
|
|
||||||
self.assertEqual(1, tab.original_table.rowCount())
|
self.assertEqual(1, tab.original_table.rowCount())
|
||||||
self.assertEqual("1", tab.original_table.item(0, 0).text())
|
self.assertEqual("主图 #1", tab.original_table.item(0, 0).text())
|
||||||
|
self.assertFalse(tab.original_table.item(0, 0).icon().isNull())
|
||||||
self.assertEqual("远程待下载", tab.original_table.item(0, 1).text())
|
self.assertEqual("远程待下载", tab.original_table.item(0, 1).text())
|
||||||
self.assertEqual(1, tab.pool_table.rowCount())
|
self.assertEqual(1, tab.pool_table.rowCount())
|
||||||
self.assertEqual("原图", tab.pool_table.item(0, 0).text())
|
self.assertEqual(f"原图 #{original.id}", tab.pool_table.item(0, 0).text())
|
||||||
|
self.assertFalse(tab.pool_table.item(0, 0).icon().isNull())
|
||||||
self.assertEqual("远程待下载", tab.pool_table.item(0, 2).text())
|
self.assertEqual("远程待下载", tab.pool_table.item(0, 2).text())
|
||||||
self.assertEqual(original.id, tab.pool_table.item(0, 0).data(gui.Qt.UserRole)["asset_id"])
|
self.assertEqual(original.id, tab.pool_table.item(0, 0).data(gui.Qt.UserRole)["asset_id"])
|
||||||
|
|
||||||
@@ -584,7 +604,10 @@ class GuiTests(TempDirMixin, unittest.TestCase):
|
|||||||
self.assertEqual([second.id, first.id], [
|
self.assertEqual([second.id, first.id], [
|
||||||
tab.main_selection_list.item(row).data(gui.Qt.UserRole)
|
tab.main_selection_list.item(row).data(gui.Qt.UserRole)
|
||||||
for row in range(tab.main_selection_list.count())
|
for row in range(tab.main_selection_list.count())
|
||||||
|
if tab.main_selection_list.item(row).data(gui.Qt.UserRole) is not None
|
||||||
])
|
])
|
||||||
|
self.assertEqual(9, tab.main_selection_list.count())
|
||||||
|
self.assertIn("空位", tab.main_selection_list.item(2).text())
|
||||||
self.assertEqual("#fff8c5", tab.main_selection_list.item(0).background().color().name())
|
self.assertEqual("#fff8c5", tab.main_selection_list.item(0).background().color().name())
|
||||||
|
|
||||||
self.assertFalse(tab.add_asset_to_selection("main", first.id))
|
self.assertFalse(tab.add_asset_to_selection("main", first.id))
|
||||||
@@ -592,6 +615,8 @@ class GuiTests(TempDirMixin, unittest.TestCase):
|
|||||||
self.assertTrue(tab.add_asset_to_selection("detail", first.id))
|
self.assertTrue(tab.add_asset_to_selection("detail", first.id))
|
||||||
detail = image_studio.list_selections(project.id, "detail", path=cfg["db_path"])
|
detail = image_studio.list_selections(project.id, "detail", path=cfg["db_path"])
|
||||||
self.assertEqual([first.id], [selection.asset_id for selection in detail])
|
self.assertEqual([first.id], [selection.asset_id for selection in detail])
|
||||||
|
self.assertEqual(12, tab.detail_selection_list.count())
|
||||||
|
self.assertIn("空位", tab.detail_selection_list.item(1).text())
|
||||||
|
|
||||||
self.assertTrue(tab.move_selection_asset("main", 1, 0))
|
self.assertTrue(tab.move_selection_asset("main", 1, 0))
|
||||||
main = image_studio.list_selections(project.id, "main", path=cfg["db_path"])
|
main = image_studio.list_selections(project.id, "main", path=cfg["db_path"])
|
||||||
@@ -674,6 +699,9 @@ class GuiTests(TempDirMixin, unittest.TestCase):
|
|||||||
tab._select_project(project.id)
|
tab._select_project(project.id)
|
||||||
tab._select_source_asset(source)
|
tab._select_source_asset(source)
|
||||||
tab.prompt_edit.setPlainText("生成商品主图")
|
tab.prompt_edit.setPlainText("生成商品主图")
|
||||||
|
self.assertIn(f"原图 #{source.id}", tab.source_label.text())
|
||||||
|
self.assertEqual("", tab.source_preview_label.text())
|
||||||
|
self.assertFalse(tab.source_preview_label.pixmap().isNull())
|
||||||
|
|
||||||
class FakeStudioWorker:
|
class FakeStudioWorker:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
|||||||
Reference in New Issue
Block a user