一、技术前言

在智慧物业管理领域,安全巡检是保障建筑运行的第一道防线。从消防通道占用复查到配电间红外测温,从喷淋管网压力巡检到水泵启动试运行测试,每一项巡检任务都需要精确的流程管控、清晰的进度追踪和即时的异常告警能力。传统巡检应用往往面临三大痛点:取证影像模糊导致责任争议、告警铃声单一导致响应迟缓、巡检路径复杂导致界面交互割裂。

HarmonyOS ArkUI 框架以其声明式 UI 范式为这些问题提供了系统级的解决方案。ArkUI 基于 TypeScript 扩展的 ArkTS 语言,通过 @Component 装饰器封装可复用组件,通过 @State@Observed 等状态管理装饰器实现数据驱动渲染,通过 @Builder 方法将复杂的 UI 结构拆分为可组合的构建块。这种架构天然适合巡检场景中"数据-视图-交互"紧耦合的需求。在 ArkUI 的组件树模型中,每个 @Entry 组件作为页面根节点,通过 StackColumnRow 等容器组件构建层次化布局,状态变量的变更自动触发依赖该状态的 @Builder 方法重新渲染,实现了"数据变化即视图更新"的响应式编程体验。

本平台深度融合了 HarmonyOS 6.1.1 的四大前沿特性。Camera Kit 提供了 VideoSession 的 AUTO_FRAMING(影随人动)能力链——通过 isControlCenterSupported 检测控制中心支持、getSupportedEffectTypes 查询效果枚举、enableControlCenter(true) 三步实现巡检取景时人员始终居中;同时 PhotoSession 的手动对焦三接口 isFocusDistanceSupportedsetFocusDistancegetFocusDistance 实现铭牌近拍到机房全景的精确对焦控制,并通过"设置值-读回值"差值校验确保对焦生效。Notification Kit 实现了沙箱自定义铃声链路——通过 buildWavBytes 生成正弦波 PCM 音频字节流,写入 EL1 沙箱 filesDir 目录,再以 'uri::' + fileUri.getUriFromPath(沙箱路径) 拼接填入 NotificationRequest.sound 字段,让不同等级告警拥有差异化铃声。Tabs 嵌套滚动 通过 nestedScroll(TabsNestedScrollMode) 让内层检查项列表滑到边缘后联动外层楼宇频道,实现巡检路径的自然流转,并通过 SELF_FIRSTSELF_ONLY 两种模式灵活控制滚动接力行为。

二、整体架构流程图

四大特性引擎

安全哨兵主组件

头部渐变Banner
Tab联动副标题+四特性状态胶囊

内容区 7 Tab 切换

底部导航Tab栏

全屏弹窗遮罩层

Tab0 任务
完成率统计+进度条清单+月度柱状图

Tab1 相机
XComponent预览+影随人动能力链

Tab2 对焦
能力查询+三档预设+Slider+读回校验

Tab3 频道
楼宇×检查项双层Tabs嵌套滚动

Tab4 日志
nestedScroll翻页时间轴

Tab5 告警
上报表单+授权卡+铃声库+发布历史

Tab6 我的
巡检员渐变大卡+绩效清单

Camera Kit
AUTO_FRAMING影随人动

Camera Kit
手动对焦三接口

Tabs嵌套滚动
nestedScroll模式

Notification Kit
沙箱自定义铃声

新建巡检任务面板

编辑巡检进度面板

删除确认面板

整体架构以主组件为根节点,采用 Stack 容器实现页面层叠:底层是 Column 纵向布局的头部 Banner + 内容区 + 底部 Tab 栏,顶层是全屏弹窗遮罩。内容区通过 currentTab 状态索引在 7 个 @Builder 方法间切换,每个 Tab 拥有完全独立的布局结构。四大特性(Camera Kit 影随人动、手动对焦、Tabs 嵌套滚动、Notification 沙箱铃声)分别挂载在相机、对焦、频道、告警四个 Tab 上,但它们的状态变量统一声明在组件顶层,实现跨 Tab 数据共享。弹窗系统通过三个布尔状态标志(addModaleditModaldelModal)控制三种面板的显示,点击遮罩区域统一关闭。

三、色彩体系设计

3.1 ColorPalette 接口定义

平台采用深色安全蓝灰主题,通过 ColorPalette 接口集中声明全部颜色字段。这种接口化设计确保了颜色引用的类型安全和可维护性,任何颜色字段的增删都可在接口层面获得编译器的检查反馈:

interface ColorPalette {
  bg: string;      // 页面背景(安全蓝灰黑)
  card: string;    // 卡片底色(深蓝灰)
  title: string;   // 主标题(冷白)
  sub: string;     // 副标题(蓝灰)
  text3: string;   // 三级弱文本(暗蓝灰)
  orange: string;  // 警示橙(主色)
  orangeD: string; // 警示橙深色(渐变起点)
  blue: string;    // 信息蓝(电气 / 内层日志)
  green: string;   // 合格绿(消防 / 已生效)
  red: string;     // 警示红(失败 / 删除)
  line: string;    // 分割线
  tabOn: string;    // Tab 选中色
  mask: string;    // 弹窗遮罩
  onMain: string;  // 橙底文字色(深暖黑)
}

3.2 COLORS 常量逐色分析

const COLORS: ColorPalette = {
  bg: '#14171C',      // 极深蓝灰黑,模拟夜间巡检环境
  card: '#1E232B',    // 卡片底色,比背景略亮一档
  dark: '#283039',    // 次级容器底色(统计格 / 进度条底)
  title: '#EDF1F5',    // 冷白色标题,高对比度保证暗光可读
  sub: '#ADBAC7',     // 蓝灰副标题,层次柔和过渡
  text3: '#74818E',   // 暗蓝灰弱文本,辅助信息不抢视觉
  orange: '#FF8A2B',  // 警示橙主色,巡检进度的视觉锚点
  orangeD: '#E06A10', // 深橙渐变起点,头部Banner到背景的过渡
  blue: '#4E9BE3',    // 信息蓝,电气检查项与内层日志标识
  green: '#34C98A',   // 合格绿,已完成状态与读回校验通过
  red: '#E85555',     // 警示红,逾期任务与删除操作
  line: '#2A313A',    // 分割线,低对比度不干扰内容
  tabOn: '#FF8A2B',   // Tab 选中色与主色一致
  mask: 'rgba(0,0,0,0.6)', // 半透黑遮罩
  onMain: '#231507'   // 橙底深字,保证按钮文字对比度
};

色彩设计遵循"安全警示"原则:橙绿蓝红四色分别对应"巡检中/已合格/信息参考/危险警告"四种语义状态,使用户在深色环境下凭颜色即可快速识别任务优先级。头部 Banner 的 linearGradientorangeDbg 实现警示橙到蓝灰黑的自然过渡,底部 7 Tab 栏选中态使用 orange 高亮,未选中态使用 text3 暗蓝灰弱化。onMain 字段是深暖黑色,专用于警示橙底色上的文字,确保按钮文案在橙色背景上依然保持足够的对比度。dark 色比 card 略深一档,用于统计格、进度条底等次级容器,形成卡片内的层次递进。

四、Tab 元数据与常量定义

4.1 底部导航 Tab 定义

interface TabMeta {
  icon: string;
  label: string;
}

const TAB_LIST: TabMeta[] = [
  { icon: '📋', label: '任务' },
  { icon: '📷', label: '相机' },
  { icon: '🎯', label: '对焦' },
  { icon: '🌀', label: '频道' },
  { icon: '📜', label: '日志' },
  { icon: '🚨', label: '告警' },
  { icon: '👤', label: '我的' }
];

7 个 Tab 单排排列,从任务管理到个人中心覆盖巡检全流程。每个 Tab 的图标与其功能语义紧密对应:📋 代表任务清单,📷 代表取证拍照,🎯 代表对焦精准,🌀 代表频道流转,📜 代表日志记录,🚨 代表告警上报,👤 代表巡检员中心。TabMeta 接口仅包含 iconlabel 两个字段,保持导航数据的极简性,ForEach 渲染时以 tab.label 作为键值确保唯一性。

4.2 Camera Kit 效果枚举

