养老服务 App 深度技术解析HarmonyOS ArkTS API 24:contentArea 是一个 @Builder 自定义构建函数,根据 activeTab 的值渲染对应的页面组件
一、开篇引言

随着全球人口老龄化趋势的加剧,养老服务已经成为社会关注的焦点话题。如何让老年人享受更便捷、更贴心的养老服务,如何用科技手段提升老年人的生活质量,是每个科技工作者都应当思考的问题。本文将深入剖析一款基于 ArkTS 开发的养老服务 App,从业务背景、架构设计、数据模型到组件实现,全方位展示这款面向老年用户的应用是如何通过技术传递温度的。
这款养老服务 App 是一款专为老年人打造的智慧康养应用,涵盖了健康管理、用药提醒、社区活动、紧急联系和个人档案等核心功能。App 采用暖橙色与静谧蓝的配色方案,橙色传递温暖关怀,蓝色带来安心可靠,两种颜色的结合营造出温馨而专业的养老服务氛围。技术上,应用基于 ArkTS 声明式 UI 框架,通过 @Component、@State、@Builder 等装饰器构建了一套模块化的组件体系,同时充分考虑了老年用户的使用习惯,采用了较大的字号和清晰的视觉层次。
从业务场景来看,这款 App 覆盖了养老服务的多个维度:首页提供健康评分、步数统计、紧急联系人和社区活动等综合信息;健康页面展示各项身体指标和体检记录;用药页面管理每日用药提醒;社区页面展示社区活动和养生知识;我的页面管理个人档案和功能设置。每个模块都围绕老年人的核心需求设计,力求操作简单、信息清晰、使用安心。
技术实现上,这款 App 展现了诸多亮点:面向老年用户的友好设计(大字体、高对比度、清晰按钮)、完整的健康数据模型(8项健康指标)、贴心的用药提醒系统(紧急标识、服用状态)、完善的紧急联系人体系(一级联系人、快速呼叫)、丰富的养生知识推送。特别值得一提的是,App 在"适老化"设计方面做了很多细致的考虑,比如更大的点击区域、更清晰的状态区分、更直接的操作入口。本文将逐一深入分析这些技术细节,探讨科技如何为养老服务增添温度。
二、整体架构流程图

在深入代码细节之前,让我们通过架构图从宏观层面了解这款养老服务 App 的整体结构、数据流向和组件关系。
数据交互流程图
健康数据流转图
三、颜色与主题系统详解

3.1 颜色调色板接口设计
颜色系统是 App 视觉风格的基础。这款养老服务 App 采用接口定义 + 常量实现的双层架构,先定义结构,再提供色值。
interface ElderColorPalette {
primary: string
primaryLight: string
primaryDark: string
accent: string
accentLight: string
warm: string
bg: string
cardBg: string
textPrimary: string
textSecondary: string
textHint: string
border: string
success: string
warning: string
danger: string
white: string
}
ElderColorPalette 接口定义了 16 个颜色字段,构成完整的调色板系统。接口先行的设计方式确保了类型安全和良好的可维护性。
字段命名采用语义化策略:primary 系列是主色调(暖橙),accent 系列是强调色(静谧蓝),warm 是暖色调辅助色,textPrimary/textSecondary/textHint 三级文本色阶,success/warning/danger 三个语义色分别对应成功、警告、危险状态。
对于老年用户来说,颜色的对比度和辨识度尤为重要。这款 App 的颜色系统在设计时充分考虑了这一点——主色与背景色、文本色与背景色之间都保持了足够的对比度,确保老年用户能够清晰地阅读和识别。
3.2 配色方案的设计理念
const COLORS: ElderColorPalette = {
primary: '#FF8A65',
primaryLight: '#FFAB91',
primaryDark: '#E64A19',
accent: '#5C6BC0',
accentLight: '#9FA8DA',
warm: '#FFF3E0',
bg: '#FFF8F5',
cardBg: '#FFFFFF',
textPrimary: '#3E2723',
textSecondary: '#6D4C41',
textHint: '#BCAAA4',
border: '#FFE0B2',
success: '#43A047',
warning: '#FB8C00',
danger: '#E53935',
white: '#FFFFFF'
}
这款养老服务 App 的配色方案围绕"温暖、安心、关怀"的核心主题展开。
主色调——暖橙色 #FF8A65:这是一种温暖柔和的橙色,不像鲜红色那样刺激,也不像淡黄色那样清淡。暖橙色传递出温暖、关怀、亲切的感觉,如同阳光般让人感到安心舒适。对于养老服务来说,温暖感是最重要的情感诉求,橙色恰好能唤起这种情感。primaryLight 浅橙 #FFAB91 和 primaryDark 深橙 #E64A19 构成完整的橙色色阶。
强调色——静谧蓝 #5C6BC0:这是一种柔和的蓝紫色,沉稳而不失活力。蓝色代表信任、专业、可靠,与医疗健康行业的属性高度契合。静谧蓝与暖橙色形成了温暖与冷静的平衡——橙色传递关怀,蓝色提供专业保障。accentLight 浅蓝紫 #9FA8DA 用于渐变和次要强调。
暖色辅助色 #FFF3E0:这是一种非常浅的橙色,几乎是带有橙色调的白色。它主要用于输入框背景、标签背景等次要 UI 元素,既保持了温暖的主题色调,又提供了足够的对比度。
背景色 #FFF8F5:一种淡淡的暖橙色背景,比纯白更柔和,对眼睛更友好,尤其适合老年用户长时间使用。暖色调的背景也让整个 App 看起来更加温馨。
文本色设计:主要文本使用深棕色 #3E2723,而非常规的深灰色。棕色比黑色更柔和,又比灰色更有温度,在保证可读性的同时增添了温暖感。次要文本 #6D4C41 和提示文本 #BCAAA4 形成了清晰的三级色阶。
边框色 #FFE0B2:浅橙色边框,与暖色调背景和主题保持一致,而不是使用冷冰冰的灰色边框。
整体来看,这套配色方案成功地营造了温暖、安心、专业的养老服务氛围。橙色的温暖、蓝色的专业、棕色的柔和相互交织,形成了独特而统一的视觉语言。对于老年用户来说,高对比度的文本、清晰的状态颜色、温暖的整体色调,都体现了"适老化"的设计考量。
3.3 工具函数:状态颜色映射
function getStatusColor(status: string): string {
if (status === '正常' || status === '良好' || status === '达标') {
return COLORS.success
} else if (status === '偏胖' || status === '需补牙') {
return COLORS.warning
}
return COLORS.textSecondary
}
getStatusColor 函数根据健康状态返回对应的颜色。正常/良好/达标状态返回绿色(成功色),偏胖/需补牙等需要关注的状态返回橙色(警告色),其他情况返回次要文本色。
这种"状态-颜色"映射机制在健康管理类应用中非常重要。老年人通常对数字的敏感度较低,但对颜色的感知仍然很好。通过用绿色表示健康、用橙色表示需要关注,用户可以一目了然地了解自己的健康状况,不需要仔细阅读每个数值和说明。这是一种非常符合老年用户认知特点的设计。
四、数据模型层深度解析
数据模型是应用功能的基石。这款养老服务 App 定义了 8 个数据接口,覆盖用药、健康、活动、联系人、记录、知识等多个养老服务领域。
4.1 MedicineMeta 用药接口
interface MedicineMeta {
name: string
emoji: string
dose: string
time: string
isDone: boolean
isUrgent: boolean
}
MedicineMeta 用药模型是养老 App 的核心数据模型之一。它包含药品名称、图标、剂量、服用时间、是否已服用、是否紧急等字段。
dose 剂量字段记录每次服用的药量(如"1粒"、“半片”、"1包"等),确保用药安全准确。time 字段是服用时间,支持一天多次用药。isDone 字段标记是否已经服用,是用药提醒功能的核心状态。
isUrgent 紧急字段是一个重要的设计亮点。它标记紧急/重要的药品(如心脏药、降压药等),在 UI 上会以红色突出显示,提醒用户这些药物必须按时服用,不能遗漏。这种分级提醒机制对于保障老年人用药安全非常重要。
4.2 HealthMeta 健康指标接口
interface HealthMeta {
name: string
emoji: string
value: string
status: string
isNormal: boolean
}
HealthMeta 健康指标模型描述各项健康数据。value 字段使用字符串类型(而不是数字),因为不同的健康指标有不同的单位和格式——血压是"130/85",血糖是"6.2",体重是"65.8kg",步数是"6,580"。使用字符串可以统一存储各种格式的健康数据。
status 字段是健康状态描述(正常/偏胖/良好/达标等),isNormal 布尔字段标记是否正常。两个字段配合使用,既提供了文字描述,又方便逻辑判断。
4.3 ActivityMeta 社区活动接口
interface ActivityMeta {
name: string
emoji: string
time: string
location: string
isHot: boolean
}
ActivityMeta 社区活动模型描述社区为老年人组织的各类活动。与其他 App 的活动模型不同,这里的活动增加了 location 地点字段,因为社区活动通常在社区内的不同场所(活动中心、棋牌室、音乐室、花园等)举行,地点信息对老年人来说很重要。
活动类型也非常贴合老年人的需求:太极晨练、健康讲座、棋牌比赛、手工编织、合唱排练、散步交友、电影放映、血压测量等,涵盖了健身、学习、娱乐、社交、医疗等多个方面。
4.4 WeekStepMeta 周步数接口
interface WeekStepMeta {
label: string
value: number
}
WeekStepMeta 周步数模型用于步数柱状图展示。采用通用的 label-value 结构,使得柱状图组件可以复用。value 的单位是千步(5 代表 5000 步),这样数值不会太大,便于柱状图的高度计算。
4.5 ContactMeta 紧急联系人接口
interface ContactMeta {
name: string
emoji: string
relation: string
phone: string
isPrimary: boolean
}
ContactMeta 紧急联系人模型是养老 App 的特色功能。它包含联系人姓名、头像、关系、电话和是否为主要联系人等字段。
relation 关系字段说明联系人与老人的关系(儿子、女儿、家庭医生、社区护士、邻居、好友等),帮助老人快速识别联系人身份。isPrimary 字段标记主要联系人,主要联系人的呼叫按钮使用醒目的红色背景,非主要联系人使用边框样式。这种分级设计让老人在紧急情况下能够快速找到最应该联系的人。
紧急联系人功能是养老服务中最核心的功能之一。对于独居老人来说,一键呼叫功能可能在关键时刻挽救生命。因此,这个功能被放在了首页的显著位置,而且操作按钮足够大、足够醒目。
4.6 RecordMeta 体检记录接口
interface RecordMeta {
name: string
emoji: string
date: string
result: string
isDone: boolean
}
RecordMeta 体检记录模型记录老人的各项体检和检查记录。result 字段存储检查结果的摘要信息,isDone 区分已完成的检查和待进行的检查。
体检记录功能帮助老人管理自己的健康档案。随着年龄的增长,老年人需要做的检查项目越来越多,容易忘记。通过记录管理,老人可以清楚地知道做过哪些检查、结果如何、还有哪些检查需要做。
4.7 KnowledgeMeta 养生知识接口
interface KnowledgeMeta {
title: string
emoji: string
desc: string
}
KnowledgeMeta 养生知识模型提供健康养生相关的小知识。每个知识点有一个标题和简短描述,配合 emoji 图标增加可读性。养生知识涵盖饮食、运动、睡眠、慢病管理、心理健康、安全防护等多个方面,帮助老年人学习健康知识,提高自我保健能力。
4.8 TabMeta 与 MenuMeta
interface TabMeta {
title: string
icon: string
}
interface MenuMeta {
icon: string
name: string
sub: string
}
TabMeta 和 MenuMeta 是通用型配置接口,分别用于导航和功能菜单。菜单列表中的紧急呼叫、定位服务等功能都与养老服务的核心需求密切相关。
五、配置常量层分析

