feat(client): connect PDD task list to SQLite (#9)

This commit is contained in:
chengma
2026-08-06 16:31:34 +08:00
parent be6ee3d880
commit 583e441e17
4 changed files with 334 additions and 90 deletions
+68 -89
View File
@@ -78,7 +78,9 @@ class TaskRow:
class TaskTableModel(QAbstractTableModel):
"""PDD 任务表格模型,当前使用内存数据模拟 Repository 查询结果。"""
"""PDD 任务表格模型,只保存已经从数据库读取的轻量行。"""
loadMoreRequested = pyqtSignal(int, int)
HEADERS = (
"任务类型",
@@ -94,14 +96,9 @@ class TaskTableModel(QAbstractTableModel):
def __init__(self, parent=None):
super().__init__(parent)
self._source_rows: list[TaskRow] = []
self._filtered_rows: list[TaskRow] = []
self._rows: list[TaskRow] = []
self._task_type = "全部"
self._status = "全部"
self._keyword = ""
self._sort_column = 7
self._sort_order = Qt.DescendingOrder
self._has_more = False
self._loading = False
self._placeholder_row_count = 1
def rowCount(self, parent=QModelIndex()) -> int:
@@ -151,54 +148,66 @@ class TaskTableModel(QAbstractTableModel):
return Qt.ItemIsEnabled | Qt.ItemIsSelectable
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:
"""每次再展示最多 50 行,模拟后续 Repository 的分页行为。"""
"""请求事件层从数据库读取下一页。"""
if parent.isValid() or not self.canFetchMore(parent):
return
start = len(self._rows)
end = min(start + PAGE_SIZE, len(self._filtered_rows))
self.beginInsertRows(QModelIndex(), start, end - 1)
self._rows.extend(self._filtered_rows[start:end])
self.endInsertRows()
self._loading = True
self.loadMoreRequested.emit(len(self._rows), PAGE_SIZE)
def sort(self, column, order=Qt.AscendingOrder) -> None:
if not 0 <= column < len(self.HEADERS):
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()
"""列表顺序由 Repository 固定为更新时间倒序。"""
def set_tasks(self, tasks: Iterable[TaskRow]) -> None:
"""替换内存任务列表;后续由 Repository 查询结果调用。"""
"""替换全部任务;保留给简单测试和静态预览使用。"""
self.beginResetModel()
self._source_rows = list(tasks)
self._rebuild_filtered_rows()
self._rows = self._filtered_rows[:PAGE_SIZE]
self._rows = list(tasks)
self._has_more = False
self._loading = False
self.endResetModel()
def apply_filters(self, task_type: str, status: str, keyword: str) -> None:
"""按类型、状态和关键词同时筛选当前内存任务。"""
def reset_for_reload(self) -> None:
"""清空旧结果,准备从数据库重新读取第一页。"""
self.beginResetModel()
self._task_type = task_type
self._status = status
self._keyword = keyword.strip().casefold()
self._rebuild_filtered_rows()
self._rows = self._filtered_rows[:PAGE_SIZE]
self._rows = []
self._has_more = True
self._loading = False
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]:
"""根据当前可见行获取任务,越界时返回 None。"""
@@ -215,7 +224,7 @@ class TaskTableModel(QAbstractTableModel):
return -1
def filtered_count(self) -> int:
return len(self._filtered_rows)
return len(self._rows)
def data_row_count(self) -> int:
"""返回真实任务行数,不包含填满空表格的占位行。"""
@@ -237,48 +246,6 @@ class TaskTableModel(QAbstractTableModel):
self._placeholder_row_count = count
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
def _display_value(task: TaskRow, column: int) -> str:
if column == 0:
@@ -381,7 +348,7 @@ class PDDTaskPage(QWidget):
self.taskTable.setEditTriggers(QAbstractItemView.NoEditTriggers)
self.taskTable.setAlternatingRowColors(True)
self.taskTable.setWordWrap(False)
self.taskTable.setSortingEnabled(True)
self.taskTable.setSortingEnabled(False)
self.taskTable.verticalHeader().setVisible(False)
header = self.taskTable.horizontalHeader()
@@ -396,7 +363,6 @@ class PDDTaskPage(QWidget):
self.taskTable.setColumnWidth(6, 116)
self.taskTable.setColumnWidth(7, 156)
self.taskTable.setColumnWidth(8, 72)
self.taskTable.sortByColumn(7, Qt.DescendingOrder)
self.taskTable.viewport().installEventFilter(self)
self.emptyStateCard = CardWidget(self)
@@ -464,6 +430,24 @@ class PDDTaskPage(QWidget):
self._update_empty_state()
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:
"""显示正在读取本地任务的状态。"""
@@ -472,6 +456,7 @@ class PDDTaskPage(QWidget):
def set_load_error(self, message: str) -> None:
"""显示本地任务加载失败状态,不清除当前筛选条件。"""
self.taskModel.finish_loading()
self._show_empty_state(
"任务加载失败",
message or "无法读取本地任务,请检查日志后重试。",
@@ -498,7 +483,7 @@ class PDDTaskPage(QWidget):
self.statusLabel.setText(text or "自动获取:已停止 · 当前没有执行中的任务")
def clear_filters(self) -> None:
"""清除全部搜索条件并恢复当前内存任务列表。"""
"""清除全部搜索条件并重新查询本地任务。"""
self.taskTypeCombo.setCurrentIndex(0)
self.statusCombo.setCurrentIndex(0)
@@ -513,17 +498,11 @@ class PDDTaskPage(QWidget):
return task.remote_task_id if task else ""
def _apply_filters(self) -> None:
current_task_id = self.current_task_id()
filters = {
"task_type": self.taskTypeCombo.currentText(),
"status": self.statusCombo.currentText(),
"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)
def _on_table_clicked(self, index: QModelIndex) -> None: