#我的鸿蒙开发手记#鸿蒙广场模块开发练习 原创

瑞龙范超杰
发布于 2025-5-6 09:45
浏览
0收藏

一、背景与说明
1.背景
本项目是在手机上基于相应开发环境进行鸿蒙广场模块开发的实践。工程结构上,广场模块位于 SocialDating 工程的 feed 模块中,包含页面文件、数据模型和服务层等文件。广场界面整体构建布局设计方面,广场主界面通过标签页切换实现不同功能模块的展示,如动态列表等。以动态列表页为例,它包含推荐与关注页签,能够自动加载和展示动态数据,同时还有动态详情页和发布页等。在实现过程中,涉及到从全局状态获取当前用户信息、加载动态数据、处理动态响应数据等核心操作,以及构建顶部页签导航栏、动态列表主体、新增动态按钮等页面构建工作,最终呈现出一个具备多种交互功能和良好用户体验的广场模块。
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.模块设计
广场模块:FeedListPage.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.工程结构
首页功能位于SocialDating工程的feed模块中,主要包含以下文件:
(1)页面文件:
FeedListPage.ets:动态列表页(包含推荐与关注页签)
FeedDetailPage.ets:动态详情页
PublishFeedPage.ets:动态发布页
(2)数据模型:
Feed.ets:动态内容模型(含文本、图片、点赞数等字段)
Comment.ets:评论模型
(3)服务层:MockService.ets(本地模拟数据服务)
FeedService.ets:动态数据加载与更新服务
UserService.ets:用户关注状态管理
(4)广场界面结构(FeedListPage.ets):

// ------------------- 模块导入 -------------------
import {
  BasicUserInfo, CommonRsp, Constants, DateUtils, 
  QueryFeedType, Comment, Feed, FollowType, QueryFeedResponse, LikeOperType
} from 'commons'; // 基础数据模型、工具类、常量定义
import { FeedDetailPageRouteParam } from '../model/FeedDetailPageRouteParam'; // 动态详情页路由参数
import { FeedListPageRouteParam } from '../model/FeedListPageRouteParam';    // 动态列表页路由参数
import Logger from 'commons/src/main/ets/utils/Logger';                      // 日志工具
import FeedService from '../service/FeedService';                            // 动态数据服务
import LikeService from '../service/LikeService';                            // 点赞服务
import CommentService from '../service/CommentService';                      // 评论服务

// ------------------- 动态列表页组件 -------------------
@Builder
export function FeedListPageBuilder() {
  FeedListPage() // 动态列表页入口构建器
}

// 可拖拽按钮位置限制常量
const ADD_BUTTON_DRAG_MAX_X = 280;  // 拖拽按钮最大X坐标
const ADD_BUTTON_DRAG_MIN_Y = 60;   // 拖拽按钮最小Y坐标
const ADD_BUTTON_DRAG_MAX_Y = 550;  // 拖拽按钮最大Y坐标

@Component
export struct FeedListPage {
  // ----------- 状态管理 -----------
  @Consume('pageStack') pageStack: NavPathStack;   // 导航堆栈
  @State feedInfos: Feed[] | null = null;          // 动态数据列表
  @State currentIndex: number = 0;                 // 当前页签索引(0-推荐,1-关注)
  @State noMoreFlag: boolean = false;              // 是否无更多数据标志
  @State theUserToQueryFeed: BasicUserInfo | null = null; // 查询动态的目标用户信息
  @State addButtonX: number = 270;                 // 新增动态按钮X坐标
  @State addButtonY: number = 500;                 // 新增动态按钮Y坐标
  @Prop @Watch('onChange') index: number = 0;      // 监听外部页签变化
  private currentUser: BasicUserInfo | null = null;// 当前登录用户
  private scroller: Scroller = new Scroller();     // 滚动控制器
  private curPage: number = 1;                     // 当前页码
  private queryFeedType: QueryFeedType = QueryFeedType.QUERY_RECOMMEND; // 查询类型
  private showUserInfo: boolean = true;            // 是否显示用户信息

