实操流程里,车辆可以在近光灯保持开启时使用左转向灯;雨雪场景还要求近光与示廓灯同时存在。当前页面却只有一个 lightState: string:左转向、近光、示廓双闪、雾灯双闪和关闭动作都会覆盖这一个值。代码能够显示某个“当前状态”,却很难回答多个独立灯组此刻分别是什么。

正交灯组状态建模封面

本文不重新讲动作常量、颜色映射或远近光动画,而是处理状态空间本身。示例把前照灯、转向灯、示廓灯、雾灯和危险报警灯拆成正交字段,再用纯 reducer 更新快照。建议代码尚未进入原工程,文中不会把静态建模缺口写成已经发生的设备故障。

一、一个字符串正在承担多个灯组

Index.ets 第139行声明:

@State lightState: string = LIGHT_CLOSED

这行代码适合表示互斥的单选状态,例如页面主题只能是亮色或暗色。但当前实操按钮不是一组互斥选项。QuestionBank.ets 第67—76行同时定义左转向、右转向、关闭转向、远近交替、近光、近光加示廓、示廓加双闪、雾灯加双闪和关闭全部。

命令也明确包含组合语义:

命令文案中要求的灯组
雾天行驶前照灯、前后雾灯、危险报警闪光灯
雨雪行驶近光灯、示廓灯
路边临时停车示廓灯、危险报警闪光灯
变道完成只关闭转向灯,不应暗示关闭其他灯组

这些事实来自 QuestionBank.ets 第119、130—133行。一个字符串只能保留最后一次赋值,因此它表达的是“最近选中的标签”,不是车辆灯组快照。

二、特殊组合值会随功能增长而膨胀

当前代码已经为“近光+示廓”增加了特殊值 LIGHT_LOW_MARKER。应用动作后,页面把整个 lightState 改成这个组合标签:

private applyPracticalActionToState(action: string): void {
  if (action === ACTION_LEFT_SIGNAL) {
    this.lightState = LIGHT_LEFT_SIGNAL
  } else if (action === ACTION_RIGHT_SIGNAL) {
    this.lightState = LIGHT_RIGHT_SIGNAL
  } else if (action === ACTION_CLOSE_SIGNAL) {
    this.lightState = ACTION_CLOSE_SIGNAL
  } else if (action === ACTION_LOW_MARKER) {
    this.lightState = LIGHT_LOW_MARKER
  } else {
    this.applyActionToState(action)
  }
}

状态行为了让这个组合至少显示近光,又在 isLightStatusActive() 中补了一条例外:当当前值为 LIGHT_LOW_MARKER 时,把近光也视为激活。这个分支位于 Index.ets 第1697—1699行。

如果继续沿用组合字符串,下一步会出现 LOW_LEFTLOW_RIGHTLOW_MARKER_LEFTFOG_HAZARD 等更多排列。每新增一个灯组,组合数量都可能成倍增长,显示、判题、历史和动画还要各自认识所有组合。

三、先画出互不覆盖的灯组轴

正交建模的关键不是把字符串换成数组,而是先确定哪些字段能独立变化。一个适合当前训练范围的最小快照如下:

export type HeadLampMode = 'off' | 'low' | 'high'
export type IndicatorMode = 'off' | 'left' | 'right'

export interface LampState {
  headLamp: HeadLampMode
  indicator: IndicatorMode
  markerOn: boolean
  fogOn: boolean
  hazardOn: boolean
  alternatingActive: boolean
  alternatingPhase: 'low' | 'high'
}

export const INITIAL_LAMP_STATE: LampState = {
  headLamp: 'off',
  indicator: 'off',
  markerOn: false,
  fogOn: false,
  hazardOn: false,
  alternatingActive: false,
  alternatingPhase: 'low'
}

headLampindicator 各自在有限集合中互斥;markerOnfogOnhazardOn 可以与它们同时为真;交替动作的业务状态与可见相位分开。这个模型不追求还原真实车辆全部电气系统,只覆盖当前题库已经出现的维度。

危险报警灯与左右转向灯的优先级仍需产品规则。若开启双闪时仪表应压制单侧转向显示,可以在派生显示层决定,不要让一个显示优先级反过来删除底层状态。

四、LampReducer让每个动作只修改自己的字段

页面不应在多个点击回调里手工拼对象。统一 reducer 可以让“关闭转向只关闭转向”成为可执行的不变量:

export enum LampAction {
  LOW,
  HIGH,
  LEFT_SIGNAL,
  RIGHT_SIGNAL,
  CLOSE_SIGNAL,
  LOW_MARKER,
  PARKING_HAZARD,
  FOG_HAZARD,
  CLOSE_ALL
}

