一、技术前言

在这里插入图片描述

HarmonyOS ArkUI 框架是华为面向全场景多设备的应用开发框架,其核心设计语言 ArkTS 在 TypeScript 基础上扩展了声明式 UI 语法,开发者可以通过 @Component@Entry@State@Builder 等装饰器以极简的代码结构描述出复杂的界面层级与状态驱动逻辑。ArkUI 采用 MVVM 思想,状态变量一旦被 @State 修饰,其变化会自动触发依赖该状态的 Builder 重新构建,从而实现"数据驱动视图"的开发范式。相较于传统命令式 UI,ArkUI 的声明式描述让房产找房这类需要频繁列表刷新、弹窗切换、地图交互的业务场景,可以以更少的样板代码完成更稳健的状态同步。

在这里插入图片描述
Map Kit 是 HarmonyOS 为位置服务场景提供的一站式地图能力套件,其核心组件 MapComponent 可以像普通 UI 组件一样被嵌入 ArkUI 声明式树中,通过 mapOptions 指定初始中心点和缩放级别,通过 mapCallback 异步拿到 MapComponentController 控制器,再基于控制器获取 MapEventManager 事件管理器。这一"组件化 + 异步回调 + 控制器 + 事件管理器"的四段式设计,让地图从初始化到交互监听的全链路都可在 ArkUI 体系内闭环完成,开发者无需跳出声明式树去对接原生地图 SDK,极大降低了地图与业务 UI 的集成成本。

在这里插入图片描述
在 HarmonyOS 6.1.1 版本中,Map Kit 的 site 模块对 searchByText 接口返回的 Site 类型进行了重要能力升级——新增 reliability 相关性分数字段。该字段取值范围为 [0, 1],其中 1 表示完全相关,0 表示几乎不相关。在传统 POI 搜索中,结果列表只给出名称、地址、距离三要素,开发者无法判断哪条结果更贴合用户输入的关键字语义;而 reliability 字段的引入,使开发者能够在 UI 层直接对搜索结果按相关度排序、分级着色、进度条可视化,从而帮助用户在海量 POI 中快速锁定真正匹配意图的房源。这一字段尤其适用于"两居室"“学区房”"满五唯一"这类语义模糊、容易产生歧义检索的房产垂直搜索场景。

在这里插入图片描述
同样在 HarmonyOS 6.1.1 版本中,MapEventManager 新增了两类长按事件监听接口:onMarkerLongClick / offMarkerLongClick 用于订阅和注销地图标记(Marker)的长按手势;onPoiLongClick / offPoiLongClick 用于订阅和注销地图兴趣点(POI)的长按手势。前者回调参数为 map.Marker 对象,开发者可从中读取 getId()getPosition() 获取标记 ID 与经纬度;后者回调参数为 mapCommon.Poi 对象,可直接拿到 poi.namepoi.position。这两组接口的补齐,意味着地图层面的"长按收藏"“长按对比”"长按导航"等房产高频交互可以脱离自定义浮层方案,直接以原生事件流的形式接入业务逻辑,事件链路更短、性能更优、手势识别更精准。

在这里插入图片描述
房产交易与租赁服务是一个对"位置精度"与"信息密度"双重敏感的行业。买方在找房过程中既需要地图直观呈现小区分布,又需要列表承载户型、面积、总价、单价、备注等多维度信息;既需要关键字搜索快速定位意向房源,又需要长按手势对地图上的具体点位执行收藏、对比、记笔记等操作。本文要剖析的"安家找房"应用,正是在 HarmonyOS ArkUI + Map Kit 6.1.1 双新特性的加持下,以 4 个 Tab(房源/地图/搜索/我的)为骨架,将 Map Kit 的 reliability 评分与长按事件流两条技术线,与房产找房的真实业务流程深度融合,形成一套可复用、可演示、可扩展的垂直行业落地范式。

在这里插入图片描述
从工程结构看,该应用采用"接口先行 + 常量沉淀 + 辅助函数 + Observed 数据模型 + 单 Entry 组件 + Builder 分区"的分层组织方式。颜色系统、Tab 元数据、Mock 数据全部以接口与常量集中声明,确保主题切换与数据替换时只需修改一处;辅助函数 reliabilityScorepriceColorareaColor 将业务规则与 UI 着色解耦;@Observed 修饰的 HouseItemSearchRecordEventLog 三类数据模型支持备注编辑、搜索刷新、事件流置顶等细粒度状态驱动;最终在 Page1133 单组件内以十余个 @Builder 函数完成头部、4 个 Tab 内容、3 套弹窗、底部导航的分区构建。这种"数据层-规则层-视图层"清晰分层的写法,是 ArkUI 中大型业务页面值得借鉴的工程范式。

在这里插入图片描述

二、整体架构流程图

常量与规则层

数据模型层

ArkUI视图层

site搜索能力层

MapKit能力层

入口层

aboutToAppear 生命周期

setupMapCallback 初始化地图回调

mapCallback 异步回调

MapComponentController 控制器

getEventManager 事件管理器

addMarker 批量打点 6 个小区房源

onMarkerLongClick Marker 长按监听

onPoiLongClick POI 长按监听

EventLog 日志流 unshift 置顶

runSearch 关键字搜索

site.searchByText

Site 数组含 reliability 字段

SearchRecord 映射 0~1 分数

分数条 + 等级标签可视化

Page1133 Entry组件

房源Tab 带看三宫格+精选+全部列表

地图Tab MapComponent+监听开关+日志流

搜索Tab 关键字+分数条+代码预览

我的Tab VIP卡+三宫格+功能清单

三层弹窗 收藏/编辑/删除

底部4Tab导航

HouseItem Observed 房源备注可编辑

SearchRecord Observed 搜索结果含reliability

EventLog Observed 长按事件日志

COLORS 主题色板

MARKER_SPOTS 地图标注点

reliabilityScore 分数等级映射

priceColor/areaColor 着色规则

上图展示了从生命周期入口出发,地图能力层与 site 搜索能力层如何并行接入 ArkUI 视图层,而数据模型层与常量规则层则横向贯穿四个 Tab,构成一个"四纵四横"的立体架构。下面我们逐段拆解源码,深入每个技术细节。

三、颜色系统:浅色主题的集中化声明

在这里插入图片描述

3.1 ColorPalette 接口定义

interface ColorPalette {
  bg: string;      // 页面纸白底色
  card: string;    // 卡片纯白底色
  chip: string;    // 胶囊与输入框底色
  title: string;   // 主标题深墨蓝
  sub: string;     // 副文本灰蓝
  text3: string;   // 弱文本浅蓝灰
  blue: string;    // 家园蓝主色
  blueD: string;   // 深家园蓝
  blueL: string;   // 浅家园蓝(渐变浅端)
  teal: string;    // 青绿辅助色
  orange: string;  // 高价警示橙
  red: string;     // 删除警示红
  line: string;    // 分隔线淡蓝
  tabOn: string;   // 底部 Tab 激活色
  mask: string;    // 弹窗遮罩色
  codeBg: string;  // 代码预览卡深底色
}

这段代码定义了一个 ColorPalette 接口,将页面涉及的全部 17 个颜色字段以契约形式集中声明。接口本身不产生运行时开销,它的价值在于"以类型约束防止拼写错误"与"以注释固化语义意图"。可以看到,字段命名并非 color1/color2 这种无意义序号,而是按用途语义化命名为 bgcardtitlesubblueteal 等,每个字段后紧跟中文注释说明其用途。这种命名-注释双约束的方式,使得后续在 Builder 中引用 COLORS.blue 时,IDE 不仅能自动补全,还能在悬浮提示中展示语义说明,极大降低了多人协作时的颜色误用概率。

在房产找房这类信息密度高的业务中,颜色承担着"信息分级"的关键职责:主标题需要深墨蓝高对比以承担信息锚点,副文本需要灰蓝降低视觉权重,弱文本需要浅蓝灰进一步退后,警示色需要橙红跳出。ColorPalette 接口将这些分级预定义为不可遗漏的契约字段,迫使开发者在引入新颜色前先思考它属于哪一级,从而避免"随手写死值"导致的视觉混乱。

3.2 COLORS 浅色主题常量实例

const COLORS: ColorPalette = {
  bg: '#F7F9FC',
  card: '#FFFFFF',
  chip: '#E8EEF7',
  title: '#1E2A3A',
  sub: '#5E7290',
  text3: '#93A5BF',
  blue: '#2563EB',
  blueD: '#1741A6',
  blueL: '#DCE8FB',
  teal: '#0FA98E',
  orange: '#E8862E',
  red: '#E5484D',
  line: '#DCE4F0',
  tabOn: '#2563EB',
  mask: 'rgba(30,42,58,0.5)',
  codeBg: '#16233A'
};

COLORS 常量以 ColorPalette 接口为类型约束,将所有颜色十六进制值一次性实例化。这里值得注意的设计有三点。第一,主底色 #F7F9FC 是一种偏冷的纸白,比纯白 #FFFFFF 更柔和,长时间浏览房源列表时不刺眼,符合房产这类重阅读场景的视觉人体工学。第二,主色 #2563EB 是一种饱和度适中的"家园蓝",既保留了科技蓝的信任感,又通过略低的饱和度传递"安家"的稳重气质,与房产交易所需的"可信赖"调性匹配。第三,辅助色 #0FA98E 青绿用于正向指标(如低总价上车盘、主流面积),与蓝色形成冷暖对比,使数据维度可被颜色快速区分。

