一、技术前言

在这里插入图片描述

HarmonyOS ArkUI 框架是华为为鸿蒙生态打造的核心 UI 开发框架,其采用 ArkTS 语言作为开发载体,在 TypeScript 的类型系统基础上扩展了 @Entry@Component@State@Builder@Observed 等装饰器,使开发者能够以声明式语法构建高度响应式的用户界面。ArkUI 的核心运行机制基于状态驱动渲染——当 @State 修饰的状态变量发生变化时,框架会自动触发关联的 UI 组件重新构建,这一机制让开发者无需手动操作 DOM 节点,只需关注数据状态与视图的映射关系。在本文涉及的智慧停车应用中,ArkUI 的声明式能力被充分运用于四个完全不同布局风格的 Tab 页面切换、弹窗系统的条件渲染、地图事件日志流的实时更新以及搜索结果的动态加载,体现了框架在复杂业务场景下的表达力。

在这里插入图片描述
Map Kit 是 HarmonyOS 提供的地图服务能力集合,其核心渲染载体 MapComponent 组件能够以原生方式嵌入 ArkUI 页面树中,与其它 UI 组件共同参与布局计算和事件分发。MapComponent 接收两个关键参数:mapOptions 用于设定地图初始化时的中心点坐标和缩放级别,mapCallback 则是一个异步回调函数,在地图引擎完成初始化后触发,返回 MapComponentController 控制器实例。这个控制器是整个地图交互的"大脑"——通过它可以调用 addMarker 添加地图标注、通过 getEventManager 获取事件管理器、执行相机动画等操作。在本文的停车应用中,MapComponent 被放置在地图 Tab 页面中并以 layoutWeight(1) 占据剩余高度,同时底部保留一个固定高度的日志流区域,形成"地图主体 + 事件日志"的双层交互布局。

在这里插入图片描述
HarmonyOS 6.1.1 版本为 Map Kit 的 site 模块 searchByText 接口带来了一个重要的新字段——reliability(相关性分数)。当开发者通过 site.searchByText(params) 执行关键字搜索时,返回的 SearchByTextResult 中包含 sites 数组,其中每个 Site 对象除了原有的 name(地点名称)、formatAddress(格式化地址)、distance(直线距离)、position(经纬度坐标)等字段外,新增了 reliability 字段。该字段取值范围为 [0, 1]1 表示搜索结果与关键字完全相关,0 表示完全不相关。这个字段的出现,让应用能够从"仅返回结果列表"升级到"量化评估每个结果的相关程度",对于停车场搜索这类存在大量同名/近义地点的场景尤为关键——用户搜索"停车场"时,真正的停车场 reliability 可能高达 0.97,而一个名为"停车场道闸设备厂"的地点 reliability 仅 0.19,应用可以据此进行结果排序、过滤和可视化分级展示。

在这里插入图片描述
MapEventManager 是 Map Kit 中负责管理地图交互事件的核心类,在 HarmonyOS 6.1.1 版本中新增了两组长按监听接口。第一组是 onMarkerLongClick(callback) / offMarkerLongClick(),用于监听用户长按地图上自定义 Marker 标注的事件,回调参数为 map.Marker 对象,开发者可通过 marker.getPosition() 获取被长按标注的经纬度、通过 marker.getId() 获取标注唯一标识。第二组是 onPoiLongClick(callback) / offPoiLongClick(),用于监听用户长按地图上原生 POI(Point of Interest)地点的事件,回调参数为 mapCommon.Poi 对象,包含 name(POI 名称)和 position(POI 坐标)信息。off* 方法不接收参数,调用即清除该类型的全部订阅。这两组接口的加入,使得地图交互从"仅点击"扩展到"长按"维度,为右键菜单、信息详情弹窗、地点收藏等场景提供了事件入口。

在这里插入图片描述
城市智慧停车服务是现代城市治理的重要组成部分。随着机动车保有量持续增长,"停车难"已成为一线城市的突出痛点——驾驶员在陌生商圈常常需要绕行多圈才能找到空位,而停车场信息分散、实时空位不透明、入场价格不清晰等问题进一步加剧了时间浪费和交通拥堵。一个优秀的智慧停车应用需要解决三个核心问题:一是让驾驶员快速获知附近停车场的实时空位和价格信息,二是提供直观的地图找位体验,三是支持精准的停车场搜索与结果筛选。本文所分析的"停呗·智慧停车场找位"应用正是围绕这三个问题构建的,其四个 Tab 页面分别对应停车总览、地图找位、关键字搜索和个人中心,并将 Map Kit 6.1.1 的两大新特性深度融入地图交互和搜索结果展示中,形成了一套完整的技术演示方案。

在这里插入图片描述
在工程实践层面,本文将逐段分析该应用的完整源码实现,从颜色系统设计、常量数据定义、辅助函数封装,到 MapComponent 初始化流程、searchByText 调用与 reliability 字段消费、onMarkerLongClick / onPoiLongClick 监听注册与切换,再到四个 Tab 页面的布局构建和弹窗系统,覆盖 ArkUI 声明式 UI 开发的全部关键环节。通过这一分析,开发者可以掌握如何在 HarmonyOS 应用中集成 Map Kit 最新特性,并将之应用于具体的行业场景。

在这里插入图片描述

二、整体架构流程图

切换 Tab

0 停车

1 地图

2 搜索

3 我的

长按 Marker

长按 POI

输入关键字搜索

成功

失败

收藏/编辑/删除

应用启动 aboutToAppear

setupMapCallback 注册地图回调

MapComponent 渲染触发 mapCallback

err 是否为空?

console.error 记录错误并返回

获取 mapController

getEventManager 获取事件管理器

遍历 MARKER_SPOTS 批量 addMarker

onMarkerLongClick 注册 Marker 长按监听

onPoiLongClick 注册 POI 长按监听

地图就绪 · 等待用户交互

用户操作

currentTab 状态更新

Tab 索引判断

tabPark: 三宫格 + 推荐车场 + 全部列表

tabMap: MapComponent + 监听开关 + 日志流

tabSearch: 搜索框 + reliability 分数条

tabMine: 月卡 + 三宫格 + 功能清单

onMarkerLongClick 回调触发

unshift EventLog 到日志流

onPoiLongClick 回调触发

runSearch 调用 site.searchByText

是否成功?

遍历 sites 读取 reliability

生成 SearchRecord 列表 + 分数条渲染

catch 保留 Mock 演示数据

弹窗系统: panelAdd/panelEdit/panelDel

操作完成刷新 parkList

三、模块导入与依赖声明

3.1 Map Kit 与基础服务导入

import { MapComponent, mapCommon, map, site } from '@kit.MapKit';
import { AsyncCallback, BusinessError } from '@kit.BasicServicesKit';

第一行从 @kit.MapKit 导入了四个核心成员。MapComponent 是地图渲染组件,直接作为 ArkUI 组件在 build() 中使用;mapCommon 是公共类型命名空间,包含 LatLng(经纬度)、MapOptions(地图初始化参数)、MarkerOptions(标注配置)、Poi(POI 对象)等类型定义;map 是核心功能命名空间,包含 MapComponentController(地图控制器)、MapEventManager(事件管理器)、Marker(标注实例)等;site 是搜索服务命名空间,提供 searchByText 函数及 SearchByTextParamsSearchByTextResultSite 等类型。

第二行从 @kit.BasicServicesKit 导入两个基础类型。AsyncCallback<T> 是 HarmonyOS 通用的异步回调类型签名,用于定义 mapCallback 的类型;BusinessError 是业务错误对象,包含 codemessage 字段,用于在 catch 块中类型化捕获异常。这两个导入虽小,但构成了整个应用异步流程和错误处理的类型基础。设计上将 Map Kit 和 BasicServicesKit 分开导入,体现了 HarmonyOS Kit 化架构的模块隔离原则——每个 Kit 只暴露其职责范围内的 API,开发者按需引入,避免不必要的包体积开销。

四、颜色系统与主题设计

4.1 颜色接口定义

