feat(product-suite): compact controls and remember settings

This commit is contained in:
chengma
2026-07-14 15:16:09 +08:00
parent 17bb7521fd
commit 21d2dd5253
10 changed files with 431 additions and 47 deletions
+29
View File
@@ -112,6 +112,14 @@ DEFAULT_CONFIG = {
"dry_run": False,
"max_parallel_accounts": 1,
},
"product_suite": {
"last_settings": {
"platform": "Shopee",
"country": "中国台湾",
"language": "繁体中文",
"ratio": "1:1",
},
},
}
DEFAULT_AI_MODELS_CONFIG = {
@@ -426,9 +434,22 @@ def _normalize_config_values(config, migrate_old_cmhub_connect_timeout=False):
9999,
DEFAULT_CONFIG["shopee_update"]["max_items_per_run"],
)
suite = config.get("product_suite")
if not isinstance(suite, dict):
suite = {}
config["product_suite"] = suite
suite["last_settings"] = _normalize_product_suite_last_settings(
suite.get("last_settings")
)
return config
def _normalize_product_suite_last_settings(value):
from . import product_suite
return product_suite.last_suite_settings(value)
def _clamp_int(value, minimum, maximum, default):
try:
number = int(value)
@@ -673,6 +694,14 @@ def ai_config(config=None) -> dict:
return copy.deepcopy(_config_or_load(config).get("ai", DEFAULT_CONFIG["ai"]))
def product_suite_last_settings(config=None) -> dict:
cfg = _config_or_load(config)
suite = cfg.get("product_suite", {})
if not isinstance(suite, dict):
suite = {}
return _normalize_product_suite_last_settings(suite.get("last_settings"))
def normalize_generate_mode(value=None, generate_cover=None) -> str:
text = str(value or "").strip().lower()
if text in AI_GENERATE_MODES:
+145 -35
View File
@@ -30,6 +30,7 @@ from PySide6.QtWidgets import (
QProgressBar,
QPushButton,
QScrollArea,
QSizePolicy,
QSplitter,
QTabBar,
QToolButton,
@@ -381,9 +382,9 @@ class SuiteTaskState:
class ProductSuiteTab(QWidget):
"""Native PySide6 product-suite UI backed by image_studio services."""
PLATFORM_OPTIONS = ("Shopee", "Lazada", "TikTok Shop", "Amazon")
COUNTRY_OPTIONS = ("中国台湾", "新加坡", "马来西亚", "菲律宾", "泰国", "越南")
LANGUAGE_OPTIONS = ("繁体中文", "简体中文", "英文", "泰文", "越南文")
PLATFORM_OPTIONS = product_suite.PLATFORMS
COUNTRY_OPTIONS = product_suite.COUNTRIES
LANGUAGE_OPTIONS = product_suite.LANGUAGES
def __init__(
self,
@@ -443,6 +444,31 @@ class ProductSuiteTab(QWidget):
self.task_tabs.setTabsClosable(True)
self.task_tabs.setMovable(True)
self.task_tabs.setExpanding(False)
self.task_tabs.setFixedHeight(36)
self.task_tabs.setStyleSheet(
"""
QTabBar#suiteTaskTabs::tab {
min-width: 96px;
min-height: 26px;
max-height: 26px;
padding: 3px 10px;
margin-right: 4px;
border: 1px solid #c9d1d9;
border-bottom-color: #b8c0ca;
background: #f4f6f8;
color: #24292f;
}
QTabBar#suiteTaskTabs::tab:selected {
background: #ffffff;
border-color: #687785;
border-bottom-color: #ffffff;
font-weight: 600;
}
QTabBar#suiteTaskTabs::tab:hover:!selected {
background: #eaf2ff;
}
"""
)
layout.addWidget(self.task_tabs, 1)
self.add_task_button = QToolButton()
self.add_task_button.setObjectName("suiteAddTaskButton")
@@ -457,20 +483,38 @@ class ProductSuiteTab(QWidget):
frame = QFrame()
frame.setObjectName("suiteContextBar")
layout = QHBoxLayout(frame)
self.context_bar_layout = layout
layout.setContentsMargins(10, 7, 10, 7)
layout.setSpacing(8)
self.history_button = QPushButton("历史生成")
self.history_button.setObjectName("suiteHistoryButton")
self.history_button.setCheckable(True)
layout.addWidget(self.history_button)
self.open_folder_button = QPushButton("打开结果文件夹")
self.open_folder_button.setObjectName("suiteOpenFolderButton")
layout.addWidget(self.open_folder_button)
self.add_images_button = QPushButton("添加图片")
self.add_images_button.setObjectName("suiteAddImagesButton")
layout.addWidget(self.add_images_button)
layout.addStretch(1)
layout.addWidget(QLabel("账号"))
self.account_combo = QComboBox()
self.account_combo.setObjectName("suiteAccountCombo")
self.account_combo.setMinimumWidth(180)
self.account_combo.setMinimumWidth(120)
self.account_combo.setMaximumWidth(160)
self.account_combo.setMinimumContentsLength(10)
layout.addWidget(self.account_combo)
layout.addWidget(QLabel("商品ID"))
self.item_id_edit = QLineEdit()
self.item_id_edit.setObjectName("suiteItemIdEdit")
self.item_id_edit.setPlaceholderText("请输入蝦皮商品ID")
self.item_id_edit.setMinimumWidth(150)
self.item_id_edit.setPlaceholderText("输入商品ID")
item_width = max(
120,
min(140, self.item_id_edit.fontMetrics().horizontalAdvance("0" * 13) + 30),
)
self.item_id_edit.setMinimumWidth(item_width)
self.item_id_edit.setMaximumWidth(item_width)
layout.addWidget(self.item_id_edit)
layout.addStretch(1)
self.pull_button = QPushButton("拉取蝦皮主图")
self.pull_button.setObjectName("suitePullButton")
layout.addWidget(self.pull_button)
@@ -478,6 +522,7 @@ class ProductSuiteTab(QWidget):
def _build_config_panel(self):
panel = QWidget()
panel.setMinimumWidth(400)
layout = QVBoxLayout(panel)
layout.setContentsMargins(0, 0, 4, 0)
layout.setSpacing(7)
@@ -524,9 +569,6 @@ class ProductSuiteTab(QWidget):
self.original_count_label.setStyleSheet("color: #6b7280;")
title_row.addWidget(self.original_count_label)
title_row.addStretch(1)
self.add_images_button = QPushButton("添加图片")
self.add_images_button.setObjectName("suiteAddImagesButton")
title_row.addWidget(self.add_images_button)
layout.addLayout(title_row)
self.original_list = ProductOriginalList()
self.original_list.setFixedHeight(210)
@@ -541,28 +583,69 @@ class ProductSuiteTab(QWidget):
title.setStyleSheet("font-weight: 600;")
layout.addWidget(title)
grid = QGridLayout()
self.settings_grid = grid
grid.setContentsMargins(0, 0, 0, 0)
grid.setSpacing(6)
self.platform_combo = self._prefixed_combo("suitePlatformCombo", "平台", self.PLATFORM_OPTIONS)
self.country_combo = self._prefixed_combo("suiteCountryCombo", "国家", self.COUNTRY_OPTIONS)
self.language_combo = self._prefixed_combo("suiteLanguageCombo", "语言", self.LANGUAGE_OPTIONS)
self.ratio_combo = self._prefixed_combo("suiteRatioCombo", "比例", product_suite.RATIOS)
grid.addWidget(self.platform_combo, 0, 0)
grid.addWidget(self.country_combo, 0, 1)
grid.addWidget(self.language_combo, 1, 0)
grid.addWidget(self.ratio_combo, 1, 1)
grid.setHorizontalSpacing(6)
grid.setVerticalSpacing(3)
settings = (
(
"platform_label",
"platform_combo",
"平台",
"suitePlatformLabel",
"suitePlatformCombo",
self.PLATFORM_OPTIONS,
),
(
"country_label",
"country_combo",
"站点",
"suiteCountryLabel",
"suiteCountryCombo",
self.COUNTRY_OPTIONS,
),
(
"language_label",
"language_combo",
"语言",
"suiteLanguageLabel",
"suiteLanguageCombo",
self.LANGUAGE_OPTIONS,
),
(
"ratio_label",
"ratio_combo",
"比例",
"suiteRatioLabel",
"suiteRatioCombo",
product_suite.RATIOS,
),
)
for column, setting in enumerate(settings):
label_attr, combo_attr, text, label_name, combo_name, values = setting
label = QLabel(text)
label.setObjectName(label_name)
setattr(self, label_attr, label)
combo = self._value_combo(combo_name, text, values)
setattr(self, combo_attr, combo)
grid.addWidget(label, 0, column)
grid.addWidget(combo, 1, column)
grid.setColumnStretch(column, 1)
layout.addLayout(grid)
self.per_image_checkbox = QCheckBox("每张上传图分别作为主图生成")
self.per_image_checkbox.setObjectName("suitePerImageCheckbox")
layout.addWidget(self.per_image_checkbox)
return frame
def _prefixed_combo(self, object_name, prefix, values):
def _value_combo(self, object_name, label, values):
combo = QComboBox()
combo.setObjectName(object_name)
combo.setToolTip("%s设置" % prefix)
combo.setToolTip("%s设置" % label)
combo.setAccessibleName("%s设置" % label)
combo.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Fixed)
for value in values:
combo.addItem("%s %s" % (prefix, value), value)
combo.addItem(value, value)
combo.setItemData(combo.count() - 1, value, Qt.ToolTipRole)
return combo
def _build_prompt_section(self):
@@ -628,6 +711,7 @@ class ProductSuiteTab(QWidget):
layout.setContentsMargins(4, 0, 0, 0)
layout.setSpacing(7)
toolbar = QHBoxLayout()
self.results_toolbar_layout = toolbar
title = QLabel("生成结果")
title.setStyleSheet("font-weight: 600; font-size: 15px;")
toolbar.addWidget(title)
@@ -642,13 +726,6 @@ class ProductSuiteTab(QWidget):
self.undo_button.setObjectName("suiteUndoButton")
self.undo_button.hide()
toolbar.addWidget(self.undo_button)
self.history_button = QPushButton("历史生成")
self.history_button.setObjectName("suiteHistoryButton")
self.history_button.setCheckable(True)
toolbar.addWidget(self.history_button)
self.open_folder_button = QPushButton("打开文件夹")
self.open_folder_button.setObjectName("suiteOpenFolderButton")
toolbar.addWidget(self.open_folder_button)
self.more_button = QToolButton()
self.more_button.setText("⋯")
self.more_button.setToolTip("更多操作")
@@ -750,9 +827,12 @@ class ProductSuiteTab(QWidget):
try:
self.account_combo.clear()
for account in self.accounts:
self.account_combo.addItem(
"%s(%s)" % (account.account_name, account.alias),
account.alias,
text = "%s(%s)" % (account.account_name, account.alias)
self.account_combo.addItem(text, account.alias)
self.account_combo.setItemData(
self.account_combo.count() - 1,
text,
Qt.ToolTipRole,
)
if not self.accounts:
self.account_combo.addItem("暂无账号,请先到④账号管理添加", "")
@@ -761,17 +841,21 @@ class ProductSuiteTab(QWidget):
self.account_combo.setCurrentIndex(index)
finally:
self._loading = False
self._update_account_tooltip()
def add_task(self, checked=False, inherit=True):
source = self._displayed_state if inherit else None
initial_settings = (
source.settings
if source is not None
else appconfig.product_suite_last_settings(self.config)
)
state = SuiteTaskState(
key=self._next_key,
serial=self._next_serial,
account_alias=(source.account_alias if source is not None else ""),
prompt=(source.prompt if source is not None else ""),
settings=product_suite.normalize_suite_settings(
source.settings if source is not None else None
),
settings=product_suite.normalize_suite_settings(initial_settings),
)
if not state.account_alias and self.accounts:
state.account_alias = self.accounts[0].alias
@@ -877,6 +961,7 @@ class ProductSuiteTab(QWidget):
return product_suite.normalize_suite_settings(settings)
def _on_account_changed(self, index):
self._update_account_tooltip()
if self._loading or self._displayed_state is None:
return
state = self._displayed_state
@@ -896,6 +981,9 @@ class ProductSuiteTab(QWidget):
state.account_alias = alias
self._update_context_actions(state)
def _update_account_tooltip(self):
self.account_combo.setToolTip(self.account_combo.currentText())
def _on_item_finished(self):
if self._loading or self._displayed_state is None:
return
@@ -1000,9 +1088,31 @@ class ProductSuiteTab(QWidget):
return
state = self._displayed_state
state.settings = self._settings_from_controls()
self._persist_last_settings(state.settings)
self._persist_state(state)
self._refresh_totals(state)
def _persist_last_settings(self, settings):
last_settings = product_suite.last_suite_settings(settings)
try:
if os.path.exists(self.config_path):
saved = appconfig.update_config(
{"product_suite": {"last_settings": last_settings}},
path=self.config_path,
)
else:
base = dict(self.config)
suite = base.get("product_suite", {})
suite = dict(suite) if isinstance(suite, dict) else {}
suite["last_settings"] = last_settings
base["product_suite"] = suite
saved = appconfig.save_config(base, path=self.config_path)
except Exception as exc:
self._status("商品套图最近设置保存失败:%s" % _user_error(exc), "danger")
return
self.config.clear()
self.config.update(saved)
def _on_prompt_changed(self):
if self._loading or self._displayed_state is None:
return
+18 -5
View File
@@ -9,7 +9,11 @@ FIXED_CATEGORIES = ("白底图", "场景图", "卖点图")
DEFAULT_CATEGORY_COUNTS = OrderedDict(
(("白底图", 1), ("场景图", 2), ("卖点图", 2))
)
PLATFORMS = ("Shopee", "Lazada", "TikTok Shop", "Amazon")
COUNTRIES = ("中国台湾", "新加坡", "马来西亚", "菲律宾", "泰国", "越南")
LANGUAGES = ("繁体中文", "简体中文", "英文", "泰文", "越南文")
RATIOS = ("1:1", "3:4", "4:3", "16:9", "9:16")
LAST_SETTING_KEYS = ("platform", "country", "language", "ratio")
MAX_CATEGORY_NAME_LENGTH = 10
MAX_GENERATION_COUNT_WITHOUT_CONFIRM = 16
@@ -29,11 +33,10 @@ def default_suite_settings():
def normalize_suite_settings(value=None):
raw = dict(value or {}) if isinstance(value, dict) else {}
normalized = default_suite_settings()
normalized["platform"] = str(raw.get("platform") or "Shopee")
normalized["country"] = str(raw.get("country") or "中国台湾")
normalized["language"] = str(raw.get("language") or "繁体中文")
ratio = str(raw.get("ratio") or "1:1")
normalized["ratio"] = ratio if ratio in RATIOS else "1:1"
normalized["platform"] = _choice(raw.get("platform"), PLATFORMS, "Shopee")
normalized["country"] = _choice(raw.get("country"), COUNTRIES, "中国台湾")
normalized["language"] = _choice(raw.get("language"), LANGUAGES, "繁体中文")
normalized["ratio"] = _choice(raw.get("ratio"), RATIOS, "1:1")
normalized["per_image_primary"] = bool(raw.get("per_image_primary", False))
raw_categories = raw.get("categories") if isinstance(raw.get("categories"), dict) else {}
@@ -56,6 +59,11 @@ def normalize_suite_settings(value=None):
return normalized
def last_suite_settings(value=None):
normalized = normalize_suite_settings(value)
return {key: normalized[key] for key in LAST_SETTING_KEYS}
def suite_name_error(name, existing=None, old_name=""):
value = str(name or "")
if not value.strip():
@@ -146,3 +154,8 @@ def _count(value):
return max(0, int(value or 0))
except (TypeError, ValueError):
return 0
def _choice(value, choices, default):
text = str(value or "").strip()
return text if text in choices else default