【寻迹校园 HarmonyOS NEXT 实战 39】1280vp 临界点不丢状态:响应式断点往返的状态恢复测试
本章导读:这是“寻迹校园 HarmonyOS NEXT 实战”系列第 39 篇。本文结合
Index.ets、HomePage.ets、xunji-responsive-focus-state-audit.md与xunji-exact-xl-breakpoint-input-audit.md,复盘窗口在xl → lg → xl之间往返时,如何保持搜索、筛选、左栏滚动、查询记录、候选预览与中间详情,并说明 1278.95vp/1280vp 精确断点证据为什么仍不能替代实体 2-in-1 验收。

上图为原创生成的断点测试插画,不是项目截图。窗口变化只切换可见布局,状态账本位于布局分支之上,因此右栏暂时隐藏不等于候选状态被删除。
一、断点 Bug 往往不是布局错,而是状态重置
页面从三栏缩到双栏再放大,最容易出现的不是宽度计算错误,而是:
- 搜索关键词被清空;
- 组合筛选面板收起;
- 左栏滚动回到顶部;
- 查询记录切回第一条;
- 中栏从候选详情跳回查询本身;
- 右栏恢复后忘记哪个候选已选中;
- 路由栈被新 Shell 重建;
- 重复触发 Service 初始化。
这些问题的共同根因,是把业务/交互状态放在只对某个断点存在的组件内部。
二、本章验证的精确临界点
测试设备是 2026-08-12 的 MateBook Pro API 22 模拟器,VPR 为 1.9:
| 根窗口像素宽度 | 逻辑宽度 | 预期布局 |
|---|---|---|
| 2430px | 1278.95vp | lg 双栏 |
| 2432px | 1280vp | xl 三栏 |
由于物理像素是离散值,2430px 无法精确得到 1279vp,但 1278.95vp 足以证明 <1280 分支;下一档 2432px 恰好命中 1280vp。
三、测试场景不是空页面缩放
为了验证状态恢复,测试先构造有意义的上下文:
- 在 xl 打开筛选面板;
- 滚动左侧记录列表;
- 选择一条查询记录;
- 在右栏点击候选 2;
- 确认中栏显示候选详情;
- 缩到 lg;
- 确认右栏隐藏但中栏仍是候选;
- 再放大到 xl;
- 确认筛选、滚动、查询和候选选择恢复。
只验证“三栏出现/消失”无法发现状态问题。
四、根宽度状态位于 Shell 之上
Index 保存:
@Local selectedTab: BottomTab = BottomTab.HOME;
@Local dataRevision: number = 0;
@Local screenWidth: number = 0;
screenWidth 只决定渲染哪种 Shell,selectedTab 与 pathStack 不属于某个 Shell 分支。窗口变化不会重新创建 Index,因此全局导航上下文得以保留。
五、HomePage 状态也位于 compact/split 分支之上
HomePage 在 builder 之外保存:
@Local keyword: string = '';
@Local selectedFilter: HomeReportFilter = HomeReportFilter.ALL;
@Local selectedCategory: string = '';
@Local selectedAreaKeyword: string = '';
@Local selectedTimeRange: HomeTimeRange = HomeTimeRange.ALL;
@Local selectedColor: string = '';
@Local filterPanelVisible: boolean = false;
@Local selectedReportId: string = '';
@Local previewReportId: string = '';
compactHome()、splitScreenHome() 和 workspaceView 只是消费这些状态,没有各自创建副本。
六、为什么状态必须高于 if 分支
如果把 previewReportId 声明在右栏组件内部,lg 布局不渲染右栏时组件会销毁,恢复 xl 后只能重新选择默认候选。
当前设计把预览 ID 放在 HomePage,右栏只是根据它显示选中边框。即使右栏暂时不存在,中栏仍可以继续展示候选详情。
七、xl → lg 时到底隐藏了什么
当宽度从 1280vp 降到 1278.95vp:
workspaceView从 true 变为 false;- 右栏
MatchResultsPage不再渲染; - 左栏从 34% 变为 38%;
- 中栏从固定 44% 变为
layoutWeight(1); selectedReportId不变;previewReportId不变;- 中栏仍用
previewReportId加载详情。
隐藏的是辅助视图,不是业务选择。
八、lg → xl 时如何恢复候选标记
恢复到 1280vp 后,右栏重新创建,并接收:
selectedCandidateId:
this.previewReportId === this.selectedReportId ? '' : this.previewReportId
如果中栏仍在预览候选,右栏对应卡片立即显示“正在中间预览”和品牌边框。恢复不依赖重新点击。