/** 主题色板接口:集中声明页面所有颜色字段(浅灰蓝底+停车蓝+活力青浅色系) */
interface ColorPalette {
  bg: string;      // 页面浅灰蓝底色
  card: string;    // 卡片纯白底色
  chip: string;    // 胶囊与输入框底色
  title: string;   // 主标题深藏青
  sub: string;     // 副文本灰蓝
  text3: string;   // 弱文本浅灰蓝
  blue: string;    // 停车蓝主色
  blueD: string;   // 深停车蓝
  blueL: string;   // 浅停车蓝(渐变浅端)
  cyan: string;    // 活力青辅助色
  red: string;     // 紧张/删除警示红
  line: string;    // 分隔线淡蓝灰
  tabOn: string;   // 底部 Tab 激活色
  mask: string;    // 弹窗遮罩色
  codeBg: string;  // 代码预览卡深底色(浅色主题也保留深底放代码文本)
}

ColorPalette 接口将页面所有颜色字段集中声明为强类型契约,共定义了 15 个颜色字段,覆盖了背景、卡片、胶囊、标题文本、副文本、弱文本、主色、深浅渐变色、辅助色、警示色、分隔线、Tab 激活色、遮罩色和代码块底色等全部使用场景。这种接口先行的方式有两个核心优势:第一是类型安全,任何使用 COLORS.xxx 的地方都能获得编译期类型检查,拼写错误会在编译阶段暴露而非运行时才表现为颜色异常;第二是语义明确,每个字段名本身就是设计意图的文档——bg 明确表示背景色,title 明确表示标题色,red 专门用于紧张空位和删除操作的警示,开发者通过字段名就能判断何时使用何种颜色。

4.2 浅色主题色板常量

/** 浅色主题色板常量(停呗 · 浅灰蓝底 + 停车蓝 + 活力青) */
const COLORS: ColorPalette = {
  bg: '#EEF3F8',
  card: '#FFFFFF',
  chip: '#E2EAF3',
  title: '#1F2D3D',
  sub: '#5D7288',
  text3: '#94A7BC',
  blue: '#1E66D0',
  blueD: '#13488F',
  blueL: '#D7E6FA',
  cyan: '#00A8A0',
  red: '#E5484D',
  line: '#D8E2EE',
  tabOn: '#1E66D0',
  mask: 'rgba(31,45,61,0.5)',
  codeBg: '#14202E'
};

COLORS 常量实现了 ColorPalette 接口,定义了"停呗"应用完整的浅色主题色板。整体配色策略以浅灰蓝底色 #EEF3F8 为基调,营造清爽现代的城市服务视觉感受;卡片使用纯白 #FFFFFF 与背景形成层次对比;主色采用停车蓝 #1E66D0,深端 #13488F 用于渐变和强调,浅端 #D7E6FA 用于渐变 Banner 的起始色;辅助色活力青 #00A8A0 用于"充足""实惠"等正面语义标识;警示红 #E5484D 用于紧张空位和删除操作。文本色系分为三级——深藏青 #1F2D3D 用于主标题,灰蓝 #5D7288 用于副文本,浅灰蓝 #94A7BC 用于弱化信息,形成清晰的视觉层级。

值得注意的是 mask 使用 rgba(31,45,61,0.5) 半透明值而非纯色,确保弹窗遮罩既能遮挡背景内容又不完全阻断视觉上下文。codeBg 在浅色主题中刻意保留深色底 #14202E,这是因为代码文本使用 monospace 字体配合蓝色/青色前景色,在深底上的可读性远优于浅底,体现了"主题一致性"与"可读性优先"的权衡设计。

五、常量定义与 Mock 数据体系

5.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: '我的' }
];

TabMeta 接口定义了底部导航每个 Tab 的数据结构,仅包含 icon(emoji 图标)和 label(标签文案)两个字段。TAB_LIST 常量数组按顺序定义了四个 Tab:停车、地图、搜索、我的,分别对应业务主功能、地图找位、关键字搜索和个人中心。使用 emoji 作为图标的好处是无需引入图片资源即可实现跨平台一致的视觉表现,且 emoji 自带的色彩与语义(🅿️ 代表停车、🗺 代表地图、🔍 代表搜索、👤 代表个人)天然契合各 Tab 的功能含义。ForEach 渲染时以 tab.label 作为键值,确保 Tab 列表的稳定渲染。

5.2 筛选标签与城市中心点

/** 头部横滑筛选 chips 文案(停车场类型筛选) */
const CATE_TAGS: string[] = ['全部', '室内车场', '地面车场', '充电车位', '首小时免费', '月卡专享', '访客车位', '大型车位'];

/** 城市中心点(深圳市民中心,地图初始化中心与 Map Kit 搜索 location 参数) */
const CITY_CENTER: mapCommon.LatLng = { latitude: 22.5431, longitude: 114.0579 };

CATE_TAGS 定义了头部横滑筛选区的八个标签,覆盖了停车场分类的主要维度——室内/地面区分物理形态、充电车位服务新能源车主、首小时免费和月卡专享涉及价格策略、访客车位和大型车位面向特殊车型需求。这些标签以横向滚动 chips 形式展示,当前选中索引存储在 @State cateIdx 中,选中态使用停车蓝底色+浅色文字,未选中态使用浅灰蓝底色+灰蓝文字。

CITY_CENTER 是整个应用的地理锚点,设定为深圳市民中心坐标(纬度 22.5431,经度 114.0579),类型为 mapCommon.LatLng。这个常量在两处被复用:一是作为 MapComponent 初始化时 mapOptions.position.target 的中心点,确保地图打开即聚焦深圳核心区域;二是作为 site.searchByTextlocation 参数,使关键字搜索以该坐标为圆心、5000 米为半径进行空间约束。这种"一个常量、多处复用"的设计保证了地图视图与搜索范围的空间一致性。

5.3 停车场标注点与推荐数据

/** 地图标注点接口(停车场 Marker 群,长按事件的数据来源) */
interface SpotItem {
  name: string;   // 车场名称
  lat: number;    // 纬度
  lng: number;    // 经度
  tag: string;    // 车场类型标签
}

/** 停车场标注点 Mock 数据(6 个,围绕城市中心点 ±0.02 度散布) */
const MARKER_SPOTS: SpotItem[] = [
  { name: '停呗·华强北智慧停车场', lat: 22.5472, lng: 114.0750, tag: '室内' },
  { name: '停呗·科苑路科技园车库', lat: 22.5405, lng: 114.0575, tag: '充电车位' },
  { name: '停呗·深南东路地面车场', lat: 22.5441, lng: 114.0700, tag: '地面' },
  { name: '停呗·后海大道立体车库', lat: 22.5279, lng: 114.0455, tag: '立体' },
  { name: '停呗·新安街道社区车场', lat: 22.5546, lng: 114.0440, tag: '月卡' },
  { name: '停呗·中心城商业车库', lat: 22.5345, lng: 114.0681, tag: '商场' }
];

SpotItem 接口定义了地图标注的数据结构,包含车场名称、经纬度和类型标签。MARKER_SPOTS 数组提供了六个 Mock 标注点,其经纬度围绕城市中心点 CITY_CENTER 在 ±0.02 度范围内散布,大致覆盖深圳福田、南山等核心区域。这六个标注点在 setupMapCallback 中被遍历调用 addMarker 逐一添加到地图上,成为 onMarkerLongClick 事件的数据来源——当用户长按其中任一标注时,回调中的 marker.getId() 返回的索引即对应此数组的元素位置。

/** 推荐车场接口(停车 Tab 头部精选大卡) */
interface ParkRec {
  icon: string;      // 车场 emoji
  name: string;      // 车场名
  dist: string;      // 距离文本
  free: number;      // 空位数
  firstHour: number; // 首小时价(元)
}

/** 推荐车场 Mock 数据(3 条,头部渐变大卡下方列表) */
const PARK_RECS: ParkRec[] = [
  { icon: '🅿️', name: '华强北智慧停车场', dist: '240m', free: 18, firstHour: 8 },
  { icon: '🏢', name: '科苑路科技园车库', dist: '800m', free: 46, firstHour: 6 },
  { icon: '🏗', name: '后海大道立体车库', dist: '1.9km', free: 32, firstHour: 10 }
];

