一、技术前言

在智慧物业管理领域,安全巡检是建筑运行保障的核心环节。从消防通道堆物排查到配电间红外测温,从喷淋管网压力巡检到水泵启动试运行测试,每一项巡检任务都要求精确的流程管控、清晰的进度追踪和即时的异常告警。传统巡检应用长期面临三大技术瓶颈:取证影像模糊导致责任界定困难、告警铃声千篇一律导致响应迟缓、巡检路径层级复杂导致界面交互割裂。物业巡检员每天穿梭于楼宇、地库、配电房与消防泵房之间,手持终端既要取证拍照又要回填记录,还要在发现异常时第一时间发出告警——这对移动端应用的性能、交互流畅度和状态管理提出了极高要求。

HarmonyOS ArkUI 框架为这些痛点提供了系统级的声明式解决方案。ArkUI 基于 TypeScript 扩展的 ArkTS 语言,通过 @Component 装饰器将 UI 封装为可复用的组件单元,通过 @State@Observed@ObjectLink 等状态管理装饰器实现数据驱动的自动渲染,通过 @Builder 方法将复杂界面拆分为可组合的构建块。这种"状态即视图"的架构天然适合巡检场景中数据与交互紧密耦合的需求。@Entry 装饰器标识入口组件,ForEach 实现列表的高效渲染与 diff 更新,Stack 容器实现弹窗遮罩与页面内容的层叠覆盖,Progress 组件实现巡检进度的可视化追踪,Slider 组件实现对焦距离的精确调节,共同构成了巡检平台的 UI 骨架。@State 装饰的变量一旦变更,框架自动触发所有引用该变量的 UI 节点重渲染,无需开发者手动调用 setStatenotify,这种声明式的数据流模型让巡检进度、对焦记录、告警历史等动态数据的展示变得极为简洁。

本平台深度融合了 HarmonyOS 6.1.1 的三大前沿特性。Camera Kit 提供了 VideoSession 的 AUTO_FRAMING(影随人动)能力链——通过 isControlCenterSupported 判断设备是否支持控制中心,通过 getSupportedEffectTypes 获取已声明的效果类型列表,通过 enableControlCenter(true) 请求系统接管构图,三步实现巡检取景时人员始终居中;同时 PhotoSession 的手动对焦三接口 isFocusDistanceSupportedsetFocusDistancegetFocusDistance 实现从铭牌近拍到机房全景的精确对焦控制,并以读回值与设置值的差值校验生效性。两种会话通过互斥切换设计(释放 VideoSession 再建 PhotoSession)保证资源安全,避免摄像头资源被同时占用导致的冲突。Notification Kit 实现了沙箱自定义铃声的完整链路——通过 buildWavBytes 函数以正弦波数学模型生成 16bit 单声道 PCM 音频数据,写入 EL1 沙箱的 filesDir 目录,再以 'uri::' + fileUri.getUriFromPath(沙箱路径) 的格式填入 NotificationRequest.sound 字段,让不同等级告警拥有差异化铃声。这一链路打破了通知铃声只能使用 rawfile 资源的限制,为告警分级响应提供了声音层面的技术支撑。Tabs 嵌套滚动 通过 nestedScroll(TabsNestedScrollMode) 让内层检查项列表滑到边缘后联动外层楼宇频道,实现巡检路径在多层级间的自然流转,并通过 SwipeLog 时间轴完整记录翻页事件。

二、整体架构流程图

数据模型层

弹窗系统

四大核心特性

内容区七Tab

页面根容器

主组件入口

headerBanner 头部渐变Banner

内容区 七Tab切换

tabBar 底部导航栏

modalOverlay 弹窗遮罩层

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

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

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

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

Tab4 日志
SwipeLog翻页状态时间轴

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

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

Camera Kit
AUTO_FRAMING影随人动

Camera Kit
手动对焦三接口

Tabs嵌套滚动
nestedScroll模式

Notification Kit
沙箱自定义铃声

panelAdd 新建巡检任务

panelEdit 编辑巡检进度

panelDel 删除确认弹窗

TaskItem 巡检任务

FocusRecord 对焦记录

InnerCard 检查项卡片

SwipeLog 翻页日志

RingItem 告警铃声

NoticeLog 通知历史

整体架构以主组件为根,采用 Stack 容器实现页面层叠结构。底层是 Column 纵向布局,自上而下依次为头部渐变 Banner、内容区(通过 currentTab 状态索引在 7 个 @Builder 方法间切换)和底部 Tab 导航栏。顶层是全屏弹窗遮罩,在 addModaleditModaldelModal 三个布尔状态任一为真时渲染,覆盖整个页面。四大核心特性分别挂载在相机、对焦、频道、告警四个 Tab 上,但它们的状态变量(如 sessionModefocusSupportednestedModegranted)统一声明在组件顶层,实现跨 Tab 数据共享和头部状态胶囊的实时联动。

数据模型层的六个 @Observed 类分别支撑各自 Tab 的列表渲染。TaskItem 同时被弹窗系统引用以实现 CRUD 操作——新建任务通过 unshift 置顶到列表头部,删除任务通过 splice 移除,编辑进度时若 progress >= 100 则自动将状态置为"已完成"。FocusRecord 记录每次手动对焦的设置值、读回值和校验结论,通过 unshift 置顶且最多保留 20 条。SwipeLog 记录双层 Tabs 的翻页事件,最多保留 40 条。RingItem 管理告警铃声库的生成与沙箱导入状态。NoticeLog 记录通知发布历史,最多保留 8 条。

三、色彩体系设计

3.1 ColorPalette 接口定义

平台采用深色安全蓝灰主题,通过 ColorPalette 接口集中声明全部颜色字段。该接口是全文件的色彩契约,所有 Builder 函数和工具函数均引用此接口定义的常量,确保视觉一致性:

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

这段接口定义体现了 ArkTS 的类型安全优势。与普通 JavaScript 动态添加属性不同,ColorPalette 接口在编译期即约束所有颜色字段必须是 string 类型,任何拼写错误或类型不匹配都会在编译阶段暴露。接口注释采用"字段名 + 用途"的格式,使每个颜色的语义角色一目了然,后续维护者无需追踪代码即可理解色彩用途。onMain 字段是橙底深色文字专用色,确保橙色按钮上的文字对比度符合可访问性标准,避免浅色文字在橙色背景上产生的眩光问题。

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'   // 橙底深字,保证按钮文字对比度
};

色彩设计遵循"安全警示"原则:橙、绿、蓝、红四色分别对应"巡检中、已合格、信息参考、危险警告"四种语义状态。bg#14171C 极深蓝灰黑,模拟夜间巡检的暗光环境,降低屏幕亮度对巡检员眼睛的刺激,同时为后续的橙色警示提供高对比度背景。card#1E232B 深蓝灰,卡片与背景形成一档亮度差,保证信息区块的清晰边界但不突兀。dark#283039 次级容器底色,用于统计格、进度条底、徽章底等内部元素,比卡片底色再亮一档,形成"背景→卡片→次级容器"的三级灰度层次。

title#EDF1F5 冷白色,主标题文字色,与深色背景形成极高对比度,保证暗光环境下文字清晰可读。sub#ADBAC7 蓝灰,副标题色,在标题与弱文本之间架起层次过渡。text3#74818E 暗蓝灰,三级弱文本,用于辅助说明和时间戳,视觉权重最低。

orange#FF8A2B 警示橙,平台主色,贯穿头部 Banner 的 linearGradient 渐变(从 orangeDbg)、巡检进度条、完成率大数字、对焦距离值和按钮背景,实现警示橙到蓝灰黑的自然过渡。orangeD#E06A10 深橙,渐变起点色,用于头部 Banner 的渐变和已选中对焦预设的按钮态。blue#4E9BE3 信息蓝,专用于电气检查项、内层检查项翻页日志和待复查状态,与主橙色形成冷暖对比。green#34C98A 合格绿,用于已完成状态和读回校验通过提示。red#E85555 警示红,仅用于逾期任务和删除按钮,通过低频使用强化警示语义。tabOnorange 同值,保证 Tab 选中态与主色一致。mask 为半透明黑色 rgba(0,0,0,0.6),弹窗遮罩使用 RGBA 格式实现 60% 透明度。onMain#231507 深暖黑,橙底文字专用色,确保橙色按钮上的文字对比度符合可访问性标准。

3.3 三级灰度层次与语义色系

平台色彩体系的核心设计理念是"三级灰度 + 四色语义"。三级灰度指的是 bg(#14171C)→ card(#1E232B)→ dark(#283039)三个层级的底色递进,每一级亮度差约 6%~8%,肉眼可辨但不刺眼。这种设计使得页面背景、卡片容器和内部元素(如统计格、进度条底、徽章底)自然形成视觉层次,无需依赖边框线即可区分内容区块。

四色语义指的是橙、绿、蓝、红四种功能色,每种颜色承担明确的语义角色:橙色代表"巡检进行中",绿色代表"已合格完成",蓝色代表"信息参考",红色代表"危险警告"。颜色与业务状态的绑定通过工具函数群实现,如 taskStatusColor 将"已完成"映射为绿色、"进行中"映射为橙色、"待复查"映射为蓝色、"已逾期"映射为红色。这种"状态→颜色"的映射机制确保了全平台色彩语义的一致性,巡检员在不同页面看到相同颜色时能立即联想到相同的业务含义。

四、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: '我的' }
];