interface EffectInfo {
  type: number;
  name: string;
  desc: string;
}

const EFFECT_INFOS: EffectInfo[] = [
  { type: 0, name: 'BEAUTY', desc: '美颜 · since 20' },
  { type: 1, name: 'PORTRAIT', desc: '人像 · since 20' },
  { type: 2, name: 'AUTO_FRAMING', desc: '影随人动 · 6.1.1 新增' }
];

三效果枚举展示了 ControlCenterEffectType 的完整谱系。BEAUTY 和 PORTRAIT 自 API 20 起就存在,AUTO_FRAMING 是 6.1.1 新增能力,本平台正是利用这一新特性实现巡检跟拍时人员始终居中。在相机 Tab 的效果枚举表中,当 AUTO_FRAMING 被本机声明时,会在对应行追加"已声明"绿色徽标,使能力检测结果一目了然。

4.3 对焦预设三档

interface FocusPreset {
  label: string;
  distance: number;
  scene: string;
}

const FOCUS_PRESETS: FocusPreset[] = [
  { label: '铭牌近拍', distance: 0.1, scene: '0.1 · 铭牌/序列号' },
  { label: '设备中距', distance: 0.5, scene: '0.5 · 配电柜整柜' },
  { label: '环境远距', distance: 0.9, scene: '0.9 · 机房全景' }
];

对焦距离值范围 0.0(最近)到 1.0(最远),三档预设覆盖巡检取证的三个典型景别:0.1 用于拍摄设备铭牌和出厂序列号,0.5 用于拍摄配电柜整体和管线走向,0.9 用于拍摄机房全景和疏散通道。预设卡片选中态使用警示橙背景,未选中态使用卡片底色,通过颜色反差直观提示当前对焦景别。

4.4 嵌套滚动频道数据

const OUTER_CHANNELS: ChannelItem[] = [
  { name: '1 号楼', icon: '🏢' },
  { name: '2 号楼', icon: '🏬' },
  { name: '地下车库', icon: '🅿️' },
  { name: '配电房', icon: '⚡' },
  { name: '消防泵房', icon: '🚒' }
];
const INNER_TABS: string[] = ['消防', '电气', '管道', '通道', '监控'];

外层 5 个楼宇频道代表物业巡检的责任分区,内层 5 个检查项类别覆盖消防、电气、管道、通道、监控五大专业领域。两层 Tabs 嵌套形成 25 个检查矩阵,每个矩阵下有 8 条检查卡片,共 200 个检查点位,足以验证 nestedScroll 在真实业务量下的边缘接力效果。

4.5 月度隐患柱状图数据

const MONTH_NAME: string[] = ['03', '04', '05', '06', '07', '08'];
const MONTH_HAZARD: number[] = [12, 9, 15, 7, 11, 6];
const MONTH_MAX: number = 16;

近 6 个月隐患发现数据呈波动下降趋势(15→7→6),反映巡检整改效果。满刻度 16 处用于柱高归一化换算,柱状图随呼吸动画在正负 6% 区间交替波动,奇偶柱交替产生视觉律动感。

4.6 巡检员绩效数据

interface PerfRow {
  label: string;
  value: string;
  note: string;
}

const PERF_ROWS: PerfRow[] = [
  { label: '本月完成点位', value: '312', note: '应检 320 · 完成率 97.5%' },
  { label: '隐患整改闭环', value: '54/56', note: '闭环率 96.4% · 超期 2 项' },
  { label: '平均响应时长', value: '8 分钟', note: '严重告警到场时限 15 分钟' },
  { label: '本月巡检里程', value: '46.8 km', note: '含地库 B1/B2 两层环线' },
  { label: '拍照取证张数', value: '218 张', note: '含隐患整改前后对比图' },
  { label: '连续安全达标', value: '12 天', note: '当班期间安全零事故' }
];

绩效清单以 6 行数据呈现巡检员月度工作成果,前三行(完成点位、整改闭环、响应时长)使用警示橙数值色突出关键指标,后三行使用蓝灰弱化色呈现辅助数据。每行都附带 note 说明字段,提供完整的数据上下文。

五、工具函数

5.1 时间戳生成

function nowTime(): string {
  const d = new Date();
  return `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}:${String(d.getSeconds()).padStart(2, '0')}`;
}

nowTime 函数为 SwipeLog、FocusRecord、NoticeLog 三类时间线提供统一的时间戳格式化,使用 padStart(2, '0') 保证时分秒始终两位数,格式为 HH:mm:ss。该函数在数据模型构造器中被调用,确保所有时间戳格式一致。

5.2 嵌套模式文案映射

function modeLabel(mode: TabsNestedScrollMode): string {
  return mode === TabsNestedScrollMode.SELF_FIRST
    ? 'SELF_FIRST·先内后外' : 'SELF_ONLY·仅内层';
}
function modeShort(mode: TabsNestedScrollMode): string {
  return mode === TabsNestedScrollMode.SELF_FIRST ? '先内后外' : '仅内层';
}

两个函数将 TabsNestedScrollMode 枚举翻译为中文文案。modeLabel 返回完整技术文案用于日志记录和说明行,modeShort 返回短文案用于头部状态胶囊和模式切换 chips。SELF_FIRST 模式下内层滑到边缘会接力触发外层切换,SELF_ONLY 模式下内层滑动不联动外层。

5.3 影随人动状态配色

function framingStateColor(s: string): string {
  if (s === '影随人动已启用') return COLORS.green;
  if (s === '控制中心不支持' || s === 'AUTO_FRAMING 未声明') return COLORS.blue;
  if (s.indexOf('失败') >= 0 || s.indexOf('被拒') >= 0 || s.indexOf('未就绪') >= 0) return COLORS.red;
  return COLORS.text3;
}

该函数将影随人动能力链的各阶段结果映射为颜色:已启用为合格绿(表示巡检跟拍已就位),能力不支持为信息蓝(设备限制非故障),失败/被拒为警示红(需要处理),未查询为暗蓝灰弱化。通过字符串匹配实现多状态色彩路由,在 UI 渲染时提供即时的视觉反馈。

5.4 对焦距离与校验配色

function distanceLabel(v: number): string {
  if (v < 0.3) return '近拍 · 设备铭牌与出厂序列号';
  if (v < 0.7) return '中距 · 配电柜整柜与管线走向';
  return '远距 · 机房全景与疏散通道';
}

function focusOkColor(ok: string): string {
  if (ok === '已生效') return COLORS.green;
  if (ok.indexOf('失败') >= 0) return COLORS.red;
  return COLORS.orange;
}

distanceLabel 将 0.0 到 1.0 的连续对焦距离值映射为三段巡检景别文案,在 Slider 拖动时实时显示当前景别。focusOkColor 将对焦校验结论映射为颜色:已生效为绿(设置值与读回值偏差小于 0.01),失败为红(会话异常或接口调用出错),偏差为橙(设置值与读回值不一致,需关注但不致命)。

5.5 任务状态与会话模式配色

function taskStatusColor(s: string): string {
  if (s === '已完成') return COLORS.green;
  if (s === '进行中') return COLORS.orange;
  if (s === '待复查') return COLORS.blue;
  if (s === '已逾期') return COLORS.red;
  return COLORS.text3;
}

function sessionLabel(mode: string): string {
  if (mode === 'video') return 'VideoSession · 影随人动宿主';
  if (mode === 'photo') return 'PhotoSession · 手动对焦宿主';
  return 'idle · 未启动会话';
}

taskStatusColor 将四种巡检任务状态映射为语义色:已完成绿、进行中橙、待复查蓝、已逾期红,用户扫一眼即可判断任务紧急度。sessionLabel 将相机会话模式翻译为中文说明,标注 VideoSession 是影随人动的宿主、PhotoSession 是手动对焦的宿主,帮助用户理解两种会话的职能分工。

5.6 告警等级配色与 WAV 估算

function levelColor(level: string): string {
  if (level === '严重') return COLORS.orange;
  if (level === '紧急') return COLORS.red;
  return COLORS.blue;
}

function wavSizeText(durationMs: number): string {
  const bytes = 44 + Math.floor(44100 * durationMs / 1000) * 2;
  return `${(bytes / 1024).toFixed(1)} KB`;
}

