亲子育儿应用:基于HarmonyOS API 24育儿管理平台最外层是一个 Scroll 容器,设置 .scrollable(ScrollDirection.Horizontal) 使其支持水平滚动
引言:数字化时代的育儿新方式
在现代社会中,育儿早已不再是凭借经验口口相传的传统模式。随着移动互联网技术的飞速发展,越来越多的年轻父母开始借助数字工具来记录和管理孩子的成长过程。从宝宝出生的那一刻起,每一次体重变化、每一个发育里程碑、每一针疫苗接种,都承载着父母无尽的关爱与期待。然而,这些珍贵的信息如果仅仅依靠记忆或者零散的纸质记录,很容易遗失或混淆。正是在这样的背景下,亲子育儿类应用应运而生,成为了现代家庭不可或缺的数字助手。

本文将深入剖析一款功能完善的亲子育儿应用。该应用集成了育儿日记、成长记录、疫苗提醒、育儿社区和个人中心五大核心模块,旨在为家长提供一个一站式、全方位的育儿管理平台。通过这款应用,家长不仅可以便捷地记录宝宝日常生活的点点滴滴,还能系统性地追踪宝宝的身高体重发育情况,及时接收疫苗接种提醒,同时与其他家长在社区中交流育儿经验。
从技术角度来看,这款应用采用了声明式 UI 开发范式,基于 TypeScript 语言进行开发。声明式 UI 的核心思想是"状态驱动视图"——开发者只需要描述界面在不同状态下应该呈现的样子,框架会自动处理状态变化时的界面更新。这种开发模式极大地简化了 UI 逻辑,让开发者能够将更多精力放在业务功能和用户体验上。此外,应用大量使用了组件化思想,将复杂的界面拆分为多个独立的、可复用的构建单元(Builder),使得代码结构清晰、易于维护。
应用整体架构概述
在深入代码细节之前,我们首先从宏观层面了解一下这款应用的整体架构。整个应用围绕一个主组件展开,主组件负责管理全局状态、渲染底部导航栏以及协调各个功能页面之间的切换。应用采用了经典的底部 Tab 导航模式,共包含五个主要功能入口:育儿日记、成长记录、疫苗提醒、育儿社区和我的。
在数据层面,应用定义了一套完整的接口(Interface)体系来描述各种业务实体,包括日记条目、成长记录、疫苗信息、社区帖子等。同时,为了让界面在开发阶段就能呈现出真实的数据效果,应用内置了丰富的模拟数据(Mock Data)。这些模拟数据不仅覆盖了各种业务场景,还经过精心设计,使得每一项数据都具有真实感和代表性。
在交互层面,应用实现了完善的弹框系统,包括添加弹框、编辑弹框、删除确认弹框和详情查看弹框。这些弹框通过统一的遮罩层机制实现,确保了交互体验的一致性。此外,应用还包含了一些细腻的交互动效,例如发育里程碑图标的点击放大动画,为用户带来了更加生动的使用体验。
接下来,让我们逐段深入代码,一探这款应用的技术实现细节。
一、数据模型设计:用接口定义业务实体
在任何一款应用中,数据模型的设计都是至关重要的基础工作。良好的数据模型不仅能够清晰地表达业务概念,还能为后续的组件开发提供类型安全保障。这款应用在文件开头定义了一系列接口,每一个接口都对应一种业务实体。让我们逐一分析这些数据模型。
1.1 日记相关数据模型
育儿日记是这个应用的核心功能之一,因此日记相关的数据模型设计得尤为细致。应用定义了两个与日记相关的接口:DiaryItem 用于列表展示,DiaryDetail 用于详情查看。
interface DiaryItem {
id: number
date: string
title: string
summary: string
mood: string
weather: string
hasImage: boolean
}

让我们逐行分析这个接口的定义:
id: number:日记的唯一标识符,使用数字类型。这个字段在列表渲染时作为 key 使用,确保框架能够高效地进行 diff 比较和更新。date: string:日记的日期,采用字符串格式存储,例如2026-08-08。使用字符串而非 Date 对象的好处是序列化方便,且在前端展示时无需额外格式化。title: string:日记标题,简明扼要地概括当天的主要内容,例如"今天宝宝会叫妈妈了"。summary: string:日记摘要,在列表中展示的简短内容预览,通常限制为两行显示。mood: string:宝宝当天的心情,取值包括"开心"“平静”“兴奋”“担心”"难过"等,在界面上以粉色标签形式展示。weather: string:当天天气,如"晴"“多云”“阴”“雨”,配合天气图标在日记卡片中展示。hasImage: boolean:是否包含图片的标志位。这个布尔值字段用于控制列表项中图片占位块的显示与隐藏,是条件渲染的典型应用场景。
interface DiaryDetail {
id: number
date: string
title: string
mood: string
weather: string
food: string
sleep: string
note: string
}

DiaryDetail 接口是日记详情的数据模型,相比 DiaryItem 增加了更多细节字段:
food: string:饮食记录,记录宝宝当天的饮食内容,如"米糊+南瓜泥"。sleep: string:睡眠记录,记录宝宝的睡眠时长,如"10小时30分"。note: string:备注信息,用于记录其他需要补充的内容。
通过将列表项和详情项分离为两个接口,应用实现了数据的按需加载——列表页面只需要轻量级的摘要数据,而详情页面才加载完整信息。这种设计理念在大规模应用中尤为重要,能够有效减少不必要的数据传输和内存占用。
1.2 成长记录数据模型
成长记录模块需要追踪宝宝的身高、体重发育情况以及各项发育里程碑的达成情况,因此定义了两个接口。
interface GrowthRecord {
id: number
monthAge: number
height: number
weight: number
percentile: string
}

逐行解析:
id: number:成长记录的唯一标识符。monthAge: number:月龄,即宝宝出生后的月数。使用数字类型便于排序和计算,也便于在图表中作为横坐标使用。height: number:身高,单位为厘米,使用浮点数精确到小数点后一位。weight: number:体重,单位为千克,同样使用浮点数。percentile: string:百分位数,表示宝宝身高体重在同龄儿童中所处的位置,如"75%"表示超过了 75% 的同龄儿童。这个指标对于评估宝宝发育是否正常非常关键。
interface Milestone {
id: number
monthAge: number
name: string
date: string
note: string
achieved: boolean
}

Milestone 接口描述的是发育里程碑,每个字段的意义如下:
monthAge: number:达成该里程碑时的月龄。name: string:里程碑名称,如"独立站立"“叫爸爸妈妈”"爬行"等。date: string:达成日期,记录宝宝具体是在哪一天达成这个里程碑的。note: string:补充说明,对该里程碑的详细描述。achieved: boolean:是否已达成。这个字段为未来的"待达成"里程碑预留了扩展空间,虽然当前数据中所有里程碑都已达成,但接口设计上考虑了未达成的情况。
1.3 疫苗提醒数据模型
interface VaccineItem {
id: number
name: string
date: string
status: string
age: string
desc: string
}

疫苗信息的数据模型设计简洁而实用:
name: string:疫苗名称,如"乙肝疫苗(第三针)""百白破疫苗(第三针)"等,名称中包含接种针次信息,方便家长了解接种进度。date: string:接种日期或计划接种日期。status: string:接种状态,取值为"已种""待种"或其他。这个字段是界面渲染状态指示器的核心依据。age: string:建议接种的年龄阶段,如"6月龄"“出生”"8月龄"等。desc: string:疫苗描述,说明该疫苗预防的疾病,例如"预防乙型肝炎病毒感染",帮助家长理解疫苗的重要性。
1.4 社区相关数据模型
育儿社区模块涉及帖子展示和话题分类,因此定义了两个接口。
interface CommunityPost {
id: number
author: string
babyAge: string
title: string
summary: string
likes: number
comments: number
hasImage: boolean
}

逐行解析社区帖子模型:
author: string:发帖人昵称,如"糖糖妈妈""果果爸"等,采用"宝宝名+称呼"的命名方式,符合育儿社区的用户习惯。babyAge: string:发帖人宝宝的年龄,如"1岁2月""8月"等,帮助读者了解帖子的参考背景。title: string:帖子标题。summary: string:帖子摘要,在列表中预览展示。likes: number:点赞数,数字类型便于排序和统计。comments: number:评论数。hasImage: boolean:是否包含图片。
interface TopicItem {
id: number
name: string
count: number
}

话题模型非常简洁:
name: string:话题名称,如"辅食食谱"“睡眠训练”"早教启蒙"等。count: number:参与人数,展示该话题的热度。
1.5 个人中心数据模型
interface BabyInfo {
id: number
name: string
age: string
gender: string
birthday: string
}

宝宝信息模型:
name: string:宝宝昵称,如"小花朵"“小树苗”。age: string:年龄描述。gender: string:性别。birthday: string:出生日期。
interface DateItem {
id: number
day: string
weekday: string
date: string
isToday: boolean
}