TabMeta 接口定义了 Tab 导航项的最小数据结构:icon 为 emoji 字符串,label 为中文标签文字。TAB_LIST 常量数组按顺序声明七个 Tab 项,分别对应任务、相机、对焦、频道、日志、告警和我的。每个 Tab 的 Emoji 图标与其功能语义紧密对应:📋 代表任务清单,📷 代表取证拍照,🎯 代表对焦精准,🌀 代表频道流转,📜 代表日志记录,🚨 代表告警上报,👤 代表巡检员中心。

这种将导航元数据与 UI 渲染分离的设计使 Tab 配置可独立维护,新增或调整 Tab 只需修改数组而无需触碰 @Builder 方法。底部导航栏在 tabBar() 构建器中通过 ForEach 遍历此数组渲染,ForEach 的第三个参数(键值生成器)使用 tab.label 作为唯一键,保证 Tab 切换时的高效 diff 更新。选中态通过 currentTab 索引与 index 比较判断,选中时文字使用 COLORS.tabOn(警示橙),未选中时使用 COLORS.text3(暗蓝灰弱化)。

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

EffectInfo 接口定义了相机控制中心效果类型的三字段结构。type 为数字枚举值,对应 ControlCenterEffectType 枚举的底层值;name 为大写英文名,用于代码展示;desc 为中文描述及版本信息。三效果枚举展示了 ControlCenterEffectType 的完整谱系:BEAUTY(美颜)和 PORTRAIT(人像)自 API 20 起就存在,AUTO_FRAMING(影随人动)是 6.1.1 新增能力。

在相机 Tab 的枚举表中,当 info.type === 2 && this.framingSupported 时会额外渲染"已声明"绿色徽章,直观展示本机是否支持影随人动。这种设计让巡检员在进入相机页面时立即了解到设备的完整能力谱系,而不需要通过实际操作来试探功能是否可用。

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 用于拍摄机房全景和疏散通道的远距取证,需要呈现设备间的空间关系和通道畅通情况。

点击预设档位时直接赋值 this.focusDistance = preset.distance,再通过"设置并读回校验"按钮触发 setFocusDistancegetFocusDistance 的验证流程。当前选中的档位以橙色背景高亮,三档文字使用 COLORS.onMain 深暖黑保证在橙色背景上的可读性。这种"预设一键到位 + 校验确认"的交互模式,让巡检员无需在 Slider 上反复微调,大幅提升取证效率。

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[] = ['消防', '电气', '管道', '通道', '监控'];

外层 5 个楼宇频道代表物业巡检的责任分区,覆盖 1 号楼、2 号楼、地下车库、配电房和消防泵房五个典型巡检区域。内层 5 个检查项类别覆盖消防、电气、管道、通道、监控五大专业领域。两层 Tabs 嵌套形成 25 个检查矩阵,每个矩阵下通过 innerMockData 函数生成 8 条检查卡片,共 200 个检查点位。

内容刻意超过一屏,是 nestedScroll 模式演示"滑到边缘接力"效果的前提条件。当内层检查项列表在 SELF_FIRST 模式下滑到底部边缘时,继续滑动手势会自动触发外层楼宇频道的 Tab 切换,实现巡检路径从"1 号楼消防"到"2 号楼消防"的自然流转,而无需巡检员先回到外层 Tab 再重新选择内层页签。

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;

ALARM_LEVELS 定义了告警上报的三档等级:一般、严重、紧急。三档等级通过 levelColor 函数映射为不同的颜色——一般为信息蓝、严重为警示橙、紧急为警示红,在告警 Tab 的等级 chips 中以不同背景色呈现,让巡检员在视觉上立即感知告警的紧迫程度。

近 6 个月隐患发现数据整体呈下降趋势(15→7→6),反映巡检整改效果。3 月发现 12 处、4 月降至 9 处、5 月反弹至 15 处(可能因季度全面排查)、6 月降至 7 处、7 月回升至 11 处、8 月降至最低的 6 处。满刻度 MONTH_MAX 设为 16 处(略高于最大值 15),用于柱高归一化换算,使柱状图在视觉上不会顶满整个图表区域。柱状图随呼吸动画在正负 6% 区间交替波动,通过 barHeight 函数中 (i % 2 === 0) === this.breath ? 1.06 : 0.94 实现奇偶柱交替起伏的视觉效果。

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 项核心指标,分为两个视觉层次。前三项(本月完成点位、隐患整改闭环、平均响应时长)使用 COLORS.orange 警示橙高亮数值,这三项是巡检质量的核心考核指标,直接反映巡检员的工作成效。后三项(巡检里程、取证张数、安全天数)使用 COLORS.sub 蓝灰弱化数值,作为辅助参考信息。每行包含指标标签、数值和备注说明,备注说明提供了更详细的上下文信息(如"应检 320 · 完成率 97.5%"解释了 312 个点位在 320 个应检点位中的完成比例),形成"主指标→数值→说明"的三级信息层次。

五、工具函数分析

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。该函数在 FocusRecordSwipeLogNoticeLog 三个数据模型的构造函数中被调用,确保全平台时间戳格式一致。统一时间戳格式的价值在于:日志 Tab 的时间轴列宽固定为 52px,等宽字体 monospace 配合两位数时间戳保证对齐美观,不会因时分秒位数变化导致列宽跳动。

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 返回完整技术文案用于 SwipeLog 日志记录,格式为"枚举名·中文说明",既保留了技术可读性又提供了语义解释。modeShort 返回短文案用于头部状态胶囊和频道 Tab 的模式切换 chips。SELF_FIRST 模式下内层滑到边缘会接力触发外层切换,SELF_ONLY 模式下内层滑动不联动外层,这两个模式通过 nestedScroll(this.nestedMode) 方法挂载到内层 Tabs 上。

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

framingStateColor 函数将影随人动能力链的执行状态字符串映射为颜色常量。已启用为合格绿(COLORS.green),表示 AUTO_FRAMING 已成功生效;能力不支持(控制中心不支持或 AUTO_FRAMING 未声明)为信息蓝(COLORS.blue),表示设备能力缺失但非错误;失败/被拒/未就绪为警示红(COLORS.red),表示能力链执行过程中出现异常;未查询为暗蓝灰(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)翻译为巡检景别文案。小于 0.3 为近拍场景(设备铭牌与出厂序列号),0.3 至 0.7 之间为中距场景(配电柜整柜与管线走向),大于等于 0.7 为远距场景(机房全景与疏散通道)。这种映射让巡检员在调节 Slider 时能直观感知当前对焦距离对应的实际拍摄场景,而非仅看到抽象的数字。

focusOkColor 函数将校验结论映射为颜色:已生成为合格绿、失败为警示红、读回偏差为警示橙。校验结论通过 Math.abs(readBack - this.focusDistance) < 0.01 判定——差值小于 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 函数将相机会话模式翻译为中文说明:video 模式说明该会话是影随人动的宿主,photo 模式说明该会话是手动对焦的宿主,idle 表示尚未启动会话。

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 函数将告警三档等级映射为颜色:一般为信息蓝、严重为警示橙、紧急为警示红。颜色递进与告警紧迫程度正相关,让巡检员在告警 Tab 的等级 chips 上一眼感知告警级别。wavSizeText 函数根据时长估算 WAV 文件大小——44 字节文件头加上 44100 * durationMs / 1000 * 2 字节的 PCM 采样数据(16bit 单声道,每采样 2 字节),再除以 1024 转为 KB 并保留一位小数。例如 900ms 的铃声约为 79.7KB,450ms 的铃声约为 43.3KB,这些大小信息在铃声行的元数据中展示。

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);
  const writeStr = (offset: number, s: string) => {
    for (let i = 0; i < s.length; i++) { view.setUint8(offset + i, s.charCodeAt(i)); }
  };
  // 44 字节 WAV 文件头:RIFF/WAVE/fmt/data 四段
  writeStr(0, 'RIFF');
  view.setUint32(4, 36 + dataSize, true);
  writeStr(8, 'WAVE');
  writeStr(12, 'fmt ');
  view.setUint32(16, 16, true);        // fmt 段长度
  view.setUint16(20, 1, true);         // PCM 编码
  view.setUint16(22, 1, true);         // 单声道
  view.setUint32(24, sampleRate, true); // 采样率 44100
  view.setUint32(28, sampleRate * 2, true);
  view.setUint16(32, 2, true);
  view.setUint16(34, 16, true);         // 16bit 量化
  writeStr(36, 'data');
  view.setUint32(40, dataSize, true);
  // PCM 采样数据:正弦波 + 起音包络 + 自然衰减
  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;
}