5.1 TAB_CONFIG 导航配置
const TAB_CONFIG: Record<string, TabMeta> = {
'HOME': { title: '首页', icon: '🏠' },
'HEALTH': { title: '健康', icon: '❤️' },
'MEDS': { title: '用药', icon: '💊' },
'COMM': { title: '社区', icon: '👥' },
'MINE': { title: '我的', icon: '👤' }
}
TAB_CONFIG 定义了五个 Tab 的配置。五个 Tab 分别是:首页(🏠 房子)、健康(❤️ 心形)、用药(💊 药丸)、社区(👥 人群)、我的(👤 用户)。
Tab 的图标选择充分考虑了老年用户的认知特点——心形代表健康、药丸代表用药,都是非常直观的符号,不需要学习就能理解。五个 Tab 的功能划分也很清晰:首页看概览、健康管身体、用药管吃药、社区找活动、我的管个人。
5.2 ElderTab 枚举
enum ElderTab {
HOME,
HEALTH,
MEDS,
COMM,
MINE
}
ElderTab 枚举定义了五个 Tab 的标识符。枚举与配置常量配合使用,形成"逻辑标识 + 展示数据"的双层结构。在代码逻辑中使用枚举值保证类型安全,在 UI 渲染时从配置中读取展示数据。
5.3 配置层设计模式总结
这款 App 的配置层同样采用了接口-枚举-常量的三层架构:
- 接口层:定义数据结构和类型约束
- 枚举层:定义业务逻辑标识符
- 常量层:提供具体的配置数据
这种分层设计保证了类型安全、易于维护、扩展性好。对于面向老年用户的应用来说,配置驱动的方式也便于后续进行适老化调整——比如需要增大字号、修改文案时,只需要修改配置而不需要改动逻辑代码。
六、Mock 数据层分析

6.1 Mock 数据的设计思路
const MEDICINE_LIST: MedicineMeta[] = [
{ name: '降压药', emoji: '💊', dose: '1粒', time: '08:00', isDone: true, isUrgent: false },
{ name: '降糖药', emoji: '🟡', dose: '半片', time: '08:00', isDone: true, isUrgent: false },
{ name: '钙片', emoji: '🦴', dose: '2粒', time: '12:30', isDone: true, isUrgent: false },
// ... 共10种药
]
Mock 数据是原型开发的重要组成部分。这款养老 App 的 Mock 数据设计非常贴近真实场景,数据类型丰富,分布合理。
用药数据(MEDICINE_LIST) 包含 10 种药品,涵盖降压药、降糖药、钙片、鱼油、维生素、安眠药、心脏药、感冒药、胃药等老年人常见的用药类型。服药时间分布在 08:00、09:00、12:30、15:00、18:00、20:00、22:00 等多个时间点,模拟了老人一天多次服药的真实情况。其中 4 种已服、6 种待服,2 种紧急药品(降压药和心脏药),数据分布非常真实。
用药数据的设计充分考虑了老年人的用药特点:
- 药品种类多(10 种)
- 服药时间分散(一天 5-6 个时间点)
- 有紧急/非紧急之分
- 剂量单位多样(粒、片、包)
这种真实感强的 Mock 数据不仅让界面看起来专业,也能帮助产品团队验证功能设计是否符合老年用户的实际需求。
6.2 健康数据的设计
const HEALTH_LIST: HealthMeta[] = [
{ name: '血压', emoji: '🩸', value: '130/85', status: '正常', isNormal: true },
{ name: '血糖', emoji: '🍬', value: '6.2', status: '正常', isNormal: true },
{ name: '心率', emoji: '❤️', value: '72', status: '正常', isNormal: true },
// ... 共8项指标
]
健康数据包含 8 项常见的健康指标:血压、血糖、心率、体温、体重、睡眠、血氧、步数。其中 6 项正常,2 项异常(体重偏胖)。这样的设计更符合真实情况——老年人通常有几项指标需要关注,但不会全部都有问题。
健康指标的数据格式各不相同,有范围值(血压)、单数值(血糖、心率、体温)、带单位的(体重、睡眠)、百分比(血氧)、千分位(步数)。这种多样性验证了 UI 组件对不同数据格式的适应性。
6.3 紧急联系人数据
const CONTACT_LIST: ContactMeta[] = [
{ name: '张明', emoji: '👨', relation: '儿子', phone: '138****8888', isPrimary: true },
{ name: '张丽', emoji: '👩', relation: '女儿', phone: '139****6666', isPrimary: true },
{ name: '王医生', emoji: '👨⚕️', relation: '家庭医生', phone: '137****2222', isPrimary: false },
// ... 共6位联系人
]
紧急联系人数据包含 6 位联系人,分为两级:主要联系人 2 位(儿子、女儿),普通联系人 4 位(家庭医生、社区护士、邻居、好友)。联系人类型覆盖了家人、医护人员、社区邻居等多个维度,构成了老年人的紧急支持网络。
电话号码使用脱敏格式(138****8888),既展示了号码格式,又保护了隐私安全。这是一个很细节的设计,体现了对用户隐私的尊重。
6.4 体检记录数据
const RECORD_LIST: RecordMeta[] = [
{ name: '年度体检', emoji: '🏥', date: '08-10', result: '基本正常', isDone: true },
{ name: '血压复查', emoji: '🩸', date: '08-05', result: '130/85 正常', isDone: true },
// ... 共10条记录
]
体检记录数据有 10 条,包括年度体检、血压复查、心电图、血常规、骨密度、眼底检查、血糖检测、B超检查、牙科检查等常见的老年体检项目。其中 9 条已完成,1 条待检查(下次体检)。
检查结果有好有坏——基本正常、指标正常,也有轻度疏松、轻度白内障、脂肪肝、需补牙等需要关注的问题。这种真实的数据分布让健康管理功能更有实际意义。
6.5 Mock 数据的价值
精心设计的 Mock 数据在产品开发中有多重价值:
功能验证:通过真实场景的数据,可以验证功能设计是否合理、信息展示是否清晰。
用户测试:在用户调研中使用真实感强的数据,可以获得更准确的用户反馈。
开发效率:前端可以基于 Mock 数据独立开发,不依赖后端接口。
演示效果:产品演示时,丰富真实的数据能让观众更好地理解产品价值。
对于养老类产品来说,Mock 数据的真实性尤为重要。老年人的需求和使用习惯与年轻人有很大差异,只有基于真实场景的数据,才能检验产品是否真正适合老年用户。
七、主入口组件逐段分析

