HarmonyOS 6.1.1 Map Kit 相关性评分 × 长按事件监听:城市夜骑补给驿站的地图搜索可信度与标记交互实践
一、技术前言

HarmonyOS 的 ArkUI 框架是华为面向全场景分布式应用打造的声明式 UI 开发体系。它以 @Entry、@Component、@State、@Builder 等一整套装饰器为语法骨架,让开发者用接近自然语言的方式声明界面的最终形态与状态绑定关系,再由框架的差分渲染引擎自动计算最小变更集合完成真实渲染。与传统命令式 UI 相比,声明式范式将"界面是什么"与"界面怎么变"彻底解耦——开发者只需描述任意时刻的界面快照,框架自动处理从旧状态到新状态的过渡。在骑行驿站这类信息密度高、状态联动频繁的场景中,每个 Tab 的列表数据、弹窗开关、地图事件日志都需要实时刷新,声明式绑定让多源数据的同步变得可控且不易出错,尤其适合 4 Tab 切换 + 三层弹窗叠加的复杂交互结构。

Map Kit 是 HarmonyOS 提供的地图服务能力集合,它将地图渲染、地图交互、地点搜索三大能力封装为统一的 ArkTS 模块。MapComponent 是地图的渲染容器组件,通过 mapOptions(包含中心点 position.target 与 zoom 缩放级别)和 mapCallback 回调两个核心参数完成初始化;MapComponentController 是地图的控制器实例,在回调中获取后可用于添加标注点(addMarker)、移动视角、获取事件管理器等操作;MapEventManager 则是地图交互事件的管理中枢,负责注册和注销各类地图上的手势监听。在城市骑行场景中,骑手需要直观地看到分布在城市各处的补给驿站位置,Map Kit 的 MapComponent 正好承担了"驿站分布全景图"的角色——它不仅渲染地图底图,还承载了标注点群的长按交互能力。

HarmonyOS 6.1.1 在 Map Kit 的 site 搜索模块上做了一项关键的体验增强——searchByText 接口返回的 Site 类型新增了 reliability 字段。这是一个取值范围为 [0, 1] 的浮点数相关性分数,1 表示搜索结果与查询关键字完全相关,0 表示完全不相关。在过去的版本中,searchByText 只返回地点名称、格式化地址、直线距离等基础信息,应用只能按距离排序展示结果,无法判断某条结果到底是不是用户真正想找的驿站。例如搜索"骑行驿站"时可能返回"电动车行维修部"或"共享单车停放点"——这些地点名称里包含"骑行"或"车"的语义片段,但实际并非补给驿站。reliability 字段的引入让应用可以量化每条结果与关键字的关联程度,从而在结果列表上以分数条和等级标签直观呈现可信度,帮助骑手快速识别哪些结果值得前往、哪些应当过滤。

HarmonyOS 6.1.1 同时为 MapEventManager 增加了两组长按事件监听接口:onMarkerLongClick / offMarkerLongClick 负责地图标注点(Marker)的长按监听,onPoiLongClick / offPoiLongClick 负责地图兴趣点(POI)的长按监听。onMarkerLongClick 的回调参数是 map.Marker 对象,可通过 marker.getId() 获取标注 ID、marker.getPosition() 获取经纬度坐标;onPoiLongClick 的回调参数是 mapCommon.Poi 对象,包含 name(POI 名称)与 position(经纬度)两个字段。这两组接口的 off 版本不接收任何参数,调用后直接清除该类型的全部订阅。在骑行驿站场景中,骑手在地图上长按某个驿站 Marker 可以快速记录该驿站位置到事件日志流,长按某个 POI 地点(如"昆明老街")可以临时标记一个非驿站兴趣点作为导航参考,双长按监听为地图交互提供了更丰富的手势入口。

深色主题在夜骑场景中不仅是审美选择,更是安全与功能性的刚需。夜间骑行时骑手的瞳孔已经适应低光环境,如果应用界面采用亮色背景,每次低头查看手机都会造成瞳孔急剧收缩,抬头后需要数秒才能重新适应黑暗,这段时间的视觉盲区在山路或车流密集路段极其危险。本项目采用"夜幕蓝 #0D1526 + 霓虹青 #3EE6D2 + 警示黄 #FFD84D"的三色主题体系:夜幕蓝作为背景主色模拟夜空环境,对瞳孔刺激最小;霓虹青作为品牌主色与驿站标识色,在深色背景上高对比度跳出,模拟夜骑车灯的荧光效果;警示黄用于副色与中等相关度标记,模拟交通警示牌的醒目度。整套配色通过 ColorPalette 接口集中声明,确保全应用颜色的一致性与可维护性。

骑行驿站作为城市骑行补给服务的新业态,其核心痛点是"夜间骑手如何在陌生路段快速找到可信的补给点"。骑手在夜骑过程中可能面临车胎漏气、电池耗尽、体力透支等突发状况,需要快速定位最近的补给驿站获取补胎工具、充电口、热水淋浴等资源。传统的地图搜索只返回距离信息,无法区分"真正的骑行驿站"与"名称含骑行字样的电动车行";传统的地图标注只支持点击查看详情,无法在骑行途中通过长按快速记录感兴趣的驿站。本应用通过 reliability 相关性评分解决搜索可信度问题,通过双长按事件监听解决地图标记快速记录问题,两者结合为夜骑补给场景提供了完整的"搜索可信 + 地图可交互"解决方案。

