最近技术点 Map Kit 6.1.1 reliability 相关性评分与长按事件监听:城市文化旅游攻略场景下 HarmonyOS ArkUI 途悦景点应用如何解决搜索结果排序与地图交互痛点
一、技术前言:HarmonyOS ArkUI 与 Map Kit 在城市文化旅游场景中的演进

HarmonyOS ArkUI 是华为鸿蒙生态面向全场景应用开发的核心声明式 UI 框架,其采用 ArkTS 语言(TypeScript 的超集)作为开发载体,通过 @Entry、@Component、@Builder、@State、@Observed 等装饰器构建出一套从数据驱动到视图渲染的完整闭环。与传统的命令式 UI 不同,ArkUI 的声明式范式要求开发者只需声明界面在不同状态下的最终形态,框架自身的差分算法会自动计算出从旧状态到新状态的最小更新路径,从而实现高效的局部刷新。在城市文化旅游攻略这类信息密集、交互频繁的应用场景中,这种数据驱动的渲染模型能够显著降低状态同步的心智负担——开发者无需手动调用 setText、setImage 等命令式接口,只需修改 @State 修饰的状态变量,界面就会自动响应。本篇所分析的"途悦·城市景点攻略"应用正是基于这一框架能力,构建了景点浏览、地图交互、关键字搜索、个人中心四个完全异构的 Tab 页面,每个页面拥有独立的布局结构与数据源,却共享同一套主题色板与组件状态。

Map Kit 是 HarmonyOS 提供的地图能力套件,它以 @kit.MapKit 为统一入口,向外暴露 MapComponent、mapCommon、map、site 四个核心模块。MapComponent 是可直接放入 ArkUI 组件树的地图视图组件,它接收 mapOptions(地图初始化参数,包含中心点 position.target 和缩放级别 zoom)与 mapCallback(初始化回调)两个参数,在组件挂载后异步完成地图引擎的加载,并通过回调将 map.MapComponentController 控制器实例回传给业务层。这个控制器是整个地图交互的中枢——添加标注点(addMarker)、移动视角、获取事件管理器(getEventManager)都依赖它。而 map.MapEventManager 则是事件订阅的统一出口,开发者通过它注册点击、长按等监听。site 模块则专注于地理编码与关键字搜索能力,其中 searchByText 接口是景点类应用最常调用的搜索入口,它根据用户输入的 query 关键字、location 中心点、radius 搜索半径与 language 语言偏好,返回一组匹配的 Site 结果。

HarmonyOS 6.1.1 版本对 site 模块的 searchByText 返回结果 Site 类型进行了重要增强——新增了 reliability 字段。这是一个取值范围为 [0,1] 的浮点数相关性分数,1 表示搜索结果与用户输入的关键字完全相关,0 则表示几乎无关。在传统的地图搜索场景中,开发者只能拿到地点名称、地址、距离三个维度的信息,却无法判断这个结果到底与用户意图匹配到什么程度。例如用户搜索"博物馆"时,返回结果里可能既有真正的博物馆,也有名字带"博"字的文创店、甚至住宅小区,仅凭名称和距离难以区分。reliability 字段的引入让应用层能够据此做结果过滤、排序、分级展示——高相关的优先展示并配高亮标签,低相关的可以折叠或剔除,从而显著提升搜索结果的有效性与用户决策效率。本应用在搜索 Tab 中将 reliability 映射为高/中/低三档等级标签,并配以线性进度条直观呈现分数高低,是这一新字段在文旅场景的典型落地。

同一版本中 MapEventManager 也新增了两组长按监听能力:onMarkerLongClick 与 offMarkerLongClick 用于订阅与取消订阅地图标注点(Marker)的长按事件,onPoiLongClick 与 offPoiLongClick 则用于地图 POI(Point of Interest,兴趣点)的长按事件。Marker 是开发者通过 addMarker 主动添加到地图上的标注点,携带业务自定义的位置、图标、锚点等属性;POI 则是地图引擎内置的公开兴趣点(如商场、学校、地标),用户长按地图上任意 POI 即可触发回调。这两组 API 的参数签名各异——Marker 长按回调接收 map.Marker 对象(可进一步调用 getPosition、getId 等方法),POI 长按回调接收 mapCommon.Poi 对象(包含 name 与 position 字段)。off 系列方法不传参时表示清除该类型的全部订阅,这种设计简化了批量解绑的操作。在城市景点场景中,长按是"查详情、加收藏、规划行程"等深度操作的天然入口,本应用将长按事件记录到日志流并置顶展示,清晰呈现了事件触发的时序。

城市文化旅游攻略是一个高度依赖地图能力的场景。游客在一个陌生城市需要快速了解周边有哪些景点、各景点的门票与开放时长、距离当前位置多远、评分如何,并据此规划当日行程。这些需求落在应用层就转化为:地图组件承载景点标注点、搜索能力支撑关键字检索、列表组件展示结构化景点信息、弹窗系统处理收藏与备注编辑。途悦应用将这四类能力分别映射到景点、地图、搜索、我的四个 Tab,每个 Tab 聚焦单一职责但彼此数据互通——景点 Tab 的收藏列表与地图 Tab 的 Marker 共享同一份景点数据源,搜索 Tab 的结果可触发地图跳转,我的 Tab 的统计来自全应用的行为沉淀。这种"多 Tab 异构布局 + 共享状态"的架构是中型文旅应用的常见范式。

在视觉层面,本应用采用浅色主题——以米白 #FAF6EF 为页面底色,朱砂红 #C8402E 为强调主色,琉璃金 #C99A3C 为次强调色,搭配白色卡片与浅米色胶囊底,营造出与"古都西安"历史文化气质相符的暖色温润感。颜色系统通过 ColorPalette 接口集中声明并封装为 COLORS 常量对象,所有 Builder 函数统一引用,保证全应用视觉一致性。渐变 Banner(linearGradient 从深朱砂红渐变到白色卡片底)用于头部与会员卡,强化视觉层次。这种"接口约束 + 常量集中 + Builder 引用"的颜色管理方式,让后续主题切换或暗色模式扩展变得可控。下面将逐段剖析整个实现,从颜色系统到数据模型,再到 Map Kit 双新特性的落地细节。

