警示橙如何驱动巡检闭环?ArkUI 物业安全平台的声明式实现

一、技术前言

在智慧物业管理赛道,安全巡检始终是建筑运行保障的第一道防线。从消防通道堆物复查到配电间红外测温,从喷淋管网压力巡检到水泵启动试运行测试,从应急照明断电测试到电梯机房月度点检------每一项巡检任务都要求精确的流程管控、清晰的进度追踪和即时的异常告警能力。传统巡检应用长期面临三大痛点:取证影像模糊导致责任争议频发、告警铃声单一导致值班人员响应迟缓、巡检路径跨楼宇跨楼层导致界面交互割裂。

HarmonyOS ArkUI 框架以其声明式 UI 范式为上述痛点提供了系统级的解决方案。ArkUI 基于 TypeScript 扩展出的 ArkTS 语言,通过 @Component 装饰器封装可复用的组件单元,通过 @State@Observed@Link 等状态管理装饰器实现数据驱动的自动渲染,通过 @Builder 方法将复杂的 UI 结构拆分为可组合、可嵌套的构建块。这种组件化架构天然契合巡检场景中"数据---视图---交互"紧耦合的需求:每一条巡检任务、每一次对焦记录、每一条翻页日志都可以建模为独立的数据实体,驱动对应的 UI 片段自动更新,开发者无需手动操作 DOM 节点。

本平台深度融合了 HarmonyOS 6.1.1 的三大前沿特性。Camera Kit 提供了 VideoSession 的 AUTO_FRAMING(影随人动)能力链------通过 isControlCenterSupported 判断控制中心可用性、getSupportedEffectTypes 查询本机声明的效果类型集合、enableControlCenter(true) 请求系统接管构图,三步实现巡检取景时巡查人员始终居中;同时 PhotoSession 的手动对焦三接口 isFocusDistanceSupportedsetFocusDistancegetFocusDistance 实现从铭牌近拍到机房全景的精确对焦控制。Notification Kit 实现了沙箱自定义铃声链路------通过 buildWavBytes 生成正弦波 PCM 音频字节流,写入 EL1 沙箱 filesDir 目录,再以 'uri::' + fileUri.getUriFromPath(沙箱路径) 拼接填入 NotificationRequest.sound 字段,让一般、严重、紧急三个等级的告警拥有差异化铃声。Tabs 嵌套滚动 通过 nestedScroll(TabsNestedScrollMode) 让内层检查项列表滑到边缘后联动外层楼宇频道,实现巡检路径从楼宇到检查项的自然流转。

二、整体架构流程图

#mermaid-svg-SBp9cCoX3Atb6sgd{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-SBp9cCoX3Atb6sgd .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-SBp9cCoX3Atb6sgd .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-SBp9cCoX3Atb6sgd .error-icon{fill:#552222;}#mermaid-svg-SBp9cCoX3Atb6sgd .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-SBp9cCoX3Atb6sgd .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-SBp9cCoX3Atb6sgd .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-SBp9cCoX3Atb6sgd .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-SBp9cCoX3Atb6sgd .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-SBp9cCoX3Atb6sgd .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-SBp9cCoX3Atb6sgd .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-SBp9cCoX3Atb6sgd .marker{fill:#333333;stroke:#333333;}#mermaid-svg-SBp9cCoX3Atb6sgd .marker.cross{stroke:#333333;}#mermaid-svg-SBp9cCoX3Atb6sgd svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-SBp9cCoX3Atb6sgd p{margin:0;}#mermaid-svg-SBp9cCoX3Atb6sgd .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-SBp9cCoX3Atb6sgd .cluster-label text{fill:#333;}#mermaid-svg-SBp9cCoX3Atb6sgd .cluster-label span{color:#333;}#mermaid-svg-SBp9cCoX3Atb6sgd .cluster-label span p{background-color:transparent;}#mermaid-svg-SBp9cCoX3Atb6sgd .label text,#mermaid-svg-SBp9cCoX3Atb6sgd span{fill:#333;color:#333;}#mermaid-svg-SBp9cCoX3Atb6sgd .node rect,#mermaid-svg-SBp9cCoX3Atb6sgd .node circle,#mermaid-svg-SBp9cCoX3Atb6sgd .node ellipse,#mermaid-svg-SBp9cCoX3Atb6sgd .node polygon,#mermaid-svg-SBp9cCoX3Atb6sgd .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-SBp9cCoX3Atb6sgd .rough-node .label text,#mermaid-svg-SBp9cCoX3Atb6sgd .node .label text,#mermaid-svg-SBp9cCoX3Atb6sgd .image-shape .label,#mermaid-svg-SBp9cCoX3Atb6sgd .icon-shape .label{text-anchor:middle;}#mermaid-svg-SBp9cCoX3Atb6sgd .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-SBp9cCoX3Atb6sgd .rough-node .label,#mermaid-svg-SBp9cCoX3Atb6sgd .node .label,#mermaid-svg-SBp9cCoX3Atb6sgd .image-shape .label,#mermaid-svg-SBp9cCoX3Atb6sgd .icon-shape .label{text-align:center;}#mermaid-svg-SBp9cCoX3Atb6sgd .node.clickable{cursor:pointer;}#mermaid-svg-SBp9cCoX3Atb6sgd .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-SBp9cCoX3Atb6sgd .arrowheadPath{fill:#333333;}#mermaid-svg-SBp9cCoX3Atb6sgd .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-SBp9cCoX3Atb6sgd .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-SBp9cCoX3Atb6sgd .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-SBp9cCoX3Atb6sgd .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-SBp9cCoX3Atb6sgd .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-SBp9cCoX3Atb6sgd .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-SBp9cCoX3Atb6sgd .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-SBp9cCoX3Atb6sgd .cluster text{fill:#333;}#mermaid-svg-SBp9cCoX3Atb6sgd .cluster span{color:#333;}#mermaid-svg-SBp9cCoX3Atb6sgd div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-SBp9cCoX3Atb6sgd .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-SBp9cCoX3Atb6sgd rect.text{fill:none;stroke-width:0;}#mermaid-svg-SBp9cCoX3Atb6sgd .icon-shape,#mermaid-svg-SBp9cCoX3Atb6sgd .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-SBp9cCoX3Atb6sgd .icon-shape p,#mermaid-svg-SBp9cCoX3Atb6sgd .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-SBp9cCoX3Atb6sgd .icon-shape .label rect,#mermaid-svg-SBp9cCoX3Atb6sgd .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-SBp9cCoX3Atb6sgd .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-SBp9cCoX3Atb6sgd .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-SBp9cCoX3Atb6sgd :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Page1221 主组件
headerBanner 头部渐变 Banner
内容区 7 Tab 切换
tabBar 底部导航
modalOverlay 弹窗遮罩
Tab0 任务

