feat: 优化任务表批量选择和详情入口 (#131)
This commit is contained in:
+80
-25
@@ -12,7 +12,6 @@
|
||||
并用 `remote_task_id` 作为任务身份。**不要用表格行号**——
|
||||
行号会随排序和筛选变化,拿它当身份必出错。
|
||||
- 不要为每个单元格创建常驻 `QWidget`(几万行会把内存吃光)。
|
||||
“详情”那一列用委托绘制。
|
||||
- 不要把完整的 `pdd_data` 放进表格模型,那是详情数据,
|
||||
查看详情时再从数据库读。
|
||||
- 表格数据只增不减(已完成任务永久保留),所以增量加载是必须的,
|
||||
@@ -57,7 +56,6 @@ from qfluentwidgets import (
|
||||
|
||||
PAGE_SIZE = 50
|
||||
CHECK_COLUMN = 0
|
||||
DETAIL_COLUMN = 9
|
||||
REMOTE_TASK_ID_ROLE = Qt.UserRole + 1
|
||||
|
||||
|
||||
@@ -94,7 +92,6 @@ class TaskTableModel(QAbstractTableModel):
|
||||
"数量",
|
||||
"状态",
|
||||
"更新时间",
|
||||
"操作",
|
||||
)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
@@ -136,7 +133,7 @@ class TaskTableModel(QAbstractTableModel):
|
||||
return self._display_value(task, column)
|
||||
|
||||
if role == Qt.TextAlignmentRole:
|
||||
if column in (0, 1, 7, 9):
|
||||
if column in (0, 1, 7):
|
||||
return Qt.AlignCenter
|
||||
if column in (5, 6):
|
||||
return Qt.AlignRight | Qt.AlignVCenter
|
||||
@@ -145,8 +142,6 @@ class TaskTableModel(QAbstractTableModel):
|
||||
if role == Qt.ToolTipRole:
|
||||
if column == 2:
|
||||
return task.title or "尚未获取标题"
|
||||
if column == DETAIL_COLUMN:
|
||||
return f"查看任务 {task.remote_task_id} 的详情"
|
||||
|
||||
if role == REMOTE_TASK_ID_ROLE:
|
||||
return task.remote_task_id
|
||||
@@ -295,6 +290,36 @@ class TaskTableModel(QAbstractTableModel):
|
||||
)
|
||||
self.checkedTaskIdsChanged.emit(())
|
||||
|
||||
def check_all_loaded_tasks(self) -> None:
|
||||
"""勾选当前已经加载的真实任务,不包含占位行和后续分页。"""
|
||||
|
||||
loaded_task_ids = {task.remote_task_id for task in self._rows}
|
||||
if not loaded_task_ids or loaded_task_ids.issubset(
|
||||
self._checked_task_ids
|
||||
):
|
||||
return
|
||||
self._checked_task_ids.update(loaded_task_ids)
|
||||
self._emit_loaded_check_state_changed()
|
||||
|
||||
def invert_loaded_task_checks(self) -> None:
|
||||
"""反选当前已经加载的真实任务,不改变尚未加载的勾选。"""
|
||||
|
||||
loaded_task_ids = {task.remote_task_id for task in self._rows}
|
||||
if not loaded_task_ids:
|
||||
return
|
||||
self._checked_task_ids.symmetric_difference_update(loaded_task_ids)
|
||||
self._emit_loaded_check_state_changed()
|
||||
|
||||
def _emit_loaded_check_state_changed(self) -> None:
|
||||
"""批量勾选后一次刷新复选框列和已选数量。"""
|
||||
|
||||
self.dataChanged.emit(
|
||||
self.index(0, CHECK_COLUMN),
|
||||
self.index(len(self._rows) - 1, CHECK_COLUMN),
|
||||
[Qt.CheckStateRole],
|
||||
)
|
||||
self.checkedTaskIdsChanged.emit(self.checked_task_ids())
|
||||
|
||||
def uncheck_tasks(self, remote_task_ids: Iterable[str]) -> None:
|
||||
"""取消指定任务的勾选,供批量操作成功后更新界面。"""
|
||||
|
||||
@@ -356,7 +381,7 @@ class TaskTableModel(QAbstractTableModel):
|
||||
return task.status
|
||||
if column == 8:
|
||||
return task.updated_at or "—"
|
||||
return "详情"
|
||||
return ""
|
||||
|
||||
|
||||
class PDDTaskPage(QWidget):
|
||||
@@ -423,6 +448,18 @@ class PDDTaskPage(QWidget):
|
||||
self.refreshButton = PushButton(FIF.SYNC, "刷新", self)
|
||||
self.refreshButton.setAccessibleName("刷新本地任务表格")
|
||||
|
||||
self.selectAllButton = PushButton("全选", self)
|
||||
self.selectAllButton.setAccessibleName("勾选当前已加载的全部任务")
|
||||
self.selectAllButton.setToolTip("勾选当前已加载的全部任务")
|
||||
self.selectAllButton.setEnabled(False)
|
||||
|
||||
self.invertSelectionButton = PushButton("反选", self)
|
||||
self.invertSelectionButton.setAccessibleName(
|
||||
"反选当前已加载的任务"
|
||||
)
|
||||
self.invertSelectionButton.setToolTip("反选当前已加载的任务")
|
||||
self.invertSelectionButton.setEnabled(False)
|
||||
|
||||
self.rerunButton = PushButton(FIF.UPDATE, "重新执行", self)
|
||||
self.rerunButton.setAccessibleName("重新执行当前选中的采集任务")
|
||||
self.rerunButton.setEnabled(False)
|
||||
@@ -452,7 +489,7 @@ class PDDTaskPage(QWidget):
|
||||
if compact == self._compact_commands and self.commandLayout.count():
|
||||
return
|
||||
self._compact_commands = compact
|
||||
for column in range(14):
|
||||
for column in range(16):
|
||||
self.commandLayout.setColumnStretch(column, 0)
|
||||
|
||||
if compact:
|
||||
@@ -462,16 +499,18 @@ class PDDTaskPage(QWidget):
|
||||
(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.removeButton, 0, 9, 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.selectAllButton, 2, 5, 1, 1),
|
||||
(self.invertSelectionButton, 2, 6, 1, 1),
|
||||
(self.checkedCountLabel, 2, 7, 1, 1),
|
||||
(self.rerunButton, 2, 8, 1, 1),
|
||||
(self.resubmitButton, 2, 9, 1, 1),
|
||||
(self.removeButton, 2, 10, 1, 1),
|
||||
)
|
||||
self.commandLayout.setColumnStretch(5, 1)
|
||||
self.commandLayout.setColumnStretch(4, 1)
|
||||
else:
|
||||
positions = (
|
||||
(self.autoFetchButton, 0, 0, 1, 1),
|
||||
@@ -483,10 +522,12 @@ class PDDTaskPage(QWidget):
|
||||
(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.removeButton, 0, 13, 1, 1),
|
||||
(self.selectAllButton, 0, 10, 1, 1),
|
||||
(self.invertSelectionButton, 0, 11, 1, 1),
|
||||
(self.checkedCountLabel, 0, 12, 1, 1),
|
||||
(self.rerunButton, 0, 13, 1, 1),
|
||||
(self.resubmitButton, 0, 14, 1, 1),
|
||||
(self.removeButton, 0, 15, 1, 1),
|
||||
)
|
||||
self.commandLayout.setColumnStretch(6, 4)
|
||||
self.commandLayout.setColumnStretch(9, 1)
|
||||
@@ -500,6 +541,10 @@ class PDDTaskPage(QWidget):
|
||||
self.taskTable = TableView(self)
|
||||
self.taskTable.setModel(self.taskModel)
|
||||
self.taskTable.setAccessibleName("本机 PDD 任务列表")
|
||||
self.taskTable.setAccessibleDescription(
|
||||
"双击任务行或按 Enter 查看详情"
|
||||
)
|
||||
self.taskTable.setToolTip("双击任务行或按 Enter 查看详情")
|
||||
self.taskTable.setSelectionBehavior(QAbstractItemView.SelectRows)
|
||||
self.taskTable.setSelectionMode(QAbstractItemView.SingleSelection)
|
||||
self.taskTable.setEditTriggers(QAbstractItemView.NoEditTriggers)
|
||||
@@ -520,7 +565,6 @@ class PDDTaskPage(QWidget):
|
||||
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)
|
||||
@@ -568,11 +612,16 @@ class PDDTaskPage(QWidget):
|
||||
self.searchButton.clicked.connect(self._apply_filters)
|
||||
self.keywordInput.returnPressed.connect(self._apply_filters)
|
||||
self.refreshButton.clicked.connect(self.refreshRequested.emit)
|
||||
self.selectAllButton.clicked.connect(
|
||||
self.taskModel.check_all_loaded_tasks
|
||||
)
|
||||
self.invertSelectionButton.clicked.connect(
|
||||
self.taskModel.invert_loaded_task_checks
|
||||
)
|
||||
self.rerunButton.clicked.connect(self._request_rerun)
|
||||
self.resubmitButton.clicked.connect(self._request_resubmit)
|
||||
self.removeButton.clicked.connect(self._request_remove)
|
||||
self.clearFiltersButton.clicked.connect(self.clear_filters)
|
||||
self.taskTable.clicked.connect(self._on_table_clicked)
|
||||
self.taskTable.activated.connect(self._on_table_activated)
|
||||
self.taskModel.checkedTaskIdsChanged.connect(self._update_checked_actions)
|
||||
|
||||
@@ -587,7 +636,7 @@ class PDDTaskPage(QWidget):
|
||||
def resizeEvent(self, event) -> None:
|
||||
"""按内容区宽度切换顶部命令区的单行或双行布局。"""
|
||||
|
||||
self._update_command_layout(compact=event.size().width() < 1180)
|
||||
self._update_command_layout(compact=event.size().width() < 1460)
|
||||
super().resizeEvent(event)
|
||||
|
||||
def set_tasks(self, tasks: Iterable[TaskRow]) -> None:
|
||||
@@ -596,6 +645,7 @@ class PDDTaskPage(QWidget):
|
||||
current_task_id = self.current_task_id()
|
||||
self.taskModel.set_tasks(tasks)
|
||||
self._update_empty_state()
|
||||
self._update_checked_actions(self.taskModel.checked_task_ids())
|
||||
self._restore_current_task(current_task_id)
|
||||
|
||||
def begin_task_reload(self) -> None:
|
||||
@@ -612,6 +662,7 @@ class PDDTaskPage(QWidget):
|
||||
|
||||
self.taskModel.append_tasks(tasks, has_more)
|
||||
self._update_empty_state()
|
||||
self._update_checked_actions(self.taskModel.checked_task_ids())
|
||||
task_id = getattr(self, "_task_id_before_reload", "")
|
||||
self._restore_current_task(task_id)
|
||||
self._task_id_before_reload = ""
|
||||
@@ -723,6 +774,8 @@ class PDDTaskPage(QWidget):
|
||||
self.rerunButton.setEnabled(True)
|
||||
self.resubmitButton.setEnabled(False)
|
||||
self.removeButton.setEnabled(False)
|
||||
self.selectAllButton.setEnabled(False)
|
||||
self.invertSelectionButton.setEnabled(False)
|
||||
self.autoFetchButton.setEnabled(False)
|
||||
return
|
||||
if self._rerun_state == "cancelling":
|
||||
@@ -731,13 +784,18 @@ class PDDTaskPage(QWidget):
|
||||
self.rerunButton.setEnabled(False)
|
||||
self.resubmitButton.setEnabled(False)
|
||||
self.removeButton.setEnabled(False)
|
||||
self.selectAllButton.setEnabled(False)
|
||||
self.invertSelectionButton.setEnabled(False)
|
||||
self.autoFetchButton.setEnabled(False)
|
||||
return
|
||||
|
||||
self.rerunButton.setText("重新执行")
|
||||
self.rerunButton.setAccessibleName("重新执行当前选中的采集任务")
|
||||
idle = not self._resubmit_running and not self._remove_running
|
||||
has_tasks = self.taskModel.data_row_count() > 0
|
||||
self.autoFetchButton.setEnabled(idle)
|
||||
self.selectAllButton.setEnabled(idle and has_tasks)
|
||||
self.invertSelectionButton.setEnabled(idle and has_tasks)
|
||||
self.rerunButton.setEnabled(
|
||||
idle and len(self.taskModel.checked_task_ids()) == 1
|
||||
)
|
||||
@@ -770,12 +828,9 @@ class PDDTaskPage(QWidget):
|
||||
}
|
||||
self.searchRequested.emit(filters)
|
||||
|
||||
def _on_table_clicked(self, index: QModelIndex) -> None:
|
||||
if index.column() == DETAIL_COLUMN:
|
||||
self._emit_detail(index)
|
||||
|
||||
def _on_table_activated(self, index: QModelIndex) -> None:
|
||||
self._emit_detail(index)
|
||||
if index.column() != CHECK_COLUMN:
|
||||
self._emit_detail(index)
|
||||
|
||||
def _emit_detail(self, index: QModelIndex) -> None:
|
||||
task = self.taskModel.row_at(index.row())
|
||||
|
||||
@@ -49,6 +49,38 @@ class TaskTableModelCheckTests(unittest.TestCase):
|
||||
|
||||
self.assertEqual(model.checked_task_ids(), ())
|
||||
|
||||
def test_check_all_and_invert_only_loaded_real_tasks(self):
|
||||
model = TaskTableModel()
|
||||
changes = []
|
||||
model.checkedTaskIdsChanged.connect(changes.append)
|
||||
model.append_tasks(
|
||||
[TaskRow("TASK-1", "采集"), TaskRow("TASK-2", "采集")],
|
||||
has_more=True,
|
||||
)
|
||||
|
||||
model.check_all_loaded_tasks()
|
||||
|
||||
self.assertEqual(model.checked_task_ids(), ("TASK-1", "TASK-2"))
|
||||
self.assertEqual(len(changes), 1)
|
||||
|
||||
model.invert_loaded_task_checks()
|
||||
|
||||
self.assertEqual(model.checked_task_ids(), ())
|
||||
self.assertEqual(len(changes), 2)
|
||||
|
||||
def test_new_page_is_not_checked_after_select_all(self):
|
||||
model = TaskTableModel()
|
||||
model.append_tasks([TaskRow("TASK-1", "采集")], has_more=True)
|
||||
model.check_all_loaded_tasks()
|
||||
|
||||
model.append_tasks([TaskRow("TASK-2", "采集")], has_more=False)
|
||||
|
||||
self.assertEqual(model.checked_task_ids(), ("TASK-1",))
|
||||
self.assertEqual(
|
||||
model.data(model.index(1, CHECK_COLUMN), Qt.CheckStateRole),
|
||||
Qt.Unchecked,
|
||||
)
|
||||
|
||||
def test_search_clears_checks_and_refresh_is_next_to_search(self):
|
||||
page = PDDTaskPage()
|
||||
page.set_tasks([TaskRow("TASK-1", "采集")])
|
||||
@@ -56,26 +88,39 @@ class TaskTableModelCheckTests(unittest.TestCase):
|
||||
page.taskModel.index(0, CHECK_COLUMN), Qt.Checked, Qt.CheckStateRole
|
||||
)
|
||||
|
||||
page.resize(1300, 650)
|
||||
self.app.processEvents()
|
||||
page._update_command_layout(compact=False)
|
||||
layout = page.commandCard.layout()
|
||||
search_position = layout.getItemPosition(layout.indexOf(page.searchButton))
|
||||
refresh_position = layout.getItemPosition(layout.indexOf(page.refreshButton))
|
||||
rerun_position = layout.getItemPosition(layout.indexOf(page.rerunButton))
|
||||
resubmit_position = layout.getItemPosition(layout.indexOf(page.resubmitButton))
|
||||
remove_position = layout.getItemPosition(layout.indexOf(page.removeButton))
|
||||
select_all_position = layout.getItemPosition(
|
||||
layout.indexOf(page.selectAllButton)
|
||||
)
|
||||
invert_position = layout.getItemPosition(
|
||||
layout.indexOf(page.invertSelectionButton)
|
||||
)
|
||||
self.assertEqual(refresh_position[1], search_position[1] + 1)
|
||||
self.assertEqual(search_position[0], 0)
|
||||
self.assertEqual(select_all_position[0], 0)
|
||||
self.assertEqual(resubmit_position[1], rerun_position[1] + 1)
|
||||
self.assertEqual(remove_position[1], resubmit_position[1] + 1)
|
||||
self.assertEqual(invert_position[1], select_all_position[1] + 1)
|
||||
self.assertEqual(page.checkedCountLabel.text(), "已选 1 条")
|
||||
self.assertTrue(page.removeButton.isEnabled())
|
||||
self.assertTrue(page.selectAllButton.isEnabled())
|
||||
self.assertTrue(page.invertSelectionButton.isEnabled())
|
||||
|
||||
page.resize(1000, 650)
|
||||
self.app.processEvents()
|
||||
page._update_command_layout(compact=True)
|
||||
compact_search = layout.getItemPosition(layout.indexOf(page.searchButton))
|
||||
compact_refresh = layout.getItemPosition(layout.indexOf(page.refreshButton))
|
||||
compact_select_all = layout.getItemPosition(
|
||||
layout.indexOf(page.selectAllButton)
|
||||
)
|
||||
self.assertEqual(compact_search[0], 1)
|
||||
self.assertEqual(compact_refresh[1], compact_search[1] + 1)
|
||||
self.assertEqual(compact_select_all[0], 2)
|
||||
|
||||
page.searchButton.click()
|
||||
|
||||
@@ -84,6 +129,47 @@ class TaskTableModelCheckTests(unittest.TestCase):
|
||||
self.assertFalse(page.removeButton.isEnabled())
|
||||
page.deleteLater()
|
||||
|
||||
def test_selection_buttons_are_disabled_during_batch_work(self):
|
||||
page = PDDTaskPage()
|
||||
page.set_tasks([TaskRow("TASK-1", "采集")])
|
||||
|
||||
page.set_resubmit_running(True)
|
||||
|
||||
self.assertFalse(page.selectAllButton.isEnabled())
|
||||
self.assertFalse(page.invertSelectionButton.isEnabled())
|
||||
|
||||
page.set_resubmit_running(False)
|
||||
|
||||
self.assertTrue(page.selectAllButton.isEnabled())
|
||||
self.assertTrue(page.invertSelectionButton.isEnabled())
|
||||
page.deleteLater()
|
||||
|
||||
def test_selection_buttons_ignore_empty_placeholder_rows(self):
|
||||
page = PDDTaskPage()
|
||||
|
||||
self.assertFalse(page.selectAllButton.isEnabled())
|
||||
self.assertFalse(page.invertSelectionButton.isEnabled())
|
||||
page.selectAllButton.click()
|
||||
page.invertSelectionButton.click()
|
||||
|
||||
self.assertEqual(page.taskModel.checked_task_ids(), ())
|
||||
page.deleteLater()
|
||||
|
||||
def test_detail_has_no_column_and_checkbox_activation_does_not_open(self):
|
||||
page = PDDTaskPage()
|
||||
page.set_tasks([TaskRow("TASK-1", "采集")])
|
||||
received = []
|
||||
page.detailRequested.connect(received.append)
|
||||
|
||||
self.assertNotIn("操作", page.taskModel.HEADERS)
|
||||
self.assertEqual(page.taskModel.columnCount(), 9)
|
||||
page._on_table_activated(page.taskModel.index(0, CHECK_COLUMN))
|
||||
page._on_table_activated(page.taskModel.index(0, 2))
|
||||
|
||||
self.assertEqual(received, ["TASK-1"])
|
||||
self.assertIn("Enter", page.taskTable.accessibleDescription())
|
||||
page.deleteLater()
|
||||
|
||||
def test_remove_emits_stable_checked_task_ids(self):
|
||||
page = PDDTaskPage()
|
||||
page.set_tasks(
|
||||
|
||||
@@ -79,7 +79,7 @@ Admin 想知道某台 Client 是不是卡死了,用"领取后超时重派"就
|
||||
| 信号 / signal | Qt 里跨线程传消息的机制。后台线程算完了,用信号通知主线程去更新界面 | 后台结果回主线程的唯一正规途径 |
|
||||
| 模型/视图 | 表格分成两半:数据在"模型"里,显示在"视图"里。好处是几万行也不卡 | `TaskTableModel` + `TableView` |
|
||||
| 增量加载 | 表格先只加载 50 行,用户滚到底了再去数据库拿下一批 | `canFetchMore` / `fetchMore`,见 [05](05-ui-specification.md) §5.3 |
|
||||
| 委托 / delegate | 告诉表格"这一列要画成按钮/链接"的画笔对象。**比给每个格子塞一个真控件省资源得多** | "详情"那一列 |
|
||||
| 委托 / delegate | 控制表格单元格怎样绘制或编辑的对象。**比给每个格子塞一个真控件省资源得多** | 将来需要特殊绘制的表格列 |
|
||||
| InfoBar | Fluent 风格的横条提示。可以停着不消失,还能带按钮 | 可恢复错误的标准提示方式 |
|
||||
| 模态对话框 | 弹出来必须先处理它、否则动不了主窗口的窗口。**很打断人,只在必须做决定时用** | 高危确认 |
|
||||
| DPI / 显示缩放 | Windows 里"文字和应用的大小"那个百分比设置 | 需验证 100%~200% |
|
||||
|
||||
@@ -116,7 +116,7 @@ Client 应执行:
|
||||
- 默认按 `updated_at DESC, id DESC` 稳定排序。
|
||||
- 所有任务可通过增量加载访问,禁止为每个单元格创建常驻复杂控件。
|
||||
- `pdd_data` 是详情数据,不作为隐藏表格列整体加载;表格模型只保留稳定任务编号,查看详情时从数据库读取。
|
||||
- 单击行只选择,双击、Enter 或“详情”打开任务详情。
|
||||
- 单击行只选择,双击任务行或按 Enter 打开任务详情。
|
||||
|
||||
### 5.3 底部状态区
|
||||
|
||||
|
||||
@@ -35,9 +35,9 @@
|
||||
```text
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ PDD 任务 │
|
||||
│ [自动获取] [类型▼] [状态▼] [关键词...] [搜索] [刷新] [已选0条] [重新执行] [重新上报] [删除] │
|
||||
│ [自动获取] [类型▼] [状态▼] [关键词...] [搜索] [刷新] [全选] [反选] [已选0条] [重新执行] [重新上报] [删除] │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ 类型 │ 商品标题 │ 颜色 │ 尺码 │ 价格 │ 数量 │ 状态 │ 更新时间 │详情│
|
||||
│ 选择 │ 类型 │ 商品标题 │ 颜色 │ 尺码 │ 价格 │ 数量 │ 状态 │ 更新时间 │
|
||||
│ │
|
||||
│ 任务数据表格 │
|
||||
│ │
|
||||
@@ -83,10 +83,12 @@
|
||||
|
||||
筛选条件之间采用 AND。点击搜索或在关键词输入框按 Enter 执行本地数据库查询,不请求领取任务。活动筛选必须可见,筛选无结果时保留条件并提供清除入口。
|
||||
|
||||
### 4.3 刷新、重新执行、重新上报与删除
|
||||
### 4.3 刷新、全选、反选与批量操作
|
||||
|
||||
- “刷新”紧邻“搜索”右侧,只重新读取本地任务列表,不请求 Admin,也不操作手机。
|
||||
- 页面右侧显示“已选 N 条”,操作顺序是“重新执行”“重新上报”“删除”。
|
||||
- 页面右侧显示“全选”“反选”和“已选 N 条”,之后依次是“重新执行”“重新上报”“删除”。
|
||||
- “全选”和“反选”只作用于当前筛选条件下已经加载的真实任务行,空白占位行不参与。全选后新领取或继续增量加载的任务默认不勾选,防止批量操作范围在用户不知情时扩大。
|
||||
- 没有真实任务,或正在重新执行、重新上报、删除时,禁用“全选”和“反选”。
|
||||
- “重新执行”只在恰好勾选一条任务时启用;多选不允许批量操作手机。
|
||||
- 点击后先校验任务,再显示明确的“重新采集”确认弹窗。弹窗显示任务编号、商品标题,并说明新结果会覆盖 Client 和 Admin 的当前采集数据。“暂不重新采集”是默认聚焦的安全操作,点击该按钮、按 `Escape` 或关闭弹窗都不得启动任务。
|
||||
- 用户确认后,工作线程必须先检查已保存 Android 设备的实际连接状态,再重置任务和创建执行记录。检查失败时保留原任务状态、采集结果和执行历史。
|
||||
@@ -128,7 +130,6 @@
|
||||
| 数量 | 右对齐 | 采购数量;采集显示 `—` |
|
||||
| 状态 | 居中 | 中文状态文字和状态图标/标记 |
|
||||
| 更新时间 | 左对齐 | 本地时区,精确到秒或按产品确认格式 |
|
||||
| 操作 | 居中 | “详情”链接或委托绘制按钮 |
|
||||
|
||||
### 5.2 数据行为
|
||||
|
||||
@@ -138,8 +139,8 @@
|
||||
- 勾选集使用 `remote_task_id` 保存。普通刷新保留仍存在的勾选,搜索条件变化清空勾选,避免操作被筛选隐藏的旧任务。
|
||||
- 初始加载有限批次,滚动时通过 `canFetchMore/fetchMore` 增量加载后续任务。
|
||||
- 单击非交互区域只设置当前行和选择。
|
||||
- 双击行、按 Enter 或点击“详情”执行相同的非破坏性详情命令。
|
||||
- “详情”使用委托或链接语义,不为每行创建常驻 QWidget。
|
||||
- 双击任务行的非复选框区域或按 Enter 执行非破坏性的详情命令;复选框只改变勾选状态。
|
||||
- 表格提供“双击任务行或按 Enter 查看详情”的工具提示和无障碍说明。
|
||||
- 空状态要分情况,文案不能一样:
|
||||
- 加载中;
|
||||
- **从没领取过任务** —— “还没有领取过任务,点击‘获取任务’开始领取”(不要写“暂无数据”,操作人员会以为是出错了);
|
||||
@@ -212,7 +213,7 @@ class TaskTableModel(QAbstractTableModel):
|
||||
| 直接 `self._rows.append(...)` 不加 `beginInsertRows` | 界面不刷新,或者直接崩 |
|
||||
| 把整个 `pdd_data` 存进 `_rows` | 几万条 JSON 全在内存里,程序会变得很卡 |
|
||||
| 在 `fetchMore` 里发网络请求 | 这是主线程,界面会卡住。这里只能查本地 SQLite |
|
||||
| 给每个单元格 `setIndexWidget` 放按钮 | 几万个控件,内存直接爆。"详情"那列用委托画 |
|
||||
| 给每个单元格 `setIndexWidget` 放按钮 | 几万个控件会明显增加内存;任务详情使用行默认操作,不创建单元格按钮 |
|
||||
|
||||
换筛选之后要恢复用户原来选中的行:**先记下当前行的 `remote_task_id`,重新加载完再按这个编号找回来**,不要记行号。
|
||||
|
||||
@@ -220,7 +221,7 @@ class TaskTableModel(QAbstractTableModel):
|
||||
|
||||
详情是信息查看,不要求用户立即决策,优先使用非模态详情窗口或右侧详情面板,不使用简单消息框承载长 JSON。
|
||||
|
||||
当前 Client 使用可调整大小的非模态详情窗口。用户可以点击表格“详情”、双击任务行,或选中任务后按 Enter 打开。相同任务只保留一个详情窗口;再次打开时切回已有窗口。按 Esc 或“关闭”按钮返回任务列表。
|
||||
当前 Client 使用可调整大小的非模态详情窗口。用户可以双击任务行,或选中任务后按 Enter 打开。相同任务只保留一个详情窗口;再次打开时切回已有窗口。按 Esc 或“关闭”按钮返回任务列表。
|
||||
|
||||
详情面向采购人员,任务类型、状态、当前步骤和错误说明优先使用中文。数据库中的 `current_step` 等内部值只用于业务判断和日志,不直接显示;遇到尚未适配的新步骤时显示“未知步骤”。
|
||||
采购演练和恢复至少要明确显示“演练已在提交前停止”、“结果待提交”、
|
||||
@@ -410,7 +411,7 @@ self.show_recoverable_error(
|
||||
|
||||
## 10. 键盘与无障碍
|
||||
|
||||
- Tab 顺序:自动获取 → 类型 → 状态 → 关键词 → 搜索 → 刷新 → 重新执行 → 重新上报 → 删除 → 表格 → 状态区可操作项。
|
||||
- Tab 顺序:自动获取 → 类型 → 状态 → 关键词 → 搜索 → 刷新 → 全选 → 反选 → 重新执行 → 重新上报 → 删除 → 表格 → 状态区可操作项。
|
||||
- `Ctrl+F` 聚焦关键词,Enter 打开当前行详情。不绑定 `F5`——界面上没有需要刷新的远端数据。
|
||||
- 仅图标按钮必须设置准确的无障碍名称和工具提示。
|
||||
- 表单具有可见标签,占位符不能替代标签。
|
||||
|
||||
Reference in New Issue
Block a user