统计 Tab 从最初的四维数字卡片扩展成了三个区块——概览卡片、月历活跃图、心情分布柱状图。月历用日期网格展示每天封印了多少胶囊心情分布用水平进度条对比不同情绪的使用频率。这两个可视化组件把数字变成了图形让用户一眼就能感知自己的记录习惯和情绪模式。完整效果一、统计页面的三个区块区块内容数据来源概览卡片总数/已开启/等待中/可开启StatsSummary本月活跃日历网格 每日计数WeeklyMood / Calendar心情分布水平进度条 计数TagCount新增的数据类型从 import 推断统计页需要这些数据结构// 从 model/Database.ets 导入interfaceWeeklyMood{...}// 周心情数据interfaceTagCount{...}// 标签/心情计数interfaceCalendar{...}// 月历数据二、概览卡片的 2×2 网格Flex({wrap:FlexWrap.Wrap}){Column(){Text(String(this.statsSummary.total)).fontSize(FontSize.hero).fontColor(this.getColors().primary).fontWeight(FontWeight.Bold)Text(总数).fontSize(FontSize.xs).fontColor(this.getColors().textSecondary)}.width(48%).padding(Spacing.md).margin({right:2%,bottom:Spacing.sm}).backgroundColor(this.getColors().surface).borderRadius(BorderRadius.md).border({width:1,color:this.getColors().border}).alignItems(HorizontalAlign.Center)// opened / waiting / ready 同结构...}.width(100%)Flex 2×2 网格的布局原理Flex(wrap: Wrap) ├→ Column(width: 48%) margin-right: 2% ← 第一列 ├→ Column(width: 48%) ← 第二列 ├→ Column(width: 48%) margin-right: 2% ← 第二行第一列 └→ Column(width: 48%) ← 第二行第二列48% 2% margin 50%两列刚好占满一行。FlexWrap.Wrap 自动换行——前两个元素占第一行后两个元素换到第二行。这是 ArkTS 里实现网格布局的标准方式——不需要 Grid 组件Flex 就够了。四个数字的颜色编码卡片颜色含义总数primary主色所有胶囊的总和已开启#10B981 绿色成功、完成等待中#F59E0B 黄色进行中、等待可开启#8B5CF6 紫色可操作、特殊颜色不只是装饰——它传递语义。绿色完成黄色进行中紫色特殊状态。用户扫一眼颜色就能理解数字的含义不需要读文字标签。三、本月活跃的月历视图月历的渲染结构Column本月活跃区块 ├→ Row标题行 │ └→ Text(本月活跃) └→ Column日期网格 ├→ Row星期头日 一 二 三 四 五 六 ├→ Row第一周1 2 3 4 5 6 7 ├→ Row第二周8 9 10 11 12 13 14 ├→ Row第三周15 16 17 18 19 20 21 ├→ Row第四周22 23 24 25 26 27 28 └→ Row第五周29 30 31日期单元格的三种状态状态条件背景色文字色今天isToday(day)primary白色有记录dayCount 0primary ‘20’primary无记录dayCount 0transparenttextSecondary今天最醒目——主色实心背景。有记录的日期用 20% 透明度主色背景——和 MoodPicker 的选中背景用同样的透明度。无记录的日期最弱——只有灰色文字。月历数据的计算privategetDaysInMonth():number{constnownewDate()returnnewDate(now.getFullYear(),now.getMonth()1,0).getDate()}privategetFirstDayOfWeek():number{constnownewDate()returnnewDate(now.getFullYear(),now.getMonth(),1).getDay()}new Date(year, month1, 0).getDate()是获取某月天数的技巧。传入下个月第 0 天——自动回退到本月最后一天返回日期就是本月天数。getFirstDayOfWeek()返回本月第一天是星期几。用于确定日期网格的起始偏移——如果本月 1 号是星期三前面需要空 3 个格子。每日活跃度的颜色深浅从截图看有胶囊记录的日期背景色深浅不同——记录越多颜色越深privategetDayOpacity(count:number):number{if(count0)return0if(count1)return0.2if(count2)return0.4return0.6}透明度随记录数量递增——0/20%/40%/60%。但有上限60%——太深会影响日期数字的可读性。这种热力图式的设计让用户一眼看出哪天记录最多。四、心情分布的水平进度条渲染结构Column心情分布区块 ├→ Text(心情分布) └→ ForEach(MOOD_OPTIONS) Row一行 ├→ Text(opt.emoji) 心情图标36px 宽 ├→ Text(opt.label) 心情名称48px 宽 ├→ Row (进度条容器) 弹性宽度 │ └→ Row (进度条填充) 宽度按比例 └→ Text(count) 计数28px 宽进度条的宽度计算privategetMoodBarPct(emoji:string):string{constcountthis.getMoodCount(emoji)constmaxthis.maxMoodVal()constpctmax0?Math.floor(count/max*100):0returnString(pct)%}privatemaxMoodVal():number{letm0constkeysObject.keys(this.moodStats)for(leti0;ikeys.length;i){if(this.moodStats[keys[i]]m)mthis.moodStats[keys[i]]}returnm0?m:1}归一化计算——以最大值为 100%。如果开心有 5 次最多它的进度条占 100% 宽度。难过有 2 次进度条占 40%。这样进度条的长度反映的是相对频率而非绝对数量。进度条的渐变效果Row().height(14).width(this.getMoodBarPct(emoji)).backgroundColor(this.getMoodCount(emoji)0?this.getColors().primary:this.getColors().border).borderRadius(BorderRadius.sm)有记录时用主色背景无记录时用边框灰色。进度条高度 14px——和统计卡片的数字相比更矮因为它不是核心信息只是辅助可视化。五种心情的固定顺序ForEach(MOOD_OPTIONS,(opt:MoodOption){...})心情顺序固定——和 MOOD_OPTIONS 定义的顺序一致。不按使用频率排序——因为心情有固定的逻辑顺序开心→难过→思考→生气→庆祝打乱顺序会降低辨识度。五、数据库新增的统计方法从 import 推断// 从 model/Database.ets 导入interfaceCapsuleStats{total:numberopened:numbersealed:number// 等待中ready:number// 可开启moodCount:Recordstring,number// 心情分布}新增的统计维度维度数据结构用途心情分布Recordstring, number心情进度条月历数据Calendar日期网格周数据WeeklyMood可能用于周视图getStats 的扩展getStats():CapsuleStats{constnowDate.now()lettotalthis.capsules.lengthletopened0letsealed0letready0constmoodCount:Recordstring,number{}for(leti0;ithis.capsules.length;i){constcthis.capsules[i]if(c.isOpened){opened}elseif(c.unlockAtnow){sealed}else{ready}// 统计心情constmoodc.mood moodCount[mood](moodCount[mood]||0)1}return{total,opened,sealed,ready,moodCount}}一次遍历统计所有维度——不需要多次循环。遍历一次 capsules 数组同时统计状态和心情。这是性能优化的基本原则——减少循环次数。六、月历的热力图原理日期计数的获取privategetDayCount(day:number):number{constnownewDate()constyearnow.getFullYear()constmonthnow.getMonth()letcount0for(leti0;ithis.capsules.length;i){constcthis.capsules[i]constdnewDate(c.createdAt)if(d.getFullYear()yeard.getMonth()monthd.getDate()day){count}}returncount}遍历所有胶囊匹配年月日。这是最直接的实现——性能不是问题胶囊数量通常在百级别。如果数据量大可以用 Map 预计算每日计数。今天日期的判断privateisToday(day:number):boolean{constnownewDate()returndaynow.getDate()}只比较日期——不比较年月。因为月历只显示当月年月已经确定。只比较getDate()即可。七、统计 Tab 的数据刷新时机// 首页底部导航栏点击.onClick((){this.currentTabtab.keyif(tab.keystats||tab.keytimeline){this.loadStats()}})切换到统计 Tab 时自动刷新——保证数据最新。如果用户在封印 Tab 新增了胶囊切换到统计 Tab 时总数和心情分布都会更新。onPageShow 也刷新onPageShow():void{if(this.database){this.capsulesthis.database.getAllCapsules()this.loadStats()}}从其他页面返回时也刷新。比如用户从详情页返回如果在详情页开启了胶囊统计 Tab 的已开启数量需要更新。八、统计页面的嵌套结构Column 页面容器 ├→ Row (标题) 统计 └→ Scroll 可滚动 └→ Column 垂直排列 ├→ Flex (概览卡片) 2×2 网格 ├→ Column (本月活跃) 日历网格 └→ Column (心情分布) 进度条列表嵌套深度 4 层——和首页一样。Scroll 包裹所有内容——月历在小屏手机上可能需要滚动。九、三个可视化组件的设计对比组件布局方式数据维度交互概览卡片Flex 2×24 个数字无月历Row×N 网格31 个日期无心情进度条ForEach 列表5 种心情无三个组件都没有交互——统计页是纯展示页面。用户不需要点击统计图表目前没有点击日期查看详情的功能。这是原型阶段的设计——先展示数据后续再加交互。十、从数字到图形的演进基础版统计总数: 12 已开启: 5 等待中: 6 可开启: 1优化版统计┌──────┐ ┌──────┐ │ 12 │ │ 5 │ │ 总数 │ │已开启│ ├──────┤ ├──────┤ │ 6 │ │ 1 │ │等待中│ │可开启│ └──────┘ └──────┘ 本月活跃 心情分布 日 一 二 三 四 五 六 开心 ████████ 5 1 2 3 4 5 6 难过 ████ 2 7 8 9 10 11 12 13 思考 ██████ 3 14 15 16 17 18 19 20 生气 █ 1 21 22 23 24 25 26 27 庆祝 ██ 2 28 29 30 31从纯数字到图形化展示——信息密度提升认知成本下降。用户不需要逐个阅读数字再在脑子里汇总——月历一眼看出哪天记录多心情条一眼看出哪种情绪最多。这是数据可视化的核心价值不是让数据更好看而是让数据更容易理解。