HarmonyOS知识库——个人中心的标签云与数据统计展示
知识库 App 的 ProfilePage 不是传统的"设置页"——它更像一个"知识概览仪表盘",用标签云展示知识分布、用统计卡片量化笔记成果、用渐变头像强化品牌感。这种"个人中心 = 数据可视化"的设计在工具类 App 里越来越常见。
完整效果
一、页面结构:三个区块
| 区块 | 内容 | 数据来源 |
|---|---|---|
| 个人信息 | 渐变头像 + 昵称 + 统计摘要 | getNotes().length + getFavorites().length |
| 统计卡片 | 笔记/收藏/标签三列 | getNotes() + getFavorites() + getAllTags() |
| 标签云 | 所有标签及使用次数 | getAllTags() |
三个区块的数据都来自 NoteService 的查询函数——页面没有自己的 @State 数据,全部实时查询。
二、数据加载的双重触发
@State tags: TagCount[] = []
aboutToAppear(): void { this.tags = getAllTags() }
onPageShow(): void { this.tags = getAllTags() }

为什么只给 tags 加了 @State,笔记和收藏数没有? 因为 tags 需要在标签云里渲染——标签数量和顺序可能变化(新增笔记带来新标签)。笔记数和收藏数直接在 build 里调用 getNotes().length 和 getFavorites().length,每次 build 都重新计算,不需要 @State 缓存。
踩坑记录: 最初 getNotes().length 写在 aboutToAppear 里赋值给 @State,用户新建笔记后返回 ProfilePage,统计数字没更新。改成在 build 里直接调用后,每次 build 都获取最新数据。
三、个人信息卡片的渐变头像
Row() {
Stack() {
Column() {}.width(56).height(56).borderRadius(28)
.linearGradient({ angle: 135, colors: [[A, 0], ['#A78BFA', 1]] })
Text('📝').fontSize(24)
}
Column() {
Text('知识管理者').fontSize(18).fontWeight(FontWeight.Bold).fontColor(T1)
Text('累计 ' + getNotes().length + ' 篇笔记 · ' + getFavorites().length + ' 篇收藏')
.fontSize(12).fontColor(T3).margin({ top: 2 })
}.alignItems(HorizontalAlign.Start).margin({ left: 14 }).layoutWeight(1)
}