export function reduceLamp(
  state: LampState,
  action: LampAction
): LampState {
  if (action === LampAction.LEFT_SIGNAL) {
    return { ...state, indicator: 'left' }
  }
  if (action === LampAction.RIGHT_SIGNAL) {
    return { ...state, indicator: 'right' }
  }
  if (action === LampAction.CLOSE_SIGNAL) {
    return { ...state, indicator: 'off' }
  }
  if (action === LampAction.LOW) {
    return { ...state, headLamp: 'low', alternatingActive: false }
  }
  if (action === LampAction.HIGH) {
    return { ...state, headLamp: 'high', alternatingActive: false }
  }
  if (action === LampAction.LOW_MARKER) {
    return {
      ...state,
      headLamp: 'low',
      markerOn: true,
      alternatingActive: false
    }
  }
  if (action === LampAction.PARKING_HAZARD) {
    return { ...state, markerOn: true, hazardOn: true }
  }
  if (action === LampAction.FOG_HAZARD) {
    return { ...state, fogOn: true, hazardOn: true }
  }
  return INITIAL_LAMP_STATE
}

这段 reducer 使用不可变返回值,便于 ArkUI 状态刷新,也便于比较动作前后哪些字段改变。实际接入时还要补齐业务规则:从远光切近光是否关闭雾灯、关闭全部是否取消交替调度、开启雾灯时是否自动带近光等,都应由明确命令要求决定。

重要的是,CLOSE_SIGNAL 只把 indicator 设为 off。原来已开启的近光、示廓或雾灯不会因为“关闭转向”被一个全局字符串覆盖。

单一状态到多灯组快照的转换流程

流程图展示 action 先经过 reducer 形成完整快照,判题、状态行、播报和历史随后读取同一结果。任何消费方都不再单独维护“近光加示廓”特判。

五、命令应描述目标状态,而不只描述最后一个按钮

当前 PracticalCommand 只保存一个 action,handlePracticalAction() 也用 action 相等判断正确。这适合一次性按钮题,不足以表达“近光与示廓都开启”“只关闭转向,其他状态保持”等要求。

可以把目标改成部分状态谓词:

export interface LampRequirement {
  headLamp?: HeadLampMode
  indicator?: IndicatorMode
  markerOn?: boolean
  fogOn?: boolean
  hazardOn?: boolean
}

export function matchesRequirement(
  state: LampState,
  required: LampRequirement
): boolean {
  if (required.headLamp !== undefined &&
      state.headLamp !== required.headLamp) {
    return false
  }
  if (required.indicator !== undefined &&
      state.indicator !== required.indicator) {
    return false
  }
  if (required.markerOn !== undefined &&
      state.markerOn !== required.markerOn) {
    return false
  }
  if (required.fogOn !== undefined &&
      state.fogOn !== required.fogOn) {
    return false
  }
  if (required.hazardOn !== undefined &&
      state.hazardOn !== required.hazardOn) {
    return false
  }
  return true
}

“雨雪行驶”可以声明 headLamp: 'low', markerOn: true;“关闭转向”只声明 indicator: 'off';“关闭全部”则单独使用全量关闭命令。未列出的字段表示本题不关心,不应擅自要求为false。

判题顺序也应改为:先把 action 交给 reducer,再用新快照匹配 requirement。这样用户分两步完成组合灯时,系统能看到累积结果,而不是只看最后一次点击。

六、交替动画相位不能擦掉稳定快照

当前 startAltFlash() 每160毫秒把 lightState 改成近光或远光,六步后固定回到近光。若灯态还是单字符串,交替过程中任何转向或示廓信息都无法同时保留。

更稳的方式是只更新前照灯相位:

export function beginAlternating(state: LampState): LampState {
  return {
    ...state,
    alternatingActive: true,
    alternatingPhase: 'low',
    headLamp: 'low'
  }
}

export function advanceAlternating(state: LampState): LampState {
  if (!state.alternatingActive) {
    return state
  }
  const nextPhase: 'low' | 'high' =
    state.alternatingPhase === 'low' ? 'high' : 'low'
  return {
    ...state,
    alternatingPhase: nextPhase,
    headLamp: nextPhase
  }
}

export function finishAlternating(state: LampState): LampState {
  return {
    ...state,
    alternatingActive: false,
    alternatingPhase: 'low',
    headLamp: 'low'
  }
}

这三个函数只拥有前照灯与交替字段。indicatormarkerOnfogOnhazardOn 原样保留。计时器仍由页面或调度服务管理,但每次回调只派发 ALT_TICK 事件,不直接修改多个 @State 字段。

第6篇已经讨论过如何取消旧闪烁任务,本篇只增加一个约束:动画任务不能成为整个车辆灯态的所有者。

七、状态行从快照派生,不再猜组合字符串

当前 PracticalStatusRow() 展示左转向、右转向、近光、交替、示廓双闪和雾灯双闪。接入新模型后,可以为每个状态项写清布尔来源:

