feat(product-suite): add grouped generation history
This commit is contained in:
+524
-22
@@ -24,6 +24,7 @@ from PySide6.QtWidgets import (
|
||||
QApplication,
|
||||
QCheckBox,
|
||||
QComboBox,
|
||||
QDialog,
|
||||
QFileDialog,
|
||||
QFrame,
|
||||
QGridLayout,
|
||||
@@ -684,6 +685,452 @@ class SuiteResultCard(QFrame):
|
||||
layout.addLayout(footer)
|
||||
|
||||
|
||||
class SuiteHistoryImageCard(QFrame):
|
||||
"""Read-only image card used by the product-suite history dialog."""
|
||||
|
||||
selected = Signal(object)
|
||||
previewRequested = Signal(object, object)
|
||||
menuRequested = Signal(object, object, object)
|
||||
|
||||
def __init__(self, job, asset=None, retry_count=0, parent=None):
|
||||
super().__init__(parent)
|
||||
self.job = job
|
||||
self.asset = asset
|
||||
self.retry_count = max(0, int(retry_count or 0))
|
||||
self._selected = False
|
||||
self.setObjectName("suiteHistoryImageCard")
|
||||
self.setFixedSize(166, 190)
|
||||
self.setContextMenuPolicy(Qt.CustomContextMenu)
|
||||
self.customContextMenuRequested.connect(
|
||||
lambda point: self.menuRequested.emit(
|
||||
self.job,
|
||||
self.asset,
|
||||
self.mapToGlobal(point),
|
||||
)
|
||||
)
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setContentsMargins(6, 6, 6, 6)
|
||||
layout.setSpacing(4)
|
||||
|
||||
title = QLabel(str(getattr(job, "job_type", "套图") or "套图"))
|
||||
title.setStyleSheet("font-weight: 600; color: #24292f;")
|
||||
title.setAttribute(Qt.WA_TransparentForMouseEvents, True)
|
||||
layout.addWidget(title)
|
||||
|
||||
image = QLabel()
|
||||
image.setObjectName("suiteHistoryImage")
|
||||
image.setAlignment(Qt.AlignCenter)
|
||||
image.setFixedSize(154, 118)
|
||||
image.setAttribute(Qt.WA_TransparentForMouseEvents, True)
|
||||
status = str(getattr(job, "status", "pending") or "pending")
|
||||
if asset is not None and _asset_usable(asset):
|
||||
image.setPixmap(_image_pixmap(asset.local_path, QSize(154, 118)))
|
||||
else:
|
||||
image.setPixmap(
|
||||
_placeholder_pixmap(
|
||||
"图片文件不可用" if status == "succeeded" else self._status_text(status),
|
||||
QSize(154, 118),
|
||||
"#fff8c5" if status == "succeeded" else "#f3f4f6",
|
||||
)
|
||||
)
|
||||
layout.addWidget(image)
|
||||
|
||||
footer_text = self._status_text(status)
|
||||
if self.retry_count:
|
||||
footer_text += " · 重试%d次" % self.retry_count
|
||||
footer = QLabel(footer_text)
|
||||
footer.setObjectName("suiteHistoryImageStatus")
|
||||
footer.setWordWrap(True)
|
||||
footer.setMaximumHeight(34)
|
||||
footer.setAttribute(Qt.WA_TransparentForMouseEvents, True)
|
||||
layout.addWidget(footer)
|
||||
|
||||
tooltip = [
|
||||
"类型:%s" % str(getattr(job, "job_type", "套图") or "套图"),
|
||||
"状态:%s" % self._status_text(status),
|
||||
"生成时间:%s" % _history_time_text(getattr(job, "created_at", "")),
|
||||
]
|
||||
if self.retry_count:
|
||||
tooltip.append("本槽位已重试%d次" % self.retry_count)
|
||||
if asset is None or not _asset_usable(asset):
|
||||
tooltip.append("本地图片文件不可用")
|
||||
self.setToolTip("\n".join(tooltip))
|
||||
self._apply_selected_style()
|
||||
|
||||
@staticmethod
|
||||
def _status_text(status):
|
||||
return {
|
||||
"pending": "等待提交",
|
||||
"submitted": "已提交",
|
||||
"running": "生成中",
|
||||
"succeeded": "生成成功",
|
||||
"failed": "生成失败",
|
||||
"expired": "任务过期",
|
||||
"cancelled": "已停止",
|
||||
}.get(str(status or ""), "处理中")
|
||||
|
||||
def set_selected(self, selected):
|
||||
self._selected = bool(selected)
|
||||
self._apply_selected_style()
|
||||
|
||||
def _apply_selected_style(self):
|
||||
border = "#0969da" if self._selected else "#d8dee4"
|
||||
background = "#eef6ff" if self._selected else "#ffffff"
|
||||
self.setStyleSheet(
|
||||
"QFrame#suiteHistoryImageCard {"
|
||||
"border: %dpx solid %s; border-radius: 6px; background: %s;"
|
||||
"}" % (2 if self._selected else 1, border, background)
|
||||
)
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
if event.button() == Qt.LeftButton:
|
||||
self.selected.emit(self)
|
||||
super().mousePressEvent(event)
|
||||
|
||||
def mouseDoubleClickEvent(self, event):
|
||||
if event.button() == Qt.LeftButton:
|
||||
self.previewRequested.emit(self.job, self.asset)
|
||||
super().mouseDoubleClickEvent(event)
|
||||
|
||||
|
||||
def _history_time_text(value):
|
||||
text = str(value or "").strip().replace("T", " ")
|
||||
if not text:
|
||||
return "时间未知"
|
||||
return text[:19]
|
||||
|
||||
|
||||
class ProductSuiteHistoryDialog(QDialog):
|
||||
"""Read-only, project-scoped generation history grouped by persisted rounds."""
|
||||
|
||||
PAGE_SIZE = 20
|
||||
|
||||
def __init__(self, project_id, *, db_path=None, parent=None):
|
||||
super().__init__(parent)
|
||||
self.project_id = int(project_id)
|
||||
self.db_path = db_path
|
||||
self._offset = 0
|
||||
self._has_more = False
|
||||
self._assets = {}
|
||||
self._selected_card = None
|
||||
self._round_count = 0
|
||||
self._available_image_count = 0
|
||||
self._project_unavailable = False
|
||||
|
||||
self.setObjectName("suiteHistoryDialog")
|
||||
self.setWindowTitle("历史生成记录")
|
||||
self.setModal(False)
|
||||
self.setAttribute(Qt.WA_DeleteOnClose, True)
|
||||
self.setMinimumSize(740, 520)
|
||||
self.resize(940, 680)
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setContentsMargins(14, 14, 14, 14)
|
||||
layout.setSpacing(10)
|
||||
|
||||
header = QHBoxLayout()
|
||||
header.setSpacing(10)
|
||||
header_text = QVBoxLayout()
|
||||
header_text.setSpacing(2)
|
||||
self.context_label = QLabel()
|
||||
self.context_label.setObjectName("suiteHistoryContextLabel")
|
||||
self.context_label.setStyleSheet("font-weight: 600; color: #24292f;")
|
||||
self.summary_label = QLabel()
|
||||
self.summary_label.setObjectName("suiteHistorySummaryLabel")
|
||||
self.summary_label.setStyleSheet("color: #57606a;")
|
||||
header_text.addWidget(self.context_label)
|
||||
header_text.addWidget(self.summary_label)
|
||||
header.addLayout(header_text, 1)
|
||||
self.refresh_button = QPushButton("刷新")
|
||||
self.refresh_button.setObjectName("suiteHistoryRefreshButton")
|
||||
self.refresh_button.setToolTip("重新读取当前商品的生成历史")
|
||||
self.refresh_button.clicked.connect(self.refresh_history)
|
||||
header.addWidget(self.refresh_button)
|
||||
layout.addLayout(header)
|
||||
|
||||
self.notice_label = QLabel()
|
||||
self.notice_label.setObjectName("suiteHistoryNoticeLabel")
|
||||
self.notice_label.setWordWrap(True)
|
||||
self.notice_label.hide()
|
||||
layout.addWidget(self.notice_label)
|
||||
|
||||
self.scroll = QScrollArea()
|
||||
self.scroll.setObjectName("suiteHistoryScrollArea")
|
||||
self.scroll.setWidgetResizable(True)
|
||||
self.history_content = QWidget()
|
||||
self.history_content.setObjectName("suiteHistoryContent")
|
||||
self.history_layout = QVBoxLayout(self.history_content)
|
||||
self.history_layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.history_layout.setSpacing(12)
|
||||
self.history_layout.setAlignment(Qt.AlignTop)
|
||||
self.scroll.setWidget(self.history_content)
|
||||
layout.addWidget(self.scroll, 1)
|
||||
|
||||
self.load_more_button = QPushButton("加载更多")
|
||||
self.load_more_button.setObjectName("suiteHistoryLoadMoreButton")
|
||||
self.load_more_button.clicked.connect(self.load_more)
|
||||
self.load_more_button.hide()
|
||||
layout.addWidget(self.load_more_button, 0, Qt.AlignHCenter)
|
||||
|
||||
self.refresh_history()
|
||||
|
||||
def refresh_history(self, checked=False):
|
||||
scroll_value = self.scroll.verticalScrollBar().value()
|
||||
self._clear_history_content()
|
||||
self._offset = 0
|
||||
self._has_more = False
|
||||
self._assets = {}
|
||||
self._round_count = 0
|
||||
self._available_image_count = 0
|
||||
if not self._load_next_page():
|
||||
return
|
||||
QTimer.singleShot(
|
||||
0,
|
||||
lambda: self.scroll.verticalScrollBar().setValue(
|
||||
min(scroll_value, self.scroll.verticalScrollBar().maximum())
|
||||
),
|
||||
)
|
||||
|
||||
def load_more(self, checked=False):
|
||||
if self._has_more:
|
||||
self._load_next_page()
|
||||
|
||||
def _load_next_page(self):
|
||||
try:
|
||||
project = image_studio.get_project(self.project_id, path=self.db_path)
|
||||
if project is None:
|
||||
self._set_project_unavailable()
|
||||
return False
|
||||
self._project_unavailable = False
|
||||
self._set_context(project)
|
||||
if not self._assets:
|
||||
self._assets = {
|
||||
int(asset.id): asset
|
||||
for asset in image_studio.list_assets(
|
||||
self.project_id,
|
||||
path=self.db_path,
|
||||
)
|
||||
}
|
||||
rounds = image_studio.list_generation_rounds(
|
||||
self.project_id,
|
||||
limit=self.PAGE_SIZE,
|
||||
offset=self._offset,
|
||||
path=self.db_path,
|
||||
)
|
||||
except Exception as exc:
|
||||
self._set_error("历史记录读取失败:%s" % _user_error(exc))
|
||||
return False
|
||||
|
||||
self.notice_label.hide()
|
||||
if not rounds and self._offset == 0:
|
||||
self._show_empty_state()
|
||||
else:
|
||||
for round_info in rounds:
|
||||
self._add_round(round_info)
|
||||
self._offset += len(rounds)
|
||||
self._has_more = len(rounds) == self.PAGE_SIZE
|
||||
self.load_more_button.setVisible(self._has_more)
|
||||
self.load_more_button.setEnabled(self._has_more)
|
||||
self._update_summary()
|
||||
return True
|
||||
|
||||
def _set_context(self, project):
|
||||
account_name = str(project.account_name or project.account_alias or "未命名账号")
|
||||
item_text = "临时草稿" if image_studio.is_draft_project(project) else "商品ID:%s" % project.item_id
|
||||
self.context_label.setText("店铺:%s · %s" % (account_name, item_text))
|
||||
|
||||
def _update_summary(self):
|
||||
refreshed = time.strftime("%H:%M:%S")
|
||||
self.summary_label.setText(
|
||||
"已加载 %d 轮 · 可用图片 %d 张 · 最近刷新 %s"
|
||||
% (self._round_count, self._available_image_count, refreshed)
|
||||
)
|
||||
|
||||
def _clear_history_content(self):
|
||||
self._selected_card = None
|
||||
while self.history_layout.count():
|
||||
item = self.history_layout.takeAt(0)
|
||||
widget = item.widget()
|
||||
if widget is not None:
|
||||
widget.deleteLater()
|
||||
|
||||
def _set_project_unavailable(self):
|
||||
self._project_unavailable = True
|
||||
self._clear_history_content()
|
||||
self.context_label.setText("当前商品项目不可用")
|
||||
self.summary_label.setText("该商品项目已删除或不可访问")
|
||||
self._set_notice("当前商品项目已删除或不可访问,无法继续读取历史生成记录。", "#cf222e")
|
||||
self.refresh_button.setEnabled(False)
|
||||
self.load_more_button.hide()
|
||||
|
||||
def _set_error(self, message):
|
||||
self._clear_history_content()
|
||||
self._set_notice(message, "#cf222e")
|
||||
self.load_more_button.hide()
|
||||
self.refresh_button.setEnabled(True)
|
||||
|
||||
def _set_notice(self, text, color="#57606a"):
|
||||
self.notice_label.setText(str(text))
|
||||
self.notice_label.setStyleSheet("color: %s; padding: 8px 0;" % color)
|
||||
self.notice_label.show()
|
||||
|
||||
def _show_empty_state(self):
|
||||
empty = QLabel("暂无历史生成记录,完成套图生成后会自动出现在这里")
|
||||
empty.setObjectName("suiteHistoryEmptyLabel")
|
||||
empty.setAlignment(Qt.AlignCenter)
|
||||
empty.setStyleSheet("color: #6b7280; padding: 56px;")
|
||||
self.history_layout.addWidget(empty)
|
||||
|
||||
def _add_round(self, round_info):
|
||||
try:
|
||||
jobs = image_studio.list_generation_round_current_jobs(
|
||||
self.project_id,
|
||||
round_info.generation_round_key,
|
||||
path=self.db_path,
|
||||
)
|
||||
attempts = image_studio.list_generation_round_attempts(
|
||||
self.project_id,
|
||||
round_info.generation_round_key,
|
||||
path=self.db_path,
|
||||
)
|
||||
except Exception as exc:
|
||||
self._set_error("历史轮次读取失败:%s" % _user_error(exc))
|
||||
return
|
||||
|
||||
section = QWidget()
|
||||
section.setObjectName("suiteHistoryRound")
|
||||
section.setStyleSheet(
|
||||
"QWidget#suiteHistoryRound { border-bottom: 1px solid #d8dee4; }"
|
||||
)
|
||||
layout = QVBoxLayout(section)
|
||||
layout.setContentsMargins(2, 2, 2, 12)
|
||||
layout.setSpacing(7)
|
||||
|
||||
header = QHBoxLayout()
|
||||
header.setSpacing(8)
|
||||
time_label = QLabel(
|
||||
"旧版历史记录" if round_info.is_legacy else "生成于 %s" % _history_time_text(round_info.created_at)
|
||||
)
|
||||
time_label.setStyleSheet("font-weight: 600; color: #24292f;")
|
||||
header.addWidget(time_label)
|
||||
if round_info.is_current:
|
||||
current = QLabel("当前")
|
||||
current.setObjectName("suiteHistoryCurrentBadge")
|
||||
current.setStyleSheet(
|
||||
"color: #0969da; background: #ddf4ff; border: 1px solid #54aeff; "
|
||||
"border-radius: 6px; padding: 1px 6px; font-weight: 600;"
|
||||
)
|
||||
header.addWidget(current)
|
||||
elif round_info.is_legacy:
|
||||
legacy = QLabel("旧版")
|
||||
legacy.setStyleSheet(
|
||||
"color: #57606a; background: #f6f8fa; border: 1px solid #d8dee4; "
|
||||
"border-radius: 6px; padding: 1px 6px;"
|
||||
)
|
||||
header.addWidget(legacy)
|
||||
header.addStretch(1)
|
||||
stats = self._round_stats_text(round_info)
|
||||
stats_label = QLabel(stats)
|
||||
stats_label.setObjectName("suiteHistoryRoundStats")
|
||||
stats_label.setStyleSheet("color: #57606a;")
|
||||
header.addWidget(stats_label)
|
||||
layout.addLayout(header)
|
||||
|
||||
attempts_by_slot = {}
|
||||
for attempt in attempts:
|
||||
slot = getattr(attempt, "generation_slot_index", None)
|
||||
if slot is not None:
|
||||
attempts_by_slot[int(slot)] = attempts_by_slot.get(int(slot), 0) + 1
|
||||
|
||||
if not jobs:
|
||||
message = QLabel("本轮没有可展示的图片,已保留生成状态记录。")
|
||||
message.setStyleSheet("color: #6b7280; padding: 12px 0;")
|
||||
layout.addWidget(message)
|
||||
else:
|
||||
grid = QGridLayout()
|
||||
grid.setContentsMargins(0, 0, 0, 0)
|
||||
grid.setHorizontalSpacing(8)
|
||||
grid.setVerticalSpacing(8)
|
||||
columns = 5
|
||||
for index, job in enumerate(jobs):
|
||||
asset = self._assets.get(int(job.output_asset_id or 0))
|
||||
slot = getattr(job, "generation_slot_index", None)
|
||||
retry_count = max(0, attempts_by_slot.get(int(slot), 1) - 1) if slot is not None else 0
|
||||
card = SuiteHistoryImageCard(job, asset, retry_count)
|
||||
card.selected.connect(self._select_card)
|
||||
card.previewRequested.connect(self._preview_job)
|
||||
card.menuRequested.connect(self._show_job_menu)
|
||||
grid.addWidget(card, index // columns, index % columns)
|
||||
if asset is not None and _asset_usable(asset):
|
||||
self._available_image_count += 1
|
||||
layout.addLayout(grid)
|
||||
|
||||
self.history_layout.addWidget(section)
|
||||
self._round_count += 1
|
||||
|
||||
@staticmethod
|
||||
def _round_stats_text(round_info):
|
||||
parts = ["记录 %d" % int(round_info.job_count or 0)]
|
||||
if round_info.slot_count:
|
||||
parts.append("图片 %d" % int(round_info.slot_count))
|
||||
parts.append("成功 %d" % int(round_info.succeeded_count or 0))
|
||||
if round_info.failed_count:
|
||||
parts.append("失败 %d" % int(round_info.failed_count))
|
||||
if round_info.cancelled_count:
|
||||
parts.append("停止 %d" % int(round_info.cancelled_count))
|
||||
if round_info.active_count:
|
||||
parts.append("进行中 %d" % int(round_info.active_count))
|
||||
if round_info.retry_count:
|
||||
parts.append("重试 %d" % int(round_info.retry_count))
|
||||
return " · ".join(parts)
|
||||
|
||||
def _select_card(self, card):
|
||||
if self._selected_card is card:
|
||||
return
|
||||
if self._selected_card is not None:
|
||||
self._selected_card.set_selected(False)
|
||||
self._selected_card = card
|
||||
self._selected_card.set_selected(True)
|
||||
|
||||
def _preview_job(self, job, asset):
|
||||
if asset is None or not _asset_usable(asset):
|
||||
self._set_notice("这张图片的本地文件不可用,无法预览。", "#9a6700")
|
||||
return
|
||||
ProductSuitePreviewDialog(
|
||||
asset.local_path,
|
||||
"%s预览" % str(getattr(job, "job_type", "套图") or "套图"),
|
||||
self,
|
||||
).exec()
|
||||
|
||||
def _show_job_menu(self, job, asset, global_position):
|
||||
menu = QMenu(self)
|
||||
preview_action = menu.addAction("预览")
|
||||
copy_action = menu.addAction("复制路径")
|
||||
folder_action = menu.addAction("打开所在文件夹")
|
||||
action = menu.exec(global_position)
|
||||
if action is preview_action:
|
||||
self._preview_job(job, asset)
|
||||
elif action is copy_action:
|
||||
if asset is None or not _asset_usable(asset):
|
||||
self._set_notice("当前图片没有可复制的本地路径。", "#9a6700")
|
||||
return
|
||||
QApplication.clipboard().setText(asset.local_path)
|
||||
self._set_notice("图片路径已复制。", "#1a7f37")
|
||||
elif action is folder_action:
|
||||
if asset is None or not _asset_usable(asset):
|
||||
self._set_notice("当前图片没有可打开的本地文件夹。", "#9a6700")
|
||||
return
|
||||
try:
|
||||
file_manager.open_in_file_manager(os.path.dirname(asset.local_path))
|
||||
except Exception as exc:
|
||||
self._set_notice("打开文件夹失败:%s" % _user_error(exc), "#cf222e")
|
||||
|
||||
def closeEvent(self, event):
|
||||
self._clear_history_content()
|
||||
super().closeEvent(event)
|
||||
|
||||
|
||||
@dataclass
|
||||
class SuiteTaskState:
|
||||
key: int
|
||||
@@ -697,7 +1144,6 @@ class SuiteTaskState:
|
||||
settings: dict = field(default_factory=product_suite.default_suite_settings)
|
||||
current_job_ids: list = field(default_factory=list)
|
||||
current_generation_round_key: str = ""
|
||||
show_history: bool = False
|
||||
generation_job_ids: list = field(default_factory=list)
|
||||
generation_mode: str = "batch"
|
||||
generation_retry_job_id: int = None
|
||||
@@ -777,6 +1223,7 @@ class ProductSuiteTab(QWidget):
|
||||
self._loading = False
|
||||
self._result_refresh_pending = False
|
||||
self._prompt_template_init_error = ""
|
||||
self._history_dialog = None
|
||||
|
||||
try:
|
||||
prompts.ensure_default_product_suite_prompt(self.product_suite_prompt_path)
|
||||
@@ -869,7 +1316,7 @@ class ProductSuiteTab(QWidget):
|
||||
layout.setSpacing(8)
|
||||
self.history_button = QPushButton("历史生成")
|
||||
self.history_button.setObjectName("suiteHistoryButton")
|
||||
self.history_button.setCheckable(True)
|
||||
self.history_button.setToolTip("查看当前商品的历史生成记录")
|
||||
layout.addWidget(self.history_button)
|
||||
self.open_folder_button = QPushButton("打开结果文件夹")
|
||||
self.open_folder_button.setObjectName("suiteOpenFolderButton")
|
||||
@@ -1222,7 +1669,7 @@ class ProductSuiteTab(QWidget):
|
||||
self.custom_category_edit.returnPressed.connect(self._commit_custom_category)
|
||||
self.custom_category_edit.editingFinished.connect(self._finish_custom_category_edit)
|
||||
self.generate_button.clicked.connect(self.toggle_generation)
|
||||
self.history_button.toggled.connect(self._toggle_history)
|
||||
self.history_button.clicked.connect(self.open_history_dialog)
|
||||
self.open_folder_button.clicked.connect(self.open_project_folder)
|
||||
self.undo_button.clicked.connect(self.undo_delete)
|
||||
self.more_button.clicked.connect(self._show_more_menu)
|
||||
@@ -1407,6 +1854,8 @@ class ProductSuiteTab(QWidget):
|
||||
self._generation_run_states.pop(state.generation_run_token, None)
|
||||
if state.pull_run_token:
|
||||
self._pull_run_states.pop(state.pull_run_token, None)
|
||||
if state.project_id is not None:
|
||||
self._close_history_dialog_for_project(state.project_id)
|
||||
self._retired_states.append(state)
|
||||
self._states.pop(state.key, None)
|
||||
self.task_tabs.removeTab(index)
|
||||
@@ -1474,7 +1923,6 @@ class ProductSuiteTab(QWidget):
|
||||
self._set_combo_value(self.language_combo, state.settings.get("language"))
|
||||
self._set_combo_value(self.ratio_combo, state.settings.get("ratio"))
|
||||
self.per_image_checkbox.setChecked(bool(state.settings.get("per_image_primary")))
|
||||
self.history_button.setChecked(bool(state.show_history))
|
||||
finally:
|
||||
self._loading = False
|
||||
self._refresh_originals(state)
|
||||
@@ -3162,7 +3610,6 @@ class ProductSuiteTab(QWidget):
|
||||
state.started_at = time.monotonic()
|
||||
if not retrying:
|
||||
state.current_job_ids = []
|
||||
state.show_history = False
|
||||
self._generation_run_states[run_token] = state.key
|
||||
worker.progress.connect(self._on_generation_progress_signal)
|
||||
worker.finished.connect(self._on_generation_finished_signal)
|
||||
@@ -3179,12 +3626,6 @@ class ProductSuiteTab(QWidget):
|
||||
"mode": state.generation_mode,
|
||||
},
|
||||
)
|
||||
if state is self._displayed_state and not retrying:
|
||||
self._loading = True
|
||||
try:
|
||||
self.history_button.setChecked(False)
|
||||
finally:
|
||||
self._loading = False
|
||||
if state is self._displayed_state:
|
||||
self._apply_running_state(state)
|
||||
self._refresh_results(state)
|
||||
@@ -3426,7 +3867,7 @@ class ProductSuiteTab(QWidget):
|
||||
if retry_job_id in current:
|
||||
index = current.index(retry_job_id)
|
||||
current[index:index + 1] = normalized
|
||||
elif not state.show_history:
|
||||
else:
|
||||
for job_id in normalized:
|
||||
if job_id not in current:
|
||||
current.append(job_id)
|
||||
@@ -3794,13 +4235,6 @@ class ProductSuiteTab(QWidget):
|
||||
else:
|
||||
self.ai_write_button.setText("AI 帮写")
|
||||
|
||||
def _toggle_history(self, checked):
|
||||
if self._loading or self._displayed_state is None:
|
||||
return
|
||||
self._displayed_state.show_history = bool(checked)
|
||||
self.history_button.setText("返回本轮" if checked else "历史生成")
|
||||
self._refresh_results(self._displayed_state)
|
||||
|
||||
def _jobs_for_state(self, state):
|
||||
if state.project_id is None:
|
||||
return []
|
||||
@@ -3809,8 +4243,6 @@ class ProductSuiteTab(QWidget):
|
||||
except Exception as exc:
|
||||
self._status("生成结果读取失败:%s" % _user_error(exc), "danger")
|
||||
return []
|
||||
if state.show_history:
|
||||
return jobs
|
||||
by_id = {int(job.id): job for job in jobs}
|
||||
return [
|
||||
by_id[int(job_id)]
|
||||
@@ -3854,7 +4286,76 @@ class ProductSuiteTab(QWidget):
|
||||
self.result_grid.addWidget(card, index // columns, index % columns)
|
||||
self.result_summary_label.setText("共 %d 张 · 成功 %d 张" % (len(jobs), success))
|
||||
self.undo_button.setVisible(bool(state.undo_records))
|
||||
self.history_button.setText("返回本轮" if state.show_history else "历史生成")
|
||||
|
||||
def open_history_dialog(self, checked=False):
|
||||
state = self._displayed_state
|
||||
if state is None or state.project_id is None:
|
||||
self._message(
|
||||
"未创建商品套图",
|
||||
"请先选择账号并添加本地图片,完成生成后再查看历史记录。",
|
||||
)
|
||||
return
|
||||
try:
|
||||
project = image_studio.get_project(state.project_id, path=self.db_path)
|
||||
rounds = image_studio.list_generation_rounds(
|
||||
state.project_id,
|
||||
limit=1,
|
||||
path=self.db_path,
|
||||
)
|
||||
except Exception as exc:
|
||||
self._message("读取历史生成失败", _user_error(exc))
|
||||
return
|
||||
if project is None:
|
||||
self._message("商品项目不存在", "当前商品套图项目已经不存在或已删除。")
|
||||
return
|
||||
if not rounds:
|
||||
self._message(
|
||||
"暂无历史生成记录",
|
||||
"完成套图生成后会自动出现在历史生成记录中。",
|
||||
)
|
||||
return
|
||||
|
||||
dialog = self._history_dialog
|
||||
if dialog is not None:
|
||||
try:
|
||||
if dialog.project_id == int(project.id):
|
||||
dialog.refresh_history()
|
||||
dialog.show()
|
||||
dialog.raise_()
|
||||
dialog.activateWindow()
|
||||
return
|
||||
dialog.close()
|
||||
except RuntimeError:
|
||||
pass
|
||||
self._history_dialog = None
|
||||
|
||||
dialog = ProductSuiteHistoryDialog(
|
||||
project.id,
|
||||
db_path=self.db_path,
|
||||
parent=self,
|
||||
)
|
||||
dialog.destroyed.connect(
|
||||
lambda _object=None, current=dialog: self._clear_history_dialog(current)
|
||||
)
|
||||
self._history_dialog = dialog
|
||||
dialog.show()
|
||||
dialog.raise_()
|
||||
dialog.activateWindow()
|
||||
|
||||
def _clear_history_dialog(self, dialog):
|
||||
if self._history_dialog is dialog:
|
||||
self._history_dialog = None
|
||||
|
||||
def _close_history_dialog_for_project(self, project_id=None):
|
||||
dialog = self._history_dialog
|
||||
if dialog is None:
|
||||
return
|
||||
try:
|
||||
if project_id is None or dialog.project_id == int(project_id):
|
||||
dialog.close()
|
||||
self._history_dialog = None
|
||||
except RuntimeError:
|
||||
self._history_dialog = None
|
||||
|
||||
def preview_job(self, job):
|
||||
asset = image_studio.get_asset(job.output_asset_id, path=self.db_path) if job.output_asset_id else None
|
||||
@@ -4017,6 +4518,7 @@ class ProductSuiteTab(QWidget):
|
||||
self._refresh_results(self._displayed_state)
|
||||
|
||||
def closeEvent(self, event):
|
||||
self._close_history_dialog_for_project()
|
||||
if self._displayed_state is not None:
|
||||
self._save_controls_to_state(self._displayed_state)
|
||||
for state in list(self._states.values()):
|
||||
|
||||
@@ -1259,6 +1259,7 @@ def list_generation_rounds(project_id, *, limit=None, offset=0, path=None, conn=
|
||||
WHERE project_id = ?
|
||||
GROUP BY generation_round_key
|
||||
ORDER BY CASE WHEN generation_round_key IS NULL THEN 1 ELSE 0 END,
|
||||
MIN(created_at) DESC,
|
||||
MAX(id) DESC
|
||||
"""
|
||||
params = [project_id]
|
||||
|
||||
Reference in New Issue
Block a user