ParkRec 接口服务于停车 Tab 的推荐车场区域,相比 SpotItem 增加了距离文本、空位数和首小时价三个业务字段。三条 Mock 数据分别代表近距离高价位、中距离中价位和远距离高价位的典型车场,空位数覆盖了 18(适中蓝)、46(充足青)、32(充足青)三种区间,配合 spaceColor 函数呈现差异化的空位状态色彩。推荐车场以大卡形式展示,每张卡片包含车场图标、名称、价格空位信息和"找位"按钮,点击"找位"会跳转到地图 Tab,形成停车总览到地图找位的功能衔接。

5.4 我的页功能清单

/** 我的页功能清单条目接口 */
interface FuncItem {
  icon: string;   // 功能图标
  label: string;  // 功能名
  value: string;  // 状态/数值文本
}

/** 我的页功能清单 Mock 数据(8 条,智慧停车语义) */
const FUNC_LIST: FuncItem[] = [
  { icon: '📋', label: '停车记录', value: '本月 23 次' },
  { icon: '🎫', label: '月卡套餐', value: '2 张有效' },
  { icon: '💳', label: '停车钱包', value: '余额 46.50 元' },
  { icon: '⭐', label: '收藏车场', value: '9 个' },
  { icon: '🧾', label: '发票管理', value: '本月 5 张' },
  { icon: '📍', label: '常用车位', value: 'B2-116' },
  { icon: '🔔', label: '空位提醒', value: '已开启' },
  { icon: '⚙', label: '偏好设置', value: '室内优先' }
];

FuncItem 接口定义了"我的"Tab 功能清单的每行数据结构,包含功能图标、功能名称和状态/数值文本。八条 Mock 数据覆盖了智慧停车用户的全生命周期需求:停车记录提供历史回顾、月卡套餐和停车钱包涉及支付、收藏车场和常用车位提升找位效率、空位提醒和偏好设置个性化体验、发票管理满足企业用户报销需求。每行以"图标 + 标题 + 右侧数值 + 箭头"的经典列表式布局呈现,点击可进入对应功能详情页(当前为演示形态)。

六、辅助函数封装

6.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.blue };
  }
  if (score >= 0.5) {
    return { label: '中相关', color: COLORS.cyan };
  }
  return { label: '低相关', color: COLORS.red };
}

reliabilityScore 函数是 Map Kit 6.1.1 reliability 字段在 UI 层的核心消费逻辑,它将 [0, 1] 区间的数值分数映射为语义化的等级标签和对应颜色。映射规则采用三档分级:≥0.8 判定为"高相关",使用停车蓝 #1E66D0 表示强匹配;≥0.5 判定为"中相关",使用活力青 #00A8A0 表示弱匹配但可接受;<0.5 判定为"低相关",使用警示红 #E5484D 提示用户该结果可能与关键字关联度低。这个函数在搜索 Tab 的每个结果条目中被调用两次——一次用于等级标签胶囊的文案和颜色,一次用于分数进度条的颜色,确保同一结果条目中标签与进度条的色彩语义完全一致。

分级阈值的选择基于实际搜索场景的考量。当用户搜索"停车场"时,真正的停车场 reliability 通常在 0.85 以上,而名称中仅包含"停车场"但实际是设备厂、培训中心的地点 reliability 往往低于 0.3。将高相关阈值设在 0.8,能有效区分"真停车场"与"名称含停车场关键词的非停车场所";将中相关阈值设在 0.5,则给那些名称部分匹配但确有停车功能的地点(如"某某商业中心地下停车场"搜"停车场"时可能得到 0.6-0.7 的分数)一个可接受的中间档位。这种分级策略让用户在浏览搜索结果时,通过颜色和标签即可快速判断结果可信度,无需逐条阅读地址详情。

6.2 空位与价格颜色映射

/** 空位数颜色映射:>30 充足活力青 / >10 正常停车蓝 / 其余紧张红 */
function spaceColor(free: number): string {
  if (free > 30) {
    return COLORS.cyan;
  }
  if (free > 10) {
    return COLORS.blue;
  }
  return COLORS.red;
}

/** 首小时价颜色映射:≤5 元实惠青 / ≤10 元适中蓝 / 其余偏贵红 */
function feeColor(fee: number): string {
  if (fee <= 5) {
    return COLORS.cyan;
  }
  if (fee <= 10) {
    return COLORS.blue;
  }
  return COLORS.red;
}

spaceColorfeeColor 两个函数分别处理空位数和首小时价到颜色的映射,采用与 reliabilityScore 一致的三档分级逻辑,但语义方向相反。spaceColor 中数值越大表示空位越充足,因此高档用活力青(正面)、中档用停车蓝(正常)、低档用警示红(紧张),当空位数 ≤10 时显示红色提醒用户该车场即将满位。feeColor 中数值越小表示价格越实惠,因此低档(≤5 元)用活力青(实惠)、中档(≤10 元)用停车蓝(适中)、高档(>10 元)用警示红(偏贵)。

这两个函数在整个应用中被多处调用:spaceColor 用于推荐车场大卡的空位状态标签、全部车场列表的实时空位数字、以及推荐车场的"充足/适中"文案着色;feeColor 用于全部车场列表底部的"首小时 X 元"胶囊标签。通过统一的颜色映射函数,确保了同一数值在不同位置呈现的色彩语义完全一致,避免了"同一空位数在不同卡片显示不同颜色"的视觉不一致问题。

七、数据模型与 @Observed 响应式

7.1 车场条目模型

/** 车场条目(停车 Tab 推荐列表,@Observed 支持备注编辑刷新) */
@Observed export class ParkItem {
  icon: string;     // 车场 emoji 图标
  name: string;     // 车场名
  free: number;     // 实时空位数
  firstHour: number; // 首小时价(元)
  dist: string;     // 距离文本
  note: string;     // 用户备注(可编辑)

  constructor(icon: string, name: string, free: number,
    firstHour: number, dist: string, note: string) {
    this.icon = icon;
    this.name = name;
    this.free = free;
    this.firstHour = firstHour;
    this.dist = dist;
    this.note = note;
  }
}

/** 车场列表 Mock 数据(7 条,深圳各区停车场) */
const PARK_LIST: Array<ParkItem> = [
  new ParkItem('🅿️', '华强北智慧停车场', 18, 8, '240m', '室内·充电车位 12 个'),
  new ParkItem('🏢', '科苑路科技园车库', 46, 6, '800m', '月卡专享快速通道'),
  new ParkItem('🚗', '深南东路地面车场', 5, 5, '1.1km', '露天·按时计费'),
  new ParkItem('🏗', '后海大道立体车库', 32, 10, '1.9km', '机械车位限高 1.9m'),
  new ParkItem('🏘', '新安街道社区车场', 64, 3, '2.4km', '业主月卡 350 元/月'),
  new ParkItem('🏬', '中心城商业车库', 12, 10, '3.2km', '消费满 100 抵 2 小时'),
  new ParkItem('🅿️', '皇岗口岸 P3 车场', 88, 12, '4.6km', '过夜封顶 60 元')
];

ParkItem 类使用 @Observed 装饰器修饰,这是 ArkUI 响应式数据模型的关键装饰器。@Observed 使得类的实例属性变化能够被框架追踪——当 @State 持有的 ParkItem 数组中某个实例的 note 属性被修改时,引用该属性的 UI 组件会自动刷新。在本应用中,编辑备注弹窗保存后,updatePark 方法直接修改 this.parkList[this.editIdx].note 的值,再通过 this.parkList = this.parkList.slice() 刷新数组引用以触发列表重渲染,@Observed 确保了属性级变更被正确捕获。

PARK_LIST 提供了七条覆盖深圳各区的 Mock 车场数据,空位数从 5(紧张红)到 88(充足青),首小时价从 3 元(实惠青)到 12 元(偏贵红),距离从 240m 到 4.6km,备注信息包含了充电车位、月卡专享、限高、消费抵扣、过夜封顶等丰富的真实停车场景语义。每条数据通过 new ParkItem(...) 构造实例而非对象字面量,确保所有条目都是 @Observed 类实例,具备响应式能力。

7.2 搜索结果模型(reliability 数据载体)

