diff --git a/docs/tasks/T-582.md b/docs/tasks/T-582.md index 5762b76..daddf73 100644 --- a/docs/tasks/T-582.md +++ b/docs/tasks/T-582.md @@ -1,6 +1,6 @@ --- id: T-582 -title: 修复弹窗偶发只显示一半:HiDPI 舍入策略 + 统一弹窗 helper(延迟冲刷 + 屏幕钳制) +title: 修复①导入 Excel 后完成弹窗偶发只显示一半(延迟一拍弹窗 + 可选 HiDPI 一行) phase: 7 deps: [] status: TODO @@ -9,36 +9,28 @@ created: 2026-07-10 ## 问题 / 背景 -用户反馈:部分弹窗偶发只显示一半(例:①采集导入 Excel 后的弹窗)。「偶尔」这一特征本身排除了纯 HiDPI 缩放(那会稳定半截),指向**绘制/几何时序竞态**。 +用户反馈:①采集导入 Excel 后的完成/错误弹窗**偶发只显示一半**。「偶尔」排除纯 HiDPI 缩放(那会稳定半截),指向绘制时序竞态。 -已核代码事实: -- ①导入是在 GUI 线程**同步**跑(`collect.py:242` `excel.import_tasks(...)`,非 worker),解析+入库期间事件循环阻塞;返回后紧接 `refresh_tasks()`(整表重绘)+ `_set_status()`,**再**弹框——模态框在积压重绘未冲刷时被 `exec()`,首帧可能几何/内容没算稳(偶发半截)。 -- 应用启动**未设任何 HiDPI 缩放策略**(`app/gui/__init__.py:125` 裸 `QApplication(sys.argv)`)——分数缩放(125/150%)下放大上述问题。 -- 弹窗**无屏幕边界钳制**:只有主窗口在 `main_window.py:66-71` 用 `move()` 钳到 `availableGeometry`,`QMessageBox` 靠近屏幕边缘/多显示器时下半截会被切。 +已核代码事实:①导入在 GUI 线程**同步**执行(`app/gui/tabs/collect.py:242` `excel.import_tasks(...)` 在 `import_excel` 按钮 handler 内直接调用),解析+入库期间事件循环阻塞;返回后紧接 `refresh_tasks()`(整表重绘)+ 弹窗,模态框在积压重绘未冲刷时被 `exec()`,首帧几何/内容没算稳 → 偶发半截。 -根因排序:① 绘制竞态(最可能,吻合「偶尔」);② 弹窗跑出屏幕被切(次可能);③ 缺 HiDPI 舍入策略(放大器,非主因)。本任务修 ①②③ 中低风险的 HiDPI 策略 + 统一 helper(延迟冲刷治①、屏幕钳制治②、舍入策略治③);导入 worker 化(治本的另一半)见 T-583。 +范围收敛:本任务**只修导入这一处弹窗**,不建统一弹窗 helper、不重构其它 `QMessageBox` 调用点、不改导入线程模型(导入 worker 化不在本任务)。 ## 方案(改哪个文件、改成什么) -### 1. 启动加 HiDPI 舍入策略(`app/gui/__init__.py`) -- 在 `QApplication` **创建之前**设 `QGuiApplication.setHighDpiScaleFactorRoundingPolicy(Qt.HighDpiScaleFactorRoundingPolicy.PassThrough)`。 -- 放在 `main()` 里 `app = QApplication.instance() or QApplication(sys.argv)` 之前;`QApplication.instance()` 已存在时(测试/复用)跳过设置避免告警。 +### 1. 让导入完成/错误弹窗晚一拍弹(`app/gui/tabs/collect.py`) +- 导入返回后,把该路径上的弹窗(`_show_error` 失败框、账号未就绪 warning、导入结果通知框等**导入链路**的通知类弹窗)改为用 `QTimer.singleShot(0, lambda: <弹窗>)` 推迟显示——让紧邻的整表重绘(`refresh_tasks`)先在事件循环里冲刷完,再打开模态框。 +- 只改导入链路的通知类弹窗;不动 question/确认类(导入链路本身无需返回值的确认)。 +- 仅 collect.py 导入相关处,不外扩。 -### 2. 统一弹窗 helper:延迟一拍 + 屏幕钳制 -- 新增共享 helper(放 `app/gui/widgets.py` 或合适模块),如 `show_message(parent, icon, title, text, buttons=...) -> QMessageBox.StandardButton`: - - 构造 `QMessageBox` 后,`box.show()` → 用 `QApplication.processEvents()` 或 `box.adjustSize()` 让尺寸算稳,再把窗口 `move()` **钳制到 `parent` 所在屏幕的 `availableGeometry`**(复用 `main_window.py:66-71` 同款:居中后夹到 max_x/max_y,防下半/右半出屏),最后 `exec()`。 - - 对"弹框紧跟重活"的场景,用 `QTimer.singleShot(0, ...)` 把 `exec()` 推迟到当前事件循环把积压重绘冲刷完之后(治绘制竞态)。实现时确保 helper 在需要返回用户选择的场景仍能同步拿到结果(question/确认类),或为「仅通知类」(information/warning) 与「需返回值类」(question) 分别提供入口。 -- 把 ①②③ 里高频、且出现在"重活之后"的通知类弹窗改走 helper:至少 `collect.py` 导入相关(`_show_error`、账号未就绪 warning、导入结果框)先接入;其余 `QMessageBox.information/warning` 逐步收敛(本任务先覆盖采集导入链路 + 提供 helper,不强制一次性全量替换)。 - -### 边界内的取舍 -- question/确认类弹窗需要同步返回值,`QTimer.singleShot(0)` 延迟不适用——这类只做「屏幕钳制 + adjustSize」,不做延迟 exec;延迟冲刷只用于不需返回值的通知类。实现时区分清楚。 +### 2.(可选保险)启动加 HiDPI 舍入策略(`app/gui/__init__.py`) +- `QApplication` 创建前设 `QGuiApplication.setHighDpiScaleFactorRoundingPolicy(Qt.HighDpiScaleFactorRoundingPolicy.PassThrough)`(`main()` 里 `app = QApplication.instance() or QApplication(sys.argv)` 之前;实例已存在时跳过避免告警)。 +- 零风险、对整体弹窗清晰度有益;如实现时评估有副作用可省略,验收不强制。 ## 验收要点 -- 启动设置了 `HighDpiScaleFactorRoundingPolicy.PassThrough`,且在 QApplication 创建前(单测可断言调用顺序,或至少不回归启动)。 -- helper 存在并被采集导入链路的通知类弹窗使用;helper 会把弹窗几何钳制进父窗口所在屏幕 `availableGeometry`(单测:给一个会越界的父窗口位置,断言 helper 计算出的目标坐标被夹在可用区域内)。 -- 通知类弹窗经 helper 延迟到重绘冲刷后再 exec(单测可 mock `QTimer.singleShot`/`processEvents` 断言被调用;不真正弹窗)。 -- question/确认类仍同步返回用户选择、行为不回归(如③开始更新确认、重置确认)。 -- 现有走 `QMessageBox` 的测试不回归。 +- 导入完成/失败后的通知类弹窗经 `QTimer.singleShot(0, ...)` 延迟显示(单测:mock `QTimer.singleShot` 断言被用于导入完成/错误弹窗路径;不真正弹窗)。 +- 导入结果/错误的既有行为(stats 更新、run_log、状态栏文案、`_show_error` 内容)不回归。 +- 若采纳 HiDPI 行:启动设置在 QApplication 创建前、不回归启动。 +- 未改动导入链路以外的弹窗调用点(`git diff` 范围仅 collect.py 导入链路 +(可选)__init__.py 一行)。 - 验证命令(unittest,不引入 pytest;GUI 测试用 offscreen): - `py -3.10 -m unittest tests.test_gui` - `python -m ruff check app tests main.py` @@ -47,10 +39,10 @@ created: 2026-07-10 - `git diff --check` ## 边界(不改什么) -- 不把①导入改成 worker 线程(治本另一半在 T-583)。 -- 不改弹窗文案/业务语义、不改主窗口尺寸逻辑。 -- 不强制一次性替换全部 `QMessageBox` 调用点(先提供 helper + 覆盖采集导入链路)。 -- 不改 CDP/DB/AI/cmhub/Excel 解析逻辑。 +- 不建统一弹窗 helper、不收敛/替换其它模块的 `QMessageBox` 调用。 +- 不把①导入改成 worker 线程(如后续要治界面卡死另立任务/backlog)。 +- 不改弹窗文案/业务语义、不改 `excel.import_tasks` 解析入库逻辑。 +- 不改 CDP/DB/AI/cmhub、主窗口尺寸逻辑。 ## 执行记录 diff --git a/docs/tasks/T-583.md b/docs/tasks/T-583.md deleted file mode 100644 index db238e4..0000000 --- a/docs/tasks/T-583.md +++ /dev/null @@ -1,55 +0,0 @@ ---- -id: T-583 -title: ①Excel 导入改为 worker 线程执行,消除 GUI 阻塞与弹窗绘制积压(治本) -phase: 7 -deps: [T-582] -status: TODO -created: 2026-07-10 ---- - -## 问题 / 背景 - -①导入 Excel 目前在 GUI 线程**同步**执行(`app/gui/tabs/collect.py:242` `excel.import_tasks(file_paths, ...)` 在 `import_excel` 按钮 handler 内直接调用)。解析+入库期间**事件循环被阻塞**:界面卡死、重绘积压,紧随其后的完成/错误弹窗容易在积压未冲刷时绘制,导致偶发只显示一半(T-582 分析的根因①之治本项)。 - -T-582 已用 HiDPI 策略 + 统一弹窗 helper(延迟冲刷 + 屏幕钳制)缓解症状;本任务把导入挪进后台 worker,从根上消除 GUI 阻塞与重绘积压,顺带解决"导入大文件时界面卡死"。 - -已有可复用范式:①采集本身就是 `CollectWorker` + `run_worker` 线程模型(`collect.py` 已有 `_on_collect_finished`/`finished.connect`/`_forget_*_thread` 一套),导入照搬即可。 - -## 方案(改哪个文件、改成什么) - -### 新增 `ImportWorker`(`app/gui/workers.py`) -- 接收 `file_paths` + `db_path`,在 `run()` 里调 `excel.import_tasks(...)`,通过 `finished` 信号回传 `result`(stats/batch_id/errors)或异常信息;沿用现有 worker 基类/信号约定。 -- 不在 worker 线程里碰 Qt 控件;只发信号,GUI 线程收信号后更新。 - -### 改 `collect.py` `import_excel` -- 选完文件后:置"导入中"状态(禁用导入按钮、状态栏提示"正在导入...")→ 起 `ImportWorker` 线程(复用 `run_worker`/`_forget_*_thread` 模式)→ 立即返回,不阻塞。 -- 新增 `_on_import_finished(payload)`:搬迁现同步路径里返回后的逻辑(写 run_log、`has_import_result`/`last_import_stats`/`current_batch_id` 赋值、file_errors/row_errors 落日志、`refresh_tasks`、`_set_status`、错误走 `_show_error`)。弹窗统一走 T-582 的 helper。 -- 导入进行中:禁用「导入 Excel...」按钮,防重复导入并发写库;完成/失败后恢复。 - -### 线程安全 / 边界 -- run_log 的写入若在 worker 线程发生,确认 DB 访问与现有 worker 一致(各自连接、不跨线程共享 sqlite 连接)。 -- 导入与①采集/删除批次等其它写库操作互斥或至少不并发同一批次(沿用现有 running 守卫思路)。 - -## 验收要点 -- 点「导入 Excel...」选文件后:GUI 不卡死(导入在后台线程),按钮置为导入中/禁用,状态栏提示进行中。 -- 导入完成:`_on_import_finished` 更新 stats/batch_id、刷新列表、状态栏汇总,与旧同步路径结果一致(单测:mock `excel.import_tasks` 返回固定 stats,断言完成回调产生同样的 UI 状态/日志)。 -- 导入失败:走 `_show_error`(经 T-582 helper)、写失败 run_log/诊断日志,不吞异常。 -- 导入进行中「导入 Excel...」按钮禁用,完成后恢复。 -- worker 线程内不触碰 Qt 控件(review + 单测 mock 校验信号路径)。 -- 大文件导入期间界面可响应(人工验证点,记录到执行记录)。 -- 验证命令(unittest,不引入 pytest): - - `py -3.10 -m unittest tests.test_gui` - - `python -m ruff check app tests main.py` - - `py -3.10 -m compileall app main.py` - - `py -3.10 -m unittest discover -s tests` - - `git diff --check` - -## 边界(不改什么) -- 不改 `excel.import_tasks` 解析/入库逻辑本身,只改调用它的线程。 -- 不改导入结果的数据结构/stats 字段、批次生成逻辑。 -- 不改①采集 `CollectWorker`、③更新、CDP/DB schema/AI/cmhub。 -- 弹窗渲染修复本身属于 T-582,本任务只保证导入走 helper。 - -## 执行记录 - -(做完在这里写:改了什么文件、跑了什么验证命令及结果、遇到的阻塞、关键决策。)