二、应用整体架构流程
本应用采用典型的"单页面多 Tab + 弹窗层叠"架构。顶层是一个 Stack 容器,底层是主内容列(头部渐变 Banner + 滚动区 + 底部导航),顶层根据三个弹窗状态变量(addModal、editModal、delModal)叠加不同类型的全屏弹窗。四个 Tab 的内容通过 if-else 条件分支按需渲染,避免同时构建四个完整 Tab 带来的性能开销。地图 Tab 在 mapCallback 回调就绪后依次完成控制器获取、事件管理器获取、Marker 群批量添加、双长按监听注册四步初始化。下面用流程图展示整体架构与数据流。
三、颜色系统与主题色板
应用首先定义了一个 ColorPalette 接口,集中声明页面所有用到的颜色字段;随后以一个 COLORS 常量实例化这个接口,作为全应用唯一的颜色取值来源。这种"接口约束 + 常量实例"的写法既保证了类型安全,又让主题切换成为可能——未来如果要支持浅色主题,只需要再定义一个 COLORS_LIGHT 常量即可。
/** 主题色板接口:集中声明页面所有颜色字段(夜幕蓝+霓虹青+警示黄深色系) */
interface ColorPalette {
bg: string; // 页面底色(夜幕蓝)
card: string; // 卡片底色(深蓝灰)
chip: string; // 胶囊与输入框底色(暗蓝)
title: string; // 主标题色(月白)
sub: string; // 次级文本色(雾蓝)
text3: string; // 弱文本色(暗灰蓝)
accent: string; // 行业主色(霓虹青)
accentD: string; // 主色深(深青,渐变起点)
accentL: string; // 主色浅(荧光青白)
second: string; // 副色(警示黄)
danger: string; // 警示色(低相关/删除/告急)
info: string; // 信息色(亮蓝)
line: string; // 分割线色
tabOn: string; // 底部 Tab 选中色
mask: string; // 弹窗遮罩色
codeBg: string; // 代码预览卡底色(深夜蓝黑)
}
/** 深色主题色板常量(夜骑社 · 夜幕蓝 + 霓虹青 + 警示黄) */
const COLORS: ColorPalette = {
bg: '#0D1526',
card: '#141F35',
chip: '#1B2A45',
title: '#E8F0FA',
sub: '#9FB2CC',
text3: '#66788F',
accent: '#3EE6D2',
accentD: '#1B9C8E',
accentL: '#D6F7F2',
second: '#FFD84D',
danger: '#FF5D6C',
info: '#5B8DEF',
line: '#24344F',
tabOn: '#3EE6D2',
mask: 'rgba(5,10,20,0.7)',
codeBg: '#0A101E'
};
ColorPalette 接口定义了 16 个颜色字段,每个字段对应一种语义化用途。bg 是页面整体背景色(夜幕蓝 #0D1526),card 是卡片背景色(深蓝灰 #141F35),chip 是更小一级的胶囊与输入框背景色(暗蓝 #1B2A45),三者形成背景的层次递进,让卡片在夜幕蓝底色上自然浮起。
title(月白 #E8F0FA)、sub(雾蓝 #9FB2CC)、text3(暗灰蓝 #66788F)是三档文字色,从高对比的标题白到弱化的辅助灰,用于区分主标题、副标题、辅助说明等不同信息层级。accent 霓虹青 #3EE6D2 是行业主色,出现在选中态、主按钮、Marker 长按日志类型标签等位置;accentD 深青 #1B9C8E 是渐变 Banner 的起始色,让渐变有"由深到亮"的视觉张力;accentL 荧光青白 #D6F7F2 用于高相关度等极端场景。
second 警示黄 #FFD84D 作为副色出现在中相关度标签、安全贴士标题等位置;danger 警示红 #FF5D6C 用于低相关度、删除按钮、充电口告急等需要警示的场景;info 亮蓝 #5B8DEF 用于编辑按钮等信息性操作。line 是分割线色,tabOn 是底部 Tab 选中色(与主色一致),mask 是弹窗遮罩色(半透明深蓝黑 rgba(5,10,20,0.7)),codeBg 是代码预览块的深夜蓝黑底色 #0A101E。整套色板通过 COLORS 常量集中管理,所有 Builder 与组件都通过 COLORS.xxx 引用,保证主题一致性。
四、常量定义与 Mock 数据
4.1 Tab 元数据与筛选标签
/** Tab 元数据接口:底部导航图标 + 标签 */
interface TabMeta {
icon: string; // Tab 图标 emoji
label: string; // Tab 标签文案
}
/** 底部导航 Tab 常量列表(4 Tab 单排) */
const TAB_LIST: TabMeta[] = [
{ icon: '🚴', label: '骑行' },
{ icon: '🗺', label: '地图' },
{ icon: '🔍', label: '搜索' },
{ icon: '👤', label: '我的' }
];
/** 头部横滑筛选 chips 文案(骑行驿站筛选) */
const CATE_TAGS: string[] = ['全部', '24小时', '充电口', '补胎工具', '夜光路标', '热水淋浴', '社群驻点', '山地友好'];
TabMeta 接口定义了底部导航每一项的图标与标签两个字段。TAB_LIST 常量数组按顺序声明了四个 Tab:骑行(自行车图标)、地图(地图图标)、搜索(放大镜图标)、我的(人像图标)。使用 emoji 作为图标的好处是无需引入图片资源,跨设备显示一致且体积为零;缺点是无法精细控制颜色,但通过 fontColor 在选中态(霓虹青 #3EE6D2)与未选中态(暗灰蓝 #66788F)之间切换可以部分弥补。
CATE_TAGS 是头部横滑筛选 chips 的文案列表,覆盖了骑行驿站的八大筛选维度:全部、24 小时营业、充电口可用、补胎工具、夜光路标、热水淋浴、社群驻点、山地友好。这些筛选维度精确对应了夜骑骑手的实际补给需求——夜间骑行最看重 24 小时营业与夜光路标,长途骑行需要充电口与补胎工具,山地骑手关注山地友好与热水淋浴。用户点击后会高亮对应 chip,选中态使用霓虹青底色加夜幕蓝文字,未选中态使用暗蓝底色加雾蓝文字。
4.2 城市中心点与地图标注点
/** 城市中心点(地图初始化中心,Map Kit 搜索 location 参数:昆明) */
const CITY_CENTER: mapCommon.LatLng = { latitude: 24.8801, longitude: 102.8329 };
/** 地图标注点接口(骑行驿站 Marker 群,长按事件的数据来源) */
interface SpotItem {
name: string; // 驿站名称
lat: number; // 纬度
lng: number; // 经度
tag: string; // 驿站类型标签
}
/** 骑行驿站标注点 Mock 数据(6 个,围绕昆明城市中心点 ±0.02 度散布) */
const MARKER_SPOTS: SpotItem[] = [
{ name: '夜骑社·翠湖夜航站', lat: 24.892, lng: 102.828, tag: '夜航' },
{ name: '夜骑社·南屏街星光站', lat: 24.877, lng: 102.837, tag: '深夜' },
{ name: '夜骑社·金马坊灯骑站', lat: 24.875, lng: 102.831, tag: '灯光' },
{ name: '夜骑社·北京路晨骑站', lat: 24.888, lng: 102.845, tag: '晨骑' },
{ name: '夜骑社·西昌路爬坡站', lat: 24.882, lng: 102.82, tag: '爬坡' },
{ name: '夜骑社·白塔路通勤站', lat: 24.886, lng: 102.848, tag: '通勤' }
];
CITY_CENTER 定义了昆明市中心点的经纬度坐标(纬度 24.8801、经度 102.8329),这个常量同时服务于两个用途:一是 MapComponent 初始化时 mapOptions.position.target 的中心点参数,让地图打开后自动定位到昆明城区;二是 site.searchByText 搜索时 location 参数的参考坐标,让搜索以昆明为中心、5000 米为半径圈定范围。选择昆明作为示例城市是因为其气候宜人、四季可骑,且城市路网兼顾平路通勤与西山爬坡,适合展示多种驿站类型。
SpotItem 接口定义了地图标注点的数据结构:name 是驿站全名(统一以"夜骑社·"前缀标识品牌),lat 与 lng 是经纬度坐标,tag 是驿站类型标签。MARKER_SPOTS 数组声明了 6 个骑行驿站标注点,围绕昆明城市中心点 ±0.02 度散布——翠湖夜航站偏北(夜航标签,翠湖周边夜间灯光好)、南屏街星光站偏南(深夜标签,商业区 24 小时营业)、金马坊灯骑站偏西南(灯光标签,金马坊灯光改装驻点)、北京路晨骑站偏东北(晨骑标签,早骑集合点)、西昌路爬坡站偏西(爬坡标签,西山爬坡起点)、白塔路通勤站偏东(通勤标签,通勤车停放)。这些标注点在 mapCallback 回调中被批量添加为 Marker,是 Marker 长按事件的数据来源。
4.3 骑行驿站列表与本周速览
/** 沿途驿站接口(骑行 Tab 精选列表) */
interface RideRec {
icon: string; // 驿站 emoji
name: string; // 驿站名
dist: string; // 距离文本
tool: string; // 补胎工具文案
charge: number; // 充电口空闲数
hours: string; // 营业时长文本
}
/** 沿途驿站 Mock 数据(3 条,头部渐变大卡下方列表) */
const RIDE_RECS: RideRec[] = [
{ icon: '🛠', name: '翠湖夜航站', dist: '1.2km', tool: '补胎工具齐全', charge: 4, hours: '24 小时' },
{ icon: '🔋', name: '北京路晨骑站', dist: '2.1km', tool: '便携打气筒', charge: 6, hours: '06:00-23:00' },
{ icon: '🔦', name: '金马坊灯骑站', dist: '2.8km', tool: '补胎+刹车调试', charge: 2, hours: '18:00-02:00' }
];
/** 本周速览指标接口(骑行 Tab 数据速览行) */
interface RideStat {
icon: string; // 指标图标
label: string; // 指标名
value: string; // 指标值文本
}
/** 本周速览 Mock 数据(3 项,横排三列小卡) */
const RIDE_STATS: RideStat[] = [
{ icon: '🌙', label: '本周夜骑', value: '86km' },
{ icon: '⚡', label: '平均时速', value: '21.4km/h' },
{ icon: '🚴', label: '出骑次数', value: '3 次' }
];
RideRec 接口定义了沿途驿站精选列表的数据结构,比 SpotItem 多了距离、补胎工具、充电口空闲数、营业时长等补给信息字段。RIDE_RECS 数组精选了 3 条驿站,按距离从近到远排列:翠湖夜航站最近(1.2km,补胎工具齐全,4 个充电口空闲,24 小时营业)、北京路晨骑站次近(2.1km,便携打气筒,6 个充电口富余,06:00-23:00 营业)、金马坊灯骑站最远(2.8km,补胎+刹车调试,仅 2 个充电口告急,18:00-02:00 营业)。充电口数量通过 chargeColor 函数映射颜色——6 个以上霓虹青(富余)、2 个以上警示黄(够用)、其余警示红(告急),让骑手一眼判断补给充裕度。
RideStat 接口定义了本周速览的三项指标。RIDE_STATS 数组声明了本周夜骑里程(86km,月亮图标)、平均时速(21.4km/h,闪电图标)、出骑次数(3 次,自行车图标)三项数据。这三项指标在骑行 Tab 以横排三列小卡形式呈现,数值用霓虹青高亮,与卡片底色形成强对比,让骑手快速掌握本周骑行概况。
4.4 我的页功能清单
/** 我的页功能清单条目接口 */
interface FuncItem {
icon: string; // 功能图标
label: string; // 功能名
value: string; // 状态/数值文本
}
/** 我的页功能清单 Mock 数据(8 条) */
const FUNC_LIST: FuncItem[] = [
{ icon: '🚴', label: '骑行记录', value: '本月 11 次 · 386km' },
{ icon: '📈', label: '累计里程', value: '1286km · 夜骑占 6 成' },
{ icon: '🔋', label: '补给卡余额', value: '3 次充电免费' },
{ icon: '🏆', label: '夜光徽章', value: '已点亮 7 / 12 枚' },
{ icon: '⭐', label: '收藏驿站', value: '6 站' },
{ icon: '🧾', label: '补给消费', value: '本月 5 笔' },
{ icon: '🗺', label: '常去驿站', value: '翠湖 / 南屏街' },
{ icon: '⚙', label: '偏好设置', value: '夜骑模式常开' }
];
FuncItem 接口定义了我的页功能清单条目的数据结构,每条包含图标、功能名、状态值三个字段。FUNC_LIST 数组声明了 8 条功能项,覆盖了骑行记录(本月 11 次 · 386km)、累计里程(1286km · 夜骑占 6 成,说明该用户是资深夜骑士手)、补给卡余额(3 次充电免费,社员权益体现)、夜光徽章(已点亮 7 / 12 枚,游戏化成就系统)、收藏驿站(6 站)、补给消费(本月 5 笔)、常去驿站(翠湖 / 南屏街)、偏好设置(夜骑模式常开)。
这 8 条功能项以统一的行式卡片呈现:左侧 emoji 图标、中间功能名加 layoutWeight(1) 撑满剩余宽度、右侧状态值雾蓝色、最右箭头符号。这种布局让每行功能项的信息层级清晰——图标用于快速识别、功能名用于定位、状态值用于一眼掌握当前数据。每行可点击跳转到对应功能详情页(本示例中未实现跳转逻辑,但已预留 onClick 扩展点)。
五、辅助函数与数据模型
5.1 reliability 相关性分数等级映射
/** 相关性等级接口(分数条旁的标签) */
interface ScoreLevel {
label: string; // 等级文案
color: string; // 等级颜色
}
/**
* 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.accent };
}
if (score >= 0.5) {
return { label: '中相关', color: COLORS.second };
}
return { label: '低相关', color: COLORS.danger };
}
ScoreLevel 接口定义了相关性等级的标签与颜色两个字段。reliabilityScore 函数是 Map Kit 6.1.1 新增 reliability 字段的核心映射逻辑——它接收一个 [0, 1] 范围的浮点数分数,返回对应的等级标签与颜色。
函数内部采用三档分段判断:分数 ≥ 0.8 返回"高相关"标签配霓虹青 #3EE6D2(与行业主色一致,表示该结果完全值得前往);分数 ≥ 0.5 返回"中相关"标签配警示黄 #FFD84D(表示结果可能部分匹配,需骑手自行判断);分数 < 0.5 返回"低相关"标签配警示红 #FF5D6C(表示该结果很可能不是真正的骑行驿站,建议过滤)。
这个映射函数在搜索 Tab 的结果列表中被调用两次——一次用于等级标签的文案与颜色,一次用于分数条 Progress 组件的 color 属性。将分数映射逻辑封装为独立函数的好处是:如果未来需要调整分档阈值(例如将高相关阈值从 0.8 调整为 0.75),只需要修改一处函数体,所有引用处自动同步。函数返回的 ScoreLevel 对象同时携带标签与颜色,避免了在调用处重复编写条件分支。
5.2 充电口颜色映射
/** 充电口颜色映射:≥6 个富余主色 / ≥2 个够用副色 / 其余告急警示色 */
function chargeColor(charge: number): string {
if (charge >= 6) { return COLORS.accent; }
if (charge >= 2) { return COLORS.second; }
return COLORS.danger;
}
chargeColor 函数是充电口空闲数的颜色映射逻辑。它接收一个整数充电口数量,返回对应的颜色字符串。函数内部同样采用三档分段判断:充电口 ≥ 6 个返回霓虹青(富余,骑手可放心前往充电);充电口 ≥ 2 个返回警示黄(够用,但可能需要排队);充电口 < 2 个返回警示红(告急,建议寻找其他驿站)。
这个函数在骑行 Tab 的沿途驿站列表和全部补给站列表中被调用,用于充电口数字的颜色着色。通过颜色直观传达补给充裕度,比纯数字更易于在骑行途中快速判断。函数的实现逻辑与 reliabilityScore 保持一致的三档分段风格,让全应用的"分数→颜色"映射模式统一。
5.3 驿站条目数据模型
/** 驿站条目(骑行 Tab 收藏列表) */
@Observed export class RideItem {
icon: string; // 驿站 emoji 图标
name: string; // 驿站名
tool: string; // 补胎工具文案
charge: number; // 充电口空闲数
hours: string; // 营业时长文本
note: string; // 用户备注(可编辑)
constructor(icon: string, name: string, tool: string, charge: number,
hours: string, note: string) {
this.icon = icon;
this.name = name;
this.tool = tool;
this.charge = charge;
this.hours = hours;
this.note = note;
}
}
/** 驿站收藏列表 Mock 数据(7 条) */
const RIDE_LIST: Array<RideItem> = [
new RideItem('🛠', '翠湖夜航站', '补胎工具齐全', 4, '24 小时', '环翠湖夜骑终点'),
new RideItem('🔋', '北京路晨骑站', '便携打气筒', 6, '06:00-23:00', '早骑集合点'),
new RideItem('🔦', '金马坊灯骑站', '补胎+刹车调试', 2, '18:00-02:00', '灯光改装驻点'),
new RideItem('🌙', '南屏街星光站', '补胎工具齐全', 3, '全天营业', '深夜补给热茶'),
new RideItem('⛰', '西昌路爬坡站', '便携打气筒', 2, '05:00-22:00', '西山爬坡起点'),
new RideItem('🚴', '白塔路通勤站', '全套维修台', 8, '24 小时', '通勤车停放'),
new RideItem('💧', '滇池方向补给站', '补水+补胎', 5, '07:00-21:00', '环滇中继站')
];
RideItem 类使用 @Observed 装饰器标记为可观察对象,这是 ArkUI 状态管理系统的关键装饰器之一。@Observed 让类的实例属性变化可以被框架追踪——当 rideList 数组中某个 RideItem 实例的 note 属性被修改时,引用该实例的 UI 组件会自动刷新。这比普通对象的"整体替换数组引用"方式更精细,允许只修改单个属性而触发最小化刷新。
RideItem 类定义了驿站收藏条目的完整数据结构:icon 是驿站 emoji 图标,name 是驿站名,tool 是补胎工具文案,charge 是充电口空闲数,hours 是营业时长文本,note 是用户备注(可通过编辑弹窗修改)。note 字段是该模型的核心可变字段——骑手可以为每个收藏的驿站添加个性化备注,如"环翠湖夜骑终点"“早骑集合点”"灯光改装驻点"等,这些备注帮助骑手快速回忆每个驿站的用途。
RIDE_LIST 常量数组初始化了 7 条驿站收藏数据,覆盖了翠湖夜航站、北京路晨骑站、金马坊灯骑站、南屏街星光站、西昌路爬坡站、白塔路通勤站、滇池方向补给站。每条数据的 charge 字段覆盖了从 2 到 8 的不同档位,配合 chargeColor 函数可以展示富余(8 个白塔路通勤站)、够用(4 个翠湖夜航站、3 个南屏街星光站、5 个滇池方向补给站)、告急(2 个金马坊灯骑站、2 个西昌路爬坡站)三种颜色状态。
5.4 搜索结果数据模型(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('翠湖夜航骑行驿站', '昆明市五华区翠湖南路 67 号', 1230, 0.96, '刚刚'),
new SearchRecord('南屏街星光补给站', '昆明市五华区南屏街 88 号', 1580, 0.89, '刚刚'),
new SearchRecord('金马坊灯骑驿站', '昆明市西山区金碧路 519 号', 2360, 0.7, '刚刚'),
new SearchRecord('北京路晨骑驿站', '昆明市盘龙区北京路 928 号', 2840, 0.53, '刚刚'),
new SearchRecord('电动车行维修部(非驿站)', '昆明市五华区人民中路 210 号', 3610, 0.3, '刚刚'),
new SearchRecord('共享单车停放点(无补给)', '昆明市官渡区北京路 157 号', 4980, 0.1, '刚刚')
];
SearchRecord 类是 Map Kit 6.1.1 新增 reliability 字段的核心数据载体,同样使用 @Observed 装饰器标记为可观察对象。该类定义了搜索结果条目的完整数据结构,每个字段都对应 site.Site 类型的属性:name 对应 site.name(地点名称),address 对应 site.formatAddress(格式化地址),distance 对应 site.distance(直线距离,单位米),reliability 对应 6.1.1 新增的 site.reliability(相关性分数)。
reliability 字段是本类的灵魂字段——它用注释 ★ 明确标注为 Map Kit 6.1.1 新字段,取值范围 [0, 1],1 表示完全相关。在 runSearch 方法中,从 site.searchByText 返回的 Site 数组中提取 s.reliability ?? 0(使用空值合并运算符兜底为 0,因为 reliability 是可选字段,旧版 Map Kit 或某些搜索结果可能不返回该字段)。
SEARCH_RECORDS 常量数组初始化了 6 条搜索结果 Mock 数据,精心设计了 reliability 值覆盖高/中/低三档:翠湖夜航骑行驿站(0.96 高相关,真正的驿站)、南屏街星光补给站(0.89 高相关,真正的驿站)、金马坊灯骑驿站(0.7 中相关,名称含驿站但可能服务不完整)、北京路晨骑驿站(0.53 中相关,边界值)、电动车行维修部(0.3 低相关,名称含"车"但非驿站)、共享单车停放点(0.1 低相关,完全无关)。这组数据完美演示了 reliability 字段的核心价值——它能将"真正的骑行驿站"与"名称含骑行字样的非驿站"区分开来。
5.5 长按事件日志数据模型
/** 长按事件日志条目(★ 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', '昆明老街', 24.879, 102.835, '演示事件'),
new EventLog('Marker', '#0', 24.892, 102.828, '演示事件')
];
EventLog 类是 Map Kit 6.1.1 新增双长按事件监听的数据载体,使用 @Observed 装饰器标记为可观察对象。该类定义了长按事件日志条目的数据结构:type 字段区分事件来源——'Marker' 表示来自 onMarkerLongClick 回调(用户长按了地图上的驿站标注点),'POI' 表示来自 onPoiLongClick 回调(用户长按了地图上的兴趣点);name 字段存储 Marker ID(如 '#0')或 POI 名称(如 '昆明老街');lat 与 lng 存储事件发生位置的经纬度;time 存储事件时间文案。
EVENT_LOGS 常量数组初始化了 2 条演示日志,让地图 Tab 打开时日志流不为空,直观展示日志条目的形态:第一条是 POI 类型事件(昆明老街,24.879, 102.835),第二条是 Marker 类型事件(#0 编号标注点,24.892, 102.828)。在实际运行中,用户长按地图上的 Marker 或 POI 后,新的 EventLog 实例会通过 unshift 方法置顶插入 eventLogs 数组,最新的事件始终显示在日志流顶部。
六、组件主体与状态管理
6.1 状态变量声明
/** 1150 夜骑社 · 骑行驿站主页面 */
@Entry
@Component
struct Page1150 {
/** 当前选中 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 rideList: Array<RideItem> = RIDE_LIST;
/** 我的页功能清单数据 */
@State funcList: FuncItem[] = FUNC_LIST;
主页面 Page1150 使用 @Entry 装饰器标记为应用入口组件,@Component 装饰器标记为自定义组件。组件内部通过 @State 装饰器声明了一系列状态变量,这些变量的变化会自动触发关联 UI 的刷新。
currentTab 是当前选中 Tab 的索引(0=骑行、1=地图、2=搜索、3=我的),默认值为 0 让应用打开后直接展示骑行主 Tab。cateIdx 是头部筛选 chips 的选中索引,默认值为 0 对应"全部"筛选。addModal、editModal、delModal 是三个弹窗开关布尔值,分别控制收藏补给站弹窗、编辑备注弹窗、删除确认弹窗的显示与隐藏。editIdx 与 delIdx 分别记录当前正在编辑或删除的驿站索引,确保弹窗操作能精确定位到正确的数据项。
rideList 是驿站收藏列表数据,初始化为 RIDE_LIST 常量(7 条 Mock 数据),该数组支持增(saveRide 方法的 unshift)、删(delRide 方法的 splice)、改(updateRide 方法的属性修改 + 数组引用刷新)。funcList 是我的页功能清单数据,初始化为 FUNC_LIST 常量(8 条 Mock 数据),该数组在当前示例中为只读展示。
6.2 Map Kit 状态与地图初始化参数
// --- 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 = '';
Map Kit 相关状态分为私有成员(private,不需要触发 UI 刷新)和响应式状态(@State,变化需要触发 UI 刷新)两类。
私有成员部分:mapOptions 是地图初始化参数,包含 position.target(中心点,设为 CITY_CENTER 昆明坐标)和 zoom(缩放级别 13,适合城市级地图展示)。mapCallback 是地图初始化回调,类型为 AsyncCallback<map.MapComponentController>,在 aboutToAppear 生命周期中被赋值。mapController 是地图控制器,在回调成功分支中获取,用于添加 Marker。mapEventManager 是地图事件管理器,通过 mapController.getEventManager() 获取,用于注册长按监听。这四个私有成员都使用可选类型标记 ?,因为它们在组件构建时还未赋值,只有在 mapCallback 回调执行后才真正初始化。
响应式状态部分:markerListenOn 与 poiListenOn 是两个长按监听开关布尔值,默认都为 true(监听开启),与地图 Tab 的两个 Toggle 开关组件双向绑定。eventLogs 是长按事件日志流数组,初始化为 EVENT_LOGS 常量(2 条演示数据),每次长按事件触发后通过 unshift 置顶插入新日志。queryInput 是搜索关键字输入值,默认为 '骑行驿站'。searchState 是搜索状态文案,默认为 '待搜索 · 演示数据',搜索过程中会更新为"搜索中…"“返回 N 条结果”"搜索失败"等状态。searchRecords 是搜索结果列表,初始化为 SEARCH_RECORDS 常量(6 条 Mock 数据),搜索成功后会被真实结果替换。formName、formAddr、editNote 是三个弹窗的输入框值,用于收藏弹窗的驿站名/地址输入和编辑弹窗的备注输入。
6.3 地图初始化回调(核心特性实现)
/**
* 地图初始化: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();
// 批量添加骑行驿站 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}`);
}
}
// ★ 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, '刚刚'));
});
// ★ 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, '刚刚'));
});
};
}
setupMapCallback 方法是整个 Map Kit 集成的核心入口,它将地图初始化的完整流程封装为一个 AsyncCallback 回调赋值给 this.mapCallback。这个回调在 MapComponent 组件完成地图引擎初始化后被框架调用,接收两个参数:err(BusinessError 类型,初始化失败时的错误信息)和 mapController(map.MapComponentController 类型,初始化成功后的地图控制器)。
回调的第一行检查 err 是否存在——如果地图初始化失败(例如设备不支持 Map Kit、网络异常等),直接打印错误日志并 return 退出,避免后续对 mapController 的空引用操作。成功分支中,首先将 mapController 赋值给 this.mapController 保存,然后通过 mapController.getEventManager() 获取事件管理器赋值给 this.mapEventManager。这一步是后续长按监听注册的前提——事件管理器必须从已就绪的控制器中获取。
接下来是批量添加骑行驿站 Marker 的循环。遍历 MARKER_SPOTS 数组(6 个驿站标注点),为每个标注点构建 mapCommon.MarkerOptions 配置对象。该对象的字段含义:position 是标注点经纬度(从 SpotItem 的 lat 与 lng 取值);clickable 设为 true 让标注点可点击(长按监听的前提);visible 设为 true 让标注点可见;rotation 设为 0 不旋转;zIndex 设为 0 默认层级;alpha 设为 1 不透明;anchorU 与 anchorV 设为 0.5 和 1 让标注点图标的底部中心对准经纬度坐标(模拟图钉效果);draggable 设为 false 不可拖拽;flat 设为 false 让标注点始终面向屏幕(不贴合地图平面)。
每个标注点的 addMarker 调用返回一个 Promise,使用 await 逐个等待完成。try-catch 包裹确保单个标注点添加失败不会中断整个循环——失败时打印错误日志继续下一个。这种"逐个 await + try-catch"的写法比 Promise.all 并发更稳妥,避免了地图引擎对并发添加标注点的限制问题。
Marker 群添加完成后,进入双长按监听注册环节。第一组监听 onMarkerLongClick 接收一个回调函数,参数类型为 map.Marker。当用户长按地图上的某个驿站标注点时,框架调用该回调,传入被长按的 Marker 对象。回调内部通过 marker.getPosition() 获取标注点经纬度(返回 mapCommon.LatLng 类型),通过 marker.getId() 获取标注点 ID(返回数字,格式化为 '#0' 形式),然后构建一条 EventLog 实例(type 为 'Marker')并通过 unshift 置顶插入 eventLogs 数组。
第二组监听 onPoiLongClick 接收一个回调函数,参数类型为 mapCommon.Poi。当用户长按地图上的某个兴趣点(POI,如"昆明老街""翠湖公园"等地图自带的地名标注)时,框架调用该回调,传入被长按的 Poi 对象。回调内部直接读取 poi.name(POI 名称)和 poi.position.latitude / poi.position.longitude(POI 经纬度),构建一条 EventLog 实例(type 为 'POI')并置顶插入日志流。两组监听在回调内完成注册,确保 mapEventManager 已就绪。
6.4 长按监听开关切换
/** 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 方法是 Marker 长按监听的开关切换逻辑。方法首先检查 this.mapEventManager 是否存在——如果地图还未初始化完成(例如用户在地图引擎加载期间就点击了开关),直接 return 退出避免空引用。然后根据当前 markerListenOn 状态决定操作:如果当前是开启状态(true),调用 offMarkerLongClick() 关闭监听;如果当前是关闭状态(false),调用 onMarkerLongClick() 重新注册监听(注册逻辑与 setupMapCallback 中完全一致)。最后翻转 markerListenOn 布尔值。
offMarkerLongClick() 方法不接收任何参数,调用后直接清除该类型的全部订阅。这是 Map Kit 事件管理的简洁设计——一个事件类型只能有一个回调,off 不需要指定清除哪个回调,直接清除整个类型的监听。如果未来需要支持多回调,可能需要参数化 off 方法,但当前版本的单回调模式已足够满足骑行驿站的场景需求。
togglePoiListen 方法的逻辑与 toggleMarkerListen 完全对称,只是操作对象从 Marker 长按监听换成了 POI 长按监听,调用 offPoiLongClick() 关闭或 onPoiLongClick() 重新注册。两个方法在地图 Tab 的两个 Toggle 开关的 onChange 回调中被调用,让用户可以独立控制两类长按监听的开关。这种独立控制的设计考虑了实际使用场景——骑手可能只想记录驿站 Marker 的长按事件而不想被 POI 长按事件打扰,或者只想记录 POI 长按事件而不想被 Marker 干扰。
6.5 关键字搜索(reliability 字段读取)
/**
* ★ 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'
};
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}) · 保留演示数据`;
}
}
runSearch 方法是 Map Kit 6.1.1 新增 reliability 字段的核心读取逻辑,使用 async 标记为异步方法。方法首先将 searchState 更新为"搜索中…“,让用户在搜索框下方看到进行中状态。然后构建 site.SearchByTextParams 搜索参数对象:query 是搜索关键字(从 this.queryInput 取值,默认"骑行驿站”);location 是搜索参考坐标(设为 CITY_CENTER 昆明中心点);radius 是搜索半径(5000 米,覆盖城区主要驿站分布);language 是返回结果语言('zh' 中文)。
try 块中调用 site.searchByText(params) 发起搜索,使用 await 等待异步结果。返回的 site.SearchByTextResult 对象包含 sites 数组(Array<site.Site> 类型),使用 ?? [] 空值合并兜底为空数组。如果 sites 长度为 0,更新 searchState 为"无结果 · 保留演示数据"并退出,此时 searchRecords 保持原有 Mock 数据不变,确保搜索无结果时界面不会空白。
非空分支遍历 sites 数组,将每个 site.Site 对象转换为 SearchRecord 实例。转换过程中每个字段都使用 ?? 空值合并运算符兜底:s.name ?? '未命名地点'(地点名空时显示"未命名地点")、s.formatAddress ?? '暂无地址'(地址空时显示"暂无地址")、s.distance ?? 0(距离空时为 0)、s.reliability ?? 0(★ 相关性分数空时为 0)。s.reliability ?? 0 这行是整个方法的关键——reliability 是 6.1.1 新增的可选字段,旧版 Map Kit 返回的 Site 对象可能不包含该字段,使用 ?? 0 兜底确保代码向前兼容。
转换完成后将 records 数组赋值给 this.searchRecords 替换原有数据,触发搜索结果列表的 UI 刷新。同时更新 searchState 为"返回 N 条结果"让用户看到搜索结果数量。catch 块捕获 BusinessError(例如未配置 AGC 服务、无网络连接等场景),将错误码格式化到 searchState 中显示"搜索失败(code) · 保留演示数据",同时保留原有 Mock 数据保证演示链路不中断。这种"真实搜索 + Mock 兜底"的双轨设计让应用在没有 Map Kit 服务配置的开发环境中也能完整演示搜索结果与 reliability 分数条效果。
6.6 弹窗操作方法
/** 打开编辑备注弹窗(回填当前驿站备注) */
openEditRide(idx: number) {
this.editIdx = idx;
this.editNote = this.rideList[idx].note;
this.editModal = true;
}
/** 保存收藏补给站(空名兜底默认演示驿站) */
saveRide() {
const name = this.formName === '' ? '夜骑社·新收藏站' : this.formName;
const addr = this.formAddr === '' ? '昆明市五华区(地图选点)' : this.formAddr;
this.rideList.unshift(new RideItem('⭐', name, '便携打气筒', 3, '24 小时', addr));
this.formName = '';
this.formAddr = '';
this.addModal = false;
}
/** 保存编辑备注(整体刷新数组引用以刷新列表) */
updateRide() {
if (this.editIdx >= 0 && this.editIdx < this.rideList.length) {
if (this.editNote !== '') {
this.rideList[this.editIdx].note = this.editNote;
}
this.rideList = this.rideList.slice();
}
this.editModal = false;
}
/** 删除收藏驿站(确认弹窗回调) */
delRide() {
if (this.delIdx >= 0 && this.delIdx < this.rideList.length) {
this.rideList.splice(this.delIdx, 1);
}
this.delModal = false;
}
/** 生命周期:初始化地图回调(监听注册在 mapCallback 内完成) */
aboutToAppear() {
this.setupMapCallback();
}
openEditRide 方法接收驿站索引 idx,将索引赋值给 this.editIdx 记录当前编辑项,从 rideList 数组对应位置取出 note 字段回填到 this.editNote(让编辑弹窗的输入框显示当前备注内容),最后将 editModal 设为 true 打开编辑弹窗。这种"回填当前值"的设计让用户可以在原有备注基础上修改,而非从空白开始输入。
saveRide 方法处理收藏新补给站逻辑。首先对输入的驿站名和地址做空值兜底——如果用户未输入驿站名,默认使用"夜骑社·新收藏站";如果未输入地址,默认使用"昆明市五华区(地图选点)“。然后通过 unshift 在 rideList 数组头部插入一条新的 RideItem 实例(图标用⭐标识为用户新收藏,补胎工具默认"便携打气筒”,充电口默认 3 个,营业时长默认"24 小时")。最后清空输入框状态并关闭弹窗。
updateRide 方法处理编辑备注保存逻辑。首先检查 editIdx 是否在有效范围内(边界保护),然后检查 editNote 是否非空(空备注不覆盖原有值)。关键一步是 this.rideList = this.rideList.slice()——虽然 RideItem 使用 @Observed 装饰器可以追踪属性变化,但数组的整体引用替换可以确保所有引用该数组的 UI 组件都刷新。slice() 方法返回数组的浅拷贝,通过替换引用触发 ArkUI 的差分渲染引擎重新计算列表。最后关闭编辑弹窗。
delRide 方法处理删除收藏逻辑。同样先检查 delIdx 边界,然后通过 splice(this.delIdx, 1) 从数组中移除对应位置的驿站条目。splice 方法会直接修改原数组并触发 @State 的变更通知,无需额外的引用替换。最后关闭删除确认弹窗。
aboutToAppear 是组件的生命周期钩子,在组件创建后、build 方法执行前调用。这里调用 this.setupMapCallback() 完成地图回调的赋值——将异步回调函数赋值给 this.mapCallback,后续 MapComponent 组件构建时会读取这个回调并在地图引擎就绪后执行。将回调赋值放在 aboutToAppear 而非 build 中,确保回调在组件构建前就已就绪。
七、页面构建与 Builder 函数群
7.1 主构建函数
/** 页面主构建:Stack 包裹主内容与三层弹窗 */
build() {
Stack() {
Column() {
this.headerMain()
Divider().strokeWidth(1).color(COLORS.line)
Scroll() {
Column() {
if (this.currentTab === 0) {
this.tabRide()
} 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(占满全屏),然后根据三个弹窗状态变量(addModal、editModal、delModal)条件性地叠加弹窗层——当某个弹窗状态为 true 时,对应的弹窗 Builder 函数会被调用并叠加在主内容之上。
主内容 Column 从上到下依次包含:headerMain() 头部渐变 Banner、一条分割线 Divider、Scroll 滚动区(layoutWeight(1) 占满剩余高度,scrollBar(BarState.Off) 隐藏滚动条)、tabBar() 底部导航栏。滚动区内部的 Column 通过 if-else 条件分支按 currentTab 索引渲染对应 Tab 的 Builder 函数——0 渲染 tabRide、1 渲染 tabMap、2 渲染 tabSearch、else 渲染 tabMine。这种条件渲染确保同一时刻只构建一个 Tab 的 UI 树,避免四个 Tab 同时存在带来的性能开销和状态冲突。
三个弹窗的渲染采用相同的模式:检查状态变量为 true 时调用对应 Builder 函数,传入一个关闭回调(() => { this.xxxModal = false; })。这个关闭回调会在弹窗内部的遮罩点击或取消按钮中被调用,实现"点击空白处或取消按钮关闭弹窗"的交互。Stack 容器本身设置了 width('100%')、height('100%') 和 backgroundColor(COLORS.bg) 让整个页面背景为夜幕蓝。
7.2 头部渐变 Banner
/** 头部:渐变 Banner(本周夜骑里程+均速)+ 筛选 chips 横滑 */
@Builder
headerMain() {
Column({ space: 12 }) {
// 顶部渐变 Banner:夜骑里程 + 均速进度 + 领用工具入口
Column({ space: 10 }) {
Row({ space: 12 }) {
Column({ space: 2 }) {
Text('86km').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('本周 3 骑 · 均速 21.4km/h').fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
}
Row({ space: 6 }) {
Text('🔦').fontSize(12)
Text('最近驿站 1.2km · 充电口 4 个空闲').fontSize(11).fontColor(COLORS.sub)
}
Row({ space: 6 }) {
Text('🛠').fontSize(12)
Text('补胎工具可领用 · 免押金').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.accent)
Text('🗺 地图找站').fontSize(12).fontColor(COLORS.accent)
.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.accentD, 0.0], [COLORS.card, 0.7]]
})
// 筛选 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.accent : 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%')
}
headerMain 是 @Builder 装饰的头部构建函数,包含渐变 Banner 和筛选 chips 两部分。渐变 Banner 是一个 Column,通过 linearGradient 设置 135 度对角渐变——起始色为 COLORS.accentD(深青 #1B9C8E,0% 位置),终止色为 COLORS.card(深蓝灰 #141F35,70% 位置)。这种"深青到深蓝灰"的渐变模拟了夜骑车灯在黑暗中投射的光锥效果,让 Banner 区域在夜幕蓝背景上自然突出。
Banner 内部上半部分是数据展示行:左侧大字号显示本周夜骑里程"86km"(30 号粗体月白色),下方小字"本周夜骑"标签(10 号雾蓝色)。右侧三行信息分别展示本周骑行概况(3 骑 · 均速 21.4km/h)、最近驿站信息(1.2km · 充电口 4 个空闲)、补胎工具领用信息(免押金)。右侧使用 layoutWeight(1) 撑满剩余宽度,与左侧里程数字形成左右分栏布局。
Banner 内部下半部分是两个快捷入口胶囊:左侧"领用工具"按钮使用霓虹青底色配夜幕蓝文字(高对比度主操作按钮),右侧"地图找站"按钮使用暗蓝底色配霓虹青文字(次级操作按钮),通过 justifyContent(FlexAlign.SpaceBetween) 让两个按钮分居左右两端。"地图找站"按钮的 onClick 将 currentTab 设为 1 跳转到地图 Tab,实现头部到地图的快速导航。
筛选 chips 横滑区域使用 Scroll 容器包裹 Row,scrollable(ScrollDirection.Horizontal) 启用水平滚动,scrollBar(BarState.Off) 隐藏滚动条。ForEach 遍历 CATE_TAGS 数组生成 8 个筛选 chip,每个 chip 的 fontColor 和 backgroundColor 根据 cateIdx === idx 条件切换——选中态使用霓虹青底色配夜幕蓝文字,未选中态使用暗蓝底色配雾蓝文字。点击 chip 更新 cateIdx 触发高亮切换。
7.3 骑行 Tab(业务主 Tab)
/** 骑行 Tab:沿途驿站 + 本周速览 + 收藏列表(业务主 Tab) */
@Builder
tabRide() {
Column({ space: 10 }) {
// 区块标题行:更多入口
Row() {
Text('沿途补给驿站').fontSize(15).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
Blank()
Text('收藏新补给站 +').fontSize(11).fontColor(COLORS.accent)
.onClick(() => { this.addModal = true; })
}
.width('100%')
// 沿途驿站大卡(3 条精选)
ForEach(RIDE_RECS, (rec: RideRec) => {
Row({ space: 10 }) {
Text(rec.icon).fontSize(26)
Column({ space: 4 }) {
Text(rec.name).fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
Text(`${rec.tool} · ${rec.hours}`).fontSize(11).fontColor(COLORS.sub)
Row({ space: 6 }) {
Text(rec.dist).fontSize(10).fontColor(COLORS.text3)
Text(`充电口 ${rec.charge} 个`).fontSize(10).fontColor(chargeColor(rec.charge))
}
}
.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.accent)
.onClick(() => { this.currentTab = 1; })
}
}
.padding(12)
.borderRadius(12)
.backgroundColor(COLORS.card)
.width('100%')
}, (rec: RideRec) => rec.name)
tabRide 是骑行 Tab 的构建函数,作为业务主 Tab 承载了应用最核心的补给信息展示。Tab 顶部是区块标题行,左侧"沿途补给驿站"标题(15 号粗体月白色),右侧"收藏新补给站 +"链接(11 号霓虹青),点击右侧链接将 addModal 设为 true 打开收藏弹窗。Blank() 组件在中间撑开空间让标题与链接分居左右两端。
沿途驿站大卡区域使用 ForEach 遍历 RIDE_RECS 数组(3 条精选驿站)生成卡片列表。每张卡片的布局是左右分栏:左侧大字号 emoji 图标(26 号),中间驿站信息列(驿站名 13 号粗体、补胎工具与营业时长 11 号雾蓝、距离与充电口数 10 号),右侧"去补给"按钮(霓虹青底色配夜幕蓝文字)。充电口数通过 chargeColor(rec.charge) 动态着色——富余显示霓虹青、够用显示警示黄、告急显示警示红。"去补给"按钮点击后跳转到地图 Tab(currentTab = 1),让骑手在地图上查看驿站具体位置。
// 本周速览(横排三列小卡)
Row({ space: 8 }) {
ForEach(RIDE_STATS, (stat: RideStat) => {
Column({ space: 4 }) {
Text(stat.icon).fontSize(16)
Text(stat.value).fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLORS.accent)
Text(stat.label).fontSize(10).fontColor(COLORS.sub)
}
.layoutWeight(1)
.padding({ top: 10, bottom: 10 })
.borderRadius(12)
.backgroundColor(COLORS.card)
}, (stat: RideStat) => stat.label)
}
.width('100%')
// 全部补给站列表(长按 Marker 的数据同源)
Row() {
Text('全部补给站(地图 Marker 同源)').fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
}
.width('100%')
ForEach(this.rideList, (ride: RideItem, idx: number) => {
Column({ space: 8 }) {
Row({ space: 10 }) {
Text(ride.icon).fontSize(22)
Column({ space: 3 }) {
Text(ride.name).fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
Text(`${ride.tool} · ${ride.hours}`).fontSize(11).fontColor(COLORS.sub)
Text(`备注:${ride.note}`).fontSize(10).fontColor(COLORS.text3)
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Column({ space: 6 }) {
Text(`${ride.charge}`).fontSize(16).fontWeight(FontWeight.Bold)
.fontColor(chargeColor(ride.charge))
Text('个充电口').fontSize(9).fontColor(COLORS.text3)
}
}
.width('100%')
Row({ space: 8 }) {
Text('编辑').fontSize(10).fontColor(COLORS.info)
.padding({ left: 10, right: 10, top: 4, bottom: 4 })
.borderRadius(10).backgroundColor(COLORS.chip)
.onClick(() => { this.openEditRide(idx); })
Text('删除').fontSize(10).fontColor(COLORS.danger)
.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%')
}, (ride: RideItem) => ride.name)
本周速览区域使用横排三列小卡展示三项指标。ForEach 遍历 RIDE_STATS 数组,每项指标以 Column 形式呈现——顶部 emoji 图标(16 号)、中间数值(13 号粗体霓虹青)、底部标签(10 号雾蓝)。三个小卡通过 layoutWeight(1) 平均分配宽度,space: 8 设置间距,形成等宽三列布局。这种紧凑的速览设计让骑手一眼掌握本周骑行概况。
全部补给站列表是骑行 Tab 的核心数据展示区。标题行特别标注"地图 Marker 同源"——说明这里的 rideList 数据与地图 Tab 的 MARKER_SPOTS 标注点是同一数据源的不同视图。ForEach 遍历 this.rideList 数组(7 条收藏驿站),每条驿站以卡片形式呈现。卡片上半部分是驿站信息行——左侧 emoji 图标、中间驿站名与工具时长备注、右侧充电口数(大字号 16 号配 chargeColor 着色 + "个充电口"小字说明)。卡片下半部分是操作按钮行——"编辑"按钮(亮蓝色文字)调用 openEditRide(idx) 打开编辑弹窗,"删除"按钮(警示红文字)设置 delIdx 后打开删除确认弹窗。按钮行通过 justifyContent(FlexAlign.End) 右对齐。
// 夜骑安全贴士卡
Column({ space: 6 }) {
Text('💡 夜骑安全贴士').fontSize(12).fontWeight(FontWeight.Bold).fontColor(COLORS.second)
Text('夜骑请开启前后车灯并佩戴头盔,驿站可免费借用反光背心;雨季山路湿滑,过弯请提前减速并避开井盖与积水。')
.fontSize(10).fontColor(COLORS.sub).lineHeight(16)
}
.padding(12)
.borderRadius(12)
.backgroundColor(COLORS.chip)
.width('100%')
}
.width('100%')
}
骑行 Tab 底部是夜骑安全贴士卡,使用 COLORS.chip 暗蓝底色与主卡片区分。贴士标题"💡 夜骑安全贴士"使用警示黄色(COLORS.second)强调安全警示属性。贴士内容详细提醒骑手:开启前后车灯、佩戴头盔、驿站可借用反光背心、雨季山路湿滑过弯减速、避开井盖与积水。lineHeight(16) 设置行高让多行文字阅读舒适。这张贴士卡不仅是信息展示,更体现了应用对骑手安全的关怀——在补给信息之外提供安全指导,是骑行服务场景的完整闭环。
7.4 地图 Tab(长按事件特性页)
/** 地图 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.accent)
.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.accent)
.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 是地图 Tab 的构建函数,作为 Map Kit 6.1.1 长按事件特性的展示页。Tab 顶部是特性说明卡,用暗蓝底色突出标注"Map Kit 6.1.1 · 长按事件监听"标题和"长按地图上的驿站 Marker 或 POI 地点,事件将记录到下方日志流"说明文案,让用户一眼理解该 Tab 的核心功能。
监听开关行包含两个 Toggle 开关组件,分别控制 Marker 长按监听和 POI 长按监听的开关状态。Toggle 使用 ToggleType.Switch 样式(iOS 风格滑动开关),isOn 属性绑定 markerListenOn 和 poiListenOn 状态变量,selectedColor 设为霓虹青让开启状态的滑块高亮。onChange 回调分别调用 toggleMarkerListen() 和 togglePoiListen() 方法,实现开关切换时动态注册或注销监听。每个开关旁边标注"Marker长按"或"POI长按"说明文字,让用户明确每个开关控制的监听类型。
MapComponent 是地图的本体组件,通过 mapOptions 传入地图初始化参数(中心点昆明、缩放级别 13),通过 mapCallback 传入初始化回调(在 aboutToAppear 中赋值的异步回调函数)。组件使用 layoutWeight(1) 占满剩余高度,width('100%') 撑满宽度,borderRadius(12) 设置圆角让地图与卡片风格统一。MapComponent 渲染后框架会自动调用 mapCallback,在回调中完成控制器获取、事件管理器获取、Marker 群添加、双长按监听注册四步初始化。
// 长按事件日志流(固定高度可滚动,新事件置顶)
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.accent : COLORS.second)
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 的核心交互反馈区。日志流容器是一个 Column,顶部标题行显示"长按事件日志流"(12 号粗体月白色)和日志总数"共 N 条"(10 号暗灰蓝,通过 this.eventLogs.length 动态计算)。Blank() 在标题与计数之间撑开空间。
日志流主体是固定高度 120 像素的 Scroll 容器(scrollBar(BarState.Off) 隐藏滚动条),内部 ForEach 遍历 this.eventLogs 数组渲染每条日志。每条日志是一个 Row——左侧 emoji 图标根据 log.type 切换(Marker 用📍定位针、POI 用🏷标签),右侧信息列包含类型标签(Marker 类型用霓虹青、POI 类型用警示黄)、名称或 ID、时间文案,以及经纬度坐标(toFixed(4) 保留四位小数,fontFamily('monospace') 等宽字体让坐标对齐美观)。
日志的 ForEach 键值使用 ${log.type}-${log.name}-${log.time} 组合字符串,确保每条日志的唯一性。由于新日志通过 unshift 置顶插入数组,最新的事件始终显示在日志流顶部,用户无需滚动即可看到最近的长按记录。这种"固定高度 + 置顶插入 + 可滚动"的设计,既保证日志流不占用过多地图空间,又让历史日志可回溯查看。
7.5 搜索 Tab(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.accent)
.onClick(() => { this.runSearch(); })
}
.width('100%')
// 搜索状态文案
Text(this.searchState).fontSize(10).fontColor(COLORS.text3).width('100%')
tabSearch 是搜索 Tab 的构建函数,作为 Map Kit 6.1.1 reliability 相关性分数特性的展示页。Tab 顶部特性说明卡标注"searchByText · reliability 相关性评分"标题和"Site 新增 reliability 字段([0,1],1 为完全相关),判断结果与关键字关联程度"说明,让用户理解该 Tab 展示的是搜索结果的可信度评估能力。
搜索框区域是 Row 布局,左侧 TextInput 输入框(layoutWeight(1) 撑满宽度,38 像素高度,暗蓝底色)的 text 属性绑定 this.queryInput(默认"骑行驿站"),onChange 回调实时更新 queryInput 状态。右侧"搜索"按钮使用霓虹青底色,onClick 调用 this.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.chip)
}
.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%')
}
搜索结果列表是搜索 Tab 的核心展示区,使用 List 容器(space: 8 设置列表项间距)包裹 ForEach 遍历的 searchRecords 数组。每条搜索结果必须用 ListItem 包裹(这是 ArkUI List 组件的强制要求——List 的直接子元素只能是 ListItem)。
每条结果卡片内部包含四个信息层级。第一层是名称与等级标签行——左侧地点名称(13 号粗体月白色,maxLines(1) 单行显示,textOverflow 溢出省略号),右侧等级标签通过 reliabilityScore(rec.reliability) 获取标签文案与颜色(高相关霓虹青、中相关警示黄、低相关警示红),标签使用暗蓝底色胶囊形式。第二层是地址行(11 号雾蓝色,单行省略号)。第三层是 reliability 分数条——左侧 Progress 线性进度条(value 为 rec.reliability * 100 将 0~1 映射到 0~100,total: 100,type: ProgressType.Linear,color 使用 reliabilityScore 返回的颜色),右侧数值文本(reliability 0.96 格式,toFixed(2) 保留两位小数,等宽字体)。第四层是直线距离与时间行(10 号暗灰蓝,距离从米转换为千米显示)。
reliability 分数条是该 Tab 的视觉焦点——进度条的长度直观反映相关性高低,颜色(霓虹青/警示黄/警示红)让用户瞬间判断结果可信度。这种"分数条 + 等级标签 + 数值文本"三重呈现方式,确保不同信息偏好的用户都能快速获取相关性信息。列表底部是 codePreviewCard() 代码预览卡,展示 Map Kit 6.1.1 双新特性的核心调用代码。
7.6 我的 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('夜骑 128 次 · 驿站补给 92 折').fontSize(11).fontColor(COLORS.sub)
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
}
.width('100%')
Divider().strokeWidth(1).color(COLORS.line)
Row() {
Text('本月夜骑 11 次').fontSize(11).fontColor(COLORS.sub)
Blank()
Text('里程 386km').fontSize(11).fontColor(COLORS.accent)
}
.width('100%')
}
.padding(14)
.borderRadius(14)
.linearGradient({
angle: 135,
colors: [[COLORS.accentD, 0.0], [COLORS.card, 0.75]]
})
.width('100%')
tabMine 是我的 Tab 的构建函数,展示社员信息与功能入口。Tab 顶部是社员渐变大卡,采用与头部 Banner 相同的 135 度对角渐变(深青到深蓝灰),让社员卡在视觉上与头部形成呼应。卡片上半部分是社员头像(🚴 34 号大字号)与信息列(社员名"夜骑社社员 · 夜光徽章"15 号粗体、社员身份"夜骑 128 次 · 驿站补给 92 折"11 号雾蓝)。Divider 分割线下方是本月数据行——左侧"本月夜骑 11 次"雾蓝色、右侧"里程 386km"霓虹青高亮,通过 Blank() 分居两端。
// 社员权益卡(三条权益说明行)
Column({ space: 8 }) {
Text('🏅 社员专属权益').fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
Row({ space: 8 }) {
Text('·').fontSize(12).fontColor(COLORS.accent)
Text('驿站补给 92 折,充电口每日前 3 次免费').fontSize(11).fontColor(COLORS.sub)
}
.width('100%')
Row({ space: 8 }) {
Text('·').fontSize(12).fontColor(COLORS.accent)
Text('夜光徽章集齐 12 枚可兑换定制码表').fontSize(11).fontColor(COLORS.sub)
}
.width('100%')
Row({ space: 8 }) {
Text('·').fontSize(12).fontColor(COLORS.accent)
Text('每月社群夜骑活动优先报名,含领骑护航').fontSize(11).fontColor(COLORS.sub)
}
.width('100%')
}
.padding(12)
.borderRadius(12)
.backgroundColor(COLORS.card)
.width('100%')
社员权益卡以深蓝灰底色卡片形式展示三条专属权益。每条权益以霓虹青圆点 · 开头(12 号霓虹青),后接权益说明文案(11 号雾蓝色)。三条权益分别涵盖:补给折扣与充电免费(92 折 + 每日前 3 次充电免费)、徽章兑换(集齐 12 枚夜光徽章可兑换定制码表)、社群活动优先报名(含领骑护航)。这三条权益设计精准对应了夜骑骑手的核心需求——经济优惠、游戏化激励、社交归属。
// 功能清单
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%')
}
功能清单区域使用 ForEach 遍历 this.funcList 数组(8 条功能项)生成行式卡片列表。每行布局是经典的"图标 + 名称 + 值 + 箭头"四段式:左侧 emoji 图标(18 号)、功能名(13 号月白色,layoutWeight(1) 撑满中间)、状态值(11 号雾蓝色)、右侧箭头符号 ›(14 号暗灰蓝,暗示可点击进入详情)。这种布局是移动端设置页的标准范式,信息层级清晰、视觉一致性好。
7.7 代码预览卡与底部导航
/** 双特性代码预览卡(深夜蓝黑底 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.accent).fontFamily('monospace')
Text('const score = site.reliability // 驿站相关性')
.fontSize(9).fontColor(COLORS.accent).fontFamily('monospace')
Text('eventManager.onMarkerLongClick(cb) // 24+')
.fontSize(9).fontColor(COLORS.second).fontFamily('monospace')
Text('eventManager.onPoiLongClick(cb) // 24+')
.fontSize(9).fontColor(COLORS.second).fontFamily('monospace')
}
.padding(10)
.borderRadius(8)
.backgroundColor(COLORS.codeBg)
.width('100%')
}
.padding(10)
.borderRadius(10)
.backgroundColor(COLORS.chip)
.width('100%')
}
codePreviewCard 是代码预览卡构建函数,以深夜蓝黑底色(COLORS.codeBg = #0A101E)模拟代码编辑器风格展示 Map Kit 6.1.1 双新特性的核心调用。卡片标题"⌨️ Map Kit 6.1.1 双新特性调用"使用 12 号粗体月白色。代码区域包含四行调用:前两行用霓虹青展示搜索特性(site.searchByText 与 site.reliability),后两行用警示黄展示长按事件特性(onMarkerLongClick 与 onPoiLongClick),注释 // 24+ 标注 API 版本要求。所有代码行使用 fontFamily('monospace') 等宽字体,9 号字号紧凑展示。这种代码预览卡让开发者在应用内即可回顾核心技术调用,是面向开发者场景的友好设计。
/** 底部导航 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)
}
tabBar 是底部导航栏构建函数,使用 Row 容器水平排列 4 个 Tab 项。ForEach 遍历 TAB_LIST 数组,每个 Tab 项是一个 Column——上方 emoji 图标(18 号)、下方标签文字(10 号)。标签文字的 fontColor 通过 this.currentTab === idx 条件切换——选中态使用 COLORS.tabOn(霓虹青 #3EE6D2),未选中态使用 COLORS.text3(暗灰蓝 #66788F)。每个 Tab 项通过 layoutWeight(1) 平均分配宽度,onClick 更新 currentTab 实现页面切换。底部导航栏使用深蓝灰底色(COLORS.card),上下各 8 像素内边距,让导航项有舒适的触摸区域。
7.8 弹窗系统
/** 弹窗遮罩层(点击空白处关闭) */
@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.accent)
.onClick(() => { this.saveRide(); })
}
.width('100%')
}
.padding(16)
.borderRadius(14)
.backgroundColor(COLORS.card)
.width('82%')
}
.width('100%')
.height('100%')
}
弹窗系统由 modalOverlay 遮罩层和三个面板 Builder 函数组成。modalOverlay 是全屏遮罩层,使用半透明深蓝黑(COLORS.mask = rgba(5,10,20,0.7))覆盖主内容,onClick 调用 onClose 回调实现"点击空白处关闭弹窗"。这个遮罩层是所有弹窗的底层,确保弹窗内容上方和下方的交互都被遮罩拦截。
panelAdd 是收藏补给站弹窗,使用 Stack 层叠遮罩层与内容面板。内容面板是一个 Column(82% 宽度居中),包含标题"收藏新补给站"、驿站名称输入框、地址输入框(placeholder 提示"可留空地图选点")、取消与收藏按钮行。两个输入框的 text 属性分别绑定 this.formName 和 this.formAddr,onChange 实时更新状态。按钮行中"取消"使用暗蓝底色配雾蓝文字(次级操作),"收藏"使用霓虹青底色(主操作)调用 this.saveRide() 保存。
/** 编辑备注弹窗:回填当前驿站备注 */
@Builder
panelEdit(onClose: () => void) {
Stack() {
this.modalOverlay(onClose)
Column({ space: 12 }) {
Text('编辑驿站备注').fontSize(15).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
Text(this.editIdx < this.rideList.length ? this.rideList[this.editIdx].name : '')
.fontSize(11)
.fontColor(COLORS.sub)
.width('100%')
TextInput({ placeholder: '输入新备注', text: this.editNote })
.height(38)
.fontSize(12)
.fontColor(COLORS.title)
.placeholderColor(COLORS.text3)
.backgroundColor(COLORS.chip)
.onChange((v: string) => { this.editNote = v; })
Row({ space: 10 }) {
Button('取消')
.layoutWeight(1)
.fontSize(12)
.backgroundColor(COLORS.chip)
.fontColor(COLORS.sub)
.onClick(() => { onClose(); })
Button('保存')
.layoutWeight(1)
.fontSize(12)
.backgroundColor(COLORS.accent)
.onClick(() => { this.updateRide(); })
}
.width('100%')
}
.padding(16)
.borderRadius(14)
.backgroundColor(COLORS.card)
.width('82%')
}
.width('100%')
.height('100%')
}
panelEdit 是编辑备注弹窗,与 panelAdd 结构类似但内容不同。面板标题"编辑驿站备注"下方显示当前编辑的驿站名(通过 this.editIdx < this.rideList.length 边界检查后读取 this.rideList[this.editIdx].name,避免索引越界)。备注输入框的 text 绑定 this.editNote(在 openEditRide 方法中已回填当前备注值),让用户在原有备注基础上修改。"保存"按钮调用 this.updateRide() 保存编辑。
/** 删除确认弹窗:驿站名 + 确认/取消 */
@Builder
panelDel(onClose: () => void) {
Stack() {
this.modalOverlay(onClose)
Column({ space: 12 }) {
Text('删除收藏驿站').fontSize(15).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
Text(this.delIdx < this.rideList.length
? `确定删除「${this.rideList[this.delIdx].name}」吗?` : '确定删除吗?')
.fontSize(12)
.fontColor(COLORS.sub)
.width('100%')
Row({ space: 10 }) {
Button('取消')
.layoutWeight(1)
.fontSize(12)
.backgroundColor(COLORS.chip)
.fontColor(COLORS.sub)
.onClick(() => { onClose(); })
Button('删除')
.layoutWeight(1)
.fontSize(12)
.backgroundColor(COLORS.danger)
.onClick(() => { this.delRide(); })
}
.width('100%')
}
.padding(16)
.borderRadius(14)
.backgroundColor(COLORS.card)
.width('82%')
}
.width('100%')
.height('100%')
}
panelDel 是删除确认弹窗,是三个弹窗中最简洁的。面板标题"删除收藏驿站"下方显示确认提示文案(通过边界检查后格式化为"确定删除「驿站名」吗?“,越界时显示"确定删除吗?”)。按钮行中"取消"使用暗蓝底色(次级操作),"删除"使用警示红底色(COLORS.danger = #FF5D6C,危险操作用红色警示)调用 this.delRide() 执行删除。这种"红色删除按钮 + 确认弹窗"的双保险设计,避免了用户误触删除收藏驿站。
三个弹窗共享相同的结构模式:Stack 包裹遮罩层与内容面板、内容面板 82% 宽度居中、统一的内边距与圆角、统一的标题字号与按钮布局。这种模式一致性让用户在不同弹窗间的操作体验统一,降低了学习成本。所有弹窗的 onClose 回调都由 build 方法传入(() => { this.xxxModal = false; }),确保关闭弹窗时正确重置状态变量。
八、Map Kit 6.1.1 双新特性对比
| 特性维度 | searchByText reliability 相关性分数 | onMarkerLongClick / onPoiLongClick 长按监听 |
|---|---|---|
| 所属模块 | site 搜索模块 | MapEventManager 事件模块 |
| 特性类型 | 数据字段增强 | 事件监听增强 |
| 新增内容 | Site 类型新增 reliability 字段([0,1] 浮点数) | 新增 4 个方法:on/offMarkerLongClick、on/offPoiLongClick |
| 核心价值 | 量化搜索结果与关键字的相关程度 | 捕获地图标注点和兴趣点的长按手势 |
| 回调参数 | 无(字段直接读取) | map.Marker / mapCommon.Poi |
| 数据提取 | s.reliability ?? 0(空值兜底) | marker.getId() / marker.getPosition() / poi.name / poi.position |
| 典型场景 | 搜索"骑行驿站"时区分真正的驿站与名称含"骑行"的非驿站 | 骑手长按地图驿站 Marker 记录位置、长按 POI 标记临时兴趣点 |
| 视觉呈现 | 分数条(Progress 线性进度)+ 等级标签(高/中/低三色) | 日志流(EventLog 数组 unshift 置顶) |
| 兜底策略 | reliability 可选字段,?? 0 兜底为低相关 | off 不传参清除全部订阅 |
| 容错设计 | 搜索失败时保留 Mock 数据保证演示链路 | mapEventManager 空值检查后 return |
| 颜色映射 | ≥0.8 霓虹青 / ≥0.5 警示黄 / <0.5 警示红 | Marker 类型霓虹青 / POI 类型警示黄 |
| 交互开关 | 无(每次搜索自动读取) | 两个 Toggle 开关独立控制 |
九、总结
本文以"夜骑社·骑行驿站"应用为载体,深入剖析了 HarmonyOS 6.1.1 Map Kit 的两大新特性在城市夜骑补给场景中的完整实践。site.searchByText 返回的 Site 类型新增的 reliability 相关性分数字段,从根本上解决了传统地图搜索"只返回距离不返回可信度"的痛点——骑手搜索"骑行驿站"时,应用可以量化每条结果与关键字的关联程度,通过分数条和等级标签直观呈现高相关(≥0.8 霓虹青)、中相关(≥0.5 警示黄)、低相关(<0.5 警示红)三档可信度,帮助骑手快速识别哪些结果是真正的补给驿站、哪些是需要过滤的非驿站地点(如电动车行维修部、共享单车停放点)。
MapEventManager 新增的 onMarkerLongClick / offMarkerLongClick 和 onPoiLongClick / offPoiLongClick 四个方法,为地图交互提供了更丰富的手势入口。传统地图标注只支持点击查看详情,长按监听让骑手在骑行途中可以通过长按快速记录感兴趣的驿站 Marker 或临时 POI 地点到事件日志流——日志条目包含类型(Marker/POI)、名称或 ID、经纬度坐标、时间,新事件通过 unshift 置顶插入确保最新记录始终可见。两组监听通过独立的 Toggle 开关控制,让骑手可以根据实际需要灵活启用或关闭。
在工程实现层面,本应用展示了多个值得借鉴的实践模式。地图初始化采用"回调链"设计——mapCallback 回调中依次完成控制器获取、事件管理器获取、Marker 群批量添加(逐个 await + try-catch)、双长按监听注册四步,确保每一步都在前一步就绪后执行。搜索方法采用"真实搜索 + Mock 兜底"的双轨设计——site.searchByText 成功时用真实结果替换 Mock 数据,失败时保留 Mock 数据保证演示链路不中断,同时 reliability ?? 0 空值合并运算符确保向前兼容旧版 Map Kit。弹窗系统采用"遮罩层 + 内容面板"的 Stack 层叠模式,三个弹窗(收藏/编辑/删除)共享统一的结构与交互范式。
深色主题的"夜幕蓝 + 霓虹青 + 警示黄"三色体系不仅是审美选择,更是夜骑场景的功能性刚需。夜幕蓝 #0D1526 模拟夜空环境对瞳孔刺激最小,霓虹青 #3EE6D2 模拟夜骑车灯荧光效果在高对比度位置跳出,警示黄 #FFD84D 用于中等相关度与安全贴士等需要醒目度的场景。整套配色通过 ColorPalette 接口集中声明 16 个语义化颜色字段,所有 Builder 与组件都通过 COLORS.xxx 引用,保证主题一致性与可维护性。
从行业视角看,城市骑行补给驿站服务是一个正在兴起的细分赛道。骑手的核心痛点是"夜间陌生路段如何快速找到可信的补给点"——传统的地图搜索无法区分真正的驿站与名称含骑行字样的非驿站,传统的地图标注无法在骑行途中快速记录感兴趣的位置。本应用通过 reliability 相关性评分解决搜索可信度问题,通过双长按事件监听解决地图标记快速记录问题,两者结合为夜骑补给场景提供了完整的"搜索可信 + 地图可交互"解决方案,是 Map Kit 6.1.1 新特性在垂直行业场景中的典型落地实践。
附录: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)