reliability 相关性评分与长按事件双新特性落地验光门店:HarmonyOS 6.1.1 Map Kit 从 searchByText 分数条到 Marker/POI 长按日志流的视光场景工程
一、技术前言:当专业视光验配服务遇见 Map Kit 双新特性

HarmonyOS 的 ArkUI 框架是华为面向全场景分布式设备打造的声明式 UI 开发范式,以 ArkTS 语言为根基,在 TypeScript 的类型安全之上引入了 @Entry、@Component、@State、@Builder、@Observed 等装饰器体系。开发者通过接近自然语言的方式描述界面结构,框架则采用数据驱动渲染机制——当状态变量发生变更时,自动精确刷新与该状态绑定的组件节点,无需手动调用 findViewById 或 setText。这种范式极大地简化了复杂界面的状态管理,特别适合需要频繁局部刷新的验光门店类应用:搜索结果列表根据相关性分数动态着色、长按事件日志流实时置顶新增、收藏门店列表增删后即时刷新,都可以通过状态绑定自动完成。

Map Kit 是 HarmonyOS 提供的地图服务能力套件,它让开发者可以在 ArkUI 应用中嵌入一个功能完整的地图组件。通过 MapComponent 组件和 mapCommon.MapOptions 初始化参数,应用可以在页面中加载可交互的矢量地图,支持缩放、平移、旋转等手势操作。通过 map.MapComponentController 控制器,应用可以编程式地控制地图视角、添加标注 Marker、注册事件监听。通过 map.MapEventManager 事件管理器,应用可以监听地图上的用户交互行为——点击、长按、拖拽等,将这些行为转化为业务数据。Map Kit 还提供了 site 模块用于地点搜索,开发者可以通过关键字、位置、半径等参数检索周边地点信息。

HarmonyOS 6.1.1 为 site 模块的 searchByText 接口带来了一个重磅新增字段——Site.reliability。当开发者调用 site.searchByText(params) 传入搜索关键字(如"眼镜店")、位置中心点、搜索半径和语言参数后,返回的 SearchByTextResult 中包含一个 Site 数组。每个 Site 对象除了已有的 name(地点名称)、formatAddress(格式化地址)、distance(直线距离)等字段外,新增了 reliability 相关性分数字段。该字段取值范围为 [0, 1],其中 1 表示搜索结果与关键字完全相关,0 表示完全不相关。这个分数让开发者可以精确判断每条搜索结果与用户输入关键字的关联程度,从而对结果进行分级展示、过滤排序或差异化着色,而不再只能笼统地接受搜索引擎返回的全部结果。

HarmonyOS 6.1.1 同时为 MapEventManager 带来了两个新的长按事件监听能力——onMarkerLongClick / offMarkerLongClick 和 onPoiLongClick / offPoiLongClick。onMarkerLongClick 监听用户长按地图上自定义标注 Marker 的行为,回调参数是 map.Marker 对象,开发者可以通过 marker.getId() 获取标注 ID、通过 marker.getPosition() 获取标注经纬度坐标。onPoiLongClick 监听用户长按地图上原生 POI(兴趣点,如地标建筑、商铺等)的行为,回调参数是 mapCommon.Poi 对象,包含 name(POI 名称)和 position(POI 经纬度)等字段。配套的 off 方法不传参即清除该类型的全部订阅,实现了灵活的监听开关控制。这两个长按事件让地图交互从单一的点击进入到了长按维度,为应用提供了更丰富的用户意图捕获能力。

专业视光验配服务是医疗与零售的交叉业态。从儿童近视防控建档、青少年散瞳验光、成人配镜矫治到中老年老花定制,验光门店承载着眼健康全生命周期的服务职能。用户在选择验光门店时,关注的核心信息远比普通零售门店复杂——门店是否具备医学验光资质、是否配备角膜地形图仪和眼轴生物仪等专业设备、今日还有多少可约时段、距上次验光已过多久是否该复查、双眼度数变化趋势如何。这些信息需要在有限的手机屏幕上以结构化、可视化、可操作的方式呈现。传统的门店列表页往往只展示名称和地址,无法满足验光场景下用户对专业度和时效性的高要求。