mask 字段使用了 rgba(30,42,58,0.5) 而非纯黑半透明,这是因为遮罩下方的内容在房产场景中往往仍需隐约可见(如正在编辑的房源列表),使用与主标题同色系(#1E2A3A 的 rgb 即 30,42,58)的半透明值,可以让遮罩与整体冷调融合,避免"灰蒙蒙的黑纱"割裂感。codeBg 字段则在浅色主题中刻意保留深底 #16233A,用于代码预览卡的 monospace 文本展示,因为代码文本在深底浅字下可读性远高于浅底深字,这是"主题统一性"与"局部可读性"之间的一次合理取舍。

四、常量定义:Tab 元数据与城市中心点

4.1 Tab 元数据与筛选 chips

interface TabMeta {
  icon: string;   // Tab 图标 emoji
  label: string;  // Tab 标签文案
}

const TAB_LIST: TabMeta[] = [
  { icon: '🏠', label: '房源' },
  { icon: '🗺', label: '地图' },
  { icon: '🔍', label: '搜索' },
  { icon: '👤', label: '我的' }
];

const CATE_TAGS: string[] = ['全部', '两居室', '三居室', '近地铁', '电梯房', '满五唯一', '学区房', '次新房'];

TabMeta 接口将底部导航的图标与标签抽象为 {icon, label} 二元组,TAB_LIST 常量则把 4 个 Tab 的元数据以数组形式集中声明。这种写法的好处在于底部导航栏的构建可以直接 ForEach(TAB_LIST, ...) 渲染,新增或调整 Tab 顺序时只需修改常量数组,无需触碰视图代码。图标采用 emoji 而非图片资源,既免去了资源管理的开销,又保证了在所有设备上的即时可用性——在房产这类 MVP 阶段快速验证业务流程的应用中,emoji 是一种高性价比的图标方案。

CATE_TAGS 则定义了头部横滑筛选 chips 的文案数组,覆盖了二手房交易中最常见的 8 个筛选维度:全部 作为兜底入口,两居室/三居室 是户型维度,近地铁/电梯房 是配套维度,满五唯一/学区房/次新房 是产权与属性维度。这 8 个标签的选取并非随意,而是浓缩了北京二手房市场的核心筛选心智——满五唯一 关乎个税节省,学区房 关乎教育资源,次新房 关乎房龄与贷款年限,每一个标签背后都是一笔数十万到数百万的真实交易决策。将这些业务语义以常量形式前置声明,使代码本身就成为一份业务知识文档。

4.2 城市中心点与地图标注点

const CITY_CENTER: mapCommon.LatLng = { latitude: 39.9042, longitude: 116.4074 };

interface SpotItem {
  name: string;   // 小区房源名称
  lat: number;    // 纬度
  lng: number;    // 经度
  tag: string;    // 房源标签
}

const MARKER_SPOTS: SpotItem[] = [
  { name: '安家找房·王府井两居室', lat: 39.9149, lng: 116.4115, tag: '两居室' },
  { name: '安家找房·金融街学区三居', lat: 39.9135, lng: 116.3880, tag: '学区' },
  { name: '安家找房·什刹海四合院', lat: 39.9238, lng: 116.3895, tag: '四合院' },
  { name: '安家找房·东直门次新两居', lat: 39.9217, lng: 116.4264, tag: '次新' },
  { name: '安家找房·崇文门地铁一居', lat: 39.8992, lng: 116.4187, tag: '近地铁' },
  { name: '安家找房·广安门满五三居', lat: 39.8946, lng: 116.3874, tag: '满五唯一' }
];

CITY_CENTER 常量定义了北京故宫周边的经纬度坐标(39.9042, 116.4074),它同时承担两个角色:一是 MapComponent 初始化时 mapOptions.position.target 的中心点,二是 site.searchByText 搜索时 location 参数的基准点。将同一坐标复用于"地图视野中心"与"搜索基准点",保证了用户在地图 Tab 看到的视野范围与在搜索 Tab 检索的结果范围在地理上完全一致,避免了"地图看的是 A 区域、搜索返回的是 B 区域"的体验割裂。mapCommon.LatLng 是 Map Kit 提供的经纬度类型,使用它而非自定义 {lat, lng} 对象,可以直接满足 Map Kit API 的类型签名要求,免去类型转换。

SpotItem 接口定义了地图标注点的数据结构,MARKER_SPOTS 数组则沉淀了 6 个围绕城市中心点 ±0.02 度散布的 Mock 小区房源。每个标注点的 tag 字段与 CATE_TAGS 中的筛选标签形成语义呼应——两居室学区四合院次新近地铁满五唯一 六个标签覆盖了不同购房需求画像。这些标注点将在 setupMapCallback 中被批量转换为 mapCommon.MarkerOptions 并通过 addMarker 打到地图上,成为后续 onMarkerLongClick 长按事件的数据来源。换言之,MARKER_SPOTS 是"地图可视化"与"长按事件"两条链路的共同数据源,体现了"一份数据驱动多个视图"的设计思想。

五、房源与功能 Mock 数据

5.1 精选房源与功能清单

interface HouseRec {
  icon: string;   // 房源 emoji
  name: string;   // 小区名
  dist: string;   // 距离文本
  layout: string; // 户型与面积文本
  total: number;  // 总价(万,用于颜色映射)
  unit: number;   // 单价(万/㎡)
}

const HOUSE_RECS: HouseRec[] = [
  { icon: '🏢', name: '望京西园三区', dist: '650m', layout: '2室1厅 · 89㎡', total: 560, unit: 6.3 },
  { icon: '🏘', name: '方庄芳古园', dist: '1.2km', layout: '2室1厅 · 76㎡', total: 430, unit: 5.7 },
  { icon: '🏠', name: '芍药居北里', dist: '2.8km', layout: '3室1厅 · 112㎡', total: 720, unit: 6.4 }
];

interface FuncItem {
  icon: string;   // 功能图标
  label: string;  // 功能名
  value: string;  // 状态/数值文本
}

const FUNC_LIST: FuncItem[] = [
  { icon: '📅', label: '看房记录', value: '本月 6 次' },
  { icon: '📋', label: '我的委托', value: '1 个 · 在售' },
  { icon: '💰', label: '购房预算', value: '500 万 · 可贷 7 成' },
  { icon: '⭐', label: '收藏房源', value: '8 套' },
  { icon: '🧮', label: '房贷计算器', value: '商贷利率 4.0%' },
  { icon: '⚖', label: '小区对比', value: '已加 2 个小区' },
  { icon: '🔔', label: '降价提醒', value: '已开启' },
  { icon: '⚙', label: '账号设置', value: '实名已认证' }
];

HouseRec 接口服务于房源 Tab 头部的精选房源大卡,3 条数据覆盖了朝阳望京、丰台方庄、朝阳芍药居三个典型北京二手房聚居区,总价从 430 万到 720 万梯度分布,单价从 5.7 万到 6.4 万/㎡ 反映了不同片区的市场行情。total 字段的类型为 number 而非 string,这一看似细节的选择实则承载着重要设计意图——只有数值类型才能被 priceColor() 函数判断区间并映射颜色,若写成字符串则每次着色都需要 parseInt,既损失类型安全又增加运行时开销。将"需要参与计算的属性"与"纯展示文本"在数据模型层就区分类型,是 ArkUI 数据驱动着色的基础前提。

FuncItem 接口服务于"我的"Tab 的功能清单,8 条数据覆盖了从看房记录、委托管理、预算计算、收藏、房贷计算、小区对比、降价提醒到账号设置的完整房产用户旅程。value 字段采用字符串而非数值,是因为这里的"状态/数值"是面向用户展示的最终文案(如"500 万 · 可贷 7 成"包含分隔符与多个数值),将其以字符串整体沉淀,比拆成多个数值字段再在视图层拼接更直接。这种"该数值就数值、该文本就文本"的务实分类,避免了过度建模带来的复杂度,是中小型业务页面值得借鉴的数据建模态度。

六、辅助函数:相关性等级与颜色映射规则

6.1 reliability 分数等级映射

interface ScoreLevel {
  label: string;  // 等级文案
  color: string;  // 等级颜色
}

function reliabilityScore(score: number): ScoreLevel {
  if (score >= 0.8) {
    return { label: '高相关', color: COLORS.blue };
  }
  if (score >= 0.5) {
    return { label: '中相关', color: COLORS.teal };
  }
  return { label: '低相关', color: COLORS.orange };
}

reliabilityScore 函数是 Map Kit 6.1.1 新增 reliability 字段在 UI 层落地的核心桥梁。它接收一个 [0, 1] 区间的分数值,返回一个 {label, color} 二元组,将连续的数值映射为离散的三档等级与对应颜色:≥0.8 为高相关(家园蓝)、≥0.5 为中相关(青绿)、<0.5 为低相关(警示橙)。这一映射策略的精妙之处在于,它没有采用等分三分法(0.33/0.66 分界),而是以 0.8 和 0.5 为阈值——0.8 是一个偏高的门槛,意味着只有真正高度匹配的搜索结果才会被标蓝;0.5 作为中低分界,让"勉强相关"的结果以青绿呈现而非直接归为低相关;低于 0.5 才以橙色警示,提示用户"这条结果可能不是你要的"。

在房产搜索场景中,这种分级策略尤其关键。以"两居室"关键字为例,searchByText 可能返回"望京西园三区 两居室"(reliability 0.94,高相关)、“通州新华西街 两居在售”(0.52,中相关)、“两居装修设计工作室”(0.28,低相关)、“合租两居次卧(非整租)”(0.09,低相关)四类结果。后两类虽然字面上含"两居",但语义上一个是装修公司、一个是合租次卧,都不是买方真正想要的整租两居室。reliabilityScore 函数将这种语义差距以颜色和标签直观呈现,使用户无需逐条点开结果详情即可在列表层完成第一轮筛选,大幅缩短了决策路径。

函数返回 ScoreLevel 接口类型而非匿名对象,同样是类型安全的设计——调用方 reliabilityScore(rec.reliability).label.color 都能得到 IDE 补全与编译期检查。值得注意的是,函数内部使用了"早返回"(early return)的写法而非嵌套 if-else,这在三档分级场景下既保证了可读性,又避免了深层嵌套的认知负担,是 ArkTS 中处理区间映射的推荐风格。

6.2 总价与面积颜色映射

function priceColor(total: number): string {
  if (total <= 350) {
    return COLORS.teal;
  }
  if (total <= 700) {
    return COLORS.blue;
  }
  return COLORS.orange;
}

function areaColor(area: number): string {
  if (area <= 60) {
    return COLORS.blue;
  }
  if (area <= 120) {
    return COLORS.teal;
  }
  return COLORS.orange;
}

priceColorareaColor 两个函数分别承担总价与面积的颜色映射,将业务规则与 UI 着色解耦。priceColor 以 350 万和 700 万为分界:≤350 万为青绿(低总价上车盘,正向鼓励色)、≤700 万为家园蓝(主流价位,中性主色)、>700 万为橙色(高端或超预算,警示色)。这一阈值设定贴合北京二手房市场的真实分层——350 万以内是刚需上车的典型预算区间,350-700 万是改善型主流区间,700 万以上则进入高端或核心地段区间。颜色不再是无意义的装饰,而是承载了"这套房是否在你的预算舒适区"的业务语义。

areaColor 则以 60㎡ 和 120㎡ 为分界,但颜色映射策略与 priceColor 有所不同:≤60㎡ 小户型为蓝色(紧凑型,强调稀缺与高单价)、≤120㎡ 主流面积为青绿(最常见区间,中性正向)、>120㎡ 大户型为橙色(大面积高总价,需注意总价压力)。两个函数虽然结构相似,但颜色档位的语义不同——priceColor 的青绿代表"低总价友好",areaColor 的青绿代表"主流面积舒适",同一颜色在不同维度下承载不同含义,这正是集中化色板 COLORS 的价值所在:颜色种类有限但语义多维复用,避免引入过多色相导致视觉混乱。

将这两类映射逻辑以独立函数的形式前置声明,而非散落在 Builder 内联三元表达式里,使得后续修改阈值(如某城市房价整体下移时调整 350 万为 250 万)只需改一处函数体,所有引用该函数的房源卡片总价与面积颜色都会同步更新。这是"规则集中化"带来的可维护性红利,也是 ArkUI 声明式 UI 在大型业务页面中保持代码整洁的关键实践。

七、数据模型:Observed 类与状态驱动

7.1 HouseItem 房源条目模型

@Observed export class HouseItem {
  icon: string;    // 房源 emoji 图标
  name: string;    // 小区名
  layout: string;  // 户型
  area: number;    // 建筑面积(㎡)
  total: number;   // 总价(万)
  unit: number;    // 单价(万/㎡)
  note: string;    // 用户备注(可编辑)

  constructor(icon: string, name: string, layout: string, area: number,
    total: number, unit: number, note: string) {
    this.icon = icon;
    this.name = name;
    this.layout = layout;
    this.area = area;
    this.total = total;
    this.unit = unit;
    this.note = note;
  }
}

const HOUSE_LIST: Array<HouseItem> = [
  new HouseItem('🏢', '望京西园三区', '2室1厅', 89, 560, 6.3, '满五唯一·南北通透'),
  new HouseItem('🏠', '芍药居北里', '3室1厅', 112, 720, 6.4, '近地铁 10 号线'),
  new HouseItem('🏘', '方庄芳古园', '2室1厅', 76, 430, 5.7, '电梯房·中层采光好'),
  new HouseItem('🏬', '金融街丰汇园', '1室1厅', 58, 610, 10.5, '学区房·配套成熟'),
  new HouseItem('🏡', '广安门外椿树馆', '3室2厅', 128, 690, 5.4, '业主自住·看房方便'),
  new HouseItem('🏢', '东直门当代MOMA', '2室2厅', 96, 830, 8.6, '次新房·物业费 3.8 元'),
  new HouseItem('🏘', '崇文门新景家园', '1室0厅', 44, 310, 7.0, '满二·低总价上车盘')
];

HouseItem 类被 @Observed 装饰器修饰,这是 ArkUI V2 状态管理库提供的类装饰器,它使该类的实例属性变化能够被 @State 修饰的数组感知并触发 UI 刷新。在房产找房场景中,note 字段是用户可编辑的备注(如"满五唯一·南北通透"“近地铁 10 号线”),当用户通过编辑弹窗修改某条房源的备注后,@Observed 机制确保列表中对应卡片的备注文本自动刷新,无需手动调用 forceUpdate 或重建整个列表。这一机制是 ArkUI 区别于传统命令式框架的核心能力之一,它让"数据变更→视图刷新"的链路在框架层自动完成。

HOUSE_LIST 常量数组沉淀了 7 条北京城区二手房 Mock 数据,覆盖了望京、芍药居、方庄、金融街、广安门外、东直门、崇文门 7 个典型片区,户型从 1室0厅到 3室2厅,面积从 44㎡ 到 128㎡,总价从 310 万到 830 万,单价从 5.4 万到 10.5 万/㎡,备注信息涵盖满五唯一、近地铁、电梯房、学区房、业主自住、次新房、满二等核心交易要素。每条数据的备注都不是随机编造,而是浓缩了该房源的核心卖点或交易关键点——如金融街丰汇园的"学区房·配套成熟"直接点明其高单价(10.5 万/㎡)的支撑因素,崇文门新景家园的"满二·低总价上车盘"则同时传递了产权属性(满二)与价格属性(低总价上车)。这种"数据即业务文档"的建模方式,使 Mock 数据本身就具备产品演示与开发联调的双重价值。

7.2 SearchRecord 搜索结果模型(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.reliability = reliability;
    this.distance = distance;
    this.time = time;
  }
}

