一、技术前言
在智慧物业管理赛道,安全巡检始终是建筑运行保障的第一道防线。从消防通道堆物复查到配电间红外测温,从喷淋管网压力巡检到水泵启动试运行测试,从应急照明断电测试到电梯机房月度点检------每一项巡检任务都要求精确的流程管控、清晰的进度追踪和即时的异常告警能力。传统巡检应用长期面临三大痛点:取证影像模糊导致责任争议频发、告警铃声单一导致值班人员响应迟缓、巡检路径跨楼宇跨楼层导致界面交互割裂。
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 的手动对焦三接口 isFocusDistanceSupported、setFocusDistance、getFocusDistance 实现从铭牌近拍到机房全景的精确对焦控制。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' 是分割线色,低对比度不干扰内容。tabOn 与 orange 值相同,保持 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 接口仅包含 icon 和 label 两个字段,使用 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 ? '先内后外' : '仅内层';
}
modeLabel 和 modeShort 两个函数分别提供嵌套滚动模式的完整文案和短文案。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 框架感知------当 progress 或 status 在编辑弹窗中被修改后,任务清单列表中的对应卡片会自动刷新。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 存储检查项类别名,title 和 desc 分别为检查点标题和描述。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 区分外层楼宇和内层检查项,fromIdx 和 toIdx 记录切换前后的索引,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 是告警铃声条目实体。freq 和 duration 用于 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(任务页)。
弹窗状态组 :addModal、editModal、delModal 三个布尔值分别控制新建、编辑、删除三种弹窗的显示;editIdx 和 delIdx 记录当前操作的列表索引。
动画状态组 :breath 布尔值每秒翻转一次,驱动呼吸圆点透明度和柱状图波动;timer 存储定时器 ID。
Camera Kit 成员组 :previewController 是 XComponent 的控制器实例,cameraInput、previewOutput、videoSession、photoSession 四个相机核心对象声明为 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 并 beginConfig→addInput→addOutput→commitConfig。配置提交后调用 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 将自动执行以下操作:
- 生成项目骨架(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 语法:
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 版本编写,不同版本界面可能存在细微差异。