本应用名为"视界·眼镜验光门店",定位为专业视光验配服务平台,采用瓷白底(#F7F6F1)+ 青瓷绿(#3D8B6E)+ 琥珀棕(#A67B4F)的浅色主题方案。瓷白底色带有微弱的暖调,比纯白更柔和、更护眼,契合视光行业"眼健康"的核心诉求。青瓷绿作为品牌主色用于按钮、选中态、渐变起点和高相关标签,传递专业、自然、可信赖的品牌调性。琥珀棕作为副色用于中相关标签和权益提示,与青瓷绿形成冷暖互补的视觉层次。应用设计了四个功能完全独立的 Tab 页面:门店(验光渐变大卡 + 推荐门店列表 + 护眼速览 + 收藏门店全列表)、地图(MapComponent + 长按事件监听开关 + 事件日志流)、搜索(关键字搜索 + reliability 分数条 + 等级标签结果列表 + 双特性代码预览卡)、我的(会员渐变大卡 + 权益卡 + 功能清单行),每个 Tab 的布局都经过差异化设计。同时应用内置了完整的弹窗系统,支持收藏门店的新增、备注编辑、删除确认全流程操作。

二、整体架构流程图
三、模块导入与依赖引入
3.1 导入语句
import { MapComponent, mapCommon, map, site } from '@kit.MapKit';
import { AsyncCallback, BusinessError } from '@kit.BasicServicesKit';
这两行导入语句是整个 Map Kit 验光门店双新特性功能的基石。第一行从 @kit.MapKit 中一次性导入了四个命名空间和组件:MapComponent 是地图组件本体,作为 ArkUI 的内置组件直接在 build() 中以 MapComponent({ mapOptions, mapCallback }) 的形式声明使用,渲染出一个可交互的矢量地图;mapCommon 是地图通用类型命名空间,包含 LatLng(经纬度坐标)、MapOptions(地图初始化参数)、MarkerOptions(标注配置)、Poi(兴趣点)等基础类型;map 是地图核心命名空间,包含 MapComponentController(地图控制器)、MapEventManager(事件管理器)、Marker(标注对象)等核心类;site 是地点搜索命名空间,包含 searchByText 方法及其 SearchByTextParams 参数和 SearchByTextResult 结果、Site 地点类型。
第二行从 @kit.BasicServicesKit 中导入了 AsyncCallback 和 BusinessError 两个类型。AsyncCallback<T> 是 HarmonyOS 的异步回调泛型接口,用于定义"接收一个错误和一个结果的回调"签名,这里用于声明 mapCallback 的类型——当地图初始化完成时,框架会回调该函数并传入 BusinessError(错误信息)和 map.MapComponentController(地图控制器实例)。BusinessError 是统一错误接口,包含 code(错误码)和 message(错误描述),在 addMarker 失败和 site.searchByText 失败时都会被 catch 并读取错误信息,用于向用户展示友好的失败提示。
四、颜色系统设计
4.1 颜色接口定义
interface ColorPalette {
bg: string; // 页面底色(瓷白)
card: string; // 卡片底色(纯白)
chip: string; // 胶囊与输入框底色(青瓷白)
title: string; // 主标题色(墨绿黑)
sub: string; // 次级文本色(灰绿)
text3: string; // 弱文本色(浅灰绿)
accent: string; // 行业主色(青瓷绿)
accentD: string; // 主色深(深青瓷绿)
accentL: string; // 主色浅(渐变起点淡青)
second: string; // 副色(琥珀棕)
danger: string; // 警示色(低相关/删除/约满)
info: string; // 信息色(灰蓝)
line: string; // 分割线色
tabOn: string; // 底部 Tab 选中色
mask: string; // 弹窗遮罩色
codeBg: string; // 代码预览卡底色(深青绿,浅色主题也用深底放代码)
}
ColorPalette 接口将页面用到的所有颜色字段集中声明为一个类型契约。这种设计的好处在于:当需要切换主题时,只需创建一个新的 ColorPalette 实现对象替换 COLORS 常量即可,无需在代码中逐一搜索替换散落的颜色值。接口中的字段涵盖了验光门店界面所需的全部色彩角色:bg 是瓷白页面底色(#F7F6F1),带有微弱暖调,比纯白更护眼;card 是卡片纯白底色,用于在瓷白背景上提升卡片的层次感;chip 是青瓷白色,用于胶囊标签和输入框底色,与主色青瓷绿形成同色系深浅呼应。
title 是墨绿黑色主标题色(#2B3A32),在浅色背景上具有高对比度且不刺眼;sub 是灰绿色次级文本色,用于补充信息;text3 是浅灰绿色弱文本色,用于辅助说明和数值标签。accent 是青瓷绿行业主色(#3D8B6E),用于按钮、选中态、高相关标签等强引导元素;accentD 是深青瓷绿色,用于需要更深沉强调的场景;accentL 是淡青色,用作渐变 Banner 的起点色,与卡片白色底色形成柔和过渡。second 是琥珀棕色副色,用于中相关标签和权益提示,与青瓷绿形成冷暖互补。danger 是警示色(#C4574E),用于低相关分数标签、删除按钮和约满提示。info 是灰蓝色信息色,用于编辑等中性操作。line 是分割线色,tabOn 是底部 Tab 选中色(与主色一致),mask 是弹窗遮罩色,codeBg 是深青绿色代码预览卡底色——浅色主题中特例使用深底,让代码文字有更好的可读性。
4.2 浅色主题色板常量
const COLORS: ColorPalette = {
bg: '#F7F6F1',
card: '#FFFFFF',
chip: '#ECEFE7',
title: '#2B3A32',
sub: '#6B7A6E',
text3: '#9AA89C',
accent: '#3D8B6E',
accentD: '#2A6450',
accentL: '#DCEDE4',
second: '#A67B4F',
danger: '#C4574E',
info: '#4A7FA8',
line: '#E2E5DC',
tabOn: '#3D8B6E',
mask: 'rgba(40,52,44,0.42)',
codeBg: '#1B2A22'
};
COLORS 常量是浅色主题的具体实现。瓷白底色 #F7F6F1 的 RGB 值为 (247, 246, 241),三通道中红色和绿色略高于蓝色,呈现出微弱的暖黄调,模拟瓷器的温润质感,契合视光行业"眼健康"的核心诉求——长时间浏览不会像纯白背景那样刺激眼睛。卡片纯白底色 #FFFFFF 用于在瓷白背景上的卡片元素,通过"瓷白→纯白→瓷白"的层次过渡制造出微妙的纵深感。
青瓷绿主色 #3D8B6E 的 RGB 值为 (61, 139, 110),绿色通道最高、红色次之、蓝色最低,属于偏黄绿的青绿色调,让人联想到青瓷釉面和自然草木,传递专业、自然、可信赖的品牌调性。琥珀棕副色 #A67B4F 的 RGB 值为 (166, 123, 79),红色最高、绿色次之、蓝色最低,属于偏橙的棕色调,与青瓷绿形成经典的冷暖互补配色。警示色 #C4574E 是偏红的暖色,在浅色背景上足够醒目但不刺眼,用于低相关分数、删除确认和约满提示等需要用户注意的场景。弹窗遮罩色 rgba(40,52,44,0.42) 使用了墨绿色的半透明,与主题色系保持统一,42% 的透明度既保证了遮罩下方内容的可读性暗示,又突出弹窗面板本身。
五、常量定义
5.1 Tab 元数据与筛选标签
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 接口定义了底部导航 Tab 的元数据结构,包含 icon(emoji 图标)和 label(标签文案)两个字段。使用 emoji 作为 Tab 图标的策略在验光门店场景下有其独到优势——emoji 是跨平台标准化的图形字符,无需准备图标资源文件即可直接使用,且验光行业的相关 emoji(👓 眼镜、🗺 地图、🔍 放大镜、👤 人像)语义准确、辨识度高。
TAB_LIST 常量定义了 4 个 Tab 的完整配置:门店 Tab 用 👓 眼镜图标代表业务主入口,地图 Tab 用 🗺 地图图标代表地图找店,搜索 Tab 用 🔍 放大镜图标代表关键字搜索,我的 Tab 用 👤 人像图标代表个人中心。4 个 Tab 单排排列在底部导航栏中,每个 Tab 占据等宽的 layoutWeight(1) 空间。
CATE_TAGS 常量定义了头部横滑筛选 chips 的 8 个标签文案,覆盖了验光门店的全部业务场景:‘全部’ 是默认全选,‘医学验光’ 对应需要医疗资质的医学验光服务,‘儿童防控’ 对应青少年近视防控建档,‘隐形适配’ 对应隐形眼镜验配,‘快配立取’ 对应急件快配一小时取镜,‘防蓝光’ 对应防蓝光镜片升级,‘老花定制’ 对应中老年老花镜定制,‘免费试戴’ 对应镜架免费试戴服务。这些标签通过横向滚动展示,用户点击后 cateIdx 状态更新,当前选中的 chip 用青瓷绿底色高亮。
5.2 城市中心点与地图标注
const CITY_CENTER: mapCommon.LatLng = { latitude: 32.0603, longitude: 118.7969 };
interface SpotItem {
name: string; // 门店名称
lat: number; // 纬度
lng: number; // 经度
tag: string; // 门店类型标签
}
const MARKER_SPOTS: SpotItem[] = [
{ name: '视界·新街口旗舰店', lat: 32.043, lng: 118.782, tag: '旗舰' },
{ name: '视界·珠江路视光中心', lat: 32.052, lng: 118.79, tag: '医学' },
{ name: '视界·鼓楼公园店', lat: 32.066, lng: 118.788, tag: '快配' },
{ name: '视界·太平南路儿童店', lat: 32.041, lng: 118.8, tag: '儿童' },
{ name: '视界·云南北路复古店', lat: 32.073, lng: 118.79, tag: '复古' },
{ name: '视界·明故宫校园店', lat: 32.049, lng: 118.81, tag: '校园' }
];
CITY_CENTER 常量定义了南京的城市中心点经纬度坐标(纬度 32.0603、经度 118.7969),它同时服务于两个用途:一是作为 mapCommon.MapOptions 中地图初始化的中心位置和 site.searchByText 的 location 参数,让地图打开时即定位到南京核心区域;二是作为搜索半径的中心参照点。选择南京作为示例城市有其考量——南京是新一线城市的代表,验光门店分布密集且业态成熟,从新街口商圈到鼓楼、秦淮各区域都有丰富的视光服务资源。
SpotItem 接口定义了地图标注点的数据结构,包含 name(门店名称)、lat(纬度)、lng(经度)、tag(门店类型标签)四个字段。MARKER_SPOTS 常量定义了 6 个验光门店标注点的 Mock 数据,围绕南京城市中心点 ±0.02 度散布——这个经纬度跨度大约对应 2-3 公里的实际距离,足以覆盖南京主城区的验光门店分布。6 个门店各有特色:新街口旗舰店是品牌旗舰,珠江路视光中心侧重医学验光,鼓楼公园店主打快配立取,太平南路儿童店专注青少年防控,云南北路复古店提供复古镜框,明故宫校园店面向学生群体。这些标注点在 mapCallback 中通过 addMarker 批量添加到地图上,成为长按事件的数据来源。
5.3 推荐门店与护眼速览
interface OpticRec {
icon: string; // 门店 emoji
name: string; // 门店名
dist: string; // 距离文本
price: string; // 验光价文本
device: string; // 主力验光设备
book: number; // 今日可约时段数
}
const OPTIC_RECS: OpticRec[] = [
{ icon: '👓', name: '新街口旗舰店', dist: '1.1km', price: '验光 68元', device: '综合验光仪', book: 6 },
{ icon: '🔬', name: '珠江路视光中心', dist: '1.6km', price: '验光 98元', device: '角膜地形图仪', book: 3 },
{ icon: '🕶', name: '鼓楼公园店', dist: '2.2km', price: '验光 58元', device: '数码裂隙灯', book: 9 }
];
OpticRec 接口定义了推荐门店的数据结构,相比 SpotItem 增加了距离、验光价、主力验光设备和今日可约时段数等验光场景特有信息。OPTIC_RECS 常量定义了 3 条精选推荐门店数据,展示在门店 Tab 头部渐变大卡下方的列表中。每条数据都包含验光门店的核心决策信息:距离(1.1km/1.6km/2.2km)帮助用户判断可达性,验光价(68/98/58 元)帮助用户预估消费门槛,主力验光设备(综合验光仪/角膜地形图仪/数码裂隙灯)帮助用户判断专业度,今日可约时段数(6/3/9 段)帮助用户判断即时可用性。
book 字段的数值通过 bookColor 辅助函数映射为不同颜色——≥6 段充裕用青瓷绿主色、≥2 段正常用琥珀棕副色、其余紧张用警示色,让用户一眼就能识别哪些门店今天还能约到。这种"时段数 + 颜色编码"的设计在预约制验光服务中极具实用价值,避免了用户点进门店详情才发现已约满的体验落差。
5.4 护眼速览与功能清单
interface VisionStat {
icon: string; // 指标图标
label: string; // 指标名
value: string; // 指标值文本
}
const VISION_STATS: VisionStat[] = [
{ icon: '📋', label: '验光档案', value: '3 份' },
{ icon: '🔔', label: '复查提醒', value: '1 条' },
{ icon: '🎫', label: '会员折扣', value: '85 折' }
];
interface FuncItem {
icon: string; // 功能图标
label: string; // 功能名
value: string; // 状态/数值文本
}
const FUNC_LIST: FuncItem[] = [
{ icon: '📋', label: '验光档案', value: '3 份 · 最近 2 个月前' },
{ icon: '👓', label: '配镜订单', value: '历史 3 副' },
{ icon: '👁', label: '度数变化', value: '左 -4.50 · 右 -4.25' },
{ icon: '💳', label: '会员卡包', value: '视界卡 · 85 折' },
{ icon: '⭐', label: '收藏门店', value: '5 家' },
{ icon: '🧾', label: '消费账单', value: '本年 3 笔' },
{ icon: '🗺', label: '常去门店', value: '新街口 / 珠江路' },
{ icon: '⚙', label: '偏好设置', value: '每年复查提醒' }
];
VisionStat 接口定义了护眼速览指标的数据结构,用于门店 Tab 中横排三列小卡的展示。VISION_STATS 常量定义了 3 项指标:验光档案 3 份(历史记录)、复查提醒 1 条(待办通知)、会员折扣 85 折(权益展示),帮助用户快速了解自己的视光健康状态和权益。FuncItem 接口定义了我的页功能清单条目的数据结构,FUNC_LIST 常量定义了 8 条功能清单数据,覆盖验光档案、配镜订单、度数变化、会员卡包、收藏门店、消费账单、常去门店、偏好设置等完整的个人视光管理功能入口。
六、辅助函数
6.1 reliability 相关性分数等级映射
interface ScoreLevel {
label: string; // 等级文案
color: string; // 等级颜色
}
function reliabilityScore(score: number): ScoreLevel {
if (score >= 0.8) {
return { label: '高相关', color: COLORS.accent };
}
if (score >= 0.5) {
return { label: '中相关', color: COLORS.second };
}
return { label: '低相关', color: COLORS.danger };
}
ScoreLevel 接口定义了相关性等级的返回结构,包含 label(等级文案)和 color(等级颜色)两个字段。reliabilityScore 函数是 HarmonyOS 6.1.1 Site.reliability 新字段的核心展示逻辑,它将 [0, 1] 区间的相关性分数映射为三个等级:分数 ≥ 0.8 返回"高相关"标签和青瓷绿主色,表示搜索结果与关键字高度匹配;分数 ≥ 0.5 且 < 0.8 返回"中相关"标签和琥珀棕副色,表示部分匹配需要用户判断;分数 < 0.5 返回"低相关"标签和警示色,表示匹配度低可能不符合预期。
这种三档分级策略在验光门店搜索场景下有其合理性:当用户搜索"眼镜店"时,返回的结果中可能有真正的验光门店(reliability ≥ 0.8),也可能有一些边缘相关的商铺如眼镜城批发档口(reliability ≈ 0.13),还可能有一些部分匹配的视光所或镜框集合店(reliability ≈ 0.5-0.7)。通过分数条 + 等级标签 + 颜色编码的三重视觉提示,用户可以快速筛选出真正相关的验光门店,过滤掉匹配度低的结果,避免被无关结果误导。该函数在搜索结果列表的等级标签和分数条颜色中各被调用一次,确保标签文案和进度条颜色保持一致。
6.2 可约时段颜色映射
function bookColor(book: number): string {
if (book >= 6) { return COLORS.accent; }
if (book >= 2) { return COLORS.second; }
return COLORS.danger;
}
bookColor 函数将今日可约时段数映射为三种颜色:≥6 段返回青瓷绿主色表示充裕,≥2 段返回琥珀棕副色表示正常,<2 段返回警示色表示紧张。这个函数在门店 Tab 的推荐门店列表和收藏门店列表中各被调用,确保时段数颜色编码的一致性。在验光预约场景下,时段数的颜色编码帮助用户快速识别哪些门店今天还能约到——绿色表示时段充裕可以随时去,棕色表示时段正常需要尽快预约,红色表示时段紧张可能即将约满。
七、数据模型
7.1 门店收藏条目模型
@Observed export class OpticItem {
icon: string; // 门店 emoji 图标
name: string; // 门店名
price: string; // 验光价文本
device: string; // 主力验光设备
book: number; // 今日可约时段数
note: string; // 用户备注(可编辑)
constructor(icon: string, name: string, price: string, device: string,
book: number, note: string) {
this.icon = icon;
this.name = name;
this.price = price;
this.device = device;
this.book = book;
this.note = note;
}
}
const OPTIC_LIST: Array<OpticItem> = [
new OpticItem('👓', '新街口旗舰店', '验光 68元', '综合验光仪', 6, '配镜老地方'),
new OpticItem('🔬', '珠江路视光中心', '验光 98元', '角膜地形图仪', 3, '青少年建档'),
new OpticItem('🕶', '鼓楼公园店', '验光 58元', '数码裂隙灯', 9, '快配一小时取'),
new OpticItem('🧒', '太平南路儿童店', '验光 88元', '眼轴生物仪', 2, '娃的复查点'),
new OpticItem('🪞', '云南北路复古店', '验光 78元', '综合验光仪', 5, '镜框款式多'),
new OpticItem('🎓', '明故宫校园店', '验光 48元', '电脑验光仪', 12, '学生特惠价'),
new OpticItem('👓', '夫子庙轻奢店', '验光 128元', '眼底照相机', 1, '蔡司合作店')
];
OpticItem 类使用了 @Observed 装饰器,这是 ArkUI 响应式数据系统的关键。@Observed 装饰的类,其实例在被 @State 状态变量引用时,对象内部属性的变化可以被框架追踪。当用户在编辑备注弹窗中修改了某个门店的 note 属性后,配合 slice() 创建数组新引用,ForEach 列表会自动刷新对应条目的显示。constructor 构造函数接收 6 个参数初始化全部字段,其中 note 是用户自定义备注,可以被编辑修改。
OPTIC_LIST 常量定义了 7 条收藏门店 Mock 数据,每条数据包含验光门店的完整决策信息。7 家门店各有特色:新街口旗舰店标注"配镜老地方"说明是用户常去的门店,珠江路视光中心标注"青少年建档"说明用于儿童近视防控,鼓楼公园店标注"快配一小时取"说明用于急件配镜,太平南路儿童店标注"娃的复查点"说明用于孩子复查,云南北路复古店标注"镜框款式多"说明用于挑选镜框,明故宫校园店标注"学生特惠价"说明用于学生验光,夫子庙轻奢店标注"蔡司合作店"说明是高端合作门店。这些备注展示了 note 字段在实际使用中的价值——用户为每家收藏门店打上个性化的使用场景标签。
7.2 搜索结果 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;
}
}
const SEARCH_RECORDS: Array<SearchRecord> = [
new SearchRecord('新街口验光旗舰店', '南京市玄武区中山路 18 号', 1120, 0.95, '刚刚'),
new SearchRecord('珠江路视光中心', '南京市玄武区珠江路 699 号', 1580, 0.87, '刚刚'),
new SearchRecord('鼓楼公园眼镜直营店', '南京市鼓楼区中山北路 55 号', 2050, 0.73, '刚刚'),
new SearchRecord('太平南路儿童视光所', '南京市秦淮区太平南路 174 号', 2610, 0.51, '刚刚'),
new SearchRecord('云南北路复古镜框集合店', '南京市鼓楼区云南北路 60 号', 3170, 0.29, '刚刚'),
new SearchRecord('眼镜城批发档口(散光需预约)', '南京市秦淮区洪武路 239 号', 4520, 0.13, '刚刚')
];
SearchRecord 类是 HarmonyOS 6.1.1 Site.reliability 新字段的核心数据载体。该类的五个字段与 site.Site 对象的属性一一对应:name 来自 site.name,address 来自 site.formatAddress,distance 来自 site.distance,reliability 来自 site.reliability(6.1.1 新增字段),time 是记录时间文案。@Observed 装饰器确保当搜索结果列表更新时,ForEach 能自动刷新。
SEARCH_RECORDS 常量定义了 6 条搜索结果 Mock 数据,reliability 分数覆盖了高/中/低三档:0.95 和 0.87 属于高相关(≥0.8),分别是新街口验光旗舰店和珠江路视光中心——这些是用户搜索"眼镜店"时最期望看到的结果;0.73 属于中相关(≥0.5),是鼓楼公园眼镜直营店;0.51 处于中相关边界,是太平南路儿童视光所;0.29 和 0.13 属于低相关(<0.5),分别是云南北路复古镜框集合店和眼镜城批发档口——这些结果与"眼镜店"关键字的匹配度较低。Mock 数据中地址使用南京真实街道地址(中山路、珠江路、中山北路、太平南路、云南北路、洪武路),增加了数据的真实感。这组数据完整演示了 reliability 字段从高到低的分布形态,让搜索 Tab 的分数条和等级标签有了丰富的展示素材。
7.3 长按事件日志数据载体
@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', '南京图书馆', 32.046, 118.792, '演示事件'),
new EventLog('Marker', '#0', 32.043, 118.782, '演示事件')
];
EventLog 类是 MapEventManager 长按监听数据的核心载体。该类的设计巧妙之处在于用 type 字段统一了两种长按事件的数据结构:当 type 为 ‘Marker’ 时,name 字段存储的是 Marker ID(如 #0、#1),对应 onMarkerLongClick 回调中 marker.getId() 的返回值;当 type 为 ‘POI’ 时,name 字段存储的是 POI 名称(如"南京图书馆"),对应 onPoiLongClick 回调中 poi.name 的值。lat 和 lng 字段统一存储经纬度坐标——Marker 事件来自 marker.getPosition(),POI 事件来自 poi.position。time 字段存储事件时间文案,新增事件统一用"刚刚"。
EVENT_LOGS 常量定义了 2 条演示事件 Mock 数据:一条 POI 长按事件(南京图书馆,纬度 32.046,经度 118.792)和一条 Marker 长按事件(ID 为 #0,纬度 32.043,经度 118.782)。这两条演示数据让地图 Tab 的日志流在应用启动时即有内容展示,用户可以直观理解日志的形态,然后通过实际长按地图上的 Marker 或 POI 来触发新的日志条目。新事件通过 unshift 方法置顶到列表最前方,配合 @Observed 装饰器实现日志流的实时刷新。
八、组件主体状态管理
8.1 组件声明与 Tab 状态
@Entry
@Component
struct Page1148 {
@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 opticList: Array<OpticItem> = OPTIC_LIST;
@State funcList: FuncItem[] = FUNC_LIST;
@Entry 装饰器标记 Page1148 为页面入口组件,@Component 声明它是一个 ArkUI 组件。组件内部通过 @State 装饰器声明了一系列响应式状态变量。currentTab 是当前选中 Tab 索引(0=门店、1=地图、2=搜索、3=我的),控制 build() 中条件渲染哪个 Tab 内容。cateIdx 是头部筛选 chips 的选中索引,控制哪个筛选标签高亮。addModal/editModal/delModal 三个布尔值分别控制收藏门店弹窗、编辑备注弹窗、删除确认弹窗的显示与隐藏。
editIdx 和 delIdx 分别记录当前编辑和删除的门店索引,确保弹窗操作作用于正确的门店条目。opticList 是门店收藏列表数据,初始值来自 OPTIC_LIST 常量,支持新增(unshift)、编辑(修改 note 属性后 slice 刷新引用)和删除(splice)操作。funcList 是我的页功能清单数据,初始值来自 FUNC_LIST 常量。这些状态变量的变更都会自动触发绑定的 UI 组件刷新,无需手动调用任何刷新方法。
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 formAddr: string = '';
@State editNote: string = '';
这一组状态变量围绕 Map Kit 6.1.1 双新特性展开。mapOptions 是地图初始化参数,设置 position.target 为南京城市中心点 CITY_CENTER、zoom 为 13 级(城市级缩放,可以看到主城区的门店分布)。注意 mapOptions 声明为非可选属性并直接赋默认值,避免 MapComponent 组件参数传 undefined 导致初始化异常。mapCallback 是地图初始化回调,类型为 AsyncCallback<map.MapComponentController>,初始为 undefined,在 aboutToAppear 生命周期中通过 setupMapCallback() 赋值。
mapController 和 mapEventManager 分别是地图控制器和事件管理器,初始均为 undefined,在 mapCallback 回调中获取赋值——mapController 直接从回调参数获取,mapEventManager 通过 mapController.getEventManager() 获取。markerListenOn 和 poiListenOn 两个布尔值是长按监听开关状态,初始均为 true(监听开启),通过地图 Tab 的 Toggle 开关切换。eventLogs 是长按事件日志流数据,初始值来自 EVENT_LOGS 常量,新增事件通过 unshift 置顶。queryInput 是搜索关键字输入值,初始为"眼镜店";searchState 是搜索状态文案,初始为"待搜索 · 演示数据";searchRecords 是搜索结果列表,初始值来自 SEARCH_RECORDS 常量。formName/formAddr/editNote 三个变量分别用于收藏弹窗的门店名输入、地址输入和编辑弹窗的备注输入。
九、Map Kit 初始化与回调
9.1 地图初始化回调设置
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 集成的核心初始化入口,在 aboutToAppear 生命周期中被调用。该方法将一个 async 异步回调函数赋值给 this.mapCallback,当 MapComponent 组件初始化完成后,框架会回调该函数并传入两个参数:err 是 BusinessError 错误对象(初始化成功时为 null/undefined),mapController 是地图控制器实例。
回调函数的第一步是错误检查——如果 err 不为空,说明地图初始化失败,通过 console.error 输出错误码和错误消息后直接 return,不执行后续逻辑。这种防御性编程确保了地图初始化失败时不会引发后续的空指针异常。回调函数的第二步是获取控制器和事件管理器——将 mapController 保存到组件状态,再通过 mapController.getEventManager() 获取事件管理器并保存,这两个对象是后续添加 Marker 和注册长按监听的基础。
回调函数的第三步是批量添加验光门店 Marker。遍历 MARKER_SPOTS 数组的 6 个标注点,为每个标注点构造 mapCommon.MarkerOptions 配置对象:position 设置标注的经纬度坐标,clickable: true 允许标注可点击(长按监听的前提),visible: true 设置标注可见,rotation: 0 不旋转,zIndex: 0 默认层级,alpha: 1 完全不透明,anchorU: 0.5 和 anchorV: 1 设置锚点在标注图标的底部中心(让图标的尖端指向实际坐标位置),draggable: false 禁止拖拽,flat: false 保持立体效果。每个 addMarker 调用返回 Promise,通过 await 逐个等待完成,并用 try-catch 包裹捕获可能抛出的 BusinessError——这种逐个 await + try-catch 的策略确保了即使某个 Marker 添加失败,后续 Marker 的添加不会中断。
回调函数的第四步是注册 6.1.1 双长按监听。this.mapEventManager.onMarkerLongClick 注册 Marker 长按监听,回调参数是 map.Marker 对象,通过 marker.getPosition() 获取标注的经纬度坐标,通过 marker.getId() 获取标注 ID,构造一个 EventLog 实例(type 为 ‘Marker’,name 为 #${id})并 unshift 到日志流顶部。this.mapEventManager.onPoiLongClick 注册 POI 长按监听,回调参数是 mapCommon.Poi 对象,直接读取 poi.name 和 poi.position.latitude/poi.position.longitude,构造一个 EventLog 实例(type 为 ‘POI’)并 unshift 到日志流顶部。这两个监听必须在 mapCallback 的 err 为空分支内注册——因为只有 controller 就绪后才有 eventManager,只有 eventManager 就绪后才能注册监听。
9.2 Marker 长按监听开关
toggleMarkerListen() {
if (!this.mapEventManager) {
return;
}
if (this.markerListenOn) {
this.mapEventManager.offMarkerLongClick();
} else {
this.mapEventManager.onMarkerLongClick((marker: map.Marker) => {
const pos: mapCommon.LatLng = marker.getPosition();
this.eventLogs.unshift(new EventLog('Marker', `#${marker.getId()}`,
pos.latitude, pos.longitude, '刚刚'));
});
}
this.markerListenOn = !this.markerListenOn;
}
toggleMarkerListen 方法实现了 Marker 长按监听的开关切换。方法首先检查 mapEventManager 是否存在——如果事件管理器尚未初始化(地图尚未加载完成),直接 return 不执行任何操作,避免了空指针异常。然后根据当前 markerListenOn 状态决定操作:如果当前监听已开启(true),调用 offMarkerLongClick() 关闭监听;如果当前监听已关闭(false),重新调用 onMarkerLongClick() 注册监听,回调函数内容与初始化时完全一致。最后翻转 markerListenOn 状态值。
值得注意的是 offMarkerLongClick() 不传参即清除该类型的全部订阅——这是 HarmonyOS 6.1.1 事件管理器的设计约定,off 方法不需要传入回调函数引用,直接清除该事件类型的所有监听器。这种设计简化了监听器的管理——开发者无需保存回调函数的引用,调用 off 即可统一清除。地图 Tab 中的 Toggle 开关绑定了 onChange 事件调用此方法,用户可以自由控制 Marker 长按监听的开关。
9.3 POI 长按监听开关
togglePoiListen() {
if (!this.mapEventManager) {
return;
}
if (this.poiListenOn) {
this.mapEventManager.offPoiLongClick();
} else {
this.mapEventManager.onPoiLongClick((poi: mapCommon.Poi) => {
this.eventLogs.unshift(new EventLog('POI', poi.name,
poi.position.latitude, poi.position.longitude, '刚刚'));
});
}
this.poiListenOn = !this.poiListenOn;
}
togglePoiListen 方法与 toggleMarkerListen 方法结构完全对称,实现了 POI 长按监听的开关切换。同样先检查 mapEventManager 存在性,再根据 poiListenOn 状态决定调用 offPoiLongClick() 关闭还是 onPoiLongClick() 重新注册。offPoiLongClick() 同样不传参清除全部 POI 长按订阅。两个监听开关方法的设计完全对称,体现了 Marker 和 POI 两种长按事件在 API 设计上的对称性——它们使用相同的事件管理器、相同的 on/off 模式、相同的无参清除策略,只是回调参数类型不同(map.Marker vs mapCommon.Poi)。
9.4 searchByText reliability 搜索
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 方法是 HarmonyOS 6.1.1 site.searchByText + Site.reliability 新特性的核心调用逻辑。方法首先将 searchState 设置为"搜索中…“,让用户知道搜索正在进行。然后构造 site.SearchByTextParams 搜索参数对象:query 是搜索关键字(来自 queryInput 状态,初始为"眼镜店”),location 是搜索中心点(南京城市中心 CITY_CENTER),radius 是搜索半径 5000 米(覆盖南京主城区),language 是返回结果语言为中文。
方法的核心在 try-catch 块中。await site.searchByText(params) 调用搜索接口,返回 SearchByTextResult 结果对象。通过 result.sites ?? [] 获取 Site 数组并做空值兜底——?? 空值合并运算符确保即使 sites 为 undefined 也会得到空数组。如果数组为空,设置 searchState 为"无结果 · 保留演示数据"并 return,不覆盖已有的 Mock 搜索结果,保证演示链路完整。
如果搜索返回了结果,遍历 sites 数组,为每个 site.Site 对象构造一个 SearchRecord 实例。这里的关键是读取 6.1.1 新增的 s.reliability 字段——由于该字段是可选的(可能为 undefined),使用 ?? 0 兜底为 0 分。同样地,s.name、s.formatAddress、s.distance 也都使用 ?? 运算符兜底,分别兜底为"未命名地点"、“暂无地址”、0。构造完成后将 records 数组赋值给 searchRecords 状态触发列表刷新,并设置 searchState 为"返回 N 条结果"。如果搜索抛出异常(如无 AGC 配置或无网络),catch 块捕获 BusinessError,设置 searchState 为"搜索失败(错误码) · 保留演示数据",同样不覆盖已有的 Mock 数据——这种"失败保留演示数据"的策略确保了应用在任何环境下都有可展示的内容。
十、弹窗业务方法
10.1 打开编辑与保存收藏
openEditOptic(idx: number) {
this.editIdx = idx;
this.editNote = this.opticList[idx].note;
this.editModal = true;
}
saveOptic() {
const name = this.formName === '' ? '视界·新收藏店' : this.formName;
const addr = this.formAddr === '' ? '南京市玄武区(地图选点)' : this.formAddr;
this.opticList.unshift(new OpticItem('⭐', name, '验光 68元', '综合验光仪', 4, addr));
this.formName = '';
this.formAddr = '';
this.addModal = false;
}
openEditOptic 方法处理打开编辑备注弹窗的逻辑。接收门店索引 idx 参数,将其保存到 editIdx 状态,同时将当前门店的备注值 this.opticList[idx].note 回填到 editNote 状态——这种"先回填再显示"的设计确保了编辑弹窗的输入框中预填充了当前备注内容,用户可以在此基础上修改而非从空白开始。最后将 editModal 设为 true 打开弹窗。
saveOptic 方法处理保存新收藏门店的逻辑。首先对输入进行空值兜底:如果门店名为空,使用默认名"视界·新收藏店";如果地址为空,使用默认地址"南京市玄武区(地图选点)"。然后通过 unshift 方法将新的 OpticItem 实例添加到 opticList 数组头部——新收藏的门店会出现在列表最顶部。新门店的图标统一为 ⭐ 星标,验光价 68 元、设备为综合验光仪、可约时段 4 段、备注为地址值。最后清空 formName 和 formAddr 输入框并关闭弹窗。
10.2 更新备注与删除确认
updateOptic() {
if (this.editIdx >= 0 && this.editIdx < this.opticList.length) {
if (this.editNote !== '') {
this.opticList[this.editIdx].note = this.editNote;
}
this.opticList = this.opticList.slice();
}
this.editModal = false;
}
delOptic() {
if (this.delIdx >= 0 && this.delIdx < this.opticList.length) {
this.opticList.splice(this.delIdx, 1);
}
this.delModal = false;
}
updateOptic 方法处理保存编辑备注的逻辑。方法首先做边界检查——确保 editIdx 在 opticList 的有效索引范围内,防止越界访问。然后检查 editNote 是否为空——如果不为空,将编辑后的备注值赋给对应门店的 note 属性。最关键的一步是 this.opticList = this.opticList.slice()——直接修改数组元素的属性不会触发 ArkUI 的 ForEach 列表刷新,必须通过 slice() 方法创建数组的新引用,让框架检测到数组引用的变化从而触发列表重渲染。这是 ArkUI 响应式系统的关键技巧——@Observed 类的属性变更虽然可追踪,但数组元素属性变更需要配合新引用才能确保列表刷新。
delOptic 方法处理删除收藏门店的逻辑。同样做边界检查后,通过 splice(delIdx, 1) 从数组中删除指定索引的门店条目。splice 方法直接修改了原数组,在 ArkUI 中数组本身的增删操作(unshift/splice/push 等)可以直接触发 ForEach 列表的刷新,不需要像属性修改那样创建新引用。最后关闭删除确认弹窗。
10.3 生命周期
aboutToAppear() {
this.setupMapCallback();
}
aboutToAppear 是 ArkUI 组件的生命周期回调,在组件实例创建后、build() 执行前被调用。这里仅调用 this.setupMapCallback() 设置地图初始化回调——注意此时只是将回调函数赋值给 this.mapCallback 状态,地图组件尚未实际渲染。当 build() 中的 MapComponent({ mapOptions, mapCallback }) 被渲染时,框架才会真正初始化地图,初始化完成后回调 mapCallback 函数,在回调中完成 controller 获取、Marker 添加和长按监听注册。这种"aboutToAppear 设置回调 → build 渲染组件 → 框架回调初始化"的异步链路是 Map Kit 的标准使用模式。
十一、页面主构建
build() {
Stack() {
Column() {
this.headerMain()
Divider().strokeWidth(1).color(COLORS.line)
Scroll() {
Column() {
if (this.currentTab === 0) {
this.tabOptic()
} else if (this.currentTab === 1) {
this.tabMap()
} else if (this.currentTab === 2) {
this.tabSearch()
} else {
this.tabMine()
}
}
.padding({ left: 14, right: 14, top: 12, bottom: 12 })
}
.layoutWeight(1)
.scrollBar(BarState.Off)
this.tabBar()
}
.width('100%')
.height('100%')
if (this.addModal) {
this.panelAdd(() => {
this.addModal = false;
})
}
if (this.editModal) {
this.panelEdit(() => {
this.editModal = false;
})
}
if (this.delModal) {
this.panelDel(() => {
this.delModal = false;
})
}
}
.width('100%')
.height('100%')
.backgroundColor(COLORS.bg)
}
build 方法是 ArkUI 组件的渲染入口。整体使用 Stack 作为最外层容器——Stack 的叠层特性让弹窗面板可以覆盖在主内容之上。Stack 内部首先是 Column 主内容列:顶部是 headerMain() 头部 Builder(渐变 Banner + 筛选 chips),接着是一条 Divider 分割线,中间是 Scroll 可滚动区域包裹的 Tab 内容,底部是 tabBar() 底部导航。
Scroll 内部的 Column 通过 if-else if-else 条件渲染根据 currentTab 的值决定显示哪个 Tab 的 Builder:currentTab === 0 显示门店 Tab,currentTab === 1 显示地图 Tab,currentTab === 2 显示搜索 Tab,其余显示我的 Tab。这种条件渲染策略在 Tab 切换时会自动销毁旧 Tab 的组件树并创建新 Tab 的组件树,确保只有一个 Tab 的内容在内存中——对于包含 MapComponent 这种重量级组件的页面,这种策略比一次性渲染所有 Tab 然后通过显隐控制更节省内存。Scroll 设置了 layoutWeight(1) 占满剩余高度、scrollBar(BarState.Off) 隐藏滚动条。
Stack 的第二层是三个条件渲染的弹窗:addModal 为 true 时渲染 panelAdd 收藏弹窗,editModal 为 true 时渲染 panelEdit 编辑弹窗,delModal 为 true 时渲染 panelDel 删除弹窗。每个弹窗接收一个 onClose 回调函数,在弹窗内部需要关闭时调用——这种回调式关闭让弹窗的关闭逻辑由调用方决定,弹窗本身只需在合适时机调用回调即可。最外层 Stack 设置了 backgroundColor(COLORS.bg) 应用瓷白底色。
十二、头部 Builder
12.1 头部渐变 Banner
@Builder
headerMain() {
Column({ space: 12 }) {
Column({ space: 10 }) {
Row({ space: 12 }) {
Column({ space: 2 }) {
Text('2 年').fontSize(30).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
Text('距上次验光').fontSize(10).fontColor(COLORS.sub)
}
.alignItems(HorizontalAlign.Start)
Column({ space: 6 }) {
Row({ space: 6 }) {
Text('👓').fontSize(14)
Text('左 -4.50 · 右 -4.25').fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
}
Row({ space: 6 }) {
Text('🔬').fontSize(12)
Text('最近门店 1.1km · 医学验光可约').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.accent)
Text('🗺 地图找店').fontSize(12).fontColor(COLORS.accent)
.padding({ left: 14, right: 14, top: 8, bottom: 8 })
.borderRadius(16).backgroundColor(COLORS.chip)
.onClick(() => { this.currentTab = 1; })
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
}
.padding(14)
.borderRadius(14)
.linearGradient({
angle: 135,
colors: [[COLORS.accentL, 0.0], [COLORS.card, 0.7]]
})
headerMain 是头部 Builder,使用 @Builder 装饰器声明。外层 Column 设置 space: 12 控制子元素间距。内层第一个 Column 是渐变 Banner 主体,包含两个部分:上半部分是复查间隔 + 度数档案信息行,下半部分是两个快捷入口胶囊。
上半部分是一个 Row,左侧 Column 显示"2 年"大号数字(30px 粗体)和"距上次验光"小号说明(10px),让用户一眼看到自己距上次验光已经过了多久——这在验光场景中是关键提醒信息,验光师通常建议每 12 个月复查一次,2 年已超出建议间隔。右侧 Column 通过三行 Row 展示度数档案(左 -4.50 · 右 -4.25)、最近门店信息(1.1km · 医学验光可约)、复查提醒(每 12 个月复查 · 已设置提醒)。右侧 Column 设置 layoutWeight(1) 占满剩余宽度,alignItems(HorizontalAlign.Start) 左对齐。
下半部分是两个快捷入口胶囊:左侧"预约验光"用青瓷绿底色 + 瓷白文字(强引导主操作),右侧"地图找店"用青瓷白色底色 + 青瓷绿文字(弱引导次操作),点击后切换到地图 Tab。两个胶囊通过 justifyContent(FlexAlign.SpaceBetween) 分列两端。整个渐变 Banner 使用 linearGradient 设置 135 度渐变,从淡青色 COLORS.accentL(0% 位置)渐变到纯白 COLORS.card(70% 位置),营造出柔和的青瓷光泽效果。
12.2 筛选 chips 横滑
Scroll() {
Row({ space: 8 }) {
ForEach(CATE_TAGS, (tag: string, idx: number) => {
Text(tag)
.fontSize(11)
.fontColor(this.cateIdx === idx ? COLORS.bg : COLORS.sub)
.padding({ left: 12, right: 12, top: 6, bottom: 6 })
.borderRadius(14)
.backgroundColor(this.cateIdx === idx ? COLORS.accent : COLORS.chip)
.onClick(() => { this.cateIdx = idx; })
}, (tag: string) => tag)
}
}
.scrollable(ScrollDirection.Horizontal)
.scrollBar(BarState.Off)
.width('100%')
}
.padding({ left: 14, right: 14, top: 12, bottom: 8 })
.width('100%')
}
头部 Builder 的下半部分是筛选 chips 横滑区域。使用 Scroll 组件设置 scrollable(ScrollDirection.Horizontal) 水平滚动,内含一个 Row 包裹的 ForEach 遍历 CATE_TAGS 数组的 8 个标签。每个标签是一个 Text 组件,根据 cateIdx === idx 的比较结果决定样式:选中时用瓷白文字 + 青瓷绿底色高亮,未选中时用灰绿色文字 + 青瓷白色底色。点击标签更新 cateIdx 状态。ForEach 的第三个参数 (tag: string) => tag 是键值生成器,用标签文案本身作为唯一键,确保列表 diff 的高效性。整个头部 Builder 外层 Column 设置 padding({ left: 14, right: 14, top: 12, bottom: 8 }) 与页面边距对齐。
十三、门店 Tab
@Builder
tabOptic() {
Column({ space: 10 }) {
Row() {
Text('附近验光门店').fontSize(15).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
Blank()
Text('收藏新门店 +').fontSize(11).fontColor(COLORS.accent)
.onClick(() => { this.addModal = true; })
}
.width('100%')
门店 Tab 是业务主 Tab,tabOptic Builder 使用 Column({ space: 10 }) 组织内容。首先是区块标题行——左侧"附近验光门店"标题(15px 粗体墨绿黑),Blank() 占据中间空间,右侧"收藏新门店 +"链接(11px 青瓷绿),点击打开收藏弹窗(addModal = true)。Blank() 组件是 ArkUI 的弹性占位组件,在 Row 中自动占据剩余空间,将两侧元素分推到两端。
13.1 推荐门店大卡
ForEach(OPTIC_RECS, (rec: OpticRec) => {
Row({ space: 10 }) {
Text(rec.icon).fontSize(26)
Column({ space: 4 }) {
Text(rec.name).fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
Text(`${rec.price} · ${rec.device}`).fontSize(11).fontColor(COLORS.sub)
Row({ space: 6 }) {
Text(rec.dist).fontSize(10).fontColor(COLORS.text3)
Text(`今日可约 ${rec.book} 段`).fontSize(10).fontColor(bookColor(rec.book))
}
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Column({ space: 4 }) {
Text('去验光').fontSize(11).fontColor(COLORS.bg).fontWeight(FontWeight.Bold)
.padding({ left: 12, right: 12, top: 6, bottom: 6 })
.borderRadius(12).backgroundColor(COLORS.accent)
.onClick(() => { this.currentTab = 1; })
}
}
.padding(12)
.borderRadius(12)
.backgroundColor(COLORS.card)
.width('100%')
}, (rec: OpticRec) => rec.name)
推荐门店大卡通过 ForEach 遍历 OPTIC_RECS 的 3 条精选数据。每张卡片是一个 Row:左侧大号 emoji 图标(26px),中间 Column 展示门店名(13px 粗体)、验光价和设备组合(11px)、距离和可约时段数(10px)——可约时段数通过 bookColor(rec.book) 函数映射为颜色,≥6 段青瓷绿、≥2 段琥珀棕、其余警示色。右侧"去验光"按钮用青瓷绿底色 + 瓷白文字,点击切换到地图 Tab。每张卡片用纯白底色(COLORS.card)在瓷白页面上制造层次感,12px 圆角和 12px 内边距让卡片视觉舒适。
13.2 护眼速览与收藏列表
Row({ space: 8 }) {
ForEach(VISION_STATS, (stat: VisionStat) => {
Column({ space: 4 }) {
Text(stat.icon).fontSize(16)
Text(stat.value).fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLORS.accent)
Text(stat.label).fontSize(10).fontColor(COLORS.sub)
}
.layoutWeight(1)
.padding({ top: 10, bottom: 10 })
.borderRadius(12)
.backgroundColor(COLORS.card)
}, (stat: VisionStat) => stat.label)
}
.width('100%')
护眼速览是横排三列小卡,通过 ForEach 遍历 VISION_STATS 的 3 项指标。每张小卡是一个 Column:顶部 emoji 图标(16px),中间指标值(13px 粗体青瓷绿),底部指标名(10px 灰绿色)。每个 Column 设置 layoutWeight(1) 三等分宽度,纯白底色 + 12px 圆角。这组速览卡让用户快速了解验光档案份数、复查提醒条数和会员折扣力度。
13.3 收藏门店全列表
Row() {
Text('全部门店(地图 Marker 同源)').fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
}
.width('100%')
ForEach(this.opticList, (optic: OpticItem, idx: number) => {
Column({ space: 8 }) {
Row({ space: 10 }) {
Text(optic.icon).fontSize(22)
Column({ space: 3 }) {
Text(optic.name).fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
Text(`${optic.price} · ${optic.device}`).fontSize(11).fontColor(COLORS.sub)
Text(`备注:${optic.note}`).fontSize(10).fontColor(COLORS.text3)
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Column({ space: 6 }) {
Text(`${optic.book}`).fontSize(16).fontWeight(FontWeight.Bold)
.fontColor(bookColor(optic.book))
Text('段可约').fontSize(9).fontColor(COLORS.text3)
}
}
.width('100%')
Row({ space: 8 }) {
Text('编辑').fontSize(10).fontColor(COLORS.info)
.padding({ left: 10, right: 10, top: 4, bottom: 4 })
.borderRadius(10).backgroundColor(COLORS.chip)
.onClick(() => { this.openEditOptic(idx); })
Text('删除').fontSize(10).fontColor(COLORS.danger)
.padding({ left: 10, right: 10, top: 4, bottom: 4 })
.borderRadius(10).backgroundColor(COLORS.chip)
.onClick(() => { this.delIdx = idx; this.delModal = true; })
}
.justifyContent(FlexAlign.End)
.width('100%')
}
.padding(12)
.borderRadius(12)
.backgroundColor(COLORS.card)
.width('100%')
}, (optic: OpticItem) => optic.name)
收藏门店全列表的标题"全部门店(地图 Marker 同源)"特意标注了与地图 Marker 的数据同源关系——这些门店的地理坐标与地图 Tab 中添加的 Marker 标注来自相同的数据源,让用户理解门店列表和地图标注是同一份数据的两种呈现。
列表通过 ForEach 遍历 this.opticList(注意是 @State 状态变量而非静态常量,支持增删改)。每条门店卡片分为上下两部分:上半部分是门店信息行——左侧 emoji 图标(22px),中间 Column 展示门店名、验光价和设备组合、用户备注,右侧可约时段数大号数字(16px 粗体,颜色通过 bookColor 映射)和"段可约"小字说明。下半部分是操作按钮行——"编辑"按钮用灰蓝色文字 + 青瓷白底色,点击调用 openEditOptic(idx) 打开编辑弹窗;"删除"按钮用警示色文字 + 青瓷白底色,点击设置 delIdx 并打开删除确认弹窗。按钮行通过 justifyContent(FlexAlign.End) 右对齐。ForEach 的键值生成器用门店名作为唯一键。
13.4 青少年护眼贴士
Column({ space: 6 }) {
Text('💡 青少年护眼贴士').fontSize(12).fontWeight(FontWeight.Bold).fontColor(COLORS.second)
Text('16 岁以下首次验光建议散瞳检查以排除假性近视,门店提供免费近视防控建档,每半年随访一次眼轴变化。')
.fontSize(10).fontColor(COLORS.sub).lineHeight(16)
}
.padding(12)
.borderRadius(12)
.backgroundColor(COLORS.chip)
.width('100%')
}
.width('100%')
}
门店 Tab 的底部是青少年护眼贴士卡,用琥珀棕色标题"💡 青少年护眼贴士"和灰绿色正文,内容为专业视光建议:"16 岁以下首次验光建议散瞳检查以排除假性近视,门店提供免费近视防控建档,每半年随访一次眼轴变化。"这条贴士体现了验光门店的专业属性——散瞳检查是排除假性近视的标准流程,眼轴随访是青少年近视防控的核心指标。贴士卡使用青瓷白色底色(COLORS.chip)与纯白卡片形成区分,lineHeight(16) 设置行高提升多行文字的可读性。
十四、地图 Tab — 长按事件特性页
14.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%')
Row({ space: 12 }) {
Row({ space: 6 }) {
Toggle({ type: ToggleType.Switch, isOn: this.markerListenOn })
.selectedColor(COLORS.accent)
.width(36)
.height(20)
.onChange(() => { this.toggleMarkerListen(); })
Text('Marker长按').fontSize(11).fontColor(COLORS.sub)
}
Row({ space: 6 }) {
Toggle({ type: ToggleType.Switch, isOn: this.poiListenOn })
.selectedColor(COLORS.accent)
.width(36)
.height(20)
.onChange(() => { this.togglePoiListen(); })
Text('POI长按').fontSize(11).fontColor(COLORS.sub)
}
}
.width('100%')
地图 Tab 是 HarmonyOS 6.1.1 MapEventManager 长按事件监听特性的展示页。首先是特性说明卡——标题"🗺 Map Kit 6.1.1 · 长按事件监听"(13px 粗体)和说明文案"长按地图上的门店 Marker 或 POI 地点,事件将记录到下方日志流"(10px),让用户理解这个 Tab 的功能。说明卡用青瓷白色底色与地图区域形成视觉分隔。
接着是监听开关行——两个 Toggle 开关分别控制 Marker 长按和 POI 长按监听的开关状态。Toggle 使用 ToggleType.Switch 样式(iOS 风格拨动开关),isOn 绑定 markerListenOn/poiListenOn 状态,selectedColor 设置为青瓷绿主色,onChange 事件分别调用 toggleMarkerListen()/togglePoiListen() 方法。开关旁的文案"Marker长按"/"POI长按"帮助用户识别两个开关的功能。用户可以通过这两个开关自由控制是否监听对应类型的长按事件——关闭后长按不再产生日志,开启后恢复监听。
14.2 MapComponent 本体
MapComponent({ mapOptions: this.mapOptions, mapCallback: this.mapCallback })
.layoutWeight(1)
.width('100%')
.borderRadius(12)
MapComponent 是 Map Kit 的核心组件,接收两个参数:mapOptions 是地图初始化参数(已在组件状态中定义为南京中心点 zoom 13 级),mapCallback 是地图初始化回调(已在 aboutToAppear 中通过 setupMapCallback 设置)。组件设置 layoutWeight(1) 占满中间区域的剩余高度,width('100%') 占满宽度,borderRadius(12) 设置圆角。地图组件渲染后,框架会异步初始化地图引擎,初始化完成后回调 mapCallback 函数,在回调中完成 controller 获取、6 个门店 Marker 的批量添加、以及 6.1.1 双长按监听的注册。
14.3 长按事件日志流
Column({ space: 6 }) {
Row() {
Text('长按事件日志流').fontSize(12).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
Blank()
Text(`共 ${this.eventLogs.length} 条`).fontSize(10).fontColor(COLORS.text3)
}
.width('100%')
Scroll() {
Column({ space: 6 }) {
ForEach(this.eventLogs, (log: EventLog) => {
Row({ space: 8 }) {
Text(log.type === 'Marker' ? '📍' : '🏷')
.fontSize(12)
Column({ space: 2 }) {
Row({ space: 6 }) {
Text(log.type).fontSize(10).fontColor(log.type === 'Marker' ? COLORS.accent : COLORS.second)
Text(log.name).fontSize(11).fontColor(COLORS.title)
Text(log.time).fontSize(9).fontColor(COLORS.text3)
}
Text(`${log.lat.toFixed(4)}, ${log.lng.toFixed(4)}`)
.fontSize(9).fontColor(COLORS.text3).fontFamily('monospace')
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
}
.padding({ left: 8, right: 8, top: 6, bottom: 6 })
.borderRadius(8)
.backgroundColor(COLORS.card)
.width('100%')
}, (log: EventLog) => `${log.type}-${log.name}-${log.time}`)
}
}
.scrollBar(BarState.Off)
.height(120)
.width('100%')
}
.padding(10)
.borderRadius(12)
.backgroundColor(COLORS.chip)
.width('100%')
}
.width('100%')
.height('100%')
}
长按事件日志流是地图 Tab 的底部区域,展示所有长按事件的记录。日志流容器用青瓷白色底色包裹,标题行左侧"长按事件日志流"(12px 粗体),右侧实时显示日志总数共 ${this.eventLogs.length} 条——每次新增日志后 eventLogs 数组长度变化,这个数字会自动更新。
日志列表通过 ForEach 遍历 this.eventLogs 数组。每条日志是一个 Row:左侧根据 log.type 显示不同 emoji(Marker 用 📍 定位针,POI 用 🏷 标签),右侧 Column 展示事件详情——第一行是类型标签(Marker 用青瓷绿、POI 用琥珀棕)、名称(Marker 显示 ID 如 #0,POI 显示名称如"南京图书馆")、时间文案;第二行是经纬度坐标(toFixed(4) 保留 4 位小数,fontFamily('monospace') 等宽字体)。日志列表使用 Scroll 包裹并设置 height(120) 固定高度,超出部分可滚动,scrollBar(BarState.Off) 隐藏滚动条。新事件通过 unshift 置顶到列表最前方,配合 @Observed 装饰器实现日志流的实时刷新。ForEach 的键值生成器用 type-name-time 组合字符串确保唯一性。
十五、搜索 Tab — reliability 相关性分数特性页
15.1 特性说明与搜索框
@Builder
tabSearch() {
Column({ space: 10 }) {
Column({ space: 4 }) {
Text('🔍 searchByText · reliability 相关性评分').fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
Text('Site 新增 reliability 字段([0,1],1 为完全相关),判断结果与关键字关联程度')
.fontSize(10).fontColor(COLORS.sub)
}
.padding(10)
.borderRadius(10)
.backgroundColor(COLORS.chip)
.width('100%')
Row({ space: 8 }) {
TextInput({ text: this.queryInput, placeholder: '输入关键字,如:眼镜店' })
.layoutWeight(1)
.height(38)
.fontSize(12)
.fontColor(COLORS.title)
.placeholderColor(COLORS.text3)
.backgroundColor(COLORS.card)
.onChange((v: string) => { this.queryInput = v; })
Button('搜索')
.height(38)
.fontSize(12)
.backgroundColor(COLORS.accent)
.onClick(() => { this.runSearch(); })
}
.width('100%')
Text(this.searchState).fontSize(10).fontColor(COLORS.text3).width('100%')
搜索 Tab 是 HarmonyOS 6.1.1 site.searchByText + Site.reliability 新特性的展示页。特性说明卡的标题"🔍 searchByText · reliability 相关性评分"和说明文案"Site 新增 reliability 字段([0,1],1 为完全相关),判断结果与关键字关联程度",让用户理解这个 Tab 展示的是搜索结果的相关性评分能力。
搜索框区域是 Row 包裹的 TextInput + Button:TextInput 绑定 queryInput 状态,placeholder 为"输入关键字,如:眼镜店",layoutWeight(1) 占满剩余宽度,height(38) 固定高度,纯白底色在瓷白页面上制造层次。“搜索"按钮用青瓷绿底色,点击调用 runSearch() 方法发起搜索。搜索状态文案 Text(this.searchState) 在搜索框下方显示,包括"待搜索 · 演示数据”、“搜索中…”、“返回 N 条结果”、“无结果 · 保留演示数据”、"搜索失败(错误码) · 保留演示数据"等多种状态,让用户随时了解搜索进度和结果。
15.2 搜索结果列表与分数条
List({ space: 8 }) {
ForEach(this.searchRecords, (rec: SearchRecord) => {
ListItem() {
Column({ space: 6 }) {
Row() {
Text(rec.name).fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
.layoutWeight(1)
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Text(reliabilityScore(rec.reliability).label)
.fontSize(10)
.fontColor(reliabilityScore(rec.reliability).color)
.padding({ left: 8, right: 8, top: 3, bottom: 3 })
.borderRadius(8)
.backgroundColor(COLORS.chip)
}
.width('100%')
Text(rec.address).fontSize(11).fontColor(COLORS.sub).width('100%')
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
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%')
搜索结果列表是 reliability 特性的核心展示区域。使用 List({ space: 8 }) 包裹 ForEach,每个搜索结果条目必须用 ListItem 包裹——这是 ArkUI List 组件的强制要求,List 的直接子组件只能是 ListItem。
每条搜索结果卡片包含四行信息:第一行是地点名称(13px 粗体墨绿黑,maxLines(1) + textOverflow({ overflow: TextOverflow.Ellipsis }) 超长省略)和等级标签——等级标签通过 reliabilityScore(rec.reliability) 函数获取文案和颜色,≥0.8"高相关"青瓷绿、≥0.5"中相关"琥珀棕、<0.5"低相关"警示色。第二行是格式化地址(11px 灰绿色,同样超长省略)。第三行是 reliability 分数条——Progress 组件设置 value: rec.reliability * 100(将 0-1 的分数映射为 0-100 的进度值)、total: 100、type: ProgressType.Linear 线性进度条,color 与等级标签颜色一致,height(6) 细条样式。分数条右侧显示 reliability ${rec.reliability.toFixed(2)} 数值文本(10px 等宽字体,保留 2 位小数)。第四行是直线距离(米转公里,保留 2 位小数)和时间文案。
分数条 + 等级标签 + 数值文本的三重展示让用户可以从视觉条形、颜色标签、精确数值三个维度理解每条搜索结果的相关性。例如 reliability 0.95 的结果会显示满格青瓷绿分数条 + "高相关"青瓷绿标签 + "reliability 0.95"数值文本;reliability 0.13 的结果会显示极短警示色分数条 + "低相关"警示色标签 + "reliability 0.13"数值文本,让用户一目了然地识别高相关和低相关结果。ForEach 的键值生成器用 name-reliability 组合字符串确保唯一性。
15.3 双特性代码预览卡
this.codePreviewCard()
}
.width('100%')
.height('100%')
}
搜索 Tab 的底部调用 this.codePreviewCard() 展示双特性代码预览卡,让用户在搜索结果列表下方直接看到 6.1.1 双新特性的核心调用代码,形成"特性说明 → 搜索操作 → 结果展示 → 代码速览"的完整体验闭环。
十六、我的 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('每年免费验光 2 次 · 镜架 85 折').fontSize(11).fontColor(COLORS.sub)
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
}
.width('100%')
Divider().strokeWidth(1).color(COLORS.line)
Row() {
Text('已配镜 3 副').fontSize(11).fontColor(COLORS.sub)
Blank()
Text('度数稳定 -0.25').fontSize(11).fontColor(COLORS.accent)
}
.width('100%')
}
.padding(14)
.borderRadius(14)
.linearGradient({
angle: 135,
colors: [[COLORS.accentL, 0.0], [COLORS.card, 0.75]]
})
.width('100%')
我的 Tab 以会员渐变大卡开场。大卡使用 linearGradient 135 度渐变(从淡青色到纯白 75% 位置),与头部 Banner 渐变风格一致但角度略不同。大卡上半部分是大号眼镜 emoji(34px)和会员信息——"视界会员 · 蓝光守护"标题(15px 粗体)和"每年免费验光 2 次 · 镜架 85 折"权益摘要(11px)。Divider 分割后下半部分是"已配镜 3 副"灰绿色文案和"度数稳定 -0.25"青瓷绿文案,通过 Blank() 分列两端。这条度数变化信息是验光场景的关键指标——度数稳定说明视力状况良好,度数变化大需要关注。
16.1 会员权益卡
Column({ space: 8 }) {
Text('👓 会员护眼权益').fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
Row({ space: 8 }) {
Text('·').fontSize(12).fontColor(COLORS.second)
Text('每年 2 次免费医学验光,含眼轴与眼压检查').fontSize(11).fontColor(COLORS.sub)
}
.width('100%')
Row({ space: 8 }) {
Text('·').fontColor(COLORS.second).fontSize(12)
Text('镜架会员价 85 折,防蓝光镜片免费升级').fontSize(11).fontColor(COLORS.sub)
}
.width('100%')
Row({ space: 8 }) {
Text('·').fontSize(12).fontColor(COLORS.second)
Text('镜架终身免费清洗整形,螺丝松脱随时修').fontSize(11).fontColor(COLORS.sub)
}
.width('100%')
}
.padding(12)
.borderRadius(12)
.backgroundColor(COLORS.card)
.width('100%')
会员权益卡用纯白底色展示三条权益说明,每条用琥珀棕色"·"项目符号 + 灰绿色正文:每年 2 次免费医学验光含眼轴与眼压检查、镜架会员价 85 折防蓝光镜片免费升级、镜架终身免费清洗整形螺丝松脱随时修。这三条权益覆盖了验光、配镜、售后三个环节,体现了验光门店会员体系的全生命周期服务承诺。
16.2 功能清单
ForEach(this.funcList, (item: FuncItem) => {
Row({ space: 10 }) {
Text(item.icon).fontSize(18)
Text(item.label).fontSize(13).fontColor(COLORS.title).layoutWeight(1)
Text(item.value).fontSize(11).fontColor(COLORS.sub)
Text('›').fontSize(14).fontColor(COLORS.text3)
}
.padding(12)
.borderRadius(12)
.backgroundColor(COLORS.card)
.width('100%')
}, (item: FuncItem) => item.label)
}
.width('100%')
}
功能清单通过 ForEach 遍历 this.funcList 的 8 条数据。每条功能行是一个 Row:左侧 emoji 图标(18px),中间功能名(13px 墨绿黑,layoutWeight(1) 占满中间),右侧状态/数值文本(11px 灰绿色),最右箭头符号"›"(14px 浅灰绿色)引导用户点击进入。8 条功能覆盖验光档案(3 份 · 最近 2 个月前)、配镜订单(历史 3 副)、度数变化(左 -4.50 · 右 -4.25)、会员卡包(视界卡 · 85 折)、收藏门店(5 家)、消费账单(本年 3 笔)、常去门店(新街口 / 珠江路)、偏好设置(每年复查提醒),构成了完整的个人视光管理功能入口。每行用纯白底色 + 12px 圆角,视觉上统一整洁。
十七、双特性代码预览卡
@Builder
codePreviewCard() {
Column({ space: 6 }) {
Text('⌨️ Map Kit 6.1.1 双新特性速览').fontSize(12).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
Column({ space: 4 }) {
Text('const result = await site.searchByText(params)')
.fontSize(9).fontColor(COLORS.accentL).fontFamily('monospace')
Text('const score = site.reliability // 门店相关性')
.fontSize(9).fontColor(COLORS.accentL).fontFamily('monospace')
Text('eventManager.onMarkerLongClick(cb) // 24+')
.fontSize(9).fontColor(COLORS.second).fontFamily('monospace')
Text('eventManager.onPoiLongClick(cb) // 24+')
.fontSize(9).fontColor(COLORS.second).fontFamily('monospace')
}
.padding(10)
.borderRadius(8)
.backgroundColor(COLORS.codeBg)
.width('100%')
}
.padding(10)
.borderRadius(10)
.backgroundColor(COLORS.chip)
.width('100%')
}
codePreviewCard 是双特性代码预览卡 Builder,位于搜索 Tab 的底部。外层用青瓷白色底色 + 10px 圆角包裹,标题"⌨️ Map Kit 6.1.1 双新特性速览"(12px 粗体墨绿黑)。内层用深青绿色底色(COLORS.codeBg = '#1B2A22')——浅色主题中特例使用深底放代码,让代码文字在深色背景上有更好的可读性,这是代码展示的通用设计原则。
四行代码使用 fontFamily('monospace') 等宽字体展示 6.1.1 双新特性的核心调用:前两行用淡青色(COLORS.accentL)展示搜索特性——const result = await site.searchByText(params) 和 const score = site.reliability // 门店相关性;后两行用琥珀棕色(COLORS.second)展示长按事件特性——eventManager.onMarkerLongClick(cb) 和 eventManager.onPoiLongClick(cb)。注释 // 24+ 标注了 API 版本要求(API 24 及以上即 HarmonyOS 6.1.1)。这四行代码浓缩了整个应用的两个技术核心,让用户在浏览搜索结果时可以随时回顾特性原理。
十八、底部导航 Tab 栏
@Builder
tabBar() {
Row() {
ForEach(TAB_LIST, (tab: TabMeta, idx: number) => {
Column({ space: 3 }) {
Text(tab.icon).fontSize(18)
Text(tab.label).fontSize(10)
.fontColor(this.currentTab === idx ? COLORS.tabOn : COLORS.text3)
}
.layoutWeight(1)
.onClick(() => { this.currentTab = idx; })
}, (tab: TabMeta) => tab.label)
}
.padding({ top: 8, bottom: 8 })
.width('100%')
.backgroundColor(COLORS.card)
}
tabBar 是底部导航 Builder,通过 ForEach 遍历 TAB_LIST 的 4 个 Tab。每个 Tab 是一个 Column:顶部 emoji 图标(18px),底部标签文案(10px)——选中时用青瓷绿色(COLORS.tabOn),未选中时用浅灰绿色(COLORS.text3)。每个 Tab 设置 layoutWeight(1) 四等分宽度,onClick 点击更新 currentTab 状态切换 Tab。整个 Tab 栏用纯白底色(COLORS.card)与瓷白页面底色形成区分,padding({ top: 8, bottom: 8 }) 控制上下间距。ForEach 的键值生成器用 Tab 标签文案作为唯一键。
十九、弹窗系统
19.1 弹窗遮罩层
@Builder
modalOverlay(onClose: () => void) {
Column()
.width('100%')
.height('100%')
.backgroundColor(COLORS.mask)
.onClick(() => { onClose(); })
}
modalOverlay 是弹窗系统的通用遮罩 Builder,接收一个 onClose 回调函数。遮罩是一个全屏的 Column,背景色为 COLORS.mask(rgba(40,52,44,0.42),墨绿色 42% 透明度)。遮罩的 onClick 绑定 onClose 回调,用户点击遮罩区域即可关闭弹窗——这是弹窗交互的标准设计,点击遮罩关闭比强制点击关闭按钮更符合用户直觉。遮罩作为公共 Builder 被三个弹窗面板复用,避免了重复代码。
19.2 收藏门店弹窗
@Builder
panelAdd(onClose: () => void) {
Stack() {
this.modalOverlay(onClose)
Column({ space: 12 }) {
Text('收藏新门店').fontSize(15).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
TextInput({ placeholder: '门店名称', text: this.formName })
.height(38)
.fontSize(12)
.fontColor(COLORS.title)
.placeholderColor(COLORS.text3)
.backgroundColor(COLORS.chip)
.onChange((v: string) => { this.formName = v; })
TextInput({ placeholder: '地址(可留空地图选点)', text: this.formAddr })
.height(38)
.fontSize(12)
.fontColor(COLORS.title)
.placeholderColor(COLORS.text3)
.backgroundColor(COLORS.chip)
.onChange((v: string) => { this.formAddr = v; })
Row({ space: 10 }) {
Button('取消')
.layoutWeight(1)
.fontSize(12)
.backgroundColor(COLORS.chip)
.fontColor(COLORS.sub)
.onClick(() => { onClose(); })
Button('收藏')
.layoutWeight(1)
.fontSize(12)
.backgroundColor(COLORS.accent)
.onClick(() => { this.saveOptic(); })
}
.width('100%')
}
.padding(16)
.borderRadius(14)
.backgroundColor(COLORS.card)
.width('82%')
}
.width('100%')
.height('100%')
}
收藏门店弹窗 panelAdd 使用 Stack 叠层遮罩和面板。面板宽度为 82%,居中显示在遮罩之上(Stack 默认居中)。面板内含标题"收藏新门店"(15px 粗体)、门店名称输入框、地址输入框(placeholder"地址(可留空地图选点)"提示地址可留空)、取消和收藏两个按钮。两个输入框绑定 formName 和 formAddr 状态,onChange 更新状态值。按钮区域"取消"用青瓷白底色 + 灰绿色文字(弱操作),"收藏"用青瓷绿底色(强操作),两个按钮 layoutWeight(1) 等宽分布。点击"收藏"调用 saveOptic() 方法保存新门店并关闭弹窗。
19.3 编辑备注弹窗
@Builder
panelEdit(onClose: () => void) {
Stack() {
this.modalOverlay(onClose)
Column({ space: 12 }) {
Text('编辑门店备注').fontSize(15).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
Text(this.editIdx < this.opticList.length ? this.opticList[this.editIdx].name : '')
.fontSize(11)
.fontColor(COLORS.sub)
.width('100%')
TextInput({ placeholder: '输入新备注', text: this.editNote })
.height(38)
.fontSize(12)
.fontColor(COLORS.title)
.placeholderColor(COLORS.text3)
.backgroundColor(COLORS.chip)
.onChange((v: string) => { this.editNote = v; })
Row({ space: 10 }) {
Button('取消')
.layoutWeight(1)
.fontSize(12)
.backgroundColor(COLORS.chip)
.fontColor(COLORS.sub)
.onClick(() => { onClose(); })
Button('保存')
.layoutWeight(1)
.fontSize(12)
.backgroundColor(COLORS.accent)
.onClick(() => { this.updateOptic(); })
}
.width('100%')
}
.padding(16)
.borderRadius(14)
.backgroundColor(COLORS.card)
.width('82%')
}
.width('100%')
.height('100%')
}
编辑备注弹窗 panelEdit 的结构与收藏弹窗类似但更精简。标题"编辑门店备注"下方显示当前门店名(通过 this.editIdx < this.opticList.length 边界检查后读取 this.opticList[this.editIdx].name),让用户确认正在编辑哪家门店。备注输入框绑定 editNote 状态——该状态在 openEditOptic 方法中被预填充了当前门店的备注值,实现编辑表单的回填。用户修改后点击"保存"调用 updateOptic() 方法,通过 slice() 创建数组新引用触发列表刷新。
19.4 删除确认弹窗
@Builder
panelDel(onClose: () => void) {
Stack() {
this.modalOverlay(onClose)
Column({ space: 12 }) {
Text('删除收藏门店').fontSize(15).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
Text(this.delIdx < this.opticList.length
? `确定删除「${this.opticList[this.delIdx].name}」吗?` : '确定删除吗?')
.fontSize(12)
.fontColor(COLORS.sub)
.width('100%')
Row({ space: 10 }) {
Button('取消')
.layoutWeight(1)
.fontSize(12)
.backgroundColor(COLORS.chip)
.fontColor(COLORS.sub)
.onClick(() => { onClose(); })
Button('删除')
.layoutWeight(1)
.fontSize(12)
.backgroundColor(COLORS.danger)
.onClick(() => { this.delOptic(); })
}
.width('100%')
}
.padding(16)
.borderRadius(14)
.backgroundColor(COLORS.card)
.width('82%')
}
.width('100%')
.height('100%')
}
删除确认弹窗 panelDel 比收藏和编辑弹窗更简洁,因为不需要输入框。标题"删除收藏门店"下方显示确认文案——通过边界检查后读取门店名拼接为"确定删除「门店名」吗?"的问句,让用户明确知道将要删除的是哪家门店。按钮区域"取消"用青瓷白底色(弱操作),"删除"用警示色底色(COLORS.danger,强破坏性操作),红色在 UI 设计中代表警示和危险,提醒用户这是一个需要谨慎确认的操作。点击"删除"调用 delOptic() 方法通过 splice 删除门店条目。
二十、技术特性对比
| 特性维度 | 本应用实现方案 | 传统实现方案 | 优势说明 |
|---|---|---|---|
| 搜索结果相关性 | site.searchByText 返回 Site.reliability 分数 [0,1] | 仅返回结果列表,无相关性量化 | 分数条 + 等级标签 + 颜色编码三重展示,用户可精确判断每条结果与关键字的关联程度 |
| 相关性分级展示 | reliabilityScore 函数三档映射(≥0.8 高/≥0.5 中/其余低) | 无分级,所有结果同等展示 | 高中低三档 + 青瓷绿/琥珀棕/警示色颜色编码,一目了然识别高相关结果 |
| 地图标注长按 | MapEventManager.onMarkerLongClick 监听 Marker 长按 | 仅支持 Marker 点击事件 | 长按维度捕获用户深层意图,可触发收藏、详情、导航等二次操作 |
| POI 长按监听 | MapEventManager.onPoiLongClick 监听原生 POI 长按 | 不支持 POI 长按,仅支持自定义 Marker | 覆盖地图上所有原生兴趣点,长按地标建筑即可记录,监听范围更广 |
| 监听开关控制 | offMarkerLongClick/offPoiLongClick 不传参清除全部订阅 | 需保存回调引用才能移除监听 | 无需管理回调引用,一键开关,API 设计简洁 |
| 事件日志流 | EventLog 数据模型 + unshift 置顶 + @Observed 响应式 | 手动维护日志列表,需手动刷新 | 新事件实时置顶展示,类型/名称/坐标/时间结构化记录,自动刷新 |
| 地图初始化 | mapCallback 异步回调 + controller 就绪后注册监听 | 同步初始化,监听注册时机不确定 | 确保控制器和事件管理器就绪后再注册监听,避免空指针异常 |
| Marker 批量添加 | addMarker 逐个 await + try-catch 包裹 | 批量同步添加,单个失败影响全局 | 单个 Marker 添加失败不影响后续,防御性编程保证健壮性 |
| 数据响应式刷新 | @Observed 类 + @State 数组 + slice 创建新引用 | 手动调用 notifyDataSetChanged | 属性修改配合 slice 新引用确保 ForEach 列表刷新,精准局部更新 |
| 弹窗系统 | Stack 叠层 + 条件渲染 + 回调式关闭 + 公共遮罩 | Dialog 组件或第三方弹窗库 | 无外部依赖,遮罩与面板解耦复用,关闭逻辑灵活可控 |
| 主题方案 | 瓷白底 + 青瓷绿 + 琥珀棕浅色三色系 | Material Design 标准色板或深色主题 | 护眼温润、专业可信赖、品牌辨识度高,契合视光行业调性 |
| 布局策略 | 四 Tab 完全差异化(渐变大卡/长按事件/分数条/会员卡) | 统一列表式布局 | 视觉丰富度高,功能区分明确,避免审美疲劳 |
| 搜索失败兜底 | catch BusinessError + 保留 Mock 数据 + 错误码展示 | 异常冒泡或空白页 | 任何环境下都有可展示内容,错误码可追溯,用户体验不中断 |
| 度数档案展示 | 头部 Banner 双眼度数 + 我的页度数变化趋势 | 仅在门店详情页展示 | 核心指标首屏可见,度数稳定/变化一眼判断,强化复查提醒 |
二十一、总结
本应用完整展示了 HarmonyOS ArkUI 框架在专业视光验配服务场景下的工程实践能力,核心围绕 Map Kit 的 HarmonyOS 6.1.1 两大新特性——site.searchByText 返回的 Site.reliability 相关性分数字段和 MapEventManager 新增的 onMarkerLongClick/onPoiLongClick 长按事件监听展开。从整体架构来看,代码通过颜色系统、常量定义、辅助函数、数据模型、组件主体、Builder 函数群六个层次的清晰划分,实现了高内聚低耦合的代码组织。颜色系统集中管理 16 个主题色字段(包括专属的 codeBg 深底代码背景色),为未来主题切换预留了接口契约;常量定义将所有 Mock 数据和配置选项抽离为独立常量,便于后续替换为真实接口数据;辅助函数封装了相关性分数等级映射和可约时段颜色映射等可复用逻辑;数据模型使用 @Observed 装饰器实现响应式数据追踪。
Map Kit 6.1.1 的 reliability 相关性分数特性是本应用的技术灵魂之一。通过 site.searchByText(params) 传入搜索关键字、位置中心点、搜索半径和语言参数后,返回的 SearchByTextResult 中的每个 Site 对象都携带了 reliability 字段——这是一个取值范围 [0, 1] 的浮点数,1 表示搜索结果与关键字完全相关,0 表示完全不相关。应用通过 reliabilityScore 辅助函数将分数映射为三档等级:≥0.8 为"高相关"用青瓷绿主色,≥0.5 为"中相关"用琥珀棕副色,<0.5 为"低相关"用警示色。在搜索结果列表中,每条结果都用 Progress 线性进度条 + 等级标签 + 数值文本三重展示相关性——分数条长度映射 0-1 分数为 0-100% 进度,颜色与等级标签一致,数值文本用等宽字体保留 2 位小数。这种三重展示让用户可以从视觉条形、颜色标签、精确数值三个维度理解每条搜索结果的关联程度,在高相关结果中快速筛选出真正想要的验光门店,过滤掉低相关度的无关结果。
Map Kit 6.1.1 的长按事件监听特性是本应用的另一技术核心。MapEventManager 新增的 onMarkerLongClick 监听用户长按地图上自定义标注 Marker 的行为,回调参数是 map.Marker 对象,通过 marker.getId() 获取标注 ID、通过 marker.getPosition() 获取标注经纬度。onPoiLongClick 监听用户长按地图上原生 POI 兴趣点的行为,回调参数是 mapCommon.Poi 对象,包含 name 和 position 字段。配套的 offMarkerLongClick/offPoiLongClick 不传参即清除该类型的全部订阅。应用通过 EventLog 数据模型统一了两种长按事件的数据结构——type 字段区分 Marker 和 POI,name 字段存储 Marker ID 或 POI 名称,lat/lng 字段统一存储坐标,time 字段记录时间。新事件通过 unshift 方法置顶到日志流列表最前方,配合 @Observed 装饰器实现日志流的实时刷新。地图 Tab 还提供了两个 Toggle 开关,让用户可以自由控制 Marker 长按和 POI 长按监听的开关状态。
地图初始化的异步回调链路设计体现了工程严谨性。aboutToAppear 生命周期中调用 setupMapCallback() 设置地图回调函数,但此时地图组件尚未实际渲染。当 build() 中的 MapComponent({ mapOptions, mapCallback }) 被渲染时,框架才真正初始化地图引擎,初始化完成后回调 mapCallback 函数。在回调函数中,首先检查 err 是否为空——如果地图初始化失败,输出错误日志后直接返回不执行后续逻辑;如果初始化成功,依次完成 controller 获取、eventManager 获取、6 个门店 Marker 的批量添加、以及 6.1.1 双长按监听的注册。Marker 的批量添加采用逐个 await + try-catch 的策略——每个 addMarker 调用返回 Promise,逐个等待完成并捕获可能抛出的 BusinessError,确保单个 Marker 添加失败不会中断后续 Marker 的添加。这种防御性编程保证了地图标注的健壮性。
四 Tab 差异化布局设计是本应用在 UI 层面的亮点。门店 Tab 采用头部渐变大卡 + 推荐门店列表 + 护眼速览三列小卡 + 收藏门店全列表 + 青少年护眼贴士的组合,信息密度高、视觉层次丰富,是验光场景的业务主入口;地图 Tab 采用特性说明卡 + 监听开关行 + MapComponent 本体 + 长按事件日志流的组合,完整展示了 6.1.1 长按事件监听能力;搜索 Tab 采用特性说明卡 + 搜索框 + 搜索状态文案 + reliability 分数条结果列表 + 双特性代码预览卡的组合,将搜索操作和相关性评分展示融为一体;我的 Tab 采用会员渐变大卡 + 权益卡 + 功能清单行的组合,简洁地组织个人信息和功能入口。每个 Tab 的布局都经过独立设计,避免了千篇一律的列表式界面。
浅色主题方案的实现也值得称道。瓷白底色 #F7F6F1 带有微弱暖调,比纯白更柔和护眼,契合视光行业"眼健康"的核心诉求。青瓷绿 #3D8B6E 和琥珀棕 #A67B4F 的冷暖对比创造了专业温润的视觉层次。多处使用 linearGradient 渐变背景——头部 Banner 用 135 度渐变、会员大卡也用 135 度渐变,从淡青色到纯白的柔和过渡营造出青瓷光泽效果。代码预览卡在浅色主题中特例使用深青绿色底色(#1B2A22),让代码文字在深色背景上有更好的可读性,这是代码展示的通用设计原则。reliability 分数条的颜色与等级标签颜色完全一致,确保视觉一致性——高相关青瓷绿、中相关琥珀棕、低相关警示色,三色编码与主题色板深度融合。
弹窗系统采用了 Stack 叠层 + 条件渲染 + 回调式关闭的架构。modalOverlay 提供通用的半透明墨绿色遮罩,panelAdd/panelEdit/panelDel 三个面板分别处理收藏、编辑、删除场景。每个面板接收 onClose 回调函数,实现了关闭逻辑的灵活委托。编辑保存时通过 slice() 创建数组新引用确保 ForEach 列表刷新——直接修改数组元素属性不会触发刷新,必须通过创建新引用让框架检测到变化,这是 ArkUI 响应式系统的关键技巧。删除确认弹窗使用警示色背景的确认按钮和"确定删除「门店名」吗?"的确认文案,遵循了破坏性操作的设计规范。搜索失败时通过 catch 捕获 BusinessError 并保留 Mock 数据,确保任何环境下都有可展示的内容,错误码可追溯便于排查。
从工程实践的角度看,本应用虽然是一个功能演示级别的单文件应用,但其代码组织方式、状态管理策略、UI 设计理念都体现了成熟的前端工程思维。@Observed + @State 的响应式数据流、Builder 函数的参数化复用、条件渲染驱动的弹窗系统、try-catch 包裹 BusinessError 的防御性编程、mapCallback 异步回调链路、reliability 字段的 ?? 空值兜底,这些模式都可以直接迁移到更大规模的模块化项目中。对于希望学习 HarmonyOS ArkUI 开发和 Map Kit 6.1.1 双新特性(searchByText reliability 相关性分数 + MapEventManager 长按事件监听)的开发者来说,本应用是一个结构完整、注释详尽、可直接运行参考的优质学习材料,尤其在专业视光验配服务这一垂直行业的场景化落地方面提供了可复用的工程范式。
附录:DevEco Studio 创建新项目与查看 SDK 版本
本章节演示如何使用 DevEco Studio 创建一个 HarmonyOS 新项目,并查看当前 IDE 已安装的 SDK 版本,适合作为其他技术博文的补充操作指南。
一、创建新项目
1.1 进入欢迎界面
启动 DevEco Studio 后,首先看到的是欢迎界面。左侧导航栏默认选中 “项目”,右侧提供三个主要入口:
- 新建项目:从头创建新项目
- 打开项目:打开本地已有项目
- 克隆仓库:从 Git 等版本控制拉取代码
点击 “新建项目” 按钮,进入项目创建向导。

1.2 选择项目模板
在弹出的"新建项目"对话框中,左侧分类标签提供了两种项目类型:
| 类型 | 说明 |
|---|---|
| 应用(Application) | 开发标准的 HarmonyOS 应用,具备完整的 Ability 生命周期 |
| 元服务(Atomic Service) | 开发轻量级的原子化服务,无需安装即可使用 |
选择 “应用” 标签后,右侧展示多种模板。对于大多数场景,推荐选择 “Empty Ability” —— 这是一个最基础的入门模板,仅包含 Hello World 功能,适合从零开始构建应用。

1.3 配置项目信息
点击 “下一步” 后,进入项目配置界面,需要填写以下核心参数:
| 配置项 | 示例值 | 说明 |
|---|---|---|
| 项目名称(Project name) | rollboat | 应用的项目名称,建议使用英文命名 |
| 包名(Bundle name) | com.rollboat.myapplication | 应用唯一标识,采用反向域名格式 |
| 保存路径(Save location) | D:\CodeFactory\rollboat | 项目本地存储路径,避免使用中文和空格 |
| 兼容 SDK(Compatible SDK) | 6.1.1(24) | 目标 HarmonyOS API 版本,点击"查看参考"可了解各版本差异 |
| 模块名称(Module name) | entry | 主模块名称,默认 entry 为应用入口模块 |
| 设备类型(Device types) | ☑ Phone | 勾选目标设备:Phone / Tablet / 2in1 / Car / Wearable / TV |
右侧预览区会实时展示当前模板的默认效果 —— 一个居中显示的 “Hello World” 文本。

1.4 完成创建
确认配置无误后,点击右下角 “完成” 按钮,IDE 将自动执行以下操作:
- 生成项目骨架(Stage 模型目录结构)
- 执行
ohpm install安装依赖 - 运行 Hvigor 构建初始化(
Build Init)
构建日志中显示 “退出代码为 0” 表示项目初始化成功。

1.5 项目结构概览
创建完成后,左侧项目面板展示的是标准的 Stage 模型 目录结构:
rollboat/
├── .hvigor/ # Hvigor 构建工具缓存
├── .idea/ # IDE 配置文件
├── AppScope/ # 应用级全局配置
│ └── app.json5
├── entry/ # 主模块(入口模块)
│ ├── src/main/ets/
│ │ ├── entryability/ # Ability 生命周期管理
│ │ │ └── EntryAbility.ets
│ │ └── pages/ # UI 页面
│ │ └── Index.ets # 首页(默认 Hello World)
│ ├── src/main/resources/ # 资源文件
│ ├── module.json5 # 模块配置
│ └── build-profile.json5 # 构建配置
├── oh_modules/ # OHPM 依赖包
├── build-profile.json5 # 工程构建配置
├── hvigorfile.ts # Hvigor 构建脚本
└── oh-package.json5 # 包管理配置
核心文件 Index.ets 的默认代码如下,采用 ArkTS 声明式 UI 语法:
@Entry
@Component
struct Index {
@State message: string = 'Hello World';
build() {
RelativeContainer() {
Text(this.message)
.id('HelloWorld')
.fontSize($r('app.float.page_text_font_size'))
.fontWeight(FontWeight.Bold)
.alignRules({
center: { anchor: '__container__', align: VerticalAlign.Center },
middle: { anchor: '__container__', align: HorizontalAlign.Center }
})
.onClick(() => {
this.message = 'Welcome';
})
}
.height('100%')
.width('100%')
}
}
| 关键语法 | 作用 |
|---|---|
@Entry | 标记为页面入口,可用于路由跳转 |
@Component | 声明为自定义组件 |
@State | 状态变量,数据变更时自动触发 UI 刷新 |
RelativeContainer | 相对布局容器,替代传统线性布局 |
.onClick() | 点击事件,此处点击后文本变为 “Welcome” |
打开右侧 Previewer(预览器),选择 Phone 设备,即可实时预览 Hello World 效果,无需连接真机或启动模拟器。

二、查看 SDK 版本
2.1 查看 HarmonyOS SDK
DevEco Studio 安装时已内置 HarmonyOS SDK,无需单独下载。通过以下路径查看:
文件 → 设置 → HarmonyOS SDK(或快捷键
Ctrl + Alt + S搜索 “HarmonyOS SDK”)
在设置面板中,可以看到当前已安装的 SDK 版本信息:
| 名称 | 阶段 | 状态 |
|---|---|---|
| HarmonyOS 6.1.1 | Release | ✅ 已安装 |
界面顶部提示:“HarmonyOS SDK 已经包含在 IDE,无需单独安装”,省去了手动配置 SDK 的繁琐步骤。

2.2 查看 ArkUI-X SDK(跨平台扩展)
如果项目需要将 ArkUI 框架扩展到多个 OS 平台(Android / iOS / OpenHarmony),还需要配置 ArkUI-X SDK。路径如下:
文件 → 设置 → 语言和框架 → ArkUI-X
在这里可以查看已安装和可选的 ArkUI-X SDK 版本:
| 版本 | SDK 版本号 | 阶段 | 状态 |
|---|---|---|---|
| API Version 24 | 6.1.1.100 | Release | ✅ 已安装 |
| API Version 23 | 6.1.0.28 | Beta1 | 未安装 |
| API Version 22 | 6.0.2.112 | Release | 未安装 |
安装路径示例:D:\DevTools\ArkUI-X\sdk
说明:ArkUI-X 允许开发者使用一套 ArkTS 主代码,同时构建多平台应用。如果仅开发 HarmonyOS 原生应用,无需额外安装 ArkUI-X SDK。

三、小结
| 步骤 | 操作 | 关键点 |
|---|---|---|
| 创建项目 | 欢迎页 → 新建项目 → 选择 Empty Ability 模板 → 配置项目信息 → 完成 | 使用 Stage 模型 + ArkTS 语言 |
| 查看 SDK | 设置 → HarmonyOS SDK | SDK 已内置,无需手动安装 |
| 跨平台扩展 | 设置 → ArkUI-X | 根据需要安装对应 API 版本 |
至此,DevEco Studio 的项目创建与 SDK 环境确认全部完成,可以开始 HarmonyOS 应用的功能开发。
本文基于 DevEco Studio 6.1.1 Release 版本编写,不同版本界面可能存在细微差异。
更多推荐
所有评论(0)