
#我的鸿蒙开发手记#鸿蒙实体店铺首页效果 原创 精华
一、背景与前提
1.背景
本学习练习基于 HarmonyOS 5.0.0 及以上版本、DevEco Studio 5.0.0 及以上版本等环境进行鸿蒙实体店铺首页效果的开发展开。
对门店首页整体构建布局设计进行了介绍,包括首页模块(Home.ets)的设置以及在 ets 下创建 model 数据层文件和 contants 常量文件等。说明了 TabListItem.ets 和 Constants.ets 的代码实现,分别用于导出底部导航栏选项卡数据模型和配置应用底部导航栏。重点对 products 产品层下的 pages>MainEntry.ets 关键代码实现进行了讲解,展示了如何引入各功能模块组件、创建导航容器以及实现底部导航栏的标签切换等。阐述了首页功能模块与实现流程,首页 UI 由 Banner 轮播图、服务分类栅格、预约参观手势导航等核心组件构成。分别对 Home.ets 文件的创建与实现代码进行了说明,包括首页的生命周期方法、页面构建等。介绍了 Banner 轮播组件、服务分类栅格布局以及附近门店动态加载的开发,分别通过 Banner.ets、SeviceGrids.ets 和 NearbyStores.ets 的代码实现来展示相应的功能。
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.模块设计
首页模块:Home.ets
2.首页UI布局开发
(1)在ets下创建model数据层文件,创建contants常量文件。
(2)TabListItem.ets代码实现:
//导出底部导航栏选项卡数据模型
export interface TabListItem {
label: string;
icon: Resource;
iconChecked: Resource;
}
(3)Constants.ets代码实现:
// 导入底部导航栏选项卡数据模型
import { TabListItem } from '../model/TabListItem';
/**
* 应用底部导航栏配置常量
*/
export const TAB_LIST: TabListItem[] = [
{
label: '首页', // 选项卡显示文字
icon: $r('app.media.ic_home'), // 常规状态图标(鸿蒙资源引用语法)
iconChecked: $r('app.media.ic_home_checked'), // 选中状态图标
},
{
label: '门店',
icon: $r('app.media.ic_stores'), // 图标资源路径:resources/base/media/ic_stores.svg
iconChecked: $r('app.media.ic_stores_checked'), // 激活态图标带指示红点
},
{
label: '活动',
icon: $r('app.media.ic_activities'), // 建议图标尺寸:24x24vp
iconChecked: $r('app.media.ic_activities_checked'), // 选中态增加缩放动画
},
{
label: '我的',
icon: $r('app.media.ic_mine'), // 未登录状态显示轮廓图标
iconChecked: $r('app.media.ic_mine_checked'), // 登录后显示头像缩略图
},
];
/* 使用说明:
1. 在MainEntry.ets中通过Tabs组件绑定该配置
2. 图标状态切换通过currentIndex控制
3. 新增模块时需同步更新:
- 添加新配置项
- 在resources目录补充图标资源
- 更新路由配置
*/
(4)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结构设计
首页由以下核心组件构成:
·Banner轮播图:展示品牌活动或服务亮点。
·服务分类栅格:提供产后护理、月子餐等分类入口。
·预约参观手势导航:用于用户咨询相关,填写个人信息表单进行预约。
2.功能实现步骤
(1)Home.ets文件创建与实现代码:
①在根目录PostpartumCareCenter下创建scenes场景文件目录
②在scenes目录下创建Home模块(HSP动态共享)
③Home.ets关键代码实现:
import { Banner } from '../view/Banner';
import { NearbyStores } from '../view/NearbyStores';
import { ServiceGrids } from '../view/SeviceGrids';
import { MainEntryVM, StoreModel, TitleTop } from '@ohos_agcit/postpartum_care_center_uicomponents';
const STORE_LIST : StoreModel[] = [
new StoreModel(1, '南京涵江楼', '南京市高新技术产业园区软件大道101号酒店16F', '12345678910',
$r('app.media.store_pic1'), '南京', 31.98, 118.76),
new StoreModel(2, '南京卓美悦', '南京市高新技术产业园区软件大道101号酒店16F', '12345678910',
$r('app.media.store_pic2'), '南京', 31.99, 118.71),
new StoreModel(5, '苏州华轩苑', '苏州市高新技术产业园区软件大道101号酒店16F', '12345678910',
$r('app.media.store_pic1'), '苏州', 31.29, 120.64),
new StoreModel(3, '南京华轩府', '南京市高新技术产业园区软件大道101号酒店16F', '12345678910',
$r('app.media.store_pic2'), '南京', 32.03, 118.80),
new StoreModel(4, '无锡雅辉阁', '无锡市高新技术产业园区软件大道101号酒店16F', '12345678910',
$r('app.media.store_pic1'), '无锡', 31.58, 120.30),
new StoreModel(6, '杭州科技城', '杭州市高新技术产业园区软件大道101号酒店16F', '12345678910',
$r('app.media.store_pic2'), '杭州', 30.22, 120.12),
new StoreModel(7, '北京卓美悦', '北京市高新技术产业园区软件大道101号酒店16F', '12345678910',
$r('app.media.store_pic1'), '北京', 39.91, 116.47),
new StoreModel(8, '深圳海月楼', '深圳市高新技术产业园区软件大道101号酒店16F', '12345678910',
$r('app.media.store_pic2'), '深圳', 22.54, 114.06),
new StoreModel(9, '东莞华轩苑', '东莞市高新技术产业园区软件大道101号酒店16F', '12345678910',
$r('app.media.store_pic1'), '东莞', 23.02, 113.75),
new StoreModel(10, '广州银月楼', '广州市高新技术产业园区软件大道101号酒店16F', '12345678910',
$r('app.media.store_pic2'), '广州', 23.13, 113.26),
new StoreModel(11, '汕头卓美悦', '汕头市高新技术产业园区软件大道101号酒店16F', '12345678910',
$r('app.media.store_pic1'), '汕头', 23.35, 116.68),
new StoreModel(12, '常州银月楼', '常州市高新技术产业园区软件大道101号酒店16F', '12345678910',
$r('app.media.store_pic2'), '常州', 31.81, 119.94),
new StoreModel(13, '上海银月楼', '上海市高新技术产业园区软件大道101号酒店16F', '12345678910',
$r('app.media.store_pic1'), '上海', 31.23, 121.47),
new StoreModel(14, '上海云顶轩', '上海市高新技术产业园区软件大道101号酒店16F', '12345678910',
$r('app.media.store_pic2'), '上海', 31.20, 121.36),
new StoreModel(15, '北京云顶轩', '北京市高新技术产业园区软件大道101号酒店16F', '12345678910',
$r('app.media.store_pic1'), '北京', 39.90, 116.41),
new StoreModel(16, '常州星辉阁', '常州市高新技术产业园区软件大道101号酒店16F', '12345678910',
$r('app.media.store_pic2'), '常州', 31.78, 119.89),
new StoreModel(17, '深圳华轩苑', '深圳市高新技术产业园区软件大道101号酒店16F', '12345678910',
$r('app.media.store_pic1'), '深圳', 22.57, 113.94),
new StoreModel(18, '广州卓美拉', '广州市高新技术产业园区软件大道101号酒店16F', '12345678910',
$r('app.media.store_pic2'), '广州', 23.17, 113.39),
new StoreModel(19, '上海雅阁楼', '上海市高新技术产业园区软件大道101号酒店16F', '12345678910',
$r('app.media.store_pic1'), '上海', 31.18, 121.38),
new StoreModel(20, '上海华轩府', '上海市高新技术产业园区软件大道101号酒店16F', '12345678910',
$r('app.media.store_pic2'), '上海', 31.22, 121.35),
new StoreModel(21, '杭州涵江楼', '杭州市高新技术产业园区软件大道101号酒店16F', '12345678910',
$r('app.media.store_pic1'), '杭州', 30.25, 120.16),
new StoreModel(22, '杭州卓美悦', '杭州市高新技术产业园区软件大道101号酒店16F', '12345678910',
$r('app.media.store_pic2'), '杭州', 30.20, 119.99),
new StoreModel(23, '无锡卓美悦', '无锡市高新技术产业园区软件大道101号酒店16F', '12345678910',
$r('app.media.store_pic1'), '无锡', 31.52, 120.45),
];
@Entry
@Preview
@ComponentV2
export struct Home {
// 获取ViewModel单例实例,用于数据管理和页面跳转控制
vm: MainEntryVM = MainEntryVM.instance;
// 生命周期方法:页面即将显示时触发
aboutToAppear(): void {
// 初始化门店列表数据(开发环境模拟数据)
this.vm.storeList.storeList = STORE_LIST; // 完整门店列表
this.vm.storeListNearby.storeList = STORE_LIST; // 附近门店列表
// 请求定位权限(需在manifest中声明ohos.permission.LOCATION)
this.vm.requestLocationPermission();
}
build() {
// 主容器:纵向布局填充全屏
Column() {
// 顶部标题栏组件(传入国际化标题资源)
TitleTop({ title: $r('app.string.title_home')})
// 滚动容器(关闭滚动条显示)
Scroll() {
Column() {
// 1. Banner轮播模块
Banner()
// 2. 服务分类模块
Column() {
Text($r('app.string.title_sub_service'))
.fontColor('rgba(0,0,0,0.90)')
.fontSize($r('app.string.font_size_18'))
.fontFamily('HarmonyHeiTi')
.fontWeight(FontWeight.Bold)
.height(56)
.padding({
top: $r('app.string.padding_24'),
bottom: $r('app.string.padding_8')
});
// 服务网格组件(3列布局)
ServiceGrids() // 包含月子护理/产后修复等分类
.margin({ top: $r('app.string.margin_xs') }); // 顶部小间距
}
.backgroundColor('#F1F3F5') // 浅灰色背景
.padding({
left: $r('app.string.padding_16'),
right: $r('app.string.padding_16')
});
// 3. 预约参观模块
Column() {
// 模块标题行(左右布局)
Row() {
// 左侧标题
Text($r('app.string.title_sub_booking')) // "预约参观"
.fontColor('rgba(0,0,0,0.90)')
.fontSize($r('app.string.font_size_18'))
.fontFamily('HarmonyHeiTi')
.fontWeight(FontWeight.Bold)
.width('50%') // 占据50%宽度
.height(56) // 与右侧等高
.padding({
top: $r('app.string.padding_24'),
bottom: $r('app.string.padding_8')
})
.textAlign(TextAlign.Start); // 左对齐
// 右侧"全部门店"可点击区域
Row() {
Text($r('app.string.title_sub_all_store')) // "全部门店"
.fontColor('rgba(0,0,0,0.60)')
.fontSize($r('app.string.font_size_14'))
.height(56)
.padding({
top: $r('app.string.padding_29'), // 精密垂直居中
bottom: $r('app.string.padding_8')
});
// 右箭头图标
Image($r('app.media.chevron_right')) // 资源路径:media/chevron_right.svg
.width(12) // 图标尺寸
.height(56)
.padding({
top: $r('app.string.padding_26'),
bottom: $r('app.string.padding_6'),
left: $r('app.string.padding_5') // 文字与图标间距
});
}
.justifyContent(FlexAlign.End) // 子组件右对齐
.width('50%')
.height(56)
.onClick(() => { // 点击跳转门店页
this.vm.curIndex = 1; // 切换底部Tab索引
});
}
.padding({ // 左右安全边距
left: $r('app.string.padding_16'),
right: $r('app.string.padding_16')
})
.justifyContent(FlexAlign.SpaceBetween) // 两端对齐
.width('100%');
// 附近门店列表组件
NearbyStores() // 包含距离/预约按钮等功能
.margin({ top: $r('app.string.margin_xs') }); // 顶部小间距
};
}
.padding({ // 滚动区域内部间距
top: $r('app.string.padding_8'),
bottom: $r('app.string.padding_10')
})
}
.scrollBar(BarState.Off) // 禁用滚动条(保持视觉简洁)
.layoutWeight(1) // 权重分配(撑满剩余空间)
}
// 页面级样式设置
.width('100%') // 满宽
.height('100%') // 满高
.align(Alignment.TopStart)// 顶部对齐
.backgroundColor('#F1F3F5') // 页面背景色
.clip(false) // 允许子组件溢出
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP]) // 适配刘海屏
}
}
(2)步骤一:Banner轮播组件开发
描述:自动轮播品牌宣传图,支持手动滑动
Banner.ets代码实现:
// 引入轮播图数据源
import { BANNER_LIST } from '../constants/Constants';
@Preview
@ComponentV2
export struct Banner {
build() {
// 创建轮播容器
Swiper() {
// 遍历BANNER_LIST数据生成轮播项
ForEach(BANNER_LIST, (item: Resource) => {
// 单张轮播图容器
Image(item)
.objectFit(ImageFit.Fill) // 图片填充模式:充满容器
.width('100%')
.height(246)
.borderRadius($r('app.string.border_radius_16')) // 圆角16单位(从资源文件读取)
.padding({
right: $r('app.string.padding_16'),
left: $r('app.string.padding_16') })
}, (item: Resource) => JSON.stringify(item)) // 唯一键生成规则
}
// 轮播指示器配置
.indicator(
Indicator.dot()
.selectedColor('#FFFFFF')
.selectedItemWidth(12)
.selectedItemHeight(8)
.itemWidth(8)
.itemHeight(8)
.color('rgba(0,0,0,0.10)')
)
.clip(false) // 禁止内容裁剪
.autoPlay(true)
.loop(true)
.backgroundColor('#F1F3F5')
.expandSafeArea( // 安全区域扩展配置
[SafeAreaType.SYSTEM], // 适配系统安全区域(如刘海屏)
[SafeAreaEdge.TOP] // 顶部区域扩展
);
}
}
(3)服务分类栅格布局
描述:以3列栅格形式展示服务分类,点击跳转至详情页
SeviceGrids.ets代码实现:
//引用资源路径
import { MainEntryVM } from '@ohos_agcit/postpartum_care_center_uicomponents';
import { SERVICE_LIST } from '../constants/Constants';
import { ServiceGridItem } from '../model/ServiceGridItem';
@Preview
@ComponentV2
export struct ServiceGrids {
// 使用单例模式获取ViewModel实例,用于页面导航等逻辑控制
vm: MainEntryVM = MainEntryVM.instance;
build() {
// 网格布局容器
Grid() {
// 遍历服务列表数据生成网格项
ForEach(SERVICE_LIST, (item: ServiceGridItem, index: number) => {
// 单个网格项定义
GridItem() {
// 垂直排列的列容器(图标+文字)
Column() {
// 服务图标
Image(item.icon)
.width(40)
.height(40)
.objectFit(ImageFit.Cover); // 图片填充模式为覆盖
// 服务名称文本
Text(item.label)
.fontFamily('HarmonyHeiTi')
.fontWeight(FontWeight.Regular)
.fontSize($r('app.string.font_size_12'))
.fontColor('rgba(0, 0, 0, 0.9)')
.margin({ top: $r('app.string.margin_xxs') }); // 顶部间距使用xxs尺寸
};
}
.width(68)
.onClick(() => { // 点击事件处理
// 通过ViewModel跳转到服务详情页,传递服务名称参数
this.vm.navStack.pushPathByName('ServiceDetail', item.label);
});
}, (item: ServiceGridItem) => JSON.stringify(item)); // 使用JSON序列化作为ForEach的key生成器
}
// 网格布局样式设置
.columnsGap(8) // 列间距8vp
.rowsGap(16) // 行间距16vp
.padding({
top: $r('app.string.padding_12'),
bottom: $r('app.string.padding_12'),
right: $r('app.string.padding_16'),
left: $r('app.string.padding_16')
})
.borderRadius($r('app.string.border_radius_16'))
.backgroundColor('#FFFFFF')
.width('100%');
}
}
(4)附近门店动态加载
描述:用于用户咨询相关,填写个人信息表单进行预约
NearbyStores.ets代码实现:
// 导入必要的组件和模块
import { MainEntryVM, StoreCard, StoreModel } from '@ohos_agcit/postpartum_care_center_uicomponents';
import { Logger } from '@ohos_agcit/postpartum_care_center_utils';
// 常量定义:最多显示的门店卡片数量
const STORE_SHOWN_NUM: number = 2;
// 附近门店卡片滑动组件
@Preview
@ComponentV2
export struct NearbyStores {
// 使用单例模式获取ViewModel实例
vm: MainEntryVM = MainEntryVM.instance;
// 当前显示卡片的索引(使用@Local装饰器表示组件内部状态)
@Local currentIndex: number = 0;
// 手势拖动的X轴偏移量(用于滑动动画计算)
@Local panTranslateX: number = 0;
build() {
Column() {
// 使用堆叠布局实现卡片重叠效果
Stack() {
// 遍历附近门店列表数据
ForEach(this.vm.storeListNearby.storeList, (item: StoreModel, index: number) => {
// 单个门店卡片组件
StoreCard({ store: item })
// 基础样式设置
.padding({
left: $r('app.string.padding_16'),
right: $r('app.string.padding_16')
})
// 动态宽度计算:当前卡片100%,后续卡片逐渐缩小
.width(`${(1.0 - 0.9 * (index - this.currentIndex) * 0.05) * 100}%`)
.align(Alignment.Center)
// 层级控制:保证前面的卡片显示在上层
.zIndex(this.vm.storeListNearby.storeList.length - index)
// 动态位置:实现卡片堆叠效果
.position({
y: item.translateY + (index - this.currentIndex) * 10, // Y轴偏移
x: item.translateX + (index - this.currentIndex) * 8, // X轴偏移
})
// 透明度控制:只显示前STORE_SHOWN_NUM个卡片
.opacity((index <= STORE_SHOWN_NUM) ? 1 : 0)
// 手势处理(优先级高于子组件手势)
.priorityGesture(
// 拖动手势定义
PanGesture()
.onActionStart(() => { // 手势开始
this.panTranslateX = 0; // 重置偏移量
Logger.debug('testTag', 'Pan start'); // 日志记录
})
.onActionUpdate((event: GestureEvent) => { // 手势更新
this.panTranslateX += event.offsetX; // 累加X轴偏移
Logger.debug('testTag', `Pan update: ${this.panTranslateX}`);
})
.onActionEnd(() => { // 手势结束
const distanceThreshold = 100; // 滑动阈值100vp
// 向右滑动超过阈值时触发切换动画
if (this.panTranslateX > distanceThreshold) {
animateTo({
duration: 200, // 动画时长200ms
curve: Curve.EaseOut, // 缓动曲线
onFinish: () => { // 动画结束回调
// 重置卡片位置
item.translateX = 0;
item.translateY = 0;
// 实现循环列表效果:将第一个元素移到末尾
const storeList = [...this.vm.storeListNearby.storeList];
storeList.push(storeList[0]);
this.vm.storeListNearby.storeList = storeList.slice(1);
// 标记不需要重新排序
this.vm.storeListNearby.needSortStore = false;
Logger.debug('testTag', 'Animation finished');
},
}, () => { // 动画执行过程
// 设置飞出动画效果
item.translateX += 400; // 向右飞出400vp
item.translateY -= 60; // 向上飞出60vp
});
}
Logger.debug('testTag', 'Pan gesture ended');
}),
);
}, (item: StoreModel) => JSON.stringify(item)); // 使用JSON序列化作为key
}
.height(340); // 固定堆叠容器高度
};
}
}
四、最终成果展示
五、注意事项
权限配置:需在module.json5中声明ohos.permission.LOCATION权限。
兼容性:仅支持HarmonyOS 5.0.0及以上版本。
数据安全:用户手机号需通过华为账号服务授权获取。
