一、技术前言

在智慧城市与精细化物业管理深度融合的今天,物业安全巡检正从"纸质表单+人工流转"向"移动端取证+实时告警+数据闭环"全面转型。从消防通道占用复查到配电间红外测温,从喷淋管网压力巡检到水泵启动试运行测试,每一个巡检点位都承载着生命财产安全的重量。传统巡检管理应用面临四大痛点:巡查跟拍画面构图不稳定导致取证重点丢失、设备铭牌近拍对焦不准导致序列号模糊、楼宇与检查项双层维度切换时滚动体验割裂、告警铃声无法自定义导致推送辨识度低。

HarmonyOS ArkUI 框架为这些痛点提供了系统级解决方案。ArkUI 的声明式 UI 范式通过 @Component 封装可复用组件、@State 管理响应式状态、@Builder 拆分复杂 UI 结构,天然适合"任务-取证-告警"三层架构。@Observed 装饰器让数据模型字段级变化被 UI 感知,实现"进度更新即视图刷新"的流畅体验。@Entry 标记入口组件,配合 Stack 层叠容器实现内容区与弹窗系统的精准叠加。

本平台深度融合 HarmonyOS 6.1.1 的四大前沿特性。Camera Kit 提供 VideoSession 的 AUTO_FRAMING(影随人动)能力链——通过 isControlCenterSupportedgetSupportedEffectTypesenableControlCenter 三步实现巡查跟拍时巡检人员始终居中构图;同时 PhotoSession 的手动对焦三接口 isFocusDistanceSupportedsetFocusDistancegetFocusDistance 实现从设备铭牌近拍到机房全景远距的精确对焦控制,并带读回校验确保设置值与硬件实际值一致。Notification Kit 引入了沙箱自定义铃声链路——通过 buildWavBytes 正弦波生成 16bit 单声道 PCM 的 WAV 字节流,写入 EL1 沙箱 filesDir 目录后,以 fileUri.getUriFromPath 转换路径为 URI,最终在通知请求的 sound 字段填入 'uri::' + uri 前缀格式,实现巡检告警铃声的完全自定义。Tabs 嵌套滚动 通过 nestedScroll(TabsNestedScrollMode) 属性实现内层检查项页签滑到边缘后是否接力联动外层楼宇频道,SELF_FIRST(先内后外)与 SELF_ONLY(仅内层)两种模式可实时切换。Canvas 绘制 实现月度隐患柱状图,配合 breath 状态驱动的呼吸动画定时器,柱体高度随奇偶索引交替微动,模拟实时数据更新效果。

二、整体架构流程图

Page1213 主组件

headerBanner 头部渐变横幅

内容区 7 Tab 切换

tabBar 底部导航

弹窗系统 add/edit/del

Tab0 任务
完成率渐变大数字卡+进度条清单+月度隐患柱状图

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

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

Tab3 频道
楼宇×检查项双层Tabs+nestedScroll

Tab4 日志
SwipeLog状态时间轴

Tab5 告警
上报表单+授权卡+铃声行+sound预览+发布历史

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

Camera Kit
VideoSession AUTO_FRAMING 影随人动

Camera Kit
PhotoSession 手动对焦三接口

Tabs 嵌套滚动
nestedScroll 边缘接力

Notification Kit
buildWavBytes 正弦波写EL1沙箱铃声

panelAdd 新建巡检任务

panelEdit 编辑巡检进度

panelDel 删除确认

架构以 Page1213 为根组件,使用 Stack 容器层叠:底层 Column 纵向排列头部渐变横幅、分割线、内容区和底部 Tab 栏,顶层是全屏弹窗遮罩(addModal/editModal/delModal 三态条件渲染)。内容区通过 currentTab 在 7 个 @Builder 方法间切换,四大特性分散在相机(影随人动)、对焦(手动对焦)、频道(嵌套滚动)和告警(沙箱铃声)四个 Tab 上。状态变量统一声明在组件顶层实现跨 Tab 共享——sessionMode 在相机与对焦两个 Tab 间互斥切换会话,framingState 贯穿相机会话生命周期,nestedMode 在频道 Tab 切换后其翻页事件被日志 Tab 消费展示。

三、色彩体系设计

3.1 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;  // 橙底文字色(深暖黑)
}

接口定义了 14 个语义化色彩字段。与普通色值常量不同,每个字段都附带行业语义注释:orange 不仅是一个色值,更代表"警示橙"这一安全巡检场景中的核心视觉语言;green 对应"合格绿",用于消防检查通过与对焦校验已生效的状态反馈;onMain 是一个特殊字段——当文字需要叠加在橙色渐变背景上时使用深暖黑色,确保对比度达标。

3.2 COLORS 常量逐色分析

const COLORS: ColorPalette = {
  bg: '#14171C',
  card: '#1E232B',
  dark: '#283039',
  title: '#EDF1F5',
  sub: '#ADBAC7',
  text3: '#74818E',
  orange: '#FF8A2B',
  orangeD: '#E06A10',
  blue: '#4E9BE3',
  green: '#34C98A',
  red: '#E85555',
  line: '#2A313A',
  tabOn: '#FF8A2B',
  mask: 'rgba(0,0,0,0.6)',
  onMain: '#231507'
};

色彩体系以"安全蓝灰 + 警示橙"为核心对比。背景色 #14171C 是近乎纯黑的深蓝灰,模拟夜间巡检或地下车库等暗光环境下的视觉沉浸感。卡片底色 #1E232B 比背景亮一档蓝,形成层次但不刺眼。主色 #FF8A2B 警示橙在深色背景上具有极高的视觉穿透力,用于渐变横幅、进度条、按钮主色和 Tab 选中色。渐变起点 #E06A10 是更深的橙色,用于头部横幅 160 度线性渐变的起始色,模拟警示灯由近及远的衰减效果。

值得注意的是 onMain 字段取值 #231507——这是一个带有暖色调的深棕黑色,专门用于叠加在橙色渐变背景上的文字。头部横幅的标题、副标题和"我的"Tab 渐变大卡上的文字均使用此色,而非纯白,这是因为纯白在饱和橙色上的眩光效应会降低可读性,而暖深色既保证对比度又维持视觉和谐。mask 遮罩使用 rgba(0,0,0,0.6) 半透明黑色,在弹窗场景下既遮挡底层内容又保持微弱可见,符合用户对弹窗层级的认知预期。

四、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 由 Emoji 图标与中文标签组成。Tab 顺序经过精心设计:任务为入口 Tab 紧贴左侧,相机与对焦相邻因为共享同一 XComponent 预览面,频道展示嵌套滚动后日志承接翻页事件,告警是 Notification 压缩模式的核心,我的放在末尾作为巡检员个人中心。TabMeta 接口仅含 iconlabel 两个字段,保持导航数据的极简性。

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 新增' }
];

EFFECT_INFOS 展示了 ControlCenterEffectType 枚举的三种效果类型。BEAUTY(美颜)和 PORTRAIT(人像)自 API 20 起即存在,而 AUTO_FRAMING(影随人动)是 HarmonyOS 6.1.1 新增能力。在相机 Tab 的效果类型枚举卡中,当 info.type === 2framingSupported 为真时,会额外渲染"已声明"绿色徽章,让用户直观看到本机硬件是否支持影随人动。

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 · 配电柜整柜' },
  { scene: '0.9 · 机房全景', ... }
];

对焦距离取值范围为 0.0(最近)至 1.0(最远),三档预设覆盖了巡检取证的三个典型景别。0.1 铭牌近拍用于拍摄设备出厂序列号与铭牌信息,这是设备台账登记的必拍环节;0.5 设备中距用于拍摄配电柜整柜外观与管线走向,是电气巡检的标配景别;0.9 环境远距用于拍摄机房全景与疏散通道,用于环境合规性取证。预设档位与 Slider 滑杆联动,点击档位即跳转对应距离值,配合 distanceLabel 函数生成景别说明文案。

4.4 楼宇频道与检查项

interface ChannelItem {
  name: string;
  icon: string;
}

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

const INNER_TABS: string[] = ['消防', '电气', '管道', '通道', '监控'];

