HarmonyOS ArkTS 实战:实现一个心情日记与情绪追踪应用

一、项目背景与效果预览

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。

十、运行项目

  1. 新建一个 ArkTS 项目(Empty Ability)
  2. 将上述代码按顺序合并entry/src/main/ets/pages/Index.ets
  3. 如需完整数据库功能,请参考 HarmonyOS 关系型数据库文档 补充 insertDiaryToDB 等方法
  4. 点击 Run 按钮运行到模拟器或真机

💡 提示 :如果只想快速体验 UI,可以忽略数据库部分,直接使用 @State diaries 的初始数据测试界面效果。

十一、项目总结与进阶方向

11.1 技术收获

通过本项目,你掌握了以下 ArkUI 核心技能:

  • ✅ 状态管理:@State 声明式驱动 UI 更新
  • ✅ 组件复用:@Builder 抽取可复用组件
  • ✅ 列表渲染:ForEach 高效渲染日记列表
  • ✅ 布局组合:Column / Row / Scroll 搭建复杂页面
  • ✅ 数据统计:连续天数算法、心情占比计算
  • ✅ 条件渲染:if 实现空状态展示

11.2 进阶方向

方向 技术点 难度
🤖 AI 情绪分析 接入华为意图框架或第三方 NLP API,根据日记内容自动判断情绪 ⭐⭐⭐
🎵 心情音乐推荐 根据当前心情匹配本地音乐播放列表 ⭐⭐
👥 社区分享 将日记匿名发布到社区广场 ⭐⭐⭐
🖼️ 心情壁纸生成 用 Canvas 根据情绪数据生成渐变壁纸 ⭐⭐
📊 年度报告 汇总全年数据,生成可视化年度情绪报告 ⭐⭐⭐
☁️ 数据备份 使用华为云空间或分布式数据库实现多端同步 ⭐⭐⭐

🚀 建议先从"数据库持久化 + 日历视图"开始迭代,这是提升应用实用性的最快路径。情故事生成等。

相关推荐
●VON2 小时前
鸿蒙 PC Markdown 编辑器通信架构:受限 ArkTS-JavaScript Bridge
华为·架构·编辑器·harmonyos·鸿蒙
一缕清烟在人间3 小时前
HarmonyOS开发实战:小分享-TextEditPage文字编辑器——Header+TextArea+工具栏
后端·华为·harmonyos·鸿蒙
2501_918582373 小时前
HarmonyOS应用开发实战:小事记 - 应用包结构:HAP/HSP/HAR 的三层架构与 deliveryWithInstall 策略
华为·架构·harmonyos·鸿蒙
不肥嘟嘟右卫门3 小时前
鸿蒙原生ArkTS布局方式之Scroll+Column+Sticky粘性布局深度解析
华为·harmonyos
<小智>4 小时前
鸿蒙多功能工具箱开发实战(二十四)-单元测试与自动化测试
ui·华为·harmonyos
youtootech4 小时前
HarmonyOS实战教程《台词拼图》(二)—— 响应式布局与断点系统
华为·harmonyos
Catrice04 小时前
HarmonyOS ArkTS 实战:实现一个电影追剧与观影记录应用(完整源码)
华为·harmonyos
●VON4 小时前
鸿蒙 PC Markdown 编辑器自由窗口:覆盖侧栏与响应式预算
安全·华为·编辑器·harmonyos·鸿蒙
SameX4 小时前
ArkTS 用 Preferences 存 App 配置的正确姿势 —— 从踩坑到 singleton 封装
harmonyos