
#我的鸿蒙开发手记#鸿蒙门店UI与功能实现
#我的鸿蒙开发手记#鸿蒙门店UI与功能实现
一、说明与准备
1.整体说明
本模块是在华为手机上,基于 HarmonyOS 5.0.0 及以上版本、DevEco Studio 5.0.0 及以上版本等环境进行鸿蒙门店 UI 与功能实现的开发过程。
先对应用的整体构建布局设计进行了说明,包括模块设计中的门店模块(Stores.ets),以及门店 UI 布局开发中在 products 产品层下的 pages>MainEntry.ets 关键代码实现,展示了如何引入各功能模块组件并构建主布局容器、实现底部导航标签的切换等。
介绍了门店功能模块与实现流程,门店页面由城市选择器、门店列表、门店详情页等核心组件构成。重点对 Stores.ets 文件的创建与实现代码进行了讲解,包括自定义 Tab 页签构建器、城市筛选与门店列表动态加载的实现等。其中,StoreList.ets 实现了根据城市筛选门店并展示的功能,StoreCard.ets 则构建了门店卡片组件,实现了展示门店信息、拨打电话咨询、预约参观以及导航等功能。
还有权限配置这一重要环节,需在 module.json5 中声明相关权限以确保应用正常运行。
2.基础准备
(1)设备类型:华为手机
(2)HarmonyOS版本:HarmonyOS 5.0.0 Release及以上
(3)DevEco Studio版本:DevEco Studio 5.0.0 Release及以上
(4)HarmonyOS SDK版本:HarmonyOS 5.0.0 Release SDK及以上
二、门店整体构建布局设计
1.模块设计
门店模块:Stores.ets
2.门店UI布局开发
(1)products产品层下的pages>MainEntry.ets关键代码实现:
// 引入各功能模块组件
import { Home } from '@ohos_agcit/postpartum_care_center_home'; // 首页模块
import { Mine } from '@ohos_agcit/postpartum_care_center_mine'; // 我的模块
import { Stores } from '@ohos_agcit/postpartum_care_center_stores'; // 门店模块
import { Activities } from '@ohos_agcit/postpartum_care_center_activities'; // 活动模块
import { TAB_LIST } from '../contants/Constants'; // 底部导航标签配置数据
import { MainEntryVM } from '@ohos_agcit/postpartum_care_center_uicomponents'; // 主入口视图模型
import { TabListItem } from '../model/TabListItem'; // 标签项数据模型
import { Logger } from '@ohos_agcit/postpartum_care_center_utils'; // 日志工具类
const TAG: string = '[MainEntry]'; // 日志标签
@Entry // 标识为入口组件
@ComponentV2 // 声明为V2版本组件
struct MainEntry {
vm: MainEntryVM = MainEntryVM.instance; // 绑定视图模型单例
build() {
Navigation(this.vm.navStack) { // 创建导航容器,绑定导航栈
Column() { // 主布局容器
Tabs({
barPosition: BarPosition.End, // 标签栏位置:底部(End表示底端)
index: this.vm.curIndex // 当前选中标签索引
}) {
// ---------- 首页标签 ----------
TabContent() {
Home(); // 加载首页组件
}
.clip(false) // 禁用内容裁剪
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP]) // 适配顶部安全区域
.tabBar(this.tabBarBuilder(TAB_LIST[0], 0)) // 绑定标签栏构建器
// ---------- 门店标签 ----------
TabContent() {
Stores(); // 加载门店组件
}
.clip(false)
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP])
.tabBar(this.tabBarBuilder(TAB_LIST[1], 1))
// ---------- 活动标签 ----------
TabContent() {
//Activities(); // 加载活动组件
}
.clip(false)
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP])
.tabBar(this.tabBarBuilder(TAB_LIST[2], 2))
// ---------- 我的标签 ----------
TabContent() {
//Mine(); // 加载个人中心组件
}
.clip(false)
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP])
.tabBar(this.tabBarBuilder(TAB_LIST[3], 3))
}
// Tabs全局配置
.clip(false)
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP])
.scrollable(false) // 禁用标签栏滚动
.height('100%')
.animationDuration(0) // 切换动画时长0毫秒(无动画)
.barMode(BarMode.Fixed) // 标签栏模式:固定宽度
.barHeight(56)
.onChange((index: number) => { // 标签切换事件监听
this.vm.curIndex = index; // 更新当前选中索引
});
}
.clip(false)
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP])
}
// Navigation全局配置
.height('100%') // 高度充满屏幕
.clip(false)
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM]) // 适配顶部和底部安全区域
.hideTitleBar(true) // 隐藏标题栏
.hideToolBar(true) // 隐藏工具栏
.hideBackButton(true) // 隐藏返回按钮
.mode(NavigationMode.Stack) // 导航模式:堆栈模式
}
// 自定义标签栏构建器
@Builder
tabBarBuilder(item: TabListItem, index: number) {
Column() {
Image(this.vm.curIndex === index ? item.iconChecked : item.icon) // 动态切换选中/未选图标
.width(24)
.height(24);
Text(item.label)
.fontColor(this.vm.curIndex === index ?
'rgba(0,0,0,0.90)' :
'rgba(0,0,0,0.60)')
.fontFamily('HarmonyHeiTi')
.fontWeight(this.vm.curIndex === index ?
FontWeight.Medium :
FontWeight.Regular)
.fontSize($r('app.string.font_size_10'))
.margin({ top: $r('app.string.margin_xxs') });
}.width('100%');
}
}
三、门店功能模块与实现流程
1.门店UI结构设计
门店页面由以下核心组件构成:
·城市选择器:支持按城市筛选门店。
·门店列表:展示门店名称、距离、特色服务标签及缩略图。
·门店详情页:包含套餐价目、环境图片、预约按钮、导航入口等。
2.功能实现步骤
(1)Stores.ets文件创建与实现代码:
①在之前创建好scenes目录下创建Stores模块(HSP动态共享)
②Stores.ets关键代码实现:
// 导入所需组件和数据模型
import { CITY_LIST, CityItem, MainEntryVM, TitleTop } from '@ohos_agcit/postpartum_care_center_uicomponents';
import { StoreList } from '../view/StoreList';
@Entry
@Preview
@ComponentV2
export struct Stores {
// 使用单例视图模型管理数据状态
vm: MainEntryVM = MainEntryVM.instance;
// 当前选中城市索引(@Local表示组件内部状态)
@Local currentIndex: number = 0;
// Tabs控制器实例(用于程序化控制页签切换)
private tabsController: TabsController = new TabsController();
/**
* 自定义Tab页签构建器
* @param title 城市名称
* @param targetIndex 对应城市ID
*/
@Builder
tabBarBuilder(title: string, targetIndex: number) {
Column() {
// 城市名称文本
Text(title)
.fontFamily('HarmonyHeiTi') // 鸿蒙系统字体
.fontWeight(this.currentIndex === targetIndex ? FontWeight.Bold : FontWeight.Regular) // 选中态加粗
.fontSize($r('app.string.font_size_14'))
.fontColor(this.currentIndex === targetIndex ? 'rgba(0,0,0,0.90)' : 'rgba(0,0,0,0.60)')
.margin({ bottom: $r('app.string.margin_9') });
// 选中态下划线指示器
Divider()
.width(28) // 下划线宽度
.strokeWidth(2) // 线宽
.color(this.currentIndex === targetIndex ? '#000000' : 'transparent') // 选中时显示黑色
.opacity(this.currentIndex === targetIndex ? 1 : 0); // 非选中态完全透明
}
// 动态边距:首个标签左侧增加间距
.margin(targetIndex === 0 ?
{ left: $r('app.string.margin_ms'), right: $r('app.string.margin_s') } :
{ left: $r('app.string.margin_s'), right: $r('app.string.margin_s') }
)
.padding({
top: $r('app.string.padding_14'),
bottom: $r('app.string.padding_4')
})
.height(48) // 固定高度
.onClick(() => { // 点击事件
this.currentIndex = targetIndex;
this.tabsController.changeIndex(targetIndex); // 联动切换Tabs内容
});
}
build() {
// 垂直布局容器
Column() {
// 顶部导航栏组件
TitleTop({ title: $r('app.string.title_store') }) // "门店"(国际化资源)
.margin({ bottom: $r('app.string.margin_xs') }); // 底部小间距
// 横向Tab页签容器
Tabs({
barPosition: BarPosition.Start, // 标签栏位于顶部
controller: this.tabsController // 绑定控制器
}) {
// 遍历城市数据生成Tab页
ForEach(CITY_LIST, (item: CityItem) => {
TabContent() {
// 门店列表组件(传入当前城市名称)
StoreList({ cityName: item.name }); // 复用门店列表组件
}
// 绑定自定义Tab栏样式
.tabBar(this.tabBarBuilder(item.name, item.id));
}, (item: CityItem) => JSON.stringify(item)) // 唯一键生成
}
// Tabs样式配置
.vertical(false) // 横向布局
.barWidth('100%') // 标签栏满宽
.barMode(BarMode.Scrollable) // 可滚动模式(城市较多时)
.backgroundColor('#F1F3F5') // 背景色与页面统一
.scrollable(false) // 禁用内容区域滚动
.layoutWeight(1); // 权重分配(撑满剩余空间)
}
// 页面级样式
.backgroundColor('#F1F3F5') // 页面背景色
.clip(false) // 允许子组件溢出
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP]); // 适配系统安全区域
}
}
(2)步骤一:城市筛选与门店列表动态加载
描述:用户可通过下拉选择城市或自动获取定位加载附近门店。
StoreList.ets代码实现:
// 导入必要的组件和模块
import { MainEntryVM, StoreCard, StoreModel } from '@ohos_agcit/postpartum_care_center_uicomponents';
@Preview
@ComponentV2
export struct StoreList {
// 组件参数:接收父组件传入的城市信息
@Param cityName: string = ''; // 当前城市名称(如"北京")
@Param cityId: string = ''; // 城市ID(预留字段)
// 使用单例视图模型管理数据状态
vm: MainEntryVM = MainEntryVM.instance;
build() {
// 垂直布局容器(占满父容器)
Column() {
// 滚动容器(支持上下滑动)
Scroll() {
// 列表容器(设置项间距12vp)
List({ space: 12 }) {
// 条件分支:处理"附近"特殊逻辑
if (this.cityName === '附近') {
// 遍历全部门店数据(无过滤)
ForEach(this.vm.storeList.storeList, (item: StoreModel) => {
ListItem() {
// 单店卡片组件(传入门店数据)
StoreCard({ store: item }); // 包含距离/套餐等信息
};
}, (item: StoreModel) => JSON.stringify(item)); // 唯一键生成
} else {
// 常规城市列表(按城市名称过滤)
ForEach(this.vm.storeList.storeList, (item: StoreModel) => {
// 城市匹配条件判断
if (item.city === this.cityName) {
ListItem() {
StoreCard({ store: item });
};
}
}, (item: StoreModel) => JSON.stringify(item));
}
}
.padding({
left: $r('app.string.padding_16'),
right: $r('app.string.padding_16'),
top: $r('app.string.padding_8'),
bottom: $r('app.string.padding_8'),
});
}
.width('100%')
.height('auto');
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Start);
}
}
(3)步骤二:门店详情页功能集成
描述:点击门店卡片跳转至详情页,展示套餐信息并支持预约、导航等操作。
StoreCard.ets代码实现:
import { call } from '@kit.TelephonyKit'; // 导入电话服务模块
import { BusinessError } from '@kit.BasicServicesKit'; // 导入基础服务错误类型
import { MainEntryVM } from '../viewmodel/MainEntryVM'; // 导入主入口视图模型
import { StoreModel } from '../model/StoreModel'; // 导入门店数据模型
import { ComponentContent } from '@kit.ArkUI'; // 导入ArkUI组件内容模块
import { PromptActionClass } from '@ohos_agcit/postpartum_care_center_utils'; // 导入自定义弹窗工具类
import { callPhone } from './StoreDetail'; // 导入拨打电话功能模块
const TAG: string = '[StoreCard]'; // 日志标签
// 构建电话弹窗组件的装饰器
@Builder
export function callDialog(callPhone: Function) {
Column() {
Row() {
Text('025-0000 0000') // 显示电话号码(需替换为动态数据)
.fontSize(16)
.margin({ top:24 })
}
.width('100%')
.justifyContent(FlexAlign.Center) // 水平居中
Row() {
// 取消按钮
Text('取消')
.width('50%')
.height(64)
.fontSize(16)
.fontColor('rgba(0,0,0,0.60)')
.textAlign(TextAlign.Center)
.onClick(() => {
PromptActionClass.closeDialog(); // 关闭弹窗
})
// 呼叫按钮
Text('呼叫')
.onClick(() => {
callPhone(); // 执行拨号操作
PromptActionClass.closeDialog(); // 关闭弹窗
})
.width('50%')
.height(64)
.fontColor('rgba(0,0,0,0.90)')
.fontSize(16)
.textAlign(TextAlign.Center)
}
.width('100%')
}
// 弹窗样式设置
.width(328)
.height(116)
.borderRadius(32)
.backgroundColor(Color.White)
}
// 门店卡片组件(预览模式)
@Preview
@ComponentV2
export struct StoreCard {
vm: MainEntryVM = MainEntryVM.instance; // 视图模型实例
private ctx: UIContext = this.getUIContext(); // 获取UI上下文
private contentNode: ComponentContent<object> = new ComponentContent(
this.ctx,
wrapBuilder(callDialog), // 包装弹窗构建器
callPhone
)
// 门店数据参数(默认值用于预览)
@Param store: StoreModel = new StoreModel(
1,
'南京涵江楼',
'软件大道101号',
'11100001111',
$r('app.media.store_pic1'),
'南京',
31.98,
118.76
);
// 组件构建函数
build() {
Column() {
Stack() {
// 门店图片展示
Image(this.store.image)
.width('100%')
.borderRadius({ topLeft: $r('app.string.border_radius_16'), topRight: $r('app.string.border_radius_16') })
.height(180)
.objectFit(ImageFit.Cover); // 图片填充模式
// 距离显示(点击可重新请求定位权限)
Text(this.store.distance !== null ? `距离${this.store.distance}km` : this.vm.locationServiceMessage)
.fontSize($r('app.string.font_size_10'))
.fontColor('#FFFFFF')
.backgroundColor('rgba(0,0,0,0.40)')
.position({ top: 8, right: 8 })
.height(16)
.onClick(() => {
this.vm.requestLocationPermission(); // 点击触发定位权限请求
});
}
.width('100%')
.height(180)
.margin({ bottom: $r('app.string.margin_ms') });
// 门店名称
Text(this.store.name)
.fontColor('rgba(0,0,0,0.90)')
.fontSize($r('app.string.font_size_16'))
.margin({ bottom: $r('app.string.margin_xxs') });
// 地址与导航图标行
Row() {
Text(this.store.address) // 地址文本
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis }); // 超出部分显示省略号
Image($r('app.media.ic_navigation')) // 导航图标
.width(16)
.height(16)
.margin({ left: $r('app.string.margin_xxs') });
}
.onClick(() => {
this.vm.queryLocation(this.store); // 点击触发地址查询
});
// 操作按钮行
Row({ space: 24 }) {
// 电话咨询按钮
Button({ type: ButtonType.Normal }) {
Text($r('app.string.visit_Consulting')) // 文本资源引用
.fontSize($r('app.string.font_size_12'));
}
.backgroundColor('#FFFFFF')
.borderColor('rgba(0,0,0,0.40)')
.onClick(() => {
// 打开电话弹窗
PromptActionClass.openDialog(this.getUIContext(), this.contentNode);
});
// 预约参观按钮
Button({ type: ButtonType.Normal }) {
Text($r('app.string.visit_booking'))
.fontColor('#FFFFFF');
}
.backgroundColor('#333333')
.onClick(() => {
// 跳转到预约页面
this.vm.navStack.pushPathByName('Booking', this.store.name);
});
}
}
// 卡片整体样式
.shadow({ radius: 8, color: 'rgba(0,0,0,0.08)' }) // 阴影效果
.height(312)
.borderRadius($r('app.string.border_radius_16'))
.backgroundColor('#FFFFFF')
.onClick(() => {
// 点击卡片跳转到详情页
const params: Record<string, Object> = { 'store': this.store, 'contentNode': this.contentNode };
this.vm.navStack.pushPathByName('StoreDetail', params);
});
}
}
四、最终成果展示
五、注意事项
权限配置:需在module.json5中声明以下权限
"reqPermissions": [
{ "name": "ohos.permission.LOCATION" },
{ "name": "ohos.permission.WRITE_CALENDAR" } ]
