知识库 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().lengthgetFavorites().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 管理,直接查询更简洁。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