  // ----------- 生命周期方法 -----------
  aboutToAppear(): void {
    // 从导航参数获取查询条件
    let params: FeedListPageRouteParam[] = this.pageStack.getParamByName('FeedList');
    if (params?.[0]) {
      this.queryFeedType = params[0].queryFeedType;           // 动态查询类型
      this.theUserToQueryFeed = params[0].theUserToQueryFeed; // 目标用户信息
      this.pageTitle = params[0].feedListPageTitle || '';     // 页面标题
    }
    this.getCurrentUserBasicFromAppStorage(); // 获取当前用户信息
    this.getFeedFromServer();                // 加载动态数据
  }

  // ----------- 核心方法 -----------
  // 从全局状态获取当前用户
  private getCurrentUserBasicFromAppStorage() {
    this.currentUser = AppStorage.get(Constants.CURRENT_USER_BASIC) || null;
  }

  // 加载动态数据
  private getFeedFromServer() {
    if (this.queryFeedType === QueryFeedType.QUERY_RECOMMEND || 
       this.queryFeedType === QueryFeedType.QUERY_FOLLOWED) {
      // 推荐/关注动态
      FeedService.getFeedList(this.currentIndex.toString(), this.curPage)
        .then(this.processQueryFeedRsp);
    } else if (this.theUserToQueryFeed) {
      // 指定用户动态
      FeedService.getFeedList(this.queryFeedType.toString(), 
        this.theUserToQueryFeed.uid, this.curPage)
        .then(this.processQueryFeedRsp);
    }
  }

  // 处理动态响应数据
  private processQueryFeedRsp = (data: QueryFeedResponse | null) => {
    if (data?.list?.length) {
      this.feedInfos = data.list;                      // 更新动态列表
      this.noMoreFlag = data.page.totalPages <= this.curPage; // 判断是否最后一页
      this.noFeedAtAll = data.page.totalCount === 0;    // 空数据状态
    }
  }

  // ----------- 页面构建 -----------
  build() {
    // 自动加载模式(推荐/关注页)
    if (this.needAutoGetFeeds) {
      NavDestination() {
        this.buildTabBarRow()  // 构建顶部页签
        Tabs({ index: this.currentIndex }) {
          // 推荐页 & 关注页共用相同内容
          TabContent() { this.buildFeedListPage() }
          TabContent() { this.buildFeedListPage() }
        }
        .onChange((index: number) => {
          this.currentIndex = index;
          this.getFeedFromServer(); // 页签切换时重新加载
        })
      }
    } 
    // 指定用户动态页
    else {
      NavDestination() {
        this.buildFeedListPage() // 直接构建动态列表
      }
      .title(this.pageTitle)     // 显示自定义标题
    }
  }

  // ----------- 自定义构建方法 -----------
  // 构建顶部页签导航栏
  @Builder
  buildTabBarRow() {
    Row() {
      // 推荐页签
      Text('推荐')
        .fontColor(this.currentIndex === 0 ? 
          $r('app.color.button_background_color') : Color.Black)
        .onClick(() => this.currentIndex = 0)
      
      // 关注页签
      Text('关注')
        .fontColor(this.currentIndex === 1 ? 
          $r('app.color.button_background_color') : Color.Black)
        .onClick(() => this.currentIndex = 1)
    }
    .backgroundColor($r('app.color.page_background_color'))
  }

  // 构建动态列表主体
  @Builder
  buildFeedListPage() {
    Stack() {
      if (this.noFeedAtAll) {
        // 空状态提示(无动态)
        emptyActionPageContent(...) 
      } else {
        Scroll(this.scroller) {
          Column() {
            ForEach(this.feedInfos, (item: Feed) => {
              FeedBasic({  // 动态卡片组件
                item: item,
                showFollowButton: !item.currentUserHasFollowed,
                pageStack: this.pageStack,
                currentUser: this.currentUser,
                feedChange: () => this.getFeedFromServer() // 数据更新回调
              })
            })
          }
        }
        .onScrollEdge((side: Edge) => {
          if (side === Edge.Bottom && !this.noMoreFlag) {
            this.curPage++; this.getFeedFromServer(); // 滚动到底部加载更多
          }
        })
      }
      // 新增动态按钮(可拖拽)
      if (this.needAutoGetFeeds) {
        this.showCreateFeedIcon() 
          .position({ x: this.addButtonX, y: this.addButtonY })
      }
    }
  }