完成率统计+进度条清单+月度柱状图
Tab1 相机

XComponent预览+影随人动能力链
Tab2 对焦

能力查询+三档预设+Slider+读回校验
Tab3 频道

楼宇×检查项双层Tabs嵌套滚动
Tab4 日志

nestedScroll翻页时间轴
Tab5 告警

上报表单+授权卡+铃声库+发布历史
Tab6 我的

巡检员渐变大卡+绩效清单
Camera Kit

AUTO_FRAMING影随人动
Camera Kit

手动对焦三接口
Tabs嵌套滚动

nestedScroll模式
Notification Kit

沙箱自定义铃声
panelAdd 新建巡检任务
panelEdit 编辑巡检进度
panelDel 删除确认

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

三、色彩体系设计

3.1 ColorPalette 接口定义

平台采用深色安全蓝灰主题,通过 ColorPalette 接口集中声明全部颜色字段。接口设计的核心理念是"语义即颜色"------每个字段名直接对应其在业务中的语义角色,而非抽象的色名,这样开发者在引用时能立即理解该颜色所代表的业务含义。

typescript 复制代码
interface ColorPalette {
  bg: string;      // 页面背景(安全蓝灰黑)
  card: string;    // 卡片底色(深蓝灰)
  dark: 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;  // 橙底文字色(深暖黑)
}

接口中一共定义了十五个颜色字段。值得注意的是 dark 字段在接口定义的注释块中并未列出,但在实际常量赋值中被使用,这是为统计格、进度条底等次级容器预留的过渡色。onMain 字段专门为橙色背景上的文字设计,采用深暖黑色调,保证在警示橙底色上的可读性对比度。

3.2 COLORS 常量逐色分析

typescript 复制代码
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'
};

下面对每一种颜色进行逐一解读。

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' 是分割线色,低对比度不干扰内容。tabOnorange 值相同,保持 Tab 选中态与主色一致。mask: 'rgba(0,0,0,0.6)' 是半透黑遮罩,用于弹窗背景。onMain: '#231507' 是橙底深字色,保证按钮文字对比度。

色彩设计遵循"安全警示"原则:橙、绿、蓝、红四色分别对应"巡检中/已合格/信息参考/危险警告"四种语义状态,使巡检员在深色环境下凭颜色即可快速识别任务优先级。

四、Tab 元数据与辅助数据

4.1 底部导航 Tab 定义

typescript 复制代码
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 项布局。TabMeta 接口仅包含 iconlabel 两个字段,使用 Emoji 作为图标避免了图片资源依赖。七个 Tab 按巡检工作流排列:任务总览 → 相机取证 → 对焦校准 → 频道浏览 → 日志回溯 → 告警上报 → 个人中心,覆盖了巡检员从接单到归档的完整闭环。

4.2 Camera Kit 效果类型与对焦预设

typescript 复制代码
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 的三种效果类型。其中 AUTO_FRAMING(type=2)是 6.1.1 新增的影随人动能力,也是本平台相机 Tab 的核心演示目标。通过 getSupportedEffectTypes() 返回的数组 includes 判断本机是否声明该能力。

typescript 复制代码
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 · 机房全景' }
];

FOCUS_PRESETS 定义了三个巡检取证景别。对焦距离取值范围 0.0(最近)到 1.0(最远),0.1 用于铭牌近拍以辨认设备序列号,0.5 用于配电柜整柜巡检,0.9 用于机房全景与疏散通道。这三档预设覆盖了巡检取证最典型的三种距离场景。

4.3 楼宇频道与检查项数据

typescript 复制代码
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 嵌套后形成 5×5=25 个组合的检查矩阵,每个组合下再生成 8 条检查卡片,总计 200 条检查内容,足以演示嵌套滚动的效果。

4.4 月度隐患数据与绩效清单

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

月度隐患柱状图数据展示了近 6 个月的隐患发现数(单位:处)。MONTH_MAX 设为 16 作为柱状图满高基准,所有柱高按 MONTH_HAZARD[i] / MONTH_MAX 换算。

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

绩效清单行定义了六项指标,前三项用橙色高亮(核心绩效),后三项用副标题色弱化。每行包含标签、数值和补充说明三个字段,信息密度高但不显拥挤。