levelColor 将告警三等级映射为颜色:一般为蓝(常规提示)、严重为橙(需尽快处理)、紧急为红(立即响应)。wavSizeText 根据时长估算 WAV 文件大小:44 字节头部加上 44100Hz 采样率、16bit 单声道 PCM 数据量,用于铃声列表中显示文件体积。

5.7 正弦波 WAV 音频生成

function buildWavBytes(freq: number, durationMs: number): ArrayBuffer {
  const sampleRate = 44100;
  const numSamples = Math.floor(sampleRate * durationMs / 1000);
  const dataSize = numSamples * 2;
  const buf = new ArrayBuffer(44 + dataSize);
  const view = new DataView(buf);
  // ... 44 字节 WAV 头写入(RIFF/WAVE/fmt /data)...
  for (let i = 0; i < numSamples; i++) {
    const t = i / sampleRate;
    const env = Math.min(1, i / (sampleRate * 0.02));       // 起音包络
    const decay = Math.max(0, 1 - t / (durationMs / 1000));  // 自然衰减
    view.setInt16(44 + i * 2, Math.round(Math.sin(2 * Math.PI * freq * t) * 0.5 * env * decay * 32767), true);
  }
  return buf;
}

这是 Notification Kit 沙箱铃声链路的起点函数。它生成标准 WAV 格式音频字节流:44 字节头部包含 RIFF 标识、WAVE 格式、PCM 编码、单声道、44100Hz 采样率、16bit 位深等标准字段。音频数据部分通过正弦波公式 sin(2*PI*freq*t) 生成纯音,叠加起音包络(前 20ms 渐入)和自然衰减(线性递减),避免点击杂音。不同频率(880Hz 疏散警报、660Hz 消防长鸣、1046Hz 门禁提示、1320Hz 周界蜂鸣)生成不同音高的告警铃声,返回的 ArrayBuffer 直接写入 EL1 沙箱文件。

5.8 检查要点池

function checkPoint(tabName: string, i: number): string {
  const pool: string[] = tabName === '消防'
    ? ['灭火器压力表指针', '消火栓水带卡扣', ...]
    : tabName === '电气'
      ? ['配电柜母排温度', '断路器接线端子', ...]
      : // ... 管道、通道、监控各 8 条
}

该函数为内层检查项卡片提供行业语义要点。五类检查各 8 条要点,覆盖灭火器压力表、配电柜母排温度、喷淋管网压力表、安全出口堆物、监控镜头遮挡等专业巡检内容。通过 tabName 参数路由到对应要点池,再按索引取值,确保每个检查矩阵的 8 张卡片都有差异化、真实化的内容。

六、数据模型层

6.1 TaskItem 巡检任务实体

@Observed export class TaskItem {
  building: string;  // 楼宇 / 分区名
  item: string;      // 巡检项目名
  progress: number;  // 进度(0~100)
  status: string;    // 状态(已完成/进行中/待复查/已逾期)
}

TaskItem 使用 @Observed 装饰器标记为可观察对象,当其属性变更时自动触发依赖该实例的 UI 组件重渲染。7 条种子数据覆盖四种状态:2 项已完成(消防通道复查、电梯机房点检)、3 项进行中(配电间测温、喷淋管网、直流屏电池)、1 项待复查(水泵试运行)、1 项已逾期(应急照明断电测试),为任务 Tab 的统计卡和进度条清单提供完整的数据基础。

6.2 FocusRecord 对焦记录

@Observed export class FocusRecord {
  time: string;      // 操作时间戳
  distance: number;  // 设置的对焦距离(0.0~1.0)
  readback: number;  // 读回值(-1 表示调用失败)
  ok: string;        // 校验结论(已生效/读回偏差/失败)
}

每条记录保存一次手动对焦操作的完整证据链:设置值、读回值、差值校验结论和时间戳。aboutToAppear 时种入 3 条历史记录(0.9 远距、0.5 中距、0.1 近拍),使对焦 Tab 首次进入即有时间线内容。列表使用 unshift 置顶最新记录,超过 20 条时 pop 移除尾部,保持时间线精简。

6.3 InnerCard 内层检查卡片

@Observed export class InnerCard {
  id: string;     // ForEach 键
  tag: string;    // 检查项类别名
  title: string;  // 检查点标题
  desc: string;   // 检查内容描述
}

innerMockData 生成器为每个楼宇频道 × 检查项类别组合生成 8 条卡片,id 格式为 {频道名}-{类别名}-{序号} 确保 ForEach 键唯一。描述文本包含频道图标、频道名、检查类别、序号和检查要点,使每张卡片的信息完整且可区分。8 条数据量保证内容超过一屏,为 nestedScroll 的边缘接力演示提供前提条件。

6.4 SwipeLog 两层翻页日志

@Observed export class SwipeLog {
  layer: string;    // '外层楼宇' / '内层检查项'
  tabName: string;  // 切换到的页签名
  fromIdx: number;  // 起始索引
  toIdx: number;    // 目标索引
  mode: string;     // 事发时的嵌套模式
  time: string;     // 时间戳
}

每当内层或外层 Tabs 发生 onChange 翻页事件,就会生成一条 SwipeLoglayer 字段区分翻页发生在哪一层(外层楼宇为橙、内层检查项为蓝),mode 字段记录事发时的 nestedScroll 模式,用于日志 Tab 的时间轴展示。日志列表封顶 40 条,超出时移除尾部,确保内存可控。在 SELF_FIRST 模式下,内层滑到边缘后触发外层切换,此时日志会连续记录"内层翻页"和"外层翻页"两条记录,形成"接力"证据链。

6.5 RingItem 告警铃声与 NoticeLog 发布历史

@Observed export class RingItem {
  name: string;       // 铃声名
  file: string;       // 沙箱文件名
  freq: number;       // 生成频率 Hz
  duration: number;   // 时长 ms
  size: string;       // 文件大小展示
  inSandbox: boolean; // 是否已写入沙箱
}

@Observed export class NoticeLog {
  title: string;  // 通知标题
  text: string;   // 通知正文
  time: string;   // 发布时间戳
}

RingItem 封装了告警铃声的完整元数据。4 条种子铃声覆盖不同频率和时长:880Hz/900ms 疏散警报、660Hz/1200ms 消防长鸣、1046Hz/600ms 门禁提示、1320Hz/450ms 周界蜂鸣。inSandbox 标志跟踪铃声是否已写入 EL1 沙箱,size 在导入沙箱后更新为实际文件大小。NoticeLog 记录每次通知发布的结果,成功和失败均记一条,失败记录标题包含"失败"关键字以红色高亮显示。

七、组件主体

7.1 状态变量声明

主组件声明了五大类状态变量。Tab 状态包含 currentTab 索引,控制 7 个 Tab 间的切换。弹窗状态包含 addModaleditModaldelModal 三个布尔标志和 editIdxdelIdx 两个索引,绑定 TaskItem 巡检任务实体的增删改操作。Camera Kit 成员包含 previewController(XComponent 控制器)、cameraInputpreviewOutputvideoSessionphotoSession 等控制器类成员(private 不参与渲染),以及 surfaceReadysessionModeframingStateframingSupportedfocusSupportedfocusDistancefocusRecordspermState 等渲染状态。Notification Kit 成员包含 grantednotifyIdringListcurrentRingIdxnoticeLogsalarmTitlealarmLevelalarmDescTabs 嵌套滚动成员包含 nestedModeouterIndexinnerIndexswipeLogs

7.2 生命周期管理

aboutToAppear() {
  notificationManager.isNotificationEnabled().then((enabled: boolean) => {
    this.granted = enabled;
  }).catch(() => {});
  this.focusRecords.unshift(new FocusRecord(0.9, 0.9, '已生效'));
  this.focusRecords.unshift(new FocusRecord(0.5, 0.51, '已生效'));
  this.focusRecords.unshift(new FocusRecord(0.1, 0.12, '读回偏差'));
  this.timer = setInterval(() => { this.breath = !this.breath; }, 1000);
}

aboutToDisappear() {
  clearInterval(this.timer);
  this.releaseSession();
}