  // 构建新增动态按钮(带拖拽手势)
  @Builder
  showCreateFeedIcon() {
    Image($r('app.media.ic_new_feed'))
      .onClick(() => this.pageStack.pushPathByName('NewFeed'))
      .gesture(PanGesture() // 拖拽手势处理
        .onActionUpdate((event) => {
          // 限制拖拽范围并更新坐标
          if (event.offsetX在允许范围内) this.offsetX/Y = ...
        })
      )
  }
}

// ------------------- 动态卡片组件 -------------------
@Component
export struct FeedBasic {
  @State item: Feed | null = null;        // 动态数据
  @State showFollowButton: boolean;       // 是否显示关注按钮
  @State currentUser: BasicUserInfo | null; // 当前用户
  @State showCommentSheet: boolean = false; // 评论弹窗状态
  @State clickLike: boolean = false;       // 点赞状态
  @State blurRadius: number = 0;           // 模糊效果(非VIP限制)

  // ----------- 生命周期方法 -----------
  aboutToAppear() {
    this.setClickLike(); // 初始化点赞状态
    if (!this.isCurrentUserVip()) this.blurRadius = 20; // 非VIP模糊处理
  }

  // ----------- 核心交互方法 -----------
  // 点赞/取消点赞
  private handleLike() {
    const type = this.clickLike ? LikeOperType.CancelLike : LikeOperType.DoLike;
    LikeService.likeFeed(this.currentUser?.uid, this.item?.feedId, type)
      .then(() => this.refreshFeedDetail());
  }

  // 刷新动态详情
  private refreshFeedDetail() {
    FeedService.getFeedDetail(this.item?.feedId).then((data) => {
      this.item = data;
      this.setClickLike(); // 更新点赞状态
    });
  }

  // ----------- 页面构建 -----------
  build() {
    Column() {
      // 用户信息行
      this.buildFeedUserRow()
      // 动态文字内容
      this.buildFeedMsg()
      // 动态图片
      this.buildFeedImg()
      // 互动统计(点赞/评论)
      this.buildFeedInteractionSummary()
      // 互动详情(带VIP限制)
      if (this.showInteraction) this.buildFeedInteraction()
    }
    .overlay(this.FeedInteractionDetailMask(), { align: Alignment.Start })
  }

  // 构建用户信息行
  @Builder
  buildFeedUserRow() {
    Row() {
      // 用户头像
      Image(this.item?.profilePictureUrl)
        .clipShape(Circle)
      // 用户昵称 & ID
      Column() {
        Text(this.item?.nickName)
        Text(`ID: ${this.item?.userNo}`)
      }
      // 关注按钮(非自己时显示)
      if (this.currentUser?.uid !== this.item?.uid) {
        Button(this.showFollowButton ? '+关注' : '已关注')
          .onClick(this.handleFollow)
      }
    }
  }

  // 处理关注操作
  private handleFollow = () => {
    const type = this.showFollowButton ? FollowType.Follow : FollowType.UnFollow;
    FeedService.followUser(this.item?.uid, this.currentUser?.uid, type)
      .then(() => this.showFollowButton = !this.showFollowButton);
  }
}

// ------------------- 辅助组件 -------------------
// 空状态提示组件
@Builder
export function emptyActionPageContent(...) {
  Column() {
    Image($r('app.media.img_no_feed')) // 空状态图片
    Text('暂时还没有动态哦~')          // 提示文案
    Button('发布动态')                // 引导按钮
      .onClick(() => pageStack.pushPathByName('NewFeed'))
  }
}

// 图片网格组件
@Component
export struct GridShowImage {
  @Prop resourceUrl: string[];    // 图片资源数组
  @State gridItemHeight: number;  // 单图高度

  build() {
    Grid() {
      ForEach(this.resourceUrl, (url) => {
        GridItem() {
          Image(url)  // 单张图片
            .objectFit(ImageFit.Cover)
            .height(this.gridItemHeight)
        }
      })
    }
    .rowsTemplate('1fr 1fr') // 两行布局
    .columnsTemplate('1fr 1fr') // 两列布局
  }
}

三、最终效果
#我的鸿蒙开发手记#鸿蒙广场模块开发练习-鸿蒙开发者社区

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