HarmonyOS 6.1.1 最新技术点 Map Kit 长按事件与 reliability 相关性评分:新能源汽车充电桩查找场景如何解决精准找桩与地图交互难题
一、技术前言

HarmonyOS ArkUI 框架是华为面向全场景多设备的应用开发框架,它采用 ArkTS 语言作为底层脚本支撑,通过声明式 UI 范式让开发者能够以一套代码同时适配手机、平板、车机、穿戴等多种终端设备。ArkUI 的核心思想是"状态驱动视图",开发者只需要声明数据状态与视图的映射关系,当状态发生变化时框架会自动完成差异计算并精准刷新受影响的组件节点,从而避免了传统命令式 UI 中繁琐的手动 DOM 操作。在 ArkUI 中,@Entry 标注入口组件、@Component 标注自定义组件、@State 标注组件内可变状态、@Builder 标注构建函数、@Observed 标注可观察数据类,这一套装饰器体系构成了声明式 UI 的骨架。对于复杂业务页面,ArkUI 还提供了 Stack、Column、Row、Scroll、List、ForEach 等布局与渲染容器组件,配合 linearGradient、borderRadius、padding 等样式属性,可以构建出层次丰富、交互细腻的现代化界面。

Map Kit 是华为为 HarmonyOS 提供的地图服务套件,它在 ArkUI 中以 MapComponent 组件的形式直接嵌入页面布局树,开发者可以通过 mapCommon.MapOptions 配置地图的初始中心点和缩放层级,通过 mapCommon.MarkerOptions 配置标注点的位置、可见性、锚点、旋转角等属性。MapComponent 接收两个核心参数:mapOptions(地图初始化参数)和 mapCallback(初始化回调),在回调中开发者可以拿到 map.MapComponentController 控制器实例,进而调用 addMarker、getEventManager 等方法完成标注添加与事件注册。Map Kit 的优势在于其与 HarmonyOS 系统底层的深度集成——地图渲染走系统原生图层,性能稳定、内存占用可控,并且能够与 site 模块(地点搜索模块)无缝联动,形成"搜索—展示—交互"的完整闭环。

在 HarmonyOS 6.1.1 版本中,Map Kit 的 site 模块对 searchByText 接口的返回类型 Site 进行了重要能力增强:新增了 reliability 相关性分数字段。该字段是一个取值范围为 [0, 1] 的浮点数,其中 1 表示搜索结果与用户输入关键字完全相关,0 表示完全不相关。在以往版本中,searchByText 虽然能够根据关键字返回地点列表,但开发者无法量化判断每条结果与用户意图的匹配程度——例如用户搜索"充电站",返回结果中可能既包含真正的充电桩站点,也可能包含"电池专卖店"这类名称中带"电"字但实际不提供充电服务的地点。reliability 字段的出现让开发者可以在 UI 层根据分数对结果进行二次排序、分级展示、过滤低质量结果,从而显著提升搜索体验的精准度,这在充电桩查找这类"结果质量直接影响用户出行决策"的场景中价值尤为突出。

同样是 HarmonyOS 6.1.1 版本,Map Kit 的 MapEventManager 事件管理器新增了两个长按监听接口:onMarkerLongClick / offMarkerLongClick(地图标记长按监听)和 onPoiLongClick / offPoiLongClick(地图 POI 长按监听)。此前 Map Kit 已经提供了 Marker 点击(onMarkerClick)、POI 点击(onPoiClick)、地图点击(onMapClick)等短按事件,但在充电桩这类需要"快速查看与深度操作并存"的业务中,短按通常用于触发导航或详情查看,而长按则更适合承载"收藏到常去地点"“标记为问题桩站”"发起群组分享"等次级操作。长按事件接口的加入,让地图交互从"单一点击"升级为"短按+长按"双层级,为复杂的业务场景提供了更丰富的手势空间。值得注意的是,offMarkerLongClick 与 offPoiLongClick 在不传参时会清除该类型的全部订阅,这种设计既支持精细化的单回调注销,也支持批量清空,灵活性很高。