const SEARCH_RECORDS: Array<SearchRecord> = [
  new SearchRecord('望京西园三区 两居室', '北京市朝阳区望京西园三区 308 楼', 650, 0.94, '刚刚'),
  new SearchRecord('芍药居北里 两居室', '北京市朝阳区芍药居北里 203 楼', 1240, 0.86, '刚刚'),
  new SearchRecord('方庄芳古园 两居室', '北京市丰台区方庄芳古园一区 5 楼', 1980, 0.69, '3 分钟前'),
  new SearchRecord('通州新华西街 两居在售', '北京市通州区新华西街 58 号院', 4600, 0.52, '6 分钟前'),
  new SearchRecord('两居装修设计工作室', '北京市海淀区中关村大街 27 号', 5300, 0.28, '10 分钟前'),
  new SearchRecord('合租两居次卧(非整租)', '北京市昌平区回龙观西大街 36 号', 8100, 0.09, '15 分钟前')
];

SearchRecord 类是 Map Kit 6.1.1 新增 reliability 字段在应用层的数据载体。它的 5 个字段中,nameaddressdistance 分别映射 site.SitenameformatAddressdistance 三个传统字段,而 reliability 字段则直接映射 site.Site.reliability 这一 6.1.1 新增字段,是本应用"特性一"的数据核心。time 字段则是应用层补充的"记录时间文案",用于在列表中展示"刚刚""3 分钟前"等相对时间,增强搜索结果的时效感。

SEARCH_RECORDS 常量数组沉淀了 6 条 Mock 搜索结果,其 reliability 值精心覆盖了高/中/低三档:0.94 和 0.86 为高相关(望京、芍药居的两居室),0.69 和 0.52 为中相关(方庄的两居室、通州的在售),0.28 和 0.09 为低相关(装修工作室、合租次卧)。这种"刻意覆盖三档"的 Mock 设计,使得搜索 Tab 在未接入真实 searchByText 接口时,也能完整演示 reliability 字段从高分到低分的全谱系可视化效果。当真实接口接入后,只需将 runSearch 方法返回的 sites 数组映射为 SearchRecord 数组替换 searchRecords 状态,UI 即可自动刷新,无需修改任何视图代码——这正是 @Observed + @State 数据驱动的工程价值。

7.3 EventLog 长按事件日志模型

@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;
  }
}

const EVENT_LOGS: Array<EventLog> = [
  new EventLog('POI', '故宫博物院', 39.9163, 116.3972, '演示事件'),
  new EventLog('Marker', '#0', 39.9149, 116.4115, '演示事件')
];