aboutToAppear 执行三件初始化:异步查询通知授权状态并回填 granted、种入 3 条对焦历史记录使时间线非空、启动 1 秒间隔的呼吸动画定时器驱动 breath 翻转。aboutToDisappear 执行清理:清除定时器防内存泄漏、释放相机会话防后台占用。这种对称的生命周期管理确保组件退出时资源不残留。

7.3 Tab 切换与会话互斥

switchTab(idx: number) {
  if ((this.currentTab === 1 || this.currentTab === 2) && idx !== 1 && idx !== 2) {
    this.releaseSession();
  }
  this.currentTab = idx;
}

Tab 切换时有一个关键的互斥逻辑:当从相机 Tab(索引 1)或对焦 Tab(索引 2)切换到其他 Tab 时,必须释放相机会话。这是因为 cameraInput 同一时间只能绑定一个 session(VideoSession 或 PhotoSession),不复释会导致后续会话创建失败。相机和对焦两个 Tab 共用同一个 XComponent 的 Surface,因此切换到非相机/非对焦 Tab 时主动释放资源。

八、Camera Kit 方法群

8.1 权限申请

async requestCameraPermission(): Promise<boolean> {
  const ctx = this.getUIContext().getHostContext();
  if (ctx === undefined || ctx === null) { this.permState = '上下文未就绪'; return false; }
  const atManager = abilityAccessCtrl.createAtManager();
  const result = await atManager.requestPermissionsFromUser(ctx, ['ohos.permission.CAMERA']);
  const granted = result.authResults.length > 0 && result.authResults[0] === 0;
  this.permState = granted ? '已授权' : '权限被拒';
  return granted;
}

CAMERA 权限属于 user_grant 级别,需要动态申请。通过 abilityAccessCtrl.createAtManager() 创建权限管理器,调用 requestPermissionsFromUser 弹出系统授权框。判断条件为 authResults[0] === 0(0 表示已授权)。权限状态实时回填到 permState,在相机 Tab 的授权卡上以绿/橙色显示。上下文获取使用 getUIContext().getHostContext() 而非已废弃的 getContext(this),并进行判空兜底。

8.2 影随人动模式:VideoSession + AUTO_FRAMING 能力链

startVideoMode 方法是相机 Tab 的核心入口,完整流程为:检查 Surface 就绪 → 释放可能存在的 PhotoSession → 申请 CAMERA 权限 → 获取 CameraManager → 选后摄 → 查询预览 Profile → 创建 CameraInput 并 open → 创建 PreviewOutput 绑定 Surface → 创建 VideoSession → beginConfig/addInput/addOutput/commitConfig → 查询 AUTO_FRAMING 能力链 → start。

其中 AUTO_FRAMING 能力链(queryFraming 方法)是 6.1.1 的核心新特性,分三步执行:

queryFraming(session: camera.VideoSession) {
  // 第一步:检测控制中心是否支持
  if (!session.isControlCenterSupported()) {
    this.framingState = '控制中心不支持';
    return;
  }
  // 第二步:查询支持的效果类型枚举
  const effects = session.getSupportedEffectTypes();
  this.framingSupported = effects.includes(camera.ControlCenterEffectType.AUTO_FRAMING);
  if (!this.framingSupported) { this.framingState = 'AUTO_FRAMING 未声明'; return; }
  // 第三步:启用控制中心,系统接管构图
  session.enableControlCenter(true);
  this.framingState = '影随人动已启用';
}

三步形成"能力检测 → 能力枚举 → 能力启用"的完整链路。第一步 isControlCenterSupported 是同步方法,返回布尔值表示设备是否支持控制中心能力。第二步 getSupportedEffectTypes 返回当前设备支持的效果类型数组,通过 includes 判断是否包含 AUTO_FRAMING。第三步 enableControlCenter(true) 请求系统接管画面构图,巡查人员在取景框内移动时画面自动跟随,始终居中。任何一步失败都会设置相应的状态文案,在 UI 上以不同颜色显示。

8.3 手动对焦模式:PhotoSession + 三接口

switchToPhotoMode 方法先释放可能存在的 VideoSession(会话互斥),再创建 PhotoSession。创建流程与 VideoSession 类似,区别在于使用 SceneMode.NORMAL_PHOTO 场景模式。PhotoSession 启动后调用 queryFocusSupport 查询手动对焦能力:

queryFocusSupport() {
  this.focusSupported = this.photoSession.isFocusDistanceSupported();
}

isFocusDistanceSupported 是同步方法,返回当前设备是否支持手动设置对焦距离。当返回 false 时,对焦 Tab 的预设档位、Slider 和应用按钮都会降级为不可用状态。

手动对焦的核心操作在 applyFocus 方法中:

applyFocus() {
  this.photoSession.setFocusDistance(this.focusDistance);      // 设置对焦距离
  const readBack = this.photoSession.getFocusDistance();       // 读回对焦距离
  const ok = Math.abs(readBack - this.focusDistance) < 0.01 ? '已生效' : '读回偏差';
  this.focusRecords.unshift(new FocusRecord(this.focusDistance, readBack, ok));
}

三接口形成"查询 → 设置 → 读回"的完整闭环。setFocusDistance 设置目标对焦距离(0.0 最近到 1.0 最远),getFocusDistance 读回实际生效的对焦距离。通过差值校验(差值小于 0.01 判定已生效),将设置值、读回值和校验结论一并记入 FocusRecord 时间线。这种"设置后读回验证"的设计模式确保对焦操作的可靠性,避免静默失败。

8.4 会话释放

async releaseSession() {
  const session = this.videoSession ?? this.photoSession;
  // 先置空引用(防并发),再依次释放
  this.videoSession = undefined;
  this.photoSession = undefined;
  this.previewOutput = undefined;
  this.cameraInput = undefined;
  if (session) { session.off('error'); await session.stop(); await session.release(); }
  if (preview) { await preview.release(); }
  if (input) { await input.close(); }
  this.sessionMode = 'idle';
}

释放链遵循"先置空引用再释放资源"的安全模式,防止异步释放期间的其他代码路径误用已释放的对象。五步释放顺序为:session.off(‘error’) 移除错误监听 → session.stop() 停止会话 → session.release() 释放会话 → previewOutput.release() 释放预览输出 → cameraInput.close() 关闭相机输入。任何步骤异常都被 catch 捕获并记录日志,不阻断后续释放。

九、Notification Kit 方法群

9.1 通知授权

requestAuth() {
  const hostCtx = this.getUIContext().getHostContext() as common.UIAbilityContext;
  notificationManager.requestEnableNotification(hostCtx).then(() => {
    this.granted = true;
  }).catch((err: BusinessError) => {
    // 曾拒绝时返回 1600004,拉起通知设置页引导手动开启
    notificationManager.openNotificationSettings(hostCtx);
  });
}

通知授权采用"先请求后引导"的两级策略。requestEnableNotification 首次调用时弹出系统授权框;如果用户曾拒绝则返回错误码 1600004,此时通过 openNotificationSettings 拉起系统通知设置页,引导用户手动开启。两处都使用 getUIContext().getHostContext() 获取宿主上下文并传入,而非使用已废弃的无参版本。

9.2 沙箱铃声写入

saveRingToSandbox(fileName: string, freq: number, durationMs: number): string {
  const hostCtx = this.getUIContext().getHostContext() as common.UIAbilityContext;
  const appCtx = hostCtx.getApplicationContext();
  appCtx.area = contextConstant.AreaMode.EL1;   // 必须在 EL1 沙箱下
  const dir = appCtx.filesDir;                   // EL1 区域 files 目录
  const path = dir + '/' + fileName;
  const data = buildWavBytes(freq, durationMs);
  const file = fs.openSync(path, fs.OpenMode.CREATE | fs.OpenMode.WRITE_ONLY | fs.OpenMode.TRUNC);
  fs.writeSync(file.fd, data);
  fs.closeSync(file);
  return path;
}

沙箱铃声写入是 Notification 自定义铃声链路的核心环节。首先设置 appCtx.area = contextConstant.AreaMode.EL1,确保在 EL1 沙箱区域操作(设备级加密区,应用卸载后清除)。然后获取 filesDir 作为铃声文件存储目录。通过 buildWavBytes 生成正弦波 PCM 音频字节流,使用 fs.openSync 以"创建+只写+截断"模式打开文件,writeSync 写入数据,closeSync 关闭文件句柄。返回的沙箱路径后续用于 fileUri.getUriFromPath 转换。

