引言

当恋爱从轰轰烈烈的告白回归到柴米油盐的日常,那些看似微不足道的小瞬间——一起看的樱花、早晨收到的消息、亲手做的提拉米苏、海边许下的愿望——才是一段感情最珍贵的记忆。恋爱日记正是这样一款概念应用,它将情侣日常的点点滴滴,浓缩进一个 ArkTS 单文件之中,用温暖粉调的视觉语言,记录"鹿与熊"这对情侣的甜蜜时光。

不同于传统日记App的单人记录路线,恋爱日记选择了"情侣专属·温暖粉调"的定位:深紫红 #2D1B2E 铺底,玫瑰粉 #FF6B8A 作为主强调色贯穿按钮、Tab激活、标题、爱心,浅粉 #FF9EB5 作为辅助强调色用于标签、统计,如同深夜里两人依偎时的温暖烛光。Tab图标缩放、日记点赞缩放——2 组交互动画让整个界面如同心跳般温柔起伏。

6 个 Tab 覆盖完整恋爱记录链路:甜蜜日记(8篇日记,写日记弹框,点赞缩放)、纪念日(4个纪念日,添加弹框,在一起天数)、心愿清单(8个心愿,添加弹框,勾选完成,已实现统计)、情侣互动(6个任务,添加弹框,勾选完成)、回忆相册(8张照片,三列宫格)、我的(情侣资料卡,三项统计,设置菜单)。4 套弹框各具特色:写日记(心情emoji选择+内容TextArea+发布)、添加纪念日(名称+日期+类型chips)、添加心愿(名称+描述+发起者chips)、添加互动任务(内容+发起者chips)。

本文将从项目概述、色彩系统、架构设计、数据模型、六大Tab、四大弹框、交互动画、日记与纪念日系统、心愿与互动系统、相册与我的系统、性能优化、情侣App文化、温暖粉调生活方式、开发环境搭建共十五个维度,对恋爱日记进行深度解析,文末附完整 ArkTS 源码。

恋爱日记主界面


第一章 项目概述与设计理念

1.1 项目定位

恋爱日记是一款面向情侣专属的温暖粉调日记记录应用,核心定位是"日记+纪念日+心愿+互动+相册+我的"六位一体的情侣日常记录 App 风格。目标用户是 18-30 岁的恋爱中情侣、新婚夫妇、异地恋伴侣。应用场景包括:记录日常甜蜜瞬间、倒计时重要纪念日、共同规划心愿清单、完成情侣互动任务、珍藏回忆照片、管理情侣资料。

1.2 设计理念

深夜依偎温暖美学:以深紫红 #2D1B2E 为背景,玫瑰粉 #FF6B8A 作为主强调色贯穿按钮、Tab激活、标题、爱心、在一起天数,浅粉 #FF9EB5 作为辅助强调色用于标签、统计数,如同深夜里两人依偎时的温暖烛光。深紫红背景营造私密、温馨的氛围,玫瑰粉在深紫红上具有柔和而温暖的视觉穿透力,如同恋爱中的心跳。

情侣专属双视角:日记由"小鹿"(她🦌)和"小熊"(他🐻)轮流记录,每条日记标注发布者头像和名字,心愿和互动任务标注发起者(他/她),我的页面展示双人头像+💕连接,处处体现"两个人"的专属感。这种双视角设计让每一条记录都有明确的归属,增加了情侣间的互动和仪式感。

温暖日常仪式感:日记的心情emoji选择(😊🥰😍🤗😘💕)、纪念日的天数倒计时、心愿的勾选完成(✅/⬜)、互动任务的完成标记、相册的回忆标题——每一个功能都在为恋爱日常增添仪式感。简单的勾选动作,让"一起完成一件事"变得有迹可循;天数倒计时,让"在一起多久"变成看得见的数字。

1.3 核心功能矩阵

模块 核心功能 数据量
日记 日记列表、点赞缩放、写日记弹框(心情+内容) 8篇/6心情
纪念日 纪念日列表、天数倒计时、添加弹框(名称+日期+类型) 4个/3类型
心愿 心愿清单、勾选完成、已实现统计、添加弹框(名称+描述+发起者) 8个/2发起者
互动 情侣任务、勾选完成、添加弹框(内容+发起者) 6个任务
相册 回忆照片、三列宫格、标题+日期 8张照片
我的 情侣资料卡、三项统计、设置菜单 1对情侣

1.4 技术亮点

  • ArkTS 严格模式:无 Object.assign,所有对象更新使用对象字面量显式重建,所有变量和函数参数显式类型标注,所有语句以分号结尾,符合 ArkTS 严格模式规范
  • 单文件零依赖:约 1000 行代码在一个 .ets 文件中,复制即运行
  • 2 组交互动画:Tab图标缩放(scale 1.15,200ms EaseOut)、日记点赞缩放(scale 1.2,200ms)
  • 4 差异化弹框:写日记 / 添加纪念日 / 添加心愿 / 添加互动任务
  • 日期计算引擎:daysSince 纯函数计算距今天数,formatDate 格式化日期,todayStr 获取今日日期字符串,genId 生成随机ID
  • 不可变状态更新:所有 @State 数组更新使用 map/filter/展开运算符返回新数组,不直接修改原数组
  • 情侣双视角:日记发布者、心愿发起者、任务发起者、双人头像,处处体现"两个人"
  • 三列相册宫格:Flex Wrap 实现三列紧凑布局,emoji占位照片

第二章 色彩系统与视觉风格

2.1 色彩体系

恋爱日记定义了 11 色完整调色板(LovePalette 接口),分为四个层级:

基础层(深紫红暖调)

  • bg #2D1B2E:应用背景,深紫红
  • card #3D263F:卡片背景,紫红
  • cardDeep #2E1C30:深层卡片/头部/Tab栏,更深紫红
  • chip #4D334F:标签/搜索栏背景,紫褐
  • white #FFFFFF:纯白

强调层(玫瑰粉 × 浅粉)

  • primary #FF6B8A:玫瑰粉,主按钮、Tab激活、标题、爱心、在一起天数、统计数
  • secondary #FF9EB5:浅粉,辅助标签、纪念日提醒、统计数
  • warn #FF6B8A:警告色(与主色相同,温暖粉调无强烈警告)

文字层

  • text #F5E6F0:主文字,浅粉白
  • textSub #B88FA8:次要文字,灰粉

2.2 色彩应用策略

玫瑰粉 #FF6B8A 是应用的灵魂色,承担"爱与温暖"角色:底部 Tab 激活文字与图标、主操作按钮(写日记/发布/添加)、页面标题(甜蜜日记/纪念日等)、爱心点赞(❤️)、在一起天数(❤️在一起X天)、日记发布者名字、我的页面情侣名称。玫瑰粉在深紫红背景上柔和而温暖,如同恋爱中的心跳,是深夜依偎时的温暖光源。

浅粉 #FF9EB5 承担"辅助与标签"角色:纪念日提醒标签、心愿已实现统计、我的页面纪念日统计数、快捷入口标签。浅粉与玫瑰粉形成深浅渐变,在数据可视化中区分"主强调"与"辅助标签"。

深紫红 #2D1B2E 承担"私密温馨氛围"角色:应用背景、卡片间留白、弹框遮罩外区域。深紫红色调营造私密、温馨、浪漫的氛围,符合情侣日记的深夜记录场景,与玫瑰粉形成柔和对比,让强调色更加温暖突出。

2.3 温暖粉调渐变与质感