二、整体架构流程图
三、颜色系统与常量定义
3.1 主题色板接口与浅色配色常量
/** 主题色板接口:集中声明页面所有颜色字段(米白底+朱砂红+琉璃金浅色系) */
interface ColorPalette {
bg: string; // 页面底色(米白)
card: string; // 卡片底色
chip: string; // 胶囊/浅层底色
title: string; // 主标题色
sub: string; // 次级文本色
text3: string; // 三级弱文本色
red: string; // 朱砂红(主强调色)
redD: string; // 深朱砂红(渐变起点)
redL: string; // 浅朱砂红(高亮文本)
gold: string; // 琉璃金(次强调色)
green: string; // 免费开放绿
blue: string; // 编辑蓝(编辑入口)
line: string; // 分割线色
tabOn: string; // 底部 Tab 选中色
btnText: string; // 强调色按钮上的文字色
mask: string; // 弹窗遮罩色
codeBg: string; // 代码预览卡深底色
}
/** 浅色主题色板常量(途悦 · 米白 + 朱砂红 + 琉璃金) */
const COLORS: ColorPalette = {
bg: '#FAF6EF',
card: '#FFFFFF',
chip: '#F1E8D8',
title: '#2B2118',
sub: '#7A6A56',
text3: '#AA9984',
red: '#C8402E',
redD: '#A02A1A',
redL: '#F6D9D2',
gold: '#C99A3C',
green: '#4E8D6E',
blue: '#3E6FA8',
line: '#E8DFCE',
tabOn: '#C8402E',
btnText: '#FFFFFF',
mask: 'rgba(43,33,24,0.42)',
codeBg: '#2B2118'
};
ColorPalette 接口是整个应用颜色管理的契约层。它将页面可能用到的所有颜色角色——底色、卡片底、胶囊底、主标题、次级文本、三级弱文本、主强调色、深浅强调色变体、次强调色、功能色(绿/蓝)、分割线、Tab 选中色、按钮文字色、遮罩色、代码预览深底色——全部以字段形式集中声明。这种"接口先行"的设计意图在于让颜色的语义与值解耦:业务代码引用 COLORS.red 而非直接写 ‘#C8402E’,当后续需要切换主题或做暗色模式时,只需替换 COLORS 常量的赋值,所有引用点自动跟随变化。
COLORS 常量是浅色主题的具体落地。米白 #FAF6EF 作为页面底色,营造温暖柔和的古都气质;白色 #FFFFFF 作为卡片底,与米白底形成微弱但清晰的层次差;浅米色 #F1E8D8 作为胶囊/筛选 chips 底色,是介于底色与卡片色之间的中间层。主标题用深棕 #2B2118 而非纯黑,避免对比过强显得冰冷;次级文本 #7A6A56 与三级弱文本 #AA9984 形成三级灰阶,用于景点备注、距离、时间等辅助信息的层级区分。
朱砂红 #C8402E 是全应用的主强调色,用于 Tab 选中、主按钮、关键数值、收藏入口等需要视觉锚定的位置;深朱砂红 #A02A1A 作为渐变起点用于 Banner 与会员卡,浅朱砂红 #F6D9D2 用于高亮文本底。琉璃金 #C99A3C 作为次强调色,用于城市印章、评分、收费景点等"价值类"信息。绿色 #4E8D6E 与蓝色 #3E6FA8 分别承担"免费开放"与"编辑入口"的语义色,形成"红金绿蓝"四色功能体系。遮罩色用 rgba(43,33,24,0.42) 而非纯黑半透明,保证遮罩与暖色主题的协调。
3.2 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[] = ['全部', '历史古迹', '博物馆', '皇家园林', '夜游专场', '亲子研学', '免费开放', '需预约'];
TabMeta 接口定义了底部导航每个 Tab 的数据结构——icon 与 label 两个字段,分别承载 emoji 图标与中文标签。将 Tab 元数据抽象为接口而非直接硬编码在 Builder 中,使得导航结构可配置、可扩展:未来若要新增"行程"Tab 或调整图标,只需修改 TAB_LIST 常量数组,无需改动 tabBar Builder 的渲染逻辑。这种"数据与视图分离"的设计在多 Tab 应用中能显著降低维护成本。
TAB_LIST 常量声明了四个 Tab:景点(🏛)、地图(🗺)、搜索(🔍)、我的(👤),单排排列在底部。这四个 Tab 覆盖了文旅应用的核心用户旅程——浏览景点、地图定位、搜索发现、个人中心,是行业应用的通用骨架。使用 emoji 作为图标而非图片资源,既减小了包体积,又保证了跨设备一致性,符合 HarmonyOS 的轻量化设计理念。
CATE_TAGS 是头部横滑筛选 chips 的文案列表,包含"全部、历史古迹、博物馆、皇家园林、夜游专场、亲子研学、免费开放、需预约"八个标签。这些标签覆盖了西安文旅的典型分类维度——类型(古迹/博物馆/园林)、场景(夜游/亲子)、属性(免费/预约),用户点击后 cateIdx 状态更新,chips 的选中态视觉随之变化。当前实现中 chips 主要用于视觉筛选演示,未来可对接景点数据的实际过滤逻辑。
3.3 城市中心点与地图标注数据
/** 城市中心点(西安,地图初始化中心 + Map Kit 搜索 location 参数) */
const CITY_CENTER: mapCommon.LatLng = { latitude: 34.3416, longitude: 108.9398 };
/** 地图标注点接口(景点 Marker 群,长按事件的数据来源) */
interface SpotItem {
name: string; // 景点名称
lat: number; // 纬度
lng: number; // 经度
tag: string; // 景点类型标签
}
/** 景点标注点 Mock 数据(6 个,围绕西安中心点 ±0.02 度散布) */
const MARKER_SPOTS: SpotItem[] = [
{ name: '途悦·钟鼓楼地标点', lat: 34.3405, lng: 108.9381, tag: '古迹' },
{ name: '途悦·回民街巷口点', lat: 34.3389, lng: 108.9357, tag: '美食' },
{ name: '途悦·永宁门城墙点', lat: 34.3372, lng: 108.9430, tag: '城墙' },
{ name: '途悦·碑林书院门点', lat: 34.3345, lng: 108.9455, tag: '碑林' },
{ name: '途悦·革命公园点', lat: 34.3520, lng: 108.9472, tag: '公园' },
{ name: '途悦·莲湖公园点', lat: 34.3505, lng: 108.9318, tag: '园林' }
];
CITY_CENTER 是 mapCommon.LatLng 类型的城市中心点常量,经纬度为西安市中心(34.3416, 108.9398)。这个常量在应用中承担双重职责:一是作为 MapComponent 初始化时 mapOptions.position.target 的中心点,地图加载后默认聚焦于此;二是作为 site.searchByText 搜索参数 SearchByTextParams.location 的中心点,限定搜索范围围绕该坐标展开。将中心点提取为常量而非硬编码在多处,保证了地图视图与搜索范围的坐标一致性,避免因分散硬编码导致的坐标偏移。
SpotItem 接口定义了景点标注点的数据结构——名称、纬度、经度、类型标签四字段。这个接口是地图 Marker 群的数据来源:setupMapCallback 中遍历 MARKER_SPOTS,将每个 SpotItem 转换为 mapCommon.MarkerOptions 添加到地图。name 字段在当前实现中主要用于业务层标识,Marker 添加后通过 getId 获取的是引擎分配的 ID 而非 name,但保留 name 为未来"点击 Marker 显示景点名气泡"等扩展预留了数据基础。
MARKER_SPOTS 提供了 6 个 Mock 标注点,围绕西安中心点 ±0.02 度散布,覆盖钟鼓楼、回民街、永宁门城墙、碑林书院门、革命公园、莲湖公园等真实景点。Mock 数据的设计意图是让应用在无后端依赖时也能完整演示地图交互——6 个点在 zoom=13 的地图视野内可见,用户可立即长按任一 Marker 触发事件日志。tag 字段(古迹/美食/城墙/碑林/公园/园林)为后续按类型筛选 Marker 颜色或图标预留了扩展空间。
3.4 精选景点与功能清单数据
/** 精选景点接口(景点 Tab 顶部推荐大卡) */
interface SightRec {
icon: string; // 景点 emoji 图标
name: string; // 景点名
dist: string; // 距离文本
ticket: string; // 门票文本
hours: string; // 建议时长文本
rating: number; // 评分(满分 5)
}
/** 精选景点 Mock 数据(3 条,渐变 Banner 下方推荐列表) */
const SIGHT_RECS: SightRec[] = [
{ icon: '🏛', name: '钟鼓楼联票点', dist: '900m', ticket: '联票 50元', hours: '2.5小时', rating: 4.7 },
{ icon: '🎫', name: '永宁门城墙点', dist: '1.6km', ticket: '门票 54元', hours: '1.5小时', rating: 4.6 },
{ icon: '🖌', name: '碑林书院门点', dist: '2.1km', ticket: '免费开放', hours: '1小时', rating: 4.4 }
];
/** 我的页功能清单条目接口 */
interface FuncItem {
icon: string; // 功能图标
label: string; // 功能名
value: string; // 状态/数值文本
}
/** 我的页功能清单 Mock 数据(8 条) */
const FUNC_LIST: FuncItem[] = [
{ icon: '🏛', label: '累计打卡', value: '47 处 · 12 城' },
{ icon: '🔖', label: '城市印章', value: '集满 12 枚' },
{ icon: '🎧', label: '讲解收藏', value: '26 段语音' },
{ icon: '⭐', label: '收藏景点', value: '21 处' },
{ icon: '🎫', label: '门票订单', value: '本月 3 笔' },
{ icon: '🗺', label: '足迹城市', value: '西安 / 洛阳 / 开封' },
{ icon: '🔔', label: '开票提醒', value: '已开启' },
{ icon: '⚙', label: '偏好设置', value: '优先免费景点' }
];
SightRec 接口是景点 Tab 顶部"今日精选景点"区块的数据结构,包含图标、名称、距离、门票、建议时长、评分六字段。这六字段是游客决策的核心信息维度——距离决定可达性、门票决定预算、时长决定行程编排、评分决定优先级。rating 字段为 number 类型而非 string,使得 ratingColor 函数可直接做数值比较(>=4.5 返回琉璃金),避免字符串解析的开销与类型风险。
SIGHT_RECS 提供 3 条精选数据,对应钟鼓楼联票、永宁门城墙、碑林书院门三个景点。这 3 条数据在视觉上以横向卡片形式排列在 Banner 下方,每张卡片右侧带"导航"按钮,点击后跳转地图 Tab(currentTab=1)。Mock 数据覆盖了"收费联票/收费单票/免费开放"三种门票类型与 4.7/4.6/4.4 三档评分,能完整演示 ratingColor 与 ticketColor 两个颜色映射函数的分支逻辑。
FuncItem 接口定义了"我的"页功能清单的条目结构——图标、功能名、状态值三字段。FUNC_LIST 提供 8 条数据,涵盖累计打卡、城市印章、讲解收藏、收藏景点、门票订单、足迹城市、开票提醒、偏好设置八项功能。这些条目以列表行形式排列,每行右侧带">"箭头表示可点击进入详情,是个人中心页的经典布局模式。8 条数据覆盖了"统计类(打卡/印章/收藏)、订单类(门票)、设置类(提醒/偏好)"三类功能,呈现了文旅应用个人中心的完整功能图谱。
四、辅助函数与数据模型
4.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.green };
}
if (score >= 0.5) {
return { label: '中相关', color: COLORS.gold };
}
return { label: '低相关', color: COLORS.red };
}
reliabilityScore 是本应用对 Map Kit 6.1.1 新字段 reliability 的核心可视化映射函数。它接收一个 [0,1] 的浮点数,返回 ScoreLevel 对象(包含 label 与 color 两字段)。函数内部采用两段式阈值判断:≥0.8 判定为"高相关"配绿色、≥0.5 判定为"中相关"配琉璃金、其余为"低相关"配朱砂红。这种三级分档的设计将连续的数值分数离散化为可读的语义标签,降低了用户理解分数的认知成本——用户无需比较 0.91 与 0.78 谁高谁低,直接看"高相关"与"中相关"标签即可决策。
阈值 0.8 与 0.5 的选取有明确的业务含义。0.8 以上意味着搜索结果与关键字高度匹配(如搜"博物馆"返回"碑林博物馆"),应当优先展示并配绿色"推荐"信号;0.5~0.8 意味着部分匹配(如搜"景点"返回名字带"点"的文创店),可展示但需提示"中相关";0.5 以下意味着弱匹配或误匹配(如搜"景点"返回住宅小区),应当视觉弱化甚至过滤。开发者可根据实际业务调整阈值——例如对 precision 要求高的场景可提高至 0.85/0.6,对 recall 要求高的场景可降低至 0.7/0.4。
ScoreLevel 接口将"等级标签文案"与"等级颜色"绑定为一个对象返回,调用方在搜索结果列表中同时使用 .label(渲染文本标签)与 .color(渲染标签文字色与分数条进度色),保证两者视觉一致。这种"语义+视觉"的打包返回避免了调用方分别取标签与颜色可能产生的不一致,是函数式映射的优雅实践。返回值被 ForEach 内的 Text 与 Progress 共同消费,实现了"分数→标签→颜色"的端到端映射。
4.2 评分与门票颜色映射
/** 景点评分颜色映射:≥4.5 必去琉璃金 / ≥4.0 值得绿 / 其余一般红 */
function ratingColor(rating: number): string {
if (rating >= 4.5) { return COLORS.gold; }
if (rating >= 4.0) { return COLORS.green; }
return COLORS.red;
}
/** 门票颜色映射:免费开放绿 / 收费景点琉璃金 */
function ticketColor(ticket: string): string {
if (ticket === '免费开放' || ticket === '免费') { return COLORS.green; }
return COLORS.gold;
}
ratingColor 函数将景点评分(满分 5)映射为颜色:≥4.5 返回琉璃金("必去"语义)、≥4.0 返回绿色("值得"语义)、其余返回朱砂红("一般"语义)。这个映射在精选景点卡与全部景点列表两处复用,保证评分视觉一致性。将评分颜色逻辑封装为函数而非内联在每个 Text 的 fontColor 中,既避免了重复代码,又让"评分阈值与颜色的对应关系"这一业务规则有了单一数据源——调整阈值只需改一处。
ticketColor 函数将门票文本映射为颜色:包含"免费开放"或"免费"返回绿色、其余返回琉璃金。这里用字符串精确匹配而非 includes 模糊匹配,因为门票文本是受控的 Mock 数据(“联票 50元”“门票 54元”“免费开放”),精确匹配避免误判。在景点列表中,ticketColor 的返回值既用于"免费/收费"标签的文字色,也用于判断标签文案(ticketColor(ticket) === COLORS.green ? ‘免费’ : ‘收费’),实现了颜色与文案的联动。
这两个函数共同体现了本应用"语义化颜色"的设计哲学——颜色不随意选取,而是承载明确的业务语义:金=价值/必去、绿=推荐/免费、红=强调/一般。这种语义化的颜色映射让用户在不同页面看到相同语义的信息时,颜色保持一致,形成跨页面的视觉记忆,提升信息获取效率。
4.3 景点收藏数据模型 SightItem
/** 景点条目(景点 Tab 收藏列表) */
@Observed export class SightItem {
name: string; // 景点名
ticket: string; // 门票文本
hours: string; // 建议游览时长
rating: number; // 评分(满分 5)
note: string; // 用户备注(可编辑)
constructor(name: string, ticket: string, hours: string,
rating: number, note: string) {
this.name = name;
this.ticket = ticket;
this.hours = hours;
this.rating = rating;
this.note = note;
}
}
/** 景点收藏列表 Mock 数据(7 条) */
const SIGHT_LIST: Array<SightItem> = [
new SightItem('钟鼓楼联票点', '联票 50元', '2.5小时', 4.7, '晨钟暮鼓全记录'),
new SightItem('永宁门城墙点', '门票 54元', '1.5小时', 4.6, '傍晚登墙看落日'),
new SightItem('碑林书院门点', '免费开放', '1小时', 4.4, '顺路买张拓片'),
new SightItem('回民街巷口点', '免费开放', '2小时', 4.3, '小吃一路吃到饱'),
new SightItem('革命公园点', '免费开放', '1小时', 4.0, '本地人晨练场'),
new SightItem('莲湖公园点', '免费开放', '40分钟', 4.1, '夏天看满池荷花'),
new SightItem('大明宫微缩馆', '门票 30元', '1小时', 3.7, '雨天备选行程')
];
SightItem 类使用 @Observed 装饰器修饰,这是 ArkUI 响应式数据模型的关键装饰器。@Observed 使得类的实例属性在被修改时能够被框架追踪,配合 @State 修饰的数组引用,实现"修改对象属性 → 列表对应项刷新"的细粒度更新。在 updateSight 方法中,直接修改 this.sightList[this.editIdx].note 后还需 this.sightList = this.sightList.slice() 整体刷新数组引用,正是因为 @State 数组本身需要引用变化才触发 ForEach 重渲染,而 @Observed 保证对象内部属性变化能被捕获。
SightItem 包含 name、ticket、hours、rating、note 五字段,其中 note 是用户可编辑的备注字段。constructor 显式声明所有属性的初始化,保证实例创建时属性都有值,避免 undefined 导致的渲染异常。Mock 数据 SIGHT_LIST 提供 7 条景点,覆盖收费与免费、高评分与低评分(4.7~3.7)、不同游览时长,能完整演示 ratingColor、ticketColor 的所有分支与列表项的编辑/删除交互。
7 条 Mock 数据的备注字段(“晨钟暮鼓全记录”“傍晚登墙看落日"等)模拟了真实用户的个性化记录,这些备注在景点列表中以"备注:xxx"形式展示,并在编辑弹窗中回填到 TextInput。备注的可编辑性是收藏列表的核心交互——用户收藏景点后通常会补充"为什么想去”"什么时候去"等个人化信息,note 字段承载了这一需求。
4.4 搜索结果数据模型 SearchRecord
/** 搜索结果条目(★ 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('西安钟楼', '陕西省西安市碑林区南大街 30 号', 860, 0.96, '刚刚'),
new SearchRecord('永宁门(南门)城楼', '陕西省西安市碑林区南大街 2 号', 1420, 0.91, '刚刚'),
new SearchRecord('碑林博物馆侧门', '陕西省西安市碑林区三学街 15 号', 1880, 0.78, '刚刚'),
new SearchRecord('文创纪念品集合店', '陕西省西安市莲湖区北院门 144 号', 2450, 0.52, '刚刚'),
new SearchRecord('城市观光车售票咨询处', '陕西省西安市新城区东新街 258 号', 3260, 0.36, '刚刚'),
new SearchRecord('钟楼小区(住宅非景点)', '陕西省西安市雁塔区小寨西路 12 号', 4650, 0.11, '刚刚')
];
SearchRecord 是 searchByText 返回结果的数据载体,字段与 site.Site 类型一一对应:name 对应 site.name、address 对应 site.formatAddress、distance 对应 site.distance、reliability 对应 site.reliability(6.1.1 新增)。这个类的核心价值在于将 Map Kit 的原始 Site 对象转换为应用层可管理的响应式数据模型——@Observed 装饰使得搜索结果列表在数据替换时能正确触发 ForEach 重渲染,time 字段则补充了 Site 原生不具备的"记录时间"信息。
reliability 字段是 SearchRecord 区别于传统搜索结果模型的关键。在 6.1.1 之前,搜索结果模型通常只有 name、address、distance 三字段,开发者无法判断结果与关键字的匹配程度。reliability 字段的加入让应用层能够做结果分级——高相关的"西安钟楼"(0.96)配绿色"高相关"标签优先展示,低相关的"钟楼小区(住宅非景点)"(0.11)配红色"低相关"标签视觉弱化。这种分级在搜索结果列表中通过 reliabilityScore 函数 + Progress 分数条直观呈现。
SEARCH_RECORDS 的 6 条 Mock 数据精心覆盖了 reliability 的三个档次:前两条(0.96、0.91)为高相关,是真正的景点;中间两条(0.78、0.52)为中相关,是博物馆侧门与文创店;最后两条(0.36、0.11)为低相关,是售票处与住宅小区。这种覆盖三档的数据设计让搜索 Tab 在无网络环境下也能完整演示 reliability 分数条与等级标签的视觉差异,是 Mock 数据"覆盖全分支"原则的典型实践。距离字段从 860m 到 4650m 的递增也模拟了真实搜索结果"按相关性排序但距离递增"的特征。
4.5 长按事件日志数据模型 EventLog
/** 长按事件日志条目(★ MapEventManager 长按监听数据载体) */
@Observed export class EventLog {
type: string; // 事件类型:'Marker' / 'POI'
name: string; // Marker ID 或 POI 名称
lat: number; // 纬度
lng: number; // 经度
time: string; // 事件时间文案
constructor(type: string, name: string, lat: number, lng: number, time: string) {
this.type = type;
this.name = name;
this.lat = lat;
this.lng = lng;
this.time = time;
}
}
/** 长按事件日志 Mock 数据(2 条,演示日志流形态) */
const EVENT_LOGS: Array<EventLog> = [
new EventLog('POI', '西安钟楼', 34.3405, 108.9381, '演示事件'),
new EventLog('Marker', '#0', 34.3520, 108.9472, '演示事件')
];
EventLog 是 MapEventManager 长按监听事件的数据载体,type 字段区分事件来源——‘Marker’ 表示用户长按了开发者添加的标注点,‘POI’ 表示长按了地图引擎内置的兴趣点。name 字段在 Marker 事件中存储 marker.getId() 返回的 ID(如 “#0”),在 POI 事件中存储 poi.name(如"西安钟楼")。lat/lng 双字段记录事件触发的地理坐标,time 字段存储事件时间文案。这五字段完整描述了一次长按事件的"谁、在哪、何时"三要素。
@Observed 装饰使得 EventLog 数组在 unshift 新日志时能被 ForEach 捕获并渲染新条目。在地图 Tab 的日志流中,新事件通过 this.eventLogs.unshift(…) 置顶插入,ForEach 自动在列表顶部渲染新条目,实现了"事件触发→日志置顶→视图刷新"的响应式链路。Mock 数据 EVENT_LOGS 提供 2 条演示数据,分别模拟 POI 与 Marker 两种事件类型,让日志流在应用启动时就有内容展示,避免空状态。
两条 Mock 数据的 type 与 name 字段值得注意——POI 事件的 name 是可读的景点名"西安钟楼",Marker 事件的 name 是引擎 ID"#0"。这种差异反映了两种事件源的本质区别:POI 是地图引擎内置的公开兴趣点,天然携带名称;Marker 是开发者添加的标注点,引擎只分配 ID,业务需自行维护 ID 与名称的映射。在本应用中,Marker 的 name 暂时用 ID 展示,未来可通过维护 markerId→spotName 的 Map 来展示可读名称。
五、组件主体与状态管理
5.1 状态变量声明
@Entry
@Component
struct Page1139 {
/** 当前选中 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 sightList: Array<SightItem> = SIGHT_LIST;
/** 我的页功能清单数据 */
@State funcList: FuncItem[] = 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 = '';
@Entry 与 @Component 装饰器将 struct Page1139 标记为应用的入口组件与可复用组件。@Entry 表示这是页面级组件,框架会将其作为渲染根节点;@Component 表示这是一个 ArkUI 组件,struct 内部可声明 @State、@Builder、@Prop 等装饰器修饰的成员。struct 是 ArkTS 的组件语法,类似 class 但专门用于 UI 组件,不支持继承但支持 Builder 方法与生命周期回调。
状态变量分为三层。第一层是 UI 交互状态:currentTab(当前 Tab)、cateIdx(筛选选中)、addModal/editModal/delModal(三个弹窗开关)、editIdx/delIdx(当前操作的景点索引),这些状态直接驱动视图渲染。第二层是业务数据状态:sightList(收藏列表)、funcList(功能清单)、eventLogs(事件日志)、searchRecords(搜索结果),这些状态是应用的数据源,通过 @Observed 类的实例数组承载。第三层是 Map Kit 状态:mapOptions(地图初始化参数)、mapCallback(初始化回调)、mapController(控制器)、mapEventManager(事件管理器),这四个 private 成员不参与响应式渲染但承载地图交互的核心能力。
Map Kit 的四个 private 成员值得特别注意。mapOptions 在声明时直接赋默认值(position.target 为 CITY_CENTER、zoom 为 13),避免 MapComponent 组件参数传 undefined 导致初始化异常。mapCallback、mapController、mapEventManager 声明为可选(?),因为它们在 aboutToAppear 之后才陆续就绪——mapCallback 在 setupMapCallback 中赋值,mapController 与 mapEventManager 在 mapCallback 回调触发后才获取。这种"声明可选 + 回调中赋值"的模式是异步初始化组件的常见做法。
markerListenOn 与 poiListenOn 两个 @State 布尔值是长按监听开关的视觉状态,它们驱动 Toggle 开关的 isOn 属性与 Toggle 文案,同时 toggleMarkerListen/togglePoiListen 方法根据当前状态决定是 off(关闭监听)还是 on(重新注册监听)。searchState 文案状态在 runSearch 方法的不同阶段被更新(“搜索中…”“返回 N 条结果”“搜索失败(code)”),驱动搜索框下方的状态提示文本,让用户实时感知搜索进度。
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 方法的核心职责是为 this.mapCallback 赋值一个异步回调函数。这个回调函数是 MapComponent 初始化完成后的入口——MapComponent 组件挂载后,地图引擎异步加载,加载完成后调用 mapCallback,传入 err 与 mapController 两个参数。err 为空表示初始化成功,mapController 可用;err 不为空表示初始化失败,需记录错误并 return 终止后续逻辑。这种"err-first 回调"是 HarmonyOS 异步 API 的通用模式,与 Node.js 的 error-first callback 一脉相承。
回调内部的第一步是获取 mapController 与 mapEventManager。mapController 是地图操作的中枢——addMarker、移动视角、获取事件管理器都依赖它。mapEventManager 通过 mapController.getEventManager() 获取,它是事件订阅的统一出口,后续的 onMarkerLongClick 与 onPoiLongClick 都注册在它上面。这种"控制器→管理器"的层级关系决定了监听注册必须在 controller 就绪之后,不能在 aboutToAppear 中直接调用 getEventManager(此时 controller 尚未就绪)。
Marker 群添加逻辑遍历 MARKER_SPOTS 数组,为每个景点构造 mapCommon.MarkerOptions 并调用 addMarker。MarkerOptions 包含 position(经纬度)、clickable(可点击)、visible(可见)、rotation(旋转角)、zIndex(层级)、alpha(透明度)、anchorU/anchorV(锚点比例,0.5/1 表示底部中心)、draggable(可拖拽)、flat(平贴地图)等字段。anchorU=0.5、anchorV=1 是标注点图标的经典锚点设置——让图标的底部中心对齐经纬度坐标,避免图标偏移。每个 addMarker 返回 Promise,用 await 等待 + try-catch 容错,保证单个 Marker 添加失败不影响后续 Marker 的添加。
两组长按监听是 HarmonyOS 6.1.1 的新特性落地。onMarkerLongClick 注册 Marker 长按监听,回调接收 map.Marker 对象,通过 marker.getPosition() 获取经纬度、marker.getId() 获取引擎分配的 ID,构造 EventLog 并 unshift 到 eventLogs 日志流置顶。onPoiLongClick 注册 POI 长按监听,回调接收 mapCommon.Poi 对象,直接读取 poi.name 与 poi.position 构造 EventLog。两个回调的差异体现了 Marker 与 POI 的本质区别——Marker 是开发者添加的标注点,需通过 getId 标识;POI 是引擎内置兴趣点,天然携带 name。
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;
}
/** 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 长按监听的开/关切换。方法首先检查 mapEventManager 是否存在,不存在则直接 return(防御性编程,避免在地图未初始化时调用导致空指针)。然后根据 markerListenOn 当前状态决定操作——若为 true(当前监听已开启),调用 offMarkerLongClick() 关闭监听;若为 false(当前监听已关闭),调用 onMarkerLongClick 重新注册监听。最后反转 markerListenOn 状态,驱动 Toggle 开关的视觉切换。
offMarkerLongClick 不传参的设计是 HarmonyOS 6.1.1 事件 API 的一个便利特性——不传参表示清除该类型的全部订阅。这种设计简化了批量解绑的操作,无需维护回调函数引用逐一解绑。在实际应用中,一个页面可能注册多个 onMarkerLongClick 回调,off 不传参一次性清除全部,避免了回调引用管理与内存泄漏的风险。
重新注册 onMarkerLongClick 时,回调函数与 setupMapCallback 中的完全一致——获取 Marker 位置与 ID,构造 EventLog 并 unshift。这里存在一个设计权衡:回调逻辑重复但保证了开关切换后的行为一致性;若要消除重复,可提取回调为私有方法 toggleMarkerListen 与 setupMapCallback 共同引用。当前实现的意图是让每个注册点自包含,便于理解事件注册的完整上下文。
togglePoiListen 方法的结构与 toggleMarkerListen 完全对称,区别仅在调用的是 offPoiLongClick/onPoiLongClick,回调接收的是 mapCommon.Poi 而非 map.Marker。这种对称结构体现了 Map Kit 事件 API 的设计一致性——Marker 与 POI 两组长按 API 在方法签名、开关逻辑、回调模式上高度对称,降低了开发者的学习成本。
5.4 searchByText 搜索实现
/**
* ★ 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 是本应用对 site.searchByText 的核心调用方法,也是 reliability 新字段落地的关键入口。方法首先将 searchState 置为"搜索中…“,让用户立即感知搜索已触发。然后构造 site.SearchByTextParams 参数对象——query 为用户输入的关键字(默认"景点”),location 为 CITY_CENTER 城市中心点,radius 为 5000 米搜索半径,language 为"zh"中文。这四个参数限定了搜索的空间范围与语言偏好,是 searchByText 的标准用法。
try-catch 结构是搜索方法的容错骨架。try 块内 await site.searchByText(params) 返回 site.SearchByTextResult,其 sites 字段是 site.Site 数组(?? 兜底空数组防止 undefined)。sites 为空时更新状态为"无结果 · 保留演示数据"并 return,保留 Mock 数据避免空列表。sites 非空时遍历构造 SearchRecord 数组,每个 Site 的字段都用 ?? 兜底——name 兜底"未命名地点"、formatAddress 兜底"暂无地址"、distance 兜底 0、reliability 兜底 0。这种"每个字段都兜底"的防御式编程保证了即使 Site 对象某些字段为 undefined,SearchRecord 也能正常构造。
reliability 字段的 ?? 0 兜底尤其值得注意。作为 6.1.1 新增字段,旧版本的 Site 对象可能不包含此字段(API 升级后的兼容场景),?? 0 保证字段缺失时默认为 0(低相关),避免 undefined 传入 reliabilityScore 函数导致比较异常。这种对新字段做兜底处理的实践,是处理"版本新增可选字段"的标准模式,让应用在旧版本 Map Kit 上也能运行而不崩溃。
catch 块捕获 BusinessError,将错误码拼入状态文案"搜索失败(code) · 保留演示数据"。searchByText 在无 AGC(AppGallery Connect)配置或无网络时会抛 BusinessError,catch 块保留 Mock 数据保证演示链路不断裂。这种"失败保留 Mock"的策略让应用在各种异常环境下都能展示搜索结果的视觉形态,便于开发调试与演示验证,是 Demo 类应用的常见容错模式。
5.5 景点收藏增删改方法
/** 打开编辑备注弹窗(回填当前景点备注) */
openEditSight(idx: number) {
this.editIdx = idx;
this.editNote = this.sightList[idx].note;
this.editModal = true;
}
/** 保存收藏景点(空名兜底默认演示景点) */
saveSight() {
const name = this.formName === '' ? '途悦·新收藏景点' : this.formName;
const tag = this.formTag === '' ? '待定' : this.formTag;
const addr = this.formAddr === '' ? '陕西省西安市(地图选点)' : this.formAddr;
this.sightList.unshift(new SightItem(name, '免费开放', '1小时', 4.2, `${tag} · ${addr}`));
this.formName = '';
this.formTag = '';
this.formAddr = '';
this.addModal = false;
}
/** 保存编辑备注(整体刷新数组引用以刷新列表) */
updateSight() {
if (this.editIdx >= 0 && this.editIdx < this.sightList.length) {
if (this.editNote !== '') {
this.sightList[this.editIdx].note = this.editNote;
}
this.sightList = this.sightList.slice();
}
this.editModal = false;
}
/** 删除收藏景点(确认弹窗回调) */
delSight() {
if (this.delIdx >= 0 && this.delIdx < this.sightList.length) {
this.sightList.splice(this.delIdx, 1);
}
this.delModal = false;
}
openEditSight 方法在打开编辑弹窗前完成两件事——记录当前编辑的景点索引 editIdx,并将该景点的 note 回填到 editNote 状态。回填是编辑弹窗的关键交互——用户期望编辑框初始显示当前备注内容而非空白,回填保证编辑体验的连续性。最后将 editModal 置为 true 触发弹窗渲染。
saveSight 方法处理收藏新景点的表单提交。三个表单字段(formName、formTag、formAddr)都做了空值兜底——name 兜底"途悦·新收藏景点"、tag 兜底"待定"、addr 兜底"陕西省西安市(地图选点)“。兜底逻辑保证了用户不填任何字段也能创建一个有效景点,降低了表单填写的强制要求。新建的 SightItem 门票默认"免费开放”、时长默认"1小时"、评分默认 4.2,note 字段拼接 tag 与 addr 作为综合备注。新建后 unshift 到 sightList 头部置顶,并清空三个表单状态、关闭弹窗。
updateSight 方法的 this.sightList = this.sightList.slice() 是 ArkUI 响应式更新的关键技巧。直接修改 this.sightList[this.editIdx].note 后,虽然 @Observed SightItem 的属性变化能被捕获,但 @State 数组本身的引用未变,ForEach 不会重渲染整个列表。调用 slice() 创建数组副本并重新赋值,强制 @State 数组引用变化,触发 ForEach 重渲染。这是"修改对象属性后需整体刷新数组引用"模式的典型实现,保证了列表项的视觉更新。
delSight 方法用 splice 删除指定索引的景点。splice 会原地修改数组并改变其长度,这个操作本身会触发 @State 数组的响应式更新(与 slice 不同,splice 是变异方法,ArkUI 框架对其做了拦截)。删除后关闭确认弹窗。两个边界检查(delIdx >= 0 && delIdx < sightList.length)保证了索引越界时的安全,避免删除不存在的项。
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.tabSight()
} 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() 完成地图回调的初始化,保证 MapComponent 组件渲染时 mapCallback 已就绪。将监听注册放在 mapCallback 内部(而非 aboutToAppear)的原因是——mapController 与 mapEventManager 在回调触发后才可用,aboutToAppear 时尚未就绪。这种"生命周期初始化回调 + 回调内注册监听"的两阶段模式是异步组件初始化的标准实践。
build 方法是组件的渲染入口,采用 Stack 包裹主内容与三层弹窗。Stack 的层叠特性让弹窗能覆盖在主内容之上——最底层是 Column(headerMain + Divider + Scroll + tabBar),上层根据 addModal/editModal/delModal 的布尔值条件渲染三个弹窗。这种"Stack 层叠 + 条件弹窗"的模式是 ArkUI 实现全屏遮罩弹窗的典型布局。
主内容 Column 的结构是头部、分割线、可滚动内容区、底部 Tab 栏四段。Scroll 组件 layoutWeight(1) 占满中间剩余高度,scrollBar(BarState.Off) 隐藏滚动条保证视觉简洁。Scroll 内部的 Column 通过 if-else if-else 根据 currentTab 条件渲染四个 Tab 页面之一——currentTab===0 渲染 tabSight、===1 渲染 tabMap、===2 渲染 tabSearch、其余渲染 tabMine。这种条件渲染保证了同一时刻只有一个 Tab 的内容在组件树中,避免了不必要的组件创建与渲染开销。
三个弹窗的渲染采用"布尔状态条件渲染 + 闭包传关闭回调"的模式。每个弹窗 Builder(panelAdd/panelEdit/panelDel)接收一个 onClose 回调参数,弹窗内部的取消按钮与遮罩点击都调用 onClose()。在 build 中,onClose 闭包分别设置 addModal=false、editModal=false、delModal=false,实现点击关闭弹窗。这种"状态控制渲染 + 回调控制关闭"的模式让弹窗的显隐完全受 @State 布尔值驱动,响应式且可预测。
六、Builder 函数群:四大 Tab 页面
6.1 头部渐变 Banner 与筛选 chips
/** 头部:渐变 Banner(今日行程+收藏景点数)+ 筛选 chips 横滑 */
@Builder
headerMain() {
Column({ space: 12 }) {
// 顶部渐变 Banner:今日行程 + 收藏景点数 + 出行贴士
Column({ space: 10 }) {
Row({ space: 12 }) {
Column({ space: 2 }) {
Text('2 处').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('钟鼓楼 · 永宁门 · 碑林').fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
}
Row({ space: 6 }) {
Text('⭐').fontSize(12)
Text('收藏景点 21 处 · 3 处免预约').fontSize(11).fontColor(COLORS.sub)
}
Row({ space: 6 }) {
Text('🌤').fontSize(12)
Text('晴 18℃ · 建议傍晚登城墙看落日').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.red)
Text('🗺 地图找景点').fontSize(12).fontColor(COLORS.red)
.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.redD, 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.btnText : COLORS.sub)
.padding({ left: 12, right: 12, top: 6, bottom: 6 })
.borderRadius(14)
.backgroundColor(this.cateIdx === idx ? COLORS.red : 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 是视觉焦点,采用 linearGradient 从 135 度角的深朱砂红(COLORS.redD #A02A1A)渐变到白色卡片底(COLORS.card #FFFFFF),渐变进度在 0.7 处完成。135 度角让渐变方向从左上到右下,深色集中在左上角,营造出"晨光映红墙"的古都意境。Banner 内部分为上下两段——上段是今日行程概览,下段是两个快捷入口胶囊。
今日行程概览采用 Row 横向布局,左侧 Column 突出"2 处"大字号(fontSize 30、Bold)与"今日行程"小字号说明,右侧 Column 用 layoutWeight(1) 占满剩余宽度,内含三行信息——景点列表(🏛 钟鼓楼·永宁门·碑林)、收藏统计(⭐ 收藏景点 21 处)、出行贴士(🌤 晴 18℃·建议傍晚登城墙看落日)。emoji 图标 + 文本的 Row 组合让信息行既直观又紧凑,三行信息覆盖了"去哪、收藏多少、天气如何"三个决策维度。
两个快捷入口胶囊采用 SpaceBetween 两端对齐——"预约讲解"是主操作,用朱砂红底白字强调;"地图找景点"是次操作,用浅米底朱砂红字弱化,点击后 currentTab=1 跳转地图 Tab。这种"主次胶囊 + 跨 Tab 跳转"的设计让头部不仅是信息展示区,更是导航枢纽,用户可直接从头部进入地图场景。
横滑筛选 chips 用 Scroll 横向滚动 + Row 容纳 ForEach 渲染的 8 个标签。每个 chip 的选中态由 cateIdx === idx 三元表达式控制——选中时白字红底、未选中时灰字米底。点击 chip 更新 cateIdx 状态,chips 视觉立即响应。scrollable(ScrollDirection.Horizontal) 限定横向滚动,scrollBar(BarState.Off) 隐藏滚动条保持视觉简洁。这种横滑 chips 是内容筛选的常见交互模式,适合标签数量较多且可能动态增减的场景。
6.2 景点 Tab:打卡统计与精选列表
/** 景点 Tab:打卡统计三宫格 + 精选景点 + 收藏景点列表(业务主 Tab) */
@Builder
tabSight() {
Column({ space: 10 }) {
// 打卡统计三宫格(累计打卡/城市印章/收藏景点)
Row({ space: 8 }) {
Column({ space: 2 }) {
Text('47').fontSize(20).fontWeight(FontWeight.Bold).fontColor(COLORS.red)
Text('累计打卡(处)').fontSize(9).fontColor(COLORS.sub)
}
.layoutWeight(1)
.padding({ top: 10, bottom: 10 })
.borderRadius(10)
.backgroundColor(COLORS.card)
Column({ space: 2 }) {
Text('12').fontSize(20).fontWeight(FontWeight.Bold).fontColor(COLORS.gold)
Text('城市印章(枚)').fontSize(9).fontColor(COLORS.sub)
}
.layoutWeight(1)
.padding({ top: 10, bottom: 10 })
.borderRadius(10)
.backgroundColor(COLORS.card)
Column({ space: 2 }) {
Text('21').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)
}
.width('100%')
// 区块标题行:更多入口
Row() {
Text('今日精选景点').fontSize(15).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
Blank()
Text('收藏新景点 +').fontSize(11).fontColor(COLORS.red)
.onClick(() => { this.addModal = true; })
}
.width('100%')
// 精选景点大卡(3 条精选)
ForEach(SIGHT_RECS, (rec: SightRec) => {
Row({ space: 10 }) {
Text(rec.icon).fontSize(26)
Column({ space: 4 }) {
Text(rec.name).fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
Text(`${rec.ticket} · 建议 ${rec.hours}`).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.red)
.onClick(() => { this.currentTab = 1; })
}
}
.padding(12)
.borderRadius(12)
.backgroundColor(COLORS.card)
.width('100%')
}, (rec: SightRec) => rec.name)
// 全部景点列表(长按 Marker 的数据同源)
Row() {
Text('全部景点(地图 Marker 同源)').fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
}
.width('100%')
ForEach(this.sightList, (sight: SightItem, idx: number) => {
Column({ space: 8 }) {
Row({ space: 10 }) {
Column({ space: 3 }) {
Text(sight.name).fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
Text(`门票 ${sight.ticket} · 建议 ${sight.hours}`).fontSize(11).fontColor(COLORS.sub)
Text(`备注:${sight.note}`).fontSize(10).fontColor(COLORS.text3)
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Column({ space: 4 }) {
Text(`${sight.rating.toFixed(1)}`).fontSize(16).fontWeight(FontWeight.Bold)
.fontColor(ratingColor(sight.rating))
Text('综合评分').fontSize(9).fontColor(COLORS.text3)
}
}
.width('100%')
Row({ space: 8 }) {
Text(ticketColor(sight.ticket) === COLORS.green ? '免费' : '收费').fontSize(10)
.fontColor(ticketColor(sight.ticket))
.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.openEditSight(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%')
}, (sight: SightItem) => sight.name)
}
.width('100%')
}
tabSight 是业务主 Tab,自上而下分为三段——打卡统计三宫格、今日精选景点卡片、全部景点收藏列表。三宫格采用 Row 横向布局三个等宽 Column(layoutWeight(1)),分别展示"累计打卡 47 处"(朱砂红)、“城市印章 12 枚”(琉璃金)、“收藏景点 21 处”(绿色)。三色对应三种统计维度,数值用 fontSize 20 Bold 突出,说明文字用 fontSize 9 弱化,形成"大数字+小说明"的统计卡片视觉范式。白色卡片底 + borderRadius(10) 让三宫格在米白页面上浮起,层次清晰。
精选景点区块的标题行用 Blank() 将"今日精选景点"标题与"收藏新景点 +"入口两端对齐。点击"收藏新景点 +"触发 addModal=true 弹出收藏弹窗。精选卡片用 ForEach 渲染 SIGHT_RECS 三条数据,每张卡片左侧 emoji 大图标(fontSize 26)、中间景点信息(名称+门票时长+距离评分)、右侧"导航"朱砂红按钮。评分文本的 fontColor 调用 ratingColor(rec.rating) 函数,4.7 与 4.6 返回琉璃金、4.4 返回绿色,视觉区分必去与值得。点击"导航"跳转地图 Tab(currentTab=1),实现了景点列表到地图的跨 Tab 跳转。
全部景点列表的标题"全部景点(地图 Marker 同源)"点明了一个重要设计——sightList 收藏列表与地图 MARKER_SPOTS 标注点是同一份数据源的两个视图。虽然当前实现中两者是独立的 Mock 数据,但标题暗示了"列表与地图共享数据"的架构意图,未来可统一为单一数据源。每条景点卡片的左侧 Column 展示名称、门票时长、备注,右侧 Column 突出评分(fontSize 16 Bold + ratingColor 着色)。底部的三个操作标签(免费/收费、编辑、删除)用 chip 样式排列,End 对齐靠右。编辑调用 openEditSight(idx) 弹出编辑弹窗,删除设置 delIdx 并弹出删除确认弹窗。
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%')
// 监听开关行:Marker 长按 / POI 长按
Row({ space: 12 }) {
Row({ space: 6 }) {
Toggle({ type: ToggleType.Switch, isOn: this.markerListenOn })
.selectedColor(COLORS.red)
.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.red)
.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)
// 长按事件日志流(固定高度可滚动,新事件置顶)
Column({ space: 6 }) {
Row() {
Text('长按事件日志流').fontSize(12).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
Blank()
Text(`共 ${this.eventLogs.length} 条`).fontSize(10).fontColor(COLORS.text3)
}
.width('100%')
Scroll() {
Column({ space: 6 }) {
ForEach(this.eventLogs, (log: EventLog) => {
Row({ space: 8 }) {
Text(log.type === 'Marker' ? '📍' : '🏷')
.fontSize(12)
Column({ space: 2 }) {
Row({ space: 6 }) {
Text(log.type).fontSize(10).fontColor(log.type === 'Marker' ? COLORS.green : COLORS.gold)
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%')
}
tabMap 是 Map Kit 6.1.1 长按事件特性的展示页,自上而下分为特性说明卡、监听开关行、MapComponent 地图本体、长按事件日志流四段。特性说明卡用浅米底(COLORS.chip)突出,标题"🗺 Map Kit 6.1.1 · 长按事件监听"明确版本与特性,副文案"长按地图上的景点 Marker 或 POI 地点,事件将记录到下方日志流"指导用户操作。这种"特性说明卡"是技术 Demo 类页面的常见元素,让用户在交互前就理解页面能力。
监听开关行包含两个 Toggle 开关——Marker 长按与 POI 长按。Toggle 的 isOn 属性绑定 markerListenOn 与 poiListenOn 状态,selectedColor 设为朱砂红与主题统一。onChange 回调调用 toggleMarkerListen/togglePoiListen 方法,实现开关切换时的监听注册/注销。两个开关并排让用户可独立控制两种长按事件的订阅状态,便于分别验证 Marker 与 POI 事件的行为差异。
MapComponent 是地图本体的渲染组件,接收 mapOptions(地图初始化参数)与 mapCallback(初始化回调)两个参数。layoutWeight(1) 让地图占满除说明卡、开关行、日志流之外的剩余高度,borderRadius(12) 圆角与卡片风格统一。MapComponent 挂载后异步加载地图引擎,加载完成调用 mapCallback,在回调中完成控制器获取、Marker 添加、长按监听注册的完整初始化链路。这是整个地图 Tab 的核心——6.1.1 的两组长按 API(onMarkerLongClick/onPoiLongClick)就注册在此回调内。
长按事件日志流是事件触发的可视化反馈区。顶部标题行"长按事件日志流"与"共 N 条"计数两端对齐,下方 Scroll(height 120)容纳 ForEach 渲染的日志条目。每条日志用 Row 展示——左侧 emoji(📍 Marker / 🏷 POI)、右侧 Column 含类型标签(Marker 绿/POI 金)、名称、时间、经纬度。经纬度用 fontFamily(‘monospace’) 等宽字体展示,保证小数位对齐。新事件通过 unshift 置顶插入 eventLogs 数组,ForEach 自动在顶部渲染新条目,实现"事件触发→日志置顶"的实时反馈。
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%')
// 搜索框 + 触发按钮
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.red)
.onClick(() => { this.runSearch(); })
}
.width('100%')
// 搜索状态文案
Text(this.searchState).fontSize(10).fontColor(COLORS.text3).width('100%')
// 搜索结果列表(reliability 分数条 + 等级标签,List 子项必须用 ListItem 包裹)
List({ space: 8 }) {
ForEach(this.searchRecords, (rec: SearchRecord) => {
ListItem() {
Column({ space: 6 }) {
Row() {
Text(rec.name).fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
.layoutWeight(1)
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Text(reliabilityScore(rec.reliability).label)
.fontSize(10)
.fontColor(reliabilityScore(rec.reliability).color)
.padding({ left: 8, right: 8, top: 3, bottom: 3 })
.borderRadius(8)
.backgroundColor(COLORS.card)
}
.width('100%')
Text(rec.address).fontSize(11).fontColor(COLORS.sub).width('100%')
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
// ★ reliability 分数条:0~1 映射为线性进度 + 数值文本
Row({ space: 8 }) {
Progress({ value: rec.reliability * 100, total: 100, type: ProgressType.Linear })
.layoutWeight(1)
.height(6)
.color(reliabilityScore(rec.reliability).color)
Text(`reliability ${rec.reliability.toFixed(2)}`)
.fontSize(10)
.fontColor(COLORS.sub)
.fontFamily('monospace')
}
.width('100%')
Row({ space: 10 }) {
Text(`直线距离 ${(rec.distance / 1000).toFixed(2)}km`).fontSize(10).fontColor(COLORS.text3)
Text(rec.time).fontSize(10).fontColor(COLORS.text3)
}
.width('100%')
}
.padding(12)
.borderRadius(12)
.backgroundColor(COLORS.card)
.width('100%')
}
}, (rec: SearchRecord) => `${rec.name}-${rec.reliability}`)
}
.layoutWeight(1)
.scrollBar(BarState.Off)
.width('100%')
// 双特性代码预览卡(体现技术点)
this.codePreviewCard()
}
.width('100%')
.height('100%')
}
tabSearch 是 reliability 新字段的特性展示页。顶部特性说明卡明确"Site 新增 reliability 字段([0,1],1 为完全相关)“,让用户理解分数含义。搜索框区由 TextInput(layoutWeight(1) 占满宽度)与 Button(朱砂红"搜索"按钮)组成,TextInput 的 text 绑定 queryInput 状态,onChange 实时更新 queryInput,点击搜索按钮调用 runSearch 方法触发 site.searchByText。搜索状态下方的 Text 绑定 searchState,实时显示"搜索中…”“返回 N 条结果”"搜索失败(code)"等状态文案。
搜索结果列表用 List + ListItem 包裹 ForEach,这是 ArkUI List 组件的强制要求——List 的直接子节点必须是 ListItem,否则渲染异常。每条结果卡片包含四段信息——名称与等级标签、地址、reliability 分数条、距离与时间。名称用 layoutWeight(1) 占满左侧、maxLines(1) + textOverflow(Ellipsis) 保证单行省略;等级标签调用 reliabilityScore(rec.reliability) 获取 label 与 color,白底圆角小标签形式展示在右侧。
reliability 分数条是本 Tab 的视觉核心。Progress 组件 type=ProgressType.Linear 线性进度条,value=rec.reliability * 100 将 [0,1] 映射到 [0,100],total=100,height(6) 细条状。进度条颜色调用 reliabilityScore(rec.reliability).color,高相关绿、中相关金、低相关红,与等级标签颜色一致。右侧文本"reliability 0.96"用 monospace 等宽字体展示精确数值,方便技术 Demo 场景观察分数。这种"进度条+数值"的双重视觉呈现,既直观(进度条长度)又精确(数值文本),是数值型数据展示的推荐模式。
底部距离与时间行用 (rec.distance / 1000).toFixed(2)km 将米转换为千米保留两位小数,rec.time 展示"刚刚"。这两项辅助信息让用户了解结果的地理位置与时效。卡片底部还通过 codePreviewCard Builder 渲染了双特性代码预览卡,用深色底 monospace 字体展示四行关键调用,强化技术 Demo 属性。
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('金印级 · 讲解畅听 · 生日月免预约').fontSize(11).fontColor(COLORS.sub)
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
}
.width('100%')
Divider().strokeWidth(1).color(COLORS.line)
Row() {
Text('本季打卡 12 处').fontSize(11).fontColor(COLORS.sub)
Blank()
Text('城市印章 12 枚').fontSize(11).fontColor(COLORS.red)
}
.width('100%')
}
.padding(14)
.borderRadius(14)
.linearGradient({
angle: 135,
colors: [[COLORS.redD, 0.0], [COLORS.card, 0.75]]
})
.width('100%')
// 打卡统计三宫格
Row({ space: 8 }) {
Column({ space: 2 }) {
Text('47').fontSize(18).fontWeight(FontWeight.Bold).fontColor(COLORS.red)
Text('打卡景点').fontSize(9).fontColor(COLORS.sub)
}
.layoutWeight(1)
.padding({ top: 10, bottom: 10 })
.borderRadius(10)
.backgroundColor(COLORS.card)
Column({ space: 2 }) {
Text('12').fontSize(18).fontWeight(FontWeight.Bold).fontColor(COLORS.gold)
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)
}
.width('100%')
// 功能清单
ForEach(this.funcList, (item: FuncItem) => {
Row({ space: 10 }) {
Text(item.icon).fontSize(18)
Text(item.label).fontSize(13).fontColor(COLORS.title).layoutWeight(1)
Text(item.value).fontSize(11).fontColor(COLORS.sub)
Text('›').fontSize(14).fontColor(COLORS.text3)
}
.padding(12)
.borderRadius(12)
.backgroundColor(COLORS.card)
.width('100%')
}, (item: FuncItem) => item.label)
}
.width('100%')
}
tabMine 是个人中心页,自上而下分为会员渐变大卡、打卡统计三宫格、功能清单三段。会员卡采用与头部 Banner 相同的 linearGradient(135 度,深朱砂红到白色,渐变进度 0.75),视觉上形成"头部 Banner 与会员卡呼应"的统一渐变语言。卡内左侧大 emoji(🏛 fontSize 34),右侧"途悦·长安通票会员"标题与"金印级·讲解畅听·生日月免预约"权益说明。Divider 分割线下方是"本季打卡 12 处"与"城市印章 12 枚"两端对齐,强化会员成就感。
打卡统计三宫格与景点 Tab 的三宫格结构一致,但数据维度不同——“打卡景点 47”(红)、“城市印章 12”(金)、“讲解收藏 26”(绿)。这种跨 Tab 的三宫格复用保证了统计卡片的视觉一致性,用户在两个 Tab 看到相同形态的统计区,认知成本降低。fontSize 18 比景点 Tab 的 20 略小,因为"我的"页的三宫格是次要信息(会员卡才是主视觉),字号收敛避免抢焦。
功能清单用 ForEach 渲染 FUNC_LIST 八条数据,每行 Row 包含 emoji 图标(fontSize 18)、功能名(layoutWeight(1) 占满中间)、状态值(次级文本色)、">"箭头(三级弱文本色)。这种"图标+名称+值+箭头"的四段式列表行是个人中心功能入口的经典布局,用户一眼就能看到功能名与当前状态,箭头暗示可点击进入详情。八条功能覆盖打卡、印章、讲解、收藏、订单、足迹、提醒、设置,呈现了文旅应用个人中心的完整功能图谱。
七、弹窗系统与底部导航
7.1 收藏景点弹窗
/** 弹窗遮罩层(点击空白处关闭) */
@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.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.red)
.onClick(() => { this.saveSight(); })
}
.width('100%')
}
.padding(16)
.borderRadius(14)
.backgroundColor(COLORS.card)
.width('82%')
}
.width('100%')
.height('100%')
}
modalOverlay 是弹窗遮罩层的通用 Builder,接收 onClose 回调。它是一个铺满全屏的 Column,背景色为 COLORS.mask(rgba(43,33,24,0.42) 半透明深棕),onClick 调用 onClose 实现点击遮罩关闭弹窗。这个 Builder 被 panelAdd、panelEdit、panelDel 三个弹窗复用,是弹窗系统的通用遮罩层。将其提取为独立 Builder 避免了三个弹窗各自重复遮罩代码,体现了 Builder 的复用价值。
panelAdd 是收藏新景点弹窗,采用 Stack 层叠 modalOverlay 与表单 Column。表单包含标题"收藏新景点"与三个 TextInput——景点名称(formName)、游览时段(formTag)、地址(formAddr)。每个 TextInput 的 text 绑定对应状态,onChange 实时更新,保证表单输入与状态同步。placeholderColor 与 backgroundColor 用浅色系,与主题协调。三个输入框的 placeholder 分别提示"景点名称"“游览时段(如:夜游/晨游)”“地址(可留空地图选点)”,引导用户填写。
底部按钮行用 Row SpaceBetween 布局两个等宽按钮(layoutWeight(1))——"取消"用浅米底次级文本色,onClick 调用 onClose 关闭弹窗;"收藏"用朱砂红底,onClick 调用 saveSight 保存。弹窗 Column 宽度 82%,居中显示在 Stack 内(Stack 默认居中对齐),padding(16) 与 borderRadius(14) 保证内部留白与圆角。这种"Stack 层叠遮罩+表单+双按钮"的弹窗模式在本应用三个弹窗中保持一致,形成了统一的弹窗交互语言。
7.2 编辑备注弹窗
/** 编辑备注弹窗:回填当前景点备注 */
@Builder
panelEdit(onClose: () => void) {
Stack() {
this.modalOverlay(onClose)
Column({ space: 12 }) {
Text('编辑景点备注').fontSize(15).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
Text(this.editIdx < this.sightList.length ? this.sightList[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.red)
.onClick(() => { this.updateSight(); })
}
.width('100%')
}
.padding(16)
.borderRadius(14)
.backgroundColor(COLORS.card)
.width('82%')
}
.width('100%')
.height('100%')
}
panelEdit 是编辑备注弹窗,结构与 panelAdd 对称但表单内容不同。标题"编辑景点备注"下方多了一行景点名称展示——this.editIdx < this.sightList.length ? this.sightList[this.editIdx].name : '',这个三元表达式做了边界检查,避免 editIdx 越界时 sightList[this.editIdx] 报错。展示景点名让用户明确当前编辑的是哪个景点,避免误操作。
核心是单个 TextInput 绑定 editNote 状态,placeholder 提示"输入新备注"。editNote 在 openEditSight 方法中已被回填为当前景点的 note 值,所以编辑框初始显示当前备注内容,用户可在此基础上修改。这种"回填+修改+保存"的编辑模式比"清空+重新输入"更符合用户习惯,减少了重复输入的成本。
底部"取消"与"保存"按钮,保存调用 updateSight 方法。updateSight 内部会修改 sightList[editIdx].note 并 slice() 刷新数组引用,触发列表对应项的视觉更新。弹窗宽度 82% 与 panelAdd 一致,保证三个弹窗的视觉宽度统一。padding(16) + borderRadius(14) 的内边距与圆角也与其他弹窗保持一致,形成弹窗系统的视觉规范。
7.3 删除确认弹窗与底部 Tab 栏
/** 删除确认弹窗:景点名 + 确认/取消 */
@Builder
panelDel(onClose: () => void) {
Stack() {
this.modalOverlay(onClose)
Column({ space: 12 }) {
Text('删除收藏景点').fontSize(15).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
Text(this.delIdx < this.sightList.length
? `确定删除「${this.sightList[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.delSight(); })
}
.width('100%')
}
.padding(16)
.borderRadius(14)
.backgroundColor(COLORS.card)
.width('82%')
}
.width('100%')
.height('100%')
}
/** 底部导航 Tab 栏(4 Tab 单排) */
@Builder
tabBar() {
Row() {
ForEach(TAB_LIST, (tab: TabMeta, idx: number) => {
Column({ space: 3 }) {
Text(tab.icon).fontSize(18)
Text(tab.label).fontSize(10)
.fontColor(this.currentTab === idx ? COLORS.tabOn : COLORS.text3)
}
.layoutWeight(1)
.onClick(() => { this.currentTab = idx; })
}, (tab: TabMeta) => tab.label)
}
.padding({ top: 8, bottom: 8 })
.width('100%')
.backgroundColor(COLORS.card)
}
panelDel 是删除确认弹窗,标题"删除收藏景点"下方是确认文案——确定删除「${this.sightList[this.delIdx].name}」吗?,用模板字符串将景点名嵌入问句,让用户明确删除目标。边界检查 this.delIdx < this.sightList.length 保证越界时显示通用文案"确定删除吗?"。删除是不可逆操作,确认弹窗是防止误删的标准交互模式。
底部"取消"与"删除"按钮,删除调用 delSight 方法。delSight 内部用 splice 删除指定索引项并关闭弹窗。这种"确认弹窗+回调"的模式让删除操作有了二次确认的机会,避免了用户误点击删除按钮直接删除数据的风险。
tabBar 是底部导航栏,用 Row 容纳 ForEach 渲染的四个 Tab。每个 Tab 是一个 Column(图标 emoji fontSize 18 + 标签 fontSize 10),layoutWeight(1) 等宽分布。标签颜色用三元表达式 this.currentTab === idx ? COLORS.tabOn : COLORS.text3 控制——选中时朱砂红、未选中时三级弱文本色,选中态视觉明确。onClick 设置 currentTab=idx 触发 Tab 切换,build 中的 if-else 条件渲染会响应此状态变化。白色卡片底 + padding(8) 让 Tab 栏在页面底部浮起,与米白页面形成层次。
八、技术对比与总结
8.1 Map Kit 6.1.1 双新特性对比
| 对比维度 | searchByText reliability 字段 | MapEventManager 长按事件监听 |
|---|---|---|
| 所属模块 | site 模块(搜索能力) | map 模块(事件能力) |
| HarmonyOS 版本 | 6.1.1 新增 | 6.1.1 新增 |
| 数据类型 | number 浮点数 [0,1] | 回调函数(Marker/Poi 参数) |
| 解决的问题 | 搜索结果无法量化相关性,用户难判断匹配度 | 地图标注/POI 仅支持点击,缺少长按深度操作入口 |
| API 形式 | Site 对象新增只读字段 | on/off 成对方法(onMarkerLongClick/offMarkerLongClick、onPoiLongClick/offPoiLongClick) |
| 回调参数 | 无(字段直接读取) | map.Marker(标记长按)/ mapCommon.Poi(POI 长按) |
| 典型场景 | 搜索结果分级展示、排序、过滤 | 长按 Marker 查详情、长按 POI 加收藏、规划行程 |
| 本应用落地 | 搜索 Tab 分数条+等级标签(高/中/低三档) | 地图 Tab 日志流置顶记录事件 |
| 关闭/解绑方式 | 无需关闭(字段读取) | off 系列方法不传参清除全部订阅 |
| 容错处理 | ?? 0 兜底(旧版本字段缺失) | mapEventManager 存在性检查后注册 |
| 视觉反馈 | Progress 线性进度条 + 数值文本 | EventLog 日志条目 unshift 置顶 |
8.2 四大 Tab 页面布局对比
| 对比维度 | 景点 Tab | 地图 Tab | 搜索 Tab | 我的 Tab |
|---|---|---|---|---|
| 布局结构 | 三宫格+精选卡+列表 | 说明卡+开关+地图+日志流 | 说明卡+搜索框+列表+代码卡 | 会员卡+三宫格+功能清单 |
| 核心组件 | ForEach 卡片列表 | MapComponent + Toggle | TextInput + List + Progress | linearGradient 卡 + ForEach 行 |
| 数据源 | SIGHT_RECS + sightList | MARKER_SPOTS + eventLogs | searchRecords | FUNC_LIST |
| Map Kit 关联 | 数据与 Marker 同源(标注说明) | 长按事件监听主战场 | searchByText + reliability | 无直接关联(统计沉淀) |
| 交互复杂度 | 中(增删改弹窗) | 高(地图+事件+开关) | 中(搜索+分数展示) | 低(只读列表) |
| 视觉重点 | 卡片列表+评分颜色 | 地图本体+日志流 | 分数条+等级标签 | 会员渐变卡 |
8.3 弹窗系统对比
| 对比维度 | panelAdd 收藏弹窗 | panelEdit 编辑弹窗 | panelDel 删除弹窗 |
|---|---|---|---|
| 触发入口 | "收藏新景点 +"文本 | 列表项"编辑"标签 | 列表项"删除"标签 |
| 表单字段 | 景点名+时段+地址(3 输入框) | 备注单输入框 | 无输入框(确认文案) |
| 回填逻辑 | 无(新建空表单) | 回填当前 note | 显示当前景点名 |
| 主操作 | saveSight(unshift 新建) | updateSight(修改+slice 刷新) | delSight(splice 删除) |
| 边界检查 | 空值兜底默认值 | editIdx 越界检查 | delIdx 越界检查 |
8.4 总结
本文围绕"途悦·城市景点攻略"这一城市文化旅游攻略应用,系统剖析了基于 HarmonyOS ArkUI 框架与 Map Kit 6.1.1 双新特性构建的四 Tab 异构页面架构。从技术前言对 ArkUI 声明式渲染模型、Map Kit 四模块分工、site.searchByText 的 reliability 新字段、MapEventManager 长按监听 API 的逐一阐述,到整体架构流程图的 mermaid 可视化,再到颜色系统、常量定义、辅助函数、数据模型、组件主体、四大 Tab Builder、弹窗系统的逐段代码分析,完整呈现了一个中型文旅应用从数据建模到视图渲染、从 Map Kit 集成到事件响应的全链路实现。
Map Kit 6.1.1 的 reliability 字段为景点搜索场景带来了质变。在 6.1.1 之前,应用层只能基于名称、地址、距离三个维度展示搜索结果,用户面对"搜博物馆返回文创店""搜景点返回住宅小区"的噪声结果时缺乏判断依据。reliability 字段将搜索结果与关键字的匹配程度量化为 [0,1] 的浮点数,应用层据此可做分级展示(高/中/低三档标签)、结果排序(高相关优先)、噪声过滤(低相关折叠或剔除)。本应用通过 reliabilityScore 函数将分数映射为等级标签与颜色,配以 Progress 线性进度条与 monospace 数值文本,实现了"直观+精确"的双重视觉呈现,是这一新字段在文旅场景的典型落地。
MapEventManager 的 onMarkerLongClick/onPoiLongClick 两组长按 API 则补齐了地图交互的最后一块拼图。在 6.1.1 之前,地图仅支持点击(onClick)监听,长按这一"深度操作入口"的交互手势无法被捕获。6.1.1 新增的两组 API 让应用能够区分"点击=快速查看"与"长按=深度操作"两种交互意图——长按 Marker 可触发查详情、加收藏、规划行程等深度操作,长按 POI 可捕获引擎内置兴趣点并扩展业务数据。本应用通过 Toggle 开关独立控制两组监听的开/关,配以事件日志流 unshift 置顶的可视化反馈,清晰演示了长按事件的触发时序与数据流。
在工程实践层面,本应用展示了多个值得借鉴的模式。颜色系统采用"接口约束+常量集中+Builder 引用"的三层管理,保证全应用视觉一致性且支持主题扩展。数据模型使用 @Observed 装饰器实现响应式对象属性更新,配以 slice() 刷新数组引用的技巧保证 ForEach 重渲染。地图初始化采用"生命周期初始化回调+回调内注册监听"的两阶段模式,适配异步组件的就绪时序。searchByText 的每个字段都做 ?? 兜底,处理新增可选字段的版本兼容。事件监听采用 off 不传参清除全部订阅的便利特性,简化批量解绑。这些模式共同构成了一个健壮、可维护、可扩展的 HarmonyOS 地图应用工程范式。
从行业场景看,城市文化旅游攻略是一个高度依赖地图能力与搜索能力的场景。游客的核心需求——发现景点、了解详情、规划行程、收藏管理——分别对应应用的景点 Tab、搜索 Tab、地图 Tab、我的 Tab 四个页面。Map Kit 的 MapComponent 承载景点标注可视化,site.searchByText 支撑关键字检索,reliability 字段量化搜索质量,长按事件提供深度操作入口,四者协同构成了文旅应用的地图能力底座。本应用的实现为同类文旅应用(如博物馆导览、城市漫步、景区攻略)提供了可直接复用的架构参考。HarmonyOS 6.1.1 的 Map Kit 持续演进,reliability 与长按监听只是最新一环,未来可期待更多场景化能力(如路线规划、实时人流、AR 导览)的加入,进一步丰富文旅应用的能力边界。
附录: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)