Files
cmautobuy/docs/client/05-ui-specification.md
T

449 lines
28 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 05 Client 界面交互规范
- 文档状态:基线草案,待界面评审
- 技术栈:PyQt5、Qt Widgets、PyQt-Fluent-Widgets
- 当前验证组件版本:PyQt-Fluent-Widgets 1.11.3(版本以 `client/requirements.txt` 为准)
本文档中 `[必须]` / `[建议]` / `[待定]` 的含义见 [文档索引](../README.md#文档标注说明)。没有标注的默认是 `[必须]`。看不懂的词查 [术语表](00-glossary.md)。
## 1. 设计目标
- 让操作人员在一个页面完成任务监控、搜索和异常定位。
- “获取任务”“重新采集”“重新采购”和“重新上报”会产生外部后果;重新上报只重发既有 Outbox,并优先处理未发送事件。“删除”只软隐藏本地记录。
- 长任务状态始终可找到,不使用连续模态弹窗打断工作。
- 任务表格在数据增长后仍保持响应速度、稳定选择和可访问性。
- 界面只展示任务状态,不在 Qt 主线程执行 Admin 或手机自动化。
## 2. 应用外壳
主窗口使用 `FluentWindow`,顶级导航保持稳定顺序:
1. **PDD 任务**:主要业务页面。
2. **设置**:固定在导航底部。
启动检查确认存在新版本时,设置导航显示“有更新”文字并使用提醒色,不能只靠颜色表达。
只有确认线上版本不高于本地版本时才清除提醒;进入设置页不会自动清除提醒,检查失败也
不会制造更新提示。软件更新区域同时显示本地版本和线上版本。
页面必须使用稳定对象名:
- `pddTaskPage`
- `settingsPage`
> **现状提醒:** 当前代码里是 3 个导航项(pdd / 设备 / 设置),和这里写的 2 个不一样。这是已知差异,见 [02 架构 §3.1](02-architecture.md)。设备相关设置最终要并入设置页,但要按工单单独做,不要顺手改。
Client 首次显示主窗口时使用标准 Windows 最大化状态,不使用全屏模式。用户仍可通过标题栏
最小化、还原、调整大小、再次最大化或关闭窗口。窗口变窄时允许表格水平滚动和顶部命令
换行,不能隐藏主要任务入口。
## 3. PDD 任务页布局
```text
┌─────────────────────────────────────────────────────────────┐
│ PDD 任务 │
│ [自动获取] [类型▼] [状态▼] [关键词...] [搜索] [刷新] [全选] [反选] [已选0条] [重新上报] [重新采集] [重新采购] [删除] │
├─────────────────────────────────────────────────────────────┤
│ 选择 │ 类型 │ 商品标题 │ 店铺名 │ 颜色 │ 尺码 │ 价格 │ 数量 │ 状态 │ 用时 │ 更新时间 │
│ │
│ 任务数据表格 │
│ │
├─────────────────────────────────────────────────────────────┤
│ 自动获取:已开启 · 当前 PDD-0001 · 正在采集 SKU · 已完成 42 个 │
└─────────────────────────────────────────────────────────────┘
```
垂直区域依次为顶部命令区、可伸展表格区和稳定状态区。表格获得全部剩余高度,状态区不得遮挡最后一行或滚动条。
## 4. 顶部命令区
### 4.1 自动获取 / 停止自动获取
- 使用 `PrimaryPushButton`,是页面唯一主要强调操作。
- 初始文本为“自动获取”,图标表达开始。
- 启动成功后文本变为“停止自动获取”,按钮位置和宽度尽量稳定。
- `[必须]` 这两个文案是**按钮标签**。状态栏里的“自动获取:已开启/已停止”说的是**功能状态**,两者不是一回事,不要混改。
- 启动过程中禁用重复点击,显示“正在启动”。
- 停止表示不再领取新任务;当前任务进入安全停止流程。
- 如果设备、Admin 或必要设置无效,按钮可禁用,但附近必须说明缺少的条件。
每一轮先补交一条本地 Outbox;若无待提交结果则执行最早的本地采集任务,再没有
才向 Admin 领取一条。领取、手机采集和提交都在单独的工作线程完成,一轮结束且
线程完全退出后,由主线程的单次定时器安排下一轮。暂无任务时默认 5 秒后重试;
可恢复的 Admin 错误按 5、10、20、30 秒退避。需要人工处理、不可恢复错误、设备
或配置错误会停止自动获取。采购 Adapter 和 Android 设备都准备好时,可以领取
采购任务;任务详情必须按任务的不可变执行模式明确显示“演练”或“真实下单”。
- 补交本地 Outbox 不依赖手机,可以在 Android 设备断开时继续执行。
- 执行本地任务或向 Admin 领取新任务前,工作线程必须用 `adb devices -l` 检查已保存的精确设备号。USB 和 Wi-Fi 设备都只接受 `device` 状态。
- 设备未连接、`offline`、`unauthorized` 或 ADB 检查失败时,立即停止本轮和自动获取;不得领取新任务,也不得启动本地任务。
- 采集、采购、重新采集和设备错误使用较大的 `InfoBar` 提示,显示在软件窗口顶部水平居中,5 秒后自动关闭,并提供明显的“关闭提示”按钮。设备错误另外提供“打开设置”;重复错误替换旧提示,不得堆叠。完整错误原因保留在底部状态区。
### 4.2 搜索与筛选
建议控件:
- 任务类型:`ComboBox`,选项为“全部、采集、采购”。
- 任务状态:`ComboBox`,选项为“全部”及标准任务状态。
- 关键词:`SearchLineEdit`,提示“任务编号、商品编号或商品标题”;建议宽度 150~240,随命令区布局自适应。
- 搜索:普通 `PushButton`。
筛选条件之间采用 AND。点击搜索或在关键词输入框按 Enter 执行本地数据库查询,不请求领取任务。活动筛选必须可见,筛选无结果时保留条件并提供清除入口。
### 4.3 刷新、全选、反选与批量操作
- “刷新”紧邻“搜索”右侧,只重新读取本地任务列表,不请求 Admin,也不操作手机。
- 页面右侧显示“全选”“反选”和“已选 N 条”,之后依次是“重新上报”“重新采集”“重新采购”“删除”。
- “全选”和“反选”只作用于当前筛选条件下已经加载的真实任务行,空白占位行不参与。全选后新领取或继续增量加载的任务默认不勾选,防止批量操作范围在用户不知情时扩大。
- 没有真实任务,或正在重新执行、重新上报、删除时,禁用“全选”和“反选”。
- 勾选一条或多条任务后可以点击“重新采集”或“重新采购”。点击后先批量预检,再显示可执行、类型过滤和安全阻止数量;“取消”默认聚焦,点击、按 `Escape` 或关闭弹窗都不得启动任务。
- 用户确认后,工作线程必须先检查已保存 Android 设备的实际连接状态,再重置任务和创建执行记录。检查失败时保留原任务状态、采集结果和执行历史。
- 重新采集只处理采集任务,重新采购只处理采购任务,另一类型计入过滤数量。执行中、结果待提交、仍有未发送 Outbox、已经排队或自动获取忙碌时必须阻止,并用中文说明原因。
- 确认后只执行选中的稳定任务编号,不领取新任务,不先处理其他任务或 Outbox。
- 重新采集在工作线程运行。执行期间禁用“获取任务”,“重新执行”变为可点击的“停止重新采集”。
- 点击停止后按钮显示“正在停止…”并禁用重复点击;底部明确说明正在等待手机当前操作结束。uiautomator2/ADB 的单次调用返回后,采集在下一个安全检查点停止;不得强制结束工作线程。
- 重新采集的前置校验警告和后台错误 `InfoBar` 都显示在软件窗口顶部水平居中,并有可见的“关闭提示”按钮;提示 5 秒后自动关闭,同一时间只保留一条,新提示替换旧提示。
- 每次重新采集创建新的执行记录和幂等键;当前结果更新,旧结果保存在历史执行记录中。
- 重新采购逐条串行执行。任一任务未形成“执行、核单、上报”成功闭环时立即停止剩余队列。历史上任何一次执行已有 `irreversible_action_at` 的任务永久禁止重新采购,只能只读核单。
- 用户确认批量操作后立即刷新列表,并在状态区显示已加入队列的数量。排队中的任务保持原状态;
只有工作线程已经把当前任务写成 `running` 后,才通知主线程刷新该行并显示“采集中”或
“采购中”。每条完成、失败、跳过或取消后再次刷新,再开始下一条。
- 自动流程对每条任务只执行一次,不显示“等待重试”。普通失败按任务类型显示
“采集失败”或“采购失败”,详情保留具体原因;失败信息成功上报后继续下一条。
只有设备、登录、验证码、风控、Admin 或不可逆采购等全局问题才停止队列。
- “重新上报”作用于当前已经加载并勾选的任务。执行前显示任务数量,并明确说明不会重新采集、采购或操作手机。
- 重新上报先查找每条任务最早一条尚未发送的 Outbox,包括 `task_failure`;全部事件都已发送时,才重发最新的 `collect_result` 或 `purchase_result`。
- 重新上报使用事件原来的 `idempotency_key` 和 `payload_json`,不重新组装数据、不创建新 Outbox。历史结果再次得到 Admin 确认时不得覆盖任务最新执行状态;最新失败信息上报成功后应恢复对应失败状态。
- 重新上报不依赖 Android 设备,在独立工作线程中逐条提交。自动获取、重新执行或另一批上报运行时不得启动。
- 批量完成后显示成功、失败和跳过数量。成功任务取消勾选;失败及没有结果 Outbox 的跳过任务保留勾选,方便继续处理。
- 相同幂等键和相同内容只用于让 Admin 再次确认已接收,不代表创建新结果或覆盖 Admin 数据。
- “删除”作用于当前勾选集。未勾选时禁用;执行前用主窗口居中的确认框显示准确数量,“暂不删除”默认聚焦,按 `Escape` 或关闭弹窗不修改数据。
- 删除只允许已完成、失败或已取消的任务。存在待发送、发送中、发送失败 Outbox,或任一执行记录已进入不可逆阶段时,整批拒绝并显示中文原因。
- 删除只把任务从普通列表软隐藏,不请求 Admin、不操作 Android,也不修改任务状态、执行记录或 Outbox。成功后清除对应勾选并刷新当前筛选结果。
## 5. 任务表格
表格显示的是**本机已领取且未被软隐藏的任务**,包括正在做的和早已做完的。已完成任务和审计数据永久保留;用户删除后只是不再出现在普通列表,所以数据库数据仍只增不减——增量加载和索引是必须的,不是优化。
使用 `qfluentwidgets.TableView`、自定义 `QAbstractTableModel` 和 Repository 查询。不得使用行号作为任务身份,也不得把完整 `pdd_data` 放入模型。
### 5.1 列定义
| 列 | 对齐 | 显示规则 |
|---|---|---|
| 选择 | 居中 | 行首复选框;占位空行不可勾选,身份使用 `remote_task_id` |
| 任务类型 | 居中 | “采集”或“采购”,颜色只作辅助 |
| 商品标题 | 左对齐、可伸展 | 空值显示“尚未获取标题”,截断时提供完整工具提示 |
| 店铺名 | 左对齐 | 采集到的店铺名;无值显示 `—` |
| 颜色 | 左对齐 | 无值显示 `—` |
| 尺码 | 左对齐 | 无值显示 `—` |
| 价格 | 右对齐 | 格式为 `¥39.90`;采集摘要可显示 `¥39.90 起` |
| 数量 | 右对齐 | 采购数量;采集显示 `—` |
| 状态 | 居中 | 中文状态文字和状态图标/标记 |
| 用时 | 居中 | 已结束显示整数秒,执行中显示“进行中”,尚未执行显示 `—` |
| 更新时间 | 左对齐 | 本地时区,精确到秒或按产品确认格式 |
### 5.2 数据行为
- 默认排序为 `updated_at DESC, id DESC`。
- 排序、筛选和数据变化后以稳定任务编号恢复当前行。
- 当前行和勾选集相互独立;点击复选框不打开详情,也不启动任何业务操作。
- 勾选集使用 `remote_task_id` 保存。普通刷新保留仍存在的勾选,搜索条件变化清空勾选,避免操作被筛选隐藏的旧任务。
- 初始加载有限批次,滚动时通过 `canFetchMore/fetchMore` 增量加载后续任务。
- 单击非交互区域只设置当前行和选择。
- 双击任务行的非复选框区域或按 Enter 执行非破坏性的详情命令;复选框只改变勾选状态。
- 表格提供“双击任务行或按 Enter 查看详情”的工具提示和无障碍说明。
- 空状态要分情况,文案不能一样:
- 加载中;
- **从没领取过任务** —— “还没有领取过任务,点击‘获取任务’开始领取”(不要写“暂无数据”,操作人员会以为是出错了);
- 筛选无结果 —— 提供清除条件入口;
- Admin 离线 —— 本地数据照常显示,只在信息条说明领取失败;
- 加载失败。
> **界面上说的"任务编号"一律指 `remote_task_id`**(例如 `PDD-20260806-0001`),不是数据库自增 `id`,更不是表格行号。行号会随排序和筛选变化,拿它当任务身份必出错。
### 5.3 增量加载模板(照抄即可)
任务可能有几万条,一次全查出来界面会卡住。做法是:先查 50 条,用户滚到底了再查下一批。Qt 的模型/视图自带这个机制,实现 `canFetchMore` 和 `fetchMore` 两个方法就行。
```python
from PyQt5.QtCore import QAbstractTableModel, QModelIndex, Qt
PAGE_SIZE = 50
class TaskTableModel(QAbstractTableModel):
"""任务表格的数据来源。只存显示需要的几个字段。"""
def __init__(self, repository, parent=None):
super().__init__(parent)
self._repository = repository
self._rows: list[dict] = [] # 已经加载进来的行
self._filters: dict = {} # 当前搜索条件
self._has_more = True # 数据库里还有没有没取完的
def rowCount(self, parent=QModelIndex()) -> int:
return 0 if parent.isValid() else len(self._rows)
def canFetchMore(self, parent=QModelIndex()) -> bool:
"""Qt 会自己调用它,问"还能再加载吗"。"""
return not parent.isValid() and self._has_more
def fetchMore(self, parent=QModelIndex()) -> None:
"""Qt 在用户滚到底时自己调用它。"""
if parent.isValid():
return
page = self._repository.list_tasks(
filters=self._filters, limit=PAGE_SIZE, offset=len(self._rows)
)
if not page:
self._has_more = False
return
# 必须用 begin/end 包住,直接改 self._rows 界面不会刷新
start = len(self._rows)
self.beginInsertRows(QModelIndex(), start, start + len(page) - 1)
self._rows.extend(page)
self.endInsertRows()
self._has_more = len(page) == PAGE_SIZE
def apply_filters(self, filters: dict) -> None:
"""换搜索条件时整表重来。"""
self.beginResetModel()
self._filters = filters
self._rows = []
self._has_more = True
self.endResetModel()
```
注意:
| 别这么做 | 为什么 |
|---|---|
| 直接 `self._rows.append(...)` 不加 `beginInsertRows` | 界面不刷新,或者直接崩 |
| 把整个 `pdd_data` 存进 `_rows` | 几万条 JSON 全在内存里,程序会变得很卡 |
| 在 `fetchMore` 里发网络请求 | 这是主线程,界面会卡住。这里只能查本地 SQLite |
| 给每个单元格 `setIndexWidget` 放按钮 | 几万个控件会明显增加内存;任务详情使用行默认操作,不创建单元格按钮 |
换筛选之后要恢复用户原来选中的行:**先记下当前行的 `remote_task_id`,重新加载完再按这个编号找回来**,不要记行号。
## 6. 任务详情
详情是信息查看,不要求用户立即决策,优先使用非模态详情窗口或右侧详情面板,不使用简单消息框承载长 JSON。
当前 Client 使用可调整大小的非模态详情窗口。用户可以双击任务行,或选中任务后按 Enter 打开。相同任务只保留一个详情窗口;再次打开时切回已有窗口。按 Esc 或“关闭”按钮返回任务列表。
详情中的字段值支持鼠标选择和键盘选择。双击字段值或在字段获得焦点后按 `Ctrl+C`,
复制该字段的完整文字,并在窗口底部短暂显示“已复制:字段名”。颜色名称、颜色价格和
尺码分别复制;字段名称和章节标题不可复制。商品链接只复制,不自动打开浏览器。
详情面向采购人员,任务类型、状态、当前步骤和错误说明优先使用中文。数据库中的 `current_step` 等内部值只用于业务判断和日志,不直接显示;遇到尚未适配的新步骤时显示“未知步骤”。
采购演练和恢复至少要明确显示“演练已在提交前停止”、“结果待提交”、
“只允许核对订单”和“需要人工处理”;不能只用颜色表达安全状态。
真实提交后的详情还要区分“订单已提交,只允许核对未付款订单”、
“已提交待付款,等待向 Admin 上报”和“订单结果不确定或存在多个候选”。
采集规格按下面顺序展示:
1. **颜色:**放在上方,每个颜色旁边直接显示采集到的价格;未采集到价格时显示“价格未采集”。同一颜色意外出现多个价格时显示最低价到最高价,并显示“价格不一致”,方便检查采集结果。
2. **尺码:**放在颜色下方,只显示尺码文字。
颜色和尺码按采集顺序显示并去重。不可用颜色除文字外还要明确显示“不可用”,不能只改变颜色。没有规格数据时保留对应分区并显示空状态。
详情至少分为:
1. **基本信息:**任务编号、类型、商品、规格、数量、金额和状态。
2. **执行状态:**当前步骤、设备、尝试次数、开始/结束时间和最近错误。
3. **PDD 数据:**标题、店铺、销量、评价、规格维度和 SKU 表格。
4. **采购结果:**演练/真实模式、确认规格、价格、订单编号、下单时间和核对状态。
5. **原始数据与诊断:**格式化 `admin_payload`、`pdd_data`、日志和 Artifact 入口。
原始 JSON 默认折叠,提供复制或导出操作;复制前不得包含凭据和敏感信息。
## 7. 底部状态区
状态区占据稳定位置,示例:
```text
自动获取:已开启 · 当前任务 PDD-0001(采集)· 正在遍历规格 · 最近领取 16:20:31
```
应表达:
- 引擎状态:已停止、轮询中、领取中、执行中、提交中、退避等待、正在停止。
- 当前任务和当前步骤。
- 最近一次领取到任务的时间。
- 可恢复错误摘要及下一步。
高频轮询无任务时不要连续弹提示。设备断开、认证失败、结果提交持续失败等需要操作的问题使用 `InfoBar`,并提供“重试”“打开设置”或“查看详情”。
## 8. 设置页
设置页使用可滚动布局和 Fluent 设置卡片,分组如下。
### 当前设备
- 第一行依次显示设备号和设备名;设备号只读,设备名可以修改,两组输入随窗口宽度平均伸缩;
- 第二行显示“管理端地址”,对应 SQLite 中已有的 `admin.base_url`;默认地址为 `https://buy.833729.com`,没有保存值或保存值仍是旧默认地址 `http://127.0.0.1:8080` 时自动迁移,用户手动保存的其他地址不得覆盖;
- 点击“保存”前先验证地址;只接受有效的 HTTP/HTTPS 地址,不能包含账号、密码、查询参数或片段。验证失败时不保存设备号、设备名或地址,并把焦点放回地址输入框;
- 保存成功后立即用新地址登记 Client,后续任务领取、结果提交和失败提交也统一切换到新地址;已经发出的请求等待自身结束,不强制中断;
- 保存不新增数据库表,不增加心跳,也不连接或操作 Android 设备。
### Admin
- 服务地址;
- Client 编号;
- 认证状态,不直接回显完整令牌;
- 请求超时;
- “测试连接”及最近结果。
### Android 设备
- Client 只使用发布包内置的 ADB,不回退系统 `Path`;启动时缺少 `adb.exe` 或配套 DLL,应在主窗口中央弹出带明显关闭按钮的中文提示,并禁用需要 ADB 的设备命令;
- ADB 地址;
- PDD 包名;
- “测试连接”;
- 当前设备型号、Android 版本和 PDD 当前状态摘要。
- 点击设备“搜索”时可以先尝试恢复已保存的 Wi-Fi ADB,但单台设备恢复失败
不能中断普通设备枚举。表格仍展示 `adb devices -l` 实际找到的 USB 和 Wi-Fi
设备,状态文字同时说明恢复失败原因以及“已保存配置仍保留”。
- 只有普通设备枚举本身失败时才显示“搜索失败”并保留旧表格;程序不得因为
设备暂时离线自动删除已保存配置。
### 自动化
- 轮询周期;
- 任务超时;
- 最大重试次数;
- 演练模式开关。
### 真实采购运行就绪(不支付)
- 设置页不显示真实采购授权、确认文字或启用/关闭按钮;
- Client 身份和 Android 设备保存后,真实采购 Adapter 就绪的 Client 自动登记 live 能力;
- 身份、设备或 Adapter 任一未就绪时不领取真实采购任务,页面继续通过现有设备状态说明缺失项;
- 自动就绪只允许创建未付款订单,不允许自动付款。
### 安全与诊断
- 最大购买数量;
- 价格允许偏差;
- Artifact 目录;
- 保留天数;
- 打开日志目录。
### 软件更新
- 显示只读的当前版本;
- 清单地址独占一行;账号和密码在下一行并排显示,输入区域按约 45:55 自适应伸缩;
- 三个字段都有可见标签,密码框默认隐藏且不回填;
- “保存”把 URL/账号写入 SQLite,把密码写入 Windows 凭据管理器;已有密码时留空
表示不修改,修改账号时必须重新输入密码;
- “检查更新”只使用已经保存的配置;存在未保存修改时先提示保存;检查和下载期间
输入框及按钮禁用,防止并行任务和配置竞态;
- 已保存完整配置时启动后自动检查一次;没有新版或网络失败不弹模态框;
- 状态文字稳定显示未配置、检查中、已是最新、发现新版、下载进度、已准备、失败和
恢复建议;
- 发现新版本时使用有明确“下载更新 / 暂不下载”的确认框;下载完成不强制重启,
提示操作人员完成当前任务后自行关闭并重新启动。
设置采用显式“保存设置”或项目统一的即时保存模式,不能在同一页面随机混用。MVP 推荐显式保存,验证失败时保留输入并聚焦第一个错误字段。
## 9. 状态与反馈
| 场景 | 反馈方式 |
|---|---|
| 领取到新任务 | 更新表格和状态区,不弹模态框 |
| 暂时没有可领取的任务 | 只更新状态区,**不要连续弹提示** |
| 搜索无结果 | 表格空状态和清除条件入口 |
| Admin 暂时离线 | 窗口顶部居中的较大信息条,5 秒后自动关闭;底部保留错误,本地数据继续可用 |
| 设备断开 | 窗口顶部居中的较大信息条,提供打开设置和关闭提示,5 秒后自动关闭 |
| 任务运行 | 底部状态区和必要进度,不阻塞整个窗口 |
| 任务失败 | 行状态、详情和信息条,不显示原始堆栈 |
| 多个订单候选 | 状态改为“需要人工处理”,打开详情决策 |
| 普通任务成功 | 更新行和状态区,不弹“成功”对话框 |
| 更新检查或下载失败 | 软件更新卡片保留地址并显示原因和重试方式,当前程序不变 |
| 更新设置保存成功 | 状态区提示已安全保存,密码输入框清空并显示已保存占位文字 |
| 更新下载完成 | 软件更新卡片持续提示“下次启动生效”,不强制关闭程序 |
### 9.1 错误提示模板(照抄即可)
规则很简单:**普通成功什么都不弹**(更新界面就够了);**出错用 `InfoBar`**;**只有必须让用户当场做决定时才用对话框**。
```python
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QPushButton
from qfluentwidgets import InfoBar, InfoBarPosition
def show_recoverable_error(self, title: str, content: str, on_retry) -> None:
"""显示一条可重试的错误提示。
title:一句话说清出了什么事
content:说清已经保留了什么、下一步该干什么
on_retry:点"重试"时调用的函数
"""
bar = InfoBar.error(
title=title,
content=content,
orient=Qt.Horizontal,
isClosable=True,
position=InfoBarPosition.TOP_RIGHT,
duration=-1, # -1 = 不自动消失。重要错误必须用 -1
parent=self,
)
retry_button = QPushButton("重试", bar)
retry_button.clicked.connect(on_retry)
retry_button.clicked.connect(bar.close)
bar.addWidget(retry_button)
```
调用示例:
```python
self.show_recoverable_error(
title="Admin 连接失败",
content="本地任务仍可查看,结果已保存,联网后会自动提交。",
on_retry=self._sync_task_list,
)
```
写提示语的三条要求(对应 [06 质量与安全](06-quality-security.md) §5):
1. **说清发生了什么** —— "Admin 连接失败",不是"操作失败"。
2. **说清保住了什么** —— "结果已保存"。用户最怕的是白干。
3. **说清下一步** —— 给"重试"按钮,或者给"打开设置"。
不要做的事:
| 别这么做 | 改成 |
|---|---|
| 把 Python 异常堆栈贴到界面上 | 界面显示人话,堆栈写进日志 |
| 轮询没任务也弹一条提示 | 只更新底部状态区 |
| 用 `QMessageBox` 报告普通错误 | 用 `InfoBar`,别打断用户 |
| 错误提示 3 秒自动消失 | 重要错误 `duration=-1`,让用户自己关 |
## 10. 键盘与无障碍
- Tab 顺序:自动获取 → 类型 → 状态 → 关键词 → 搜索 → 刷新 → 全选 → 反选 → 重新上报 → 重新采集 → 重新采购 → 删除 → 表格 → 状态区可操作项。
- `Ctrl+F` 聚焦关键词,Enter 打开当前行详情。不绑定 `F5`——界面上没有需要刷新的远端数据。
- 仅图标按钮必须设置准确的无障碍名称和工具提示。
- 表单具有可见标签,占位符不能替代标签。
- 状态、错误和任务类型不能只依赖颜色。
- 表格焦点、当前行和选择状态必须可区分。
- 支持浅色、深色、高对比度和 100%–200% 显示缩放。
## 11. 原型与验收
该页面属于主要界面改版。正式重写桌面界面前,建议先制作使用模拟数据的可运行 HTML 原型,确认信息架构、列宽、筛选、详情和状态变化。HTML 原型只用于确认交互,不能替代 PyQt 的窗口、性能、DPI 和无障碍验证。
桌面实现至少验证:
- 空数据、少量数据、大量数据和超长标题;
- 紧凑、默认、最大化和 Windows 贴靠;
- 筛选和增量加载后的当前行保持;
- 快速重复点击、后台迟到结果和窗口关闭;
- 键盘、浅色、深色、高对比度和 200% 缩放。