HarmonyOS 6效果实现:ArkUI‑Toggle 开关状态监听,onChange 捕获开关切换事件同步更新页面布尔响应式状态变量
引言
当恋爱从轰轰烈烈的告白回归到柴米油盐的日常,那些看似微不足道的小瞬间——一起看的樱花、早晨收到的消息、亲手做的提拉米苏、海边许下的愿望——才是一段感情最珍贵的记忆。恋爱日记正是这样一款概念应用,它将情侣日常的点点滴滴,浓缩进一个 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 套弹框各具特色:
- 写日记:心情emoji选择(6种)+ 日记内容TextArea + 取消/发布(底部抽屉式)
- 添加纪念日:名称输入 + 日期输入 + 类型chips(在一起/生日/纪念日)+ 取消/添加(底部抽屉式)
- 添加心愿:心愿名称输入 + 描述输入 + 发起者chips(他/她)+ 取消/添加(底部抽屉式)
- 添加互动任务:任务内容输入 + 发起者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)
日记是入口,承担"浏览日记→点赞缩放→写日记"路径。页面结构:
- 甜蜜日记标题:📖甜蜜日记 + 共X篇
- 日记列表(8篇):每篇含头像+发布者+日期+心情emoji + 日记内容 + 评论数+点赞 + 爱心点赞(点赞缩放动画 scale 1.2,200ms)
- 到底留白
点赞缩放动画是日记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 数组头部,清空表单并关闭弹框。

5.2 纪念日 Tab(AnniversaryContent)
纪念日是倒计时,承担"浏览纪念日→天数倒计时→添加纪念日"路径。页面结构:
- 纪念日标题:🎉纪念日 + ❤️在一起X天(玫瑰粉标签,daysSince(‘2024-12-01’)实时计算)
- 纪念日列表(4个):每个含图标(💑在一起/🎂生日/🎉纪念日)+标题+类型+日期 + 天数(X天前,玫瑰粉加粗)
- ➕添加纪念日按钮
天数倒计时是纪念日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 数组尾部,清空表单并关闭弹框。

5.3 心愿 Tab(WishContent)
心愿是共同规划,承担"浏览心愿→勾选完成→已实现统计→添加心愿"路径。页面结构:
- 心愿清单标题:⭐心愿清单 + 已实现X/Y(filter统计已完成数量)
- 心愿列表(8个):每个含勾选框(⬜/✅,点击切换)+名称+描述+发起者 + 日期
- ➕添加心愿按钮
勾选完成是心愿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 数组尾部,清空表单并关闭弹框。

5.4 互动 Tab(TaskContent)
互动是情侣任务,承担"浏览任务→勾选完成→添加任务"路径。页面结构:
- 情侣互动标题:💞情侣互动 + 今日任务标签
- 提示语:💡完成互动任务,让感情升温吧!
- 任务列表(6个):每个含勾选框(⬜/✅,点击切换)+任务内容+发起者
- ➕添加互动任务按钮
勾选完成是互动Tab的核心交互:与心愿Tab相同,每个任务的勾选框点击时 map 重建数组(done取反),已完成任务名称添加删除线。互动任务设计为简单可完成的日常小任务,如"给对方一个拥抱🤗"、“说一句我爱你💕”、“一起做一顿晚餐🍳”、“给对方一个惊喜🎁”、“一起看一部电影🎬”、“写一封情书💌”。这些简单的日常任务让情侣间的互动变得有计划、有记录,增加恋爱的新鲜感和仪式感。
任务卡片设计:每个含勾选框(20vp)+任务内容(13vp中等,已完成添加删除线)+由X发起(9vp灰粉)。卡片紫红背景#3D263F,14vp圆角,12vp内边距,10vp顶部间距。6个任务由他和她轮流发起。
添加互动任务弹框:含任务内容输入 + 发起者chips(他/她)+ 取消/添加按钮。添加时校验内容非空,创建新 TaskItem(genId生成ID,done为false),追加到 tasks 数组尾部,清空表单并关闭弹框。