应用大量使用纯色块构建视觉层次:

  • 日记卡片:纯色紫红背景 #3D263F,14vp圆角
  • 心愿勾选:⬜未完成 / ✅已完成,已完成心愿名称添加删除线
  • 互动任务勾选:⬜未完成 / ✅已完成,已完成任务名称添加删除线
  • 相册照片:emoji占位(📸🎆🌹🌸🌊🍂❄️🍷),紫褐背景 #4D334F,12vp圆角
  • 快捷入口标签:今日甜蜜(玫瑰粉背景#4A2A3A)、纪念日提醒(浅粉背景#3A2A40)、心愿清单(淡粉背景#4A2A2A)

温暖日常元素是恋爱日记的标志性设计:💕连接双人头像、❤️在一起天数、😍心情emoji、🎂生日图标、💑在一起图标、🎉纪念日图标、⭐心愿清单、💞情侣互动、🖼️回忆相册。这些元素共同营造温暖、甜蜜、浪漫的情侣日常氛围。

不对称圆角贯穿全应用:卡片使用 14vp 统一圆角,弹框使用顶部 20vp 圆角(底部抽屉式),搜索栏 17vp 圆角,按钮 13-16vp 圆角,相册照片 12vp 圆角。

2.4 心跳缩放与点赞动画

项目通过交互动画模拟恋爱中的心跳感:

  • Tab图标缩放:底部 Tab 切换时,激活图标 scale 从 1.0 到 1.15,200ms EaseOut,如同选中时的微弹心跳
  • 日记点赞缩放:日记卡片爱心点击时 scale 从 1.0 到 1.2,200ms,点赞时爱心从白色🤍变为红色❤️并放大1.2倍,如同心跳加速

这些动画共同营造恋爱中的心跳感,让静态界面"活"起来。点赞缩放是本应用的特色动画,每次点赞时爱心放大跳动,如同看到对方日记时的心跳加速。Tab图标缩放提供选中时的微弹反馈,如同切换到不同恋爱场景时的心情起伏。

恋爱日记色彩与视觉风格


第三章 项目架构与文件组织

3.1 单文件组件化架构

恋爱日记采用单文件组件化架构,所有代码在一个 LoveApp.ets 文件中,通过 @Component 装饰器划分为约 8 个独立组件(6 Tab + 头部 + 内联弹框)。代码量约 1000 行,处于单文件架构的合理范围。

3.2 组件层次结构

LoveApp (@Entry 根组件)
├── 顶部标题栏(LoveHeader:Logo+搜索+快捷入口)
├── 内容区 Stack
│   ├── 6 个 Tab 内容组件(6 选 1)
│   │   ├── DiaryContent(日记)
│   │   ├── AnniversaryContent(纪念日)
│   │   ├── WishContent(心愿)
│   │   ├── TaskContent(互动)
│   │   ├── PhotoContent(相册)
│   │   └── MineContent(我的)
│   └── 弹框(各Tab内联管理,zIndex 999)
│       ├── 写日记弹框(DiaryContent内)
│       ├── 添加纪念日弹框(AnniversaryContent内)
│       ├── 添加心愿弹框(WishContent内)
│       └── 添加互动任务弹框(TaskContent内)
└── 底部 Tab 导航栏(6 项,图标缩放动画)

根组件管理当前 Tab(activeTab)。每个 Tab 组件内部管理自己的弹框显隐状态和交互状态,这种内联管理方式减少了根组件的状态复杂度。根组件使用 if/else if 链切换 Tab 内容(而非多个独立 if),代码更简洁。

3.3 @Builder 自定义构建函数

项目使用多个 @Builder 方法封装可复用 UI 片段:tabItem(底部Tab项,含图标缩放动画)、modalOverlay(弹框遮罩层)、addDiaryModal(写日记弹框)、addAnniversaryModal(添加纪念日弹框)、addWishModal(添加心愿弹框)、addTaskModal(添加互动任务弹框)。

3.4 弹框架构设计

弹框采用"遮罩层 + 内容层"Stack 叠加模式,每个 Tab 组件内部管理自己的弹框。4 套弹框各具特色:

  1. 写日记:心情emoji选择(6种)+ 日记内容TextArea + 取消/发布(底部抽屉式)
  2. 添加纪念日:名称输入 + 日期输入 + 类型chips(在一起/生日/纪念日)+ 取消/添加(底部抽屉式)
  3. 添加心愿:心愿名称输入 + 描述输入 + 发起者chips(他/她)+ 取消/添加(底部抽屉式)
  4. 添加互动任务:任务内容输入 + 发起者chips(他/她)+ 取消/添加(底部抽屉式)

所有弹框均为底部抽屉式设计,统一的视觉风格,20vp顶部圆角,rgba(0,0,0,0.6)遮罩,zIndex 999。


第四章 数据模型与全局状态管理

4.1 接口定义体系

项目定义 7 个 TypeScript 接口描述数据模型:LovePalette(调色板)、DiaryItem(日记)、AnniversaryItem(纪念日)、WishItem(心愿)、TaskItem(互动任务)、PhotoItem(照片)、CoupleInfo(情侣信息)。所有接口属性均显式类型标注,以分号结尾,符合 ArkTS 严格模式规范。

4.2 全局数据常量

约 9 组全局 const 常量:核心业务数据(DIARIES 8篇 / ANNIVERSARIES 4个 / WISHES 8个 / TASKS 6个 / PHOTOS 8张 / COUPLE 1对)、配置数据(MOODS 6心情 / ANNIVERSARY_TYPES 3类型 / PARTNERS 2发起者)。

4.3 三级状态管理策略

  • 第一级 全局常量(const):业务数据和配置,任何组件可直接读取
  • 第二级 组件局部状态(@State):组件内交互状态,如 diaries(日记列表,可点赞添加)、showAdd(弹框显隐)、newContent/newMood(写日记表单)、anniversaries(纪念日列表,可添加)、wishes(心愿列表,可勾选添加)、tasks(任务列表,可勾选添加)
  • 第三级 根组件协调状态(@State):跨组件状态,如 activeTab(当前 Tab)

4.4 全局纯函数

4 个全局纯函数处理日期计算和ID生成:

  • daysSince(dateStr):计算距今天数,返回整数
  • formatDate(d):格式化 Date 对象为 YYYY-MM-DD 字符串
  • todayStr():获取今日日期字符串(调用 formatDate(new Date()))
  • genId():生成随机ID(Math.floor(Math.random() * 1000000))

纯函数相同输入永远产生相同输出(genId除外,为随机生成),无副作用,易于测试和复用。daysSince 纯函数是纪念日和在一起天数的核心计算逻辑,基于 Date 对象的 getTime() 差值计算天数。formatDate 纯函数使用 padStart 补零,确保日期格式统一为 YYYY-MM-DD。

4.5 ArkTS 严格模式特性

本项目严格遵循 ArkTS 严格模式规范:

  • 无 Object.assign:所有对象更新使用对象字面量显式重建,如日记点赞时 { id: item.id, user: item.user, ... } 而非 Object.assign({}, item, { loved: !item.loved })
  • 显式类型标注:所有变量、函数参数、函数返回值均显式类型标注,如 function daysSince(dateStr: string): number
  • 分号结尾:所有语句以分号结尾,如 this.activeTab = tab;
  • 不可变数组更新:所有 @State 数组更新使用 map/filter/展开运算符返回新数组,如 this.diaries = [newDiary, ...this.diaries],不直接使用 push/pop/splice 修改原数组
  • 显式对象重建:map 回调中更新对象时使用完整对象字面量,不使用部分更新

这些严格模式特性确保代码在 ArkTS 严格模式下可编译运行,类型安全,无运行时类型错误。


第五章 六大 Tab 页面深度解析

5.1 日记 Tab(DiaryContent)

日记是入口,承担"浏览日记→点赞缩放→写日记"路径。页面结构:

  1. 甜蜜日记标题:📖甜蜜日记 + 共X篇
  2. 日记列表(8篇):每篇含头像+发布者+日期+心情emoji + 日记内容 + 评论数+点赞 + 爱心点赞(点赞缩放动画 scale 1.2,200ms)
  3. 到底留白

点赞缩放动画是日记Tab的核心交互:每篇日记的爱心(🤍/❤️)点击时,使用 map 遍历 diaries 数组,找到匹配 id 的日记,用完整对象字面量重建(loved 取反,likes 加减1),返回新数组赋值给 this.diaries。爱心 scale 根据 d.loved 在 1.2 和 1.0 之间切换,200ms 动画。点赞时爱心从白色🤍变为红色❤️并放大1.2倍,点赞数+1;取消点赞时爱心缩小变回白色,点赞数-1。1.2倍放大增加点赞的心跳感,如同看到对方日记时的心跳加速。

日记卡片设计:每篇含头像(emoji 20vp,紫褐背景圆角18)+发布者名字+日期+心情emoji,日记内容(11vp浅粉白),底部评论数+点赞按钮。卡片紫红背景#3D263F,14vp圆角,12vp内边距,10vp顶部间距。8篇日记由小鹿(她🦌)和小熊(他🐻)轮流发布,内容温暖甜蜜,如"今天一起去看了樱花,你说我比花还好看"、“第一次一起看日出,在海边许愿永远在一起”。

写日记弹框:头部搜索栏的"写日记"按钮和(当前为空操作,可扩展)触发,含心情emoji选择(6种:😊🥰😍🤗😘💕,选中玫瑰粉背景)+ 日记内容TextArea + 取消/发布按钮。发布时校验内容非空,创建新 DiaryItem(genId生成ID,发布者固定为小鹿🦌,日期为todayStr()),插入到 diaries 数组头部,清空表单并关闭弹框。

日记Tab

5.2 纪念日 Tab(AnniversaryContent)

纪念日是倒计时,承担"浏览纪念日→天数倒计时→添加纪念日"路径。页面结构:

  1. 纪念日标题:🎉纪念日 + ❤️在一起X天(玫瑰粉标签,daysSince(‘2024-12-01’)实时计算)
  2. 纪念日列表(4个):每个含图标(💑在一起/🎂生日/🎉纪念日)+标题+类型+日期 + 天数(X天前,玫瑰粉加粗)
  3. ➕添加纪念日按钮

天数倒计时是纪念日Tab的核心功能:每个纪念日的 days 属性在数据初始化时调用 daysSince(date) 计算,显示为"X天前"。在一起的第一天(2024-12-01)的天数同时显示在标题栏的"❤️在一起X天"标签中,实时计算。daysSince 纯函数基于 Date 对象的 getTime() 差值计算,Math.floor 取整。天数倒计时让"在一起多久"变成看得见的数字,增加恋爱的仪式感和成就感。

纪念日卡片设计:每个含图标(28vp)+标题(14vp加粗)+类型·日期(10vp灰粉),右侧天数(16vp玫瑰粉加粗+"天前"9vp灰粉)。卡片紫红背景#3D263F,14vp圆角,12vp内边距,10vp顶部间距。4个纪念日:在一起的第一天、小鹿的生日、小熊的生日、100天纪念日。

添加纪念日弹框:含名称输入 + 日期输入(YYYY-MM-DD格式,默认todayStr())+ 类型chips(在一起/生日/纪念日,选中玫瑰粉背景)+ 取消/添加按钮。添加时校验名称和日期非空,创建新 AnniversaryItem(genId生成ID,图标根据类型自动选择:在一起💑/生日🎂/纪念日🎉,days调用daysSince计算),追加到 anniversaries 数组尾部,清空表单并关闭弹框。

纪念日Tab

5.3 心愿 Tab(WishContent)

心愿是共同规划,承担"浏览心愿→勾选完成→已实现统计→添加心愿"路径。页面结构:

  1. 心愿清单标题:⭐心愿清单 + 已实现X/Y(filter统计已完成数量)
  2. 心愿列表(8个):每个含勾选框(⬜/✅,点击切换)+名称+描述+发起者 + 日期
  3. ➕添加心愿按钮

勾选完成是心愿Tab的核心交互:每个心愿的勾选框(⬜未完成/✅已完成)点击时,使用 map 遍历 wishes 数组,找到匹配 id 的心心愿,用完整对象字面量重建(done 取反),返回新数组赋值给 this.wishes。已完成的心心愿名称添加删除线(TextDecorationType.LineThrough),勾选框变为✅。标题栏的"已实现X/Y"实时统计已完成数量(filter(w => w.done).length)。勾选完成让"一起实现一个愿望"变得有迹可循,增加恋爱的目标感和成就感。

心愿卡片设计:每个含勾选框(20vp)+名称(14vp加粗,已完成添加删除线)+描述·发起者(10vp灰粉),右侧日期(9vp灰粉)。卡片紫红背景#3D263F,14vp圆角,12vp内边距,10vp顶部间距。8个心愿由他和她轮流发起,如"去北海道看雪"(她)、“养一只猫”(他)、“去迪士尼看烟花”(她,已实现✅)、“学做一道对方喜欢的菜”(他)。

添加心愿弹框:含心愿名称输入 + 描述输入 + 发起者chips(他/她,选中玫瑰粉背景)+ 取消/添加按钮。添加时校验名称非空,创建新 WishItem(genId生成ID,描述默认为"暂无描述",done为false,日期为todayStr()),追加到 wishes 数组尾部,清空表单并关闭弹框。

心愿Tab

5.4 互动 Tab(TaskContent)

互动是情侣任务,承担"浏览任务→勾选完成→添加任务"路径。页面结构:

  1. 情侣互动标题:💞情侣互动 + 今日任务标签
  2. 提示语:💡完成互动任务,让感情升温吧!
  3. 任务列表(6个):每个含勾选框(⬜/✅,点击切换)+任务内容+发起者
  4. ➕添加互动任务按钮

勾选完成是互动Tab的核心交互:与心愿Tab相同,每个任务的勾选框点击时 map 重建数组(done取反),已完成任务名称添加删除线。互动任务设计为简单可完成的日常小任务,如"给对方一个拥抱🤗"、“说一句我爱你💕”、“一起做一顿晚餐🍳”、“给对方一个惊喜🎁”、“一起看一部电影🎬”、“写一封情书💌”。这些简单的日常任务让情侣间的互动变得有计划、有记录,增加恋爱的新鲜感和仪式感。

任务卡片设计:每个含勾选框(20vp)+任务内容(13vp中等,已完成添加删除线)+由X发起(9vp灰粉)。卡片紫红背景#3D263F,14vp圆角,12vp内边距,10vp顶部间距。6个任务由他和她轮流发起。

添加互动任务弹框:含任务内容输入 + 发起者chips(他/她)+ 取消/添加按钮。添加时校验内容非空,创建新 TaskItem(genId生成ID,done为false),追加到 tasks 数组尾部,清空表单并关闭弹框。

互动Tab

5.5 相册 Tab(PhotoContent)

相册是回忆珍藏,承担"浏览照片→三列宫格"路径。页面结构:

  1. 回忆相册标题:🖼️回忆相册 + 📸X张
  2. 三列宫格(8张):每张含emoji照片(40vp,紫褐背景)+标题+日期

三列宫格设计:使用 Flex({wrap: FlexWrap.Wrap, justifyContent: FlexAlign.SpaceBetween}) 实现三列布局,每卡宽度 31.8%,自动换行。每卡居中对齐,emoji照片(40vp,紫褐背景#4D334F,12vp圆角,16vp内边距),标题(11vp中等),日期(9vp灰粉)。三列紧凑布局最大化展示照片数量,如同真实相册的照片墙。

照片数据:8张照片覆盖恋爱中的重要时刻,使用emoji占位:第一次约会📸、跨年夜🎆、情人节🌹、春游赏花🌸、夏日海边🌊、秋日银杏🍂、冬日初雪❄️、纪念日晚餐🍷。每张照片有标题和日期,记录恋爱中的美好回忆。emoji占位简洁可爱,符合温暖粉调的整体风格。

相册Tab

5.6 我的 Tab(MineContent)

我的是情侣中心,承担"资料卡→统计→设置菜单"路径。页面结构:

  1. 情侣资料卡:双人头像(她🦌+💕+他🐻,水平居中)+情侣名称(鹿与熊,玫瑰粉18vp加粗)+❤️在一起X天(13vp)+情侣简介(从今天起,每一天都是甜的。)
  2. 三项统计卡:日记48(玫瑰粉)/纪念日12(浅粉)/心愿23(浅粉白),每项含数字+标签
  3. 设置菜单卡:编辑资料/消息通知/深色模式/关于恋爱日记,每项含图标+名称+>箭头,分割线分隔
  4. 底部标语:— 愿每一对情侣都甜甜蜜蜜 —

情侣资料卡设计是我的Tab的核心视觉:双人头像水平排列,中间用💕连接,体现"两个人"的专属感。情侣名称"鹿与熊"玫瑰粉加粗,在一起天数实时计算(COUPLE.days,初始化时调用daysSince),情侣简介温暖甜蜜。资料卡紫红背景#3D263F,14vp圆角,14vp内边距,内容居中对齐。

三项统计卡:日记/纪念日/心愿三项统计,水平排列,每项居中对齐。数字分别使用玫瑰粉(日记)、浅粉(纪念日)、浅粉白(心愿),标签9vp灰粉。统计卡紫红背景#3D263F,14vp圆角,14vp内边距。

设置菜单卡:编辑资料/消息通知/深色模式/关于恋爱日记四项,每项含图标+名称+>箭头,分割线(chip色)分隔。设置菜单紫红背景#3D263F,14vp圆角,14vp内边距。当前为静态展示,可扩展为实际设置功能。

我的Tab


第六章 四大弹框组件设计

弹框是恋爱日记交互体系的核心组成部分。项目共设计了 4 套弹框组件,覆盖写日记、添加纪念日、添加心愿、添加互动任务四大业务场景。所有弹框均为底部抽屉式设计,统一的视觉风格。

6.1 写日记弹框(addDiaryModal)

定位:日记记录入口,心情emoji选择+内容TextArea+发布设计。

核心功能

  1. 心情选择(6 选 1):😊🥰😍🤗😘💕,选中玫瑰粉背景,emoji 18vp
  2. 日记内容:TextArea 多行输入(记录今天的故事…),100vp高度
  3. 取消/发布按钮:取消灰粉边框/发布玫瑰粉背景,发布时校验内容非空

写日记弹框采用底部抽屉式设计,心情emoji选择是特色功能,6种心情emoji覆盖日常恋爱中的各种情绪,让每篇日记都有心情标签。发布时创建新 DiaryItem(genId生成ID,发布者固定为小鹿🦌,日期为todayStr(),心情为选中的emoji,likes为0,loved为false,comments为0),插入到 diaries 数组头部(最新日记在最前)。

6.2 添加纪念日弹框(addAnniversaryModal)

定位:纪念日添加入口,名称+日期+类型chips设计。

核心功能

  1. 名称输入:TextInput(例如:第一次约会)
  2. 日期输入:TextInput(YYYY-MM-DD格式,默认todayStr())
  3. 类型选择(3 选 1):在一起/生日/纪念日,选中玫瑰粉背景
  4. 取消/添加按钮:取消灰粉边框/添加玫瑰粉背景,添加时校验名称和日期非空

添加纪念日弹框采用底部抽屉式设计,类型chips选择是特色功能,3种类型(在一起/生日/纪念日)覆盖情侣间的重要日子。添加时创建新 AnniversaryItem(genId生成ID,图标根据类型自动选择:在一起💑/生日🎂/纪念日🎉,days调用daysSince(date)计算),追加到 anniversaries 数组尾部。

6.3 添加心愿弹框(addWishModal)

定位:心愿添加入口,名称+描述+发起者chips设计。

核心功能

  1. 心愿名称输入:TextInput(例如:一起去旅行)
  2. 描述输入:TextInput(简单描述)
  3. 发起者选择(2 选 1):他/她,选中玫瑰粉背景
  4. 取消/添加按钮:取消灰粉边框/添加玫瑰粉背景,添加时校验名称非空

添加心愿弹框采用底部抽屉式设计,发起者chips选择是特色功能,2种发起者(他/她)体现情侣双视角。添加时创建新 WishItem(genId生成ID,描述默认为"暂无描述",done为false,日期为todayStr()),追加到 wishes 数组尾部。

6.4 添加互动任务弹框(addTaskModal)

定位:互动任务添加入口,内容+发起者chips设计。

核心功能

  1. 任务内容输入:TextInput(例如:给对方一个拥抱)
  2. 发起者选择(2 选 1):他/她,选中玫瑰粉背景
  3. 取消/添加按钮:取消灰粉边框/添加玫瑰粉背景,添加时校验内容非空

添加互动任务弹框采用底部抽屉式设计,与添加心愿弹框结构相似但更简洁(无描述字段)。添加时创建新 TaskItem(genId生成ID,done为false),追加到 tasks 数组尾部。

6.5 弹框统一设计规范

4套弹框遵循统一设计规范:

  • 布局:底部抽屉式,Column 垂直布局,modalOverlay 遮罩层 + 内容层
  • 遮罩:rgba(0,0,0,0.6) 半透明黑色,zIndex 999
  • 内容层:紫红背景#3D263F,顶部20vp圆角,18vp内边距,底部28vp内边距
  • 标题栏:标题(16vp加粗浅粉白)+ ✕关闭按钮(灰粉),底部12vp内边距
  • 表单标签:11vp灰粉,宽度100%
  • 输入框:TextInput/TextArea,紫褐背景#4D334F,10vp圆角,13vp浅粉白文字
  • chips选择:选中玫瑰粉背景深紫红字,未选中紫褐背景浅粉白字,10vp圆角
  • 操作按钮:取消灰粉边框/主操作玫瑰粉背景深紫红字,16vp圆角,水平居中

统一设计规范确保4套弹框视觉一致,用户体验统一。


第七章 交互动画与用户体验

7.1 ArkTS 交互动画机制

ArkTS 通过属性动画(.animation())实现交互动画。恋爱日记主要使用属性动画(.animation()),在状态变化时自动应用过渡动画:

.scale({ x: this.activeTab === tab ? 1.15 : 1, y: this.activeTab === tab ? 1.15 : 1 })
.animation({ duration: 200, curve: Curve.EaseOut })

属性动画绑定在组件属性上,当依赖的状态变化时自动触发动画过渡,无需手动管理动画帧。本应用 2 组交互动画全部使用属性动画方式。

7.2 两组交互动画详解

动画 组件 属性 时长 曲线 效果
Tab图标缩放 底部Tab scale 1.0↔1.15 200ms EaseOut 激活时放大1.15
日记点赞缩放 日记爱心 scale 1.0↔1.2 200ms 默认 点赞时放大1.2

Tab图标缩放动画:底部Tab切换时,激活图标 scale 从 1.0 到 1.15,200ms EaseOut 动画。激活时图标微弹放大,配合玫瑰粉文字颜色和底部分割线,形成三重选中反馈。1.15倍微放大增加Tab切换的弹性反馈,如同切换到不同恋爱场景时的心情起伏。Tab图标(📖🎉⭐💞🖼️👤)本身就是可爱的emoji,放大后更加生动。

日记点赞缩放动画:日记卡片爱心点击时,爱心 scale 从 1.0 到 1.2,200ms 动画。点赞时爱心从白色🤍变为红色❤️并放大1.2倍,点赞数+1。1.2倍放大增加点赞的心跳感,如同看到对方日记时的心跳加速。200ms时长适中,既不过快显得突兀,也不过慢显得拖沓。爱心颜色变化(🤍→❤️)+缩放动画+点赞数变化三重反馈,让点赞操作有充分的满足感。

7.3 属性动画的优势

传统逐帧动画需要手动管理动画帧和定时器,属性动画通过框架自动调度:状态变化时自动触发过渡、动画参数(时长/曲线)声明式配置、多个属性动画可同时运行互不干扰、组件销毁时动画自动停止。恋爱日记 2 组交互动画全部采用属性动画方式,代码简洁且性能优良。

7.4 交互反馈设计

项目通过多种方式提供即时交互反馈:

  • Tab切换:图标缩放+文字颜色+底部分割线三重反馈
  • 日记点赞:爱心颜色变化(🤍→❤️)+缩放动画+点赞数变化三重反馈
  • 心愿勾选:勾选框变化(⬜→✅)+名称删除线+已实现统计更新三重反馈
  • 任务勾选:勾选框变化(⬜→✅)+名称删除线双重反馈
  • chips选择:点击后立即切换选中态,颜色变化(玫瑰粉背景)
  • 弹框操作:发布/添加后立即关闭弹框并更新列表,数据实时同步
  • 日期计算:在一起天数和纪念日天数实时计算(daysSince纯函数),每次打开页面都显示最新天数

这些即时反馈确保用户操作的"确定性",提升交互流畅度和恋爱记录的仪式感。心愿勾选的三重反馈(勾选框+删除线+统计更新)是本应用的特色交互,每次完成一个心愿都有充分的成就感,如同一起实现一个小目标的喜悦。

7.5 情侣双视角交互

恋爱日记的交互设计处处体现"情侣双视角":

  • 日记发布者:每条日记标注发布者头像和名字(小鹿🦌/小熊🐻),让每篇日记有明确的归属
  • 心愿发起者:每个心愿标注发起者(他/她),让共同规划有明确的提议方
  • 任务发起者:每个互动任务标注发起者(他/她),让情侣互动有明确的发起方
  • 双人头像:我的页面展示双人头像(她🦌+💕+他🐻),体现"两个人"的专属感
  • 写日记默认发布者:写日记弹框发布时默认发布者为小鹿🦌(可扩展为切换发布者)

情侣双视角设计让每一条记录都有明确的归属,增加了情侣间的互动和仪式感。用户可以清晰地看到"这是她写的日记"、“这是他发起的心愿”,如同两人共同书写的恋爱故事。


第八章 日记与纪念日系统深度解析

8.1 日记数据体系

项目定义 8 篇日记(DIARIES 常量),每篇包含:id、user(发布者)、avatar(emoji头像)、content(日记内容)、date(日期)、mood(心情emoji)、likes(点赞数)、loved(是否已点赞)、comments(评论数)。

8 篇日记由小鹿(她🦌)和小熊(他🐻)轮流发布,时间跨度从 2026-03-12 到 2026-03-28,内容温暖甜蜜:

  • 小鹿🦌:今天一起去看了樱花,你说我比花还好看。(😍,56赞,已点赞)
  • 小熊🐻:早晨醒来看到你发的消息,一整天都甜甜的。(🥰,43赞)
  • 小鹿🦌:你亲手做的提拉米苏,虽然有点苦但心里好甜。(😘,78赞,已点赞)
  • 小熊🐻:第一次一起看日出,在海边许愿永远在一起。(💕,102赞,最高赞)
  • 小鹿🦌:今天下雨了,你把伞都倾向我这边,自己淋湿了半边。(🤗,67赞)
  • 小熊🐻:你送的围巾好暖和,是这个冬天最棒的礼物。(😊,34赞)
  • 小鹿🦌:今天是我们第100天,晚上去吃了烛光晚餐,好浪漫。(😍,210赞,最高赞,已点赞)
  • 小熊🐻:晚上视频通话了两个小时,舍不得挂断。(💕,45赞)

日记内容真实感强,如同真实情侣的日常记录。心情emoji覆盖6种(😊🥰😍🤗😘💕),每篇日记的心情与内容匹配。点赞数从34到210不等,第100天纪念日的日记获得最高210赞,符合真实社交规律。

8.2 日记列表与点赞交互

日记Tab通过 ForEach 渲染 diaries 数组,每篇日记卡片包含:头像+发布者+日期+心情 + 日记内容 + 评论数+点赞按钮 + 爱心点赞。

点赞交互是日记Tab的核心功能:点击爱心时,使用 map 遍历 diaries 数组,找到匹配 id 的日记,用完整对象字面量重建(loved 取反,likes 加减1),返回新数组赋值给 this.diaries。这种不可变更新方式符合 ArkTS 严格模式规范(无 Object.assign,无直接修改原数组)。爱心 scale 根据 d.loved 在 1.2 和 1.0 之间切换,200ms 动画,点赞时心跳加速的视觉效果。

日记排序:新发布的日记插入到 diaries 数组头部([newDiary, …this.diaries]),最新日记在最前,符合社交App的时间线排序习惯。原有8篇日记按日期倒序排列(最新的在最前)。

8.3 写日记功能

写日记弹框包含心情选择和内容输入:

  • 心情选择:6种心情emoji(😊🥰😍🤗😘💕),ForEach 渲染,选中玫瑰粉背景,点击切换 newMood 状态
  • 内容输入:TextArea 多行输入,100vp高度,紫褐背景,onChange 更新 newContent 状态
  • 发布逻辑:校验 newContent.trim() 非空,创建新 DiaryItem(genId生成ID,user固定为’小鹿’,avatar固定为’🦌’,content为输入内容,date为todayStr(),mood为选中的心情,likes为0,loved为false,comments为0),插入到 diaries 数组头部,清空 newContent,关闭弹框

写日记功能让用户可以记录自己的恋爱日常,心情emoji选择增加了记录的趣味性。当前发布者固定为小鹿🦌,可扩展为切换发布者(他/她),实现情侣双视角记录。

8.4 纪念日数据体系

项目定义 4 个纪念日(ANNIVERSARIES 常量),每个包含:id、title(标题)、date(日期)、type(类型:在一起/生日/纪念日)、icon(emoji图标)、days(距今天数,初始化时调用daysSince计算)。

4 个纪念日覆盖情侣间的重要日子:

  • 在一起的第一天(2024-12-01,在一起,💑)
  • 小鹿的生日(2025-03-10,生日,🎂)
  • 小熊的生日(2025-07-22,生日,🎂)
  • 100天纪念日(2025-03-11,纪念日,🎉)

每个纪念日的 days 属性在数据初始化时调用 daysSince(date) 计算,显示为"X天前"。在一起的第一天的天数同时显示在标题栏的"❤️在一起X天"标签中,实时计算(daysSince(‘2024-12-01’)),每次打开页面都显示最新天数。

8.5 纪念日天数计算

daysSince 纯函数是纪念日系统的核心计算逻辑:

function daysSince(dateStr: string): number {
  const now = new Date();
  const d = new Date(dateStr);
  const diff = now.getTime() - d.getTime();
  return Math.floor(diff / (1000 * 60 * 60 * 24));
}

基于 Date 对象的 getTime() 差值计算毫秒数,除以 (1000 * 60 * 60 * 24) 转换为天数,Math.floor 取整。这个函数简单高效,无副作用,相同输入永远返回相同输出(在同一天内)。

formatDate 纯函数格式化 Date 对象为 YYYY-MM-DD 字符串:

function formatDate(d: Date): string {
  return d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0');
}

使用 padStart(2, ‘0’) 补零,确保月份和日期都是两位数,格式统一为 YYYY-MM-DD。todayStr 纯函数调用 formatDate(new Date()) 获取今日日期字符串,用于新日记和新纪念日的默认日期。

8.6 添加纪念日功能

添加纪念日弹框包含名称、日期、类型三个输入:

  • 名称输入:TextInput(例如:第一次约会),onChange 更新 newTitle
  • 日期输入:TextInput(YYYY-MM-DD格式,默认todayStr()),onChange 更新 newDate
  • 类型选择:3种类型chips(在一起/生日/纪念日),ForEach 渲染,选中玫瑰粉背景,点击切换 newType
  • 添加逻辑:校验 newTitle.trim() 和 newDate 非空,创建新 AnniversaryItem(genId生成ID,图标根据类型自动选择:在一起💑/生日🎂/纪念日🎉,days调用daysSince(newDate)计算),追加到 anniversaries 数组尾部,清空 newTitle,重置 newDate 为 todayStr(),关闭弹框

添加纪念日功能让用户可以记录自己的重要日子,类型chips选择和图标自动匹配增加了便捷性。日期输入当前为文本输入(YYYY-MM-DD格式),可扩展为日期选择器组件,提升用户体验。


第九章 心愿与互动系统深度解析

9.1 心愿数据体系

项目定义 8 个心愿(WISHES 常量),每个包含:id、name(心愿名称)、desc(描述)、done(是否已完成)、createdBy(发起者:他/她)、date(创建日期)。

8 个心愿由他和她轮流发起,内容浪漫温馨:

  • 去北海道看雪(她,未完成,一起在雪地里散步)
  • 养一只猫(他,未完成,领养一只橘猫)
  • 去迪士尼看烟花(她,已完成✅,穿着情侣装)
  • 学做一道对方喜欢的菜(他,未完成,想学做红烧肉)
  • 一起看一场流星雨(她,未完成,许个愿)
  • 拍一组情侣写真(他,未完成,记录我们的美好)
  • 去海边露营(她,未完成,听着海浪入睡)
  • 一起写一本恋爱日记(他,未完成,轮流记录每天的小事)

8个心愿中只有1个已完成(去迪士尼看烟花✅),已实现率12.5%,符合真实情侣心愿清单的进度(大部分心愿还在规划中)。心愿内容覆盖旅行、宠物、美食、娱乐、写真、露营、记录等多个方面,体现情侣对未来的共同规划。

9.2 心愿勾选与已实现统计

心愿Tab的核心交互是勾选完成和已实现统计:

  • 勾选完成:每个心愿的勾选框(⬜未完成/✅已完成)点击时,使用 map 遍历 wishes 数组,找到匹配 id 的心心愿,用完整对象字面量重建(done 取反),返回新数组赋值给 this.wishes。已完成的心心愿名称添加删除线(TextDecorationType.LineThrough),勾选框变为✅。
  • 已实现统计:标题栏的"已实现X/Y"实时统计已完成数量,使用 filter(w => w.done).length 计算。每次勾选/取消勾选时统计自动更新。

勾选完成让"一起实现一个愿望"变得有迹可循,已实现统计让情侣看到共同完成的进度,增加恋爱的目标感和成就感。已完成心愿的删除线设计(TextDecorationType.LineThrough)直观区分已完成和未完成,如同真实清单上的划线。

9.3 添加心愿功能

添加心愿弹框包含名称、描述、发起者三个输入:

  • 名称输入:TextInput(例如:一起去旅行),onChange 更新 newWishName
  • 描述输入:TextInput(简单描述),onChange 更新 newWishDesc
  • 发起者选择:2种发起者chips(他/她),ForEach 渲染,选中玫瑰粉背景,点击切换 newWishBy
  • 添加逻辑:校验 newWishName.trim() 非空,创建新 WishItem(genId生成ID,desc默认为"暂无描述",done为false,date为todayStr()),追加到 wishes 数组尾部,清空 newWishName 和 newWishDesc,关闭弹框

添加心愿功能让情侣可以共同规划未来,发起者选择(他/她)体现情侣双视角。描述字段可选,默认为"暂无描述",降低添加门槛。

9.4 互动任务数据体系

项目定义 6 个互动任务(TASKS 常量),每个包含:id、text(任务内容)、done(是否已完成)、partner(发起者:他/她)。

6 个互动任务设计为简单可完成的日常小任务:

  • 给对方一个拥抱 🤗(他,未完成)
  • 说一句"我爱你" 💕(她,未完成)
  • 一起做一顿晚餐 🍳(他,未完成)
  • 给对方一个惊喜 🎁(她,未完成)
  • 一起看一部电影 🎬(他,未完成)
  • 写一封情书 💌(她,未完成)

6个任务全部未完成,作为"今日任务"等待情侣完成。任务内容简单日常,不需要特殊准备,随时可以完成。emoji图标增加了任务的趣味性和视觉识别度。发起者(他/她)轮流分配,体现情侣双方的互动。

9.5 互动任务勾选

互动Tab的核心交互是勾选完成,与心愿Tab相同:

  • 勾选完成:每个任务的勾选框(⬜未完成/✅已完成)点击时,map 重建数组(done取反),已完成任务名称添加删除线。
  • 提示语:页面顶部显示"💡完成互动任务,让感情升温吧!",鼓励情侣完成任务。

互动任务勾选让情侣间的日常互动变得有计划、有记录,简单的任务(拥抱、说我爱你、一起做饭)容易完成,完成后的勾选带来成就感。6个任务作为"今日任务",每天可以重置或添加新任务,增加恋爱的新鲜感和仪式感。

9.6 添加互动任务功能

添加互动任务弹框包含内容、发起者两个输入(比添加心愿更简洁,无描述字段):

  • 内容输入:TextInput(例如:给对方一个拥抱),onChange 更新 newTaskText
  • 发起者选择:2种发起者chips(他/她),选中玫瑰粉背景,点击切换 newTaskPartner
  • 添加逻辑:校验 newTaskText.trim() 非空,创建新 TaskItem(genId生成ID,done为false),追加到 tasks 数组尾部,清空 newTaskText,关闭弹框

添加互动任务功能让情侣可以自定义日常互动任务,发起者选择体现情侣双视角。任务内容简洁,不需要描述字段,降低添加门槛。


第十章 相册与我的系统深度解析

10.1 相册数据体系

项目定义 8 张照片(PHOTOS 常量),每张包含:id、title(标题)、emoji(emoji占位照片)、date(日期)。

8 张照片覆盖恋爱中的重要时刻,按时间顺序排列:

  • 第一次约会 📸(2024-12-01)
  • 跨年夜 🎆(2024-12-31)
  • 情人节 🌹(2025-02-14)
  • 春游赏花 🌸(2025-03-20)
  • 夏日海边 🌊(2025-07-15)
  • 秋日银杏 🍂(2025-10-25)
  • 冬日初雪 ❄️(2025-12-12)
  • 纪念日晚餐 🍷(2026-01-01)

8张照片覆盖四季(春🌸/夏🌊/秋🍂/冬❄️)和重要节日(跨年夜🎆/情人节🌹/纪念日晚餐🍷),如同真实情侣的回忆相册。emoji占位简洁可爱,符合温暖粉调的整体风格。每张照片有标题和日期,记录恋爱中的美好回忆。

10.2 三列相册宫格

相册Tab使用三列宫格展示8张照片:

  • 布局:Flex({wrap: FlexWrap.Wrap, justifyContent: FlexAlign.SpaceBetween}) 实现三列布局,每卡宽度 31.8%,自动换行
  • 卡片设计:每卡居中对齐,emoji照片(40vp,紫褐背景#4D334F,12vp圆角,16vp内边距,宽度100%居中),标题(11vp中等,宽度100%居中),日期(9vp灰粉)
  • 卡片样式:紫红背景#3D263F,14vp圆角,10vp内边距,10vp顶部间距

三列宫格布局最大化展示照片数量,如同真实相册的照片墙。emoji占位照片虽然简洁,但配合标题和日期,能够唤起对应的回忆。实际生产环境可替换为真实图片(Image组件+网络图片懒加载),提升视觉真实感。

10.3 情侣资料卡

我的Tab顶部是情侣资料卡,是整个应用的核心视觉:

  • 双人头像:她🦌 + 💕 + 他🐻,水平排列,居中对齐,头像48vp紫褐背景圆角24,中间💕20vp
  • 情侣名称:鹿与熊,玫瑰粉#FF6B8A,18vp加粗
  • 在一起天数:❤️在一起X天,13vp浅粉白,COUPLE.days实时计算(初始化时调用daysSince)
  • 情侣简介:从今天起,每一天都是甜的。,11vp灰粉
  • 卡片样式:紫红背景#3D263F,14vp圆角,14vp内边距,内容居中对齐,12vp顶部间距

情侣资料卡是"情侣专属"定位的核心体现,双人头像+💕连接,一眼就能看出这是两个人的应用。在一起天数实时计算,让"在一起多久"变成看得见的数字。情侣简介温暖甜蜜,奠定整个应用的情感基调。

10.4 三项统计卡

我的Tab的三项统计卡展示情侣的记录数据:

  • 日记:48篇,玫瑰粉#FF6B8A,16vp加粗
  • 纪念日:12个,浅粉#FF9EB5,16vp加粗
  • 心愿:23个,浅粉白#F5E6F0,16vp加粗
  • 布局:Row水平排列,每项layoutWeight(1)居中对齐,数字+标签(9vp灰粉)
  • 卡片样式:紫红背景#3D263F,14vp圆角,14vp内边距,12vp顶部间距

三项统计卡让情侣看到自己的记录数据,增加使用成就感。数字分别使用玫瑰粉(日记,主强调)、浅粉(纪念日,辅助)、浅粉白(心愿,中性),形成视觉层次。当前统计数为静态数据(48/12/23),可扩展为实时统计(diaries.length/anniversaries.length/wishes.length)。

10.5 设置菜单卡

我的Tab的设置菜单卡包含4个设置项:

  • 编辑资料 👤
  • 消息通知 🔔
  • 深色模式 🌙
  • 关于恋爱日记 💌
  • 布局:每项Row水平排列,图标+名称(13vp浅粉白)+ >箭头(灰粉),分割线(chip色#4D334F)分隔
  • 卡片样式:紫红背景#3D263F,14vp圆角,14vp内边距,12vp顶部间距

设置菜单当前为静态展示(点击无响应),可扩展为实际设置功能。深色模式🌙项与应用当前的深紫红暖调主题呼应,应用本身就是深色模式设计。关于恋爱日记💌项可展示应用版本、开发者信息等。

10.6 底部标语

我的Tab底部显示温暖标语:“— 愿每一对情侣都甜甜蜜蜜 —”,10vp灰粉,宽度94%居中对齐,14vp顶部间距,16vp底部间距。这个标语为整个应用画上温暖的句号,传递美好的祝愿,符合恋爱日记的情感定位。


第十一章 性能优化与工程实践

11.1 单文件架构的性能考量

恋爱日记采用单文件架构(约 1000 行),所有组件、数据、函数在一个 LoveApp.ets 文件中。单文件架构的性能优势:无跨文件导入开销、编译时类型检查更高效、运行时模块加载更快。对于中等规模应用(1000行以内),单文件架构是合理选择。

11.2 @State 状态最小化原则

项目遵循"状态最小化"原则:

  • 全局数据用 const 常量(DIARIES/ANNIVERSARIES/WISHES/TASKS/PHOTOS/COUPLE 等),不进入响应式系统
  • 组件内交互状态用 @State(diaries/showAdd/newContent/newMood/anniversaries/wishes/tasks 等),仅在需要时声明
  • 根组件仅管理 activeTab 一个跨组件状态

状态最小化减少响应式系统的依赖追踪开销,提升渲染性能。每个 @State 变化时仅触发依赖该状态的组件重新渲染,不影响其他组件。diaries 状态在点赞/添加时更新,触发日记列表重新渲染;wishes 状态在勾选/添加时更新,触发心愿列表重新渲染;tasks 状态在勾选/添加时更新,触发任务列表重新渲染。

11.3 ForEach 键值优化

项目所有 ForEach 均提供键值生成函数(第二个参数),如 (d: DiaryItem) => d.id.toString()(a: AnniversaryItem) => a.id.toString()(w: WishItem) => w.id.toString()。键值生成函数确保列表项在数据变化时被正确复用,而非全量重建,大幅提升列表渲染性能。

ForEach 键值优化在列表数据变化时(如日记点赞、心愿勾选、添加新项)尤为重要:框架通过键值识别哪些项变化、哪些项不变,仅重新渲染变化的项,不变的项直接复用。日记列表(8篇)、纪念日列表(4个)、心愿列表(8个)、任务列表(6个)、相册(8张)均使用键值优化。

11.4 不可变状态更新

项目严格遵循不可变状态更新原则:

  • 数组更新:使用 map/filter/展开运算符返回新数组,不直接使用 push/pop/splice 修改原数组
    • 点赞:this.diaries = this.diaries.map(...) 返回新数组
    • 添加:this.diaries = [newDiary, ...this.diaries] 返回新数组
    • 勾选:this.wishes = this.wishes.map(...) 返回新数组
  • 对象更新:使用完整对象字面量重建,不使用 Object.assign 或直接修改属性
    • 日记点赞:{ id: item.id, user: item.user, ..., loved: !item.loved } 完整重建
    • 心愿勾选:{ id: item.id, name: item.name, ..., done: !item.done } 完整重建

不可变状态更新是 ArkTS 严格模式的核心要求,也是 React/Vue 等现代框架的最佳实践。不可变更新让状态变化可追踪、可调试、可回滚,避免直接修改原状态导致的难以排查的bug。完整对象字面量重建虽然代码稍长,但类型安全,无运行时错误。

11.5 ArkTS 严格模式合规

本项目严格遵循 ArkTS 严格模式规范:

  • 无 Object.assign:所有对象更新使用对象字面量显式重建
  • 显式类型标注:所有变量、函数参数、函数返回值均显式类型标注
    • 函数:function daysSince(dateStr: string): number
    • 变量:@State diaries: DiaryItem[] = DIARIES
    • map回调:(item: DiaryItem): DiaryItem => { ... }
  • 分号结尾:所有语句以分号结尾,如 this.activeTab = tab;
  • 无 any 类型:所有类型均为具体类型,无 any 逃逸
  • 无隐式类型转换:所有类型转换显式进行

ArkTS 严格模式合规确保代码在 ArkTS 严格模式下可编译运行,类型安全,无运行时类型错误。显式类型标注虽然增加了代码量,但提升了代码的可读性和可维护性,IDE 可以提供更好的代码提示和类型检查。

11.6 属性动画性能

项目 2 组交互动画全部使用属性动画(.animation()),属性动画由框架自动调度,性能优于手动逐帧动画。属性动画仅在依赖的状态变化时触发,不持续占用 CPU。动画时长控制在 200ms,符合移动端交互动画的最佳时长范围(200-500ms)。

Tab图标缩放 200ms EaseOut(适中选中反馈)、日记点赞缩放 200ms(适中点赞反馈),不同动画时长匹配不同交互场景。200ms是移动端交互动画的黄金时长,既不过快显得突兀,也不过慢显得拖沓。

11.7 Flex 布局性能

项目大量使用 Flex 布局(双列/三列宫格 FlexWrap.Wrap、Tab栏 FlexAlign.Center、统计栏 layoutWeight),Flex 布局是 ArkTS 推荐的高性能布局方式,框架对 Flex 布局有专门优化。避免使用复杂的 position 绝对定位(仅在弹框等必要场景使用 Stack 叠加),减少布局计算开销。

11.8 图片占位与懒加载

相册使用 emoji 占位(非真实图片加载),避免网络图片加载的性能开销。对于概念应用,emoji 占位是合理选择,既展示布局又保证性能。实际生产环境可替换为 LazyForEach + 网络图片懒加载。

11.9 代码组织规范

项目代码组织遵循以下规范:

  • 调色板接口和常量在前(LovePalette interface + SP const)
  • 业务接口定义次之(7个interface)
  • 静态选项数组再次之(3组const)
  • 辅助函数再次之(4个function)
  • 业务数据常量再次之(6组const)
  • Tab枚举次之(LoveTab enum)
  • 根入口组件最后(LoveApp @Entry)
  • 子组件按Tab顺序排列(DiaryContent→AnniversaryContent→WishContent→TaskContent→PhotoContent→MineContent)

这种"定义→配置→函数→数据→组件"的组织方式符合阅读习惯,便于维护和扩展。


第十二章 情侣App文化与恋爱记录趋势

12.1 情侣App的兴起

情侣App是近年来移动互联网的新兴品类,它将社交、记录、工具等功能融合,专为恋爱中的情侣设计。情侣App的核心魅力:专属二人空间、共同记录回忆、互动增进感情、私密安全。Z世代是情侣App的核心用户群体,他们追求"专属感"和"仪式感",不愿意在大众社交平台上展示恋爱日常,更倾向于使用专属的情侣App。恋爱日记正是瞄准这一市场,以温暖粉调的视觉语言和"日记+纪念日+心愿+互动+相册+我的"六位一体的功能,为情侣提供专属的恋爱记录空间。

12.2 恋爱日记的仪式感

日记是恋爱记录的核心功能,它让日常的小瞬间变得有迹可循。恋爱日记的日记功能设计了心情emoji选择(😊🥰😍🤗😘💕),让每篇日记都有心情标签;点赞功能让对方可以表达喜爱;评论数展示互动热度。日记内容由双方轮流发布(小鹿🦌/小熊🐻),如同两人共同书写的恋爱故事。

仪式感是恋爱日记的核心价值:写一篇日记、选一个心情emoji、对方点一个赞——这些简单的操作让日常的小瞬间变得有意义。“今天一起去看了樱花,你说我比花还好看”、“第一次一起看日出,在海边许愿永远在一起”——这些文字记录的不仅是事件,更是当时的心情和感动。多年后回看,这些日记就是最珍贵的回忆。

12.3 纪念日倒计时的情感价值

纪念日是情侣App的标配功能,它让重要的日子变得可视化。恋爱日记的纪念日功能设计了天数倒计时(X天前)和在一起天数实时计算(❤️在一起X天),让"在一起多久"变成看得见的数字。

纪念日倒计时的情感价值:

  • 成就感:在一起天数每天增加,如同恋爱的"经验值",让情侣看到共同走过的时光
  • 期待感:即将到来的纪念日(生日、周年)可以倒计时,增加期待感
  • 仪式感:纪念日当天的特别记录(如第100天的烛光晚餐日记),让这一天变得与众不同
  • 归属感:"我们在一起X天"这个数字,是情侣关系的具象化,增强归属感

恋爱日记的纪念日数据包含在一起的第一天、双方生日、100天纪念日,覆盖了情侣间最重要的日子。在一起天数同时显示在标题栏和我的页面,处处可见,增强归属感。

12.4 心愿清单的共同规划

心愿清单是情侣共同规划未来的功能,它让"想一起做的事"变得有计划、有追踪。恋爱日记的心愿功能设计了勾选完成(⬜/✅)、已实现统计(已实现X/Y)、发起者标注(他/她),让共同规划变得可追踪。

心愿清单的价值:

  • 目标感:一起列出想做的事,让未来有方向
  • 成就感:完成一个心愿后勾选,看到已实现数量增加,带来成就感
  • 沟通:心愿清单是情侣沟通的契机,了解对方想做什么
  • 回忆:已完成的心愿是美好的回忆,未完成的心愿是未来的期待

恋爱日记的8个心愿覆盖旅行、宠物、美食、娱乐、写真、露营、记录等多个方面,体现情侣对未来的丰富规划。已完成的"去迪士尼看烟花"✅是美好的回忆,未完成的"去北海道看雪"是未来的期待。

12.5 情侣互动任务的新鲜感

情侣互动任务是增进感情的功能,它让日常互动变得有计划、有记录。恋爱日记的互动功能设计了简单可完成的日常任务(拥抱、说我爱你、一起做饭、给惊喜、看电影、写情书),勾选完成,让情侣间的互动变得有迹可循。

互动任务的价值:

  • 新鲜感:每天完成一个小任务,为日常增添新鲜感
  • 仪式感:简单的拥抱、说我爱你,因为有了任务的形式而变得更有仪式感
  • 沟通:互动任务是情侣沟通的契机,了解对方的需求
  • 成就感:完成任务后勾选,带来小小的成就感

恋爱日记的6个互动任务全部简单日常,不需要特殊准备,随时可以完成。这种"低门槛高频率"的设计,让情侣更容易坚持完成,持续增进感情。

12.6 回忆相册的珍藏价值

相册是珍藏回忆的功能,它让美好的瞬间变得可视化。恋爱日记的相册功能设计了三列宫格布局,每张照片有标题和日期,记录恋爱中的重要时刻(第一次约会、跨年夜、情人节、四季风景、纪念日晚餐)。

相册的珍藏价值:

  • 回忆:照片是回忆的载体,回看照片能唤起当时的心情和感动
  • 时间线:按日期排列的照片,如同恋爱的时间线,看到两人一起走过的路
  • 分享:相册可以分享给对方,共同回忆美好时光
  • 仪式感:重要时刻的照片(第一次约会、纪念日),让这些时刻变得更有仪式感

恋爱日记的8张照片覆盖四季和重要节日,如同真实情侣的回忆相册。emoji占位虽然简洁,但配合标题和日期,能够唤起对应的回忆。

12.7 情侣双视角设计

恋爱日记的核心设计理念是"情侣双视角",处处体现"两个人":

  • 日记发布者:每条日记标注发布者(小鹿🦌/小熊🐻),让每篇日记有明确的归属
  • 心愿发起者:每个心愿标注发起者(他/她),让共同规划有明确的提议方
  • 任务发起者:每个互动任务标注发起者(他/她),让情侣互动有明确的发起方
  • 双人头像:我的页面展示双人头像(她🦌+💕+他🐻),体现"两个人"的专属感
  • 情侣名称:"鹿与熊"是两人的合称,体现情侣的专属身份

情侣双视角设计让每一条记录都有明确的归属,增加了情侣间的互动和仪式感。用户可以清晰地看到"这是她写的日记"、“这是他发起的心愿”,如同两人共同书写的恋爱故事。这种设计区别于单人日记App,是情侣App的核心特色。

12.8 温暖粉调的情感传达

恋爱日记的色彩体系(深紫红#2D1B2E底 × 玫瑰粉#FF6B8A主 × 浅粉#FF9EB5辅)传达了温暖、甜蜜、浪漫的情感:

  • 深紫红背景:私密、温馨、浪漫,如同深夜里两人依偎的氛围
  • 玫瑰粉主色:温暖、甜蜜、爱情,如同恋爱中的心跳
  • 浅粉辅助色:柔和、温馨、甜蜜,如同恋爱中的温柔

温暖粉调是恋爱类App的经典配色,它直观地传达了"爱与温暖"的情感。深紫红背景营造私密感,玫瑰粉和浅粉在深紫红上柔和而温暖,如同深夜里的烛光。这种配色让用户一打开应用就能感受到温暖甜蜜的氛围,符合恋爱日记的情感定位。


第十三章 温暖粉调生活方式与恋爱仪式感

13.1 恋爱日记作为生活方式

恋爱日记不仅是一个工具App,更是一种生活方式的倡导。它鼓励情侣记录日常、规划未来、互动增进感情、珍藏回忆。写一篇日记、完成一个互动任务、勾选一个心愿、添加一个纪念日——这些简单的操作,让恋爱从"顺其自然"变成"用心经营"。

生活方式的核心是"仪式感",而恋爱日记的每一个功能都在为恋爱增添仪式感:心情emoji选择让每篇日记有情绪标签、在一起天数让时间变得可视化、心愿勾选让目标变得可追踪、互动任务让日常变得有计划、相册让回忆变得可珍藏。这些仪式感让平凡的日常变得不平凡,让恋爱更加甜蜜和持久。

13.2 深紫红与玫瑰粉的色彩心理学

恋爱日记的配色体系具有深层的色彩心理学考量:

  • 深紫红 #2D1B2E:神秘、私密、浪漫、高级,营造深夜依偎的氛围,让用户进入私密的二人世界
  • 玫瑰粉 #FF6B8A:温暖、甜蜜、爱情、心跳,作为主强调色激发爱的情感,如同恋爱中的心跳加速
  • 浅粉 #FF9EB5:柔和、温馨、温柔、甜蜜,作为辅助色传递温柔的情感,如同恋爱中的温柔体贴

深紫红+玫瑰粉的配色组合如同深夜里的温暖烛光,既私密又温暖,既浪漫又甜蜜。深紫红背景让用户进入专注的记录状态,玫瑰粉强调色激发爱的情感,浅粉辅助色增添温柔感。三色搭配层次分明,符合恋爱日记的情感定位。

13.3 心跳缩放与情感反馈

心跳缩放动画是恋爱日记的特色交互,它模拟了恋爱中的心跳感:

  • Tab图标缩放:切换Tab时图标放大1.15倍,如同切换到不同恋爱场景时的心情起伏
  • 日记点赞缩放:点赞时爱心放大1.2倍,如同看到对方日记时的心跳加速

心跳缩放动画虽然微小,但对情感反馈有显著提升:

  • 真实感:心跳缩放模拟了真实的心跳感,让交互更有情感温度
  • 满足感:点赞时的心跳放大带来满足感,如同对方回应了你的心意
  • 仪式感:每次点赞都有心跳反馈,让简单的操作变得有仪式感
  • 情感连接:心跳缩放让用户感受到"对方也在心动",增强情感连接

心跳缩放动画是恋爱日记的灵魂交互,它让静态的界面有了"心跳",如同恋爱中的心动瞬间。200ms的动画时长适中,既不过快显得突兀,也不过慢显得拖沓,如同真实的心跳节奏。

13.4 在一起天数与时间感知

在一起天数是恋爱日记最具情感价值的功能之一,它让抽象的时间变得具象化:

  • 成就感:在一起天数每天增加,如同恋爱的"经验值",让情侣看到共同走过的时光
  • 归属感:"我们在一起X天"这个数字,是情侣关系的具象化,增强归属感
  • 仪式感:整数天数(100天、365天、1000天)成为特别的纪念日,增加仪式感
  • 珍惜感:看到在一起的天数,让情侣更加珍惜彼此和当下的时光

在一起天数同时显示在纪念日标题栏和我的页面,处处可见,增强归属感。daysSince纯函数实时计算,每次打开应用都显示最新天数,让时间的流逝变得可见。这种"时间可视化"设计,让情侣更加珍惜在一起的每一天。

13.5 心愿清单与未来规划

心愿清单是情侣共同规划未来的功能,它让"想一起做的事"从口头说说变成书面计划:

  • 目标感:一起列出想做的事,让未来有方向和期待
  • 沟通契机:心愿清单是情侣沟通的契机,了解对方的梦想和期待
  • 成就感:完成一个心愿后勾选,看到已实现数量增加,带来成就感
  • 回忆价值:已完成的心愿是美好的回忆,记录了一起实现梦想的过程

恋爱日记的8个心愿覆盖旅行、宠物、美食、娱乐、写真、露营、记录等多个方面,体现情侣对未来的丰富规划。已完成的"去迪士尼看烟花"✅是美好的回忆,未完成的"去北海道看雪"是未来的期待。这种"已完成+未完成"的混合,让心愿清单既有成就感又有期待感。

13.6 互动任务与日常新鲜感

互动任务是为恋爱日常增添新鲜感的功能,它让简单的日常互动变得有计划、有记录:

  • 新鲜感:每天完成一个小任务,为平淡的日常增添新鲜感
  • 仪式感:简单的拥抱、说我爱你,因为有了任务的形式而变得更有仪式感
  • 沟通契机:互动任务是情侣沟通的契机,了解对方的需求和期待
  • 低门槛高频率:任务简单日常,不需要特殊准备,随时可以完成,容易坚持

恋爱日记的6个互动任务(拥抱、说我爱你、一起做饭、给惊喜、看电影、写情书)全部简单日常,低门槛高频率。这种设计让情侣更容易坚持完成,持续增进感情。完成后的勾选带来小小的成就感,让日常互动变得有迹可循。

13.7 情侣双视角与共同书写

情侣双视角是恋爱日记的核心设计理念,它让恋爱记录变成"两人共同书写的故事":

  • 归属感:每条记录有明确的归属(他/她),让用户感受到"这是我们的故事"
  • 互动感:看到对方写的日记、发起的心愿,增加情侣间的互动
  • 完整感:双方的记录合在一起,才是完整的恋爱故事
  • 专属感:双人头像、情侣名称、在一起天数,处处体现"两个人"的专属感

情侣双视角设计区别于单人日记App,是情侣App的核心特色。它让恋爱记录从"一个人的独白"变成"两个人的对话",增加了互动性和仪式感。用户可以清晰地看到"这是她写的日记"、“这是他发起的心愿”,如同两人共同书写的恋爱故事。

13.8 温暖粉调的情感疗愈

温暖粉调配色不仅是视觉选择,更具有情感疗愈的作用:

  • 安全感:深紫红背景营造私密、安全的氛围,让用户可以放心地记录内心感受
  • 温暖感:玫瑰粉和浅粉传递温暖、甜蜜的情感,让用户感受到被爱和被关心
  • 治愈感:温暖的色彩有心理疗愈作用,能够缓解压力和焦虑
  • 积极感:粉色调传递积极、乐观的情感,让用户对恋爱和生活充满希望

温暖粉调的情感疗愈作用,让恋爱日记不仅是一个记录工具,更是一个情感空间。用户在这里记录甜蜜、规划未来、珍藏回忆,同时也被温暖的色彩所治愈。这种"工具+疗愈"的双重价值,让恋爱日记成为情侣生活中不可或缺的一部分。


第十四章 开发环境搭建与运行指南

本章为安装软件步骤,按要求放在最后一章。

14.1 开发环境概述

恋爱日记基于 HarmonyOS ArkTS 声明式 UI(ArkUI)开发,API 9+,Stage 模型,单文件 .ets,严格遵循 ArkTS 严格模式规范(无 Object.assign,显式类型标注,分号结尾)。开发环境需要:DevEco Studio(HarmonyOS 官方 IDE)、HarmonyOS SDK(API 9+)、Node.js(构建工具依赖)、HarmonyOS 设备或模拟器(运行调试)。

14.2 安装 DevEco Studio

DevEco Studio 是华为官方推出的 HarmonyOS 应用开发 IDE,基于 IntelliJ IDEA 社区版构建,提供 ArkTS/ArkUI 代码编辑、预览、调试、打包等完整开发能力。

安装步骤

  1. 访问华为开发者联盟官网(developer.huawei.com),进入"开发"→"DevEco Studio"下载页面
  2. 选择对应操作系统版本(Windows/macOS)下载安装包
  3. Windows 版运行 .exe 安装程序,按向导完成安装(建议安装路径不含中文和空格)
  4. macOS 版打开 .dmg 镜像,将 DevEco Studio 拖入 Applications 文件夹
  5. 首次启动 DevEco Studio,选择不导入设置,进入欢迎界面
  6. 配置 Node.js 路径(DevEco Studio 内置 Node.js,也可使用本地安装的 Node.js 16+)
  7. 配置 npm 镜像(建议使用华为云 npm 镜像或淘宝镜像,加速依赖下载)

安装DevEco Studio

14.3 配置 HarmonyOS SDK

DevEco Studio 安装完成后,需要配置 HarmonyOS SDK(API 9+)。SDK 包含 ArkTS 编译器、ArkUI 组件库、系统 API、模拟器镜像等。

配置步骤

  1. 打开 DevEco Studio,进入欢迎界面
  2. 点击"Configure"→"Settings"(macOS 为"DevEco Studio"→"Settings")
  3. 左侧导航选择"Appearance & Behavior"→"System Settings"→"HarmonyOS SDK"
  4. 在 SDK Platforms 标签页,勾选 API 9 或更高版本(建议 API 9,兼容性最好)
  5. 在 SDK Tools 标签页,勾选"ArkTS"、“ArkUI”、"Previewer"等工具
  6. 点击"Apply"开始下载安装 SDK(下载大小约 2-4GB,需耐心等待)
  7. 安装完成后,SDK 路径会自动配置,可在 Settings 中查看和修改

SDK 版本说明

  • API 9:首个支持 Stage 模型和 ArkTS 声明式 UI 的稳定版本,兼容性最好
  • API 10+:新增更多 ArkUI 组件和 API,功能更丰富,但兼容性需考虑
  • 建议使用 API 9 开发,确保在更多设备上运行

配置HarmonyOS SDK

14.4 创建 HarmonyOS 项目

SDK 配置完成后,创建一个新的 HarmonyOS 项目。

创建步骤

  1. 打开 DevEco Studio,进入欢迎界面
  2. 点击"Create Project"创建新项目
  3. 选择项目模板:选择"Empty Ability"(空能力模板),这是最基础的 ArkTS 项目模板
  4. 配置项目信息:
    • Project name:项目名称(如 LoveApp)
    • Bundle name:应用包名(如 com.example.loveapp,需唯一)
    • Save location:项目保存路径(建议不含中文和空格)
    • Compile SDK:选择 API 9 或更高版本
    • Model:选择 Stage 模型(ArkTS 声明式 UI 推荐使用 Stage 模型)
    • Language:选择 ArkTS
  5. 点击"Finish"创建项目,DevEco Studio 会自动生成项目结构和基础代码
  6. 等待项目同步完成(Gradle 同步,首次同步可能需要几分钟下载依赖)

项目结构说明

  • entry/src/main/ets/:ArkTS 源代码目录
  • entry/src/main/ets/entryability/:Ability 入口文件
  • entry/src/main/ets/pages/:页面文件目录
  • entry/src/main/resources/:资源文件目录(图片、字符串、主题等)
  • entry/src/main/module.json5:模块配置文件
  • build-profile.json5:构建配置文件

创建项目后,将恋爱日记的完整代码(LoveApp.ets)复制到 entry/src/main/ets/pages/ 目录下,或替换默认的 Index.ets 文件。注意修改 @Entry 装饰器的组件名和 module.json5 中的入口页面配置。

创建HarmonyOS项目

14.5 运行应用

项目创建和代码导入完成后,可以在真机或模拟器上运行应用。

运行步骤

  1. 连接 HarmonyOS 设备(通过 USB 数据线),或启动模拟器
  2. 真机运行需开启开发者模式和 USB 调试:设置→关于手机→连续点击版本号开启开发者模式→设置→系统和更新→开发人员选项→开启 USB 调试
  3. 模拟器运行:点击 DevEco Studio 顶部工具栏的设备选择器,选择"Device Manager"→创建模拟器→选择设备型号和 API 版本→启动模拟器
  4. 在 DevEco Studio 顶部工具栏,确认运行配置为"entry",设备选择为已连接的真机或已启动的模拟器
  5. 点击运行按钮(绿色三角形 ▶)或按快捷键 Shift+F10(macOS 为 Control+R)
  6. DevEco Studio 会自动编译、打包、安装应用到设备,并启动应用
  7. 首次运行可能需要 1-2 分钟编译时间,后续运行会更快(增量编译)

运行注意事项

  • 确保设备已开启 USB 调试,且已授权电脑调试
  • 确保设备系统版本 >= API 9(HarmonyOS 3.0+)
  • 模拟器运行性能可能不如真机,建议最终在真机上测试
  • 如果运行失败,查看 Build 窗口和 Log 窗口的错误信息

恋爱日记的 2 组交互动画(Tab图标缩放、日记点赞缩放)建议在真机上测试,确保动画流畅度和交互反馈符合预期。预览器可以查看静态布局,但动画效果需要在真机或模拟器上运行才能完整体验。

运行应用

14.6 预览与调试

DevEco Studio 提供 ArkUI 预览器和调试工具,方便开发过程中实时查看界面和调试代码。

预览器使用

  1. 打开 .ets 页面文件,右侧会自动显示 Previewer 预览面板
  2. 预览器实时渲染当前页面的 ArkUI 界面,代码修改后自动刷新
  3. 预览器支持多种设备尺寸和方向切换,测试不同屏幕适配
  4. 预览器支持深色/浅色模式切换(恋爱日记为深色主题,建议在深色模式下预览)
  5. 如果预览器未显示,点击右侧边栏的"Previewer"图标开启

调试工具使用

  1. 在代码行号左侧点击设置断点(红色圆点)
  2. 点击调试按钮(绿色虫子 🐛)或按快捷键 Shift+F9(macOS 为 Control+D)
  3. 应用运行到断点处会暂停,可查看变量值、调用栈、表达式等
  4. 使用调试工具栏的单步跳过(F8)、单步进入(F7)、单步跳出(Shift+F8)等操作逐步执行代码
  5. 在 Variables 面板查看和修改变量值,在 Watches 面板添加监视表达式
  6. 在 Log 面板查看应用日志(console.log 输出和系统日志)

常见问题排查

  • 预览器不显示:检查代码是否有语法错误,查看 Previewer 面板的错误提示
  • 运行失败:检查 SDK 版本是否匹配,查看 Build 窗口的编译错误
  • 界面显示异常:检查 ArkUI 组件属性是否正确,使用预览器实时调试
  • 动画不流畅:检查是否有过多同时运行的动画,优化动画时长和曲线
  • ArkTS 严格模式报错:检查是否使用了 Object.assign、是否有隐式类型、是否缺少分号

恋爱日记严格遵循 ArkTS 严格模式规范(无 Object.assign,显式类型标注,分号结尾),在严格模式下可正常编译运行。开发过程中如遇到严格模式报错,可对照规范检查代码。

预览与调试


总结与展望

核心回顾

恋爱日记是一款情侣专属的温暖粉调日记记录应用,基于 HarmonyOS ArkTS 声明式 UI(ArkUI)开发,API 9+,Stage 模型,单文件 .ets,约 1000 行代码,严格遵循 ArkTS 严格模式规范。应用以深紫红 #2D1B2E 为背景,玫瑰粉 #FF6B8A 为主强调色,浅粉 #FF9EB5 为辅助强调色,营造深夜依偎的温暖氛围。

6 个 Tab 覆盖完整恋爱记录链路:甜蜜日记(8篇日记,写日记弹框,心情emoji选择,点赞缩放动画)、纪念日(4个纪念日,添加弹框,在一起天数实时计算)、心愿清单(8个心愿,添加弹框,勾选完成,已实现统计)、情侣互动(6个任务,添加弹框,勾选完成)、回忆相册(8张照片,三列宫格)、我的(情侣资料卡,三项统计,设置菜单)。4 套弹框各具特色:写日记(心情emoji+内容TextArea+发布)、添加纪念日(名称+日期+类型chips)、添加心愿(名称+描述+发起者chips)、添加互动任务(内容+发起者chips)。2 组交互动画:Tab图标缩放(scale 1.15,200ms EaseOut)、日记点赞缩放(scale 1.2,200ms)。4 个全局纯函数:daysSince、formatDate、todayStr、genId。7 个 TypeScript 接口,约 9 组全局数据常量。

设计亮点

  1. 情侣双视角设计:日记发布者、心愿发起者、任务发起者、双人头像,处处体现"两个人"的专属感,区别于单人日记App
  2. ArkTS 严格模式合规:无 Object.assign,所有对象更新使用对象字面量显式重建,所有变量和函数参数显式类型标注,所有语句分号结尾
  3. 心跳缩放动画:Tab图标缩放(1.15倍)和日记点赞缩放(1.2倍),模拟恋爱中的心跳感,让静态界面有了"心跳"
  4. 在一起天数实时计算:daysSince纯函数实时计算,标题栏和我的页面双重展示,让时间变得可视化,增强归属感
  5. 心愿勾选与已实现统计:勾选完成+名称删除线+已实现统计三重反馈,让共同规划变得可追踪,增加目标感和成就感
  6. 温暖粉调配色:深紫红背景+玫瑰粉主色+浅粉辅助色,营造深夜依偎的温暖氛围,传递爱与甜蜜的情感
  7. 单文件零依赖:约1000行代码在一个.ets文件中,复制即运行,无第三方依赖,适合学习和快速原型验证
  8. 不可变状态更新:所有@State数组更新使用map/filter/展开运算符返回新数组,对象更新使用完整对象字面量重建,类型安全无运行时错误

技术价值

恋爱日记展示了 ArkTS 声明式 UI 的完整开发能力,特别是 ArkTS 严格模式的合规实践:@Component/@Entry 组件化架构、@State 状态管理、@Builder 自定义构建函数、ForEach 列表渲染(含键值优化)、属性动画(.animation())、Stack 弹框叠加、Flex 布局、Scroll 滚动、TextInput/TextArea 表单、Divider 分割线、不可变状态更新(map/filter/展开运算符)、显式类型标注、分号结尾。4 个全局纯函数展示了函数式编程在 ArkTS 中的应用,daysSince 日期计算、formatDate 格式化、todayStr 今日日期、genId 随机ID生成。

恋爱日记的 ArkTS 严格模式合规实践具有参考价值:无 Object.assign 的对象更新方式、显式类型标注的代码规范、不可变状态更新的最佳实践,这些都可以应用到其他 ArkTS 严格模式项目中。

未来展望

恋爱日记作为概念应用,未来可扩展方向:

  1. 真实图片替换 emoji:将相册的 emoji 占位替换为真实图片,支持拍照和相册选择,提升视觉真实感
  2. 后端接口对接:将静态数据常量替换为真实后端 API,实现日记云端同步、纪念日提醒、心愿共享、用户系统等真实业务功能
  3. 情侣双向同步:实现情侣双方的数据实时同步,一方写日记、添加心愿、完成任务,另一方实时看到
  4. 纪念日提醒推送:基于纪念日日期实现推送提醒,生日、周年、100天等重要日子提前提醒
  5. 心情统计与情感分析:基于日记的心情emoji和内容,统计情侣的情感趋势,提供情感建议
  6. 心愿进度追踪:为心愿添加进度条、截止日期、分工等,让共同规划更可追踪
  7. 互动任务每日重置:实现互动任务的每日重置和历史记录,让情侣每天都有新任务
  8. 相册时间线:将相册改为时间线布局,按日期分组,增加回忆的叙事感
  9. 发布者切换:写日记时支持切换发布者(他/她),实现真正的情侣双视角记录
  10. 多端适配:适配手机、平板、折叠屏等多种设备,支持横屏和分屏模式

恋爱日记以情侣专属的温暖粉调视觉语言和"日记+纪念日+心愿+互动+相册+我的"六位一体功能,展示了情侣App的创新可能性。它不仅是一个概念应用,更是 ArkTS 严格模式开发的完整教学案例,涵盖组件化架构、状态管理、交互动画、不可变更新、严格模式合规等核心技术点。希望本文能为 HarmonyOS 开发者和情侣App从业者提供有价值的参考。

愿每一对情侣都甜甜蜜蜜。💕

恋爱日记总结


附录:完整 ArkTS 源码


// ============================================================
// LoveApp.ets —— 恋爱日记(情侣专属 · 温暖粉调)
// 适配 ArkTS 严格模式(无 Object.assign,显式类型标注)
// ============================================================

// ---------- 调色板接口 ----------
interface LovePalette {
  bg: string;
  primary: string;
  secondary: string;
  card: string;
  cardDeep: string;
  text: string;
  textSub: string;
  chip: string;
  warn: string;
  white: string;
}

const SP: LovePalette = {
  bg: '#2D1B2E',
  primary: '#FF6B8A',
  secondary: '#FF9EB5',
  card: '#3D263F',
  cardDeep: '#2E1C30',
  text: '#F5E6F0',
  textSub: '#B88FA8',
  chip: '#4D334F',
  warn: '#FF6B8A',
  white: '#FFFFFF'
};

// ---------- 接口定义 ----------
interface DiaryItem {
  id: number;
  user: string;
  avatar: string;
  content: string;
  date: string;
  mood: string;
  likes: number;
  loved: boolean;
  comments: number;
}

interface AnniversaryItem {
  id: number;
  title: string;
  date: string;
  type: string;
  icon: string;
  days: number;
}

interface WishItem {
  id: number;
  name: string;
  desc: string;
  done: boolean;
  createdBy: string;
  date: string;
}

interface TaskItem {
  id: number;
  text: string;
  done: boolean;
  partner: string;
}

interface PhotoItem {
  id: number;
  title: string;
  emoji: string;
  date: string;
}

interface CoupleInfo {
  name: string;
  since: string;
  days: number;
  hisName: string;
  herName: string;
  hisAvatar: string;
  herAvatar: string;
  bio: string;
}

// ---------- 静态选项 ----------
const MOODS: string[] = ['😊', '🥰', '😍', '🤗', '😘', '💕'];
const ANNIVERSARY_TYPES: string[] = ['在一起', '生日', '纪念日'];
const PARTNERS: string[] = ['他', '她'];

// ---------- 辅助函数 ----------
function daysSince(dateStr: string): number {
  const now = new Date();
  const d = new Date(dateStr);
  const diff = now.getTime() - d.getTime();
  return Math.floor(diff / (1000 * 60 * 60 * 24));
}

function formatDate(d: Date): string {
  return d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0');
}

function todayStr(): string {
  return formatDate(new Date());
}

function genId(): number {
  return Math.floor(Math.random() * 1000000);
}

// ---------- 数据 ----------
const DIARIES: DiaryItem[] = [
  { id: 1, user: '小鹿', avatar: '🦌', content: '今天一起去看了樱花,你说我比花还好看。', date: '2026-03-28', mood: '😍', likes: 56, loved: true, comments: 5 },
  { id: 2, user: '小熊', avatar: '🐻', content: '早晨醒来看到你发的消息,一整天都甜甜的。', date: '2026-03-27', mood: '🥰', likes: 43, loved: false, comments: 3 },
  { id: 3, user: '小鹿', avatar: '🦌', content: '你亲手做的提拉米苏,虽然有点苦但心里好甜。', date: '2026-03-25', mood: '😘', likes: 78, loved: true, comments: 8 },
  { id: 4, user: '小熊', avatar: '🐻', content: '第一次一起看日出,在海边许愿永远在一起。', date: '2026-03-22', mood: '💕', likes: 102, loved: false, comments: 12 },
  { id: 5, user: '小鹿', avatar: '🦌', content: '今天下雨了,你把伞都倾向我这边,自己淋湿了半边。', date: '2026-03-20', mood: '🤗', likes: 67, loved: false, comments: 6 },
  { id: 6, user: '小熊', avatar: '🐻', content: '你送的围巾好暖和,是这个冬天最棒的礼物。', date: '2026-03-18', mood: '😊', likes: 34, loved: false, comments: 2 },
  { id: 7, user: '小鹿', avatar: '🦌', content: '今天是我们第100天,晚上去吃了烛光晚餐,好浪漫。', date: '2026-03-15', mood: '😍', likes: 210, loved: true, comments: 20 },
  { id: 8, user: '小熊', avatar: '🐻', content: '晚上视频通话了两个小时,舍不得挂断。', date: '2026-03-12', mood: '💕', likes: 45, loved: false, comments: 4 }
];

const ANNIVERSARIES: AnniversaryItem[] = [
  { id: 1, title: '在一起的第一天', date: '2024-12-01', type: '在一起', icon: '💑', days: daysSince('2024-12-01') },
  { id: 2, title: '小鹿的生日', date: '2025-03-10', type: '生日', icon: '🎂', days: daysSince('2025-03-10') },
  { id: 3, title: '小熊的生日', date: '2025-07-22', type: '生日', icon: '🎂', days: daysSince('2025-07-22') },
  { id: 4, title: '100天纪念日', date: '2025-03-11', type: '纪念日', icon: '🎉', days: daysSince('2025-03-11') }
];

const WISHES: WishItem[] = [
  { id: 1, name: '去北海道看雪', desc: '一起在雪地里散步', done: false, createdBy: '她', date: '2026-01-10' },
  { id: 2, name: '养一只猫', desc: '领养一只橘猫', done: false, createdBy: '他', date: '2026-01-15' },
  { id: 3, name: '去迪士尼看烟花', desc: '穿着情侣装', done: true, createdBy: '她', date: '2026-02-01' },
  { id: 4, name: '学做一道对方喜欢的菜', desc: '想学做红烧肉', done: false, createdBy: '他', date: '2026-02-14' },
  { id: 5, name: '一起看一场流星雨', desc: '许个愿', done: false, createdBy: '她', date: '2026-03-01' },
  { id: 6, name: '拍一组情侣写真', desc: '记录我们的美好', done: false, createdBy: '他', date: '2026-03-20' },
  { id: 7, name: '去海边露营', desc: '听着海浪入睡', done: false, createdBy: '她', date: '2026-04-01' },
  { id: 8, name: '一起写一本恋爱日记', desc: '轮流记录每天的小事', done: false, createdBy: '他', date: '2026-04-10' }
];

const TASKS: TaskItem[] = [
  { id: 1, text: '给对方一个拥抱 🤗', done: false, partner: '他' },
  { id: 2, text: '说一句“我爱你” 💕', done: false, partner: '她' },
  { id: 3, text: '一起做一顿晚餐 🍳', done: false, partner: '他' },
  { id: 4, text: '给对方一个惊喜 🎁', done: false, partner: '她' },
  { id: 5, text: '一起看一部电影 🎬', done: false, partner: '他' },
  { id: 6, text: '写一封情书 💌', done: false, partner: '她' }
];

const PHOTOS: PhotoItem[] = [
  { id: 1, title: '第一次约会', emoji: '📸', date: '2024-12-01' },
  { id: 2, title: '跨年夜', emoji: '🎆', date: '2024-12-31' },
  { id: 3, title: '情人节', emoji: '🌹', date: '2025-02-14' },
  { id: 4, title: '春游赏花', emoji: '🌸', date: '2025-03-20' },
  { id: 5, title: '夏日海边', emoji: '🌊', date: '2025-07-15' },
  { id: 6, title: '秋日银杏', emoji: '🍂', date: '2025-10-25' },
  { id: 7, title: '冬日初雪', emoji: '❄️', date: '2025-12-12' },
  { id: 8, title: '纪念日晚餐', emoji: '🍷', date: '2026-01-01' }
];

const COUPLE: CoupleInfo = {
  name: '鹿与熊',
  since: '2024-12-01',
  days: daysSince('2024-12-01'),
  hisName: '小熊',
  herName: '小鹿',
  hisAvatar: '🐻',
  herAvatar: '🦌',
  bio: '从今天起,每一天都是甜的。'
};

// ---------- Tab 枚举 ----------
enum LoveTab {
  DIARY = 0,
  ANNIVERSARY = 1,
  WISH = 2,
  TASK = 3,
  PHOTO = 4,
  MINE = 5
}

// ---------- 入口 ----------
@Entry @Component struct LoveApp {
  @State activeTab: number = LoveTab.DIARY;

  build() {
    Column() {
      LoveHeader();
      Stack() {
        if (this.activeTab === LoveTab.DIARY) {
          DiaryContent();
        } else if (this.activeTab === LoveTab.ANNIVERSARY) {
          AnniversaryContent();
        } else if (this.activeTab === LoveTab.WISH) {
          WishContent();
        } else if (this.activeTab === LoveTab.TASK) {
          TaskContent();
        } else if (this.activeTab === LoveTab.PHOTO) {
          PhotoContent();
        } else {
          MineContent();
        }
      }
      .layoutWeight(1)
      .width('100%');

      Row() {
        this.tabItem('📖', '日记', LoveTab.DIARY);
        this.tabItem('🎉', '纪念日', LoveTab.ANNIVERSARY);
        this.tabItem('⭐', '心愿', LoveTab.WISH);
        this.tabItem('💞', '互动', LoveTab.TASK);
        this.tabItem('🖼️', '相册', LoveTab.PHOTO);
        this.tabItem('👤', '我的', LoveTab.MINE);
      }
      .width('100%')
      .height(58)
      .backgroundColor(SP.cardDeep)
      .padding({ left: 6, right: 6, top: 4 });
    }
    .width('100%')
    .height('100%')
    .backgroundColor(SP.bg);
  }

  @Builder
  tabItem(icon: string, label: string, tab: number) {
    Column() {
      Text(icon)
        .fontSize(20)
        .scale({
          x: this.activeTab === tab ? 1.15 : 1,
          y: this.activeTab === tab ? 1.15 : 1
        })
        .animation({ duration: 200, curve: Curve.EaseOut });
      Text(label)
        .fontSize(10)
        .fontColor(this.activeTab === tab ? SP.primary : SP.textSub)
        .margin({ top: 1 });
      Divider()
        .strokeWidth(2)
        .color(this.activeTab === tab ? SP.primary : Color.Transparent)
        .width(16)
        .borderRadius(1)
        .margin({ top: 2 });
    }
    .layoutWeight(1)
    .onClick(() => {
      this.activeTab = tab;
    });
  }
}

// ---------- 头部 ----------
@Component struct LoveHeader {
  build() {
    Column() {
      Row() {
        Text('💕')
          .fontSize(24);
        Column() {
          Text('恋爱日记')
            .fontSize(19)
            .fontWeight(FontWeight.Bold)
            .fontColor(SP.primary);
          Text('记录我们的小美好')
            .fontSize(9)
            .fontColor(SP.textSub)
            .margin({ top: 1 });
        }
        .alignItems(HorizontalAlign.Start)
        .margin({ left: 8 })
        .layoutWeight(1);

        Text('🔔')
          .fontSize(19)
          .margin({ right: 14 });
        Text('💌')
          .fontSize(19);
      }
      .width('100%')
      .padding({ left: 16, right: 16, top: 10, bottom: 8 });

      Row() {
        Text('⌕')
          .fontSize(15)
          .fontColor(SP.textSub)
          .margin({ left: 12 });
        Text('搜索日记、心愿...')
          .fontSize(12)
          .fontColor(SP.textSub)
          .margin({ left: 6 });
        Column()
          .layoutWeight(1);
        Text('写日记')
          .fontSize(12)
          .fontColor(SP.cardDeep)
          .backgroundColor(SP.primary)
          .borderRadius(13)
          .padding({ left: 12, right: 12, top: 5, bottom: 5 })
          .margin({ right: 8 })
          .onClick(() => {});
      }
      .width('92%')
      .height(34)
      .backgroundColor(SP.card)
      .borderRadius(17);

      Row() {
        Text('❤️ 今日甜蜜')
          .fontSize(10)
          .fontColor(SP.primary)
          .backgroundColor('#4A2A3A')
          .borderRadius(9)
          .padding({ left: 9, right: 9, top: 4, bottom: 4 });
        Text('🎂 纪念日提醒')
          .fontSize(10)
          .fontColor(SP.secondary)
          .backgroundColor('#3A2A40')
          .borderRadius(9)
          .padding({ left: 9, right: 9, top: 4, bottom: 4 });
        Text('✨ 心愿清单')
          .fontSize(10)
          .fontColor('#FFB3B3')
          .backgroundColor('#4A2A2A')
          .borderRadius(9)
          .padding({ left: 9, right: 9, top: 4, bottom: 4 });
      }
      .width('92%')
      .margin({ top: 10 })
      .justifyContent(FlexAlign.SpaceBetween);
    }
    .width('100%')
    .backgroundColor(SP.cardDeep)
    .borderRadius({ bottomLeft: 16, bottomRight: 16 })
    .padding({ bottom: 12 });
  }
}

// ---------- Tab1 日记 ----------
@Component struct DiaryContent {
  @State diaries: DiaryItem[] = DIARIES;
  @State showAdd: boolean = false;
  @State newContent: string = '';
  @State newMood: string = '😊';

  @Builder
  modalOverlay(onClose: () => void) {
    Column()
      .layoutWeight(1)
      .width('100%')
      .onClick(() => { onClose(); });
  }

  build() {
    Stack() {
      Scroll() {
        Column() {
          Row() {
            Text('📖 甜蜜日记')
              .fontSize(16)
              .fontWeight(FontWeight.Bold)
              .fontColor(SP.text);
            Column().layoutWeight(1);
            Text('共 ' + this.diaries.length + ' 篇')
              .fontSize(10)
              .fontColor(SP.textSub);
          }
          .width('94%')
          .margin({ top: 12 });

          ForEach(this.diaries, (d: DiaryItem) => {
            Column() {
              Row() {
                Text(d.avatar)
                  .fontSize(20)
                  .backgroundColor(SP.chip)
                  .borderRadius(18)
                  .width(38)
                  .height(38)
                  .textAlign(TextAlign.Center);
                Column() {
                  Text(d.user)
                    .fontSize(12)
                    .fontWeight(FontWeight.Medium)
                    .fontColor(SP.text);
                  Text(d.date + '  ' + d.mood)
                    .fontSize(10)
                    .fontColor(SP.textSub)
                    .margin({ top: 2 });
                }
                .alignItems(HorizontalAlign.Start)
                .layoutWeight(1)
                .margin({ left: 10 });
                Text(d.loved ? '❤️' : '🤍')
                  .fontSize(16)
                  .scale({
                    x: d.loved ? 1.2 : 1,
                    y: d.loved ? 1.2 : 1
                  })
                  .animation({ duration: 200 })
                  .onClick(() => {
                    this.diaries = this.diaries.map((item: DiaryItem): DiaryItem => {
                      if (item.id === d.id) {
                        return {
                          id: item.id,
                          user: item.user,
                          avatar: item.avatar,
                          content: item.content,
                          date: item.date,
                          mood: item.mood,
                          likes: item.likes + (item.loved ? -1 : 1),
                          loved: !item.loved,
                          comments: item.comments
                        };
                      }
                      return item;
                    });
                  });
                Text(d.likes + '')
                  .fontSize(10)
                  .fontColor(SP.textSub)
                  .margin({ left: 4 });
              }
              .width('100%');

              Text(d.content)
                .fontSize(11)
                .fontColor(SP.text)
                .width('100%')
                .margin({ top: 8 });

              Row() {
                Text('💬 ' + d.comments + ' 条评论')
                  .fontSize(9)
                  .fontColor(SP.textSub);
                Column().layoutWeight(1);
                Text('点赞')
                  .fontSize(9)
                  .fontColor(SP.primary);
              }
              .width('100%')
              .margin({ top: 8 });
            }
            .width('94%')
            .backgroundColor(SP.card)
            .borderRadius(14)
            .padding(12)
            .margin({ top: 10 });
          });
        }
        .width('100%')
        .padding({ bottom: 16 });
      }
      .scrollable(ScrollDirection.Vertical)
      .width('100%')
      .height('100%');

      if (this.showAdd) {
        this.addDiaryModal();
      }
    }
    .width('100%')
    .height('100%');
  }

  @Builder
  addDiaryModal() {
    Column() {
      this.modalOverlay(() => { this.showAdd = false; });

      Column() {
        Row() {
          Text('📝 写日记')
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .fontColor(SP.text);
          Column().layoutWeight(1);
          Text('✕')
            .fontSize(16)
            .fontColor(SP.textSub)
            .onClick(() => { this.showAdd = false; });
        }
        .width('100%')
        .padding({ bottom: 12 });

        Text('今天的心情')
          .fontSize(11)
          .fontColor(SP.textSub)
          .width('100%');
        Row() {
          ForEach(MOODS, (m: string) => {
            Text(m)
              .fontSize(18)
              .padding(6)
              .backgroundColor(this.newMood === m ? SP.primary : SP.chip)
              .borderRadius(10)
              .margin({ right: 6 })
              .onClick(() => { this.newMood = m; });
          });
        }
        .width('100%')
        .margin({ top: 6 });

        Text('日记内容')
          .fontSize(11)
          .fontColor(SP.textSub)
          .width('100%')
          .margin({ top: 14 });
        TextArea({ placeholder: '记录今天的故事...', text: this.newContent })
          .fontSize(12)
          .fontColor(SP.text)
          .backgroundColor(SP.chip)
          .borderRadius(10)
          .height(100)
          .margin({ top: 6 })
          .onChange((v: string) => { this.newContent = v; });

        Row() {
          Text('取消')
            .fontSize(13)
            .fontColor(SP.textSub)
            .border({ width: 1, color: SP.textSub })
            .borderRadius(16)
            .padding({ left: 20, right: 20, top: 8, bottom: 8 })
            .margin({ top: 16 })
            .onClick(() => { this.showAdd = false; });
          Text('发布')
            .fontSize(13)
            .fontColor(SP.cardDeep)
            .backgroundColor(SP.primary)
            .borderRadius(16)
            .padding({ left: 20, right: 20, top: 8, bottom: 8 })
            .margin({ top: 16, left: 12 })
            .onClick(() => {
              if (this.newContent.trim()) {
                const newDiary: DiaryItem = {
                  id: genId(),
                  user: '小鹿',
                  avatar: '🦌',
                  content: this.newContent,
                  date: todayStr(),
                  mood: this.newMood,
                  likes: 0,
                  loved: false,
                  comments: 0
                };
                this.diaries = [newDiary, ...this.diaries];
                this.newContent = '';
                this.showAdd = false;
              }
            });
        }
        .width('100%')
        .justifyContent(FlexAlign.Center);
      }
      .width('100%')
      .backgroundColor(SP.card)
      .borderRadius({ topLeft: 20, topRight: 20 })
      .padding(18)
      .padding({ bottom: 28 });
    }
    .width('100%')
    .height('100%')
    .backgroundColor('rgba(0,0,0,0.6)')
    .zIndex(999);
  }
}

// ---------- Tab2 纪念日 ----------
@Component struct AnniversaryContent {
  @State anniversaries: AnniversaryItem[] = ANNIVERSARIES;
  @State showAdd: boolean = false;
  @State newTitle: string = '';
  @State newDate: string = todayStr();
  @State newType: string = '在一起';

  @Builder
  modalOverlay(onClose: () => void) {
    Column()
      .layoutWeight(1)
      .width('100%')
      .onClick(() => { onClose(); });
  }

  build() {
    Stack() {
      Scroll() {
        Column() {
          Row() {
            Text('🎉 纪念日')
              .fontSize(16)
              .fontWeight(FontWeight.Bold)
              .fontColor(SP.text);
            Column().layoutWeight(1);
            Text('❤️ 在一起 ' + (daysSince('2024-12-01')) + ' 天')
              .fontSize(12)
              .fontColor(SP.primary)
              .backgroundColor('#4A2A3A')
              .borderRadius(9)
              .padding({ left: 8, right: 8, top: 3, bottom: 3 });
          }
          .width('94%')
          .margin({ top: 12 });

          ForEach(this.anniversaries, (a: AnniversaryItem) => {
            Column() {
              Row() {
                Text(a.icon)
                  .fontSize(28)
                  .margin({ right: 12 });
                Column() {
                  Text(a.title)
                    .fontSize(14)
                    .fontWeight(FontWeight.Bold)
                    .fontColor(SP.text);
                  Text(a.type + ' · ' + a.date)
                    .fontSize(10)
                    .fontColor(SP.textSub)
                    .margin({ top: 3 });
                }
                .alignItems(HorizontalAlign.Start)
                .layoutWeight(1);
                Column() {
                  Text(a.days + '')
                    .fontSize(16)
                    .fontWeight(FontWeight.Bold)
                    .fontColor(SP.primary);
                  Text('天前')
                    .fontSize(9)
                    .fontColor(SP.textSub);
                }
                .alignItems(HorizontalAlign.End);
              }
              .width('100%');
            }
            .width('94%')
            .backgroundColor(SP.card)
            .borderRadius(14)
            .padding(12)
            .margin({ top: 10 });
          });

          Text('➕ 添加纪念日')
            .fontSize(13)
            .fontColor(SP.primary)
            .width('94%')
            .textAlign(TextAlign.Center)
            .padding(12)
            .margin({ top: 12 })
            .onClick(() => { this.showAdd = true; });
        }
        .width('100%')
        .padding({ bottom: 16 });
      }
      .scrollable(ScrollDirection.Vertical)
      .width('100%')
      .height('100%');

      if (this.showAdd) {
        this.addAnniversaryModal();
      }
    }
    .width('100%')
    .height('100%');
  }

  @Builder
  addAnniversaryModal() {
    Column() {
      this.modalOverlay(() => { this.showAdd = false; });

      Column() {
        Row() {
          Text('➕ 添加纪念日')
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .fontColor(SP.text);
          Column().layoutWeight(1);
          Text('✕')
            .fontSize(16)
            .fontColor(SP.textSub)
            .onClick(() => { this.showAdd = false; });
        }
        .width('100%')
        .padding({ bottom: 12 });

        Text('名称')
          .fontSize(11)
          .fontColor(SP.textSub)
          .width('100%');
        TextInput({ placeholder: '例如:第一次约会', text: this.newTitle })
          .fontSize(13)
          .fontColor(SP.text)
          .backgroundColor(SP.chip)
          .borderRadius(10)
          .height(40)
          .margin({ top: 6 })
          .onChange((v: string) => { this.newTitle = v; });

        Text('日期')
          .fontSize(11)
          .fontColor(SP.textSub)
          .width('100%')
          .margin({ top: 14 });
        TextInput({ placeholder: 'YYYY-MM-DD', text: this.newDate })
          .fontSize(13)
          .fontColor(SP.text)
          .backgroundColor(SP.chip)
          .borderRadius(10)
          .height(40)
          .margin({ top: 6 })
          .onChange((v: string) => { this.newDate = v; });

        Text('类型')
          .fontSize(11)
          .fontColor(SP.textSub)
          .width('100%')
          .margin({ top: 14 });
        Row() {
          ForEach(ANNIVERSARY_TYPES, (t: string) => {
            Text(t)
              .fontSize(11)
              .fontColor(this.newType === t ? SP.cardDeep : SP.text)
              .backgroundColor(this.newType === t ? SP.primary : SP.chip)
              .borderRadius(10)
              .padding({ left: 12, right: 12, top: 5, bottom: 5 })
              .margin({ right: 8 })
              .onClick(() => { this.newType = t; });
          });
        }
        .width('100%')
        .margin({ top: 6 });

        Row() {
          Text('取消')
            .fontSize(13)
            .fontColor(SP.textSub)
            .border({ width: 1, color: SP.textSub })
            .borderRadius(16)
            .padding({ left: 20, right: 20, top: 8, bottom: 8 })
            .margin({ top: 16 })
            .onClick(() => { this.showAdd = false; });
          Text('添加')
            .fontSize(13)
            .fontColor(SP.cardDeep)
            .backgroundColor(SP.primary)
            .borderRadius(16)
            .padding({ left: 20, right: 20, top: 8, bottom: 8 })
            .margin({ top: 16, left: 12 })
            .onClick(() => {
              if (this.newTitle.trim() && this.newDate) {
                const newAnn: AnniversaryItem = {
                  id: genId(),
                  title: this.newTitle,
                  date: this.newDate,
                  type: this.newType,
                  icon: this.newType === '在一起' ? '💑' : (this.newType === '生日' ? '🎂' : '🎉'),
                  days: daysSince(this.newDate)
                };
                this.anniversaries = [...this.anniversaries, newAnn];
                this.newTitle = '';
                this.newDate = todayStr();
                this.showAdd = false;
              }
            });
        }
        .width('100%')
        .justifyContent(FlexAlign.Center);
      }
      .width('100%')
      .backgroundColor(SP.card)
      .borderRadius({ topLeft: 20, topRight: 20 })
      .padding(18)
      .padding({ bottom: 28 });
    }
    .width('100%')
    .height('100%')
    .backgroundColor('rgba(0,0,0,0.6)')
    .zIndex(999);
  }
}

// ---------- Tab3 心愿 ----------
@Component struct WishContent {
  @State wishes: WishItem[] = WISHES;
  @State showAdd: boolean = false;
  @State newWishName: string = '';
  @State newWishDesc: string = '';
  @State newWishBy: string = '她';

  @Builder
  modalOverlay(onClose: () => void) {
    Column()
      .layoutWeight(1)
      .width('100%')
      .onClick(() => { onClose(); });
  }

  build() {
    Stack() {
      Scroll() {
        Column() {
          Row() {
            Text('⭐ 心愿清单')
              .fontSize(16)
              .fontWeight(FontWeight.Bold)
              .fontColor(SP.text);
            Column().layoutWeight(1);
            Text('已实现 ' + this.wishes.filter(w => w.done).length + '/' + this.wishes.length)
              .fontSize(10)
              .fontColor(SP.textSub);
          }
          .width('94%')
          .margin({ top: 12 });

          ForEach(this.wishes, (w: WishItem) => {
            Column() {
              Row() {
                Text(w.done ? '✅' : '⬜')
                  .fontSize(20)
                  .margin({ right: 10 })
                  .onClick(() => {
                    this.wishes = this.wishes.map((item: WishItem): WishItem => {
                      if (item.id === w.id) {
                        return {
                          id: item.id,
                          name: item.name,
                          desc: item.desc,
                          done: !item.done,
                          createdBy: item.createdBy,
                          date: item.date
                        };
                      }
                      return item;
                    });
                  });
                Column() {
                  Text(w.name)
                    .fontSize(14)
                    .fontWeight(FontWeight.Bold)
                    .fontColor(SP.text)
                    .decoration({ type: w.done ? TextDecorationType.LineThrough : TextDecorationType.None });
                  Text(w.desc + ' · 发起者 ' + w.createdBy)
                    .fontSize(10)
                    .fontColor(SP.textSub)
                    .margin({ top: 3 });
                }
                .alignItems(HorizontalAlign.Start)
                .layoutWeight(1)
                .margin({ left: 4 });
                Text(w.date)
                  .fontSize(9)
                  .fontColor(SP.textSub);
              }
              .width('100%');
            }
            .width('94%')
            .backgroundColor(SP.card)
            .borderRadius(14)
            .padding(12)
            .margin({ top: 10 });
          });

          Text('➕ 添加心愿')
            .fontSize(13)
            .fontColor(SP.primary)
            .width('94%')
            .textAlign(TextAlign.Center)
            .padding(12)
            .margin({ top: 12 })
            .onClick(() => { this.showAdd = true; });
        }
        .width('100%')
        .padding({ bottom: 16 });
      }
      .scrollable(ScrollDirection.Vertical)
      .width('100%')
      .height('100%');

      if (this.showAdd) {
        this.addWishModal();
      }
    }
    .width('100%')
    .height('100%');
  }

  @Builder
  addWishModal() {
    Column() {
      this.modalOverlay(() => { this.showAdd = false; });

      Column() {
        Row() {
          Text('⭐ 添加心愿')
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .fontColor(SP.text);
          Column().layoutWeight(1);
          Text('✕')
            .fontSize(16)
            .fontColor(SP.textSub)
            .onClick(() => { this.showAdd = false; });
        }
        .width('100%')
        .padding({ bottom: 12 });

        Text('心愿名称')
          .fontSize(11)
          .fontColor(SP.textSub)
          .width('100%');
        TextInput({ placeholder: '例如:一起去旅行', text: this.newWishName })
          .fontSize(13)
          .fontColor(SP.text)
          .backgroundColor(SP.chip)
          .borderRadius(10)
          .height(40)
          .margin({ top: 6 })
          .onChange((v: string) => { this.newWishName = v; });

        Text('描述')
          .fontSize(11)
          .fontColor(SP.textSub)
          .width('100%')
          .margin({ top: 14 });
        TextInput({ placeholder: '简单描述', text: this.newWishDesc })
          .fontSize(13)
          .fontColor(SP.text)
          .backgroundColor(SP.chip)
          .borderRadius(10)
          .height(40)
          .margin({ top: 6 })
          .onChange((v: string) => { this.newWishDesc = v; });

        Text('发起者')
          .fontSize(11)
          .fontColor(SP.textSub)
          .width('100%')
          .margin({ top: 14 });
        Row() {
          ForEach(PARTNERS, (p: string) => {
            Text(p)
              .fontSize(11)
              .fontColor(this.newWishBy === p ? SP.cardDeep : SP.text)
              .backgroundColor(this.newWishBy === p ? SP.primary : SP.chip)
              .borderRadius(10)
              .padding({ left: 12, right: 12, top: 5, bottom: 5 })
              .margin({ right: 8 })
              .onClick(() => { this.newWishBy = p; });
          });
        }
        .width('100%')
        .margin({ top: 6 });

        Row() {
          Text('取消')
            .fontSize(13)
            .fontColor(SP.textSub)
            .border({ width: 1, color: SP.textSub })
            .borderRadius(16)
            .padding({ left: 20, right: 20, top: 8, bottom: 8 })
            .margin({ top: 16 })
            .onClick(() => { this.showAdd = false; });
          Text('添加')
            .fontSize(13)
            .fontColor(SP.cardDeep)
            .backgroundColor(SP.primary)
            .borderRadius(16)
            .padding({ left: 20, right: 20, top: 8, bottom: 8 })
            .margin({ top: 16, left: 12 })
            .onClick(() => {
              if (this.newWishName.trim()) {
                const newWish: WishItem = {
                  id: genId(),
                  name: this.newWishName,
                  desc: this.newWishDesc || '暂无描述',
                  done: false,
                  createdBy: this.newWishBy,
                  date: todayStr()
                };
                this.wishes = [...this.wishes, newWish];
                this.newWishName = '';
                this.newWishDesc = '';
                this.showAdd = false;
              }
            });
        }
        .width('100%')
        .justifyContent(FlexAlign.Center);
      }
      .width('100%')
      .backgroundColor(SP.card)
      .borderRadius({ topLeft: 20, topRight: 20 })
      .padding(18)
      .padding({ bottom: 28 });
    }
    .width('100%')
    .height('100%')
    .backgroundColor('rgba(0,0,0,0.6)')
    .zIndex(999);
  }
}

// ---------- Tab4 互动 ----------
@Component struct TaskContent {
  @State tasks: TaskItem[] = TASKS;
  @State showAdd: boolean = false;
  @State newTaskText: string = '';
  @State newTaskPartner: string = '他';

  @Builder
  modalOverlay(onClose: () => void) {
    Column()
      .layoutWeight(1)
      .width('100%')
      .onClick(() => { onClose(); });
  }

  build() {
    Stack() {
      Scroll() {
        Column() {
          Row() {
            Text('💞 情侣互动')
              .fontSize(16)
              .fontWeight(FontWeight.Bold)
              .fontColor(SP.text);
            Column().layoutWeight(1);
            Text('今日任务')
              .fontSize(10)
              .fontColor(SP.primary)
              .backgroundColor('#4A2A3A')
              .borderRadius(9)
              .padding({ left: 8, right: 8, top: 3, bottom: 3 });
          }
          .width('94%')
          .margin({ top: 12 });

          Text('💡 完成互动任务,让感情升温吧!')
            .fontSize(11)
            .fontColor(SP.textSub)
            .width('94%')
            .margin({ top: 6 });

          ForEach(this.tasks, (t: TaskItem) => {
            Column() {
              Row() {
                Text(t.done ? '✅' : '⬜')
                  .fontSize(20)
                  .margin({ right: 10 })
                  .onClick(() => {
                    this.tasks = this.tasks.map((item: TaskItem): TaskItem => {
                      if (item.id === t.id) {
                        return {
                          id: item.id,
                          text: item.text,
                          done: !item.done,
                          partner: item.partner
                        };
                      }
                      return item;
                    });
                  });
                Column() {
                  Text(t.text)
                    .fontSize(13)
                    .fontWeight(FontWeight.Medium)
                    .fontColor(SP.text)
                    .decoration({ type: t.done ? TextDecorationType.LineThrough : TextDecorationType.None });
                  Text('由 ' + t.partner + ' 发起')
                    .fontSize(9)
                    .fontColor(SP.textSub)
                    .margin({ top: 3 });
                }
                .alignItems(HorizontalAlign.Start)
                .layoutWeight(1)
                .margin({ left: 4 });
              }
              .width('100%');
            }
            .width('94%')
            .backgroundColor(SP.card)
            .borderRadius(14)
            .padding(12)
            .margin({ top: 10 });
          });

          Text('➕ 添加互动任务')
            .fontSize(13)
            .fontColor(SP.primary)
            .width('94%')
            .textAlign(TextAlign.Center)
            .padding(12)
            .margin({ top: 12 })
            .onClick(() => { this.showAdd = true; });
        }
        .width('100%')
        .padding({ bottom: 16 });
      }
      .scrollable(ScrollDirection.Vertical)
      .width('100%')
      .height('100%');

      if (this.showAdd) {
        this.addTaskModal();
      }
    }
    .width('100%')
    .height('100%');
  }

  @Builder
  addTaskModal() {
    Column() {
      this.modalOverlay(() => { this.showAdd = false; });

      Column() {
        Row() {
          Text('➕ 添加任务')
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .fontColor(SP.text);
          Column().layoutWeight(1);
          Text('✕')
            .fontSize(16)
            .fontColor(SP.textSub)
            .onClick(() => { this.showAdd = false; });
        }
        .width('100%')
        .padding({ bottom: 12 });

        Text('任务内容')
          .fontSize(11)
          .fontColor(SP.textSub)
          .width('100%');
        TextInput({ placeholder: '例如:给对方一个拥抱', text: this.newTaskText })
          .fontSize(13)
          .fontColor(SP.text)
          .backgroundColor(SP.chip)
          .borderRadius(10)
          .height(40)
          .margin({ top: 6 })
          .onChange((v: string) => { this.newTaskText = v; });

        Text('发起者')
          .fontSize(11)
          .fontColor(SP.textSub)
          .width('100%')
          .margin({ top: 14 });
        Row() {
          ForEach(PARTNERS, (p: string) => {
            Text(p)
              .fontSize(11)
              .fontColor(this.newTaskPartner === p ? SP.cardDeep : SP.text)
              .backgroundColor(this.newTaskPartner === p ? SP.primary : SP.chip)
              .borderRadius(10)
              .padding({ left: 12, right: 12, top: 5, bottom: 5 })
              .margin({ right: 8 })
              .onClick(() => { this.newTaskPartner = p; });
          });
        }
        .width('100%')
        .margin({ top: 6 });

        Row() {
          Text('取消')
            .fontSize(13)
            .fontColor(SP.textSub)
            .border({ width: 1, color: SP.textSub })
            .borderRadius(16)
            .padding({ left: 20, right: 20, top: 8, bottom: 8 })
            .margin({ top: 16 })
            .onClick(() => { this.showAdd = false; });
          Text('添加')
            .fontSize(13)
            .fontColor(SP.cardDeep)
            .backgroundColor(SP.primary)
            .borderRadius(16)
            .padding({ left: 20, right: 20, top: 8, bottom: 8 })
            .margin({ top: 16, left: 12 })
            .onClick(() => {
              if (this.newTaskText.trim()) {
                const newTask: TaskItem = {
                  id: genId(),
                  text: this.newTaskText,
                  done: false,
                  partner: this.newTaskPartner
                };
                this.tasks = [...this.tasks, newTask];
                this.newTaskText = '';
                this.showAdd = false;
              }
            });
        }
        .width('100%')
        .justifyContent(FlexAlign.Center);
      }
      .width('100%')
      .backgroundColor(SP.card)
      .borderRadius({ topLeft: 20, topRight: 20 })
      .padding(18)
      .padding({ bottom: 28 });
    }
    .width('100%')
    .height('100%')
    .backgroundColor('rgba(0,0,0,0.6)')
    .zIndex(999);
  }
}

// ---------- Tab5 相册 ----------
@Component struct PhotoContent {
  build() {
    Scroll() {
      Column() {
        Row() {
          Text('🖼️ 回忆相册')
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .fontColor(SP.text);
          Column().layoutWeight(1);
          Text('📸 ' + PHOTOS.length + ' 张')
            .fontSize(10)
            .fontColor(SP.textSub);
        }
        .width('94%')
        .margin({ top: 12 });

        Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.SpaceBetween }) {
          ForEach(PHOTOS, (p: PhotoItem) => {
            Column() {
              Text(p.emoji)
                .fontSize(40)
                .backgroundColor(SP.chip)
                .borderRadius(12)
                .padding(16)
                .width('100%')
                .textAlign(TextAlign.Center);
              Text(p.title)
                .fontSize(11)
                .fontWeight(FontWeight.Medium)
                .fontColor(SP.text)
                .width('100%')
                .textAlign(TextAlign.Center)
                .margin({ top: 6 });
              Text(p.date)
                .fontSize(9)
                .fontColor(SP.textSub)
                .margin({ top: 2 });
            }
            .width('31.8%')
            .backgroundColor(SP.card)
            .borderRadius(14)
            .padding(10)
            .margin({ top: 10 })
            .alignItems(HorizontalAlign.Center);
          });
        }
        .width('94%')
        .padding({ bottom: 16 });
      }
      .width('100%');
    }
    .scrollable(ScrollDirection.Vertical)
    .width('100%')
    .height('100%');
  }
}

// ---------- Tab6 我的 ----------
@Component struct MineContent {
  build() {
    Scroll() {
      Column() {
        Column() {
          Row() {
            Text(COUPLE.herAvatar)
              .fontSize(30)
              .backgroundColor(SP.chip)
              .borderRadius(24)
              .width(48)
              .height(48)
              .textAlign(TextAlign.Center);
            Text('💕')
              .fontSize(20)
              .margin({ left: 10, right: 10 });
            Text(COUPLE.hisAvatar)
              .fontSize(30)
              .backgroundColor(SP.chip)
              .borderRadius(24)
              .width(48)
              .height(48)
              .textAlign(TextAlign.Center);
          }
          .width('100%')
          .justifyContent(FlexAlign.Center)
          .margin({ top: 4 });

          Text(COUPLE.name)
            .fontSize(18)
            .fontWeight(FontWeight.Bold)
            .fontColor(SP.primary)
            .margin({ top: 6 });

          Text('❤️ 在一起 ' + COUPLE.days + ' 天')
            .fontSize(13)
            .fontColor(SP.text)
            .margin({ top: 4 });

          Text(COUPLE.bio)
            .fontSize(11)
            .fontColor(SP.textSub)
            .margin({ top: 6 });
        }
        .width('94%')
        .backgroundColor(SP.card)
        .borderRadius(14)
        .padding(14)
        .margin({ top: 12 })
        .alignItems(HorizontalAlign.Center);

        Row() {
          Column() {
            Text('48')
              .fontSize(16)
              .fontWeight(FontWeight.Bold)
              .fontColor(SP.primary);
            Text('日记')
              .fontSize(9)
              .fontColor(SP.textSub)
              .margin({ top: 2 });
          }
          .layoutWeight(1)
          .alignItems(HorizontalAlign.Center);
          Column() {
            Text('12')
              .fontSize(16)
              .fontWeight(FontWeight.Bold)
              .fontColor(SP.secondary);
            Text('纪念日')
              .fontSize(9)
              .fontColor(SP.textSub)
              .margin({ top: 2 });
          }
          .layoutWeight(1)
          .alignItems(HorizontalAlign.Center);
          Column() {
            Text('23')
              .fontSize(16)
              .fontWeight(FontWeight.Bold)
              .fontColor(SP.text);
            Text('心愿')
              .fontSize(9)
              .fontColor(SP.textSub)
              .margin({ top: 2 });
          }
          .layoutWeight(1)
          .alignItems(HorizontalAlign.Center);
        }
        .width('94%')
        .backgroundColor(SP.card)
        .borderRadius(14)
        .padding(14)
        .margin({ top: 12 });

        Column() {
          Row() {
            Text('👤 编辑资料')
              .fontSize(13)
              .fontColor(SP.text);
            Column().layoutWeight(1);
            Text('>')
              .fontColor(SP.textSub);
          }
          .width('100%')
          .padding({ top: 8, bottom: 8 });
          Divider().color(SP.chip);
          Row() {
            Text('🔔 消息通知')
              .fontSize(13)
              .fontColor(SP.text);
            Column().layoutWeight(1);
            Text('>')
              .fontColor(SP.textSub);
          }
          .width('100%')
          .padding({ top: 8, bottom: 8 });
          Divider().color(SP.chip);
          Row() {
            Text('🌙 深色模式')
              .fontSize(13)
              .fontColor(SP.text);
            Column().layoutWeight(1);
            Text('>')
              .fontColor(SP.textSub);
          }
          .width('100%')
          .padding({ top: 8, bottom: 8 });
          Divider().color(SP.chip);
          Row() {
            Text('💌 关于恋爱日记')
              .fontSize(13)
              .fontColor(SP.text);
            Column().layoutWeight(1);
            Text('>')
              .fontColor(SP.textSub);
          }
          .width('100%')
          .padding({ top: 8, bottom: 8 });
        }
        .width('94%')
        .backgroundColor(SP.card)
        .borderRadius(14)
        .padding(14)
        .margin({ top: 12 });

        Text('— 愿每一对情侣都甜甜蜜蜜 —')
          .fontSize(10)
          .fontColor(SP.textSub)
          .width('94%')
          .textAlign(TextAlign.Center)
          .margin({ top: 14, bottom: 16 });
      }
      .width('100%');
    }
    .scrollable(ScrollDirection.Vertical)
    .width('100%')
    .height('100%');
  }
}
Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