7.1 状态管理设计
@Entry
@Component
struct ElderCareApp {
@State activeTab: ElderTab = ElderTab.HOME
ElderCareApp 是应用的主入口组件。@Entry 装饰器标记这是页面入口,@Component 标记这是一个组件。
组件内部只有一个状态变量 activeTab,使用 @State 装饰器修饰,初始值为首页。@State 是 ArkTS 的响应式状态装饰器,被它修饰的变量变化时,依赖它的 UI 会自动重新渲染。
对于养老 App 来说,简单的状态管理尤其重要。复杂的状态管理方案不仅增加了开发成本,也可能引入更多的 bug。单一状态变量控制 Tab 切换的方式简单直观,状态流向清晰,不容易出错。这符合养老产品"稳"和"简"的设计原则。
7.2 contentArea 内容构建器
@Builder contentArea() {
Column() {
if (this.activeTab === ElderTab.HOME) {
ElderHomeContent()
} else if (this.activeTab === ElderTab.HEALTH) {
ElderHealthContent()
} else if (this.activeTab === ElderTab.MEDS) {
ElderMedsContent()
} else if (this.activeTab === ElderTab.COMM) {
ElderCommContent()
} else if (this.activeTab === ElderTab.MINE) {
ElderMineContent()
}
}
.width('100%')
}
contentArea 是一个 @Builder 自定义构建函数,根据 activeTab 的值渲染对应的页面组件。使用条件渲染的方式实现页面切换,逻辑简单直观。
五个页面对应五个独立的组件,每个组件有自己的状态和逻辑,实现了关注点分离。首页负责概览信息,健康页负责健康数据,用药页负责用药提醒,社区页负责活动信息,我的页负责个人管理。职责清晰,边界明确。
使用 @Builder 将内容区域抽离出来,让主 build 方法更加简洁,只需要关注整体布局结构。这种代码组织方式也便于后续维护——修改内容区域时只需要改这个构建函数,不影响主布局。
7.3 tabItem 导航项构建器
@Builder tabItem(icon: string, label: string, tab: ElderTab) {
Column() {
Text(icon).fontSize(20)
Text(label).fontSize(10)
.fontColor(this.activeTab === tab ? COLORS.primary : COLORS.textHint)
.fontWeight(this.activeTab === tab ? FontWeight.Bold : FontWeight.Normal)
.margin({ top: 2 })
}
.layoutWeight(1)
.onClick(() => { this.activeTab = tab })
}
tabItem 构建函数创建底部导航的每个 Tab 项。接收图标、标签和枚举值三个参数,返回一个垂直排列的 Column。
图标使用 20px 的 emoji,标签文字 10px。选中状态使用主色(暖橙)粗体,未选中状态使用提示色常规字重。橙色的选中状态在浅橙色的背景上非常醒目,老年用户可以清楚地看到当前在哪个页面。
每个 Tab 项使用 layoutWeight(1) 平均分配宽度,五个 Tab 等宽排列。点击事件更新 activeTab 状态,触发页面切换。
对于老年用户来说,底部导航的设计需要特别注意可点击区域的大小。虽然文字只有 10px,但整个 Tab 项的高度足够(包含图标、文字和上下内边距),确保了足够的点击区域,不容易误触。
7.4 build 方法整体布局
build() {
Column() {
this.contentArea()
Row() {
this.tabItem(TAB_CONFIG.HOME.icon, TAB_CONFIG.HOME.title, ElderTab.HOME)
this.tabItem(TAB_CONFIG.HEALTH.icon, TAB_CONFIG.HEALTH.title, ElderTab.HEALTH)
this.tabItem(TAB_CONFIG.MEDS.icon, TAB_CONFIG.MEDS.title, ElderTab.MEDS)
this.tabItem(TAB_CONFIG.COMM.icon, TAB_CONFIG.COMM.title, ElderTab.COMM)
this.tabItem(TAB_CONFIG.MINE.icon, TAB_CONFIG.MINE.title, ElderTab.MINE)
}
.width('100%')
.padding({ top: 8, bottom: 8 })
.backgroundColor(COLORS.white)
.shadow({ radius: 10, color: '#FF8A6526', offsetY: -2 })
}
.width('100%').height('100%')
.backgroundColor(COLORS.bg)
}
build 方法定义了整体布局。外层是一个全屏的 Column,分为内容区和底部导航栏两部分。
导航栏是白色背景,上下各 8px 内边距。导航栏有一个向上的阴影,阴影颜色使用主色的半透明版本(#FF8A6526,即暖橙色加上约 15% 的不透明度)。暖橙色的阴影与白色导航栏和暖米色背景搭配,营造出温暖柔和的视觉效果。
页面背景使用 COLORS.bg 暖橙色背景,温暖柔和。整个应用的色调统一在暖橙色系中,给人温馨舒适的感觉,非常符合养老服务的主题。
八、各页面组件逐段代码分析

8.1 首页组件 ElderHomeContent
首页是老年用户打开 App 后看到的第一个页面,需要展示最关键的健康信息和最常用的功能入口。养老 App 的首页包含健康评分头卡、周步数柱状图、紧急联系人和社区活动等模块。
8.1.1 新增用药提醒弹窗
@Builder addReminderModal() {
Stack() {
this.modalOverlay(() => { this.showAddReminderModal = false })
Column() {
Row() {
Text('⏰ 新增用药提醒').fontSize(18).fontWeight(FontWeight.Bold).fontColor(COLORS.textPrimary)
Column().layoutWeight(1)
Text('✕').fontSize(18).fontColor(COLORS.textHint).onClick(() => { this.showAddReminderModal = false })
}
.width('100%').padding({ left: 20, right: 20, top: 18, bottom: 12 })
Divider().color(COLORS.border)
Column() {
Text('药品名称').fontSize(12).fontColor(COLORS.textSecondary).margin({ top: 12, left: 20 })
TextInput({ placeholder: '如 降压药' })
.placeholderColor(COLORS.textHint).fontSize(14).width('90%')
.backgroundColor(COLORS.warm).borderRadius(8)
.margin({ left: 20, right: 20, top: 4 })
.onChange((v: string) => { this.medName = v })
Text('服用时间').fontSize(12).fontColor(COLORS.textSecondary).margin({ top: 14, left: 20 })
TextInput({ placeholder: '如 08:00' })
.placeholderColor(COLORS.textHint).fontSize(14).width('90%')
.backgroundColor(COLORS.warm).borderRadius(8)
.margin({ left: 20, right: 20, top: 4 })
.onChange((v: string) => { this.medTime = v })
Text('剂量').fontSize(12).fontColor(COLORS.textSecondary).margin({ top: 14, left: 20 })
TextInput({ placeholder: '如 1粒' })
.placeholderColor(COLORS.textHint).fontSize(14).width('90%')
.backgroundColor(COLORS.warm).borderRadius(8)
.margin({ left: 20, right: 20, top: 4 })
Row() {
Text('取消').fontSize(14).fontColor(COLORS.textSecondary)
.layoutWeight(1).textAlign(TextAlign.Center)
.padding({ top: 10, bottom: 10 })
.backgroundColor(COLORS.warm).borderRadius(8)
.onClick(() => { this.showAddReminderModal = false })
Column().width(12)
Text('添加').fontSize(14).fontColor(COLORS.white)
.layoutWeight(1).textAlign(TextAlign.Center)
.padding({ top: 10, bottom: 10 })
.backgroundColor(COLORS.primary).borderRadius(8)
.onClick(() => { this.showAddReminderModal = false })
}
.margin({ left: 20, right: 20, top: 20, bottom: 20 })
}
.constraintSize({ maxHeight: '70%' })
}
.width('88%')
.backgroundColor(COLORS.cardBg).borderRadius(16)
}
.width('100%').height('100%')
}
新增用药提醒弹窗是首页的重要功能入口。弹窗采用 Stack 堆叠布局,底层是半透明遮罩,上层是白色内容卡片。
弹窗内容包含三个表单字段:
- 药品名称:用户输入要添加的药品名称
- 服用时间:用户输入服药时间
- 剂量:用户输入每次服用的剂量
输入框的背景使用暖色 COLORS.warm(浅橙色),与整体暖色调主题一致。输入框有足够的高度和宽度,便于老年用户点击和输入。
底部操作按钮有两个:取消按钮(暖色背景、次要文字色)和添加按钮(主色背景、白色文字)。两个按钮大小相同、位置对称,符合老年用户的操作习惯。
弹窗内容设置了 constraintSize({ maxHeight: '70%' }),确保在小屏幕设备上也不会超出显示范围。
8.1.2 健康评分+步数头卡
Stack() {
Column()
.width('100%').height(190)
.linearGradient({ angle: 135, colors: [[COLORS.primary, 0.0], [COLORS.primaryDark, 1.0]] })
.borderRadius({ bottomLeft: 24, bottomRight: 24 })
Column() {
Row() {
Column() {
Text('❤️ 今日健康').fontSize(11).fontColor('#FFCCBC')
Text('92').fontSize(44).fontWeight(FontWeight.Bold).fontColor(COLORS.white)
.margin({ top: 2 })
Text('健康评分 · 良好').fontSize(10).fontColor('#FFCCBC').margin({ top: 2 })
}
.alignItems(HorizontalAlign.Start).layoutWeight(1)
Stack() {
Column().width(76).height(76).borderRadius(38)
.backgroundColor('rgba(255,255,255,0.15)')
Column().width(56).height(56).borderRadius(28)
.backgroundColor(COLORS.accent)
Column() {
Text('6,580').fontSize(14).fontWeight(FontWeight.Bold).fontColor(COLORS.white)
Text('步').fontSize(9).fontColor('#C5CAE9')
}
.alignItems(HorizontalAlign.Center)
}
.width(76).height(76)
}
.width('100%')
.padding({ left: 20, right: 20, top: 34 })
Row() {
Text('👣 6,580步 / 8,000步').fontSize(10).fontColor('#FFCCBC')
Column().layoutWeight(1)
Text('血压 130/85 ✓').fontSize(10).fontColor(COLORS.accentLight)
.backgroundColor('rgba(255,255,255,0.15)')
.borderRadius(8).padding({ left: 6, right: 6, top: 2, bottom: 2 })
}
.width('100%')
.padding({ left: 20, right: 20, top: 14 })
}
}
.width('100%').height(190)
健康评分头卡是首页最醒目的元素,高度 190px,使用从暖橙到深橙的 135 度渐变背景。
头卡内容分为上下两行:
上行左侧是健康评分信息——小标题"今日健康"、大大的数字"92"(健康评分)、评分等级"良好"。44px 的超大数字在渐变背景上非常醒目,老年用户可以一眼看到自己的健康评分。
上行右侧是步数信息,用三层同心圆装饰:最外层是半透明白色圆环,中间是静谧蓝实心圆,内部是步数数字(6,580)和单位(步)。蓝色的圆形在橙色背景上形成鲜明对比,突出了步数信息。
下行左侧显示当日步数和目标(6,580步 / 8,000步),右侧是血压状态标签(血压 130/85 ✓)。血压标签使用半透明白色背景和浅蓝色文字,呈现为信息徽章的形式。
头卡的信息层次设计非常合理:健康评分最大最醒目(用户最关心的综合指标),步数信息次之(重要的运动指标),血压等其他信息作为补充。通过字号和位置的差异,用户可以快速获取关键信息。
对于老年用户来说,大字号的健康评分设计尤为重要。很多老年人视力下降,小字体阅读困难,而大数字配合颜色编码(92分+良好=健康),让老人可以直观地了解自己的健康状况。
8.1.3 周步数柱状图
Column() {
Text('📊 本周步数(千步)').fontSize(14).fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary).margin({ left: 16, top: 16 })
Row() {
ForEach(WEEK_STEP, (w: WeekStepMeta) => {
Column() {
Stack({ alignContent: Alignment.Bottom }) {
Column().width(22).height(90)
.backgroundColor(COLORS.warm)
.borderRadius(4)
Column().width(22).height(Number(w.value) * 7)
.linearGradient({ angle: 0, colors: [[COLORS.accent, 0.0], [COLORS.primary, 1.0]] })
.borderRadius(4)
}
.width(22).height(90)
Text(w.label).fontSize(9).fontColor(COLORS.textSecondary)
.margin({ top: 4 })
}
.alignItems(HorizontalAlign.Center)
.layoutWeight(1)
})
}
.width('100%')
.padding({ left: 16, right: 16, top: 12, bottom: 8 })
}
周步数柱状图是一个纯代码实现的数据可视化组件,展示本周每天的步数情况。
柱状图使用 Stack 堆叠布局,底部对齐。底层是暖色背景柱(全高 90px),上层是渐变数据柱(高度 = 步数 × 7)。数据柱使用从蓝色到橙色的垂直渐变,上蓝下橙,与 App 的配色主题一致。
每个柱子宽 22px,圆角 4px。七根柱子通过 layoutWeight(1) 均匀分布。下方是星期标签,使用 9px 的次要文字色。
标题中明确标注了"千步"单位,避免用户对数值的单位产生困惑。对于老年用户来说,明确的单位标注非常重要,可以减少理解成本。
柱状图整体放在白色圆角卡片中,与其他模块保持一致的卡片样式。
8.1.4 紧急联系人列表
Column() {
Text('📞 紧急联系人').fontSize(14).fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary).margin({ left: 16, top: 8, bottom: 8 })
ForEach(CONTACT_LIST, (c: ContactMeta) => {
Row() {
Stack() {
Column().width(40).height(40).borderRadius(20)
.backgroundColor(c.isPrimary ? COLORS.warm : COLORS.border)
Text(c.emoji).fontSize(24)
}
.width(40).height(40).margin({ right: 12 })
Column() {
Text(c.name).fontSize(13).fontColor(COLORS.textPrimary).fontWeight(FontWeight.Medium)
Text(c.relation + ' · ' + c.phone).fontSize(10).fontColor(COLORS.textSecondary).margin({ top: 2 })
}
.alignItems(HorizontalAlign.Start).layoutWeight(1)
if (c.isPrimary) {
Text('呼叫').fontSize(11).fontColor(COLORS.white)
.backgroundColor(COLORS.danger).borderRadius(12)
.padding({ left: 12, right: 12, top: 5, bottom: 5 })
} else {
Text('呼叫').fontSize(11).fontColor(COLORS.accent)
.border({ width: 1, color: COLORS.accent }).borderRadius(12)
.padding({ left: 10, right: 10, top: 4, bottom: 4 })
}
}
.width('100%')
.padding({ left: 16, right: 16, top: 10, bottom: 10 })
.border({ width: { bottom: 1 }, color: COLORS.border })
})
}
紧急联系人模块是养老 App 最核心的功能之一。它以列表形式展示所有紧急联系人,每条记录包含头像、姓名关系电话和呼叫按钮。
左侧头像:40x40px 的圆形头像。主要联系人使用暖色背景(COLORS.warm),普通联系人使用边框色背景。通过背景色区分联系人等级,主要联系人更加醒目。
中间信息:姓名使用 13px 中等字重,关系和电话使用 10px 次要文字色。电话号码使用脱敏格式(138****8888),保护隐私。
右侧呼叫按钮:主要联系人使用红色(危险色 COLORS.danger)实心按钮,白色文字,非常醒目。普通联系人使用静谧蓝边框按钮。红色的实心按钮在视觉上更加突出,引导用户在紧急情况下优先联系最亲近的人。
呼叫按钮的尺寸足够大(左右各 12px 内边距,上下 5px),确保老年用户可以轻松点击,不容易误触。
紧急联系人放在首页的重要位置,体现了养老 App 的核心价值——安全。对于独居老人来说,一键呼叫功能是最重要的安全保障。
8.1.5 社区活动横滑
Row() {
Text('🏡 社区活动').fontSize(14).fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary).layoutWeight(1)
Text('查看全部 >').fontSize(11).fontColor(COLORS.textHint)
}
.width('100%')
.padding({ left: 16, right: 16, top: 16 })
Scroll() {
Row() {
ForEach(ACTIVITY_LIST, (a: ActivityMeta) => {
Column() {
Stack() {
Column().width(68).height(68).borderRadius(14)
.linearGradient({ angle: 135, colors: [[COLORS.warm, 0.0], [COLORS.accentLight, 1.0]] })
Text(a.emoji).fontSize(34)
if (a.isHot) {
Text('🔥').fontSize(12).position({ x: 46, y: -4 })
}
}
.width(68).height(68)
Text(a.name).fontSize(10).fontColor(COLORS.textPrimary)
.margin({ top: 6 }).maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
Text('⏰ ' + a.time).fontSize(8).fontColor(COLORS.textSecondary).margin({ top: 2 })
Text('📍 ' + a.location).fontSize(8).fontColor(COLORS.textHint).margin({ top: 1 })
}
.width(76)
.margin({ left: 8 })
.alignItems(HorizontalAlign.Center)
})
}
.padding({ left: 16, right: 16 })
}
社区活动横滑模块展示社区的各类活动,采用水平滚动的卡片形式。
每个活动卡片宽 76px,包含:
- 68x68px 的渐变图标区域(暖色到浅蓝紫渐变)
- 活动名称(10px,一行省略)
- 活动时间(8px,次要文字色)
- 活动地点(8px,提示色)
活动信息虽然字号不大,但层次清晰——名称最大最醒目,时间和地点作为辅助信息。对于老年人来说,活动的时间和地点非常重要,因此都显示在了卡片上,不需要点击进入详情就能看到关键信息。
水平滚动的方式节省了垂直空间,同时也给了用户自主浏览的控制权。用户可以通过左右滑动查看更多活动,找到自己感兴趣的内容。
8.2 健康页面 ElderHealthContent
健康页面展示用户的各项健康指标和体检记录,是健康管理的核心页面。
8.2.1 健康指标网格
Text('健康指标').fontSize(14).fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
.margin({ left: 16, top: 16, bottom: 8 })
Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.SpaceBetween }) {
ForEach(HEALTH_LIST, (h: HealthMeta) => {
Column() {
Stack() {
Column().width(48).height(48).borderRadius(12)
.backgroundColor(COLORS.warm)
Text(h.emoji).fontSize(28)
}
.width(48).height(48)
Text(h.name).fontSize(11).fontColor(COLORS.textPrimary)
.fontWeight(FontWeight.Medium).margin({ top: 8 })
Text(h.value).fontSize(14).fontColor(h.isNormal ? COLORS.accent : COLORS.warning)
.fontWeight(FontWeight.Bold).margin({ top: 2 })
Text(h.status).fontSize(9)
.fontColor(getStatusColor(h.status))
.margin({ top: 2 })
}
.width('23%')
.padding({ top: 14, bottom: 14 })
.backgroundColor(COLORS.cardBg)
.borderRadius(12)
.margin({ bottom: 8 })
.alignItems(HorizontalAlign.Center)
})
}
健康指标使用四列网格布局(每个占 23% 宽度),展示 8 项健康指标。每个指标是一个独立的白色圆角卡片,内容居中对齐。
每张卡片包含:
- 图标:48x48px 暖色圆角背景 + emoji 图标
- 指标名称:11px 中等字重
- 指标数值:14px 粗体。正常指标使用静谧蓝色,异常指标使用警告橙色
- 状态文字:9px,颜色由
getStatusColor函数动态决定
颜色编码是健康指标展示的关键设计。正常的指标显示为蓝色,异常的显示为橙色。老年用户即使看不清具体的数值文字,也可以通过颜色大致判断健康状况——大部分是蓝色就说明身体不错,有橙色就说明需要关注。
四列网格布局在有限的屏幕空间内展示了所有 8 项指标,信息密度适中。卡片化的设计让每个指标都有独立的视觉边界,不容易混淆。
8.2.2 体检记录列表
ForEach(RECORD_LIST, (r: RecordMeta) => {
Row() {
Stack() {
Column().width(40).height(40).borderRadius(10)
.backgroundColor(COLORS.warm)
Text(r.emoji).fontSize(24)
}
.width(40).height(40).margin({ right: 12 })
Column() {
Text(r.name).fontSize(13).fontColor(COLORS.textPrimary).fontWeight(FontWeight.Medium)
Text(r.date + ' · ' + r.result).fontSize(10).fontColor(COLORS.textSecondary).margin({ top: 2 })
}
.alignItems(HorizontalAlign.Start).layoutWeight(1)
if (r.isDone) {
Text('已查').fontSize(10).fontColor(COLORS.success)
.backgroundColor('#E8F5E9').borderRadius(8)
.padding({ left: 8, right: 8, top: 4, bottom: 4 })
} else {
Text('待查').fontSize(10).fontColor(COLORS.warning)
.backgroundColor('#FFF3E0').borderRadius(8)
.padding({ left: 8, right: 8, top: 4, bottom: 4 })
}
}
.width('100%')
.padding({ left: 16, right: 16, top: 10, bottom: 10 })
.backgroundColor(COLORS.cardBg)
.border({ width: { bottom: 1 }, color: COLORS.border })
})
体检记录列表展示所有体检和检查项目的记录。每条记录包含左侧图标、中间信息和右侧状态标签。
左侧是 40x40px 的暖色圆角图标。中间是检查项目名称和日期+结果。右侧是状态标签:已完成的检查使用绿色背景+绿色文字(“已查”),待检查的使用橙色背景+橙色文字(“待查”)。
状态标签的设计让用户可以快速浏览记录列表,识别哪些检查已经做了、哪些还需要做。对于老年人来说,定期体检很重要,但也很容易忘记。清晰的"已查/待查"状态标记可以帮助老人管理自己的健康检查计划。
8.3 用药页面 ElderMedsContent
用药页面是养老 App 的核心功能之一,帮助老年人管理每日用药,避免漏服或错服。
ForEach(MEDICINE_LIST, (m: MedicineMeta) => {
Row() {
Stack() {
Column().width(44).height(44).borderRadius(12)
.backgroundColor(COLORS.warm)
Text(m.emoji).fontSize(26)
}
.width(44).height(44).margin({ right: 12 })
Column() {
Text(m.name).fontSize(14).fontColor(COLORS.textPrimary).fontWeight(FontWeight.Medium)
Row() {
Text('剂量 ' + m.dose).fontSize(10).fontColor(COLORS.textSecondary)
Text(' · ⏰ ' + m.time).fontSize(10).fontColor(COLORS.textSecondary)
if (m.isUrgent) {
Text(' · 紧急').fontSize(10).fontColor(COLORS.danger).fontWeight(FontWeight.Bold)
}
}
.margin({ top: 2 })
}
.alignItems(HorizontalAlign.Start).layoutWeight(1)
if (m.isDone) {
Text('✓ 已服').fontSize(11).fontColor(COLORS.success)
.backgroundColor('#E8F5E9').borderRadius(8)
.padding({ left: 10, right: 10, top: 5, bottom: 5 })
} else {
Text('提醒').fontSize(11).fontColor(COLORS.white)
.backgroundColor(m.isUrgent ? COLORS.danger : COLORS.primary)
.borderRadius(8)
.padding({ left: 10, right: 10, top: 5, bottom: 5 })
}
}
.width('100%')
.padding({ left: 16, right: 16, top: 10, bottom: 10 })
.backgroundColor(COLORS.cardBg)
.border({ width: { bottom: 1 }, color: COLORS.border })
})
用药列表展示所有需要服用的药品。每条药品记录包含左侧图标、中间信息和右侧状态/操作按钮。
左侧图标:44x44px 暖色圆角背景 + 药品 emoji 图标。
中间信息:
- 药品名称:14px 中等字重
- 剂量和时间:10px 次要文字色,用"·"分隔
- 紧急标记:如果是紧急药品,显示红色粗体的"紧急"字样
右侧按钮:
- 已服用:绿色背景 + 绿色文字"✓ 已服"
- 未服用:紧急药品显示红色背景"提醒",普通药品显示橙色背景"提醒"
紧急药品的特殊标记是这个功能的亮点。紧急药品在信息区有红色"紧急"标签,在操作区也使用红色背景按钮。双重红色警示确保用户不会忽视这些重要药物。对于患有心脏病、高血压等慢性病的老人来说,按时服药至关重要,红色警示可以有效提醒。
操作按钮设计得很大(左右各 10px 内边距,上下 5px),方便老年用户点击。"提醒"按钮的文案也很直观——用户一看就知道点击可以设置或查看提醒。
8.4 社区页面 ElderCommContent
社区页面展示社区组织的各类活动和养生知识,丰富老年人的精神生活。
8.4.1 今日活动列表
ForEach(ACTIVITY_LIST, (a: ActivityMeta) => {
Row() {
Stack() {
Column().width(44).height(44).borderRadius(12)
.linearGradient({ angle: 135, colors: [[COLORS.warm, 0.0], [COLORS.accentLight, 1.0]] })
Text(a.emoji).fontSize(26)
}
.width(44).height(44).margin({ right: 12 })
Column() {
Row() {
Text(a.name).fontSize(14).fontColor(COLORS.textPrimary).fontWeight(FontWeight.Medium)
if (a.isHot) {
Text(' 🔥').fontSize(10)
}
}
Text('⏰ ' + a.time + ' · 📍 ' + a.location).fontSize(10).fontColor(COLORS.textSecondary).margin({ top: 2 })
}
.alignItems(HorizontalAlign.Start).layoutWeight(1)
Text('报名').fontSize(11).fontColor(COLORS.white)
.backgroundColor(COLORS.accent).borderRadius(12)
.padding({ left: 12, right: 12, top: 5, bottom: 5 })
}
.width('100%')
.padding({ left: 16, right: 16, top: 10, bottom: 10 })
.backgroundColor(COLORS.cardBg)
.border({ width: { bottom: 1 }, color: COLORS.border })
})
今日活动列表以列表形式展示当天的社区活动。每条活动包含左侧渐变图标、中间活动信息和右侧报名按钮。
活动信息包含活动名称、热门标记、时间和地点。时间和地点放在同一行,用 emoji 图标辅助说明,老年用户可以直观地看到活动什么时候、在哪里举行。
右侧是静谧蓝色的"报名"按钮,白色文字,按钮尺寸足够大。报名按钮的设计鼓励老年人参与社区活动,丰富退休生活。社区活动对于老年人的身心健康非常重要,社交互动可以有效预防认知衰退和抑郁。
8.4.2 养生知识横滑
Text('📖 养生知识').fontSize(14).fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
.margin({ left: 16, top: 16, bottom: 8 })
Scroll() {
Row() {
ForEach(KNOWLEDGE_LIST, (k: KnowledgeMeta) => {
Column() {
Stack() {
Column().width(64).height(64).borderRadius(12)
.linearGradient({ angle: 135, colors: [[COLORS.warm, 0.0], [COLORS.accentLight, 1.0]] })
Text(k.emoji).fontSize(32)
}
.width(64).height(64)
Text(k.title).fontSize(10).fontColor(COLORS.textPrimary)
.fontWeight(FontWeight.Medium).margin({ top: 6 })
Text(k.desc).fontSize(8).fontColor(COLORS.textHint).margin({ top: 1 })
}
.width(72)
.margin({ left: 8 })
.alignItems(HorizontalAlign.Center)
})
}
.padding({ left: 16, right: 16 })
}
养生知识模块以水平滚动的卡片形式展示健康养生小知识。每个知识点包含渐变图标、标题和简短描述。
知识点涵盖了秋冬饮食、关节保养、睡眠改善、血压管理、心理健康、防跌倒等老年人关心的健康话题。每个知识点只有一个核心要点(如"温补为主"、“适度运动”、“规律作息”),简洁易记。
对于老年人来说,健康知识的获取非常重要,但太多太复杂的内容又记不住。这种"一个知识点一句话"的形式,既传播了健康知识,又不会给老人造成认知负担。用户可以在浏览中轻松学习,日积月累就能掌握不少养生知识。
8.5 我的页面 ElderMineContent
我的页面管理个人信息和系统功能,包含个人头卡、数据统计、编辑档案、体检记录和功能菜单。
8.5.1 编辑档案弹窗
@Builder editProfileModal() {
Stack() {
this.modalOverlay(() => { this.showEditProfileModal = false })
Column() {
Row() {
Text('✏️ 编辑档案').fontSize(18).fontWeight(FontWeight.Bold).fontColor(COLORS.textPrimary)
Column().layoutWeight(1)
Text('✕').fontSize(18).fontColor(COLORS.textHint).onClick(() => { this.showEditProfileModal = false })
}
.width('100%').padding({ left: 20, right: 20, top: 18, bottom: 12 })
Divider().color(COLORS.border)
Column() {
Text('姓名').fontSize(12).fontColor(COLORS.textSecondary).margin({ top: 12, left: 20 })
TextInput({ placeholder: '张大爷' })
.placeholderColor(COLORS.textHint).fontSize(14).width('90%')
.backgroundColor(COLORS.warm).borderRadius(8)
.margin({ left: 20, right: 20, top: 4 })
Text('年龄').fontSize(12).fontColor(COLORS.textSecondary).margin({ top: 14, left: 20 })
TextInput({ placeholder: '68' })
.placeholderColor(COLORS.textHint).fontSize(14).width('90%')
.backgroundColor(COLORS.warm).borderRadius(8)
.margin({ left: 20, right: 20, top: 4 })
Text('健康备注').fontSize(12).fontColor(COLORS.textSecondary).margin({ top: 14, left: 20 })
TextInput({ placeholder: '高血压·轻度骨质疏松' })
.placeholderColor(COLORS.textHint).fontSize(14).width('90%')
.backgroundColor(COLORS.warm).borderRadius(8)
.margin({ left: 20, right: 20, top: 4 })
Row() {
Text('取消').fontSize(14).fontColor(COLORS.textSecondary)
.layoutWeight(1).textAlign(TextAlign.Center)
.padding({ top: 10, bottom: 10 })
.backgroundColor(COLORS.warm).borderRadius(8)
.onClick(() => { this.showEditProfileModal = false })
Column().width(12)
Text('保存').fontSize(14).fontColor(COLORS.white)
.layoutWeight(1).textAlign(TextAlign.Center)
.padding({ top: 10, bottom: 10 })
.backgroundColor(COLORS.primary).borderRadius(8)
.onClick(() => { this.showEditProfileModal = false })
}
.margin({ left: 20, right: 20, top: 20, bottom: 20 })
}
.constraintSize({ maxHeight: '70%' })
}
.width('88%')
.backgroundColor(COLORS.cardBg).borderRadius(16)
}
.width('100%').height('100%')
}
编辑档案弹窗允许用户修改个人健康档案。弹窗包含三个字段:姓名、年龄、健康备注。
健康备注字段是一个很实用的设计。老年人通常有多种慢性病,将主要的健康问题记录在档案中,可以在紧急情况下帮助医护人员快速了解老人的健康状况。placeholder 中的示例"高血压·轻度骨质疏松"也很有指导意义,告诉用户应该填写什么内容。
输入框使用暖色背景,大圆角,字体大小适中,适合老年用户阅读和输入。保存按钮使用主色实心样式,醒目易点击。
8.5.2 删除记录弹窗
@Builder deleteRecordModal() {
Stack() {
this.modalOverlay(() => { this.showDeleteModal = false })
Column() {
Text('⚠️ 删除记录').fontSize(18).fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
.margin({ top: 24 })
Text('确定要删除「' + this.selectedRecord + '」吗?')
.fontSize(13).fontColor(COLORS.textSecondary)
.textAlign(TextAlign.Center)
.margin({ left: 20, right: 20, top: 12 })
Row() {
Text('取消').fontSize(14).fontColor(COLORS.textSecondary)
.layoutWeight(1).textAlign(TextAlign.Center)
.padding({ top: 10, bottom: 10 })
.backgroundColor(COLORS.warm).borderRadius(8)
.onClick(() => { this.showDeleteModal = false })
Column().width(12)
Text('删除').fontSize(14).fontColor(COLORS.white)
.layoutWeight(1).textAlign(TextAlign.Center)
.padding({ top: 10, bottom: 10 })
.backgroundColor(COLORS.danger).borderRadius(8)
.onClick(() => { this.showDeleteModal = false })
}
.margin({ left: 20, right: 20, top: 24, bottom: 24 })
}
.width('80%')
.backgroundColor(COLORS.cardBg).borderRadius(16)
}
.width('100%').height('100%')
}
删除记录弹窗是一个确认对话框,用于在删除体检记录前进行二次确认。删除按钮使用红色危险色背景,取消按钮使用暖色次要样式。
对于老年用户来说,确认删除尤其重要。老年人可能对触屏操作不太熟练,容易误触。二次确认可以有效防止误删除,避免数据丢失带来的麻烦。确认文字中动态插入了要删除的记录名称,让用户明确知道自己在删除什么。
九、弹窗组件详解
9.1 弹窗系统的统一架构
这款养老 App 包含三种弹窗:新增用药提醒、编辑档案、删除记录。它们共享统一的架构设计:
Stack 堆叠布局
├── modalOverlay 遮罩层(半透明黑色,点击关闭)
└── Column 内容卡片
├── Row 标题栏(图标+标题 + 关闭按钮)
├── Divider 分隔线
├── Column 内容区(表单字段 / 确认文字)
└── Row 操作按钮(取消 + 确认操作)
统一的弹窗架构保证了交互的一致性,老年用户在使用不同功能时遇到的弹窗模式相同,不需要重新学习,降低了使用门槛。
9.2 适老化设计考量
弹窗的适老化设计体现在多个方面:
大按钮:操作按钮足够大,左右内边距充足,确保老年用户可以准确点击。
高对比度:按钮文字与背景色对比度高,文字清晰可读。
明确的操作结果:取消和确认按钮分别使用不同的视觉样式,不会混淆。
清晰的标题:弹窗标题带有图标和明确的文字说明,用户一看就知道弹窗的用途。
遮罩点击关闭:点击遮罩层可以关闭弹窗,提供了多种关闭方式,适应用户不同的操作习惯。
对于养老类产品来说,弹窗的设计尤其需要谨慎。过多或过于复杂的弹窗会让老年用户感到困惑。但必要的确认和表单弹窗又是功能实现不可或缺的。关键是要让弹窗足够简单、足够清晰、操作路径足够短。
9.3 双弹窗绑定技巧
.bindSheet(this.showEditProfileModal, this.editProfileModal(), {
height: SheetSize.LARGE,
})
.bindSheet(this.showDeleteModal, this.deleteRecordModal(), {
height: SheetSize.LARGE,
})
在"我的"页面中,一个组件绑定了两个不同的弹窗——编辑档案弹窗和删除记录弹窗。这通过两次链式调用 bindSheet 实现,每个弹窗有独立的显示状态和内容构建器。
这种设计方式灵活高效——一个页面可以有多个弹窗,每个弹窗独立管理自己的状态,互不干扰。当需要显示某个弹窗时,只需要将对应的状态变量设为 true 即可。
十、核心 UI 组件分析
10.1 渐变头卡组件
渐变头卡是每个页面顶部的标志性组件,也是视觉冲击力最强的元素。
设计特点:
- 135 度线性渐变,从暖橙到深橙(或从蓝到橙)
- 底部大圆角(20-24px)
- 深色渐变背景配白色文字
- 大字号核心数据 + 小字号辅助信息
- 高度从 120px 到 190px 不等
首页头卡最为丰富,包含健康评分、步数、血压等多项信息,高度也最高(190px)。用药/健康头卡中等高度,展示统计概览。我的页头卡展示用户头像和基本信息。
渐变头卡不仅提供了视觉美感,也为每个页面建立了独特的身份标识——用户通过头卡的内容和颜色就能快速识别当前页面。
10.2 紧急联系人组件
紧急联系人是养老 App 的特色核心组件,直接关系到用户的安全保障。
设计亮点:
- 分级联系人体系(主要联系人/普通联系人)
- 红色紧急呼叫按钮(主要联系人)
- 大号点击区域,防止误触
- 关系+电话信息一目了然
- 首页突出位置展示
紧急联系人功能体现了养老 App 的核心价值——安全感。对于独居老人来说,一键联系到家人或医生是最重要的功能。将这个功能放在首页,并使用醒目的红色按钮,是非常正确的产品决策。
10.3 用药提醒列表
用药提醒列表是养老 App 的核心功能组件,帮助老年人安全用药。
设计亮点:
- 紧急药品红色双重警示(标签+按钮)
- 已服/待服状态清晰区分
- 剂量和时间信息完整
- 大按钮便于操作
- 暖色背景温馨舒适
用药安全是老年人健康管理的重中之重。很多老人需要同时服用多种药物,容易漏服或错服。清晰的用药列表和提醒功能可以有效帮助老人按时按量服药,减少用药差错。
10.4 健康指标网格
健康指标网格以四列卡片的形式展示各项健康数据。
设计亮点:
- 颜色编码(正常蓝色/异常橙色)
- 大字号数值便于阅读
- 图标+名称+数值+状态四级信息
- 卡片式设计边界清晰
- 四列布局信息密度适中
颜色编码是这个组件的核心设计。对于视力下降的老年人来说,颜色的差异可能比文字更容易识别。一眼扫过去,如果大部分是蓝色,说明身体状况不错;如果看到醒目的橙色,就知道那项指标需要关注。
十一、技术亮点总结表格
| 功能模块 | 技术实现方式 | 设计模式 | 代码量估计 | 亮点说明 |
|---|---|---|---|---|
| 颜色系统 | interface + const 常量 | 配置驱动 | 约 40 行 | 暖橙+静谧蓝主题,温暖专业,老年友好高对比度 |
| 数据模型 | 8 个 interface | 类型优先 | 约 75 行 | 用药紧急分级,健康状态标记,紧急联系人体系 |
| Tab 导航 | enum + @State + @Builder | 状态驱动 | 约 60 行 | 枚举驱动,配置分离,大点击区域适老化 |
| 首页模块 | 健康评分头卡 + 步数柱状图 + 联系人 | 信息聚合 | 约 170 行 | 健康评分大数字,紧急联系人红色呼叫,步数可视化 |
| 健康页面 | 四列指标网格 + 体检记录 | 卡片网格 | 约 95 行 | 颜色编码健康状态,四列布局紧凑,体检记录管理 |
| 用药页面 | 用药列表 + 状态分级 | 提醒系统 | 约 70 行 | 紧急药品红色双警示,已服/待服清晰区分,大按钮操作 |
| 社区页面 | 活动列表 + 养生知识横滑 | 社区互动 | 约 90 行 | 活动时间地点一目了然,养生知识轻量化学习 |
| 个人中心 | 头卡 + 数据 + 编辑 + 记录 + 菜单 | 个人管理 | 约 170 行 | 双弹窗绑定,档案编辑,记录删除确认,功能菜单 |
| 弹窗系统 | modalOverlay + bindSheet | 函数式组件 | 约 140 行 | 三种弹窗统一架构,适老化大按钮,二次确认防误触 |
| Mock 数据 | 7 组模拟数据 | 数据驱动 | 约 130 行 | 真实用药场景,健康数据多样性,紧急联系人分级 |
安装DevEco Studio程序