外层楼宇频道代表物业巡检的责任分区,五个分区覆盖了典型物业的安全管理范围。内层检查项是对每个分区需要检查的专业类别拆分——消防类检查灭火器与消火栓,电气类检查配电柜与断路器,管道类检查喷淋管网与排水系统,通道类检查疏散通道净宽与堆物,监控类检查镜头遮挡与录像保存天数。这两层维度构成了"楼宇×检查项"的双层 Tabs 结构,是 nestedScroll 嵌套滚动的核心演示场景。

4.5 告警等级与月度隐患数据

const ALARM_LEVELS: string[] = ['一般', '严重', '紧急'];

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;

告警等级分为三档:一般对应蓝色提示、严重对应橙色警示、紧急对应红色告警,levelColor 函数将其映射为对应色彩。月度隐患数据展示近 6 个月各月发现的隐患数量(单位:处),MONTH_MAX 取值 16 作为柱状图满量程参照,确保所有月份的柱高均不超过容器边界。5 月份数据 15 接近满量程,体现了汛期前消防设施全面排查的隐患发现高峰。

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: '当班期间安全零事故' }
];

绩效清单行展示了巡检员月度工作的六个核心指标。每行包含 label(指标名)、value(主数值)和 note(辅助说明),其中前三行(完成点位、整改闭环、响应时长)使用警示橙高亮数值,因为这三项是安全考核的核心 KPI。后三行(巡检里程、取证张数、连续达标)使用蓝灰弱化色,作为辅助参考。note 字段中的"应检 320""闭环率 96.4%"等补充数据让数字不再孤立,形成了完整的绩效画像。

五、工具函数

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')}`;
}

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

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

nowTime 是全局时间戳工具,使用 padStart(2, '0') 确保时分秒均为两位数格式,被 SwipeLogFocusRecordNoticeLog 三种日志实体共用,保证时间格式统一。modeLabelmodeShortTabsNestedScrollMode 枚举的双语翻译函数——完整版用于日志记录(含枚举原名便于排查),短版用于 UI chips 标签节省空间。这种同一概念提供两种粒度文案的设计在信息密集型界面中尤为重要。

5.2 状态色彩映射函数群

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;
}

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

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 levelColor(level: string): string {
  if (level === '严重') return COLORS.orange;
  if (level === '紧急') return COLORS.red;
  return COLORS.blue;
}

这组函数将业务状态字符串映射为色彩值。framingStateColor 处理影随人动能力链的四种状态:已启用(绿)、不支持/未声明(蓝为信息提示而非错误)、失败/被拒/未就绪(红为错误)。focusOkColor 区分对焦校验三态。taskStatusColor 覆盖巡检任务四态生命周期。levelColor 将告警等级映射为三色。这些函数将"状态→颜色"的映射逻辑从 UI 渲染代码中解耦,避免在 @Builder 方法中堆砌条件判断,提高了代码可维护性。

5.3 会话标签与对焦景别函数

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

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

sessionLabelsessionMode 状态值翻译为带技术语义的中文说明,同时标注每种会话类型的职责——VideoSession 是影随人动的宿主,PhotoSession 是手动对焦的宿主,idle 表示未启动。distanceLabel 按对焦距离值划分三个景别区间,低于 0.3 为近拍、0.3 至 0.7 为中距、0.7 以上为远距,每个景别附带巡检场景说明,让用户在拖动 Slider 时实时看到当前距离对应的取证用途。

5.4 WAV 音频生成与文件大小估算

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

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 字节 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;
}

wavSizeText 按公式"44 字节头 + 采样数 × 2 字节"估算 WAV 文件大小,用于铃声导入后的文件大小展示。buildWavBytes 是 Notification Kit 沙箱铃声链路的核心——它生成符合 RIFF/WAVE 规范的完整 WAV 字节流。44 字节头部依次写入 RIFF 标识、文件大小、WAVE 标识、fmt 子块、PCM 编码(1)、单声道(1)、采样率(44100)、字节率、块对齐(2)、位深(16bit)以及 data 子块头。音频数据部分按 sin(2πf·t) 正弦波公式生成,叠加了两层包络:起音包络 env 在前 20ms 内从 0 渐升到 1 避免爆音,自然衰减 decay 从 1 线性降到 0 模拟铃声衰减。最终乘以 0.5 振幅和 32767(16bit 最大值),通过 setInt16 以小端序写入。

5.5 检查要点池函数

function checkPoint(tabName: string, i: number): string {
  const pool: string[] = tabName === '消防'
    ? ['灭火器压力表指针', '消火栓水带卡扣', '疏散指示标识灯', ...]
    : tabName === '电气'
      ? ['配电柜母排温度', '断路器接线端子', '电缆桥架盖板', ...]
      : ...;
  const idx = Math.min(pool.length - 1, Math.max(0, i - 1));
  return pool[idx];
}

checkPoint 是检查项卡片的要点生成器,按五类检查(消防/电气/管道/通道/监控)各提供 8 个行业语义要点。函数内部使用三层三元运算符根据 tabName 选择对应的要点池,再按索引 i(取值 1~8)取出对应要点。Math.minMath.max 做了边界保护,防止索引越界返回空值。每个要点都是真实巡检场景中的专业术语,如"配电柜母排温度""喷淋管网压力表"等,确保生成的检查卡片内容具有行业可信度。

六、数据模型层

6.1 TaskItem 巡检任务条目

@Observed export class TaskItem {
  building: string;
  item: string;
  progress: number;
  status: string;
  constructor(building: string, item: string, progress: number, status: string) {
    this.building = building; this.item = item; this.progress = progress; this.status = status;
  }
}

const TASK_LIST: TaskItem[] = [
  new TaskItem('1 号楼', '消防通道占用复查', 100, '已完成'),
  new TaskItem('2 号楼', '配电间红外测温巡检', 72, '进行中'),
  new TaskItem('地下车库', '喷淋管网压力巡检', 45, '进行中'),
  new TaskItem('消防泵房', '水泵启动试运行测试', 88, '待复查'),
  new TaskItem('配电房', '直流屏电池巡检', 30, '进行中'),
  new TaskItem('3 号楼', '电梯机房月度点检', 100, '已完成'),
  new TaskItem('5 号楼', '应急照明断电测试', 0, '已逾期')
];

TaskItem 使用 @Observed 装饰器,使其字段级变化(如 progress 从 45 变为 100)被 ArkUI 框架自动感知并触发 UI 刷新。四个字段 building(楼宇/分区)、item(巡检项目名)、progress(进度 0~100)、status(状态枚举)构成了巡检任务的核心实体。TASK_LIST 种子数据包含 7 条任务,覆盖了已完成、进行中、待复查、已逾期四种状态,其中"5 号楼应急照明断电测试"进度为 0 且状态为已逾期,模拟了遗漏巡检的真实场景。

6.2 FocusRecord 对焦记录

@Observed export class FocusRecord {
  time: string;
  distance: number;
  readback: number;
  ok: string;
  constructor(distance: number, readback: number, ok: string) {
    this.time = nowTime(); this.distance = distance; this.readback = readback; this.ok = ok;
  }
}

FocusRecord 记录每次手动对焦操作的三元组:设置值 distance、读回值 readback(-1 表示调用失败)、校验结论 ok(已生效/读回偏差/失败)。构造函数自动调用 nowTime() 记录操作时间戳。在 aboutToAppear 生命周期中预置了 3 条种子记录——距离 0.9 读回 0.9 已生效、距离 0.5 读回 0.51 已生效、距离 0.1 读回 0.12 读回偏差——展示了硬件读回值与设置值之间可能存在的微小偏差。

6.3 InnerCard 与 SwipeLog

@Observed export class InnerCard {
  id: string; tag: string; title: string; desc: string;
  constructor(id: string, tag: string, title: string, desc: string) { ... }
}

@Observed export class SwipeLog {
  layer: string; tabName: string; fromIdx: number; toIdx: number; mode: string; time: string;
  constructor(layer: string, tabName: string, fromIdx: number, toIdx: number, mode: string) { ... }
}

InnerCard 是内层检查项的卡片实体,id 字段作为 ForEach 的键值保证列表项稳定。innerMockData 函数为每个楼宇频道×检查项组合生成 8 条卡片数据,内容超一屏是 nestedScroll 演示的前提条件——只有内容超出可视区域,滑动到边缘的"接力"行为才能被感知。