9.3 通知发布与 sound 链路

publishNotice(title: string, text: string) {
  const ring = this.ringList[this.currentRingIdx];
  const appCtx = hostCtx.getApplicationContext();
  appCtx.area = contextConstant.AreaMode.EL1;
  const sandboxPath = appCtx.filesDir + '/' + ring.file;
  // ★ 沙箱路径 → fileUri → 'uri::' 前缀
  const uri = fileUri.getUriFromPath(sandboxPath);
  const soundVal = 'uri::' + uri;
  const request: notificationManager.NotificationRequest = {
    id: this.notifyId++,
    notificationSlotType: notificationManager.SlotType.SOCIAL_COMMUNICATION,
    content: { ... },
    sound: soundVal    // ★ 原来只能传 rawfile 文件名,现在支持沙箱 uri
  };
  notificationManager.publish(request);
}

publishNotice 是整个 Notification 链路的终点。sound 字段的值是 'uri::' + fileUri.getUriFromPath(沙箱路径)——这是 6.1.1 的新能力,原来 sound 只能填 rawfile 资源文件名,现在支持沙箱 URI 协议。通知槽类型设为 SOCIAL_COMMUNICATION(社交通信类,高优先级),通知内容包含标题、正文和附加文本(铃声名称)。notifyId 自增管理确保每条通知可独立撤销。发布失败时记录错误码并提示用户开启授权。

getSoundValue 方法提供 sound 字段值的实时预览,在告警 Tab 底部以等宽字体绿色显示完整的 'uri::...' 字符串,让开发者直观看到实际填入通知请求的 sound 值。

十、头部详解

@Builder
headerBanner() {
  Column({ space: 10 }) {
    Row() {
      Column({ space: 4 }) {
        Text('安全哨兵 · 物业安全巡检')
        Text(this.currentTab === 0 ? `任务 · 今日 ${this.taskList.length} 项巡检`
          : this.currentTab === 1 ? '相机 · 影随人动取证预览'
            : /* ... 7 个 Tab 联动副标题 */)
      }
      Circle({ width: 10, height: 10 }).fill(COLORS.onMain)
        .opacity(this.breath ? 0.9 : 0.45)  // 呼吸圆点
    }
    Row({ space: 8 }) {
      // 任务胶囊:巡检 N 项
      // 特性A胶囊:相机会话状态(idle灰/video绿/photo橙)
      // 特性B胶囊:通知授权状态(已授权绿/未授权红)
      // 特性C胶囊:嵌套模式(SELF_FIRST蓝/SELF_ONLY橙)
    }
  }.linearGradient({ angle: 160, colors: [[COLORS.orangeD, 0], [COLORS.bg, 1]] })
}

头部 Banner 是整个页面的信息枢纽,采用 160 度角的 linearGradientorangeD(深橙)渐变到 bg(蓝灰黑),营造警示橙到暗色背景的自然过渡。Banner 分为两行:第一行是应用名"安全哨兵 · 物业安全巡检"和 Tab 联动副标题——副标题根据 currentTab 显示当前 Tab 的功能描述(如"任务 · 今日 7 项巡检"、“相机 · 影随人动取证预览”、"对焦 · 手动对焦三接口"等),右侧是呼吸圆点,透明度随 breath 在 0.9 和 0.45 之间交替,形成 1 秒一次的呼吸效果。

第二行是四个状态胶囊,横向排列:任务胶囊显示巡检总数;相机胶囊显示会话模式(idle 灰圆点、video 绿圆点、photo 橙圆点);通知胶囊显示授权状态(已授权绿圆点、未授权红圆点);嵌套胶囊显示 nestedScroll 模式(SELF_FIRST 蓝圆点、SELF_ONLY 橙圆点)。四个胶囊让用户一眼掌握四大特性的实时状态,无需进入对应 Tab 即可感知系统运行情况。

十一、各 Tab 分析

11.1 Tab0 任务:完成率渐变大数字卡

任务 Tab 采用 Scroll 纵向滚动布局,包含四段内容。第一段是完成率统计卡:46px 等宽字体的巨大完成率数字(橙色),旁边是"今日巡检完成率"标题和数据同步说明。下方是橙到绿的渐变完成条,再下是三格统计(进行中橙、待复查蓝、已逾期红),每格独立背景色,数值用对应语义色。第二段是任务清单头,右侧"+ 新增"入口触发新建弹窗。第三段是任务进度条清单,每张卡片包含楼宇徽标、状态徽章、项目名、Progress 线性进度条和百分比,右侧"编""删"按钮触发编辑和删除弹窗。第四段是月度隐患柱状图。

11.2 Tab1 相机:XComponent 取证预览 + 影随人动

相机 Tab 采用固定高度 + Scroll 混合布局。顶部是授权状态卡,显示 CAMERA 权限状态和 Surface 就绪状态,两个按钮分别触发权限申请和显示 Surface 状态。中间是 XComponent 取证预览本体,使用 SURFACE 类型绑定 previewControllerlayoutWeight(1) 占满剩余高度,右下角叠加会话模式标签。底部是模式切换行(开启影随人动/停止会话)和可滚动的影随人动状态卡 + 效果枚举表。状态卡展示能力链三步结果和"巡查跟拍:人员始终居中"的场景说明。效果枚举表列出 BEAUTY、PORTRAIT、AUTO_FRAMING 三种效果,AUTO_FRAMING 行在本机已声明时追加绿色"已声明"徽标。

11.3 Tab2 对焦:能力查询 + 三档预设 + Slider + 读回校验

对焦 Tab 全部包裹在 Scroll 中,包含五段内容。能力查询卡显示 isFocusDistanceSupported 结果和当前会话状态,右侧"启动对焦会话"按钮触发 PhotoSession 创建。三档预设卡片(铭牌近拍 0.1、设备中距 0.5、环境远距 0.9)横向排列,选中态使用橙色背景高亮。焦距滑杆卡提供 0.0 到 1.0 步进 0.01 的连续调节,下方实时显示 distanceLabel 景别文案。应用按钮行包含"设置并读回校验"(触发 applyFocus 三接口闭环)和"刷新能力查询"。对焦记录时间线以 Scroll 列表展示 FocusRecord 数组,每行显示时间、设置值、箭头、读回值和校验结论徽标。

11.4 Tab3 频道:双层 Tabs 嵌套滚动

频道 Tab 是 nestedScroll 特性的演示主场。顶部是模式说明行和两个切换 chips(SELF_ONLY/SELF_FIRST),当前模式以橙色背景高亮。下方是双层位置说明行,橙圆点标示外层楼宇位置,蓝圆点标示内层检查项位置。核心是外层宿主 Tabs(5 个楼宇频道,BarMode.Scrollable 横滑页签),每个 TabContent 内嵌 innerTabs Builder——内层 Tabs(5 个检查项类别),内层挂载 nestedScroll(this.nestedMode)。当模式为 SELF_FIRST 时,内层检查项列表滑到边缘后继续滑动会联动触发外层楼宇频道切换;SELF_ONLY 模式下内层滑动止于边缘不联动。每次翻页都生成 SwipeLog 记录,在日志 Tab 可查看。

内层每个检查项子页签下是一个 List,包含 8 条 InnerCard 检查卡片,每张卡片显示检查类别标题、检查点标题、检查内容描述和两个标签徽标(责任区橙、拍照取证必填绿)。8 条数据保证内容超过一屏,为 nestedScroll 边缘接力提供前提。

11.5 Tab4 日志:nestedScroll 翻页时间轴

日志 Tab 以时间轴形式展示 SwipeLog 数组。顶部是计数行和清空按钮,下方是图例行(外层楼宇橙圆点、内层检查项蓝圆点、当前模式)。主体是无记录时的空态卡或时间轴列表。每条日志项高度固定 72px,左侧是时间列(HH:mm:ss 等宽字体 + OUT/IN 缩写徽标),中间是 3px 宽的竖线(外层橙、内层蓝,60% 透明度),右侧是内容卡(层级徽标 + 页签名 + 索引箭头 + 模式文案)。双层颜色编码使用户可以一眼区分翻页发生在哪一层。