上图把持久状态、短生命周期交互状态和布局可见性分开。lg 阶段只暂停右栏呈现,查询与预览状态仍留在 HomePage。
九、selectedReportId 与 previewReportId 的断点意义
两者分别表示:
selectedReportId:左栏查询来源;previewReportId:中栏当前详情。
断点往返时,查询来源必须保持,候选预览也应保持。一个 ID 无法同时表达这两个事实,会迫使页面在缩放时选择丢弃其中之一。
十、筛选面板展开态为什么值得保留
用户在 xl 展开组合筛选后缩小窗口,通常只是调整窗口,不代表取消任务。如果 filterPanelVisible 随布局重建变回 false,用户会误以为筛选条件被清除。
当前展开态保存在 HomePage,因此往返后仍保持。显示形式可以改变,但用户意图不应被断点覆盖。
十一、筛选值如何与列表刷新协调
搜索或筛选变化会调用 refreshReports()。刷新后页面先判断原查询是否仍在结果中:
const selectionAvailable = this.reports.some(
(report: ItemReport) => report.id === this.selectedReportId
);
只有查询被筛掉时才选择新的第一条记录。窗口变化本身不调用重置筛选,因此不会改变结果集合。
十二、左栏滚动位置属于视图状态
左栏 Scroll 在布局往返时保持位置,是 2026-08-12 验收的一部分。滚动位置不是 Repository 数据,也不需要写入数据库;它属于当前页面会话的短生命周期状态。
但如果整个 HomePage 被断点分支替换为不同页面实例,Scroll 也会重建。当前实现保持同一 HomePage,只改变内部布局。
十三、右栏滚动恢复为什么曾是 not applicable
第一轮验收中,最大化窗口内容高度约 942vp,当前业务最多返回 3 条候选,全部已经可见,没有有效额外滚动距离,因此“右栏滚动恢复”被标记为 not applicable。
后续精确 1280vp 验收在更窄右栏中注入滚动,候选 2、候选 3 进入视口,证明滚动容器可达。但这仍不是“滚动偏移在所有窗口往返中持久化”的完整证明。
十四、Navigation 为什么不能随 Shell 重建新实例
Phone、md、lg/xl 的外壳不同,但 pathStack 是 Index 的单一字段。假如每个 builder 内部临时创建 new NavPathStack(),断点切换会丢失当前详情、发布或认领路径。
共享路由栈让 Shell 切换只影响导航呈现,不影响用户已经进入的流程。
十五、业务初始化为什么不能绑定 onAreaChange
onAreaChange 可能在窗口拖动过程中高频触发。当前回调只在宽度变化时更新数字,不打开数据库、不请求匹配、不重建 Service。
业务初始化发生在 aboutToAppear(),并通过现有 Service 统一执行。把 Repository 初始化放进宽度回调会形成请求风暴和状态闪烁。
十六、边界判断要避免重叠与空洞
当前范围为:
sm: width < 600
md: 600 <= width < 840
lg: 840 <= width < 1280
xl: width >= 1280
每个宽度只属于一个档位,没有 600vp 同时命中两种布局,也没有 839.5vp 落入空白。测试必须覆盖等号所在的点,而不只测 500、900、1500 三个普通宽度。
十七、Number(newArea.width) 的输入风险
Area.width 被转换为 number 后参与比较。实现应关注:
- 初次布局前
screenWidth为 0; - 宽度字符串能否稳定转换;
- 系统缩放产生小数 vp;
- 浮点值在 1280 附近的离散性;
- 同一个物理像素变化是否频繁跨界。
本次 1278.95/1280 证据正是对小数逻辑宽度的实际检查。
十八、为什么需要往返而不是单向切换
单向 lg → xl 只能证明第三栏出现。xl → lg → xl 才能检查:
- 隐藏组件是否销毁权威状态;
- 中栏是否保留候选;
- 右栏能否恢复选中态;
- 筛选与滚动是否仍在;
- Tab 焦点链是否重新闭合;
- 列表是否被重复刷新。
状态恢复测试必须包含“回到原布局”。
十九、焦点状态与数据状态不能混为一谈
断点切换后,具体 DOM/ArkUI 节点可能被替换,键盘焦点不一定能保持在完全相同的按钮上;但查询、筛选和候选选择仍必须保持。
测试应分别记录:
- 数据状态:Report、候选、筛选值;
- 交互状态:选中、展开、滚动;
- 焦点状态:当前可聚焦节点与顺序;
- 路由状态:页面栈;
- 布局状态:sm/md/lg/xl。
一项通过不能覆盖其他项。
二十、2026-08-12 的验收结果
xunji-responsive-focus-state-audit.md 记录:
- xl → lg → xl 状态恢复 passed;
- 筛选面板、左栏滚动、查询记录、候选 2 预览和中栏详情保持;
- xl 焦点链在应用内闭环;
- 377vp Phone 回归 passed;
- 当时本地测试 15/15、APP 构建 passed。
xunji-exact-xl-breakpoint-input-audit.md 进一步记录:
- 1278.95vp 为 lg;
- 1280vp 为 xl;
- 候选点击联动 passed;
- Tab/Shift+Tab 输入 passed;
- 右栏滚动 passed。
二十一、为什么不能把旧构建结果当成今天的新构建
上述记录来自明确日期和当时基线。当前文章重新读取了现有 Index.ets 与 HomePage.ets,确认状态结构仍存在,但没有重新执行 2026-08-26 的 MateBook 模拟器安装与构建。
因此文章引用“2026-08-12 验收 passed”,不会写成“今天重新真机通过”。
二十二、实体 2-in-1 仍需补哪些证据
模拟器输入通道无法覆盖:
- 实体窗口吸附与系统缩放;
- 触控板滚动惯性;
- 鼠标焦点与 hover;
- 蓝牙键盘组合键;
- 外接显示器 DPI 切换;
- 睡眠恢复和多窗口生命周期;
- 折叠形态与铰链区域。
这些项目应继续标记为 not run,直到出现相应设备记录。
二十三、首发仅 Phone 对测试结论的影响
当前 manifest 只声明 Phone。大屏断点测试属于保留能力的工程验证,不等于当前 AGC 包面向 2-in-1 分发。
后续恢复设备类型前,应基于最终 release 包重新执行矩阵,而不是直接复用开发阶段的模拟器结论。
二十四、可自动化的状态断言
断点测试可以把状态写成结构化断言:
before:
width=1280, query=A, preview=C2, filterOpen=true
shrink:
width=1278.95, rightPanel=false, query=A, preview=C2
restore:
width=1280, rightPanel=true, query=A, preview=C2, candidate2Selected=true
截图用于视觉证据,布局树/状态记录用于精确断言。只比较截图容易受字体、时间和动画影响。
二十五、工程复盘:布局可变,用户意图必须稳定
窗口变化是展示环境变化,不是用户重新开始任务。查询、筛选、候选预览和路由表达用户意图,生命周期应该高于 Shell 和右栏组件。
将状态提升到稳定父组件后,断点分支可以自由出现或消失,而无需把所有状态写入全局存储或数据库。
二十六、本文小结
“寻迹校园”的 1280vp 状态恢复依赖两层稳定状态:Index 保存全局导航与宽度,HomePage 保存筛选、查询与预览;lg/xl 分支只改变右栏可见性和列宽。selectedReportId 与 previewReportId 的分离,使候选详情在右栏隐藏期间仍能保留。
2026-08-12 的 1278.95vp/1280vp 模拟器证据证明了当时的临界切换、往返状态和输入链路;实体 2-in-1、外设、当前 release 包与商店设备支持仍需独立验证。
系列导航:第 39 篇 / 共 50 篇。上一篇:《34%/44%/22% 三栏工作台》;下一篇:《键盘焦点与可访问名称》。
更多推荐


所有评论(0)