HarmonyOS旅行探索——个人中心页面与用户数据服务
旅行探索 App 的"我的"页面和个人中心在结构上和运动健康 App 类似——用户卡片、统计数字、菜单列表。但有一个关键区别:旅行 App 的个人中心引入了一个 UserDataService 数据服务层,负责管理用户在其他页面产生的状态数据(保存的城市、打卡的景点、收藏的美食)。这让个人中心从"纯展示"升级成了"数据聚合"。
完整效果

页面结构
从截图看,页面从上到下依次是:
- 导航栏:返回按钮 + "我的"标题
- 用户卡片:头像 + 昵称 + 副标题(已探索城市数)
- 三栏统计:探索城市、打卡景点、收藏美食
- 已保存的城市:用户在城市详情页收藏的城市列表
- 菜单列表:我的行程、美食收藏、旅行工具、设置
前三个区域是固定布局,第四区域是动态的(取决于用户收藏了哪些城市),第五区域是固定入口。
UserDataService:用户状态的管理
import { getSavedCities, getVisitedCount, getFoodSaved } from '../services/UserDataService'
这个服务暴露了三个方法:
getSavedCities():返回用户收藏的城市列表getVisitedCount():返回已探索的城市数量getFoodSaved():返回收藏的美食数量
这三个方法被个人中心页面调用,用来填充统计数字和城市列表。从调用方式看,UserDataService 应该是一个全局单例——各页面通过调用它的方法来读写用户状态,而不需要自己管理状态存储。
这种设计的好处是数据源单一——所有页面的"用户状态"都从同一个服务获取,不会出现不同页面数据不一致的问题。城市详情页收藏了一个城市后,getSavedCities() 会立即返回更新后的列表。
onPageShow 的实时刷新
onPageShow(): void { this.refresh() }
aboutToAppear(): void { this.refresh() }
private refresh(): void {
this.saved = getSavedCities()
}

refresh() 在两个生命周期里被调用:
aboutToAppear:页面首次创建时调用onPageShow:每次页面从后台回到前台时调用
onPageShow 是关键——它保证了用户从城市详情页返回时,"已保存的城市"列表会自动更新。如果只在 aboutToAppear 里加载数据,用户收藏了一个城市后返回个人中心,列表不会刷新,因为页面没有被销毁重建。
这种"每次显示时刷新"的模式在需要跨页面同步数据的场景里很常见。代价是每次显示都会重新调用 getSavedCities(),如果数据量很大或查询逻辑很复杂,可能需要优化。但对于城市列表这种小数据量的场景,完全没问题。
用户卡片
Row() {
Stack() {
Column() {}.width(60).height(60).borderRadius(30)
.linearGradient({ angle: 135, colors: [[A, 0], ['#FF9F43', 1]] })
Text('🧳').fontSize(28)
}
Column() {
Text('旅行者').fontSize(18).fontWeight(FontWeight.Bold).fontColor(T1)
Text('已探索 ' + getVisitedCount() + ' 个城市 · 旅途愉快!')
.fontSize(11).fontColor(T3).margin({ top: 2 })
}.alignItems(HorizontalAlign.Start).margin({ left: 14 }).layoutWeight(1)
}