buildWavBytes 是 Notification Kit 沙箱铃声链路的起点。它使用 DataView 手工构造标准 WAV 文件,不依赖任何第三方音频库,纯数学运算生成完整音频数据。44 字节文件头包含 RIFF 容器标识(标识这是一个 RIFF 格式文件)、WAVE 格式标识(标识这是一个 WAV 音频)、fmt 子段(包含 PCM 编码/单声道/44100 采样率/16bit 量化等格式信息)和 data 子段(标识后续为音频数据)。

音频数据采用正弦波模型 Math.sin(2 * Math.PI * freq * t) 生成,频率参数 freq 决定音高——880Hz 生成疏散警报的高频尖锐声、660Hz 生成消防长鸣的中低频持续声、1046Hz 生成门禁提示的清脆短声、1320Hz 生成周界蜂鸣的超高频警示声。起音包络 env 在前 20ms 内从 0 线性升到 1 避免爆音(直接从 0 跳到最大振幅会产生"啪"的杂音),自然衰减 decay 从 1 线性降到 0 模拟铃声渐弱。最终振幅乘以 0.5 和 32767(16bit 有符号最大值),保证写入值在 Int16 范围内(-32768~32767)。

5.8 检查要点池

function checkPoint(tabName: string, i: number): string {
  const pool: string[] = tabName === '消防'
    ? ['灭火器压力表指针', '消火栓水带卡扣', '疏散指示标识灯', '防火门闭门器', '卷帘门导轨积尘', '消防电话分机', '排烟阀执行机构', '水泵接合器锈蚀']
    : tabName === '电气'
      ? ['配电柜母排温度', '断路器接线端子', '电缆桥架盖板', '接地扁铁连接', '应急照明蓄电池', '双电源切换柜', '线槽穿墙封堵', '电表箱铅封完好']
      : tabName === '管道'
        ? ['喷淋管网压力表', '湿式报警阀阀瓣', '排水沟防鼠网', '集水坑液位浮球', '阀门铅封完好性', '法兰垫片渗漏点', '给水立管支架', '污水提升泵试运行']
        : tabName === '通道'
          ? ['安全出口堆物核查', '疏散通道净宽度', '台阶防滑条完好', '卷帘下净空高度', '消防车道占位车辆', '楼梯间杂物清理', '门禁断电常开测试', '指示牌反光膜状态']
          : ['监控镜头遮挡检查', '硬盘录像机容量', '周界红外对射探测', '电子巡更读卡点位', '录像保存天数达标', '视频画面雪花排查', '机房温湿度记录', 'UPS 备用电池电压'];
  const idx = Math.min(pool.length - 1, Math.max(0, i - 1));
  return pool[idx];
}

checkPoint 函数为嵌套频道的 5 类检查项各提供 8 个行业语义化检查要点。通过 tabName 参数分发到对应的要点池——消防类的"灭火器压力表指针"和"消火栓水带卡扣"、电气类的"配电柜母排温度"和"断路器接线端子"、管道类的"喷淋管网压力表"和"湿式报警阀阀瓣"、通道类的"安全出口堆物核查"和"疏散通道净宽度"、监控类的"监控镜头遮挡检查"和"硬盘录像机容量"。再按 i 索引取出,通过 Math.min(pool.length - 1, Math.max(0, i - 1)) 做边界保护。这些要点使检查卡片内容真实可信,贴近实际巡检场景,而非使用"检查项1""检查项2"等占位文本。

六、数据模型层

平台使用 @Observed 装饰器定义了 5 个可观察数据模型,确保数据变更时绑定的 UI 自动刷新。@Observed 装饰器的核心机制是:被装饰的类的实例在属性变更时会通知所有通过 @ObjectLink 或直接 @State 引用该实例的组件,触发组件的重渲染。这种"数据变更→自动刷新"的机制使巡检员在操作数据(如新增任务、修改进度、发布通知)后无需手动刷新界面,UI 会自动呈现最新状态。

6.1 TaskItem 巡检任务模型

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

TaskItem 是进度条清单的主体实体,弹窗的新增/编辑/删除均绑定该模型。四个字段分别记录楼宇分区名(如"1 号楼"“地下车库”)、巡检项目名(如"消防通道占用复查")、进度值(0~100 的整数)和状态字符串。状态字段有四种取值:已完成(progress 达到 100)、进行中(progress 在 0~99 之间)、待复查(progress 达到 100 但需要复查确认)、已逾期(progress 为 0 且超过截止时间)。

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, '已逾期')
];

初始化数据 TASK_LIST 包含 7 条覆盖全部四种状态的巡检任务。这 7 条数据精心设计了状态分布:2 条已完成、3 条进行中、1 条待复查、1 条已逾期,确保任务 Tab 的统计卡和进度条清单能完整展示所有状态色。新建任务通过 unshift 置顶到列表头部,删除任务通过 splice 移除,编辑进度时若 progress >= 100 则自动将状态置为"已完成",否则保持"进行中"。

6.2 FocusRecord 对焦记录模型

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

FocusRecord 记录每次手动对焦操作的设置值、读回值和校验结论。构造函数自动调用 nowTime() 生成时间戳,确保记录时间与实际操作时间一致。readback 字段为 -1 时表示调用失败(如无会话或异常抛出),正常情况下存储 getFocusDistance 的返回值。ok 字段通过 Math.abs(readBack - this.focusDistance) < 0.01 判定——差值小于 0.01 为"已生效",差值超出阈值为"读回偏差",调用异常时为"失败"。

aboutToAppear 生命周期中种子了 3 条对焦记录,其中 0.1 档故意制造"读回偏差"(设置 0.1 读回 0.12),用于演示校验机制能发现设置值与实际值之间的偏差。记录列表通过 unshift 置顶,最多保留 20 条,超出时通过 pop() 移除尾部最旧记录。

6.3 InnerCard 检查项卡片模型

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

InnerCard 是楼宇频道与检查项子页签交叉矩阵中的列表内容单元。id 字段格式为 楼宇名-检查类-序号(如"1 号楼-消防-3"),作为 ForEach 的键值保证渲染效率和 diff 正确性。tag 存储检查项类别名(消防/电气/管道/通道/监控),title 为检查点标题(如"【消防】灭火器压力表指针核查"),desc 为检查内容描述,包含楼宇图标、楼宇名、类别、序号和检查要点。

function innerMockData(channel: ChannelItem, tabName: string): InnerCard[] {
  const list: InnerCard[] = [];
  for (let i = 1; i <= 8; i++) {
    const point = checkPoint(tabName, i);
    list.push(new InnerCard(`${channel.name}-${tabName}-${i}`, tabName,
      `${tabName}${point}核查`,
      `${channel.icon}${channel.name}${tabName}类第 ${i} 项:核查${point},拍照取证并回填巡检结论`));
  }
  return list;
}

innerMockData 函数为每个矩阵生成 8 条卡片,内容通过 checkPoint 函数从要点池获取。8 条保证内容超过一屏,这是 nestedScroll 模式演示"滑到边缘接力"效果的前提条件。每条卡片的描述包含楼宇图标、楼宇名、检查类别、序号和检查要点,格式统一但内容各异,使检查卡片真实可信。

6.4 SwipeLog 翻页日志模型

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

SwipeLog 记录双层 Tabs 的翻页事件。layer 字段区分外层楼宇翻页和内层检查项翻页,在日志 Tab 的时间轴中以橙色和蓝色竖线区分两种层级。fromIdxtoIdx 记录索引变化(如从第 0 页切换到第 1 页,显示为"0→1"),直观展示翻页方向。mode 字段记录事发时的嵌套模式,这对于分析"内层滑到边缘是否触发了外层切换"至关重要——如果 mode 为 SELF_FIRST 且 layer 为"外层楼宇",说明内层滑到边缘后成功接力触发了外层切换。日志通过 unshift 置顶,最多保留 40 条。

6.5 RingItem 告警铃声模型

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

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)
];

RingItem 是告警铃声条目,记录铃声名、沙箱文件名、生成频率、时长、文件大小和沙箱导入状态。初始数据 RING_LIST 包含 4 条铃声,覆盖四种典型巡检告警场景。疏散警报以 880Hz 高频持续 900ms,模拟紧急疏散时的尖锐警报声;消防长鸣以 660Hz 中低频持续 1200ms,模拟消防系统的长鸣警报;门禁提示以 1046Hz 清脆短声持续 600ms,模拟门禁系统的提示音;周界蜂鸣以 1320Hz 超高频持续 450ms,模拟周界安防的蜂鸣警示。初始状态 inSandbox 均为 false,size 为"—"(未生成),用户点击"生成"后写入沙箱并更新大小。

6.6 NoticeLog 通知发布历史

@Observed export class NoticeLog {
  title: string;  // 通知标题
  text: string;   // 通知正文(失败时为失败原因)
  time: string;   // 发布时间戳
  constructor(title: string, text: string) {
    this.title = title; this.text = text; this.time = nowTime();
  }
}

