知识库 App 的 ProfilePage 从简单的"头像+标签云"升级为完整的"数据仪表盘"——渐变个人信息卡、本周活跃柱状图、常用标签进度条、最近编辑列表、设置面板,五个区块覆盖了用户对"个人中心"的所有预期。

完整效果
在这里插入图片描述
在这里插入图片描述

一、五个区块的职责

区块 内容 数据来源
渐变个人信息卡 头像+昵称+ID+四维统计 getNotes/getFavorites/getAllTags+总字数
本周活跃 柱状图+连续天数 weekDays 数组(mock)
常用标签 标签列表+进度条 getAllTags + topTags() 排序
最近编辑 3 篇笔记预览 getNotes() 取前 3
设置面板 6 个设置项 静态数据

二、渐变个人信息卡

Column() {
  Row() {
    Stack() {
      Column() { Text('📝').fontSize(28) }
      .width(64).height(64).borderRadius(32)
      .linearGradient({ angle: 135, colors: [['#8B5CF6', 0], ['#A78BFA', 0.5], ['#C4B5FD', 1]] })
      .shadow({ radius: 8, color: 'rgba(108,92,231,0.3)', offsetY: 4 })
    }
    Column() {
      Text('知识管理者').fontSize(20).fontWeight(FontWeight.Bold).fontColor(CW)
      Row({ space: 4 }) {
        Text('ID: 20260715').fontSize(11).fontColor('rgba(255,255,255,0.6)')
      }.margin({ top: 4 })
    }.alignItems(HorizontalAlign.Start).margin({ left: 16 }).layoutWeight(1)
  }.width('100%').margin({ bottom: 20 })

  Row() {
    this.ProfileStat(getNotes().length.toString(), '笔记')
    Divider().vertical(true).height(28).color('rgba(255,255,255,0.2)')
    this.ProfileStat(getFavorites().length.toString(), '收藏')
    Divider().vertical(true).height(28).color('rgba(255,255,255,0.2)')
    this.ProfileStat(this.totalWords.toString(), '总字数')
    Divider().vertical(true).height(28).color('rgba(255,255,255,0.2)')
    this.ProfileStat(this.tags.length.toString(), '标签')
  }.width('100%').justifyContent(FlexAlign.SpaceAround)
}
.linearGradient({ angle: 160, colors: [['#6C5CE7', 0], ['#8B5CF6', 0.6], ['#A78BFA', 1]] })
.borderRadius(20).margin({ left: 16, right: 16, bottom: 16 })

在这里插入图片描述

三色渐变的设计

