fix: 放大任务错误提示并自动关闭 (#84)
This commit is contained in:
@@ -95,6 +95,12 @@ TASK_STATUS_TEXT = {
|
|||||||
TaskStatus.CANCELLED: "已取消",
|
TaskStatus.CANCELLED: "已取消",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ERROR_FEEDBACK_DURATION_MS = 5_000
|
||||||
|
ERROR_FEEDBACK_MIN_WIDTH = 520
|
||||||
|
ERROR_FEEDBACK_MIN_HEIGHT = 112
|
||||||
|
ERROR_FEEDBACK_BUTTON_MIN_WIDTH = 120
|
||||||
|
ERROR_FEEDBACK_BUTTON_MIN_HEIGHT = 40
|
||||||
|
|
||||||
|
|
||||||
class ClaimTaskWorker(QObject):
|
class ClaimTaskWorker(QObject):
|
||||||
"""在后台补交、领取或执行至多一条任务。"""
|
"""在后台补交、领取或执行至多一条任务。"""
|
||||||
@@ -233,6 +239,7 @@ class PDDTaskPageEvent(QObject):
|
|||||||
self._claim_worker: Optional[ClaimTaskWorker] = None
|
self._claim_worker: Optional[ClaimTaskWorker] = None
|
||||||
self._rerun_cancel_requested = False
|
self._rerun_cancel_requested = False
|
||||||
self._rerun_feedback: Optional[InfoBar] = None
|
self._rerun_feedback: Optional[InfoBar] = None
|
||||||
|
self._claim_feedback: Optional[InfoBar] = None
|
||||||
self._device_feedback: Optional[InfoBar] = None
|
self._device_feedback: Optional[InfoBar] = None
|
||||||
self._auto_fetch_running = False
|
self._auto_fetch_running = False
|
||||||
self._stop_requested = False
|
self._stop_requested = False
|
||||||
@@ -384,6 +391,7 @@ class PDDTaskPageEvent(QObject):
|
|||||||
|
|
||||||
assert self._claim_gateway is not None
|
assert self._claim_gateway is not None
|
||||||
self._close_rerun_feedback()
|
self._close_rerun_feedback()
|
||||||
|
self._close_claim_feedback()
|
||||||
self._close_device_feedback()
|
self._close_device_feedback()
|
||||||
self._rerun_cancel_requested = False
|
self._rerun_cancel_requested = False
|
||||||
self._claim_busy = True
|
self._claim_busy = True
|
||||||
@@ -498,13 +506,13 @@ class PDDTaskPageEvent(QObject):
|
|||||||
self._replace_rerun_feedback(bar)
|
self._replace_rerun_feedback(bar)
|
||||||
|
|
||||||
def _show_rerun_error(self, title: str, content: str) -> None:
|
def _show_rerun_error(self, title: str, content: str) -> None:
|
||||||
"""显示一条可明确关闭的重新采集错误。"""
|
"""显示一条较大、可明确关闭且会自动消失的重新采集错误。"""
|
||||||
|
|
||||||
bar = InfoBar.error(
|
bar = InfoBar.error(
|
||||||
title=title,
|
title=title,
|
||||||
content=content,
|
content=content,
|
||||||
isClosable=True,
|
isClosable=True,
|
||||||
duration=-1,
|
duration=ERROR_FEEDBACK_DURATION_MS,
|
||||||
position=InfoBarPosition.TOP_RIGHT,
|
position=InfoBarPosition.TOP_RIGHT,
|
||||||
parent=self._page,
|
parent=self._page,
|
||||||
)
|
)
|
||||||
@@ -515,7 +523,9 @@ class PDDTaskPageEvent(QObject):
|
|||||||
|
|
||||||
self._close_rerun_feedback()
|
self._close_rerun_feedback()
|
||||||
self._rerun_feedback = bar
|
self._rerun_feedback = bar
|
||||||
|
self._set_large_error_feedback_size(bar)
|
||||||
close_button = PushButton("关闭提示", bar)
|
close_button = PushButton("关闭提示", bar)
|
||||||
|
self._set_large_feedback_button_size(close_button)
|
||||||
close_button.setAccessibleName("关闭重新采集提示")
|
close_button.setAccessibleName("关闭重新采集提示")
|
||||||
close_button.clicked.connect(
|
close_button.clicked.connect(
|
||||||
lambda _checked=False, current=bar: self._close_rerun_feedback(current)
|
lambda _checked=False, current=bar: self._close_rerun_feedback(current)
|
||||||
@@ -783,16 +793,43 @@ class PDDTaskPageEvent(QObject):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def _show_claim_error(self, title: str, content: str) -> None:
|
def _show_claim_error(self, title: str, content: str) -> None:
|
||||||
"""显示不会自动消失的可恢复错误,同时保留底部状态文字。"""
|
"""显示较大的临时错误,同时在底部保留完整状态。"""
|
||||||
|
|
||||||
InfoBar.error(
|
self._close_claim_feedback()
|
||||||
|
bar = InfoBar.error(
|
||||||
title=title,
|
title=title,
|
||||||
content=content,
|
content=content,
|
||||||
isClosable=True,
|
isClosable=True,
|
||||||
duration=-1,
|
duration=ERROR_FEEDBACK_DURATION_MS,
|
||||||
position=InfoBarPosition.TOP_RIGHT,
|
position=InfoBarPosition.TOP_RIGHT,
|
||||||
parent=self._page,
|
parent=self._page,
|
||||||
)
|
)
|
||||||
|
self._claim_feedback = bar
|
||||||
|
self._set_large_error_feedback_size(bar)
|
||||||
|
close_button = PushButton("关闭提示", bar)
|
||||||
|
self._set_large_feedback_button_size(close_button)
|
||||||
|
close_button.setAccessibleName("关闭采集采购错误提示")
|
||||||
|
close_button.clicked.connect(
|
||||||
|
lambda _checked=False, current=bar: self._close_claim_feedback(current)
|
||||||
|
)
|
||||||
|
bar.addWidget(close_button)
|
||||||
|
bar.destroyed.connect(
|
||||||
|
lambda _object=None, current=bar: self._forget_claim_feedback(current)
|
||||||
|
)
|
||||||
|
|
||||||
|
def _close_claim_feedback(self, expected: Optional[InfoBar] = None) -> None:
|
||||||
|
bar = self._claim_feedback
|
||||||
|
if bar is None or (expected is not None and bar is not expected):
|
||||||
|
return
|
||||||
|
self._claim_feedback = None
|
||||||
|
try:
|
||||||
|
bar.close()
|
||||||
|
except RuntimeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _forget_claim_feedback(self, bar: InfoBar) -> None:
|
||||||
|
if self._claim_feedback is bar:
|
||||||
|
self._claim_feedback = None
|
||||||
|
|
||||||
def _show_device_unavailable(self, content: str) -> None:
|
def _show_device_unavailable(self, content: str) -> None:
|
||||||
"""显示一条可进入设置且不会堆叠的设备错误。"""
|
"""显示一条可进入设置且不会堆叠的设备错误。"""
|
||||||
@@ -802,17 +839,20 @@ class PDDTaskPageEvent(QObject):
|
|||||||
title="Android 设备不可用",
|
title="Android 设备不可用",
|
||||||
content=content,
|
content=content,
|
||||||
isClosable=True,
|
isClosable=True,
|
||||||
duration=-1,
|
duration=ERROR_FEEDBACK_DURATION_MS,
|
||||||
position=InfoBarPosition.TOP_RIGHT,
|
position=InfoBarPosition.TOP_RIGHT,
|
||||||
parent=self._page,
|
parent=self._page,
|
||||||
)
|
)
|
||||||
self._device_feedback = bar
|
self._device_feedback = bar
|
||||||
|
self._set_large_error_feedback_size(bar)
|
||||||
settings_button = PushButton("打开设置", bar)
|
settings_button = PushButton("打开设置", bar)
|
||||||
|
self._set_large_feedback_button_size(settings_button)
|
||||||
settings_button.setAccessibleName("打开 Android 设备设置")
|
settings_button.setAccessibleName("打开 Android 设备设置")
|
||||||
settings_button.clicked.connect(
|
settings_button.clicked.connect(
|
||||||
lambda _checked=False, current=bar: self._open_device_settings(current)
|
lambda _checked=False, current=bar: self._open_device_settings(current)
|
||||||
)
|
)
|
||||||
close_button = PushButton("关闭提示", bar)
|
close_button = PushButton("关闭提示", bar)
|
||||||
|
self._set_large_feedback_button_size(close_button)
|
||||||
close_button.setAccessibleName("关闭 Android 设备提示")
|
close_button.setAccessibleName("关闭 Android 设备提示")
|
||||||
close_button.clicked.connect(
|
close_button.clicked.connect(
|
||||||
lambda _checked=False, current=bar: self._close_device_feedback(current)
|
lambda _checked=False, current=bar: self._close_device_feedback(current)
|
||||||
@@ -841,6 +881,24 @@ class PDDTaskPageEvent(QObject):
|
|||||||
if self._device_feedback is bar:
|
if self._device_feedback is bar:
|
||||||
self._device_feedback = None
|
self._device_feedback = None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _set_large_error_feedback_size(bar: InfoBar) -> None:
|
||||||
|
"""设置适合长中文错误和高缩放环境的最小尺寸。"""
|
||||||
|
|
||||||
|
bar.setMinimumSize(
|
||||||
|
ERROR_FEEDBACK_MIN_WIDTH,
|
||||||
|
ERROR_FEEDBACK_MIN_HEIGHT,
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _set_large_feedback_button_size(button: PushButton) -> None:
|
||||||
|
"""扩大操作按钮,避免用户只能点击右上角的小关闭图标。"""
|
||||||
|
|
||||||
|
button.setMinimumSize(
|
||||||
|
ERROR_FEEDBACK_BUTTON_MIN_WIDTH,
|
||||||
|
ERROR_FEEDBACK_BUTTON_MIN_HEIGHT,
|
||||||
|
)
|
||||||
|
|
||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
def _on_claim_thread_finished(self) -> None:
|
def _on_claim_thread_finished(self) -> None:
|
||||||
self._claim_worker = None
|
self._claim_worker = None
|
||||||
@@ -937,6 +995,7 @@ class PDDTaskPageEvent(QObject):
|
|||||||
self._stop_requested = True
|
self._stop_requested = True
|
||||||
self._next_cycle_timer.stop()
|
self._next_cycle_timer.stop()
|
||||||
self._close_rerun_feedback()
|
self._close_rerun_feedback()
|
||||||
|
self._close_claim_feedback()
|
||||||
self._close_device_feedback()
|
self._close_device_feedback()
|
||||||
|
|
||||||
for window in list(self._detail_windows.values()):
|
for window in list(self._detail_windows.values()):
|
||||||
|
|||||||
@@ -16,6 +16,11 @@ from src.android_device_service import AndroidDeviceSearchError
|
|||||||
from src.pdd_ui import PDDTaskPage
|
from src.pdd_ui import PDDTaskPage
|
||||||
from src.admin_gateway import AdminGatewayError, AdminTask, SubmissionReceipt
|
from src.admin_gateway import AdminGatewayError, AdminTask, SubmissionReceipt
|
||||||
from src.pdd_ui_event import (
|
from src.pdd_ui_event import (
|
||||||
|
ERROR_FEEDBACK_BUTTON_MIN_HEIGHT,
|
||||||
|
ERROR_FEEDBACK_BUTTON_MIN_WIDTH,
|
||||||
|
ERROR_FEEDBACK_DURATION_MS,
|
||||||
|
ERROR_FEEDBACK_MIN_HEIGHT,
|
||||||
|
ERROR_FEEDBACK_MIN_WIDTH,
|
||||||
PDDTaskPageEvent,
|
PDDTaskPageEvent,
|
||||||
admin_task_to_new_claimed_task,
|
admin_task_to_new_claimed_task,
|
||||||
summary_to_row,
|
summary_to_row,
|
||||||
@@ -463,6 +468,7 @@ class PDDTaskPageEventTest(unittest.TestCase):
|
|||||||
events._show_rerun_error("重新采集失败", "测试错误")
|
events._show_rerun_error("重新采集失败", "测试错误")
|
||||||
second = events._rerun_feedback
|
second = events._rerun_feedback
|
||||||
self.assertIsNot(first, second)
|
self.assertIsNot(first, second)
|
||||||
|
self.assertEqual(second.duration, ERROR_FEEDBACK_DURATION_MS)
|
||||||
|
|
||||||
close_buttons = [
|
close_buttons = [
|
||||||
button
|
button
|
||||||
@@ -470,12 +476,55 @@ class PDDTaskPageEventTest(unittest.TestCase):
|
|||||||
if button.text() == "关闭提示"
|
if button.text() == "关闭提示"
|
||||||
]
|
]
|
||||||
self.assertEqual(len(close_buttons), 1)
|
self.assertEqual(len(close_buttons), 1)
|
||||||
|
self.assertGreaterEqual(second.minimumWidth(), ERROR_FEEDBACK_MIN_WIDTH)
|
||||||
|
self.assertGreaterEqual(second.minimumHeight(), ERROR_FEEDBACK_MIN_HEIGHT)
|
||||||
|
self.assertGreaterEqual(
|
||||||
|
close_buttons[0].minimumWidth(),
|
||||||
|
ERROR_FEEDBACK_BUTTON_MIN_WIDTH,
|
||||||
|
)
|
||||||
|
self.assertGreaterEqual(
|
||||||
|
close_buttons[0].minimumHeight(),
|
||||||
|
ERROR_FEEDBACK_BUTTON_MIN_HEIGHT,
|
||||||
|
)
|
||||||
close_buttons[0].click()
|
close_buttons[0].click()
|
||||||
self.app.processEvents()
|
self.app.processEvents()
|
||||||
self.assertIsNone(events._rerun_feedback)
|
self.assertIsNone(events._rerun_feedback)
|
||||||
events.shutdown()
|
events.shutdown()
|
||||||
page.deleteLater()
|
page.deleteLater()
|
||||||
|
|
||||||
|
def test_claim_error_is_large_closable_and_replaces_previous(self):
|
||||||
|
page = PDDTaskPage()
|
||||||
|
events = PDDTaskPageEvent(page, self.repository)
|
||||||
|
|
||||||
|
events._show_claim_error("采集失败", "第一条错误")
|
||||||
|
first = events._claim_feedback
|
||||||
|
events._show_claim_error("采购失败", "第二条错误")
|
||||||
|
second = events._claim_feedback
|
||||||
|
|
||||||
|
self.assertIsNot(first, second)
|
||||||
|
self.assertEqual(second.duration, ERROR_FEEDBACK_DURATION_MS)
|
||||||
|
self.assertGreaterEqual(second.minimumWidth(), ERROR_FEEDBACK_MIN_WIDTH)
|
||||||
|
self.assertGreaterEqual(second.minimumHeight(), ERROR_FEEDBACK_MIN_HEIGHT)
|
||||||
|
close_buttons = [
|
||||||
|
button
|
||||||
|
for button in second.findChildren(type(page.rerunButton))
|
||||||
|
if button.text() == "关闭提示"
|
||||||
|
]
|
||||||
|
self.assertEqual(len(close_buttons), 1)
|
||||||
|
self.assertGreaterEqual(
|
||||||
|
close_buttons[0].minimumWidth(),
|
||||||
|
ERROR_FEEDBACK_BUTTON_MIN_WIDTH,
|
||||||
|
)
|
||||||
|
self.assertGreaterEqual(
|
||||||
|
close_buttons[0].minimumHeight(),
|
||||||
|
ERROR_FEEDBACK_BUTTON_MIN_HEIGHT,
|
||||||
|
)
|
||||||
|
close_buttons[0].click()
|
||||||
|
self.app.processEvents()
|
||||||
|
self.assertIsNone(events._claim_feedback)
|
||||||
|
events.shutdown()
|
||||||
|
page.deleteLater()
|
||||||
|
|
||||||
def test_late_rerun_success_does_not_override_cancel_status(self):
|
def test_late_rerun_success_does_not_override_cancel_status(self):
|
||||||
page = PDDTaskPage()
|
page = PDDTaskPage()
|
||||||
events = PDDTaskPageEvent(page, self.repository)
|
events = PDDTaskPageEvent(page, self.repository)
|
||||||
@@ -527,6 +576,7 @@ class PDDTaskPageEventTest(unittest.TestCase):
|
|||||||
before = self.repository.get_task("PDD-001")
|
before = self.repository.get_task("PDD-001")
|
||||||
before_run = self.repository.latest_task_run("PDD-001")
|
before_run = self.repository.latest_task_run("PDD-001")
|
||||||
page = PDDTaskPage()
|
page = PDDTaskPage()
|
||||||
|
|
||||||
def disconnected(_serial):
|
def disconnected(_serial):
|
||||||
raise AndroidDeviceSearchError(
|
raise AndroidDeviceSearchError(
|
||||||
"USB Android 设备 USB-001 未连接"
|
"USB Android 设备 USB-001 未连接"
|
||||||
@@ -566,12 +616,24 @@ class PDDTaskPageEventTest(unittest.TestCase):
|
|||||||
events._show_device_unavailable("第二条")
|
events._show_device_unavailable("第二条")
|
||||||
second = events._device_feedback
|
second = events._device_feedback
|
||||||
self.assertIsNot(first, second)
|
self.assertIsNot(first, second)
|
||||||
|
self.assertEqual(second.duration, ERROR_FEEDBACK_DURATION_MS)
|
||||||
|
self.assertGreaterEqual(second.minimumWidth(), ERROR_FEEDBACK_MIN_WIDTH)
|
||||||
|
self.assertGreaterEqual(second.minimumHeight(), ERROR_FEEDBACK_MIN_HEIGHT)
|
||||||
buttons = {
|
buttons = {
|
||||||
button.text(): button
|
button.text(): button
|
||||||
for button in second.findChildren(type(page.rerunButton))
|
for button in second.findChildren(type(page.rerunButton))
|
||||||
}
|
}
|
||||||
self.assertIn("打开设置", buttons)
|
self.assertIn("打开设置", buttons)
|
||||||
self.assertIn("关闭提示", buttons)
|
self.assertIn("关闭提示", buttons)
|
||||||
|
for button in buttons.values():
|
||||||
|
self.assertGreaterEqual(
|
||||||
|
button.minimumWidth(),
|
||||||
|
ERROR_FEEDBACK_BUTTON_MIN_WIDTH,
|
||||||
|
)
|
||||||
|
self.assertGreaterEqual(
|
||||||
|
button.minimumHeight(),
|
||||||
|
ERROR_FEEDBACK_BUTTON_MIN_HEIGHT,
|
||||||
|
)
|
||||||
buttons["打开设置"].click()
|
buttons["打开设置"].click()
|
||||||
self.app.processEvents()
|
self.app.processEvents()
|
||||||
self.assertEqual(opened, [True])
|
self.assertEqual(opened, [True])
|
||||||
|
|||||||
@@ -70,7 +70,7 @@
|
|||||||
- 补交本地 Outbox 不依赖手机,可以在 Android 设备断开时继续执行。
|
- 补交本地 Outbox 不依赖手机,可以在 Android 设备断开时继续执行。
|
||||||
- 执行本地任务或向 Admin 领取新任务前,工作线程必须用 `adb devices -l` 检查已保存的精确设备号。USB 和 Wi-Fi 设备都只接受 `device` 状态。
|
- 执行本地任务或向 Admin 领取新任务前,工作线程必须用 `adb devices -l` 检查已保存的精确设备号。USB 和 Wi-Fi 设备都只接受 `device` 状态。
|
||||||
- 设备未连接、`offline`、`unauthorized` 或 ADB 检查失败时,立即停止本轮和自动获取;不得领取新任务,也不得启动本地任务。
|
- 设备未连接、`offline`、`unauthorized` 或 ADB 检查失败时,立即停止本轮和自动获取;不得领取新任务,也不得启动本地任务。
|
||||||
- 设备错误使用一条持久 `InfoBar` 提示,提供“打开设置”和“关闭提示”。重复错误替换旧提示,不得堆叠。
|
- 采集、采购、重新采集和设备错误使用较大的 `InfoBar` 提示,5 秒后自动关闭,并提供明显的“关闭提示”按钮。设备错误另外提供“打开设置”;重复错误替换旧提示,不得堆叠。完整错误原因保留在底部状态区。
|
||||||
|
|
||||||
### 4.2 搜索与筛选
|
### 4.2 搜索与筛选
|
||||||
|
|
||||||
@@ -93,7 +93,7 @@
|
|||||||
- 确认后只执行选中的稳定任务编号,不领取新任务,不先处理其他任务或 Outbox。
|
- 确认后只执行选中的稳定任务编号,不领取新任务,不先处理其他任务或 Outbox。
|
||||||
- 重新采集在工作线程运行。执行期间禁用“获取任务”,“重新执行”变为可点击的“停止重新采集”。
|
- 重新采集在工作线程运行。执行期间禁用“获取任务”,“重新执行”变为可点击的“停止重新采集”。
|
||||||
- 点击停止后按钮显示“正在停止…”并禁用重复点击;底部明确说明正在等待手机当前操作结束。uiautomator2/ADB 的单次调用返回后,采集在下一个安全检查点停止;不得强制结束工作线程。
|
- 点击停止后按钮显示“正在停止…”并禁用重复点击;底部明确说明正在等待手机当前操作结束。uiautomator2/ADB 的单次调用返回后,采集在下一个安全检查点停止;不得强制结束工作线程。
|
||||||
- 重新采集的警告和错误 `InfoBar` 必须有可见的“关闭提示”按钮;同一时间只保留一条,新提示替换旧提示。
|
- 重新采集的警告和错误 `InfoBar` 必须有可见的“关闭提示”按钮,错误提示 5 秒后自动关闭;同一时间只保留一条,新提示替换旧提示。
|
||||||
- 每次重新采集创建新的执行记录和幂等键;当前结果更新,旧结果保存在历史执行记录中。
|
- 每次重新采集创建新的执行记录和幂等键;当前结果更新,旧结果保存在历史执行记录中。
|
||||||
- 采购任务始终不能通过“重新执行”入口启动;需要处理时由自动获取的安全恢复流程决定。
|
- 采购任务始终不能通过“重新执行”入口启动;需要处理时由自动获取的安全恢复流程决定。
|
||||||
- “等待重试”当前没有倒计时。自动获取因可恢复采集错误停止时,底部状态显示
|
- “等待重试”当前没有倒计时。自动获取因可恢复采集错误停止时,底部状态显示
|
||||||
@@ -295,8 +295,8 @@ class TaskTableModel(QAbstractTableModel):
|
|||||||
| 领取到新任务 | 更新表格和状态区,不弹模态框 |
|
| 领取到新任务 | 更新表格和状态区,不弹模态框 |
|
||||||
| 暂时没有可领取的任务 | 只更新状态区,**不要连续弹提示** |
|
| 暂时没有可领取的任务 | 只更新状态区,**不要连续弹提示** |
|
||||||
| 搜索无结果 | 表格空状态和清除条件入口 |
|
| 搜索无结果 | 表格空状态和清除条件入口 |
|
||||||
| Admin 暂时离线 | 持久信息条,本地数据继续可用 |
|
| Admin 暂时离线 | 较大信息条,5 秒后自动关闭;底部保留错误,本地数据继续可用 |
|
||||||
| 设备断开 | 持久信息条,提供打开设置和重试 |
|
| 设备断开 | 较大信息条,提供打开设置和关闭提示,5 秒后自动关闭 |
|
||||||
| 任务运行 | 底部状态区和必要进度,不阻塞整个窗口 |
|
| 任务运行 | 底部状态区和必要进度,不阻塞整个窗口 |
|
||||||
| 任务失败 | 行状态、详情和信息条,不显示原始堆栈 |
|
| 任务失败 | 行状态、详情和信息条,不显示原始堆栈 |
|
||||||
| 多个订单候选 | 状态改为“需要人工处理”,打开详情决策 |
|
| 多个订单候选 | 状态改为“需要人工处理”,打开详情决策 |
|
||||||
|
|||||||
Reference in New Issue
Block a user