const NOTICE_SEED: NoticeLog[] = [
  new NoticeLog('守护巡查 · 严重告警', '地下车库 B2 喷淋管网压力低于阈值,已派单整改'),
  new NoticeLog('守护巡查 · 一般告警', '3 号楼 12 层疏散指示灯故障 1 处,已更换灯箱')
];

NoticeLog 是通知发布历史,记录标题、正文(失败时为失败原因)和时间戳。种子数据包含 2 条历史记录,模拟之前发布过的告警通知。发布成功或失败均记一条,最多保留 8 条,通过 unshift 置顶最新记录。失败记录的标题包含"失败"二字,在 noticeRow 构建器中通过 log.title.indexOf('失败') >= 0 判断并使用 COLORS.red 警示红着色,与成功记录形成视觉区分。

七、组件主体结构

7.1 状态变量声明

主组件声明了六大类状态变量,覆盖 Tab 切换、弹窗控制、动画驱动、Camera Kit 会话、Notification Kit 授权和 Tabs 嵌套滚动:

@Entry
@Component
struct Page1285 {
  // --- Tab 状态 ---
  @State currentTab: number = 0;

  // --- 弹窗状态(绑定 TaskItem 巡检任务实体) ---
  @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;
  @State formBuilding: string = '';
  @State formItem: string = '';
  @State formProgress: number = 0;
  @State editProgress: number = 0;

Tab 状态currentTab 控制当前显示的 Tab 索引,初始值为 0(任务 Tab)。该变量在 build() 方法的 if-else 链中决定渲染哪个 @Builder 方法,在 tabBar() 中决定哪个 Tab 项高亮,在 headerBanner() 中决定副标题文案。

弹窗状态addModaleditModaldelModal 三个布尔标志控制三种弹窗的显示,editIdxdelIdx 记录当前操作的任务索引。三个布尔标志互斥(同一时间只有一个为 true),在 modalOverlay 构建器中通过 if-else if 链选择渲染哪个弹窗面板。

动画状态breath 布尔值每秒翻转一次,驱动呼吸圆点透明度变化(0.9↔0.45)和柱状图奇偶柱交替波动(1.06↔0.94)。timer 存储 setInterval 的定时器 ID,在 aboutToDisappear 时用于清理。

任务数据与表单taskList 绑定 TASK_LIST 初始数据,弹窗操作直接修改该数组。formBuildingformItemformProgress 绑定新建弹窗的三个表单字段,editProgress 绑定编辑弹窗的进度 Slider。

  // --- Camera Kit 成员(控制器类 private,不参与渲染) ---
  private previewController: XComponentController = new XComponentController();
  private cameraInput?: camera.CameraInput;
  private previewOutput?: camera.PreviewOutput;
  private videoSession?: camera.VideoSession;
  private photoSession?: camera.PhotoSession;
  @State surfaceReady: boolean = false;
  @State sessionMode: string = 'idle';
  @State framingState: string = '未查询';
  @State framingSupported: boolean = false;
  @State focusSupported: boolean = false;
  @State focusDistance: number = 1.0;
  @State focusRecords: FocusRecord[] = [];
  @State permState: string = '未申请';

Camera Kit 成员:控制器类(previewControllercameraInputpreviewOutputvideoSessionphotoSession)声明为 private,不参与 UI 渲染,仅作为相机管线的内部对象。UI 相关的状态使用 @State 装饰:surfaceReady 标记 XComponent Surface 是否就绪,sessionMode 跟踪当前会话模式(idle/video/photo),framingState 记录影随人动能力链状态,framingSupported 标记本机是否声明 AUTO_FRAMING,focusSupported 标记是否支持设置对焦距离,focusDistance 存储当前对焦距离(0.0~1.0),focusRecords 保存对焦记录时间线,permState 跟踪 CAMERA 权限状态。

  // --- Notification Kit 成员 ---
  @State granted: boolean = false;
  @State notifyId: number = 200;
  @State ringList: RingItem[] = RING_LIST;
  @State currentRingIdx: number = 0;
  @State noticeLogs: NoticeLog[] = NOTICE_SEED;
  @State alarmTitle: string = '';
  @State alarmLevel: string = '一般';
  @State alarmDesc: string = '';

  // --- Tabs 嵌套滚动成员 ---
  @State nestedMode: TabsNestedScrollMode = TabsNestedScrollMode.SELF_FIRST;
  @State outerIndex: number = 0;
  @State innerIndex: number = 0;
  @State swipeLogs: SwipeLog[] = [];

Notification Kit 成员granted 标记通知授权状态,notifyId 从 200 开始自增管理通知 ID,ringList 维护铃声库(初始 4 条),currentRingIdx 跟踪当前默认铃声索引,noticeLogs 保存发布历史(初始 2 条种子数据),alarmTitlealarmLevelalarmDesc 绑定上报表单的三个字段。

嵌套滚动成员nestedMode 存储当前嵌套模式枚举值(默认 SELF_FIRST),outerIndexinnerIndex 跟踪双层 Tabs 当前位置,swipeLogs 保存翻页日志(初始为空,最多保留 40 条)。

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 在组件创建时执行三项初始化。第一项是异步查询通知授权状态——调用 notificationManager.isNotificationEnabled() 返回 Promise,成功时将 granted 置为查询结果,异常时静默捕获不阻塞后续逻辑。第二项是种子对焦记录——三条记录分别对应三档预设的验证结果,其中 0.1 档故意制造"读回偏差"(设置 0.1 读回 0.12,差值 0.02 超过 0.01 阈值),用于演示校验机制能发现设置值与实际值之间的偏差。第三项是启动 1 秒间隔的呼吸动画定时器——setInterval 每秒翻转 breath 布尔值,驱动呼吸圆点透明度变化和柱状图奇偶柱交替波动。

aboutToDisappear 在组件销毁时清理定时器并释放相机资源,防止后台占用摄像头。clearInterval(this.timer) 清除呼吸动画定时器避免内存泄漏,this.releaseSession() 释放可能正在运行的相机会话(VideoSession 或 PhotoSession),防止摄像头资源在组件销毁后仍被占用导致其他应用无法访问摄像头。

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

switchTab 方法在 Tab 切换时检查是否离开相机/对焦页。如果当前在相机页(index=1)或对焦页(index=2),且目标页不是这两个页面中的任一个,则调用 releaseSession 释放会话。这是因为 cameraInput 同一时间只能绑定一个 session,VideoSession 与 PhotoSession 互斥,切换 Tab 时必须先释放当前会话再允许用户操作其他页面。但相机和对焦之间的切换不需要释放会话(它们共享同一 Surface),只有离开这两个页面时才需要释放。

7.3 根构建方法

build() {
  Stack({ alignContent: Alignment.Center }) {
    Column() {
      this.headerBanner()
      Divider().strokeWidth(1).color(COLORS.line)
      Column() {
        if (this.currentTab === 0) {
          this.tabTask()
        } else if (this.currentTab === 1) {
          this.tabCamera()
        } else if (this.currentTab === 2) {
          this.tabFocus()
        } else if (this.currentTab === 3) {
          this.tabChannel()
        } else if (this.currentTab === 4) {
          this.tabLogs()
        } else if (this.currentTab === 5) {
          this.tabAlarm()
        } else {
          this.tabMine()
        }
      }.layoutWeight(1).width('100%')
      this.tabBar()
    }.width('100%').height('100%')
    if (this.addModal || this.editModal || this.delModal) {
      this.modalOverlay(() => { this.closeAllModals(); })
    }
  }.width('100%').height('100%').backgroundColor(COLORS.bg)
}

build 方法是组件的根构建入口,采用 Stack 容器实现页面层叠。底层是 Column 纵向布局:头部渐变 Banner(headerBanner)+ 1px 分割线 + 内容区(layoutWeight(1) 占满剩余高度)+ 底部 Tab 导航栏(tabBar)。内容区通过 if-else if-else 链根据 currentTab 索引选择渲染对应的 @Builder 方法,七个 Tab 各有独立的布局结构。

顶层是弹窗遮罩层,当 addModaleditModaldelModal 任一为真时渲染 modalOverlay 构建器。弹窗遮罩使用 Stack 层叠覆盖在整个页面上方,justifyContent(FlexAlign.End) 使弹窗面板从底部弹出。整个 Stack 使用 COLORS.bg 深色背景。

八、头部渐变 Banner 详解

头部 Banner 是全平台的视觉锚点,通过 linearGradient 实现警示橙到蓝灰黑的自然过渡:

@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 }) {
      // 任务胶囊
      Row({ space: 6 }) {
        Text('📋').fontSize(10)
        Text(`巡检 ${this.taskList.length}`).fontSize(10).fontColor(COLORS.sub)
      }.padding({ left: 10, right: 10, top: 6, bottom: 6 })
      .borderRadius(12).backgroundColor(COLORS.mask)

      // 相机会话胶囊
      Row({ space: 4 }) {
        Circle({ width: 6, height: 6 })
          .fill(this.sessionMode === 'idle' ? COLORS.text3
            : this.sessionMode === 'video' ? COLORS.green : COLORS.orange)
        Text(`相机 ${this.sessionMode}`).fontSize(10).fontColor(COLORS.sub)
      }.padding({ left: 10, right: 10, top: 6, bottom: 6 })
      .borderRadius(12).backgroundColor(COLORS.mask)

      // 通知授权胶囊
      Row({ space: 4 }) {
        Circle({ width: 6, height: 6 }).fill(this.granted ? COLORS.green : COLORS.red)
        Text(this.granted ? '通知已授权' : '通知未授权').fontSize(10).fontColor(COLORS.sub)
      }.padding({ left: 10, right: 10, top: 6, bottom: 6 })
      .borderRadius(12).backgroundColor(COLORS.mask).layoutWeight(1)

      // 嵌套模式胶囊
      Row({ space: 4 }) {
        Circle({ width: 6, height: 6 })
          .fill(this.nestedMode === TabsNestedScrollMode.SELF_FIRST ? COLORS.blue : COLORS.orange)
        Text(`嵌套 ${modeShort(this.nestedMode)}`).fontSize(10).fontColor(COLORS.sub)
      }.padding({ left: 10, right: 10, top: 6, bottom: 6 })
      .borderRadius(12).backgroundColor(COLORS.mask)
    }.width('100%')
  }.padding({ left: 16, right: 16, top: 12, bottom: 12 })
  .width('100%')
  .linearGradient({ angle: 160, colors: [[COLORS.orangeD, 0], [COLORS.bg, 1]] })
}