11.6 Tab5 告警:上报表单 + 授权卡 + 铃声库 + 发布历史

告警 Tab 将 Notification Kit 的全部能力压缩在单个 Tab 中,包含五个区块。区块一是异常上报表单:标题输入框、三级告警 chips(一般/严重/紧急,选中态使用对应语义色)、描述文本域和发布按钮。区块二是通知授权状态卡,提供"重新查询"和"申请授权"两个操作。区块三是告警铃声库:当前默认铃声名 + 生成按钮 + 4 条铃声精简行,每行显示名称、频率/时长/大小/沙箱状态,提供"生成"和"设默认"操作。区块四是 sound 字段实时值预览,以等宽字体绿色显示完整的 'uri::...' 字符串。区块五是发布历史,以 NoticeLog 时间线展示最近 8 条通知发布记录,失败记录以红色标题高亮。

11.7 Tab6 我的:巡检员渐变大卡 + 绩效清单

我的 Tab 顶部是巡检员渐变大卡,使用 140 度角的 linearGradientorangeDorange 渐变背景。卡片内左侧是巡检员 Emoji 头像和半透明圆形背景,右侧是姓名(周正涛)、工号和班次信息。卡片下方分割线后是三格统计(本月点位 312、整改闭环 54、巡检里程 46.8km),全部使用 onMain 深暖黑色文字保证橙色背景上的可读性。底部是绩效清单 6 行,前三行数值使用警示橙突出关键指标,后三行使用蓝灰弱化。每行包含序号、标签、说明和数值,以 8px 间距排列在卡片背景中。

十二、图表卡片

@Builder
chartCard() {
  Column({ space: 10 }) {
    Row() {
      Text('📊 月度隐患发现数')
      Text('近 6 个月 · 单位处')
    }
    Row({ space: 10 }) {
      ForEach(MONTH_HAZARD, (val: number, idx: number) => {
        Column({ space: 5 }) {
          Text(val.toString())               // 数值标注
          Column().height(this.barHeight(idx)) // 渐变柱
            .linearGradient({ angle: 180, colors: [[COLORS.orange, 0], [COLORS.orangeD, 1]] })
          Text(MONTH_NAME[idx])              // 月份标签
        }
      })
    }.alignItems(VerticalAlign.Bottom)
  }
}

月度隐患柱状图采用 ArkUI 原生的 Column + ForEach 方案实现传统柱状图。每根柱子由数值标注、渐变柱体和月份标签三部分组成,垂直排列。柱高通过 barHeight 方法换算:基准高度 = MONTH_HAZARD[i] / MONTH_MAX * 96,再乘以呼吸波动系数(奇偶柱交替 1.06/0.94),取最小值 8 确保零数据也有可见柱体。渐变方向为 180 度(从上到下),颜色从 orangeorangeD,形成上亮下暗的立体效果。Row 容器使用 VerticalAlign.Bottom 对齐,使所有柱子底部齐平。

十三、底部 Tab 栏

@Builder
tabBar() {
  Row() {
    ForEach(TAB_LIST, (tab: TabMeta, index: number) => {
      Column({ space: 3 }) {
        Text(tab.icon).fontSize(17)
        Text(tab.label).fontSize(9)
          .fontColor(this.currentTab === index ? COLORS.tabOn : COLORS.text3)
      }.layoutWeight(1).onClick(() => { this.switchTab(index); })
    })
  }.backgroundColor(COLORS.card).border({ width: { top: 1 }, color: COLORS.line })
}

底部 Tab 栏采用自绘方案而非系统 Tabs 组件,实现完全的视觉控制。7 个 Tab 等宽排列(layoutWeight(1)),每个 Tab 上方是 17px 的 Emoji 图标,下方是 9px 的中文标签。选中态标签使用 tabOn(警示橙),未选中态使用 text3(暗蓝灰),通过颜色反差清晰标示当前页。顶部 1px 分割线使用 line 色(低对比度),与卡片背景色协调。点击触发 switchTab 方法,在切换前检查是否需要释放相机会话。

十四、弹窗系统

14.1 遮罩层

@Builder
modalOverlay(onClose: () => void) {
  Column() {
    Column().width('100%').layoutWeight(1).onClick(() => { onClose(); })  // 空白遮罩区
    if (this.addModal) { this.panelAdd(onClose) }
    else if (this.editModal) { this.panelEdit(onClose) }
    else if (this.delModal) { this.panelDel(onClose) }
  }.backgroundColor(COLORS.mask).justifyContent(FlexAlign.End)
}

弹窗系统采用 Stack 顶层覆盖方案。modalOverlay 是全屏 Column,背景为 rgba(0,0,0,0.6) 半透明遮罩。上半部分空白区域点击即关闭弹窗,下半部分根据 addModal/editModal/delModal 三个布尔标志三选一渲染对应面板。justifyContent(FlexAlign.End) 使面板从底部弹出,顶部圆角(borderRadius({ topLeft: 16, topRight: 16 }))形成底部抽屉效果。当三个标志同时为 false 时,遮罩层不渲染(由 build 中的条件判断控制)。

14.2 三态面板

新建面板panelAdd)包含楼宇输入框、巡检项目输入框和初始进度 Slider(步进 5),提交后 unshift 置顶任务清单,初始状态根据进度判断(100% 为已完成,否则为进行中)。

编辑面板panelEdit)显示当前任务的楼宇、项目和状态信息,提供进度 Slider(0~100 步进 5),保存时 100% 自动置为已完成状态。

删除面板panelDel)显示确认信息,包含楼宇和项目名,删除按钮使用 COLORS.red 红色背景强调危险操作,确认后 splice 从列表移除。

三个面板共享 closeAllModals 方法,统一关闭并复位所有表单字段和索引,确保下次打开时不会残留上次输入。

十五、功能模块对比表

模块核心特性关键接口数据模型状态变量交互入口
任务 Tab巡检进度管理Progress/ForEachTaskItemtaskList/formBuilding/formProgress新建/编辑/删除弹窗
相机 Tab影随人动取证VideoSession/isControlCenterSupported/enableControlCentersessionMode/framingState/framingSupported开启影随人动/停止会话
对焦 Tab手动对焦三接口PhotoSession/isFocusDistanceSupported/setFocusDistance/getFocusDistanceFocusRecordfocusSupported/focusDistance/focusRecords预设档位/Slider/设置并读回
频道 Tab嵌套滚动接力Tabs.nestedScroll(TabsNestedScrollMode)InnerCard/SwipeLognestedMode/outerIndex/innerIndex/swipeLogs模式切换chips/内外层翻页
日志 Tab翻页时间轴List/ForEachSwipeLogswipeLogs清空日志
告警 Tab沙箱自定义铃声buildWavBytes/saveRingToSandbox/fileUri.getUriFromPath/NotificationRequest.soundRingItem/NoticeLoggranted/ringList/currentRingIdx/noticeLogs上报表单/授权申请/铃声生成/发布通知
我的 Tab绩效看板linearGradient/ForEachPerfRow滚动浏览

深化解析:从代码结构到业务闭环

布局方式与数据流

物业巡检页面需要把任务、取证、日志、告警和人员绩效串成闭环。任务卡不只是展示进度,它还是后续相机取证与告警记录的业务入口;日志用于说明操作何时发生;状态颜色帮助巡检员优先处理逾期和严重隐患。分析这些模块时,重点要观察同一个任务实体如何被列表、弹窗、统计卡与进度图共同消费,以及修改后哪些区域会随状态更新。

页面根结构通常由头部、内容区和底部 Tab 栏组成。头部负责展示当前业务状态,内容区根据索引选择不同的 @Builder,底部导航负责修改索引。这样的结构把“当前显示什么”收敛为一个明确状态:用户点击 Tab 后先更新索引,ArkUI 再重新计算相关分支。各个 Builder 虽然共享主题色和页面级数据,却可以采用完全不同的布局方式;高密度列表适合纵向 Scroll,概览数据适合横向统计卡或双列 Flex,实时预览类组件需要独占有界高度,历史事件则适合时间轴或固定行高 List。

