feat: 增加任务批量勾选和重新上报 (#89)
This commit is contained in:
+223
-46
@@ -56,7 +56,8 @@ from qfluentwidgets import (
|
||||
|
||||
|
||||
PAGE_SIZE = 50
|
||||
DETAIL_COLUMN = 8
|
||||
CHECK_COLUMN = 0
|
||||
DETAIL_COLUMN = 9
|
||||
REMOTE_TASK_ID_ROLE = Qt.UserRole + 1
|
||||
|
||||
|
||||
@@ -81,8 +82,10 @@ class TaskTableModel(QAbstractTableModel):
|
||||
"""PDD 任务表格模型,只保存已经从数据库读取的轻量行。"""
|
||||
|
||||
loadMoreRequested = pyqtSignal(int, int)
|
||||
checkedTaskIdsChanged = pyqtSignal(object)
|
||||
|
||||
HEADERS = (
|
||||
"选择",
|
||||
"任务类型",
|
||||
"商品标题",
|
||||
"颜色",
|
||||
@@ -100,6 +103,7 @@ class TaskTableModel(QAbstractTableModel):
|
||||
self._has_more = False
|
||||
self._loading = False
|
||||
self._placeholder_row_count = 1
|
||||
self._checked_task_ids: set[str] = set()
|
||||
|
||||
def rowCount(self, parent=QModelIndex()) -> int:
|
||||
if parent.isValid():
|
||||
@@ -121,18 +125,25 @@ class TaskTableModel(QAbstractTableModel):
|
||||
task = self._rows[index.row()]
|
||||
column = index.column()
|
||||
|
||||
if role == Qt.CheckStateRole and column == CHECK_COLUMN:
|
||||
return (
|
||||
Qt.Checked
|
||||
if task.remote_task_id in self._checked_task_ids
|
||||
else Qt.Unchecked
|
||||
)
|
||||
|
||||
if role == Qt.DisplayRole:
|
||||
return self._display_value(task, column)
|
||||
|
||||
if role == Qt.TextAlignmentRole:
|
||||
if column in (0, 6, 8):
|
||||
if column in (0, 1, 7, 9):
|
||||
return Qt.AlignCenter
|
||||
if column in (4, 5):
|
||||
if column in (5, 6):
|
||||
return Qt.AlignRight | Qt.AlignVCenter
|
||||
return Qt.AlignLeft | Qt.AlignVCenter
|
||||
|
||||
if role == Qt.ToolTipRole:
|
||||
if column == 1:
|
||||
if column == 2:
|
||||
return task.title or "尚未获取标题"
|
||||
if column == DETAIL_COLUMN:
|
||||
return f"查看任务 {task.remote_task_id} 的详情"
|
||||
@@ -145,7 +156,30 @@ class TaskTableModel(QAbstractTableModel):
|
||||
def flags(self, index):
|
||||
if not index.isValid() or index.row() >= len(self._rows):
|
||||
return Qt.NoItemFlags
|
||||
return Qt.ItemIsEnabled | Qt.ItemIsSelectable
|
||||
flags = Qt.ItemIsEnabled | Qt.ItemIsSelectable
|
||||
if index.column() == CHECK_COLUMN:
|
||||
flags |= Qt.ItemIsUserCheckable
|
||||
return flags
|
||||
|
||||
def setData(self, index, value, role=Qt.EditRole):
|
||||
"""切换任务勾选状态;业务身份始终使用远端任务编号。"""
|
||||
|
||||
if (
|
||||
not index.isValid()
|
||||
or index.column() != CHECK_COLUMN
|
||||
or index.row() >= len(self._rows)
|
||||
or role != Qt.CheckStateRole
|
||||
):
|
||||
return False
|
||||
|
||||
task_id = self._rows[index.row()].remote_task_id
|
||||
if value == Qt.Checked:
|
||||
self._checked_task_ids.add(task_id)
|
||||
else:
|
||||
self._checked_task_ids.discard(task_id)
|
||||
self.dataChanged.emit(index, index, [Qt.CheckStateRole])
|
||||
self.checkedTaskIdsChanged.emit(self.checked_task_ids())
|
||||
return True
|
||||
|
||||
def canFetchMore(self, parent=QModelIndex()) -> bool:
|
||||
"""告诉 Qt 数据库中是否还有下一页。"""
|
||||
@@ -172,6 +206,7 @@ class TaskTableModel(QAbstractTableModel):
|
||||
self._has_more = False
|
||||
self._loading = False
|
||||
self.endResetModel()
|
||||
self._prune_checked_task_ids()
|
||||
|
||||
def reset_for_reload(self) -> None:
|
||||
"""清空旧结果,准备从数据库重新读取第一页。"""
|
||||
@@ -192,6 +227,8 @@ class TaskTableModel(QAbstractTableModel):
|
||||
self._has_more = has_more
|
||||
self._loading = False
|
||||
self.endResetModel()
|
||||
if not has_more:
|
||||
self._prune_checked_task_ids()
|
||||
return
|
||||
|
||||
if page:
|
||||
@@ -201,6 +238,8 @@ class TaskTableModel(QAbstractTableModel):
|
||||
self.endInsertRows()
|
||||
self._has_more = has_more
|
||||
self._loading = False
|
||||
if not has_more:
|
||||
self._prune_checked_task_ids()
|
||||
|
||||
def finish_loading(self) -> None:
|
||||
"""查询失败时停止继续请求,等待用户刷新或重新搜索。"""
|
||||
@@ -231,6 +270,54 @@ class TaskTableModel(QAbstractTableModel):
|
||||
|
||||
return len(self._rows)
|
||||
|
||||
def checked_task_ids(self) -> tuple[str, ...]:
|
||||
"""按当前列表顺序返回已勾选的稳定任务编号。"""
|
||||
|
||||
loaded_order = [
|
||||
task.remote_task_id
|
||||
for task in self._rows
|
||||
if task.remote_task_id in self._checked_task_ids
|
||||
]
|
||||
loaded = set(loaded_order)
|
||||
return tuple(loaded_order) + tuple(sorted(self._checked_task_ids - loaded))
|
||||
|
||||
def clear_checked_tasks(self) -> None:
|
||||
"""清空全部勾选,并只刷新复选框列。"""
|
||||
|
||||
if not self._checked_task_ids:
|
||||
return
|
||||
self._checked_task_ids.clear()
|
||||
if self._rows:
|
||||
self.dataChanged.emit(
|
||||
self.index(0, CHECK_COLUMN),
|
||||
self.index(len(self._rows) - 1, CHECK_COLUMN),
|
||||
[Qt.CheckStateRole],
|
||||
)
|
||||
self.checkedTaskIdsChanged.emit(())
|
||||
|
||||
def uncheck_tasks(self, remote_task_ids: Iterable[str]) -> None:
|
||||
"""取消指定任务的勾选,供批量操作成功后更新界面。"""
|
||||
|
||||
removed = self._checked_task_ids.intersection(remote_task_ids)
|
||||
if not removed:
|
||||
return
|
||||
self._checked_task_ids.difference_update(removed)
|
||||
for row, task in enumerate(self._rows):
|
||||
if task.remote_task_id in removed:
|
||||
index = self.index(row, CHECK_COLUMN)
|
||||
self.dataChanged.emit(index, index, [Qt.CheckStateRole])
|
||||
self.checkedTaskIdsChanged.emit(self.checked_task_ids())
|
||||
|
||||
def _prune_checked_task_ids(self) -> None:
|
||||
"""完整加载结束后移除本地已不存在的勾选。"""
|
||||
|
||||
loaded = {task.remote_task_id for task in self._rows}
|
||||
removed = self._checked_task_ids - loaded
|
||||
if not removed:
|
||||
return
|
||||
self._checked_task_ids.intersection_update(loaded)
|
||||
self.checkedTaskIdsChanged.emit(self.checked_task_ids())
|
||||
|
||||
def set_placeholder_row_count(self, count: int) -> None:
|
||||
"""设置无数据时显示的禁用空行数量。"""
|
||||
|
||||
@@ -248,24 +335,26 @@ class TaskTableModel(QAbstractTableModel):
|
||||
|
||||
@staticmethod
|
||||
def _display_value(task: TaskRow, column: int) -> str:
|
||||
if column == 0:
|
||||
return task.task_type
|
||||
if column == CHECK_COLUMN:
|
||||
return ""
|
||||
if column == 1:
|
||||
return task.title or "尚未获取标题"
|
||||
return task.task_type
|
||||
if column == 2:
|
||||
return task.color or "—"
|
||||
return task.title or "尚未获取标题"
|
||||
if column == 3:
|
||||
return task.size or "—"
|
||||
return task.color or "—"
|
||||
if column == 4:
|
||||
return task.size or "—"
|
||||
if column == 5:
|
||||
if task.price_cents is None:
|
||||
return "—"
|
||||
suffix = " 起" if task.price_is_starting else ""
|
||||
return f"¥{task.price_cents / 100:.2f}{suffix}"
|
||||
if column == 5:
|
||||
return "—" if task.quantity is None else str(task.quantity)
|
||||
if column == 6:
|
||||
return task.status
|
||||
return "—" if task.quantity is None else str(task.quantity)
|
||||
if column == 7:
|
||||
return task.status
|
||||
if column == 8:
|
||||
return task.updated_at or "—"
|
||||
return "详情"
|
||||
|
||||
@@ -278,6 +367,7 @@ class PDDTaskPage(QWidget):
|
||||
refreshRequested = pyqtSignal()
|
||||
rerunRequested = pyqtSignal(str)
|
||||
rerunCancelRequested = pyqtSignal()
|
||||
resubmitRequested = pyqtSignal(object)
|
||||
openSettingsRequested = pyqtSignal()
|
||||
detailRequested = pyqtSignal(str)
|
||||
|
||||
@@ -286,6 +376,7 @@ class PDDTaskPage(QWidget):
|
||||
self.setObjectName("pddTaskPage")
|
||||
|
||||
self._rerun_state = "idle"
|
||||
self._resubmit_running = False
|
||||
self.taskModel = TaskTableModel(self)
|
||||
self._build_command_area()
|
||||
self._build_content_area()
|
||||
@@ -301,11 +392,13 @@ class PDDTaskPage(QWidget):
|
||||
self.autoFetchButton.setMinimumWidth(132)
|
||||
self.autoFetchButton.setAccessibleName("开始自动获取 PDD 任务")
|
||||
|
||||
self.taskTypeLabel = CaptionLabel("任务类型", self)
|
||||
self.taskTypeCombo = ComboBox(self)
|
||||
self.taskTypeCombo.addItems(("全部", "采集", "采购"))
|
||||
self.taskTypeCombo.setMinimumWidth(100)
|
||||
self.taskTypeCombo.setAccessibleName("任务类型")
|
||||
|
||||
self.statusLabelCaption = CaptionLabel("任务状态", self)
|
||||
self.statusCombo = ComboBox(self)
|
||||
self.statusCombo.addItems(
|
||||
("全部", "待执行", "执行中", "已完成", "失败", "需要人工处理")
|
||||
@@ -313,6 +406,7 @@ class PDDTaskPage(QWidget):
|
||||
self.statusCombo.setMinimumWidth(130)
|
||||
self.statusCombo.setAccessibleName("任务状态")
|
||||
|
||||
self.keywordLabel = CaptionLabel("关键词", self)
|
||||
self.keywordInput = SearchLineEdit(self)
|
||||
self.keywordInput.setPlaceholderText("任务编号、商品编号或商品标题")
|
||||
self.keywordInput.setClearButtonEnabled(True)
|
||||
@@ -331,22 +425,68 @@ class PDDTaskPage(QWidget):
|
||||
self.rerunButton.setAccessibleName("重新执行当前选中的采集任务")
|
||||
self.rerunButton.setEnabled(False)
|
||||
|
||||
self.resubmitButton = PushButton(FIF.SEND, "重新上报", self)
|
||||
self.resubmitButton.setAccessibleName("重新上报勾选任务的已有结果")
|
||||
self.resubmitButton.setEnabled(False)
|
||||
|
||||
self.commandCard = CardWidget(self)
|
||||
commandLayout = QGridLayout(self.commandCard)
|
||||
commandLayout.setContentsMargins(20, 14, 20, 14)
|
||||
commandLayout.setHorizontalSpacing(12)
|
||||
commandLayout.addWidget(self.autoFetchButton, 0, 0)
|
||||
commandLayout.addWidget(CaptionLabel("任务类型", self.commandCard), 0, 1)
|
||||
commandLayout.addWidget(self.taskTypeCombo, 0, 2)
|
||||
commandLayout.addWidget(CaptionLabel("任务状态", self.commandCard), 0, 3)
|
||||
commandLayout.addWidget(self.statusCombo, 0, 4)
|
||||
commandLayout.addWidget(CaptionLabel("关键词", self.commandCard), 0, 5)
|
||||
commandLayout.addWidget(self.keywordInput, 0, 6)
|
||||
commandLayout.addWidget(self.searchButton, 0, 7)
|
||||
commandLayout.setColumnStretch(6, 4)
|
||||
commandLayout.setColumnStretch(8, 1)
|
||||
commandLayout.addWidget(self.refreshButton, 0, 9)
|
||||
commandLayout.addWidget(self.rerunButton, 0, 10)
|
||||
self.checkedCountLabel = CaptionLabel("已选 0 条", self.commandCard)
|
||||
self.checkedCountLabel.setAccessibleName("已勾选任务数量")
|
||||
self.commandLayout = QGridLayout(self.commandCard)
|
||||
self.commandLayout.setContentsMargins(20, 14, 20, 14)
|
||||
self.commandLayout.setHorizontalSpacing(12)
|
||||
self.commandLayout.setVerticalSpacing(10)
|
||||
self._compact_commands = False
|
||||
# 先使用安全的双行布局,窗口真正变宽后再切换为单行。
|
||||
self._update_command_layout(compact=True)
|
||||
|
||||
def _update_command_layout(self, compact: bool) -> None:
|
||||
"""在窄窗口把搜索区换到第二行,不重建任何控件。"""
|
||||
|
||||
if compact == self._compact_commands and self.commandLayout.count():
|
||||
return
|
||||
self._compact_commands = compact
|
||||
for column in range(13):
|
||||
self.commandLayout.setColumnStretch(column, 0)
|
||||
|
||||
if compact:
|
||||
positions = (
|
||||
(self.autoFetchButton, 0, 0, 1, 1),
|
||||
(self.taskTypeLabel, 0, 1, 1, 1),
|
||||
(self.taskTypeCombo, 0, 2, 1, 1),
|
||||
(self.statusLabelCaption, 0, 3, 1, 1),
|
||||
(self.statusCombo, 0, 4, 1, 1),
|
||||
(self.checkedCountLabel, 0, 6, 1, 1),
|
||||
(self.rerunButton, 0, 7, 1, 1),
|
||||
(self.resubmitButton, 0, 8, 1, 1),
|
||||
(self.keywordLabel, 1, 0, 1, 1),
|
||||
(self.keywordInput, 1, 1, 1, 5),
|
||||
(self.searchButton, 1, 6, 1, 1),
|
||||
(self.refreshButton, 1, 7, 1, 1),
|
||||
)
|
||||
self.commandLayout.setColumnStretch(5, 1)
|
||||
else:
|
||||
positions = (
|
||||
(self.autoFetchButton, 0, 0, 1, 1),
|
||||
(self.taskTypeLabel, 0, 1, 1, 1),
|
||||
(self.taskTypeCombo, 0, 2, 1, 1),
|
||||
(self.statusLabelCaption, 0, 3, 1, 1),
|
||||
(self.statusCombo, 0, 4, 1, 1),
|
||||
(self.keywordLabel, 0, 5, 1, 1),
|
||||
(self.keywordInput, 0, 6, 1, 1),
|
||||
(self.searchButton, 0, 7, 1, 1),
|
||||
(self.refreshButton, 0, 8, 1, 1),
|
||||
(self.checkedCountLabel, 0, 10, 1, 1),
|
||||
(self.rerunButton, 0, 11, 1, 1),
|
||||
(self.resubmitButton, 0, 12, 1, 1),
|
||||
)
|
||||
self.commandLayout.setColumnStretch(6, 4)
|
||||
self.commandLayout.setColumnStretch(9, 1)
|
||||
|
||||
for widget, row, column, row_span, column_span in positions:
|
||||
self.commandLayout.addWidget(
|
||||
widget, row, column, row_span, column_span
|
||||
)
|
||||
|
||||
def _build_content_area(self) -> None:
|
||||
self.taskTable = TableView(self)
|
||||
@@ -362,16 +502,17 @@ class PDDTaskPage(QWidget):
|
||||
|
||||
header = self.taskTable.horizontalHeader()
|
||||
header.setSectionResizeMode(QHeaderView.Interactive)
|
||||
header.setSectionResizeMode(1, QHeaderView.Stretch)
|
||||
header.setSectionResizeMode(2, QHeaderView.Stretch)
|
||||
header.setMinimumSectionSize(64)
|
||||
self.taskTable.setColumnWidth(0, 82)
|
||||
self.taskTable.setColumnWidth(2, 108)
|
||||
self.taskTable.setColumnWidth(3, 82)
|
||||
self.taskTable.setColumnWidth(4, 96)
|
||||
self.taskTable.setColumnWidth(5, 70)
|
||||
self.taskTable.setColumnWidth(6, 116)
|
||||
self.taskTable.setColumnWidth(7, 156)
|
||||
self.taskTable.setColumnWidth(8, 72)
|
||||
self.taskTable.setColumnWidth(0, 64)
|
||||
self.taskTable.setColumnWidth(1, 82)
|
||||
self.taskTable.setColumnWidth(3, 108)
|
||||
self.taskTable.setColumnWidth(4, 82)
|
||||
self.taskTable.setColumnWidth(5, 96)
|
||||
self.taskTable.setColumnWidth(6, 70)
|
||||
self.taskTable.setColumnWidth(7, 116)
|
||||
self.taskTable.setColumnWidth(8, 156)
|
||||
self.taskTable.setColumnWidth(9, 72)
|
||||
self.taskTable.viewport().installEventFilter(self)
|
||||
|
||||
self.emptyStateCard = CardWidget(self)
|
||||
@@ -420,12 +561,11 @@ class PDDTaskPage(QWidget):
|
||||
self.keywordInput.returnPressed.connect(self._apply_filters)
|
||||
self.refreshButton.clicked.connect(self.refreshRequested.emit)
|
||||
self.rerunButton.clicked.connect(self._request_rerun)
|
||||
self.resubmitButton.clicked.connect(self._request_resubmit)
|
||||
self.clearFiltersButton.clicked.connect(self.clear_filters)
|
||||
self.taskTable.clicked.connect(self._on_table_clicked)
|
||||
self.taskTable.activated.connect(self._on_table_activated)
|
||||
self.taskTable.selectionModel().selectionChanged.connect(
|
||||
self._update_rerun_button
|
||||
)
|
||||
self.taskModel.checkedTaskIdsChanged.connect(self._update_checked_actions)
|
||||
|
||||
self.findShortcut = QShortcut(QKeySequence.Find, self)
|
||||
self.findShortcut.activated.connect(self.keywordInput.setFocus)
|
||||
@@ -435,6 +575,12 @@ class PDDTaskPage(QWidget):
|
||||
self._update_placeholder_rows()
|
||||
return super().eventFilter(watched, event)
|
||||
|
||||
def resizeEvent(self, event) -> None:
|
||||
"""按内容区宽度切换顶部命令区的单行或双行布局。"""
|
||||
|
||||
self._update_command_layout(compact=event.size().width() < 1180)
|
||||
super().resizeEvent(event)
|
||||
|
||||
def set_tasks(self, tasks: Iterable[TaskRow]) -> None:
|
||||
"""显示任务数据,并尽量恢复原来选中的稳定任务编号。"""
|
||||
|
||||
@@ -516,6 +662,12 @@ class PDDTaskPage(QWidget):
|
||||
self._rerun_state = "running" if running else "idle"
|
||||
self._update_rerun_button()
|
||||
|
||||
def set_resubmit_running(self, running: bool) -> None:
|
||||
"""设置重新上报忙碌状态并防止重复提交。"""
|
||||
|
||||
self._resubmit_running = running
|
||||
self._update_checked_actions(self.taskModel.checked_task_ids())
|
||||
|
||||
def set_rerun_cancelling(self) -> None:
|
||||
"""显示取消请求已发出,防止用户重复点击。"""
|
||||
|
||||
@@ -528,33 +680,58 @@ class PDDTaskPage(QWidget):
|
||||
return
|
||||
if self._rerun_state != "idle":
|
||||
return
|
||||
task_id = self.current_task_id()
|
||||
if task_id:
|
||||
self.rerunRequested.emit(task_id)
|
||||
task_ids = self.taskModel.checked_task_ids()
|
||||
if len(task_ids) == 1:
|
||||
self.rerunRequested.emit(task_ids[0])
|
||||
|
||||
def _request_resubmit(self) -> None:
|
||||
if self._resubmit_running:
|
||||
return
|
||||
task_ids = self.taskModel.checked_task_ids()
|
||||
if task_ids:
|
||||
self.resubmitRequested.emit(task_ids)
|
||||
|
||||
def _update_rerun_button(self) -> None:
|
||||
if self._rerun_state == "running":
|
||||
self.rerunButton.setText("停止重新采集")
|
||||
self.rerunButton.setAccessibleName("停止当前重新采集")
|
||||
self.rerunButton.setEnabled(True)
|
||||
self.resubmitButton.setEnabled(False)
|
||||
self.autoFetchButton.setEnabled(False)
|
||||
return
|
||||
if self._rerun_state == "cancelling":
|
||||
self.rerunButton.setText("正在停止…")
|
||||
self.rerunButton.setAccessibleName("正在停止重新采集")
|
||||
self.rerunButton.setEnabled(False)
|
||||
self.resubmitButton.setEnabled(False)
|
||||
self.autoFetchButton.setEnabled(False)
|
||||
return
|
||||
|
||||
self.rerunButton.setText("重新执行")
|
||||
self.rerunButton.setAccessibleName("重新执行当前选中的采集任务")
|
||||
self.autoFetchButton.setEnabled(True)
|
||||
has_selection = self.taskTable.selectionModel().hasSelection()
|
||||
self.autoFetchButton.setEnabled(not self._resubmit_running)
|
||||
self.rerunButton.setEnabled(
|
||||
has_selection and bool(self.current_task_id())
|
||||
not self._resubmit_running
|
||||
and len(self.taskModel.checked_task_ids()) == 1
|
||||
)
|
||||
self.resubmitButton.setEnabled(
|
||||
not self._resubmit_running
|
||||
and bool(self.taskModel.checked_task_ids())
|
||||
)
|
||||
|
||||
def _update_checked_actions(self, task_ids=None) -> None:
|
||||
"""同步勾选数量和批量命令启用状态。"""
|
||||
|
||||
checked = tuple(task_ids or ())
|
||||
self.checkedCountLabel.setText(f"已选 {len(checked)} 条")
|
||||
self.resubmitButton.setText(
|
||||
"正在上报…" if self._resubmit_running else "重新上报"
|
||||
)
|
||||
self.resubmitButton.setEnabled(bool(checked) and not self._resubmit_running)
|
||||
self._update_rerun_button()
|
||||
|
||||
def _apply_filters(self) -> None:
|
||||
self.taskModel.clear_checked_tasks()
|
||||
filters = {
|
||||
"task_type": self.taskTypeCombo.currentText(),
|
||||
"status": self.statusCombo.currentText(),
|
||||
|
||||
Reference in New Issue
Block a user