五、工具函数分析

5.1 时间与模式标签函数

typescript 复制代码
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 函数返回当前时刻的 HH:mm:ss 格式字符串。它在 SwipeLog(翻页日志)、FocusRecord(对焦记录)、NoticeLog(通知历史)三处统一使用,保证全平台时间戳格式一致。padStart(2, '0') 确保个位数时补零。

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

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

modeLabelmodeShort 两个函数分别提供嵌套滚动模式的完整文案和短文案。SELF_FIRST 表示内层先消费滚动,滑到边缘后接力给外层;SELF_ONLY 表示仅内层消费,不联动外层。短文案用于切换 chips,完整文案用于日志记录。

5.2 状态颜色映射函数

平台定义了五个状态→颜色的映射函数,每个函数根据不同的业务状态值返回对应的主题色。

typescript 复制代码
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 将影随人动的状态字符串映射为颜色:已启用为绿、能力缺失为蓝、失败为红、其他默认为暗蓝灰。这种基于字符串包含判断的方式虽然不如枚举严谨,但在快速原型开发中足够灵活。

distanceLabel 函数将对焦距离数值映射为巡检景别文案,以 0.3 和 0.7 为分界点划分近拍、中距、远距三档。focusOkColor 将对焦校验结果映射为颜色:已生效为绿、失败为红、偏差为橙。taskStatusColor 将任务状态映射为颜色:已完成为绿、进行中为橙、待复查为蓝、已逾期为红。levelColor 将告警等级映射为颜色:一般为蓝、严重为橙、紧急为红。

5.3 WAV 音频生成函数

typescript 复制代码
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)); }
  };
  writeStr(0, 'RIFF');
  view.setUint32(4, 36 + dataSize, true);
  writeStr(8, 'WAVE');
  writeStr(12, 'fmt ');
  view.setUint32(16, 16, true);
  view.setUint16(20, 1, true);
  view.setUint16(22, 1, true);
  view.setUint32(24, sampleRate, true);
  view.setUint32(28, sampleRate * 2, true);
  view.setUint16(32, 2, true);
  view.setUint16(34, 16, true);
  writeStr(36, 'data');
  view.setUint32(40, dataSize, true);
  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 沙箱铃声链路的起点。它生成一个标准 WAV 文件格式的 ArrayBuffer:44 字节的 WAV 头部 + 16bit 单声道 PCM 数据。头部按 RIFF/WAVE/fmt /data 标准结构写入,数据区为正弦波采样。每个采样点叠加了起音包络(env,前 20ms 线性上升避免爆音)和自然衰减(decay,随时间线性衰减至零),最终振幅限制在 32767(16bit 有符号最大值)的 50%。这种生成的铃声音质虽不如专业音频文件,但足以区分不同频率的告警铃声。

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

wavSizeText 根据时长估算 WAV 文件大小,用于铃声列表展示。

5.4 检查要点池函数

typescript 复制代码
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 函数是一个嵌套的三元表达式查找表。它根据检查项类别名(消防/电气/管道/通道/监控)从对应的 8 条行业语义要点池中取第 i 项。这些要点来自真实物业巡检场景,如"灭火器压力表指针""配电柜母排温度""喷淋管网压力表"等,增强了内容的专业性和可信度。

六、数据模型层

6.1 TaskItem 巡检任务实体

typescript 复制代码
@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;
  }
}

TaskItem 是进度条清单的主体实体,也是弹窗新增/编辑/删除操作的绑定对象。@Observed 装饰器使其属性变化能被 ArkUI 框架感知------当 progressstatus 在编辑弹窗中被修改后,任务清单列表中的对应卡片会自动刷新。building 存储楼宇/分区名,item 存储巡检项目名,progress 为 0~100 的进度值,status 为已完成/进行中/待复查/已逾期四态之一。

种子数据 TASK_LIST 包含七条巡检任务,覆盖了消防通道复查、配电间测温、喷淋管网巡检、水泵试运行等典型场景,进度从 0% 到 100% 分布,状态涵盖全部四种。

6.2 FocusRecord 对焦记录实体

typescript 复制代码
@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 → 校验结论 ok。构造函数自动以 nowTime() 填充时间戳。readback 为 -1 表示调用 getFocusDistance 失败。校验逻辑在 applyFocus 方法中实现:差值小于 0.01 判为"已生效",否则为"读回偏差"。

6.3 InnerCard 内层检查项卡片

typescript 复制代码
@Observed export class InnerCard {
  id: string;
  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 是频道 Tab 中楼宇×检查项矩阵的内容载体。id 作为 ForEach 的键保证列表高效更新,tag 存储检查项类别名,titledesc 分别为检查点标题和描述。innerMockData 生成器为每个楼宇×检查项组合生成 8 条卡片,保证内容超出一屏以触发嵌套滚动。

6.4 SwipeLog 翻页日志实体

typescript 复制代码
@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) {
    this.layer = layer; this.tabName = tabName; this.fromIdx = fromIdx;
    this.toIdx = toIdx; this.mode = mode; this.time = nowTime();
  }
}

SwipeLog 记录两层翻页事件:layer 区分外层楼宇和内层检查项,fromIdxtoIdx 记录切换前后的索引,mode 记录事发时的嵌套模式(modeLabel 结果)。这是嵌套滚动特性的可视化证据------在 SELF_FIRST 模式下,内层滑到边缘继续滑动会触发外层 onChange,此时日志中会出现 layer='外层楼宇' 的记录。

