知识库 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 的简洁设计BuilderProfileStat(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 是白色文字渐变底。总字数的实时计算privaterefresh():void{letwords:number0for(leti:number0;iall.length;i){wordsall[i].content.length}this.totalWordswords}遍历所有笔记的 content 字段累加字符串长度。这是一个粗略的字数统计——中文一个字算一个字符英文一个字母算一个字符。精确的字数需要按语言分别计算但对知识库 App 来说字符数已经够用了。三、本周活跃柱状图StateweekDays:number[][3,1,4,2,5,1,3]StatecurrentStreak:number5Row(){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:[[A40,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。即使 count0当天没有笔记柱子也有 8px 高度——视觉上不会出现完全没有柱子的空白。渐变柱子的颜色设计Column().width(24).height(Math.max(8,count*12)).borderRadius(12).linearGradient({angle:0,colors:[[A40,0],[A,1]]})从下到上的渐变angle: 0——底部 40% 透明度顶部全色。这种渐变让柱子看起来有深度——底部淡、顶部浓视觉上更立体。count * 12是柱子高度系数。count5 时柱子高 60px刚好等于底座高度count1 时高 12px。这个系数让柱子高度差异明显但不夸张。dayName 的星期转换privatedayName(idx:number):string{constdays:string[][一,二,三,四,五,六,日]return周days[idx]}idx 0周一idx 6周日。数组索引和星期对应——weekDays 数组的顺序就是周一到周日。当前数据是 mockweekDays: number[] [3, 1, 4, 2, 5, 1, 3]是硬编码的 mock 数据。真实 App 需要根据用户的笔记创建/更新时间统计每天的活跃度。currentStreak: number 5也是 mock——需要计算连续多少天有笔记操作。四、常用标签的排序与进度条topTags 的冒泡排序privatetopTags():TagCount[]{constsorted:TagCount[][]for(leti:number0;ithis.tags.length;i){sorted.push(this.tags[i])}for(leti:number0;isorted.length-1;i){for(letj:numberi1;jsorted.length;j){if(sorted[j].countsorted[i].count){consttmp:TagCountsorted[i];sorted[i]sorted[j];sorted[j]tmp}}}constresult:TagCount[][]for(leti:number0;isorted.lengthi5;i){result.push(sorted[i])}returnresult}先复制数组避免修改原始 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],[A40,1]]})}.width(80).height(6).backgroundColor(#F0EEF4).borderRadius(3)Stack 效果用嵌套 RowColumn 实现——外层 Row 是底座灰色80px 宽内层 Column 是进度条渐变色宽度动态。进度条宽度Math.max(4, t.count * 14)保证最小 4px。count3 时宽 42pxcount2 时宽 28px。80px 的底座宽度是固定的——进度条在底座内从左向右填充。渐变方向 angle: 90 从左到右——左端全色右端 40% 透明度。和柱状图的渐变方向不同——柱状图从下到上进度条从左到右。不同的渐变方向增加了视觉丰富度。标签行的完整结构Row 标签行白色背景 圆角 ├→ Text(# t.name) 标签名13px 粗体 ├→ Text(t.count 篇) 数量11px 灰色 └→ Row(进度条底座) 80×6 灰色底座 └→ Column(进度条填充) 渐变色填充点击标签跳转 TagFilterPage。和首页笔记卡片的标签点击逻辑一致——传入标签名TagFilterPage 显示筛选结果。五、最近编辑列表StaterecentNotes:Note[][]privaterefresh():void{constallgetNotes()constrecent:Note[][]for(leti:number0;iall.lengthi3;i){recent.push(all[i])}this.recentNotesrecent}取前 3 篇笔记——因为 getNotes() 返回的数组刚好按 id 降序排列新笔记放最前面。这依赖 addNote 的实现——新笔记插入数组最前面。如果 addNote 改成 push 到末尾这里需要按 updatedAt 排序。踩坑记录最初取了全部笔记显示在最近编辑里列表太长。限制为 3 篇后用户一屏就能看完。nbIcon/nbColor 的新实现privatenbIcon(id:number):string{consticons:Recordnumber,string{1:,2:,3:,4:}returnicons[id]||}privatenbColor(id:number):string{constcolors:Recordnumber,string{1:#FF6B6B,2:#6C5CE7,3:#00B894,4:#FF9F43}returncolors[id]||#CCC}用 Record 对象替代 for 循环查找——代码更简洁。icons[id]直接通过 key 取值不需要遍历 NOTEBOOKS 数组。|| 是默认值——id 不在映射表里时返回默认图标/颜色。和之前 for 循环版本的对比方案代码量可读性性能for 循环3 行需要理解循环逻辑O(n)Record 查找1 行直观key→valueO(1)Record 查找在各方面都更优——代码更少、更直观、更快。唯一的缺点是需要手动维护映射表但 4 个笔记本的映射维护成本很低。六、设置面板的 SettingRowBuilderSettingRow(icon:string,title:string,desc:string){Row(){Text(icon).fontSize(18).width(32)Column(){Text(title).fontSize(14).fontColor(T1)if(desc.length0){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 后总字数、标签数、最近编辑列表都会自动更新。