SwipeLog 记录两层翻页事件:layer 标识来源(外层楼宇/内层检查项),fromIdxtoIdx 记录切换前后的索引,mode 记录事发时的嵌套模式。这条日志是验证 nestedScroll 接力行为的核心证据——在 SELF_FIRST 模式下,内层滑到边缘继续滑动会触发外层 onChange 回调,此时日志中会出现 layer='外层楼宇' 的记录。

6.4 RingItem 与 NoticeLog

@Observed export class RingItem {
  name: string; file: string; freq: number; duration: number; size: string; inSandbox: boolean;
  constructor(name: string, file: string, freq: number, duration: number, size: string, inSandbox: boolean) { ... }
}

const RING_LIST: RingItem[] = [
  new RingItem('疏散警报', 'ring_evacuate.wav', 880, 900, '—', false),
  new RingItem('消防长鸣', 'ring_firelong.wav', 660, 1200, '—', false),
  new RingItem('门禁提示', 'ring_access.wav', 1046, 600, '—', false),
  new RingItem('周界蜂鸣', 'ring_perimeter.wav', 1320, 450, '—', false)
];

@Observed export class NoticeLog {
  title: string; text: string; time: string;
  constructor(title: string, text: string) { this.title = title; this.text = text; this.time = nowTime(); }
}

RingItem 描述告警铃声条目的完整元数据。四条预置铃声覆盖了不同巡检场景:疏散警报(880Hz/900ms)用于人员疏散引导,消防长鸣(660Hz/1200ms)用于火情告警,门禁提示(1046Hz/600ms)用于门禁异常,周界蜂鸣(1320Hz/450ms)用于周界入侵。inSandbox 初始为 false,表示铃声尚未生成写入沙箱;size 初始为"—",导入沙箱后由 wavSizeText 计算并更新。

NoticeLog 记录通知发布历史,无论成功或失败都会记一条。NOTICE_SEED 预置了两条历史记录展示告警系统的运作模式。noticeLogs 数组使用 unshift 置顶新记录并封顶 8 条,确保历史列表始终最新在前且不无限增长。

七、组件主体结构

7.1 状态变量声明

@Entry
@Component
struct Page1213 {
  @State currentTab: number = 0;
  @State addModal: boolean = false;
  @State editModal: boolean = false;
  @State delModal: boolean = false;
  @State editIdx: number = -1;
  @State delIdx: number = -1;
  @State breath: boolean = false;
  timer: number = -1;
  @State taskList: TaskItem[] = TASK_LIST;
  // ... Camera Kit 成员 ...
  // ... Notification Kit 成员 ...
  // ... Tabs 嵌套滚动成员 ...
}

组件顶层声明了三大类状态变量。弹窗状态组(addModal/editModal/delModal/editIdx/delIdx)绑定 TaskItem 巡检任务实体,控制三个弹窗的显示与操作目标索引。breath 布尔值配合 timer 定时器(非 @State,不参与渲染)实现呼吸动画——每秒翻转一次,驱动月度柱状图奇偶柱交替微动和头部呼吸圆点透明度变化。

Camera Kit 成员中,控制器类(previewControllercameraInputpreviewOutputvideoSessionphotoSession)声明为 private,不参与 UI 渲染;而状态类(surfaceReadysessionModeframingState 等)声明为 @State,其变化驱动相机 Tab 的 UI 刷新。sessionMode 取值 idle/video/photo 三态,在相机与对焦两个 Tab 间共享,实现会话互斥切换。

Notification Kit 成员包含通知授权状态 granted、通知 ID 自增管理 notifyId、铃声库 ringList、当前默认铃声索引 currentRingIdx、发布历史 noticeLogs 以及上报表单三字段(alarmTitle/alarmLevel/alarmDesc)。嵌套滚动成员包含模式 nestedMode、外层索引 outerIndex、内层索引 innerIndex 和翻页日志 swipeLogs

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 秒间隔的呼吸动画定时器。通知授权查询使用 Promise 异步方式且 catch 静默处理,避免组件初始化阶段因权限查询失败而阻塞渲染。

aboutToDisappear 在组件卸载时执行两项清理:clearInterval 停止呼吸定时器防止内存泄漏、releaseSession 释放相机资源防止后台占用摄像头。这种成对的生命周期管理是 ArkUI 组件设计的最佳实践——aboutToAppear 中获取的资源必须在 aboutToDisappear 中释放。

7.3 Tab 切换与会话释放

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

switchTab 方法处理 Tab 切换时的资源释放逻辑。当用户从相机(1)或对焦(2) Tab 切换到其他 Tab 时,需要释放当前会话——这是因为 cameraInput 在同一时间只能绑定一个 session,离开相机相关页面不释放会导致后续重新进入时创建会话失败。如果用户在相机和对焦之间切换则不释放,因为两者共享同一 XComponent 预览面。

八、头部区域详解

@Builder
headerBanner() {
  Column({ space: 10 }) {
    Row() {
      Column({ space: 4 }) {
        Text('安防巡查 · 物业安全巡检').fontSize(20).fontWeight(FontWeight.Bold)
          .fontColor(COLORS.onMain)
        Text(this.currentTab === 0 ? `任务 · 今日 ${this.taskList.length} 项巡检`
          : this.currentTab === 1 ? '相机 · 影随人动取证预览'
            : this.currentTab === 2 ? '对焦 · 手动对焦三接口'
              : this.currentTab === 3 ? '频道 · 楼宇×检查项双层 Tabs'
                : this.currentTab === 4 ? '日志 · nestedScroll 时间线'
                  : this.currentTab === 5 ? '告警 · 沙箱自定义铃声'
                    : '巡检员中心').fontSize(11).fontColor(COLORS.onMain).opacity(0.85)
      }.alignItems(HorizontalAlign.Start).layoutWeight(1)
      Circle({ width: 10, height: 10 }).fill(COLORS.onMain).opacity(this.breath ? 0.9 : 0.45)
    }.width('100%')

    Row({ space: 8 }) {
      // 任务胶囊 + 相机会话胶囊 + 通知授权胶囊 + 嵌套模式胶囊
    }.width('100%')
  }.padding({ left: 16, right: 16, top: 12, bottom: 12 }).width('100%')
    .linearGradient({ angle: 160, colors: [[COLORS.orangeD, 0], [COLORS.bg, 1]] })
}

头部横幅是整个应用的视觉锚点,采用 160 度线性渐变从 orangeD(深橙)过渡到 bg(安全蓝灰黑),模拟警示灯由近及远的衰减光效。横幅内部分为上下两行。

第一行是应用名与 Tab 联动副标题。应用名"安防巡查 · 物业安全巡检"使用 20 号粗体字,颜色为 onMain(深暖黑)确保在橙色渐变背景上可读。副标题通过六层嵌套三元运算符根据 currentTab 切换文案——任务 Tab 显示巡检总数、相机 Tab 显示影随人动定位、对焦 Tab 显示手动对焦三接口定位、频道 Tab 显示双层 Tabs 定位、日志 Tab 显示 nestedScroll 定位、告警 Tab 显示沙箱铃声定位、我的 Tab 显示巡检员中心。这种 Tab 联动副标题让用户在任何 Tab 下都能看到当前页面的技术定位。

右侧的呼吸圆点使用 Circle 组件,透明度由 breath 状态控制在 0.9 与 0.45 之间交替,每秒翻转一次,模拟安防系统的"在线监测"指示灯。

第二行是四枚状态胶囊。任务胶囊显示巡检总项数;相机会话胶囊用 6px 圆点颜色映射 sessionMode 三态(idle 灰/video 绿/photo 橙);通知授权胶囊用圆点颜色映射 granted 布尔值(绿已授权/红未授权)并 layoutWeight(1) 占满剩余宽度;嵌套模式胶囊显示 modeShort 当前嵌套模式的短文案,圆点颜色区分 SELF_FIRST(蓝)与 SELF_ONLY(橙)。四枚胶囊让用户一眼掌握应用的四大特性运行状态。

九、Tab0 任务分析

9.1 完成率渐变大数字卡

