HarmonyOS时光胶囊——统计页面的月历与心情可视化
统计 Tab 从最初的四维数字卡片扩展成了三个区块——概览卡片、月历活跃图、心情分布柱状图。月历用日期网格展示每天封印了多少胶囊,心情分布用水平进度条对比不同情绪的使用频率。这两个可视化组件把"数字"变成了"图形",让用户一眼就能感知自己的记录习惯和情绪模式。
完整效果
一、统计页面的三个区块
| 区块 | 内容 | 数据来源 |
|---|---|---|
| 概览卡片 | 总数/已开启/等待中/可开启 | StatsSummary |
| 本月活跃 | 日历网格 + 每日计数 | WeeklyMood / Calendar |
| 心情分布 | 水平进度条 + 计数 | TagCount |
新增的数据类型
从 import 推断,统计页需要这些数据结构:
// 从 model/Database.ets 导入
interface WeeklyMood { ... } // 周心情数据
interface TagCount { ... } // 标签/心情计数
interface Calendar { ... } // 月历数据
二、概览卡片的 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 > 0 | primary + ‘20’ | primary |
| 无记录 | dayCount === 0 | transparent | textSecondary |
今天最醒目——主色实心背景。 有记录的日期用 20% 透明度主色背景——和 MoodPicker 的选中背景用同样的透明度。无记录的日期最弱——只有灰色文字。
月历数据的计算
private getDaysInMonth(): number {
const now = new Date()
return new Date(now.getFullYear(), now.getMonth() + 1, 0).getDate()
}
private getFirstDayOfWeek(): number {
const now = new Date()
return new Date(now.getFullYear(), now.getMonth(), 1).getDay()
}

new Date(year, month+1, 0).getDate() 是获取某月天数的技巧。 传入下个月第 0 天——自动回退到本月最后一天,返回日期就是本月天数。
getFirstDayOfWeek() 返回本月第一天是星期几。 用于确定日期网格的起始偏移——如果本月 1 号是星期三,前面需要空 3 个格子。
每日活跃度的颜色深浅
从截图看,有胶囊记录的日期背景色深浅不同——记录越多颜色越深:
private getDayOpacity(count: number): number {
if (count === 0) return 0
if (count === 1) return 0.2
if (count === 2) return 0.4
return 0.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 宽)
进度条的宽度计算
private getMoodBarPct(emoji: string): string {
const count = this.getMoodCount(emoji)
const max = this.maxMoodVal()
const pct = max > 0 ? Math.floor(count / max * 100) : 0
return String(pct) + '%'
}
private maxMoodVal(): number {
let m = 0
const keys = Object.keys(this.moodStats)
for (let i = 0; i < keys.length; i++) {
if (this.moodStats[keys[i]] > m) m = this.moodStats[keys[i]]
}
return m > 0 ? 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 导入
interface CapsuleStats {
total: number
opened: number
sealed: number // 等待中
ready: number // 可开启
moodCount: Record<string, number> // 心情分布
}
新增的统计维度
| 维度 | 数据结构 | 用途 |
|---|---|---|
| 心情分布 | Record<string, number> | 心情进度条 |
| 月历数据 | Calendar | 日期网格 |
| 周数据 | WeeklyMood | 可能用于周视图 |
getStats 的扩展
getStats(): CapsuleStats {
const now = Date.now()
let total = this.capsules.length
let opened = 0
let sealed = 0
let ready = 0
const moodCount: Record<string, number> = {}
for (let i = 0; i < this.capsules.length; i++) {
const c = this.capsules[i]
if (c.isOpened) {
opened++
} else if (c.unlockAt > now) {
sealed++
} else {
ready++
}
// 统计心情
const mood = c.mood
moodCount[mood] = (moodCount[mood] || 0) + 1
}
return { total, opened, sealed, ready, moodCount }
}
一次遍历统计所有维度——不需要多次循环。 遍历一次 capsules 数组,同时统计状态和心情。这是性能优化的基本原则——减少循环次数。
六、月历的热力图原理
日期计数的获取
private getDayCount(day: number): number {
const now = new Date()
const year = now.getFullYear()
const month = now.getMonth()
let count = 0
for (let i = 0; i < this.capsules.length; i++) {
const c = this.capsules[i]
const d = new Date(c.createdAt)
if (d.getFullYear() === year && d.getMonth() === month && d.getDate() === day) {
count++
}
}
return count
}
遍历所有胶囊,匹配年月日。 这是最直接的实现——性能不是问题(胶囊数量通常在百级别)。如果数据量大,可以用 Map 预计算每日计数。
今天日期的判断
private isToday(day: number): boolean {
const now = new Date()
return day === now.getDate()
}
只比较日期——不比较年月。 因为月历只显示当月,年月已经确定。只比较 getDate() 即可。
七、统计 Tab 的数据刷新时机
// 首页底部导航栏点击
.onClick(() => {
this.currentTab = tab.key
if (tab.key === 'stats' || tab.key === 'timeline') {
this.loadStats()
}
})
切换到统计 Tab 时自动刷新——保证数据最新。 如果用户在封印 Tab 新增了胶囊,切换到统计 Tab 时总数和心情分布都会更新。
onPageShow 也刷新
onPageShow(): void {
if (this.database) {
this.capsules = this.database.getAllCapsules()
this.loadStats()
}
}
从其他页面返回时也刷新。 比如用户从详情页返回,如果在详情页开启了胶囊,统计 Tab 的"已开启"数量需要更新。
八、统计页面的嵌套结构
Column 页面容器
├→ Row (标题) "统计"
└→ Scroll 可滚动
└→ Column 垂直排列
├→ Flex (概览卡片) 2×2 网格
├→ Column (本月活跃) 日历网格
└→ Column (心情分布) 进度条列表
嵌套深度 4 层——和首页一样。 Scroll 包裹所有内容——月历在小屏手机上可能需要滚动。
九、三个可视化组件的设计对比
| 组件 | 布局方式 | 数据维度 | 交互 |
|---|---|---|---|
| 概览卡片 | Flex 2×2 | 4 个数字 | 无 |
| 月历 | 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

从纯数字到图形化展示——信息密度提升,认知成本下降。 用户不需要逐个阅读数字再在脑子里汇总——月历一眼看出哪天记录多,心情条一眼看出哪种情绪最多。
这是数据可视化的核心价值: 不是让数据"更好看",而是让数据"更容易理解"。
更多推荐



所有评论(0)