Files
cmshoppe/app/gui/models.py
T

432 lines
15 KiB
Python
Raw Normal View History

2026-07-02 16:47:37 +08:00
"""Task table models for GUI tabs."""
from __future__ import annotations
from .widgets import *
class TaskTableModel(QAbstractTableModel):
"""Table model for task rows shared by workflow tabs."""
HEADERS = ["账号", "别名", "商品ID", "阶段"]
STAGE_TEXT = {
"imported": "待采集",
"collected": "已采集",
"generated": "已生成",
"applied": "已更新",
}
STATUS_TEXT = {
"running": "处理中",
"failed": "失败",
"skipped": "略过",
"cancelled": "已取消",
}
PRODUCT_UNAVAILABLE_MARKERS = (
"商品失效",
"please input correct product id",
"商品不存在",
"商品已删除",
"商品已下架",
"無效商品",
"无效商品",
"無權限",
"无权限",
"product not found",
"item not found",
"invalid product",
"invalid item",
)
2026-07-02 16:47:37 +08:00
def __init__(self, parent=None):
super().__init__(parent)
self.tasks = []
self.all_tasks = []
self.account_by_alias = {}
self.filter_mode = "all"
def set_tasks(self, tasks, accounts):
self.beginResetModel()
self.all_tasks = list(tasks)
self.account_by_alias = {
str(account.alias).strip(): account
for account in accounts
if str(account.alias).strip()
}
self.tasks = self._filtered_tasks()
self.endResetModel()
def set_filter_mode(self, mode):
self.beginResetModel()
self.filter_mode = mode if mode in {"all", "unmatched"} else "all"
self.tasks = self._filtered_tasks()
self.endResetModel()
def _filtered_tasks(self):
if self.filter_mode == "unmatched":
return [task for task in self.all_tasks if self.is_unmatched(task)]
return list(self.all_tasks)
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.ForegroundRole and index.column() == 3:
return self._stage_color(task)
if role == Qt.ToolTipRole:
if self.is_unmatched(task):
return "别名未匹配账号,采集时将略过"
error = getattr(task, "last_error", "") or ""
if error:
return str(error)
2026-07-02 16:47:37 +08:00
return None
def flags(self, index):
if not index.isValid():
return Qt.NoItemFlags
return Qt.ItemIsEnabled | Qt.ItemIsSelectable
def task_at(self, row):
if row < 0 or row >= len(self.tasks):
return None
return self.tasks[row]
def is_unmatched(self, task) -> bool:
return str(task.alias).strip() not in self.account_by_alias
def unmatched_count(self) -> int:
return sum(1 for task in self.all_tasks if self.is_unmatched(task))
def _account_name(self, task) -> str:
account = self.account_by_alias.get(str(task.alias).strip())
if account is not None:
return account.account_name
return task.account_name or ""
def _is_product_unavailable(self, task) -> bool:
if getattr(task, "status", "") != "failed":
return False
error = str(getattr(task, "last_error", "") or "").lower()
return bool(error) and any(marker.lower() in error for marker in self.PRODUCT_UNAVAILABLE_MARKERS)
2026-07-02 16:47:37 +08:00
def _stage_text(self, task) -> str:
if self.is_unmatched(task):
return "略过"
if self._is_product_unavailable(task):
return "商品失效"
2026-07-02 16:47:37 +08:00
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 _stage_color(self, task):
if self.is_unmatched(task):
return _qcolor(COLOR_MUTED)
base_color = _status_base_color(getattr(task, "status", None))
if base_color is not None:
return _qcolor(base_color)
if getattr(task, "stage", None) in {"collected", "generated", "applied"}:
return _qcolor(COLOR_SUCCESS)
return _qcolor(COLOR_PENDING)
def _display_value(self, task, column):
values = [
self._account_name(task),
task.alias,
task.item_id,
self._stage_text(task),
]
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, db_path=None, status_callback=None):
super().__init__(parent)
self.tasks = []
self.account_by_alias = {}
self.db_path = db_path
self.status_callback = status_callback
self.last_edit_error = None
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 in (Qt.DisplayRole, Qt.EditRole):
return self._display_value(task, index.column())
if role == Qt.ForegroundRole and index.column() == 4:
return self._status_color(task)
if role == Qt.ToolTipRole:
if index.column() == 3 and self._can_edit_title(task):
return "双击可微调新标题,只修改本地待更新内容"
if task.last_error:
return task.last_error
return None
def setData(self, index, value, role=Qt.EditRole):
if role != Qt.EditRole or not index.isValid() or index.column() != 3:
return False
task = self.tasks[index.row()]
if not self._can_edit_title(task):
self._set_status("该任务不能修改新标题")
return False
title = str(value or "").strip()
if title == str(task.new_title or ""):
return True
try:
db.update_generated_title(task.id, title, path=self.db_path)
updated = db.get_task(task.id, path=self.db_path)
except Exception as exc:
self.last_edit_error = str(exc)
self._set_status(f"新标题修改失败:{exc}")
return False
self.tasks[index.row()] = updated
self.last_edit_error = None
self.dataChanged.emit(index, index, [Qt.DisplayRole, Qt.EditRole, Qt.ToolTipRole])
self._set_status(f"已修改新标题:商品 {task.item_id}")
return True
def flags(self, index):
if not index.isValid():
return Qt.NoItemFlags
flags = Qt.ItemIsEnabled | Qt.ItemIsSelectable
if index.column() == 3 and self._can_edit_title(self.tasks[index.row()]):
flags |= Qt.ItemIsEditable
return flags
def _can_edit_title(self, task):
return (
getattr(task, "stage", None) == "generated"
and getattr(task, "status", None) != "running"
and int(getattr(task, "committed", 0) or 0) == 0
and bool(getattr(task, "new_title", None))
)
2026-07-07 15:42:19 +08:00
def _set_status(self, message, level=None):
_emit_status(self.status_callback, message, level=level)
2026-07-02 16:47:37 +08:00
def task_at(self, row):
if row < 0 or row >= len(self.tasks):
return None
return self.tasks[row]
def account_name_for(self, task):
return self._account_name(task)
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 == "failed":
return self._failed_status_text(task)
2026-07-02 16:47:37 +08:00
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 _failed_status_text(self, task):
collect_attempts = self._attempt_count(task, "collect_attempts")
generate_attempts = self._attempt_count(task, "generate_attempts")
apply_attempts = self._attempt_count(task, "apply_attempts")
if apply_attempts > 0:
return "更新失败"
if generate_attempts > 0:
return "生成失败"
if collect_attempts > 0 or getattr(task, "stage", None) == "imported":
return "采集失败"
return "失败"
def _attempt_count(self, task, field):
try:
return int(getattr(task, field, 0) or 0)
except (TypeError, ValueError):
return 0
2026-07-02 16:47:37 +08:00
def _status_color(self, task):
base_color = _status_base_color(getattr(task, "status", None))
if base_color is not None:
return _qcolor(base_color)
if getattr(task, "stage", None) in {"generated", "applied"}:
return _qcolor(COLOR_SUCCESS)
return _qcolor(COLOR_PENDING)
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 ApplyTaskTableModel(QAbstractTableModel):
"""Table model for Tab 3 update candidates."""
HEADERS = ["店铺", "商品ID", "新标题", "新封面", "阶段", "结果"]
STATUS_TEXT = {
"running": "处理中",
"failed": "失败",
"skipped": "略过",
"cancelled": "已取消",
"pending": "待更新",
"success": "成功",
}
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.ForegroundRole:
if index.column() == 4:
return self._stage_color(task)
if index.column() == 5:
return self._result_color(task)
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 task_at(self, row):
if row < 0 or row >= len(self.tasks):
return None
return self.tasks[row]
def account_name_for(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 _display_value(self, task, column):
values = [
self.account_name_for(task),
task.item_id,
task.new_title or "",
os.path.basename(task.new_cover_path or ""),
self._stage_text(task),
self._result_text(task),
]
return values[column] if 0 <= column < len(values) else None
def _stage_text(self, task):
if task.stage == "generated":
return "待更新"
if task.stage == "applied":
return "已更新"
return task.stage
def _result_text(self, task):
if task.status == "success" and task.stage == "generated":
return "待更新"
return self.STATUS_TEXT.get(task.status, task.status)
def _stage_color(self, task):
if getattr(task, "stage", None) == "applied":
return _qcolor(COLOR_SUCCESS)
return _qcolor(COLOR_PENDING)
def _result_color(self, task):
base_color = _status_base_color(getattr(task, "status", None))
if base_color is not None:
return _qcolor(base_color)
if getattr(task, "status", None) == "pending":
return _qcolor(COLOR_PENDING)
if getattr(task, "status", None) == "success" and getattr(task, "stage", None) == "generated":
return _qcolor(COLOR_PENDING)
if getattr(task, "stage", None) == "applied" or getattr(task, "status", None) == "success":
return _qcolor(COLOR_SUCCESS)
return _qcolor(COLOR_PENDING)