/** 搜索结果条目(★ Map Kit 6.1.1 reliability 字段数据载体) */
@Observed export class SearchRecord {
  name: string;         // 地点名称(site.name)
  address: string;      // 格式化地址(site.formatAddress)
  distance: number;     // 直线距离米(site.distance)
  reliability: number;  // ★ 相关性分数(site.reliability,[0,1])
  time: string;         // 记录时间文案

  constructor(name: string, address: string, distance: number,
    reliability: number, time: string) {
    this.name = name;
    this.address = address;
    this.distance = distance;
    this.reliability = reliability;
    this.time = time;
  }
}

/** 搜索结果 Mock 数据(6 条,reliability 覆盖高/中/低三档) */
const SEARCH_RECORDS: Array<SearchRecord> = [
  new SearchRecord('华强北智慧停车场', '深圳市福田区华强北路 1010 号', 360, 0.97, '刚刚'),
  new SearchRecord('科苑路科技园地下车库', '深圳市南山区科苑路 15 号', 810, 0.89, '刚刚'),
  new SearchRecord('深南东路公共停车场', '深圳市罗湖区深南东路 5001 号', 1280, 0.63, '3 分钟前'),
  new SearchRecord('后海大道立体停车库', '深圳市南山区后海大道 2388 号', 1890, 0.55, '6 分钟前'),
  new SearchRecord('停车场道闸设备厂', '深圳市宝安区新安街道创业二路 82 号', 3140, 0.19, '9 分钟前'),
  new SearchRecord('停车场管理培训中心', '深圳市龙岗区中心城吉祥路 500 号', 4990, 0.07, '13 分钟前')
];

SearchRecord 类是 site.searchByText 返回结果在应用层的数据载体,每个字段的注释明确标注了其对应的 Site 原始字段来源。其中 reliability 字段是 HarmonyOS 6.1.1 新增字段的映射,注释以 符号特别标识。这个类同样使用 @Observed 装饰,因为搜索结果列表需要在 runSearch 完成后被整体替换,@Observed 确保 ForEach 渲染的每个条目在属性变化时能精确刷新。

SEARCH_RECORDS 的六条 Mock 数据精心设计了 reliability 值的分布:前两条 0.97 和 0.89 属于高相关档位(≥0.8),对应真正的停车场;第三条 0.63 和第四条 0.55 属于中相关档位(≥0.5),对应名称部分匹配的停车场所;第五条 0.19 和第六条 0.07 属于低相关档位(<0.5),分别是一个"停车场道闸设备厂"和一个"停车场管理培训中心"——名称中包含"停车场"关键字但实际并非停车场所。这组数据完美演示了 reliability 字段的核心价值:仅凭关键字匹配会返回大量无关结果,而 reliability 分数能让用户一眼辨别哪些是真正想要找的停车场。

7.3 长按事件日志模型

/** 长按事件日志条目(★ 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', '深圳市民中心', 22.5460, 114.0545, '演示事件'),
  new EventLog('Marker', '#0', 22.5472, 114.0750, '演示事件')
];

EventLog 类是 MapEventManager 长按事件回调在应用层的数据载体。type 字段区分事件来源——'Marker' 表示用户长按了自定义标注,'POI' 表示用户长按了地图原生 POI 地点;name 字段对 Marker 存储的是 marker.getId() 返回的标注 ID(如 '#0'),对 POI 存储的是 poi.name 返回的地点名称(如 '深圳市民中心');latlng 记录事件发生时的经纬度坐标,精度保留四位小数用于日志展示。@Observed 装饰确保每次 unshift 新日志后列表能实时刷新。

EVENT_LOGS 提供了两条初始演示数据,分别对应 POI 长按和 Marker 长按两种事件类型,让用户在打开地图 Tab 时即可看到日志流的预期形态,降低理解成本。在实际运行中,当用户长按地图上的标注或 POI 时,新 EventLog 实例会通过 unshift 方法插入到数组头部,实现"最新事件置顶"的日志流效果。

八、主组件状态管理

8.1 组件声明与 UI 状态

/** 1135 停呗 · 智慧停车场找位主页面 */
@Entry
@Component
struct Page1135 {
  /** 当前选中 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 parkList: Array<ParkItem> = PARK_LIST;
  /** 我的页功能清单数据 */
  @State funcList: FuncItem[] = FUNC_LIST;

@Entry 装饰器标识 Page1135 为页面入口组件,@Component 声明其为 ArkUI 组件。组件内部首先声明了八个 UI 交互相关的 @State 状态变量。currentTab 控制当前显示的 Tab 页面,初始值为 0 即停车 Tab;cateIdx 控制头部筛选 chips 的选中项;addModaleditModaldelModal 三个布尔值分别控制收藏、编辑、删除三个弹窗的显示与隐藏;editIdxdelIdx 记录当前操作的车场索引,供弹窗回填数据或执行删除;parkListfuncList 是两个列表数据源。

这些 @State 变量的核心机制在于:当任何一个变量的值发生变化时,ArkUI 框架会自动重新构建引用了该变量的 UI 子树。例如 currentTab 从 0 变为 1 时,build() 中的 if-else 链会重新求值,停车 Tab 的内容被卸载、地图 Tab 的内容被挂载;addModalfalse 变为 true 时,Stack 中的 panelAdd 弹窗层会被条件渲染出来。这种状态驱动的渲染模型是 ArkUI 的核心,开发者只需维护状态数据,无需手动操作视图层级。

8.2 Map Kit 状态变量

  // --- Map Kit 状态(6.1.1 特性:搜索 reliability + 长按事件) ---
  /** 地图初始化参数(非可选并给默认值,避免组件参数传 undefined) */
  private mapOptions: mapCommon.MapOptions = {
    position: { target: CITY_CENTER, zoom: 13 }
  };
  /** 地图初始化回调(setupMapCallback 中赋值) */
  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 formFee: string = '';
  /** 收藏弹窗:地址输入 */
  @State formAddr: string = '';
  /** 编辑弹窗:备注输入 */
  @State editNote: string = '';

Map Kit 相关状态分为两类:private 不可变引用和 @State 响应式状态。mapOptions 是地图初始化参数,设定中心点为 CITY_CENTER、缩放级别为 13(城市街道级别),使用 private 因为它在组件生命周期内不需要变化。mapCallbackmapControllermapEventManager 三个可选类型变量分别存储地图初始化回调函数、控制器实例和事件管理器实例,它们在 setupMapCallbackaboutToAppear 中被赋值,之后作为方法调用的目标对象。

@State 响应式状态中,markerListenOnpoiListenOn 控制两种长按监听的开关状态,初始均为 true 即默认开启,与 Toggle 组件绑定实现开关切换;eventLogs 是长按事件日志流数据源,新事件通过 unshift 插入数组头部;queryInput 存储搜索框输入值,初始为 '停车场'searchState 是搜索状态文案,随搜索过程动态更新;searchRecords 是搜索结果列表,搜索成功后被整体替换。formNameformFeeformAddr 三个变量服务于收藏弹窗的表单输入,editNote 服务于编辑备注弹窗,均在弹窗关闭时被清空。

九、地图初始化与长按事件注册(Map Kit 6.1.1 特性二)

9.1 setupMapCallback 核心初始化流程