private isStatusActive(statusId: string): boolean {
  if (statusId === 'left') {
    return this.lampState.indicator === 'left'
  }
  if (statusId === 'right') {
    return this.lampState.indicator === 'right'
  }
  if (statusId === 'low') {
    return this.lampState.headLamp === 'low'
  }
  if (statusId === 'alternating') {
    return this.lampState.alternatingActive
  }
  if (statusId === 'parkingHazard') {
    return this.lampState.markerOn && this.lampState.hazardOn
  }
  if (statusId === 'fogHazard') {
    return this.lampState.fogOn && this.lampState.hazardOn
  }
  return false
}

显示层只做派生,不修改状态。颜色仍可复用原有语义资源,但“是否激活”和“激活后用什么颜色”继续分开,避免把第17篇的配色问题混入本次状态迁移。

“关闭转向”没有必要成为一个持续点亮的灯组,它是把 indicator 设为off的事件。若界面需要反馈,可以显示短暂操作提示或提供“转向已关闭”文本,不要把事件伪装成长期灯态。

八、迁移要避免旧字段和新快照双写

一次把全部页面分支改完容易出现旧 lightState 与新 lampState 并存。推荐按下面顺序迁移:

  1. 新增纯 LampStateLampAction 和 reducer,并用固定输入验证;
  2. 把实操判题改为 requirement 匹配,但先保持旧 UI;
  3. 让状态行改读 lampState
  4. 把交替动画改成派发 reducer 事件;
  5. 删除 LIGHT_LOW_MARKER 等组合哨兵;
  6. 最后移除旧 lightState 写入口。

迁移期间应指定唯一所有者。若某个点击同时写 lightStatelampState,两者迟早会在一个未覆盖分支中分叉。调试日志可以暂时对比两种派生结果,但完成迁移后应删除双写逻辑。

LampState模块与消费关系

结构图把动作定义、纯 reducer、稳定快照、动画调度、判题和视图派生分成六个角色。状态快照只有 reducer 能创建,其他层只提交事件或读取结果。

九、验证矩阵要覆盖组合和保持不变量

单元验证可以直接给 reducer 一组初态与 action,检查整个快照:

it('closing_indicator_keeps_low_beam', 0, () => {
  const initial: LampState = {
    ...INITIAL_LAMP_STATE,
    headLamp: 'low',
    indicator: 'right',
    markerOn: true
  }

  const actual = reduceLamp(initial, LampAction.CLOSE_SIGNAL)

  expect(actual.indicator).assertEqual('off')
  expect(actual.headLamp).assertEqual('low')
  expect(actual.markerOn).assertTrue()
})

it('low_marker_satisfies_two_axes', 0, () => {
  const actual = reduceLamp(
    INITIAL_LAMP_STATE,
    LampAction.LOW_MARKER
  )
  expect(actual.headLamp).assertEqual('low')
  expect(actual.markerOn).assertTrue()
})

完整矩阵至少包括:

编号初态与动作预期
L38-01近光后开启左转向两个通道同时成立
L38-02近光加示廓headLamp=lowmarkerOn=true
L38-03关闭转向只改变 indicator
L38-04雾灯加双闪fog 与 hazard 同时为真
L38-05交替时已有示廓交替相位变化不清除示廓
L38-06关闭全部所有通道和动画状态恢复初值
L38-07重复相同 actionreducer 输出保持幂等
L38-08requirement 只列部分字段未列字段不参与失败判断
L38-09页面状态行每个高亮都能追到快照字段

十、排障与事实边界

现象根因线索处理方向
开启转向后近光高亮消失仍在写单一 lightState让动作只更新对应轴
关闭转向把其他状态一起清空把关闭事件当全局状态只修改 indicator
组合越来越多使用 LOW_MARKER_LEFT 一类哨兵拆成正交字段
动画结束后示廓状态丢失timer 覆盖整个快照tick 只改变前照灯相位
按钮正确但 requirement 失败action 与目标状态混为一谈先 reduce,再匹配目标
UI与判题结果不同两层各自解释组合字符串共同读取同一 LampState
重置后仍有闪烁只重置字段未取消任务reducer 与调度清理一起收口

当前源码能确认的是:实操命令包含多个组合灯语义,页面只保存一个字符串,并以覆盖赋值和个别特判维持显示。它不能证明某款设备已经出现状态错误,也不能代表真实车辆的完整灯控协议。

文中的 LampState、reducer、requirement 和迁移代码均为建议方案,尚未集成到 The_kemusan。本轮没有修改工程源码,没有执行项目构建,没有生成新的 HAP,也没有进行模拟器或真机验证。危险报警灯优先级、组合灯合法性和实际仪表反馈仍需在产品规则与设备环境中分别确认。

Logo

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

更多推荐