@Builder
tabTask() {
  Column({ space: 10 }) {
    Scroll() {
      Column({ space: 10 }) {
        Column({ space: 12 }) {
          Row({ space: 14 }) {
            Text(`${this.taskRate()}`).fontSize(46).fontWeight(FontWeight.Bold)
              .fontColor(COLORS.orange).fontFamily('monospace')
            Column({ space: 5 }) {
              Text('今日巡检完成率').fontSize(12).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
              Text(`已完成 ${this.taskDone()} / 共 ${this.taskList.length} 项 · 数据每 30 分钟同步工单系统`)
                .fontSize(9).fontColor(COLORS.text3).maxLines(2)
                .textOverflow({ overflow: TextOverflow.Ellipsis })
            }.alignItems(HorizontalAlign.Start).layoutWeight(1)
          }.width('100%')

          Row() {
            Column().width(`${this.taskRate()}%`).height('100%')
              .linearGradient({ angle: 0, colors: [[COLORS.orange, 0], [COLORS.green, 1]] })
          }.width('100%').height(8).borderRadius(4).backgroundColor(COLORS.dark)
          // 三格统计:进行中 / 待复查 / 已逾期
        }.width('100%').padding(14).backgroundColor(COLORS.card).borderRadius(12)

任务 Tab 第一段是完成率统计卡。大数字使用 46 号等宽字体显示 taskRate() 返回的完成率百分比,橙色主色配合等宽字体营造仪表盘般的数字展示效果。右侧副标题区显示"今日巡检完成率"主标题和数据同步说明辅助文本,maxLines(2) 限制两行并启用省略号截断。

渐变完成条使用 Column 宽度绑定 taskRate() 百分比,0 度线性渐变从橙色过渡到绿色,象征进度从"进行中"到"已完成"的状态演变。三格统计区分别使用橙色、蓝色、红色显示进行中、待复查、已逾期任务数量,每种状态的颜色与 taskStatusColor 函数保持一致。

9.2 任务进度条清单与卡片

@Builder
taskCard(item: TaskItem, idx: number) {
  Column({ space: 8 }) {
    Row({ space: 8 }) {
      Text(item.building).fontSize(10).fontColor(COLORS.sub)
        .padding({ left: 7, right: 7, top: 2, bottom: 2 }).backgroundColor(COLORS.dark).borderRadius(6)
      Text(item.status).fontSize(9).fontColor(taskStatusColor(item.status))
        .padding({ left: 7, right: 7, top: 2, bottom: 2 }).backgroundColor(COLORS.dark).borderRadius(6)
      Blank()
      Text('编').onClick(() => { this.openEdit(idx); })
      Text('删').fontColor(COLORS.red).onClick(() => { this.openDel(idx); })
    }.width('100%')
    Text(item.item).fontSize(13).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
      .maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
    Row({ space: 8 }) {
      Progress({ value: item.progress, total: 100, type: ProgressType.Linear })
        .layoutWeight(1).color(taskStatusColor(item.status))
      Text(`${item.progress}%`).fontSize(10).fontFamily('monospace').fontColor(COLORS.sub)
    }.width('100%')
  }.width('100%').padding(12).backgroundColor(COLORS.card).borderRadius(12)
}

任务清单卡片是进度条清单的主体,每张卡片承载一条 TaskItem 数据。卡片顶部行显示楼宇徽章和状态徽章——楼宇徽章用蓝灰色文字标识责任分区,状态徽章用 taskStatusColor 映射的四色文字标识任务生命周期阶段。右侧的"编""删"两个操作按钮分别绑定 openEditopenDel 方法,删除按钮使用红色文字强化危险操作警示。

中部巡检项目名使用 13 号粗体白色文字,单行显示并启用省略号截断。底部进度条使用 ArkUI 内置 Progress 组件的线性类型,颜色绑定 taskStatusColor 让进度条颜色与状态徽章保持一致——已完成绿色、进行中橙色、待复查蓝色、已逾期红色。右侧百分比数值使用等宽字体确保数字对齐。

9.3 月度隐患柱状图

@Builder
chartCard() {
  Column({ space: 10 }) {
    Row() {
      Text('📊 月度隐患发现数').fontSize(13).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
      Blank()
      Text('近 6 个月 · 单位处').fontSize(9).fontColor(COLORS.text3)
    }.width('100%')
    Row({ space: 10 }) {
      ForEach(MONTH_HAZARD, (val: number, idx: number) => {
        Column({ space: 5 }) {
          Text(val.toString()).fontSize(8).fontColor(COLORS.sub)
          Column().width('100%').height(this.barHeight(idx)).borderRadius(5)
            .linearGradient({ angle: 180, colors: [[COLORS.orange, 0], [COLORS.orangeD, 1]] })
          Text(MONTH_NAME[idx]).fontSize(8).fontColor(COLORS.text3)
        }.layoutWeight(1).alignItems(HorizontalAlign.Center)
      }, ...)
    }.width('100%').alignItems(VerticalAlign.Bottom)
  }.width('100%').padding(14).backgroundColor(COLORS.card).borderRadius(12)
}

柱状图采用 Column + ForEach 传统柱状方案而非 Canvas 绘制,每根柱子是一个 Column 组件,高度由 barHeight 方法计算。barHeight 的核心逻辑是:基础高度 = 月度数据值 / 满量程值 × 96 像素,再乘以呼吸波动系数——偶数索引柱在 breath 为 true 时放大 1.06 倍、为 false 时缩小 0.94 倍,奇数索引柱反之,实现奇偶柱交替微动的视觉效果。每根柱子使用 180 度线性渐变从橙色过渡到深橙,顶部显示数值、底部显示月份。整行使用 VerticalAlign.Bottom 底部对齐确保所有柱子站在同一基线上。

十、Tab1 相机分析

9.1 授权状态卡与 XComponent 预览

@Builder
tabCamera() {
  Column({ space: 10 }) {
    Column({ space: 8 }) {
      Row() {
        Text('📷 CAMERA 权限').fontSize(12).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
        Blank()
        Text(this.permState).fontSize(10).fontColor(this.permState === '已授权' ? COLORS.green : COLORS.orange)
      }.width('100%')
      Row({ space: 8 }) {
        Text('申请相机权限').onClick(() => { this.requestCameraPermission(); })
        Text(this.surfaceReady ? 'Surface 已就绪' : 'Surface 加载中…')
          .fontColor(this.surfaceReady ? COLORS.green : COLORS.text3)
      }.width('100%')
    }.width('100%').padding(10).backgroundColor(COLORS.card).borderRadius(12)

    Stack({ alignContent: Alignment.BottomEnd }) {
      XComponent({ id: 'patrolCamPreview', type: XComponentType.SURFACE, controller: this.previewController })
        .layoutWeight(1).width('100%').borderRadius(12).backgroundColor(COLORS.dark)
        .onLoad(() => { this.surfaceReady = true; })
      Text(sessionLabel(this.sessionMode)).fontSize(9).fontColor(COLORS.title)
        .padding({ left: 8, right: 8, top: 4, bottom: 4 }).borderRadius(8)
        .backgroundColor(COLORS.mask).margin(8)
    }.layoutWeight(1).width('100%').borderRadius(12)

相机 Tab 第一段是授权状态卡,显示 CAMERA 权限申请状态和 Surface 就绪状态。permState 状态值通过 requestCameraPermission 方法更新,该方法使用 abilityAccessCtrl.createAtManager() 创建权限管理器并调用 requestPermissionsFromUser 动态申请 ohos.permission.CAMERA 权限,authResults[0]===0 判定已授权。

XComponent 预览本体使用 SURFACE 类型,layoutWeight(1) 占满剩余高度。onLoad 回调在 Surface 就绪后将 surfaceReady 置为 true,这是后续创建相机会话的前置条件。右下角叠加会话标签 sessionLabel 函数返回的文案,使用半透明遮罩背景确保在预览画面上的可读性。

10.2 影随人动能力链

async startVideoMode() {
  if (!this.surfaceReady) { this.framingState = 'Surface 未就绪'; return; }
  if (this.sessionMode === 'video') { return; }
  if (this.sessionMode === 'photo') { await this.releaseSession(); }
  const granted = await this.requestCameraPermission();
  if (!granted) { this.framingState = '权限被拒'; return; }
  // ... 创建 CameraInput / PreviewOutput / VideoSession ...
  this.queryFraming(this.videoSession);
  await this.videoSession.start();
  this.sessionMode = 'video';
}

queryFraming(session: camera.VideoSession) {
  if (!session.isControlCenterSupported()) {
    this.framingState = '控制中心不支持';
    this.framingSupported = false;
    return;
  }
  const effects = session.getSupportedEffectTypes();
  this.framingSupported = effects.includes(camera.ControlCenterEffectType.AUTO_FRAMING);
  if (!this.framingSupported) { this.framingState = 'AUTO_FRAMING 未声明'; return; }
  try {
    session.enableControlCenter(true);
    this.framingState = '影随人动已启用';
  } catch (e) {
    this.framingState = `接管失败(${(e as BusinessError).code})`;
  }
}

startVideoMode 是影随人动模式的入口方法。方法首先进行四重前置校验:Surface 就绪、非重复进入、互斥释放 photo 会话、权限已授权。随后依次创建 CameraInput(选后摄作为巡检取证主镜头)、PreviewOutput、VideoSession,配置 beginConfigaddInputaddOutputcommitConfig 四步流水线。

queryFraming 是 AUTO_FRAMING 能力链的核心——三步验证逐步推进。第一步 isControlCenterSupported 检查设备是否支持控制中心,不支持则直接返回并设置状态为"控制中心不支持"。第二步 getSupportedEffectTypes 获取支持的效果类型列表,用 includes 检查是否声明了 AUTO_FRAMING,未声明则返回"AUTO_FRAMING 未声明"。第三步 enableControlCenter(true) 请求系统接管构图,成功后状态更新为"影随人动已启用",异常时记录错误码。三步验证的每一步都有独立状态文案,让用户清晰知道能力链卡在哪一步。

十一、Tab2 对焦分析

11.1 手动对焦能力查询

async switchToPhotoMode() {
  if (this.sessionMode === 'photo') { return; }
  if (this.sessionMode === 'video') { await this.releaseSession(); }
  if (!this.surfaceReady) { this.framingState = 'Surface 未就绪'; return; }
  const granted = await this.requestCameraPermission();
  if (!granted) { this.framingState = '权限被拒'; return; }
  // ... 创建 CameraInput / PreviewOutput / PhotoSession ...
  await this.photoSession.start();
  this.sessionMode = 'photo';
  this.queryFocusSupport();
}

queryFocusSupport() {
  if (this.photoSession === undefined) { this.focusSupported = false; return; }
  try {
    this.focusSupported = this.photoSession.isFocusDistanceSupported();
  } catch (e) {
    this.focusSupported = false;
  }
}

switchToPhotoMode 创建 PhotoSession 作为手动对焦的宿主。与 VideoSession 创建流程类似,但使用 SceneMode.NORMAL_PHOTO 而非 NORMAL_VIDEO。会话启动后立即调用 queryFocusSupport 查询手动对焦能力。

queryFocusSupport 调用 PhotoSession 的 isFocusDistanceSupported 同步方法查询本机是否支持设置对焦距离。如果异常(如 7400102/7400103 会话未就绪等错误码),则降级为 focusSupported = false,UI 上显示"未启动拍照会话"提示而非崩溃。

11.2 三档预设与焦距滑杆

Row({ space: 8 }) {
  ForEach(FOCUS_PRESETS, (preset: FocusPreset) => {
    Column({ space: 5 }) {
      Text(preset.label).fontWeight(FontWeight.Bold)
        .fontColor(this.focusDistance === preset.distance ? COLORS.onMain : COLORS.title)
      Text(preset.distance.toFixed(1)).fontFamily('monospace')
        .fontColor(this.focusDistance === preset.distance ? COLORS.onMain : COLORS.orange)
      Text(preset.scene)
        .fontColor(this.focusDistance === preset.distance ? COLORS.onMain : COLORS.text3)
    }.backgroundColor(this.focusDistance === preset.distance ? COLORS.orange : COLORS.card)
    .onClick(() => { this.focusDistance = preset.distance; })
  }, ...)
}.width('100%')

Slider({ value: this.focusDistance, min: 0, max: 1, step: 0.01 }).width('100%')
  .blockColor(COLORS.orange).trackColor(COLORS.dark).selectedColor(COLORS.orange)
  .onChange((value: number) => { this.focusDistance = value; })
Text(distanceLabel(this.focusDistance)).fontSize(10).fontColor(COLORS.text3)

三档预设卡使用 ForEach 渲染,当前选中档位(focusDistance === preset.distance)使用橙色背景和 onMain 深色文字高亮,未选中档位使用卡片底色和白色/橙色文字。点击档位即设置 focusDistance,Slider 同步跳转。

焦距滑杆使用 Slider 组件,步进 0.01,橙色滑块和选中色,拖动时 onChange 实时更新 focusDistance 状态。滑杆下方由 distanceLabel 函数根据当前距离值返回景别说明文案——近拍对应铭牌与序列号、中距对应配电柜与管线走向、远距对应机房全景与疏散通道。

11.3 设置并读回校验

applyFocus() {
  if (this.photoSession === undefined) {
    this.focusRecords.unshift(new FocusRecord(this.focusDistance, -1, '失败(无会话)'));
    if (this.focusRecords.length > 20) { this.focusRecords.pop(); }
    return;
  }
  try {
    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));
    if (this.focusRecords.length > 20) { this.focusRecords.pop(); }
  } catch (e) {
    const err = e as BusinessError;
    this.focusRecords.unshift(new FocusRecord(this.focusDistance, -1, `失败(${err.code})`));
    if (this.focusRecords.length > 20) { this.focusRecords.pop(); }
  }
}

applyFocus 是手动对焦三接口的第二、三步——设置值与读回校验。方法先调用 setFocusDistance 设置对焦距离,紧接着调用 getFocusDistance 读回硬件实际对焦距离。两者差值的绝对值小于 0.01 判定为"已生效",否则为"读回偏差"——这种偏差在真实硬件上是常见的,因为对焦马达的物理位移精度有限。每次操作生成一条 FocusRecord 记录 unshift 置顶到时间线,列表封顶 20 条自动 pop 尾部。

对焦记录时间线使用 Scroll + ForEach 展示,每行包含时间戳、设置值、箭头、读回值(-1 显示为"—")和校验结论徽章。校验结论使用 focusOkColor 函数映射色彩:已生效绿色、读回偏差橙色、失败红色。

十二、Tab3 频道分析

12.1 嵌套模式切换与双层位置说明

@Builder
tabChannel() {
  Column({ space: 10 }) {
    Row({ space: 8 }) {
      Text(`嵌套模式:${modeLabel(this.nestedMode)}`).layoutWeight(1)
      ForEach([TabsNestedScrollMode.SELF_ONLY, TabsNestedScrollMode.SELF_FIRST], (m: TabsNestedScrollMode) => {
        Text(modeShort(m))
          .fontColor(this.nestedMode === m ? COLORS.onMain : COLORS.text3)
          .backgroundColor(this.nestedMode === m ? COLORS.orange : COLORS.card)
          .onClick(() => { this.nestedMode = m; })
      }, ...)
    }.width('100%')

    Row({ space: 6 }) {
      Circle({ width: 6, height: 6 }).fill(COLORS.orange)
      Text(`外层 ${OUTER_CHANNELS[this.outerIndex].name}`)
      Blank()
      Circle({ width: 6, height: 6 }).fill(COLORS.blue)
      Text(`内层 ${INNER_TABS[this.innerIndex]}(第 ${this.innerIndex + 1}/5 页)`)
    }.width('100%')

频道 Tab 顶部是嵌套模式切换 chips。两个 chips 分别对应 SELF_ONLY(仅内层)和 SELF_FIRST(先内后外),当前选中模式使用橙色背景高亮。左侧文字由 modeLabel 函数返回完整模式说明。

双徽标行用橙色圆点标识外层楼宇频道、蓝色圆点标识内层检查项页签,实时展示当前双层位置。当用户在 SELF_FIRST 模式下滑动内层到边缘继续滑,外层会接力切换,此时橙色徽标的外层楼宇名会随之更新。

12.2 外层楼宇 Tabs 与内层检查项 Tabs

Tabs({ barPosition: BarPosition.Start }) {
  ForEach(OUTER_CHANNELS, (ch: ChannelItem) => {
    TabContent() {
      this.innerTabs(ch)
    }.tabBar(`${ch.icon} ${ch.name}`)
  }, ...)
}
.barMode(BarMode.Scrollable)
.onChange((index: number) => {
  this.swipeLogs.unshift(new SwipeLog('外层楼宇', OUTER_CHANNELS[index].name,
    this.outerIndex, index, modeLabel(this.nestedMode)));
  this.outerIndex = index;
  if (this.swipeLogs.length > 40) { this.swipeLogs.pop(); }
})

@Builder
innerTabs(channel: ChannelItem) {
  Tabs({ barPosition: BarPosition.Start }) {
    ForEach(INNER_TABS, (name: string) => {
      TabContent() {
        List({ space: 10 }) {
          ForEach(innerMockData(channel, name), (item: InnerCard) => {
            ListItem() { /* 检查项卡片 */ }
          }, ...)
        }.width('100%').height('100%')
      }.tabBar(name)
    }, ...)
}
.barMode(BarMode.Scrollable)
.onChange((index: number) => {
  this.swipeLogs.unshift(new SwipeLog('内层检查项', INNER_TABS[index],
    this.innerIndex, index, modeLabel(this.nestedMode)));
  this.innerIndex = index;
  if (this.swipeLogs.length > 40) { this.swipeLogs.pop(); }
})
.nestedScroll(this.nestedMode)

外层 Tabs 使用 BarMode.Scrollable 横滑页签模式,5 个楼宇频道各为一个 TabContent,内容调用 innerTabs 构建内层检查项。外层 onChange 回调记录一条 layer='外层楼宇'SwipeLog,包含切换前后的索引和事发时的嵌套模式。

内层 Tabs 嵌套在外层 TabContent 内部,同样使用 BarMode.Scrollable,5 个检查项页签各包含一个 List 列表,由 innerMockData 生成 8 条检查卡片。内层 onChange 回调记录一条 layer='内层检查项'SwipeLog

关键的一行是 .nestedScroll(this.nestedMode)——这行代码将 TabsNestedScrollMode 挂载到内层 Tabs 上,决定内层滑到边缘后的行为。SELF_FIRST 模式下内层滑到边缘继续滑动会触发外层 onChange(即"接力"),SELF_ONLY 模式下内层滑到边缘即止不联动外层。翻页日志封顶 40 条自动 pop 尾部,防止无限增长。

十三、Tab4 日志分析

@Builder
tabLogs() {
  Column({ space: 10 }) {
    Row() {
      Text(`已记录 ${this.swipeLogs.length} 次翻页`).fontWeight(FontWeight.Bold)
      Blank()
      Button('清空日志').enabled(this.swipeLogs.length > 0).onClick(() => { this.clearLogs(); })
    }.width('100%')

    Row({ space: 12 }) {
      Row({ space: 5 }) {
        Circle({ width: 6, height: 6 }).fill(COLORS.orange)
        Text('外层楼宇翻页')
      }
      Row({ space: 5 }) {
        Circle({ width: 6, height: 6 }).fill(COLORS.blue)
        Text('内层检查项翻页')
      }
      Blank()
      Text(`当前 ${modeLabel(this.nestedMode)}`)
    }.width('100%')

    if (this.swipeLogs.length === 0) {
      // 空态卡
    } else {
      List({ space: 0 }) {
        ForEach(this.swipeLogs, (log: SwipeLog) => {
          ListItem() {
            Row({ space: 10 }) {
              Column({ space: 4 }) {
                Text(log.time).fontFamily('monospace')
                Text(log.layer === '外层楼宇' ? 'OUT' : 'IN')
                  .fontColor(log.layer === '外层楼宇' ? COLORS.orange : COLORS.blue)
              }.width(52).height('100%')
              Column().width(3).height('100%')
                .backgroundColor(log.layer === '外层楼宇' ? COLORS.orange : COLORS.blue).opacity(0.6)
              Column({ space: 4 }) {
                Row({ space: 6 }) {
                  Text(log.layer === '外层楼宇' ? '外层楼宇' : '内层检查项')
                  Text(log.tabName).fontWeight(FontWeight.Bold)
                  Blank()
                  Text(`${log.fromIdx}${log.toIdx}`).fontFamily('monospace')
                }.width('100%')
                Text(`${log.mode} · ${log.time}`)
              }.layoutWeight(1).height('100%').justifyContent(FlexAlign.Center)
            }.width('100%').height(72)
          }
        }, ...)
      }.width('100%').layoutWeight(1)
    }
  }
}

日志 Tab 是 nestedScroll 翻页事件的可视化时间轴。顶部计数行显示已记录翻页次数和清空按钮,按钮在无记录时禁用。图例行用橙色圆点标识外层楼宇翻页、蓝色圆点标识内层检查项翻页,并显示当前嵌套模式。

时间轴主体采用固定行高 72 像素的 List 列表。每行分为三段:左侧 52 像素宽的时间列显示时间戳和 OUT/IN 层级标识(外层橙/内层蓝);中间 3 像素宽的竖线用颜色区分层级(外层橙/内层蓝,60% 透明度),竖线高度填满行高实现连续视觉引导线;右侧内容卡显示层级徽章、切换到的页签名、索引变化(如"0→2")和嵌套模式说明。

空态时显示引导卡,提示用户"去频道 Tab 滑动内外层页签,SELF_FIRST 下内层滑到边缘会接力切换外层楼宇"。这条引导文案直接点明了 nestedScroll 的核心交互方式和验证方法。

十四、Tab5 告警分析

14.1 异常上报表单

@Builder
tabAlarm() {
  Column({ space: 10 }) {
    Scroll() {
      Column({ space: 10 }) {
        Column({ space: 10 }) {
          Row() {
            Text('🚨 异常上报').fontWeight(FontWeight.Bold)
            Blank()
            Text(this.alarmLevel).fontColor(levelColor(this.alarmLevel))
          }.width('100%')
          TextInput({ placeholder: '异常点位标题,如:B2 车道喷淋管渗漏' })
            .onChange((value: string) => { this.alarmTitle = value; })
          Row({ space: 8 }) {
            ForEach(ALARM_LEVELS, (level: string) => {
              Text(level)
                .fontColor(this.alarmLevel === level ? COLORS.onMain : COLORS.sub)
                .backgroundColor(this.alarmLevel === level ? levelColor(level) : COLORS.dark)
                .onClick(() => { this.alarmLevel = level; })
            }, ...)
          }.width('100%')
          TextArea({ placeholder: '异常描述:位置 / 现象 / 初步判断(选填)' })
            .onChange((value: string) => { this.alarmDesc = value; })
          Text('🔔 发布告警通知').backgroundColor(COLORS.orange)
            .onClick(() => { this.submitAlarm(); })
        }.width('100%').padding(12).backgroundColor(COLORS.card).borderRadius(12)

告警 Tab 第一区块是异常上报表单,包含标题输入框、等级 chips(一般/严重/紧急)和描述多行文本框。等级 chips 选中时使用 levelColor 映射的三色(一般蓝/严重橙/紧急红)作为背景色,未选中使用暗灰底色。发布按钮绑定 submitAlarm 方法,该方法校验标题非空后组合等级与描述调用 publishNotice

14.2 通知授权与沙箱铃声

requestAuth() {
  const hostCtx = this.getUIContext().getHostContext() as common.UIAbilityContext;
  if (!hostCtx) { return; }
  notificationManager.requestEnableNotification(hostCtx).then(() => {
    this.granted = true;
  }).catch((err: BusinessError) => {
    notificationManager.openNotificationSettings(hostCtx).then(() => {
    }).catch(() => { this.granted = false; });
  });
}

saveRingToSandbox(fileName: string, freq: number, durationMs: number): string {
  const hostCtx = this.getUIContext().getHostContext() as common.UIAbilityContext;
  if (!hostCtx) { return ''; }
  const appCtx = hostCtx.getApplicationContext();
  appCtx.area = contextConstant.AreaMode.EL1;
  const dir = appCtx.filesDir;
  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;
}

requestAuth 使用 requestEnableNotification 方法请求通知授权,首次调用弹系统授权框。如果用户曾拒绝(返回错误码 1600004),则调用 openNotificationSettings 拉起系统通知设置页引导手动开启。值得注意的是必须传入 hostCtx 上下文参数——无参版本已废弃。

saveRingToSandbox 实现沙箱铃声写入链路。首先将 appCtx.area 设为 contextConstant.AreaMode.EL1(沙箱必须在 EL1 区域),获取 filesDir 目录路径,调用 buildWavBytes 生成 WAV 字节流,使用 fs.openSync 以 CREATE|WRITE_ONLY|TRUNC 模式打开文件,writeSync 写入数据后 closeSync 关闭文件。文件名使用预置的 ring.file 字段(如 ring_evacuate.wav)。

14.3 发布携带沙箱铃声的通知

publishNotice(title: string, text: string) {
  const ring = this.ringList[this.currentRingIdx];
  if (!ring.inSandbox) { this.importRing(this.currentRingIdx); }
  const hostCtx = this.getUIContext().getHostContext() as common.UIAbilityContext;
  const appCtx = hostCtx.getApplicationContext();
  appCtx.area = contextConstant.AreaMode.EL1;
  const sandboxPath = appCtx.filesDir + '/' + ring.file;
  const uri = fileUri.getUriFromPath(sandboxPath);
  const soundVal = 'uri::' + uri;
  const request: notificationManager.NotificationRequest = {
    id: this.notifyId++,
    notificationSlotType: notificationManager.SlotType.SOCIAL_COMMUNICATION,
    content: {
      notificationContentType: notificationManager.ContentType.NOTIFICATION_CONTENT_BASIC_TEXT,
      normal: { title: title, text: text, additionalText: '铃声:' + ring.name }
    },
    sound: soundVal
  };
  notificationManager.publish(request).then(() => {
    this.addNoticeLog(title, text);
  }).catch((err: BusinessError) => {
    this.addNoticeLog('发布失败', `错误码 ${err.code},请先开启通知授权`);
  });
}

publishNotice 是 Notification Kit 沙箱铃声链路的终点。方法先确保当前铃声已导入沙箱(未导入时自动导入),然后获取宿主上下文并将区域设为 EL1,拼接沙箱路径 filesDir + '/' + ring.file。关键的一步是 fileUri.getUriFromPath(sandboxPath) 将沙箱文件路径转换为 URI,再以 'uri::' + uri 格式拼接到通知请求的 sound 字段。

通知请求使用 SOCIAL_COMMUNICATION 槽位类型(社交通信级别,允许声音和振动),NOTIFICATION_CONTENT_BASIC_TEXT 内容类型(基础文本通知),normal 块包含标题、正文和附加文本(附加文本显示铃声名)。通知 ID 使用 notifyId++ 自增管理,确保每次发布的通知独立。publish 成功后记录一条历史日志,失败时记录错误码并引导用户去授权卡开启通知。

14.4 铃声行与发布历史

@Builder
ringRow(ring: RingItem, idx: number) {
  Row({ space: 8 }) {
    Column({ space: 3 }) {
      Text(ring.name).fontColor(idx === this.currentRingIdx ? COLORS.orange : COLORS.title)
      Text(`${ring.freq}Hz · ${ring.duration}ms · ${ring.size}${ring.inSandbox ? ' · 已在沙箱' : ' · 未生成'}`)
    }.layoutWeight(1)
    Text('生成').onClick(() => { this.importRing(idx); })
    Text('设默认')
      .backgroundColor(idx === this.currentRingIdx ? COLORS.orange : COLORS.dark)
      .onClick(() => { this.setCurrentRing(idx); })
  }.backgroundColor(idx === this.currentRingIdx ? COLORS.dark : COLORS.bg)
  .border({ width: idx === this.currentRingIdx ? 1 : 0, color: COLORS.orange })
}

@Builder
noticeRow(log: NoticeLog, idx: number) {
  Column({ space: 4 }) {
    Row({ space: 6 }) {
      Text(log.title).fontColor(log.title.indexOf('失败') >= 0 ? COLORS.red : COLORS.title)
      Text(log.time).fontFamily('monospace')
    }.width('100%')
    Text(log.text).maxLines(2)
  }.backgroundColor(idx % 2 === 0 ? COLORS.dark : COLORS.bg)
}

铃声精简行展示铃声名称、频率/时长/大小/沙箱状态,以及"生成"和"设默认"两个操作按钮。当前默认铃声行使用橙色边框和暗色背景高亮。发布历史行交替使用暗灰和背景色实现斑马纹效果,标题含"失败"二字时使用红色文字标识失败记录。

getSoundValue 方法实时展示当前通知请求的 sound 字段完整值,包括 'uri::' 前缀和沙箱路径 URI,让开发者直观看到 Notification 6.1.1 新特性的 sound 字段格式。

十五、Tab6 我的分析

@Builder
tabMine() {
  Column({ space: 10 }) {
    Scroll() {
      Column({ space: 12 }) {
        Column({ space: 12 }) {
          Row({ space: 12 }) {
            Text('🧑‍🔧').fontSize(30).borderRadius(40).backgroundColor(COLORS.mask)
            Column({ space: 4 }) {
              Text('周正涛').fontSize(18).fontWeight(FontWeight.Bold).fontColor(COLORS.onMain)
              Text('工号 PB-2041 · 金牌巡检员 · 白班 08:00-20:00').fontColor(COLORS.onMain).opacity(0.85)
            }.layoutWeight(1)
          }.width('100%')
          Divider().strokeWidth(1).color(COLORS.mask)
          Row({ space: 8 }) {
            Column({ space: 3 }) {
              Text('312').fontWeight(FontWeight.Bold).fontColor(COLORS.onMain)
              Text('本月点位').fontColor(COLORS.onMain).opacity(0.8)
            }.layoutWeight(1)
            // ... 整改闭环 54 / 巡检里程 46.8km ...
          }.width('100%')
          Text('责任区:1-3 号楼 · 地下车库 B1/B2 · 配电房 · 消防泵房').fontColor(COLORS.onMain).opacity(0.75)
        }.linearGradient({ angle: 140, colors: [[COLORS.orangeD, 0], [COLORS.orange, 1]] })

        Row() {
          Text('📈 本月绩效看板').fontWeight(FontWeight.Bold)
          Blank()
          Text('数据截止昨日 24:00')
        }.width('100%')
        Column({ space: 0 }) {
          ForEach(PERF_ROWS, (row: PerfRow, idx: number) => {
            Row({ space: 10 }) {
              Text(`${idx + 1}`).fontFamily('monospace').backgroundColor(COLORS.dark).borderRadius(7)
              Column({ space: 3 }) {
                Text(row.label).fontWeight(FontWeight.Bold)
                Text(row.note).maxLines(1)
              }.layoutWeight(1)
              Text(row.value).fontFamily('monospace').fontWeight(FontWeight.Bold)
                .fontColor(idx < 3 ? COLORS.orange : COLORS.sub)
            }.backgroundColor(COLORS.card).margin({ bottom: idx === PERF_ROWS.length - 1 ? 0 : 8 })
          }, ...)
        }.width('100%')
      }
    }
  }
}

"我的"Tab 以巡检员渐变大卡为视觉核心。大卡使用 140 度线性渐变从 orangeDorange,模拟金牌巡检员的荣誉背景。卡片内部分为三段:顶部是巡检员头像(Emoji)、姓名和工号/职级/排班信息;中部是三项核心数据(本月点位 312、整改闭环 54、巡检里程 46.8km)的三等分布局;底部是责任区文字说明。所有文字使用 onMain 深暖黑色保证在橙色渐变上的可读性。

绩效看板使用 PERF_ROWS 六行数据,每行包含序号徽章、指标名、辅助说明和主数值。前三行数值使用橙色高亮(完成点位、整改闭环、响应时长),后三行使用蓝灰弱化(巡检里程、取证张数、连续达标)。行间距 8 像素,末行不加底边距,实现紧凑的列表布局。

十六、底部 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)
      }.justifyContent(FlexAlign.Center).layoutWeight(1).padding({ top: 7, bottom: 7 })
      .onClick(() => { this.switchTab(index); })
    }, (tab: TabMeta) => tab.label)
  }.width('100%').backgroundColor(COLORS.card)
  .border({ width: { top: 1 }, color: COLORS.line })
}

底部导航栏使用 Row + ForEach 自绘单排布局,7 个 Tab 等宽分布。每个 Tab 由 Emoji 图标(17 号字)和中文标签(9 号字)纵向排列组成,选中项标签使用 tabOn(警示橙)高亮,未选中项使用 text3(暗蓝灰)弱化。点击事件绑定 switchTab 方法而非直接赋值 currentTab,因为 switchTab 包含了离开相机/对焦页时的会话释放逻辑。顶部 1 像素分割线使用 line 色值与背景低对比,不干扰视觉但界定区域边界。

十七、弹框系统

17.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)
    }
  }.width('100%').height('100%').backgroundColor(COLORS.mask)
  .justifyContent(FlexAlign.End)
}

弹窗遮罩使用 Column + justifyContent(FlexAlign.End) 实现底部弹出效果。上层空白遮罩区 layoutWeight(1) 占满除面板外的所有高度,点击触发 onClose 回调关闭弹窗。下层根据 addModal/editModal/delModal 三个布尔标志三选一渲染对应面板。整个遮罩使用 mask 半透明黑色背景覆盖全页,在 Stack 层叠容器中最后渲染确保覆盖所有内容。

17.2 新建/编辑/删除三个面板

@Builder
panelAdd(onClose: () => void) {
  Column({ space: 12 }) {
    Text('新建巡检任务').fontSize(15).fontWeight(FontWeight.Bold)
    Text('录入楼宇分区与巡检项目,提交后置顶到任务清单,初始状态为进行中')
    TextInput({ placeholder: '楼宇/分区,如:4 号楼' })
      .onChange((value: string) => { this.formBuilding = value; })
    TextInput({ placeholder: '巡检项目,如:防火门闭火器复查' })
      .onChange((value: string) => { this.formItem = value; })
    Slider({ value: this.formProgress, min: 0, max: 100, step: 5 })
      .onChange((value: number) => { this.formProgress = Math.round(value); })
    Row({ space: 10 }) {
      Button('取消').onClick(() => { onClose(); })
      Button('创建').backgroundColor(COLORS.orange).onClick(() => { this.confirmAdd(); })
    }.width('100%')
  }.padding(16).borderRadius({ topLeft: 16, topRight: 16 }).backgroundColor(COLORS.card)
}

新建面板包含楼宇输入框、项目输入框、进度滑杆(步进 5)和取消/创建按钮。confirmAdd 方法从表单取值创建新 TaskItemunshift 置顶到任务清单,进度大于等于 100 自动设为"已完成"状态。编辑面板仅含进度滑杆,confirmEdit 方法保存进度并同步更新状态(100% 自动置为已完成)。删除面板显示确认文案和取消/删除按钮,confirmDel 方法使用 splice 从任务列表移除指定索引项。三个面板均使用 borderRadius({ topLeft: 16, topRight: 16 }) 实现顶部圆角底部贴边的弹出面板视觉风格。

十八、功能模块对比表

功能模块所在 Tab核心 API / 特性数据模型关键交互业务价值
任务管理Tab0 任务Progress 组件 + linearGradient 渐变条TaskItem (@Observed)新增/编辑/删除弹窗 + 进度条清单巡检任务全生命周期闭环管理
影随人动Tab1 相机VideoSession + isControlCenterSupported + getSupportedEffectTypes + enableControlCenterframingState/framingSupported三步能力链验证 + 效果枚举卡巡查跟拍人员始终居中构图
手动对焦Tab2 对焦PhotoSession + isFocusDistanceSupported + setFocusDistance + getFocusDistanceFocusRecord (@Observed)三档预设 + Slider + 读回校验 + 时间线设备铭牌取证精确对焦
嵌套滚动Tab3 频道Tabs.nestedScroll(TabsNestedScrollMode) + SELF_FIRST/SELF_ONLYSwipeLog (@Observed) + InnerCard楼宇×检查项双层 Tabs + 模式切换 chips多维度巡检内容流畅浏览
翻页日志Tab4 日志List + ForEach 时间轴 + 双色竖线SwipeLog空态引导 + 固定行高 72 + 清空嵌套滚动行为可视化验证
沙箱铃声Tab5 告警buildWavBytes + EL1 filesDir + fileUri.getUriFromPath + sound=‘uri::’RingItem + NoticeLog (@Observed)上报表单 + 授权卡 + 铃声行 + 发布历史告警铃声完全自定义
绩效看板Tab6 我的linearGradient 渐变大卡 + ForEach 清单PerfRow渐变巡检员卡 + 六行绩效清单巡检员个人工作量化展示
弹窗系统全局Stack 层叠 + 条件渲染 + FlexAlign.EndformBuilding/formItem/formProgress三面板切换 + 点遮罩关闭任务增删改的交互入口
呼吸动画全局setInterval + breath 布尔翻转 + opacity/height 联动breath + timer1 秒间隔翻转驱动柱状图与圆点模拟实时数据更新的视觉反馈

十九、总结与展望

本文深度解析了 HarmonyOS ArkUI 智慧物业安防巡查平台的完整组件化设计。从 14 色安全蓝灰+警示橙色彩体系的精心调配,到 7 个 Tab 各自完全不同的布局结构,再到四大前沿特性的深度融合——Camera Kit 的 VideoSession AUTO_FRAMING 影随人动能力链(三步验证从能力查询到系统接管)、PhotoSession 手动对焦三接口(设置值与读回值差值校验)、Notification Kit 的沙箱自定义铃声链路(正弦波 PCM 生成→EL1 落盘→URI 转换→sound 字段填充)、Tabs 嵌套滚动的边缘接力(SELF_FIRST/SELF_ONLY 双模式可切换),每一处设计都紧扣物业安全巡检的真实业务场景。

在代码架构层面,@Observed 装饰器让 TaskItemFocusRecordSwipeLogRingItemNoticeLog 五种数据模型的字段级变化被 UI 自动感知,实现了"数据更新即视图刷新"的响应式体验。@Builder 方法将 7 个 Tab 的完全不同布局和 3 个弹窗面板拆分为独立函数,每个 Builder 职责单一、边界清晰。状态变量统一声明在组件顶层,sessionMode 在相机与对焦 Tab 间共享实现会话互斥,nestedMode 在频道 Tab 切换后其翻页事件被日志 Tab 消费展示,breath 呼吸状态同时驱动月度柱状图和头部圆点——跨 Tab 状态共享让四大特性不再是孤立的功能点,而是构成了一个有机联动的安防巡查系统。

展望未来,本平台可在以下方向继续深化。第一,对接真实巡检工单系统 API,将 TASK_LIST 种子数据替换为云端实时数据,配合 @Observed 实现工单状态变更的实时推送刷新。第二,扩展 Camera Kit 的 PhotoSession 拍照能力,在手动对焦基础上增加 takePhoto 接口实现取证拍照并自动关联到任务条目,形成"巡检-取证-归档"完整链路。第三,利用 Notification Kit 的通知分类组能力,将告警按楼宇分区分组推送,实现更精准的告警分发。第四,引入 AI 能力对巡查跟拍画面进行实时安全隐患识别(如消防通道堆物、灭火器缺失),将影随人动从"构图辅助"升级为"智能识别"。第五,接入鸿蒙分布式能力,实现多巡检员的跨设备任务协同——在手表端查看任务清单、在手机端取证拍照、在平板端填写报告,充分发挥 HarmonyOS 全场景多设备协同优势。

HarmonyOS ArkUI 的声明式 UI 范式与 HarmonyOS 6.1.1 的 Camera Kit、Notification Kit、Tabs 嵌套滚动等前沿特性,为智慧物业管理应用的开发提供了强大的系统级能力支撑。通过合理的色彩体系设计、数据模型分层、组件化拆分和状态管理策略,开发者可以构建出既视觉精美又功能完备的行业级应用。希望本文的深度解析能为从事鸿蒙生态开发的同行提供参考与启发。

附录: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 应用的功能开发。


本文基于 DevEco Studio 6.1.1 Release 版本编写,不同版本界面可能存在细微差异。

Logo

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

更多推荐