头部 Banner 包含两个水平行。第一行是标题与呼吸圆点:标题固定为"守护巡查 · 物业安全巡检",使用 COLORS.onMain 深暖黑保证在橙色渐变背景上的可读性。副标题根据 currentTab 索引实时联动显示当前 Tab 的功能描述——任务 Tab 显示"任务 · 今日 7 项巡检",相机 Tab 显示"相机 · 影随人动取证预览",对焦 Tab 显示"对焦 · 手动对焦三接口",频道 Tab 显示"频道 · 楼宇×检查项双层 Tabs",日志 Tab 显示"日志 · nestedScroll 时间线",告警 Tab 显示"告警 · 沙箱自定义铃声",我的 Tab 显示"巡检员中心"。这种联动设计让巡检员在切换 Tab 时立即感知到当前所在功能区域。

呼吸圆点通过 breath 状态每秒翻转透明度(0.9↔0.45),形成心跳脉冲效果,暗示平台正在实时运行。圆点使用 COLORS.onMain 深暖黑填充,与橙色渐变背景协调。

第二行是四个状态胶囊,分别反映四大特性的实时状态。任务胶囊显示巡检任务总数(如"巡检 7 项")。相机会话胶囊的圆点颜色随 sessionMode 变化——idle 为暗蓝灰(未启动)、video 为合格绿(影随人动运行中)、photo 为警示橙(拍照会话运行中),文字显示"相机 idle"/“相机 video”/“相机 photo”。通知授权胶囊的圆点颜色反映 granted 布尔值——绿已授权/红未授权,文字显示"通知已授权"/“通知未授权”。嵌套模式胶囊的圆点颜色反映 nestedMode 枚举——SELF_FIRST 为信息蓝/SELF_ONLY 为警示橙,文字显示"嵌套 先内后外"/“嵌套 仅内层”。四个胶囊共同构成全平台特性状态的全景仪表盘,巡检员通过头部即可掌握所有核心功能的运行状态。

Banner 整体使用 linearGradient 渐变,角度 160 度(从左上到右下),颜色从 COLORS.orangeD(深橙 #E06A10)渐变到 COLORS.bg(深蓝灰黑 #14171C),实现警示橙到页面背景的自然过渡。这种渐变设计使头部与页面主体在视觉上融为一体,而非生硬的色块拼接。

九、各 Tab 布局深度分析

9.1 任务 Tab:完成率渐变大数字卡

任务 Tab 以完成率统计卡为核心视觉焦点,自上而下分为五段。

第一段是完成率统计卡,以 fontSize(46) 的大数字居中展示今日巡检完成率(百分比),数字使用 fontFamily('monospace') 等宽字体保证数字对齐,颜色为 COLORS.orange 警示橙。下方是橙绿渐变完成条(linearGradientCOLORS.orangeCOLORS.green),宽度随完成率百分比动态变化,底色为 COLORS.dark。再下方是三格统计(进行中/待复查/已逾期),分别使用橙、蓝、红三种语义色,每格通过 layoutWeight(1) 等分宽度,背景为 COLORS.dark 次级容器底色。

第二段是任务清单头,左侧标题"📋 今日巡检任务 · 7 项",右侧"+ 新增"入口通过 this.openAdd() 打开新建弹窗。新增入口使用 COLORS.dark 背景和 COLORS.orange 文字,视觉上与主色一致但通过背景色区分操作区域。

第三段是任务进度条清单,使用 ForEach 渲染 taskCard 构建器:

@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('编').fontSize(9).fontColor(COLORS.sub)
        .padding({ left: 7, right: 7, top: 3, bottom: 3 })
        .backgroundColor(COLORS.dark).borderRadius(7)
        .onClick(() => { this.openEdit(idx); })
      Text('删').fontSize(9).fontColor(COLORS.red)
        .padding({ left: 7, right: 7, top: 3, bottom: 3 })
        .backgroundColor(COLORS.dark).borderRadius(7)
        .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))
        .backgroundColor(COLORS.dark).borderRadius(4)
      Text(`${item.progress}%`).fontSize(10).fontFamily('monospace').fontColor(COLORS.sub)
    }.width('100%')
  }.width('100%').padding(12).backgroundColor(COLORS.card).borderRadius(12)
}

每张卡片包含楼宇徽标(如"1 号楼")、状态徽章(通过 taskStatusColor 函数根据状态动态映射颜色)、项目名(粗体,单行省略)、Progress 线性进度条(颜色随状态变化)和百分比数值(等宽字体)。右侧是"编"和"删"两个操作按钮——"编"使用 COLORS.sub 蓝灰文字,点击触发 openEdit 打开编辑弹窗;"删"使用 COLORS.red 警示红文字,点击触发 openDel 打开删除确认弹窗。

第四段是月度隐患柱状图:

@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)
      }, (val: number, idx: number) => val.toString() + '_' + idx.toString())
    }.width('100%').alignItems(VerticalAlign.Bottom)
  }.width('100%').padding(14).backgroundColor(COLORS.card).borderRadius(12)
}

使用 Column + ForEach 传统柱状实现,每根柱子通过 barHeight 函数计算高度——以 MONTH_HAZARD 值除以 MONTH_MAX 再乘以 96 为基准高度,通过 breath 布尔值控制奇偶柱交替乘以 1.06 或 0.94 实现呼吸波动:

barHeight(i: number): number {
  const base = MONTH_HAZARD[i] / MONTH_MAX * 96;
  const wave = (i % 2 === 0) === this.breath ? 1.06 : 0.94;
  return Math.max(8, Math.round(base * wave));
}

柱子使用 linearGradientCOLORS.orange(顶部)到 COLORS.orangeD(底部)竖向渐变,配合 alignItems(VerticalAlign.Bottom) 使所有柱子底部对齐。每根柱子上方显示数值,下方显示月份标签。

第五段是底部说明卡,说明完成率卡与进度条清单的数据同源关系,以及新增/编辑/删除任务走底部弹窗的操作路径。

9.2 相机 Tab:XComponent 取证预览与影随人动

相机 Tab 是 Camera Kit 特性的主舞台,采用"固定预览区 + 可滚动信息区"的分区布局。

预览区使用 XComponent 组件(type: XComponentType.SURFACE),通过 layoutWeight(1) 占满剩余高度,onLoad 回调将 surfaceReady 置为 true。预览区右下角叠加 sessionLabel(this.sessionMode) 文本标签,显示当前会话模式说明,背景为半透明黑色遮罩。XComponent 是 ArkUI 提供的用于嵌入原生渲染内容的组件,通过 XComponentController 管理其 Surface 生命周期,getXComponentSurfaceId() 返回的 SurfaceId 是相机预览输出的绑定关键。

模式切换行提供"开启影随人动"和"停止会话"两个操作。点击"开启影随人动"触发 startVideoMode 方法:

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; }
  try {
    const ctx = this.getUIContext().getHostContext();
    if (ctx === undefined || ctx === null) { this.framingState = '上下文未就绪'; return; }
    const manager = camera.getCameraManager(ctx);
    let device: camera.CameraDevice | undefined = undefined;
    for (const d of manager.getSupportedCameras()) {
      if (d.cameraPosition === camera.CameraPosition.CAMERA_POSITION_BACK) { device = d; break; }
    }
    if (device === undefined) { this.framingState = '未发现后摄'; return; }
    const capability = manager.getSupportedOutputCapability(device, camera.SceneMode.NORMAL_VIDEO);
    const profile = capability.previewProfiles.length > 0 ? capability.previewProfiles[0] : undefined;
    if (profile === undefined) { this.framingState = '无预览Profile'; return; }
    this.cameraInput = manager.createCameraInput(device);
    await this.cameraInput.open();
    this.previewOutput = manager.createPreviewOutput(profile, this.previewController.getXComponentSurfaceId());
    this.videoSession = manager.createSession<camera.VideoSession>(camera.SceneMode.NORMAL_VIDEO);
    this.videoSession.on('error', (err: BusinessError) => { console.error(`session error: ${err.code}`); });
    this.videoSession.beginConfig();
    this.videoSession.addInput(this.cameraInput);
    this.videoSession.addOutput(this.previewOutput);
    await this.videoSession.commitConfig();
    this.queryFraming(this.videoSession);
    await this.videoSession.start();
    this.sessionMode = 'video';
  } catch (e) {
    const err = e as BusinessError;
    this.framingState = `会话失败(${err.code})`;
    await this.releaseSession();
  }
}

