灯光模拟HarmonyOS应用实战-72-科三流程写了六步却到不了页面:用内容可达性检查接通实操指导
灯光模拟HarmonyOS应用实战-72-科三流程写了六步却到不了页面:用内容可达性检查接通实操指导
项目里已经有一份写得很完整的科三灯光流程:从上车后的初始状态、开启前照灯,到会车、通过急弯,再到停车和关闭全部灯光,一共六步。问题是,用户进入“科三实操灯光”页面后看到的是状态栏、指令面板和操作按钮,这六步没有任何页面引用。内容存在于源码,并不等于用户能够找到、进入和读完它。
这类缺口很容易被代码审查漏掉。数据文件能搜索到 SUBJECT3_FLOW_STEPS,主页也确实有“科三实操灯光”入口,开发者便可能把“有内容”和“内容可达”合并成一个结论。更稳的办法是建立内容可达性检查:每份面向用户的指导内容都要有稳定身份、明确入口、渲染承载、返回路径和自动核对,让孤立常量在合入前暴露出来。

本文解决四个具体问题:还原六步流程为何成为孤立内容;区分实操考试与实操指导;用 GuidanceEntry 接通入口和渲染;用可达性图与回归清单守住后续改动。
一、当前六步流程只在题库模型中定义
当前 QuestionBank.ets 导出了 SUBJECT3_FLOW_STEPS。数组有六项,内容覆盖初始灯态、近光、远近光交替、停车警示以及考试结束后的关闭操作。
export const SUBJECT3_FLOW_STEPS: string[] = [
'上车后先确认灯光处于关闭或初始状态。',
'听到“请开启前照灯”后,再开启近光灯。',
'遇到会车、跟车、照明良好道路,保持或切换近光灯。',
'遇到急弯、坡路、拱桥、人行横道,使用远近光交替。',
'遇到故障难以移动或临时停车,使用示廓灯加危险报警闪光灯。',
'听到考试完成后,关闭所有灯光。'
];
这六项是当前源码事实,但全工程对 SUBJECT3_FLOW_STEPS 的引用只有定义本身。Index.ets 的导入列表包含 PRACTICAL_COMMANDS、PRACTICAL_ACTION_OPTIONS 等实操数据,没有导入这份流程数组。因此可以静态确认它尚未进入当前页面渲染链;这不能证明用户曾经点击失败,也不能说明作者原本打算把它做成独立页面还是实操页内说明。
| 证据位置 | 当前事实 | 能得出的结论 |
|---|---|---|
QuestionBank.ets | 导出六项 SUBJECT3_FLOW_STEPS | 指导内容已经写入源码 |
Index.ets 导入区 | 没有导入该常量 | 当前页面文件不消费它 |
MODULES | 有 subject3Flow 实操入口 | 用户可以进入实操考试 |
SubjectThreePracticalView() | 渲染状态、指令和按钮 | 这里没有六步指导组件 |
二、“subject3Flow”当前通向实操考试,不是六步说明
主页模块把 subject3Flow 命名为“科三实操灯光”。点击卡片后,openModule() 把 currentPage 设为模块 ID;build() 命中 PAGE_SUBJECT_THREE_FLOW 后渲染 SubjectThreePracticalView()。这条路径本身是连通的,只是终点承载的是考试交互。
const PAGE_SUBJECT_THREE_FLOW: string = 'subject3Flow';
// build() 中的当前分支
if (this.currentPage === PAGE_SUBJECT_THREE_FLOW) {
this.SubjectThreePracticalView();
}
private openModule(id: string, title: string): void {
this.currentPage = id;
this.pageTitle = title;
if (id === PAGE_SUBJECT_THREE_EXAM ||
id === PAGE_SUBJECT_THREE_FLOW) {
this.resetSubjectThreeExam();
}
}
SubjectThreePracticalView() 随后展示 PracticalStatusRow()、CommandPanel()、实操按钮以及“开始实操考试”页脚。由此应把两个用户目标拆开:想先学习步骤的人需要指导入口;准备直接练习的人需要考试入口。若简单把六段文字塞进指令面板,考试开始后内容会与倒计时、操作反馈争夺注意力。