日期项模型用于日记页面的横向日期选择器:
day: string:日期中的"日"部分,如"05"“08”,作为大号数字展示。weekday: string:星期几,用单个汉字表示,如"一"“二”“日”。date: string:完整日期格式,如"08-08"。isToday: boolean:是否为今天,用于在日期选择器中高亮显示当前日期。
interface StatItem {
label: string
value: number
}
interface SettingItem {
id: number
icon: string
title: string
subtitle: string
}
最后两个辅助模型分别用于统计展示和设置列表:
StatItem:包含标签和数值两个字段,用于展示"育儿日记 128 条""成长记录 24 条"等统计信息。SettingItem:包含图标、标题和副标题,用于设置列表项的渲染。图标使用 emoji 字符,这是一种轻量级的图标方案,无需引入额外的图标资源。
二、模拟数据层:让界面"活"起来的关键
在应用开发初期,后端接口往往尚未就绪。此时,模拟数据就成了前端开发和联调的重要工具。这款应用在接口定义之后,紧接着定义了大量精心构造的模拟数据。这些数据不仅数量充足,而且内容真实可信,让应用在开发阶段就能呈现出接近上线后的效果。
2.1 日期模拟数据
const MOCK_DATES: DateItem[] = [
{ id: 1, day: '05', weekday: '一', date: '08-05', isToday: false },
{ id: 2, day: '06', weekday: '二', date: '08-06', isToday: false },
{ id: 3, day: '07', weekday: '三', date: '08-07', isToday: false },
{ id: 4, day: '08', weekday: '四', date: '08-08', isToday: true },
{ id: 5, day: '09', weekday: '五', date: '08-09', isToday: false },
{ id: 6, day: '10', weekday: '六', date: '08-10', isToday: false },
{ id: 7, day: '11', weekday: '日', date: '08-11', isToday: false }
]
这段代码定义了一周的日期数据,共 7 条记录。值得注意的是第四条记录的 isToday 设为 true,对应 8 月 8 日(星期四)。在界面渲染时,这一天的日期卡片会显示一个"今天"的标签,帮助用户快速定位当前日期。数据按照时间顺序排列,从周一到周日,与日历的自然顺序一致。
2.2 日记模拟数据
const MOCK_DIARIES: DiaryItem[] = [
{ id: 1, date: '2026-08-08', title: '今天宝宝会叫妈妈了', summary: '下午陪宝宝玩积木的时候,宝宝突然清晰地叫了一声妈妈,感动得差点哭了...', mood: '开心', weather: '晴', hasImage: true },
{ id: 2, date: '2026-08-07', title: '第一次去动物园', summary: '带宝宝去了市动物园,宝宝看到长颈鹿特别兴奋,一直指着叫高高...', mood: '兴奋', weather: '晴', hasImage: true },
// ... 更多日记数据
]
日记模拟数据共包含 10 条记录,每一条都经过精心编写,涵盖了不同的生活场景:宝宝学会叫妈妈、第一次去动物园、学会拍手、添加辅食、发烧照顾、亲子游泳、学会爬行、第一次理发等。这些场景真实地反映了育儿生活中的常见事件,让数据具有极强的代入感。
从数据结构上看,每条日记都包含了完整的字段信息。mood 字段的取值多样,包括"开心"“兴奋”“平静”"担心"等,确保了界面展示的丰富性。hasImage 字段在有图和无图之间交替设置,这样可以同时测试带图片和不带图片两种列表项的渲染效果。
2.3 成长记录模拟数据
const MOCK_GROWTH: GrowthRecord[] = [
{ id: 1, monthAge: 12, height: 76.5, weight: 9.8, percentile: '75%' },
{ id: 2, monthAge: 10, height: 74.2, weight: 9.3, percentile: '70%' },
{ id: 3, monthAge: 9, height: 72.8, weight: 9.0, percentile: '68%' },
{ id: 4, monthAge: 8, height: 71.3, weight: 8.7, percentile: '65%' },
{ id: 5, monthAge: 7, height: 69.8, weight: 8.4, percentile: '72%' },
{ id: 6, monthAge: 6, height: 68.0, weight: 8.0, percentile: '70%' },
{ id: 7, monthAge: 3, height: 62.5, weight: 6.5, percentile: '75%' },
{ id: 8, monthAge: 1, height: 56.0, weight: 4.8, percentile: '60%' }
]
成长记录数据共 8 条,覆盖了从 1 月龄到 12 月龄的关键时间节点。数据按照月龄从大到小排列(即最新的记录在最前面),符合"最新优先"的展示原则。从数据趋势来看,身高和体重都随月龄增长而增加,符合婴儿发育的自然规律。百分位数在 60% 到 75% 之间波动,表明宝宝的发育处于中上水平,这也是大多数家长希望看到的结果。
这组数据还有一个重要用途:驱动生长趋势图表的渲染。在界面上,每个数据项会被转换为一组柱状图,身高柱和体重柱并排展示,让家长直观地看到宝宝的生长趋势。
2.4 发育里程碑模拟数据
const MOCK_MILESTONES: Milestone[] = [
{ id: 1, monthAge: 12, name: '独立站立', date: '2026-07-15', note: '可以独立站立10秒以上', achieved: true },
{ id: 2, monthAge: 11, name: '叫爸爸妈妈', date: '2026-08-08', note: '能清晰叫出爸爸妈妈', achieved: true },
{ id: 3, monthAge: 10, name: '扶物行走', date: '2026-06-20', note: '扶着沙发可以走几步', achieved: true },
{ id: 4, monthAge: 9, name: '爬行', date: '2026-08-02', note: '能熟练地往前爬', achieved: true },
{ id: 5, monthAge: 8, name: '独坐', date: '2026-05-10', note: '可以自己坐稳了', achieved: true },
{ id: 6, monthAge: 6, name: '翻身', date: '2026-03-01', note: '可以自主翻身', achieved: true }
]
里程碑数据共 6 条,按照月龄从大到小排列。每条记录都记录了宝宝在不同月龄达成的重要发育节点,从 6 月龄的翻身到 12 月龄的独立站立,完整地勾勒出宝宝第一年的运动发育轨迹。note 字段的描述生动具体,如"可以独立站立10秒以上",让数据更具可读性和参考价值。
2.5 疫苗模拟数据
const MOCK_VACCINES: VaccineItem[] = [
{ id: 1, name: '乙肝疫苗(第三针)', date: '2026-07-20', status: '已种', age: '6月龄', desc: '预防乙型肝炎病毒感染' },
{ id: 2, name: 'A群流脑疫苗(第一针)', date: '2026-08-15', status: '待种', age: '6月龄', desc: '预防流行性脑脊髓膜炎' },
// ... 更多疫苗数据
{ id: 10, name: '卡介苗', date: '2025-08-10', status: '已种', age: '出生', desc: '预防结核病' }
]
疫苗数据共 10 条,涵盖了从出生到 9 月龄的主要疫苗。数据中既有"已种"状态的记录,也有"待种"状态的记录,这种混合状态确保了界面能够同时展示已完成和待完成的接种信息。status 字段是整个疫苗模块界面渲染的核心——它决定了状态指示器的图标、颜色和背景色。在后面的辅助方法分析中,我们将看到如何根据这个字段返回不同的视觉样式。
2.6 社区与个人中心模拟数据
const MOCK_POSTS: CommunityPost[] = [
{ id: 1, author: '糖糖妈妈', babyAge: '1岁2月', title: '宝宝挑食怎么办?分享我的经验', summary: '...', likes: 328, comments: 56, hasImage: true },
// ... 共12条帖子
]
const MOCK_TOPICS: TopicItem[] = [
{ id: 1, name: '辅食食谱', count: 2341 },
{ id: 2, name: '睡眠训练', count: 1876 },
{ id: 3, name: '早教启蒙', count: 3205 },
{ id: 4, name: '疫苗攻略', count: 1543 },
{ id: 5, name: '亲子旅行', count: 987 }
]
社区帖子数据共 12 条,内容涵盖辅食、睡眠、护肤、早教、旅行、断奶、疫苗等育儿热点话题。每条帖子都有不同的点赞数和评论数,其中"宝宝疫苗全攻略分享"以 723 个点赞和 156 条评论位居热度榜首。话题数据共 5 条,参与人数从近千到三千多不等,其中"早教启蒙"话题以 3205 人参与位居第一。
const MOCK_BABIES: BabyInfo[] = [
{ id: 1, name: '小花朵', age: '1岁0月', gender: '女', birthday: '2025-08-08' },
{ id: 2, name: '小树苗', age: '2岁3月', gender: '男', birthday: '2024-05-01' }
]
const MOCK_STATS: StatItem[] = [
{ label: '育儿日记', value: 128 },
{ label: '成长记录', value: 24 },
{ label: '社区互动', value: 365 }
]
const MOCK_SETTINGS: SettingItem[] = [
{ id: 1, icon: '🔔', title: '提醒设置', subtitle: '疫苗提醒、成长提醒' },
{ id: 2, icon: '🔒', title: '隐私设置', subtitle: '社区可见范围' },
{ id: 3, icon: '📖', title: '宝宝档案', subtitle: '管理宝宝信息' },
{ id: 4, icon: '❤️', title: '关于我们', subtitle: '版本 v1.0.0' }
]
个人中心的模拟数据包括两个宝宝的档案信息、三项统计数据和四个设置项。两个宝宝的设置体现了"多宝宝管理"的应用场景——一个家庭可能有多个孩子,应用需要支持在不同宝宝之间切换。统计数据展示了用户在应用中的活跃度,而设置项则覆盖了提醒、隐私、档案和关于四个常见入口。
三、主组件与状态管理:应用的核心枢纽
3.1 组件声明与状态定义
在声明式 UI 框架中,组件是构建界面的基本单元。这款应用的主组件通过 @Entry 和 @Component 装饰器进行声明,它是整个应用的入口组件。
@Entry
@Component
struct ParentingApp {
@State currentTab: number = 0
@State showAddModal: boolean = false
@State showEditModal: boolean = false
@State showDeleteModal: boolean = false
@State showDetailModal: boolean = false
@State selectedDate: number = 4
@State selectedPostId: number = 0
@State selectedDiaryId: number = 0
@State milestoneAnimIndex: number = -1
逐行分析这些状态变量的定义和用途:
@Entry:标记该组件为应用的入口组件,框架会以此为起点构建整个页面树。@Component:声明这是一个自定义组件,可以被复用和组合。@State currentTab: number = 0:当前选中的 Tab 索引,初始值为 0,即默认显示第一个 Tab(育儿日记)。当用户点击底部导航栏时,这个值会更新,从而触发页面的切换。@State showAddModal: boolean = false:控制添加弹框的显示与隐藏。初始为false(隐藏),当用户点击"写日记"或"添加成长记录"按钮时设为true。@State showEditModal: boolean = false:控制编辑弹框的显示与隐藏。@State showDeleteModal: boolean = false:控制删除确认弹框的显示与隐藏。@State showDetailModal: boolean = false:控制详情弹框的显示与隐藏。这个弹框是一个多用途弹框,既能展示日记详情,也能展示帖子详情。@State selectedDate: number = 4:当前选中的日期 ID,初始值为 4,对应模拟数据中的"今天"(8 月 8 日)。@State selectedPostId: number = 0:当前选中的帖子 ID。当用户在社区中点击"查看详情"时,这个值会被设置为对应帖子的 ID,详情弹框据此渲染对应内容。@State selectedDiaryId: number = 0:当前选中的日记 ID,作用类似于selectedPostId。@State milestoneAnimIndex: number = -1:当前触发动画的里程碑索引。初始值为 -1 表示没有里程碑正在播放动画。
@State 装饰器是声明式 UI 的核心机制之一。被 @State 修饰的变量一旦发生变化,框架会自动重新渲染依赖该变量的 UI 部分。这种"状态驱动视图"的模式让开发者无需手动操作 DOM,大大简化了状态管理的复杂度。
3.2 遮罩层构建器
@Builder modalOverlay(onClose: () => void) {
Column().width('100%').height('100%').backgroundColor('rgba(0,0,0,0.5)').onClick(onClose)
}
@Builder 是一个用于构建可复用 UI 片段的装饰器。这里的 modalOverlay 构建器用于生成所有弹框共用的半透明遮罩层。让我们逐行解析:
@Builder modalOverlay(onClose: () => void):定义了一个名为modalOverlay的构建器,接受一个回调函数onClose作为参数。这个回调会在用户点击遮罩层时被调用,用于关闭弹框。Column().width('100%').height('100%'):创建一个占满整个屏幕的列容器。.backgroundColor('rgba(0,0,0,0.5)'):设置背景色为半透明黑色,透明度为 0.5,营造出弹框的遮罩效果。.onClick(onClose):为遮罩层绑定点击事件,点击时执行传入的onClose回调。
这种将遮罩层抽取为独立构建器的做法体现了良好的代码复用思想。所有四个弹框都共享同一个遮罩层实现,避免了重复代码。
3.3 build 方法:页面的组装入口
build() {
Column() {
if (this.currentTab === 0) {
this.diaryTab()
}
if (this.currentTab === 1) {
this.growthTab()
}
if (this.currentTab === 2) {
this.vaccineTab()
}
if (this.currentTab === 3) {
this.communityTab()
}
if (this.currentTab === 4) {
this.mineTab()
}
this.bottomTabBar()
if (this.showAddModal) {
this.addModalView()
}
if (this.showEditModal) {
this.editModalView()
}
if (this.showDeleteModal) {
this.deleteModalView()
}
if (this.showDetailModal) {
this.detailModalView()
}
}.width('100%').height('100%').backgroundColor('#FCE4EC')
}
build() 方法是每个组件必须实现的方法,它描述了组件的 UI 结构。让我们详细分析这段代码:
- 最外层是一个
Column容器,宽度和高度都设为100%,背景色为#FCE4EC(浅粉色),为整个应用奠定了温馨的视觉基调。 - 接下来是五个
if条件判断,根据currentTab的值决定渲染哪个 Tab 页面。这种通过条件判断来切换页面的方式简单直接,适合 Tab 数量较少的场景。 this.bottomTabBar()始终渲染底部导航栏,它位于页面内容下方。- 最后是四个弹框的条件渲染。每个弹框都由对应的布尔状态变量控制,只有当状态为
true时才渲染。这种"按需渲染"的方式确保了弹框在隐藏时不占用任何渲染资源。
3.4 底部导航栏
@Builder bottomTabBar() {
Row() {
ForEach([0, 1, 2, 3, 4], (index: number) => {
Column() {
Text(this.getTabIcon(index))
.fontSize(22)
Text(this.getTabName(index))
.fontSize(11)
.fontColor(this.currentTab === index ? '#EC407A' : '#999999')
.margin({ top: 2 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
.onClick(() => {
this.currentTab = index
})
}, (index: number) => index.toString())
}
.width('100%')
.height(56)
.backgroundColor('#FFFFFF')
.border({ width: 1, color: '#F5F5F5' })
}
底部导航栏是应用的核心导航组件。逐行解析:
Row():最外层使用行容器,让五个 Tab 按钮水平排列。ForEach([0, 1, 2, 3, 4], ...):遍历 0 到 4 五个索引值,为每个索引生成一个 Tab 按钮。第三个参数(index: number) => index.toString()是键值生成器,将索引转为字符串作为列表项的唯一标识。- 每个 Tab 按钮是一个
Column容器,包含两行内容:上方是 emoji 图标(字号 22),下方是文字标签(字号 11)。 .fontColor(this.currentTab === index ? '#EC407A' : '#999999'):文字颜色根据当前选中的 Tab 动态变化。选中的 Tab 显示为粉色(#EC407A),未选中的显示为灰色(#999999),提供了清晰的视觉反馈。.layoutWeight(1):每个 Tab 按钮等分宽度,确保五个按钮均匀分布。.alignItems(HorizontalAlign.Center):内容水平居中对齐。.onClick(() => { this.currentTab = index }):点击时更新currentTab状态,触发页面切换。- 导航栏整体高度为 56,背景色为白色,顶部有一条 1 像素的浅灰色边框线,将导航栏与页面内容视觉分隔。
3.5 Tab 图标和名称的辅助方法
getTabIcon(index: number): string {
const icons: string[] = ['📔', '📈', '💉', '👥', '👤']
return icons[index]
}
getTabName(index: number): string {
const names: string[] = ['育儿日记', '成长记录', '疫苗提醒', '育儿社区', '我的']
return names[index]
}
这两个方法分别返回指定索引的 Tab 图标和名称。使用数组索引的方式简洁高效。图标采用 emoji 字符,与育儿主题高度契合:📔 代表日记本、📈 代表成长曲线、💉 代表疫苗注射、👥 代表社区人群、👤 代表个人。
四、Tab1:育儿日记页面
育儿日记是应用的第一个 Tab,也是使用频率最高的功能模块。这个页面让家长能够以日期为单位,记录宝宝每天的生活点滴。
4.1 页面顶部标题栏
@Builder diaryTab() {
Column() {
Row() {
Text('育儿日记')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
Column().layoutWeight(1)
Text('✏️')
.fontSize(24)
.onClick(() => {
this.showAddModal = true
})
}
.width('100%')
.height(56)
.padding({ left: 16, right: 16 })
这段代码构建了日记页面的顶部标题栏:
- 整个标题栏是一个
Row容器,高度 56,左右内边距各 16。 - 左侧是"育儿日记"标题文字,字号 20,粗体,深灰色(#333333)。
- 中间的
Column().layoutWeight(1)是一个弹性占位容器,它占据剩余空间,将右侧的编辑按钮推到最右边。这是一个非常实用的布局技巧——在行容器中插入一个layoutWeight(1)的空容器,可以实现"左右两端对齐"的效果。 - 右侧是编辑图标(✏️),字号 24,点击后设置
showAddModal = true,触发添加日记弹框的显示。
4.2 横向日期选择器
Scroll() {
Column() {
Scroll() {
Row({ space: 10 }) {
ForEach(MOCK_DATES, (item: DateItem) => {
Column() {
Text(item.weekday)
.fontSize(12)
.fontColor(this.selectedDate === item.id ? '#FFFFFF' : '#999999')
Text(item.day)
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor(this.selectedDate === item.id ? '#FFFFFF' : '#333333')
.margin({ top: 2 })
if (item.isToday) {
Text('今天')
.fontSize(10)
.fontColor(this.selectedDate === item.id ? '#FFFFFF' : '#EC407A')
.margin({ top: 2 })
}
}
.width(50)
.height(70)
.justifyContent(FlexAlign.Center)
.backgroundColor(this.selectedDate === item.id ? '#EC407A' : '#FFFFFF')
.borderRadius(12)
.border({ width: 1, color: '#F5F5F5' })
.onClick(() => {
this.selectedDate = item.id
})
}, (item: DateItem) => item.id.toString())
}
.padding({ left: 16, right: 16 })
}
.scrollable(ScrollDirection.Horizontal)
.scrollBar(BarState.Off)
.width('100%')
.height(90)
.margin({ top: 8 })
这段代码实现了一个横向滑动的日期选择器,是日记页面最突出的交互特色之一:
- 最外层是一个
Scroll容器,设置.scrollable(ScrollDirection.Horizontal)使其支持水平滚动,.scrollBar(BarState.Off)隐藏滚动条,保持界面整洁。 - 内部是一个
Row({ space: 10 })容器,子元素间距为 10。 ForEach遍历MOCK_DATES数组,为每个日期生成一个卡片。- 每个日期卡片是一个
Column容器,宽 50、高 70,圆角 12。 - 卡片内包含三行文字:星期几(字号 12)、日期数字(字号 18,粗体),以及当
isToday为true时额外显示的"今天"标签(字号 10)。 - 文字颜色根据选中状态动态变化:选中时为白色,未选中时为灰色或深灰色。
- 背景色同样根据选中状态变化:选中时为粉色(#EC407A),未选中时为白色。
- 卡片带有 1 像素浅灰色边框,增加视觉层次感。
- 点击卡片时更新
selectedDate状态,触发选中样式的切换。
4.3 今日日记卡片
Column() {
Row() {
Text('📝 今日日记')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
Column().layoutWeight(1)
Text('开心 😊')
.fontSize(13)
.fontColor('#EC407A')
}
.width('100%')
Column({ space: 10 }) {
Row({ space: 8 }) {
Text('🍽️')
.fontSize(18)
Text('饮食')
.fontSize(13)
.fontColor('#666666')
.layoutWeight(1)
Text('米糊+南瓜泥')
.fontSize(13)
.fontColor('#333333')
}
.width('100%')
Row({ space: 8 }) {
Text('😴')
.fontSize(18)
Text('睡眠')
.fontSize(13)
.fontColor('#666666')
.layoutWeight(1)
Text('10小时30分')
.fontSize(13)
.fontColor('#333333')
}
.width('100%')
Row({ space: 8 }) {
Text('😊')
.fontSize(18)
Text('心情')
.fontSize(13)
.fontColor('#666666')
.layoutWeight(1)
Text('开心活泼')
.fontSize(13)
.fontColor('#333333')
}
.width('100%')
Row({ space: 8 }) {
Text('📌')
.fontSize(18)
Text('备注')
.fontSize(13)
.fontColor('#666666')
.layoutWeight(1)
Text('今天宝宝特别开心,学会了拍手')
.fontSize(13)
.fontColor('#333333')
}
.width('100%')
}
.margin({ top: 12 })
Row() {
Text('编辑')
.fontSize(13)
.fontColor('#EC407A')
.onClick(() => {
this.showEditModal = true
})
Text('删除')
.fontSize(13)
.fontColor('#F44336')
.margin({ left: 16 })
.onClick(() => {
this.showDeleteModal = true
})
}
.width('100%')
.margin({ top: 12 })
.justifyContent(FlexAlign.End)
}
.width('100%')
.padding(16)
.backgroundColor('#FFFFFF')
.borderRadius(16)
.margin({ left: 16, right: 16, top: 12 })
这段代码构建了今日日记的详细展示卡片,是日记页面的信息核心区域。让我们逐段分析:
卡片头部:包含"📝 今日日记"标题和心情标签。心情标签"开心 😊"以粉色显示在右侧。
信息列表:使用 Column({ space: 10 }) 创建一个垂直列表,行间距为 10。列表包含四行信息,每行采用"图标 + 标签 + 值"的三段式布局:
- 饮食行:🍽️ 图标 + "饮食"标签 + "米糊+南瓜泥"内容
- 睡眠行:😴 图标 + "睡眠"标签 + "10小时30分"内容
- 心情行:😊 图标 + "心情"标签 + "开心活泼"内容
- 备注行:📌 图标 + "备注"标签 + "今天宝宝特别开心,学会了拍手"内容
每行中,标签使用 layoutWeight(1) 占据中间空间,将值推到右侧,形成左对齐标签、右对齐值的清晰布局。
操作按钮:底部是"编辑"和"删除"两个文字按钮。编辑按钮为粉色(#EC407A),点击触发编辑弹框;删除按钮为红色(#F44336),点击触发删除确认弹框。两个按钮通过 .justifyContent(FlexAlign.End) 靠右对齐。
卡片样式:白色背景、16 像素圆角、16 像素内边距,左右外边距各 16,顶部外边距 12,营造出卡片浮于粉色背景之上的视觉效果。
4.4 历史日记列表
Row() {
Text('历史日记')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
Column().layoutWeight(1)
Text('共10条')
.fontSize(12)
.fontColor('#999999')
}
.width('100%')
.padding({ left: 16, right: 16, top: 20, bottom: 8 })
ForEach(MOCK_DIARIES, (item: DiaryItem) => {
Column() {
Row() {
Text(item.date)
.fontSize(12)
.fontColor('#999999')
Column().layoutWeight(1)
Text('☀️ ' + item.weather)
.fontSize(12)
.fontColor('#666666')
}
.width('100%')
Text(item.title)
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
.margin({ top: 6 })
Row({ space: 8 }) {
if (item.hasImage) {
Column()
.width(60)
.height(60)
.backgroundColor('#FCE4EC')
.borderRadius(8)
}
Text(item.summary)
.fontSize(13)
.fontColor('#666666')
.maxLines(2)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.layoutWeight(1)
}
.width('100%')
.margin({ top: 8 })
Row() {
Text('心情: ' + item.mood)
.fontSize(12)
.fontColor('#EC407A')
Column().layoutWeight(1)
Text('查看详情 >')
.fontSize(12)
.fontColor('#999999')
.onClick(() => {
this.selectedDiaryId = item.id
this.showDetailModal = true
})
}
.width('100%')
.margin({ top: 8 })
}
.width('100%')
.padding(14)
.backgroundColor('#FFFFFF')
.borderRadius(12)
.margin({ left: 16, right: 16, bottom: 10 })
}, (item: DiaryItem) => item.id.toString())
历史日记列表是日记页面的主体内容区域,展示了过去多天的日记记录。详细分析如下:
列表标题行:包含"历史日记"标题和"共10条"计数,采用左标题右计数的布局。
列表项渲染:ForEach 遍历 MOCK_DIARIES 数组,为每条日记生成一个卡片。每个卡片包含以下部分:
-
日期与天气行:左侧显示日期(如"2026-08-08"),右侧显示天气(如"☀️ 晴"),用
Column().layoutWeight(1)分隔两端。 -
标题:日记标题,字号 15,粗体,深灰色。
-
摘要与图片行:如果日记包含图片(
hasImage为true),则在左侧显示一个 60x60 的粉色占位块。右侧是日记摘要文字,使用.maxLines(2)限制为两行,.textOverflow({ overflow: TextOverflow.Ellipsis })在文字溢出时显示省略号。这种处理确保了列表项的高度一致,不会因为内容长短不同而参差不齐。 -
心情与详情入口行:左侧显示心情标签(粉色),右侧是"查看详情 >"链接。点击链接时,先设置
selectedDiaryId为当前日记的 ID,然后设置showDetailModal = true触发详情弹框。这种"先记录选择、再打开弹框"的模式是列表-详情交互的标准做法。
卡片样式:白色背景、12 像素圆角、14 像素内边距,底部外边距 10,形成卡片之间的间距。
五、Tab2:成长记录页面
成长记录页面是应用的第二个 Tab,专注于追踪宝宝的身体发育和运动能力发展。这个页面的特色在于它不仅展示数据表格,还包含了一个简易的柱状图来可视化生长趋势,以及发育里程碑的交互式列表。
5.1 宝宝信息卡
@Builder growthTab() {
Column() {
Row() {
Text('成长记录')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
Column().layoutWeight(1)
Text('➕')
.fontSize(24)
.onClick(() => {
this.showAddModal = true
})
}
.width('100%')
.height(56)
.padding({ left: 16, right: 16 })
Scroll() {
Column() {
Row({ space: 14 }) {
Column()
.width(64)
.height(64)
.borderRadius(32)
.backgroundColor('#F8BBD0')
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
Column({ space: 4 }) {
Text('小花朵')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
Row({ space: 8 }) {
Text('1岁0月')
.fontSize(13)
.fontColor('#666666')
Text('|')
.fontSize(13)
.fontColor('#CCCCCC')
Text('女')
.fontSize(13)
.fontColor('#EC407A')
Text('|')
.fontSize(13)
.fontColor('#CCCCCC')
Text('生日: 2025-08-08')
.fontSize(12)
.fontColor('#999999')
}
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
}
.width('100%')
.padding(16)
.backgroundColor('#FFFFFF')
.borderRadius(16)
.margin({ left: 16, right: 16 })
宝宝信息卡位于成长记录页面的顶部,展示了当前选中宝宝的基本信息:
- 标题栏与日记页面结构一致,右侧的 ➕ 按钮用于添加新的成长记录。
- 信息卡采用
Row({ space: 14 })布局,左侧是一个 64x64 的圆形头像占位块(背景色 #F8BBD0,圆角 32 实现圆形效果)。 - 右侧是一个
Column容器,包含宝宝名称和多行信息。 - 名称"小花朵"使用 18 号粗体字。
- 信息行使用竖线分隔符(|)将年龄、性别、生日三项信息串联在一起。性别"女"以粉色显示,其余信息为灰色,通过颜色差异突出关键信息。
- 卡片整体白色背景、16 像素圆角,与日记卡片保持视觉一致性。
5.2 身高体重记录表格
Text('身高体重记录')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
.width('100%')
.padding({ left: 16, top: 20, bottom: 8 })
Column() {
Row() {
Text('月龄')
.fontSize(12)
.fontColor('#999999')
.layoutWeight(1)
Text('身高(cm)')
.fontSize(12)
.fontColor('#999999')
.layoutWeight(1)
Text('体重(kg)')
.fontSize(12)
.fontColor('#999999')
.layoutWeight(1)
Text('百分位')
.fontSize(12)
.fontColor('#999999')
.layoutWeight(1)
}
.width('100%')
.padding({ top: 8, bottom: 8 })
ForEach(MOCK_GROWTH, (item: GrowthRecord) => {
Row() {
Text(item.monthAge + '月')
.fontSize(13)
.fontColor('#333333')
.layoutWeight(1)
Text(item.height.toString())
.fontSize(13)
.fontColor('#333333')
.layoutWeight(1)
Text(item.weight.toString())
.fontSize(13)
.fontColor('#333333')
.layoutWeight(1)
Text(item.percentile)
.fontSize(13)
.fontColor('#EC407A')
.layoutWeight(1)
}
.width('100%')
.padding({ top: 10, bottom: 10 })
.border({ width: 1, color: '#F5F5F5' })
}, (item: GrowthRecord) => item.id.toString())
}
.width('100%')
.padding(14)
.backgroundColor('#FFFFFF')
.borderRadius(16)
.margin({ left: 16, right: 16 })
这段代码实现了一个四列的数据表格,用于展示历次身高体重测量记录:
- 表头行包含"月龄"“身高(cm)”“体重(kg)”"百分位"四列,每列使用
layoutWeight(1)等分宽度,字号 12,灰色。 - 数据行通过
ForEach遍历MOCK_GROWTH生成,每行结构与表头一致。值得注意的是,百分位列的文字颜色设为粉色(#EC407A),以突出这个关键指标。 - 每个数据行之间通过 1 像素的浅灰色边框线分隔,形成表格的网格效果。
- 整个表格包裹在一个白色卡片中,16 像素圆角,14 像素内边距。
5.3 生长趋势柱状图
Text('生长趋势')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
.width('100%')
.padding({ left: 16, top: 20, bottom: 8 })
Column() {
Row({ space: 8 }) {
Column()
.width(10)
.height(10)
.borderRadius(5)
.backgroundColor('#EC407A')
Text('身高趋势')
.fontSize(12)
.fontColor('#666666')
Column()
.width(10)
.height(10)
.borderRadius(5)
.backgroundColor('#42A5F5')
.margin({ left: 12 })
Text('体重趋势')
.fontSize(12)
.fontColor('#666666')
}
.width('100%')
Row({ space: 12 }) {
ForEach(MOCK_GROWTH, (item: GrowthRecord) => {
Column({ space: 4 }) {
Column()
.width(12)
.height(item.height * 0.6)
.backgroundColor('#EC407A')
.borderRadius(3)
Column()
.width(12)
.height(item.weight * 4)
.backgroundColor('#42A5F5')
.borderRadius(3)
.margin({ top: 4 })
Text(item.monthAge + '月')
.fontSize(10)
.fontColor('#999999')
}
.alignItems(HorizontalAlign.Center)
}, (item: GrowthRecord) => item.id.toString())
}
.width('100%')
.height(120)
.margin({ top: 12 })
.justifyContent(FlexAlign.Center)
}
.width('100%')
.padding(16)
.backgroundColor('#FFFFFF')
.borderRadius(16)
.margin({ left: 16, right: 16 })
这是整个应用中最具技术亮点的部分之一——一个纯 UI 组件实现的简易柱状图。详细解析:
图例区域:顶部是图例,包含两个色块和文字说明。粉色圆点(#EC407A)代表身高趋势,蓝色圆点(#42A5F5)代表体重趋势。圆点尺寸为 10x10,圆角 5 实现圆形效果。
柱状图区域:使用 Row({ space: 12 }) 水平排列每个数据点的柱状组。每个数据组是一个 Column,包含:
- 身高柱:宽度 12,高度为
item.height * 0.6。这里通过将身高值乘以 0.6 来缩放柱子高度,使其适合在 120 像素高的容器中显示。例如,身高 76.5 对应柱高约 46 像素。 - 体重柱:宽度 12,高度为
item.weight * 4。体重值较小(4.8-9.8),因此乘以较大的系数 4 来放大显示效果。例如,体重 9.8 对应柱高约 39 像素。 - 月龄标签:在柱子下方显示月龄,字号 10,灰色。
两个柱子并排放置,间距 4 像素(通过 margin({ top: 4 }) 实现),让用户可以直观对比同一时间点的身高和体重变化。整个图表区域高度 120,内容居中对齐。
这种"用容器尺寸驱动图表"的实现方式虽然简单,但巧妙地利用了声明式 UI 的特性——数据直接映射到组件的尺寸属性上,无需引入图表库即可实现可视化效果。
5.4 发育里程碑列表(含交互动画)
Text('发育里程碑')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
.width('100%')
.padding({ left: 16, top: 20, bottom: 8 })
ForEach(MOCK_MILESTONES, (item: Milestone, index: number) => {
Row({ space: 12 }) {
Column() {
Text('🏆')
.fontSize(24)
.scale(this.milestoneAnimIndex === index ? { x: 1.3, y: 1.3 } : { x: 1.0, y: 1.0 })
.animation({ duration: 300 })
}
.width(44)
.height(44)
.borderRadius(22)
.backgroundColor('#FCE4EC')
.justifyContent(FlexAlign.Center)
.onClick(() => {
this.milestoneAnimIndex = index
setTimeout(() => {
this.milestoneAnimIndex = -1
}, 300)
})
Column({ space: 4 }) {
Row() {
Text(item.name)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
Column().layoutWeight(1)
Text(item.monthAge + '月龄')
.fontSize(11)
.fontColor('#EC407A')
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
.backgroundColor('#FCE4EC')
.borderRadius(8)
}
.width('100%')
Text(item.date + ' 达成')
.fontSize(12)
.fontColor('#999999')
Text(item.note)
.fontSize(12)
.fontColor('#666666')
.margin({ top: 2 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
}
.width('100%')
.padding(12)
.backgroundColor('#FFFFFF')
.borderRadius(12)
.margin({ left: 16, right: 16, bottom: 8 })
}, (item: Milestone) => item.id.toString())
发育里程碑列表是成长记录页面的最后一个模块,它的特色在于包含了一个点击放大动画效果。详细分析:
ForEach遍历MOCK_MILESTONES,注意第二个参数除了item外还接收了index,这个索引用于动画控制。- 每个里程碑项是一个
Row,左侧是奖杯图标区域,右侧是文字信息区域。 - 奖杯图标:放在一个 44x44 的圆形容器中(圆角 22),背景色为浅粉色(#FCE4EC)。图标使用 🏆 emoji,字号 24。
- 动画效果:
.scale()方法根据milestoneAnimIndex的值决定缩放比例。当milestoneAnimIndex === index时,缩放为 1.3 倍(放大效果);否则保持 1.0 倍(原始大小)。.animation({ duration: 300 })为缩放变化添加 300 毫秒的过渡动画。 - 点击交互:点击奖杯图标时,先将
milestoneAnimIndex设为当前索引(触发放大动画),然后通过setTimeout在 300 毫秒后将其重置为 -1(恢复原始大小)。这样就实现了一个"点击放大、然后回弹"的动画效果。 - 文字信息:右侧包含三行内容——里程碑名称和月龄标签(同一行,月龄标签为粉色背景的小圆角标签)、达成日期、补充说明。
这个动画效果虽然简单,但为应用增添了不少趣味性。家长在查看宝宝达成的里程碑时,点击奖杯图标看到它放大弹回,仿佛在庆祝宝宝的成长成就,这种情感化的设计正是育儿类应用所需要的。
六、Tab3:疫苗提醒页面
疫苗提醒页面是应用的第三个 Tab,它的核心职责是帮助家长管理宝宝的疫苗接种计划。这个页面的设计重点在于清晰区分"已种"和"待种"两种状态,并提供下次接种的倒计时提醒。
6.1 下次接种提醒卡片
@Builder vaccineTab() {
Column() {
Row() {
Text('疫苗提醒')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
Column().layoutWeight(1)
Text('🔔')
.fontSize(22)
}
.width('100%')
.height(56)
.padding({ left: 16, right: 16 })
Scroll() {
Column() {
Column() {
Row({ space: 8 }) {
Text('⏰')
.fontSize(28)
Column({ space: 4 }) {
Text('下次接种提醒')
.fontSize(14)
.fontColor('#FFFFFF')
.opacity(0.9)
Text('A群流脑疫苗(第一针)')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
}
.width('100%')
Row() {
Text('接种日期: 2026-08-15')
.fontSize(12)
.fontColor('#FFFFFF')
.opacity(0.9)
Column().layoutWeight(1)
Text('还有7天')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
}
.width('100%')
.margin({ top: 10 })
}
.width('100%')
.padding(16)
.backgroundColor('#EC407A')
.borderRadius(16)
.margin({ left: 16, right: 16 })
这段代码构建了一个醒目的粉色提醒卡片,是疫苗页面的视觉焦点:
- 卡片整体背景色为粉色(#EC407A),与页面其他白色卡片形成鲜明对比,立即吸引用户的注意力。
- 上半部分包含一个 ⏰ 图标(字号 28)和文字信息。"下次接种提醒"为副标题(字号 14,白色,透明度 0.9),疫苗名称"A群流脑疫苗(第一针)"为主标题(字号 16,粗体,白色)。
- 下半部分包含接种日期和倒计时。"接种日期: 2026-08-15"在左侧,"还有7天"在右侧,倒计时使用粗体白色字突出显示,让家长一目了然地知道距离下次接种还有多少时间。
- 透明度 0.9 的运用让副标题文字在粉色背景上不会过于刺眼,形成了主次分明的视觉层次。
6.2 接种时间线
Text('接种时间线')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
.width('100%')
.padding({ left: 16, top: 20, bottom: 8 })
ForEach(MOCK_VACCINES, (item: VaccineItem) => {
Row({ space: 12 }) {
Column() {
Text(this.getVaccineIcon(item.status))
.fontSize(18)
}
.width(40)
.height(40)
.borderRadius(20)
.backgroundColor(this.getVaccineBgColor(item.status))
.justifyContent(FlexAlign.Center)
Column()
.width(2)
.height(40)
.backgroundColor('#E0E0E0')
.position({ x: 19, y: 40 })
Column({ space: 4 }) {
Row() {
Text(item.name)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
.layoutWeight(1)
Text(item.status)
.fontSize(11)
.fontColor('#FFFFFF')
.padding({ left: 8, right: 8, top: 3, bottom: 3 })
.backgroundColor(this.getVaccineColor(item.status))
.borderRadius(8)
}
.width('100%')
Row({ space: 10 }) {
Text('📅 ' + item.date)
.fontSize(12)
.fontColor('#999999')
Text('👶 ' + item.age)
.fontSize(12)
.fontColor('#999999')
}
Text(item.desc)
.fontSize(12)
.fontColor('#666666')
.margin({ top: 2 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
}
.width('100%')
.padding(12)
.backgroundColor('#FFFFFF')
.borderRadius(12)
.margin({ left: 16, right: 16, bottom: 8 })
}, (item: VaccineItem) => item.id.toString())
接种时间线是疫苗页面的主体内容,以时间轴的形式展示所有疫苗记录。详细分析:
- 每个疫苗项是一个
Row,包含三个部分:状态指示器、连接竖线和内容区域。 - 状态指示器:一个 40x40 的圆形容器,内部显示状态图标。图标和背景色都通过辅助方法根据
status字段动态获取——"已种"显示 ✅ 和浅绿色背景,"待种"显示 ⏳ 和浅橙色背景,其他状态显示 ⚠️ 和浅红色背景。 - 连接竖线:一个宽 2 像素、高 40 像素的灰色竖线,使用
.position({ x: 19, y: 40 })定位在状态指示器下方,将各个时间线节点串联起来,形成连贯的时间轴视觉效果。 - 内容区域:包含疫苗名称和状态标签(同一行)、日期和年龄(同一行)、疫苗描述三行内容。状态标签是一个彩色小标签,背景色同样通过辅助方法动态获取。
- 每个疫苗项包裹在白色卡片中,12 像素圆角,底部 8 像素外边距。
6.3 疫苗状态辅助方法
getVaccineIcon(status: string): string {
if (status === '已种') {
return '✅'
}
if (status === '待种') {
return '⏳'
}
return '⚠️'
}
getVaccineColor(status: string): string {
if (status === '已种') {
return '#4CAF50'
}
if (status === '待种') {
return '#FF9800'
}
return '#F44336'
}
getVaccineBgColor(status: string): string {
if (status === '已种') {
return '#E8F5E9'
}
if (status === '待种') {
return '#FFF3E0'
}
return '#FFEBEE'
}
这三个辅助方法是疫苗模块状态管理的核心,它们将数据层的 status 字符串映射为界面层的视觉元素:
getVaccineIcon:返回状态对应的 emoji 图标。已种 → ✅(勾选),待种 → ⏳(沙漏),其他 → ⚠️(警告)。getVaccineColor:返回状态标签的背景色。已种 → #4CAF50(绿色),待种 → #FF9800(橙色),其他 → #F44336(红色)。这组颜色遵循了通用的状态色彩语义——绿色表示完成、橙色表示待办、红色表示异常。getVaccineBgColor:返回状态指示器的浅色背景。已种 → #E8F5E9(浅绿),待种 → #FFF3E0(浅橙),其他 → #FFEBEE(浅红)。这些浅色背景与对应的图标和标签颜色形成同色系的搭配。
将状态到视觉的映射逻辑抽取为独立方法,是良好的编程实践。它集中管理了状态样式的定义,当需要修改颜色方案或新增状态类型时,只需修改这些方法即可,无需在多处散落的代码中查找和替换。
七、Tab4:育儿社区页面
育儿社区页面是应用的第四个 Tab,为家长提供了一个分享和交流的平台。这个页面展示了热门话题和帖子列表,让家长可以浏览其他用户的育儿经验。
7.1 热门话题横滑区域
@Builder communityTab() {
Column() {
Row() {
Text('育儿社区')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
Column().layoutWeight(1)
Text('🔍')
.fontSize(22)
}
.width('100%')
.height(56)
.padding({ left: 16, right: 16 })
Scroll() {
Column() {
Column() {
Text('🔥 热门话题')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
.width('100%')
Scroll() {
Row({ space: 10 }) {
ForEach(MOCK_TOPICS, (item: TopicItem) => {
Column({ space: 4 }) {
Text('# ' + item.name)
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor('#EC407A')
Text(item.count + '人参与')
.fontSize(11)
.fontColor('#999999')
}
.padding(12)
.backgroundColor('#FFFFFF')
.borderRadius(12)
.border({ width: 1, color: '#FCE4EC' })
}, (item: TopicItem) => item.id.toString())
}
.padding({ left: 16, right: 16, top: 10 })
}
.scrollable(ScrollDirection.Horizontal)
.scrollBar(BarState.Off)
.width('100%')
}
.width('100%')
.padding({ left: 0, right: 0, top: 10, bottom: 10 })
热门话题区域位于社区页面的顶部,采用横向滑动的设计:
- 标题"🔥 热门话题"使用 15 号粗体字。
- 横向
Scroll容器内部是一个Row({ space: 10 }),每个话题卡片间距 10。 - 每个话题卡片包含两行内容:话题名称(以 "# " 开头,粉色粗体)和参与人数(灰色小字)。
- 卡片样式为白色背景、12 像素圆角、浅粉色边框(#FCE4EC),与页面的粉色主题呼应。
- 横向滑动、隐藏滚动条的设计让用户可以在有限的空间内浏览更多话题。
7.2 帖子列表
ForEach(MOCK_POSTS, (item: CommunityPost) => {
Column() {
Row({ space: 10 }) {
Column()
.width(40)
.height(40)
.borderRadius(20)
.backgroundColor('#F8BBD0')
Column({ space: 3 }) {
Text(item.author)
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
Row() {
Text(item.babyAge)
.fontSize(10)
.fontColor('#EC407A')
.padding({ left: 6, right: 6, top: 1, bottom: 1 })
.backgroundColor('#FCE4EC')
.borderRadius(8)
}
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
}
.width('100%')
Text(item.title)
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
.margin({ top: 8 })
Row({ space: 10 }) {
if (item.hasImage) {
Column()
.width(80)
.height(60)
.backgroundColor('#FCE4EC')
.borderRadius(8)
}
Text(item.summary)
.fontSize(13)
.fontColor('#666666')
.maxLines(2)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.layoutWeight(1)
}
.width('100%')
.margin({ top: 8 })
Row() {
Text('❤️ ' + item.likes)
.fontSize(12)
.fontColor('#999999')
Text('💬 ' + item.comments)
.fontSize(12)
.fontColor('#999999')
.margin({ left: 16 })
Column().layoutWeight(1)
Text('查看详情 >')
.fontSize(12)
.fontColor('#EC407A')
.onClick(() => {
this.selectedPostId = item.id
this.showDetailModal = true
})
}
.width('100%')
.margin({ top: 10 })
}
.width('100%')
.padding(14)
.backgroundColor('#FFFFFF')
.borderRadius(12)
.margin({ left: 16, right: 16, bottom: 10 })
}, (item: CommunityPost) => item.id.toString())
帖子列表是社区页面的主体内容。每个帖子卡片包含以下部分:
-
作者信息行:左侧是 40x40 的圆形头像占位块,右侧是作者昵称和宝宝年龄标签。宝宝年龄以粉色小标签形式展示,让读者快速了解发帖人的背景。
-
帖子标题:15 号粗体字,深灰色。
-
摘要与图片行:如果帖子包含图片,左侧显示 80x60 的图片占位块。右侧是摘要文字,限制为两行并显示省略号。
-
互动数据行:左侧显示点赞数(❤️ + 数字)和评论数(💬 + 数字),右侧是"查看详情 >"链接。点击链接时设置
selectedPostId并打开详情弹框。
帖子列表的整体结构与日记列表非常相似,这体现了应用在交互模式上的一致性——用户在不同模块中都能使用相同的操作习惯来浏览内容。
八、Tab5:我的页面
"我的"页面是应用的第五个 Tab,也是个人中心模块。这个页面集成了家长信息展示、宝宝档案管理、育儿统计和设置入口等功能。
8.1 家长信息卡
@Builder mineTab() {
Column() {
Row() {
Text('我的')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
Column().layoutWeight(1)
Text('⚙️')
.fontSize(22)
}
.width('100%')
.height(56)
.padding({ left: 16, right: 16 })
Scroll() {
Column() {
Row({ space: 14 }) {
Column()
.width(60)
.height(60)
.borderRadius(30)
.backgroundColor('#F8BBD0')
Column({ space: 4 }) {
Text('花朵妈妈')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
Text('注册时间: 2025-08-08')
.fontSize(12)
.fontColor('#999999')
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
}
.width('100%')
.padding(16)
.backgroundColor('#FFFFFF')
.borderRadius(16)
.margin({ left: 16, right: 16 })
家长信息卡与成长记录页面的宝宝信息卡结构类似:左侧是圆形头像占位块(60x60,圆角 30),右侧是昵称和注册时间。
8.2 宝宝档案切换
Text('宝宝档案')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
.width('100%')
.padding({ left: 16, top: 20, bottom: 8 })
Scroll() {
Row({ space: 12 }) {
ForEach(MOCK_BABIES, (item: BabyInfo) => {
Column({ space: 6 }) {
Column()
.width(56)
.height(56)
.borderRadius(28)
.backgroundColor('#FCE4EC')
Text(item.name)
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
Text(item.age)
.fontSize(11)
.fontColor('#999999')
}
.padding(12)
.backgroundColor('#FFFFFF')
.borderRadius(12)
.border({ width: 1, color: '#FCE4EC' })
}, (item: BabyInfo) => item.id.toString())
Column({ space: 6 }) {
Column()
.width(56)
.height(56)
.borderRadius(28)
.backgroundColor('#F5F5F5')
.justifyContent(FlexAlign.Center)
Text('➕')
.fontSize(24)
.fontColor('#CCCCCC')
}
.padding(12)
.backgroundColor('#FFFFFF')
.borderRadius(12)
.border({ width: 1, color: '#E0E0E0' })
}
.padding({ left: 16, right: 16 })
}
.scrollable(ScrollDirection.Horizontal)
.scrollBar(BarState.Off)
.width('100%')
宝宝档案切换区域采用横向滑动设计,支持多宝宝管理:
ForEach遍历MOCK_BABIES,为每个宝宝生成一个档案卡片。卡片包含圆形头像占位块、宝宝名称和年龄。- 在所有宝宝卡片之后,额外渲染了一个"添加宝宝"卡片。这个卡片的头像占位块背景色为浅灰色(#F5F5F5),中间显示 ➕ 图标,边框颜色为灰色(#E0E0E0),与正常宝宝卡片的粉色边框形成区分。
- 这种"列表 + 末尾添加项"的设计模式在许多应用中都能见到,它让用户一眼就能发现添加新内容的入口。
8.3 育儿统计
Text('育儿统计')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
.width('100%')
.padding({ left: 16, top: 20, bottom: 8 })
Row() {
ForEach(MOCK_STATS, (item: StatItem) => {
Column({ space: 6 }) {
Text(item.value.toString())
.fontSize(24)
.fontWeight(FontWeight.Bold)
.fontColor('#EC407A')
Text(item.label)
.fontSize(12)
.fontColor('#666666')
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
}, (item: StatItem) => item.label)
}
.width('100%')
.padding(16)
.backgroundColor('#FFFFFF')
.borderRadius(16)
.margin({ left: 16, right: 16 })
育儿统计区域以三列等分布局展示用户的活跃数据:
- 每个统计项包含一个大号数字(字号 24,粉色,粗体)和一个小号标签(字号 12,灰色)。
- 三项统计分别是"育儿日记 128""成长记录 24"“社区互动 365”,全面反映了用户在应用中的使用情况。
ForEach的键值生成器使用item.label而非id,因为StatItem接口没有定义id字段。这是一个值得注意的细节——键值生成器不一定要使用 id,任何能唯一标识列表项的字段都可以。- 每个统计项使用
layoutWeight(1)等分宽度,alignItems(HorizontalAlign.Center)让内容居中对齐。
8.4 设置列表
Text('设置')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
.width('100%')
.padding({ left: 16, top: 20, bottom: 8 })
ForEach(MOCK_SETTINGS, (item: SettingItem) => {
Row({ space: 12 }) {
Text(item.icon)
.fontSize(22)
Column({ space: 2 }) {
Text(item.title)
.fontSize(14)
.fontColor('#333333')
Text(item.subtitle)
.fontSize(12)
.fontColor('#999999')
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Text('>')
.fontSize(16)
.fontColor('#CCCCCC')
}
.width('100%')
.padding(14)
.backgroundColor('#FFFFFF')
.borderRadius(12)
.margin({ left: 16, right: 16, bottom: 8 })
}, (item: SettingItem) => item.id.toString())
设置列表是"我的"页面的最后一个模块:
- 每个设置项是一个
Row,包含图标、标题副标题和右箭头三部分。 - 图标使用 emoji(🔔🔒📖❤️),字号 22。
- 标题和副标题垂直排列,标题为 14 号深灰色,副标题为 12 号浅灰色。
- 右侧的 “>” 箭头是移动端列表项的经典设计语言,暗示该项可以点击进入下一级页面。
- 四个设置项分别对应提醒设置、隐私设置、宝宝档案和关于我们,覆盖了应用的核心配置入口。
九、弹框系统:统一的交互层
弹框是应用中处理用户交互的重要手段。这款应用实现了四个弹框:添加弹框、编辑弹框、删除确认弹框和详情弹框。它们共享相同的遮罩层机制,但在内容上各有不同。
9.1 添加弹框
@Builder addModalView() {
Column() {
this.modalOverlay(() => {
this.showAddModal = false
})
Column() {
Text(this.currentTab === 0 ? '写育儿日记' : '添加成长记录')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
.margin({ bottom: 16 })
Scroll() {
Column({ space: 12 }) {
Column({ space: 6 }) {
Text('标题')
.fontSize(13)
.fontColor('#666666')
TextInput({ placeholder: '请输入标题' })
.fontSize(14)
.height(44)
.borderRadius(10)
.backgroundColor('#F5F5F5')
}
.width('100%')
.alignItems(HorizontalAlign.Start)
if (this.currentTab === 0) {
Column({ space: 6 }) {
Text('宝宝心情')
.fontSize(13)
.fontColor('#666666')
Row({ space: 10 }) {
ForEach(['开心', '平静', '兴奋', '担心', '难过'], (mood: string) => {
Text(mood)
.fontSize(13)
.fontColor('#333333')
.padding({ left: 14, right: 14, top: 8, bottom: 8 })
.backgroundColor('#FCE4EC')
.borderRadius(20)
}, (mood: string) => mood)
}
.width('100%')
}
.width('100%')
.alignItems(HorizontalAlign.Start)
添加弹框是一个多用途弹框,根据 currentTab 的值展示不同的表单内容。详细分析:
- 弹框最外层是一个
Column,首先渲染modalOverlay遮罩层(点击遮罩关闭弹框),然后渲染弹框内容。 - 弹框标题根据
currentTab动态变化:在日记页面打开时显示"写育儿日记",在成长记录页面打开时显示"添加成长记录"。 - 弹框内容使用
Scroll包裹,并通过.constraintSize({ maxHeight: '60%' })限制最大高度为屏幕的 60%,防止内容过多时溢出屏幕。 - 表单包含一个公共的"标题"输入框,使用
TextInput组件,占位文字为"请输入标题"。 - 当
currentTab === 0(日记页面)时,额外显示宝宝心情选择器(五个圆角标签按钮)、饮食输入框、睡眠输入框和备注文本域(TextArea)。 - 当
currentTab === 1(成长记录页面)时,显示身高、体重和月龄三个数字输入框。 - 表单底部是"取消"和"保存"两个按钮,取消按钮为灰色背景,保存按钮为粉色背景。两个按钮都使用
layoutWeight(1)等分宽度,点击后都会关闭弹框。
if (this.currentTab === 0) {
// ... 日记表单字段
} else {
Column({ space: 6 }) {
Text('身高(cm)')
.fontSize(13)
.fontColor('#666666')
TextInput({ placeholder: '请输入身高' })
.fontSize(14)
.height(44)
.borderRadius(10)
.backgroundColor('#F5F5F5')
}
.width('100%')
.alignItems(HorizontalAlign.Start)
Column({ space: 6 }) {
Text('体重(kg)')
.fontSize(13)
.fontColor('#666666')
TextInput({ placeholder: '请输入体重' })
.fontSize(14)
.height(44)
.borderRadius(10)
.backgroundColor('#F5F5F5')
}
.width('100%')
.alignItems(HorizontalAlign.Start)
Column({ space: 6 }) {
Text('月龄')
.fontSize(13)
.fontColor('#666666')
TextInput({ placeholder: '请输入月龄' })
.fontSize(14)
.height(44)
.borderRadius(10)
.backgroundColor('#F5F5F5')
}
.width('100%')
.alignItems(HorizontalAlign.Start)
}
这段 if-else 分支根据当前 Tab 渲染不同的表单字段。这种"一个弹框服务多个场景"的设计减少了弹框组件的数量,但通过条件判断实现了内容的差异化。
9.2 编辑弹框
@Builder editModalView() {
Column() {
this.modalOverlay(() => {
this.showEditModal = false
})
Column() {
Text('编辑日记')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
.margin({ bottom: 16 })
Column({ space: 12 }) {
Column({ space: 6 }) {
Text('标题')
.fontSize(13)
.fontColor('#666666')
TextInput({ text: '今天宝宝会叫妈妈了' })
.fontSize(14)
.height(44)
.borderRadius(10)
.backgroundColor('#F5F5F5')
}
.width('100%')
.alignItems(HorizontalAlign.Start)
编辑弹框的结构与添加弹框类似,但有一个关键区别:表单字段使用 text 参数而非 placeholder 参数来预填充已有数据。例如,TextInput({ text: '今天宝宝会叫妈妈了' }) 会将输入框的初始值设为"今天宝宝会叫妈妈了"。
编辑弹框固定为日记编辑场景,包含标题、心情选择器、饮食、睡眠和备注五个字段,每个字段都预填充了模拟数据。底部同样是"取消"和"保存"按钮。
9.3 删除确认弹框
@Builder deleteModalView() {
Column() {
this.modalOverlay(() => {
this.showDeleteModal = false
})
Column({ space: 16 }) {
Text('🗑️')
.fontSize(40)
Text('删除日记')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
Text('确定要删除这篇日记吗?删除后不可恢复。')
.fontSize(14)
.fontColor('#666666')
.textAlign(TextAlign.Center)
Row({ space: 12 }) {
Button('取消')
.fontSize(14)
.fontColor('#666666')
.layoutWeight(1)
.height(44)
.backgroundColor('#F5F5F5')
.borderRadius(10)
.onClick(() => {
this.showDeleteModal = false
})
Button('确定删除')
.fontSize(14)
.fontColor('#FFFFFF')
.layoutWeight(1)
.height(44)
.backgroundColor('#F44336')
.borderRadius(10)
.onClick(() => {
this.showDeleteModal = false
})
}
.width('100%')
.margin({ top: 8 })
}
.width('80%')
.padding(24)
.backgroundColor('#FFFFFF')
.borderRadius(20)
.alignItems(HorizontalAlign.Center)
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
删除确认弹框是一个典型的危险操作确认对话框:
- 顶部是一个大号垃圾桶图标(🗑️,字号 40),起到视觉警示作用。
- 标题"删除日记"使用 18 号粗体字。
- 提示文字"确定要删除这篇日记吗?删除后不可恢复。"使用 14 号灰色字,居中对齐,明确告知用户删除操作的不可逆性。
- 底部是"取消"和"确定删除"两个按钮。取消按钮为灰色背景,确定删除按钮为红色背景(#F44336),红色强化了危险操作的视觉提示。
- 弹框宽度为 80%,内边距 24,内容居中对齐,整体居中显示在屏幕上。
9.4 详情弹框
@Builder detailModalView() {
Column() {
this.modalOverlay(() => {
this.showDetailModal = false
})
Column() {
Text(this.selectedPostId > 0 ? '帖子详情' : '日记详情')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
.margin({ bottom: 16 })
Scroll() {
Column({ space: 12 }) {
if (this.selectedPostId > 0) {
ForEach(MOCK_POSTS, (item: CommunityPost) => {
if (item.id === this.selectedPostId) {
Column({ space: 8 }) {
Row({ space: 10 }) {
Column()
.width(40)
.height(40)
.borderRadius(20)
.backgroundColor('#F8BBD0')
Column({ space: 3 }) {
Text(item.author)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
Text(item.babyAge)
.fontSize(11)
.fontColor('#EC407A')
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
}
.width('100%')
Text(item.title)
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
.margin({ top: 4 })
Text(item.summary)
.fontSize(14)
.fontColor('#666666')
if (item.hasImage) {
Column()
.width('100%')
.height(150)
.backgroundColor('#FCE4EC')
.borderRadius(12)
}
Row() {
Text('❤️ ' + item.likes)
.fontSize(13)
.fontColor('#999999')
Text('💬 ' + item.comments)
.fontSize(13)
.fontColor('#999999')
.margin({ left: 16 })
}
.width('100%')
.margin({ top: 4 })
}
.width('100%')
}
}, (item: CommunityPost) => item.id.toString())
详情弹框是应用中最复杂的弹框,它是一个双用途弹框,既能展示帖子详情,也能展示日记详情。详细分析:
- 弹框标题根据
selectedPostId的值动态变化:大于 0 时显示"帖子详情",否则显示"日记详情"。 - 当
selectedPostId > 0时,渲染帖子详情。这里使用了一个巧妙的模式:ForEach遍历所有帖子,但通过if (item.id === this.selectedPostId)条件判断只渲染匹配的那一条。虽然这种做法在数据量大时效率不高,但在模拟数据场景下完全可行。 - 帖子详情包含作者信息、标题、摘要、图片占位块和互动数据,与列表项的内容一致但排版更加宽松。
- 帖子详情下方还渲染了一个评论区,展示 5 条模拟评论。每条评论包含头像占位块、评论者昵称和评论内容。
// 评论区
Text('评论 (5)')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
.width('100%')
.margin({ top: 8 })
ForEach(['小红妈妈', '大牛爸爸', '果果妈妈', '豆豆奶奶', '棉花糖妈妈'], (name: string, index: number) => {
Row({ space: 10 }) {
Column()
.width(32)
.height(32)
.borderRadius(16)
.backgroundColor('#F8BBD0')
Column({ space: 3 }) {
Text(name)
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
Text('写得很详细,对我帮助很大,谢谢分享!')
.fontSize(12)
.fontColor('#666666')
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
}
.width('100%')
.padding({ top: 8, bottom: 8 })
.border({ width: 1, color: '#F5F5F5' })
}, (name: string, index: number) => index.toString())
评论区使用 ForEach 直接遍历一个字符串数组(评论者昵称列表),而非遍历对象数组。键值生成器使用 index(索引值),因为字符串数组没有 id 字段。每条评论包含 32x32 的圆形头像占位块、评论者昵称和固定的评论内容。
当 selectedPostId 为 0 时,弹框切换到日记详情模式,使用相同的"遍历+条件匹配"模式从 MOCK_DIARIES 中查找匹配的日记并渲染详情。日记详情包含日期、天气、标题、摘要、图片占位块和心情标签。
十、关键特性对比总结
下面通过表格形式对应用的五大功能模块进行横向对比,帮助读者快速了解各模块的核心特性:
| 特性维度 | 育儿日记 | 成长记录 | 疫苗提醒 | 育儿社区 | 我的 |
|---|---|---|---|---|---|
| 核心功能 | 记录每日育儿生活 | 追踪身高体重发育 | 管理疫苗接种计划 | 分享交流育儿经验 | 个人信息与设置管理 |
| 主要交互 | 日期选择、日记增删改查 | 数据录入、图表查看 | 状态查看、倒计时提醒 | 话题浏览、帖子查看 | 档案切换、设置入口 |
| 数据模型 | DiaryItem / DiaryDetail | GrowthRecord / Milestone | VaccineItem | CommunityPost / TopicItem | BabyInfo / StatItem / SettingItem |
| 列表渲染 | ForEach 遍历日记数组 | ForEach 遍历成长记录 | ForEach 遍历疫苗记录 | ForEach 遍历帖子数组 | ForEach 遍历宝宝/设置 |
| 可视化元素 | 日期选择器卡片 | 柱状图、数据表格 | 时间线、状态指示器 | 话题横滑、帖子卡片 | 统计数字、档案卡片 |
| 弹框支持 | 添加、编辑、删除、详情 | 添加、详情 | 无 | 详情 | 无 |
| 动画效果 | 无 | 里程碑图标点击放大 | 无 | 无 | 无 |
| 状态管理 | selectedDate、selectedDiaryId | milestoneAnimIndex | 无 | selectedPostId | 无 |
| 横向滑动 | 日期选择器 | 无 | 无 | 热门话题 | 宝宝档案 |
| 色彩主题 | 粉色(#EC407A) | 粉色+蓝色(图表) | 粉色+绿/橙/红(状态) | 粉色 | 粉色 |
| 卡片圆角 | 12-16px | 12-16px | 12-16px | 12px | 12-16px |
十一、技术总结与深度思考
11.1 架构设计总结
通过对这款亲子育儿应用的完整代码分析,我们可以总结出以下几个关键的架构设计特点。
第一,接口驱动的数据模型设计。应用在文件开头定义了 11 个接口,覆盖了日记、成长、疫苗、社区、个人中心等所有业务领域。每个接口的字段都经过精心设计,既包含了展示所需的全部信息,又避免了冗余。特别是 DiaryItem 和 DiaryDetail 的分离设计,体现了"列表轻量化、详情完整化"的数据加载策略。
第二,状态集中管理。所有应用状态都定义在主组件中,通过 @State 装饰器进行管理。这种集中式状态管理的优点是简单直观、易于调试;对于这个规模的应用来说是最合适的选择。如果应用规模进一步扩大,可以考虑引入更复杂的状态管理方案,如全局状态存储。
第三,Builder 模式的组件化。应用将每个 Tab 页面和每个弹框都实现为独立的 @Builder 方法。这种做法的好处是代码结构清晰、职责分明。每个 Builder 方法专注于构建一个功能区域的 UI,互不干扰。同时,modalOverlay 这样的公共 Builder 实现了代码复用。
11.2 UI 设计模式总结
在 UI 设计层面,这款应用展现了多个值得借鉴的设计模式。
卡片式布局:几乎所有的内容单元都采用卡片形式呈现——白色背景、圆角、内边距和外边距。这种设计让信息模块化、层次分明,在粉色背景的衬托下具有良好的视觉分离效果。
弹性占位技巧:应用大量使用 Column().layoutWeight(1) 作为弹性占位容器,实现"左右两端对齐"的布局。这是一个简单但非常实用的布局技巧,在标题栏、信息行等场景中反复出现。
条件渲染:if 条件判断在应用中扮演了重要角色——控制 Tab 页面切换、控制弹框显隐、控制图片占位块的有无、控制表单字段的差异化。声明式 UI 的条件渲染让界面能够根据状态动态变化,无需手动操作 DOM。
ForEach 列表渲染:应用中的所有列表都使用 ForEach 进行渲染,配合键值生成器确保列表更新的高效性。无论是日期、日记、成长记录、疫苗、帖子还是设置项,都遵循统一的列表渲染模式。
11.3 交互设计总结
在交互设计方面,应用注重细节和一致性。
统一的弹框机制:四个弹框共享相同的遮罩层和居中布局,点击遮罩即可关闭。这种统一的交互模式降低了用户的学习成本。
状态色彩语义:疫苗模块通过绿、橙、红三色分别表示已种、待种和异常状态,遵循了通用的色彩语义规范,让用户能够通过颜色快速识别状态。
情感化设计:发育里程碑的奖杯图标点击放大动画,是一个典型的情感化设计。它不是功能必需的,但为应用增添了趣味性和温度,契合了育儿应用的感性特质。
信息密度控制:列表项通过 maxLines 和 textOverflow 控制文字显示行数,确保每个列表项的高度一致。这种细节处理让界面整齐美观,不会因为内容长短不一而显得杂乱。
安装DevEco Studio程序

选择目标安装目录:

设置环境变量,但是需要重启一下:

新建一个空白模板:

设置API为24的模板项目:

初始化项目,自动下载相关依赖:

完整代码:
// 亲子育儿 App - 5个底部tab:育儿日记、成长记录、疫苗提醒、育儿社区、我的
// ==================== 接口定义 ====================
interface DiaryItem {
id: number
date: string
title: string
summary: string
mood: string
weather: string
hasImage: boolean
}
interface DiaryDetail {
id: number
date: string
title: string
mood: string
weather: string
food: string
sleep: string
note: string
}
interface GrowthRecord {
id: number
monthAge: number
height: number
weight: number
percentile: string
}
interface Milestone {
id: number
monthAge: number
name: string
date: string
note: string
achieved: boolean
}
interface VaccineItem {
id: number
name: string
date: string
status: string
age: string
desc: string
}
interface CommunityPost {
id: number
author: string
babyAge: string
title: string
summary: string
likes: number
comments: number
hasImage: boolean
}
interface TopicItem {
id: number
name: string
count: number
}
interface BabyInfo {
id: number
name: string
age: string
gender: string
birthday: string
}
interface DateItem {
id: number
day: string
weekday: string
date: string
isToday: boolean
}
interface StatItem {
label: string
value: number
}
interface SettingItem {
id: number
icon: string
title: string
subtitle: string
}
// ==================== Mock 数据 ====================
const MOCK_DATES: DateItem[] = [
{ id: 1, day: '05', weekday: '一', date: '08-05', isToday: false },
{ id: 2, day: '06', weekday: '二', date: '08-06', isToday: false },
{ id: 3, day: '07', weekday: '三', date: '08-07', isToday: false },
{ id: 4, day: '08', weekday: '四', date: '08-08', isToday: true },
{ id: 5, day: '09', weekday: '五', date: '08-09', isToday: false },
{ id: 6, day: '10', weekday: '六', date: '08-10', isToday: false },
{ id: 7, day: '11', weekday: '日', date: '08-11', isToday: false }
]
const MOCK_DIARIES: DiaryItem[] = [
{ id: 1, date: '2026-08-08', title: '今天宝宝会叫妈妈了', summary: '下午陪宝宝玩积木的时候,宝宝突然清晰地叫了一声妈妈,感动得差点哭了...', mood: '开心', weather: '晴', hasImage: true },
{ id: 2, date: '2026-08-07', title: '第一次去动物园', summary: '带宝宝去了市动物园,宝宝看到长颈鹿特别兴奋,一直指着叫高高...', mood: '兴奋', weather: '晴', hasImage: true },
{ id: 3, date: '2026-08-06', title: '宝宝学会拍手了', summary: '今天教宝宝拍手,没想到学得特别快,现在只要说欢迎就开始拍手...', mood: '开心', weather: '多云', hasImage: false },
{ id: 4, date: '2026-08-05', title: '添加辅食第二天', summary: '今天给宝宝尝试了米糊,宝宝吃得还不错,没有过敏反应,继续观察...', mood: '平静', weather: '阴', hasImage: true },
{ id: 5, date: '2026-08-04', title: '宝宝发烧了', summary: '半夜发现宝宝发烧38.5度,贴了退热贴,物理降温,今早已经退烧了...', mood: '担心', weather: '雨', hasImage: false },
{ id: 6, date: '2026-08-03', title: '亲子游泳课', summary: '今天带宝宝上了第二节亲子游泳课,宝宝已经不怕水了,还学会了吐泡泡...', mood: '开心', weather: '晴', hasImage: true },
{ id: 7, date: '2026-08-02', title: '宝宝会爬了', summary: '今天宝宝终于学会往前爬了,之前一直是倒着爬,进步很大...', mood: '兴奋', weather: '多云', hasImage: false },
{ id: 8, date: '2026-08-01', title: '第一次理发', summary: '今天给宝宝理了第一次头发,宝宝很配合,剃了个小光头,好可爱...', mood: '开心', weather: '晴', hasImage: true },
{ id: 9, date: '2026-07-31', title: '添加辅食第一天', summary: '宝宝满六个月了,今天开始添加辅食,先从米粉开始,宝宝吃得不多...', mood: '平静', weather: '多云', hasImage: true },
{ id: 10, date: '2026-07-30', title: '社区体检', summary: '今天带宝宝去社区卫生中心做六月体检,身高体重都达标,医生说发育很好...', mood: '开心', weather: '晴', hasImage: false }
]
const MOCK_GROWTH: GrowthRecord[] = [
{ id: 1, monthAge: 12, height: 76.5, weight: 9.8, percentile: '75%' },
{ id: 2, monthAge: 10, height: 74.2, weight: 9.3, percentile: '70%' },
{ id: 3, monthAge: 9, height: 72.8, weight: 9.0, percentile: '68%' },
{ id: 4, monthAge: 8, height: 71.3, weight: 8.7, percentile: '65%' },
{ id: 5, monthAge: 7, height: 69.8, weight: 8.4, percentile: '72%' },
{ id: 6, monthAge: 6, height: 68.0, weight: 8.0, percentile: '70%' },
{ id: 7, monthAge: 3, height: 62.5, weight: 6.5, percentile: '75%' },
{ id: 8, monthAge: 1, height: 56.0, weight: 4.8, percentile: '60%' }
]
const MOCK_MILESTONES: Milestone[] = [
{ id: 1, monthAge: 12, name: '独立站立', date: '2026-07-15', note: '可以独立站立10秒以上', achieved: true },
{ id: 2, monthAge: 11, name: '叫爸爸妈妈', date: '2026-08-08', note: '能清晰叫出爸爸妈妈', achieved: true },
{ id: 3, monthAge: 10, name: '扶物行走', date: '2026-06-20', note: '扶着沙发可以走几步', achieved: true },
{ id: 4, monthAge: 9, name: '爬行', date: '2026-08-02', note: '能熟练地往前爬', achieved: true },
{ id: 5, monthAge: 8, name: '独坐', date: '2026-05-10', note: '可以自己坐稳了', achieved: true },
{ id: 6, monthAge: 6, name: '翻身', date: '2026-03-01', note: '可以自主翻身', achieved: true }
]
const MOCK_VACCINES: VaccineItem[] = [
{ id: 1, name: '乙肝疫苗(第三针)', date: '2026-07-20', status: '已种', age: '6月龄', desc: '预防乙型肝炎病毒感染' },
{ id: 2, name: 'A群流脑疫苗(第一针)', date: '2026-08-15', status: '待种', age: '6月龄', desc: '预防流行性脑脊髓膜炎' },
{ id: 3, name: '百白破疫苗(第三针)', date: '2026-06-15', status: '已种', age: '5月龄', desc: '预防百日咳、白喉、破伤风' },
{ id: 4, name: '脊灰疫苗(第三针)', date: '2026-06-10', status: '已种', age: '5月龄', desc: '预防脊髓灰质炎' },
{ id: 5, name: '百白破疫苗(第二针)', date: '2026-05-15', status: '已种', age: '4月龄', desc: '预防百日咳、白喉、破伤风' },
{ id: 6, name: '脊灰疫苗(第二针)', date: '2026-05-10', status: '已种', age: '4月龄', desc: '预防脊髓灰质炎' },
{ id: 7, name: '麻疹疫苗(第一针)', date: '2026-09-10', status: '待种', age: '8月龄', desc: '预防麻疹病毒感染' },
{ id: 8, name: '乙脑减毒活疫苗(第一针)', date: '2026-09-15', status: '待种', age: '8月龄', desc: '预防流行性乙型脑炎' },
{ id: 9, name: 'A群流脑疫苗(第二针)', date: '2026-09-20', status: '待种', age: '9月龄', desc: '预防流行性脑脊髓膜炎加强针' },
{ id: 10, name: '卡介苗', date: '2025-08-10', status: '已种', age: '出生', desc: '预防结核病' }
]
const MOCK_POSTS: CommunityPost[] = [
{ id: 1, author: '糖糖妈妈', babyAge: '1岁2月', title: '宝宝挑食怎么办?分享我的经验', summary: '我家宝宝之前也很挑食,后来试了几个方法,现在吃饭可香了,分享给大家...', likes: 328, comments: 56, hasImage: true },
{ id: 2, author: '果果爸', babyAge: '8月', title: '推荐几款适合宝宝的辅食食谱', summary: '我家宝宝刚开始添加辅食,整理了一些简单好做的食谱,希望对新手爸妈有帮助...', likes: 512, comments: 89, hasImage: true },
{ id: 3, author: '豆豆奶奶', babyAge: '1岁6月', title: '夏季宝宝护肤小贴士', summary: '夏天到了,宝宝容易长痱子,分享几个防痱子的小妙招,亲测有效...', likes: 246, comments: 43, hasImage: false },
{ id: 4, author: '棉花糖妈妈', babyAge: '10月', title: '宝宝睡眠训练成功经验', summary: '经过两周的睡眠训练,宝宝终于能自主入睡了,分享一下整个过程...', likes: 689, comments: 128, hasImage: true },
{ id: 5, author: '小苹果爸爸', babyAge: '2岁', title: '亲子阅读从什么时候开始?', summary: '从宝宝6个月就开始亲子阅读,现在宝宝语言能力特别强,分享书单...', likes: 421, comments: 67, hasImage: true },
{ id: 6, author: '蜜桃妈妈', babyAge: '1岁', title: '宝宝语言发育里程碑参考', summary: '整理了0-2岁宝宝语言发育的关键节点,大家可以对照看看自家宝宝...', likes: 356, comments: 72, hasImage: false },
{ id: 7, author: '柠檬妈妈', babyAge: '1岁3月', title: '带娃旅行的必备清单', summary: '刚带宝宝从三亚回来,分享一下带娃旅行的行李清单和注意事项...', likes: 498, comments: 95, hasImage: true },
{ id: 8, author: '蓝莓爸爸', babyAge: '11月', title: '如何选择合适的早教机构?', summary: '考察了好几家早教机构,分享一下选择标准和经验,避免踩坑...', likes: 312, comments: 58, hasImage: false },
{ id: 9, author: '葡萄妈妈', babyAge: '1岁8月', title: '宝宝断奶全过程记录', summary: '记录了给宝宝断奶的完整过程,有焦虑有收获,希望能帮到大家...', likes: 567, comments: 103, hasImage: true },
{ id: 10, author: '樱桃妈妈', babyAge: '9月', title: '宝宝疫苗全攻略分享', summary: '整理了0-6岁疫苗清单,哪些必须打哪些自费打,一目了然...', likes: 723, comments: 156, hasImage: true },
{ id: 11, author: '芒果爸爸', babyAge: '1岁5月', title: '在家也能做的感统训练', summary: '分享几个在家就能做的感统训练小游戏,省钱又实用...', likes: 289, comments: 51, hasImage: false },
{ id: 12, author: '草莓妈妈', babyAge: '2岁1月', title: '宝宝入园准备攻略', summary: '宝宝马上要上托班了,整理了一份入园准备清单,希望对大家有帮助...', likes: 445, comments: 78, hasImage: true }
]
const MOCK_TOPICS: TopicItem[] = [
{ id: 1, name: '辅食食谱', count: 2341 },
{ id: 2, name: '睡眠训练', count: 1876 },
{ id: 3, name: '早教启蒙', count: 3205 },
{ id: 4, name: '疫苗攻略', count: 1543 },
{ id: 5, name: '亲子旅行', count: 987 }
]
const MOCK_BABIES: BabyInfo[] = [
{ id: 1, name: '小花朵', age: '1岁0月', gender: '女', birthday: '2025-08-08' },
{ id: 2, name: '小树苗', age: '2岁3月', gender: '男', birthday: '2024-05-01' }
]
const MOCK_STATS: StatItem[] = [
{ label: '育儿日记', value: 128 },
{ label: '成长记录', value: 24 },
{ label: '社区互动', value: 365 }
]
const MOCK_SETTINGS: SettingItem[] = [
{ id: 1, icon: '🔔', title: '提醒设置', subtitle: '疫苗提醒、成长提醒' },
{ id: 2, icon: '🔒', title: '隐私设置', subtitle: '社区可见范围' },
{ id: 3, icon: '📖', title: '宝宝档案', subtitle: '管理宝宝信息' },
{ id: 4, icon: '❤️', title: '关于我们', subtitle: '版本 v1.0.0' }
]
// ==================== 主组件 ====================
@Entry
@Component
struct ParentingApp {
@State currentTab: number = 0
@State showAddModal: boolean = false
@State showEditModal: boolean = false
@State showDeleteModal: boolean = false
@State showDetailModal: boolean = false
@State selectedDate: number = 4
@State selectedPostId: number = 0
@State selectedDiaryId: number = 0
@State milestoneAnimIndex: number = -1
@Builder modalOverlay(onClose: () => void) {
Column().width('100%').height('100%').backgroundColor('rgba(0,0,0,0.5)').onClick(onClose)
}
build() {
Column() {
if (this.currentTab === 0) {
this.diaryTab()
}
if (this.currentTab === 1) {
this.growthTab()
}
if (this.currentTab === 2) {
this.vaccineTab()
}
if (this.currentTab === 3) {
this.communityTab()
}
if (this.currentTab === 4) {
this.mineTab()
}
this.bottomTabBar()
if (this.showAddModal) {
this.addModalView()
}
if (this.showEditModal) {
this.editModalView()
}
if (this.showDeleteModal) {
this.deleteModalView()
}
if (this.showDetailModal) {
this.detailModalView()
}
}.width('100%').height('100%').backgroundColor('#FCE4EC')
}
@Builder bottomTabBar() {
Row() {
ForEach([0, 1, 2, 3, 4], (index: number) => {
Column() {
Text(this.getTabIcon(index))
.fontSize(22)
Text(this.getTabName(index))
.fontSize(11)
.fontColor(this.currentTab === index ? '#EC407A' : '#999999')
.margin({ top: 2 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
.onClick(() => {
this.currentTab = index
})
}, (index: number) => index.toString())
}
.width('100%')
.height(56)
.backgroundColor('#FFFFFF')
.border({ width: 1, color: '#F5F5F5' })
}
getTabIcon(index: number): string {
const icons: string[] = ['📔', '📈', '💉', '👥', '👤']
return icons[index]
}
getTabName(index: number): string {
const names: string[] = ['育儿日记', '成长记录', '疫苗提醒', '育儿社区', '我的']
return names[index]
}
// ==================== Tab1: 育儿日记 ====================
@Builder diaryTab() {
Column() {
Row() {
Text('育儿日记')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
Column().layoutWeight(1)
Text('✏️')
.fontSize(24)
.onClick(() => {
this.showAddModal = true
})
}
.width('100%')
.height(56)
.padding({ left: 16, right: 16 })
Scroll() {
Column() {
// 日期选择横滑
Scroll() {
Row({ space: 10 }) {
ForEach(MOCK_DATES, (item: DateItem) => {
Column() {
Text(item.weekday)
.fontSize(12)
.fontColor(this.selectedDate === item.id ? '#FFFFFF' : '#999999')
Text(item.day)
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor(this.selectedDate === item.id ? '#FFFFFF' : '#333333')
.margin({ top: 2 })
if (item.isToday) {
Text('今天')
.fontSize(10)
.fontColor(this.selectedDate === item.id ? '#FFFFFF' : '#EC407A')
.margin({ top: 2 })
}
}
.width(50)
.height(70)
.justifyContent(FlexAlign.Center)
.backgroundColor(this.selectedDate === item.id ? '#EC407A' : '#FFFFFF')
.borderRadius(12)
.border({ width: 1, color: '#F5F5F5' })
.onClick(() => {
this.selectedDate = item.id
})
}, (item: DateItem) => item.id.toString())
}
.padding({ left: 16, right: 16 })
}
.scrollable(ScrollDirection.Horizontal)
.scrollBar(BarState.Off)
.width('100%')
.height(90)
.margin({ top: 8 })
// 今日日记卡片
Column() {
Row() {
Text('📝 今日日记')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
Column().layoutWeight(1)
Text('开心 😊')
.fontSize(13)
.fontColor('#EC407A')
}
.width('100%')
Column({ space: 10 }) {
Row({ space: 8 }) {
Text('🍽️')
.fontSize(18)
Text('饮食')
.fontSize(13)
.fontColor('#666666')
.layoutWeight(1)
Text('米糊+南瓜泥')
.fontSize(13)
.fontColor('#333333')
}
.width('100%')
Row({ space: 8 }) {
Text('😴')
.fontSize(18)
Text('睡眠')
.fontSize(13)
.fontColor('#666666')
.layoutWeight(1)
Text('10小时30分')
.fontSize(13)
.fontColor('#333333')
}
.width('100%')
Row({ space: 8 }) {
Text('😊')
.fontSize(18)
Text('心情')
.fontSize(13)
.fontColor('#666666')
.layoutWeight(1)
Text('开心活泼')
.fontSize(13)
.fontColor('#333333')
}
.width('100%')
Row({ space: 8 }) {
Text('📌')
.fontSize(18)
Text('备注')
.fontSize(13)
.fontColor('#666666')
.layoutWeight(1)
Text('今天宝宝特别开心,学会了拍手')
.fontSize(13)
.fontColor('#333333')
}
.width('100%')
}
.margin({ top: 12 })
Row() {
Text('编辑')
.fontSize(13)
.fontColor('#EC407A')
.onClick(() => {
this.showEditModal = true
})
Text('删除')
.fontSize(13)
.fontColor('#F44336')
.margin({ left: 16 })
.onClick(() => {
this.showDeleteModal = true
})
}
.width('100%')
.margin({ top: 12 })
.justifyContent(FlexAlign.End)
}
.width('100%')
.padding(16)
.backgroundColor('#FFFFFF')
.borderRadius(16)
.margin({ left: 16, right: 16, top: 12 })
// 历史日记列表
Row() {
Text('历史日记')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
Column().layoutWeight(1)
Text('共10条')
.fontSize(12)
.fontColor('#999999')
}
.width('100%')
.padding({ left: 16, right: 16, top: 20, bottom: 8 })
ForEach(MOCK_DIARIES, (item: DiaryItem) => {
Column() {
Row() {
Text(item.date)
.fontSize(12)
.fontColor('#999999')
Column().layoutWeight(1)
Text('☀️ ' + item.weather)
.fontSize(12)
.fontColor('#666666')
}
.width('100%')
Text(item.title)
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
.margin({ top: 6 })
Row({ space: 8 }) {
if (item.hasImage) {
Column()
.width(60)
.height(60)
.backgroundColor('#FCE4EC')
.borderRadius(8)
}
Text(item.summary)
.fontSize(13)
.fontColor('#666666')
.maxLines(2)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.layoutWeight(1)
}
.width('100%')
.margin({ top: 8 })
Row() {
Text('心情: ' + item.mood)
.fontSize(12)
.fontColor('#EC407A')
Column().layoutWeight(1)
Text('查看详情 >')
.fontSize(12)
.fontColor('#999999')
.onClick(() => {
this.selectedDiaryId = item.id
this.showDetailModal = true
})
}
.width('100%')
.margin({ top: 8 })
}
.width('100%')
.padding(14)
.backgroundColor('#FFFFFF')
.borderRadius(12)
.margin({ left: 16, right: 16, bottom: 10 })
}, (item: DiaryItem) => item.id.toString())
}
}
.layoutWeight(1)
.scrollBar(BarState.Off)
.align(Alignment.Top)
}
.width('100%')
.layoutWeight(1)
}
// ==================== Tab2: 成长记录 ====================
@Builder growthTab() {
Column() {
Row() {
Text('成长记录')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
Column().layoutWeight(1)
Text('➕')
.fontSize(24)
.onClick(() => {
this.showAddModal = true
})
}
.width('100%')
.height(56)
.padding({ left: 16, right: 16 })
Scroll() {
Column() {
// 宝宝信息卡
Row({ space: 14 }) {
Column()
.width(64)
.height(64)
.borderRadius(32)
.backgroundColor('#F8BBD0')
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
Column({ space: 4 }) {
Text('小花朵')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
Row({ space: 8 }) {
Text('1岁0月')
.fontSize(13)
.fontColor('#666666')
Text('|')
.fontSize(13)
.fontColor('#CCCCCC')
Text('女')
.fontSize(13)
.fontColor('#EC407A')
Text('|')
.fontSize(13)
.fontColor('#CCCCCC')
Text('生日: 2025-08-08')
.fontSize(12)
.fontColor('#999999')
}
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
}
.width('100%')
.padding(16)
.backgroundColor('#FFFFFF')
.borderRadius(16)
.margin({ left: 16, right: 16 })
// 身高体重记录
Text('身高体重记录')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
.width('100%')
.padding({ left: 16, top: 20, bottom: 8 })
Column() {
Row() {
Text('月龄')
.fontSize(12)
.fontColor('#999999')
.layoutWeight(1)
Text('身高(cm)')
.fontSize(12)
.fontColor('#999999')
.layoutWeight(1)
Text('体重(kg)')
.fontSize(12)
.fontColor('#999999')
.layoutWeight(1)
Text('百分位')
.fontSize(12)
.fontColor('#999999')
.layoutWeight(1)
}
.width('100%')
.padding({ top: 8, bottom: 8 })
ForEach(MOCK_GROWTH, (item: GrowthRecord) => {
Row() {
Text(item.monthAge + '月')
.fontSize(13)
.fontColor('#333333')
.layoutWeight(1)
Text(item.height.toString())
.fontSize(13)
.fontColor('#333333')
.layoutWeight(1)
Text(item.weight.toString())
.fontSize(13)
.fontColor('#333333')
.layoutWeight(1)
Text(item.percentile)
.fontSize(13)
.fontColor('#EC407A')
.layoutWeight(1)
}
.width('100%')
.padding({ top: 10, bottom: 10 })
.border({ width: 1, color: '#F5F5F5' })
}, (item: GrowthRecord) => item.id.toString())
}
.width('100%')
.padding(14)
.backgroundColor('#FFFFFF')
.borderRadius(16)
.margin({ left: 16, right: 16 })
// 生长曲线简易图
Text('生长趋势')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
.width('100%')
.padding({ left: 16, top: 20, bottom: 8 })
Column() {
Row({ space: 8 }) {
Column()
.width(10)
.height(10)
.borderRadius(5)
.backgroundColor('#EC407A')
Text('身高趋势')
.fontSize(12)
.fontColor('#666666')
Column()
.width(10)
.height(10)
.borderRadius(5)
.backgroundColor('#42A5F5')
.margin({ left: 12 })
Text('体重趋势')
.fontSize(12)
.fontColor('#666666')
}
.width('100%')
// 简易柱状图
Row({ space: 12 }) {
ForEach(MOCK_GROWTH, (item: GrowthRecord) => {
Column({ space: 4 }) {
Column()
.width(12)
.height(item.height * 0.6)
.backgroundColor('#EC407A')
.borderRadius(3)
Column()
.width(12)
.height(item.weight * 4)
.backgroundColor('#42A5F5')
.borderRadius(3)
.margin({ top: 4 })
Text(item.monthAge + '月')
.fontSize(10)
.fontColor('#999999')
}
.alignItems(HorizontalAlign.Center)
}, (item: GrowthRecord) => item.id.toString())
}
.width('100%')
.height(120)
.margin({ top: 12 })
.justifyContent(FlexAlign.Center)
}
.width('100%')
.padding(16)
.backgroundColor('#FFFFFF')
.borderRadius(16)
.margin({ left: 16, right: 16 })
// 发育里程碑
Text('发育里程碑')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
.width('100%')
.padding({ left: 16, top: 20, bottom: 8 })
ForEach(MOCK_MILESTONES, (item: Milestone, index: number) => {
Row({ space: 12 }) {
Column() {
Text('🏆')
.fontSize(24)
.scale(this.milestoneAnimIndex === index ? { x: 1.3, y: 1.3 } : { x: 1.0, y: 1.0 })
.animation({ duration: 300 })
}
.width(44)
.height(44)
.borderRadius(22)
.backgroundColor('#FCE4EC')
.justifyContent(FlexAlign.Center)
.onClick(() => {
this.milestoneAnimIndex = index
setTimeout(() => {
this.milestoneAnimIndex = -1
}, 300)
})
Column({ space: 4 }) {
Row() {
Text(item.name)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
Column().layoutWeight(1)
Text(item.monthAge + '月龄')
.fontSize(11)
.fontColor('#EC407A')
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
.backgroundColor('#FCE4EC')
.borderRadius(8)
}
.width('100%')
Text(item.date + ' 达成')
.fontSize(12)
.fontColor('#999999')
Text(item.note)
.fontSize(12)
.fontColor('#666666')
.margin({ top: 2 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
}
.width('100%')
.padding(12)
.backgroundColor('#FFFFFF')
.borderRadius(12)
.margin({ left: 16, right: 16, bottom: 8 })
}, (item: Milestone) => item.id.toString())
}
}
.layoutWeight(1)
.scrollBar(BarState.Off)
.align(Alignment.Top)
}
.width('100%')
.layoutWeight(1)
}
// ==================== Tab3: 疫苗提醒 ====================
@Builder vaccineTab() {
Column() {
Row() {
Text('疫苗提醒')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
Column().layoutWeight(1)
Text('🔔')
.fontSize(22)
}
.width('100%')
.height(56)
.padding({ left: 16, right: 16 })
Scroll() {
Column() {
// 下次接种提醒卡
Column() {
Row({ space: 8 }) {
Text('⏰')
.fontSize(28)
Column({ space: 4 }) {
Text('下次接种提醒')
.fontSize(14)
.fontColor('#FFFFFF')
.opacity(0.9)
Text('A群流脑疫苗(第一针)')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
}
.width('100%')
Row() {
Text('接种日期: 2026-08-15')
.fontSize(12)
.fontColor('#FFFFFF')
.opacity(0.9)
Column().layoutWeight(1)
Text('还有7天')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
}
.width('100%')
.margin({ top: 10 })
}
.width('100%')
.padding(16)
.backgroundColor('#EC407A')
.borderRadius(16)
.margin({ left: 16, right: 16 })
// 接种时间线
Text('接种时间线')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
.width('100%')
.padding({ left: 16, top: 20, bottom: 8 })
ForEach(MOCK_VACCINES, (item: VaccineItem) => {
Row({ space: 12 }) {
// 状态指示器
Column() {
Text(this.getVaccineIcon(item.status))
.fontSize(18)
}
.width(40)
.height(40)
.borderRadius(20)
.backgroundColor(this.getVaccineBgColor(item.status))
.justifyContent(FlexAlign.Center)
// 竖线
Column()
.width(2)
.height(40)
.backgroundColor('#E0E0E0')
.position({ x: 19, y: 40 })
// 内容
Column({ space: 4 }) {
Row() {
Text(item.name)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
.layoutWeight(1)
Text(item.status)
.fontSize(11)
.fontColor('#FFFFFF')
.padding({ left: 8, right: 8, top: 3, bottom: 3 })
.backgroundColor(this.getVaccineColor(item.status))
.borderRadius(8)
}
.width('100%')
Row({ space: 10 }) {
Text('📅 ' + item.date)
.fontSize(12)
.fontColor('#999999')
Text('👶 ' + item.age)
.fontSize(12)
.fontColor('#999999')
}
Text(item.desc)
.fontSize(12)
.fontColor('#666666')
.margin({ top: 2 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
}
.width('100%')
.padding(12)
.backgroundColor('#FFFFFF')
.borderRadius(12)
.margin({ left: 16, right: 16, bottom: 8 })
}, (item: VaccineItem) => item.id.toString())
}
}
.layoutWeight(1)
.scrollBar(BarState.Off)
.align(Alignment.Top)
}
.width('100%')
.layoutWeight(1)
}
getVaccineIcon(status: string): string {
if (status === '已种') {
return '✅'
}
if (status === '待种') {
return '⏳'
}
return '⚠️'
}
getVaccineColor(status: string): string {
if (status === '已种') {
return '#4CAF50'
}
if (status === '待种') {
return '#FF9800'
}
return '#F44336'
}
getVaccineBgColor(status: string): string {
if (status === '已种') {
return '#E8F5E9'
}
if (status === '待种') {
return '#FFF3E0'
}
return '#FFEBEE'
}
// ==================== Tab4: 育儿社区 ====================
@Builder communityTab() {
Column() {
Row() {
Text('育儿社区')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
Column().layoutWeight(1)
Text('🔍')
.fontSize(22)
}
.width('100%')
.height(56)
.padding({ left: 16, right: 16 })
Scroll() {
Column() {
// 热门话题横滑
Column() {
Text('🔥 热门话题')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
.width('100%')
Scroll() {
Row({ space: 10 }) {
ForEach(MOCK_TOPICS, (item: TopicItem) => {
Column({ space: 4 }) {
Text('# ' + item.name)
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor('#EC407A')
Text(item.count + '人参与')
.fontSize(11)
.fontColor('#999999')
}
.padding(12)
.backgroundColor('#FFFFFF')
.borderRadius(12)
.border({ width: 1, color: '#FCE4EC' })
}, (item: TopicItem) => item.id.toString())
}
.padding({ left: 16, right: 16, top: 10 })
}
.scrollable(ScrollDirection.Horizontal)
.scrollBar(BarState.Off)
.width('100%')
}
.width('100%')
.padding({ left: 0, right: 0, top: 10, bottom: 10 })
// 帖子列表
ForEach(MOCK_POSTS, (item: CommunityPost) => {
Column() {
Row({ space: 10 }) {
Column()
.width(40)
.height(40)
.borderRadius(20)
.backgroundColor('#F8BBD0')
Column({ space: 3 }) {
Text(item.author)
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
Row() {
Text(item.babyAge)
.fontSize(10)
.fontColor('#EC407A')
.padding({ left: 6, right: 6, top: 1, bottom: 1 })
.backgroundColor('#FCE4EC')
.borderRadius(8)
}
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
}
.width('100%')
Text(item.title)
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
.margin({ top: 8 })
Row({ space: 10 }) {
if (item.hasImage) {
Column()
.width(80)
.height(60)
.backgroundColor('#FCE4EC')
.borderRadius(8)
}
Text(item.summary)
.fontSize(13)
.fontColor('#666666')
.maxLines(2)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.layoutWeight(1)
}
.width('100%')
.margin({ top: 8 })
Row() {
Text('❤️ ' + item.likes)
.fontSize(12)
.fontColor('#999999')
Text('💬 ' + item.comments)
.fontSize(12)
.fontColor('#999999')
.margin({ left: 16 })
Column().layoutWeight(1)
Text('查看详情 >')
.fontSize(12)
.fontColor('#EC407A')
.onClick(() => {
this.selectedPostId = item.id
this.showDetailModal = true
})
}
.width('100%')
.margin({ top: 10 })
}
.width('100%')
.padding(14)
.backgroundColor('#FFFFFF')
.borderRadius(12)
.margin({ left: 16, right: 16, bottom: 10 })
}, (item: CommunityPost) => item.id.toString())
}
}
.layoutWeight(1)
.scrollBar(BarState.Off)
.align(Alignment.Top)
}
.width('100%')
.layoutWeight(1)
}
// ==================== Tab5: 我的 ====================
@Builder mineTab() {
Column() {
Row() {
Text('我的')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
Column().layoutWeight(1)
Text('⚙️')
.fontSize(22)
}
.width('100%')
.height(56)
.padding({ left: 16, right: 16 })
Scroll() {
Column() {
// 家长信息卡
Row({ space: 14 }) {
Column()
.width(60)
.height(60)
.borderRadius(30)
.backgroundColor('#F8BBD0')
Column({ space: 4 }) {
Text('花朵妈妈')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
Text('注册时间: 2025-08-08')
.fontSize(12)
.fontColor('#999999')
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
}
.width('100%')
.padding(16)
.backgroundColor('#FFFFFF')
.borderRadius(16)
.margin({ left: 16, right: 16 })
// 宝宝档案切换
Text('宝宝档案')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
.width('100%')
.padding({ left: 16, top: 20, bottom: 8 })
Scroll() {
Row({ space: 12 }) {
ForEach(MOCK_BABIES, (item: BabyInfo) => {
Column({ space: 6 }) {
Column()
.width(56)
.height(56)
.borderRadius(28)
.backgroundColor('#FCE4EC')
Text(item.name)
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
Text(item.age)
.fontSize(11)
.fontColor('#999999')
}
.padding(12)
.backgroundColor('#FFFFFF')
.borderRadius(12)
.border({ width: 1, color: '#FCE4EC' })
}, (item: BabyInfo) => item.id.toString())
Column({ space: 6 }) {
Column()
.width(56)
.height(56)
.borderRadius(28)
.backgroundColor('#F5F5F5')
.justifyContent(FlexAlign.Center)
Text('➕')
.fontSize(24)
.fontColor('#CCCCCC')
}
.padding(12)
.backgroundColor('#FFFFFF')
.borderRadius(12)
.border({ width: 1, color: '#E0E0E0' })
}
.padding({ left: 16, right: 16 })
}
.scrollable(ScrollDirection.Horizontal)
.scrollBar(BarState.Off)
.width('100%')
// 育儿统计
Text('育儿统计')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
.width('100%')
.padding({ left: 16, top: 20, bottom: 8 })
Row() {
ForEach(MOCK_STATS, (item: StatItem) => {
Column({ space: 6 }) {
Text(item.value.toString())
.fontSize(24)
.fontWeight(FontWeight.Bold)
.fontColor('#EC407A')
Text(item.label)
.fontSize(12)
.fontColor('#666666')
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
}, (item: StatItem) => item.label)
}
.width('100%')
.padding(16)
.backgroundColor('#FFFFFF')
.borderRadius(16)
.margin({ left: 16, right: 16 })
// 设置列表
Text('设置')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
.width('100%')
.padding({ left: 16, top: 20, bottom: 8 })
ForEach(MOCK_SETTINGS, (item: SettingItem) => {
Row({ space: 12 }) {
Text(item.icon)
.fontSize(22)
Column({ space: 2 }) {
Text(item.title)
.fontSize(14)
.fontColor('#333333')
Text(item.subtitle)
.fontSize(12)
.fontColor('#999999')
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Text('>')
.fontSize(16)
.fontColor('#CCCCCC')
}
.width('100%')
.padding(14)
.backgroundColor('#FFFFFF')
.borderRadius(12)
.margin({ left: 16, right: 16, bottom: 8 })
}, (item: SettingItem) => item.id.toString())
}
}
.layoutWeight(1)
.scrollBar(BarState.Off)
.align(Alignment.Top)
}
.width('100%')
.layoutWeight(1)
}
// ==================== 弹框 ====================
@Builder addModalView() {
Column() {
this.modalOverlay(() => {
this.showAddModal = false
})
Column() {
Text(this.currentTab === 0 ? '写育儿日记' : '添加成长记录')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
.margin({ bottom: 16 })
Scroll() {
Column({ space: 12 }) {
Column({ space: 6 }) {
Text('标题')
.fontSize(13)
.fontColor('#666666')
TextInput({ placeholder: '请输入标题' })
.fontSize(14)
.height(44)
.borderRadius(10)
.backgroundColor('#F5F5F5')
}
.width('100%')
.alignItems(HorizontalAlign.Start)
if (this.currentTab === 0) {
Column({ space: 6 }) {
Text('宝宝心情')
.fontSize(13)
.fontColor('#666666')
Row({ space: 10 }) {
ForEach(['开心', '平静', '兴奋', '担心', '难过'], (mood: string) => {
Text(mood)
.fontSize(13)
.fontColor('#333333')
.padding({ left: 14, right: 14, top: 8, bottom: 8 })
.backgroundColor('#FCE4EC')
.borderRadius(20)
}, (mood: string) => mood)
}
.width('100%')
}
.width('100%')
.alignItems(HorizontalAlign.Start)
Column({ space: 6 }) {
Text('饮食')
.fontSize(13)
.fontColor('#666666')
TextInput({ placeholder: '今日饮食记录' })
.fontSize(14)
.height(44)
.borderRadius(10)
.backgroundColor('#F5F5F5')
}
.width('100%')
.alignItems(HorizontalAlign.Start)
Column({ space: 6 }) {
Text('睡眠')
.fontSize(13)
.fontColor('#666666')
TextInput({ placeholder: '睡眠时长' })
.fontSize(14)
.height(44)
.borderRadius(10)
.backgroundColor('#F5F5F5')
}
.width('100%')
.alignItems(HorizontalAlign.Start)
Column({ space: 6 }) {
Text('备注')
.fontSize(13)
.fontColor('#666666')
TextArea({ placeholder: '其他备注信息...' })
.fontSize(14)
.height(80)
.borderRadius(10)
.backgroundColor('#F5F5F5')
}
.width('100%')
.alignItems(HorizontalAlign.Start)
} else {
Column({ space: 6 }) {
Text('身高(cm)')
.fontSize(13)
.fontColor('#666666')
TextInput({ placeholder: '请输入身高' })
.fontSize(14)
.height(44)
.borderRadius(10)
.backgroundColor('#F5F5F5')
}
.width('100%')
.alignItems(HorizontalAlign.Start)
Column({ space: 6 }) {
Text('体重(kg)')
.fontSize(13)
.fontColor('#666666')
TextInput({ placeholder: '请输入体重' })
.fontSize(14)
.height(44)
.borderRadius(10)
.backgroundColor('#F5F5F5')
}
.width('100%')
.alignItems(HorizontalAlign.Start)
Column({ space: 6 }) {
Text('月龄')
.fontSize(13)
.fontColor('#666666')
TextInput({ placeholder: '请输入月龄' })
.fontSize(14)
.height(44)
.borderRadius(10)
.backgroundColor('#F5F5F5')
}
.width('100%')
.alignItems(HorizontalAlign.Start)
}
Row({ space: 12 }) {
Button('取消')
.fontSize(14)
.fontColor('#666666')
.layoutWeight(1)
.height(44)
.backgroundColor('#F5F5F5')
.borderRadius(10)
.onClick(() => {
this.showAddModal = false
})
Button('保存')
.fontSize(14)
.fontColor('#FFFFFF')
.layoutWeight(1)
.height(44)
.backgroundColor('#EC407A')
.borderRadius(10)
.onClick(() => {
this.showAddModal = false
})
}
.width('100%')
.margin({ top: 8 })
}
.width('100%')
}
.constraintSize({ maxHeight: '60%' })
.scrollBar(BarState.Off)
}
.width('90%')
.padding(20)
.backgroundColor('#FFFFFF')
.borderRadius(20)
.margin({ top: 80, bottom: 80 })
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
@Builder editModalView() {
Column() {
this.modalOverlay(() => {
this.showEditModal = false
})
Column() {
Text('编辑日记')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
.margin({ bottom: 16 })
Column({ space: 12 }) {
Column({ space: 6 }) {
Text('标题')
.fontSize(13)
.fontColor('#666666')
TextInput({ text: '今天宝宝会叫妈妈了' })
.fontSize(14)
.height(44)
.borderRadius(10)
.backgroundColor('#F5F5F5')
}
.width('100%')
.alignItems(HorizontalAlign.Start)
Column({ space: 6 }) {
Text('心情')
.fontSize(13)
.fontColor('#666666')
Row({ space: 10 }) {
ForEach(['开心', '平静', '兴奋', '担心', '难过'], (mood: string) => {
Text(mood)
.fontSize(13)
.fontColor('#333333')
.padding({ left: 14, right: 14, top: 8, bottom: 8 })
.backgroundColor('#FCE4EC')
.borderRadius(20)
}, (mood: string) => mood)
}
.width('100%')
}
.width('100%')
.alignItems(HorizontalAlign.Start)
Column({ space: 6 }) {
Text('饮食')
.fontSize(13)
.fontColor('#666666')
TextInput({ text: '米糊+南瓜泥' })
.fontSize(14)
.height(44)
.borderRadius(10)
.backgroundColor('#F5F5F5')
}
.width('100%')
.alignItems(HorizontalAlign.Start)
Column({ space: 6 }) {
Text('睡眠')
.fontSize(13)
.fontColor('#666666')
TextInput({ text: '10小时30分' })
.fontSize(14)
.height(44)
.borderRadius(10)
.backgroundColor('#F5F5F5')
}
.width('100%')
.alignItems(HorizontalAlign.Start)
Column({ space: 6 }) {
Text('备注')
.fontSize(13)
.fontColor('#666666')
TextArea({ text: '今天宝宝特别开心,学会了拍手' })
.fontSize(14)
.height(80)
.borderRadius(10)
.backgroundColor('#F5F5F5')
}
.width('100%')
.alignItems(HorizontalAlign.Start)
Row({ space: 12 }) {
Button('取消')
.fontSize(14)
.fontColor('#666666')
.layoutWeight(1)
.height(44)
.backgroundColor('#F5F5F5')
.borderRadius(10)
.onClick(() => {
this.showEditModal = false
})
Button('保存')
.fontSize(14)
.fontColor('#FFFFFF')
.layoutWeight(1)
.height(44)
.backgroundColor('#EC407A')
.borderRadius(10)
.onClick(() => {
this.showEditModal = false
})
}
.width('100%')
.margin({ top: 8 })
}
.width('100%')
}
.width('90%')
.padding(20)
.backgroundColor('#FFFFFF')
.borderRadius(20)
.margin({ top: 80, bottom: 80 })
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
@Builder deleteModalView() {
Column() {
this.modalOverlay(() => {
this.showDeleteModal = false
})
Column({ space: 16 }) {
Text('🗑️')
.fontSize(40)
Text('删除日记')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
Text('确定要删除这篇日记吗?删除后不可恢复。')
.fontSize(14)
.fontColor('#666666')
.textAlign(TextAlign.Center)
Row({ space: 12 }) {
Button('取消')
.fontSize(14)
.fontColor('#666666')
.layoutWeight(1)
.height(44)
.backgroundColor('#F5F5F5')
.borderRadius(10)
.onClick(() => {
this.showDeleteModal = false
})
Button('确定删除')
.fontSize(14)
.fontColor('#FFFFFF')
.layoutWeight(1)
.height(44)
.backgroundColor('#F44336')
.borderRadius(10)
.onClick(() => {
this.showDeleteModal = false
})
}
.width('100%')
.margin({ top: 8 })
}
.width('80%')
.padding(24)
.backgroundColor('#FFFFFF')
.borderRadius(20)
.alignItems(HorizontalAlign.Center)
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
@Builder detailModalView() {
Column() {
this.modalOverlay(() => {
this.showDetailModal = false
})
Column() {
Text(this.selectedPostId > 0 ? '帖子详情' : '日记详情')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
.margin({ bottom: 16 })
Scroll() {
Column({ space: 12 }) {
if (this.selectedPostId > 0) {
// 帖子详情
ForEach(MOCK_POSTS, (item: CommunityPost) => {
if (item.id === this.selectedPostId) {
Column({ space: 8 }) {
Row({ space: 10 }) {
Column()
.width(40)
.height(40)
.borderRadius(20)
.backgroundColor('#F8BBD0')
Column({ space: 3 }) {
Text(item.author)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
Text(item.babyAge)
.fontSize(11)
.fontColor('#EC407A')
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
}
.width('100%')
Text(item.title)
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
.margin({ top: 4 })
Text(item.summary)
.fontSize(14)
.fontColor('#666666')
if (item.hasImage) {
Column()
.width('100%')
.height(150)
.backgroundColor('#FCE4EC')
.borderRadius(12)
}
Row() {
Text('❤️ ' + item.likes)
.fontSize(13)
.fontColor('#999999')
Text('💬 ' + item.comments)
.fontSize(13)
.fontColor('#999999')
.margin({ left: 16 })
}
.width('100%')
.margin({ top: 4 })
}
.width('100%')
}
}, (item: CommunityPost) => item.id.toString())
// 评论区
Text('评论 (5)')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
.width('100%')
.margin({ top: 8 })
ForEach(['小红妈妈', '大牛爸爸', '果果妈妈', '豆豆奶奶', '棉花糖妈妈'], (name: string, index: number) => {
Row({ space: 10 }) {
Column()
.width(32)
.height(32)
.borderRadius(16)
.backgroundColor('#F8BBD0')
Column({ space: 3 }) {
Text(name)
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
Text('写得很详细,对我帮助很大,谢谢分享!')
.fontSize(12)
.fontColor('#666666')
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
}
.width('100%')
.padding({ top: 8, bottom: 8 })
.border({ width: 1, color: '#F5F5F5' })
}, (name: string, index: number) => index.toString())
} else {
// 日记详情
ForEach(MOCK_DIARIES, (item: DiaryItem) => {
if (item.id === this.selectedDiaryId) {
Column({ space: 8 }) {
Row() {
Text(item.date)
.fontSize(12)
.fontColor('#999999')
Column().layoutWeight(1)
Text('☀️ ' + item.weather)
.fontSize(12)
.fontColor('#666666')
}
.width('100%')
Text(item.title)
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
Text(item.summary)
.fontSize(14)
.fontColor('#666666')
if (item.hasImage) {
Column()
.width('100%')
.height(150)
.backgroundColor('#FCE4EC')
.borderRadius(12)
}
Row() {
Text('心情: ' + item.mood)
.fontSize(13)
.fontColor('#EC407A')
}
.width('100%')
}
.width('100%')
}
}, (item: DiaryItem) => item.id.toString())
}
Row({ space: 12 }) {
Button('关闭')
.fontSize(14)
.fontColor('#666666')
.layoutWeight(1)
.height(44)
.backgroundColor('#F5F5F5')
.borderRadius(10)
.onClick(() => {
this.showDetailModal = false
})
}
.width('100%')
.margin({ top: 8 })
}
.width('100%')
}
.constraintSize({ maxHeight: '60%' })
.scrollBar(BarState.Off)
}
.width('90%')
.padding(20)
.backgroundColor('#FFFFFF')
.borderRadius(20)
.margin({ top: 60, bottom: 60 })
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
}
11.4 可扩展性思考
虽然这款应用已经功能完善,但在真实场景中还有许多可以扩展的方向。
在数据层面,当前的模拟数据是静态的。在实际应用中,需要接入后端 API 实现数据的动态加载、创建、更新和删除。接口定义已经为这一步做好了准备——只需将 Mock 数据替换为 API 返回的数据即可。
在功能层面,社区模块可以增加发帖功能、评论功能、点赞收藏功能;疫苗模块可以接入实际的疫苗计算逻辑,根据宝宝出生日期自动生成接种计划;成长记录模块可以接入 WHO 生长标准曲线,提供更专业的发育评估。
在性能层面,当数据量增大时,"遍历全部数据+条件匹配"的详情弹框模式需要优化为直接按 ID 查找。列表渲染也需要考虑虚拟滚动等技术来处理大量数据。
在多设备适配层面,当前代码使用百分比宽度和固定高度混合的布局策略。在适配不同屏幕尺寸时,需要进一步优化布局参数,确保在各种设备上都有良好的显示效果。
11.5 结语

这款亲子育儿应用虽然以模拟数据驱动,但其代码结构、UI 设计和交互模式都展现了较高的工程水准。从接口定义到数据模拟,从状态管理到组件构建,从列表渲染到弹框交互,每一个环节都经过了认真的设计和实现。通过逐段分析这些代码,我们不仅理解了一个完整应用的构建过程,更学习到了声明式 UI 开发的核心思想和最佳实践。
对于想要学习声明式 UI 开发的开发者来说,这款应用的代码是一个非常好的学习材料。它涵盖了接口定义、状态管理、条件渲染、列表渲染、组件复用、事件处理、动画效果等几乎所有核心知识点。通过阅读和理解这些代码,开发者可以快速掌握声明式 UI 的开发方法,并将其应用到自己的项目中。
更多推荐

所有评论(0)