#我的鸿蒙开发手记#鸿蒙会员功能开发练习 原创

强哥智疗
发布于 2025-5-6 10:03
浏览
0收藏

一、背景与说明
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%')
}

三、最终效果
#我的鸿蒙开发手记#鸿蒙会员功能开发练习-鸿蒙开发者社区

©著作权归作者所有,如需转载,请注明出处,否则将追究法律责任
分类
收藏
回复
举报
回复
    相关推荐