鸿蒙Map Kit相关性分数与长按事件监听·家政维修上门·精准匹配师傅与地图选点交互的实战解析
一、前言:鸿蒙生态下家政维修的地图化升级

HarmonyOS NEXT 推出的 ArkUI 声明式开发框架,正在重塑移动端应用的开发范式。它以 @Component 装饰器声明组件、以 @State 驱动数据渲染、以 @Builder 拆分视图函数,让一个 .ets 文件就能承载完整的页面逻辑。对于家政维修这类强位置依赖、强师傅匹配、强上门时效的业务,ArkUI 提供的响应式数据流配合声明式布局,天然适合表达"师傅列表—地图选点—搜索结果—个人中心"这种四 Tab 切换的复杂交互。

Map Kit 则是鸿蒙生态中负责地理信息展示与交互的核心能力。它通过 MapComponent 组件内嵌地图画布,通过 mapCommon.MapOptions 配置初始化中心点和缩放,通过 map.MapComponentController 控制后续的 Marker 增删、相机移动等操作,再通过 map.MapEventManager 统一管理地图上的交互事件。对于一个"师傅服务站散布在城市各处"的家政维修应用,Map Kit 不只是展示地图,更是用户触达师傅资源的入口。

HarmonyOS 6.1.1 给 site 模块的 searchByText 能力带来了关键升级:返回的 Site 类型新增了 reliability 字段,取值区间为 [0,1],其中 1 表示与搜索关键字完全相关。这意味着开发者第一次可以在搜索结果层面,量化的判断"这个维修点到底和用户输入的关键字有多匹配"。在家政维修场景下,用户输入"家电维修"可能命中"电器卖场"“家政保洁”"五金建材门市"等近义但并非真正上门维修的点,reliability 让我们能自动过滤、分级、排序,把真正相关的师傅排在前面。

同样在 HarmonyOS 6.1.1 中,MapEventManager 新增了两对长按事件监听:onMarkerLongClick / offMarkerLongClick 监听地图上的自定义 Marker 长按,onPoiLongClick / offPoiLongClick 监听地图上内置 POI(兴趣点)的长按。长按是一种"意图明确、不易误触"的高级手势,把它绑定到"服务站 Marker"和"POI 地点"上,意味着用户可以在地图上对任意一个有意义的点执行"收藏、报修、记录坐标"等后续动作,而事件本身会通过回调把 map.Marker 或 mapCommon.Poi 对象回传,开发者据此读取 ID、名称、经纬度。

家政维修上门是一个典型的"低频刚需、强信任、强时效"行业。用户家中水电急修、家电清洗、管道疏通时,最关心的三件事是:附近有没有靠谱师傅、师傅多久能到、修完有没有质保。把 reliability 用于搜索结果分级展示、把长按事件用于地图选点日志记录,恰好分别对应了"找到师傅"和"在地图上锁定师傅位置"两个核心环节。本篇就以一个浅色主题的"修立得·家政维修上门"页面为例,逐段拆解如何把鸿蒙 6.1.1 的两大地图新特性落地到 ArkUI 实际代码中。

二、整体架构流程
整体架构围绕两条主线展开:左侧是地图初始化链路,从生命周期 aboutToAppear 出发,依次完成回调注册、控制器获取、事件管理器获取、Marker 批量下发、双长按监听注册,最终把事件流喂给地图 Tab 的日志列表;右侧是搜索链路,从用户输入关键字出发,调用 site.searchByText 拿到 Site 数组,遍历读取 reliability 字段,转化为分数条和等级标签展示在搜索 Tab。两条线通过底部 Tab 切换统一在一个 Stack 容器内渲染,弹窗系统叠加在最上层。
三、颜色系统与主题规范