该方法执行完整的相机管线初始化流程,每一步都设置了明确的状态反馈:检查 Surface 就绪 → 申请 CAMERA 权限 → 获取 CameraManager → 遍历 getSupportedCameras() 选中后摄 → 获取 NORMAL_VIDEO 场景的预览 Profile → 创建 CameraInput 并 open → 创建 PreviewOutput 绑定 SurfaceId → 创建 VideoSession → beginConfig/addInput/addOutput/commitConfig → 调用 queryFraming 执行能力链 → start。

queryFraming 方法是 AUTO_FRAMING 能力链的核心:

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

三步执行能力链:第一步 isControlCenterSupported() 判断设备是否支持控制中心,不支持时设置状态为"控制中心不支持"并返回;第二步 getSupportedEffectTypes() 获取已声明的效果类型列表,通过 includes(AUTO_FRAMING) 判断本机是否声明影随人动,未声明时设置状态为"AUTO_FRAMING 未声明"并返回;第三步 enableControlCenter(true) 请求系统接管构图,成功时设置状态为"影随人动已启用",异常时设置状态为"接管失败(错误码)"。三步形成层层递进的能力验证链,每一步的失败都有明确的状态反馈。

信息区还包含 ControlCenterEffectType 枚举表,展示 BEAUTY/PORTRAIT/AUTO_FRAMING 三种效果类型,当 info.type === 2 && this.framingSupported 时额外渲染"已声明"绿色徽章。

9.3 对焦 Tab:手动对焦三接口

对焦 Tab 演示 PhotoSession 的手动对焦能力,从上到下分为五个区块。

能力查询卡显示 isFocusDistanceSupported() 的查询结果和当前会话状态。点击"启动对焦会话"触发 switchToPhotoMode 方法——该方法与 startVideoMode 流程类似但创建 PhotoSession(场景为 NORMAL_PHOTO),会话启动后调用 queryFocusSupport 查询对焦能力:

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

三档预设使用 ForEach 渲染 FOCUS_PRESETS,当前选中的档位以橙色背景高亮,点击直接赋值 this.focusDistance = preset.distance

焦距滑杆卡使用 Slider 组件,范围 0~1 步进 0.01,onChange 回调实时更新 focusDistance 状态。滑杆下方通过 distanceLabel 函数显示当前距离对应的巡检景别文案。

应用按钮行触发 applyFocus 方法:

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

调用 setFocusDistance(this.focusDistance) 设置对焦距离,紧接着调用 getFocusDistance() 读回实际值,差值小于 0.01 判定"已生效",否则记录"读回偏差"。校验结果封装为 FocusRecordunshift 到记录列表头部,列表超过 20 条时通过 pop() 移除尾部最旧记录。无会话时记录"失败(无会话)“,异常时记录"失败(错误码)”。

对焦记录时间线使用嵌套 Scroll + ForEach 展示历史操作,每行显示时间戳、设置值、箭头、读回值和校验结论徽章,校验结论通过 focusOkColor 函数着色——已生效为绿、读回偏差为橙、失败为红。

9.4 频道 Tab:双层 Tabs 嵌套滚动

频道 Tab 是 nestedScroll 特性的演示场。顶部是模式切换 chips,通过 ForEach 遍历 [SELF_ONLY, SELF_FIRST] 两个枚举值,当前选中的模式以橙色背景高亮。模式切换行左侧显示当前嵌套模式的完整文案(如"嵌套模式:SELF_FIRST·先内后外"),右侧两个 chips 通过 modeShort 函数显示短文案。

中间是位置说明行,左侧橙色圆点标注外层楼宇当前位置(如"外层 1 号楼"),右侧蓝色圆点标注内层检查项当前位置和页码(如"内层 消防(第 1/5 页)")。

核心是外层宿主 Tabs

Tabs({ barPosition: BarPosition.Start }) {
  ForEach(OUTER_CHANNELS, (ch: ChannelItem) => {
    TabContent() {
      this.innerTabs(ch)
    }.tabBar(`${ch.icon} ${ch.name}`)
  }, (ch: ChannelItem) => 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(); }
})
.layoutWeight(1).width('100%')

外层 Tabs 使用 BarMode.Scrollable 横滑页签模式,5 个楼宇频道作为外层页签。每个 TabContent 内部调用 innerTabs 构建器渲染内层 Tabs。onChange 回调在外层页签切换时记录一条 SwipeLog,包含层级(“外层楼宇”)、页签名、起始与目标索引、事发时的嵌套模式和时间戳。

内层 Tabs 是 nestedScroll 的挂载点:

@Builder
innerTabs(channel: ChannelItem) {
  Tabs({ barPosition: BarPosition.Start }) {
    ForEach(INNER_TABS, (name: string) => {
      TabContent() {
        List({ space: 10 }) {
          ForEach(innerMockData(channel, name), (item: InnerCard) => {
            ListItem() {
              Column({ space: 6 }) {
                Row() {
                  Text(`${channel.icon} ${name}类检查`).fontSize(13)
                    .fontWeight(FontWeight.Bold).fontColor(COLORS.title)
                  Blank()
                  Text(item.tag).fontSize(10).fontColor(COLORS.sub)
                }.width('100%')
                Text(item.title).fontSize(12).fontColor(COLORS.sub).maxLines(1)
                  .textOverflow({ overflow: TextOverflow.Ellipsis })
                Text(item.desc).fontSize(11).fontColor(COLORS.text3).maxLines(2)
                  .textOverflow({ overflow: TextOverflow.Ellipsis })
                Row({ space: 8 }) {
                  Text(`${channel.name}责任区`).fontSize(9).fontColor(COLORS.orange)
                    .padding({ left: 5, right: 5, top: 1, bottom: 1 }).borderRadius(4)
                    .backgroundColor(COLORS.dark)
                  Text('拍照取证必填').fontSize(9).fontColor(COLORS.green)
                    .padding({ left: 5, right: 5, top: 1, bottom: 1 }).borderRadius(4)
                    .backgroundColor(COLORS.dark)
                }.width('100%')
              }.width('100%').padding(12).borderRadius(10).backgroundColor(COLORS.card)
            }
          }, (item: InnerCard) => item.id)
        }.width('100%').height('100%').scrollBar(BarState.Off)
      }.tabBar(name)
    }, (name: string) => 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)
  .layoutWeight(1).width('100%')
}

内层 Tabs 包含 5 个检查项子页签,每个子页签下是 8 条检查卡片的 List。每张检查卡片包含楼宇图标和检查类别标题、检查点标题、检查内容描述,以及两个标签徽章——“楼宇名责任区”(橙色文字)和"拍照取证必填"(绿色文字)。关键在于内层 Tabs 挂载了 .nestedScroll(this.nestedMode),这是 6.1.1 新特性——内层滑到边缘后是否联动外层楼宇由 nestedMode 决定。SELF_FIRST 模式下内层检查项列表滑到底部边缘时,继续滑动手势会自动触发外层楼宇频道的 Tab 切换。

9.5 日志 Tab:SwipeLog 时间轴

日志 Tab 以时间轴形式展示频道 Tab 产生的翻页事件。顶部是计数行和清空按钮,计数行显示"已记录 N 次翻页",清空按钮通过 clearLogs() 方法将 swipeLogs 清空。中间是图例行——外层楼宇橙色圆点标注"外层楼宇翻页",内层检查项蓝色圆点标注"内层检查项翻页",右侧显示当前嵌套模式完整文案。

列表区每行固定高度 72px,采用三列布局:左侧时间列(固定宽度 52px,等宽字体显示时间戳和 OUT/IN 层级标识),中间 3px 宽的竖线(外橙内蓝,半透明 0.6),右侧内容卡(层级徽章 + 页签名 + 索引变化箭头 + 模式说明)。竖线使用 Column().width(3).height('100%') 实现,在固定行高内填满,形成视觉上的时间轴效果。索引变化以箭头形式展示(如"0→1"),使用等宽字体保证对齐。

无记录时显示空态卡,引导用户去频道 Tab 操作——“去「频道」Tab 滑动内外层页签,SELF_FIRST 下内层滑到边缘会接力切换外层楼宇”。空态卡使用较大的上下 padding(40px)和居中对齐,视觉上引导用户转向频道 Tab。

9.6 告警 Tab:Notification Kit 全链路

告警 Tab 将 Notification Kit 的铃声生成与通知发布合并在单 Tab 内,分为五个区块。

区块一是异常上报表单:标题输入框、等级 chips(一般/严重/紧急,通过 levelColor 着色)、描述文本域和发布按钮。点击发布触发 submitAlarm 方法,组合标题、等级和描述后调用 publishNotice。空标题时阻止提交并记录失败日志。

区块二是通知授权状态卡:通过 isNotificationEnabled 查询授权状态,requestEnableNotification 申请授权(曾被拒绝返回 1600004 时通过 openNotificationSettings 拉起设置页引导手动开启):

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

区块三是告警铃声行:每条铃声显示名称、频率/时长/大小/沙箱状态,提供"生成"和"设默认"操作。点击"生成"调用 importRingsaveRingToSandboxbuildWavBytes 生成 WAV 并写入 EL1 沙箱:

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;
  try {
    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);
  } catch (e) {
    // 沙箱写入失败时忽略,发布通知时回退系统铃声
  }
  return path;
}