EventLog 类是 Map Kit 6.1.1 新增 onMarkerLongClick / onPoiLongClick 长按事件在应用层的数据载体。type 字段以字符串 'Marker''POI' 区分事件来源,name 字段在 Marker 事件中存储 marker.getId() 返回的 ID(如 #0),在 POI 事件中存储 poi.name(如"故宫博物院"),lat/lng 存储事件发生时对应的经纬度坐标,time 存储事件时间文案。这一统一模型将两类异构的长按事件(参数类型分别为 map.MarkermapCommon.Poi)抽象为同构的日志条目,使得日志流 UI 可以用同一个 ForEach 渲染,无需为两类事件编写两套列表代码。

EVENT_LOGS 常量预置了 2 条演示日志,一条 POI 类型(故宫博物院)、一条 Marker 类型(#0),用于在用户尚未触发任何长按手势时展示日志流的形态。当用户在地图上长按某个 Marker 或 POI 时,onMarkerLongClickonPoiLongClick 回调会构造一条新的 EventLog 并通过 this.eventLogs.unshift(...) 置顶插入数组,由于 eventLogs@State 修饰且 EventLog@Observed 修饰,新日志会立即在日志流顶部呈现。这种"事件→数据→视图"的即时链路,正是 Map Kit 6.1.1 长按事件新接口在 ArkUI 体系内的最佳实践形态。

八、组件主体:状态声明与 Map Kit 能力接入

8.1 Page1133 组件定义与状态变量

@Entry
@Component
struct Page1133 {
  @State currentTab: number = 0;
  @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 houseList: Array<HouseItem> = HOUSE_LIST;
  @State funcList: FuncItem[] = FUNC_LIST;

Page1133 是应用的唯一入口组件,被 @Entry@Component 双装饰器修饰。@Entry 标记该组件为页面入口,@Component 声明这是一个 ArkUI 自定义组件。组件内部首先声明了 8 个 @State 状态变量,覆盖 Tab 切换(currentTab)、筛选选中(cateIdx)、三层弹窗开关(addModal/editModal/delModal)、编辑与删除索引(editIdx/delIdx)、房源列表数据(houseList)、功能清单数据(funcList)。这些状态变量构成了应用全部的 UI 驱动源——任何一个状态的变化都会自动触发依赖该状态的 Builder 重新构建。

值得注意的设计细节是三个弹窗开关 addModal/editModal/delModal 各自独立,而非合并为一个 modalType 枚举。这种"一字段控一弹窗"的写法虽然状态变量更多,但避免了"打开 A 弹窗时需先关闭 B 弹窗"的互斥逻辑,且每个弹窗的显隐可以直接以 if (this.addModal) 简洁判断,可读性远优于 if (this.modalType === 'add')editIdxdelIdx 作为独立的索引状态,分别在编辑与删除弹窗打开时赋值,确保两类操作互不干扰。这种"宁可多状态、不要复杂互斥"的态度,在 ArkUI 中型业务页面中是一种务实的工程选择。

8.2 Map Kit 状态与控制器声明

  private mapOptions: mapCommon.MapOptions = {
    position: { target: CITY_CENTER, zoom: 13 }
  };
  private mapCallback?: AsyncCallback<map.MapComponentController>;
  private mapController?: map.MapComponentController;
  private mapEventManager?: map.MapEventManager;
  @State markerListenOn: boolean = true;
  @State poiListenOn: boolean = true;
  @State eventLogs: Array<EventLog> = EVENT_LOGS;
  @State queryInput: string = '两居室';
  @State searchState: string = '待搜索 · 演示数据';
  @State searchRecords: Array<SearchRecord> = SEARCH_RECORDS;
  @State formName: string = '';
  @State formLayout: string = '';
  @State formAddr: string = '';
  @State editNote: string = '';

这一段声明了 Map Kit 能力接入所需的全部状态与控制器引用。mapOptionsMapComponent 的初始化参数,以 mapCommon.MapOptions 类型声明,position.target 设为 CITY_CENTER 常量、zoom 设为 13(城市级视野的典型缩放级别),并直接给出默认值而非可选 ?——这一选择确保 MapComponent 在首帧渲染时不会收到 undefined 参数,避免组件参数传递异常。

mapCallbackmapControllermapEventManager 三者以 private + 可选 ? 修饰,因为它们在组件构造时还未就绪,需要等待 MapComponent 异步初始化完成后在回调中赋值。三者的获取链路是:mapCallback(回调函数)→ 回调内拿到 mapController(控制器)→ mapController.getEventManager() 拿到 mapEventManager(事件管理器)。这一链路体现了 Map Kit "组件→控制器→管理器"的三段式设计,每一段都依赖前一段就绪,因此必须在回调的 err 为空分支内顺序完成。

markerListenOnpoiListenOn 两个 @State 布尔值分别控制 Marker 长按与 POI 长按监听的开关状态,初始为 true(默认开启)。它们被 @State 修饰是因为 Toggle 开关的 UI 状态需要随用户操作实时反馈——当用户切换开关时,onChange 触发 toggleMarkerListen / togglePoiListen 方法,方法内既调用 off / on 注销或重新注册监听,又翻转 @State 值以刷新 Toggle 的视觉状态。eventLogs 作为长按事件日志流的数据源被 @State 修饰,确保每次 unshift 新日志后日志流 UI 即时刷新。queryInputsearchStatesearchRecords 三者服务于搜索 Tab,分别承载关键字输入、搜索状态文案、搜索结果列表。formName/formLayout/formAddr/editNote 四个表单状态服务于收藏与编辑弹窗的双向绑定。

九、地图初始化回调:6.1.1 双长按监听注册

9.1 setupMapCallback 全链路初始化

  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();
      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}`);
        }
      }
      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.mapEventManager.onPoiLongClick((poi: mapCommon.Poi) => {
        this.eventLogs.unshift(new EventLog('POI', poi.name,
          poi.position.latitude, poi.position.longitude, '刚刚'));
      });
    };
  }

setupMapCallback 方法是整个 Map Kit 能力链路的初始化入口,它构造一个 AsyncCallback<map.MapComponentController> 类型的异步回调函数并赋值给 this.mapCallback,该回调会在 MapComponent 组件首帧渲染后被框架调用。回调的第一件事是错误处理——若 err 非空,说明地图初始化失败(可能因设备无 Google Services、AGC 配置缺失、网络异常等),此时仅打印错误日志并 return,不执行后续控制器与监听注册逻辑,避免在未就绪的控制器上调用方法导致二次异常。这一"先判错、再取控制器"的防御性写法,是异步回调中保障链路稳健的基础实践。

错误分支为空后,回调依次完成三件事:将 mapController 赋值给 this.mapController 供后续方法使用;调用 mapController.getEventManager() 获取事件管理器赋值给 this.mapEventManager;遍历 MARKER_SPOTS 数组,为每个小区房源标注点构造 mapCommon.MarkerOptionsawait this.mapController.addMarker(...) 批量打点。MarkerOptionsclickable: true 使标记可点击(长按手势的前提)、visible: true 使标记默认可见、anchorU: 0.5anchorV: 1 将标记锚点设为底部中心(即标记图标的"针尖"对齐经纬度坐标),这些参数共同确保标记在地图上的视觉与交互行为符合房产找房的直觉预期。

批量打点完成后,回调注册了 Map Kit 6.1.1 的两个新增长按监听。onMarkerLongClick 接收一个回调函数,参数为 map.Marker 类型,当用户长按地图上的某个 Marker 时触发,回调内通过 marker.getPosition() 拿到标记当前经纬度、marker.getId() 拿到标记 ID,构造一条 EventLogunshift 到日志流顶部。onPoiLongClick 同理,参数为 mapCommon.Poi 类型,回调内直接读取 poi.namepoi.position 构造日志。这两个接口是 HarmonyOS 6.1.1 版本新增能力,在此之前的 Map Kit 只支持 Marker 的点击(onMarkerClick)而非长按,POI 层面则缺乏原生长按手势支持,开发者往往需要通过自定义浮层或叠加透明组件模拟长按,体验与性能均不理想。6.1.1 的这组新接口使长按手势真正下沉到地图原生层,手势识别精度与响应速度都得到质的提升。

9.2 长按监听开关切换

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

  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 长按监听的开关注换。两个方法的逻辑结构完全对称:首先以 if (!this.mapEventManager) return 防御事件管理器未就绪的情况(如地图初始化失败时 mapEventManager 仍为 undefined),避免在未就绪的管理器上调用方法抛出空引用异常。随后根据当前 markerListenOn / poiListenOn 状态决定操作——若当前为开启状态(true),则调用 offMarkerLongClick() / offPoiLongClick() 注销监听;若当前为关闭状态(false),则调用 onMarkerLongClick(cb) / onPoiLongClick(cb) 重新注册监听。最后翻转 @State 状态值以刷新 Toggle 开关的视觉。

这里的关键技术点是 offMarkerLongClick()offPoiLongClick() 的调用方式——不传任何参数。在 Map Kit 6.1.1 的设计中,off 系列接口不传参时表示清除该类型的全部订阅,传参时(若支持)则清除指定回调。本应用采用不传参的"全量清除"策略,因为每次重新注册时都传入了一个新的匿名回调函数,若试图用引用比对清除特定回调,由于匿名函数每次生成新引用,将无法匹配成功。因此"全量清除 + 全量重注册"是这类场景下最简洁可靠的开关实现方式。值得注意的是,重新注册时传入的回调逻辑与 setupMapCallback 中初始注册的完全一致,这虽然存在代码重复,但保证了开关切换前后的事件处理行为不变,是可接受的工程取舍。

十、搜索能力:searchByText 与 reliability 字段读取

10.1 runSearch 关键字搜索方法

  async runSearch() {
    this.searchState = '搜索中…';
    const params: site.SearchByTextParams = {
      query: this.queryInput,
      location: CITY_CENTER,
      radius: 5000,
      language: 'zh'
    };
    try {
      const result: site.SearchByTextResult = await site.searchByText(params);
      const sites: Array<site.Site> = result.sites ?? [];
      if (sites.length === 0) {
        this.searchState = '无结果 · 保留演示数据';
        return;
      }
      const records: Array<SearchRecord> = [];
      for (const s of sites) {
        records.push(new SearchRecord(
          s.name ?? '未命名地点',
          s.formatAddress ?? '暂无地址',
          s.distance ?? 0,
          s.reliability ?? 0,
          '刚刚'));
      }
      this.searchRecords = records;
      this.searchState = `返回 ${sites.length} 套房源`;
    } catch (e) {
      const err = e as BusinessError;
      this.searchState = `搜索失败(${err.code}) · 保留演示数据`;
    }
  }

runSearch 方法是 Map Kit 6.1.1 “特性一” reliability 字段的实际接入点。该方法被 async 修饰,首先将 searchState 状态设为"搜索中…“以在 UI 上反馈搜索正在进行,随后构造 site.SearchByTextParams 搜索参数对象。参数包含四个字段:query 为用户在搜索框输入的关键字(如"两居室”),location 为搜索基准点(复用 CITY_CENTER 常量),radius 为搜索半径(5000 米,覆盖城市级行政范围),language 为返回结果语言(‘zh’ 中文)。这四个参数共同界定了"以北京故宫为中心、5 公里半径内、中文返回、关键字为两居室"的搜索范围,使结果既不会过于发散(全城搜索返回过多无关结果),也不会过于狭窄(半径过小导致结果稀疏)。

try 块内调用 site.searchByText(params)await 等待异步结果,返回的 SearchByTextResult 对象的 sites 字段是 Array<site.Site> 类型。这里使用了 result.sites ?? [] 空值合并运算符兜底,防止 sites 字段为 undefinednull 时后续 for...of 遍历抛错。若 sites 数组长度为 0,则将搜索状态设为"无结果 · 保留演示数据"并 return,此时 searchRecords 状态不会被修改,UI 继续展示原有的 Mock 演示数据,保证搜索链路在无结果时仍可演示。这一设计体现了"演示数据兜底"的产品思维——在开发联调或无网络环境下,应用不会呈现空白列表,而是始终保留可交互的演示内容。

for...of 循环遍历 sites 数组,将每个 site.Site 对象映射为 SearchRecord 实例。映射过程中,s.names.formatAddresss.distance 三个传统字段均使用 ?? 兜底(分别为"未命名地点"“暂无地址”“0”),而 s.reliability 这一 6.1.1 新增字段同样使用 ?? 0 兜底——这是因为 reliability 虽然是 6.1.1 新增字段,但在某些返回路径下可能仍为 undefined(如服务端未填充或旧版本兼容场景),?? 0 确保任何情况下 reliability 都有一个合法的数值供后续 reliabilityScore 函数处理。映射完成后,records 数组被赋值给 this.searchRecords,由于该状态被 @State 修饰,搜索结果列表 UI 会自动刷新为真实接口返回的数据。catch 块捕获 BusinessError 异常(如未配置 AGC、无网络、配额超限等),将搜索状态设为"搜索失败(code) · 保留演示数据",同样保留 Mock 数据保证演示链路。这种"成功替换数据、失败保留演示"的容错策略,使搜索功能在任何环境下都不会让用户面对空白或报错页,是房产这类 C 端应用应有的鲁棒性设计。

十一、房源增删改:收藏、编辑与删除

11.1 编辑与收藏方法

  openEditHouse(idx: number) {
    this.editIdx = idx;
    this.editNote = this.houseList[idx].note;
    this.editModal = true;
  }

  saveHouse() {
    const name = this.formName === '' ? '安家找房·新收藏房源' : this.formName;
    const layout = this.formLayout === '' ? '2室1厅' : this.formLayout;
    const addr = this.formAddr === '' ? '北京市朝阳区(地图选点)' : this.formAddr;
    this.houseList.unshift(new HouseItem('⭐', name, layout, 80, 500, 6.2, addr));
    this.formName = '';
    this.formLayout = '';
    this.formAddr = '';
    this.addModal = false;
  }

  updateHouse() {
    if (this.editIdx >= 0 && this.editIdx < this.houseList.length) {
      if (this.editNote !== '') {
        this.houseList[this.editIdx].note = this.editNote;
      }
      this.houseList = this.houseList.slice();
    }
    this.editModal = false;
  }

  delHouse() {
    if (this.delIdx >= 0 && this.delIdx < this.houseList.length) {
      this.houseList.splice(this.delIdx, 1);
    }
    this.delModal = false;
  }

openEditHouse 方法在用户点击房源卡片的"编辑"按钮时被调用,接收房源索引 idx,将 editIdx 状态设为该索引、editNote 状态设为当前房源的备注文本(实现"回填"),最后将 editModal 设为 true 打开编辑弹窗。这种"先回填状态、再开弹窗"的顺序,确保弹窗打开时 TextInput 已经显示了当前备注,用户看到的是可编辑的当前值而非空白输入框。

saveHouse 方法处理收藏新房源的保存逻辑。它的核心设计是"空名兜底默认演示房源"——当用户未填写小区名、户型或地址时,分别以"安家找房·新收藏房源"“2室1厅”"北京市朝阳区(地图选点)"作为兜底值。这一设计使即使用户直接点击"收藏"按钮而不填写任何字段,也能生成一条合法的房源数据加入列表,保证演示流程的顺畅。新房源以 HouseItem('⭐', name, layout, 80, 500, 6.2, addr) 构造,其中 emoji 标识其为用户收藏房源(区别于系统推荐的 🏢/🏠/🏘 等),面积默认 80㎡、总价 500 万、单价 6.2 万/㎡ 是北京二手房的中位数水平,使新收藏房源在视觉与数据上都与推荐房源保持一致的呈现质量。unshift 将新房源插入列表顶部,符合"新收藏优先展示"的直觉。最后清空三个表单状态并关闭弹窗。

updateHouse 方法处理编辑备注的保存。首先以 if (this.editIdx >= 0 && this.editIdx < this.houseList.length) 边界检查防止索引越界,随后在 editNote 非空时将新备注写入对应房源的 note 字段。由于 HouseItem@Observed 修饰,直接修改 note 属性本应能触发 UI 刷新,但 ArkUI 对数组元素的属性修改有时需要整体刷新数组引用才能确保列表级 UI 重新构建,因此这里调用 this.houseList = this.houseList.slice() 创建一个新数组引用赋值回 this.houseList,强制触发依赖该状态的 ForEach 重新渲染。slice() 不传参等价于浅拷贝整个数组,元素引用仍指向原 HouseItem 实例,因此 @Observed 的属性级监听与数组级刷新两者协同生效。

delHouse 方法处理删除确认。同样先做边界检查,随后调用 splice(this.delIdx, 1) 从数组中移除指定索引的房源。splice 会原地修改数组并触发 @State 的变化检测,列表会自动移除对应卡片。最后关闭删除弹窗。这三个方法共同构成了房源列表的完整 CRUD 链路(Create=saveHouse、Read=tabHouse 列表渲染、Update=updateHouse、Delete=delHouse),是房产找房应用"我的房源"管理能力的最小完整集。

十二、生命周期与主构建

12.1 aboutToAppear 与 build

  aboutToAppear() {
    this.setupMapCallback();
  }

  build() {
    Stack() {
      Column() {
        this.headerMain()
        Divider().strokeWidth(1).color(COLORS.line)
        Scroll() {
          Column() {
            if (this.currentTab === 0) {
              this.tabHouse()
            } 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 首次执行前被框架调用。这里在 aboutToAppear 中调用 this.setupMapCallback() 初始化地图回调,确保 mapCallbackMapComponent 首帧渲染前就绑定就绪。这一时序非常关键——若在 build 中才赋值 mapCallback,可能因渲染时序导致 MapComponent 拿到 undefined 回调而无法触发初始化;而在 aboutToAppear 中提前赋值,则保证了 build 执行时 mapCallback 已是就绪的函数引用。

build 方法以 Stack 为根容器,其内嵌一个 Column 承载主内容(头部+分隔线+可滚动 Tab 内容+底部导航),并在 Stack 顶层叠加三个条件渲染的弹窗。Stack 的"层叠"特性使弹窗可以覆盖在主内容之上,形成"全屏遮罩+居中卡片"的经典弹窗视觉。主内容 Column 的结构是垂直排列:headerMain() 头部、Divider 分隔线、Scroll 可滚动区(内含根据 currentTab 条件渲染的 4 个 Tab Builder)、tabBar() 底部导航。Scroll 设置 layoutWeight(1) 占满头部与底部导航之间的剩余高度,scrollBar(BarState.Off) 隐藏滚动条保持视觉整洁。

Tab 内容的切换采用 if-else if-else 条件渲染而非 ForEach + visibility 隐藏方案,这意味着未选中的 Tab 内容不会被构建到组件树中,节省了内存与渲染开销,但代价是切换 Tab 时需要重新构建对应 Tab 的 Builder。对于房产找房这类"单时刻只关注一个 Tab"的场景,条件渲染是比 visibility 隐藏更高效的选择。三个弹窗的显隐分别由 addModal/editModal/delModal 三个 @State 布尔值控制,每个弹窗 Builder 接收一个 onClose 回调函数(() => { this.xxxModal = false; }),在弹窗内部点击遮罩或取消按钮时调用,实现弹窗的自关闭。这种"弹窗自管显隐、外层只传关闭回调"的设计,使弹窗组件具备良好的可复用性。

十三、头部 Builder:渐变 Banner 与筛选 chips

  @Builder
  headerMain() {
    Column({ space: 12 }) {
      Column({ space: 10 }) {
        Row({ space: 12 }) {
          Column({ space: 2 }) {
            Text('12').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('望京均价 5.8 万/㎡ · 环比 -0.6%').fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
            }
            Row({ space: 6 }) {
              Text('🆕').fontSize(12)
              Text('今日新上 36 套 · 降价 8 套').fontSize(11).fontColor(COLORS.sub)
            }
            Row({ space: 6 }) {
              Text('💰').fontSize(12)
              Text('预算 500 万 · 首付 3 成可贷 7 成').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]]
      })
      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 度角从 COLORS.blueL(浅家园蓝 #DCE8FB)到 COLORS.card(纯白 #FFFFFF)的渐变,渐变终点 0.65 意味着从顶部浅蓝过渡到 65% 位置时已变为纯白,下半部分保持纯白底。这种"上浅下白"的渐变既保留了品牌蓝的视觉印记,又避免了纯色蓝底过于抢眼影响下方信息阅读,是房产应用头部 Banner 的雅致处理方式。

Banner 内部左侧是"在看房源数"大数字(12,30px 粗体深墨蓝)+ 小标签(在看房源,10px 灰蓝)的纵向排列,右侧是三条楼市动态信息行:望京均价与环比、今日新上与降价套数、预算与首付成数。这三条信息的选择并非随意——均价与环比反映市场趋势,新上与降价反映实时供给,预算与首付反映用户购买力,三者共同构成用户在找房产最关心的"市场-供给-自身购买力"三角信息。Banner 底部是两个快捷入口胶囊:“预约看房”(家园蓝底白字,主操作)与"小区地图"(白底蓝字,次操作,点击切换到地图 Tab),两者以 justifyContent(FlexAlign.SpaceBetween) 两端对齐,形成清晰的行动召唤层级。

横滑筛选 chips 区以 Scroll + scrollable(ScrollDirection.Horizontal) 实现横向滚动,内部 Row 通过 ForEach(CATE_TAGS, ...) 渲染 8 个筛选标签。每个标签的 fontColorbackgroundColor 根据 this.cateIdx === idx 三元判断:选中时为白底蓝字(COLORS.bg 底 + COLORS.blue 字),未选中时为浅蓝底灰蓝字(COLORS.chip 底 + COLORS.sub 字)。点击任一标签时 this.cateIdx = idx 切换选中状态,由于 cateIdx@State 修饰,所有标签的颜色会即时刷新。ForEach 的第三个参数 (tag: string) => tag 是键值生成器,以标签文案作为唯一键,确保数组变化时 ArkUI 能正确复用 DOM 节点而非全量重建。

十四、房源 Tab:带看统计与房源列表

14.1 statCell 通用统计单元格

  @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)
  }

statCell 是一个接收 valuelabel 两个参数的通用 @Builder 函数,用于渲染"大数值+小标签"的统计单元格。它被设计为可复用组件——在房源 Tab 中渲染"本月带看/今日新上/近7天降价"三宫格,在"我的"Tab 中渲染"累计带看/收藏房源/在管委托"三宫格,两处复用同一 Builder 保证了统计单元格的视觉一致性。layoutWeight(1) 使单元格在父 Row 中等分宽度,三个 statCell 并列即形成三宫格布局。backgroundColor(COLORS.card) 纯白底与 borderRadius(10) 圆角使单元格在浅蓝底色页面上形成"白卡"视觉,数值以家园蓝粗体 17px 突出,标签以灰蓝 10px 退后,形成清晰的"数据-说明"层级。

ArkUI 的 @Builder 函数支持参数传递,这使得"通用单元格"这类高频复用的 UI 片段可以像函数一样被调用,避免了在多个 Tab 中复制粘贴相同的布局代码。当未来需要调整统计单元格的视觉(如改字号、改颜色)时,只需修改 statCell 一处,所有引用它的 Tab 都会同步更新,这是声明式 UI 中"组件化复用"的基本功。

14.2 tabHouse 房源主 Tab

  @Builder
  tabHouse() {
    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('12 次', '本月带看')
        this.statCell('36 套', '今日新上')
        this.statCell('8 套', '近7天降价')
      }
      .width('100%')
      ForEach(HOUSE_RECS, (rec: HouseRec) => {
        Row({ space: 10 }) {
          Text(rec.icon).fontSize(26)
          Column({ space: 4 }) {
            Text(rec.name).fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
            Text(`${rec.layout} · ${rec.unit.toFixed(1)}万/㎡`).fontSize(11).fontColor(COLORS.sub)
            Row({ space: 6 }) {
              Text(rec.dist).fontSize(10).fontColor(COLORS.text3)
              Text(`总价 ${rec.total}`).fontSize(10).fontColor(priceColor(rec.total))
            }
          }
          .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: HouseRec) => rec.name)
      Row() {
        Text('全部房源(地图 Marker 同源)').fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
      }
      .width('100%')
      ForEach(this.houseList, (house: HouseItem, idx: number) => {
        Column({ space: 8 }) {
          Row({ space: 10 }) {
            Text(house.icon).fontSize(22)
            Column({ space: 3 }) {
              Text(house.name).fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
              Text(`${house.layout} · ${house.area}㎡ · ${house.unit.toFixed(1)}万/㎡`).fontSize(11).fontColor(COLORS.sub)
              Text(`备注:${house.note}`).fontSize(10).fontColor(COLORS.text3)
            }
            .alignItems(HorizontalAlign.Start)
            .layoutWeight(1)
            Column({ space: 6 }) {
              Text(`${house.total}`).fontSize(16).fontWeight(FontWeight.Bold)
                .fontColor(priceColor(house.total))
              Text('总价(万)').fontSize(9).fontColor(COLORS.text3)
            }
          }
          .width('100%')
          Row({ space: 8 }) {
            Text(`${house.area}`).fontSize(10).fontColor(areaColor(house.area))
              .padding({ left: 10, right: 10, top: 4, bottom: 4 })
              .borderRadius(10).backgroundColor(COLORS.chip)
            Blank()
            Text('编辑').fontSize(10).fontColor(COLORS.teal)
              .padding({ left: 10, right: 10, top: 4, bottom: 4 })
              .borderRadius(10).backgroundColor(COLORS.chip)
              .onClick(() => { this.openEditHouse(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%')
      }, (house: HouseItem) => house.name)
      this.tipsCard()
    }
    .width('100%')
  }

tabHouse 是房源主 Tab 的完整构建,结构上分为五个区块:标题行、带看数据三宫格、精选房源大卡列表、全部房源列表、看房小贴士卡。标题行以 Row + Blank() 实现"左标题+右入口"的两端对齐——Blank() 是 ArkUI 提供的弹性空白组件,自动占据剩余空间将两侧元素推开,实现 justifyContent(FlexAlign.SpaceBetween) 的等价效果但更灵活。"收藏新房源 +"文本点击时 this.addModal = true 打开收藏弹窗,+ 符号暗示这是一个"添加"操作,符合移动端交互惯例。

带看数据三宫格复用 statCell,三个数据"本月带看 12 次"“今日新上 36 套”"近7天降价 8 套"分别对应用户的带看活跃度、市场供给量、价格波动信号,是房产用户判断"市场温度"的核心指标。精选房源大卡列表以 ForEach(HOUSE_RECS, ...) 渲染 3 条精选房源,每张卡片左侧 emoji 图标、中部小区名+户型+单价+距离+总价(总价以 priceColor(rec.total) 着色)、右侧"看房"按钮(点击切换到地图 Tab)。这里的"看房"按钮采用家园蓝底白字胶囊,是卡片的主行动召唤,而总价文本以 priceColor 动态着色(如 560 万为蓝色、430 万为青绿、720 万为橙色),使用户在浏览列表时能快速识别哪些房源在自己总价舒适区。

全部房源列表以 ForEach(this.houseList, ...) 渲染 HOUSE_LIST 的 7 条数据,每张卡片的信息密度更高:左侧 emoji、中部小区名+户型+面积+单价+备注、右侧总价(priceColor 着色)+总价标签、底部面积胶囊(areaColor 着色)+编辑按钮+删除按钮。编辑按钮点击调用 this.openEditHouse(idx) 打开编辑弹窗,删除按钮点击设置 this.delIdx = idx 并打开删除确认弹窗。ForEach 的第二个参数 (house: HouseItem, idx: number) 同时拿到元素与索引,索引被传递给编辑与删除方法以定位操作目标。列表末尾的 this.tipsCard() 调用看房小贴士卡,为列表提供一个知识性收尾。

14.3 tipsCard 看房小贴士

  @Builder
  tipsCard() {
    Column({ space: 6 }) {
      Text('💡 看房小贴士').fontSize(12).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
      Text('· 满五唯一可省个税,签约前务必核验房本年限与抵押状态')
        .fontSize(10).fontColor(COLORS.sub).width('100%')
      Text('· 周二至周四看房人少,可与业主就价格充分沟通')
        .fontSize(10).fontColor(COLORS.sub).width('100%')
    }
    .padding(12)
    .borderRadius(12)
    .backgroundColor(COLORS.chip)
    .width('100%')
    .alignItems(HorizontalAlign.Start)
  }

tipsCard 是房源列表底部的看房小贴士卡,采用 COLORS.chip 浅蓝底而非纯白底,使其在白色卡片列表中形成视觉区分,暗示这是"知识性内容"而非"房源数据"。两条贴士内容分别涉及交易关键点(满五一个税、房本年限、抵押状态)与看房时机(周二至周四看房人少),都是真实二手房交易中的实操经验。alignItems(HorizontalAlign.Start) 使文本左对齐,符合贴士列表的阅读习惯。虽然只是两张小卡片,但其内容选材体现了房产垂直领域的业务深度——这不是泛泛的"找房源"应用,而是融入了真实交易智慧的垂直工具。

十五、地图 Tab:MapComponent 与长按事件日志流

  @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%')
      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({ 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.teal)
                    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 本体、长按事件日志流。特性说明卡以浅蓝底卡片简要介绍"长按地图上的小区房源 Marker 或 POI 地点,事件将记录到下方日志流”,为用户提供操作指引。监听开关行包含两个 Toggle 开关,分别控制 Marker 长按与 POI 长按监听的启用状态,selectedColor(COLORS.blue) 使开关激活时呈家园蓝,onChange 回调调用 toggleMarkerListen / togglePoiListen 方法执行实际的 off / on 操作。

MapComponent({ mapOptions: this.mapOptions, mapCallback: this.mapCallback }) 是 Map Kit 的核心组件实例化,接收两个参数:mapOptions 指定地图初始中心点(北京故宫)与缩放级别(13),mapCallback 指定初始化完成后的异步回调(在 setupMapCallback 中赋值)。layoutWeight(1) 使地图占据开关行与日志流之间的全部剩余高度,borderRadius(12) 为地图添加圆角,使其与卡片化整体视觉风格统一。MapComponent 作为 ArkUI 声明式组件的一员,可以像普通组件一样被嵌入 Column/Row/Stack 等容器,与业务 UI 无缝共存,这是 Map Kit 设计上的重要优势。

长按事件日志流以固定高度 120px 的 Scroll 容器呈现,内部 ForEach(this.eventLogs, ...) 渲染每条日志。每条日志卡片的左侧 emoji 根据 log.type 区分(Marker 为 📍、POI 为 🏠),中部三行信息:类型标签(Marker 蓝色/POI 青绿)+名称+时间、经纬度坐标(monospace 字体保持等宽对齐)。日志以 unshift 置顶插入,最新事件始终在顶部,符合"最新优先"的日志阅读直觉。ForEach 键值生成器 ${log.type}-${log.name}-${log.time} 以三字段组合作为唯一键,确保相同类型与名称但不同时间的日志不会被误判为同一条而复用。当日志数量超过 120px 高度时,Scroll 自动启用纵向滚动,scrollBar(BarState.Off) 隐藏滚动条保持视觉整洁。这一日志流是 Map Kit 6.1.1 长按事件新接口在 UI 层的"可视化证据",用户每长按一次地图点位,即可看到一条新日志即时出现在顶部,形成"操作-反馈"的闭环体验。

十六、搜索 Tab: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%')
      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 })
              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 是 Map Kit 6.1.1 “特性一” reliability 字段的主展示页,结构上分为五个区块:特性说明卡、搜索框+触发按钮、搜索状态文案、搜索结果列表、代码预览卡。特性说明卡以浅蓝底卡片介绍 Site 新增 reliability 字段([0,1],1 为完全相关),衡量结果与关键字关联程度,点明本页的技术核心。搜索框 TextInput 与"搜索"按钮以 Row 并列,TextInput 通过 layoutWeight(1) 占满按钮左侧的剩余宽度,onChange 回调将输入值同步到 queryInput 状态实现双向绑定,“搜索"按钮点击调用 this.runSearch() 触发实际的 site.searchByText 调用。搜索状态文案 Text(this.searchState) 展示"待搜索 · 演示数据”“搜索中…”“返回 N 套房源”"搜索失败(code) · 保留演示数据"等动态状态,使用户始终了解搜索链路的当前进度。

搜索结果列表以 List({ space: 8 }) + ForEach(this.searchRecords, ...) 渲染,每条结果以 ListItem 包裹。这里值得注意的技术细节是 List 的子项必须用 ListItem 包裹——ArkUI 的 List 容器要求直接子元素为 ListItemListItemGroup,这与 Column + ForEach 直接渲染任意组件的写法不同。若在 List 内直接放 Column 而非 ListItem,将导致运行时警告或布局异常。每条搜索结果卡片的信息分四层:顶部行(名称+等级标签)、地址行、reliability 分数条行、底部信息行(距离+时间)。

reliability 分数条是本页的视觉核心。Progress({ value: rec.reliability * 100, total: 100, type: ProgressType.Linear })[0, 1] 区间的 reliability 值乘以 100 映射为 0~100 的进度值,以线性进度条形式呈现。进度条 color(reliabilityScore(rec.reliability).color) 根据分数档位动态着色——高相关(≥0.8)为家园蓝、中相关(≥0.5)为青绿、低相关(<0.5)为警示橙,使用户在浏览列表时能以颜色快速识别每条结果的相关度。进度条右侧的 reliability ${rec.reliability.toFixed(2)} 文本以 monospace 等宽字体展示两位小数,与进度条形成"图形+数值"的双重表达。等级标签 reliabilityScore(rec.reliability).label 以小胶囊形式出现在名称右侧,是分数档位的文字凝练。这种"分数条+数值+标签"的三重可视化,使 reliability 这一抽象的数值字段被转化为用户可直观理解的视觉语言,是 Map Kit 6.1.1 新字段在 UI 层的最佳实践形态。

名称与地址文本都设置了 maxLines(1)textOverflow({ overflow: TextOverflow.Ellipsis }),确保长文本单行显示并在溢出时以省略号截断,避免某条结果因名称过长而撑破卡片布局。ForEach 键值生成器 ${rec.name}-${rec.reliability} 以名称与分数组合作为唯一键,由于同一条结果的名称与分数在数据更新时通常同步变化,这一键值能正确区分新旧数据。列表末尾的 this.codePreviewCard() 调用代码预览卡,为搜索页提供一个技术点呼应的收尾。

十七、我的 Tab:VIP 卡与功能清单

  @Builder
  tabMine() {
    Column({ space: 10 }) {
      Column({ space: 8 }) {
        Row({ space: 12 }) {
          Text('🏠').fontSize(34)
          Column({ space: 3 }) {
            Text('安家 VIP · 白金服务').fontSize(15).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
            Text('专属带看 · 房源降价 30 分钟内提醒').fontSize(11).fontColor(COLORS.sub)
          }
          .alignItems(HorizontalAlign.Start)
          .layoutWeight(1)
        }
        .width('100%')
        Divider().strokeWidth(1).color(COLORS.line)
        Row() {
          Text('本月看房 6 次').fontSize(11).fontColor(COLORS.sub)
          Blank()
          Text('已省中介费 8000 元').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('12 次', '累计带看')
        this.statCell('8 套', '收藏房源')
        this.statCell('1 个', '在管委托')
      }
      .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.8.0 · Map Kit 6.1.1 双新特性演示').fontSize(9).fontColor(COLORS.text3)
    }
    .width('100%')
  }

tabMine 是"我的"Tab 的完整构建,结构上分为四个区块:VIP 渐变大卡、数据三宫格、功能清单、版本脚注。VIP 渐变大卡采用与头部 Banner 相同的 135 度 linearGradient(从 COLORS.blueLCOLORS.card),但渐变终点设为 0.72(比头部的 0.65 略长),使浅蓝区域占比更大,视觉上更"高级"。卡片内部分为上下两部分:上部是 🏠 emoji + VIP 标题 + 副标题(专属带看·降价 30 分钟内提醒),下部以 Divider 分隔后展示"本月看房 6 次"+"已省中介费 8000 元"两段文案,后者以家园蓝突出"省钱"这一 VIP 核心价值主张。这种"品牌色渐变+价值主张文案"的组合,是会员卡视觉的典型范式。

数据三宫格复用 statCell,三个数据"累计带看 12 次"“收藏房源 8 套”"在管委托 1 个"分别相应用户的活跃度、收藏量、委托量,是用户画像的三个核心维度。功能清单以 ForEach(this.funcList, ...) 渲染 8 条 FUNC_LIST 数据,每条以 Row 布局:左侧 emoji 图标、中部功能名(layoutWeight(1) 占满中间)+右侧状态/数值+最右 箭头。 箭头是移动端"可点击进入详情"的视觉暗示,虽然本应用未实际绑定点击跳转,但其存在使功能清单具备了完整的"可交互列表"视觉语义。版本脚注 Text('安家找房 v2.8.0 · Map Kit 6.1.1 双新特性演示') 以 9px 弱文本色展示版本号与技术特性来源,既是对应用元信息的声明,也是对"本应用演示 Map Kit 6.1.1 双新特性"这一技术意图的明示。

十八、代码预览卡与底部导航

18.1 codePreviewCard 双特性代码预览

  @Builder
  codePreviewCard() {
    Column({ space: 6 }) {
      Text('⌨️ Map Kit 6.1.1 双新特性调用').fontSize(12).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
      Column({ space: 4 }) {
        Text('const res = await site.searchByText(params)')
          .fontSize(9).fontColor(COLORS.blue).fontFamily('monospace')
        Text('res.sites[0].reliability // 0~1 相关性分')
          .fontSize(9).fontColor(COLORS.blue).fontFamily('monospace')
        Text('manager.onMarkerLongClick(cb) // 24+')
          .fontSize(9).fontColor(COLORS.teal).fontFamily('monospace')
        Text('manager.offMarkerLongClick() // 注销监听')
          .fontSize(9).fontColor(COLORS.teal).fontFamily('monospace')
      }
      .padding(10)
      .borderRadius(8)
      .backgroundColor(COLORS.codeBg)
      .width('100%')
    }
    .padding(10)
    .borderRadius(10)
    .backgroundColor(COLORS.chip)
    .width('100%')
  }

codePreviewCard 是一个"技术点展示卡",以深色底(COLORS.codeBg#16233A)+ monospace 等宽字体展示 Map Kit 6.1.1 双新特性的核心调用代码。四行代码分为两组:前两行蓝色字展示"特性一" site.searchByText + reliability 字段读取,后两行青绿字展示"特性二" onMarkerLongClick / offMarkerLongClick 长按监听。颜色分组使两类特性在视觉上可被快速区分。在浅色主题中刻意保留深底代码卡,是因为代码文本在深底浅字下的对比度与可读性远优于浅底深字——这是一种基于"内容可读性优先于主题统一性"的局部取舍,在前文颜色系统章节已有阐述。

外层以 COLORS.chip 浅蓝底 + borderRadius(10) 圆角包裹深底代码区,形成"浅蓝框+深蓝底+浅字"的三层视觉,既有层次感又不破坏整体浅色主题。这种"在产品 UI 中嵌入代码片段"的设计,常见于技术演示类应用,使非开发者用户也能直观感知到"这个功能背后用了什么技术",在房产这类面向 C 端但需展示技术实力的场景中,是一种兼顾产品体验与技术品牌的表达方式。

18.2 tabBar 底部导航

  @Builder
  tabBar() {
    Row() {
      ForEach(TAB_LIST, (tab: TabMeta, idx: number) => {
        Column({ space: 3 }) {
          Text(tab.icon).fontSize(18)
          Text(tab.label).fontSize(10)
            .fontColor(this.currentTab === idx ? COLORS.tabOn : COLORS.text3)
        }
        .layoutWeight(1)
        .onClick(() => { this.currentTab = idx; })
      }, (tab: TabMeta) => tab.label)
    }
    .padding({ top: 8, bottom: 8 })
    .width('100%')
    .backgroundColor(COLORS.card)
  }

tabBar 是底部 4 Tab 导航栏,以 Row + ForEach(TAB_LIST, ...) 渲染 4 个 Tab 项。每个 Tab 项以 Column 纵向排列 emoji 图标(18px)与标签文案(10px),layoutWeight(1) 使 4 个 Tab 等分宽度。标签 fontColor 根据 this.currentTab === idx 三元判断:选中时为 COLORS.tabOn(家园蓝 #2563EB)、未选中时为 COLORS.text3(浅蓝灰 #93A5BF),形成清晰的"激活-非激活"视觉对比。点击 Tab 项时 this.currentTab = idx 切换状态,由于该状态被 @State 修饰,主内容区的条件渲染会即时切换到对应 Tab 的 Builder,同时底部导航的标签颜色也会即时刷新。

ForEach 键值生成器 (tab: TabMeta) => tab.label 以标签文案作为唯一键,由于 4 个 Tab 的标签互不重复(房源/地图/搜索/我的),这一键值能正确区分各 Tab 项。backgroundColor(COLORS.card) 纯白底使底部导航在浅蓝页面底色上形成"白条"视觉,与顶部头部 Banner 的渐变白卡形成上下呼应。底部导航作为应用最高频的交互入口,其简洁的"图标+标签+激活色"三要素设计,是移动端 Tab 导航的成熟范式,无需过多装饰即可保证可用性。

十九、弹窗系统:遮罩与三套面板

19.1 modalOverlay 通用遮罩

  @Builder
  modalOverlay(onClose: () => void) {
    Column()
      .width('100%')
      .height('100%')
      .backgroundColor(COLORS.mask)
      .onClick(() => { onClose(); })
  }

modalOverlay 是弹窗的通用遮罩层 Builder,接收一个 onClose 回调函数。它以全屏 Columnwidth('100%') + height('100%'))+ COLORS.maskrgba(30,42,58,0.5) 半透明深墨蓝)覆盖底层内容,onClick 绑定 onClose 回调实现"点击遮罩关闭弹窗"的交互惯例。这一遮罩被三套弹窗(收藏/编辑/删除)复用,确保三套弹窗的遮罩视觉与行为完全一致。onClose 作为回调参数传入而非硬编码 this.addModal = false,使 modalOverlay 成为真正可复用的通用组件——任何弹窗都可以传入自己的关闭逻辑。

19.2 panelAdd 收藏房源弹窗

  @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: '户型(如:2室1厅)', text: this.formLayout })
          .height(38)
          .fontSize(12)
          .fontColor(COLORS.title)
          .placeholderColor(COLORS.text3)
          .backgroundColor(COLORS.chip)
          .onChange((v: string) => { this.formLayout = 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.saveHouse(); })
        }
        .width('100%')
      }
      .padding(16)
      .borderRadius(14)
      .backgroundColor(COLORS.card)
      .width('82%')
    }
    .width('100%')
    .height('100%')
  }

panelAdd 是收藏新房源弹窗,以 Stack 为根,底层叠加 modalOverlay(onClose) 遮罩,上层居中展示一个 Column 卡片(width('82%') 居中、borderRadius(14) 圆角、COLORS.card 纯白底)。卡片内含标题"收藏新房源"+ 三个 TextInput(小区名称、户型、地址)+ 取消/收藏双按钮行。三个 TextInputtext 参数分别绑定 formName/formLayout/formAddr 状态,onChange 回调将输入值同步回状态,实现双向数据流。placeholderColor(COLORS.text3) 使占位符以浅蓝灰呈现,与实际输入文本的深墨蓝形成"弱-强"对比,引导用户聚焦已填内容。

"取消"按钮以 COLORS.chip 浅蓝底 + COLORS.sub 灰蓝字呈现,是次操作;"收藏"按钮以 COLORS.blue 家园蓝底呈现,是主操作。两个按钮 layoutWeight(1) 等分宽度,形成"左次右主"的对称行动召唤布局。"取消"按钮点击调用 onClose() 关闭弹窗,"收藏"按钮点击调用 this.saveHouse() 执行实际的收藏逻辑(在 saveHouse 内部会关闭弹窗)。这一"主按钮调业务方法、次按钮调 onClose"的分工,是弹窗按钮交互的标准范式。

19.3 panelEdit 编辑备注与 panelDel 删除确认

  @Builder
  panelEdit(onClose: () => void) {
    Stack() {
      this.modalOverlay(onClose)
      Column({ space: 12 }) {
        Text('编辑房源备注').fontSize(15).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
        Text(this.editIdx < this.houseList.length ? this.houseList[this.editIdx].name : '')
          .fontSize(11)
          .fontColor(COLORS.sub)
          .width('100%')
        TextInput({ placeholder: '输入新备注', text: this.editNote })
          .height(38)
          .fontSize(12)
          .fontColor(COLORS.title)
          .placeholderColor(COLORS.text3)
          .backgroundColor(COLORS.chip)
          .onChange((v: string) => { this.editNote = v; })
        Row({ space: 10 }) {
          Button('取消')
            .layoutWeight(1)
            .fontSize(12)
            .backgroundColor(COLORS.chip)
            .fontColor(COLORS.sub)
            .onClick(() => { onClose(); })
          Button('保存')
            .layoutWeight(1)
            .fontSize(12)
            .backgroundColor(COLORS.blue)
            .onClick(() => { this.updateHouse(); })
        }
        .width('100%')
      }
      .padding(16)
      .borderRadius(14)
      .backgroundColor(COLORS.card)
      .width('82%')
    }
    .width('100%')
    .height('100%')
  }

  @Builder
  panelDel(onClose: () => void) {
    Stack() {
      this.modalOverlay(onClose)
      Column({ space: 12 }) {
        Text('删除收藏房源').fontSize(15).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
        Text(this.delIdx < this.houseList.length
          ? `确定删除「${this.houseList[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.delHouse(); })
        }
        .width('100%')
      }
      .padding(16)
      .borderRadius(14)
      .backgroundColor(COLORS.card)
      .width('82%')
    }
    .width('100%')
    .height('100%')
  }

