一、项目背景与效果预览
1.1 为什么做这个项目
在快节奏的生活中,情绪管理变得越来越重要。心情日记不仅能帮助我们记录日常点滴,更能让我们直观地看到自己的情绪变化趋势。本文基于 HarmonyOS ArkTS 声明式 UI 框架,从零搭建一个集日记编写、心情选择、情绪统计、数据持久化于一体的心情日记应用。
技术栈 :ArkTS + ArkUI + 关系型数据库(RelationalStore)
适配版本 :API 24+,DevEco Studio 5.0.3 以上
核心知识点:
- 复杂 UI 布局(
Column/Row/Scroll/Flex) @State+@Builder状态驱动视图更新- 条件渲染与列表渲染
- 关系型数据库的增删改查
- 日期处理与简单数据统计
1.2 项目效果
本文实现的应用拥有完整的日记 CRUD 功能,粉紫治愈系配色,卡片式设计,支持心情 emoji 快速选择、日历视图、情绪趋势统计。项目可直接复制代码运行,适合作为 ArkUI 练手项目或课程设计。
项目使用 DevEco Studio 开发,适配 API 24 及以上版本。
运行效果

二、完整功能介绍
本应用围绕"心情日记"这一核心场景,设计了以下功能模块:
| 模块 | 功能点 |
|---|---|
| 🎭 心情选择 | 五种基础心情 emoji:😊 开心 / 😌 平静 / 😔 难过 / 😠 生气 / 😰 焦虑 |
| 📝 日记编辑 | 富内容输入、心情原因标签、天气关联、图片添加 |
| 📅 日历视图 | 按月查看日记记录,有日记的日期高亮显示 |
| 📊 情绪统计 | 饼图展示各心情占比、连续记录天数、情绪趋势折线图 |
| 🔍 搜索 | 按关键词搜索日记内容与标签 |
| 🔒 安全 | 密码锁保护隐私 |
| 💾 数据管理 | 日记导出(文本/JSON)、数据库持久化 |
| 🌈 治愈体验 | 治愈文案随机推荐、心情提醒通知 |
三、数据结构设计
清晰的数据结构是应用稳定的基石。我们定义 Diary 接口来描述一条日记的所有属性:
typescript
interface Diary {
id: number; // 日记唯一 ID(自增主键)
mood: string; // 心情 emoji
content: string; // 日记正文
date: string; // 日期(格式:YYYY-MM-DD)
weather: string; // 天气 emoji
tags: string[]; // 心情原因标签(如:"工作"、"家庭")
images: string[]; // 图片路径数组
}
// 心情选项映射
const MOOD_MAP: Record<string, string> = {
'😊': '开心',
'😌': '平静',
'😔': '难过',
'😠': '生气',
'😰': '焦虑'
};
// 天气选项映射
const WEATHER_OPTIONS = ['☀️', '⛅', '🌧️', '❄️', '🌩️'];
设计要点 :
tags使用字符串数组而非单一标签,方便后续扩展"多原因标注"和"按标签筛选"功能。images同样用数组,支持多图记录。
四、页面状态管理
ArkUI 的核心思想是 "状态驱动视图" ------数据变,UI 自动跟着变。我们用 @State 声明所有需要触发 UI 刷新的变量:
typescript
@Entry
@Component
struct MoodDiary {
// ========== 数据状态 ==========
@State private diaries: Diary[] = []; // 日记列表
@State private selectedMood: string = '😊'; // 当前选中的心情 emoji
@State private selectedDate: string = ''; // 当前选中日期(日历用)
@State private showAddDiary: boolean = false; // 是否显示新增日记弹窗
// ========== UI 控制状态 ==========
@State private currentTab: number = 0; // 底部 Tab:0-日记 1-日历 2-统计 3-我的
@State private searchKeyword: string = ''; // 搜索关键词
@State private showPasswordLock: boolean = false; // 密码锁弹窗
// ========== 统计状态 ==========
@State private consecutiveDays: number = 0; // 连续记录天数
@State private moodStats: Record<string, number> = {}; // 各心情出现次数
// 初始化时从数据库加载数据
aboutToAppear(): void {
this.loadDiariesFromDB();
this.calculateStats();
}
}
💡 为什么要拆分这么多状态?
每个
@State变量只负责一类 UI 变化:selectedMood只管心情高亮,showAddDiary只管弹窗显隐。单一职责让状态管理更清晰,避免"改了 A 导致 B 也意外刷新"的混乱。
五、核心功能方法(完整实现)
以下方法覆盖了日记的增、删、改、查、统计五大核心操作。每个方法都直接操作数据库并同步更新 UI 状态。
typescript
// ========== 1. 添加日记 ==========
private addDiary(content: string, weather: string, tags: string[], images: string[]): void {
if (content.trim() === '') {
// ArkUI 中可用 promptAction.showToast 给出提示
return;
}
const newDiary: Diary = {
id: Date.now(), // 用时间戳生成临时 ID,数据库会替换为自增主键
mood: this.selectedMood,
content: content,
date: this.getToday(),
weather: weather,
tags: tags,
images: images
};
// 插入数据库
this.insertDiaryToDB(newDiary);
// 更新 UI 列表(新数据插入到最前面)
this.diaries.unshift(newDiary);
// 重新计算统计数据
this.calculateStats();
// 关闭弹窗
this.showAddDiary = false;
}
// ========== 2. 删除日记 ==========
private deleteDiary(id: number): void {
// 从数据库删除
this.deleteDiaryFromDB(id);
// 从 UI 列表中移除
const index = this.diaries.findIndex(item => item.id === id);
if (index !== -1) {
this.diaries.splice(index, 1);
}
// 重新计算统计数据
this.calculateStats();
}
// ========== 3. 按心情筛选 ==========
private filterByMood(mood: string): Diary[] {
return this.diaries.filter(item => item.mood === mood);
}
// ========== 4. 搜索日记 ==========
private searchDiaries(keyword: string): Diary[] {
if (keyword.trim() === '') {
return this.diaries;
}
const lowerKeyword = keyword.toLowerCase();
return this.diaries.filter(item =>
item.content.toLowerCase().includes(lowerKeyword) ||
item.tags.some(tag => tag.toLowerCase().includes(lowerKeyword))
);
}
// ========== 5. 计算连续天数 ==========
private calculateConsecutiveDays(): void {
if (this.diaries.length === 0) {
this.consecutiveDays = 0;
return;
}
// 按日期排序(从新到旧)
const sortedDates = this.diaries
.map(item => item.date)
.sort()
.reverse();
let count = 1;
const today = this.getToday();
// 最新日记不是今天 → 连续天数归零
if (sortedDates[0] !== today) {
this.consecutiveDays = 0;
return;
}
// 向前统计连续天数
for (let i = 1; i < sortedDates.length; i++) {
const prevDate = this.addDays(today, -count);
if (sortedDates[i] === prevDate) {
count++;
} else {
break;
}
}
this.consecutiveDays = count;
}
// ========== 6. 情绪统计 ==========
private calculateStats(): void {
const stats: Record<string, number> = {
'😊': 0, '😌': 0, '😔': 0, '😠': 0, '😰': 0
};
this.diaries.forEach(item => {
if (stats[item.mood] !== undefined) {
stats[item.mood]++;
}
});
this.moodStats = stats;
this.calculateConsecutiveDays();
}
// ========== 7. 数据库操作(骨架示例) ==========
// 实际开发请使用 @ohos.data.relationalStore
private insertDiaryToDB(diary: Diary): void {
// TODO: 调用 RelationalStore.insert()
console.info('Diary inserted:', JSON.stringify(diary));
}
private deleteDiaryFromDB(id: number): void {
// TODO: 调用 RelationalStore.delete()
console.info('Diary deleted, id:', id);
}
private loadDiariesFromDB(): void {
// TODO: 调用 RelationalStore.query() 加载所有日记
// this.diaries = result;
}
// ========== 工具方法 ==========
private getToday(): string {
const now = new Date();
const y = now.getFullYear();
const m = String(now.getMonth() + 1).padStart(2, '0');
const d = String(now.getDate()).padStart(2, '0');
return `${y}-${m}-${d}`;
}
private addDays(dateStr: string, days: number): string {
const date = new Date(dateStr);
date.setDate(date.getDate() + days);
const y = date.getFullYear();
const m = String(date.getMonth() + 1).padStart(2, '0');
const d = String(date.getDate()).padStart(2, '0');
return `${y}-${m}-${d}`;
}
⚠️ 关于数据库 :以上代码中
insertDiaryToDB等方法是骨架。正式项目中建议封装一个DiaryDBHelper类,统一管理RelationalStore的建表、增删改查操作。
六、@Builder 可复用组件
ArkUI 的 @Builder 装饰器允许我们将重复 UI 抽取为可复用组件,减少代码冗余:
typescript
// ========== 日记卡片组件 ==========
@Builder
DiaryCard(diary: Diary) {
Column({ space: 10 }) {
// 顶行:心情 + 日期 + 天气 + 删除按钮
Row() {
Text(diary.mood)
.fontSize(28)
Column({ space: 4 }) {
Text(diary.date)
.fontSize(14)
.fontWeight(FontWeight.Medium)
.fontColor('#1F2937')
Text(diary.weather)
.fontSize(12)
.fontColor('#6B7280')
}
.margin({ left: 12 })
.alignItems(HorizontalAlign.Start)
Blank()
Text('🗑️')
.fontSize(20)
.onClick(() => this.deleteDiary(diary.id))
}
.width('100%')
// 日记正文
Text(diary.content)
.fontSize(14)
.fontColor('#4B5563')
.width('100%')
.maxLines(3)
.textOverflow({ overflow: TextOverflow.Ellipsis })
// 标签行
if (diary.tags.length > 0) {
Row({ space: 6 }) {
ForEach(diary.tags, (tag: string) => {
Text(tag)
.fontSize(11)
.fontColor('#BE185D')
.padding({ left: 8, right: 8, top: 2, bottom: 2 })
.backgroundColor('#FCE7F3')
.borderRadius(8)
})
}
.width('100%')
}
}
.width('100%')
.padding(16)
.backgroundColor(Color.White)
.borderRadius(14)
.shadow({ radius: 8, color: '#00000008' })
.onClick(() => {
// 点击卡片进入详情或编辑模式
})
}
// ========== 心情选择器组件 ==========
@Builder
MoodSelector() {
Row({ space: 20 }) {
ForEach(['😊', '😌', '😔', '😠', '😰'], (mood: string) => {
Text(mood)
.fontSize(32)
.padding(10)
.backgroundColor(this.selectedMood === mood ? '#FCE7F3' : Color.Transparent)
.borderRadius(50)
.onClick(() => {
this.selectedMood = mood;
})
})
}
.width('100%')
.justifyContent(FlexAlign.Center)
}
🎯 抽取原则 :一个
@Builder只做一件事。DiaryCard负责日记展示,MoodSelector负责心情选择。这样每个组件都清晰可测。
七、完整 build() 页面布局
我们将主页面分为四个区域:顶部标题栏 → 心情选择器 → 日记列表 → 底部 Tab 栏。
typescript
build() {
Column() {
// ==================== 1. 顶部标题栏 ====================
Row() {
Text('💜 心情日记')
.fontSize(24)
.fontWeight(FontWeight.Bold)
.fontColor('#1F2937')
Blank()
// 搜索按钮
Text('🔍')
.fontSize(22)
.margin({ right: 12 })
.onClick(() => { /* 打开搜索 */ })
// 新增日记按钮
Text('+')
.fontSize(28)
.fontColor('#BE185D')
.onClick(() => this.showAddDiary = true)
}
.width('100%')
.padding({ left: 20, right: 20, top: 12, bottom: 8 })
// ==================== 2. 心情选择器 ====================
this.MoodSelector()
.margin({ bottom: 16 })
// ==================== 3. 日记列表(可滚动) ====================
Scroll() {
Column({ space: 12 }) {
if (this.diaries.length === 0) {
// 空状态
Column({ space: 12 }) {
Text('📝')
.fontSize(64)
Text('还没有日记哦~')
.fontSize(16)
.fontColor('#9CA3AF')
}
.width('100%')
.height(300)
.justifyContent(FlexAlign.Center)
} else {
ForEach(this.diaries, (diary: Diary) => {
this.DiaryCard(diary)
}, (diary: Diary) => diary.id.toString())
}
}
.width('100%')
.padding({ left: 20, right: 20, bottom: 80 })
}
.layoutWeight(1)
.scrollBar(BarState.Off)
}
.width('100%')
.height('100%')
.backgroundColor('#FDF2F8') // 粉紫底色
}
📌 布局要点 :
layoutWeight(1)让Scroll撑满剩余空间;BarState.Off隐藏滚动条保持视觉纯净;ForEach的第三个参数是 key 生成函数,必须提供以保证列表渲染性能。
八、页面设计说明
8.1 配色方案
| 用途 | 色值 | 说明 |
|---|---|---|
| 主背景 | #FDF2F8 |
浅粉色,柔和护眼 |
| 卡片背景 | #FFFFFF |
纯白,提升内容可读性 |
| 强调色 | #BE185D |
深粉色,按钮/选中态 |
| 强调色浅 | #FCE7F3 |
浅粉,心情选中背景 |
| 主文字 | #1F2937 |
深灰,保证对比度 |
| 辅助文字 | #6B7280 / #4B5563 |
中灰,弱化次要信息 |
8.2 交互设计
- 卡片式设计 :
borderRadius(14)圆角 +shadow阴影营造层次感 - 即时反馈:点击心情 emoji 立即高亮(背景色切换),无需等待
- 空状态处理:无日记时显示引导插图,避免空白页尴尬
- 滚动优化 :
scrollBar(BarState.Off)隐藏滚动条
九、SDK 配置
在 build-profile.json5 中配置:
json
{
"compatibleSdkVersion": "6.1.1(24)",
"runtimeOS": "HarmonyOS"
}
确保 DevEco Studio 版本 ≥ 5.0.3,API Level ≥ 24。
十、运行项目
- 新建一个 ArkTS 项目(Empty Ability)
- 将上述代码按顺序合并 到
entry/src/main/ets/pages/Index.ets - 如需完整数据库功能,请参考 HarmonyOS 关系型数据库文档 补充
insertDiaryToDB等方法 - 点击 Run 按钮运行到模拟器或真机
💡 提示 :如果只想快速体验 UI,可以忽略数据库部分,直接使用
@State diaries的初始数据测试界面效果。
十一、项目总结与进阶方向
11.1 技术收获
通过本项目,你掌握了以下 ArkUI 核心技能:
- ✅ 状态管理:
@State声明式驱动 UI 更新 - ✅ 组件复用:
@Builder抽取可复用组件 - ✅ 列表渲染:
ForEach高效渲染日记列表 - ✅ 布局组合:
Column/Row/Scroll搭建复杂页面 - ✅ 数据统计:连续天数算法、心情占比计算
- ✅ 条件渲染:
if实现空状态展示
11.2 进阶方向
| 方向 | 技术点 | 难度 |
|---|---|---|
| 🤖 AI 情绪分析 | 接入华为意图框架或第三方 NLP API,根据日记内容自动判断情绪 | ⭐⭐⭐ |
| 🎵 心情音乐推荐 | 根据当前心情匹配本地音乐播放列表 | ⭐⭐ |
| 👥 社区分享 | 将日记匿名发布到社区广场 | ⭐⭐⭐ |
| 🖼️ 心情壁纸生成 | 用 Canvas 根据情绪数据生成渐变壁纸 | ⭐⭐ |
| 📊 年度报告 | 汇总全年数据,生成可视化年度情绪报告 | ⭐⭐⭐ |
| ☁️ 数据备份 | 使用华为云空间或分布式数据库实现多端同步 | ⭐⭐⭐ |
🚀 建议先从"数据库持久化 + 日历视图"开始迭代,这是提升应用实用性的最快路径。情故事生成等。