
-
个人首页: VON
-
鸿蒙系列专栏: 鸿蒙开发小型案例总结
-
综合案例 :鸿蒙综合案例开发
-
鸿蒙6.0:从0开始的开源鸿蒙6.0.0
-
鸿蒙5.0:鸿蒙5.0零基础入门到项目实战
-
Electron适配开源鸿蒙专栏:Electron for OpenHarmony
-
Flutter 适配开源鸿蒙专栏:Flutter for OpenHarmony
-
本文所属专栏:鸿蒙综合案例开发
-
本文atomgit地址:小V健身
小V健身助手开发手记(四)
- [打造专属健康空间------以 `PersonContent` 构建统一风格的个人中心](#打造专属健康空间——以
PersonContent构建统一风格的个人中心) -
- [🧩 一、为什么是 `PersonContent` 而非 `PersonalPage`?](#🧩 一、为什么是
PersonContent而非PersonalPage?) - [🖼️ 二、UI 结构与视觉语言](#🖼️ 二、UI 结构与视觉语言)
- [💻 三、核心代码解析](#💻 三、核心代码解析)
-
- [1. 组件定义与状态管理](#1. 组件定义与状态管理)
- [2. 用户信息区(`buildUserInfo`)](#2. 用户信息区(
buildUserInfo)) - [3. 功能列表(基于 `List` + `PersonalItem`)](#3. 功能列表(基于
List+PersonalItem))
- [🔗 四、路由与集成](#🔗 四、路由与集成)
- [🛠️ 五、扩展性与未来演进](#🛠️ 五、扩展性与未来演进)
- [✅ 六、结语](#✅ 六、结语)
- 代码总结
- [🧩 一、为什么是 `PersonContent` 而非 `PersonalPage`?](#🧩 一、为什么是

打造专属健康空间------以 PersonContent 构建统一风格的个人中心
在移动应用的用户体验地图中,个人中心往往被低估。它不像首页那样高频曝光,也不如成就页那样充满激励感,但它却是用户建立"归属感"与"掌控感"的关键节点。一个设计良好的个人中心,能让用户感受到:"这是我的空间,一切尽在掌握。"
在「小V健身助手」的开发进程中,我们始终坚持组件化、一致性与轻量化 三大原则。继首页、成就页之后,我们以同样的架构思想,构建了名为 PersonContent 的个人中心视图组件------它不是独立页面,而是可嵌入、可复用、风格统一的 UI 模块。
本文将详解 PersonContent 的设计思路、代码实现与工程价值。
🧩 一、为什么是 PersonContent 而非 PersonalPage?
在 HarmonyOS 的 Stage 模型中,页面通常由 @Entry 装饰的组件作为入口。但在 Tab 导航或多场景复用的场景下,将内容逻辑与路由入口解耦是更优的选择。
参考我们已有的 AchievementContent:
ts
@Component
export default struct AchievementContent { ... }
它不带 @Entry,仅负责渲染成就区域的内容,可被主页面按需嵌入。
同理,我们将个人中心也抽象为 PersonContent:
- ✅ 职责单一:只管 UI 渲染,不处理路由跳转逻辑(除内部子跳转);
- ✅ 高度复用:未来可用于侧边栏、设置弹窗等场景;
- ✅ 风格统一 :与
AchievementContent共享布局规范、间距系统与交互反馈。
这是一种"页面即组件"的现代前端思维。
🖼️ 二、UI 结构与视觉语言
PersonContent 采用经典的"头像区 + 功能列表"布局:
[ 头像 + 昵称 + 日期 ]
──────────────────────
[ 目标设置 → ]
[ 历史记录 → ]
[ 我的成就 → ]
[ 隐私与数据 → ]
[ 关于小V v1.0.0 ]
[ 退出应用(红色)]

设计细节:
- 背景色 :使用浅灰
#f5f5f5,区别于首页的深色运动氛围,营造"后台管理"的冷静感; - 列表项:白底圆角卡片,内含图标、标题、描述与箭头,信息层级清晰;
- 退出按钮:单独高亮为红色文字,避免误触,同时传递"重要操作"信号;
- 日期同步 :顶部展示当前选中日期(来自全局
AppStorage),与首页、成就页保持上下文一致。
💻 三、核心代码解析
1. 组件定义与状态管理
ts
@Component
export struct PersonContent {
// 双向绑定全局日期,确保与其他页面同步
@StorageLink('date') date: number = DateUtil.beginTimeOfDay(new Date())
// 本地状态:今日目标(后续可接入持久化)
@State dailyTarget: number = 2000
build() { /* ... */ }
}
使用
@StorageLink而非@StorageProp,是因为我们希望个人中心也能响应日期变更(例如从日历选择新日期后,顶部自动刷新)。
2. 用户信息区(buildUserInfo)
ts
@Builder
buildUserInfo() {
Row() {
Image($r('app.media.ic_default_avatar'))
.width(60).height(60).borderRadius(30)
Column() {
Text('小V用户').fontSize(18).fontWeight(600)
Text(DateUtil.formatDate(this.date)).fontSize(12).fontColor('#888')
}.margin({ left: 15 })
Blank() // 推动右侧对齐
}
.padding({ bottom: 25 })
}
简洁、克制,突出身份认同而非社交属性(当前无账号体系)。
3. 功能列表(基于 List + PersonalItem)
我们封装了可复用的 PersonalItem 组件:
ts
// component/PersonalItem.ets
@Component
export struct PersonalItem {
icon: ResourceStr
title: string
desc: string
showArrow: boolean
onClick: () => void
build() {
Row() {
Image(this.icon).width(24).height(24).margin({ right: 15 })
Column() {
Text(this.title).fontSize(16).fontWeight(500)
if (this.desc) Text(this.desc).fontSize(12).opacity(0.6)
}
Blank()
if (this.showArrow) Image($r('app.media.arrow_right')).width(16)
}
.backgroundColor(Color.White)
.borderRadius(12)
.padding({ horizontal: 16, vertical: 12 })
.onClick(this.onClick)
}
}
通过组合 PersonalItem,PersonContent 的列表代码变得极其简洁且语义清晰。
🔗 四、路由与集成
在主页面中,只需一行即可嵌入:
ts
// MainIndexPage.ets
if (tabIndex === 2) {
PersonContent()
}
内部跳转使用标准 ArkTS 路由:
ts
router.pushUrl({ url: 'pages/AchievementPage' })
注意:
AchievementPage应是一个带@Entry的完整页面,而AchievementContent是其内部的内容组件------这种"页面壳 + 内容体"模式,是我们推荐的分层方式。
🛠️ 五、扩展性与未来演进
当前 PersonContent 是 MVP 版本,但已预留扩展路径:
| 功能 | 实现方式 |
|---|---|
| 动态昵称/头像 | 从 preferences 读取,支持编辑弹窗 |
| 目标持久化 | 将 dailyTarget 存入 AppStorage 或 data_preferences |
| 缓存清理 | 在"隐私与数据"页调用 preferences.clear() |
| 深色模式适配 | 使用 @Watch 监听系统主题,动态切换颜色 |
所有这些,都不会破坏现有组件结构。
✅ 六、结语
PersonContent 的诞生,标志着「小V健身助手」完成了从"功能驱动"到"体验驱动"的一次跃迁。它不再只是一个功能列表,而是一个有温度、有秩序、有控制感的个人健康空间。
更重要的是,它延续了我们对代码可维护性的坚持:
好的 UI,不仅是看起来舒服,更是写起来清晰、改起来安全。
代码总结
此次更新只有这两部分代码,这里路由部分没有实现

PersonalItem
ts
// component/PersonalItem.ets
interface PersonalItemFace{
icon: ResourceStr
title: string
desc?: string
showArrow?: boolean
}
@Component
export default struct PersonalItem {
@Prop icon: ResourceStr;
@Prop title: string;
@Prop desc: string = '';
@Prop showArrow: boolean = true;
// @Prop onClick: (event?: ClickEvent) => void;
build() {
Row() {
Image(this.icon)
.width(24)
.height(24)
.margin({ right: 15 })
Column() {
Text(this.title)
.fontSize(16)
.fontWeight(500)
.alignSelf(ItemAlign.Start)
if (this.desc) {
Text(this.desc)
.fontSize(12)
.opacity(0.6)
.alignSelf(ItemAlign.Start)
.margin({ top: 3 })
}
}
Blank()
if (this.showArrow) {
Image($r('app.media.arrow_right'))
.width(16)
.height(16)
.opacity(0.5)
}
}
.width('100%')
.height(60)
.backgroundColor(Color.White)
.borderRadius(12)
.padding({ left: 16, right: 16 })
// .onClick(this.onClick)
}
}
PersonContent
ts
// view/PersonContent.ets
import { router } from '@kit.ArkUI'
import DateUtil from '../../util/DateUtil'
import PersonalItem from '../../component/PersonalItem'
@Component
export struct PersonContent {
// 从全局 Storage 获取当前日期(用于展示)
@StorageLink('date') date: number = DateUtil.beginTimeOfDay(new Date())
// 当前用户的每日目标(实际项目中应从持久化存储读取)
@State dailyTarget: number = 2000
build() {
Column() {
// 用户信息区域
this.buildUserInfo()
// 功能列表
List({ space: 12 }) {
// 目标设置
ListItem() {
PersonalItem({
icon: $r('app.media.ic_target'),
title: '今日目标',
desc: `${this.dailyTarget} 千卡`,
showArrow: true,
// onClick: () => {
// // TODO: 弹出目标设置弹窗(后续可扩展)
// console.log('打开目标设置')
// }
})
}
// 历史记录
ListItem() {
PersonalItem({
icon: $r('app.media.ic_history'),
title: '历史记录',
desc: '',
showArrow: true,
// onClick: () => {
// router.pushUrl({ url: 'pages/HistoryPage' })
// }
})
}
// 成就系统
ListItem() {
PersonalItem({
icon: $r('app.media.ic_achieve'),
title: '我的成就',
desc: '',
showArrow: true,
// onClick: () => {
// router.pushUrl({ url: 'pages/AchievementPage' })
// }
})
}
// 隐私与数据
ListItem() {
PersonalItem({
icon: $r('app.media.ic_privacy'),
title: '隐私与数据',
desc: '',
showArrow: true,
// onClick: () => {
// router.pushUrl({ url: 'pages/PrivacyDataPage' })
// }
})
}
// 关于小V
ListItem() {
PersonalItem({
icon: $r('app.media.ic_about'),
title: '关于小V',
desc: 'v1.0.0',
showArrow: false,
// onClick: () => {}
})
}
// 退出应用
ListItem() {
Button('退出应用')
.width('100%')
.height(50)
.fontSize(16)
.fontColor('#ff3b30')
.backgroundColor(Color.Transparent)
.onClick(() => {
router.clear()
})
}
}
.width('100%')
.alignListItem(ListItemAlign.Start)
}
.width('100%')
.height('100%')
.padding({ top: 20, left: 20, right: 20 })
.backgroundColor('#f5f5f5')
}
@Builder
buildUserInfo() {
Row() {
Image($r('app.media.ic_default_avatar'))
.width(60)
.height(60)
.borderRadius(30)
Column() {
Text('小V用户')
.fontSize(18)
.fontWeight(600)
.fontColor(Color.Black)
Text(DateUtil.formatDate(this.date))
.fontSize(12)
.fontColor('#888888')
}
.alignItems(HorizontalAlign.Start)
.margin({ left: 15 })
Blank()
}
.width('100%')
.padding({ bottom: 25 })
}
}