灯光模拟HarmonyOS应用实战-72-科三流程写了六步却到不了页面:用内容可达性检查接通实操指导

项目里已经有一份写得很完整的科三灯光流程:从上车后的初始状态、开启前照灯,到会车、通过急弯,再到停车和关闭全部灯光,一共六步。问题是,用户进入“科三实操灯光”页面后看到的是状态栏、指令面板和操作按钮,这六步没有任何页面引用。内容存在于源码,并不等于用户能够找到、进入和读完它。

这类缺口很容易被代码审查漏掉。数据文件能搜索到 SUBJECT3_FLOW_STEPS,主页也确实有“科三实操灯光”入口,开发者便可能把“有内容”和“内容可达”合并成一个结论。更稳的办法是建立内容可达性检查:每份面向用户的指导内容都要有稳定身份、明确入口、渲染承载、返回路径和自动核对,让孤立常量在合入前暴露出来。

科三实操指导可达性封面

本文解决四个具体问题:还原六步流程为何成为孤立内容;区分实操考试与实操指导;用 GuidanceEntry 接通入口和渲染;用可达性图与回归清单守住后续改动。

一、当前六步流程只在题库模型中定义

当前 QuestionBank.ets 导出了 SUBJECT3_FLOW_STEPS。数组有六项,内容覆盖初始灯态、近光、远近光交替、停车警示以及考试结束后的关闭操作。

export const SUBJECT3_FLOW_STEPS: string[] = [
  '上车后先确认灯光处于关闭或初始状态。',
  '听到“请开启前照灯”后,再开启近光灯。',
  '遇到会车、跟车、照明良好道路,保持或切换近光灯。',
  '遇到急弯、坡路、拱桥、人行横道,使用远近光交替。',
  '遇到故障难以移动或临时停车,使用示廓灯加危险报警闪光灯。',
  '听到考试完成后,关闭所有灯光。'
];

这六项是当前源码事实,但全工程对 SUBJECT3_FLOW_STEPS 的引用只有定义本身。Index.ets 的导入列表包含 PRACTICAL_COMMANDSPRACTICAL_ACTION_OPTIONS 等实操数据,没有导入这份流程数组。因此可以静态确认它尚未进入当前页面渲染链;这不能证明用户曾经点击失败,也不能说明作者原本打算把它做成独立页面还是实操页内说明。

证据位置当前事实能得出的结论
QuestionBank.ets导出六项 SUBJECT3_FLOW_STEPS指导内容已经写入源码
Index.ets 导入区没有导入该常量当前页面文件不消费它
MODULESsubject3Flow 实操入口用户可以进入实操考试
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_lightsenable_low_beamordinary_low_beamalternate_beamparking_warningfinish_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 恢复比按第几个更可靠。

六、用内容可达图核对“定义—入口—页面—返回”

GuidanceEntry与页面可达图结构

页面可读性可以转成一张小型有向图。内容节点必须从主页或模块入口可达,还要存在返回边。下面的实现放在构建期脚本或单元层更合适,不必把图算法带入运行页面。

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,也没有在模拟器或真机上点击这些建议入口。完成实现后仍需按项目环境执行编译、页面交互、窗口适配和回归核对,不能把静态源码分析当成运行结果。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