feat: 完成Tab①任务导入列表
实现 CollectTab 与 TaskTableModel,Tab① 接入导入 Excel 按钮、QTableView 任务列表和未匹配别名略过标记。 新增 GUI 测试覆盖 Tab① 接入、任务展示、未匹配标记和导入刷新;同步任务状态、API 合约、当前状态与 progress。 加入标准空模板 shopee待处理任务模板.xlsx,并更新 ignore/文档规则:模板可提交,运营填写后的 Excel 业务文件默认忽略。
This commit is contained in:
+210
-2
@@ -6,11 +6,13 @@ import os
|
||||
import sys
|
||||
|
||||
try:
|
||||
from PySide6.QtCore import QAbstractTableModel, QModelIndex, Qt
|
||||
from PySide6.QtWidgets import (
|
||||
QAbstractItemView,
|
||||
QApplication,
|
||||
QDialog,
|
||||
QDialogButtonBox,
|
||||
QFileDialog,
|
||||
QFormLayout,
|
||||
QHBoxLayout,
|
||||
QHeaderView,
|
||||
@@ -20,6 +22,7 @@ try:
|
||||
QMessageBox,
|
||||
QPlainTextEdit,
|
||||
QPushButton,
|
||||
QTableView,
|
||||
QSpinBox,
|
||||
QTableWidget,
|
||||
QTableWidgetItem,
|
||||
@@ -72,10 +75,209 @@ QTabBar::tab:hover:!selected {
|
||||
|
||||
|
||||
if QT_IMPORT_ERROR is None:
|
||||
from . import accounts, appconfig
|
||||
from . import accounts, appconfig, db, excel
|
||||
from . import config as account_config
|
||||
|
||||
|
||||
def _database_path(db_path=None, config=None) -> str:
|
||||
return db_path or appconfig.db_path(config)
|
||||
|
||||
|
||||
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": "已取消",
|
||||
}
|
||||
|
||||
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 self.is_unmatched(task):
|
||||
return "别名未匹配账号,采集时将略过"
|
||||
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.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 _stage_text(self, task) -> str:
|
||||
if self.is_unmatched(task):
|
||||
return "略过"
|
||||
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.alias,
|
||||
task.item_id,
|
||||
self._stage_text(task),
|
||||
]
|
||||
return values[column] if 0 <= column < len(values) else None
|
||||
|
||||
|
||||
class CollectTab(QWidget):
|
||||
"""Tab 1: import Excel files and list imported tasks."""
|
||||
|
||||
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.current_batch_id = None
|
||||
|
||||
self.import_button = QPushButton("导入 Excel...")
|
||||
self.refresh_button = QPushButton("刷新")
|
||||
|
||||
toolbar = QHBoxLayout()
|
||||
toolbar.addWidget(self.import_button)
|
||||
toolbar.addWidget(self.refresh_button)
|
||||
toolbar.addStretch(1)
|
||||
|
||||
self.model = TaskTableModel(self)
|
||||
self.table = QTableView()
|
||||
self.table.setModel(self.model)
|
||||
self.table.setSelectionBehavior(QAbstractItemView.SelectRows)
|
||||
self.table.setSelectionMode(QAbstractItemView.SingleSelection)
|
||||
self.table.setEditTriggers(QAbstractItemView.NoEditTriggers)
|
||||
self.table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
|
||||
self.table.verticalHeader().setVisible(False)
|
||||
|
||||
self.empty_label = QLabel("")
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setContentsMargins(18, 18, 18, 18)
|
||||
layout.addLayout(toolbar)
|
||||
layout.addWidget(self.table, 1)
|
||||
layout.addWidget(self.empty_label)
|
||||
|
||||
self.import_button.clicked.connect(self.import_excel)
|
||||
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 _show_error(self, message):
|
||||
QMessageBox.warning(self, "导入采集", str(message))
|
||||
self._set_status(str(message))
|
||||
|
||||
def _choose_excel_files(self):
|
||||
files, _selected_filter = QFileDialog.getOpenFileNames(
|
||||
self,
|
||||
"选择 Excel 文件",
|
||||
"",
|
||||
"Excel 文件 (*.xlsx *.xlsm)",
|
||||
)
|
||||
return files
|
||||
|
||||
def import_excel(self, checked=False):
|
||||
file_paths = self._choose_excel_files()
|
||||
if not file_paths:
|
||||
return
|
||||
try:
|
||||
result = excel.import_tasks(file_paths, path=self.db_path)
|
||||
except Exception as exc:
|
||||
self._show_error(exc)
|
||||
return
|
||||
if result.get("batch_id"):
|
||||
self.current_batch_id = result["batch_id"]
|
||||
self.refresh_tasks()
|
||||
stats = result.get("stats") or {}
|
||||
self._set_status(
|
||||
"导入完成:有效{valid},无效{invalid},入库{inserted},未匹配{unmatched}".format(
|
||||
valid=stats.get("valid", 0),
|
||||
invalid=stats.get("invalid", 0),
|
||||
inserted=stats.get("inserted", 0),
|
||||
unmatched=self.model.unmatched_count(),
|
||||
)
|
||||
)
|
||||
|
||||
def refresh_tasks(self, checked=False):
|
||||
try:
|
||||
db.init_db(self.db_path)
|
||||
task_rows = db.list_tasks(batch_id=self.current_batch_id, path=self.db_path)
|
||||
account_rows = accounts.list_accounts(path=self.db_path, config=self.config)
|
||||
except Exception as exc:
|
||||
self.model.set_tasks([], [])
|
||||
self.empty_label.setText("任务读取失败")
|
||||
self._set_status(f"任务读取失败:{exc}")
|
||||
return
|
||||
self.model.set_tasks(task_rows, account_rows)
|
||||
if task_rows:
|
||||
unmatched = self.model.unmatched_count()
|
||||
self.empty_label.setText(
|
||||
"" if unmatched == 0 else f"{unmatched} 条任务别名未匹配账号,阶段显示为“略过”"
|
||||
)
|
||||
else:
|
||||
self.empty_label.setText("暂无任务")
|
||||
|
||||
|
||||
class AccountDialog(QDialog):
|
||||
"""Dialog for adding or editing one account."""
|
||||
|
||||
@@ -441,8 +643,8 @@ if QT_IMPORT_ERROR is None:
|
||||
|
||||
def __init__(self, db_path=None, config=None):
|
||||
super().__init__()
|
||||
self.db_path = db_path
|
||||
self.config = appconfig.load_config() if config is None else config
|
||||
self.db_path = _database_path(db_path, self.config)
|
||||
self.setWindowTitle("cmshopee")
|
||||
self.resize(1180, 760)
|
||||
self.tabs = QTabWidget()
|
||||
@@ -455,6 +657,12 @@ if QT_IMPORT_ERROR is None:
|
||||
self.statusBar().showMessage("就绪")
|
||||
|
||||
def _build_tab(self, title):
|
||||
if title == "① 导入采集":
|
||||
return CollectTab(
|
||||
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