三、先给内容建立身份,而不是继续传字符串数组
数组只能表达顺序,无法回答内容属于哪个页面、入口文案是什么、是否允许在考试中展开。建议先定义内容清单,让六步流程成为一个可引用的指导单元。
export interface GuidanceStep {
id: string;
title: ResourceStr;
detail: ResourceStr;
}
export interface GuidanceEntry {
id: string;
title: ResourceStr;
summary: ResourceStr;
sourceModuleId: string;
steps: GuidanceStep[];
}
export const SUBJECT3_PRACTICAL_GUIDE_ID =
'subject3.practical.guide.v1';
export const SUBJECT3_PRACTICAL_GUIDE: GuidanceEntry = {
id: SUBJECT3_PRACTICAL_GUIDE_ID,
title: $r('app.string.subject3_guide_title'),
summary: $r('app.string.subject3_guide_summary'),
sourceModuleId: 'subject3Flow',
steps: [
{ id: 'prepare_lights',
title: $r('app.string.guide_prepare_title'),
detail: $r('app.string.guide_prepare_detail') },
{ id: 'enable_low_beam',
title: $r('app.string.guide_enable_title'),
detail: $r('app.string.guide_enable_detail') },
{ id: 'ordinary_low_beam',
title: $r('app.string.guide_low_title'),
detail: $r('app.string.guide_low_detail') },
{ id: 'alternate_beam',
title: $r('app.string.guide_alt_title'),
detail: $r('app.string.guide_alt_detail') },
{ id: 'parking_warning',
title: $r('app.string.guide_parking_title'),
detail: $r('app.string.guide_parking_detail') },
{ id: 'finish_close',
title: $r('app.string.guide_finish_title'),
detail: $r('app.string.guide_finish_detail') }
]
};
id 用于路由、日志和回归,不随展示文案变化;sourceModuleId 说明它从哪个业务模块进入;步骤标题和详情使用资源引用,便于统一文案和后续语言适配。当前工程的六步是 string[],上述结构属于建议方案,尚未写入项目。
迁移时不要依据数组下标永久生成身份。更适合使用 prepare_lights、enable_low_beam、ordinary_low_beam、alternate_beam、parking_warning、finish_close 这类稳定键。若中间新增步骤,原有身份不会整体漂移。
四、入口要明确区分“先看步骤”和“开始实操”
最小接法可以保留现有 subject3Flow 页面,在考试未开始时增加一个“实操步骤”按钮或摘要卡。它打开指导态,返回时仍回到实操页,不改变现有主页模块 ID。
enum SubjectThreeFlowSurface {
PRACTICE = 'practice',
GUIDE = 'guide'
}
@State subjectThreeFlowSurface: SubjectThreeFlowSurface =
SubjectThreeFlowSurface.PRACTICE;
private openPracticalGuide(): void {
if (this.examActive) {
this.message = '请先结束当前实操,再查看完整步骤';
return;
}
this.subjectThreeFlowSurface = SubjectThreeFlowSurface.GUIDE;
}
private closePracticalGuide(): void {
this.subjectThreeFlowSurface = SubjectThreeFlowSurface.PRACTICE;
}
这里没有再发明一个与 currentPage 平级的任意字符串。实操模块仍由 PAGE_SUBJECT_THREE_FLOW 管理,模块内部只切换两个受限表面。若产品决定指导必须支持外部直达,再为它建立正式路由;在单页状态机里只加一个内容态,修改范围更小。
入口策略还要明确考试中的行为。为了避免学员在倒计时期间误触,完整指导可以在 examActive 时禁用;简短的当前指令解释则继续由考试面板承担。两种内容粒度不同,不应共用一个开关。
五、指导组件消费结构化步骤并保留阅读位置
指导页不需要复制六个 Text()。让组件遍历清单,可以统一编号、语义和长文本布局,也便于以后核对每个步骤是否真正渲染。
@Builder
PracticalGuideView(entry: GuidanceEntry) {
Column() {
List({ space: 12 }) {
ForEach(entry.steps, (step: GuidanceStep, index: number) => {
ListItem() {
Row({ space: 12 }) {
Text(`${index + 1}`)
.fontWeight(FontWeight.Bold)
Column({ space: 4 }) {
Text(step.title).fontWeight(FontWeight.Bold)
Text(step.detail).maxLines(0)
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
}
.padding(16)
}
}, (step: GuidanceStep) => step.id)
}
.layoutWeight(1)
this.RowActionButton('返回实操', () => {
this.closePracticalGuide();
});
}
}
步骤键用 step.id,不使用展示文字,避免改文案后列表身份变化。maxLines(0) 只是表达“不按两行截断”的示例,具体取值要按当前 ArkUI SDK 和页面规范核对。长文本还应覆盖大字体、窄屏和深色模式,不能只在默认字号下看一眼。
如果希望返回后保持阅读位置,可保存最后可见的步骤 ID,而不是保存数组下标。内容版本升级导致步骤插入时,按 ID 恢复比按第几个更可靠。
六、用内容可达图核对“定义—入口—页面—返回”

页面可读性可以转成一张小型有向图。内容节点必须从主页或模块入口可达,还要存在返回边。下面的实现放在构建期脚本或单元层更合适,不必把图算法带入运行页面。
interface ReachabilityEdge {
from: string;
to: string;
kind: 'open' | 'render' | 'back';
}
const edges: ReachabilityEdge[] = [
{ from: 'home', to: 'subject3Flow', kind: 'open' },
{ from: 'subject3Flow', to: 'subject3.practical.guide.v1', kind: 'render' },
{ from: 'subject3.practical.guide.v1', to: 'subject3Flow', kind: 'back' }
];
function isReachable(start: string, target: string,
edges: ReachabilityEdge[]): boolean {
const queue: string[] = [start];
const visited: string[] = [];
while (queue.length > 0) {
const node = queue.shift();
if (node === undefined || visited.indexOf(node) >= 0) continue;
if (node === target) return true;
visited.push(node);
for (let index = 0; index < edges.length; index++) {
if (edges[index].from === node) queue.push(edges[index].to);
}
}
return false;
}
清单中每个 GuidanceEntry.id 都要满足 isReachable('home', id, edges)。此外还要核对入口实际绑定了处理函数、处理函数确实切到对应表面、页面组件确实消费同一个内容 ID。仅在测试里手写一条边而实际 UI 没有按钮,仍然会得到假阳性,因此图应尽量从路由表和内容注册表生成。
七、进入考试前只做轻量确认,不把指导变成强制弹窗
接通内容后,产品还要决定何时提示。每次都先弹六步,会拖慢熟练用户;永远不提示,新用户又可能继续找不到。可以保存“指导版本是否读过”,但它只能改善推荐时机,不能撤销入口。
interface GuideReadState {
guideId: string;
contentVersion: number;
completedAt: number;
}
function shouldSuggestGuide(
currentVersion: number,
state: GuideReadState | undefined
): boolean {
return state === undefined ||
state.contentVersion < currentVersion;
}
即使 shouldSuggestGuide() 返回 false,“实操步骤”入口也应继续存在。版本变化时可以重新显示一次轻量提示;若只是修正标点,不一定提高内容版本。读过状态属于偏好,不应和考试成绩混在同一个记录模型里。
八、验证要覆盖内容数量、入口和返回闭环
这次回归不能只断言数组长度是 6。真正需要覆盖的是“六项都能从用户入口读到,而且不破坏原实操考试”。
describe('科三实操指导可达性', () => {
it('主页能够到达指导内容并返回实操页', () => {
expect(isReachable(
'home',
'subject3.practical.guide.v1',
edges
)).assertTrue();
});
it('六个稳定步骤身份不重复', () => {
const ids = SUBJECT3_PRACTICAL_GUIDE.steps
.map((step: GuidanceStep) => step.id);
expect(new Set(ids).size).assertEqual(6);
});
});
页面层还要覆盖:默认进入仍显示实操面板;打开指导后出现六张步骤卡;返回后未意外开始考试;考试活动中不会被完整指导覆盖;主页返回能清理模块内部指导态;字体放大后文字可滚动阅读。
九、常见问题与排查顺序
| 现象 | 优先核对 | 常见原因 | 处理方向 |
|---|---|---|---|
| 源码有六步,页面搜不到 | 全工程引用 | 常量只导出未消费 | 加入内容注册与真实入口 |
| 点击“实操步骤”仍显示考试 | 模块内部表面状态 | 只改标题,未切渲染分支 | 让状态直接选择指导组件 |
| 返回后首页标题错乱 | 返回路径与 pageTitle | 共用了不完整的页面切换函数 | 明确 GUIDE→FLOW→HOME 路径 |
| 第三步以后不显示 | List 高度与滚动容器 | 外层、内层滚动约束冲突 | 保留一个主要滚动容器 |
| 改步骤文案后读过状态丢失 | 列表键和持久化键 | 使用文字或下标作身份 | 使用稳定步骤 ID |
| 开始考试后仍能展开全文 | examActive 门禁 | 入口没有区分学习和考试 | 活动会话中禁用完整指导 |
| 自动检查通过但按钮不存在 | 图边来源 | 测试硬编码了虚构入口 | 从注册表和入口声明生成图 |
排查顺序应从内容注册、入口绑定、状态迁移、组件渲染一路向下。先确认用户能否到达,再调间距和颜色;否则页面样式再完整,也只是不可见内容。
十、发布前验证清单与事实边界
- 六个步骤都有稳定 ID,顺序符合当前业务说明。
-
SUBJECT3_FLOW_STEPS已迁移或由注册表明确接管,没有两份漂移副本。 - “科三实操灯光”仍能直接进入原考试交互。
- 未考试时可以进入完整指导,并能返回实操页。
- 考试活动中不会因打开指导丢失倒计时或当前指令。
- 指导页能读到全部六步,没有两行截断和不可滚动区域。
- 主页到指导、指导到实操、实操到主页的路径都有回归用例。
- 读过状态按内容版本更新,不会永久隐藏入口。
- 文案资源、列表键和可访问说明经过逐项核对。
- 旧实操指令、计分、历史写入和灯态操作保持原行为。
当前源码能够确认的是:SUBJECT3_FLOW_STEPS 定义了六步且没有其他引用;主页的 subject3Flow 已经通往 SubjectThreePracticalView();该页面承载实操考试,没有渲染六步指导。GuidanceEntry、模块内部指导态、可达图和读过状态都是本文提出的接入方案,尚未修改 The_kemusan。
本文没有运行工程构建,没有生成新的 HAP,也没有在模拟器或真机上点击这些建议入口。完成实现后仍需按项目环境执行编译、页面交互、窗口适配和回归核对,不能把静态源码分析当成运行结果。
更多推荐



所有评论(0)