6.5 RingItem 与 NoticeLog

typescript 复制代码
@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) {
    this.name = name; this.file = file; this.freq = freq;
    this.duration = duration; this.size = size; this.inSandbox = inSandbox;
  }
}

RingItem 是告警铃声条目实体。freqduration 用于 buildWavBytes 生成音频,inSandbox 标记是否已写入 EL1 沙箱,size 在写入沙箱后更新为实际文件大小。种子数据包含四条铃声:疏散警报(880Hz)、消防长鸣(660Hz)、门禁提示(1046Hz)、周界蜂鸣(1320Hz),频率差异使每种铃声听感分明。

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

NoticeLog 记录通知发布历史,无论成功或失败均记一条,最新在前,最多保留 8 条。失败记录的 title 包含"失败"二字,在 UI 中会被标红显示。

七、组件主体结构

7.1 @State 状态变量群

Page1221 组件声明了大量状态变量,按功能分为五组。

Tab 状态组currentTab 控制当前展示的 Tab 索引,初始为 0(任务页)。

弹窗状态组addModaleditModaldelModal 三个布尔值分别控制新建、编辑、删除三种弹窗的显示;editIdxdelIdx 记录当前操作的列表索引。

动画状态组breath 布尔值每秒翻转一次,驱动呼吸圆点透明度和柱状图波动;timer 存储定时器 ID。

Camera Kit 成员组previewController 是 XComponent 的控制器实例,cameraInputpreviewOutputvideoSessionphotoSession 四个相机核心对象声明为 private(不参与渲染)。状态变量包括 surfaceReady(Surface 就绪标志)、sessionMode(idle/video/photo)、framingState(影随人动状态文案)、framingSupported(本机是否声明 AUTO_FRAMING)、focusSupported(是否支持设置对焦距离)、focusDistance(当前对焦距离 0.0~1.0)、focusRecords(对焦记录数组)、permState(CAMERA 权限状态)。

Notification Kit 成员组granted(通知授权状态)、notifyId(通知 ID 自增管理)、ringList(铃声库)、currentRingIdx(当前默认铃声索引)、noticeLogs(发布历史)、alarmTitle/alarmLevel/alarmDesc(上报表单三字段)。

Tabs 嵌套滚动成员组nestedMode(嵌套模式枚举)、outerIndex(外层楼宇索引)、innerIndex(内层检查项索引)、swipeLogs(翻页日志数组,封顶 40 条)。

7.2 生命周期方法

typescript 复制代码
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 在组件即将出现时执行三件事:异步查询通知授权状态、灌入三条种子对焦记录(0.9 已生效、0.5 已生效、0.1 读回偏差)、启动每秒翻转的呼吸动画定时器。aboutToDisappear 在组件即将消失时清理定时器并释放相机资源,防止后台占用摄像头。

7.3 build() 根构建

typescript 复制代码
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、内容区(layoutWeight(1) 占满中间剩余空间)、底部 Tab 栏。内容区通过 if-else if-else 链根据 currentTab 索引切换 7 个 @Builder 方法。顶层是条件渲染的弹窗遮罩层------仅当三个弹窗标志之一为 true 时才渲染 modalOverlay

八、头部区域详解

头部 Banner 是全平台的视觉锚点,采用警示橙到蓝灰黑的 linearGradient 渐变背景。

typescript 复制代码
@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%')

头部第一行是标题区和呼吸圆点。主标题固定为"物业安巡 · 物业安全巡检",使用 onMain 深暖黑色保证在橙底上的对比度。副标题根据 currentTab 动态切换,实现 Tab 联动------切换到任务页显示今日巡检项数,切换到相机页显示"影随人动取证预览",以此类推。右侧呼吸圆点通过 breath 布尔值在 0.9 和 0.45 透明度间切换,每秒闪烁一次,表示系统活跃。

头部第二行是四个状态胶囊,横向排列。

任务胶囊显示当前巡检项总数。相机胶囊用一个 6px 的圆点表示会话状态:idle 时暗灰、video 时绿、photo 时橙。通知胶囊用圆点表示授权状态:已授权绿、未授权红。嵌套胶囊用圆点表示嵌套模式:SELF_FIRST 蓝、SELF_ONLY 橙。四个胶囊让巡检员一眼掌握平台三大特性的运行状态。

渐变配置 linearGradient({ angle: 160, colors: [[COLORS.orangeD, 0], [COLORS.bg, 1]] }) 从左上 160 度角的深橙过渡到右下的蓝灰黑背景,形成自然的视觉收束。

九、任务 Tab 深度分析

任务 Tab 是默认首页,由四个区块组成:完成率统计卡、任务清单头、进度条清单、月度隐患柱状图。

9.1 完成率统计卡

typescript 复制代码
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%')

统计卡左侧用一个 46px 等宽字体的大数字展示完成率百分比,橙色主色使其成为视觉焦点。右侧是标题和说明文字,提及"数据每 30 分钟同步工单系统"增强专业感。

下方是渐变完成条,从橙色到绿色的 linearGradient 渐变直观表达进度------橙色代表进行中、绿色代表已完成。再下方是三格统计区,分别用橙、蓝、红三色展示进行中、待复查、已逾期的任务数,三种颜色与状态语义一一对应。

9.2 任务清单与进度条卡片

typescript 复制代码
@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)
}

