鸿蒙原生应用实战(五):功能打磨 — 个人中心与项目优化总结
鸿蒙原生应用实战(五):功能打磨 — 个人中心与项目优化总结
本文是系列终篇,完成「心情日记」最后一个人中心页面(ProfilePage)的开发,并对整个项目进行全面的技术总结,包括连续签到算法分析、@Builder 复用模式、数据持久化方案、编码规范等实战经验。
一、个人中心页面(ProfilePage.ets)全面拆解
个人中心是用户管理数据和查看统计概览的页面,它包含:
- 用户信息卡片:应用名称与标语
- 统计卡片:总篇数 / 标签数 / 心情数
- 标签云:所有使用过的标签集合
- 功能菜单:导出日记 / 重置数据 / 关于应用
1.1 页面布局
┌──────────────────────────────────────────┐
│ < 返回 我的 │
├──────────────────────────────────────────┤
│ ┌──────────────────────────────────┐ │
│ │ 📖 │ │
│ │ 心情日记 │ │ ← 用户信息
│ │ 记录每一天的心情故事 │ │
│ └──────────────────────────────────┘ │
│ │
│ ┌────────┐ ┌────────┐ ┌────────┐ │
│ │ 📝 │ │ 🏷️ │ │ 😊 │ │
│ │ 15 │ │ 6 │ │ 9种 │ │ ← 统计卡片
│ │ 总篇数 │ │ 标签数 │ │ 心情数 │ │
│ └────────┘ └────────┘ └────────┘ │
│ │
│ ┌──────────────────────────────────┐ │
│ │ 常用标签 │ │
│ │ #工作 #家庭 #阅读 #生活 │ │ ← 标签云
│ │ #友情 #旅行 #感恩 #焦虑 │ │
│ └──────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────┐ │
│ │ 📤 导出日记 │ │
│ │ 将所有日记导出为文本 > │ │ ← 功能菜单
│ ├──────────────────────────────────┤ │
│ │ 🔄 重置数据 │ │
│ │ 清除所有日记数据 > │ │
│ ├──────────────────────────────────┤ │
│ │ ℹ️ 关于应用 │ │
│ │ 心情日记 v1.0.0 > │ │
│ └──────────────────────────────────┘ │
└──────────────────────────────────────────┘
1.2 状态变量
@State entries: DiaryEntry[] = []; // 所有日记
@State totalCount: number = 0; // 总篇数
@State tagCloud: string[] = []; // 标签集合
相比其他页面,个人中心的状态变量较少,重在展示统计数据。
1.3 数据加载与标签提取
loadData(): void {
let stored = AppStorage.get<DiaryEntry[]>('entries');
this.entries = stored ? stored : [];
this.totalCount = this.entries.length;
// 提取所有标签(去重)
let tagSet: string[] = [];
for (let i = 0; i < this.entries.length; i++) {
let tagsStr = this.entries[i].tags;
if (tagsStr) {
let parts: string[] = tagsStr.split(',');
for (let j = 0; j < parts.length; j++) {
let t = parts[j].trim();
if (t && tagSet.indexOf(t) === -1) {
tagSet.push(t); // indexOf === -1 确保去重
}
}
}
}
this.tagCloud = tagSet;
}
标签提取算法:
- 遍历所有日记
- 对每条日记的 tags 字段按逗号
,分割 - 对每个标签 trim 去空格
- 使用
indexOf检查是否已存在,实现去重
1.4 标签云 UI
if (this.tagCloud.length > 0) {
Column() {
Text('常用标签').fontSize(16).fontWeight(FontWeight.Bold)
.width('100%').margin({ bottom: 10 })
Flex({ wrap: FlexWrap.Wrap }) { // 自动换行
ForEach(this.tagCloud, (tag: string) => {
Text('#' + tag)
.fontSize(13).fontColor('#6C63FF')
.backgroundColor('#EEEAFF')
.padding({ left: 12, right: 12, top: 4, bottom: 4 })
.borderRadius(12)
.margin({ right: 6, bottom: 6 })
}, (tag: string) => tag)
}
.width('100%')
}
.padding(14).backgroundColor('#FFFFFF').borderRadius(12)
}
Flex 换行布局要点:
Flex({ wrap: FlexWrap.Wrap }):当一行放不下时自动换行- 每个标签使用胶囊样式(大圆角)
- 标签前缀
#增加可视性
1.5 重置数据功能
clearAllData(): void {
AppStorage.set<DiaryEntry[]>('entries', []); // 清空数据
this.loadData(); // 刷新页面
}
功能菜单使用 @Builder 复用:
@Builder menuRow(icon: string, title: string, desc: string, onClick: () => void) {
Row() {
Text(icon).fontSize(20).margin({ right: 10 })
Column() {
Text(title).fontSize(15).fontWeight(FontWeight.Medium)
Text(desc).fontSize(12).fontColor('#999999').margin({ top: 1 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
Text('>').fontSize(14).fontColor('#CCCCCC')
}
.width('100%')
.padding({ top: 10, bottom: 10 })
.onClick(onClick)
}
// 使用
Column() {
this.menuRow('📤', '导出日记', '将所有日记导出为文本', () => {})
this.menuRow('🔄', '重置数据', '清除所有日记数据', () => {
this.clearAllData();
})
this.menuRow('ℹ️', '关于应用', '心情日记 v1.0.0', () => {})
}
二、@Builder 复用模式总结
在整个项目中,@Builder 被大量使用,形成了统一的组件复用模式:
2.1 @Builder 模式分类
| 页面 | @Builder 名称 | 用途 | 参数 |
|---|---|---|---|
| Index | quickBtn |
快捷操作按钮 | icon, label, onClick |
| Index | diaryRow |
日记列表行 | item: DiaryEntry |
| CalendarPage | — | 使用 ForEach 直接渲染 | — |
| StatsPage | overviewBox |
统计概览卡片 | icon, label, value |
| ProfilePage | roundedBox |
统计卡片 | icon, label, value |
| ProfilePage | menuRow |
功能菜单行 | icon, title, desc, onClick |
2.2 @Builder 设计模式
// 模式1:纯展示型 — 接收数据展示
@Builder diaryRow(item: DiaryEntry) { /* 展示 item 数据 */ }
// 模式2:动作型 — 接收回调
@Builder quickBtn(icon: string, label: string, onClick: () => void) {
Column() {
Text(icon).fontSize(26)
Text(label).fontSize(12)
}
.onClick(onClick)
}
// 模式3:混合型 — 数据 + 回调
@Builder menuRow(icon: string, title: string, desc: string, onClick: () => void) {
// 展示 icon/title/desc,点击触发 onClick
}
2.3 @Builder 的参数传递技巧
回调函数作为参数:
// 定义时接收 () => void 类型
@Builder quickBtn(icon: string, label: string, onClick: () => void)
// 使用时传入箭头函数
this.quickBtn('✏️', '写日记', () => {
router.pushUrl({ url: 'pages/WritePage' });
})
三、连续签到算法深度分析
3.1 算法回顾
let streakCount = 0;
let checkDate = new Date();
while (true) {
// 构造日期字符串 YYYY-MM-DD
let y = checkDate.getFullYear();
let m = (checkDate.getMonth() + 1).toString().padStart(2, '0');
let d = checkDate.getDate().toString().padStart(2, '0');
let ds = `${y}-${m}-${d}`;
// 查找当天是否有日记
let found = false;
for (let i = 0; i < this.entries.length; i++) {
if (this.entries[i].date === ds) {
found = true;
break;
}
}
if (found) {
streakCount++;
checkDate.setDate(checkDate.getDate() - 1); // 往前一天
} else {
break; // 断签,停止检查
}
}
this.streak = streakCount;
3.2 时间复杂度分析
| 场景 | 检查天数 | 最坏复杂度 |
|---|---|---|
| 连续1天 | 1 | O(1×n) |
| 连续3天 | 3 | O(3×n) |
| 连续30天 | 30 | O(30×n) |
| 连续365天 | 365 | O(365×n) |
其中 n 为日记总数。对于日常使用(n < 1000),这个算法完全够用。
3.3 优化方案(供参考)
// 优化:使用 Set 存储所有有日记的日期,查找复杂度 O(1)
optimizedCalcStreak(): number {
let dateSet = new Set<string>();
for (let entry of this.entries) {
dateSet.add(entry.date);
}
let streak = 0;
let checkDate = new Date();
while (true) {
let ds = formatDate(checkDate);
if (dateSet.has(ds)) {
streak++;
checkDate.setDate(checkDate.getDate() - 1);
} else {
break;
}
}
return streak;
}
使用 Set 后,时间复杂度从 O(days×n) 降为 O(n + days),其中 days 是连续天数。
四、AppStorage 数据持久化思考
4.1 当前方案:内存存储
// 存
AppStorage.set<DiaryEntry[]>('entries', list);
// 取
let stored = AppStorage.get<DiaryEntry[]>('entries');
优点:
- 实现简单,无需文件操作
- 响应式,自动触发 UI 更新
- 适合演示项目
缺点:
- 应用被杀后数据丢失
- 不支持持久化存储
4.2 升级方案:PersistentStorage
// 声明持久化键(在 Ability 中初始化)
PersistentStorage.persistProp('entries', []);
// 使用(与 AppStorage 一致)
AppStorage.get<DiaryEntry[]>('entries');
AppStorage.set<DiaryEntry[]>('entries', list);
4.3 更进一步:关系型数据库(RDB)
对于生产级应用,推荐使用鸿蒙的 RDB(关系型数据库):
import { relationalStore } from '@kit.ArkData';
// 创建数据库
const store = await relationalStore.getRdbStore(context, {
name: 'Diary.db',
securityLevel: relationalStore.SecurityLevel.S1
});
// 建表
await store.executeSql(
'CREATE TABLE IF NOT EXISTS diary (' +
'id TEXT PRIMARY KEY, ' +
'date TEXT NOT NULL, ' +
'mood TEXT NOT NULL, ' +
'title TEXT, ' +
'content TEXT, ' +
'tags TEXT)'
);
// 增删改查
await store.insert('diary', {
id: generateId(), date: '2025-01-20',
mood: 'happy', title: '发年终奖了',
content: '...', tags: '工作,家庭'
});
4.4 三种方案对比
| 方案 | 持久化 | 响应式 | 复杂度 | 适用场景 |
|---|---|---|---|---|
| AppStorage | ❌ | ✅ | ⭐ | 演示/快速原型 |
| PersistentStorage | ✅ | ✅ | ⭐⭐ | 轻量数据存储 |
| RDB | ✅ | ❌ | ⭐⭐⭐ | 复杂数据查询 |
五、项目技术全景
5.1 技术栈总结
┌──────────────────────────────────────────┐
│ 心情日记 应用 │
├──────────────────────────────────────────┤
│ 框架:HarmonyOS Stage 模型 │
│ 语言:ArkTS (API 23) │
│ 构建:Hvigor │
│ 状态管理:AppStorage │
│ 导航:@ohos.router │
│ 设备:Phone │
│ SDK:compatibleSdkVersion 23 │
│ targetSdkVersion 24 │
└──────────────────────────────────────────┘
5.2 页面功能矩阵
| 页面 | 核心功能 | 状态变量数 | @Builder数 | 生命周期钩子 |
|---|---|---|---|---|
| Index | 今日心情、连续签到、快捷入口、最近列表 | 5 | 2 | aboutToAppear + onPageShow |
| WritePage | 日期显示、心情选择、标题/正文/标签输入、保存 | 6 | 0 | aboutToAppear |
| CalendarPage | 月历生成、月份切换、日期选中、日记详情、删除 | 7 | 0 | aboutToAppear + onPageShow |
| StatsPage | 总数统计、本月统计、心情分布柱状图、7天趋势 | 6 | 1 | aboutToAppear + onPageShow |
| ProfilePage | 统计卡片、标签云、功能菜单、数据重置 | 3 | 2 | aboutToAppear + onPageShow |
5.3 路由关系图
┌──────────┐
│ Index │ ← 首页(入口)
└────┬─────┘
│
┌─────────────┼─────────────┐
│ │ │
┌─────▼────┐ ┌────▼────┐ ┌─────▼────┐
│WritePage │ │Calendar │ │StatsPage │
│ 写日记 │ │ 日历 │ │ 统计 │
└──────────┘ └────┬────┘ └──────────┘
│
┌────▼────┐
│Profile │
│ 个人中心 │
└─────────┘
所有页面通过 router.pushUrl() 跳转,通过 router.back() 返回。
六、ArkTS 编码规范实战总结
6.1 命名规范
// 枚举:帕斯卡命名
enum MoodLevel { HAPPY = 'happy', CALM = 'calm' }
// 接口:帕斯卡命名
interface DiaryEntry { id: string; title: string; }
// 变量/函数:驼峰命名
let selectedMood: MoodLevel = MoodLevel.HAPPY;
function getMoodInfo(level: MoodLevel): MoodInfo { }
// 常量:全大写加下划线
const ALL_MOODS: MoodInfo[] = [];
// @Builder:驼峰命名
@Builder quickBtn(icon: string, label: string) { }
6.2 组件划分原则
// ✅ 好:每个页面一个文件,职责清晰
pages/
├── Index.ets // 首页
├── WritePage.ets // 写日记
├── CalendarPage.ets // 日历
├── StatsPage.ets // 统计
└── ProfilePage.ets // 个人中心
// ✅ 好:数据层独立文件
models/
└── DiaryData.ets // 模型+工具函数
6.3 状态管理规范
// 原则1:每个 @State 变量使用前必须初始化
@State entries: DiaryEntry[] = [];
// 原则2:onPageShow 中同步数据
onPageShow(): void { this.loadData(); }
// 原则3:修改数据后立即更新 AppStorage
AppStorage.set<DiaryEntry[]>('entries', newList);
6.4 生命周期使用规范
aboutToAppear(): void {
// 1. 读取参数
// 2. 初始化数据
// 3. 调用 loadData
this.loadData();
}
onPageShow(): void {
// 1. 重新加载数据(确保与全局状态同步)
this.loadData();
}
loadData(): void {
// 1. 从 AppStorage 读取
// 2. 更新 @State 变量
// 3. 执行计算(如统计、日历构建)
}
七、构建与部署
7.1 完整构建命令
"D:\DevEco Studio\tools\node\node.exe" \
"D:\DevEco Studio\tools\hvigor\bin\hvigorw.js" \
--mode module \
-p module=entry@default \
-p product=default \
-p requiredDeviceType=phone \
assembleHap \
--analyze=normal --parallel --incremental --daemon
7.2 生成产物
构建成功后,在 entry/build/default/outputs/default/ 下生成 .hap 文件,可直接安装到鸿蒙真机或模拟器。
八、踩坑记录全集
🕳️ 坑1:router 导入路径错误
问题:API 23 下 @kit.AbilityKit 不导出 router
解决:使用 import router from '@ohos.router'
🕳️ 坑2:ArkTS 严格模式对象字面量
问题:arkts-no-untyped-obj-literals
解决:先定义 interface,再显式指定类型
🕳️ 坑3:数组字面量类型推断
问题:arkts-no-noninferrable-arr-literals
解决:let arr: DiaryEntry[] = [...]
🕳️ 坑4:ForEach key 重复
问题:CalendarPage 中空位日期没有唯一 key,导致渲染警告
解决:空位使用 Math.random().toString() 作为 key
🕳️ 坑5:getDay() 返回值
问题:getDay() 返回 0=周日,而期望 7=周日
解决:let startWeekday = firstDay.getDay() || 7;
🕳️ 坑6:app_name 重复定义
问题:在 entry 和 AppScope 中都定义了 app_name
解决:app_name 只需在 AppScope 中定义一次
九、项目扩展方向
本应用虽然功能完整,但仍有广阔的扩展空间:
9.1 数据持久化
// 升级到 RDB 数据库,支持数据持久化
// 支持数据导出为文件(txt/csv/json)
// 支持云备份
9.2 功能增强
// 图片附件:拍照/相册选择照片
// 地理位置:记录日记时的位置
// 语音输入:语音转文字
// 分享功能:将日记分享为图片
// 数据可视化:更多图表(年度报告、情绪曲线)
// 暗黑模式:适配深色主题
9.3 性能优化
// 使用 Set/Map 优化查找
// 大数据量时分页加载
// 使用 LazyForEach 优化长列表
十、系列总结
通过这五篇文章,我们从零到一完成了一个完整的鸿蒙原生应用开发:
| 博文 | 主题 | 核心知识点 |
|---|---|---|
| 第一篇 | 项目起航 | Stage 模型、项目目录、路由注册、构建配置 |
| 第二篇 | 数据层设计 | 枚举、接口、AppStorage、模拟数据、工具函数 |
| 第三篇 | UI 构建 | 首页卡片、快捷按钮、@Builder 复用、写日记表单 |
| 第四篇 | 交互进阶 | 月历算法、Grid 网格、柱状图、7天趋势 |
| 第五篇 | 功能打磨 | 标签云、数据管理、性能优化、编码规范 |
项目地址
框架:Stage 模型 + ArkTS
SDK:API 23
写在最后
鸿蒙原生开发虽然与传统 Android/iOS 开发有所不同,但 ArkTS 的声明式 UI 设计、强大的 @Builder 装饰器、灵活的 Grid/Layout 布局,都让开发体验非常顺畅。Stage 模型的模块化设计理念,更是为大型应用的开发提供了良好的架构基础。
希望本系列的五篇文章能为正在学习鸿蒙开发的你提供实际帮助。从搭建框架到数据层,从 UI 构建到交互实现,再到最终的功能打磨,每一个环节都是实际项目开发的真实经验。
如果你有任何问题或建议,欢迎在评论区留言交流!我们下个项目见!
本系列所有代码均来自真实的鸿蒙原生项目「心情日记」,如需完整源码,欢迎关注作者。
更多推荐

所有评论(0)