HarmonyOS知识库——个人中心的数据可视化与交互升级
知识库 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 后总字数、标签数、最近编辑列表都会自动更新。
更多推荐



所有评论(0)