选择目标安装目录:

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

新建一个空白模板:

设置API为24的模板项目:
初始化项目,自动下载相关依赖:

完整代码:
// ============================================================
// 533.ets 养老服务 APP — ElderCareApp
// 主色调: 暖橙 #FF8A65 + 静谧蓝 #5C6BC0
// 布局: 健康评分环+步数头卡 + 周步数柱状图 + 用药提醒列表
// + 健康档案双列 + 社区活动横滑 + 紧急联系人 + 我的
// 弹窗: 新增提醒 / 编辑档案 / 删除记录 (全部 modalOverlay)
// ============================================================
interface ElderColorPalette {
primary: string
primaryLight: string
primaryDark: string
accent: string
accentLight: string
warm: string
bg: string
cardBg: string
textPrimary: string
textSecondary: string
textHint: string
border: string
success: string
warning: string
danger: string
white: string
}
interface TabMeta {
title: string
icon: string
}
interface MedicineMeta {
name: string
emoji: string
dose: string
time: string
isDone: boolean
isUrgent: boolean
}
interface HealthMeta {
name: string
emoji: string
value: string
status: string
isNormal: boolean
}
interface ActivityMeta {
name: string
emoji: string
time: string
location: string
isHot: boolean
}
interface WeekStepMeta {
label: string
value: number
}
interface ContactMeta {
name: string
emoji: string
relation: string
phone: string
isPrimary: boolean
}
interface RecordMeta {
name: string
emoji: string
date: string
result: string
isDone: boolean
}
interface MenuMeta {
icon: string
name: string
sub: string
}
interface KnowledgeMeta {
title: string
emoji: string
desc: string
}
const COLORS: ElderColorPalette = {
primary: '#FF8A65',
primaryLight: '#FFAB91',
primaryDark: '#E64A19',
accent: '#5C6BC0',
accentLight: '#9FA8DA',
warm: '#FFF3E0',
bg: '#FFF8F5',
cardBg: '#FFFFFF',
textPrimary: '#3E2723',
textSecondary: '#6D4C41',
textHint: '#BCAAA4',
border: '#FFE0B2',
success: '#43A047',
warning: '#FB8C00',
danger: '#E53935',
white: '#FFFFFF'
}
const TAB_CONFIG: Record<string, TabMeta> = {
'HOME': { title: '首页', icon: '🏠' },
'HEALTH': { title: '健康', icon: '❤️' },
'MEDS': { title: '用药', icon: '💊' },
'COMM': { title: '社区', icon: '👥' },
'MINE': { title: '我的', icon: '👤' }
}
const MEDICINE_LIST: MedicineMeta[] = [
{ name: '降压药', emoji: '💊', dose: '1粒', time: '08:00', isDone: true, isUrgent: false },
{ name: '降糖药', emoji: '🟡', dose: '半片', time: '08:00', isDone: true, isUrgent: false },
{ name: '钙片', emoji: '🦴', dose: '2粒', time: '12:30', isDone: true, isUrgent: false },
{ name: '鱼油', emoji: '🐟', dose: '1粒', time: '12:30', isDone: true, isUrgent: false },
{ name: '维生素D', emoji: '☀️', dose: '1粒', time: '18:00', isDone: false, isUrgent: false },
{ name: '降压药', emoji: '💊', dose: '1粒', time: '20:00', isDone: false, isUrgent: true },
{ name: '安眠药', emoji: '🌙', dose: '半片', time: '22:00', isDone: false, isUrgent: false },
{ name: '心脏药', emoji: '❤️', dose: '1粒', time: '22:00', isDone: false, isUrgent: true },
{ name: '感冒药', emoji: '🤧', dose: '1包', time: '09:00', isDone: true, isUrgent: false },
{ name: '胃药', emoji: '🟠', dose: '1粒', time: '15:00', isDone: false, isUrgent: false }
]
const HEALTH_LIST: HealthMeta[] = [
{ name: '血压', emoji: '🩸', value: '130/85', status: '正常', isNormal: true },
{ name: '血糖', emoji: '🍬', value: '6.2', status: '正常', isNormal: true },
{ name: '心率', emoji: '❤️', value: '72', status: '正常', isNormal: true },
{ name: '体温', emoji: '🌡️', value: '36.5', status: '正常', isNormal: true },
{ name: '体重', emoji: '⚖️', value: '65.8kg', status: '偏胖', isNormal: false },
{ name: '睡眠', emoji: '😴', value: '7.2h', status: '良好', isNormal: true },
{ name: '血氧', emoji: '🫁', value: '98%', status: '正常', isNormal: true },
{ name: '步数', emoji: '👣', value: '6,580', status: '达标', isNormal: true }
]
const ACTIVITY_LIST: ActivityMeta[] = [
{ name: '太极晨练', emoji: '🧘', time: '06:30', location: '社区广场', isHot: true },
{ name: '健康讲座', emoji: '📢', time: '09:00', location: '活动中心', isHot: true },
{ name: '棋牌比赛', emoji: '♟️', time: '14:00', location: '棋牌室', isHot: false },
{ name: '手工编织', emoji: '🧶', time: '15:00', location: '手工室', isHot: false },
{ name: '合唱排练', emoji: '🎵', time: '16:00', location: '音乐室', isHot: true },
{ name: '散步交友', emoji: '🚶', time: '17:00', location: '花园', isHot: false },
{ name: '电影放映', emoji: '🎬', time: '19:00', location: '放映室', isHot: true },
{ name: '血压测量', emoji: '🩺', time: '08:00', location: '医务室', isHot: false }
]
const WEEK_STEP: WeekStepMeta[] = [
{ label: '周一', value: 5 },
{ label: '周二', value: 8 },
{ label: '周三', value: 6 },
{ label: '周四', value: 7 },
{ label: '周五', value: 9 },
{ label: '周六', value: 12 },
{ label: '周日', value: 10 }
]
const CONTACT_LIST: ContactMeta[] = [
{ name: '张明', emoji: '👨', relation: '儿子', phone: '138****8888', isPrimary: true },
{ name: '张丽', emoji: '👩', relation: '女儿', phone: '139****6666', isPrimary: true },
{ name: '王医生', emoji: '👨⚕️', relation: '家庭医生', phone: '137****2222', isPrimary: false },
{ name: '李护士', emoji: '👩⚕️', relation: '社区护士', phone: '135****3333', isPrimary: false },
{ name: '刘邻居', emoji: '🧓', relation: '邻居', phone: '136****5555', isPrimary: false },
{ name: '赵阿姨', emoji: '👵', relation: '好友', phone: '133****7777', isPrimary: false }
]
const RECORD_LIST: RecordMeta[] = [
{ name: '年度体检', emoji: '🏥', date: '08-10', result: '基本正常', isDone: true },
{ name: '血压复查', emoji: '🩸', date: '08-05', result: '130/85 正常', isDone: true },
{ name: '心电图', emoji: '💓', date: '07-28', result: '窦性心律', isDone: true },
{ name: '血常规', emoji: '🧪', date: '07-15', result: '指标正常', isDone: true },
{ name: '骨密度', emoji: '🦴', date: '06-20', result: '轻度疏松', isDone: true },
{ name: '眼底检查', emoji: '👁️', date: '06-08', result: '轻度白内障', isDone: true },
{ name: '血糖检测', emoji: '🍬', date: '05-22', result: '6.2 正常', isDone: true },
{ name: 'B超检查', emoji: '🔬', date: '05-10', result: '脂肪肝', isDone: true },
{ name: '牙科检查', emoji: '🦷', date: '04-18', result: '需补牙', isDone: true },
{ name: '下次体检', emoji: '📋', date: '08-25', result: '待检查', isDone: false }
]
const MENU_LIST: MenuMeta[] = [
{ icon: '📞', name: '紧急呼叫', sub: '一键拨打' },
{ icon: '📍', name: '定位服务', sub: '实时位置共享' },
{ icon: '📝', name: '病历档案', sub: '10 份记录' },
{ icon: '💊', name: '药品管理', sub: '10 种药品' },
{ icon: '⚙️', name: '账号设置', sub: '个人信息' },
{ icon: '❓', name: '帮助中心', sub: '使用指南' }
]
const KNOWLEDGE_LIST: KnowledgeMeta[] = [
{ title: '秋冬饮食', emoji: '🍲', desc: '温补为主' },
{ title: '关节保养', emoji: '🦴', desc: '适度运动' },
{ title: '睡眠改善', emoji: '😴', desc: '规律作息' },
{ title: '血压管理', emoji: '🩸', desc: '定期监测' },
{ title: '心理健康', emoji: '🧠', desc: '保持乐观' },
{ title: '防跌倒', emoji: '🚶', desc: '注意安全' }
]
function getStatusColor(status: string): string {
if (status === '正常' || status === '良好' || status === '达标') {
return COLORS.success
} else if (status === '偏胖' || status === '需补牙') {
return COLORS.warning
}
return COLORS.textSecondary
}
enum ElderTab {
HOME,
HEALTH,
MEDS,
COMM,
MINE
}
@Entry
@Component
struct ElderCareApp {
@State activeTab: ElderTab = ElderTab.HOME
@Builder contentArea() {
Column() {
if (this.activeTab === ElderTab.HOME) {
ElderHomeContent()
} else if (this.activeTab === ElderTab.HEALTH) {
ElderHealthContent()
} else if (this.activeTab === ElderTab.MEDS) {
ElderMedsContent()
} else if (this.activeTab === ElderTab.COMM) {
ElderCommContent()
} else if (this.activeTab === ElderTab.MINE) {
ElderMineContent()
}
}
.width('100%')
}
@Builder tabItem(icon: string, label: string, tab: ElderTab) {
Column() {
Text(icon).fontSize(20)
Text(label).fontSize(10)
.fontColor(this.activeTab === tab ? COLORS.primary : COLORS.textHint)
.fontWeight(this.activeTab === tab ? FontWeight.Bold : FontWeight.Normal)
.margin({ top: 2 })
}
.layoutWeight(1)
.onClick(() => { this.activeTab = tab })
}
build() {
Column() {
this.contentArea()
Row() {
this.tabItem(TAB_CONFIG.HOME.icon, TAB_CONFIG.HOME.title, ElderTab.HOME)
this.tabItem(TAB_CONFIG.HEALTH.icon, TAB_CONFIG.HEALTH.title, ElderTab.HEALTH)
this.tabItem(TAB_CONFIG.MEDS.icon, TAB_CONFIG.MEDS.title, ElderTab.MEDS)
this.tabItem(TAB_CONFIG.COMM.icon, TAB_CONFIG.COMM.title, ElderTab.COMM)
this.tabItem(TAB_CONFIG.MINE.icon, TAB_CONFIG.MINE.title, ElderTab.MINE)
}
.width('100%')
.padding({ top: 8, bottom: 8 })
.backgroundColor(COLORS.white)
.shadow({ radius: 10, color: '#FF8A6526', offsetY: -2 })
}
.width('100%').height('100%')
.backgroundColor(COLORS.bg)
}
}
@Component
struct ElderHomeContent {
@State showAddReminderModal: boolean = false
@State medName: string = ''
@State medTime: string = ''
@Builder modalOverlay(onClose: () => void) {
Column()
.width('100%').height('100%')
.backgroundColor('rgba(0,0,0,0.55)')
.onClick(onClose)
}
@Builder addReminderModal() {
Stack() {
this.modalOverlay(() => { this.showAddReminderModal = false })
Column() {
Row() {
Text('⏰ 新增用药提醒').fontSize(18).fontWeight(FontWeight.Bold).fontColor(COLORS.textPrimary)
Column().layoutWeight(1)
Text('✕').fontSize(18).fontColor(COLORS.textHint).onClick(() => { this.showAddReminderModal = false })
}
.width('100%').padding({ left: 20, right: 20, top: 18, bottom: 12 })
Divider().color(COLORS.border)
Column() {
Text('药品名称').fontSize(12).fontColor(COLORS.textSecondary).margin({ top: 12, left: 20 })
TextInput({ placeholder: '如 降压药' })
.placeholderColor(COLORS.textHint).fontSize(14).width('90%')
.backgroundColor(COLORS.warm).borderRadius(8)
.margin({ left: 20, right: 20, top: 4 })
.onChange((v: string) => { this.medName = v })
Text('服用时间').fontSize(12).fontColor(COLORS.textSecondary).margin({ top: 14, left: 20 })
TextInput({ placeholder: '如 08:00' })
.placeholderColor(COLORS.textHint).fontSize(14).width('90%')
.backgroundColor(COLORS.warm).borderRadius(8)
.margin({ left: 20, right: 20, top: 4 })
.onChange((v: string) => { this.medTime = v })
Text('剂量').fontSize(12).fontColor(COLORS.textSecondary).margin({ top: 14, left: 20 })
TextInput({ placeholder: '如 1粒' })
.placeholderColor(COLORS.textHint).fontSize(14).width('90%')
.backgroundColor(COLORS.warm).borderRadius(8)
.margin({ left: 20, right: 20, top: 4 })
Row() {
Text('取消').fontSize(14).fontColor(COLORS.textSecondary)
.layoutWeight(1).textAlign(TextAlign.Center)
.padding({ top: 10, bottom: 10 })
.backgroundColor(COLORS.warm).borderRadius(8)
.onClick(() => { this.showAddReminderModal = false })
Column().width(12)
Text('添加').fontSize(14).fontColor(COLORS.white)
.layoutWeight(1).textAlign(TextAlign.Center)
.padding({ top: 10, bottom: 10 })
.backgroundColor(COLORS.primary).borderRadius(8)
.onClick(() => { this.showAddReminderModal = false })
}
.margin({ left: 20, right: 20, top: 20, bottom: 20 })
}
.constraintSize({ maxHeight: '70%' })
}
.width('88%')
.backgroundColor(COLORS.cardBg).borderRadius(16)
}
.width('100%').height('100%')
}
build() {
Scroll() {
Column() {
// 健康评分环+步数头卡
Stack() {
Column()
.width('100%').height(190)
.linearGradient({ angle: 135, colors: [[COLORS.primary, 0.0], [COLORS.primaryDark, 1.0]] })
.borderRadius({ bottomLeft: 24, bottomRight: 24 })
Column() {
Row() {
Column() {
Text('❤️ 今日健康').fontSize(11).fontColor('#FFCCBC')
Text('92').fontSize(44).fontWeight(FontWeight.Bold).fontColor(COLORS.white)
.margin({ top: 2 })
Text('健康评分 · 良好').fontSize(10).fontColor('#FFCCBC').margin({ top: 2 })
}
.alignItems(HorizontalAlign.Start).layoutWeight(1)
Stack() {
Column().width(76).height(76).borderRadius(38)
.backgroundColor('rgba(255,255,255,0.15)')
Column().width(56).height(56).borderRadius(28)
.backgroundColor(COLORS.accent)
Column() {
Text('6,580').fontSize(14).fontWeight(FontWeight.Bold).fontColor(COLORS.white)
Text('步').fontSize(9).fontColor('#C5CAE9')
}
.alignItems(HorizontalAlign.Center)
}
.width(76).height(76)
}
.width('100%')
.padding({ left: 20, right: 20, top: 34 })
Row() {
Text('👣 6,580步 / 8,000步').fontSize(10).fontColor('#FFCCBC')
Column().layoutWeight(1)
Text('血压 130/85 ✓').fontSize(10).fontColor(COLORS.accentLight)
.backgroundColor('rgba(255,255,255,0.15)')
.borderRadius(8).padding({ left: 6, right: 6, top: 2, bottom: 2 })
}
.width('100%')
.padding({ left: 20, right: 20, top: 14 })
}
}
.width('100%').height(190)
// 周步数柱状图
Column() {
Text('📊 本周步数(千步)').fontSize(14).fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary).margin({ left: 16, top: 16 })
Row() {
ForEach(WEEK_STEP, (w: WeekStepMeta) => {
Column() {
Stack({ alignContent: Alignment.Bottom }) {
Column().width(22).height(90)
.backgroundColor(COLORS.warm)
.borderRadius(4)
Column().width(22).height(Number(w.value) * 7)
.linearGradient({ angle: 0, colors: [[COLORS.accent, 0.0], [COLORS.primary, 1.0]] })
.borderRadius(4)
}
.width(22).height(90)
Text(w.label).fontSize(9).fontColor(COLORS.textSecondary)
.margin({ top: 4 })
}
.alignItems(HorizontalAlign.Center)
.layoutWeight(1)
})
}
.width('100%')
.padding({ left: 16, right: 16, top: 12, bottom: 8 })
}
.width('100%')
.backgroundColor(COLORS.cardBg)
.borderRadius(16)
.margin({ left: 12, right: 12, top: 12 })
// 紧急联系人
Column() {
Text('📞 紧急联系人').fontSize(14).fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary).margin({ left: 16, top: 8, bottom: 8 })
ForEach(CONTACT_LIST, (c: ContactMeta) => {
Row() {
Stack() {
Column().width(40).height(40).borderRadius(20)
.backgroundColor(c.isPrimary ? COLORS.warm : COLORS.border)
Text(c.emoji).fontSize(24)
}
.width(40).height(40).margin({ right: 12 })
Column() {
Text(c.name).fontSize(13).fontColor(COLORS.textPrimary).fontWeight(FontWeight.Medium)
Text(c.relation + ' · ' + c.phone).fontSize(10).fontColor(COLORS.textSecondary).margin({ top: 2 })
}
.alignItems(HorizontalAlign.Start).layoutWeight(1)
if (c.isPrimary) {
Text('呼叫').fontSize(11).fontColor(COLORS.white)
.backgroundColor(COLORS.danger).borderRadius(12)
.padding({ left: 12, right: 12, top: 5, bottom: 5 })
} else {
Text('呼叫').fontSize(11).fontColor(COLORS.accent)
.border({ width: 1, color: COLORS.accent }).borderRadius(12)
.padding({ left: 10, right: 10, top: 4, bottom: 4 })
}
}
.width('100%')
.padding({ left: 16, right: 16, top: 10, bottom: 10 })
.border({ width: { bottom: 1 }, color: COLORS.border })
})
}
.width('100%')
.backgroundColor(COLORS.cardBg)
.borderRadius(16)
.margin({ left: 12, right: 12, top: 8 })
// 社区活动横滑
Row() {
Text('🏡 社区活动').fontSize(14).fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary).layoutWeight(1)
Text('查看全部 >').fontSize(11).fontColor(COLORS.textHint)
}
.width('100%')
.padding({ left: 16, right: 16, top: 16 })
Scroll() {
Row() {
ForEach(ACTIVITY_LIST, (a: ActivityMeta) => {
Column() {
Stack() {
Column().width(68).height(68).borderRadius(14)
.linearGradient({ angle: 135, colors: [[COLORS.warm, 0.0], [COLORS.accentLight, 1.0]] })
Text(a.emoji).fontSize(34)
if (a.isHot) {
Text('🔥').fontSize(12).position({ x: 46, y: -4 })
}
}
.width(68).height(68)
Text(a.name).fontSize(10).fontColor(COLORS.textPrimary)
.margin({ top: 6 }).maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
Text('⏰ ' + a.time).fontSize(8).fontColor(COLORS.textSecondary).margin({ top: 2 })
Text('📍 ' + a.location).fontSize(8).fontColor(COLORS.textHint).margin({ top: 1 })
}
.width(76)
.margin({ left: 8 })
.alignItems(HorizontalAlign.Center)
})
}
.padding({ left: 16, right: 16 })
}
.scrollable(ScrollDirection.Horizontal)
.scrollBar(BarState.Off)
.width('100%')
.margin({ top: 8, bottom: 8 })
// 新增提醒按钮
Row() {
Text('+ 新增用药提醒').fontSize(14).fontColor(COLORS.white)
.fontWeight(FontWeight.Bold)
.backgroundColor(COLORS.primary)
.borderRadius(24)
.padding({ left: 24, right: 24, top: 10, bottom: 10 })
}
.width('100%')
.justifyContent(FlexAlign.Center)
.margin({ top: 16, bottom: 20 })
.onClick(() => { this.showAddReminderModal = true })
}
.width('100%')
}
.width('100%').height('100%')
.scrollBar(BarState.Off)
.bindSheet(this.showAddReminderModal, this.addReminderModal(), {
height: SheetSize.LARGE,
})
}
}
@Component
struct ElderHealthContent {
build() {
Scroll() {
Column() {
// 健康档案头卡
Column() {
Row() {
Text('❤️').fontSize(28).margin({ right: 8 })
Column() {
Text('健康档案').fontSize(20).fontWeight(FontWeight.Bold).fontColor(COLORS.white)
Text('8 项指标 · 6 项正常').fontSize(11).fontColor('#FFCCBC').margin({ top: 2 })
}
.alignItems(HorizontalAlign.Start).layoutWeight(1)
Text('📋').fontSize(22).fontColor(COLORS.white)
}
.width('100%').padding({ left: 20, right: 20, top: 16, bottom: 16 })
}
.width('100%')
.linearGradient({ angle: 135, colors: [[COLORS.accent, 0.0], [COLORS.primary, 1.0]] })
.borderRadius({ bottomLeft: 20, bottomRight: 20 })
// 健康指标双列
Text('健康指标').fontSize(14).fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
.margin({ left: 16, top: 16, bottom: 8 })
Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.SpaceBetween }) {
ForEach(HEALTH_LIST, (h: HealthMeta) => {
Column() {
Stack() {
Column().width(48).height(48).borderRadius(12)
.backgroundColor(COLORS.warm)
Text(h.emoji).fontSize(28)
}
.width(48).height(48)
Text(h.name).fontSize(11).fontColor(COLORS.textPrimary)
.fontWeight(FontWeight.Medium).margin({ top: 8 })
Text(h.value).fontSize(14).fontColor(h.isNormal ? COLORS.accent : COLORS.warning)
.fontWeight(FontWeight.Bold).margin({ top: 2 })
Text(h.status).fontSize(9)
.fontColor(getStatusColor(h.status))
.margin({ top: 2 })
}
.width('23%')
.padding({ top: 14, bottom: 14 })
.backgroundColor(COLORS.cardBg)
.borderRadius(12)
.margin({ bottom: 8 })
.alignItems(HorizontalAlign.Center)
})
}
.width('92%')
.margin({ left: 16, right: 16, bottom: 16 })
// 体检记录列表
Text('📝 体检记录').fontSize(14).fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
.margin({ left: 16, bottom: 8 })
ForEach(RECORD_LIST, (r: RecordMeta) => {
Row() {
Stack() {
Column().width(40).height(40).borderRadius(10)
.backgroundColor(COLORS.warm)
Text(r.emoji).fontSize(24)
}
.width(40).height(40).margin({ right: 12 })
Column() {
Text(r.name).fontSize(13).fontColor(COLORS.textPrimary).fontWeight(FontWeight.Medium)
Text(r.date + ' · ' + r.result).fontSize(10).fontColor(COLORS.textSecondary).margin({ top: 2 })
}
.alignItems(HorizontalAlign.Start).layoutWeight(1)
if (r.isDone) {
Text('已查').fontSize(10).fontColor(COLORS.success)
.backgroundColor('#E8F5E9').borderRadius(8)
.padding({ left: 8, right: 8, top: 4, bottom: 4 })
} else {
Text('待查').fontSize(10).fontColor(COLORS.warning)
.backgroundColor('#FFF3E0').borderRadius(8)
.padding({ left: 8, right: 8, top: 4, bottom: 4 })
}
}
.width('100%')
.padding({ left: 16, right: 16, top: 10, bottom: 10 })
.backgroundColor(COLORS.cardBg)
.border({ width: { bottom: 1 }, color: COLORS.border })
})
}
.width('100%')
}
.width('100%').height('100%')
.scrollBar(BarState.Off)
}
}
@Component
struct ElderMedsContent {
build() {
Scroll() {
Column() {
// 用药提醒头卡
Stack() {
Column()
.width('100%').height(120)
.linearGradient({ angle: 135, colors: [[COLORS.primary, 0.0], [COLORS.accent, 1.0]] })
.borderRadius({ bottomLeft: 24, bottomRight: 24 })
Column() {
Text('💊 用药提醒').fontSize(18).fontWeight(FontWeight.Bold).fontColor(COLORS.white)
Text('今日 10 种 · 已服 4 种 · 待服 6 种').fontSize(11).fontColor('#FFCCBC').margin({ top: 4 })
}
.alignItems(HorizontalAlign.Center)
.padding({ top: 30 })
}
.width('100%').height(120)
// 用药列表
ForEach(MEDICINE_LIST, (m: MedicineMeta) => {
Row() {
Stack() {
Column().width(44).height(44).borderRadius(12)
.backgroundColor(COLORS.warm)
Text(m.emoji).fontSize(26)
}
.width(44).height(44).margin({ right: 12 })
Column() {
Text(m.name).fontSize(14).fontColor(COLORS.textPrimary).fontWeight(FontWeight.Medium)
Row() {
Text('剂量 ' + m.dose).fontSize(10).fontColor(COLORS.textSecondary)
Text(' · ⏰ ' + m.time).fontSize(10).fontColor(COLORS.textSecondary)
if (m.isUrgent) {
Text(' · 紧急').fontSize(10).fontColor(COLORS.danger).fontWeight(FontWeight.Bold)
}
}
.margin({ top: 2 })
}
.alignItems(HorizontalAlign.Start).layoutWeight(1)
if (m.isDone) {
Text('✓ 已服').fontSize(11).fontColor(COLORS.success)
.backgroundColor('#E8F5E9').borderRadius(8)
.padding({ left: 10, right: 10, top: 5, bottom: 5 })
} else {
Text('提醒').fontSize(11).fontColor(COLORS.white)
.backgroundColor(m.isUrgent ? COLORS.danger : COLORS.primary)
.borderRadius(8)
.padding({ left: 10, right: 10, top: 5, bottom: 5 })
}
}
.width('100%')
.padding({ left: 16, right: 16, top: 10, bottom: 10 })
.backgroundColor(COLORS.cardBg)
.border({ width: { bottom: 1 }, color: COLORS.border })
})
}
.width('100%')
}
.width('100%').height('100%')
.scrollBar(BarState.Off)
}
}
@Component
struct ElderCommContent {
build() {
Scroll() {
Column() {
// 社区活动头卡
Column() {
Row() {
Text('👥').fontSize(28).margin({ right: 8 })
Column() {
Text('社区活动').fontSize(20).fontWeight(FontWeight.Bold).fontColor(COLORS.white)
Text('8 项活动 · 今日 4 场').fontSize(11).fontColor('#FFCCBC').margin({ top: 2 })
}
.alignItems(HorizontalAlign.Start).layoutWeight(1)
}
.width('100%').padding({ left: 20, right: 20, top: 16, bottom: 16 })
}
.width('100%')
.linearGradient({ angle: 135, colors: [[COLORS.accent, 0.0], [COLORS.primary, 1.0]] })
.borderRadius({ bottomLeft: 20, bottomRight: 20 })
// 今日活动
Text('📅 今日活动').fontSize(14).fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
.margin({ left: 16, top: 16, bottom: 8 })
ForEach(ACTIVITY_LIST, (a: ActivityMeta) => {
Row() {
Stack() {
Column().width(44).height(44).borderRadius(12)
.linearGradient({ angle: 135, colors: [[COLORS.warm, 0.0], [COLORS.accentLight, 1.0]] })
Text(a.emoji).fontSize(26)
}
.width(44).height(44).margin({ right: 12 })
Column() {
Row() {
Text(a.name).fontSize(14).fontColor(COLORS.textPrimary).fontWeight(FontWeight.Medium)
if (a.isHot) {
Text(' 🔥').fontSize(10)
}
}
Text('⏰ ' + a.time + ' · 📍 ' + a.location).fontSize(10).fontColor(COLORS.textSecondary).margin({ top: 2 })
}
.alignItems(HorizontalAlign.Start).layoutWeight(1)
Text('报名').fontSize(11).fontColor(COLORS.white)
.backgroundColor(COLORS.accent).borderRadius(12)
.padding({ left: 12, right: 12, top: 5, bottom: 5 })
}
.width('100%')
.padding({ left: 16, right: 16, top: 10, bottom: 10 })
.backgroundColor(COLORS.cardBg)
.border({ width: { bottom: 1 }, color: COLORS.border })
})
// 养生知识横滑
Text('📖 养生知识').fontSize(14).fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
.margin({ left: 16, top: 16, bottom: 8 })
Scroll() {
Row() {
ForEach(KNOWLEDGE_LIST, (k: KnowledgeMeta) => {
Column() {
Stack() {
Column().width(64).height(64).borderRadius(12)
.linearGradient({ angle: 135, colors: [[COLORS.warm, 0.0], [COLORS.accentLight, 1.0]] })
Text(k.emoji).fontSize(32)
}
.width(64).height(64)
Text(k.title).fontSize(10).fontColor(COLORS.textPrimary)
.fontWeight(FontWeight.Medium).margin({ top: 6 })
Text(k.desc).fontSize(8).fontColor(COLORS.textHint).margin({ top: 1 })
}
.width(72)
.margin({ left: 8 })
.alignItems(HorizontalAlign.Center)
})
}
.padding({ left: 16, right: 16 })
}
.scrollable(ScrollDirection.Horizontal)
.scrollBar(BarState.Off)
.width('100%')
.margin({ bottom: 20 })
}
.width('100%')
}
.width('100%').height('100%')
.scrollBar(BarState.Off)
}
}
@Component
struct ElderMineContent {
@State showEditProfileModal: boolean = false
@State showDeleteModal: boolean = false
@State selectedRecord: string = ''
@Builder modalOverlay(onClose: () => void) {
Column()
.width('100%').height('100%')
.backgroundColor('rgba(0,0,0,0.55)')
.onClick(onClose)
}
@Builder editProfileModal() {
Stack() {
this.modalOverlay(() => { this.showEditProfileModal = false })
Column() {
Row() {
Text('✏️ 编辑档案').fontSize(18).fontWeight(FontWeight.Bold).fontColor(COLORS.textPrimary)
Column().layoutWeight(1)
Text('✕').fontSize(18).fontColor(COLORS.textHint).onClick(() => { this.showEditProfileModal = false })
}
.width('100%').padding({ left: 20, right: 20, top: 18, bottom: 12 })
Divider().color(COLORS.border)
Column() {
Text('姓名').fontSize(12).fontColor(COLORS.textSecondary).margin({ top: 12, left: 20 })
TextInput({ placeholder: '张大爷' })
.placeholderColor(COLORS.textHint).fontSize(14).width('90%')
.backgroundColor(COLORS.warm).borderRadius(8)
.margin({ left: 20, right: 20, top: 4 })
Text('年龄').fontSize(12).fontColor(COLORS.textSecondary).margin({ top: 14, left: 20 })
TextInput({ placeholder: '68' })
.placeholderColor(COLORS.textHint).fontSize(14).width('90%')
.backgroundColor(COLORS.warm).borderRadius(8)
.margin({ left: 20, right: 20, top: 4 })
Text('健康备注').fontSize(12).fontColor(COLORS.textSecondary).margin({ top: 14, left: 20 })
TextInput({ placeholder: '高血压·轻度骨质疏松' })
.placeholderColor(COLORS.textHint).fontSize(14).width('90%')
.backgroundColor(COLORS.warm).borderRadius(8)
.margin({ left: 20, right: 20, top: 4 })
Row() {
Text('取消').fontSize(14).fontColor(COLORS.textSecondary)
.layoutWeight(1).textAlign(TextAlign.Center)
.padding({ top: 10, bottom: 10 })
.backgroundColor(COLORS.warm).borderRadius(8)
.onClick(() => { this.showEditProfileModal = false })
Column().width(12)
Text('保存').fontSize(14).fontColor(COLORS.white)
.layoutWeight(1).textAlign(TextAlign.Center)
.padding({ top: 10, bottom: 10 })
.backgroundColor(COLORS.primary).borderRadius(8)
.onClick(() => { this.showEditProfileModal = false })
}
.margin({ left: 20, right: 20, top: 20, bottom: 20 })
}
.constraintSize({ maxHeight: '70%' })
}
.width('88%')
.backgroundColor(COLORS.cardBg).borderRadius(16)
}
.width('100%').height('100%')
}
@Builder deleteRecordModal() {
Stack() {
this.modalOverlay(() => { this.showDeleteModal = false })
Column() {
Text('⚠️ 删除记录').fontSize(18).fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
.margin({ top: 24 })
Text('确定要删除「' + this.selectedRecord + '」吗?')
.fontSize(13).fontColor(COLORS.textSecondary)
.textAlign(TextAlign.Center)
.margin({ left: 20, right: 20, top: 12 })
Row() {
Text('取消').fontSize(14).fontColor(COLORS.textSecondary)
.layoutWeight(1).textAlign(TextAlign.Center)
.padding({ top: 10, bottom: 10 })
.backgroundColor(COLORS.warm).borderRadius(8)
.onClick(() => { this.showDeleteModal = false })
Column().width(12)
Text('删除').fontSize(14).fontColor(COLORS.white)
.layoutWeight(1).textAlign(TextAlign.Center)
.padding({ top: 10, bottom: 10 })
.backgroundColor(COLORS.danger).borderRadius(8)
.onClick(() => { this.showDeleteModal = false })
}
.margin({ left: 20, right: 20, top: 24, bottom: 24 })
}
.width('80%')
.backgroundColor(COLORS.cardBg).borderRadius(16)
}
.width('100%').height('100%')
}
build() {
Scroll() {
Column() {
// 个人头卡
Stack() {
Column()
.width('100%').height(160)
.linearGradient({ angle: 135, colors: [[COLORS.primary, 0.0], [COLORS.primaryDark, 1.0]] })
.borderRadius({ bottomLeft: 24, bottomRight: 24 })
Column() {
Stack() {
Column().width(64).height(64).borderRadius(32)
.backgroundColor('rgba(255,255,255,0.25)')
Text('👴').fontSize(36)
}
.width(64).height(64)
Text('张大爷').fontSize(16).fontWeight(FontWeight.Bold)
.fontColor(COLORS.white).margin({ top: 8 })
Row() {
Text('68岁').fontSize(10).fontColor(COLORS.accentLight)
.backgroundColor('rgba(255,255,255,0.2)')
.borderRadius(8).padding({ left: 6, right: 6, top: 2, bottom: 2 })
Text(' · 高血压·骨质疏松').fontSize(10).fontColor('#FFCCBC')
}
.margin({ top: 2 })
}
.alignItems(HorizontalAlign.Center)
.padding({ top: 24 })
}
.width('100%').height(160)
// 个人数据
Row() {
Column() {
Text('92').fontSize(20).fontWeight(FontWeight.Bold).fontColor(COLORS.primary)
Text('健康评分').fontSize(10).fontColor(COLORS.textSecondary).margin({ top: 2 })
}
.layoutWeight(1).alignItems(HorizontalAlign.Center)
Column() {
Text('6,580').fontSize(20).fontWeight(FontWeight.Bold).fontColor(COLORS.accent)
Text('今日步数').fontSize(10).fontColor(COLORS.textSecondary).margin({ top: 2 })
}
.layoutWeight(1).alignItems(HorizontalAlign.Center)
Column() {
Text('10').fontSize(20).fontWeight(FontWeight.Bold).fontColor(COLORS.warning)
Text('用药提醒').fontSize(10).fontColor(COLORS.textSecondary).margin({ top: 2 })
}
.layoutWeight(1).alignItems(HorizontalAlign.Center)
Column() {
Text('10').fontSize(20).fontWeight(FontWeight.Bold).fontColor(COLORS.danger)
Text('体检次数').fontSize(10).fontColor(COLORS.textSecondary).margin({ top: 2 })
}
.layoutWeight(1).alignItems(HorizontalAlign.Center)
}
.width('100%')
.padding({ top: 16, bottom: 16 })
.backgroundColor(COLORS.cardBg)
.margin({ left: 12, right: 12, top: 12 })
.borderRadius(16)
// 编辑档案按钮
Row() {
Text('✏️ 编辑档案').fontSize(14).fontColor(COLORS.white)
.fontWeight(FontWeight.Bold)
.backgroundColor(COLORS.accent)
.borderRadius(24)
.padding({ left: 24, right: 24, top: 10, bottom: 10 })
}
.width('100%')
.justifyContent(FlexAlign.Center)
.margin({ top: 16, bottom: 8 })
.onClick(() => { this.showEditProfileModal = true })
// 体检记录列表(可删除)
Text('📋 体检记录').fontSize(14).fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
.margin({ left: 16, top: 8, bottom: 8 })
ForEach(RECORD_LIST, (r: RecordMeta) => {
Row() {
Stack() {
Column().width(40).height(40).borderRadius(10)
.backgroundColor(COLORS.warm)
Text(r.emoji).fontSize(24)
}
.width(40).height(40).margin({ right: 12 })
Column() {
Text(r.name).fontSize(13).fontColor(COLORS.textPrimary).fontWeight(FontWeight.Medium)
Text(r.date + ' · ' + r.result).fontSize(10).fontColor(COLORS.textSecondary).margin({ top: 2 })
}
.alignItems(HorizontalAlign.Start).layoutWeight(1)
Text('删除').fontSize(11).fontColor(COLORS.danger)
.onClick(() => {
this.selectedRecord = r.name
this.showDeleteModal = true
})
}
.width('100%')
.padding({ left: 16, right: 16, top: 10, bottom: 10 })
.backgroundColor(COLORS.cardBg)
.border({ width: { bottom: 1 }, color: COLORS.border })
})
// 功能列表
Column() {
ForEach(MENU_LIST, (m: MenuMeta) => {
Row() {
Text(m.icon).fontSize(22).margin({ right: 12 })
Text(m.name).fontSize(14).fontColor(COLORS.textPrimary).layoutWeight(1)
Text(m.sub).fontSize(11).fontColor(COLORS.textHint)
Text(' >').fontSize(14).fontColor(COLORS.textHint)
}
.width('100%')
.padding({ left: 16, right: 16, top: 14, bottom: 14 })
.border({ width: { bottom: 1 }, color: COLORS.border })
})
}
.width('100%')
.backgroundColor(COLORS.cardBg)
.borderRadius(16)
.margin({ left: 12, right: 12, bottom: 20 })
}
.width('100%')
}
.width('100%').height('100%')
.scrollBar(BarState.Off)
.bindSheet(this.showEditProfileModal, this.editProfileModal(), {
height: SheetSize.LARGE,
})
.bindSheet(this.showDeleteModal, this.deleteRecordModal(), {
height: SheetSize.LARGE,
})
}
}
十二、结尾总结