  /**
   * 地图初始化:controller → eventManager → Marker 群 → 6.1.1 双长按监听
   * 必须在 mapCallback 的 err 为空分支内注册监听(controller 就绪后才有管理器)
   */
  setupMapCallback() {
    this.mapCallback = async (err: BusinessError, mapController: map.MapComponentController) => {
      if (err) {
        console.error(`Map init failed, code: ${err.code}, message: ${err.message}`);
        return;
      }
      this.mapController = mapController;
      this.mapEventManager = mapController.getEventManager();
      // 批量添加停车场 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 是整个地图功能的初始化入口,它将一个 async 异步回调函数赋值给 this.mapCallback,该回调会在 MapComponent 完成底层地图引擎初始化后被调用。回调接收两个参数:errBusinessError 类型,非空表示初始化失败;mapControllerMapComponentController 实例,是后续所有地图操作的控制器。回调首先检查 err,若非空则通过 console.error 记录错误码和消息并提前返回,这是 HarmonyOS 异步回调的标准错误处理范式。

回调主体执行四个步骤。第一步,将 mapController 保存到实例变量 this.mapController,供后续方法使用。第二步,通过 mapController.getEventManager() 获取 MapEventManager 事件管理器实例并保存到 this.mapEventManager——这是长按监听注册的前提条件,事件管理器只能在控制器就绪后获取。第三步,遍历 MARKER_SPOTS 数组,为每个停车场标注点构建 MarkerOptions 配置对象并调用 await this.mapController.addMarker(markerOptions) 添加到地图上。MarkerOptions 配置了标注位置(position)、可点击(clickable: true)、可见(visible: true)、锚点位置(anchorU: 0.5, anchorV: 1 表示锚点在图标底部中心)、不可拖拽(draggable: false)等属性。每个 addMarker 调用被独立 try-catch 包裹,确保单个标注添加失败不会中断后续标注的添加。

第四步是本文的核心——注册 HarmonyOS 6.1.1 新增的两组长按监听。onMarkerLongClick 接收一个回调函数,参数类型为 map.Marker,当用户长按地图上的自定义 Marker 时触发。回调内通过 marker.getPosition() 获取标注当前经纬度,通过 marker.getId() 获取标注唯一标识,然后用这些信息构造 EventLog 实例并 unshift 到日志流数组头部。onPoiLongClick 同理,参数类型为 mapCommon.Poi,当用户长按地图上的原生 POI 地点时触发,回调内直接读取 poi.namepoi.position 构造日志。这两个监听必须在 mapCallbackerr 为空分支内注册,因为只有在控制器就绪后才能获取事件管理器,这是整个初始化链路的时序约束。

9.2 长按监听开关切换

  /** 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;
  }

toggleMarkerListentogglePoiListen 两个方法分别实现 Marker 长按和 POI 长按监听的开关切换。两个方法的逻辑结构完全对称:首先检查 this.mapEventManager 是否存在,若不存在(地图尚未初始化)则直接返回,避免空指针异常;然后根据当前开关状态决定操作——若当前为开启状态则调用 off* 方法清除监听,若当前为关闭状态则调用 on* 方法重新注册监听;最后翻转 @State 布尔值触发 Toggle 组件的视觉更新。

offMarkerLongClick()offPoiLongClick() 均不接收参数,调用即清除该类型的全部订阅回调。这种设计简化了 API 使用——开发者无需保存回调引用来精确移除,一次调用即可解绑全部。重新注册时传入的回调函数与 setupMapCallback 中注册的完全一致,确保开关切换前后的事件处理行为不变。这个开关功能在演示场景中非常实用,开发者可以直观地验证监听注册和移除的效果——关闭 Marker 监听后长按标注不再产生日志,重新开启后恢复响应。

十、关键字搜索与 reliability 评分(Map Kit 6.1.1 特性一)

10.1 runSearch 搜索方法

  /**
   * ★ 6.1.1 新特性·搜索:关键字搜索 searchByText → Site 数组
   * 读取 Site.reliability 相关性分数(可选字段,?? 兜底 0)
   * 无 AGC 配置/无网络时抛 BusinessError,catch 保留 Mock 数据保证演示链路
   */
  async runSearch() {
    this.searchState = '搜索中…';
    const params: site.SearchByTextParams = {
      query: this.queryInput,
      location: CITY_CENTER,
      radius: 5000,
      language: 'zh'
    };
    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 是搜索 Tab 的核心方法,完整演示了 site.searchByText 接口的调用流程和 reliability 字段的消费方式。方法首先将 searchState 更新为 '搜索中…',给用户即时反馈。然后构建 SearchByTextParams 参数对象:query 使用用户在搜索框输入的关键字(this.queryInput),location 使用城市中心点 CITY_CENTER 作为搜索圆心,radius 设为 5000 米覆盖城市核心区域,language 设为 'zh' 确保返回中文地址。这四个参数共同约束了搜索的空间范围和语言偏好。

方法主体使用 try-catch 包裹 await site.searchByText(params) 调用。成功时,从返回的 SearchByTextResult 中提取 sites 数组(使用 ?? [] 空值兜底),若数组为空则更新状态文案为"无结果 · 保留演示数据"并返回,保留原有 Mock 数据不清空,确保页面不会出现空白状态。若数组非空,遍历每个 Site 对象,将其字段映射到 SearchRecord 实例——s.names.formatAddresss.distance 均使用 ?? 操作符进行空值兜底,s.reliability 同样使用 ?? 0 兜底为 0 分,因为 reliability 作为 6.1.1 新增字段在某些返回场景下可能为 undefined。映射完成后将 records 数组赋值给 this.searchRecords 触发列表刷新,并更新状态文案为"返回 N 个车场"。

catch 块处理搜索失败的情况。在无 AGC(AppGallery Connect)配置或无网络连接的环境下,site.searchByText 会抛出 BusinessError 异常。catch 块将异常类型化为 BusinessError,提取 err.code 拼接到状态文案中(如"搜索失败(801) · 保留演示数据"),同时不清空 this.searchRecords,保留原有 Mock 数据使演示链路在离线环境下也能完整展示。这种"失败不破坏现有数据"的策略是优秀错误处理设计的体现——用户不会因为一次失败的搜索而丢失之前浏览的结果。

十一、车场管理与弹窗系统

11.1 编辑与删除操作

  /** 打开编辑备注弹窗(回填当前车场备注) */
  openEditPark(idx: number) {
    this.editIdx = idx;
    this.editNote = this.parkList[idx].note;
    this.editModal = true;
  }

  /** 保存收藏车场(空名兜底默认演示车场) */
  savePark() {
    const name = this.formName === '' ? '停呗·新收藏车场' : this.formName;
    const fee = this.formFee === '' ? '6' : this.formFee;
    const addr = this.formAddr === '' ? '深圳市福田区(地图选点)' : this.formAddr;
    this.parkList.unshift(new ParkItem('⭐', name, 10, 6, '待定位', `首小时 ${fee} 元 · ${addr}`));
    this.formName = '';
    this.formFee = '';
    this.formAddr = '';
    this.addModal = false;
  }

  /** 保存编辑备注(整体刷新数组引用以刷新列表) */
  updatePark() {
    if (this.editIdx >= 0 && this.editIdx < this.parkList.length) {
      if (this.editNote !== '') {
        this.parkList[this.editIdx].note = this.editNote;
      }
      this.parkList = this.parkList.slice();
    }
    this.editModal = false;
  }

  /** 删除收藏车场(确认弹窗回调) */
  delPark() {
    if (this.delIdx >= 0 && this.delIdx < this.parkList.length) {
      this.parkList.splice(this.delIdx, 1);
    }
    this.delModal = false;
  }

这四个方法共同构成了车场列表的增删改操作链路。openEditPark 接收车场索引,将其保存到 this.editIdx,同时从 parkList 中读取当前车场的备注回填到 this.editNote,最后打开编辑弹窗。回填操作确保用户看到的输入框预填了已有备注内容,而非空白,这是良好的表单编辑体验。

savePark 处理收藏新车场的保存逻辑。三个表单字段(车场名、首小时价、地址)均进行了空值兜底——若用户留空则使用默认值,确保新增的车场条目信息完整。保存时通过 new ParkItem(...) 构造新实例并 unshiftparkList 头部,使新车场出现在列表顶部。保存后清空三个表单变量并关闭弹窗,为下次打开提供干净的表单状态。

updatePark 保存编辑后的备注。方法首先校验 editIdx 的有效性(非负且不越界),然后修改 parkList[editIdx].note 属性。关键的一步是 this.parkList = this.parkList.slice()——虽然 @Observed 能追踪属性级变更,但 @State 数组需要引用变化才能触发 ForEach 重建。slice() 创建数组浅拷贝,改变引用地址,确保列表重新渲染显示新备注。delPark 使用 splice 删除指定索引的元素,splice 会原地修改数组但同样需要确保引用变化触发渲染,由于 splice 后 ArkUI 框架能检测到数组变化,因此无需额外 slice

11.2 生命周期与页面构建

  /** 生命周期:初始化地图回调(监听注册在 mapCallback 内完成) */
  aboutToAppear() {
    this.setupMapCallback();
  }

  /** 页面主构建:Stack 包裹主内容与三层弹窗 */
  build() {
    Stack() {
      Column() {
        this.headerMain()
        Divider().strokeWidth(1).color(COLORS.line)
        Scroll() {
          Column() {
            if (this.currentTab === 0) {
              this.tabPark()
            } 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() 执行前调用。此处调用 this.setupMapCallback() 完成地图回调函数的初始化,确保 MapComponent 渲染时 this.mapCallback 已有值可传。将监听注册逻辑放在 mapCallback 内部而非 aboutToAppear 中,是因为事件管理器依赖控制器实例,而控制器只有在地图引擎初始化完成后才可用。

build() 方法定义了页面的整体布局结构。最外层 Stack 是堆叠容器,用于将主内容层和弹窗层叠加——弹窗层在 Stack 中位于主内容之后,渲染在顶层。主内容层是一个 Column,从上到下依次是 headerMain()(头部渐变 Banner + 筛选 chips)、分隔线、可滚动内容区(Scroll 包裹的 Column,内部根据 currentTab 条件渲染四个 Tab 页面之一)、底部导航栏 tabBar()Scroll 使用 layoutWeight(1) 占据除头部和底部导航外的全部高度,scrollBar(BarState.Off) 隐藏滚动条保持视觉简洁。

弹窗层通过三个 if 条件渲染实现。当 addModaleditModaldelModal 任一为 true 时,对应的弹窗 Builder 被调用并渲染到 Stack 顶层。每个弹窗接收一个 onClose 回调函数用于关闭操作,回调内将对应状态变量设为 false,条件渲染移除弹窗层。这种"状态控制条件渲染 + Stack 堆叠"的弹窗实现方式是 ArkUI 中实现模态弹窗的经典模式,无需引入额外的弹窗管理组件。

十二、Builder 函数群与页面布局

12.1 头部渐变 Banner 与筛选 chips

  /** 头部:渐变 Banner(最近车场空位+入场价)+ 筛选 chips 横滑 */
  @Builder
  headerMain() {
    Column({ space: 12 }) {
      // 顶部渐变 Banner:空位数 + 入场价动态 + 扫码入口
      Column({ space: 10 }) {
        Row({ space: 12 }) {
          Column({ space: 2 }) {
            Text('18').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('华强北智慧停车场 · 240m').fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
            }
            Row({ space: 6 }) {
              Text('💳').fontSize(12)
              Text('首小时均价 6.0 元 · 扫码即入场').fontSize(11).fontColor(COLORS.sub)
            }
            Row({ space: 6 }) {
              Text('🔌').fontSize(12)
              Text('充电车位 12 个 · 新能源友好').fontSize(11).fontColor(COLORS.sub)
            }
          }
          .alignItems(HorizontalAlign.Start)
          .layoutWeight(1)
        }
        .width('100%')
        Row({ space: 8 }) {
          Text('🅿️ 扫码入场').fontSize(12).fontColor(COLORS.bg).fontWeight(FontWeight.Bold)
            .padding({ left: 14, right: 14, top: 8, bottom: 8 })
            .borderRadius(16).backgroundColor(COLORS.blue)
          Text('🗺 地图找位').fontSize(12).fontColor(COLORS.blue)
            .padding({ left: 14, right: 14, top: 8, bottom: 8 })
            .borderRadius(16).backgroundColor(COLORS.card)
            .onClick(() => { this.currentTab = 1; })
        }
        .width('100%')
        .justifyContent(FlexAlign.SpaceBetween)
      }
      .padding(14)
      .borderRadius(14)
      .linearGradient({
        angle: 135,
        colors: [[COLORS.blueL, 0.0], [COLORS.card, 0.65]]
      })
      // 筛选 chips 横滑
      Scroll() {
        Row({ space: 8 }) {
          ForEach(CATE_TAGS, (tag: string, idx: number) => {
            Text(tag)
              .fontSize(11)
              .fontColor(this.cateIdx === idx ? COLORS.bg : COLORS.sub)
              .padding({ left: 12, right: 12, top: 6, bottom: 6 })
              .borderRadius(14)
              .backgroundColor(this.cateIdx === idx ? COLORS.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%')
  }

headerMain Builder 构建了页面头部区域,包含渐变 Banner 和筛选 chips 两部分。渐变 Banner 使用 linearGradient 属性实现 135 度从浅停车蓝 #D7E6FA 到纯白 #FFFFFF 的渐变效果,营造清爽通透的视觉感受。Banner 内部左侧是大号空位数字"18"和"最近车场空位"标签,右侧三行信息分别展示最近车场名称距离、首小时均价和充电车位数量,底部两个胶囊按钮"扫码入场"和"地图找位"使用 justifyContent(FlexAlign.SpaceBetween) 分散对齐,"地图找位"按钮点击切换到地图 Tab。

筛选 chips 使用 Scroll 横向滚动容器包裹 Row,内部 ForEach 遍历 CATE_TAGS 数组生成八个标签胶囊。每个胶囊的 fontColorbackgroundColor 通过三元表达式根据 cateIdx === idx 判断切换选中态——选中时使用停车蓝底色 + 浅色文字,未选中时使用浅灰蓝底色 + 灰蓝文字,点击更新 cateIdxscrollable(ScrollDirection.Horizontal) 设置横向滚动,scrollBar(BarState.Off) 隐藏滚动条,八个标签在一屏内通常无法全部展示,用户可横滑浏览更多筛选条件。

12.2 停车 Tab 页面

  /** 数据统计小单元格(三宫格通用,浅色白底) */
  @Builder
  statCell(value: string, label: string) {
    Column({ space: 4 }) {
      Text(value).fontSize(17).fontWeight(FontWeight.Bold).fontColor(COLORS.blue)
      Text(label).fontSize(10).fontColor(COLORS.sub)
    }
    .layoutWeight(1)
    .padding({ top: 10, bottom: 10 })
    .borderRadius(10)
    .backgroundColor(COLORS.card)
  }

  /** 停车 Tab:停车数据三宫格 + 推荐车场 + 全部车场列表(业务主 Tab) */
  @Builder
  tabPark() {
    Column({ space: 10 }) {
      // 区块标题行:更多入口
      Row() {
        Text('附近车场').fontSize(15).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
        Blank()
        Text('收藏新车场 +').fontSize(11).fontColor(COLORS.blue)
          .onClick(() => { this.addModal = true; })
      }
      .width('100%')
      // 停车数据三宫格
      Row({ space: 8 }) {
        this.statCell('23 次', '本月停车')
        this.statCell('46 h', '本月时长')
        this.statCell('208 元', '本月花费')
      }
      .width('100%')
      // 推荐车场大卡(3 条)
      ForEach(PARK_RECS, (rec: ParkRec) => {
        Row({ space: 10 }) {
          Text(rec.icon).fontSize(26)
          Column({ space: 4 }) {
            Text(rec.name).fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
            Text(`首小时 ${rec.firstHour} 元 · 空位 ${rec.free}`).fontSize(11).fontColor(COLORS.sub)
            Row({ space: 6 }) {
              Text(rec.dist).fontSize(10).fontColor(COLORS.text3)
              Text(`空位${rec.free > 30 ? '充足' : '适中'}`).fontSize(10).fontColor(spaceColor(rec.free))
            }
          }
          .alignItems(HorizontalAlign.Start)
          .layoutWeight(1)
          Column({ space: 4 }) {
            Text('找位').fontSize(11).fontColor(COLORS.bg).fontWeight(FontWeight.Bold)
              .padding({ left: 12, right: 12, top: 6, bottom: 6 })
              .borderRadius(12).backgroundColor(COLORS.blue)
              .onClick(() => { this.currentTab = 1; })
          }
        }
        .padding(12)
        .borderRadius(12)
        .backgroundColor(COLORS.card)
        .width('100%')
      }, (rec: ParkRec) => rec.name)
      // 全部车场列表(长按 Marker 的数据同源)
      Row() {
        Text('全部车场(地图 Marker 同源)').fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
      }
      .width('100%')
      ForEach(this.parkList, (park: ParkItem, idx: number) => {
        Column({ space: 8 }) {
          Row({ space: 10 }) {
            Text(park.icon).fontSize(22)
            Column({ space: 3 }) {
              Text(park.name).fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
              Text(`空位 ${park.free} · 首小时 ${park.firstHour} 元 · ${park.dist}`).fontSize(11).fontColor(COLORS.sub)
              Text(`备注:${park.note}`).fontSize(10).fontColor(COLORS.text3)
            }
            .alignItems(HorizontalAlign.Start)
            .layoutWeight(1)
            Column({ space: 6 }) {
              Text(`${park.free}`).fontSize(16).fontWeight(FontWeight.Bold)
                .fontColor(spaceColor(park.free))
              Text('实时空位').fontSize(9).fontColor(COLORS.text3)
            }
          }
          .width('100%')
          Row({ space: 8 }) {
            Text(`首小时 ${park.firstHour}`).fontSize(10).fontColor(feeColor(park.firstHour))
              .padding({ left: 10, right: 10, top: 4, bottom: 4 })
              .borderRadius(10).backgroundColor(COLORS.chip)
            Blank()
            Text('编辑').fontSize(10).fontColor(COLORS.cyan)
              .padding({ left: 10, right: 10, top: 4, bottom: 4 })
              .borderRadius(10).backgroundColor(COLORS.chip)
              .onClick(() => { this.openEditPark(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; })
          }
          .width('100%')
        }
        .padding(12)
        .borderRadius(12)
        .backgroundColor(COLORS.card)
        .width('100%')
      }, (park: ParkItem) => park.name)
      // 停车小贴士卡
      this.tipsCard()
    }
    .width('100%')
  }

statCell 是一个通用的数据统计单元格 Builder,接收数值和标签两个参数,以白底圆角卡片形式展示。它被停车 Tab 和我的 Tab 的三宫格区域复用,体现了 Builder 函数的复用价值——一次定义、多处调用,保证统计单元格的视觉一致性。

tabPark Builder 构建了业务主 Tab 的完整内容。区块标题行使用 Blank() 组件实现"标题左对齐 + 操作右对齐"的布局,"收藏新车场 +"文本点击打开收藏弹窗。三宫格区域调用三次 statCell 展示本月停车次数、时长和花费。推荐车场区域 ForEach 遍历 PARK_RECS,每张大卡包含车场图标、名称、价格空位信息、距离和空位状态文案(通过 spaceColor 函数着色),以及"找位"按钮跳转地图 Tab。

全部车场列表区域遍历 this.parkList,每个条目展示更详细的信息——车场名称、空位数、首小时价、距离、备注,右侧大号空位数字通过 spaceColor 着色直观反映紧张程度。条目底部的操作行包含首小时价胶囊(通过 feeColor 着色)、编辑按钮和删除按钮。编辑按钮调用 openEditPark(idx) 打开编辑弹窗,删除按钮设置 delIdx 并打开删除确认弹窗。列表底部是 tipsCard 停车小贴士卡,提供商场消费抵扣、机械车位限高等实用提示。

12.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.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%')
      // ★ 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.cyan)
                    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 Builder 构建了地图 Tab 页面,这是 Map Kit 6.1.1 长按事件特性的核心演示页。页面从上到下分为四个区域:特性说明卡、监听开关行、MapComponent 地图本体、长按事件日志流。

特性说明卡使用浅灰蓝底色卡片展示"Map Kit 6.1.1 · 长按事件监听"标题和说明文案,让用户快速理解该页面的技术特性。监听开关行包含两个 Toggle 开关组件,分别绑定 markerListenOnpoiListenOn 状态,onChange 回调调用 toggleMarkerListentogglePoiListen 方法。Toggle 使用 ToggleType.Switch 样式和停车蓝选中色,尺寸精简为 36x20 适应紧凑布局。

MapComponent 是地图渲染本体,接收 mapOptions(初始化参数)和 mapCallback(初始化回调)两个参数。layoutWeight(1) 使地图占据开关行和日志流之间的全部剩余高度,borderRadius(12) 添加圆角与页面整体风格一致。地图渲染后,引擎完成初始化会触发 mapCallback,执行控制器获取、Marker 添加和长按监听注册的完整流程。

长按事件日志流区域固定高度 120 像素,内部 Scroll 可滚动。日志标题行显示"长按事件日志流"和条目总数,ForEach 遍历 this.eventLogs 渲染每条日志。每条日志的图标根据 type 字段选择——Marker 用 📍、POI 用 🅿️;类型标签颜色也根据 type 区分——Marker 用停车蓝、POI 用活力青;坐标使用 fontFamily('monospace') 等宽字体显示,保持经纬度数字对齐。新事件通过 unshift 插入数组头部,自动出现在日志流顶部,实现"最新事件置顶"的实时效果。

12.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.blue)
          .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 Builder 构建了搜索 Tab 页面,这是 Map Kit 6.1.1 reliability 相关性分数特性的核心演示页。页面从上到下分为四个区域:特性说明卡、搜索框与触发按钮、搜索状态文案、搜索结果列表、代码预览卡。

特性说明卡展示"searchByText · reliability 相关性评分"标题和字段说明文案。搜索框区域使用 TextInput + Button 的经典搜索栏布局,TextInputonChange 回调实时更新 this.queryInput,按钮点击调用 this.runSearch() 触发搜索。搜索状态文案 this.searchState 随搜索过程动态变化——待搜索、搜索中、返回 N 个车场、无结果、搜索失败等状态一一呈现。

搜索结果列表使用 List 组件(而非 ForEach 直接渲染),因为列表项需要滚动且 List 提供了更完善的滚动回收机制。每个 ListItem 内是一个 Column 卡片,展示搜索结果的完整信息。第一行是地点名称和 reliability 等级标签胶囊——名称使用 maxLines(1)textOverflow({ overflow: TextOverflow.Ellipsis }) 实现单行省略,标签通过 reliabilityScore 函数获取文案和颜色。第二行是格式化地址,同样单行省略。

第三行是 reliability 分数条的核心可视化——Progress 组件以 ProgressType.Linear 线性进度条形式展示分数,valuerec.reliability * 100(将 [0,1] 映射到 [0,100]),color 使用 reliabilityScore 返回的等级颜色。进度条旁是 reliability 0.97 格式的数值文本,使用等宽字体确保数字对齐。最后一行展示直线距离(米转公里保留两位小数)和记录时间。列表底部是 codePreviewCard 代码预览卡,以深色底等宽字体展示 6.1.1 双新特性的核心调用代码。

12.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('合作车场 9 折 · 无感支付自动抬杆').fontSize(11).fontColor(COLORS.sub)
          }
          .alignItems(HorizontalAlign.Start)
          .layoutWeight(1)
        }
        .width('100%')
        Divider().strokeWidth(1).color(COLORS.line)
        Row() {
          Text('本月停车 46 小时').fontSize(11).fontColor(COLORS.sub)
          Blank()
          Text('已省 58 元').fontSize(11).fontColor(COLORS.blue)
        }
        .width('100%')
      }
      .padding(14)
      .borderRadius(14)
      .linearGradient({
        angle: 135,
        colors: [[COLORS.blueL, 0.0], [COLORS.card, 0.72]]
      })
      .width('100%')
      // 数据三宫格
      Row({ space: 8 }) {
        this.statCell('236 次', '累计停车')
        this.statCell('9 个', '收藏车场')
        this.statCell('58 元', '本月省下')
      }
      .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)
      // 版本脚注
      Text('停呗 v2.4.0 · Map Kit 6.1.1 双新特性演示').fontSize(9).fontColor(COLORS.text3)
    }
    .width('100%')
  }

tabMine Builder 构建了"我的"个人中心页面。顶部是停车月卡渐变大卡,使用与头部 Banner 相同的 135 度渐变(浅停车蓝到纯白),展示月卡名称、权益说明(9 折 + 无感支付)、本月停车时长和已省金额。分隔线 Divider 将卡片分为标题区和数据区,"已省 58 元"使用停车蓝突出展示省钱效果。

数据三宫格复用 statCell 展示累计停车次数、收藏车场数和本月省下金额。功能清单区域 ForEach 遍历 FUNC_LIST,每行以"图标 + 标题 + 右侧数值 + 箭头"的经典列表式布局呈现,箭头 暗示可点击进入详情。页面底部是版本脚注,标注"停呗 v2.4.0 · Map Kit 6.1.1 双新特性演示",明确应用版本和演示的技术特性。

12.6 弹窗系统

  /** 弹窗遮罩层(点击空白处关闭) */
  @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: '首小时价(元,如:6)', text: this.formFee })
          .height(38)
          .fontSize(12)
          .fontColor(COLORS.title)
          .placeholderColor(COLORS.text3)
          .backgroundColor(COLORS.chip)
          .onChange((v: string) => { this.formFee = 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.savePark(); })
        }
        .width('100%')
      }
      .padding(16)
      .borderRadius(14)
      .backgroundColor(COLORS.card)
      .width('82%')
    }
    .width('100%')
    .height('100%')
  }

弹窗系统由 modalOverlaypanelAddpanelEditpanelDel 四个 Builder 组成。modalOverlay 是通用遮罩层,全屏半透明背景,点击触发 onClose 回调关闭弹窗——这是模态弹窗的标准交互模式,用户点击遮罩区域即可取消操作。

panelAdd 是收藏车场弹窗,使用 Stack 将遮罩层和内容卡片堆叠。内容卡片宽度 82%居中显示,内部包含标题、三个 TextInput 输入框(车场名、首小时价、地址)和取消/收藏按钮行。每个输入框的 onChange 回调实时更新对应的状态变量,"取消"按钮调用 onClose 关闭弹窗,"收藏"按钮调用 savePark 保存数据。panelEditpanelDel 的结构与 panelAdd 类似——panelEdit 包含一个备注输入框,标题下方显示当前编辑的车场名称;panelDel 是删除确认弹窗,显示待删除车场名称和确认/取消按钮,确认按钮使用警示红底色,"删除"操作调用 delPark 方法。

三个弹窗均通过 build()Stack 的条件渲染实现显示与隐藏——if (this.addModal) 等条件为 true 时弹窗 Builder 被调用并渲染到 Stack 顶层,为 false 时弹窗从渲染树中移除。这种实现方式无需调用 bindSheetbindContentCover 等内置弹窗 API,通过纯声明式 UI 即可完成模态弹窗的全部交互,体现了 ArkUI 状态驱动渲染的灵活性。

十三、Map Kit 6.1.1 双新特性对比

对比维度searchByText reliability 字段onMarkerLongClick / onPoiLongClick 事件
特性类别搜索结果数据增强地图交互事件扩展
所属模块site 模块map 模块 MapEventManager
数据类型number,取值 [0,1]回调函数,参数为 Marker / Poi
核心价值量化搜索结果与关键字的相关程度捕获用户长按标注/POI的交互意图
典型场景停车场搜索结果分级展示、过滤排序长按标注弹出详情、长按POI收藏地点
UI 呈现分数进度条 + 等级标签(高/中/低相关)事件日志流(类型 + 名称 + 坐标 + 时间)
空值处理s.reliability ?? 0 兜底为 0无空值风险,回调注册即生效
开关控制无开关,随搜索结果返回Toggle 开关,off* 方法清除订阅
错误处理searchByText 抛 BusinessError,catch 保留 Mock监听注册在 mapCallback err 为空分支内
兼容性HarmonyOS 6.1.1+ 新增字段HarmonyOS 6.1.1+ 新增接口
生命周期每次搜索调用时返回新值一次注册持续生效,off* 清除

十四、总结

本文围绕 HarmonyOS 6.1.1 Map Kit 的两大新特性,完整分析了一个城市智慧停车找位应用的源码实现。从整体架构看,应用采用四 Tab 单排底部导航的经典移动端布局,每个 Tab 的布局风格完全不同——停车 Tab 以数据三宫格 + 推荐大卡 + 全部列表为主,地图 Tab 以 MapComponent + 监听开关 + 事件日志流为主,搜索 Tab 以搜索框 + reliability 分数条结果列表为主,我的 Tab 以月卡渐变大卡 + 功能清单为主。这种"一个应用、四种布局"的设计体现了 ArkUI 声明式 UI 在布局表达上的灵活性。

reliability 字段是 searchByText 接口在 6.1.1 版本的关键增强。在此之前,搜索结果仅返回地点名称、地址和距离,开发者无法判断结果与关键字的真实关联程度——用户搜索"停车场"可能收到"停车场道闸设备厂"这样的无关结果,而应用无法自动区分。reliability 字段以 [0,1] 的量化分数解决了这一问题,应用通过 reliabilityScore 函数将分数映射为高/中/低三档等级标签和对应颜色,配合 Progress 线性进度条实现分数可视化,让用户一眼辨别搜索结果可信度。空值兜底用 ?? 0 处理,搜索失败用 catch 保留 Mock 数据,保证了演示链路在离线环境下的完整性。

onMarkerLongClickonPoiLongClick 两组长按监听接口是 MapEventManager 在 6.1.1 版本的重要扩展。在此之前,地图交互仅支持点击(onMarkerClick / onPoiClick),长按这一常见移动端交互手势无法被捕获。6.1.1 版本补齐了这一缺口——onMarkerLongClick 回调参数为 map.Marker,可通过 getPosition()getId() 获取标注信息;onPoiLongClick 回调参数为 mapCommon.Poi,可直接读取 nameposition。应用将长按事件转化为 EventLog 实例并 unshift 到日志流,实现实时事件记录。toggleMarkerListentogglePoiListen 方法通过 off* 方法实现监听清除,配合 Toggle 开关组件让开发者可以动态控制监听状态。

在工程实践方面,应用的代码组织遵循了清晰的分层结构:颜色系统接口先行、常量与 Mock 数据集中定义、辅助函数封装颜色映射逻辑、@Observed 数据模型支持响应式刷新、主组件状态管理区分 private 不可变引用和 @State 响应式变量、@Builder 函数群按页面区域拆分为可复用的布局单元。弹窗系统采用"Stack 条件渲染 + 遮罩层"的纯声明式实现,无需额外弹窗 API。地图初始化遵循"aboutToAppear 注册回调 → MapComponent 渲染触发回调 → controller 就绪 → eventManager 获取 → Marker 添加 → 长按监听注册"的严格时序链路,每一步都依赖前一步的完成。这些实践为 HarmonyOS 应用集成 Map Kit 最新特性提供了可复用的参考范式。

从行业场景看,城市智慧停车服务对地图能力和搜索能力的依赖是刚性的——驾驶员需要地图找位、需要搜索定位车场、需要长按标注查看详情。Map Kit 6.1.1 的两大新特性恰好命中了这两个核心痛点:reliability 让搜索结果更精准可信,长按事件让地图交互更丰富自然。随着鸿蒙生态的持续发展,Map Kit 的能力矩阵将不断完善,开发者可以基于这些新特性构建更贴近用户真实需求的智慧出行应用。

附录: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 将自动执行以下操作:

  1. 生成项目骨架(Stage 模型目录结构)
  2. 执行 ohpm install 安装依赖
  3. 运行 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.1Release✅ 已安装

界面顶部提示:“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 246.1.1.100Release✅ 已安装
API Version 236.1.0.28Beta1未安装
API Version 226.0.2.112Release未安装

安装路径示例:D:\DevTools\ArkUI-X\sdk

说明:ArkUI-X 允许开发者使用一套 ArkTS 主代码,同时构建多平台应用。如果仅开发 HarmonyOS 原生应用,无需额外安装 ArkUI-X SDK。

在这里插入图片描述


三、小结

步骤操作关键点
创建项目欢迎页 → 新建项目 → 选择 Empty Ability 模板 → 配置项目信息 → 完成使用 Stage 模型 + ArkTS 语言
查看 SDK设置 → HarmonyOS SDKSDK 已内置,无需手动安装
跨平台扩展设置 → ArkUI-X根据需要安装对应 API 版本

至此,DevEco Studio 的项目创建与 SDK 环境确认全部完成,可以开始 HarmonyOS 应用的功能开发。


本文基于 DevEco Studio 6.1.1 Release 版本编写,不同版本界面可能存在细微差异。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