Stack 实现渐变底座 + emoji 图标
Stack 让两层内容叠放——底层是渐变色圆形,上层是 emoji 图标。 linearGradient({ angle: 135, colors: [[A, 0], ['#A78BFA', 1]] }) 从左上角(#6C5CE7 紫色)到右下角(#A78BFA 浅紫色)的 135° 渐变。
为什么用 Stack 而不是给 Text 加背景色? Text 的 backgroundColor 是纯色,不支持渐变。Stack + 渐变 Column 是 ArkTS 里实现渐变背景的标准方式。
56×56 的尺寸 + borderRadius(28) = 正好是圆形。 28 = 56/2,borderRadius 设为宽高的一半就是正圆。
统计摘要的实时计算
Text('累计 ' + getNotes().length + ' 篇笔记 · ' + getFavorites().length + ' 篇收藏')
直接在 build 里调用 getNotes().length——每次 build 都重新计算。 这比 @State 缓存更简单——不需要 aboutToAppear 初始化,不需要 onPageShow 刷新。在数据量小的场景下(12 篇笔记),每次 build 重新查询的性能开销可以忽略。
和首页的对比: 首页用 @State favCount 缓存收藏数,ProfilePage 直接调用 getFavorites().length。首页需要 favCount 是因为有其他 @State 变化(如搜索)会触发 build,缓存避免不必要的重复查询。ProfilePage 没有其他 @State,直接调用更简洁。
四、统计卡片的复用
@Builder Stat(icon: string, value: string, label: string) {
Column() {
Text(icon).fontSize(20).margin({ bottom: 4 })
Text(value).fontSize(18).fontWeight(FontWeight.Bold).fontColor(A)
Text(label).fontSize(10).fontColor(T3)
}.layoutWeight(1).padding(14).backgroundColor('#FFFFFF').borderRadius(14)
}
和首页的 Stat Builder 完全一样。 两个页面各写了一份——因为 Builder 只能在当前组件内调用,不能跨页面复用。
两个页面的 Stat 调用对比
| 页面 | 调用 | 数据来源 |
|---|---|---|
| 首页 | Stat('📝', this.notes.length.toString(), '总笔记') |
@State notes |
| ProfilePage | Stat('📝', getNotes().length.toString(), '笔记') |
直接调用 getNotes() |
label 不同——首页叫"总笔记",ProfilePage 叫"笔记"。 因为两个页面的语境不同:首页是仪表盘,"总笔记"强调总量;ProfilePage 是个人中心,"笔记"更简洁。
ProfilePage 多了一个"标签"统计卡片。 首页统计笔记/收藏/笔记本,ProfilePage 统计笔记/收藏/标签。不同的统计维度反映了两个页面的不同职责——首页关注"内容管理",ProfilePage 关注"知识分布"。
五、标签云的 Flex 布局
Text('标签云').fontSize(16).fontWeight(FontWeight.Bold).fontColor(T1)
.width('100%').padding({ left: 16 }).margin({ bottom: 8 })
Flex({ wrap: FlexWrap.Wrap }) {
ForEach(this.tags, (t: TagCount) => {
Text('#' + t.name + ' (' + t.count + ')').fontSize(12).fontColor(A)
.padding({ left: 10, right: 10, top: 5, bottom: 5 })
.backgroundColor(A + '10').borderRadius(12)
.margin({ right: 6, bottom: 6 })
.onClick(() => {
router.pushUrl({ url: 'pages/TagFilterPage',
params: { 'tag': t.name } as Record<string,Object> })
})
})
}.width('100%').padding({ left: 16, right: 16 })

标签云 vs TagFilterPage 的标签网格
| 维度 | ProfilePage 标签云 | TagFilterPage 标签网格 |
|---|---|---|
| 每个标签的结构 | 单行文字(#标签名 + 次数) | 两行文字(标签名 + 次数) |
| 样式 | 浅紫背景 + 紫色文字 | 白色背景 + 紫色文字 |
| 点击行为 | 跳转 TagFilterPage | 同页面切换到筛选列表 |
| 尺寸 | 较小(padding 5-10) | 较大(padding 14) |
ProfilePage 的标签云更紧凑——一屏能展示更多标签。 因为标签云是"概览"性质的,用户快速扫一眼就知道有哪些标签。TagFilterPage 的标签网格是"操作"性质的,需要更大的点击区域方便用户选择。
标签文本的拼接
Text('#' + t.name + ' (' + t.count + ')')
格式:#标签名 (次数)。 # 前缀是 hashtag 风格,(次数) 用括号包裹数量。这种格式在社交媒体里很常见——用户一看就知道"这是标签,后面是使用次数"。
和首页笔记卡片的标签格式对比: 首页用 Text(' #' + t) 只显示标签名不显示次数,因为首页空间有限。ProfilePage 空间充裕,可以同时显示标签名和次数。
六、标签云的点击跳转
.onClick(() => {
router.pushUrl({ url: 'pages/TagFilterPage',
params: { 'tag': t.name } as Record<string,Object> })
})
点击标签跳转到 TagFilterPage,并传入标签名。 TagFilterPage 收到参数后直接显示该标签的筛选结果——跳过标签网格,直达笔记列表。
跳转链路: ProfilePage → 点击标签 → TagFilterPage(selectedTag=标签名)→ 显示筛选笔记列表。用户从"看到标签"到"看到笔记"只需要一次点击 + 一次跳转。
七、build 里直接调用查询函数的设计
build() {
Column() {
// ...
Text('累计 ' + getNotes().length + ' 篇笔记 · ' + getFavorites().length + ' 篇收藏')
// ...
this.Stat('📝', getNotes().length.toString(), '笔记')
this.Stat('❤️', getFavorites().length.toString(), '收藏')
this.Stat('🏷️', this.tags.length.toString(), '标签')
}
}
build 里有 4 次函数调用: getNotes() 两次、getFavorites() 两次、getAllTags() 零次(tags 已缓存在 @State 里)。
为什么 getNotes 和 getFavorites 调用了两次? 一次在个人信息卡片的统计摘要里,一次在 Stat Builder 里。两次调用返回相同结果——因为 _notes 和 _favs 在同一次 build 里不会变化。
优化方案: 可以在 aboutToAppear 里缓存笔记数和收藏数到 @State,build 里直接读取。但在 12 篇笔记的规模下,两次查询的性能差异可以忽略不计。代码简洁性比微小的性能优化更重要。
八、整体嵌套结构
Column 页面容器
├→ Row (导航栏) 返回 + "我的"标题
└→ Scroll 可滚动内容
└→ Column
├→ Row (个人信息卡片) Stack 渐变头像 + 文字信息
├→ Row + @Builder Stat × 3 统计卡片
├→ Text ("标签云" 标题)
└→ Flex(FlexWrap.Wrap) 标签云
嵌套深度 3 层——和 NoteList 一样。 没有底部导航栏,不需要 Stack 层叠。个人信息卡片是最复杂的区块——Stack 实现渐变底座,Row 实现左右布局。
和首页的嵌套对比: 首页 4 层(Stack→Scroll→Column→Row),ProfilePage 3 层(Scroll→Column→Row)。ProfilePage 更浅是因为没有底部导航栏。
九、页面间的数据一致性
ProfilePage 展示的数据(笔记数、收藏数、标签数)来自 NoteService 的查询函数。用户在 NoteEditor 新建笔记后返回 ProfilePage,onPageShow 重新调用 getAllTags(),标签云自动更新。笔记数和收藏数在 build 里直接调用 getNotes().length,每次 build 都获取最新数据。
数据一致性的保证方式:
| 数据 | 更新方式 | 触发时机 |
|---|---|---|
| tags | @State + onPageShow 刷新 | 页面显示时 |
| 笔记数 | build 里直接调用 getNotes().length | 每次 build |
| 收藏数 | build 里直接调用 getFavorites().length | 每次 build |
三种数据用两种更新方式——tags 用 @State 缓存,笔记数和收藏数实时查询。 这种混合策略在简单场景下很实用——不需要所有数据都用 @State 管理,直接查询更简洁。
更多推荐



所有评论(0)