每张任务卡片分三层。顶部行是楼宇徽标和状态徽章,右侧是"编"和"删"两个操作按钮。中间是巡检项目名(单行省略)。底部是线性进度条和百分比数值,进度条颜色随状态变化:已完成绿、进行中橙、待复查蓝、已逾期红。Progress 组件是 ArkUI 内置的进度指示器,type: ProgressType.Linear 渲染为水平条形。

9.3 月度隐患柱状图

柱状图采用 Column + ForEach 的传统方式实现,而非使用图表库。

typescript 复制代码
@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 组件,高度由 barHeight(idx) 方法计算。柱体使用从橙色到深橙的 180 度渐变。barHeight 方法中加入了呼吸动画效果:奇偶柱在 breath 翻转时交替放大 1.06 倍和缩小 0.94 倍,使柱状图呈现微妙的波动感。

typescript 复制代码
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));
}

十、相机 Tab 深度分析

相机 Tab 是 Camera Kit 影随人动特性的主舞台,由授权卡、XComponent 预览、模式切换行、能力链状态卡、效果枚举表五个区块组成。

10.1 XComponent 取证预览

typescript 复制代码
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)

XComponent 是 ArkUI 的原生组件渲染容器,type: XComponentType.SURFACE 创建一个 Surface 供相机预览输出挂载。onLoad 回调在 Surface 创建完毕后将 surfaceReady 置为 true------这是启动会话的前置条件。右下角叠浮一个会话模式标签,通过 sessionLabel 函数将 idle/video/photo 映射为中文说明。XComponent 必须设置 layoutWeight(1) 以占满剩余高度,否则预览面将无法正确渲染。

10.2 影随人动能力链

影随人动是本平台的核心特性,通过三步能力链实现。

typescript 复制代码
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';
}

startVideoMode 方法是影随人动的入口。它依次检查 Surface 就绪状态、避免重复进入 video 模式、互斥释放 photo 会话、动态申请 CAMERA 权限。权限通过后,通过 camera.getCameraManager(ctx) 获取相机管理器,筛选后摄(CAMERA_POSITION_BACK),创建 CameraInput 并 open,创建 PreviewOutput 绑定到 XComponent 的 SurfaceId,创建 VideoSession 并 beginConfigaddInputaddOutputcommitConfig。配置提交后调用 queryFraming 执行能力链三步,最后 start() 启动会话。