数据模型层承担界面与业务之间的契约。使用 @Observed 的实体保存可编辑字段,页面级 @State 数组负责驱动 ForEach。新增时创建新实体并插入数组,编辑时修改目标实体,删除时移除对应项。为了让列表差分稳定,key 应来自不会改变的唯一标识,不宜使用标题等可编辑字段。统计数字、完成比例和分类数量属于派生信息,可以从数组即时计算,避免同时维护两份状态后出现卡片已经更新、图表仍显示旧值的情况。

弹窗表单使用独立缓存是必要的。打开新增弹窗时清空缓存,打开编辑弹窗时复制目标字段,用户确认后才写回正式模型。这样点击取消不会污染列表数据。若直接把 TextInput 双向绑定到列表实体,用户尚未保存时卡片就可能跟着变化,破坏“确认提交”的交互语义。删除弹窗还需要保存目标索引或唯一标识,并在确认时再次校验目标存在,避免列表变化后误删其他项。

核心代码与状态驱动机制

@State 的价值不是简单替代普通变量,而是建立状态与界面之间的依赖关系。当前 Tab、筛选条件、动画开关、弹窗显隐、下载进度或能力状态发生变化时,只有读取这些变量的组件需要刷新。代码段中连续的修饰器调用分别控制尺寸、间距、背景、字体和事件,它们共同构成声明式描述;阅读时应从容器方向、子项分布、状态绑定和交互回调四个层面理解,而不是逐个孤立翻译属性名称。

ForEach 负责把数组映射为重复 UI。回调中的 item 提供业务字段,index 适合显示顺序,但不适合作为长期身份。列表发生新增或删除时,稳定 key 可以让框架复用未变化节点,减少重建。若直接修改对象属性后界面没有按预期刷新,可在保持实体身份的前提下替换数组引用;但不应为了刷新把所有元素都重新构造,否则会增加无意义渲染并丢失局部状态。

条件渲染体现了页面状态机。空闲时展示引导,准备中展示进度,成功时展示结果,失败时展示原因和重试入口。相比一个布尔值,四态文案更能覆盖异步能力。系统接口调用前先检查权限、设备支持和会话状态,调用后再读取结果校验。异常处理除了记录错误码,还要把可理解的反馈写入响应式状态,让用户知道失败发生在哪一步。

动画效果与颜色使用策略

呼吸动画通常由定时器周期翻转 breath,再把该状态映射为透明度、柱高或圆点半径的小幅变化。它适合表达“正在运行”或让统计图保持生命感,但幅度应克制,不能改变核心数据含义。柱状图的基础高度仍由真实数值计算,动画只能在很小范围内偏移;进度环的角度仍由完成比例决定,不能为了视觉效果显示超过真实进度的结果。页面离开时必须清理定时器,避免后台继续刷新。

颜色常量应按语义使用。主色承担选中态和主要操作,辅助色突出数据或次级动作,绿色表达完成与可用,橙色表达进行中或需要注意,红色只用于失败、逾期和删除等高风险场景。弱文本与分割线降低视觉权重,遮罩色用于聚焦弹窗。颜色不能成为唯一的状态信息,还要配合文字、图标或进度值,保证色觉差异用户也能理解。

渐变更适合头部大卡、核心指标或柱状图,不宜在每个小元素上重复使用。深色主题要检查正文与卡片背景的对比度,浅色主题则要避免辅助文字过淡。选中和未选中 Tab 除颜色差异外,还可以通过字重、图标透明度或底部指示器区分。这样既保持主题统一,又能建立清晰的信息层级。

各 Tab 之间的交互联动

各 Tab 不应只共享一个导航索引,还应围绕业务对象建立必要联动。列表页新增或编辑数据后,头部计数、图表和个人统计要同步更新;网页或地图产生的结果应写入记录模型,供下载、日志或我的页面继续展示;通知、字幕、相机等系统能力的状态应在头部胶囊或对应 Tab 中保持一致。跨 Tab 跳转时先更新必要参数,再修改当前索引,可以避免目标页面读取到旧条件。

切换离开重型组件时需要处理资源边界。相机输入、地图监听、字幕控制器、Web 下载代理和定时器都不能只创建不释放。可以在统一的 switchTab 方法中判断来源与目标,离开能力页时解除监听或停止会话;页面销毁时再执行兜底释放。释放方法应允许重复调用,并对每个资源独立判空,确保一次异常不会阻止后续清理。

交互反馈要覆盖成功与失败。按钮点击后先进入处理中状态并防止重复提交;成功后更新模型、关闭弹窗并显示结果;失败后保留用户输入,展示错误原因和重试入口。权限拒绝、能力不支持、网络失败、文件不存在和输入非法都属于正常业务分支。通过状态卡或行内提示展示这些分支,比只在控制台打印更符合完整产品体验。

边界场景与验证思路

空列表时应显示占位说明和新增入口,不能只留下空白。长标题需要限制行数并使用省略号,数字字段需要限定上下界,文本提交前要去除首尾空格。筛选后无结果应保留清除条件的入口。删除最后一项后,当前选择索引要回退到有效范围。异步搜索连续触发时,应防止较早请求晚返回后覆盖新结果。

验证数据链路时,可以依次检查新增、编辑、删除和筛选:新增后列表条数、统计数字和图表是否同时变化;编辑取消后正式数据是否保持不变;删除后 ForEach key 是否稳定;切换 Tab 再返回时必要数据是否仍在。验证系统能力时分别模拟支持、拒绝和异常,确认界面都有明确状态。验证动画时检查页面离开后是否停止,低性能设备上是否仍保持流畅。

视觉验收需要检查不同屏幕宽度、系统字体放大、深浅背景对比和长文本换行。表格中的布局方式、模型、字段数、核心操作、动画、状态颜色、数据量和特殊组件应与正文一致。Mermaid 图则需要对应真实的数据流和能力链路,节点文字加引号以避免中文或特殊字符导致解析失败。

组件化设计的进一步理解

参数化 Builder 适合抽取重复的统计格、状态行、标签和按钮组。参数只传入渲染所需数据和事件,不让子构建器直接依赖过多页面变量,可以降低耦合。业务复杂后,可把模型与系统能力封装为独立控制器,页面只负责组合 UI 和响应状态。这样既保留声明式代码的直观性,也能让权限、错误码翻译和资源释放得到集中管理。

当前单页面集中展示完整源码,便于博文逐段讲解。若演进为正式项目,可以按领域拆分组件:导航和页面框架位于容器层,列表、图表和弹窗位于展示层,数据读写和 Kit 接入位于服务层。组件之间通过参数、回调、@Link@ObjectLink 传递状态,不使用全局变量代替清晰的数据流。

性能优化首先来自减少不必要刷新。派生数据不要重复存储,动画状态不要进入列表 key,长列表使用稳定标识,Canvas 只在数据或尺寸变化时重绘。其次是控制资源生命周期,页面不可见时停止高成本任务。最后才是微调阴影、渐变和绘制细节。这样的优先级能保证页面在功能增加后仍然可维护。

通过以上补充,可以看到 ArkUI 的声明式模式并非只让布局语法更简洁,它更重要的价值是把数据变化、界面刷新和交互反馈连接为可追踪链路。理解每个代码段读取什么状态、写入什么状态、影响哪些组件,才能真正掌握文章中多个 Tab、图表、弹窗和系统能力协同工作的原理。

十六、总结与展望

本平台以"安全哨兵"为产品定位,在深色安全蓝灰 + 警示橙的主题下,完整实现了物业安全巡检的七大功能模块。四大 HarmonyOS 6.1.1 前沿特性的深度融合是平台的技术核心:

Camera Kit 影随人动通过 VideoSession 的三步能力链(isControlCenterSupported → getSupportedEffectTypes → enableControlCenter),实现了巡检跟拍时人员始终居中的智能构图。这一能力解决了传统巡检取证中"自拍不到位"的痛点——巡检员无需手动调整取景框,系统自动跟踪人员移动,确保取证画面中巡检员始终处于画面中心。

