T-567 增加封面候选画廊

This commit is contained in:
chengma
2026-07-09 11:32:08 +08:00
parent 71e51d0608
commit a391d2db99
4 changed files with 573 additions and 42 deletions
+349 -36
View File
@@ -10,6 +10,343 @@ from ..workers import GenerateWorker as _RealGenerateWorker
def GenerateWorker(*args, **kwargs): def GenerateWorker(*args, **kwargs):
return _call_package_attr("GenerateWorker", _RealGenerateWorker, *args, **kwargs) return _call_package_attr("GenerateWorker", _RealGenerateWorker, *args, **kwargs)
class _CoverThumbnailLabel(QLabel):
def __init__(self, image_path, open_callback, parent=None):
super().__init__(parent)
self.image_path = image_path
self.open_callback = open_callback
self.setCursor(Qt.PointingHandCursor)
def mouseDoubleClickEvent(self, event):
if self.open_callback is not None:
self.open_callback(self.image_path)
event.accept()
return
super().mouseDoubleClickEvent(event)
class OriginalImageDialog(QDialog):
def __init__(self, image_path, parent=None):
super().__init__(parent)
self.image_path = image_path
self.setWindowTitle(os.path.basename(str(image_path or "")) or "原图")
layout = QVBoxLayout(self)
image = QImage(str(image_path))
scroll = QScrollArea()
scroll.setWidgetResizable(False)
image_label = QLabel()
image_label.setAlignment(Qt.AlignCenter)
if image.isNull():
image_label.setText("图片读取失败")
else:
self.setWindowTitle(
f"{os.path.basename(str(image_path))} · {image.width()}x{image.height()}"
)
pixmap = QPixmap.fromImage(image)
image_label.setPixmap(pixmap)
image_label.resize(pixmap.size())
scroll.setWidget(image_label)
layout.addWidget(scroll, 1)
button_layout = QHBoxLayout()
button_layout.addStretch(1)
close_button = QPushButton("关闭")
close_button.clicked.connect(self.reject)
button_layout.addWidget(close_button)
layout.addLayout(button_layout)
self._fit_to_screen(image)
def _fit_to_screen(self, image):
available = _available_geometry()
if available is None:
self.resize(720, 520)
return
max_width = max(360, int(available.width() * 0.9))
max_height = max(300, int(available.height() * 0.9))
image_width = image.width() if not image.isNull() else 640
image_height = image.height() if not image.isNull() else 480
self.resize(min(max_width, image_width + 48), min(max_height, image_height + 92))
_center_dialog(self, available)
class CoverGalleryDialog(QDialog):
THUMBNAIL_SIZE = 180
OLD_THUMBNAIL_SIZE = 300
def __init__(self, task, image_root, db_path, account=None, parent=None):
super().__init__(parent)
self.task = task
self.image_root = image_root
self.db_path = db_path
self.account = account
self.current_path = _normalize_file_path(getattr(task, "new_cover_path", None))
self.selected_path = None
self.changed = False
self.candidate_buttons = {}
self.button_group = QButtonGroup(self)
self.button_group.setExclusive(True)
self.candidates = image_paths.list_task_cover_candidates(image_root, task, account=account)
self.setWindowTitle(f"封面画廊:{getattr(task, 'item_id', '')}")
layout = QVBoxLayout(self)
body_layout = QHBoxLayout()
body_layout.addWidget(self._old_cover_panel(), 0)
body_layout.addWidget(self._candidate_panel(), 1)
layout.addLayout(body_layout, 1)
button_layout = QHBoxLayout()
button_layout.addStretch(1)
self.save_button = QPushButton("保存")
self.cancel_button = QPushButton("取消")
self.save_button.clicked.connect(self.save_selection)
self.cancel_button.clicked.connect(self.reject)
button_layout.addWidget(self.save_button)
button_layout.addWidget(self.cancel_button)
layout.addLayout(button_layout)
self._sync_initial_selection()
self._update_status()
self._fit_to_screen()
def _old_cover_panel(self):
panel = QWidget()
layout = QVBoxLayout(panel)
layout.addWidget(QLabel("旧封面"))
old_path = getattr(self.task, "old_cover_path", None)
layout.addWidget(
_build_image_label(
old_path,
self.OLD_THUMBNAIL_SIZE,
empty_text="暂无旧封面",
)
)
layout.addStretch(1)
return panel
def _candidate_panel(self):
panel = QWidget()
layout = QVBoxLayout(panel)
layout.addWidget(QLabel("生成封面候选"))
self.status_label = QLabel()
self.status_label.setObjectName("coverGalleryStatusLabel")
self.status_label.setWordWrap(True)
layout.addWidget(self.status_label)
scroll = QScrollArea()
scroll.setObjectName("coverGalleryScrollArea")
scroll.setWidgetResizable(False)
scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
scroll.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
content = QWidget()
self.candidate_layout = QHBoxLayout(content)
self.candidate_layout.setContentsMargins(0, 0, 0, 0)
self.candidate_layout.setSpacing(12)
if self.candidates:
for index, candidate_path in enumerate(self.candidates):
self.candidate_layout.addWidget(self._candidate_item(candidate_path, index))
self.candidate_layout.addStretch(1)
else:
empty_label = QLabel("暂无生成封面图片")
empty_label.setObjectName("coverGalleryEmptyLabel")
empty_label.setMinimumSize(320, self.THUMBNAIL_SIZE)
empty_label.setAlignment(Qt.AlignCenter)
self.candidate_layout.addWidget(empty_label)
scroll.setWidget(content)
layout.addWidget(scroll, 1)
return panel
def _candidate_item(self, candidate_path, index):
item = QWidget()
item.setFixedWidth(self.THUMBNAIL_SIZE + 16)
layout = QVBoxLayout(item)
layout.setContentsMargins(0, 0, 0, 0)
thumbnail = _CoverThumbnailLabel(candidate_path, self.open_original_image)
thumbnail.setObjectName("coverCandidateThumbnail")
thumbnail.setAlignment(Qt.AlignCenter)
image = _load_image(candidate_path)
if image.isNull():
thumbnail.setText("图片读取失败")
thumbnail.setMinimumSize(self.THUMBNAIL_SIZE, self.THUMBNAIL_SIZE)
else:
thumbnail.setPixmap(
QPixmap.fromImage(
image.scaled(
self.THUMBNAIL_SIZE,
self.THUMBNAIL_SIZE,
Qt.KeepAspectRatio,
Qt.SmoothTransformation,
)
)
)
thumbnail.setMinimumSize(self.THUMBNAIL_SIZE, self.THUMBNAIL_SIZE)
radio = QRadioButton(_candidate_label(candidate_path, self.current_path))
radio.setObjectName(f"coverCandidateRadio{index}")
radio.toggled.connect(
lambda checked, path=candidate_path: self._select_candidate(path) if checked else None
)
self.button_group.addButton(radio, index)
self.candidate_buttons[candidate_path] = radio
meta = QLabel(_candidate_meta(candidate_path, self.current_path))
meta.setWordWrap(True)
meta.setObjectName("coverCandidateMetaLabel")
layout.addWidget(thumbnail)
layout.addWidget(radio)
layout.addWidget(meta)
layout.addStretch(1)
return item
def _sync_initial_selection(self):
for candidate_path, radio in self.candidate_buttons.items():
if _same_file(candidate_path, self.current_path):
radio.setChecked(True)
self.selected_path = candidate_path
break
self.save_button.setEnabled(self.selected_path is not None)
def _select_candidate(self, candidate_path):
self.selected_path = candidate_path
self.save_button.setEnabled(True)
self._update_status()
def _update_status(self):
if not self.candidates:
self.status_label.setText("暂无生成封面图片")
self.save_button.setEnabled(False)
return
if self.current_path and not os.path.exists(self.current_path):
self.status_label.setText("当前生效封面文件不存在,请选择一张后保存")
return
if self.current_path and self.selected_path is None:
self.status_label.setText("当前生效封面不在候选列表,请选择一张后保存")
return
if self.selected_path is None:
self.status_label.setText("请选择一张生成封面后保存")
return
if _same_file(self.selected_path, self.current_path):
self.status_label.setText("当前生效封面已选中")
return
self.status_label.setText("已选择新的生成封面,保存后用于后续更新蝦皮")
def save_selection(self, checked=False):
if not self.selected_path:
QMessageBox.warning(self, "封面画廊", "请先选择一张生成封面")
return False
if _same_file(self.selected_path, self.current_path):
self.changed = False
self.accept()
return True
if self._needs_committed_confirmation() and not self._confirm_committed_save():
return False
try:
db.update_generated_cover(self.task.id, self.selected_path, path=self.db_path)
except Exception as exc:
QMessageBox.warning(self, "封面画廊", f"保存当前封面失败:{exc}")
return False
self.current_path = _normalize_file_path(self.selected_path)
self.changed = True
self.accept()
return True
def _needs_committed_confirmation(self):
return int(getattr(self.task, "committed", 0) or 0) == 1 or getattr(self.task, "stage", None) == "applied"
def _confirm_committed_save(self):
box = QMessageBox(self)
box.setWindowTitle("确认保存封面")
box.setText(
"该商品已经提交过线上。本地换封面不会回滚蝦皮,重复更新会再次提交线上。"
)
save_button = box.addButton("确认保存", QMessageBox.AcceptRole)
cancel_button = box.addButton("取消", QMessageBox.RejectRole)
box.setDefaultButton(cancel_button)
box.exec()
return box.clickedButton() is save_button
def open_original_image(self, image_path):
dialog = OriginalImageDialog(image_path, self)
dialog.exec()
def _fit_to_screen(self):
available = _available_geometry()
if available is None:
self.resize(960, 580)
return
max_width = max(520, int(available.width() * 0.9))
max_height = max(420, int(available.height() * 0.9))
self.resize(min(960, max_width), min(580, max_height))
_center_dialog(self, available)
def _load_image(image_path):
return QImage(str(image_path or ""))
def _build_image_label(image_path, size, empty_text):
label = QLabel()
label.setAlignment(Qt.AlignCenter)
label.setMinimumSize(size, size)
image = _load_image(image_path) if image_path else QImage()
if image.isNull():
label.setText(empty_text)
return label
label.setPixmap(
QPixmap.fromImage(
image.scaled(
size,
size,
Qt.KeepAspectRatio,
Qt.SmoothTransformation,
)
)
)
return label
def _candidate_label(candidate_path, current_path):
return "当前生效" if _same_file(candidate_path, current_path) else os.path.basename(candidate_path)
def _candidate_meta(candidate_path, current_path):
if _same_file(candidate_path, current_path):
return "当前生效封面"
stem, _ext = os.path.splitext(os.path.basename(candidate_path))
parts = stem.rsplit("_", 2)
if len(parts) >= 2 and len(parts[-1]) == 14 and parts[-1].isdigit():
return _format_archive_timestamp(parts[-1])
if len(parts) >= 3 and len(parts[-2]) == 14 and parts[-2].isdigit() and parts[-1].isdigit():
return f"{_format_archive_timestamp(parts[-2])} · 同秒第 {parts[-1]} 张"
if stem.endswith("_new"):
return "当前输出槽"
return "历史候选"
def _format_archive_timestamp(value):
return f"{value[0:4]}-{value[4:6]}-{value[6:8]} {value[8:10]}:{value[10:12]}:{value[12:14]}"
def _normalize_file_path(path):
value = str(path or "").strip()
return os.path.abspath(value) if value else None
def _same_file(left, right):
if not left or not right:
return False
return os.path.normcase(os.path.abspath(str(left))) == os.path.normcase(os.path.abspath(str(right)))
def _available_geometry():
app = QApplication.instance()
screen = app.primaryScreen() if app is not None else None
return screen.availableGeometry() if screen is not None else None
def _center_dialog(dialog, available):
frame = dialog.frameGeometry()
frame.moveCenter(available.center())
dialog.move(frame.topLeft())
class GenerateTab(QWidget): class GenerateTab(QWidget):
"""Tab 2: prompt area plus generation task filters/list.""" """Tab 2: prompt area plus generation task filters/list."""
@@ -875,44 +1212,20 @@ class GenerateTab(QWidget):
if task is None: if task is None:
self._set_status("没有可预览的任务") self._set_status("没有可预览的任务")
return return
dialog = QDialog(self) account = self.model.account_by_alias.get(str(getattr(task, "alias", "") or "").strip())
dialog.setWindowTitle(f"封面对照:{task.item_id}") dialog = CoverGalleryDialog(
layout = QVBoxLayout(dialog) task,
images_layout = QHBoxLayout() image_root=appconfig.image_dir(self.config),
images_layout.addWidget(self._image_panel("旧封面", task.old_cover_path)) db_path=self.db_path,
images_layout.addWidget(self._image_panel("新封面", task.new_cover_path)) account=account,
layout.addLayout(images_layout) parent=self,
buttons = QDialogButtonBox(QDialogButtonBox.Close)
buttons.rejected.connect(dialog.reject)
layout.addWidget(buttons)
dialog.resize(720, 420)
dialog.exec()
def _image_panel(self, title, path):
panel = QWidget()
layout = QVBoxLayout(panel)
layout.addWidget(QLabel(title))
image_label = QLabel()
image_label.setAlignment(Qt.AlignCenter)
image_label.setMinimumSize(260, 260)
image_label.setWordWrap(True)
if path and os.path.exists(str(path)):
pixmap = QPixmap(str(path))
if not pixmap.isNull():
image_label.setPixmap(
pixmap.scaled(
260,
260,
Qt.KeepAspectRatio,
Qt.SmoothTransformation,
)
) )
if dialog.exec() == QDialog.Accepted:
self.refresh_tasks()
if dialog.changed:
self._set_status(f"已保存当前新封面:{task.item_id}")
else: else:
image_label.setText(f"图片无法读取\n{path}") self._set_status(f"当前新封面未变更:{task.item_id}")
else:
image_label.setText(f"无图片\n{path or ''}".strip())
layout.addWidget(image_label, 1)
return panel
def _set_generate_running(self, running): def _set_generate_running(self, running):
self.generate_button.setEnabled(not running) self.generate_button.setEnabled(not running)
+3 -1
View File
@@ -10,10 +10,11 @@ from concurrent.futures import ThreadPoolExecutor, as_completed
try: try:
from PySide6.QtCore import QAbstractTableModel, QModelIndex, Qt, QTimer from PySide6.QtCore import QAbstractTableModel, QModelIndex, Qt, QTimer
from PySide6.QtGui import QColor, QIcon, QPainter, QPixmap from PySide6.QtGui import QColor, QIcon, QImage, QPainter, QPixmap
from PySide6.QtWidgets import ( from PySide6.QtWidgets import (
QAbstractItemView, QAbstractItemView,
QApplication, QApplication,
QButtonGroup,
QCheckBox, QCheckBox,
QComboBox, QComboBox,
QDialog, QDialog,
@@ -32,6 +33,7 @@ try:
QPlainTextEdit, QPlainTextEdit,
QProgressBar, QProgressBar,
QPushButton, QPushButton,
QRadioButton,
QScrollArea, QScrollArea,
QSplitter, QSplitter,
QTableView, QTableView,
+17 -2
View File
@@ -3,7 +3,7 @@ id: T-567
title: ②封面画廊基础:候选图查看、单选保存、原图弹窗 title: ②封面画廊基础:候选图查看、单选保存、原图弹窗
phase: 7 phase: 7
deps: [T-566] deps: [T-566]
status: TODO status: DONE
created: 2026-07-09 created: 2026-07-09
--- ---
@@ -81,4 +81,19 @@ T-566 已提供数据层能力:重置封面时归档旧图、`db.update_genera
## 执行记录 ## 执行记录
(做完在这里写:改了什么文件、跑了什么验证命令及结果、遇到的阻塞、关键决策。) - 2026-07-09 完成 T-567。
- 修改 `app/gui/tabs/generate.py`:
- 将②任务列表双击封面查看入口重构为 `CoverGalleryDialog`,左侧显示旧封面参照,右侧横向滚动画廊显示该任务全部 `_new*.jpg` 候选。
- 画廊打开时调用 T-566 的 `image_paths.list_task_cover_candidates()` 现场扫描目录;默认选中 DB 当前 `new_cover_path`,当前路径缺失时不自动改库并提示选择后保存;无候选时显示空状态并禁用保存。
- 保存时未变更直接关闭;变更时调用 `db.update_generated_cover()`,保存成功后刷新②任务列表。已提交线上任务保存前弹中文二次确认,说明本地换封面不会回滚蝦皮、重复更新会再次提交线上。
- 缩略图和旧封面预览统一用 `QImage` 读入内存后转 `QPixmap`,不长期持有文件句柄;候选缩略图双击打开 `OriginalImageDialog` 原图弹窗,标题显示文件名与原始分辨率,超出屏幕时可滚动。
- 修改 `app/gui/widgets.py`:补充 `QImage`、`QButtonGroup`、`QRadioButton` 聚合导入,供画廊弹窗使用。
- 修改 `tests/test_gui.py`:
- 覆盖候选图展示和 `_old` 过滤、当前封面默认选中、打开画廊后候选文件可重命名、保存切换当前封面、已提交任务取消确认不写库、当前路径缺失/无候选提示和禁用保存、原图弹窗分辨率标题。
- 验证通过:
- `py -3.10 -m unittest tests.test_gui`
- `py -3.10 -m unittest tests.test_gui tests.test_db tests.test_image_paths`
- `python -m ruff check app tests main.py`
- `py -3.10 -m compileall app main.py`
- `py -3.10 -m unittest discover -s tests`(290 tests;PySide6 字体目录警告不影响结果)
- `git diff --check`
+203 -2
View File
@@ -12,13 +12,13 @@ sys.path.insert(0, os.path.dirname(__file__))
from _helpers import TempDirMixin from _helpers import TempDirMixin
from app import gui from app import gui
from app import accounts, ai, appconfig, db, prompts, update_check from app import accounts, ai, appconfig, db, image_paths, prompts, update_check
if gui.QT_IMPORT_ERROR is not None: if gui.QT_IMPORT_ERROR is not None:
raise unittest.SkipTest("PySide6 未安装") raise unittest.SkipTest("PySide6 未安装")
from PySide6.QtCore import QItemSelectionModel, QRect from PySide6.QtCore import QItemSelectionModel, QRect
from PySide6.QtGui import QTextCursor from PySide6.QtGui import QImage, QTextCursor
from PySide6.QtWidgets import QApplication, QCheckBox, QLineEdit, QPlainTextEdit, QProgressBar, QTableView from PySide6.QtWidgets import QApplication, QCheckBox, QLineEdit, QPlainTextEdit, QProgressBar, QTableView
from app.gui import ( from app.gui import (
@@ -39,6 +39,7 @@ from app.gui import (
TAB_TITLES, TAB_TITLES,
WriteBackWorker, WriteBackWorker,
) )
from app.gui.tabs.generate import CoverGalleryDialog, OriginalImageDialog
from app.gui.main_window import _fit_and_center_window from app.gui.main_window import _fit_and_center_window
@@ -81,6 +82,13 @@ class GuiTests(TempDirMixin, unittest.TestCase):
} }
return cfg return cfg
def write_test_image(self, path, width=32, height=32):
os.makedirs(os.path.dirname(path), exist_ok=True)
image = QImage(width, height, QImage.Format_RGB32)
image.fill(0xFF336699)
self.assertTrue(image.save(path))
return path
def assert_foreground(self, model, row, column, color): def assert_foreground(self, model, row, column, color):
value = model.data(model.index(row, column), gui.Qt.ForegroundRole) value = model.data(model.index(row, column), gui.Qt.ForegroundRole)
self.assertIsNotNone(value) self.assertIsNotNone(value)
@@ -2068,6 +2076,199 @@ class GuiTests(TempDirMixin, unittest.TestCase):
self.assert_removed(temp_dir) self.assert_removed(temp_dir)
def test_cover_gallery_lists_candidates_and_selects_current_cover(self):
with self.make_temp_dir() as temp_dir:
cfg = self.make_config(temp_dir)
account = accounts.create_account("主店", "alias-a", debug_port=9222, config=cfg)
db.init_db(cfg["db_path"])
batch_id = db.create_batch(["input.xlsx"], path=cfg["db_path"])
db.insert_tasks(
batch_id,
[
{
"source_file_abs": os.path.join(temp_dir, "input.xlsx"),
"source_sheet": "商品",
"source_row": 2,
"account_name": "Excel主店",
"alias": "alias-a",
"item_id": "51100639510",
}
],
path=cfg["db_path"],
)
task = db.list_tasks(batch_id=batch_id, path=cfg["db_path"])[0]
old_cover = self.write_test_image(os.path.join(temp_dir, "old.jpg"))
canonical = image_paths.task_image_path(cfg["image_dir"], task, account, "new")
prefix = os.path.splitext(canonical)[0]
archive_newer = self.write_test_image(f"{prefix}_20260709101000.jpg")
archive_older = self.write_test_image(f"{prefix}_20260709094800.jpg")
old_candidate = f"{prefix.replace('_new', '_old')}.jpg"
self.write_test_image(canonical)
self.write_test_image(old_candidate)
db.set_collected(task.id, "旧标题", old_cover, path=cfg["db_path"])
db.set_generated(task.id, "新标题", canonical, path=cfg["db_path"])
task = db.get_task(task.id, path=cfg["db_path"])
dialog = CoverGalleryDialog(task, cfg["image_dir"], cfg["db_path"], account=account)
self.addCleanup(dialog.close)
self.assertEqual([canonical, archive_newer, archive_older], dialog.candidates)
self.assertTrue(dialog.candidate_buttons[canonical].isChecked())
self.assertEqual(canonical, dialog.selected_path)
self.assertNotIn(old_candidate, dialog.candidates)
renamed_archive = archive_newer.replace(".jpg", "_renamed.jpg")
os.rename(archive_newer, renamed_archive)
self.assertTrue(os.path.exists(renamed_archive))
self.assert_removed(temp_dir)
def test_cover_gallery_save_switches_current_cover(self):
with self.make_temp_dir() as temp_dir:
cfg = self.make_config(temp_dir)
account = accounts.create_account("主店", "alias-a", debug_port=9222, config=cfg)
db.init_db(cfg["db_path"])
batch_id = db.create_batch(["input.xlsx"], path=cfg["db_path"])
db.insert_tasks(
batch_id,
[
{
"source_file_abs": os.path.join(temp_dir, "input.xlsx"),
"source_sheet": "商品",
"source_row": 2,
"account_name": "Excel主店",
"alias": "alias-a",
"item_id": "51100639510",
}
],
path=cfg["db_path"],
)
task = db.list_tasks(batch_id=batch_id, path=cfg["db_path"])[0]
canonical = image_paths.task_image_path(cfg["image_dir"], task, account, "new")
selected = self.write_test_image(os.path.splitext(canonical)[0] + "_20260709101000.jpg")
self.write_test_image(canonical)
db.set_generated(task.id, "新标题", canonical, path=cfg["db_path"])
task = db.get_task(task.id, path=cfg["db_path"])
dialog = CoverGalleryDialog(task, cfg["image_dir"], cfg["db_path"], account=account)
self.addCleanup(dialog.close)
dialog.candidate_buttons[selected].setChecked(True)
self.assertTrue(dialog.save_selection())
updated = db.get_task(task.id, path=cfg["db_path"])
self.assertEqual(os.path.abspath(selected), updated.new_cover_path)
self.assertEqual("generated", updated.stage)
self.assertEqual("pending", updated.status)
self.assertTrue(dialog.changed)
self.assert_removed(temp_dir)
def test_cover_gallery_committed_cancel_does_not_write_db(self):
with self.make_temp_dir() as temp_dir:
cfg = self.make_config(temp_dir)
account = accounts.create_account("主店", "alias-a", debug_port=9222, config=cfg)
db.init_db(cfg["db_path"])
batch_id = db.create_batch(["input.xlsx"], path=cfg["db_path"])
db.insert_tasks(
batch_id,
[
{
"source_file_abs": os.path.join(temp_dir, "input.xlsx"),
"source_sheet": "商品",
"source_row": 2,
"account_name": "Excel主店",
"alias": "alias-a",
"item_id": "51100639510",
}
],
path=cfg["db_path"],
)
task = db.list_tasks(batch_id=batch_id, path=cfg["db_path"])[0]
canonical = image_paths.task_image_path(cfg["image_dir"], task, account, "new")
selected = self.write_test_image(os.path.splitext(canonical)[0] + "_20260709101000.jpg")
self.write_test_image(canonical)
db.set_generated(task.id, "新标题", canonical, path=cfg["db_path"])
db.set_applied(task.id, True, path=cfg["db_path"])
task = db.get_task(task.id, path=cfg["db_path"])
dialog = CoverGalleryDialog(task, cfg["image_dir"], cfg["db_path"], account=account)
self.addCleanup(dialog.close)
dialog.candidate_buttons[selected].setChecked(True)
message_box, boxes = self.make_fake_message_box("取消")
with mock.patch("app.gui.tabs.generate.QMessageBox", message_box):
self.assertFalse(dialog.save_selection())
updated = db.get_task(task.id, path=cfg["db_path"])
self.assertEqual(canonical, updated.new_cover_path)
self.assertEqual("applied", updated.stage)
self.assertEqual(1, updated.committed)
self.assertIn("重复更新会再次提交线上", boxes[0].text)
self.assertFalse(dialog.changed)
self.assert_removed(temp_dir)
def test_cover_gallery_handles_missing_current_and_empty_candidates(self):
with self.make_temp_dir() as temp_dir:
cfg = self.make_config(temp_dir)
account = accounts.create_account("主店", "alias-a", debug_port=9222, config=cfg)
db.init_db(cfg["db_path"])
batch_id = db.create_batch(["input.xlsx"], path=cfg["db_path"])
db.insert_tasks(
batch_id,
[
{
"source_file_abs": os.path.join(temp_dir, "input.xlsx"),
"source_sheet": "商品",
"source_row": 2,
"account_name": "Excel主店",
"alias": "alias-a",
"item_id": "51100639510",
},
{
"source_file_abs": os.path.join(temp_dir, "input.xlsx"),
"source_sheet": "商品",
"source_row": 3,
"account_name": "Excel主店",
"alias": "alias-a",
"item_id": "51100639511",
},
],
path=cfg["db_path"],
)
first, second = db.list_tasks(batch_id=batch_id, path=cfg["db_path"])
missing_current = os.path.join(temp_dir, "missing.jpg")
db.set_generated(first.id, "新标题A", missing_current, path=cfg["db_path"])
first_candidate = image_paths.task_image_path(cfg["image_dir"], first, account, "new")
self.write_test_image(first_candidate)
first = db.get_task(first.id, path=cfg["db_path"])
missing_dialog = CoverGalleryDialog(first, cfg["image_dir"], cfg["db_path"], account=account)
self.addCleanup(missing_dialog.close)
self.assertIsNone(missing_dialog.selected_path)
self.assertFalse(missing_dialog.save_button.isEnabled())
self.assertIn("当前生效封面文件不存在", missing_dialog.status_label.text())
db.set_generated(second.id, "新标题B", os.path.join(temp_dir, "none.jpg"), path=cfg["db_path"])
second = db.get_task(second.id, path=cfg["db_path"])
empty_dialog = CoverGalleryDialog(second, cfg["image_dir"], cfg["db_path"], account=account)
self.addCleanup(empty_dialog.close)
self.assertEqual([], empty_dialog.candidates)
self.assertFalse(empty_dialog.save_button.isEnabled())
self.assertIn("暂无生成封面图片", empty_dialog.status_label.text())
self.assert_removed(temp_dir)
def test_original_image_dialog_title_contains_resolution(self):
with self.make_temp_dir() as temp_dir:
image_path = self.write_test_image(os.path.join(temp_dir, "cover.jpg"), width=64, height=32)
dialog = OriginalImageDialog(image_path)
self.addCleanup(dialog.close)
self.assertIn("cover.jpg", dialog.windowTitle())
self.assertIn("64x32", dialog.windowTitle())
self.assert_removed(temp_dir)
def test_generate_tab_allows_editing_generated_title_locally(self): def test_generate_tab_allows_editing_generated_title_locally(self):
with self.make_temp_dir() as temp_dir: with self.make_temp_dir() as temp_dir: