深色安全蓝灰与警示橙的巡检守护背后的HarmonyOS ArkUI 物业安全巡检平台
一、技术前言
在智慧物业管理领域,安全巡检是保障建筑运行的第一道防线。从消防通道占用复查到配电间红外测温,从喷淋管网压力巡检到水泵启动试运行测试,每一项巡检任务都需要精确的流程管控、清晰的进度追踪和即时的异常告警能力。传统巡检应用往往面临三大痛点:取证影像模糊导致责任争议、告警铃声单一导致响应迟缓、巡检路径复杂导致界面交互割裂。
HarmonyOS ArkUI 框架以其声明式 UI 范式为这些问题提供了系统级的解决方案。ArkUI 基于 TypeScript 扩展的 ArkTS 语言,通过 @Component 装饰器封装可复用组件,通过 @State、@Observed 等状态管理装饰器实现数据驱动渲染,通过 @Builder 方法将复杂的 UI 结构拆分为可组合的构建块。这种架构天然适合巡检场景中"数据-视图-交互"紧耦合的需求。在 ArkUI 的组件树模型中,每个 @Entry 组件作为页面根节点,通过 Stack、Column、Row 等容器组件构建层次化布局,状态变量的变更自动触发依赖该状态的 @Builder 方法重新渲染,实现了"数据变化即视图更新"的响应式编程体验。
本平台深度融合了 HarmonyOS 6.1.1 的四大前沿特性。Camera Kit 提供了 VideoSession 的 AUTO_FRAMING(影随人动)能力链——通过 isControlCenterSupported 检测控制中心支持、getSupportedEffectTypes 查询效果枚举、enableControlCenter(true) 三步实现巡检取景时人员始终居中;同时 PhotoSession 的手动对焦三接口 isFocusDistanceSupported、setFocusDistance、getFocusDistance 实现铭牌近拍到机房全景的精确对焦控制,并通过"设置值-读回值"差值校验确保对焦生效。Notification Kit 实现了沙箱自定义铃声链路——通过 buildWavBytes 生成正弦波 PCM 音频字节流,写入 EL1 沙箱 filesDir 目录,再以 'uri::' + fileUri.getUriFromPath(沙箱路径) 拼接填入 NotificationRequest.sound 字段,让不同等级告警拥有差异化铃声。Tabs 嵌套滚动 通过 nestedScroll(TabsNestedScrollMode) 让内层检查项列表滑到边缘后联动外层楼宇频道,实现巡检路径的自然流转,并通过 SELF_FIRST 与 SELF_ONLY 两种模式灵活控制滚动接力行为。
二、整体架构流程图
整体架构以主组件为根节点,采用 Stack 容器实现页面层叠:底层是 Column 纵向布局的头部 Banner + 内容区 + 底部 Tab 栏,顶层是全屏弹窗遮罩。内容区通过 currentTab 状态索引在 7 个 @Builder 方法间切换,每个 Tab 拥有完全独立的布局结构。四大特性(Camera Kit 影随人动、手动对焦、Tabs 嵌套滚动、Notification 沙箱铃声)分别挂载在相机、对焦、频道、告警四个 Tab 上,但它们的状态变量统一声明在组件顶层,实现跨 Tab 数据共享。弹窗系统通过三个布尔状态标志(addModal、editModal、delModal)控制三种面板的显示,点击遮罩区域统一关闭。
三、色彩体系设计
3.1 ColorPalette 接口定义
平台采用深色安全蓝灰主题,通过 ColorPalette 接口集中声明全部颜色字段。这种接口化设计确保了颜色引用的类型安全和可维护性,任何颜色字段的增删都可在接口层面获得编译器的检查反馈:
interface ColorPalette {
bg: string; // 页面背景(安全蓝灰黑)
card: string; // 卡片底色(深蓝灰)
title: string; // 主标题(冷白)
sub: string; // 副标题(蓝灰)
text3: string; // 三级弱文本(暗蓝灰)
orange: string; // 警示橙(主色)
orangeD: string; // 警示橙深色(渐变起点)
blue: string; // 信息蓝(电气 / 内层日志)
green: string; // 合格绿(消防 / 已生效)
red: string; // 警示红(失败 / 删除)
line: string; // 分割线
tabOn: string; // Tab 选中色
mask: string; // 弹窗遮罩
onMain: string; // 橙底文字色(深暖黑)
}
3.2 COLORS 常量逐色分析
const COLORS: ColorPalette = {
bg: '#14171C', // 极深蓝灰黑,模拟夜间巡检环境
card: '#1E232B', // 卡片底色,比背景略亮一档
dark: '#283039', // 次级容器底色(统计格 / 进度条底)
title: '#EDF1F5', // 冷白色标题,高对比度保证暗光可读
sub: '#ADBAC7', // 蓝灰副标题,层次柔和过渡
text3: '#74818E', // 暗蓝灰弱文本,辅助信息不抢视觉
orange: '#FF8A2B', // 警示橙主色,巡检进度的视觉锚点
orangeD: '#E06A10', // 深橙渐变起点,头部Banner到背景的过渡
blue: '#4E9BE3', // 信息蓝,电气检查项与内层日志标识
green: '#34C98A', // 合格绿,已完成状态与读回校验通过
red: '#E85555', // 警示红,逾期任务与删除操作
line: '#2A313A', // 分割线,低对比度不干扰内容
tabOn: '#FF8A2B', // Tab 选中色与主色一致
mask: 'rgba(0,0,0,0.6)', // 半透黑遮罩
onMain: '#231507' // 橙底深字,保证按钮文字对比度
};
色彩设计遵循"安全警示"原则:橙绿蓝红四色分别对应"巡检中/已合格/信息参考/危险警告"四种语义状态,使用户在深色环境下凭颜色即可快速识别任务优先级。头部 Banner 的 linearGradient 从 orangeD 到 bg 实现警示橙到蓝灰黑的自然过渡,底部 7 Tab 栏选中态使用 orange 高亮,未选中态使用 text3 暗蓝灰弱化。onMain 字段是深暖黑色,专用于警示橙底色上的文字,确保按钮文案在橙色背景上依然保持足够的对比度。dark 色比 card 略深一档,用于统计格、进度条底等次级容器,形成卡片内的层次递进。
四、Tab 元数据与常量定义
4.1 底部导航 Tab 定义
interface TabMeta {
icon: string;
label: string;
}
const TAB_LIST: TabMeta[] = [
{ icon: '📋', label: '任务' },
{ icon: '📷', label: '相机' },
{ icon: '🎯', label: '对焦' },
{ icon: '🌀', label: '频道' },
{ icon: '📜', label: '日志' },
{ icon: '🚨', label: '告警' },
{ icon: '👤', label: '我的' }
];
7 个 Tab 单排排列,从任务管理到个人中心覆盖巡检全流程。每个 Tab 的图标与其功能语义紧密对应:📋 代表任务清单,📷 代表取证拍照,🎯 代表对焦精准,🌀 代表频道流转,📜 代表日志记录,🚨 代表告警上报,👤 代表巡检员中心。TabMeta 接口仅包含 icon 和 label 两个字段,保持导航数据的极简性,ForEach 渲染时以 tab.label 作为键值确保唯一性。
4.2 Camera Kit 效果枚举
interface EffectInfo {
type: number;
name: string;
desc: string;
}
const EFFECT_INFOS: EffectInfo[] = [
{ type: 0, name: 'BEAUTY', desc: '美颜 · since 20' },
{ type: 1, name: 'PORTRAIT', desc: '人像 · since 20' },
{ type: 2, name: 'AUTO_FRAMING', desc: '影随人动 · 6.1.1 新增' }
];
三效果枚举展示了 ControlCenterEffectType 的完整谱系。BEAUTY 和 PORTRAIT 自 API 20 起就存在,AUTO_FRAMING 是 6.1.1 新增能力,本平台正是利用这一新特性实现巡检跟拍时人员始终居中。在相机 Tab 的效果枚举表中,当 AUTO_FRAMING 被本机声明时,会在对应行追加"已声明"绿色徽标,使能力检测结果一目了然。
4.3 对焦预设三档
interface FocusPreset {
label: string;
distance: number;
scene: string;
}
const FOCUS_PRESETS: FocusPreset[] = [
{ label: '铭牌近拍', distance: 0.1, scene: '0.1 · 铭牌/序列号' },
{ label: '设备中距', distance: 0.5, scene: '0.5 · 配电柜整柜' },
{ label: '环境远距', distance: 0.9, scene: '0.9 · 机房全景' }
];
对焦距离值范围 0.0(最近)到 1.0(最远),三档预设覆盖巡检取证的三个典型景别:0.1 用于拍摄设备铭牌和出厂序列号,0.5 用于拍摄配电柜整体和管线走向,0.9 用于拍摄机房全景和疏散通道。预设卡片选中态使用警示橙背景,未选中态使用卡片底色,通过颜色反差直观提示当前对焦景别。
4.4 嵌套滚动频道数据
const OUTER_CHANNELS: ChannelItem[] = [
{ name: '1 号楼', icon: '🏢' },
{ name: '2 号楼', icon: '🏬' },
{ name: '地下车库', icon: '🅿️' },
{ name: '配电房', icon: '⚡' },
{ name: '消防泵房', icon: '🚒' }
];
const INNER_TABS: string[] = ['消防', '电气', '管道', '通道', '监控'];
外层 5 个楼宇频道代表物业巡检的责任分区,内层 5 个检查项类别覆盖消防、电气、管道、通道、监控五大专业领域。两层 Tabs 嵌套形成 25 个检查矩阵,每个矩阵下有 8 条检查卡片,共 200 个检查点位,足以验证 nestedScroll 在真实业务量下的边缘接力效果。
4.5 月度隐患柱状图数据
const MONTH_NAME: string[] = ['03', '04', '05', '06', '07', '08'];
const MONTH_HAZARD: number[] = [12, 9, 15, 7, 11, 6];
const MONTH_MAX: number = 16;
近 6 个月隐患发现数据呈波动下降趋势(15→7→6),反映巡检整改效果。满刻度 16 处用于柱高归一化换算,柱状图随呼吸动画在正负 6% 区间交替波动,奇偶柱交替产生视觉律动感。
4.6 巡检员绩效数据
interface PerfRow {
label: string;
value: string;
note: string;
}
const PERF_ROWS: PerfRow[] = [
{ label: '本月完成点位', value: '312', note: '应检 320 · 完成率 97.5%' },
{ label: '隐患整改闭环', value: '54/56', note: '闭环率 96.4% · 超期 2 项' },
{ label: '平均响应时长', value: '8 分钟', note: '严重告警到场时限 15 分钟' },
{ label: '本月巡检里程', value: '46.8 km', note: '含地库 B1/B2 两层环线' },
{ label: '拍照取证张数', value: '218 张', note: '含隐患整改前后对比图' },
{ label: '连续安全达标', value: '12 天', note: '当班期间安全零事故' }
];
绩效清单以 6 行数据呈现巡检员月度工作成果,前三行(完成点位、整改闭环、响应时长)使用警示橙数值色突出关键指标,后三行使用蓝灰弱化色呈现辅助数据。每行都附带 note 说明字段,提供完整的数据上下文。
五、工具函数
5.1 时间戳生成
function nowTime(): string {
const d = new Date();
return `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}:${String(d.getSeconds()).padStart(2, '0')}`;
}
nowTime 函数为 SwipeLog、FocusRecord、NoticeLog 三类时间线提供统一的时间戳格式化,使用 padStart(2, '0') 保证时分秒始终两位数,格式为 HH:mm:ss。该函数在数据模型构造器中被调用,确保所有时间戳格式一致。
5.2 嵌套模式文案映射
function modeLabel(mode: TabsNestedScrollMode): string {
return mode === TabsNestedScrollMode.SELF_FIRST
? 'SELF_FIRST·先内后外' : 'SELF_ONLY·仅内层';
}
function modeShort(mode: TabsNestedScrollMode): string {
return mode === TabsNestedScrollMode.SELF_FIRST ? '先内后外' : '仅内层';
}
两个函数将 TabsNestedScrollMode 枚举翻译为中文文案。modeLabel 返回完整技术文案用于日志记录和说明行,modeShort 返回短文案用于头部状态胶囊和模式切换 chips。SELF_FIRST 模式下内层滑到边缘会接力触发外层切换,SELF_ONLY 模式下内层滑动不联动外层。
5.3 影随人动状态配色
function framingStateColor(s: string): string {
if (s === '影随人动已启用') return COLORS.green;
if (s === '控制中心不支持' || s === 'AUTO_FRAMING 未声明') return COLORS.blue;
if (s.indexOf('失败') >= 0 || s.indexOf('被拒') >= 0 || s.indexOf('未就绪') >= 0) return COLORS.red;
return COLORS.text3;
}
该函数将影随人动能力链的各阶段结果映射为颜色:已启用为合格绿(表示巡检跟拍已就位),能力不支持为信息蓝(设备限制非故障),失败/被拒为警示红(需要处理),未查询为暗蓝灰弱化。通过字符串匹配实现多状态色彩路由,在 UI 渲染时提供即时的视觉反馈。
5.4 对焦距离与校验配色
function distanceLabel(v: number): string {
if (v < 0.3) return '近拍 · 设备铭牌与出厂序列号';
if (v < 0.7) return '中距 · 配电柜整柜与管线走向';
return '远距 · 机房全景与疏散通道';
}
function focusOkColor(ok: string): string {
if (ok === '已生效') return COLORS.green;
if (ok.indexOf('失败') >= 0) return COLORS.red;
return COLORS.orange;
}
distanceLabel 将 0.0 到 1.0 的连续对焦距离值映射为三段巡检景别文案,在 Slider 拖动时实时显示当前景别。focusOkColor 将对焦校验结论映射为颜色:已生效为绿(设置值与读回值偏差小于 0.01),失败为红(会话异常或接口调用出错),偏差为橙(设置值与读回值不一致,需关注但不致命)。
5.5 任务状态与会话模式配色
function taskStatusColor(s: string): string {
if (s === '已完成') return COLORS.green;
if (s === '进行中') return COLORS.orange;
if (s === '待复查') return COLORS.blue;
if (s === '已逾期') return COLORS.red;
return COLORS.text3;
}
function sessionLabel(mode: string): string {
if (mode === 'video') return 'VideoSession · 影随人动宿主';
if (mode === 'photo') return 'PhotoSession · 手动对焦宿主';
return 'idle · 未启动会话';
}
taskStatusColor 将四种巡检任务状态映射为语义色:已完成绿、进行中橙、待复查蓝、已逾期红,用户扫一眼即可判断任务紧急度。sessionLabel 将相机会话模式翻译为中文说明,标注 VideoSession 是影随人动的宿主、PhotoSession 是手动对焦的宿主,帮助用户理解两种会话的职能分工。
5.6 告警等级配色与 WAV 估算
function levelColor(level: string): string {
if (level === '严重') return COLORS.orange;
if (level === '紧急') return COLORS.red;
return COLORS.blue;
}
function wavSizeText(durationMs: number): string {
const bytes = 44 + Math.floor(44100 * durationMs / 1000) * 2;
return `${(bytes / 1024).toFixed(1)} KB`;
}
levelColor 将告警三等级映射为颜色:一般为蓝(常规提示)、严重为橙(需尽快处理)、紧急为红(立即响应)。wavSizeText 根据时长估算 WAV 文件大小:44 字节头部加上 44100Hz 采样率、16bit 单声道 PCM 数据量,用于铃声列表中显示文件体积。
5.7 正弦波 WAV 音频生成
function buildWavBytes(freq: number, durationMs: number): ArrayBuffer {
const sampleRate = 44100;
const numSamples = Math.floor(sampleRate * durationMs / 1000);
const dataSize = numSamples * 2;
const buf = new ArrayBuffer(44 + dataSize);
const view = new DataView(buf);
// ... 44 字节 WAV 头写入(RIFF/WAVE/fmt /data)...
for (let i = 0; i < numSamples; i++) {
const t = i / sampleRate;
const env = Math.min(1, i / (sampleRate * 0.02)); // 起音包络
const decay = Math.max(0, 1 - t / (durationMs / 1000)); // 自然衰减
view.setInt16(44 + i * 2, Math.round(Math.sin(2 * Math.PI * freq * t) * 0.5 * env * decay * 32767), true);
}
return buf;
}
这是 Notification Kit 沙箱铃声链路的起点函数。它生成标准 WAV 格式音频字节流:44 字节头部包含 RIFF 标识、WAVE 格式、PCM 编码、单声道、44100Hz 采样率、16bit 位深等标准字段。音频数据部分通过正弦波公式 sin(2*PI*freq*t) 生成纯音,叠加起音包络(前 20ms 渐入)和自然衰减(线性递减),避免点击杂音。不同频率(880Hz 疏散警报、660Hz 消防长鸣、1046Hz 门禁提示、1320Hz 周界蜂鸣)生成不同音高的告警铃声,返回的 ArrayBuffer 直接写入 EL1 沙箱文件。
5.8 检查要点池
function checkPoint(tabName: string, i: number): string {
const pool: string[] = tabName === '消防'
? ['灭火器压力表指针', '消火栓水带卡扣', ...]
: tabName === '电气'
? ['配电柜母排温度', '断路器接线端子', ...]
: // ... 管道、通道、监控各 8 条
}
该函数为内层检查项卡片提供行业语义要点。五类检查各 8 条要点,覆盖灭火器压力表、配电柜母排温度、喷淋管网压力表、安全出口堆物、监控镜头遮挡等专业巡检内容。通过 tabName 参数路由到对应要点池,再按索引取值,确保每个检查矩阵的 8 张卡片都有差异化、真实化的内容。
六、数据模型层
6.1 TaskItem 巡检任务实体
@Observed export class TaskItem {
building: string; // 楼宇 / 分区名
item: string; // 巡检项目名
progress: number; // 进度(0~100)
status: string; // 状态(已完成/进行中/待复查/已逾期)
}
TaskItem 使用 @Observed 装饰器标记为可观察对象,当其属性变更时自动触发依赖该实例的 UI 组件重渲染。7 条种子数据覆盖四种状态:2 项已完成(消防通道复查、电梯机房点检)、3 项进行中(配电间测温、喷淋管网、直流屏电池)、1 项待复查(水泵试运行)、1 项已逾期(应急照明断电测试),为任务 Tab 的统计卡和进度条清单提供完整的数据基础。
6.2 FocusRecord 对焦记录
@Observed export class FocusRecord {
time: string; // 操作时间戳
distance: number; // 设置的对焦距离(0.0~1.0)
readback: number; // 读回值(-1 表示调用失败)
ok: string; // 校验结论(已生效/读回偏差/失败)
}
每条记录保存一次手动对焦操作的完整证据链:设置值、读回值、差值校验结论和时间戳。aboutToAppear 时种入 3 条历史记录(0.9 远距、0.5 中距、0.1 近拍),使对焦 Tab 首次进入即有时间线内容。列表使用 unshift 置顶最新记录,超过 20 条时 pop 移除尾部,保持时间线精简。
6.3 InnerCard 内层检查卡片
@Observed export class InnerCard {
id: string; // ForEach 键
tag: string; // 检查项类别名
title: string; // 检查点标题
desc: string; // 检查内容描述
}
innerMockData 生成器为每个楼宇频道 × 检查项类别组合生成 8 条卡片,id 格式为 {频道名}-{类别名}-{序号} 确保 ForEach 键唯一。描述文本包含频道图标、频道名、检查类别、序号和检查要点,使每张卡片的信息完整且可区分。8 条数据量保证内容超过一屏,为 nestedScroll 的边缘接力演示提供前提条件。
6.4 SwipeLog 两层翻页日志
@Observed export class SwipeLog {
layer: string; // '外层楼宇' / '内层检查项'
tabName: string; // 切换到的页签名
fromIdx: number; // 起始索引
toIdx: number; // 目标索引
mode: string; // 事发时的嵌套模式
time: string; // 时间戳
}
每当内层或外层 Tabs 发生 onChange 翻页事件,就会生成一条 SwipeLog。layer 字段区分翻页发生在哪一层(外层楼宇为橙、内层检查项为蓝),mode 字段记录事发时的 nestedScroll 模式,用于日志 Tab 的时间轴展示。日志列表封顶 40 条,超出时移除尾部,确保内存可控。在 SELF_FIRST 模式下,内层滑到边缘后触发外层切换,此时日志会连续记录"内层翻页"和"外层翻页"两条记录,形成"接力"证据链。
6.5 RingItem 告警铃声与 NoticeLog 发布历史
@Observed export class RingItem {
name: string; // 铃声名
file: string; // 沙箱文件名
freq: number; // 生成频率 Hz
duration: number; // 时长 ms
size: string; // 文件大小展示
inSandbox: boolean; // 是否已写入沙箱
}
@Observed export class NoticeLog {
title: string; // 通知标题
text: string; // 通知正文
time: string; // 发布时间戳
}
RingItem 封装了告警铃声的完整元数据。4 条种子铃声覆盖不同频率和时长:880Hz/900ms 疏散警报、660Hz/1200ms 消防长鸣、1046Hz/600ms 门禁提示、1320Hz/450ms 周界蜂鸣。inSandbox 标志跟踪铃声是否已写入 EL1 沙箱,size 在导入沙箱后更新为实际文件大小。NoticeLog 记录每次通知发布的结果,成功和失败均记一条,失败记录标题包含"失败"关键字以红色高亮显示。
七、组件主体
7.1 状态变量声明
主组件声明了五大类状态变量。Tab 状态包含 currentTab 索引,控制 7 个 Tab 间的切换。弹窗状态包含 addModal、editModal、delModal 三个布尔标志和 editIdx、delIdx 两个索引,绑定 TaskItem 巡检任务实体的增删改操作。Camera Kit 成员包含 previewController(XComponent 控制器)、cameraInput、previewOutput、videoSession、photoSession 等控制器类成员(private 不参与渲染),以及 surfaceReady、sessionMode、framingState、framingSupported、focusSupported、focusDistance、focusRecords、permState 等渲染状态。Notification Kit 成员包含 granted、notifyId、ringList、currentRingIdx、noticeLogs、alarmTitle、alarmLevel、alarmDesc。Tabs 嵌套滚动成员包含 nestedMode、outerIndex、innerIndex、swipeLogs。
7.2 生命周期管理
aboutToAppear() {
notificationManager.isNotificationEnabled().then((enabled: boolean) => {
this.granted = enabled;
}).catch(() => {});
this.focusRecords.unshift(new FocusRecord(0.9, 0.9, '已生效'));
this.focusRecords.unshift(new FocusRecord(0.5, 0.51, '已生效'));
this.focusRecords.unshift(new FocusRecord(0.1, 0.12, '读回偏差'));
this.timer = setInterval(() => { this.breath = !this.breath; }, 1000);
}
aboutToDisappear() {
clearInterval(this.timer);
this.releaseSession();
}
aboutToAppear 执行三件初始化:异步查询通知授权状态并回填 granted、种入 3 条对焦历史记录使时间线非空、启动 1 秒间隔的呼吸动画定时器驱动 breath 翻转。aboutToDisappear 执行清理:清除定时器防内存泄漏、释放相机会话防后台占用。这种对称的生命周期管理确保组件退出时资源不残留。
7.3 Tab 切换与会话互斥
switchTab(idx: number) {
if ((this.currentTab === 1 || this.currentTab === 2) && idx !== 1 && idx !== 2) {
this.releaseSession();
}
this.currentTab = idx;
}
Tab 切换时有一个关键的互斥逻辑:当从相机 Tab(索引 1)或对焦 Tab(索引 2)切换到其他 Tab 时,必须释放相机会话。这是因为 cameraInput 同一时间只能绑定一个 session(VideoSession 或 PhotoSession),不复释会导致后续会话创建失败。相机和对焦两个 Tab 共用同一个 XComponent 的 Surface,因此切换到非相机/非对焦 Tab 时主动释放资源。
八、Camera Kit 方法群
8.1 权限申请
async requestCameraPermission(): Promise<boolean> {
const ctx = this.getUIContext().getHostContext();
if (ctx === undefined || ctx === null) { this.permState = '上下文未就绪'; return false; }
const atManager = abilityAccessCtrl.createAtManager();
const result = await atManager.requestPermissionsFromUser(ctx, ['ohos.permission.CAMERA']);
const granted = result.authResults.length > 0 && result.authResults[0] === 0;
this.permState = granted ? '已授权' : '权限被拒';
return granted;
}
CAMERA 权限属于 user_grant 级别,需要动态申请。通过 abilityAccessCtrl.createAtManager() 创建权限管理器,调用 requestPermissionsFromUser 弹出系统授权框。判断条件为 authResults[0] === 0(0 表示已授权)。权限状态实时回填到 permState,在相机 Tab 的授权卡上以绿/橙色显示。上下文获取使用 getUIContext().getHostContext() 而非已废弃的 getContext(this),并进行判空兜底。
8.2 影随人动模式:VideoSession + AUTO_FRAMING 能力链
startVideoMode 方法是相机 Tab 的核心入口,完整流程为:检查 Surface 就绪 → 释放可能存在的 PhotoSession → 申请 CAMERA 权限 → 获取 CameraManager → 选后摄 → 查询预览 Profile → 创建 CameraInput 并 open → 创建 PreviewOutput 绑定 Surface → 创建 VideoSession → beginConfig/addInput/addOutput/commitConfig → 查询 AUTO_FRAMING 能力链 → start。
其中 AUTO_FRAMING 能力链(queryFraming 方法)是 6.1.1 的核心新特性,分三步执行:
queryFraming(session: camera.VideoSession) {
// 第一步:检测控制中心是否支持
if (!session.isControlCenterSupported()) {
this.framingState = '控制中心不支持';
return;
}
// 第二步:查询支持的效果类型枚举
const effects = session.getSupportedEffectTypes();
this.framingSupported = effects.includes(camera.ControlCenterEffectType.AUTO_FRAMING);
if (!this.framingSupported) { this.framingState = 'AUTO_FRAMING 未声明'; return; }
// 第三步:启用控制中心,系统接管构图
session.enableControlCenter(true);
this.framingState = '影随人动已启用';
}
三步形成"能力检测 → 能力枚举 → 能力启用"的完整链路。第一步 isControlCenterSupported 是同步方法,返回布尔值表示设备是否支持控制中心能力。第二步 getSupportedEffectTypes 返回当前设备支持的效果类型数组,通过 includes 判断是否包含 AUTO_FRAMING。第三步 enableControlCenter(true) 请求系统接管画面构图,巡查人员在取景框内移动时画面自动跟随,始终居中。任何一步失败都会设置相应的状态文案,在 UI 上以不同颜色显示。
8.3 手动对焦模式:PhotoSession + 三接口
switchToPhotoMode 方法先释放可能存在的 VideoSession(会话互斥),再创建 PhotoSession。创建流程与 VideoSession 类似,区别在于使用 SceneMode.NORMAL_PHOTO 场景模式。PhotoSession 启动后调用 queryFocusSupport 查询手动对焦能力:
queryFocusSupport() {
this.focusSupported = this.photoSession.isFocusDistanceSupported();
}
isFocusDistanceSupported 是同步方法,返回当前设备是否支持手动设置对焦距离。当返回 false 时,对焦 Tab 的预设档位、Slider 和应用按钮都会降级为不可用状态。
手动对焦的核心操作在 applyFocus 方法中:
applyFocus() {
this.photoSession.setFocusDistance(this.focusDistance); // 设置对焦距离
const readBack = this.photoSession.getFocusDistance(); // 读回对焦距离
const ok = Math.abs(readBack - this.focusDistance) < 0.01 ? '已生效' : '读回偏差';
this.focusRecords.unshift(new FocusRecord(this.focusDistance, readBack, ok));
}
三接口形成"查询 → 设置 → 读回"的完整闭环。setFocusDistance 设置目标对焦距离(0.0 最近到 1.0 最远),getFocusDistance 读回实际生效的对焦距离。通过差值校验(差值小于 0.01 判定已生效),将设置值、读回值和校验结论一并记入 FocusRecord 时间线。这种"设置后读回验证"的设计模式确保对焦操作的可靠性,避免静默失败。
8.4 会话释放
async releaseSession() {
const session = this.videoSession ?? this.photoSession;
// 先置空引用(防并发),再依次释放
this.videoSession = undefined;
this.photoSession = undefined;
this.previewOutput = undefined;
this.cameraInput = undefined;
if (session) { session.off('error'); await session.stop(); await session.release(); }
if (preview) { await preview.release(); }
if (input) { await input.close(); }
this.sessionMode = 'idle';
}
释放链遵循"先置空引用再释放资源"的安全模式,防止异步释放期间的其他代码路径误用已释放的对象。五步释放顺序为:session.off(‘error’) 移除错误监听 → session.stop() 停止会话 → session.release() 释放会话 → previewOutput.release() 释放预览输出 → cameraInput.close() 关闭相机输入。任何步骤异常都被 catch 捕获并记录日志,不阻断后续释放。
九、Notification Kit 方法群
9.1 通知授权
requestAuth() {
const hostCtx = this.getUIContext().getHostContext() as common.UIAbilityContext;
notificationManager.requestEnableNotification(hostCtx).then(() => {
this.granted = true;
}).catch((err: BusinessError) => {
// 曾拒绝时返回 1600004,拉起通知设置页引导手动开启
notificationManager.openNotificationSettings(hostCtx);
});
}
通知授权采用"先请求后引导"的两级策略。requestEnableNotification 首次调用时弹出系统授权框;如果用户曾拒绝则返回错误码 1600004,此时通过 openNotificationSettings 拉起系统通知设置页,引导用户手动开启。两处都使用 getUIContext().getHostContext() 获取宿主上下文并传入,而非使用已废弃的无参版本。
9.2 沙箱铃声写入
saveRingToSandbox(fileName: string, freq: number, durationMs: number): string {
const hostCtx = this.getUIContext().getHostContext() as common.UIAbilityContext;
const appCtx = hostCtx.getApplicationContext();
appCtx.area = contextConstant.AreaMode.EL1; // 必须在 EL1 沙箱下
const dir = appCtx.filesDir; // EL1 区域 files 目录
const path = dir + '/' + fileName;
const data = buildWavBytes(freq, durationMs);
const file = fs.openSync(path, fs.OpenMode.CREATE | fs.OpenMode.WRITE_ONLY | fs.OpenMode.TRUNC);
fs.writeSync(file.fd, data);
fs.closeSync(file);
return path;
}
沙箱铃声写入是 Notification 自定义铃声链路的核心环节。首先设置 appCtx.area = contextConstant.AreaMode.EL1,确保在 EL1 沙箱区域操作(设备级加密区,应用卸载后清除)。然后获取 filesDir 作为铃声文件存储目录。通过 buildWavBytes 生成正弦波 PCM 音频字节流,使用 fs.openSync 以"创建+只写+截断"模式打开文件,writeSync 写入数据,closeSync 关闭文件句柄。返回的沙箱路径后续用于 fileUri.getUriFromPath 转换。
9.3 通知发布与 sound 链路
publishNotice(title: string, text: string) {
const ring = this.ringList[this.currentRingIdx];
const appCtx = hostCtx.getApplicationContext();
appCtx.area = contextConstant.AreaMode.EL1;
const sandboxPath = appCtx.filesDir + '/' + ring.file;
// ★ 沙箱路径 → fileUri → 'uri::' 前缀
const uri = fileUri.getUriFromPath(sandboxPath);
const soundVal = 'uri::' + uri;
const request: notificationManager.NotificationRequest = {
id: this.notifyId++,
notificationSlotType: notificationManager.SlotType.SOCIAL_COMMUNICATION,
content: { ... },
sound: soundVal // ★ 原来只能传 rawfile 文件名,现在支持沙箱 uri
};
notificationManager.publish(request);
}
publishNotice 是整个 Notification 链路的终点。sound 字段的值是 'uri::' + fileUri.getUriFromPath(沙箱路径)——这是 6.1.1 的新能力,原来 sound 只能填 rawfile 资源文件名,现在支持沙箱 URI 协议。通知槽类型设为 SOCIAL_COMMUNICATION(社交通信类,高优先级),通知内容包含标题、正文和附加文本(铃声名称)。notifyId 自增管理确保每条通知可独立撤销。发布失败时记录错误码并提示用户开启授权。
getSoundValue 方法提供 sound 字段值的实时预览,在告警 Tab 底部以等宽字体绿色显示完整的 'uri::...' 字符串,让开发者直观看到实际填入通知请求的 sound 值。
十、头部详解
@Builder
headerBanner() {
Column({ space: 10 }) {
Row() {
Column({ space: 4 }) {
Text('安全哨兵 · 物业安全巡检')
Text(this.currentTab === 0 ? `任务 · 今日 ${this.taskList.length} 项巡检`
: this.currentTab === 1 ? '相机 · 影随人动取证预览'
: /* ... 7 个 Tab 联动副标题 */)
}
Circle({ width: 10, height: 10 }).fill(COLORS.onMain)
.opacity(this.breath ? 0.9 : 0.45) // 呼吸圆点
}
Row({ space: 8 }) {
// 任务胶囊:巡检 N 项
// 特性A胶囊:相机会话状态(idle灰/video绿/photo橙)
// 特性B胶囊:通知授权状态(已授权绿/未授权红)
// 特性C胶囊:嵌套模式(SELF_FIRST蓝/SELF_ONLY橙)
}
}.linearGradient({ angle: 160, colors: [[COLORS.orangeD, 0], [COLORS.bg, 1]] })
}
头部 Banner 是整个页面的信息枢纽,采用 160 度角的 linearGradient 从 orangeD(深橙)渐变到 bg(蓝灰黑),营造警示橙到暗色背景的自然过渡。Banner 分为两行:第一行是应用名"安全哨兵 · 物业安全巡检"和 Tab 联动副标题——副标题根据 currentTab 显示当前 Tab 的功能描述(如"任务 · 今日 7 项巡检"、“相机 · 影随人动取证预览”、"对焦 · 手动对焦三接口"等),右侧是呼吸圆点,透明度随 breath 在 0.9 和 0.45 之间交替,形成 1 秒一次的呼吸效果。
第二行是四个状态胶囊,横向排列:任务胶囊显示巡检总数;相机胶囊显示会话模式(idle 灰圆点、video 绿圆点、photo 橙圆点);通知胶囊显示授权状态(已授权绿圆点、未授权红圆点);嵌套胶囊显示 nestedScroll 模式(SELF_FIRST 蓝圆点、SELF_ONLY 橙圆点)。四个胶囊让用户一眼掌握四大特性的实时状态,无需进入对应 Tab 即可感知系统运行情况。
十一、各 Tab 分析
11.1 Tab0 任务:完成率渐变大数字卡
任务 Tab 采用 Scroll 纵向滚动布局,包含四段内容。第一段是完成率统计卡:46px 等宽字体的巨大完成率数字(橙色),旁边是"今日巡检完成率"标题和数据同步说明。下方是橙到绿的渐变完成条,再下是三格统计(进行中橙、待复查蓝、已逾期红),每格独立背景色,数值用对应语义色。第二段是任务清单头,右侧"+ 新增"入口触发新建弹窗。第三段是任务进度条清单,每张卡片包含楼宇徽标、状态徽章、项目名、Progress 线性进度条和百分比,右侧"编""删"按钮触发编辑和删除弹窗。第四段是月度隐患柱状图。
11.2 Tab1 相机:XComponent 取证预览 + 影随人动
相机 Tab 采用固定高度 + Scroll 混合布局。顶部是授权状态卡,显示 CAMERA 权限状态和 Surface 就绪状态,两个按钮分别触发权限申请和显示 Surface 状态。中间是 XComponent 取证预览本体,使用 SURFACE 类型绑定 previewController,layoutWeight(1) 占满剩余高度,右下角叠加会话模式标签。底部是模式切换行(开启影随人动/停止会话)和可滚动的影随人动状态卡 + 效果枚举表。状态卡展示能力链三步结果和"巡查跟拍:人员始终居中"的场景说明。效果枚举表列出 BEAUTY、PORTRAIT、AUTO_FRAMING 三种效果,AUTO_FRAMING 行在本机已声明时追加绿色"已声明"徽标。
11.3 Tab2 对焦:能力查询 + 三档预设 + Slider + 读回校验
对焦 Tab 全部包裹在 Scroll 中,包含五段内容。能力查询卡显示 isFocusDistanceSupported 结果和当前会话状态,右侧"启动对焦会话"按钮触发 PhotoSession 创建。三档预设卡片(铭牌近拍 0.1、设备中距 0.5、环境远距 0.9)横向排列,选中态使用橙色背景高亮。焦距滑杆卡提供 0.0 到 1.0 步进 0.01 的连续调节,下方实时显示 distanceLabel 景别文案。应用按钮行包含"设置并读回校验"(触发 applyFocus 三接口闭环)和"刷新能力查询"。对焦记录时间线以 Scroll 列表展示 FocusRecord 数组,每行显示时间、设置值、箭头、读回值和校验结论徽标。
11.4 Tab3 频道:双层 Tabs 嵌套滚动
频道 Tab 是 nestedScroll 特性的演示主场。顶部是模式说明行和两个切换 chips(SELF_ONLY/SELF_FIRST),当前模式以橙色背景高亮。下方是双层位置说明行,橙圆点标示外层楼宇位置,蓝圆点标示内层检查项位置。核心是外层宿主 Tabs(5 个楼宇频道,BarMode.Scrollable 横滑页签),每个 TabContent 内嵌 innerTabs Builder——内层 Tabs(5 个检查项类别),内层挂载 nestedScroll(this.nestedMode)。当模式为 SELF_FIRST 时,内层检查项列表滑到边缘后继续滑动会联动触发外层楼宇频道切换;SELF_ONLY 模式下内层滑动止于边缘不联动。每次翻页都生成 SwipeLog 记录,在日志 Tab 可查看。
内层每个检查项子页签下是一个 List,包含 8 条 InnerCard 检查卡片,每张卡片显示检查类别标题、检查点标题、检查内容描述和两个标签徽标(责任区橙、拍照取证必填绿)。8 条数据保证内容超过一屏,为 nestedScroll 边缘接力提供前提。
11.5 Tab4 日志:nestedScroll 翻页时间轴
日志 Tab 以时间轴形式展示 SwipeLog 数组。顶部是计数行和清空按钮,下方是图例行(外层楼宇橙圆点、内层检查项蓝圆点、当前模式)。主体是无记录时的空态卡或时间轴列表。每条日志项高度固定 72px,左侧是时间列(HH:mm:ss 等宽字体 + OUT/IN 缩写徽标),中间是 3px 宽的竖线(外层橙、内层蓝,60% 透明度),右侧是内容卡(层级徽标 + 页签名 + 索引箭头 + 模式文案)。双层颜色编码使用户可以一眼区分翻页发生在哪一层。
11.6 Tab5 告警:上报表单 + 授权卡 + 铃声库 + 发布历史
告警 Tab 将 Notification Kit 的全部能力压缩在单个 Tab 中,包含五个区块。区块一是异常上报表单:标题输入框、三级告警 chips(一般/严重/紧急,选中态使用对应语义色)、描述文本域和发布按钮。区块二是通知授权状态卡,提供"重新查询"和"申请授权"两个操作。区块三是告警铃声库:当前默认铃声名 + 生成按钮 + 4 条铃声精简行,每行显示名称、频率/时长/大小/沙箱状态,提供"生成"和"设默认"操作。区块四是 sound 字段实时值预览,以等宽字体绿色显示完整的 'uri::...' 字符串。区块五是发布历史,以 NoticeLog 时间线展示最近 8 条通知发布记录,失败记录以红色标题高亮。
11.7 Tab6 我的:巡检员渐变大卡 + 绩效清单
我的 Tab 顶部是巡检员渐变大卡,使用 140 度角的 linearGradient 从 orangeD 到 orange 渐变背景。卡片内左侧是巡检员 Emoji 头像和半透明圆形背景,右侧是姓名(周正涛)、工号和班次信息。卡片下方分割线后是三格统计(本月点位 312、整改闭环 54、巡检里程 46.8km),全部使用 onMain 深暖黑色文字保证橙色背景上的可读性。底部是绩效清单 6 行,前三行数值使用警示橙突出关键指标,后三行使用蓝灰弱化。每行包含序号、标签、说明和数值,以 8px 间距排列在卡片背景中。
十二、图表卡片
@Builder
chartCard() {
Column({ space: 10 }) {
Row() {
Text('📊 月度隐患发现数')
Text('近 6 个月 · 单位处')
}
Row({ space: 10 }) {
ForEach(MONTH_HAZARD, (val: number, idx: number) => {
Column({ space: 5 }) {
Text(val.toString()) // 数值标注
Column().height(this.barHeight(idx)) // 渐变柱
.linearGradient({ angle: 180, colors: [[COLORS.orange, 0], [COLORS.orangeD, 1]] })
Text(MONTH_NAME[idx]) // 月份标签
}
})
}.alignItems(VerticalAlign.Bottom)
}
}
月度隐患柱状图采用 ArkUI 原生的 Column + ForEach 方案实现传统柱状图。每根柱子由数值标注、渐变柱体和月份标签三部分组成,垂直排列。柱高通过 barHeight 方法换算:基准高度 = MONTH_HAZARD[i] / MONTH_MAX * 96,再乘以呼吸波动系数(奇偶柱交替 1.06/0.94),取最小值 8 确保零数据也有可见柱体。渐变方向为 180 度(从上到下),颜色从 orange 到 orangeD,形成上亮下暗的立体效果。Row 容器使用 VerticalAlign.Bottom 对齐,使所有柱子底部齐平。
十三、底部 Tab 栏
@Builder
tabBar() {
Row() {
ForEach(TAB_LIST, (tab: TabMeta, index: number) => {
Column({ space: 3 }) {
Text(tab.icon).fontSize(17)
Text(tab.label).fontSize(9)
.fontColor(this.currentTab === index ? COLORS.tabOn : COLORS.text3)
}.layoutWeight(1).onClick(() => { this.switchTab(index); })
})
}.backgroundColor(COLORS.card).border({ width: { top: 1 }, color: COLORS.line })
}
底部 Tab 栏采用自绘方案而非系统 Tabs 组件,实现完全的视觉控制。7 个 Tab 等宽排列(layoutWeight(1)),每个 Tab 上方是 17px 的 Emoji 图标,下方是 9px 的中文标签。选中态标签使用 tabOn(警示橙),未选中态使用 text3(暗蓝灰),通过颜色反差清晰标示当前页。顶部 1px 分割线使用 line 色(低对比度),与卡片背景色协调。点击触发 switchTab 方法,在切换前检查是否需要释放相机会话。
十四、弹窗系统
14.1 遮罩层
@Builder
modalOverlay(onClose: () => void) {
Column() {
Column().width('100%').layoutWeight(1).onClick(() => { onClose(); }) // 空白遮罩区
if (this.addModal) { this.panelAdd(onClose) }
else if (this.editModal) { this.panelEdit(onClose) }
else if (this.delModal) { this.panelDel(onClose) }
}.backgroundColor(COLORS.mask).justifyContent(FlexAlign.End)
}
弹窗系统采用 Stack 顶层覆盖方案。modalOverlay 是全屏 Column,背景为 rgba(0,0,0,0.6) 半透明遮罩。上半部分空白区域点击即关闭弹窗,下半部分根据 addModal/editModal/delModal 三个布尔标志三选一渲染对应面板。justifyContent(FlexAlign.End) 使面板从底部弹出,顶部圆角(borderRadius({ topLeft: 16, topRight: 16 }))形成底部抽屉效果。当三个标志同时为 false 时,遮罩层不渲染(由 build 中的条件判断控制)。
14.2 三态面板
新建面板(panelAdd)包含楼宇输入框、巡检项目输入框和初始进度 Slider(步进 5),提交后 unshift 置顶任务清单,初始状态根据进度判断(100% 为已完成,否则为进行中)。
编辑面板(panelEdit)显示当前任务的楼宇、项目和状态信息,提供进度 Slider(0~100 步进 5),保存时 100% 自动置为已完成状态。
删除面板(panelDel)显示确认信息,包含楼宇和项目名,删除按钮使用 COLORS.red 红色背景强调危险操作,确认后 splice 从列表移除。
三个面板共享 closeAllModals 方法,统一关闭并复位所有表单字段和索引,确保下次打开时不会残留上次输入。
十五、功能模块对比表
| 模块 | 核心特性 | 关键接口 | 数据模型 | 状态变量 | 交互入口 |
|---|---|---|---|---|---|
| 任务 Tab | 巡检进度管理 | Progress/ForEach | TaskItem | taskList/formBuilding/formProgress | 新建/编辑/删除弹窗 |
| 相机 Tab | 影随人动取证 | VideoSession/isControlCenterSupported/enableControlCenter | — | sessionMode/framingState/framingSupported | 开启影随人动/停止会话 |
| 对焦 Tab | 手动对焦三接口 | PhotoSession/isFocusDistanceSupported/setFocusDistance/getFocusDistance | FocusRecord | focusSupported/focusDistance/focusRecords | 预设档位/Slider/设置并读回 |
| 频道 Tab | 嵌套滚动接力 | Tabs.nestedScroll(TabsNestedScrollMode) | InnerCard/SwipeLog | nestedMode/outerIndex/innerIndex/swipeLogs | 模式切换chips/内外层翻页 |
| 日志 Tab | 翻页时间轴 | List/ForEach | SwipeLog | swipeLogs | 清空日志 |
| 告警 Tab | 沙箱自定义铃声 | buildWavBytes/saveRingToSandbox/fileUri.getUriFromPath/NotificationRequest.sound | RingItem/NoticeLog | granted/ringList/currentRingIdx/noticeLogs | 上报表单/授权申请/铃声生成/发布通知 |
| 我的 Tab | 绩效看板 | linearGradient/ForEach | PerfRow | — | 滚动浏览 |
深化解析:从代码结构到业务闭环
布局方式与数据流
物业巡检页面需要把任务、取证、日志、告警和人员绩效串成闭环。任务卡不只是展示进度,它还是后续相机取证与告警记录的业务入口;日志用于说明操作何时发生;状态颜色帮助巡检员优先处理逾期和严重隐患。分析这些模块时,重点要观察同一个任务实体如何被列表、弹窗、统计卡与进度图共同消费,以及修改后哪些区域会随状态更新。
页面根结构通常由头部、内容区和底部 Tab 栏组成。头部负责展示当前业务状态,内容区根据索引选择不同的 @Builder,底部导航负责修改索引。这样的结构把“当前显示什么”收敛为一个明确状态:用户点击 Tab 后先更新索引,ArkUI 再重新计算相关分支。各个 Builder 虽然共享主题色和页面级数据,却可以采用完全不同的布局方式;高密度列表适合纵向 Scroll,概览数据适合横向统计卡或双列 Flex,实时预览类组件需要独占有界高度,历史事件则适合时间轴或固定行高 List。
数据模型层承担界面与业务之间的契约。使用 @Observed 的实体保存可编辑字段,页面级 @State 数组负责驱动 ForEach。新增时创建新实体并插入数组,编辑时修改目标实体,删除时移除对应项。为了让列表差分稳定,key 应来自不会改变的唯一标识,不宜使用标题等可编辑字段。统计数字、完成比例和分类数量属于派生信息,可以从数组即时计算,避免同时维护两份状态后出现卡片已经更新、图表仍显示旧值的情况。
弹窗表单使用独立缓存是必要的。打开新增弹窗时清空缓存,打开编辑弹窗时复制目标字段,用户确认后才写回正式模型。这样点击取消不会污染列表数据。若直接把 TextInput 双向绑定到列表实体,用户尚未保存时卡片就可能跟着变化,破坏“确认提交”的交互语义。删除弹窗还需要保存目标索引或唯一标识,并在确认时再次校验目标存在,避免列表变化后误删其他项。
核心代码与状态驱动机制
@State 的价值不是简单替代普通变量,而是建立状态与界面之间的依赖关系。当前 Tab、筛选条件、动画开关、弹窗显隐、下载进度或能力状态发生变化时,只有读取这些变量的组件需要刷新。代码段中连续的修饰器调用分别控制尺寸、间距、背景、字体和事件,它们共同构成声明式描述;阅读时应从容器方向、子项分布、状态绑定和交互回调四个层面理解,而不是逐个孤立翻译属性名称。
ForEach 负责把数组映射为重复 UI。回调中的 item 提供业务字段,index 适合显示顺序,但不适合作为长期身份。列表发生新增或删除时,稳定 key 可以让框架复用未变化节点,减少重建。若直接修改对象属性后界面没有按预期刷新,可在保持实体身份的前提下替换数组引用;但不应为了刷新把所有元素都重新构造,否则会增加无意义渲染并丢失局部状态。
条件渲染体现了页面状态机。空闲时展示引导,准备中展示进度,成功时展示结果,失败时展示原因和重试入口。相比一个布尔值,四态文案更能覆盖异步能力。系统接口调用前先检查权限、设备支持和会话状态,调用后再读取结果校验。异常处理除了记录错误码,还要把可理解的反馈写入响应式状态,让用户知道失败发生在哪一步。
动画效果与颜色使用策略
呼吸动画通常由定时器周期翻转 breath,再把该状态映射为透明度、柱高或圆点半径的小幅变化。它适合表达“正在运行”或让统计图保持生命感,但幅度应克制,不能改变核心数据含义。柱状图的基础高度仍由真实数值计算,动画只能在很小范围内偏移;进度环的角度仍由完成比例决定,不能为了视觉效果显示超过真实进度的结果。页面离开时必须清理定时器,避免后台继续刷新。
颜色常量应按语义使用。主色承担选中态和主要操作,辅助色突出数据或次级动作,绿色表达完成与可用,橙色表达进行中或需要注意,红色只用于失败、逾期和删除等高风险场景。弱文本与分割线降低视觉权重,遮罩色用于聚焦弹窗。颜色不能成为唯一的状态信息,还要配合文字、图标或进度值,保证色觉差异用户也能理解。
渐变更适合头部大卡、核心指标或柱状图,不宜在每个小元素上重复使用。深色主题要检查正文与卡片背景的对比度,浅色主题则要避免辅助文字过淡。选中和未选中 Tab 除颜色差异外,还可以通过字重、图标透明度或底部指示器区分。这样既保持主题统一,又能建立清晰的信息层级。
各 Tab 之间的交互联动
各 Tab 不应只共享一个导航索引,还应围绕业务对象建立必要联动。列表页新增或编辑数据后,头部计数、图表和个人统计要同步更新;网页或地图产生的结果应写入记录模型,供下载、日志或我的页面继续展示;通知、字幕、相机等系统能力的状态应在头部胶囊或对应 Tab 中保持一致。跨 Tab 跳转时先更新必要参数,再修改当前索引,可以避免目标页面读取到旧条件。
切换离开重型组件时需要处理资源边界。相机输入、地图监听、字幕控制器、Web 下载代理和定时器都不能只创建不释放。可以在统一的 switchTab 方法中判断来源与目标,离开能力页时解除监听或停止会话;页面销毁时再执行兜底释放。释放方法应允许重复调用,并对每个资源独立判空,确保一次异常不会阻止后续清理。
交互反馈要覆盖成功与失败。按钮点击后先进入处理中状态并防止重复提交;成功后更新模型、关闭弹窗并显示结果;失败后保留用户输入,展示错误原因和重试入口。权限拒绝、能力不支持、网络失败、文件不存在和输入非法都属于正常业务分支。通过状态卡或行内提示展示这些分支,比只在控制台打印更符合完整产品体验。
边界场景与验证思路
空列表时应显示占位说明和新增入口,不能只留下空白。长标题需要限制行数并使用省略号,数字字段需要限定上下界,文本提交前要去除首尾空格。筛选后无结果应保留清除条件的入口。删除最后一项后,当前选择索引要回退到有效范围。异步搜索连续触发时,应防止较早请求晚返回后覆盖新结果。
验证数据链路时,可以依次检查新增、编辑、删除和筛选:新增后列表条数、统计数字和图表是否同时变化;编辑取消后正式数据是否保持不变;删除后 ForEach key 是否稳定;切换 Tab 再返回时必要数据是否仍在。验证系统能力时分别模拟支持、拒绝和异常,确认界面都有明确状态。验证动画时检查页面离开后是否停止,低性能设备上是否仍保持流畅。
视觉验收需要检查不同屏幕宽度、系统字体放大、深浅背景对比和长文本换行。表格中的布局方式、模型、字段数、核心操作、动画、状态颜色、数据量和特殊组件应与正文一致。Mermaid 图则需要对应真实的数据流和能力链路,节点文字加引号以避免中文或特殊字符导致解析失败。
组件化设计的进一步理解
参数化 Builder 适合抽取重复的统计格、状态行、标签和按钮组。参数只传入渲染所需数据和事件,不让子构建器直接依赖过多页面变量,可以降低耦合。业务复杂后,可把模型与系统能力封装为独立控制器,页面只负责组合 UI 和响应状态。这样既保留声明式代码的直观性,也能让权限、错误码翻译和资源释放得到集中管理。
当前单页面集中展示完整源码,便于博文逐段讲解。若演进为正式项目,可以按领域拆分组件:导航和页面框架位于容器层,列表、图表和弹窗位于展示层,数据读写和 Kit 接入位于服务层。组件之间通过参数、回调、@Link 或 @ObjectLink 传递状态,不使用全局变量代替清晰的数据流。
性能优化首先来自减少不必要刷新。派生数据不要重复存储,动画状态不要进入列表 key,长列表使用稳定标识,Canvas 只在数据或尺寸变化时重绘。其次是控制资源生命周期,页面不可见时停止高成本任务。最后才是微调阴影、渐变和绘制细节。这样的优先级能保证页面在功能增加后仍然可维护。
通过以上补充,可以看到 ArkUI 的声明式模式并非只让布局语法更简洁,它更重要的价值是把数据变化、界面刷新和交互反馈连接为可追踪链路。理解每个代码段读取什么状态、写入什么状态、影响哪些组件,才能真正掌握文章中多个 Tab、图表、弹窗和系统能力协同工作的原理。
十六、总结与展望
本平台以"安全哨兵"为产品定位,在深色安全蓝灰 + 警示橙的主题下,完整实现了物业安全巡检的七大功能模块。四大 HarmonyOS 6.1.1 前沿特性的深度融合是平台的技术核心:
Camera Kit 影随人动通过 VideoSession 的三步能力链(isControlCenterSupported → getSupportedEffectTypes → enableControlCenter),实现了巡检跟拍时人员始终居中的智能构图。这一能力解决了传统巡检取证中"自拍不到位"的痛点——巡检员无需手动调整取景框,系统自动跟踪人员移动,确保取证画面中巡检员始终处于画面中心。
Camera Kit 手动对焦三接口通过 PhotoSession 的 isFocusDistanceSupported → setFocusDistance → getFocusDistance 闭环,实现了铭牌近拍到机房全景的精确对焦控制,并通过设置值-读回值差值校验确保对焦生效。三档预设覆盖了巡检取证的三个典型景别,Slider 提供连续微调能力,FocusRecord 时间线保存了每次对焦操作的完整证据链。
Notification Kit 沙箱自定义铃声通过 buildWavBytes 生成正弦波 PCM 音频 → 写入 EL1 沙箱 filesDir → fileUri.getUriFromPath 转换 → 填入 NotificationRequest.sound 的完整链路,让不同等级告警拥有差异化铃声。这一能力突破了原来 sound 字段只能填 rawfile 资源文件名的限制,使应用可以动态生成铃声而无需预置音频资源。
Tabs 嵌套滚动通过 nestedScroll(TabsNestedScrollMode) 的 SELF_FIRST/SELF_ONLY 两种模式,让内层检查项列表滑到边缘后联动外层楼宇频道,实现巡检路径的自然流转。SwipeLog 时间轴完整记录了两层翻页事件,为 nestedScroll 的接力行为提供了可视化证据。
展望未来,本平台可在以下方向持续演进:一是引入 AI 图像识别能力,对取证照片自动识别消防设施状态和隐患类型;二是集成 HMS Core 地图服务,实现巡检路径的实时轨迹追踪和最优路线推荐;三是接入鸿蒙分布式能力,实现多设备协同巡检——手机取证、平板填写报告、智慧屏展示指挥大屏;四是利用 ArkUI 的动画能力增强交互反馈,如告警发布时的全屏震动效果和铃声波形可视化。随着 HarmonyOS 的持续演进,安全巡检平台将不断融入更多系统级能力,为智慧物业管理提供更强大的技术支撑。
附录:DevEco Studio 创建新项目与查看 SDK 版本
本章节演示如何使用 DevEco Studio 创建一个 HarmonyOS 新项目,并查看当前 IDE 已安装的 SDK 版本,适合作为其他技术博文的补充操作指南。
一、创建新项目
1.1 进入欢迎界面
启动 DevEco Studio 后,首先看到的是欢迎界面。左侧导航栏默认选中 “项目”,右侧提供三个主要入口:
- 新建项目:从头创建新项目
- 打开项目:打开本地已有项目
- 克隆仓库:从 Git 等版本控制拉取代码
点击 “新建项目” 按钮,进入项目创建向导。