头像用三色渐变(#8B5CF6→#A78BFA→#C4B5FD),卡片背景也用三色渐变(#6C5CE7→#8B5CF6→#A78BFA)。两组渐变的方向不同——头像 135°,卡片 160°——视觉上有层次感但色系统一。

shadow 给头像加了紫色投影。 rgba(108,92,231,0.3) 是主色的 30% 透明度,投影颜色和卡片背景一致,视觉上头像"浮"在卡片上。

四维统计的 Divider 分隔

Divider().vertical(true).height(28).color('rgba(255,255,255,0.2)')

垂直 Divider 分隔四个统计数字。 20% 白色透明度的分割线在渐变背景上隐约可见——分隔了数字但不抢视觉焦点。height(28) 限制分割线高度,不会撑满整个卡片。

ProfileStat Builder 的简洁设计

@Builder ProfileStat(value: string, label: string) {
  Column() {
    Text(value).fontSize(18).fontWeight(FontWeight.Bold).fontColor(CW)
    Text(label).fontSize(10).fontColor('rgba(255,255,255,0.7)').margin({ top: 2 })
  }
}

在这里插入图片描述

白色文字在渐变背景上——数字用 Bold 强调,标签用 70% 白色透明度弱化。 和首页的 Stat Builder 结构一样,但颜色方案完全不同——首页是深色文字白底,ProfilePage 是白色文字渐变底。

总字数的实时计算

private refresh(): void {
  let words: number = 0
  for (let i: number = 0; i < all.length; i++) { words += all[i].content.length }
  this.totalWords = words
}

遍历所有笔记的 content 字段,累加字符串长度。 这是一个粗略的字数统计——中文一个字算一个字符,英文一个字母算一个字符。精确的"字数"需要按语言分别计算,但对知识库 App 来说,字符数已经够用了。

三、本周活跃柱状图

@State weekDays: number[] = [3, 1, 4, 2, 5, 1, 3]
@State currentStreak: number = 5

Row() {
  ForEach(this.weekDays, (count: number, idx: number) => {
    Column() {
      Text(this.dayName(idx)).fontSize(10).fontColor(T3).margin({ bottom: 6 })
      Stack({ alignContent: Alignment.Bottom }) {
        Column().width(24).height(60).borderRadius(12).backgroundColor('#F0EEF4')
        Column().width(24).height(Math.max(8, count * 12)).borderRadius(12)
          .linearGradient({ angle: 0, colors: [[A + '40', 0], [A, 1]] })
      }
      Text(count.toString()).fontSize(10).fontColor(T2).margin({ top: 4 })
    }.layoutWeight(1).alignItems(HorizontalAlign.Center)
  })
}

在这里插入图片描述

Stack 实现柱状图的底层逻辑

Stack 让两层 Column 叠放——底层是灰色底座(60px 高),上层是渐变色柱子。 alignContent: Alignment.Bottom 让柱子从底部向上生长,底座始终可见。

柱子高度 Math.max(8, count * 12) 保证最小 8px。 即使 count=0(当天没有笔记),柱子也有 8px 高度——视觉上不会出现"完全没有柱子"的空白。

渐变柱子的颜色设计

Column().width(24).height(Math.max(8, count * 12)).borderRadius(12)
  .linearGradient({ angle: 0, colors: [[A + '40', 0], [A, 1]] })

从下到上的渐变(angle: 0)——底部 40% 透明度,顶部全色。 这种渐变让柱子看起来有"深度"——底部淡、顶部浓,视觉上更立体。

count * 12 是柱子高度系数。 count=5 时柱子高 60px(刚好等于底座高度),count=1 时高 12px。这个系数让柱子高度差异明显但不夸张。

dayName 的星期转换

private dayName(idx: number): string {
  const days: string[] = ['一', '二', '三', '四', '五', '六', '日']
  return '周' + days[idx]
}

idx 0=周一,idx 6=周日。 数组索引和星期对应——weekDays 数组的顺序就是周一到周日。

当前数据是 mock

weekDays: number[] = [3, 1, 4, 2, 5, 1, 3] 是硬编码的 mock 数据。真实 App 需要根据用户的笔记创建/更新时间统计每天的活跃度。currentStreak: number = 5 也是 mock——需要计算连续多少天有笔记操作。

四、常用标签的排序与进度条

topTags 的冒泡排序

private topTags(): TagCount[] {
  const sorted: TagCount[] = []
  for (let i: number = 0; i < this.tags.length; i++) { sorted.push(this.tags[i]) }
  for (let i: number = 0; i < sorted.length - 1; i++) {
    for (let j: number = i + 1; j < sorted.length; j++) {
      if (sorted[j].count > sorted[i].count) {
        const tmp: TagCount = sorted[i]; sorted[i] = sorted[j]; sorted[j] = tmp
      }
    }
  }
  const result: TagCount[] = []
  for (let i: number = 0; i < sorted.length && i < 5; i++) { result.push(sorted[i]) }
  return result
}

先复制数组(避免修改原始 tags),再冒泡排序按 count 降序,最后取前 5 个。

为什么要复制? 直接排序 this.tags 会改变 @State 的顺序——标签云的显示顺序也会变。复制后排序不影响原始数据。

冒泡排序的时间复杂度 O(n²)。 在 15 个标签的规模下完全不是问题。如果标签数量增大,可以用更快的排序算法,但原型阶段不需要。

进度条的实现

Row() {
  Column().width(Math.max(4, t.count * 14)).height(6).borderRadius(3)
    .linearGradient({ angle: 90, colors: [[A, 0], [A + '40', 1]] })
}.width(80).height(6).backgroundColor('#F0EEF4').borderRadius(3)

Stack 效果用嵌套 Row+Column 实现——外层 Row 是底座(灰色,80px 宽),内层 Column 是进度条(渐变色,宽度动态)。

进度条宽度 Math.max(4, t.count * 14) 保证最小 4px。 count=3 时宽 42px,count=2 时宽 28px。80px 的底座宽度是固定的——进度条在底座内从左向右填充。

渐变方向 angle: 90 从左到右——左端全色,右端 40% 透明度。 和柱状图的渐变方向不同——柱状图从下到上,进度条从左到右。不同的渐变方向增加了视觉丰富度。

标签行的完整结构

Row                                    标签行(白色背景 + 圆角)
  ├→ Text('#' + t.name)               标签名(13px 粗体)
  ├→ Text(t.count + ' 篇')            数量(11px 灰色)
  └→ Row(进度条底座)                    80×6 灰色底座
       └→ Column(进度条填充)            渐变色填充

点击标签跳转 TagFilterPage。 和首页笔记卡片的标签点击逻辑一致——传入标签名,TagFilterPage 显示筛选结果。

五、最近编辑列表

@State recentNotes: Note[] = []

private refresh(): void {
  const all = getNotes()
  const recent: Note[] = []
  for (let i: number = 0; i < all.length && i < 3; i++) { recent.push(all[i]) }
  this.recentNotes = recent
}

取前 3 篇笔记——因为 getNotes() 返回的数组刚好按 id 降序排列(新笔记放最前面)。 这依赖 addNote 的实现——新笔记插入数组最前面。如果 addNote 改成 push 到末尾,这里需要按 updatedAt 排序。

踩坑记录: 最初取了全部笔记显示在"最近编辑"里,列表太长。限制为 3 篇后,用户一屏就能看完。

nbIcon/nbColor 的新实现

private nbIcon(id: number): string {
  const icons: Record<number, string> = { 1: '💼', 2: '📚', 3: '🌿', 4: '💡' }
  return icons[id] || '📝'
}

private nbColor(id: number): string {
  const colors: Record<number, string> = { 1: '#FF6B6B', 2: '#6C5CE7', 3: '#00B894', 4: '#FF9F43' }
  return colors[id] || '#CCC'
}

用 Record 对象替代 for 循环查找——代码更简洁。 icons[id] 直接通过 key 取值,不需要遍历 NOTEBOOKS 数组。|| '📝' 是默认值——id 不在映射表里时返回默认图标/颜色。

和之前 for 循环版本的对比:

方案 代码量 可读性 性能
for 循环 3 行 需要理解循环逻辑 O(n)
Record 查找 1 行 直观(key→value) O(1)

Record 查找在各方面都更优——代码更少、更直观、更快。唯一的缺点是需要手动维护映射表,但 4 个笔记本的映射维护成本很低。

六、设置面板的 SettingRow

@Builder SettingRow(icon: string, title: string, desc: string) {
  Row() {
    Text(icon).fontSize(18).width(32)
    Column() {
      Text(title).fontSize(14).fontColor(T1)
      if (desc.length > 0) {
        Text(desc).fontSize(11).fontColor(T3).margin({ top: 1 })
      }
    }.alignItems(HorizontalAlign.Start).layoutWeight(1)
    SymbolGlyph($r('sys.symbol.chevron_right')).fontSize(14).fontColor(['#CCC'])
  }.width('100%').padding({ left: 16, right: 16, top: 14, bottom: 14 })
  .border({ width: { bottom: 0.5 }, color: '#F5F5FA' })
}

SettingRow 是通用的"图标+标题+描述+箭头"组件。 接收三个参数就能渲染一个设置项,代码零重复。

if (desc.length > 0) 控制描述的显隐。 “帮助与反馈"和"关于"没有描述,不渲染第二行。有描述的设置项(如"主题设置"→"跟随系统”)显示两行。

