
#我的鸿蒙开发手记#鸿蒙会员功能开发练习 原创
一、背景与说明
1.背景
本练习主要介绍了在华为手机直板机上,基于 HarmonyOS 5.0.0 及以上版本、DevEco Studio 5.0.0 及以上版本等环境进行鸿蒙会员功能开发的过程。
首先对会员模块的整体构建布局进行了设计,包括模块设计以及会员主界面结构模块代码实现,其中涉及到导航容器的使用、不同标签页的设置与切换等。核心组件包括 Grid(会员权益展示)、@ohos.iap(华为支付服务)以及 Preferences(订单状态持久化),并通过本地 MockService 模拟会员套餐与订单数据。
工程结构上,会员功能位于 SocialDating 工程的 member 模块中,包含页面文件 MemberPrivilegePage.ets(会员权益介绍页)、OrderPage.ets(会员订购页)、OrderListPage.ets(订单管理页),数据模型 Membership.ets(会员套餐模型)、Order.ets(订单模型),以及服务层 PaymentService.ets(支付服务封装)和 OrderService.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.模块设计
会员模块:MemberPage.ets
(1)会员主界面结构模块代码实现(Index.ets):
// 导入依赖模块
import { TabItem, tabItems } from '../model/TabItem'; // 标签页模型定义
import { authentication } from '@kit.AccountKit'; // 华为账号认证服务
import { BusinessError } from '@kit.BasicServicesKit'; // 基础服务错误类型
import { BasicUserInfo, GetUserResponse, UserInfo, Constants, MockService } from 'commons'; // 公共模块定义
import { Home, Personal } from 'composite'; // 首页和个人中心组件
import { MemberPage } from 'member'; // 会员页面组件
import { FeedListPage } from 'feed'; // 动态列表页面组件
import UserService from 'user/src/main/ets/service/UserService';// 用户服务模块
import Logger from 'commons/src/main/ets/utils/Logger'; // 日志工具
@Entry
@Component
struct Index {
@State currentPageIndex: number = 0; // 当前选中页签的索引(响应式状态)
@State currentUser: BasicUserInfo | null = null; // 当前用户基本信息(响应式状态)
@Provide('pageStack') pageStack: NavPathStack = new NavPathStack(); // 提供导航堆栈实例
build() { // 组件UI构建方法
Navigation(this.pageStack) { // 导航容器包裹整个页面
this.buildPageTitle() // 自定义页面标题区域
Tabs({
barPosition: BarPosition.End, // 标签栏位置:底部
index: this.currentPageIndex // 绑定当前选中页签索引
}) {
// ----------- 首页标签页 -----------
TabContent() {
Home({ index: this.currentPageIndex }) // 首页组件
}.tabBar(this.buildTabBar( // 自定义标签栏样式
tabItems[Constants.PAGE_INDEX_HOME], // 获取首页标签配置
Constants.PAGE_INDEX_HOME // 标签索引
))
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM]) // 安全区域适配
// ----------- 动态列表标签页 -----------
TabContent() {
FeedListPage({ index: this.currentPageIndex }) // 动态页面组件
}.tabBar(this.buildTabBar(
tabItems[Constants.PAGE_INDEX_FEED],
Constants.PAGE_INDEX_FEED
))
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])
// ----------- 会员标签页 -----------
TabContent() {
//MemberPage({ index: this.currentPageIndex }) // 会员页面组件
}.tabBar(this.buildTabBar(
tabItems[Constants.PAGE_INDEX_MEMBER],
Constants.PAGE_INDEX_MEMBER
))
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])
// ----------- 个人中心标签页 -----------
TabContent() {
//Personal({ index: this.currentPageIndex }) // 个人中心组件
}.tabBar(this.buildTabBar(
tabItems[Constants.PAGE_INDEX_PERSONAL],
Constants.PAGE_INDEX_PERSONAL
))
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])
}
.width('100%') // 标签栏宽度占满
.barHeight(110) // 标签栏高度
.onChange((index: number) => { // 页签切换回调
this.currentPageIndex = index; // 更新当前页签索引
})
.vertical(false) // 水平排列标签
.backgroundColor($r('app.color.page_background_color')) // 背景色
}
.hideTitleBar(true) // 隐藏系统标题栏
.hideToolBar(true) // 隐藏系统工具栏
}
// ------------------- 自定义构建方法 -------------------
@Builder
buildPageTitle() { // 构建页面标题区域
Row() {
// 根据当前页签显示对应标题
if (this.currentPageIndex === Constants.PAGE_INDEX_HOME) {
this.buildPageTitleText(tabItems[Constants.PAGE_INDEX_HOME].title)
} else if (...) { /* 其他页签条件判断 */ }
}
.width('92%').height(56) // 标题栏尺寸
}
@Builder
buildPageTitleText(pageTitle: string) { // 构建标题文本样式
Text(pageTitle)
.fontColor($r('app.color.text_font_color_black'))
.fontSize(24)
.fontWeight(FontWeight.Medium)
}
@Builder
buildTabBar(button: TabItem, tabIndex: number) { // 构建单个标签项
Column() {
Image(this.currentPageIndex === tabIndex ? button.selectedImage : button.img) // 动态切换图标
.objectFit(ImageFit.Contain) // 图片填充方式
.width(24).height(24)
Text(button.title) // 标签文字
.fontColor(this.currentPageIndex === tabIndex ?
button.selectedTitleColor : // 选中状态颜色
$r('app.color.text_font_color_gray')
)
}
.onClick(() => { // 点击切换页签
this.currentPageIndex = tabIndex;
})
}
// ------------------- 生命周期方法 -------------------
aboutToAppear(): void { // 组件即将显示时触发
MockService.init(); // 初始化模拟数据服务
this.silentLogin(); // 执行静默登录
}
// ------------------- 业务逻辑方法 -------------------
private silentLogin() { // 静默登录逻辑
let loginRequest = new authentication.HuaweiIDProvider().createLoginWithHuaweiIDRequest();
loginRequest.forceLogin = false; // 非强制登录(无感知登录)
let controller = new authentication.AuthenticationController();
controller.executeRequest(loginRequest).then((data) => {
let unionId = data.unionID; // 获取用户唯一标识
Logger.info('静默登录成功, unionId=' + unionId);
AppStorage.setOrCreate(Constants.UNION_ID, unionId); // 存储到应用全局状态
this.getUserInfo(unionId); // 获取用户详细信息
}).catch((error: BusinessError) => {
if (error.code === authentication.AuthenticationErrorCode.ACCOUNT_NOT_LOGGED_IN) {
loginRequest.forceLogin = true; // 触发强制登录(显示登录界面)
controller.executeRequest(loginRequest).then((data) => { /* 处理强制登录 */ })
}
})
}
getUserInfo(unionId: string | undefined) { // 获取用户信息
UserService.getUserFullInfoByUnionId(unionId).then((data: GetUserResponse | null) => {
if (data?.userFullInfo?.userInfo) { // 已注册用户
this.currentUser = UserInfo.toBasicUserInfo(data.userFullInfo.userInfo);
AppStorage.setOrCreate(Constants.CURRENT_USER_BASIC, this.currentUser); // 更新全局用户信息
} else { // 未注册用户
this.pageStack.pushPathByName('UserRegister', null, (popInfo: PopInfo) => {
this.currentUser = popInfo.result as BasicUserInfo; // 注册完成后更新用户信息
});
}
})
}
}
(2)核心组件:
Grid(会员权益展示)
@ohos.iap(华为支付服务)
Preferences(订单状态持久化)
数据模拟:本地MockService模拟会员套餐与订单数据
2.工程结构
首页功能位于SocialDating工程的member模块中,主要包含以下文件:
(1)页面文件:
MemberPrivilegePage.ets:会员权益介绍页
OrderPage.ets:会员订购页
OrderListPage.ets:订单管理页
(2)数据模型:
Membership.ets:会员套餐模型(名称、价格、权益描述)
Order.ets:订单模型(订单号、状态、时间)
(3)服务层:
PaymentService.ets:支付服务封装
OrderService.ets:订单查询与状态更新
(4)会员界面结构(MemberPage.ets):
// ------------------- 模块导入 -------------------
import { BasicUserInfo, CommonRsp, Constants } from 'commons'; // 基础数据模型、工具类、常量
import { call } from '@kit.TelephonyKit'; // 鸿蒙电话服务
import { BusinessError, pasteboard } from '@kit.BasicServicesKit'; // 基础服务(粘贴板等)
import Logger from 'commons/src/main/ets/utils/Logger'; // 日志工具
import MatchMakerService from '../service/MatchMakerService'; // 红娘服务
import MemberService from '../service/MemberService'; // 会员服务
import { MemberShip } from '../model/QueryMemberShipResponse'; // 会员套餐模型
import { MemberPageParam } from '../model/MemberPageParam'; // 会员页参数模型
import { Matchmaker } from '../model/Matchmaker'; // 红娘信息模型
// ------------------- 会员页面组件 -------------------
@Builder
export function MemberPageBuilder() {
MemberPage() // 页面构建入口
}
@Component
@Preview // 启用预览模式
export struct MemberPage {
// ----------- 状态管理 -----------
@Consume('pageStack') pageStack: NavPathStack; // 导航堆栈(跨组件共享)
@State isMember: boolean = false; // 是否为会员状态
@State isShowSubscribeMember: boolean = false; // 是否显示订阅弹窗
@State selectedIdx: number | null = 0; // 选中的会员套餐索引
@State isShowContactMatchmaker: boolean = false; // 是否显示联系红娘弹窗
@Prop @Watch('onChange') index: number = 0; // 监听外部页签变化
// ----------- 数据定义 -----------
private currentUser: BasicUserInfo | null = null; // 当前用户信息
private memberService1: string[] = [...]; // 会员服务项1的图文配置
private memberService2: string[] = [...]; // 会员服务项2的图文配置
private memberService3: string[] = [...]; // 会员服务项3的图文配置
private matchMaker: Matchmaker | null = null; // 红娘信息
private memberShips: MemberShip[] = []; // 会员套餐列表
// ----------- 生命周期方法 -----------
aboutToAppear(): void {
this.getCurrentUserFromAppStorage(); // 从全局状态获取用户信息
this.getMatchMaker(); // 获取红娘信息
this.getMembershipList(); // 加载会员套餐数据
}
// ----------- 核心方法 -----------
// 从全局状态获取当前用户信息
private getCurrentUserFromAppStorage() {
const user = AppStorage.get(Constants.CURRENT_USER_BASIC);
if (user) {
this.currentUser = user;
this.isMember = user.isVip === Constants.YES; // 更新会员状态
}
}
// ----------- 页面构建 -----------
build() {
NavDestination() {
Column() {
Blank().height(10) // 顶部间距
// 构建三个会员服务模块
this.buildMemberServiceItem(this.memberService1, true); // 图文左右布局
this.buildMemberServiceItem(this.memberService2, false); // 图文右左布局
this.buildMemberServiceItem(this.memberService3, true);
Blank().height('6%') // 底部间距
// 动态显示按钮:非会员显示订阅,会员显示联系红娘
if (!this.isMember || this.isRenewSubscribe) {
this.buildSubscribeMemberButton()
} else {
this.buildContactMatchMakerButton()
}
}
}.backgroundColor($r('app.color.page_background_color'))
}
// ----------- 自定义构建方法 -----------
// 构建单个会员服务项(图文组合)
@Builder
buildMemberServiceItem(memberService: string[], textOnRight: boolean) {
Row() {
// 根据布局参数决定图文顺序
if (textOnRight) {
this.showMemberServiceImg(memberService[0]) // 显示图片
this.showDescInfo(memberService); // 显示文字描述
} else {
this.showDescInfo(memberService);
this.showMemberServiceImg(memberService[0])
}
}
.width('92%').backgroundColor(Color.White).borderRadius(16)
.margin({ bottom: 16 }).height('22%') // 固定高度占比
}
// 显示服务项图片
@Builder
showMemberServiceImg(img: string) {
Column() {
Image($r(img)).width(190).height(120) // 固定尺寸图片
}.width(190)
}
// 显示服务项文字描述
@Builder
showDescInfo(memberService: string[]) {
Column() {
// 主标题(第2个元素)
Text(memberService[1])
.fontSize($r('sys.float.Body_M')).margin({ bottom: 4 })
// 副标题(第3个元素)
Text(memberService[2]).margin({ bottom: 4 })
// 详细描述(剩余元素)
ForEach(memberService.slice(3), (item: string) => {
Text(item).fontSize(10).margin({ bottom: 4 })
})
}.margin({ left: 16, right: 16 })
}
// ----------- 订阅会员模块 -----------
// 构建订阅按钮及弹窗
@Builder
buildSubscribeMemberButton() {
Row() {
Button(this.isRenewSubscribe ? '续费' : '开通会员')
.onClick(() => this.isShowSubscribeMember = true) // 显示套餐选择
.bindSheet($$this.isShowSubscribeMember,
this.subscribeMemberBuilder(this.memberShips), // 绑定套餐选择弹窗
{ detents: [320], title: bindSheetTitleBuilder('会员套餐') }
)
}.margin({ top: 10, bottom: 10 })
}
// 构建会员套餐选择弹窗
@Builder
subscribeMemberBuilder(membershipList: MemberShip[]): CustomBuilder {
Column() {
// 套餐列表(横向滚动)
Row() {
ForEach(membershipList, (item, index) => {
Column() {
Text(item.name).margin({ top: 16, bottom: 16 }) // 套餐名称
Text(`¥${item.fee}`).fontSize(30) // 价格
Text(item.desc).fontSize(10).margin({ top: 18 })// 描述
}
.backgroundImage($r(index === this.selectedIdx ?
'app.media.img_membership_0' : 'app.media.img_membership_1')) // 选中状态样式
.onClick(() => this.selectedIdx = index) // 选择套餐
})
}.padding({ left: 12, right: 12 })
// 确认按钮
Row() {
Button('升级VIP')
.onClick(() => this.handleSubscribe()) // 处理订阅逻辑
.enabled(this.selectedIdx !== null) // 未选择时禁用
}.margin({ top: 4, bottom: 32 })
}.width('92%').backgroundColor(Color.White)
}
// 处理订阅请求
private handleSubscribe() {
if (!this.currentUser?.uid || this.selectedIdx === null) return;
MemberService.subscribeMember(
this.currentUser.uid,
this.memberShips[this.selectedIdx].id,
new Date().getTime()
).then((data: CommonRsp | null) => {
if (data?.retCode === Constants.RET_SUCCESS_CODE) {
this.currentUser!.isVip = Constants.YES; // 更新本地会员状态
AppStorage.setOrCreate(Constants.CURRENT_USER_BASIC, this.currentUser); // 同步全局状态
this.pageStack.pop(this.currentUser, false); // 返回上一页
}
})
}
// ----------- 联系红娘模块 -----------
// 构建联系红娘按钮及弹窗
@Builder
buildContactMatchMakerButton() {
Row() {
Button('联系您的专属红娘')
.onClick(() => this.isShowContactMatchmaker = true)
.bindSheet($$this.isShowContactMatchmaker,
contactMatchmakerBuilder(this.matchMaker, () => { // 绑定红娘信息弹窗
this.isShowContactMatchmaker = false
}),
{ detents: [360], title: bindSheetTitleBuilder('联系红娘') }
)
}.margin({ top: 10, bottom: 10 })
}
// ----------- 数据获取 -----------
// 获取会员套餐列表
private getMembershipList() {
MemberService.queryMembership().then((data: MemberShip[]) => {
this.memberShips = data;
})
}
// 获取红娘信息
private getMatchMaker() {
MatchMakerService.queryMatchMaker().then((data: Matchmaker[]) => {
this.matchMaker = data[0];
})
}
}
// ------------------- 工具函数 -------------------
// 电话号码脱敏处理(如:138****1234)
function desensitizePhoneNumber(phone: string): string {
return phone?.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2') || '';
}
// ------------------- 弹窗构建器 -------------------
// 构建联系红娘弹窗
@Builder
export function contactMatchmakerBuilder(matchMaker: Matchmaker | null, cancelCalled: () => void): CustomBuilder {
Column() {
if (matchMaker) {
// 红娘头像
Image(matchMaker.profilePictureUrl || $r('app.media.head_image_default'))
.clipShape(Circle).width(72).height(72)
// 微信号(带复制功能)
Row() {
Text(`微信号:${matchMaker.weixinNo}`).copyOption(CopyOptions.LocalDevice)
Image($r('app.media.ic_public_power_duplicating')) // 复制图标
.onClick(() => pasteboard.setData(matchMaker.weixinNo)) // 写入粘贴板
}
// 脱敏电话号码
Text(`电话号码:${desensitizePhoneNumber(matchMaker.phoneNo)}`)
// 一键拨号按钮
Row() {
Image($r('app.media.ic_phone_highlight')) // 电话图标
Text('一键拨号')
}
.onClick(() => call.makeCall(matchMaker.phoneNo)) // 调用拨号服务
// 取消按钮
Text('取消').onClick(cancelCalled)
}
}.backgroundColor(Color.White).width('92%')
}
三、最终效果