点击"设默认"调用 setCurrentRing,未导入沙箱时自动先导入。区块四是 sound 字段实时预览,通过 getSoundValue() 方法返回 'uri::' + fileUri.getUriFromPath(沙箱路径) 的完整值。

区块五是发布历史时间线,通过 ForEach 渲染 noticeRow,每行显示标题、正文和时间戳,失败记录标题使用警示红。

publishNotice 方法是 Notification Kit 的核心入口:

publishNotice(title: string, text: string) {
  if (this.currentRingIdx < 0 || this.currentRingIdx >= this.ringList.length) {
    this.addNoticeLog('发布失败', '铃声库为空,请先生成告警铃声');
    return;
  }
  const ring = this.ringList[this.currentRingIdx];
  if (!ring.inSandbox) { this.importRing(this.currentRingIdx); }
  const hostCtx = this.getUIContext().getHostContext() as common.UIAbilityContext;
  if (!hostCtx) {
    this.addNoticeLog(title, '宿主上下文获取失败,通知未发布');
    return;
  }
  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},请先开启通知授权`);
  });
}

取当前默认铃声(未导入沙箱时先自动导入)→ 获取宿主上下文并判空 → 设置 appCtx.area = contextConstant.AreaMode.EL1 切换到 EL1 沙箱区域 → 拼接 sandboxPath → 调用 fileUri.getUriFromPath 转换为 uri → 以 'uri::' + uri 格式填入 NotificationRequest.sound 字段 → 调用 notificationManager.publish 发布通知。发布成功记录一条 NoticeLog,失败时记录错误码并引导授权。

9.7 我的 Tab:巡检员渐变大卡

我的 Tab 顶部是巡检员渐变大卡,使用 linearGradient(angle 140,从 orangeDorange)实现橙色渐变背景,文字使用 COLORS.onMain 深暖黑保证对比度。卡片包含巡检员头像 Emoji(🧑‍🔧)、姓名(周正涛)、工号(PB-2041)、班次信息(金牌巡检员 · 白班 08:00-20:00),以及三格统计(本月点位 312/整改闭环 54/巡检里程 46.8km)和责任区说明(1-3 号楼 · 地下车库 B1/B2 · 配电房 · 消防泵房)。

下方是绩效清单行,通过 ForEach 渲染 PERF_ROWS,每行包含序号徽章、指标标签、备注说明和数值。前 3 项数值使用警示橙高亮(完成点位、闭环率、响应时长),后 3 项使用蓝灰弱化(里程、取证张数、安全天数),形成视觉重点梯度。奇偶行通过 idx % 2 === 0 ? COLORS.dark : COLORS.bg 交替背景色,增强列表可读性。

底部说明卡总结四大能力——“影随人动取证 / 手动对焦三档 / 楼宇频道嵌套滑动 / 沙箱铃声告警,四能力随时可复核”,提示巡检员该平台集成的全部核心特性。

十、底部 Tab 栏

底部 Tab 栏采用自绘单排设计,通过 ForEach 遍历 TAB_LIST 渲染 7 个 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 })
}

每个 Tab 项使用 layoutWeight(1) 等分宽度,上方 Emoji 图标 fontSize(17),下方文字标签 fontSize(9)。选中态文字使用 COLORS.tabOn(与主色 orange 一致),未选中态使用 COLORS.text3 暗蓝灰弱化。点击触发 switchTab 方法,该方法在离开相机/对焦页时自动释放会话资源。底部栏使用 COLORS.card 卡片底色,顶部 1px COLORS.line 分割线与内容区分隔。

这种自绘 Tab 栏设计相比系统 Tabs 组件的优势在于:完全可控的视觉样式(颜色、间距、字号),不受系统主题切换影响,且可以轻松适配 7 个 Tab 的单排排列。ForEach 的键值生成器使用 tab.label 作为唯一键,保证 Tab 切换时的高效 diff 更新。

十一、弹窗系统

弹窗系统采用 Stack 层叠 + 底部弹出的设计模式。modalOverlay 构建器是弹窗的统一入口,当 addModaleditModaldelModal 任一为真时渲染:

@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)
}

遮罩层使用 rgba(0,0,0,0.6) 半透黑背景,justifyContent(FlexAlign.End) 使弹窗面板从底部弹出。空白区域点击触发 onClose 回调(即 closeAllModals),关闭弹窗并复位所有表单状态。

11.1 panelAdd 新建巡检任务

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

包含楼宇输入框、项目名输入框和初始进度 Slider(步进 5),确认时调用 confirmAdd——通过 unshift 将新任务置顶到 taskList,进度 100% 自动设为"已完成"否则为"进行中"。楼宇和项目名为空时分别填充默认值"未分配楼宇"和"新增巡检点",防止空数据进入列表。

11.2 panelEdit 编辑巡检进度

@Builder
panelEdit(onClose: () => void) {
  Column({ space: 12 }) {
    Text('编辑巡检进度').fontSize(15).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
    Text(this.editIdx >= 0 && this.editIdx < this.taskList.length
      ? `${this.taskList[this.editIdx].building} · ${this.taskList[this.editIdx].item} · ${this.taskList[this.editIdx].status}`
      : '').fontSize(11).fontColor(COLORS.sub).maxLines(1)
      .textOverflow({ overflow: TextOverflow.Ellipsis }).width('100%')
    Column({ space: 6 }) {
      Row() {
        Text('巡检进度').fontSize(11).fontColor(COLORS.sub)
        Blank()
        Text(`${this.editProgress}%`).fontSize(13).fontFamily('monospace').fontColor(COLORS.orange)
      }.width('100%')
      Slider({ value: this.editProgress, min: 0, max: 100, step: 5 }).width('100%')
        .blockColor(COLORS.orange).trackColor(COLORS.dark).selectedColor(COLORS.orange)
        .onChange((value: number) => { this.editProgress = Math.round(value); })
    }.width('100%')
    Row({ space: 10 }) {
      Button('取消').fontSize(12).height(38).borderRadius(10)
        .fontColor(COLORS.sub).backgroundColor(COLORS.dark).layoutWeight(1)
        .onClick(() => { onClose(); })
      Button('保存').fontSize(12).height(38).borderRadius(10)
        .fontColor(COLORS.onMain).backgroundColor(COLORS.orange).layoutWeight(1)
        .onClick(() => { this.confirmEdit(); })
    }.width('100%')
  }.padding(16).borderRadius({ topLeft: 16, topRight: 16 })
  .backgroundColor(COLORS.card).width('100%')
}

显示当前任务的楼宇、项目名和状态,提供 0~100 步进 5 的进度 Slider,确认时调用 confirmEdit——更新进度值,100% 自动将状态置为"已完成",否则保持"进行中"。编辑弹窗通过 openEdit 方法打开时,editProgress 会带出当前任务的进度值,使 Slider 初始位置与当前进度一致。

11.3 panelDel 删除确认

@Builder
panelDel(onClose: () => void) {
  Column({ space: 12 }) {
    Text('删除巡检任务').fontSize(15).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
    Text(`确认将「${this.delIdx >= 0 && this.delIdx < this.taskList.length
      ? this.taskList[this.delIdx].building + ' · ' + this.taskList[this.delIdx].item : ''}」移出今日任务清单?删除后不可恢复。`)
      .fontSize(11).fontColor(COLORS.sub).width('100%')
    Row({ space: 10 }) {
      Button('取消').fontSize(12).height(38).borderRadius(10)
        .fontColor(COLORS.sub).backgroundColor(COLORS.dark).layoutWeight(1)
        .onClick(() => { onClose(); })
      Button('删除').fontSize(12).height(38).borderRadius(10)
        .fontColor(COLORS.onMain).backgroundColor(COLORS.red).layoutWeight(1)
        .onClick(() => { this.confirmDel(); })
    }.width('100%')
  }.padding(16).borderRadius({ topLeft: 16, topRight: 16 })
  .backgroundColor(COLORS.card).width('100%')
}

显示待删除任务的楼宇和项目名,确认时调用 confirmDel——通过 splice 从列表中移除任务。删除按钮使用 COLORS.red 警示红背景,视觉上与创建/保存的橙色按钮形成区分,通过颜色语义强化"危险操作"的警示。确认文案明确提示"删除后不可恢复",防止误操作。

三个弹窗面板均使用 borderRadius({ topLeft: 16, topRight: 16 }) 圆角顶部,COLORS.card 卡片底色,保证视觉一致。closeAllModals 方法统一复位所有弹窗标志和表单变量,防止状态残留导致下次打开弹窗时表单数据混乱。

十二、功能模块对比表

功能模块 核心技术 关键 API/装饰器 数据模型 交互特色
任务 Tab ArkUI 声明式渲染 ForEach/Progress/linearGradient TaskItem(@Observed) 渐变大数字+进度条清单+柱状图呼吸波动
相机 Tab Camera Kit VideoSession XComponent/startVideoMode/enableControlCenter AUTO_FRAMING 影随人动能力链三步
对焦 Tab Camera Kit PhotoSession isFocusDistanceSupported/setFocusDistance/getFocusDistance FocusRecord(@Observed) 三档预设+Slider+读回校验时间线
频道 Tab Tabs 嵌套滚动 nestedScroll(TabsNestedScrollMode)/onChange InnerCard(@Observed) 楼宇×检查项双层 25 矩阵+边缘接力
日志 Tab List 时间轴 ForEach/固定行高/竖线双色 SwipeLog(@Observed) 翻页事件状态时间轴+空态引导
告警 Tab Notification Kit buildWavBytes/saveRingToSandbox/publish RingItem/NoticeLog(@Observed) 正弦波生成+EL1 沙箱+uri::铃声链路
我的 Tab ArkUI 渐变卡片 linearGradient/ForEach PerfRow 巡检员渐变大卡+绩效清单梯度色
弹窗系统 Stack 层叠+底部弹出 modalOverlay/panelAdd/panelEdit/panelDel 三态弹窗+遮罩点击关闭+表单复位
头部 Banner 状态联动+呼吸动画 linearGradient/Circle/Timer Tab 副标题联动+四特性状态胶囊
底部 Tab 栏 自绘单排导航 ForEach/layoutWeight/switchTab TabMeta 等分 7 项+选中橙高亮+离开释放资源

十三、Camera Kit 双会话管线对比

Camera Kit 在本平台中提供了两种互斥的会话管线,分别服务于巡检取证的不同场景需求:

对比维度 VideoSession(影随人动) PhotoSession(手动对焦)
场景模式 NORMAL_VIDEO NORMAL_PHOTO
核心能力 AUTO_FRAMING 影随人动 手动对焦三接口
能力链入口 isControlCenterSupported isFocusDistanceSupported
能力验证 getSupportedEffectTypes + includes setFocusDistance + getFocusDistance
生效方式 enableControlCenter(true) 读回值差值 < 0.01 判生效
适用场景 巡查跟拍(人员始终居中) 取证拍照(铭牌/设备/全景三景别)
互斥切换 切换到 photo 前先 release 切换到 video 前先 release
状态反馈 framingState(5 种状态文案) focusRecords(时间线记录)

两种会话管线共享同一 XComponent Surface 和 cameraInput,但创建的 Session 类型不同。startVideoMode 创建 VideoSession 并在 commitConfig 后调用 queryFraming 执行 AUTO_FRAMING 能力链;switchToPhotoMode 创建 PhotoSession 并在 start 后调用 queryFocusSupport 查询对焦能力。两种会话通过 releaseSession 方法统一释放——先释放 Session(stop + release),再释放 PreviewOutput,最后关闭 CameraInput,五步释放链确保摄像头资源完全归还。

十四、Notification Kit 沙箱铃声链路详解

Notification Kit 的沙箱自定义铃声链路是本平台最复杂的技术链路,从音频生成到通知发布共经历七个步骤:

buildWavBytes
正弦波生成PCM

saveRingToSandbox
写入EL1沙箱

importRing
更新铃声状态

setCurrentRing
设为默认铃声

getSoundValue
拼接uri::前缀

publishNotice
发布通知

addNoticeLog
记录发布历史

第一步 buildWavBytes 以正弦波数学模型生成 16bit 单声道 PCM 音频数据,44 字节 WAV 文件头包含 RIFF/WAVE/fmt/data 四段格式信息,采样数据通过起音包络避免爆音、自然衰减模拟渐弱。第二步 saveRingToSandbox 将音频字节写入 EL1 沙箱的 filesDir 目录——必须设置 appCtx.area = contextConstant.AreaMode.EL1 切换到 EL1 沙箱区域,否则路径不可访问。第三步 importRing 更新铃声条目的 inSandbox 状态和文件大小。第四步 setCurrentRing 将铃声设为默认,未导入沙箱时自动先导入。第五步 getSoundValue 拼接 'uri::' + fileUri.getUriFromPath(沙箱路径) 格式的 sound 字段值。第六步 publishNotice 将 sound 字段填入 NotificationRequest 并调用 notificationManager.publish 发布通知。第七步 addNoticeLog 记录发布历史,成功或失败均记一条。

这条链路的关键创新在于 sound 字段的格式——原来只能传 rawfile 文件名(如 'ring.wav'),6.1.1 新特性支持沙箱 uri 格式('uri::' + fileUri.getUriFromPath(沙箱路径)),使得运行时动态生成的铃声也能作为通知提示音,打破了静态资源的限制。

十五、总结与展望

本平台以"守护巡查·物业安全巡检"为业务场景,在 HarmonyOS ArkUI 声明式框架下构建了一套完整的深色主题巡检管理界面。平台的技术价值体现在三个方面:

Camera Kit 的双会话能力链实现了巡检取证的全场景覆盖。VideoSession 的 AUTO_FRAMING 影随人动能力,让巡检人员在移动取证过程中始终保持在画面中心,解决了传统手持拍摄画面晃动、人物出画的问题。PhotoSession 的手动对焦三接口,实现了从铭牌微距到机房全景的精确对焦控制,并以读回校验机制确保对焦参数真实生效。两种会话通过互斥切换设计(释放 VideoSession 再建 PhotoSession)保证资源安全,switchTab 方法在离开相机/对焦页时自动触发 releaseSession,防止摄像头资源在后台被占用。

Notification Kit 的沙箱自定义铃声链路打破了通知铃声只能使用 rawfile 资源的限制。通过 buildWavBytes 函数以正弦波数学模型生成 16bit PCM 音频数据,写入 EL1 沙箱 filesDir 目录,再以 'uri::' + fileUri.getUriFromPath 格式填入 NotificationRequest.sound,实现了不同等级告警拥有差异化铃声的能力。这一链路从音频生成到沙箱写入再到 uri 转换完整闭环,为告警分级响应提供了声音层面的技术支撑。四种铃声覆盖疏散警报、消防长鸣、门禁提示和周界蜂鸣四种典型巡检告警场景,频率从 660Hz 到 1320Hz 跨越两个八度,满足不同紧迫程度的听觉警示需求。

Tabs 嵌套滚动通过 nestedScroll(TabsNestedScrollMode) 解决了多层级列表的交互割裂问题。SELF_FIRST 模式下内层检查项列表滑到边缘后自然接力到外层楼宇频道切换,实现了巡检路径在楼宇-检查项二维矩阵中的流畅流转。SwipeLog 时间轴完整记录了两层翻页事件的层级、索引变化和事发模式,为交互行为分析提供了数据基础。25 个检查矩阵 × 8 条检查卡片 = 200 个检查点位,每个点位都有行业语义化的检查要点,使平台内容真实可信而非空洞的占位数据。

展望未来,本平台可在以下方向深化扩展:一是引入 AI 图像识别能力,将 Camera Kit 的取证影像与隐患识别模型结合,实现拍照即报的智能巡检——巡检员拍摄配电柜后,AI 自动识别温度异常区域并标注隐患等级;二是接入 IoT 传感器数据流,将静态巡检任务升级为实时监测告警——喷淋管网压力、配电柜母排温度等指标通过传感器实时上传,超过阈值时自动触发 Notification Kit 告警链路;三是利用分布式能力实现多巡检员的跨设备协同,构建区域级物业安全巡检网络——巡检员的任务进度、对焦记录和告警历史通过分布式数据同步在团队成员间共享,实现巡检任务的智能分配和协同处置。随着 HarmonyOS 生态的持续演进,ArkUI 声明式框架与 Camera Kit、Notification Kit 等系统级能力的深度融合,将为智慧物业管理带来更丰富的技术可能性。

附录: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.1 Release ✅ 已安装

界面顶部提示:“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 24 6.1.1.100 Release ✅ 已安装
API Version 23 6.1.0.28 Beta1 未安装
API Version 22 6.0.2.112 Release 未安装

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

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

在这里插入图片描述


三、小结

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

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


Logo

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

更多推荐