feat(client): connect PDD task list to SQLite (#9)
This commit is contained in:
+68
-89
@@ -78,7 +78,9 @@ class TaskRow:
|
|||||||
|
|
||||||
|
|
||||||
class TaskTableModel(QAbstractTableModel):
|
class TaskTableModel(QAbstractTableModel):
|
||||||
"""PDD 任务表格模型,当前使用内存数据模拟 Repository 查询结果。"""
|
"""PDD 任务表格模型,只保存已经从数据库读取的轻量行。"""
|
||||||
|
|
||||||
|
loadMoreRequested = pyqtSignal(int, int)
|
||||||
|
|
||||||
HEADERS = (
|
HEADERS = (
|
||||||
"任务类型",
|
"任务类型",
|
||||||
@@ -94,14 +96,9 @@ class TaskTableModel(QAbstractTableModel):
|
|||||||
|
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self._source_rows: list[TaskRow] = []
|
|
||||||
self._filtered_rows: list[TaskRow] = []
|
|
||||||
self._rows: list[TaskRow] = []
|
self._rows: list[TaskRow] = []
|
||||||
self._task_type = "全部"
|
self._has_more = False
|
||||||
self._status = "全部"
|
self._loading = False
|
||||||
self._keyword = ""
|
|
||||||
self._sort_column = 7
|
|
||||||
self._sort_order = Qt.DescendingOrder
|
|
||||||
self._placeholder_row_count = 1
|
self._placeholder_row_count = 1
|
||||||
|
|
||||||
def rowCount(self, parent=QModelIndex()) -> int:
|
def rowCount(self, parent=QModelIndex()) -> int:
|
||||||
@@ -151,54 +148,66 @@ class TaskTableModel(QAbstractTableModel):
|
|||||||
return Qt.ItemIsEnabled | Qt.ItemIsSelectable
|
return Qt.ItemIsEnabled | Qt.ItemIsSelectable
|
||||||
|
|
||||||
def canFetchMore(self, parent=QModelIndex()) -> bool:
|
def canFetchMore(self, parent=QModelIndex()) -> bool:
|
||||||
"""告诉 Qt 内存结果中是否还有下一页。"""
|
"""告诉 Qt 数据库中是否还有下一页。"""
|
||||||
|
|
||||||
return not parent.isValid() and len(self._rows) < len(self._filtered_rows)
|
return not parent.isValid() and self._has_more and not self._loading
|
||||||
|
|
||||||
def fetchMore(self, parent=QModelIndex()) -> None:
|
def fetchMore(self, parent=QModelIndex()) -> None:
|
||||||
"""每次再展示最多 50 行,模拟后续 Repository 的分页行为。"""
|
"""请求事件层从数据库读取下一页。"""
|
||||||
|
|
||||||
if parent.isValid() or not self.canFetchMore(parent):
|
if parent.isValid() or not self.canFetchMore(parent):
|
||||||
return
|
return
|
||||||
|
|
||||||
start = len(self._rows)
|
self._loading = True
|
||||||
end = min(start + PAGE_SIZE, len(self._filtered_rows))
|
self.loadMoreRequested.emit(len(self._rows), PAGE_SIZE)
|
||||||
self.beginInsertRows(QModelIndex(), start, end - 1)
|
|
||||||
self._rows.extend(self._filtered_rows[start:end])
|
|
||||||
self.endInsertRows()
|
|
||||||
|
|
||||||
def sort(self, column, order=Qt.AscendingOrder) -> None:
|
def sort(self, column, order=Qt.AscendingOrder) -> None:
|
||||||
if not 0 <= column < len(self.HEADERS):
|
"""列表顺序由 Repository 固定为更新时间倒序。"""
|
||||||
return
|
|
||||||
|
|
||||||
loaded_count = max(PAGE_SIZE, len(self._rows))
|
|
||||||
self.beginResetModel()
|
|
||||||
self._sort_column = column
|
|
||||||
self._sort_order = order
|
|
||||||
self._sort_filtered_rows()
|
|
||||||
self._rows = self._filtered_rows[:loaded_count]
|
|
||||||
self.endResetModel()
|
|
||||||
|
|
||||||
def set_tasks(self, tasks: Iterable[TaskRow]) -> None:
|
def set_tasks(self, tasks: Iterable[TaskRow]) -> None:
|
||||||
"""替换内存任务列表;后续由 Repository 查询结果调用。"""
|
"""替换全部任务;保留给简单测试和静态预览使用。"""
|
||||||
|
|
||||||
self.beginResetModel()
|
self.beginResetModel()
|
||||||
self._source_rows = list(tasks)
|
self._rows = list(tasks)
|
||||||
self._rebuild_filtered_rows()
|
self._has_more = False
|
||||||
self._rows = self._filtered_rows[:PAGE_SIZE]
|
self._loading = False
|
||||||
self.endResetModel()
|
self.endResetModel()
|
||||||
|
|
||||||
def apply_filters(self, task_type: str, status: str, keyword: str) -> None:
|
def reset_for_reload(self) -> None:
|
||||||
"""按类型、状态和关键词同时筛选当前内存任务。"""
|
"""清空旧结果,准备从数据库重新读取第一页。"""
|
||||||
|
|
||||||
self.beginResetModel()
|
self.beginResetModel()
|
||||||
self._task_type = task_type
|
self._rows = []
|
||||||
self._status = status
|
self._has_more = True
|
||||||
self._keyword = keyword.strip().casefold()
|
self._loading = False
|
||||||
self._rebuild_filtered_rows()
|
|
||||||
self._rows = self._filtered_rows[:PAGE_SIZE]
|
|
||||||
self.endResetModel()
|
self.endResetModel()
|
||||||
|
|
||||||
|
def append_tasks(self, tasks: Iterable[TaskRow], has_more: bool) -> None:
|
||||||
|
"""追加一页任务,并结束本次加载状态。"""
|
||||||
|
|
||||||
|
page = list(tasks)
|
||||||
|
if not self._rows:
|
||||||
|
self.beginResetModel()
|
||||||
|
self._rows = page
|
||||||
|
self._has_more = has_more
|
||||||
|
self._loading = False
|
||||||
|
self.endResetModel()
|
||||||
|
return
|
||||||
|
|
||||||
|
if page:
|
||||||
|
start = len(self._rows)
|
||||||
|
self.beginInsertRows(QModelIndex(), start, start + len(page) - 1)
|
||||||
|
self._rows.extend(page)
|
||||||
|
self.endInsertRows()
|
||||||
|
self._has_more = has_more
|
||||||
|
self._loading = False
|
||||||
|
|
||||||
|
def finish_loading(self) -> None:
|
||||||
|
"""查询失败时停止继续请求,等待用户刷新或重新搜索。"""
|
||||||
|
|
||||||
|
self._has_more = False
|
||||||
|
self._loading = False
|
||||||
|
|
||||||
def row_at(self, row: int) -> Optional[TaskRow]:
|
def row_at(self, row: int) -> Optional[TaskRow]:
|
||||||
"""根据当前可见行获取任务,越界时返回 None。"""
|
"""根据当前可见行获取任务,越界时返回 None。"""
|
||||||
|
|
||||||
@@ -215,7 +224,7 @@ class TaskTableModel(QAbstractTableModel):
|
|||||||
return -1
|
return -1
|
||||||
|
|
||||||
def filtered_count(self) -> int:
|
def filtered_count(self) -> int:
|
||||||
return len(self._filtered_rows)
|
return len(self._rows)
|
||||||
|
|
||||||
def data_row_count(self) -> int:
|
def data_row_count(self) -> int:
|
||||||
"""返回真实任务行数,不包含填满空表格的占位行。"""
|
"""返回真实任务行数,不包含填满空表格的占位行。"""
|
||||||
@@ -237,48 +246,6 @@ class TaskTableModel(QAbstractTableModel):
|
|||||||
self._placeholder_row_count = count
|
self._placeholder_row_count = count
|
||||||
self.endResetModel()
|
self.endResetModel()
|
||||||
|
|
||||||
def _rebuild_filtered_rows(self) -> None:
|
|
||||||
self._filtered_rows = [
|
|
||||||
task for task in self._source_rows if self._matches_filters(task)
|
|
||||||
]
|
|
||||||
self._sort_filtered_rows()
|
|
||||||
|
|
||||||
def _matches_filters(self, task: TaskRow) -> bool:
|
|
||||||
if self._task_type != "全部" and task.task_type != self._task_type:
|
|
||||||
return False
|
|
||||||
if self._status != "全部" and task.status != self._status:
|
|
||||||
return False
|
|
||||||
if not self._keyword:
|
|
||||||
return True
|
|
||||||
|
|
||||||
searchable_text = " ".join(
|
|
||||||
(task.remote_task_id, task.goods_id, task.title)
|
|
||||||
).casefold()
|
|
||||||
return self._keyword in searchable_text
|
|
||||||
|
|
||||||
def _sort_filtered_rows(self) -> None:
|
|
||||||
reverse = self._sort_order == Qt.DescendingOrder
|
|
||||||
self._filtered_rows.sort(
|
|
||||||
key=lambda task: self._sort_value(task, self._sort_column),
|
|
||||||
reverse=reverse,
|
|
||||||
)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _sort_value(task: TaskRow, column: int):
|
|
||||||
values = (
|
|
||||||
task.task_type,
|
|
||||||
task.title,
|
|
||||||
task.color,
|
|
||||||
task.size,
|
|
||||||
task.price_cents if task.price_cents is not None else -1,
|
|
||||||
task.quantity if task.quantity is not None else -1,
|
|
||||||
task.status,
|
|
||||||
task.updated_at,
|
|
||||||
task.remote_task_id,
|
|
||||||
)
|
|
||||||
value = values[column]
|
|
||||||
return value.casefold() if isinstance(value, str) else value
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _display_value(task: TaskRow, column: int) -> str:
|
def _display_value(task: TaskRow, column: int) -> str:
|
||||||
if column == 0:
|
if column == 0:
|
||||||
@@ -381,7 +348,7 @@ class PDDTaskPage(QWidget):
|
|||||||
self.taskTable.setEditTriggers(QAbstractItemView.NoEditTriggers)
|
self.taskTable.setEditTriggers(QAbstractItemView.NoEditTriggers)
|
||||||
self.taskTable.setAlternatingRowColors(True)
|
self.taskTable.setAlternatingRowColors(True)
|
||||||
self.taskTable.setWordWrap(False)
|
self.taskTable.setWordWrap(False)
|
||||||
self.taskTable.setSortingEnabled(True)
|
self.taskTable.setSortingEnabled(False)
|
||||||
self.taskTable.verticalHeader().setVisible(False)
|
self.taskTable.verticalHeader().setVisible(False)
|
||||||
|
|
||||||
header = self.taskTable.horizontalHeader()
|
header = self.taskTable.horizontalHeader()
|
||||||
@@ -396,7 +363,6 @@ class PDDTaskPage(QWidget):
|
|||||||
self.taskTable.setColumnWidth(6, 116)
|
self.taskTable.setColumnWidth(6, 116)
|
||||||
self.taskTable.setColumnWidth(7, 156)
|
self.taskTable.setColumnWidth(7, 156)
|
||||||
self.taskTable.setColumnWidth(8, 72)
|
self.taskTable.setColumnWidth(8, 72)
|
||||||
self.taskTable.sortByColumn(7, Qt.DescendingOrder)
|
|
||||||
self.taskTable.viewport().installEventFilter(self)
|
self.taskTable.viewport().installEventFilter(self)
|
||||||
|
|
||||||
self.emptyStateCard = CardWidget(self)
|
self.emptyStateCard = CardWidget(self)
|
||||||
@@ -464,6 +430,24 @@ class PDDTaskPage(QWidget):
|
|||||||
self._update_empty_state()
|
self._update_empty_state()
|
||||||
self._restore_current_task(current_task_id)
|
self._restore_current_task(current_task_id)
|
||||||
|
|
||||||
|
def begin_task_reload(self) -> None:
|
||||||
|
"""清空旧结果并显示本地数据库加载状态。"""
|
||||||
|
|
||||||
|
self._task_id_before_reload = self.current_task_id()
|
||||||
|
self.taskModel.reset_for_reload()
|
||||||
|
self.set_loading()
|
||||||
|
|
||||||
|
def append_task_page(
|
||||||
|
self, tasks: Iterable[TaskRow], has_more: bool
|
||||||
|
) -> None:
|
||||||
|
"""把事件层读取的一页任务追加到表格。"""
|
||||||
|
|
||||||
|
self.taskModel.append_tasks(tasks, has_more)
|
||||||
|
self._update_empty_state()
|
||||||
|
task_id = getattr(self, "_task_id_before_reload", "")
|
||||||
|
self._restore_current_task(task_id)
|
||||||
|
self._task_id_before_reload = ""
|
||||||
|
|
||||||
def set_loading(self) -> None:
|
def set_loading(self) -> None:
|
||||||
"""显示正在读取本地任务的状态。"""
|
"""显示正在读取本地任务的状态。"""
|
||||||
|
|
||||||
@@ -472,6 +456,7 @@ class PDDTaskPage(QWidget):
|
|||||||
def set_load_error(self, message: str) -> None:
|
def set_load_error(self, message: str) -> None:
|
||||||
"""显示本地任务加载失败状态,不清除当前筛选条件。"""
|
"""显示本地任务加载失败状态,不清除当前筛选条件。"""
|
||||||
|
|
||||||
|
self.taskModel.finish_loading()
|
||||||
self._show_empty_state(
|
self._show_empty_state(
|
||||||
"任务加载失败",
|
"任务加载失败",
|
||||||
message or "无法读取本地任务,请检查日志后重试。",
|
message or "无法读取本地任务,请检查日志后重试。",
|
||||||
@@ -498,7 +483,7 @@ class PDDTaskPage(QWidget):
|
|||||||
self.statusLabel.setText(text or "自动获取:已停止 · 当前没有执行中的任务")
|
self.statusLabel.setText(text or "自动获取:已停止 · 当前没有执行中的任务")
|
||||||
|
|
||||||
def clear_filters(self) -> None:
|
def clear_filters(self) -> None:
|
||||||
"""清除全部搜索条件并恢复当前内存任务列表。"""
|
"""清除全部搜索条件并重新查询本地任务。"""
|
||||||
|
|
||||||
self.taskTypeCombo.setCurrentIndex(0)
|
self.taskTypeCombo.setCurrentIndex(0)
|
||||||
self.statusCombo.setCurrentIndex(0)
|
self.statusCombo.setCurrentIndex(0)
|
||||||
@@ -513,17 +498,11 @@ class PDDTaskPage(QWidget):
|
|||||||
return task.remote_task_id if task else ""
|
return task.remote_task_id if task else ""
|
||||||
|
|
||||||
def _apply_filters(self) -> None:
|
def _apply_filters(self) -> None:
|
||||||
current_task_id = self.current_task_id()
|
|
||||||
filters = {
|
filters = {
|
||||||
"task_type": self.taskTypeCombo.currentText(),
|
"task_type": self.taskTypeCombo.currentText(),
|
||||||
"status": self.statusCombo.currentText(),
|
"status": self.statusCombo.currentText(),
|
||||||
"keyword": self.keywordInput.text().strip(),
|
"keyword": self.keywordInput.text().strip(),
|
||||||
}
|
}
|
||||||
self.taskModel.apply_filters(
|
|
||||||
filters["task_type"], filters["status"], filters["keyword"]
|
|
||||||
)
|
|
||||||
self._update_empty_state()
|
|
||||||
self._restore_current_task(current_task_id)
|
|
||||||
self.searchRequested.emit(filters)
|
self.searchRequested.emit(filters)
|
||||||
|
|
||||||
def _on_table_clicked(self, index: QModelIndex) -> None:
|
def _on_table_clicked(self, index: QModelIndex) -> None:
|
||||||
|
|||||||
@@ -19,3 +19,118 @@
|
|||||||
(模板见 `docs/client/05-ui-specification.md` §9.1);
|
(模板见 `docs/client/05-ui-specification.md` §9.1);
|
||||||
只有必须让用户当场做决定时才用模态对话框。
|
只有必须让用户当场做决定时才用模态对话框。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from typing import Dict, Optional
|
||||||
|
|
||||||
|
from PyQt5.QtCore import QObject
|
||||||
|
|
||||||
|
from .pdd_ui import PDDTaskPage, TaskRow
|
||||||
|
from .task_models import TaskFilters, TaskStatus, TaskSummary, TaskType
|
||||||
|
from .task_repository import TaskRepository
|
||||||
|
|
||||||
|
|
||||||
|
TASK_TYPE_BY_TEXT = {
|
||||||
|
"采集": TaskType.COLLECT,
|
||||||
|
"采购": TaskType.PURCHASE,
|
||||||
|
}
|
||||||
|
|
||||||
|
TASK_STATUS_BY_TEXT = {
|
||||||
|
"待执行": TaskStatus.CLAIMED,
|
||||||
|
"执行中": TaskStatus.RUNNING,
|
||||||
|
"结果待提交": TaskStatus.RESULT_PENDING,
|
||||||
|
"等待重试": TaskStatus.RETRY_WAIT,
|
||||||
|
"需要人工处理": TaskStatus.MANUAL_REVIEW,
|
||||||
|
"已完成": TaskStatus.SUCCEEDED,
|
||||||
|
"失败": TaskStatus.FAILED,
|
||||||
|
"已取消": TaskStatus.CANCELLED,
|
||||||
|
}
|
||||||
|
|
||||||
|
TASK_TYPE_TEXT = {
|
||||||
|
TaskType.COLLECT: "采集",
|
||||||
|
TaskType.PURCHASE: "采购",
|
||||||
|
}
|
||||||
|
|
||||||
|
TASK_STATUS_TEXT = {
|
||||||
|
TaskStatus.CLAIMED: "待执行",
|
||||||
|
TaskStatus.RUNNING: "执行中",
|
||||||
|
TaskStatus.RESULT_PENDING: "结果待提交",
|
||||||
|
TaskStatus.RETRY_WAIT: "等待重试",
|
||||||
|
TaskStatus.MANUAL_REVIEW: "需要人工处理",
|
||||||
|
TaskStatus.SUCCEEDED: "已完成",
|
||||||
|
TaskStatus.FAILED: "失败",
|
||||||
|
TaskStatus.CANCELLED: "已取消",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class PDDTaskPageEvent(QObject):
|
||||||
|
"""把 PDD 页面只读操作连接到本地任务 Repository。"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
page: PDDTaskPage,
|
||||||
|
repository: Optional[TaskRepository] = None,
|
||||||
|
parent=None,
|
||||||
|
):
|
||||||
|
super().__init__(parent or page)
|
||||||
|
self._page = page
|
||||||
|
self._repository = repository or TaskRepository()
|
||||||
|
self._filters = TaskFilters()
|
||||||
|
|
||||||
|
page.searchRequested.connect(self.search_tasks)
|
||||||
|
page.refreshRequested.connect(self.refresh_tasks)
|
||||||
|
page.taskModel.loadMoreRequested.connect(self._load_page)
|
||||||
|
|
||||||
|
def load_initial_tasks(self) -> None:
|
||||||
|
"""应用启动后读取第一页本地任务。"""
|
||||||
|
|
||||||
|
self._reload()
|
||||||
|
|
||||||
|
def search_tasks(self, values: Dict[str, str]) -> None:
|
||||||
|
"""把界面中文筛选值转换成领域筛选,并重新查询。"""
|
||||||
|
|
||||||
|
self._filters = TaskFilters(
|
||||||
|
task_type=TASK_TYPE_BY_TEXT.get(values.get("task_type", "全部")),
|
||||||
|
status=TASK_STATUS_BY_TEXT.get(values.get("status", "全部")),
|
||||||
|
keyword=values.get("keyword", "").strip(),
|
||||||
|
)
|
||||||
|
self._reload()
|
||||||
|
|
||||||
|
def refresh_tasks(self) -> None:
|
||||||
|
"""使用当前筛选条件刷新列表。"""
|
||||||
|
|
||||||
|
self._reload()
|
||||||
|
|
||||||
|
def _reload(self) -> None:
|
||||||
|
self._page.begin_task_reload()
|
||||||
|
self._page.taskModel.fetchMore()
|
||||||
|
|
||||||
|
def _load_page(self, offset: int, limit: int) -> None:
|
||||||
|
try:
|
||||||
|
summaries = self._repository.list_tasks(
|
||||||
|
filters=self._filters,
|
||||||
|
limit=limit,
|
||||||
|
offset=offset,
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
self._page.set_load_error("无法读取本地任务,请检查数据库后重试。")
|
||||||
|
return
|
||||||
|
|
||||||
|
rows = [summary_to_row(summary) for summary in summaries]
|
||||||
|
self._page.append_task_page(rows, has_more=len(rows) == limit)
|
||||||
|
|
||||||
|
|
||||||
|
def summary_to_row(summary: TaskSummary) -> TaskRow:
|
||||||
|
"""把领域摘要转换成只供表格显示的轻量行。"""
|
||||||
|
|
||||||
|
return TaskRow(
|
||||||
|
remote_task_id=summary.remote_task_id,
|
||||||
|
task_type=TASK_TYPE_TEXT[summary.task_type],
|
||||||
|
goods_id=summary.goods_id or "",
|
||||||
|
title=summary.title or "",
|
||||||
|
color=summary.target_color or "",
|
||||||
|
size=summary.target_size or "",
|
||||||
|
price_cents=summary.price_cent,
|
||||||
|
quantity=summary.quantity,
|
||||||
|
status=TASK_STATUS_TEXT[summary.status],
|
||||||
|
updated_at=summary.updated_at,
|
||||||
|
)
|
||||||
|
|||||||
@@ -28,17 +28,24 @@ from qfluentwidgets import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
from .pdd_ui import PDDTaskPage
|
from .pdd_ui import PDDTaskPage
|
||||||
|
from .pdd_ui_event import PDDTaskPageEvent
|
||||||
from .settings_ui import SettingsPage
|
from .settings_ui import SettingsPage
|
||||||
|
from .task_repository import TaskRepository
|
||||||
|
|
||||||
|
|
||||||
class MainWindow(FluentWindow):
|
class MainWindow(FluentWindow):
|
||||||
"""应用主窗口。"""
|
"""应用主窗口。"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self, task_repository=None):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
self.pddTaskPage = PDDTaskPage(self)
|
self.pddTaskPage = PDDTaskPage(self)
|
||||||
self.settingsPage = SettingsPage(self)
|
self.settingsPage = SettingsPage(self)
|
||||||
|
self.pddTaskPageEvent = PDDTaskPageEvent(
|
||||||
|
self.pddTaskPage,
|
||||||
|
task_repository or TaskRepository(),
|
||||||
|
self,
|
||||||
|
)
|
||||||
|
|
||||||
self.addSubInterface(self.pddTaskPage, FIF.HOME, "pdd")
|
self.addSubInterface(self.pddTaskPage, FIF.HOME, "pdd")
|
||||||
self.addSubInterface(
|
self.addSubInterface(
|
||||||
@@ -52,6 +59,7 @@ class MainWindow(FluentWindow):
|
|||||||
self.setMinimumSize(820, 560)
|
self.setMinimumSize(820, 560)
|
||||||
self.resize(1040, 680)
|
self.resize(1040, 680)
|
||||||
self._moveToScreenCenter()
|
self._moveToScreenCenter()
|
||||||
|
self.pddTaskPageEvent.load_initial_tasks()
|
||||||
|
|
||||||
def _moveToScreenCenter(self):
|
def _moveToScreenCenter(self):
|
||||||
screen = QApplication.primaryScreen()
|
screen = QApplication.primaryScreen()
|
||||||
|
|||||||
@@ -0,0 +1,142 @@
|
|||||||
|
"""PDD 任务列表接入 SQLite 的离屏测试。"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
||||||
|
|
||||||
|
from PyQt5.QtWidgets import QApplication
|
||||||
|
|
||||||
|
from src.pdd_ui import PDDTaskPage
|
||||||
|
from src.pdd_ui_event import PDDTaskPageEvent, summary_to_row
|
||||||
|
from src.task_models import NewClaimedTask, TaskStatus, TaskSummary, TaskType
|
||||||
|
from src.task_repository import TaskRepository
|
||||||
|
from src.ui_main import MainWindow
|
||||||
|
|
||||||
|
|
||||||
|
class BrokenRepository:
|
||||||
|
"""模拟无法读取数据库的 Repository。"""
|
||||||
|
|
||||||
|
def list_tasks(self, filters=None, limit=50, offset=0):
|
||||||
|
raise RuntimeError("database is unavailable")
|
||||||
|
|
||||||
|
|
||||||
|
class PDDTaskPageEventTest(unittest.TestCase):
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
cls.app = QApplication.instance() or QApplication([])
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.temp_directory = tempfile.TemporaryDirectory()
|
||||||
|
self.db_path = Path(self.temp_directory.name) / "client.db"
|
||||||
|
self.repository = TaskRepository(self.db_path)
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
self.temp_directory.cleanup()
|
||||||
|
|
||||||
|
def _add_task(
|
||||||
|
self,
|
||||||
|
number: int,
|
||||||
|
task_type: TaskType = TaskType.COLLECT,
|
||||||
|
title: str = "测试商品",
|
||||||
|
) -> None:
|
||||||
|
self.repository.add_claimed_task(
|
||||||
|
NewClaimedTask(
|
||||||
|
remote_task_id=f"PDD-{number:03d}",
|
||||||
|
task_type=task_type,
|
||||||
|
goods_url=f"pdd://goods/{number}",
|
||||||
|
goods_id=str(number),
|
||||||
|
title=title,
|
||||||
|
target_color="黑色",
|
||||||
|
target_size="M",
|
||||||
|
price_cent=3990,
|
||||||
|
quantity=1 if task_type is TaskType.PURCHASE else None,
|
||||||
|
),
|
||||||
|
received_at="2026-08-06T08:00:00Z",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_initial_load_and_fetch_next_page(self):
|
||||||
|
for number in range(51):
|
||||||
|
self._add_task(number)
|
||||||
|
|
||||||
|
page = PDDTaskPage()
|
||||||
|
events = PDDTaskPageEvent(page, self.repository)
|
||||||
|
events.load_initial_tasks()
|
||||||
|
|
||||||
|
self.assertEqual(page.taskModel.data_row_count(), 50)
|
||||||
|
self.assertEqual(page.taskModel.row_at(0).remote_task_id, "PDD-050")
|
||||||
|
self.assertTrue(page.taskModel.canFetchMore())
|
||||||
|
|
||||||
|
page.taskModel.fetchMore()
|
||||||
|
|
||||||
|
self.assertEqual(page.taskModel.data_row_count(), 51)
|
||||||
|
self.assertEqual(page.taskModel.row_at(50).remote_task_id, "PDD-000")
|
||||||
|
self.assertFalse(page.taskModel.canFetchMore())
|
||||||
|
page.deleteLater()
|
||||||
|
|
||||||
|
def test_search_uses_type_status_and_keyword_together(self):
|
||||||
|
self._add_task(1, TaskType.COLLECT, "目标短袖")
|
||||||
|
self._add_task(2, TaskType.PURCHASE, "目标短袖")
|
||||||
|
self._add_task(3, TaskType.COLLECT, "其他商品")
|
||||||
|
|
||||||
|
page = PDDTaskPage()
|
||||||
|
events = PDDTaskPageEvent(page, self.repository)
|
||||||
|
page.searchRequested.emit(
|
||||||
|
{"task_type": "采集", "status": "待执行", "keyword": "目标"}
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(page.taskModel.data_row_count(), 1)
|
||||||
|
row = page.taskModel.row_at(0)
|
||||||
|
self.assertEqual(row.remote_task_id, "PDD-001")
|
||||||
|
self.assertEqual(row.task_type, "采集")
|
||||||
|
self.assertEqual(row.status, "待执行")
|
||||||
|
self.assertEqual(row.price_cents, 3990)
|
||||||
|
page.deleteLater()
|
||||||
|
|
||||||
|
def test_query_failure_shows_readable_error(self):
|
||||||
|
page = PDDTaskPage()
|
||||||
|
events = PDDTaskPageEvent(page, BrokenRepository())
|
||||||
|
|
||||||
|
events.load_initial_tasks()
|
||||||
|
|
||||||
|
self.assertFalse(page.emptyStateCard.isHidden())
|
||||||
|
self.assertEqual(page.emptyTitleLabel.text(), "任务加载失败")
|
||||||
|
self.assertIn("检查数据库", page.emptyMessageLabel.text())
|
||||||
|
self.assertFalse(page.taskModel.canFetchMore())
|
||||||
|
page.deleteLater()
|
||||||
|
|
||||||
|
def test_summary_to_row_does_not_copy_detail_json(self):
|
||||||
|
summary = TaskSummary(
|
||||||
|
id=1,
|
||||||
|
remote_task_id="PDD-001",
|
||||||
|
task_type=TaskType.PURCHASE,
|
||||||
|
goods_id=None,
|
||||||
|
title=None,
|
||||||
|
target_color=None,
|
||||||
|
target_size=None,
|
||||||
|
price_cent=None,
|
||||||
|
quantity=2,
|
||||||
|
status=TaskStatus.MANUAL_REVIEW,
|
||||||
|
updated_at="2026-08-06T08:00:00Z",
|
||||||
|
)
|
||||||
|
|
||||||
|
row = summary_to_row(summary)
|
||||||
|
|
||||||
|
self.assertEqual(row.task_type, "采购")
|
||||||
|
self.assertEqual(row.status, "需要人工处理")
|
||||||
|
self.assertEqual(row.goods_id, "")
|
||||||
|
self.assertFalse(hasattr(row, "pdd_data"))
|
||||||
|
|
||||||
|
def test_main_window_keeps_event_object_alive(self):
|
||||||
|
window = MainWindow(task_repository=self.repository)
|
||||||
|
|
||||||
|
self.assertIsInstance(window.pddTaskPageEvent, PDDTaskPageEvent)
|
||||||
|
self.assertEqual(window.pddTaskPage.taskModel.data_row_count(), 0)
|
||||||
|
window.close()
|
||||||
|
window.deleteLater()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Reference in New Issue
Block a user