Camera Kit 手动对焦三接口通过 PhotoSession 的 isFocusDistanceSupported → setFocusDistance → getFocusDistance 闭环,实现了铭牌近拍到机房全景的精确对焦控制,并通过设置值-读回值差值校验确保对焦生效。三档预设覆盖了巡检取证的三个典型景别,Slider 提供连续微调能力,FocusRecord 时间线保存了每次对焦操作的完整证据链。

Notification Kit 沙箱自定义铃声通过 buildWavBytes 生成正弦波 PCM 音频 → 写入 EL1 沙箱 filesDir → fileUri.getUriFromPath 转换 → 填入 NotificationRequest.sound 的完整链路,让不同等级告警拥有差异化铃声。这一能力突破了原来 sound 字段只能填 rawfile 资源文件名的限制,使应用可以动态生成铃声而无需预置音频资源。

Tabs 嵌套滚动通过 nestedScroll(TabsNestedScrollMode) 的 SELF_FIRST/SELF_ONLY 两种模式,让内层检查项列表滑到边缘后联动外层楼宇频道,实现巡检路径的自然流转。SwipeLog 时间轴完整记录了两层翻页事件,为 nestedScroll 的接力行为提供了可视化证据。

展望未来,本平台可在以下方向持续演进:一是引入 AI 图像识别能力,对取证照片自动识别消防设施状态和隐患类型;二是集成 HMS Core 地图服务,实现巡检路径的实时轨迹追踪和最优路线推荐;三是接入鸿蒙分布式能力,实现多设备协同巡检——手机取证、平板填写报告、智慧屏展示指挥大屏;四是利用 ArkUI 的动画能力增强交互反馈,如告警发布时的全屏震动效果和铃声波形可视化。随着 HarmonyOS 的持续演进,安全巡检平台将不断融入更多系统级能力,为智慧物业管理提供更强大的技术支撑。

附录:DevEco Studio 创建新项目与查看 SDK 版本

本章节演示如何使用 DevEco Studio 创建一个 HarmonyOS 新项目,并查看当前 IDE 已安装的 SDK 版本,适合作为其他技术博文的补充操作指南。


一、创建新项目

1.1 进入欢迎界面

启动 DevEco Studio 后,首先看到的是欢迎界面。左侧导航栏默认选中 “项目”,右侧提供三个主要入口:

  • 新建项目:从头创建新项目
  • 打开项目:打开本地已有项目
  • 克隆仓库:从 Git 等版本控制拉取代码

点击 “新建项目” 按钮,进入项目创建向导。

在这里插入图片描述

1.2 选择项目模板

在弹出的"新建项目"对话框中,左侧分类标签提供了两种项目类型:

类型说明
应用(Application)开发标准的 HarmonyOS 应用,具备完整的 Ability 生命周期
元服务(Atomic Service)开发轻量级的原子化服务,无需安装即可使用

选择 “应用” 标签后,右侧展示多种模板。对于大多数场景,推荐选择 “Empty Ability” —— 这是一个最基础的入门模板,仅包含 Hello World 功能,适合从零开始构建应用。

在这里插入图片描述

1.3 配置项目信息

点击 “下一步” 后,进入项目配置界面,需要填写以下核心参数:

配置项示例值说明
项目名称(Project name)rollboat应用的项目名称,建议使用英文命名
包名(Bundle name)com.rollboat.myapplication应用唯一标识,采用反向域名格式
保存路径(Save location)D:\CodeFactory\rollboat项目本地存储路径,避免使用中文和空格
兼容 SDK(Compatible SDK)6.1.1(24)目标 HarmonyOS API 版本,点击"查看参考"可了解各版本差异
模块名称(Module name)entry主模块名称,默认 entry 为应用入口模块
设备类型(Device types)☑ Phone勾选目标设备:Phone / Tablet / 2in1 / Car / Wearable / TV

右侧预览区会实时展示当前模板的默认效果 —— 一个居中显示的 “Hello World” 文本。

在这里插入图片描述

1.4 完成创建

确认配置无误后,点击右下角 “完成” 按钮,IDE 将自动执行以下操作:

  1. 生成项目骨架(Stage 模型目录结构)
  2. 执行 ohpm install 安装依赖
  3. 运行 Hvigor 构建初始化(Build Init

构建日志中显示 “退出代码为 0” 表示项目初始化成功。

在这里插入图片描述

1.5 项目结构概览

创建完成后,左侧项目面板展示的是标准的 Stage 模型 目录结构:

rollboat/
├── .hvigor/                   # Hvigor 构建工具缓存
├── .idea/                     # IDE 配置文件
├── AppScope/                  # 应用级全局配置
│   └── app.json5
├── entry/                     # 主模块(入口模块)
│   ├── src/main/ets/
│   │   ├── entryability/      # Ability 生命周期管理
│   │   │   └── EntryAbility.ets
│   │   └── pages/             # UI 页面
│   │       └── Index.ets      # 首页(默认 Hello World)
│   ├── src/main/resources/    # 资源文件
│   ├── module.json5           # 模块配置
│   └── build-profile.json5    # 构建配置
├── oh_modules/                # OHPM 依赖包
├── build-profile.json5        # 工程构建配置
├── hvigorfile.ts              # Hvigor 构建脚本
└── oh-package.json5           # 包管理配置

核心文件 Index.ets 的默认代码如下,采用 ArkTS 声明式 UI 语法:

@Entry
@Component
struct Index {
  @State message: string = 'Hello World';

  build() {
    RelativeContainer() {
      Text(this.message)
        .id('HelloWorld')
        .fontSize($r('app.float.page_text_font_size'))
        .fontWeight(FontWeight.Bold)
        .alignRules({
          center: { anchor: '__container__', align: VerticalAlign.Center },
          middle: { anchor: '__container__', align: HorizontalAlign.Center }
        })
        .onClick(() => {
          this.message = 'Welcome';
        })
    }
    .height('100%')
    .width('100%')
  }
}
关键语法作用
@Entry标记为页面入口,可用于路由跳转
@Component声明为自定义组件
@State状态变量,数据变更时自动触发 UI 刷新
RelativeContainer相对布局容器,替代传统线性布局
.onClick()点击事件,此处点击后文本变为 “Welcome”

打开右侧 Previewer(预览器),选择 Phone 设备,即可实时预览 Hello World 效果,无需连接真机或启动模拟器。

在这里插入图片描述


二、查看 SDK 版本

2.1 查看 HarmonyOS SDK

DevEco Studio 安装时已内置 HarmonyOS SDK,无需单独下载。通过以下路径查看:

文件 → 设置 → HarmonyOS SDK(或快捷键 Ctrl + Alt + S 搜索 “HarmonyOS SDK”)

在设置面板中,可以看到当前已安装的 SDK 版本信息:

名称阶段状态
HarmonyOS 6.1.1Release✅ 已安装

界面顶部提示:“HarmonyOS SDK 已经包含在 IDE,无需单独安装”,省去了手动配置 SDK 的繁琐步骤。

在这里插入图片描述

2.2 查看 ArkUI-X SDK(跨平台扩展)

如果项目需要将 ArkUI 框架扩展到多个 OS 平台(Android / iOS / OpenHarmony),还需要配置 ArkUI-X SDK。路径如下:

文件 → 设置 → 语言和框架 → ArkUI-X

在这里可以查看已安装和可选的 ArkUI-X SDK 版本:

版本SDK 版本号阶段状态
API Version 246.1.1.100Release✅ 已安装
API Version 236.1.0.28Beta1未安装
API Version 226.0.2.112Release未安装

安装路径示例:D:\DevTools\ArkUI-X\sdk

说明:ArkUI-X 允许开发者使用一套 ArkTS 主代码,同时构建多平台应用。如果仅开发 HarmonyOS 原生应用,无需额外安装 ArkUI-X SDK。

在这里插入图片描述


三、小结

步骤操作关键点
创建项目欢迎页 → 新建项目 → 选择 Empty Ability 模板 → 配置项目信息 → 完成使用 Stage 模型 + ArkTS 语言
查看 SDK设置 → HarmonyOS SDKSDK 已内置,无需手动安装
跨平台扩展设置 → ArkUI-X根据需要安装对应 API 版本

至此,DevEco Studio 的项目创建与 SDK 环境确认全部完成,可以开始 HarmonyOS 应用的功能开发。


Logo

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

更多推荐