5.5 相册 Tab(PhotoContent)
相册是回忆珍藏,承担"浏览照片→三列宫格"路径。页面结构:
- 回忆相册标题:🖼️回忆相册 + 📸X张
- 三列宫格(8张):每张含emoji照片(40vp,紫褐背景)+标题+日期
三列宫格设计:使用 Flex({wrap: FlexWrap.Wrap, justifyContent: FlexAlign.SpaceBetween}) 实现三列布局,每卡宽度 31.8%,自动换行。每卡居中对齐,emoji照片(40vp,紫褐背景#4D334F,12vp圆角,16vp内边距),标题(11vp中等),日期(9vp灰粉)。三列紧凑布局最大化展示照片数量,如同真实相册的照片墙。
照片数据:8张照片覆盖恋爱中的重要时刻,使用emoji占位:第一次约会📸、跨年夜🎆、情人节🌹、春游赏花🌸、夏日海边🌊、秋日银杏🍂、冬日初雪❄️、纪念日晚餐🍷。每张照片有标题和日期,记录恋爱中的美好回忆。emoji占位简洁可爱,符合温暖粉调的整体风格。

5.6 我的 Tab(MineContent)
我的是情侣中心,承担"资料卡→统计→设置菜单"路径。页面结构:
- 情侣资料卡:双人头像(她🦌+💕+他🐻,水平居中)+情侣名称(鹿与熊,玫瑰粉18vp加粗)+❤️在一起X天(13vp)+情侣简介(从今天起,每一天都是甜的。)
- 三项统计卡:日记48(玫瑰粉)/纪念日12(浅粉)/心愿23(浅粉白),每项含数字+标签
- 设置菜单卡:编辑资料/消息通知/深色模式/关于恋爱日记,每项含图标+名称+>箭头,分割线分隔
- 底部标语:— 愿每一对情侣都甜甜蜜蜜 —
情侣资料卡设计是我的Tab的核心视觉:双人头像水平排列,中间用💕连接,体现"两个人"的专属感。情侣名称"鹿与熊"玫瑰粉加粗,在一起天数实时计算(COUPLE.days,初始化时调用daysSince),情侣简介温暖甜蜜。资料卡紫红背景#3D263F,14vp圆角,14vp内边距,内容居中对齐。
三项统计卡:日记/纪念日/心愿三项统计,水平排列,每项居中对齐。数字分别使用玫瑰粉(日记)、浅粉(纪念日)、浅粉白(心愿),标签9vp灰粉。统计卡紫红背景#3D263F,14vp圆角,14vp内边距。
设置菜单卡:编辑资料/消息通知/深色模式/关于恋爱日记四项,每项含图标+名称+>箭头,分割线(chip色)分隔。设置菜单紫红背景#3D263F,14vp圆角,14vp内边距。当前为静态展示,可扩展为实际设置功能。

第六章 四大弹框组件设计
弹框是恋爱日记交互体系的核心组成部分。项目共设计了 4 套弹框组件,覆盖写日记、添加纪念日、添加心愿、添加互动任务四大业务场景。所有弹框均为底部抽屉式设计,统一的视觉风格。
6.1 写日记弹框(addDiaryModal)
定位:日记记录入口,心情emoji选择+内容TextArea+发布设计。
核心功能:
- 心情选择(6 选 1):😊🥰😍🤗😘💕,选中玫瑰粉背景,emoji 18vp
- 日记内容:TextArea 多行输入(记录今天的故事…),100vp高度
- 取消/发布按钮:取消灰粉边框/发布玫瑰粉背景,发布时校验内容非空
写日记弹框采用底部抽屉式设计,心情emoji选择是特色功能,6种心情emoji覆盖日常恋爱中的各种情绪,让每篇日记都有心情标签。发布时创建新 DiaryItem(genId生成ID,发布者固定为小鹿🦌,日期为todayStr(),心情为选中的emoji,likes为0,loved为false,comments为0),插入到 diaries 数组头部(最新日记在最前)。
6.2 添加纪念日弹框(addAnniversaryModal)
定位:纪念日添加入口,名称+日期+类型chips设计。
核心功能:
- 名称输入:TextInput(例如:第一次约会)
- 日期输入:TextInput(YYYY-MM-DD格式,默认todayStr())
- 类型选择(3 选 1):在一起/生日/纪念日,选中玫瑰粉背景
- 取消/添加按钮:取消灰粉边框/添加玫瑰粉背景,添加时校验名称和日期非空
添加纪念日弹框采用底部抽屉式设计,类型chips选择是特色功能,3种类型(在一起/生日/纪念日)覆盖情侣间的重要日子。添加时创建新 AnniversaryItem(genId生成ID,图标根据类型自动选择:在一起💑/生日🎂/纪念日🎉,days调用daysSince(date)计算),追加到 anniversaries 数组尾部。
6.3 添加心愿弹框(addWishModal)
定位:心愿添加入口,名称+描述+发起者chips设计。
核心功能:
- 心愿名称输入:TextInput(例如:一起去旅行)
- 描述输入:TextInput(简单描述)
- 发起者选择(2 选 1):他/她,选中玫瑰粉背景
- 取消/添加按钮:取消灰粉边框/添加玫瑰粉背景,添加时校验名称非空
添加心愿弹框采用底部抽屉式设计,发起者chips选择是特色功能,2种发起者(他/她)体现情侣双视角。添加时创建新 WishItem(genId生成ID,描述默认为"暂无描述",done为false,日期为todayStr()),追加到 wishes 数组尾部。
6.4 添加互动任务弹框(addTaskModal)
定位:互动任务添加入口,内容+发起者chips设计。
核心功能:
- 任务内容输入:TextInput(例如:给对方一个拥抱)
- 发起者选择(2 选 1):他/她,选中玫瑰粉背景
- 取消/添加按钮:取消灰粉边框/添加玫瑰粉背景,添加时校验内容非空
添加互动任务弹框采用底部抽屉式设计,与添加心愿弹框结构相似但更简洁(无描述字段)。添加时创建新 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 代码编辑、预览、调试、打包等完整开发能力。
安装步骤:
- 访问华为开发者联盟官网(developer.huawei.com),进入"开发"→"DevEco Studio"下载页面
- 选择对应操作系统版本(Windows/macOS)下载安装包
- Windows 版运行 .exe 安装程序,按向导完成安装(建议安装路径不含中文和空格)
- macOS 版打开 .dmg 镜像,将 DevEco Studio 拖入 Applications 文件夹
- 首次启动 DevEco Studio,选择不导入设置,进入欢迎界面
- 配置 Node.js 路径(DevEco Studio 内置 Node.js,也可使用本地安装的 Node.js 16+)
- 配置 npm 镜像(建议使用华为云 npm 镜像或淘宝镜像,加速依赖下载)

14.3 配置 HarmonyOS SDK
DevEco Studio 安装完成后,需要配置 HarmonyOS SDK(API 9+)。SDK 包含 ArkTS 编译器、ArkUI 组件库、系统 API、模拟器镜像等。
配置步骤:
- 打开 DevEco Studio,进入欢迎界面
- 点击"Configure"→"Settings"(macOS 为"DevEco Studio"→"Settings")
- 左侧导航选择"Appearance & Behavior"→"System Settings"→"HarmonyOS SDK"
- 在 SDK Platforms 标签页,勾选 API 9 或更高版本(建议 API 9,兼容性最好)
- 在 SDK Tools 标签页,勾选"ArkTS"、“ArkUI”、"Previewer"等工具
- 点击"Apply"开始下载安装 SDK(下载大小约 2-4GB,需耐心等待)
- 安装完成后,SDK 路径会自动配置,可在 Settings 中查看和修改
SDK 版本说明:
- API 9:首个支持 Stage 模型和 ArkTS 声明式 UI 的稳定版本,兼容性最好
- API 10+:新增更多 ArkUI 组件和 API,功能更丰富,但兼容性需考虑
- 建议使用 API 9 开发,确保在更多设备上运行

14.4 创建 HarmonyOS 项目
SDK 配置完成后,创建一个新的 HarmonyOS 项目。
创建步骤:
- 打开 DevEco Studio,进入欢迎界面
- 点击"Create Project"创建新项目
- 选择项目模板:选择"Empty Ability"(空能力模板),这是最基础的 ArkTS 项目模板
- 配置项目信息:
- Project name:项目名称(如 LoveApp)
- Bundle name:应用包名(如 com.example.loveapp,需唯一)
- Save location:项目保存路径(建议不含中文和空格)
- Compile SDK:选择 API 9 或更高版本
- Model:选择 Stage 模型(ArkTS 声明式 UI 推荐使用 Stage 模型)
- Language:选择 ArkTS
- 点击"Finish"创建项目,DevEco Studio 会自动生成项目结构和基础代码
- 等待项目同步完成(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 中的入口页面配置。

14.5 运行应用
项目创建和代码导入完成后,可以在真机或模拟器上运行应用。
运行步骤:
- 连接 HarmonyOS 设备(通过 USB 数据线),或启动模拟器
- 真机运行需开启开发者模式和 USB 调试:设置→关于手机→连续点击版本号开启开发者模式→设置→系统和更新→开发人员选项→开启 USB 调试
- 模拟器运行:点击 DevEco Studio 顶部工具栏的设备选择器,选择"Device Manager"→创建模拟器→选择设备型号和 API 版本→启动模拟器
- 在 DevEco Studio 顶部工具栏,确认运行配置为"entry",设备选择为已连接的真机或已启动的模拟器
- 点击运行按钮(绿色三角形 ▶)或按快捷键 Shift+F10(macOS 为 Control+R)
- DevEco Studio 会自动编译、打包、安装应用到设备,并启动应用
- 首次运行可能需要 1-2 分钟编译时间,后续运行会更快(增量编译)
运行注意事项:
- 确保设备已开启 USB 调试,且已授权电脑调试
- 确保设备系统版本 >= API 9(HarmonyOS 3.0+)
- 模拟器运行性能可能不如真机,建议最终在真机上测试
- 如果运行失败,查看 Build 窗口和 Log 窗口的错误信息
恋爱日记的 2 组交互动画(Tab图标缩放、日记点赞缩放)建议在真机上测试,确保动画流畅度和交互反馈符合预期。预览器可以查看静态布局,但动画效果需要在真机或模拟器上运行才能完整体验。

14.6 预览与调试
DevEco Studio 提供 ArkUI 预览器和调试工具,方便开发过程中实时查看界面和调试代码。
预览器使用:
- 打开 .ets 页面文件,右侧会自动显示 Previewer 预览面板
- 预览器实时渲染当前页面的 ArkUI 界面,代码修改后自动刷新
- 预览器支持多种设备尺寸和方向切换,测试不同屏幕适配
- 预览器支持深色/浅色模式切换(恋爱日记为深色主题,建议在深色模式下预览)
- 如果预览器未显示,点击右侧边栏的"Previewer"图标开启
调试工具使用:
- 在代码行号左侧点击设置断点(红色圆点)
- 点击调试按钮(绿色虫子 🐛)或按快捷键 Shift+F9(macOS 为 Control+D)
- 应用运行到断点处会暂停,可查看变量值、调用栈、表达式等
- 使用调试工具栏的单步跳过(F8)、单步进入(F7)、单步跳出(Shift+F8)等操作逐步执行代码
- 在 Variables 面板查看和修改变量值,在 Watches 面板添加监视表达式
- 在 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 组全局数据常量。
设计亮点
- 情侣双视角设计:日记发布者、心愿发起者、任务发起者、双人头像,处处体现"两个人"的专属感,区别于单人日记App
- ArkTS 严格模式合规:无 Object.assign,所有对象更新使用对象字面量显式重建,所有变量和函数参数显式类型标注,所有语句分号结尾
- 心跳缩放动画:Tab图标缩放(1.15倍)和日记点赞缩放(1.2倍),模拟恋爱中的心跳感,让静态界面有了"心跳"
- 在一起天数实时计算:daysSince纯函数实时计算,标题栏和我的页面双重展示,让时间变得可视化,增强归属感
- 心愿勾选与已实现统计:勾选完成+名称删除线+已实现统计三重反馈,让共同规划变得可追踪,增加目标感和成就感
- 温暖粉调配色:深紫红背景+玫瑰粉主色+浅粉辅助色,营造深夜依偎的温暖氛围,传递爱与甜蜜的情感
- 单文件零依赖:约1000行代码在一个.ets文件中,复制即运行,无第三方依赖,适合学习和快速原型验证
- 不可变状态更新:所有@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 严格模式项目中。
未来展望
恋爱日记作为概念应用,未来可扩展方向:
- 真实图片替换 emoji:将相册的 emoji 占位替换为真实图片,支持拍照和相册选择,提升视觉真实感
- 后端接口对接:将静态数据常量替换为真实后端 API,实现日记云端同步、纪念日提醒、心愿共享、用户系统等真实业务功能
- 情侣双向同步:实现情侣双方的数据实时同步,一方写日记、添加心愿、完成任务,另一方实时看到
- 纪念日提醒推送:基于纪念日日期实现推送提醒,生日、周年、100天等重要日子提前提醒
- 心情统计与情感分析:基于日记的心情emoji和内容,统计情侣的情感趋势,提供情感建议
- 心愿进度追踪:为心愿添加进度条、截止日期、分工等,让共同规划更可追踪
- 互动任务每日重置:实现互动任务的每日重置和历史记录,让情侣每天都有新任务
- 相册时间线:将相册改为时间线布局,按日期分组,增加回忆的叙事感
- 发布者切换:写日记时支持切换发布者(他/她),实现真正的情侣双视角记录
- 多端适配:适配手机、平板、折叠屏等多种设备,支持横屏和分屏模式
恋爱日记以情侣专属的温暖粉调视觉语言和"日记+纪念日+心愿+互动+相册+我的"六位一体功能,展示了情侣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%');
}
}
更多推荐



所有评论(0)