1.2 选择项目模板
在弹出的"新建项目"对话框中,左侧分类标签提供了两种项目类型:
| 类型 | 说明 |
|---|---|
| 应用(Application) | 开发标准的 HarmonyOS 应用,具备完整的 Ability 生命周期 |
| 元服务(Atomic Service) | 开发轻量级的原子化服务,无需安装即可使用 |
选择 “应用” 标签后,右侧展示多种模板。对于大多数场景,推荐选择 “Empty Ability” —— 这是一个最基础的入门模板,仅包含 Hello World 功能,适合从零开始构建应用。

1.3 配置项目信息
点击 “下一步” 后,进入项目配置界面,需要填写以下核心参数:
| 配置项 | 示例值 | 说明 |
|---|---|---|
| 项目名称(Project name) | rollboat | 应用的项目名称,建议使用英文命名 |
| 包名(Bundle name) | com.rollboat.myapplication | 应用唯一标识,采用反向域名格式 |
| 保存路径(Save location) | D:\CodeFactory\rollboat | 项目本地存储路径,避免使用中文和空格 |
| 兼容 SDK(Compatible SDK) | 6.1.1(24) | 目标 HarmonyOS API 版本,点击"查看参考"可了解各版本差异 |
| 模块名称(Module name) | entry | 主模块名称,默认 entry 为应用入口模块 |
| 设备类型(Device types) | ☑ Phone | 勾选目标设备:Phone / Tablet / 2in1 / Car / Wearable / TV |
右侧预览区会实时展示当前模板的默认效果 —— 一个居中显示的 “Hello World” 文本。

1.4 完成创建
确认配置无误后,点击右下角 “完成” 按钮,IDE 将自动执行以下操作:
- 生成项目骨架(Stage 模型目录结构)
- 执行
ohpm install安装依赖 - 运行 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 应用的功能开发。
本文基于 DevEco Studio 6.1.1 Release 版本编写,不同版本界面可能存在细微差异。
更多推荐



所有评论(0)