typescript 复制代码
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})`;
  }
}

queryFraming 是能力链的核心。第一步 isControlCenterSupported 判断本机是否支持控制中心;第二步 getSupportedEffectTypes 获取声明的效果类型数组,用 includes 判断是否包含 AUTO_FRAMING;第三步 enableControlCenter(true) 请求系统接管构图。三步任一失败都会设置对应的状态文案,UI 通过 framingStateColor 函数映射为对应颜色展示。

10.3 效果枚举表

效果枚举表用 ForEach 渲染 EFFECT_INFOS 的三行数据。每行展示类型值、枚举名、描述,当 type=2 且 framingSupported 为 true 时额外显示"已声明"绿色标签。这让开发者直观看到 ControlCenterEffectType 的完整枚举及本机声明情况。

十一、对焦 Tab 深度分析

对焦 Tab 展示 Camera Kit 的手动对焦三接口能力,由能力查询卡、三档预设、焦距滑杆、应用按钮行、对焦记录时间线五个区块组成。

11.1 会话切换与能力查询

typescript 复制代码
async switchToPhotoMode() {
  if (this.sessionMode === 'photo') { return; }
  if (this.sessionMode === 'video') { await this.releaseSession(); }
  // ... 创建 PhotoSession ...
  this.queryFocusSupport();
}

手动对焦能力仅挂在 PhotoSession 上,因此进入对焦 Tab 需要先释放可能存在的 VideoSession,再创建 PhotoSession。创建流程与 VideoSession 类似,但 SceneMode 改为 NORMAL_PHOTO。会话启动后立即调用 queryFocusSupport 查询能力。

typescript 复制代码
queryFocusSupport() {
  if (this.photoSession === undefined) { this.focusSupported = false; return; }
  try {
    this.focusSupported = this.photoSession.isFocusDistanceSupported();
  } catch (e) {
    this.focusSupported = false;
  }
}

isFocusDistanceSupported 是同步方法,返回布尔值表示当前 PhotoSession 是否支持设置对焦距离。异常时降级为"不支持"。

11.2 设置与读回校验

typescript 复制代码
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 末尾。这种设置→读回→校验的三步模式确保对焦指令确实被硬件执行,而非静默失败。

11.3 对焦记录时间线

时间线以滚动列表展示所有对焦记录,每行包含时间戳、设置值、箭头、读回值和校验结论徽章。校验结论通过 focusOkColor 映射颜色:已生效绿、偏差橙、失败红。时间线让巡检员追溯每次对焦操作的完整链路,是取证溯源的关键证据。

十二、频道 Tab 深度分析

频道 Tab 是 Tabs 嵌套滚动特性的演示场,由模式切换 chips、双层位置说明行、外层楼宇 Tabs 组成。

12.1 嵌套模式切换

typescript 复制代码
Row({ space: 8 }) {
  Text(`嵌套模式:${modeLabel(this.nestedMode)}`)
    .fontSize(11).fontColor(COLORS.sub).layoutWeight(1)
  ForEach([TabsNestedScrollMode.SELF_ONLY, TabsNestedScrollMode.SELF_FIRST],
    (m: TabsNestedScrollMode) => {
      Text(modeShort(m)).fontSize(10)
        .padding({ left: 10, right: 10, top: 5, bottom: 5 }).borderRadius(12)
        .fontColor(this.nestedMode === m ? COLORS.onMain : COLORS.text3)
        .backgroundColor(this.nestedMode === m ? COLORS.orange : COLORS.card)
        .onClick(() => { this.nestedMode = m; })
    }, (m: TabsNestedScrollMode) => `mode_${m}`)
}.width('100%')

两个 chips 让用户在 SELF_ONLY(仅内层)和 SELF_FIRST(先内后外)间切换。选中态为橙底深字,未选中为暗灰底。切换后 nestedMode 变化会触发内层 Tabs 的 nestedScroll 属性更新。

12.2 双层 Tabs 嵌套

外层 Tabs 有 5 个楼宇频道页签,barMode(BarMode.Scrollable) 使页签可横向滑动。每个 TabContent 内部挂载 innerTabs Builder。

typescript 复制代码
@Builder
innerTabs(channel: ChannelItem) {
  Tabs({ barPosition: BarPosition.Start }) {
    ForEach(INNER_TABS, (name: string) => {
      TabContent() {
        List({ space: 10 }) {
          ForEach(innerMockData(channel, name), (item: InnerCard) => {
            ListItem() {
              // 检查项卡片
            }
          }, (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 个检查项子页签,每个 TabContent 内是一个 List,包含 8 条 innerMockData 生成的检查卡片。.nestedScroll(this.nestedMode) 是关键一行------它将嵌套滚动模式挂载到内层 Tabs 上。在 SELF_FIRST 模式下,内层列表滑到边缘继续滑动会联动外层楼宇频道切换;在 SELF_ONLY 模式下则不会联动。两层 Tabs 的 onChange 都会 unshift 一条 SwipeLog 记录,形成翻页事件的完整时间轴。

十三、日志 Tab 深度分析

日志 Tab 以时间轴形式展示 swipeLogs 数组中的所有翻页记录。

13.1 空态处理

swipeLogs 为空时,展示一张引导卡,提示用户去频道 Tab 滑动内外层页签以产生日志。空态设计避免了空白页面的尴尬,同时引导用户发现嵌套滚动特性。

13.2 时间轴行结构

typescript 复制代码
ListItem() {
  Row({ space: 10 }) {
    Column({ space: 4 }) {
      Text(log.time).fontSize(11).fontFamily('monospace').fontColor(COLORS.sub)
      Text(log.layer === '外层楼宇' ? 'OUT' : 'IN').fontSize(8)
        .fontColor(log.layer === '外层楼宇' ? COLORS.orange : COLORS.blue)
    }.width(52).height('100%').alignItems(HorizontalAlign.Start)
    Column().width(3).height('100%').borderRadius(2)
      .backgroundColor(log.layer === '外层楼宇' ? COLORS.orange : COLORS.blue).opacity(0.6)
    Column({ space: 4 }) {
      Row({ space: 6 }) {
        Text(log.layer === '外层楼宇' ? '外层楼宇' : '内层检查项').fontSize(9)
          .fontColor(log.layer === '外层楼宇' ? COLORS.orange : COLORS.blue)
        Text(log.tabName).fontSize(12).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
        Text(`${log.fromIdx}→${log.toIdx}`).fontSize(11).fontFamily('monospace').fontColor(COLORS.sub)
      }.width('100%')
      Text(`${log.mode} · ${log.time}`).fontSize(10).fontColor(COLORS.text3)
    }.layoutWeight(1).height('100%').justifyContent(FlexAlign.Center)
  }.width('100%').height(72)
}.margin({ bottom: 6 })

每行固定高度 72px,分三列:左侧时间列(52px 宽,含时间戳和 OUT/IN 标签)、中间竖线(3px 宽,外层橙内层蓝)、右侧内容卡(layoutWeight 占满剩余)。双色竖线是时间轴的视觉主线,橙蓝交替让外层和内层翻页一目了然。内容卡显示层级徽标、切换到的页签名、索引变化(如 0→1)以及事发时的嵌套模式。

十四、告警 Tab 深度分析

告警 Tab 是 Notification Kit 沙箱铃声特性的完整演示场,由上报表单、授权卡、铃声库、sound 预览、发布历史五个区块组成。

14.1 异常上报表单

表单包含标题输入框、等级 chips(一般/严重/紧急)、描述文本域和发布按钮。等级 chips 用 levelColor 函数映射颜色,选中态为对应等级色底深字。点击发布按钮调用 submitAlarm 方法。

typescript 复制代码
submitAlarm() {
  if (this.alarmTitle.trim() === '') {
    this.addNoticeLog('发布失败', '告警标题不能为空,请填写异常点位标题');
    return;
  }
  const text = this.alarmDesc.trim() === ''
    ? `巡检中发现${this.alarmLevel}级异常,请责任人尽快到场处置`
    : this.alarmDesc.trim();
  this.publishNotice(`物业安巡 · ${this.alarmLevel}告警`, this.alarmTitle.trim() + ':' + text);
  this.alarmTitle = '';
  this.alarmDesc = '';
}

submitAlarm 先校验标题非空,描述为空时自动补全默认文案,然后调用 publishNotice 发布通知并清空表单。

14.2 沙箱铃声完整链路

publishNotice 方法是沙箱铃声特性的核心。

typescript 复制代码
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},请先开启通知授权`);
  });
}

完整链路为:获取当前默认铃声 → 未导入沙箱时先自动导入 → 获取宿主上下文 → 设置 EL1 沙箱区域 → 拼接沙箱文件路径 → fileUri.getUriFromPath 转为 uri → 加 'uri::' 前缀 → 填入 NotificationRequest.sound 字段 → notificationManager.publish 发布。sound 字段原来只能传 rawfile 文件名,6.1.1 后支持沙箱 uri,这是本平台利用的关键新特性。

