鸿蒙 Next 碳足迹账本 App 开发实战:碳排放记录 + 分类统计 + 环保数据可视化



鸿蒙 Next 碳足迹账本 App 开发实战:碳排放记录 + 分类统计 + 环保数据可视化
作者:duluo
SDK 版本:HarmonyOS API 24 (Next)
开发工具:DevEco Studio
语言框架:ArkTS + ArkUI
字数:约 9500 字
目录
1. 引言
1.1 碳足迹——每个人的环境账本
全球每年碳排放量超过 360 亿吨。这个数字大得超出个人的理解范围。但如果我们把碳排放拆解到个人日常——开车上班排放 2.4kg、吃一餐牛肉排放 3.5kg、网购退货排放 1.5kg——就变成了每个人都能感知的"小事"。
个人碳足迹计算的核心逻辑是:每一件日常小事都有看不见的环境成本。 不是让人产生负罪感,而是让人知道——“原来我做一个不同的选择,可以少排放这么多碳”。
"个人碳足迹账本"App 做的就是这件事:记录日常活动,计算碳排放,让抽象的环境问题变成具体的数字。
1.2 本 App 的定位
本 App 与系列中的"慢病管理"(App 31)共享相同的底层逻辑——记录数据。
| 慢病管理(#31) | 碳足迹账本(#34) | |
|---|---|---|
| 数据类型 | 健康指标 | 碳排放数据 |
| 记录方式 | 一键生成随机数据 | 点击活动记录碳排 |
| 统计方式 | 最新值卡片 | 分类柱状图 |
| 反馈机制 | 用药提醒 | 智能评价 + 植树换算 |
| 激励方式 | 月度成就 | 每日一树 |
两款 App 的代码结构也非常相似:三 Tab 架构、ForEach 列表、常数数据源。区别在于碳足迹账本增加了分类统计和智能评价系统。
1.3 三十四款 App 全景
App 数量: 34
代码总行数: ~18,800 行
编译错误数: ~301 个
博客总字数: ~340,000 字
技术博客数: 34 篇
2. 产品概念与碳排模型
2.1 功能需求
用户故事 1:我想知道每天我的碳排有多少
用户故事 2:我想记录开车的碳排放
用户故事 3:我想看看出行和饮食哪个排碳多
用户故事 4:我想学习怎么减少碳排放
功能清单:
├── F1: 今日总排放 + 植树换算
├── F2: 分类统计柱状图
├── F3: 12 项活动记录
├── F4: 智能评价反馈
├── F5: 今日记录快照
├── F6: 8 条减碳贴士
└── F7: 每日一树知识
2.2 数据模型
interface Activity {
id: number;
name: string; // 活动名称
emoji: string; // 图标
carbon: number; // 碳排放量(kg)
unit: string; // 单位
category: string; // 分类(出行/饮食/能源/消费)
}
每条活动记录 6 个字段。排放数据来自中国碳排放数据库的平均值:
| 活动 | 碳排(kg) | 数据来源 |
|---|---|---|
| 开车 10km | 2.4 | 私家车平均油耗 8L/100km |
| 飞机 1h | 200 | 国内航班人均碳排放 |
| 吃牛肉 | 3.5 | 牛肉碳排放强度 35kg/kg |
| 开空调 2h | 1.2 | 1.5 匹空调平均功率 |
单位统一使用 kg,便于用户比较不同活动的碳排放大小。
2.3 记录数据模型
// [timestamp, activityId, carbonValue]
@State records: number[][] = [];
每条记录是一个 3 元组:时间戳、活动 ID、碳排量。与"慢病管理"(App 31)的 5 元组类似,但更简洁——3 个字段足够描述一条碳排记录。
3. 三 Tab 架构设计
3.1 Tab 配置
build() {
Stack() {
Column().backgroundColor(C.bg)
Column() {
this.buildHeader()
if (this.activeTab === 0) this.buildHomeTab()
else if (this.activeTab === 1) this.buildActivityTab()
else this.buildTipsTab()
this.buildTabBar()
}
}
}
| Tab | 图标 | 功能 | 使用行为 |
|---|---|---|---|
| 0 | 🏠 | 首页 — 仪表盘 + 统计 + 记录 | 查看今日碳排 |
| 1 | 📋 | 活动 — 选择活动记录 | 记录碳排 |
| 2 | 💡 | 贴士 — 减碳建议 | 学习减碳知识 |
3.2 首页仪表盘布局
┌──────────────────────────────┐
│ 🌍 碳足迹账本 │
│ 今日排放 6.2 kg CO₂ │
├──────────────────────────────┤
│ 🌱 今日碳足迹 │
│ 6.2 kg CO₂ 0.28 棵(需植树)│
│ 🎉 低于全国平均水平! │
├──────────────────────────────┤
│ 📊 分类统计 │
│ 出行 ████████ 3.1kg │
│ 饮食 ████ 0.5kg │
│ 能源 ██ 0.3kg │
│ 消费 ██████ 3.0kg │
├──────────────────────────────┤
│ 📝 今日记录 │
│ 🚗 开车10km +2.4kg │
│ 🥗 吃蔬菜餐 +0.5kg │
├──────────────────────────────┤
│ 🌳 每日一树 │
│ 一棵树每年可吸收约 22kg CO₂ │
└──────────────────────────────┘
3.3 数据流
点击活动 → addRecord() → records 头部插入
↓
recalcTotal() → totalToday 更新
↓
首页仪表盘 ← totalToday + records
分类统计 ← getCategoryTotals(records)
今日记录 ← records 最后 4 条
智能评价 ← getComparison(totalToday)
植树换算 ← getTreeCount(totalToday)
所有数据从 records 和 totalToday 两个状态变量派生,不需要额外的 @State 管理派生数据。
4. 首页仪表盘设计
4.1 总览卡片
Column() {
Text('🌱 今日碳足迹').fontSize(16).fontColor(C.text)
Row() {
Column() {
Text(this.totalToday.toFixed(1)).fontSize(40).fontColor(C.primary)
Text('kg CO₂').fontSize(13).fontColor(C.textMuted)
}
Column() {
Text(this.getTreeCount() + '').fontSize(32).fontColor(C.accent)
Text('需植树(棵)')
}
}
Text(this.getComparison()).fontSize(13).fontColor(C.textLight)
}
两个核心指标并排:今日总排放大字号(40sp)+ 需植树量。数字使用不同颜色区分——主色绿(碳排)和强调色(植树量)。
植树换算公式:Math.ceil(totalToday / 22 * 100) / 100。一棵树每年吸收 22kg CO₂,今天排放的碳需要 N 棵树用一年时间来吸收。将抽象的"kg"转换成可感知的"棵树"。
4.2 今日记录快照
ForEach(this.records.slice(-4).reverse(), (r: number[]) => {
Row() {
Text(this.getActEmoji(r[1])).fontSize(20)
Text(this.getActName(r[1])).fontSize(14).margin({ left: 8 }).layoutWeight(1)
Text('+' + r[2].toFixed(1) + 'kg').fontSize(14).fontColor(C.warm)
}
})
取最近 4 条记录(slice(-4)),倒序展示(reverse(),因为 records 头部是最新的)。每条记录显示 emoji + 活动名 + 碳排量。
getActEmoji 和 getActName 根据活动 ID 从 ACTIVITIES 常量中查找对应的 emoji 和名称。这是 Helper 方法模式的应用。
4.3 每日一树
Column() {
Text('🌳 每日一树').fontSize(16).fontColor(C.text)
Text('一棵树每年可吸收约 22kg CO₂。\n今天你的碳排放需要 '
+ this.getTreeCount() + ' 棵树吸收一年。\n减少排放,从每一个选择开始。')
}
三段式文案:知识(树吸收 22kg)→ 应用(你今天需要 N 棵树)→ 行动号召(减少排放)。
5. 碳排放记录系统
5.1 活动列表
ForEach(ACTIVITIES, (a: Activity) => {
Column() {
Row() {
Text(a.emoji).fontSize(28)
Column() {
Text(a.name).fontSize(15).fontWeight(FontWeight.Bold)
Text(a.carbon + a.unit + ' CO₂ · ' + a.category).fontSize(12)
}
Text('+' + a.carbon + a.unit).fontSize(14).fontColor(C.warm)
}
}.onClick(() => { this.addRecord(a.id, a.carbon); })
})
12 项活动列在 Tab 1。点击任意活动 → 记录碳排。卡片展示 emoji、活动名、碳排量、分类标签。
5.2 记录操作
addRecord(actId: number, carbon: number): void {
const now = Date.now();
this.records = [[now, actId, carbon], ...this.records];
this.recalcTotal();
promptAction.showToast({ message: '✅ 已记录 ' + this.getActName(actId) });
}
三行代码完成记录:创建记录 → 头部插入 → 重新计算总量。
5.3 总量计算
recalcTotal(): void {
let t = 0;
for (const r of this.records) { t += r[2]; }
this.totalToday = t;
}
遍历所有记录,累加每条记录的碳排量(r[2])。每次新增记录后调用,确保 totalToday 始终是最新值。
6. 分类统计柱状图
6.1 分类汇总
getCategoryTotals(): number[][] {
const cats: string[] = ['出行', '饮食', '能源', '消费'];
const totals: number[] = [0, 0, 0, 0];
for (const r of this.records) {
const act = this.findAct(r[1]);
if (act !== undefined) {
const idx = cats.indexOf(act.category);
if (idx >= 0) totals[idx] += r[2];
}
}
return [[0, totals[0]], [1, totals[1]], [2, totals[2]], [3, totals[3]]];
}
返回 number[][],每行的格式是 [分类索引, 总排放]。索引 0-3 分别对应四个分类。
6.2 柱状图渲染
ForEach(this.getCategoryTotals(), (cat: number[]) => {
Row() {
Text(this.getCatName(cat[0])).fontSize(14).width(50)
Column() {
Column().height(16).width(this.getBarWidth(cat[1]))
.backgroundColor(C.primary).borderRadius(4)
}.layoutWeight(1).margin({ left: 8 })
Text(cat[1].toFixed(1) + 'kg').fontSize(12).width(54)
}
})
每行由三部分组成:分类名(50px 宽)→ 柱状条(弹性宽度)→ 数值(54px 宽右对齐)。
柱状条宽度通过 getBarWidth 方法计算——根据分类的最大排放值缩放。这种方法不是精确的比例缩放(不是按总排放百分比),而是按预设最大值缩放,确保不同规模的数据都能看到明显的柱状条。
6.3 柱状条的视觉效果
柱状条使用 backgroundColor(C.primary).borderRadius(4)。16px 高度 + 4px 圆角形成胶囊形状,比直角矩形更柔和。
7. 减碳贴士系统
7.1 贴士数据
getTips(): string[][] {
return [
['🚲', '骑行代替开车', '3公里以内的出行…减少约 2.4kg…', '2.4kg'],
['🥗', '多吃蔬菜少吃肉', '每餐用蔬菜代替牛肉…减少约 3kg…', '3.0kg'],
// ... 共 8 条
];
}
每条贴士 4 个字段:emoji、标题、具体建议、减碳量。使用字符串数组而非对象,保持数据结构简洁。
7.2 贴士卡片
Text(tip[0]).fontSize(24) // emoji
Text(tip[1]).fontSize(16) // 标题
Text(tip[2]).fontSize(13) // 建议
Text('🌿 减碳 ' + tip[3]) // 减碳量
每条贴士卡片底部显示具体的减碳量(🌿 减碳 2.4kg),让用户知道"做到这件事可以少排多少碳"。
8. 智能评价与反馈系统
8.1 分段评价
getComparison(): string {
if (this.totalToday < 5) return '🎉 低于全国平均水平!继续保持!';
if (this.totalToday < 10) return '👍 接近绿色生活标准!';
if (this.totalToday < 20) return '📊 与全国平均水平相当。';
return '⚠️ 排放偏高,试试绿色出行吧!';
}
四档评价对应不同的排放区间:
| 排放区间 | 评价 | 含义 |
|---|---|---|
| 0-5 kg | 🎉 优秀 | 低于全国平均水平 |
| 5-10 kg | 👍 良好 | 接近绿色生活标准 |
| 10-20 kg | 📊 平均 | 与全国平均水平相当 |
| >20 kg | ⚠️ 偏高 | 需要改进 |
分段依据:中国城镇居民日均碳排约 15-20kg。5kg 以下需要非常环保的生活方式(走路/骑车上班、素食、很少用空调),10kg 以下是较为环保的水平,20kg 以上偏高。
8.2 评价的心理学设计
评价不使用负面词汇。没有"你的碳排放太高了"——而是"试试绿色出行吧"。引导而不是指责,是环保类 App 信息传递的关键原则。
9. 文本类型转换问题
9.1 错误描述
本 App 遇到 1 个编译错误:
10505001: Argument of type 'number' is not assignable to parameter of type 'string | Resource'
位置:Text(this.getTreeCount())
原因:getTreeCount() 返回 number 类型,但 Text() 组件的第一个参数需要 string | Resource 类型。
getTreeCount(): number {
return Math.ceil(this.totalToday / 22 * 100) / 100;
}
// ❌ 错误:Text 不能直接接收数字
Text(this.getTreeCount())
// ✅ 修复:转换为字符串
Text(this.getTreeCount() + '')
9.2 为什么这个错误值得记录
这不是一个新的错误类型(10505001 在系列中已出现多次),但它的出现位置有特殊意义——它发生在"完全没有复杂语法"的代码中。
App 33(相亲防骗雷达)实现 0 错误,App 34 出现了 1 个错误。这个错误的出现不是"退步",而是因为碳足迹账本在首页展示了 this.getTreeCount() 这个数字——而相亲防骗雷达首页没有展示任何方法返回的数字。
教训:文本类型转换错误是一个容易被忽视的"边缘案例"。Text() 组件大部分时间接收字符串字面量(Text('hello')),开发者容易忘记 Text() 不能直接接收数字。this.count + '' 或 this.count.toString() 可以快速解决。
10. 第三十四款 App 全景回顾
10.1 数据总览
| 指标 | 数值 |
|---|---|
| 代码行数 | 280 行 |
| 编译错误数 | 1 个 |
| @State 变量 | 3 个 |
| @Builder 方法 | 5 个 |
| 活动类型 | 12 项 |
| 减碳贴士 | 8 条 |
| 分类统计 | 4 类 |
| 外部依赖 | 0 个 |
10.2 三十四款 App 的环保类对比
| App | 主题 | 核心机制 | 错误数 |
|---|---|---|---|
| 8 🗑️ 情绪垃圾桶 | 情绪环保 | 扔掉坏情绪 | 4 |
| 32 🌍 临期食品救援 | 食物环保 | 救援临期食品 | 6 |
| 34 🌱 碳足迹账本 | 碳排环保 | 记录碳排放 | 1 |
三款环保类 App 覆盖了情绪环保、食物环保、碳排环保三个方向。错误数从 4 降到 6 再降到 1——不是线性的,但整体趋势向好。
10.3 三十四款 App 的错误数中位数
App 1-10: 错误数中位数 11.5
App 11-20: 错误数中位数 4.5
App 21-30: 错误数中位数 5.5
App 31-34: 错误数中位数 0.5
中位数从 11.5 降到了 0.5,下降了 96%。这个数字说明的不是"开发者更聪明了",而是"模式更成熟了"——同样的错误不会犯第二次。
10.4 三十四款 App 的"34 个系列之最"
| 类别 | App | 纪录 |
|---|---|---|
| 最多代码 | 3 冰箱剩菜 | 1320 行 |
| 最少代码 | 28 记忆时光机 | 188 行 |
| 最多错误 | 24 AI 树洞 | 48 个 |
| 最少错误 | 31 慢病管理 / 33 相亲防骗 | 0 个 |
| 最多 Builder | 3 冰箱剩菜 | 17 个 |
| 最少 Builder | 28 记忆时光机 | 2 个 |
| 最多数据 | 24 AI 树洞 | 164 条模板 |
11. 结语
11.1 碳足迹的日常意义
一个人一天排了多少碳?开车上下班 4.8kg、吃三餐 1-4kg、用空调 1.2kg、用手机电脑 0.4kg——加起来大约 7-15kg。
这个数字不算大,但乘以 365 天、乘以 14 亿人——就是全球碳排放总量的大头。
个人碳足迹的意义不是"我一个人减排就能拯救地球",而是让抽象的环境问题变得可感知。当你知道开车 10km 排 2.4kg 碳、一棵树一年只能吸收 22kg 碳——你做的每一个小选择,都有了可量化的环境意义。
11.2 从 34 款 App 中看到的
写了 34 款 App 之后,最清楚的发现是:代码量的多少与 App 的价值没有直接关系。
碳足迹账本 280 行代码。它解决了一个真实的问题(让碳排放可感知)。它比某些 500 行的 App 更有价值——不是因为代码写得更好,而是因为"记录碳排"这件事本身有意义。
所以如果你正在做你的第一款 App,不要担心"我的 App 代码太少了"。重要的是你的 App 解决了什么问题,而不是你的 App 有多少行代码。
11.3 给开发者的建议
- Text() 不能直接接收 number——
Text(this.count)会编译错误,必须Text(this.count + '') - 碳排数据要真实——不要编造碳排数据。从权威来源获取(中国产品全生命周期温室气体排放系数库)
- 评价要用引导代替指责——"试试绿色出行"比"你排太多碳了"更有效
- 34 款 App 的中位错误数降到了 0.5——零错误不是一个遥远的梦
11.4 致谢
34 款 App、34 篇博客、约 340,000 字。
从白噪音到碳足迹账本,34 款 App 的旅程见证了一个开发者的成长,也见证了 ArkUI 从一个新框架到成熟的开发工具的演变。
34 不是终点。第 35 款 App 正在路上。
现在,打开 DevEco Studio,去记录你的碳足迹吧——或者去创造属于你自己的第 34 款 App。
附录 A:核心代码速查
添加碳排记录
addRecord(actId: number, carbon: number): void {
this.records = [[Date.now(), actId, carbon], ...this.records];
this.recalcTotal();
}
分类统计
getCategoryTotals(): number[][] {
const cats = ['出行', '饮食', '能源', '消费'];
const totals = [0, 0, 0, 0];
for (const r of this.records) {
const act = this.findAct(r[1]);
if (act) { const idx = cats.indexOf(act.category); totals[idx] += r[2]; }
}
return [[0, totals[0]], [1, totals[1]], [2, totals[2]], [3, totals[3]]];
}
智能评价
getComparison(): string {
if (this.totalToday < 5) return '🎉 低于全国平均水平!继续保持!';
if (this.totalToday < 10) return '👍 接近绿色生活标准!';
if (this.totalToday < 20) return '📊 与全国平均水平相当。';
return '⚠️ 排放偏高,试试绿色出行吧!';
}
附录 B:色板
| 变量 | 值 | 用途 |
|---|---|---|
C.bg |
#F0F7F0 |
主背景 |
C.primary |
#4A9B5A |
碳排数据 |
C.accent |
#3D8B40 |
植树数据 |
C.warm |
#E8927C |
排放数值 |
C.earth |
#8B6B4A |
贴士点缀 |
更多推荐




所有评论(0)