feat: 完成T-302 AI生成页布局
新增GenerateTab和GenerateTaskTableModel,将② AI生成从占位页替换为左右布局:左侧标题/封面提示词多行输入,右侧批次、店铺、状态筛选和任务列表。 任务列表展示店铺、商品ID、旧标题、新标题、状态;筛选只读SQLite,不调用AI、不写库,生成执行留给T-303,提示词管理留给T-302p。 补充GUI测试覆盖Tab②挂载、提示词输入区、任务列表展示与批次/店铺/状态筛选;同步任务看板、API、routes、current-state和progress。
This commit is contained in:
+273
@@ -10,6 +10,7 @@ try:
|
||||
from PySide6.QtWidgets import (
|
||||
QAbstractItemView,
|
||||
QApplication,
|
||||
QComboBox,
|
||||
QDialog,
|
||||
QDialogButtonBox,
|
||||
QFileDialog,
|
||||
@@ -22,6 +23,7 @@ try:
|
||||
QMessageBox,
|
||||
QPlainTextEdit,
|
||||
QPushButton,
|
||||
QSplitter,
|
||||
QTableView,
|
||||
QSpinBox,
|
||||
QTableWidget,
|
||||
@@ -193,6 +195,271 @@ if QT_IMPORT_ERROR is None:
|
||||
return values[column] if 0 <= column < len(values) else None
|
||||
|
||||
|
||||
class GenerateTaskTableModel(QAbstractTableModel):
|
||||
"""Table model for Tab 2 generation candidates."""
|
||||
|
||||
HEADERS = ["店铺", "商品ID", "旧标题", "新标题", "状态"]
|
||||
|
||||
STATUS_TEXT = {
|
||||
"running": "处理中",
|
||||
"failed": "失败",
|
||||
"skipped": "略过",
|
||||
"cancelled": "已取消",
|
||||
}
|
||||
|
||||
STAGE_TEXT = {
|
||||
"imported": "未采集",
|
||||
"collected": "待生成",
|
||||
"generated": "已生成",
|
||||
"applied": "已更新",
|
||||
}
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.tasks = []
|
||||
self.account_by_alias = {}
|
||||
|
||||
def set_tasks(self, tasks, accounts):
|
||||
self.beginResetModel()
|
||||
self.tasks = list(tasks)
|
||||
self.account_by_alias = {
|
||||
str(account.alias).strip(): account
|
||||
for account in accounts
|
||||
if str(account.alias).strip()
|
||||
}
|
||||
self.endResetModel()
|
||||
|
||||
def rowCount(self, parent=QModelIndex()):
|
||||
return 0 if parent.isValid() else len(self.tasks)
|
||||
|
||||
def columnCount(self, parent=QModelIndex()):
|
||||
return 0 if parent.isValid() else len(self.HEADERS)
|
||||
|
||||
def headerData(self, section, orientation, role=Qt.DisplayRole):
|
||||
if role != Qt.DisplayRole:
|
||||
return None
|
||||
if orientation == Qt.Horizontal and 0 <= section < len(self.HEADERS):
|
||||
return self.HEADERS[section]
|
||||
return section + 1 if orientation == Qt.Vertical else None
|
||||
|
||||
def data(self, index, role=Qt.DisplayRole):
|
||||
if not index.isValid():
|
||||
return None
|
||||
task = self.tasks[index.row()]
|
||||
if role == Qt.DisplayRole:
|
||||
return self._display_value(task, index.column())
|
||||
if role == Qt.ToolTipRole and task.last_error:
|
||||
return task.last_error
|
||||
return None
|
||||
|
||||
def flags(self, index):
|
||||
if not index.isValid():
|
||||
return Qt.NoItemFlags
|
||||
return Qt.ItemIsEnabled | Qt.ItemIsSelectable
|
||||
|
||||
def _account_name(self, task):
|
||||
account = self.account_by_alias.get(str(task.alias).strip())
|
||||
if account is not None:
|
||||
return account.account_name
|
||||
return task.account_name or task.alias or ""
|
||||
|
||||
def _status_text(self, task):
|
||||
if task.status in self.STATUS_TEXT and task.status != "pending":
|
||||
return self.STATUS_TEXT[task.status]
|
||||
return self.STAGE_TEXT.get(task.stage, task.stage)
|
||||
|
||||
def _display_value(self, task, column):
|
||||
values = [
|
||||
self._account_name(task),
|
||||
task.item_id,
|
||||
task.old_title or "",
|
||||
task.new_title or "",
|
||||
self._status_text(task),
|
||||
]
|
||||
return values[column] if 0 <= column < len(values) else None
|
||||
|
||||
|
||||
class GenerateTab(QWidget):
|
||||
"""Tab 2: prompt area plus generation task filters/list."""
|
||||
|
||||
STATUS_FILTERS = [
|
||||
("全部状态", "all"),
|
||||
("待生成", "to_generate"),
|
||||
("已生成", "generated"),
|
||||
("失败", "failed"),
|
||||
("略过", "skipped"),
|
||||
("已更新", "applied"),
|
||||
]
|
||||
|
||||
def __init__(self, parent=None, db_path=None, config=None, status_callback=None):
|
||||
super().__init__(parent)
|
||||
self.config = appconfig.load_config() if config is None else config
|
||||
self.db_path = _database_path(db_path, self.config)
|
||||
self.status_callback = status_callback
|
||||
|
||||
self.title_prompt_edit = QPlainTextEdit()
|
||||
self.title_prompt_edit.setObjectName("titlePromptEdit")
|
||||
self.title_prompt_edit.setPlaceholderText("标题提示词")
|
||||
self.cover_prompt_edit = QPlainTextEdit()
|
||||
self.cover_prompt_edit.setObjectName("coverPromptEdit")
|
||||
self.cover_prompt_edit.setPlaceholderText("封面提示词")
|
||||
|
||||
left_panel = QWidget()
|
||||
left_layout = QVBoxLayout(left_panel)
|
||||
left_layout.setContentsMargins(0, 0, 12, 0)
|
||||
left_layout.addWidget(QLabel("标题提示词"))
|
||||
left_layout.addWidget(self.title_prompt_edit, 1)
|
||||
left_layout.addWidget(QLabel("封面提示词"))
|
||||
left_layout.addWidget(self.cover_prompt_edit, 2)
|
||||
|
||||
self.batch_filter = QComboBox()
|
||||
self.batch_filter.setObjectName("batchFilter")
|
||||
self.shop_filter = QComboBox()
|
||||
self.shop_filter.setObjectName("shopFilter")
|
||||
self.status_filter = QComboBox()
|
||||
self.status_filter.setObjectName("statusFilter")
|
||||
for label, value in self.STATUS_FILTERS:
|
||||
self.status_filter.addItem(label, value)
|
||||
self.refresh_button = QPushButton("刷新")
|
||||
|
||||
filter_layout = QHBoxLayout()
|
||||
filter_layout.addWidget(QLabel("批次"))
|
||||
filter_layout.addWidget(self.batch_filter, 2)
|
||||
filter_layout.addWidget(QLabel("店铺"))
|
||||
filter_layout.addWidget(self.shop_filter, 1)
|
||||
filter_layout.addWidget(QLabel("状态"))
|
||||
filter_layout.addWidget(self.status_filter, 1)
|
||||
filter_layout.addWidget(self.refresh_button)
|
||||
|
||||
self.summary_label = QLabel("任务 0 条")
|
||||
self.task_table = QTableView()
|
||||
self.model = GenerateTaskTableModel(self.task_table)
|
||||
self.task_table.setModel(self.model)
|
||||
self.task_table.setSelectionBehavior(QAbstractItemView.SelectRows)
|
||||
self.task_table.setSelectionMode(QAbstractItemView.SingleSelection)
|
||||
self.task_table.setEditTriggers(QAbstractItemView.NoEditTriggers)
|
||||
self.task_table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
|
||||
self.task_table.verticalHeader().setVisible(False)
|
||||
|
||||
right_panel = QWidget()
|
||||
right_layout = QVBoxLayout(right_panel)
|
||||
right_layout.setContentsMargins(12, 0, 0, 0)
|
||||
right_layout.addLayout(filter_layout)
|
||||
right_layout.addWidget(self.summary_label)
|
||||
right_layout.addWidget(self.task_table, 1)
|
||||
|
||||
self.splitter = QSplitter(Qt.Horizontal)
|
||||
self.splitter.addWidget(left_panel)
|
||||
self.splitter.addWidget(right_panel)
|
||||
self.splitter.setStretchFactor(0, 1)
|
||||
self.splitter.setStretchFactor(1, 3)
|
||||
self.splitter.setSizes([280, 860])
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setContentsMargins(18, 18, 18, 18)
|
||||
layout.addWidget(self.splitter, 1)
|
||||
|
||||
self.batch_filter.currentIndexChanged.connect(self.refresh_tasks)
|
||||
self.shop_filter.currentIndexChanged.connect(self.refresh_tasks)
|
||||
self.status_filter.currentIndexChanged.connect(self.refresh_tasks)
|
||||
self.refresh_button.clicked.connect(self.refresh_tasks)
|
||||
|
||||
self.refresh_tasks()
|
||||
|
||||
def _set_status(self, message):
|
||||
if self.status_callback is not None:
|
||||
self.status_callback(message)
|
||||
|
||||
def refresh_tasks(self, checked=False):
|
||||
try:
|
||||
db.init_db(self.db_path)
|
||||
batches = db.list_batches(path=self.db_path)
|
||||
accounts_rows = accounts.list_accounts(path=self.db_path, config=self.config)
|
||||
selected_batch = self.batch_filter.currentData()
|
||||
selected_shop = self.shop_filter.currentData()
|
||||
selected_status = self.status_filter.currentData() or "all"
|
||||
self._populate_batch_filter(batches, selected_batch)
|
||||
selected_batch = self.batch_filter.currentData()
|
||||
batch_tasks = db.list_tasks(batch_id=selected_batch, path=self.db_path)
|
||||
self._populate_shop_filter(batch_tasks, accounts_rows, selected_shop)
|
||||
selected_shop = self.shop_filter.currentData()
|
||||
filtered_tasks = [
|
||||
task for task in batch_tasks
|
||||
if self._matches_shop(task, selected_shop)
|
||||
and self._matches_status(task, selected_status)
|
||||
]
|
||||
except Exception as exc:
|
||||
self.model.set_tasks([], [])
|
||||
self.summary_label.setText("任务读取失败")
|
||||
self._set_status(f"AI 生成任务读取失败:{exc}")
|
||||
return
|
||||
self.model.set_tasks(filtered_tasks, accounts_rows)
|
||||
self.summary_label.setText(
|
||||
f"任务 {len(filtered_tasks)}/{len(batch_tasks)} 条"
|
||||
)
|
||||
|
||||
def _populate_batch_filter(self, batches, selected_batch):
|
||||
previous = selected_batch if selected_batch in {batch.id for batch in batches} else None
|
||||
self.batch_filter.blockSignals(True)
|
||||
self.batch_filter.clear()
|
||||
self.batch_filter.addItem("全部批次", None)
|
||||
for batch in batches:
|
||||
self.batch_filter.addItem(self._batch_label(batch), batch.id)
|
||||
index = self.batch_filter.findData(previous)
|
||||
self.batch_filter.setCurrentIndex(index if index >= 0 else 0)
|
||||
self.batch_filter.blockSignals(False)
|
||||
|
||||
def _populate_shop_filter(self, tasks, account_rows, selected_shop):
|
||||
account_by_alias = {
|
||||
str(account.alias).strip(): account
|
||||
for account in account_rows
|
||||
if str(account.alias).strip()
|
||||
}
|
||||
aliases = []
|
||||
for task in tasks:
|
||||
alias = str(task.alias).strip()
|
||||
if alias and alias not in aliases:
|
||||
aliases.append(alias)
|
||||
previous = selected_shop if selected_shop in aliases else None
|
||||
self.shop_filter.blockSignals(True)
|
||||
self.shop_filter.clear()
|
||||
self.shop_filter.addItem("全部店铺", None)
|
||||
for alias in sorted(aliases, key=lambda value: self._shop_label(value, account_by_alias)):
|
||||
self.shop_filter.addItem(self._shop_label(alias, account_by_alias), alias)
|
||||
index = self.shop_filter.findData(previous)
|
||||
self.shop_filter.setCurrentIndex(index if index >= 0 else 0)
|
||||
self.shop_filter.blockSignals(False)
|
||||
|
||||
def _batch_label(self, batch):
|
||||
source_files = batch.source_files
|
||||
first_file = os.path.basename(source_files[0]) if source_files else batch.id
|
||||
return f"{batch.created_at} · {first_file}"
|
||||
|
||||
def _shop_label(self, alias, account_by_alias):
|
||||
account = account_by_alias.get(alias)
|
||||
if account is not None:
|
||||
return f"{account.account_name} ({alias})"
|
||||
return alias
|
||||
|
||||
def _matches_shop(self, task, selected_shop):
|
||||
return selected_shop is None or str(task.alias).strip() == selected_shop
|
||||
|
||||
def _matches_status(self, task, selected_status):
|
||||
if selected_status in (None, "all"):
|
||||
return True
|
||||
if selected_status == "to_generate":
|
||||
return task.stage == "collected" and task.status in {"success", "pending"}
|
||||
if selected_status == "generated":
|
||||
return task.stage == "generated"
|
||||
if selected_status == "applied":
|
||||
return task.stage == "applied"
|
||||
if selected_status == "failed":
|
||||
return task.status == "failed"
|
||||
if selected_status == "skipped":
|
||||
return task.status == "skipped"
|
||||
return True
|
||||
|
||||
|
||||
class CollectTab(QWidget):
|
||||
"""Tab 1: import Excel files and list imported tasks."""
|
||||
|
||||
@@ -1201,6 +1468,12 @@ if QT_IMPORT_ERROR is None:
|
||||
status_callback=self.statusBar().showMessage,
|
||||
open_accounts_callback=lambda: self.open_accounts_tab(),
|
||||
)
|
||||
if title == "② AI生成":
|
||||
return GenerateTab(
|
||||
db_path=self.db_path,
|
||||
config=self.config,
|
||||
status_callback=self.statusBar().showMessage,
|
||||
)
|
||||
if title == "④ 账号管理":
|
||||
return AccountsTab(
|
||||
db_path=self.db_path,
|
||||
|
||||
Reference in New Issue
Block a user