
#我的鸿蒙开发手记#鸿蒙广场模块开发练习 原创
一、背景与说明
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') // 两列布局
}
}
三、最终效果