14.3 铃声库管理

铃声库以 ringRow Builder 渲染每条铃声。每行包含铃声名、频率/时长/大小/沙箱状态、"生成"和"设默认"两个按钮。当前默认铃声行有橙色边框高亮。"生成"按钮调用 importRing 将 WAV 写入 EL1 沙箱,"设默认"按钮调用 setCurrentRing 切换默认索引(未导入时自动导入)。

getSoundValue 方法实时拼接当前通知请求的 sound 字段值并展示在预览区,让开发者直观看到 'uri::' + fileUri.getUriFromPath(沙箱路径) 的完整拼接结果。

十五、我的 Tab 深度分析

我的 Tab 由巡检员渐变大卡和绩效清单行两部分组成。

15.1 巡检员渐变大卡

大卡使用从深橙到警示橙的 140 度渐变背景,卡片内文字全部使用 onMain 深暖黑色调。左侧是巡检员 Emoji 头像和姓名/工号/职称信息,右侧是三项核心指标(本月点位 312、整改闭环 54、巡检里程 46.8km)。底部标注责任区域范围。

15.2 绩效清单行

typescript 复制代码
ForEach(PERF_ROWS, (row: PerfRow, idx: number) => {
  Row({ space: 10 }) {
    Text(`${idx + 1}`).fontSize(11).fontFamily('monospace').fontColor(COLORS.text3)
      .padding({ left: 8, right: 8, top: 3, bottom: 3 })
      .backgroundColor(COLORS.dark).borderRadius(7)
    Column({ space: 3 }) {
      Text(row.label).fontSize(12).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
      Text(row.note).fontSize(9).fontColor(COLORS.text3)
    }.alignItems(HorizontalAlign.Start).layoutWeight(1)
    Text(row.value).fontSize(13).fontFamily('monospace').fontWeight(FontWeight.Bold)
      .fontColor(idx < 3 ? COLORS.orange : COLORS.sub)
  }.width('100%').padding({ top: 11, bottom: 11 })
  .borderRadius(10).backgroundColor(COLORS.card)
}, (row: PerfRow, idx: number) => `perf_${row.label}_${idx}`)

每行左侧是序号徽标,中间是标签和补充说明,右侧是数值。前三行数值用橙色高亮(核心绩效指标),后三行用副标题色弱化。隔行通过 margin({ bottom: idx === PERF_ROWS.length - 1 ? 0 : 8 }) 控制间距,末行不留间距。

十六、底部 Tab 栏

typescript 复制代码
@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 栏自绘实现,不使用系统 Tabs 组件。7 个 Tab 等宽排列,选中态文字为 tabOn(警示橙),未选中为 text3(暗蓝灰)。点击调用 switchTab 方法,该方法在离开相机/对焦页时自动释放会话------因为 cameraInput 同一时间只能绑定一个 session,避免后台占用摄像头。

十七、弹窗系统

弹窗系统采用全屏遮罩 + 底部面板的三态架构。

17.1 遮罩层

typescript 复制代码
@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 实现上下分区:上部空白区 layoutWeight(1) 占满剩余空间,点击关闭弹窗;下部是面板区域,根据三个弹窗标志三选一渲染。整体 justifyContent(FlexAlign.End) 使面板贴底弹出。

17.2 新建弹窗

新建弹窗包含楼宇输入框、项目名输入框、初始进度滑杆和取消/创建按钮。进度滑杆步进为 5,范围 0~100。点击创建调用 confirmAdd 方法,将新任务 unshift 置顶到清单,进度达到 100% 时自动设置状态为"已完成"。

17.3 编辑弹窗

编辑弹窗顶部展示当前任务的楼宇、项目名和状态,下方是进度滑杆。点击保存调用 confirmEdit,更新进度和状态,100% 自动置为已完成。

17.4 删除确认弹窗

删除弹窗以红色删除按钮强化危险操作的视觉提示。点击删除调用 confirmDel,通过 splice 从清单中移除指定索引的任务。

三个弹窗共享 closeAllModals 方法统一复位所有标志和表单字段,确保下次打开时是干净的初始状态。

十八、功能模块对比表

功能模块 核心 API / 特性 状态变量 数据实体 交互方式
任务管理 ForEach + Progress + Slider taskList, formBuilding, formProgress TaskItem 弹窗新建/编辑/删除
相机预览 XComponent(SURFACE) + VideoSession surfaceReady, sessionMode, framingState --- 开启/停止会话按钮
影随人动 isControlCenterSupported + getSupportedEffectTypes + enableControlCenter framingSupported, framingState --- 能力链自动执行
手动对焦 isFocusDistanceSupported + setFocusDistance + getFocusDistance focusSupported, focusDistance, focusRecords FocusRecord 预设档位 + Slider + 校验按钮
嵌套滚动 Tabs.nestedScroll(TabsNestedScrollMode) nestedMode, outerIndex, innerIndex, swipeLogs SwipeLog, InnerCard 模式切换 chips + 滑动页签
翻页日志 List + ForEach + 时间轴布局 swipeLogs SwipeLog 清空按钮
告警上报 notificationManager.publish + NotificationRequest alarmTitle, alarmLevel, alarmDesc NoticeLog 表单提交
沙箱铃声 buildWavBytes + fs.openSync + fileUri.getUriFromPath ringList, currentRingIdx, notifyId RingItem, NoticeLog 生成/设默认/发布
授权管理 requestPermissionsFromUser + requestEnableNotification permState, granted --- 申请按钮 + 状态查询
月度柱状图 Column + ForEach + linearGradient breath --- 呼吸动画自动驱动
绩效看板 ForEach + PERF_ROWS --- PerfRow 静态展示
弹窗系统 Stack + 条件渲染 + Column addModal, editModal, delModal TaskItem 三态弹窗切换

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