头像区用 Stack 实现——底层是橙色渐变圆,上层是旅行箱 emoji。和运动健康 App 的用户卡片结构完全一样,只是 emoji 从跑步换成了旅行箱。
副标题是动态的:'已探索 ' + getVisitedCount() + ' 个城市 · 旅途愉快!'。getVisitedCount() 每次调用都返回最新值,所以用户收藏新城市后,这里的数字会自动更新。
三栏统计
Row({ space: 8 }) {
this.Stat(getVisitedCount().toString(), '探索城市')
this.Stat(this.landmarkCount().toString(), '打卡景点')
this.Stat(getFoodSaved().toString(), '收藏美食')
}
三个统计卡片共享同一个 Stat Builder,传入不同的数值和标签。三个数字都来自服务层:
- 探索城市:
getVisitedCount()直接返回 - 打卡景点:
this.landmarkCount()需要额外计算 - 收藏美食:
getFoodSaved()直接返回
landmarkCount 的计算逻辑
private landmarkCount(): number {
let c: number = 0
for (let i: number = 0; i < this.saved.length; i++) {
for (let j: number = 0; j < CITIES.length; j++) {
if (CITIES[j].id === this.saved[i].cityId) {
c += CITIES[j].landmarks.length
break
}
}
}
return c
}
景点数量不是服务层直接提供的,而是个人中心页面自己算的。逻辑是:遍历用户保存的城市,根据 cityId 从 CITIES 数组里找到对应城市,累加它的 landmarks.length。
这种"跨数据源聚合"的做法在原型阶段是合理的——UserDataService 管用户状态,TravelData 管城市数据,个人中心负责把两者关联起来。如果以后数据量增大,可以把这个计算移到服务层,减少页面的计算负担。
条件渲染:已保存的城市
if (this.saved.length > 0) {
Column() {
Text('已保存的城市').fontSize(16).fontWeight(FontWeight.Bold).fontColor(T1)
.width('100%').margin({ bottom: 10 })
Column({ space: 8 }) {
ForEach(this.saved, (item: SavedItem) => {
Row() {
Text('🏙️').fontSize(20).margin({ right: 10 })
Text(item.name).fontSize(14).fontWeight(FontWeight.Medium)
.fontColor(T1).layoutWeight(1)
SymbolGlyph($r('sys.symbol.chevron_right')).fontSize(14).fontColor(['#DDD'])
}.width('100%').padding(12).backgroundColor('#F8F7FC').borderRadius(12)
.onClick(() => {
router.pushUrl({
url: 'pages/CityDetail',
params: { 'cityId': item.cityId } as Record<string, Object>
})
})
})
}
}
}
if (this.saved.length > 0) 是一个条件渲染——只有当用户保存了至少一个城市时,才显示这个区域。如果用户没有任何收藏,整个区域不渲染,不会出现空白的标题栏。
这个条件渲染的 if 在 ArkTS 里是声明式的——当 this.saved.length 从 0 变成 1 时,整个 Column 会自动出现在 UI 上;从 1 变回 0 时,自动消失。不需要手动控制 Visibility 或 display 属性。
SavedItem 的点击跳转
每张保存的城市卡片点击后跳转到城市详情页,传递 cityId。和首页城市网格的跳转逻辑一样——传 ID,详情页根据 ID 查询数据。
SavedItem 接口只有两个字段:
interface SavedItem { cityId: number; name: string }
cityId 用于跳转时传参,name 用于列表展示。不需要存整个 City 对象——详情页已经有完整的城市数据了,传 ID 就够。
菜单列表
Column({ space: 0 }) {
this.Menu('我的行程', '🧳', () => { router.pushUrl({ url: 'pages/TripPlanner' }) })
this.Div()
this.Menu('美食收藏', '🍜', () => { router.pushUrl({ url: 'pages/CuisinePage' }) })
this.Div()
this.Menu('旅行工具', '🔧', () => { router.pushUrl({ url: 'pages/ToolsPage' }) })
this.Div()
this.Menu('设置', '⚙️', () => { })
}
四个菜单项,其中三个有跳转逻辑,"设置"是空函数——目前没有实现设置页面。和运动健康 App 的个人中心一样,先把入口放好,功能后面再补。
Menu Builder 和 Div Builder 的实现和运动健康 App 完全一样——白色背景卡片、emoji 图标 + 文字 + 右箭头、分割线从左侧 52px 开始(跳过图标区域)。
和运动健康 App 个人中心的差异
| 维度 | 运动健康 App | 旅行探索 App |
|---|---|---|
| 数据来源 | 全部硬编码 | 部分来自服务层 |
| 用户卡片 | 静态"运动达人" | 动态"已探索 X 个城市" |
| 统计数字 | 硬编码 156/48920/342 | 动态计算 |
| 条件渲染 | 无(菜单始终显示) | 有(无收藏时不显示城市列表) |
| 跨页面同步 | 无 | 有(onPageShow 刷新) |
旅行 App 的个人中心比运动健康 App 多了一个关键能力——跨页面数据同步。用户在城市详情页收藏了一个城市,返回个人中心后列表会自动更新。这得益于 UserDataService 的全局状态管理和 onPageShow 的刷新机制。
运动健康 App 的个人中心目前没有这个能力——所有数据都是写死的,不会因为用户在其他页面的操作而变化。如果以后要给运动健康 App 加上类似的功能(比如累计运动天数的实时统计),也需要引入一个类似的用户状态服务。
数据流的闭环
个人中心的数据流形成了一个闭环:
CityDetail(收藏城市)→ UserDataService(存储状态)
↓
ProfilePage(读取状态)→ getSavedCities() / getVisitedCount()
↓
UI 更新(城市列表、统计数字)
用户在详情页的操作(收藏/取消收藏)改变了服务层的状态,个人中心通过 onPageShow 感知到页面重新显示,调用 refresh() 读取最新状态,UI 自动更新。
这个闭环的关键节点是 onPageShow——它是"页面从后台回到前台"的信号。没有这个信号,个人中心就不知道什么时候该刷新数据。这也是为什么 onPageShow 比 aboutToAppear 更重要——后者只在页面首次创建时调用,前者在每次显示时都调用。
更多推荐

所有评论(0)