新能源汽车充电服务是近年来快速崛起的民生刚需行业。截至 2026 年,全国新能源汽车保有量已突破 2400 万辆,而公共充电桩数量虽然也在增长,但分布不均、空闲状态难以及时获取、不同运营商功率与价格差异大等问题依然困扰着车主。一款优秀的充电桩查找应用需要解决三个核心痛点:其一是"找得到"——要能基于用户当前位置快速返回周边充电桩并按相关性排序;其二是"看得清"——要在地图上直观展示桩站位置与空闲状态,并支持丰富的交互操作;其三是"管得好"——要让用户能够收藏常去桩站、编辑备注、管理充电账单。本文剖析的"电桩通"应用正是围绕这三个痛点展开,它以深色主题(碳晶黑 #0B1210 + 电光绿 #2ED573 + 荧光黄 #FFD24E)塑造科技感与新能源调性,以四个差异化 Tab 页(充电/地图/搜索/我的)覆盖找桩、看桩、搜桩、管桩的全流程,并在地图与搜索两个 Tab 中分别落地了 HarmonyOS 6.1.1 的两大新特性。

从工程实现角度看,本应用还体现了一套成熟的"演示链路保障"设计思路。Map Kit 的 searchByText 接口依赖 AGC(AppGallery Connect)配置与网络连通,一旦在演示或开发环境缺失,接口会抛出 BusinessError。源码在 runSearch 方法中通过 try-catch 捕获异常,并在 catch 分支保留 Mock 数据与状态文案,保证页面始终有可展示的内容;同样地,mapCallback 内的 addMarker 也逐个 try-catch,单个标注失败不会阻断后续标注的添加。这种"主链路失败时回退演示数据"的容错策略,对于需要在多种环境下稳定运行的地图类应用具有很好的借鉴意义。
二、应用整体架构流程图
本应用以单页面(@Entry 组件)承载四个 Tab,整体架构围绕"状态驱动 + Builder 分发 + 弹窗叠加"三层组织。下图展示了从应用启动到用户交互的完整数据流与控制流:
三、颜色系统与主题设计
3.1 颜色系统接口定义
/** 主题色板接口:集中声明页面所有颜色字段(碳晶黑+电光绿+荧光黄深色系) */
interface ColorPalette {
bg: string;
card: string;
chip: string;
title: string;
sub: string;
text3: string;
green: string;
greenD: string;
greenL: string;
yellow: string;
red: string;
blue: string;
line: string;
tabOn: string;
mask: string;
codeBg: string;
}
这段代码定义了一个 ColorPalette 接口,它将整个页面用到的所有颜色字段集中声明为一个类型契约。这种"接口先行"的设计有三个明显优点:第一,它让颜色的语义用途一目了然——bg 是页面背景、card 是卡片背景、chip 是筛选标签背景、title/sub/text3 是三级文字色阶、green 系列是品牌主色及其明暗变体、yellow 是荧光黄、red 是警示红、blue 是操作蓝、line 是分割线、tabOn 是 Tab 选中色、mask 是弹窗遮罩、codeBg 是代码预览底色;第二,它强制开发者在使用颜色时必须通过字段名访问,避免了魔法字符串散落在各处导致的维护困难;第三,如果未来需要支持浅色主题或节日主题,只需要再定义一个实现 ColorPalette 接口的对象即可完成主题切换,扩展性良好。
3.2 深色主题色板常量
/** 深色主题色板常量(电桩通 · 碳晶黑 + 电光绿 + 荧光黄) */
const COLORS: ColorPalette = {
bg: '#0B1210',
card: '#121C17',
chip: '#1B2A21',
title: '#EAF7EE',
sub: '#9CC2A9',
text3: '#5E7A68',
green: '#2ED573',
greenD: '#1E9E54',
greenL: '#D9F8E6',
yellow: '#FFD24E',
red: '#FF6B7E',
blue: '#3D8BFF',
line: '#223A2C',
tabOn: '#2ED573',
mask: 'rgba(5,12,8,0.66)',
codeBg: '#0A1A12'
};
COLORS 常量是 ColorPalette 接口的深色主题实现。从色值可以看出一套精心设计的深色系:主背景 #0B1210 是一种极深的碳晶黑,带有微弱的绿色色偏,比纯黑更柔和、更符合新能源的"电力"调性;卡片背景 #121C17 比主背景略亮一个层级,用于在主背景上浮起卡片层次;筛选标签 #1B2A21 再亮一个层级,构成三级深色阶梯。文字色阶则反向设计:#EAF7EE 是带绿调的近白主文字、#9CC2A9 是中等亮度的副文字、#5E7A68 是较暗的三级文字,三级阶梯既保证了深色背景下的可读性,又通过色相统一(全部带绿调)强化了品牌一致性。#2ED573 电光绿是品牌主色,用于按钮、Tab 选中、进度条等强交互元素;#1E9E54 是其深色变体,用于渐变 Banner 的起始色;#FFD24E 荧光黄作为辅助强调色,用于 POI 相关的视觉区分;#FF6B7E 是带粉调的警示红,比纯红更现代;rgba(5,12,8,0.66) 是带透明度的遮罩色,保证弹窗背景仍可微透底层内容。整套色板在深色背景上既保证了对比度,又通过"绿主黄辅"的配色呼应了新能源与电力的行业属性。
四、常量定义与 Mock 数据
4.1 Tab 元数据与筛选标签
/** Tab 元数据接口:底部导航图标 + 标签 */
interface TabMeta {
icon: string;
label: string;
}
/** 底部导航 Tab 常量列表(4 Tab 单排) */
const TAB_LIST: TabMeta[] = [
{ icon: '⚡', label: '充电' },
{ icon: '🗺', label: '地图' },
{ icon: '🔍', label: '搜索' },
{ icon: '👤', label: '我的' }
];
/** 头部横滑筛选 chips 文案(充电桩筛选) */
const CATE_TAGS: string[] = ['全部', '快充', '慢充', '超充站', '免费停车', '24小时', '带休息室', '高速沿线'];
TabMeta 接口与 TAB_LIST 常量定义了底部导航栏的数据模型。使用 emoji 作为图标是一种轻量化的选择——在不引入图标资源文件的前提下就能实现视觉化的 Tab 识别,同时 emoji 本身具备跨平台一致性,无需为不同分辨率准备多套 PNG/SVG。四个 Tab 的顺序也体现了产品逻辑:充电是主业务入口排首位,地图是充电的视觉化延伸排第二,搜索是主动找桩的工具排第三,我的是个人中心排末位,符合"主功能→辅助功能→个人管理"的导航惯例。CATE_TAGS 则是充电桩筛选标签数组,它覆盖了用户找桩时最关心的八个维度:功率类型(快充/慢充/超充)、配套服务(免费停车/24小时/带休息室)、位置属性(高速沿线),这套标签既适用于头部筛选 chips 的横滑展示,也直接对应了后文搜索结果的可过滤维度。
4.2 城市中心点与 Marker 标注点
/** 城市中心点(地图初始化中心,Map Kit 搜索 location 参数) */
const CITY_CENTER: mapCommon.LatLng = { latitude: 31.2304, longitude: 121.4737 };
/** 地图标注点接口(充电桩站 Marker 群,长按事件的数据来源) */
interface SpotItem {
name: string; // 桩站名称
lat: number; // 纬度
lng: number; // 经度
tag: string; // 桩站类型标签
}
/** 充电桩站标注点 Mock 数据(6 个,围绕城市中心点散布) */
const MARKER_SPOTS: SpotItem[] = [
{ name: '电桩通·陆家嘴超充站', lat: 31.2397, lng: 121.4862, tag: '超充' },
{ name: '电桩通·人民广场快充站', lat: 31.2323, lng: 121.4691, tag: '快充' },
{ name: '电桩通·静安寺慢充站', lat: 31.2249, lng: 121.4451, tag: '慢充' },
{ name: '电桩通·徐家汇双子站', lat: 31.1946, lng: 121.4380, tag: '快充' },
{ name: '电桩通·虹桥枢纽站', lat: 31.1946, lng: 121.3200, tag: '高速' },
{ name: '电桩通·世纪公园站', lat: 31.2170, lng: 121.5500, tag: '24h' }
];
CITY_CENTER 是以上海为坐标中心的 mapCommon.LatLng 对象,它被两处复用:一是作为 MapOptions.position.target 传入 MapComponent 的初始化参数,让地图打开时直接定位到上海;二是作为 site.searchByText 的 location 参数,让搜索结果以上海为中心向周边辐射。这种"一处定义多处复用"的写法保证了地理坐标的单一数据源,避免不同模块对同一中心点定义不同导致的数据漂移。SpotItem 接口与 MARKER_SPOTS 数组则定义了地图上要标注的六个充电桩站点,它们围绕城市中心点散布在上海市各核心商圈,覆盖超充、快充、慢充、高速、24h 五种类型,这批数据将在 mapCallback 中被遍历并逐个 addMarker 添加到地图上,同时也是长按 Marker 事件触发后的数据同源——也就是说,用户长按地图上的某个充电桩 Marker 时,日志流中记录的经纬度正是来自这批 Mock 数据。
4.3 推荐桩站与功能清单 Mock 数据
/** 推荐桩站接口(充电 Tab 业务列表) */
interface PileRec {
icon: string; // 桩站 emoji
name: string; // 桩站名
dist: string; // 距离文本
power: string; // 功率文本
price: string; // 电价文本
free: number; // 空闲桩数
}
/** 推荐桩站 Mock 数据(3 条,头部渐变大卡下方列表) */
const PILE_RECS: PileRec[] = [
{ icon: '⚡', name: '陆家嘴超充站', dist: '800m', power: '480kW 液冷', price: '1.42元/度', free: 6 },
{ icon: '🔌', name: '人民广场快充站', dist: '1.2km', power: '120kW 直流', price: '1.28元/度', free: 12 },
{ icon: '🔋', name: '静安寺慢充站', dist: '1.9km', power: '7kW 交流', price: '0.98元/度', free: 23 }
];
/** 我的页功能清单条目接口 */
interface FuncItem {
icon: string; // 功能图标
label: string; // 功能名
value: string; // 状态/数值文本
}
/** 我的页功能清单 Mock 数据(8 条) */
const FUNC_LIST: FuncItem[] = [
{ icon: '⚡', label: '累计充电', value: '386 次 · 4217 度' },
{ icon: '💚', label: '碳减排', value: '相当于种树 86 棵' },
{ icon: '💳', label: '充电钱包', value: '余额 126.80 元' },
{ icon: '⭐', label: '收藏桩站', value: '12 座' },
{ icon: '🧾', label: '充电账单', value: '本月 8 笔' },
{ icon: '🗺', label: '常去地点', value: '陆家嘴 / 徐家汇' },
{ icon: '🔔', label: '桩站动态提醒', value: '已开启' },
{ icon: '⚙', label: '偏好设置', value: '优先快充' }
];
PileRec 与 PILE_RECS 服务于充电 Tab 头部下方的"附近推荐"区域,它精选了三条不同功率档位的桩站——480kW 液冷超充、120kW 直流快充、7kW 交流慢充,让用户一眼看到不同功率的价格与距离差异。FuncItem 与 FUNC_LIST 则服务于"我的"Tab 的功能清单,八条数据覆盖了累计充电统计、碳减排公益价值、钱包余额、收藏桩站、充电账单、常去地点、动态提醒、偏好设置等新能源汽车用户最关心的个人数据维度。值得注意的设计细节是 free(空闲桩数)字段——它是一个数值而非文本,这是为了在后文 freeColor 函数中根据数值大小做颜色分级(>10 绿色充足、>3 黄色正常、其余红色紧张),这种"数据驱动视觉"的写法比硬编码文本颜色更易于维护。
五、辅助函数与数据模型
5.1 reliability 相关性分数映射函数
/**
* reliability 相关性分数 → 等级标签/颜色映射
* 取值 [0,1]:≥0.8 高相关 / ≥0.5 中相关 / 其余低相关(Map Kit 6.1.1 新字段)
*/
function reliabilityScore(score: number): ScoreLevel {
if (score >= 0.8) {
return { label: '高相关', color: COLORS.green };
}
if (score >= 0.5) {
return { label: '中相关', color: COLORS.yellow };
}
return { label: '低相关', color: COLORS.red };
}
/** 相关性等级接口(分数条旁的标签) */
interface ScoreLevel {
label: string; // 等级文案
color: string; // 等级颜色
}
reliabilityScore 是本应用最核心的辅助函数之一,它将 HarmonyOS 6.1.1 新增的 Site.reliability 浮点分数映射为人类可读的"高相关/中相关/低相关"等级标签与对应颜色。映射规则采用两段阈值:≥0.8 归为高相关(绿色 #2ED573,与品牌主色一致,表示可以放心前往)、≥0.5 归为中相关(荧光黄 #FFD24E,表示需要进一步核实)、其余归为低相关(警示红 #FF6B7E,表示大概率不是用户想要的充电站)。这种三段式分级既符合人眼对"绿黄红"三色的直觉认知,又与充电桩场景的业务语义高度匹配——高相关的结果可以直接导航,中相关的结果需要用户查看地址详情后决定,低相关的结果则建议用户重新搜索或换关键字。函数返回的 ScoreLevel 对象同时包含文案与颜色,调用方在 UI 渲染时一次性拿到"显示什么文字+用什么颜色"两个信息,避免了在 Builder 中重复书写条件判断逻辑。
5.2 空闲桩数颜色映射函数
/** 空闲桩数颜色映射:>10 充足绿 / >3 正常黄 / 其余紧张红 */
function freeColor(free: number): string {
if (free > 10) { return COLORS.green; }
if (free > 3) { return COLORS.yellow; }
return COLORS.red;
}
freeColor 函数与 reliabilityScore 设计思路一致,但服务于另一个业务维度——充电桩的空闲状态。它将空闲桩数分为三档:大于 10 为充足(绿色,用户可以慢悠悠前往)、3 到 10 之间为正常(黄色,建议尽快前往)、3 以下为紧张(红色,可能需要排队)。在充电桩场景中,空闲桩数是用户决策"去不去这个站"的关键因素,甚至比距离更重要——一个 800m 外但只剩 2 个空闲桩的站点,可能不如一个 1.5km 外但有 15 个空闲桩的站点靠谱。将这个数值映射为颜色后,用户在列表视图中无需细读数字就能通过颜色快速识别哪些站点值得前往,这体现了"视觉先于文字"的移动端信息呈现原则。
5.3 PileItem 可观察数据模型
/** 桩站条目(充电 Tab 推荐列表) */
@Observed export class PileItem {
icon: string; // 桩站 emoji 图标
name: string; // 桩站名
dist: string; // 距离文本
power: string; // 功率文本
price: string; // 电价文本
free: number; // 空闲桩数
note: string; // 用户备注(可编辑)
constructor(icon: string, name: string, dist: string, power: string,
price: string, free: number, note: string) {
this.icon = icon;
this.name = name;
this.dist = dist;
this.power = power;
this.price = price;
this.free = free;
this.note = note;
}
}
/** 桩站列表 Mock 数据(7 条) */
const PILE_LIST: Array<PileItem> = [
new PileItem('⚡', '陆家嘴超充站', '800m', '480kW 液冷', '1.42元/度', 6, '公司常驻'),
new PileItem('🔌', '人民广场快充站', '1.2km', '120kW 直流', '1.28元/度', 12, '周末首选'),
new PileItem('🔋', '静安寺慢充站', '1.9km', '7kW 交流', '0.98元/度', 23, '过夜慢充'),
new PileItem('⚡', '徐家汇双子站', '2.4km', '250kW 直流', '1.35元/度', 3, '排队预警'),
new PileItem('🔌', '虹桥枢纽站', '6.8km', '160kW 直流', '1.52元/度', 9, '出差补能'),
new PileItem('🔋', '世纪公园站', '3.5km', '60kW 直流', '1.18元/度', 18, '遛娃顺便充'),
new PileItem('⚡', '张江科学城站', '8.2km', '180kW 直流', '1.25元/度', 15, '客户拜访')
];
PileItem 类使用 @Observed 装饰器标注,这是 ArkUI 的可观察数据类装饰器,它让该类的实例属性变化能够被 @State 数组感知到。在充电 Tab 中,pileList 是一个 Array<PileItem> 类型的 @State 状态,当用户编辑某条桩站的备注后,直接修改 this.pileList[this.editIdx].note 并不会触发列表刷新,因为 @State 对数组的深层属性变化默认不敏感——所以后文 updatePile 方法中会通过 this.pileList = this.pileList.slice() 整体替换数组引用来强制刷新。这种"整体引用替换"的写法是 ArkUI 中处理 @Observed 数组局部更新的常见模式。PileItem 相比 PileRec 多了一个 note 字段,用于承载用户自定义备注(如"公司常驻"“周末首选”“排队预警”),这让桩站列表从纯展示型升级为可编辑型,用户可以根据自己的出行场景为每个桩站打上个性化标签。
5.4 SearchRecord 数据模型(reliability 字段载体)
/** 搜索结果条目(★ Map Kit 6.1.1 reliability 字段数据载体) */
@Observed export class SearchRecord {
name: string; // 地点名称(site.name)
address: string; // 格式化地址(site.formatAddress)
distance: number; // 直线距离米(site.distance)
reliability: number; // ★ 相关性分数(site.reliability,[0,1])
time: string; // 记录时间文案
constructor(name: string, address: string, distance: number,
reliability: number, time: string) {
this.name = name;
this.address = address;
this.distance = distance;
this.reliability = reliability;
this.time = time;
}
}
/** 搜索结果 Mock 数据(6 条,reliability 覆盖高/中/低三档) */
const SEARCH_RECORDS: Array<SearchRecord> = [
new SearchRecord('陆家嘴超充站', '上海市浦东新区陆家嘴环路 1000 号', 812, 0.97, '刚刚'),
new SearchRecord('人民广场充电点', '上海市黄浦区人民大道 200 号', 1240, 0.88, '刚刚'),
new SearchRecord('静安寺充电停车库', '上海市静安区南京西路 1686 号', 1930, 0.72, '刚刚'),
new SearchRecord('充电桩便民服务点', '上海市徐汇区漕溪北路 88 号', 2410, 0.54, '刚刚'),
new SearchRecord('电桩通服务网点咨询处', '上海市长宁区愚园路 1258 号', 3260, 0.31, '刚刚'),
new SearchRecord('电池专卖(非充电)', '上海市普陀区长寿路 433 号', 4780, 0.12, '刚刚')
];
SearchRecord 是 HarmonyOS 6.1.1 reliability 新字段在应用层的数据载体。它的字段设计与 site.Site 类型高度对应——name 对应 site.name、address 对应 site.formatAddress、distance 对应 site.distance、reliability 对应 site.reliability。这种"应用层数据模型与 SDK 类型字段一一对应"的设计,让 runSearch 方法中的数据转换逻辑非常直观:遍历 site.Site 数组,逐个字段读取并构造 SearchRecord。Mock 数据六条覆盖了 reliability 的三档分布:0.97/0.88 是高相关(真正的充电站)、0.72 是中相关(充电停车库,相关但可能非首选)、0.54 是中相关临界(充电桩便民服务点,需要核实)、0.31/0.12 是低相关(电桩通服务网点咨询处、电池专卖店,名称含"电"但实际不提供充电服务)。这套 Mock 数据的精妙之处在于,它模拟了真实搜索中"关键字命中但语义不匹配"的典型噪声,让 reliability 字段的过滤价值得以直观展现。
5.5 EventLog 数据模型(长按事件载体)
/** 长按事件日志条目(★ MapEventManager 长按监听数据载体) */
@Observed export class EventLog {
type: string; // 事件类型:'Marker' / 'POI'
name: string; // Marker ID 或 POI 名称
lat: number; // 纬度
lng: number; // 经度
time: string; // 事件时间文案
constructor(type: string, name: string, lat: number, lng: number, time: string) {
this.type = type;
this.name = name;
this.lat = lat;
this.lng = lng;
this.time = time;
}
}
/** 长按事件日志 Mock 数据(2 条,演示日志流形态) */
const EVENT_LOGS: Array<EventLog> = [
new EventLog('POI', '东方明珠广播电视塔', 31.2397, 121.4998, '演示事件'),
new EventLog('Marker', '#0', 31.2397, 121.4862, '演示事件')
];
EventLog 是 HarmonyOS 6.1.1 MapEventManager 长按监听接口在应用层的数据载体。它的 type 字段区分两种长按事件——'Marker' 表示用户长按了地图上的充电桩标注点(由 addMarker 添加的自定义标注),此时 name 存储的是 Marker ID(如 #0);'POI' 表示用户长按了地图上的 POI 地点(地图自带的兴趣点,如东方明珠、商场等),此时 name 存储的是 POI 名称。lat/lng 是事件触发的地理坐标,time 是事件时间文案。Mock 数据预置了两条演示事件,让用户进入地图 Tab 时就能看到日志流的形态,理解"长按可以记录事件"这一交互——这种"预置演示数据引导用户发现功能"的设计,比空列表更友好。@Observed 装饰器保证了当 eventLogs 数组通过 unshift 插入新事件时,UI 能够正确刷新。
六、组件主体与状态管理
6.1 页面组件与 State 状态声明
/** 1131 电桩通 · 新能源充电桩查找主页面 */
@Entry
@Component
struct Page1131 {
/** 当前选中 Tab 索引 */
@State currentTab: number = 0;
/** 头部筛选 chips 选中索引 */
@State cateIdx: number = 0;
/** 收藏桩站弹窗开关 */
@State addModal: boolean = false;
/** 编辑备注弹窗开关 */
@State editModal: boolean = false;
/** 删除收藏确认弹窗开关 */
@State delModal: boolean = false;
/** 当前编辑的桩站索引 */
@State editIdx: number = 0;
/** 当前删除的桩站索引 */
@State delIdx: number = 0;
/** 桩站列表数据 */
@State pileList: Array<PileItem> = PILE_LIST;
/** 我的页功能清单数据 */
@State funcList: FuncItem[] = FUNC_LIST;
Page1131 是应用的入口组件,由 @Entry 与 @Component 装饰器共同标注。@Entry 表示该组件是页面入口,会被加载为独立页面;@Component 表示这是一个自定义组件,可以被其他组件引用。组件内部声明的八个 @State 状态变量构成了页面的"数据中枢":currentTab 控制 Tab 切换、cateIdx 控制筛选标签选中、三个 xxxModal 布尔值分别控制三个弹窗的显隐、editIdx/delIdx 记录当前操作的桩站索引、pileList/funcList 是两个列表数据源。ArkUI 的 @State 装饰器会监听变量值的变化并自动触发依赖该状态的 Builder 重新构建——例如当 currentTab 从 0 变为 1 时,build 方法中的 if (this.currentTab === 0) 分支会自动从 tabCharge 切换到 tabMap,无需开发者手动调用刷新。这种"状态变更即视图刷新"的机制是声明式 UI 的核心红利。
6.2 Map Kit 状态声明(6.1.1 特性支撑)
// --- Map Kit 状态(6.1.1 特性:搜索 reliability + 长按事件) ---
/** 地图初始化参数(非可选并给默认值,避免组件参数传 undefined) */
private mapOptions: mapCommon.MapOptions = {
position: { target: CITY_CENTER, zoom: 13 }
};
/** 地图初始化回调(aboutToAppear 中赋值) */
private mapCallback?: AsyncCallback<map.MapComponentController>;
/** 地图控制器(回调中获取,添加 Marker 用) */
private mapController?: map.MapComponentController;
/** 地图事件管理器(回调中获取,长按监听注册用) */
private mapEventManager?: map.MapEventManager;
/** Marker 长按监听开关 */
@State markerListenOn: boolean = true;
/** POI 长按监听开关 */
@State poiListenOn: boolean = true;
/** 长按事件日志流(unshift 置顶) */
@State eventLogs: Array<EventLog> = EVENT_LOGS;
/** 搜索关键字输入值 */
@State queryInput: string = '充电站';
/** 搜索状态文案 */
@State searchState: string = '待搜索 · 演示数据';
/** 搜索结果列表(site.searchByText 结果数据源) */
@State searchRecords: Array<SearchRecord> = SEARCH_RECORDS;
/** 收藏弹窗:桩站名输入 */
@State formName: string = '';
/** 收藏弹窗:地址输入 */
@State formAddr: string = '';
/** 编辑弹窗:备注输入 */
@State editNote: string = '';
这段状态声明集中体现了 HarmonyOS 6.1.1 两大新特性在应用层的支撑结构。mapOptions 是 MapComponent 的初始化参数,设置了 target 为城市中心点、zoom 为 13(街道级视图),它被声明为 private 而非 @State,因为地图初始化参数在组件生命周期内不需要变化。mapCallback、mapController、mapEventManager 三个变量构成了地图能力的"三段式"获取链路:先有 mapCallback 回调、回调中拿到 mapController、再从 controller 获取 mapEventManager——这种链式获取是 Map Kit 的标准用法。markerListenOn/poiListenOn 两个布尔状态对应后文的两个 Toggle 开关,控制长按监听的动态注册与注销。eventLogs 是长按事件的日志流数据源,queryInput/searchState/searchRecords 三者服务于搜索 Tab,formName/formAddr/editNote 服务于弹窗表单。可以看到,状态声明从"UI 交互状态"到"地图能力状态"到"业务数据状态"分层清晰,便于维护。
七、地图初始化与长按事件监听(6.1.1 特性一)
7.1 setupMapCallback 回调主体
/**
* 地图初始化:controller → eventManager → Marker 群 → 6.1.1 双长按监听
* 必须在 mapCallback 的 err 为空分支内注册监听(controller 就绪后才有管理器)
*/
setupMapCallback() {
this.mapCallback = async (err: BusinessError, mapController: map.MapComponentController) => {
if (err) {
console.error(`Map init failed, code: ${err.code}, message: ${err.message}`);
return;
}
this.mapController = mapController;
this.mapEventManager = mapController.getEventManager();
setupMapCallback 方法的核心职责是组装 mapCallback 回调函数,该回调会在 MapComponent 初始化完成后被系统调用。回调签名为 AsyncCallback<map.MapComponentController>,即一个接收 BusinessError 和 MapComponentController 两参数的异步函数。方法体第一行检查 err 是否存在——如果地图初始化失败(例如设备不支持 Map Kit、AGC 配置缺失等),err 会携带错误码与错误信息,此时通过 console.error 记录日志并 return 提前退出,避免后续对 mapController 的访问导致空指针异常。这种"错误优先返回"的防御式编程是异步回调中的常见且推荐的模式。当 err 为空时,说明地图初始化成功,此时将 mapController 保存到组件实例变量,再调用 mapController.getEventManager() 获取事件管理器实例——这是注册长按监听的前提,因为 onMarkerLongClick 等方法都挂在 MapEventManager 上而非 MapComponentController 上。注释中特别强调"必须在 err 为空分支内注册监听",这是因为 controller 就绪后才有 eventManager,提前注册会报空引用。
7.2 Marker 批量添加
// 批量添加充电桩 Marker(addMarker 返回 Promise,逐个 await + try-catch)
for (const spot of MARKER_SPOTS) {
const markerOptions: mapCommon.MarkerOptions = {
position: { latitude: spot.lat, longitude: spot.lng },
clickable: true,
visible: true,
rotation: 0,
zIndex: 0,
alpha: 1,
anchorU: 0.5,
anchorV: 1,
draggable: false,
flat: false
};
try {
await this.mapController.addMarker(markerOptions);
} catch (e) {
console.error(`addMarker failed: ${(e as BusinessError).message}`);
}
}
在获取到 mapController 后,代码遍历 MARKER_SPOTS 数组(六个充电桩站点),为每个站点构造 mapCommon.MarkerOptions 标注参数并调用 addMarker 添加到地图上。MarkerOptions 的字段设置体现了标注的视觉与交互细节:position 是标注的经纬度位置;clickable: true 让标注可点击(为长按监听铺垫);visible: true 让标注默认可见;rotation: 0 不旋转;zIndex: 0 层级为 0;alpha: 1 不透明;anchorU: 0.5 和 anchorV: 1 是标注锚点——anchorU 为 0.5 表示水平居中、anchorV 为 1 表示锚点在标注图像的底部中心,这是地图标注的标准锚点设置,让标注的"针尖"精准指向经纬度坐标;draggable: false 不可拖拽;flat: false 不平贴地图(保持立体感)。addMarker 返回的是 Promise,所以使用 await 等待完成;每个标注的添加都包裹在独立的 try-catch 中,某个标注添加失败只记录日志不中断循环,保证了批量添加的健壮性。
7.3 onMarkerLongClick 监听注册(6.1.1 新特性)
// ★ 6.1.1 新特性·事件一:监听地图标记 Marker 的长按
this.mapEventManager.onMarkerLongClick((marker: map.Marker) => {
const pos: mapCommon.LatLng = marker.getPosition();
this.eventLogs.unshift(new EventLog('Marker', `#${marker.getId()}`,
pos.latitude, pos.longitude, '刚刚'));
});
这是 HarmonyOS 6.1.1 新增的 onMarkerLongClick 接口的实际调用。该方法接收一个回调函数,回调参数是 map.Marker 类型——即被长按的标注对象。在回调内部,通过 marker.getPosition() 获取标注的经纬度坐标,通过 marker.getId() 获取标注的唯一 ID(在 addMarker 时由系统分配,通常从 0 递增),然后构造一个 EventLog 对象——type 为 'Marker'、name 为 `#${marker.getId()}`(如 #0、#1)、lat/lng 为标注坐标、time 为 '刚刚'——并通过 this.eventLogs.unshift(...) 将新事件插入到日志流数组的最前面(置顶展示)。unshift 的选择是有讲究的:日志流是按时间倒序展示的,最新的事件应该出现在最顶部,这样用户长按一个 Marker 后无需滚动就能立即看到刚才触发的事件被记录。由于 eventLogs 是 @State 状态,unshift 后 ArkUI 会自动刷新日志流列表的渲染。这段代码是整个应用"地图长按交互"能力的核心落地。
7.4 onPoiLongClick 监听注册(6.1.1 新特性)
// ★ 6.1.1 新特性·事件二:监听地图 POI 的长按(参数是 mapCommon.Poi)
this.mapEventManager.onPoiLongClick((poi: mapCommon.Poi) => {
this.eventLogs.unshift(new EventLog('POI', poi.name,
poi.position.latitude, poi.position.longitude, '刚刚'));
});
};
}
紧接着 onMarkerLongClick,代码注册了 HarmonyOS 6.1.1 的另一个新接口 onPoiLongClick。POI(Point of Interest,兴趣点)与 Marker 的区别在于:Marker 是开发者通过 addMarker 主动添加的自定义标注,而 POI 是地图底图自带的兴趣点(如地标建筑、商场、公园等)。onPoiLongClick 的回调参数是 mapCommon.Poi 类型,与 map.Marker 的字段结构略有不同——Poi 直接有 name 字段(地点名称)和 position 字段(经纬度),而 Marker 需要通过 getId() 和 getPosition() 方法获取。在回调内部,构造的 EventLog 的 type 为 'POI'、name 为 poi.name(如"东方明珠广播电视塔")、坐标来自 poi.position。两种长按事件共享同一个 eventLogs 日志流,但通过 type 字段和不同的 emoji 图标(Marker 用 📍、POI 用 🏷)在 UI 上做出区分,让用户能一眼分辨事件来源。这种"双事件共享日志流但视觉区分"的设计既统一了数据结构又保留了来源可辨识性。
7.5 长按监听开关切换
/** Marker 长按监听开关切换(off 不传参 = 清除该类型全部订阅) */
toggleMarkerListen() {
if (!this.mapEventManager) {
return;
}
if (this.markerListenOn) {
this.mapEventManager.offMarkerLongClick();
} else {
this.mapEventManager.onMarkerLongClick((marker: map.Marker) => {
const pos: mapCommon.LatLng = marker.getPosition();
this.eventLogs.unshift(new EventLog('Marker', `#${marker.getId()}`,
pos.latitude, pos.longitude, '刚刚'));
});
}
this.markerListenOn = !this.markerListenOn;
}
/** POI 长按监听开关切换(off 不传参 = 清除该类型全部订阅) */
togglePoiListen() {
if (!this.mapEventManager) {
return;
}
if (this.poiListenOn) {
this.mapEventManager.offPoiLongClick();
} else {
this.mapEventManager.onPoiLongClick((poi: mapCommon.Poi) => {
this.eventLogs.unshift(new EventLog('POI', poi.name,
poi.position.latitude, poi.position.longitude, '刚刚'));
});
}
this.poiListenOn = !this.poiListenOn;
}
toggleMarkerListen 与 togglePoiListen 两个方法分别对应地图 Tab 中的两个 Toggle 开关,让用户可以动态开启或关闭长按监听。两个方法的逻辑结构完全对称:首先检查 mapEventManager 是否存在(防御式编程,避免在地图未初始化时调用报错),然后根据当前开关状态决定是注销还是注册监听。以 toggleMarkerListen 为例,当 markerListenOn 为 true(当前开启)时,调用 offMarkerLongClick() 注销监听——注意这里 off 方法不传任何参数,这是 HarmonyOS 6.1.1 的设计:不传参时清除该类型的全部订阅回调,传具体回调函数时只清除指定回调。当 markerListenOn 为 false(当前关闭)时,重新调用 onMarkerLongClick 注册与初始化时相同的回调逻辑。最后翻转 markerListenOn 状态。这种"开关动态控制监听"的能力在实际业务中很有价值——例如用户在专注浏览地图时可以关闭监听避免日志流频繁刷新干扰,需要记录地点时再开启。
八、关键字搜索与 reliability 字段读取(6.1.1 特性二)
8.1 runSearch 方法主体
/**
* ★ 6.1.1 新特性·搜索:关键字搜索 searchByText → Site 数组
* 读取 Site.reliability 相关性分数(可选字段,?? 兜底 0)
* 无 AGC 配置/无网络时抛 BusinessError,catch 保留 Mock 数据保证演示链路
*/
async runSearch() {
this.searchState = '搜索中…';
const params: site.SearchByTextParams = {
query: this.queryInput,
location: CITY_CENTER,
radius: 5000,
language: 'zh'
};
runSearch 是应用层调用 HarmonyOS 6.1.1 site.searchByText 接口的封装方法,它是整个搜索 Tab 的业务核心。方法首先将 searchState 状态文案更新为"搜索中…“,让用户立即得到反馈——这种"即时状态反馈"是移动端搜索体验的基本要求,避免用户在等待网络返回时感到困惑。接着构造 site.SearchByTextParams 搜索参数对象:query 是用户在搜索框输入的关键字(默认"充电站”)、location 是搜索中心点(复用 CITY_CENTER)、radius 是搜索半径 5000 米(覆盖城市级周边范围)、language 是返回结果语言为中文。这四个参数共同决定了搜索结果的范围与语言,是 Map Kit 地点搜索的标准参数集。
8.2 searchByText 调用与 reliability 读取
try {
const result: site.SearchByTextResult = await site.searchByText(params);
const sites: Array<site.Site> = result.sites ?? [];
if (sites.length === 0) {
this.searchState = '无结果 · 保留演示数据';
return;
}
const records: Array<SearchRecord> = [];
for (const s of sites) {
records.push(new SearchRecord(
s.name ?? '未命名地点',
s.formatAddress ?? '暂无地址',
s.distance ?? 0,
s.reliability ?? 0,
'刚刚'));
}
this.searchRecords = records;
this.searchState = `返回 ${sites.length} 条结果`;
} catch (e) {
const err = e as BusinessError;
this.searchState = `搜索失败(${err.code}) · 保留演示数据`;
}
}
这段代码是整个应用"reliability 特性"落地的关键所在。调用 await site.searchByText(params) 获取 SearchByTextResult 结果对象,其 sites 字段是 Array<site.Site> 类型——HarmonyOS 6.1.1 为每个 Site 实例新增了 reliability 字段。由于 sites 可能为空(搜索无结果),使用 ?? [] 空数组兜底;如果确实为空,更新状态文案为"无结果 · 保留演示数据"并返回,此时 searchRecords 仍保留之前的 Mock 数据,页面不会变空。当有结果时,遍历 sites 数组,逐个读取 Site 的字段并构造 SearchRecord——这里每个字段的读取都使用了 ?? 空值合并运算符兜底:s.name ?? '未命名地点'、s.formatAddress ?? '暂无地址'、s.distance ?? 0、s.reliability ?? 0。这种兜底设计非常重要,因为 reliability 是 6.1.1 新增字段,在旧版 SDK 或某些返回场景中可能为 undefined,使用 ?? 0 可以让分数缺失时默认为 0(低相关),避免 UI 渲染时出现 NaN。构造完成后,将 records 赋值给 this.searchRecords 触发列表刷新,并更新状态文案为"返回 N 条结果"。try-catch 块捕获 BusinessError 异常——当无 AGC 配置或无网络时,searchByText 会抛出错误,catch 分支更新状态文案为"搜索失败(错误码) · 保留演示数据",同时 searchRecords 不被覆盖,仍展示 Mock 数据。这种"失败时保留演示数据"的容错策略,保证了应用在各种环境下都有可展示的内容。
8.3 弹窗业务方法
/** 打开编辑备注弹窗(回填当前桩站备注) */
openEditPile(idx: number) {
this.editIdx = idx;
this.editNote = this.pileList[idx].note;
this.editModal = true;
}
/** 保存收藏桩站(空名兜底默认演示桩站) */
savePile() {
const name = this.formName === '' ? '电桩通·新收藏站' : this.formName;
const addr = this.formAddr === '' ? '上海市浦东新区(地图选点)' : this.formAddr;
this.pileList.unshift(new PileItem('⭐', name, '待定位', '120kW 直流', '1.30元/度', 8, addr));
this.formName = '';
this.formAddr = '';
this.addModal = false;
}
/** 保存编辑备注(整体刷新数组引用以刷新列表) */
updatePile() {
if (this.editIdx >= 0 && this.editIdx < this.pileList.length) {
if (this.editNote !== '') {
this.pileList[this.editIdx].note = this.editNote;
}
this.pileList = this.pileList.slice();
}
this.editModal = false;
}
/** 删除收藏桩站(确认弹窗回调) */
delPile() {
if (this.delIdx >= 0 && this.delIdx < this.pileList.length) {
this.pileList.splice(this.delIdx, 1);
}
this.delModal = false;
}
/** 生命周期:初始化地图回调(监听注册在 mapCallback 内完成) */
aboutToAppear() {
this.setupMapCallback();
}
这组方法构成了弹窗系统的业务逻辑层。openEditPile 在打开编辑弹窗前,将当前桩站的备注回填到 editNote 输入框,让用户看到原备注再修改——这是编辑类弹窗的标准交互。savePile 在保存收藏桩站时,对空名和空地址做了默认值兜底(“电桩通·新收藏站”“上海市浦东新区(地图选点)”),避免用户不填内容就提交导致空数据显示;新桩站以 ⭐ 图标和 unshift 插入到列表最前面,让用户立即看到刚添加的项。updatePile 是 ArkUI 中处理 @Observed 数组局部更新的典型写法:直接修改 this.pileList[this.editIdx].note 后,由于 @State 对数组元素的属性变化不敏感,必须通过 this.pileList = this.pileList.slice() 创建一个新数组引用赋值给 pileList,才能触发 ForEach 重新渲染。delPile 使用 splice 删除指定索引的元素,splice 会原地修改数组并触发 @State 刷新。aboutToAppear 是组件生命周期钩子,在组件出现前调用 setupMapCallback 完成地图回调的组装,保证 MapComponent 渲染时 mapCallback 已就绪。
九、UI 构建与布局
9.1 build 主构建方法
/** 页面主构建:Stack 包裹主内容与三层弹窗 */
build() {
Stack() {
Column() {
this.headerMain()
Divider().strokeWidth(1).color(COLORS.line)
Scroll() {
Column() {
if (this.currentTab === 0) {
this.tabCharge()
} else if (this.currentTab === 1) {
this.tabMap()
} else if (this.currentTab === 2) {
this.tabSearch()
} else {
this.tabMine()
}
}
.padding({ left: 14, right: 14, top: 12, bottom: 12 })
}
.layoutWeight(1)
.scrollBar(BarState.Off)
this.tabBar()
}
.width('100%')
.height('100%')
if (this.addModal) {
this.panelAdd(() => {
this.addModal = false;
})
}
if (this.editModal) {
this.panelEdit(() => {
this.editModal = false;
})
}
if (this.delModal) {
this.panelDel(() => {
this.delModal = false;
})
}
}
.width('100%')
.height('100%')
.backgroundColor(COLORS.bg)
}
build 方法是 ArkUI 组件的渲染入口,它声明了整个页面的布局结构。最外层是 Stack(层叠布局),它的作用是让弹窗能够覆盖在主内容之上——Stack 内先放主内容 Column,再根据三个 xxxModal 状态条件性地叠加三个弹窗,后声明的元素在 Stack 中层级更高,所以弹窗会自然覆盖在主内容之上。主内容 Column 的结构是"头部 + 分割线 + 可滚动内容区 + 底部 Tab 栏"三段式:headerMain() 渲染头部渐变 Banner 与筛选 chips;Divider 是一条分割线;Scroll 包裹的 Column 是可滚动的 Tab 内容区,通过 layoutWeight(1) 占满中间剩余高度,scrollBar(BarState.Off) 隐藏滚动条让视觉更干净;tabBar() 渲染底部导航。Tab 内容区内部用 if-else if-else 根据 currentTab 状态分发到四个 @Builder 函数——这是 ArkUI 中实现 Tab 切换的轻量方式,相比 Tabs 容器更灵活可控,但需要开发者自己管理切换状态。三个弹窗的显隐通过 if (this.xxxModal) 条件渲染控制,每个弹窗都接收一个 onClose 回调用于关闭自身。
9.2 headerMain 头部渐变 Banner
/** 头部:渐变 Banner(电量+续航)+ 筛选 chips 横滑 */
@Builder
headerMain() {
Column({ space: 12 }) {
// 顶部渐变 Banner:电量环 + 续航文案 + 扫码充电入口
Column({ space: 10 }) {
Row({ space: 12 }) {
Column({ space: 2 }) {
Text('62%').fontSize(30).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
Text('当前电量').fontSize(10).fontColor(COLORS.sub)
}
.alignItems(HorizontalAlign.Start)
Column({ space: 6 }) {
Row({ space: 6 }) {
Text('🚗').fontSize(14)
Text('可续航 248km').fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
}
Row({ space: 6 }) {
Text('⚡').fontSize(12)
Text('最近桩站 800m · 空闲 6 桩').fontSize(11).fontColor(COLORS.sub)
}
Row({ space: 6 }) {
Text('🕐').fontSize(12)
Text('预计 38 分钟充满 80%').fontSize(11).fontColor(COLORS.sub)
}
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
}
.width('100%')
Row({ space: 8 }) {
Text('🔌 扫码充电').fontSize(12).fontColor(COLORS.bg).fontWeight(FontWeight.Bold)
.padding({ left: 14, right: 14, top: 8, bottom: 8 })
.borderRadius(16).backgroundColor(COLORS.green)
Text('🗺 地图找桩').fontSize(12).fontColor(COLORS.green)
.padding({ left: 14, right: 14, top: 8, bottom: 8 })
.borderRadius(16).backgroundColor(COLORS.chip)
.onClick(() => { this.currentTab = 1; })
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
}
.padding(14)
.borderRadius(14)
.linearGradient({
angle: 135,
colors: [[COLORS.greenD, 0.0], [COLORS.card, 0.7]]
})
headerMain Builder 构建了页面的头部区域,它由两部分组成:顶部渐变 Banner 和筛选 chips 横滑条。渐变 Banner 是一个 Column,通过 linearGradient 属性设置了 135 度角的对角线渐变——从 COLORS.greenD(#1E9E54 深绿)起始,到 COLORS.card(#121C17 卡片背景色)在 70% 位置结束。这种"深绿到深黑"的渐变让 Banner 既呼应了品牌电光绿色调,又保持了深色主题的沉稳。Banner 内部左侧是大字号"62%"当前电量百分比与"当前电量"小字标签,右侧三行信息分别展示可续航里程、最近桩站距离与空闲数、预计充满时间——这三行信息覆盖了新能源汽车用户最关心的三个即时状态:还能跑多远、最近桩在哪、要充多久。Banner 底部是两个胶囊按钮:"扫码充电"用绿色实底(COLORS.bg 深底配 COLORS.green 绿底反转,主操作按钮的常见设计),"地图找桩"用绿字配 COLORS.chip 深绿底,点击后切换到地图 Tab。这两个按钮构成了头部到核心功能的快捷入口。
9.3 headerMain 筛选 chips 横滑
// 筛选 chips 横滑
Scroll() {
Row({ space: 8 }) {
ForEach(CATE_TAGS, (tag: string, idx: number) => {
Text(tag)
.fontSize(11)
.fontColor(this.cateIdx === idx ? COLORS.bg : COLORS.sub)
.padding({ left: 12, right: 12, top: 6, bottom: 6 })
.borderRadius(14)
.backgroundColor(this.cateIdx === idx ? COLORS.green : COLORS.chip)
.onClick(() => { this.cateIdx = idx; })
}, (tag: string) => tag)
}
}
.scrollable(ScrollDirection.Horizontal)
.scrollBar(BarState.Off)
.width('100%')
}
.padding({ left: 14, right: 14, top: 12, bottom: 8 })
.width('100%')
}
筛选 chips 区域使用 Scroll + Row 的组合实现横向滑动。Scroll 设置了 scrollable(ScrollDirection.Horizontal) 让滚动方向为水平,scrollBar(BarState.Off) 隐藏滚动条。Row 内通过 ForEach 遍历 CATE_TAGS 数组渲染八个筛选标签,每个标签是一个 Text 组件,根据 cateIdx === idx 判断是否为当前选中状态——选中时文字色为 COLORS.bg(深底色)、背景为 COLORS.green(电光绿);未选中时文字色为 COLORS.sub(副文字色)、背景为 COLORS.chip(深绿底)。点击时通过 onClick 更新 cateIdx 状态,ArkUI 自动刷新所有标签的样式。ForEach 的第三个参数是键值生成函数 (tag: string) => tag,用标签文案作为唯一键,保证列表渲染的高效 diff。这种"Scroll + Row + ForEach + 状态驱动样式"的模式是 ArkUI 中实现横滑筛选标签的标准范式。
9.4 tabCharge 充电 Tab
/** 充电 Tab:推荐桩站列表(业务主 Tab,无图表加密列表) */
@Builder
tabCharge() {
Column({ space: 10 }) {
// 区块标题行:更多入口
Row() {
Text('附近推荐').fontSize(15).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
Blank()
Text('收藏新桩站 +').fontSize(11).fontColor(COLORS.green)
.onClick(() => { this.addModal = true; })
}
.width('100%')
// 推荐桩站大卡(3 条精选)
ForEach(PILE_RECS, (rec: PileRec) => {
Row({ space: 10 }) {
Text(rec.icon).fontSize(26)
Column({ space: 4 }) {
Text(rec.name).fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
Text(`${rec.power} · ${rec.price}`).fontSize(11).fontColor(COLORS.sub)
Row({ space: 6 }) {
Text(rec.dist).fontSize(10).fontColor(COLORS.text3)
Text(`空闲 ${rec.free} 桩`).fontSize(10).fontColor(freeColor(rec.free))
}
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Column({ space: 4 }) {
Text('导航').fontSize(11).fontColor(COLORS.bg).fontWeight(FontWeight.Bold)
.padding({ left: 12, right: 12, top: 6, bottom: 6 })
.borderRadius(12).backgroundColor(COLORS.green)
.onClick(() => { this.currentTab = 1; })
}
}
.padding(12)
.borderRadius(12)
.backgroundColor(COLORS.card)
.width('100%')
}, (rec: PileRec) => rec.name)
tabCharge 是充电 Tab 的 Builder,它是应用的业务主入口。顶部是区块标题行,左侧"附近推荐"标题用大字号加粗,右侧"收藏新桩站 +"用绿色小字,点击触发收藏弹窗(addModal = true)。Blank() 组件占据中间剩余空间,把两侧内容推到两端对齐。下方是推荐桩站大卡区域,通过 ForEach 遍历 PILE_RECS(三条精选桩站)渲染。每条卡片是一个 Row,左侧大字号 emoji 图标,中间是桩站名+功率价格+距离空闲数的多行信息,右侧是"导航"绿色胶囊按钮,点击切换到地图 Tab。卡片中空闲桩数的颜色通过 freeColor(rec.free) 动态计算——空闲多时绿色、少时红色,让用户一眼识别桩站紧张程度。卡片整体用 COLORS.card 深绿底+borderRadius(12) 圆角+padding(12) 内边距构成标准的深色卡片样式。
// 全部桩站列表(长按 Marker 的数据同源)
Row() {
Text('全部桩站(地图 Marker 同源)').fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
}
.width('100%')
ForEach(this.pileList, (pile: PileItem, idx: number) => {
Column({ space: 8 }) {
Row({ space: 10 }) {
Text(pile.icon).fontSize(22)
Column({ space: 3 }) {
Text(pile.name).fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
Text(`${pile.power} · ${pile.price} · ${pile.dist}`).fontSize(11).fontColor(COLORS.sub)
Text(`备注:${pile.note}`).fontSize(10).fontColor(COLORS.text3)
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Column({ space: 6 }) {
Text(`${pile.free}`).fontSize(16).fontWeight(FontWeight.Bold)
.fontColor(freeColor(pile.free))
Text('空闲桩').fontSize(9).fontColor(COLORS.text3)
}
}
.width('100%')
Row({ space: 8 }) {
Text('编辑').fontSize(10).fontColor(COLORS.blue)
.padding({ left: 10, right: 10, top: 4, bottom: 4 })
.borderRadius(10).backgroundColor(COLORS.chip)
.onClick(() => { this.openEditPile(idx); })
Text('删除').fontSize(10).fontColor(COLORS.red)
.padding({ left: 10, right: 10, top: 4, bottom: 4 })
.borderRadius(10).backgroundColor(COLORS.chip)
.onClick(() => { this.delIdx = idx; this.delModal = true; })
}
.justifyContent(FlexAlign.End)
.width('100%')
}
.padding(12)
.borderRadius(12)
.backgroundColor(COLORS.card)
.width('100%')
}, (pile: PileItem) => pile.name)
}
.width('100%')
}
充电 Tab 的下半部分是"全部桩站"列表,标题特意标注"地图 Marker 同源",提示用户这里的桩站与地图上的标注是同一份数据。ForEach 遍历 this.pileList(@State 状态数组,支持增删改),每条桩站是一个 Column,包含两行:上行是桩站信息(emoji+名称+功率价格距离+备注+空闲桩数大字号),下行是"编辑"和"删除"两个小胶囊按钮。编辑按钮点击调用 openEditPile(idx) 打开编辑弹窗,删除按钮点击设置 delIdx 并打开删除确认弹窗。两个按钮的颜色分别用 COLORS.blue(蓝)和 COLORS.red(红)做语义区分,让操作性质一目了然。空闲桩数用 16 号大字号加粗显示,配合 freeColor 动态颜色,让用户在长列表中快速扫描空闲状态。
9.5 tabMap 地图 Tab(6.1.1 长按事件特性页)
/** 地图 Tab:★ Map Kit 6.1.1 长按事件特性页 */
@Builder
tabMap() {
Column({ space: 10 }) {
// 特性说明卡
Column({ space: 4 }) {
Text('🗺 Map Kit 6.1.1 · 长按事件监听').fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
Text('长按地图上的充电桩 Marker 或 POI 地点,事件将记录到下方日志流')
.fontSize(10).fontColor(COLORS.sub)
}
.padding(10)
.borderRadius(10)
.backgroundColor(COLORS.chip)
.width('100%')
// 监听开关行:Marker 长按 / POI 长按
Row({ space: 12 }) {
Row({ space: 6 }) {
Toggle({ type: ToggleType.Switch, isOn: this.markerListenOn })
.selectedColor(COLORS.green)
.width(36)
.height(20)
.onChange(() => { this.toggleMarkerListen(); })
Text('Marker长按').fontSize(11).fontColor(COLORS.sub)
}
Row({ space: 6 }) {
Toggle({ type: ToggleType.Switch, isOn: this.poiListenOn })
.selectedColor(COLORS.green)
.width(36)
.height(20)
.onChange(() => { this.togglePoiListen(); })
Text('POI长按').fontSize(11).fontColor(COLORS.sub)
}
}
.width('100%')
// ★ MapComponent 本体(layoutWeight(1) 占满剩余高度)
MapComponent({ mapOptions: this.mapOptions, mapCallback: this.mapCallback })
.layoutWeight(1)
.width('100%')
.borderRadius(12)
tabMap 是 HarmonyOS 6.1.1 长按事件特性的展示页。顶部是特性说明卡,用 COLORS.chip 深绿底卡片展示"Map Kit 6.1.1 · 长按事件监听"标题和说明文案,让用户进入页面就了解这里展示的是什么新能力。下方是两个 Toggle 开关行,分别控制 Marker 长按和 POI 长按监听的开启与关闭——Toggle 组件的 isOn 绑定到 markerListenOn/poiListenOn 状态,selectedColor 设为品牌绿 COLORS.green,onChange 回调调用对应的 toggleMarkerListen/togglePoiListen 方法。核心是 MapComponent 组件——它接收 mapOptions(地图初始化参数)和 mapCallback(初始化回调)两个参数,通过 layoutWeight(1) 占满中间剩余高度,borderRadius(12) 让地图四角圆角。MapComponent 是 Map Kit 在 ArkUI 中的组件化封装,它会在渲染时初始化地图实例,初始化完成后触发 mapCallback 回调,进而完成 Marker 添加与长按监听注册——整个 6.1.1 长按事件链路在此被激活。
// 长按事件日志流(固定高度可滚动,新事件置顶)
Column({ space: 6 }) {
Row() {
Text('长按事件日志流').fontSize(12).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
Blank()
Text(`共 ${this.eventLogs.length} 条`).fontSize(10).fontColor(COLORS.text3)
}
.width('100%')
Scroll() {
Column({ space: 6 }) {
ForEach(this.eventLogs, (log: EventLog) => {
Row({ space: 8 }) {
Text(log.type === 'Marker' ? '📍' : '🏷')
.fontSize(12)
Column({ space: 2 }) {
Row({ space: 6 }) {
Text(log.type).fontSize(10).fontColor(log.type === 'Marker' ? COLORS.green : COLORS.yellow)
Text(log.name).fontSize(11).fontColor(COLORS.title)
Text(log.time).fontSize(9).fontColor(COLORS.text3)
}
Text(`${log.lat.toFixed(4)}, ${log.lng.toFixed(4)}`)
.fontSize(9).fontColor(COLORS.text3).fontFamily('monospace')
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
}
.padding({ left: 8, right: 8, top: 6, bottom: 6 })
.borderRadius(8)
.backgroundColor(COLORS.card)
.width('100%')
}, (log: EventLog) => `${log.type}-${log.name}-${log.time}`)
}
}
.scrollBar(BarState.Off)
.height(120)
.width('100%')
}
.padding(10)
.borderRadius(12)
.backgroundColor(COLORS.chip)
.width('100%')
}
.width('100%')
.height('100%')
}
地图 Tab 的底部是长按事件日志流区域,它是一个固定高度 120 像素的可滚动列表。标题行左侧"长按事件日志流"加粗标题,右侧"共 N 条"显示日志总数(通过 this.eventLogs.length 动态计算,unshift 新事件后自动更新)。Scroll 内的 ForEach 遍历 eventLogs 数组渲染每条日志——左侧 emoji 图标根据 log.type 区分(Marker 用 📍、POI 用 🏷),中间是类型标签+名称+时间的三段式信息,类型标签的颜色也根据类型区分(Marker 绿色、POI 黄色),下方是经纬度的等宽字体显示(fontFamily('monospace') 让数字等宽对齐)。ForEach 的键值生成函数 `${log.type}-${log.name}-${log.time}` 用三个字段组合作为唯一键,保证日志项的高效 diff。整个日志流区域用 COLORS.chip 深绿底卡片包裹,与上方地图形成视觉层次。当用户长按地图上的 Marker 或 POI 时,新事件通过 unshift 插入到数组最前,ArkUI 自动将新日志项渲染到列表顶部,形成实时的事件记录效果。
9.6 tabSearch 搜索 Tab(6.1.1 reliability 特性页)
/** 搜索 Tab:★ Map Kit 6.1.1 reliability 相关性分数特性页 */
@Builder
tabSearch() {
Column({ space: 10 }) {
// 特性说明卡
Column({ space: 4 }) {
Text('🔍 searchByText · reliability 相关性评分').fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
Text('Site 新增 reliability 字段([0,1],1 为完全相关),判断结果与关键字关联程度')
.fontSize(10).fontColor(COLORS.sub)
}
.padding(10)
.borderRadius(10)
.backgroundColor(COLORS.chip)
.width('100%')
// 搜索框 + 触发按钮
Row({ space: 8 }) {
TextInput({ text: this.queryInput, placeholder: '输入关键字,如:充电站' })
.layoutWeight(1)
.height(38)
.fontSize(12)
.fontColor(COLORS.title)
.placeholderColor(COLORS.text3)
.backgroundColor(COLORS.card)
.onChange((v: string) => { this.queryInput = v; })
Button('搜索')
.height(38)
.fontSize(12)
.backgroundColor(COLORS.green)
.onClick(() => { this.runSearch(); })
}
.width('100%')
// 搜索状态文案
Text(this.searchState).fontSize(10).fontColor(COLORS.text3).width('100%')
tabSearch 是 HarmonyOS 6.1.1 reliability 相关性评分特性的展示页。顶部同样是特性说明卡,介绍 searchByText 接口和 Site.reliability 字段的含义。下方是搜索输入区——TextInput 组件绑定 queryInput 状态,onChange 回调实时更新输入值,placeholder 提示"输入关键字,如:充电站",placeholderColor 用三级文字色让提示文案不抢眼。右侧"搜索"按钮用品牌绿 COLORS.green 背景,点击调用 runSearch() 方法触发搜索。搜索框下方是一行状态文案 Text(this.searchState),它会随着搜索过程动态变化——"待搜索 · 演示数据"→"搜索中…“→"返回 N 条结果"或"搜索失败(错误码) · 保留演示数据”,让用户始终了解搜索的进展与结果状态。
// 搜索结果列表(reliability 分数条 + 等级标签,List 子项必须用 ListItem 包裹)
List({ space: 8 }) {
ForEach(this.searchRecords, (rec: SearchRecord) => {
ListItem() {
Column({ space: 6 }) {
Row() {
Text(rec.name).fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
.layoutWeight(1)
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Text(reliabilityScore(rec.reliability).label)
.fontSize(10)
.fontColor(reliabilityScore(rec.reliability).color)
.padding({ left: 8, right: 8, top: 3, bottom: 3 })
.borderRadius(8)
.backgroundColor(COLORS.card)
}
.width('100%')
Text(rec.address).fontSize(11).fontColor(COLORS.sub).width('100%')
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
// ★ reliability 分数条:0~1 映射为线性进度 + 数值文本
Row({ space: 8 }) {
Progress({ value: rec.reliability * 100, total: 100, type: ProgressType.Linear })
.layoutWeight(1)
.height(6)
.color(reliabilityScore(rec.reliability).color)
Text(`reliability ${rec.reliability.toFixed(2)}`)
.fontSize(10)
.fontColor(COLORS.sub)
.fontFamily('monospace')
}
.width('100%')
Row({ space: 10 }) {
Text(`直线距离 ${(rec.distance / 1000).toFixed(2)}km`).fontSize(10).fontColor(COLORS.text3)
Text(rec.time).fontSize(10).fontColor(COLORS.text3)
}
.width('100%')
}
.padding(12)
.borderRadius(12)
.backgroundColor(COLORS.card)
.width('100%')
}
}, (rec: SearchRecord) => `${rec.name}-${rec.reliability}`)
}
.layoutWeight(1)
.scrollBar(BarState.Off)
.width('100%')
// 双特性代码预览卡(体现技术点)
this.codePreviewCard()
}
.width('100%')
.height('100%')
}
搜索结果列表是 reliability 特性的核心展示区。列表使用 List 容器(而非 Scroll+Column),因为 List 对长列表有更好的性能优化(虚拟化渲染)。每个搜索结果是一个 ListItem,内部 Column 包含四行信息:第一行是地点名称(左对齐、单行省略)和可靠性等级标签(右侧胶囊,颜色和文案由 reliabilityScore 函数计算);第二行是格式化地址(单行省略);第三行是 reliability 分数条——使用 Progress 线性进度条组件,value 为 rec.reliability * 100(将 0~1 映射到 0~100),total 为 100,color 同样由 reliabilityScore 计算得出,进度条右侧是等宽字体的 reliability 0.97 数值文本;第四行是直线距离(米转千米显示)和时间。这套"分数条+等级标签+数值文本"的三重展示,让用户既能通过进度条长度直观感知分数高低,又能通过等级标签快速判断相关性,还能通过精确数值做技术性参考。列表下方是 codePreviewCard() 代码预览卡,用等宽字体深色底展示 6.1.1 两大新特性的核心调用代码,让技术点可视化。
9.7 tabMine 我的 Tab
/** 我的 Tab:会员卡 + 功能清单 */
@Builder
tabMine() {
Column({ space: 10 }) {
// 会员渐变大卡
Column({ space: 8 }) {
Row({ space: 12 }) {
Text('⚡').fontSize(34)
Column({ space: 3 }) {
Text('绿电会员 · 铂金').fontSize(15).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
Text('充电 92 折 · 每月 4 张免费停车券').fontSize(11).fontColor(COLORS.sub)
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
}
.width('100%')
Divider().strokeWidth(1).color(COLORS.line)
Row() {
Text('本月充电 86 度').fontSize(11).fontColor(COLORS.sub)
Blank()
Text('已省 23.40 元').fontSize(11).fontColor(COLORS.green)
}
.width('100%')
}
.padding(14)
.borderRadius(14)
.linearGradient({
angle: 135,
colors: [[COLORS.greenD, 0.0], [COLORS.card, 0.75]]
})
.width('100%')
// 功能清单
ForEach(this.funcList, (item: FuncItem) => {
Row({ space: 10 }) {
Text(item.icon).fontSize(18)
Text(item.label).fontSize(13).fontColor(COLORS.title).layoutWeight(1)
Text(item.value).fontSize(11).fontColor(COLORS.sub)
Text('›').fontSize(14).fontColor(COLORS.text3)
}
.padding(12)
.borderRadius(12)
.backgroundColor(COLORS.card)
.width('100%')
}, (item: FuncItem) => item.label)
}
.width('100%')
}
tabMine 是"我的"个人中心页。顶部是会员渐变大卡,与头部 Banner 同样采用 135 度对角线渐变(COLORS.greenD 到 COLORS.card),保持视觉风格统一。卡片内左侧是大号 ⚡ 图标,右侧是会员等级"绿电会员 · 铂金"和权益说明"充电 92 折 · 每月 4 张免费停车券",中间用 Divider 分割线隔开,底部是"本月充电 86 度"和"已省 23.40 元"的左右对齐统计行——已省金额用绿色强调,让用户直观感受会员价值。下方是功能清单,通过 ForEach 遍历 FUNC_LIST(八条功能项)渲染,每行是"图标+功能名+状态值+箭头"的标准列表项样式,箭头 › 暗示可点击进入详情。整个"我的"页虽不直接展示 Map Kit 新特性,但承担了用户数据管理的职责,构成了应用的业务闭环。
9.8 codePreviewCard 代码预览卡与 tabBar 底部导航
/** 双特性代码预览卡(深色底 monospace 展示 6.1.1 新调用) */
@Builder
codePreviewCard() {
Column({ space: 6 }) {
Text('⌨️ Map Kit 6.1.1 双新特性调用').fontSize(12).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
Column({ space: 4 }) {
Text('const result = await site.searchByText(params)')
.fontSize(9).fontColor(COLORS.green).fontFamily('monospace')
Text('const score = site.reliability // [0,1] 相关性')
.fontSize(9).fontColor(COLORS.green).fontFamily('monospace')
Text('eventManager.onMarkerLongClick(cb) // 24+')
.fontSize(9).fontColor(COLORS.yellow).fontFamily('monospace')
Text('eventManager.onPoiLongClick(cb) // 24+')
.fontSize(9).fontColor(COLORS.yellow).fontFamily('monospace')
}
.padding(10)
.borderRadius(8)
.backgroundColor(COLORS.codeBg)
.width('100%')
}
.padding(10)
.borderRadius(10)
.backgroundColor(COLORS.chip)
.width('100%')
}
/** 底部导航 Tab 栏(4 Tab 单排) */
@Builder
tabBar() {
Row() {
ForEach(TAB_LIST, (tab: TabMeta, idx: number) => {
Column({ space: 3 }) {
Text(tab.icon).fontSize(18)
Text(tab.label).fontSize(10)
.fontColor(this.currentTab === idx ? COLORS.tabOn : COLORS.text3)
}
.layoutWeight(1)
.onClick(() => { this.currentTab = idx; })
}, (tab: TabMeta) => tab.label)
}
.padding({ top: 8, bottom: 8 })
.width('100%')
.backgroundColor(COLORS.card)
}
codePreviewCard 是一个技术点可视化组件,它用 COLORS.codeBg(#0A1A12 最深绿底)模拟代码编辑器的深色背景,用 fontFamily('monospace') 等宽字体展示 6.1.1 两大新特性的四行核心调用代码——前两行绿色展示 searchByText 与 reliability 读取,后两行黄色展示 onMarkerLongClick 与 onPoiLongClick 注册。这种"在应用内展示核心代码"的设计既能让开发者用户快速理解技术点,也能让非开发者用户感受到应用的技术含量。tabBar 是底部导航栏,ForEach 遍历 TAB_LIST 渲染四个 Tab,每个 Tab 是"图标+标签"的纵向排列,标签颜色根据 currentTab === idx 判断——选中时 COLORS.tabOn(电光绿)、未选中 COLORS.text3(三级文字色)。点击更新 currentTab 切换页面。layoutWeight(1) 让四个 Tab 平分底部宽度。
9.9 弹窗系统(遮罩 + 收藏 + 编辑 + 删除)
/** 弹窗遮罩层(点击空白处关闭) */
@Builder
modalOverlay(onClose: () => void) {
Column()
.width('100%')
.height('100%')
.backgroundColor(COLORS.mask)
.onClick(() => { onClose(); })
}
/** 收藏桩站弹窗:桩站名 + 地址输入 */
@Builder
panelAdd(onClose: () => void) {
Stack() {
this.modalOverlay(onClose)
Column({ space: 12 }) {
Text('收藏新桩站').fontSize(15).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
TextInput({ placeholder: '桩站名称', text: this.formName })
.height(38)
.fontSize(12)
.fontColor(COLORS.title)
.placeholderColor(COLORS.text3)
.backgroundColor(COLORS.chip)
.onChange((v: string) => { this.formName = v; })
TextInput({ placeholder: '地址(可留空地图选点)', text: this.formAddr })
.height(38)
.fontSize(12)
.fontColor(COLORS.title)
.placeholderColor(COLORS.text3)
.backgroundColor(COLORS.chip)
.onChange((v: string) => { this.formAddr = v; })
Row({ space: 10 }) {
Button('取消')
.layoutWeight(1)
.fontSize(12)
.backgroundColor(COLORS.chip)
.fontColor(COLORS.sub)
.onClick(() => { onClose(); })
Button('收藏')
.layoutWeight(1)
.fontSize(12)
.backgroundColor(COLORS.green)
.onClick(() => { this.savePile(); })
}
.width('100%')
}
.padding(16)
.borderRadius(14)
.backgroundColor(COLORS.card)
.width('82%')
}
.width('100%')
.height('100%')
}
弹窗系统由一个公共遮罩 modalOverlay 和三个业务弹窗(panelAdd/panelEdit/panelDel)组成。modalOverlay 是一个全屏 Column,背景色为 COLORS.mask(rgba(5,12,8,0.66) 半透明深色),点击时触发 onClose 回调关闭弹窗——这种"点击遮罩关闭"是弹窗交互的标准约定。每个业务弹窗都用 Stack 包裹遮罩层和弹窗内容,弹窗内容居中显示,宽度为 82%(留出两侧留白让弹窗有"浮层"感)。panelAdd 收藏桩站弹窗包含标题、桩站名称输入框、地址输入框、取消/收藏按钮——输入框绑定 formName/formAddr 状态,"收藏"按钮点击调用 savePile() 保存数据并关闭弹窗,"取消"按钮直接调用 onClose 关闭。弹窗内容的背景用 COLORS.card 深绿底,borderRadius(14) 圆角,padding(16) 内边距,构成与卡片一致的视觉风格。
panelEdit 编辑备注弹窗与 panelDel 删除确认弹窗的结构与 panelAdd 类似——panelEdit 展示当前桩站名,提供备注输入框(绑定 editNote 状态,打开时回填原备注),保存按钮调用 updatePile();panelDel 展示待删除桩站名与确认文案,删除按钮用 COLORS.red 红色背景强调危险操作,点击调用 delPile()。三个弹窗通过 build 方法中的 if (this.xxxModal) 条件渲染控制显隐,状态变更时 ArkUI 自动完成弹窗的挂载与卸载,无需手动操作 DOM。这套"Stack 叠加 + 条件渲染 + 公共遮罩"的弹窗模式,是 ArkUI 中实现自定义弹窗的轻量方案,相比系统 AlertDialog 有更高的样式自由度。
十、HarmonyOS 6.1.1 两大新特性对比
| 对比维度 | reliability 相关性评分(搜索特性) | onMarkerLongClick / onPoiLongClick(事件特性) |
|---|---|---|
| 所属模块 | site 模块(地点搜索) | map 模块(地图事件管理) |
| 新增位置 | Site 类型新增字段 | MapEventManager 新增方法 |
| 触发时机 | searchByText 返回结果时携带 | 用户长按地图 Marker 或 POI 时触发 |
| 数据类型 | 浮点数,取值范围 [0,1] | 回调函数,参数为 Marker 或 Poi 对象 |
| 业务价值 | 量化搜索结果与关键字的匹配程度 | 承载次级操作(收藏、标记、分享等) |
| 应用内展示 | 分数条 + 等级标签(高/中/低相关) | 事件日志流(type + name + 坐标) |
| 容错设计 | ?? 0 空值兜底,旧版 SDK 兼容 |
off 不传参清除全部订阅 |
| 交互模式 | 被动展示(搜索结果自带) | 主动触发(用户手势长按) |
| 视觉强调 | 绿/黄/红三色分级 | 📍/🏷 图标 + 绿/黄类型标签 |
| 典型场景 | 过滤"电池专卖"等噪声结果 | 长按充电桩 Marker 收藏为常去地点 |
十一、总结
本文围绕"电桩通·新能源充电桩查找"这一应用,系统剖析了 HarmonyOS ArkUI 框架在新能源汽车充电服务场景下的落地实践,重点解读了 HarmonyOS 6.1.1 Map Kit 的两大新特性——site.searchByText 返回的 Site.reliability 相关性评分字段,以及 MapEventManager 新增的 onMarkerLongClick / offMarkerLongClick 与 onPoiLongClick / offPoiLongClick 长按事件监听接口。从颜色系统的接口化设计到 Mock 数据的分层组织,从 @Observed 数据模型的构造到 @State 状态的声明式驱动,从地图回调的链式初始化到 Marker 批量添加与长按监听注册,从 searchByText 的参数构造到 reliability 字段的兜底读取,从弹窗系统的 Stack 叠加模式到四个 Tab 的差异化布局,整套代码体现了 ArkUI 声明式 UI 的完整开发范式。
在工程容错方面,本应用展现了一套成熟的"演示链路保障"策略。searchByText 在无 AGC 配置或无网络时通过 try-catch 捕获 BusinessError 并保留 Mock 数据,addMarker 逐个 try-catch 避免单点失败阻断批量添加,Site.reliability 等可选字段使用 ?? 空值合并运算符兜底,mapEventManager 在 toggleMarkerListen/togglePoiListen 中先判空再调用——这些细节共同保证了应用在各种异常环境下仍能稳定运行并展示有意义的内容,对于需要在开发、演示、生产等多种环境中部署的地图类应用具有很好的参考价值。
在 UI/UX 设计方面,深色主题(碳晶黑 #0B1210 + 电光绿 #2ED573 + 荧光黄 #FFD24E)通过三级深色背景阶梯和三级文字色阶构建了层次分明的视觉体系,同时通过"绿主黄辅"的配色呼应了新能源与电力的行业属性。reliability 分数采用"进度条+等级标签+数值文本"三重展示,空闲桩数采用三色分级映射,长按事件采用"emoji 图标+类型标签+坐标等宽字体"的日志流形态——这些设计让数据既直观又富有信息密度,体现了移动端"视觉先于文字"的信息呈现原则。两个 Toggle 开关让用户可以动态控制长按监听的开启与关闭,off 方法不传参清除全部订阅的设计则提供了灵活的事件管理能力。
从行业落地角度看,新能源汽车充电服务场景对地图能力与搜索精准度有双重高要求——用户既要能在地图上直观看到充电桩分布并与之交互,又要能通过关键字搜索快速找到真正提供充电服务的站点。HarmonyOS 6.1.1 的两大新特性恰好分别命中了这两个痛点:reliability 字段让搜索结果可以按相关性过滤掉"电池专卖店"等名称命中但语义不匹配的噪声,onMarkerLongClick/onPoiLongClick 让地图交互从"单一短按"升级为"短按+长按"双层级,为收藏、标记、分享等次级操作提供了手势空间。这套"搜索精准过滤 + 地图深度交互"的组合拳,不仅适用于充电桩场景,对加油站、停车场、换电站、共享单车等所有"基于位置的服务 + 实体网点"类应用都有直接的借鉴意义。
最后,从 ArkUI 开发范式的演进看,本应用也体现了若干值得推广的实践:接口先行(ColorPalette、TabMeta、SpotItem 等接口约束数据形状)、常量集中(COLORS、TAB_LIST、MARKER_SPOTS 等常量统一管理)、数据模型与 SDK 类型对应(SearchRecord 字段与 site.Site 一一映射)、Builder 函数化拆分(headerMain/tabCharge/tabMap/tabSearch/tabMine/tabBar/panelAdd/panelEdit/panelDel 等按职责拆分)、状态分层声明(UI 交互状态、地图能力状态、业务数据状态分层)。这些实践让单文件千行级代码仍保持了良好的可读性与可维护性,是 HarmonyOS ArkUI 中大型页面组织的优秀样本。
附录: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)