panelEditpanelDel 分别是编辑备注弹窗与删除确认弹窗,两者结构与 panelAdd 高度对称,均采用 Stack + modalOverlay + Column 卡片的布局范式。panelEdit 的卡片内含标题"编辑房源备注"+ 当前房源名(以 this.editIdx < this.houseList.length ? this.houseList[this.editIdx].name : '' 边界安全访问,防止索引越界导致运行时错误)+ 备注输入框 + 取消/保存按钮。备注输入框的 text 绑定 editNote 状态,该状态在 openEditHouse 方法中已被回填为当前房源的备注文本,因此弹窗打开时输入框显示的是已有备注而非空白,用户可在此基础上修改。

panelDel 的卡片内含标题"删除收藏房源"+ 确认文案(以 this.delIdx < this.houseList.length ? \确定删除「${this.houseList[this.delIdx].name}」吗?` : '确定删除吗?'边界安全访问并动态拼接房源名)+ 取消/删除按钮。删除按钮以COLORS.red警示红底呈现,与收藏/保存按钮的家园蓝形成"危险-安全"对比,这是删除类操作的标准视觉规范——用红色明确警示用户此操作不可逆。三套弹窗共享modalOverlay 遮罩、width(‘82%’) 居中宽度、borderRadius(14) 圆角、COLORS.card` 纯白底等视觉参数,保证了弹窗系统内的视觉一致性,是"一套设计语言贯穿全部弹窗"的工程实践。

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

特性维度 特性一:reliability 相关性评分 特性二:长按事件监听
所属模块 site 模块(searchByText 接口) map 模块(MapEventManager)
新增内容 Site 类型新增 reliability 字段 新增 on/offMarkerLongClick、on/offPoiLongClick 四个方法
数据类型 number,取值 [0,1],1 为完全相关 回调函数,参数分别为 map.Marker 与 mapCommon.Poi
触发时机 searchByText 返回结果时随 Site 数组返回 用户长按地图上的 Marker 或 POI 时由地图手势识别触发
本页落地 搜索 Tab 的分数条 + 等级标签 + 数值文本三重可视化 地图 Tab 的日志流 unshift 置顶 + Toggle 开关切换
用户价值 在海量 POI 中按相关度分级筛选,缩短决策路径 长按地图点位即可记录事件,替代自定义浮层方案
容错策略 s.reliability ?? 0 空值合并兜底 if (!this.mapEventManager) return 空管理器防御
视觉映射 reliabilityScore 函数:≥0.8 蓝/≥0.5 青绿/<0.5 橙 EventLog 卡片:Marker 蓝/POI 青绿,emoji 📍/🏠 区分
状态驱动 @State searchRecords + @Observed SearchRecord @State eventLogs + @Observed EventLog
业务场景 关键字"两居室"搜索时区分真房源与装修公司/合租次卧 长按小区 Marker 记录看房意向,长按 POI 记录兴趣点

二十一、总结

本文以"安家找房·二手房租房"应用为载体,完整剖析了 HarmonyOS ArkUI 框架下房产交易与租赁垂直场景的工程实现,重点聚焦 Map Kit 6.1.1 的两大新特性——site 模块 searchByText 返回的 Site 类型新增 reliability 相关性分数字段,以及 MapEventManager 新增的 onMarkerLongClick / offMarkerLongClickonPoiLongClick / offPoiLongClick 长按事件监听接口。从颜色系统的集中化声明、常量与 Mock 数据的语义化沉淀、辅助函数的规则解耦,到 @Observed 数据模型的状态驱动、MapComponent 的异步回调初始化、双长按监听的注册与开关切换、searchByText 的 reliability 字段读取与三重可视化,整套代码以"接口先行-数据建模-规则函数-Builder 分区"的分层范式,展示了 ArkUI 中大型业务页面的工程化写法。

在技术深度上,reliability 字段的引入使房产搜索从"返回什么看什么"的被动呈现,升级为"按相关度分级着色与排序"的主动筛选,用户在海量 POI 中无需逐条点开详情即可在列表层完成第一轮甄别,显著缩短了从搜索到决策的路径。长按事件新接口的补齐则使地图层面的"长按收藏"“长按对比”"长按记笔记"等房产高频交互得以脱离自定义浮层方案,直接以原生事件流接入业务逻辑,事件链路更短、手势识别更精准、性能开销更低。两者一静一动——reliability 是静态的搜索结果质量评分,长按事件是动态的用户交互行为采集——共同构成了 Map Kit 在房产找房场景下的能力闭环。

从工程范式看,本应用的"一份数据驱动多个视图"思想值得借鉴:MARKER_SPOTS 同时驱动地图 Marker 打点与长按事件日志流,HOUSE_LIST 同时驱动房源列表展示与编辑/删除弹窗回填,COLORS 色板同时服务于 4 个 Tab 与 3 套弹窗的视觉一致性。@Observed@State 的组合使"数据变更→视图刷新"的链路在框架层自动完成,开发者无需手动调用刷新方法,极大降低了状态同步的心智负担。@Builder 函数的参数化复用(如 statCell 在房源与我的两个 Tab 中复用、modalOverlay 在三套弹窗中复用)保证了高频 UI 片段的视觉一致性,未来调整时只需改一处即可全局生效。

在容错设计上,runSearch 方法的"成功替换数据、失败保留演示数据"策略,使搜索功能在无 AGC 配置或无网络环境下仍能呈现可交互的演示内容,是 C 端应用应有的鲁棒性。setupMapCallback 中"先判错、再取控制器、再打点、再注册监听"的顺序防御,确保地图初始化失败时不会引发后续链路的二次异常。reliability ?? 0sites ?? []if (!this.mapEventManager) return 等空值合并与空引用防御贯穿全代码,体现了 ArkTS 异步与可选类型环境下的防御性编程素养。整体而言,这是一份兼具技术深度、业务厚度与工程稳健度的垂直行业落地参考实现,为 HarmonyOS ArkUI + 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.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 版本编写,不同版本界面可能存在细微差异。

Logo

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

更多推荐