布局方式与数据流

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

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

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

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

核心代码与状态驱动机制

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

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

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

动画效果与颜色使用策略

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

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

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

各 Tab 之间的交互联动

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

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

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

边界场景与验证思路

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

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

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

组件化设计的进一步理解

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

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

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

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

十九、总结与展望

本平台以物业安全巡检为业务载体,系统性地融合了 HarmonyOS 6.1.1 的三大前沿特性,展现了 ArkUI 声明式 UI 范式在复杂业务场景下的架构能力。

数据驱动 层面,平台通过 @Observed 装饰器将 TaskItem、FocusRecord、InnerCard、SwipeLog、RingItem、NoticeLog 六个实体类建模为响应式数据源。任何一处的属性修改------无论是弹窗中编辑进度、对焦校验生成记录、还是嵌套滚动触发翻页日志------都会自动驱动关联的 UI 片段重新渲染,开发者无需手动调用 setState 或操作 DOM 节点。这种数据与视图的自动绑定是声明式 UI 的核心价值。

组件化架构 层面,平台通过 @Builder 方法将 7 个 Tab 的复杂 UI 拆分为十余个独立的构建块。每个 Builder 方法职责单一、内聚性强:tabTask 专注任务统计与清单、tabCamera 专注相机预览与能力链、tabFocus 专注对焦三接口验证、tabChannel 专注嵌套滚动演示。这种拆分使代码可维护性大幅提升,新增或修改某个 Tab 的功能不会影响其他 Tab。

特性融合 层面,Camera Kit 的影随人动能力链通过三步查询+启用的模式实现了优雅的降级处理------本机不支持时通过状态文案和颜色映射给出明确反馈,而非崩溃或静默失败。手动对焦三接口的设置→读回→校验模式为硬件指令的执行提供了可追溯的证据链。Notification Kit 的沙箱铃声链路打通了从音频生成到文件落盘到 uri 拼接到通知发布的完整闭环,让告警铃声不再局限于系统预设。Tabs 嵌套滚动通过一个 nestedScroll 属性即可改变内外层滚动联动行为,配合翻页日志的时间轴可视化,使特性效果可观测、可验证。

展望未来,本平台可在以下方向持续演进。第一,引入 AI 能力对巡检取证影像进行智能识别------如灭火器压力表读数 OCR、配电柜红外测温区域自动标注、消防通道堆物检测等,将相机取证从"拍照存档"升级为"拍照即分析"。第二,将告警通知与飞书工作流打通,实现严重告警自动派单到责任人飞书待办、紧急告警自动创建飞书审批发起整改流程。第三,在嵌套滚动基础上引入楼层级三维导航------以楼宇 3D 模型为外层、楼层平面图为中层、检查项列表为内层,实现真正的"楼宇-楼层-检查点"三级嵌套。第四,将对焦记录时间线与区块链存证结合,使每一次对焦操作的时间戳、设置值、读回值成为不可篡改的巡检证据。

ArkUI 的声明式范式和组件化架构为这些演进提供了坚实的技术基座------数据驱动的响应式渲染让新增 AI 分析结果只需扩展数据模型,@Builder 的可组合性让新增 3D 导航 Tab 只需新增一个 Builder 方法,装饰器的状态管理能力让跨 Tab 数据共享只需在顶层声明 @State 变量。在这个基座之上,物业安全巡检的数字化、智能化、可追溯化之路将越走越宽。

附录: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 语法:

typescript 复制代码
@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 应用的功能开发。


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

相关推荐
Blockchina29 分钟前
Codex安全盲区:我用4组本地测试复盘SQLi、XSS、越权与路径穿越
sql·安全·xss
贾伟康29 分钟前
【时光清单|03】HarmonyOS ArkTS 提醒服务实战:计算触发时间并防止重复注册
harmonyos·arkts·后台任务·幂等设计·通知提醒
李蚊子30 分钟前
从代理提醒到真实响铃:懒熊闹钟的鸿蒙开发实践
前端·harmonyos
GKxx33 分钟前
在 HarmonyOS 上从源码构建 GCC 16(gcc/g++ + libstdc++ + libsanitizer):完整记录
c++·华为·harmonyos·鸿蒙·gcc
贾伟康35 分钟前
【时光清单|04】HarmonyOS ArkTS 通知服务实战:管理权限、渠道和点击跳转
harmonyos·arkts·系统通知·notificationkit·wantagent
黑臂麒麟1 小时前
HarmonyOS随手账本App——从项目搭建到数据模型设计
华为·app·鸿蒙
黑臂麒麟1 小时前
Harmony鸿蒙实战应用4:随手账本——编辑与删除账单
ubuntu·华为·鸿蒙
独守一片天1 小时前
HarmonyOS|鸿蒙新生态服务卡片设计与状态同步
华为·harmonyos
黑臂麒麟1 小时前
HarmonyOS 7 碰一碰实战:手机轻触电脑,精准插入图片到指定位置
华为·智能手机·arkts·鸿蒙