通过对这款养老服务 App 的全面深入分析,我们可以看到一款面向老年用户的应用是如何通过技术设计传递关怀与温度的。从整体架构到组件细节,从数据模型到交互逻辑,每一处设计都体现了对老年用户需求的深刻理解和对适老化原则的认真践行。
在架构层面,App 采用了简洁高效的 Tab 导航 + 状态驱动页面切换模式。单一 activeTab 状态变量控制全局导航,逻辑清晰,运行稳定。枚举 + 配置常量的双层设计实现了逻辑与展示的分离,@Builder 自定义构建器提升了代码复用率。整体架构简洁而不简单,对于五个 Tab 的养老应用来说,这种轻量级的架构方案既满足了功能需求,又保证了系统的稳定性和可维护性。
在视觉设计方面,暖橙 + 静谧蓝的配色方案是这款 App 最突出的亮点。暖橙色传递着温暖关怀,如同阳光般让人感到安心;静谧蓝代表着专业可靠,如同医护人员的白大褂般让人信任。两种颜色的结合完美诠释了养老服务"温暖 + 专业"的双重属性。更值得称道的是,整个界面的色彩运用都考虑了老年用户的视觉特点——文本与背景之间保持足够的对比度,重要信息使用醒目的颜色突出,状态标识用颜色编码辅助识别。这些细节虽然不起眼,却实实在在地提升了老年用户的使用体验。
在功能设计方面,这款 App 紧紧围绕老年人的核心需求展开。健康管理方面,不仅有各项指标的展示,还用颜色编码让健康状况一目了然;用药管理方面,区分了紧急和普通药品,用红色双重警示确保重要药物不遗漏;紧急联系人放在首页显著位置,红色呼叫按钮醒目易点击;社区活动和养生知识丰富了老年人的精神生活。每个功能都直击痛点,每项设计都为用户着想。特别是紧急联系人和用药提醒这两个功能,对于独居老人来说,可能就是救命的功能。
在适老化设计方面,App 做了很多细致的考量:足够大的按钮和点击区域,防止误触;清晰的状态颜色区分,降低认知负荷;简洁直观的交互路径,减少操作步骤;二次确认机制,避免误操作带来的损失。这些设计可能年轻用户感受不深,但对于视力下降、反应变慢的老年用户来说,每一处贴心的设计都能让他们用得更方便、更安心。
在技术实现方面,App 展示了 ArkTS 声明式开发的强大能力。纯代码实现的柱状图、灵活的 @Builder 构建器、声明式的 bindSheet 弹窗、响应式的 @State 状态管理——这些基础功能组合在一起,就能构建出功能完整、界面精美的应用。对于想要学习 ArkTS 开发的读者来说,这款 App 是一个非常好的学习范例。
更多推荐



所有评论(0)