
#我的鸿蒙开发手记#线下店铺活动模块实现 原创
#我的鸿蒙开发手记#线下店铺活动模块实现
一、说明与准备
1.整体说明
本练习是线下店铺活动模块的实现过程,涵盖了从开发前提、整体架构设计到具体功能代码实现的诸多方面。
需基于 HarmonyOS 5.0.0 Release 及以上版本进行开发,同时要求 DevEco Studio 和 HarmonyOS SDK 的版本也均需达到 5.0.0 Release 及以上,为开发工作奠定了基础环境。
线下门店活动模块的整体构建布局设计,特别是活动模块(Activities.ets)的设计与开发。在活动 UI 布局开发方面,详细展示了 products 产品层下的 pages>MainEntry.ets 关键代码实现。通过引入各功能模块组件,如门店活动模块等,构建了应用的主入口视图模型 MainEntryVM,并基于此实现了一系列复杂的布局和功能,包括导航容器的创建、标签栏的配置与事件监听等。
在活动功能模块与实现流程部分,深入介绍了活动页面的 UI 结构设计,其由活动筛选栏、活动列表和活动详情页三部分构成。活动筛选栏支持城市和活动类型的动态筛选,活动列表采用卡片式布局展示活动封面、标题、时间、地点等信息,而活动详情页则包含活动详情介绍、报名表单和地图导航入口等丰富内容。
在功能实现步骤中,详细展示了 Activities.ets 文件的创建与关键代码实现。通过导入必要的 UI 组件库、数据模型和日志工具等,构建了活动页面的视图模型实例 ActivityEntryVM,并分别实现了外层 Tab 栏(城市选择)和内层 Tab 栏(活动类型筛选)的自定义构建器。同时,还对页面生命周期进行了管理,在加载时初始化数据,并通过双层 Tabs 结构实现了动态筛选功能。
活动列表组件开发作为重要环节,介绍了如何根据城市和活动类型的筛选条件动态刷新列表。代码中通过多条件筛选逻辑,根据不同情况展示相应的活动列表项,确保了活动数据的准确性和实时性。
活动卡片组件的开发则聚焦于如何根据加载的活动数据展示对应内容。在 ActivityCard.ets 中,通过接收活动数据模型,构建了包含活动图片、活动名称文本和支持门店信息等元素的卡片布局,并为卡片添加了点击事件,实现跳转到详情页的功能。
展示了最终成果,并在注意事项中提醒需声明 ohos.permission.SET_CALENDAR 和 ohos.permission.NOTIFICATION 权限,以确保应用正常运行。
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.模块设计
活动模块:Activities.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)Activities.ets文件创建与实现代码:
①在之前创建好scenes目录下创建Activities模块(HSP动态共享)
②Activities.ets关键代码实现:
import { CITY_LIST, CityItem, TitleTop, ACTIVITY_TYPE_LIST,
ActivityTypeItem,
CITY_LIST_ACTIVITY} from ‘@ohos_agcit/postpartum_care_center_uicomponents’; // 导入UI组件库中的城市/活动类型数据模型
import { Logger } from ‘@ohos_agcit/postpartum_care_center_utils’; // 导入日志工具
import { ActivityList } from ‘…/view/ActivityList’; // 导入活动列表组件
import { ActivityEntryVM } from ‘…/viewmodel/ActivityEntryVM’; // 导入活动页面视图模型
const TAG: string = ‘ActivitiesEntry’; // 日志标签
@Entry
@Preview
@ComponentV2
export struct Activities {
vm: ActivityEntryVM = ActivityEntryVM.instance; // 活动页面视图模型实例
@Local outerCurrentIndex: number = 0; // 外层Tab当前选中索引(城市选择)
@Local innerCurrentIndex: number = 0; // 内层Tab当前选中索引(活动类型)
private outerTabsController: TabsController = new TabsController(); // 外层Tab控制器
private innerTabsController: TabsController = new TabsController(); // 内层Tab控制器
// 页面生命周期-加载时初始化数据
async aboutToAppear() {
Logger.info(TAG, ‘%{public}s’, ‘aboutToAppear’);
await this.vm.init(); // 初始化视图模型数据
}
// 构建外层Tab栏(城市选择)
@Builder
outerTabBarBuilder(title: string, targetIndex: number) {
Column() {
Text(title)
.fontFamily(‘HarmonyHeiTi’)
.fontWeight(this.outerCurrentIndex === targetIndex ? FontWeight.Bold : FontWeight.Regular) // 选中加粗
.fontSize($r(‘app.string.font_size_14’))
.fontColor(this.outerCurrentIndex === 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.outerCurrentIndex === targetIndex ? ‘#000000’ : ‘transparent’) // 选中显示
.opacity(this.outerCurrentIndex === 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(() => { // Tab点击事件
this.outerCurrentIndex = targetIndex;
this.outerTabsController.changeIndex(targetIndex); // 切换外层Tab
this.vm.cityName = title; // 更新视图模型当前城市
});
}
// 构建内层Tab栏(活动类型筛选)
@Builder
innerTabBarBuilder(title: string, targetIndex: number) {
Column() {
Text(title)
.fontFamily(‘HarmonyHeiTi’)
.fontWeight(this.innerCurrentIndex === targetIndex ? FontWeight.Medium : FontWeight.Regular) // 选中中等字重
.fontSize($r(‘app.string.font_size_12’))
.fontColor(this.innerCurrentIndex === targetIndex ? ‘rgba(0,0,0,0.90)’ : ‘rgba(0,0,0,0.60)’) // 选中颜色加深
.margin({ bottom: $r(‘app.string.margin_9’) });
}
.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_12’), bottom: $r(‘app.string.padding_12’) })
.height(40)
.onClick(() => { // Tab点击事件
this.innerCurrentIndex = targetIndex;
this.innerTabsController.changeIndex(targetIndex); // 切换内层Tab
this.vm.type = title; // 更新视图模型当前活动类型
});
}
build() {
Column() {
// 顶部标题组件
TitleTop({ title: $r(‘app.string.title_activity’) })
.margin({ bottom: $r(‘app.string.margin_xs’) });
// 外层Tabs(城市选择)
Tabs({ barPosition: BarPosition.Start, controller: this.outerTabsController }) {
ForEach(CITY_LIST_ACTIVITY, (item: CityItem) => { // 遍历城市数据
TabContent() {
// 内层Tabs(活动类型)
Tabs({ barPosition: BarPosition.Start, controller: this.innerTabsController }) {
ForEach(ACTIVITY_TYPE_LIST, (type: ActivityTypeItem) => { // 遍历活动类型数据
TabContent() {
ActivityList(); // 渲染活动列表组件
}
.tabBar(this.innerTabBarBuilder(type.name, type.id)); // 绑定内层Tab构建器
}, (type: string) => JSON.stringify(type)); // 唯一键生成
}
.vertical(false)
.barWidth('100%')
.backgroundColor('#F1F3F5')
.barMode(BarMode.Scrollable)
.scrollable(false)
.layoutWeight(1); // 权重布局
}
.tabBar(this.outerTabBarBuilder(item.name, item.id)); // 绑定外层Tab构建器
}, (item: string) => JSON.stringify(item)); // 唯一键生成
}
.vertical(false)
.barWidth('100%')
.backgroundColor('#F1F3F5')
.barMode(BarMode.Scrollable)
.scrollable(false)
.layoutWeight(1);
}
.backgroundColor('#F1F3F5') // 页面背景色
.clip(false)
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP]); // 安全区域适配
}
}
(2)步骤一:活动列表组件开发
描述:双级联动筛选(城市+活动类型),动态刷新列表
ActivityList.ets代码实现:
import { StoreModel } from ‘@ohos_agcit/postpartum_care_center_uicomponents’; // 导入门店数据模型
import { ActivityModel } from ‘@ohos_agcit/postpartum_care_center_utils’; // 导入活动数据模型
import { ActivityEntryVM } from ‘…/viewmodel/ActivityEntryVM’; // 导入活动页面视图模型
import { ActivityCard } from ‘./ActivityCard’; // 导入活动卡片组件
@Preview
@ComponentV2
export struct ActivityList {
vm: ActivityEntryVM = ActivityEntryVM.instance; // 视图模型实例(管理活动数据与状态)
build() {
Column() {
List(/{space: 10}/) { // 创建列表容器(注释掉的space参数可设置项间距)
// 遍历活动数据列表
ForEach(this.vm.activityList, (item: ActivityModel) => {
// 多条件筛选逻辑
if (this.vm.cityName === ‘全部’ && this.vm.type === ‘全部’) {
// 情况1:显示所有城市所有类型的活动
ListItem() {
ActivityCard({ activity: item }); // 渲染活动卡片
}
} else if (this.vm.cityName === ‘全部’) {
// 情况2:显示所有城市中特定类型的活动
if (this.vm.type === item.type) {
ListItem() {
ActivityCard({ activity: item });
}
}
} else if (this.vm.type === ‘全部’) {
// 情况3:显示特定城市所有类型的活动
if (this.vm.cityName === item.city) {
ListItem() {
ActivityCard({ activity: item });
}
}
} else {
// 情况4:显示特定城市特定类型的活动
if (this.vm.cityName === item.city && this.vm.type === item.type) {
ListItem() {
ActivityCard({ activity: item });
}
}
}
}, (item: StoreModel) => JSON.stringify(item)) // 警告:此处StoreModel应为ActivityModel
}
.height(‘auto’) // 列表高度自适应
.lanes(2) // 设置两列网格布局
.width(‘100%’)
.scrollBar(BarState.Off)
.padding({ left: $r(‘app.string.margin_ms’) })
}
.width(‘100%’)
.height(‘100%’)
}
}
(3)步骤二:活动卡片组件
描述:根据加载活动数据,显示对应内容。
ActivityCard.ets代码实现:
import { ActivityModel, Logger } from ‘@ohos_agcit/postpartum_care_center_utils’; // 导入活动数据模型和日志工具
import { ActivityEntryVM } from ‘…/viewmodel/ActivityEntryVM’; // 导入活动页面视图模型
@Preview
@ComponentV2
export struct ActivityCard {
vm: ActivityEntryVM = ActivityEntryVM.instance; // 活动页面视图模型实例
// 活动卡片参数(默认值用于预览模式)
@Param activity: ActivityModel = new ActivityModel(
1,
‘activityCard’,
new Date, new Date, new Date, new Date, // 活动相关日期(开始/结束时间等)
‘专家讲座’, // 活动名称
‘nanjing’, // 所在城市
‘123’, // 支持门店ID
‘activity desc’, // 活动描述
$r(‘app.media.store_pic2’), // 活动图片资源
1 // 活动类型ID
);
build() {
Column() {
// 1. 活动图片展示区
Image(this.activity.picDescUrl)
.width(‘100%’)
.borderRadius({
topLeft: $r(‘app.string.border_radius_8’), // 左上圆角
topRight: $r(‘app.string.border_radius_8’) // 右上圆角
})
.height(‘90vp’) // 使用视窗单位保证适配性
.objectFit(ImageFit.Cover); // 图片填充模式
// 2. 活动名称文本
Text(this.activity.name)
.fontColor('rgba(0,0,0,1.00)') // 纯黑色文字
.fontSize($r('app.string.font_size_14'))
.fontFamily('HarmonyHeiTi') // 使用鸿蒙黑体
.width('100%')
.padding({
top: $r('app.string.margin_xs'),
bottom: $r('app.string.margin_xs'),
left: $r('app.string.margin_xs'),
right: $r('app.string.margin_xs')
});
// 3. 支持门店信息
Text(this.activity.supportStoreName)
.fontColor('rgba(0,0,0,60)') // 60%透明度黑色
.fontSize($r('app.string.font_size_12')) // 比标题小2px
.textAlign(TextAlign.Start) // 左对齐
.width('100%')
.padding({
bottom: $r('app.string.margin_xxs'),
left: $r('app.string.margin_xs')
});
}
// 卡片容器样式
.shadow({
radius: 8, // 阴影模糊半径
color: 'rgba(0,0,0,0.08)', // 浅灰色阴影
})
.height(168)
.width(158)
.borderRadius($r('app.string.border_radius_8'))
.backgroundColor('#FFFFFF')
.margin({ bottom: $r('app.string.margin_s')})
.onClick(() => { // 点击事件
Logger.debug(JSON.stringify(this.activity)); // 打印活动数据日志
this.vm.navStack.pushPathByName('ActivityDetail', this.activity); // 跳转到详情页
})
}
}
四、最终成果展示
五、注意事项
权限管理:需声明ohos.permission.SET_CALENDAR和ohos.permission.NOTIFICATION权限。