底部边框用 border({ width: { bottom: 0.5 } }) 而不是 Divider。 因为 SettingRow 在 Column({ space: 0 }) 里紧密排列,用 border-bottom 做分隔线比插入 Divider 更简洁。

6 个设置项的当前状态

设置项 描述 当前状态
🎨 主题设置 跟随系统 纯展示
📤 导出数据 JSON / Markdown 纯展示
🔔 提醒设置 每日 21:00 纯展示
☁️ 云同步 未开启 纯展示
❓ 帮助与反馈 纯展示
ℹ️ 关于 v1.0.0 纯展示

当前 6 个设置项都是纯展示——点击后没有实际功能。 这是原型阶段的常见做法,先搭 UI 框架,后续逐步实现功能。

七、整体嵌套结构

Column                                    页面容器
  ├→ Row (导航栏)                          返回 + "我的" + 设置图标
  └→ Scroll                                可滚动内容
       └→ Column
            ├→ Column (渐变个人信息卡)       渐变背景 + 头像 + 四维统计
            ├→ Column (本周活跃)            柱状图 + 连续天数
            ├→ Column (常用标签)            排序标签 + 进度条
            ├→ Column (最近编辑)            3 篇笔记预览
            ├→ Column (设置面板)            6 个 SettingRow
            └→ Column (页脚)               版本号 + 标语

嵌套深度 3 层:Scroll → Column → 各区块。 没有底部导航栏,不需要 Stack。每个区块是一个独立的 Column 容器,白色背景 + 圆角 + margin,结构清晰。

6 个区块之间的间距都是 margin({ bottom: 12 })margin({ bottom: 16 }) 统一的间距让页面有节奏感——区块之间有呼吸,但不会太松散。

八、数据流的完整链路

ProfilePage 创建
  → aboutToAppear → refresh()
    → getAllTags() → this.tags = 15 个标签
    → getNotes() → this.recentNotes = 前 3 篇
    → 遍历 _notes 累加 content.length → this.totalWords = 1781

用户点击常用标签 "#编程"
  → router.pushUrl({ tag: '编程' })
  → TagFilterPage → filterNotes() → 显示 3 篇编程笔记

用户点击最近编辑某笔记
  → router.pushUrl({ noteId: n.id })
  → NoteEditor → 预填标题/内容/标签

refresh() 在 aboutToAppear 和 onPageShow 都调用——保证每次显示都获取最新数据。 用户在 NoteEditor 新增了笔记,返回 ProfilePage 后总字数、标签数、最近编辑列表都会自动更新。

Logo

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

更多推荐