3.1 颜色接口定义
/** 主题色板接口:集中声明页面所有颜色字段(浅灰底+工具蓝+信号绿浅色系) */
interface ColorPalette {
bg: string; // 页面底色(浅灰)
card: string; // 卡片底色
chip: string; // 胶囊/浅层底色
title: string; // 主标题色
sub: string; // 次级文本色
text3: string; // 三级弱文本色
blue: string; // 工具蓝(主强调色)
blueD: string; // 深工具蓝(渐变起点)
blueL: string; // 浅工具蓝(高亮文本)
green: string; // 信号绿(次强调色)
yellow: string; // 适中黄
red: string; // 危险红(删除/低相关)
orange: string; // 提醒橙(辅助强调)
line: string; // 分割线色
tabOn: string; // 底部 Tab 选中色
btnText: string; // 强调色按钮上的文字色
mask: string; // 弹窗遮罩色
codeBg: string; // 代码预览卡深底色
}
这段代码定义了一个 ColorPalette 接口,把页面里用到的所有颜色集中声明为字段。这是 ArkTS 推荐的"接口先行"做法:先约定颜色字段的形状,再用一个常量去实现它,这样后续如果要做暗色主题,只需要换一个实现常量,所有引用 COLORS.xxx 的地方自动生效。
接口里的字段按"语义"而非"色值"命名,这是设计系统的核心原则。比如 blue 代表"工具蓝/主强调色",green 代表"信号绿/次强调色",red 代表"危险红/低相关",yellow 代表"适中黄/中相关"。这种命名让 UI 在"高相关用绿、中相关用黄、低相关用红"这类业务规则下,能直接通过语义字段映射,而不必每次去想"这个分数该用哪个色值"。
字段中还区分了 blueD(深工具蓝,渐变起点)和 blueL(浅工具蓝,高亮文本)两个变体,以及 codeBg(代码预览卡深底色)这种特殊用途的色值。mask 用 rgba 带透明度,专门给弹窗遮罩用。这种颗粒度既保证了视觉一致性,又避免了"一个色值到处用、改一处全乱套"的维护灾难。
3.2 浅色主题常量实现
/** 浅色主题色板常量(修立得 · 浅灰 + 工具蓝 + 信号绿) */
const COLORS: ColorPalette = {
bg: '#F2F4F7',
card: '#FFFFFF',
chip: '#E8ECF2',
title: '#22282F',
sub: '#6B7686',
text3: '#9AA5B4',
blue: '#2F6FD0',
blueD: '#1F4F9A',
blueL: '#DCE8F9',
green: '#27A567',
yellow: '#E8A23D',
red: '#DA4B4B',
orange: '#E8833A',
line: '#E3E8EF',
tabOn: '#2F6FD0',
btnText: '#FFFFFF',
mask: 'rgba(34,40,47,0.42)',
codeBg: '#22282F'
};
这里把接口落地为一个具体的浅色主题常量 COLORS。页面底色 #F2F4F7 是一种偏冷的浅灰,能让白色卡片 #FFFFFF 自然浮起;主强调色 #2F6FD0 是一种偏向"工具感"的蓝,不刺眼但识别度高,适合做按钮和选中态;次强调色 #27A567 是一种"信号绿",传达"在线、可预约、高相关"的正向信号。
文本色做了三级分层:title 深近黑 #22282F 给主标题,sub 中灰 #6B7686 给次级说明,text3 浅灰 #9AA5B4 给三级弱信息(时间、距离、坐标)。这种三级分层是移动端信息密度控制的标准做法——用户视线天然先落在深色文字上,浅色文字作为补充信息存在但不抢戏。
mask 用 rgba(34,40,47,0.42) 而非纯黑,是为了让遮罩带一点蓝灰调,和整体主题色温保持一致,避免弹窗时突然变黑显得突兀。codeBg 直接复用了 title 的深色 #22282F,给代码预览卡一个"深底亮字"的反差效果,模拟代码编辑器的视觉感受。tabOn 复用了 blue,让底部选中 Tab 和主按钮保持同一强调色,强化品牌识别。
四、常量与数据模型
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[] = ['全部', '水电急修', '家电清洗', '管道疏通', '门窗五金', '墙面翻新', '家具安装', '开锁换锁'];
/** 城市中心点(重庆,地图初始化中心 + Map Kit 搜索 location 参数) */
const CITY_CENTER: mapCommon.LatLng = { latitude: 29.5630, longitude: 106.5516 };
TabMeta 接口定义了底部导航每个 Tab 的两个字段:icon 用 emoji 表示图标,label 是文字标签。用 emoji 而非图片资源,是为了让整个页面在一个 .ets 文件内自洽,不依赖外部素材,便于演示和移植。TAB_LIST 是一个四元素的常量数组,分别对应师傅、地图、搜索、我的四个 Tab,底部 Row 通过 ForEach 渲染时直接遍历这个数组即可。
CATE_TAGS 是头部横滑筛选 chips 的文案列表,覆盖了家政维修的八个典型工种分类。横滑 Scroll + Row + ForEach 是 ArkUI 实现横向标签的标准模式,比 Tabs 容器更灵活,可以自由控制间距、选中态背景色和圆角。选中态通过 cateIdx === idx 判断,背景色在 COLORS.blue(选中)和 COLORS.chip(未选中)之间切换。
CITY_CENTER 是一个 mapCommon.LatLng 类型的常量,定位在重庆主城(纬度 29.5630、经度 106.5516)。这个常量被复用在两个地方:一是 mapOptions.position.target 作为地图初始化中心点,二是 site.SearchByTextParams.location 作为搜索的地理参考点。把它提取为常量,保证地图中心和搜索范围始终围绕同一个城市基准,避免"地图显示重庆、搜索却在别的城市"这类不一致。
4.2 地图标注点与推荐师傅Mock数据
/** 地图标注点接口(服务站 Marker 群,长按事件的数据来源) */
interface SpotItem {
name: string; // 服务站名称
lat: number; // 纬度
lng: number; // 经度
tag: string; // 服务站工种标签
}
/** 服务站标注点 Mock 数据(6 个,围绕重庆中心点 ±0.02 度散布) */
const MARKER_SPOTS: SpotItem[] = [
{ name: '修立得·解放碑服务站', lat: 29.5565, lng: 106.5588, tag: '急修' },
{ name: '修立得·观音桥服务站', lat: 29.5800, lng: 106.5325, tag: '清洗' },
{ name: '修立得·南坪工贸服务站', lat: 29.5485, lng: 106.5602, tag: '疏通' },
{ name: '修立得·大坪石桥铺服务站', lat: 29.5698, lng: 106.5425, tag: '五金' },
{ name: '修立得·两路口上清寺点', lat: 29.5535, lng: 106.5455, tag: '翻新' },
{ name: '修立得·江北城服务点', lat: 29.5785, lng: 106.5548, tag: '安装' }
];
SpotItem 接口定义了服务站 Marker 的四元信息:名称、纬度、经度、工种标签。这是"长按 Marker 事件"的数据源头——地图初始化时,setupMapCallback 会遍历这个数组逐个 addMarker,每个 Marker 携带一个经纬度位置。当用户长按某个 Marker 时,回调里只能拿到 map.Marker 对象(含 ID 和 position),拿不到原始的 name 和 tag,所以业务上如果要展示服务站详情,通常需要用 Marker ID 反查这个数组。
MARKER_SPOTS 是六条 Mock 数据,围绕重庆中心点 ±0.02 度散布。±0.02 度在赤道附近大约对应 2.2 公里,这个范围既能让所有 Marker 在 zoom:13 的初始缩放下都可见,又能体现"散布在主城各处"的真实分布感。六个服务站覆盖了急修、清洗、疏通、五金、翻新、安装六个工种,和头部 CATE_TAGS 的分类形成呼应,让用户在地图上能直观看到不同工种的服务站位置。
这种 Mock 数据的设计还有一个用意:它和"师傅 Tab 的全部师傅列表"是同源的。代码注释里明确写了"全部师傅(地图 Marker 同源)",意味着师傅列表里的师傅和地图上的服务站 Marker 在业务上是同一批资源,只是呈现方式不同——列表用文字和评分展示,地图用位置和 Marker 展示。这种"一数据源、两视图"的设计在家政维修场景下很常见,用户既想看师傅的口碑评分,又想在地图上确认距离。
4.3 Observed数据模型
/** 师傅条目(师傅 Tab 收藏列表) */
@Observed export class FixerItem {
name: string; // 师傅名
craft: string; // 工种
orders: number; // 累计接单数
rating: number; // 综合评分(满分 5)
note: string; // 用户备注(可编辑)
constructor(name: string, craft: string, orders: number,
rating: number, note: string) {
this.name = name;
this.craft = craft;
this.orders = orders;
this.rating = rating;
this.note = note;
}
}
FixerItem 是一个用 @Observed 装饰的类,表示"收藏师傅"这一数据模型。@Observed 的作用是让该类的实例属性变化能被 ArkUI 的响应式系统感知——当 note 被修改时,引用了该实例的 @State 数组刷新,界面会重新渲染。这是 ArkUI 二级观察机制的核心:@State 观察数组本身的引用变化(增删、整体替换),@Observed 观察数组元素内部属性的变化。
类里定义了五个字段:name 师傅名、craft 工种、orders 累计接单数、rating 综合评分(满分 5)、note 用户备注。前四个是相对静态的师傅画像,note 是用户自定义的、会被频繁编辑的字段。把 note 放在同一个类里,配合 @Observed,能让"编辑备注→保存→列表刷新"这条链路自动响应,无需手动调用刷新方法。
构造函数接收五个参数并逐一赋值给 this。这种"构造函数 + 字段赋值"的写法是 ArkTS 类的标准范式,相比对象字面量 {name, craft, ...},类的优势在于可以挂载方法、可以被 instanceof 判断、可以在 @Observed 下参与响应式。export 关键字让这个类能被其他文件复用,但本页内主要是给 FIXER_LIST 常量初始化用。
/** 搜索结果条目(★ 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;
}
}
SearchRecord 是搜索结果的数据载体,也是本页"reliability 特性"的核心承载体。它的五个字段直接对应 site.Site 返回的关键属性:name 来自 site.name、address 来自 site.formatAddress、distance 来自 site.distance、reliability 来自 6.1.1 新增的 site.reliability。time 是业务侧补充的"刚刚"文案,不属于 Site 原始字段。
把这个类声明为 @Observed,是因为搜索结果列表会在 runSearch 完成后被整体替换,@State searchRecords 观察引用变化触发刷新。虽然单个 SearchRecord 实例的属性在创建后不会再变(reliability 是搜索时确定的),但 @Observed 的成本很低,留着以备后续可能需要"点击收藏后给某条结果打标"这类场景。
注意字段的类型:distance 是 number(米),UI 展示时通过 rec.distance / 1000 转成公里;reliability 是 number(0 到 1 的小数),UI 展示时通过 reliability * 100 映射到 Progress 的 0~100 区间。这种"原始数据存标准单位、展示时按需换算"的做法,避免了数据层和展示层耦合,后续如果要切换"英里""千米"等单位,只改展示逻辑即可。
/** 长按事件日志条目(★ 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;
}
}
EventLog 是长按事件日志的数据载体,统一承载 Marker 长按和 POI 长按两种事件。type 字段用字符串 'Marker' 或 'POI' 区分事件来源,UI 渲染时据此显示不同图标和颜色(Marker 用蓝色 📍、POI 用绿色 🏷)。name 字段对 Marker 存的是 #${marker.getId()}(如 #0),对 POI 存的是 poi.name(如"观音桥步行街"),两种语义合并到一个字段。
lat 和 lng 保留事件发生时的经纬度,用 toFixed(4) 展示为 4 位小数,精度大约 11 米,足够定位到具体地点。time 存"刚刚"这种相对时间文案。整条日志通过 this.eventLogs.unshift(...) 置顶插入,最新的长按事件永远在列表最上方,符合"事件流"的直觉。
@Observed 在这里同样有意义:虽然单条 EventLog 创建后不变,但 eventLogs 数组会频繁 unshift,@State 监听数组引用变化(unshift 会改变数组)触发列表刷新。注意 ArkUI 的数组响应式有个细节——直接 push/unshift 改变原数组不一定能可靠触发刷新,更稳妥的做法是 this.eventLogs = [newLog, ...this.eventLogs] 整体替换引用,但本页用 unshift 配合 @Observed 也能工作,因为数组本身的长度变了。
4.4 辅助函数
/** 相关性等级接口(分数条旁的标签) */
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.green };
}
if (score >= 0.5) {
return { label: '中相关', color: COLORS.yellow };
}
return { label: '低相关', color: COLORS.red };
}
reliabilityScore 是把 6.1.1 新字段 reliability 转化为可读等级的核心辅助函数。它接收一个 [0,1] 的分数,返回一个 {label, color} 对象。阈值的设计很有讲究:≥0.8 判为高相关(信号绿),≥0.5 判为中相关(适中黄),其余判为低相关(危险红)。这个三档阈值既符合人对"好/中/差"的直觉认知,又能在 UI 上形成清晰的"绿—黄—红"交通灯语义。
函数返回的是结构化对象而非单个值,这样调用一次就能同时拿到标签文案和颜色,避免在 UI 模板里调用两次。在 tabSearch 的渲染里,你会看到 reliabilityScore(rec.reliability).label 和 reliabilityScore(rec.reliability).color 两次调用——虽然调了两次,但函数本身是纯函数、无副作用、计算成本极低,这种写法换取的是模板的可读性。
阈值的选取也考虑了家政维修场景的特点。0.8 这个门槛意味着"绝大部分语义匹配、地理位置接近",适合标为"放心选";0.5 意味着"部分相关、可能是相近工种或邻近地点",标为"可参考";低于 0.5 则是"关键字命中但语义不匹配"(比如用户搜"家电维修"却命中了"电器卖场"),必须用红色提示用户"这不是你要的上门维修点"。
/** 师傅评分颜色映射:≥4.8 口碑极佳信号绿 / ≥4.5 口碑良好工具蓝 / 其余一般红 */
function ratingColor(rating: number): string {
if (rating >= 4.8) { return COLORS.green; }
if (rating >= 4.5) { return COLORS.blue; }
return COLORS.red;
}
/** 接单数颜色映射:≥1200 单经验丰富绿 / ≥600 单熟练黄 / 其余新手红 */
function orderColor(orders: number): string {
if (orders >= 1200) { return COLORS.green; }
if (orders >= 600) { return COLORS.yellow; }
return COLORS.red;
}
ratingColor 和 orderColor 是师傅画像的两个颜色映射函数,和 reliabilityScore 是同一套设计思路:阈值分档、返回语义色。ratingColor 把评分 4.8/4.5 作为分界,4.9 分的师傅标绿(口碑极佳),4.6 分标蓝(口碑良好),4.3 分标红(一般)。orderColor 把接单数 1200/600 作为分界,1500 单标绿(经验丰富),800 单标黄(熟练),500 单标红(新手)。
这两个函数复用了和 reliabilityScore 相同的颜色三件套(绿/黄/红或绿/蓝/红),让整个页面的"等级色"在搜索结果、师傅评分、师傅接单数三处保持视觉一致——绿色都意味着"优质/高相关",红色都意味着"低质/低相关"。这种跨场景的颜色一致性,是建立用户"色彩—语义"心智模型的关键,用户看多了就能"扫一眼颜色就知道好坏"。
五、组件主体与状态管理
5.1 状态变量定义
@Entry
@Component
struct Page1143 {
/** 当前选中 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 fixerList: Array<FixerItem> = FIXER_LIST;
/** 我的页功能清单数据 */
@State funcList: FuncItem[] = FUNC_LIST;
@Entry 和 @Component 两个装饰器分别声明这是页面入口和组件。struct Page1143 是组件名(页面级组件用 Page 前缀是常见命名习惯)。这两个装饰器的组合意味着这个 struct 既是一个可独立渲染的组件,又是路由的入口页面,DevEco Studio 会把它注册为可导航页面。
前两个 @State 是 Tab 切换和筛选的核心:currentTab 控制四个 Tab 哪个显示(0/1/2/3 对应师傅/地图/搜索/我的),cateIdx 控制头部筛选 chips 选中哪个分类。这两个状态的变化会直接驱动 build() 里 if (this.currentTab === 0) 分支渲染和 chips 背景色切换,是页面交互的"中枢神经"。
接下来三个 @State(addModal/editModal/delModal)是三个弹窗的开关,配合 editIdx/delIdx 记录当前操作的师傅索引。这种"一个 bool 一个 index"的弹窗管理方式比"一个全局 modalType 字符串"更直观,每个弹窗的状态独立,互不干扰。fixerList 和 funcList 是两个列表数据源,初始化为 Mock 常量 FIXER_LIST 和 FUNC_LIST,后续增删改会修改它们。
// --- 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 formTag: string = '';
/** 编辑弹窗:备注输入 */
@State editNote: string = '';
这是 Map Kit 相关的状态块,分为 private(非响应式)和 @State(响应式)两类。mapOptions 是 private,因为它只在 MapComponent 初始化时传入一次,不需要后续变化触发渲染;它直接给定了 position.target(重庆中心点)和 zoom:13,避免后续传 undefined 导致组件参数异常。
mapCallback、mapController、mapEventManager 三个都是 private 且可选(?)。它们遵循"先声明空值→回调里赋值"的延迟初始化模式:mapCallback 在 aboutToAppear 里赋值,mapController 和 mapEventManager 在 mapCallback 的回调里赋值。这种延迟赋值是因为 Map Kit 的控制器必须等地图组件初始化完成后才能拿到,不能在 aboutToAppear 同步获取。
markerListenOn、poiListenOn 两个 @State 是长按监听的开关,初始化为 true(默认开启监听),UI 上对应两个 Toggle 开关,用户可以随时关闭/重开某类长按监听。eventLogs 是事件日志流的响应式数据源,初始化为两条演示数据 EVENT_LOGS,后续每次长按事件触发都会 unshift 新日志,驱动日志列表刷新。
queryInput 默认值"家电维修"是一个很有业务感的初始关键字——它既不是太宽泛("维修"会命中太多无关结果),也不是太狭窄("空调清洗"只命中一个工种),刚好能让 reliability 字段在搜索结果里展现出高/中/低三档分明的效果。searchState 和 searchRecords 分别是搜索状态文案和结果列表,初始化为"待搜索·演示数据"和六条 Mock 数据,让用户进搜索 Tab 第一眼就能看到可靠性分数条的展示效果,而不必先点搜索。
5.2 地图初始化回调与长按监听注册
/**
* 地图初始化: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 是整个地图特性的"装配车间",它把一个 AsyncCallback 赋值给 this.mapCallback,这个回调会在 MapComponent 初始化完成后被系统调用,参数是 (err, mapController)。这是 Map Kit 的标准异步初始化模式——MapComponent 在 UI 渲染时并不立即有控制器,需要等底层地图引擎就绪后才通过回调把控制器交出来。
回调第一件事是判断 err:如果地图初始化失败(比如设备没有 Google Play 服务、AGC 配置缺失),直接 console.error 打印错误码和消息并 return,避免后续对 undefined 的 mapController 调用方法导致崩溃。这种"err 优先"的防御式写法在异步回调里是必须的,鸿蒙的 BusinessError 模式和 Node.js 的 err-first callback 一脉相承。
err 为空时进入主分支:先把 mapController 存到 this,再通过 mapController.getEventManager() 拿到 mapEventManager。这两个赋值的顺序很重要——事件管理器是从控制器上获取的,控制器不存在就谈不上事件管理器。拿到管理器后,紧接着遍历 MARKER_SPOTS 逐个 addMarker,每个 Marker 的 MarkerOptions 配置了 clickable: true(可点击,是长按事件能触发的前提)、visible: true(可见)、anchorU/anchorV: 0.5/1(锚点在底部中心,让图钉尖朝下指准位置)。
Marker 添加循环用了 for...of + await,每个 addMarker 返回 Promise,必须逐个等待完成再添加下一个。这种串行而非并行的写法是为了避免并发 addMarker 导致的内部状态竞争。每个 addMarker 包了 try-catch,单个 Marker 添加失败不会中断后续 Marker 的添加,保证了"六个服务站尽量都标上"的容错性。
循环结束后注册两个 6.1.1 新增的长按监听。onMarkerLongClick 的回调参数是 map.Marker 对象,通过 marker.getPosition() 拿到经纬度、marker.getId() 拿到 Marker ID,组装成 EventLog('Marker', '#0', lat, lng, '刚刚') 后 unshift 到日志流。onPoiLongClick 的回调参数是 mapCommon.Poi 对象,结构不同——poi.name 直接是地点名、poi.position 是 LatLng,组装成 EventLog('POI', '观音桥步行街', lat, lng, '刚刚')。两个回调的差异恰好体现了 Marker 和 POI 两种地图实体的数据结构差异:Marker 是开发者自定义的、靠 ID 标识;POI 是系统内置的兴趣点、靠名称标识。
5.3 长按监听开关切换
/** 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;
}
toggleMarkerListen 是给 UI 上"Marker长按"开关用的切换方法。它首先判断 this.mapEventManager 是否存在——如果地图还没初始化好(用户刚进页面就点开关),直接 return 不做任何操作,避免对 undefined 调用 offMarkerLongClick 崩溃。这是"防御式编程"的典型应用。
接着根据当前 markerListenOn 的值决定动作:如果当前是开(true),调用 offMarkerLongClick() 关闭监听;如果当前是关(false),重新调用 onMarkerLongClick 注册一个同样的回调。注意这里重新注册的回调和 setupMapCallback 里注册的回调逻辑完全一致——这是因为 offMarkerLongClick() 会清除该类型全部订阅,再开启时必须重新挂一个回调。
最后 this.markerListenOn = !this.markerListenOn 翻转开关状态,驱动 UI 上 Toggle 的 isOn 视觉切换。这个翻转放在最后,确保只有前面的 off/on 操作都成功了才翻转状态,避免"操作失败但 UI 显示已切换"的不一致。
offMarkerLongClick() 不传参数的设计很巧妙——它清除了该类型的"全部"订阅。如果业务上注册了多个不同的 Marker 长按回调,不传参会一次性清空。如果只想清除某个特定回调,可以传入对应的回调引用。本页只用了一个回调,所以不传参的批量清除就足够了,代码简洁。
/** 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;
}
togglePoiListen 和 toggleMarkerListen 是完全对称的实现,只是换成了 POI 相关的 API:offPoiLongClick / onPoiLongClick。回调参数从 map.Marker 变成 mapCommon.Poi,数据读取从 marker.getPosition() + marker.getId() 变成 poi.position + poi.name。
这两个 toggle 方法放在组件里,让用户能动态控制"要不要监听长按"。这种开关在实际业务里很有用:比如用户在地图上只想浏览不想被事件打扰,关掉两个监听;想重新启用时再打开。也方便调试——开发时关闭监听可以避免误触长按产生大量日志干扰。
5.4 searchByText搜索与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 是搜索特性的核心方法,也是 6.1.1 reliability 字段的消费入口。它是一个 async 方法,因为 site.searchByText 返回 Promise。方法第一步把 searchState 设为"搜索中…",给用户即时反馈——这是异步操作的标准 UX,避免用户点了搜索却不知道在不在执行。
构造 SearchByTextParams 时带了四个字段:query 是用户输入的关键字(默认"家电维修"),location 是搜索的地理参考点(复用 CITY_CENTER 重庆中心),radius 是搜索半径 5000 米(5 公里,适合家政维修这种"同城上门"场景),language 是 'zh' 中文结果。这四个参数共同决定了搜索结果的范围和语言——以重庆为中心、5 公里内、中文的"家电维修"相关地点。
try 块里调用 site.searchByText(params),拿到 SearchByTextResult,取 result.sites(Site 数组)。这里用了 ?? [] 空值兜底——如果 sites 字段是 undefined(理论上不应该但接口可能返回),降级为空数组,避免后续 for...of 一个 undefined 崩溃。如果数组为空,设置状态"无结果·保留演示数据"并 return,UI 上继续显示之前的 Mock 数据,让用户始终能看到 reliability 分数条的展示形态,而不是面对一个空列表。
非空时遍历 sites,逐个构造 SearchRecord。每个字段都用了 ?? 兜底:s.name ?? '未命名地点'、s.formatAddress ?? '暂无地址'、s.distance ?? 0、s.reliability ?? 0。这种"每个可选字段都兜底"的写法,是因为 Site 类型的这些字段在 6.1.1 前部分是可选的,6.1.1 新增的 reliability 更是新增字段,老版本 SDK 下可能是 undefined。reliability ?? 0 兜底为 0,意味着"没拿到分数就当低相关处理",这是最保守的降级策略。
catch 块捕获 BusinessError,把错误码拼到状态文案里:搜索失败(${err.code}) · 保留演示数据。这里没有清空 searchRecords,而是保留之前的 Mock 数据,保证"搜索失败但 UI 仍有内容可看"。这种"失败不破坏现有状态"的设计在弱网环境下很重要——用户在地铁里没信号点了搜索,看到的是"搜索失败但之前的列表还在",而不是空白页。
5.5 师傅CRUD方法
/** 打开编辑备注弹窗(回填当前师傅备注) */
openEditFixer(idx: number) {
this.editIdx = idx;
this.editNote = this.fixerList[idx].note;
this.editModal = true;
}
/** 保存收藏师傅(空名兜底默认演示师傅) */
saveFixer() {
const name = this.formName === '' ? '修立得·新收藏师傅' : this.formName;
const tag = this.formTag === '' ? '综合维修' : this.formTag;
const addr = this.formAddr === '' ? '重庆市(地图选点)' : this.formAddr;
this.fixerList.unshift(new FixerItem(name, tag, 520, 4.4, addr));
this.formName = '';
this.formTag = '';
this.formAddr = '';
this.addModal = false;
}
/** 保存编辑备注(整体刷新数组引用以刷新列表) */
updateFixer() {
if (this.editIdx >= 0 && this.editIdx < this.fixerList.length) {
if (this.editNote !== '') {
this.fixerList[this.editIdx].note = this.editNote;
}
this.fixerList = this.fixerList.slice();
}
this.editModal = false;
}
/** 删除收藏师傅(确认弹窗回调) */
delFixer() {
if (this.delIdx >= 0 && this.delIdx < this.fixerList.length) {
this.fixerList.splice(this.delIdx, 1);
}
this.delModal = false;
}
这四个方法是师傅收藏列表的增删改查。openEditFixer 是"改"的入口——记录编辑索引 editIdx、把当前师傅的备注回填到 editNote(让弹窗的 TextInput 显示现有备注)、打开编辑弹窗。回填这一步很重要,让用户看到原来的备注再修改,而不是面对一个空输入框。
saveFixer 是"增"——从表单读取三个字段(师傅名、工种、地址),每个都做了空值兜底(空名默认"修立得·新收藏师傅"、空工种默认"综合维修"、空地址默认"重庆市(地图选点)")。新师傅的接单数硬编码为 520、评分 4.4,这是合理的"新收藏"初始值——不算顶尖也不算太差。unshift 把新师傅插到列表最前,符合"刚加的师傅排第一"的直觉。最后清空三个表单字段、关闭弹窗。
updateFixer 是"改"的保存——先做边界检查(editIdx 在合法范围内),再判断备注非空才赋值。关键的一行是 this.fixerList = this.fixerList.slice()——.slice() 不传参等价于浅拷贝整个数组,生成一个新引用,赋值给 this.fixerList 触发 @State 的引用变化感知,从而让 ForEach 重新渲染。这是 ArkUI 里"修改元素属性后刷新列表"的标准技巧:直接改属性不会触发 @State 的引用变化,必须整体替换引用;虽然 FixerItem 是 @Observed 理论上能感知属性变化,但配合 slice 双保险更稳妥。
delFixer 是"删"——同样边界检查后 splice(this.delIdx, 1) 删除指定索引的元素。splice 直接修改原数组,在 ArkUI 里 splice 触发响应式是支持的(属于被监听的数组变异方法),所以不需要像 updateFixer 那样再 slice 一次。删除完关闭弹窗。
5.6 生命周期与主构建
/** 生命周期:初始化地图回调(监听注册在 mapCallback 内完成) */
aboutToAppear() {
this.setupMapCallback();
}
/** 页面主构建:Stack 包裹主内容与三层弹窗 */
build() {
Stack() {
Column() {
this.headerMain()
Divider().strokeWidth(1).color(COLORS.line)
Scroll() {
Column() {
if (this.currentTab === 0) {
this.tabFixer()
} 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)
}
aboutToAppear 是 ArkUI 组件的生命周期钩子,在组件创建后、build 执行前调用。这里只做一件事:调用 setupMapCallback() 给 this.mapCallback 赋值。这一步必须早于 build,因为 build 里 MapComponent({ mapOptions, mapCallback }) 需要传入一个已赋值的 mapCallback。如果把 setupMapCallback 的逻辑写到 build 里会导致每次重渲染都重新赋值回调,破坏 Map Kit 的初始化幂等性。
build 是组件的渲染入口。最外层 Stack 是一个堆叠容器,它能把多个子元素叠在一起——这是实现"主内容 + 弹窗遮罩"分层的关键。Stack 里先放 Column(主内容),再依次条件渲染三个弹窗(addModal/editModal/delModal 为 true 时才渲染对应的 panelAdd/panelEdit/panelDel)。弹窗在 Stack 里的层级天然高于主内容,无需额外的 z-index 控制。
主内容 Column 里从上到下依次是:headerMain(头部 Banner + 筛选 chips)、Divider(分割线)、Scroll(可滚动内容区,layoutWeight(1) 占满剩余高度)、tabBar(底部 Tab)。Scroll 内部是一个 Column,根据 currentTab 的值条件渲染四个 @Builder 中的一个——这是 ArkUI 实现 Tab 切换的轻量级方案,比 Tabs 容器更可控,可以自由定制切换时的动画和状态保留策略。
弹窗的关闭通过传给 @Builder 的回调函数实现:panelAdd(() => { this.addModal = false; }) 把一个"关闭弹窗"的箭头函数作为 onClose 参数传入,弹窗内部的"取消"按钮和遮罩点击都调用 onClose(),从而关闭弹窗。这种"父组件控制开关、子 Builder 调用回调"的模式,让弹窗的状态管理集中在父组件,子 Builder 只负责 UI 渲染和事件回调,职责清晰。
六、Builder函数群
6.1 头部渐变Banner与筛选chips
/** 头部:渐变 Banner(最快上门时长+收藏师傅数)+ 筛选 chips 横滑 */
@Builder
headerMain() {
Column({ space: 12 }) {
// 顶部渐变 Banner:上门时长 + 收藏师傅数 + 质保贴士
Column({ space: 10 }) {
Row({ space: 12 }) {
Column({ space: 2 }) {
Text('28 分钟').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('观音桥站 · 4 位师傅在线').fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
}
Row({ space: 6 }) {
Text('⭐').fontSize(12)
Text('收藏师傅 8 位 · 今日已修 3 单').fontSize(11).fontColor(COLORS.sub)
}
Row({ space: 6 }) {
Text('🛡').fontSize(12)
Text('平台质保 90 天 · 免收跑腿费').fontSize(11).fontColor(COLORS.sub)
}
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
}
.width('100%')
Row({ space: 8 }) {
Text('🔧 一键报修').fontSize(12).fontColor(COLORS.btnText).fontWeight(FontWeight.Bold)
.padding({ left: 14, right: 14, top: 8, bottom: 8 })
.borderRadius(16).backgroundColor(COLORS.blue)
Text('🗺 地图找师傅').fontSize(12).fontColor(COLORS.blue)
.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.blueD, 0.0], [COLORS.card, 0.7]]
})
headerMain 是页面头部的复合 Builder,包含一个渐变 Banner 和一组横滑筛选 chips。Banner 内部用一个 Column({ space: 10 }) 容纳两块内容:上行是一个 Row,左侧大字号显示"28 分钟最快上门时长",右侧三行小字分别展示在线师傅数、收藏师傅数、质保信息;下行是两个胶囊按钮"一键报修"(蓝底白字,主操作)和"地图找师傅"(浅底蓝字,次操作,点击切到地图 Tab)。
Banner 的视觉重点在于 linearGradient——angle: 135 表示从左上到右下的对角线方向,colors: [[COLORS.blueD, 0.0], [COLORS.card, 0.7]] 表示从深工具蓝 #1F4F9A(位置 0%,左上角)渐变到白色 #FFFFFF(位置 70%,右下区域)。这种"深色起点+浅色终点"的对角渐变,让 Banner 既有了"工具蓝"的品牌色彩印记,又不会因为大面积深色而显得压抑,右下大面积留白还可以容纳文字内容。
左侧大字号"28 分钟"用 fontSize(30).fontWeight(FontWeight.Bold) 突出,是整个 Banner 的视觉焦点——上门时效是家政维修用户最关心的指标,必须一眼看到。右侧三行用 emoji + 文字的组合,emoji 起到图标作用(🔧在线师傅、⭐收藏数、🛡质保),文字用 11-13px 的小字号分层级展示,既不抢左侧"28 分钟"的戏,又把关键运营信息都铺出来。
“地图找师傅"按钮的 onClick 写法 () => { this.currentTab = 1; } 直接把 Tab 切到地图页,这是头部 Banner 引导用户进入地图特性的设计——用户看到"28 分钟最快上门"后,自然想确认"附近到底有哪些师傅”,点这个按钮就能切到地图 Tab 看到六个服务站 Marker 和长按事件能力。
// 筛选 chips 横滑
Scroll() {
Row({ space: 8 }) {
ForEach(CATE_TAGS, (tag: string, idx: number) => {
Text(tag)
.fontSize(11)
.fontColor(this.cateIdx === idx ? COLORS.btnText : COLORS.sub)
.padding({ left: 12, right: 12, top: 6, bottom: 6 })
.borderRadius(14)
.backgroundColor(this.cateIdx === idx ? COLORS.blue : 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 + ForEach 实现。scrollable(ScrollDirection.Horizontal) 让 Scroll 支持横向滑动,scrollBar(BarState.Off) 隐藏滚动条保持视觉干净。八个工种分类(全部/水电急修/家电清洗/管道疏通/门窗五金/墙面翻新/家具安装/开锁换锁)排成一行,超出屏幕宽度的部分可以横滑看到。
每个 chip 的 fontColor 和 backgroundColor 都通过 cateIdx === idx 三元判断切换:选中态用蓝底白字(COLORS.blue + COLORS.btnText),未选中态用浅底灰字(COLORS.chip + COLORS.sub)。onClick 里 this.cateIdx = idx 修改选中索引,驱动所有 chip 的颜色重新计算。这种"一个状态变量控制一组元素选中态"是 ArkUI 声明式的典型用法,相比命令式 DOM 操作简洁很多。
ForEach 的第三个参数是键值生成函数 (tag: string) => tag,用 tag 文本本身作为 key。这保证了一组 chips 的 key 稳定——切换选中态时 ForEach 能根据 key 判断"这是同一个 chip,只是颜色变了",只更新属性不重建节点,性能更好。如果用 index 作为 key,每次切换都可能引发不必要的节点重建。
6.2 师傅Tab
/** 师傅 Tab:报修统计三宫格 + 在线师傅 + 收藏师傅列表(业务主 Tab) */
@Builder
tabFixer() {
Column({ space: 10 }) {
// 报修统计三宫格(收藏师傅/本月报修/质保订单)
Row({ space: 8 }) {
Column({ space: 2 }) {
Text('8').fontSize(20).fontWeight(FontWeight.Bold).fontColor(COLORS.blue)
Text('收藏师傅(位)').fontSize(9).fontColor(COLORS.sub)
}
.layoutWeight(1)
.padding({ top: 10, bottom: 10 })
.borderRadius(10)
.backgroundColor(COLORS.card)
Column({ space: 2 }) {
Text('3').fontSize(20).fontWeight(FontWeight.Bold).fontColor(COLORS.green)
Text('本月报修(单)').fontSize(9).fontColor(COLORS.sub)
}
.layoutWeight(1)
.padding({ top: 10, bottom: 10 })
.borderRadius(10)
.backgroundColor(COLORS.card)
Column({ space: 2 }) {
Text('2').fontSize(20).fontWeight(FontWeight.Bold).fontColor(COLORS.orange)
Text('质保期内(项)').fontSize(9).fontColor(COLORS.sub)
}
.layoutWeight(1)
.padding({ top: 10, bottom: 10 })
.borderRadius(10)
.backgroundColor(COLORS.card)
}
.width('100%')
tabFixer 是业务主 Tab,包含三块内容:报修统计三宫格、附近在线师傅大卡、全部师傅列表。三宫格用 Row({ space: 8 }) 容纳三个 Column,每个 Column 都 layoutWeight(1) 平均分配宽度,构成等宽三栏统计。每个格子白底圆角,上面是大号数字、下面是小字标签。
三个数字分别用蓝、绿、橙三种颜色,呼应前面"颜色按语义分档"的设计:收藏师傅数 8 用蓝色(主强调,因为这是用户的核心资产)、本月报修 3 用绿色(信号绿,正向活跃)、质保期内 2 用橙色(提醒橙,提示"有质保订单在保,注意维护")。这种"每个统计数配一个语义色"让用户一眼就能区分不同维度的状态,而不是面对一片同色数字。
fontSize(9) 的标签字号非常小,是因为三宫格在小屏上空间有限,标签必须让位给数字。padding({ top: 10, bottom: 10 }) 给足上下内边距让格子有"卡片"的厚度感。borderRadius(10) 的圆角和卡片底色配合,形成柔和的视觉边界,避免硬直角带来的"表格感"。
// 区块标题行:更多入口
Row() {
Text('附近在线师傅').fontSize(15).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
Blank()
Text('收藏新师傅 +').fontSize(11).fontColor(COLORS.blue)
.onClick(() => { this.addModal = true; })
}
.width('100%')
// 在线师傅大卡(3 条精选)
ForEach(FIX_RECS, (rec: FixRec) => {
Row({ space: 10 }) {
Text(rec.icon).fontSize(26)
Column({ space: 4 }) {
Text(rec.name).fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
Text(`工种 ${rec.craft} · 接单 ${rec.orders} 单`).fontSize(11).fontColor(COLORS.sub)
Row({ space: 6 }) {
Text(rec.dist).fontSize(10).fontColor(COLORS.text3)
Text(`评分 ${rec.rating.toFixed(1)}`).fontSize(10).fontColor(ratingColor(rec.rating))
}
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Column({ space: 4 }) {
Text('预约').fontSize(11).fontColor(COLORS.btnText).fontWeight(FontWeight.Bold)
.padding({ left: 12, right: 12, top: 6, bottom: 6 })
.borderRadius(12).backgroundColor(COLORS.blue)
.onClick(() => { this.currentTab = 1; })
}
}
.padding(12)
.borderRadius(12)
.backgroundColor(COLORS.card)
.width('100%')
}, (rec: FixRec) => rec.name)
"附近在线师傅"标题行用 Row + Blank() 实现"左标题右操作"的两端对齐布局——Blank() 是一个弹性占位元素,自动撑开剩余空间,把"收藏新师傅 +"推到最右。点击右侧文字打开收藏弹窗 this.addModal = true,让用户能添加新师傅。
在线师傅大卡遍历 FIX_RECS(三条精选师傅)。每张卡片左侧是 26px 的师傅 emoji 图标(🔌水电/🧊家电/🔧管道),中间是师傅信息(名字、工种接单数、距离评分),右侧是"预约"按钮。卡片用 backgroundColor(COLORS.card) 白底 + borderRadius(12) 圆角,整体形成独立的卡片视觉单元。
评分用 ratingColor(rec.rating) 动态着色——4.9 分的师傅评分会显示为绿色,4.8 分也是绿色,4.7 分是蓝色。这种"评分越高越绿"的颜色映射,让用户扫一眼就能识别口碑好坏,比纯文字数字更直观。“预约"按钮点击切到地图 Tab(this.currentTab = 1),引导用户从"看到师傅"到"在地图上确认师傅位置”,形成业务闭环。
ForEach 的 key 用 rec.name,每个师傅名唯一,保证列表刷新时的节点稳定性。如果以后两条数据重名,需要换成 rec.name + rec.dist 这类组合 key 避免冲突。
// 全部师傅列表(长按 Marker 的数据同源)
Row() {
Text('全部师傅(地图 Marker 同源)').fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
}
.width('100%')
ForEach(this.fixerList, (fixer: FixerItem, idx: number) => {
Column({ space: 8 }) {
Row({ space: 10 }) {
Column({ space: 3 }) {
Text(fixer.name).fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
Text(`工种 ${fixer.craft} · 接单 ${fixer.orders} 单`).fontSize(11).fontColor(COLORS.sub)
Text(`备注:${fixer.note}`).fontSize(10).fontColor(COLORS.text3)
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Column({ space: 4 }) {
Text(`${fixer.rating.toFixed(1)}`).fontSize(16).fontWeight(FontWeight.Bold)
.fontColor(ratingColor(fixer.rating))
Text('综合分').fontSize(9).fontColor(COLORS.text3)
}
}
.width('100%')
Row({ space: 8 }) {
Text(`${fixer.orders} 单`).fontSize(10)
.fontColor(orderColor(fixer.orders))
.padding({ left: 10, right: 10, top: 4, bottom: 4 })
.borderRadius(10).backgroundColor(COLORS.chip)
Text('编辑').fontSize(10).fontColor(COLORS.blue)
.padding({ left: 10, right: 10, top: 4, bottom: 4 })
.borderRadius(10).backgroundColor(COLORS.chip)
.onClick(() => { this.openEditFixer(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%')
}, (fixer: FixerItem) => fixer.name)
}
.width('100%')
}
全部师傅列表遍历 this.fixerList(七条收藏师傅数据,来自 @State,可增删改)。每张卡片比"在线师傅大卡"信息更全:上行是师傅名、工种接单数、备注(用户自定义文字);右上角是大号综合评分(16px,用 ratingColor 着色);下行是三个胶囊按钮——接单数标签(用 orderColor 着色)、编辑、删除。
这里第一次出现了"备注"字段,这是用户给师傅加的个性化标签,比如"工具箱比我家还全"“半夜也接急单”。备注用最浅的 COLORS.text3 显示,是三级弱信息。点击"编辑"调用 openEditFixer(idx),点击"删除"先记录 this.delIdx = idx 再打开删除确认弹窗——这种"先存索引再开弹窗"的两步操作是为了让弹窗知道要删的是哪条数据。
接单数标签 ${fixer.orders} 单 用 orderColor 着色,1500 单显示绿色(经验丰富),720 单显示黄色(熟练),540 单显示红色(新手)。这种"接单数也分色"的设计让用户能在列表里快速识别哪些是老师傅,配合评分的颜色映射,形成"双信号"的口碑判断——光看颜色就能大致判断一个师傅的水平和经验。
ForEach 的 key 用 fixer.name,前提是师傅名不重复。Mock 数据里七个师傅名都不同,所以这个 key 是稳定的。真实业务里应该用师傅 ID 作为 key,避免重名师傅导致 ForEach 节点复用错误。
6.3 地图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%')
地图 Tab 顶部是一个"特性说明卡",用浅底色(COLORS.chip)+ 圆角的卡片样式,明确告诉用户这一页展示的是 Map Kit 6.1.1 的长按事件监听能力。这种"先说明后展示"的设计降低了用户理解新特性的门槛——用户进地图 Tab 第一眼就知道"这页是干嘛的、该怎么操作"。
说明卡的文字精炼到两行:第一行是特性标题"Map Kit 6.1.1 · 长按事件监听"(13px 加粗),第二行是操作指引"长按地图上的服务站 Marker 或 POI 地点,事件将记录到下方日志流"(10px 灰字)。把"长按"这个手势动作明确写出来,是因为长按不是移动端的常见操作,用户可能不知道地图支持长按,必须引导。
// 监听开关行:Marker 长按 / POI 长按
Row({ space: 12 }) {
Row({ space: 6 }) {
Toggle({ type: ToggleType.Switch, isOn: this.markerListenOn })
.selectedColor(COLORS.blue)
.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.blue)
.width(36)
.height(20)
.onChange(() => { this.togglePoiListen(); })
Text('POI长按').fontSize(11).fontColor(COLORS.sub)
}
}
.width('100%')
监听开关行用两个 Toggle(Switch 类型)分别控制 Marker 长按和 POI 长按的开关。isOn 绑定到 @State markerListenOn 和 poiListenOn,onChange 里调用 toggleMarkerListen() / togglePoiListen()。selectedColor(COLORS.blue) 让开关打开时是工具蓝色,和主题色一致。
开关的存在让长按事件能力"可按需启停"。默认两个都开(markerListenOn: true、poiListenOn: true),用户可以独立关闭某一种——比如只想监听 Marker 不想监听 POI,就关掉 POI 开关。这种"双独立开关"比"一个总开关"更细粒度,体现了 6.1.1 两对 API(on/offMarkerLongClick + on/offPoiLongClick)的独立性。
Toggle 的 width(36).height(20) 是缩小后的尺寸,标准 Toggle 较大,缩小后能和文字标签紧凑排在一行。两个开关组用 Row({ space: 12 }) 隔开,每组内部 Toggle 和文字用 Row({ space: 6 }) 紧凑排列,形成清晰的"开关—标签"对。
// ★ MapComponent 本体(layoutWeight(1) 占满剩余高度)
MapComponent({ mapOptions: this.mapOptions, mapCallback: this.mapCallback })
.layoutWeight(1)
.width('100%')
.borderRadius(12)
// 长按事件日志流(固定高度可滚动,新事件置顶)
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.blue : COLORS.green)
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%')
}
MapComponent 是鸿蒙 Map Kit 的核心组件,接收两个参数:mapOptions(地图初始化参数,含中心点和缩放)和 mapCallback(初始化完成回调)。layoutWeight(1) 让它占满 Column 中除了说明卡、开关行、日志流之外的剩余高度——这是让地图在 Tab 内"撑满主体区域"的关键。borderRadius(12) 给地图加圆角,让它和周围卡片视觉风格一致,而不是一个突兀的方角矩形。
日志流区块用 Column 包裹一个标题行和一个固定高度 120 的 Scroll。标题行显示"长按事件日志流"和右侧的条数统计 ${this.eventLogs.length} 条,条数会随长按事件增加而实时更新——这是 @State eventLogs 响应式驱动的,数组长度变化即触发 Text 重新渲染。
每条日志用 Row 横向排列:左侧是图标(Marker 用 📍、POI 用 🏷),右侧是一个 Column 含两行——上行是"类型标签 + 名称 + 时间",下行是"经纬度坐标"。类型标签的颜色也按类型区分(Marker 蓝色、POI 绿色),和图标颜色呼应,让用户能扫一眼分辨事件来源。坐标用 fontFamily('monospace') 等宽字体,保证数字位数对齐,看起来更"技术感"。
ForEach 的 key 用 ${log.type}-${log.name}-${log.time} 三段组合,这是因为多条日志可能类型和名称都相同(比如多次长按同一个 Marker),加上时间字段才能保证 key 唯一。实际业务里时间戳可能重复(同一秒多次长按),更稳妥的做法是加一个自增 ID 字段。
整个日志流区块用 backgroundColor(COLORS.chip) 浅底色,和白色卡片形成层次——日志条目是白底(COLORS.card),整体容器是浅灰底,再外面是页面底色 COLORS.bg,三层灰度递进,视觉层次清晰。
6.4 搜索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%')
搜索 Tab 顶部同样是特性说明卡,标题"searchByText · reliability 相关性评分"点明了这一页展示的是 6.1.1 新增的 reliability 字段。说明文字明确给出了取值范围 [0,1] 和含义"1 为完全相关",让用户理解分数条的含义——这是把技术字段翻译成用户可感知的"相关度"的关键。
把特性说明放在每个特性 Tab 的最顶部,是一种"教育用户"的设计。鸿蒙 6.1.1 的新特性对大部分用户来说是陌生的,如果不解释清楚,用户看到分数条和等级标签可能不理解含义。说明卡用浅底色卡片样式,视觉上"温和提示"而非"硬性弹窗",不打扰用户的操作流。
// 搜索框 + 触发按钮
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.blue)
.onClick(() => { this.runSearch(); })
}
.width('100%')
// 搜索状态文案
Text(this.searchState).fontSize(10).fontColor(COLORS.text3).width('100%')
搜索框用 TextInput + Button 的经典组合。TextInput 的 text 绑定到 @State queryInput,onChange 里 this.queryInput = v 实现双向绑定。placeholder 给出了示例关键字"家电维修",引导用户输入合理的搜索词。Button 的 onClick 调用 this.runSearch() 触发异步搜索。
搜索状态文案 this.searchState 显示在搜索框下方,会随搜索过程动态变化:初始"待搜索·演示数据",搜索中"搜索中…“,成功"返回 N 条结果”,无结果"无结果·保留演示数据",失败"搜索失败(code)·保留演示数据"。这种状态文案让用户始终知道搜索在什么阶段,是异步操作 UX 的标准实践——比一个 loading 转圈更信息丰富。
// 搜索结果列表(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%')
搜索结果列表用 List + ListItem + ForEach 渲染。注意 List 的直接子元素必须是 ListItem,这是 List 组件的硬约束(和 Scroll+Column 不同),否则会编译报错。每条结果是一个 Column 容器,包含四行内容:上行是"地点名 + 等级标签"、第二行是地址、第三行是 reliability 分数条、第四行是距离和时间。
地点名用 maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis }) 限制单行+省略号,避免长名字撑破布局。等级标签 reliabilityScore(rec.reliability).label 显示"高相关/中相关/低相关",颜色用 .color 动态着色,背景是白色 COLORS.card——这种"彩色文字+白底胶囊"的标签样式在浅灰底色上很显眼,让用户一眼就能定位每条结果的相关度等级。
reliability 分数条是这一Tab的视觉核心:Progress({ value: rec.reliability * 100, total: 100, type: ProgressType.Linear }) 把 [0,1] 的小数映射到 0-100 的进度区间,用线性进度条展示。进度条颜色用 reliabilityScore(rec.reliability).color 动态着色——0.95 的高相关显示满格绿色长条,0.28 的低相关显示一小段红色短条,视觉冲击力强。进度条右侧是数值文本 reliability 0.95,用等宽字体 monospace 保证数字对齐。
距离文案 ${(rec.distance / 1000).toFixed(2)}km 把米换算成公里保留两位小数,比"950 米"更简洁。时间用"刚刚"这种相对文案。整条结果卡通过"名称+标签+地址+分数条+距离时间"五个信息层,把 Site 的关键字段(name/formatAddress/distance/reliability)完整展示,让用户能基于相关性分数做"是否预约这个师傅"的决策。
ForEach 的 key 用 ${rec.name}-${rec.reliability},组合了名称和分数。这里有个细节——如果两条结果同名同分(理论上可能),key 会冲突。更稳妥的做法是加索引或唯一 ID,但本页 Mock 数据里每条 name 和 reliability 都不同,所以这个 key 是稳定的。
// 双特性代码预览卡(体现技术点)
this.codePreviewCard()
}
.width('100%')
.height('100%')
}
搜索 Tab 最底部调用了 this.codePreviewCard(),这是一个"代码预览卡"Builder,用深色底+等宽字体展示 6.1.1 两大新特性的核心调用代码。把它放在搜索 Tab 而非地图 Tab,是因为搜索 Tab 已经展示了 reliability 特性,再附一段代码预览能强化"这是 6.1.1 新能力"的认知,形成"特性说明→搜索结果→代码预览"的完整教育链路。
6.5 我的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('绿标级 · 上门免跑腿费 · 修后质保 90 天').fontSize(11).fontColor(COLORS.sub)
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
}
.width('100%')
Divider().strokeWidth(1).color(COLORS.line)
Row() {
Text('本月报修 3 单').fontSize(11).fontColor(COLORS.sub)
Blank()
Text('已省跑腿费 60 元').fontSize(11).fontColor(COLORS.green)
}
.width('100%')
}
.padding(14)
.borderRadius(14)
.linearGradient({
angle: 135,
colors: [[COLORS.blueD, 0.0], [COLORS.card, 0.75]]
})
.width('100%')
我的 Tab 顶部是一个会员渐变大卡,和头部 Banner 用了相同的 linearGradient 配方(深工具蓝→白色,135 度对角),形成视觉呼应——告诉用户"这是一个有品牌一致性的产品"。卡片左侧是一个 34px 的大号工具 emoji 🛠,右侧是会员标题和权益说明"绿标级·上门免跑腿费·修后质保 90 天"。
卡片下半部分用 Divider 分割后展示两个运营数据:“本月报修 3 单"和"已省跑腿费 60 元”。后者用绿色(COLORS.green),强调"省钱"这个正向信号。把会员权益和省钱数据放在一起,强化了"开会员划算"的认知,是典型的会员转化设计。
// 报修统计三宫格
Row({ space: 8 }) {
Column({ space: 2 }) {
Text('8').fontSize(18).fontWeight(FontWeight.Bold).fontColor(COLORS.blue)
Text('收藏师傅').fontSize(9).fontColor(COLORS.sub)
}
.layoutWeight(1)
.padding({ top: 10, bottom: 10 })
.borderRadius(10)
.backgroundColor(COLORS.card)
Column({ space: 2 }) {
Text('26').fontSize(18).fontWeight(FontWeight.Bold).fontColor(COLORS.green)
Text('累计报修').fontSize(9).fontColor(COLORS.sub)
}
.layoutWeight(1)
.padding({ top: 10, bottom: 10 })
.borderRadius(10)
.backgroundColor(COLORS.card)
Column({ space: 2 }) {
Text('2').fontSize(18).fontWeight(FontWeight.Bold).fontColor(COLORS.orange)
Text('质保订单').fontSize(9).fontColor(COLORS.sub)
}
.layoutWeight(1)
.padding({ top: 10, bottom: 10 })
.borderRadius(10)
.backgroundColor(COLORS.card)
}
.width('100%')
我的 Tab 也有一个报修统计三宫格,和师傅 Tab 的三宫格结构相同,但数据维度不同——这里是"收藏师傅/累计报修/质保订单",对应"用户资产"维度;师傅 Tab 是"收藏师傅/本月报修/质保期内",对应"近期活跃"维度。两处三宫格的视觉风格完全一致(蓝/绿/橙三色),但数据语义不同,体现了"同一组件不同数据"的复用思想。
数字字号 18px(比师傅 Tab 的 20px 略小),是因为我的 Tab 的三宫格是次级信息(主信息是会员卡),字号小一些让视觉层次更分明——会员卡是焦点,统计宫格是补充。
// 功能清单
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%')
}
功能清单遍历 this.funcList(八条功能项),每条是一个 Row 行:左侧 emoji 图标(18px)、中间功能名(13px 主色,layoutWeight(1) 占满中间)、右侧状态/数值(11px 灰字)、最右一个 › 箭头(14px 弱灰)。这是"设置页/我的页"最常见的列表行样式,箭头 › 暗示"可点击进入详情"。
八条功能覆盖了家政维修用户的完整生命周期:累计报修、我的房屋、下次保养、收藏师傅、维修账单、质保订单、上门到达提醒、偏好设置。其中"下次保养·空调清洗·3 天后"这种带时间预警的项很有业务感——它不是静态展示,而是主动提醒用户"该保养了",是提升复购的关键设计。
ForEach 的 key 用 item.label,前提是功能名不重复。八条功能名都不同,key 稳定。这种"左图标+中标题+右状态+箭头"的标准列表行,通过 ForEach 遍历常量数组生成,是 ArkUI 列表渲染的最常见模式。
6.6 代码预览卡
/** 双特性代码预览卡(深色底 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) // 服务站标记长按')
.fontSize(9).fontColor(COLORS.blue).fontFamily('monospace')
Text('eventManager.onPoiLongClick(cb) // 服务站POI长按')
.fontSize(9).fontColor(COLORS.blue).fontFamily('monospace')
}
.padding(10)
.borderRadius(8)
.backgroundColor(COLORS.codeBg)
.width('100%')
}
.padding(10)
.borderRadius(10)
.backgroundColor(COLORS.chip)
.width('100%')
}
codePreviewCard 是一个"代码预览"Builder,用深底色(COLORS.codeBg,即 #22282F)+ 等宽字体(monospace)模拟代码编辑器的视觉。四行代码分别展示 6.1.1 两大新特性的核心调用:前两行绿色字是搜索特性(searchByText + reliability 读取),后两行蓝色字是长按事件特性(onMarkerLongClick + onPoiLongClick)。
颜色分配有讲究:搜索相关用绿色(COLORS.green),呼应搜索结果里"高相关"的绿色;事件相关用蓝色(COLORS.blue),呼应 Marker 长按日志里"Marker"类型的蓝色。这种"代码预览的颜色和实际特性展示的颜色同源"的设计,让用户在视觉上能把代码和效果关联起来,理解"这行代码对应那个效果"。
字号 9px 非常小,是因为这是辅助信息——用户主要看的是搜索结果列表,代码预览是给"想了解底层实现"的进阶用户看的。把它放在搜索 Tab 最底部,不抢主内容的视觉焦点,但需要时可以下滑看到。这种"主次分明的信息层级"是移动端页面设计的关键原则。
6.7 底部TabBar
/** 底部导航 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 + ForEach 渲染四个 Tab,每个 Tab 是一个 Column(图标+标签纵向排列),layoutWeight(1) 等分宽度。选中态通过 this.currentTab === idx 判断,标签颜色在 COLORS.tabOn(选中,工具蓝)和 COLORS.text3(未选中,浅灰)之间切换。
onClick 里 this.currentTab = idx 修改选中索引,驱动整个页面的 Tab 切换——build 里的 if (this.currentTab === 0) 分支会重新求值,渲染对应的 tabFixer/tabMap/tabSearch/tabMine。这种"一个状态变量驱动整页切换"的轻量级 Tab 实现比 Tabs 容器更灵活,可以自由控制切换动画、状态保留等行为,代价是需要手动管理每个 Tab 的滚动位置等状态。
ForEach 的 key 用 tab.label,四个标签"师傅/地图/搜索/我的"都不同,key 稳定。padding({ top: 8, bottom: 8 }) 给 TabBar 上下留白,backgroundColor(COLORS.card) 白底让 TabBar 和上方可滚动内容区分开,形成"内容区+底部导航"的两段式布局。
6.8 弹窗系统
/** 弹窗遮罩层(点击空白处关闭) */
@Builder
modalOverlay(onClose: () => void) {
Column()
.width('100%')
.height('100%')
.backgroundColor(COLORS.mask)
.onClick(() => { onClose(); })
}
modalOverlay 是弹窗遮罩层 Builder,接收一个 onClose 回调。它本身是一个铺满全屏的透明 Column,背景色是 COLORS.mask(rgba(34,40,47,0.42) 半透明蓝灰),onClick 调用 onClose()——点击遮罩任意位置都能关闭弹窗,这是弹窗的标准交互(点空白处关闭)。
把遮罩抽成独立 Builder 是为了复用——三个弹窗(panelAdd/panelEdit/panelDel)都用同一个 modalOverlay,保证遮罩视觉一致。onClose 作为参数传入,让每个弹窗能用自己的关闭逻辑(虽然本页都是 this.xxxModal = false,但留了这个参数灵活性更高)。
/** 收藏师傅弹窗:师傅名 + 擅长工种 + 常驻地址输入 */
@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.formTag })
.height(38)
.fontSize(12)
.fontColor(COLORS.title)
.placeholderColor(COLORS.text3)
.backgroundColor(COLORS.chip)
.onChange((v: string) => { this.formTag = 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.blue)
.onClick(() => { this.saveFixer(); })
}
.width('100%')
}
.padding(16)
.borderRadius(14)
.backgroundColor(COLORS.card)
.width('82%')
}
.width('100%')
.height('100%')
}
panelAdd 是收藏师傅弹窗,用 Stack 堆叠 modalOverlay(遮罩层)和内容 Column。内容区宽度 82%,居中显示在屏幕上(Stack 默认居中),borderRadius(14) 圆角 + 白底,形成"卡片浮在遮罩上"的视觉。这种"Stack+遮罩+内容卡"是 ArkUI 实现自定义弹窗的标准模式,比 CustomDialog 装饰器更灵活可控。
弹窗内三个 TextInput 分别绑定 formName/formTag/formAddr 三个 @State,onChange 实现双向绑定。placeholder 给出输入提示(“师傅称呼”“擅长工种(如:水电急修)”“常驻地址(可留空地图选点)”),引导用户填写。backgroundColor(COLORS.chip) 让输入框是浅灰底,和白底弹窗形成层次。
底部两个按钮"取消"和"收藏"用 Row({ space: 10 }) + 各自 layoutWeight(1) 实现等宽两按钮。取消按钮浅底灰字(COLORS.chip + COLORS.sub),收藏按钮蓝底白字(COLORS.blue,注意 Button 默认文字色是白色,无需显式设置)。点击取消调用 onClose() 关闭弹窗,点击收藏调用 this.saveFixer() 保存数据(方法内部会关闭弹窗)。
/** 编辑备注弹窗:回填当前师傅备注 */
@Builder
panelEdit(onClose: () => void) {
Stack() {
this.modalOverlay(onClose)
Column({ space: 12 }) {
Text('编辑师傅备注').fontSize(15).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
Text(this.editIdx < this.fixerList.length ? this.fixerList[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.blue)
.onClick(() => { this.updateFixer(); })
}
.width('100%')
}
.padding(16)
.borderRadius(14)
.backgroundColor(COLORS.card)
.width('82%')
}
.width('100%')
.height('100%')
}
panelEdit 是编辑备注弹窗,结构和 panelAdd 对称。区别在于:标题下多了一行师傅名展示(this.fixerList[this.editIdx].name),让用户知道在编辑谁的备注;只有一个 TextInput 绑定 editNote(备注字段);保存按钮调用 this.updateFixer()。
师傅名展示用了边界检查 this.editIdx < this.fixerList.length ? ... : '',避免 editIdx 越界时访问 fixerList[editIdx] 拿到 undefined 导致 .name 报错。这种防御式写法在异步弹窗场景下很重要——用户打开弹窗后如果列表被其他操作改动(比如同时打开了删除弹窗删了一条),editIdx 可能指向已不存在的元素,边界检查能优雅降级为空字符串。
TextInput 的 text: this.editNote 是回填——openEditFixer 方法在打开弹窗时已经把当前师傅的备注赋值给了 editNote,弹窗渲染时 TextInput 显示这个值,用户看到的是"现有备注",可以基于此修改。这是"编辑"和"新增"弹窗的关键区别——新增是空的,编辑是回填的。
/** 删除确认弹窗:师傅名 + 确认/取消 */
@Builder
panelDel(onClose: () => void) {
Stack() {
this.modalOverlay(onClose)
Column({ space: 12 }) {
Text('删除收藏师傅').fontSize(15).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
Text(this.delIdx < this.fixerList.length
? `确定删除「${this.fixerList[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.red)
.onClick(() => { this.delFixer(); })
}
.width('100%')
}
.padding(16)
.borderRadius(14)
.backgroundColor(COLORS.card)
.width('82%')
}
.width('100%')
.height('100%')
}
panelDel 是删除确认弹窗,没有 TextInput,只有标题、确认文案、两个按钮。确认文案动态拼接师傅名:确定删除「王师傅」吗?,让用户明确知道要删的是谁,避免误删。同样做了边界检查 this.delIdx < this.fixerList.length,越界时降级为通用的"确定删除吗?"。
删除按钮用红色 COLORS.red,这是危险操作的标准色——和收藏按钮的蓝色、保存按钮的蓝色形成对比,让用户在视觉上感受到"这个按钮不可轻易点"。红色按钮+确认文案的双重提醒,是"破坏性操作必须二次确认"这一 UX 原则的落地实现。
三个弹窗的视觉结构完全一致(Stack+遮罩+82%白底卡片+标题+内容+双按钮行),保证了弹窗系统的视觉一致性。差异只在内容区——panelAdd 三个输入框、panelEdit 一个输入框+师傅名展示、panelDel 只有确认文案。这种"统一外壳+差异内容"的弹窗设计模式,既保证了视觉规范,又兼顾了不同业务场景的表单需求。
七、对比表格
| 维度 | reliability 相关性分数 | 长按事件监听(Marker+POI) |
|---|---|---|
| 所属模块 | site 模块(搜索) | map.MapEventManager(事件) |
| HarmonyOS 版本 | 6.1.1 新增字段 | 6.1.1 新增监听对 |
| API 形态 | site.Site.reliability: number | onMarkerLongClick/offMarkerLongClick、onPoiLongClick/offPoiLongClick |
| 取值/参数 | [0,1] 小数,1 为完全相关 | 回调参数为 map.Marker 或 mapCommon.Poi |
| 触发时机 | searchByText 返回结果时携带 | 用户在地图上长按 Marker 或 POI 时触发 |
| 业务价值 | 搜索结果分级、过滤、排序,避免"关键字命中但语义不匹配" | 地图选点交互、收藏坐标、记录用户行为日志 |
| UI 呈现 | 分数条(Progress)+ 等级标签(高/中/低相关) | 事件日志流(unshift 置顶) |
| 颜色映射 | ≥0.8 绿/≥0.5 黄/其余红 | Marker 蓝/POI 绿 |
| 容错策略 | ?? 0 兜底,降级为低相关 | if (!this.mapEventManager) return 防空 |
| 关闭/重置 | 无(只读字段) | offXxxLongClick() 不传参清除全部订阅 |
| 数据载体 | SearchRecord 类 | EventLog 类 |
| 所在 Tab | 搜索 Tab | 地图 Tab |
八、总结
本篇围绕鸿蒙 HarmonyOS 6.1.1 Map Kit 的两大新特性——site.searchByText 返回的 Site.reliability 相关性分数字段、MapEventManager 新增的 onMarkerLongClick/offMarkerLongClick 与 onPoiLongClick/offPoiLongClick 长按事件监听对——在一个"修立得·家政维修上门"浅色主题页面里做了完整的工程落地。两大特性分别落在搜索 Tab 和地图 Tab,前者把抽象的"相关性"变成了用户可感知的分数条+三色等级标签,后者把地图上的长按手势变成了结构化的事件日志流,两者通过底部 Tab 切换统一在一个 Stack 容器内渲染,弹窗系统叠加在最上层。
从架构层面看,整个页面遵循"接口先行—常量铺底—Observed 模型—组件状态—Builder 渲染"的分层范式。颜色用 ColorPalette 接口集中声明、COLORS 常量实现,保证视觉一致性;Tab、筛选标签、服务站 Marker、推荐师傅、功能清单等数据都用 Mock 常量预置,让页面开箱即用;FixerItem/SearchRecord/EventLog 三个 @Observed 类承载可变数据,配合 @State 数组实现响应式刷新;setupMapCallback 负责地图初始化、Marker 批量下发、双长按监听注册,是两大特性的"装配车间";runSearch 负责调用 site.searchByText 并读取 reliability,是搜索特性的消费入口。
从落地经验看,6.1.1 的两大新特性都需要注意容错。reliability 作为新增字段,在老版本 SDK 或异常返回时可能是 undefined,必须用 ?? 0 兜底为低相关,避免 UI 展示 undefined 或计算异常。长按事件的 mapEventManager 是在 mapCallback 回调里获取的,回调外它是 undefined,所以 toggleMarkerListen/togglePoiListen 必须先 if (!this.mapEventManager) return 防御,避免用户在地图初始化完成前点开关导致崩溃。offMarkerLongClick/offPoiLongClick 不传参时清除该类型全部订阅,重新开启时必须重新挂载回调,这是 6.1.1 事件 API 的关键语义。
从业务价值看,家政维修上门是一个"低频刚需、强信任、强时效"的行业,用户最关心的是"附近有没有靠谱师傅、多久能到、修完有没有质保"。reliability 字段让搜索结果不再是"关键字命中的全部地点",而是"按相关度排序的优质维修点"——把"电器卖场"“家政保洁"这类近义但非上门维修的结果用低相关红色标出,避免用户误预约。长按事件让用户能在地图上对任意服务站 Marker 或 POI 地点执行"长按记录坐标"的操作,为后续的"地图选点报修”"收藏师傅常驻位置"等场景打下交互基础。两大特性合起来,恰好覆盖了"找到师傅"和"在地图上锁定师傅位置"两个核心环节,是鸿蒙 Map Kit 在 O2O 上门服务场景的典型应用。
从工程范式看,本页还体现了几条 ArkUI 的最佳实践:弹窗系统用 Stack+遮罩+82%白底卡片的统一外壳,差异只在内容区,保证视觉规范;@State 数组的刷新用 .slice() 整体替换引用(updateFixer 里),比依赖 @Observed 的属性感知更稳妥;ForEach 的 key 必须稳定且唯一,避免用 index 作为 key 导致节点复用错误;MapComponent 的 mapOptions 必须在 aboutToAppear 里就准备好,避免 build 里传 undefined;异步回调(mapCallback、runSearch)必须 err-first 判断,失败时保留现有状态(Mock 数据),保证弱网下 UI 不崩。这些细节合起来,构成了一个"既展示了 6.1.1 新特性、又符合生产级代码规范"的鸿蒙 ArkUI 页面范本。
鸿蒙生态在持续演进,Map Kit 的能力也在持续扩展。reliability 字段的出现,标志着鸿蒙地图搜索从"关键字命中"走向了"语义相关度量化";长按事件监听的出现,标志着鸿蒙地图交互从"点击"走向了"多手势、多实体"的丰富交互。对于 O2O 上门服务、本地生活、出行导航这类强地图依赖的应用,这两大特性都是值得优先接入的能力——它们不仅提升了用户体验,更让应用能基于"相关度"和"长按意图"做更智能的业务决策,是鸿蒙原生应用差异化竞争的关键点。
附录: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)