打造一款温暖人心的基于HarmonyOS API 24实战解析萌宠领养应用discoverContent方法使用Scroll作为外层容器,确保当内容超出屏幕高度时可以垂直滚动
一、应用概述与设计理念
在当今社会,宠物领养已经成为一种越来越被大众所接受和推崇的生活方式。相比于传统的宠物购买,领养不仅能够给流浪动物一个温暖的家,更能够传递"以领养代替购买"的公益理念。基于这样的社会背景,本文将深入剖析一款基于HarmonyOS ArkTS开发的萌宠领养应用,该应用完整覆盖了从宠物发现、领养申请、故事分享到健康追踪的全链路功能,为用户提供了一个有温度、有深度的宠物领养平台。

从功能定位来看,这款应用不仅仅是一个简单的宠物信息展示工具,而是一个集成了多种实用模块的综合性服务平台。它包含了五大核心功能模块:发现模块帮助用户浏览和筛选待领养宠物信息;申请模块让领养人可以实时跟踪自己的领养进度;故事模块记录了每一只被领养宠物的温馨后续;健康模块提供了数据可视化的健康统计功能;个人中心则整合了用户相关的所有管理功能。这五大模块相互配合,形成了一套完整的宠物领养服务闭环,让用户从"发现"到"领养"再到"后续关怀"的全过程都能在一个应用内完成。
在设计理念上,这款应用采用了"温暖科技"的设计哲学。整体色彩方案选用了暖橙色(#FF7043)与温柔粉色(#EC407A)的搭配组合,暖橙色给人以活力和温暖的感觉,象征着宠物带来的快乐与陪伴;温柔粉色则传递出关爱与柔软的触感,呼应了领养行为背后的善意与爱心。背景色采用了极浅的暖白色(#FFF8F5),营造出一种如同午后阳光般舒适的视觉体验。在布局层面,应用大量运用了卡片式设计语言,通过圆角、阴影和留白来构建层次分明的信息架构,使得每一个功能模块都像是一张精心设计的明信片,既美观又实用。
从技术架构角度分析,这款应用完全基于HarmonyOS的ArkTS声明式UI框架进行开发。ArkTS作为HarmonyOS的核心开发语言,继承了TypeScript的强类型特性,同时提供了丰富的声明式UI组件和状态管理机制。应用采用了单页面多模块的设计模式,通过底部Tab栏进行模块切换,这种方式既保证了用户体验的流畅性,又降低了页面跳转带来的性能开销。在状态管理方面,应用大量使用了@State装饰器来实现组件内部的状态响应式更新,确保了用户交互的实时反馈。此外,应用还充分利用了ArkTS提供的@Builder装饰器来封装可复用的UI构建函数,大大提升了代码的可维护性和可读性。
从应用场景来看,这款应用面向的是广大的宠物爱好者和潜在领养人群体。无论是想要领养一只狗狗或猫咪的家庭,还是关注流浪动物救助事业的爱心人士,都能在这款应用中找到自己需要的信息和服务。对于领养机构而言,这款应用也提供了一个标准化的信息展示和流程管理平台,有助于提升领养工作的效率和透明度。值得一提的是,应用中的"急寻领养"功能和"领养故事"模块特别具有社会价值,前者能够帮助那些急需被领养的宠物获得更多关注,后者则通过真实的领养故事来激励更多人参与到领养行动中来,形成了良好的社区氛围和公益传播效应。
二、类型定义与数据建模
在ArkTS开发中,良好的类型定义是构建健壮应用的基石。这款萌宠领养应用在数据建模层面展现了非常高的工程素养,通过接口(interface)对核心业务实体进行了精确的类型刻画。这种强类型的设计方式不仅能够在编译阶段捕获潜在的类型错误,还能为IDE提供精准的代码提示,大幅提升开发效率。
interface PetMeta {
id: number
name: string
species: string
breed: string
age: string
gender: string
location: string
vaccinated: boolean
neutered: boolean
description: string
imageColor: string
tags: string[]
urgency: string
}

PetMeta接口是整个应用最核心的数据类型之一,它定义了宠物档案的完整字段结构。从字段设计可以看出,开发者对宠物信息的展示需求进行了非常细致的拆解。id字段作为唯一标识符,为每一条宠物记录提供了可追踪的锚点;name、species、breed、age、gender等基础信息字段覆盖了用户最关心的宠物属性;location字段则加入了城市信息,便于用户根据地理位置筛选合适的领养对象。特别值得称赞的是vaccinated和neutered这两个布尔字段的设计,它们直接反映了宠物的健康状态,是领养决策中的关键参考因素。description字段提供了富文本描述能力,让每一只宠物都能拥有独特的个性介绍。imageColor字段是一个非常巧妙的设计,它用一个颜色字符串来替代真实的图片资源,在原型开发阶段既保证了视觉效果,又避免了图片资源管理的复杂性。tags字段采用了字符串数组的形式,支持为每只宠物打上多个标签,这种多标签体系极大地增强了宠物信息的可检索性和可分类性。最后,urgency字段用于标记宠物的紧急程度,为"急寻领养"功能提供了数据支撑。
interface ApplicationMeta {
id: number
petName: string
applicant: string
status: string
date: string
step: string
progress: number
}

ApplicationMeta接口定义了领养申请的数据结构,它代表了领养流程中的一个申请实例。这个接口的设计体现了对业务流程的深刻理解。petName字段直接关联到对应的宠物,而不使用petId,这在展示层简化了数据绑定的复杂度。applicant字段记录了申请人的姓名,date字段标记了申请提交的日期,status字段描述了当前申请的审核状态。特别值得关注的是step和progress这两个字段的协同设计:step以文字形式描述了当前所处的流程节点(如"资料审核"、“上门家访”、"领养完成"等),而progress则以数值百分比的形式量化了整体进度。这种双轨制的设计让用户既能获得直观的进度感知(通过百分比),又能了解当前具体处于哪个业务环节(通过步骤描述),极大地提升了用户体验的友好度。
interface StoryMeta {
id: number
title: string
author: string
petName: string
date: string
excerpt: string
imageColor: string
likes: number
}

StoryMeta接口负责定义领养故事的内容结构。这个类型的设计充分考虑了内容型产品的典型需求。title和excerpt分别对应故事的标题和摘要,形成了内容展示的基础。author字段记录了故事的作者,petName字段关联了故事的主角宠物,date字段标记了发布时间。likes字段引入了社交互动元素,让用户可以通过点赞来表达对故事的喜爱。与PetMeta类似,StoryMeta也使用了imageColor字段来处理封面视觉效果。整体来看,这个数据结构虽然字段不多,但每一个字段都服务于核心功能,没有冗余设计,体现了"少即是多"的极简主义设计思想。
interface HealthStatMeta {
month: string
checkups: number
vaccinations: number
adoptions: number
}

HealthStatMeta接口定义了健康统计数据的数据结构。这是一个典型的时序数据类型,以month为时间维度,checkups、vaccinations、adoptions三个数值字段分别记录了体检次数、疫苗接种次数和成功领养数量。这种多维度的统计设计,使得健康模块能够同时展示多个指标的变化趋势,为数据可视化提供了丰富的素材。从字段命名可以看出,开发者有意识地使用了完整的英文单词而非缩写,这种自描述的命名风格极大地提升了代码的可读性和可维护性。
interface ColorPalette {
primary: string;
primaryLight: string;
primaryDark: string;
accent: string;
accentLight: string;
bg: string;
cardBg: string;
textPrimary: string;
textSecondary: string;
textHint: string;
border: string;
success: string;
warning: string;
danger: string;
white: string;
}

ColorPalette接口是应用设计系统中的核心组成部分,它定义了完整的色彩令牌体系。这种将颜色抽象为语义化令牌的做法,是现代UI工程中的最佳实践。primary、primaryLight、primaryDark构成了主色的色阶体系,满足了不同场景下的色彩需求;accent和accentLight提供了辅助色及其浅色变体;bg和cardBg分别定义了页面背景和卡片背景色;textPrimary、textSecondary、textHint构成了三层文字色阶,用于区分信息的主次关系;border定义了分隔线颜色;success、warning、danger则是标准的语义化功能色,分别用于表示成功、警告和危险状态。通过这种系统化的色彩管理,整个应用保持了一致的视觉风格,同时也为后期的主题定制和品牌扩展预留了充足的空间。
三、设计令牌与常量配置
在完成了类型定义之后,应用进入到了设计令牌和常量配置的阶段。这一层是连接设计规范与代码实现的桥梁,它将视觉设计中的抽象概念转化为可在代码中直接引用的具体数值。
const COLORS: ColorPalette = {
primary: '#FF7043',
primaryLight: '#FFAB91',
primaryDark: '#D84315',
accent: '#EC407A',
accentLight: '#F48FB1',
bg: '#FFF8F5',
cardBg: '#FFFFFF',
textPrimary: '#3E2723',
textSecondary: '#8D6E63',
textHint: '#BCAAA4',
border: '#F5E6E0',
success: '#66BB6A',
warning: '#FFA726',
danger: '#EF5350',
white: '#FFFFFF'
};

COLORS常量对象是应用色彩系统的具体实现。主色#FF7043是一种温暖的珊瑚橙色,它在视觉上具有很强的亲和力,能够唤起用户积极、温暖的情感共鸣。这种颜色被广泛应用于按钮、选中状态、进度条填充等高关注度的UI元素上。primaryLight(#FFAB91)作为主色的浅色变体,在标签背景、图标容器等次要元素上发挥了重要作用,既保持了色彩家族的一致性,又通过明度差异建立了视觉层次。primaryDark(#D84315)则用于需要强调深度感的场景,如文字标签的色彩对比。辅助色#EC407A是一种温柔的粉红色,它与主色形成了完美的互补关系,在渐变色搭配、点赞图标、女性化标签等场景中频繁出现。accentLight(#F48FB1)作为辅助色的浅色版本,与primaryLight共同构成了柔和的背景色板。背景色#FFF8F5是一种非常浅的暖白色,带有微乎其微的橙色调,让整个页面呈现出如同被阳光轻抚般的温暖质感。卡片背景色则使用了纯白#FFFFFF,与背景色形成了微妙的对比,使卡片能够自然地"浮"出页面。文字色阶的设计也颇具匠心:textPrimary使用了深棕色#3E2723,相比于纯黑色更加柔和,长时间阅读不易疲劳;textSecondary采用了中性的棕灰色#8D6E63,用于次要信息;textHint则是浅灰色#BCAAA4,专门用于占位符和提示性文字。border色#F5E6E0是一种极浅的暖灰,为分隔线提供了恰到好处的存在感。三种功能色success、warning、danger分别对应绿色、橙色和红色,这是业界通用的语义化配色方案,用户无需学习即可 instinctively 理解其含义。
const TAB_CONFIG: Record<string, string> = {
'discover': '发现',
'apply': '申请',
'stories': '故事',
'health': '健康',
'mine': '我的'
};

TAB_CONFIG常量采用了Record<string, string>类型,这是一个非常优雅的设计。它使用JavaScript对象作为字典结构,将Tab的标识键(如’discover’、‘apply’)映射到对应的中文显示名称。这种配置化的设计带来了多重好处:首先,它实现了Tab标签文案的集中管理,当需要修改某个Tab的名称时,只需修改这一处即可;其次,Object.keys(TAB_CONFIG)可以被直接用于遍历生成Tab栏,避免了在渲染逻辑中硬编码Tab列表;再次,这种键值对结构天然支持通过键来查找对应的显示名称,在需要显示当前Tab名称的场景下非常方便。从键的命名来看,开发者使用了简洁的英文单词,而值则使用了用户友好的中文,这种"技术键+业务值"的分离模式是前端工程中常见的最佳实践。
const FILTER_TAGS: string[] = ['全部', '狗', '猫', '幼年', '成年', '已绝育', '已疫苗', '大型', '小型', '急寻领养']

FILTER_TAGS常量定义了发现模块中的筛选标签列表。这个数组的设计充分考虑了用户筛选宠物的实际需求。'全部'作为默认选项,提供了无筛选的完整视图;'狗'和'猫'按物种分类,是最基础的筛选维度;'幼年'和'成年'按年龄段分类,帮助用户找到符合自己照料能力的宠物;'已绝育'和'已疫苗'按健康状态分类,对于注重宠物健康的领养者来说非常实用;'大型'和'小型'按体型分类,考虑到了不同居住环境的用户需求;'急寻领养'则是一个特殊的筛选条件,用于快速定位那些急需被领养的宠物,体现了应用的社会责任感。这些标签之间并非互斥关系,但在当前的实现中采用了单选模式,用户点击一个标签后其他标签自动取消选中,这种简化的交互模式降低了用户的使用门槛。
四、数据层与模拟数据
为了让应用能够在没有后端服务的情况下完整运行,开发者准备了一套高质量的模拟数据。这套数据不仅数量充足,而且内容真实感强,能够很好地展示应用的各项功能。
const PET_LIST: PetMeta[] = [
{ id: 1, name: '豆豆', species: '狗', breed: '金毛寻回犬', age: '2岁', gender: '公', location: '上海·浦东', vaccinated: true, neutered: true, description: '活泼好动,喜欢接飞盘,对人友善,已学会握手和坐下', imageColor: '#FFCC80', tags: ['活泼', '大型犬', '已训练'], urgency: 'normal' },
{ id: 2, name: '咪咪', species: '猫', breed: '英国短毛猫', age: '1岁', gender: '母', location: '北京·朝阳', vaccinated: true, neutered: false, description: '安静乖巧,喜欢趴在窗边晒太阳,适合公寓饲养', imageColor: '#F48FB1', tags: ['安静', '小型猫', '适合公寓'], urgency: 'normal' },
{ id: 3, name: '旺财', species: '狗', breed: '中华田园犬', age: '3岁', gender: '公', location: '广州·天河', vaccinated: true, neutered: true, description: '忠诚护主,看家本领强,精力充沛需要每天遛弯', imageColor: '#A5D6A7', tags: ['忠诚', '中型犬', '需要运动'], urgency: 'urgent' },
{ id: 4, name: '雪球', species: '猫', breed: '波斯猫', age: '6月', gender: '母', location: '深圳·南山', vaccinated: false, neutered: false, description: '毛色雪白如棉花糖,性格粘人,需要定期梳毛', imageColor: '#E1BEE7', tags: ['粘人', '长毛', '需要护理'], urgency: 'urgent' },
{ id: 5, name: '大黄', species: '狗', breed: '拉布拉多', age: '4岁', gender: '公', location: '成都·武侯', vaccinated: true, neutered: true, description: '温顺聪明,曾是导盲犬候选,服从性极好', imageColor: '#FFE0B2', tags: ['温顺', '大型犬', '已训练'], urgency: 'normal' },
{ id: 6, name: '花花', species: '猫', breed: '布偶猫', age: '2岁', gender: '母', location: '杭州·西湖', vaccinated: true, neutered: true, description: '蓝眼睛仙女猫,性格温柔,喜欢被抱在怀里', imageColor: '#B3E5FC', tags: ['温柔', '大型猫', '室内猫'], urgency: 'normal' },
{ id: 7, name: '小黑', species: '狗', breed: '边境牧羊犬', age: '1岁', gender: '公', location: '武汉·洪山', vaccinated: true, neutered: false, description: '智商极高的工作犬,需要大量运动和智力游戏', imageColor: '#90A4AE', tags: ['高智商', '中型犬', '需要运动'], urgency: 'normal' },
{ id: 8, name: '团子', species: '猫', breed: '橘猫', age: '3岁', gender: '公', location: '南京·鼓楼', vaccinated: true, neutered: true, description: '十橘九胖的典范,能吃能睡,性格佛系', imageColor: '#FFCC02', tags: ['佛系', '中型猫', '易胖'], urgency: 'normal' }
]

PET_LIST数组包含了8只待领养宠物的完整信息。从数据内容可以看出,开发者在模拟数据的编写上投入了大量心血。每只宠物都有独特的名字、品种和性格描述,避免了"Lorem Ipsum"式的无意义填充。在物种分布上,4只狗和4只猫的平衡设计,确保了不同偏好的用户都能找到感兴趣的内容。年龄分布覆盖了6个月到4岁的范围,涵盖了幼年、青年和成年各个阶段。地理位置跨越了上海、北京、广州、深圳、成都、杭州、武汉、南京等多个城市,展示了应用全国性的服务覆盖能力。在健康状态方面,大部分宠物已完成疫苗接种和绝育手术,但雪球和咪咪尚未绝育,小黑未完成绝育,这种差异化的设计让数据更加真实。特别值得注意的是urgency字段的设置:旺财和雪球被标记为urgent,为"急寻领养"功能提供了展示素材。imageColor字段为每只宠物分配了独特的颜色,形成了丰富的视觉变化。tags字段的内容也经过精心设计,既有描述性格的(如"活泼"、“安静”、“佛系”),也有描述体型的(如"大型犬"、“小型猫”),还有描述特殊需求的(如"需要运动"、“需要护理”、“已训练”),这些标签在筛选功能中发挥了关键作用。
const APPLICATION_LIST: ApplicationMeta[] = [
{ id: 1, petName: '豆豆', applicant: '张小明', status: '审核中', date: '2024-03-15', step: '资料审核', progress: 40 },
{ id: 2, petName: '咪咪', applicant: '李芳', status: '家访中', date: '2024-03-12', step: '上门家访', progress: 65 },
{ id: 3, petName: '旺财', applicant: '王强', status: '已通过', date: '2024-03-08', step: '领养完成', progress: 100 },
{ id: 4, petName: '雪球', applicant: '赵静', status: '审核中', date: '2024-03-18', step: '资料审核', progress: 30 },
{ id: 5, petName: '大黄', applicant: '陈伟', status: '待面谈', date: '2024-03-10', step: '视频面谈', progress: 50 },
{ id: 6, petName: '花花', applicant: '刘洋', status: '已通过', date: '2024-03-05', step: '领养完成', progress: 100 },
{ id: 7, petName: '小黑', applicant: '孙琳', status: '已驳回', date: '2024-03-02', step: '审核终止', progress: 0 }
]
APPLICATION_LIST数组模拟了7条领养申请记录。这些记录覆盖了领养流程的各个阶段,从progress: 0的已驳回申请到progress: 100的已完成领养,再到处于中间状态(30%、40%、50%、65%)的审核中申请,完整展示了进度追踪功能的效果。状态种类也非常丰富,包括"审核中"、“家访中”、“待面谈”、“已通过”、"已驳回"五种,对应了领养审核流程中的不同节点。申请日期的设置也很有讲究,从3月2日到3月18日的时间跨度,让用户能够感受到这是一个动态更新的系统。每个申请人都与宠物列表中的某只宠物相关联,形成了数据之间的关联关系。
const STORY_LIST: StoryMeta[] = [
{ id: 1, title: '从流浪到家人:豆豆的一百天', author: '张小明', petName: '豆豆', date: '2024-03-20', excerpt: '领养豆豆已经100天了,从最初的胆小怕人到现在的活泼开朗,每一天都充满惊喜...', imageColor: '#FFCC80', likes: 328 },
{ id: 2, title: '猫咪的日常:咪咪的窗边时光', author: '李芳', petName: '咪咪', date: '2024-03-18', excerpt: '每天下午三点,咪咪都会准时出现在窗边,等待那束最温暖的阳光...', imageColor: '#F48FB1', likes: 215 },
{ id: 3, title: '旺财的新家:从看门狗到沙发客', author: '王强', petName: '旺财', date: '2024-03-15', excerpt: '谁能想到曾经在外流浪的旺财,现在已经爱上了沙发和暖气片...', imageColor: '#A5D6A7', likes: 476 },
{ id: 4, title: '雪球的成长日记:从奶猫到小公主', author: '赵静', petName: '雪球', date: '2024-03-12', excerpt: '六个月大的雪球从瘦弱的小奶猫长成了毛茸茸的小公主...', imageColor: '#E1BEE7', likes: 189 },
{ id: 5, title: '大黄退役后:导盲犬的退休生活', author: '陈伟', petName: '大黄', date: '2024-03-08', excerpt: '退役导盲犬大黄终于可以过上普通宠物狗的生活了,每天最大的烦恼是选哪个玩具...', imageColor: '#FFE0B2', likes: 612 },
{ id: 6, title: '花花的蓝眼睛:布偶猫的温柔力量', author: '刘洋', petName: '花花', date: '2024-03-05', excerpt: '花花的蓝眼睛像两颗宝石,每次它看着我的时候,一天的疲惫都消失了...', imageColor: '#B3E5FC', likes: 343 }
]
STORY_LIST数组包含了6篇温馨的领养故事。每篇故事都有引人入胜的标题和情感饱满的摘要内容。从标题风格来看,有的侧重于时间线叙事(“豆豆的一百天”、“雪球的成长日记”),有的侧重于场景描写(“咪咪的窗边时光”),有的侧重于身份转变(“从看门狗到沙发客”、“导盲犬的退休生活”),这种多样化的叙事角度让故事模块充满了阅读趣味。点赞数从189到612不等,既有热门故事也有相对冷门的内容,模拟了真实社区中的内容热度分布。值得注意的是,故事的作者与申请列表中的申请人是匹配的,这种数据一致性设计让整个应用的数据体系更加可信。
const HEALTH_STATS: HealthStatMeta[] = [
{ month: '1月', checkups: 45, vaccinations: 68, adoptions: 12 },
{ month: '2月', checkups: 52, vaccinations: 71, adoptions: 18 },
{ month: '3月', checkups: 68, vaccinations: 85, adoptions: 25 },
{ month: '4月', checkups: 59, vaccinations: 78, adoptions: 20 },
{ month: '5月', checkups: 73, vaccinations: 92, adoptions: 31 },
{ month: '6月', checkups: 81, vaccinations: 95, adoptions: 28 }
]
HEALTH_STATS数组提供了6个月的统计数据,用于驱动健康模块的数据可视化。数据呈现出明显的增长趋势,从一月的45次体检、68次疫苗接种、12次领养,增长至六月的81次体检、95次疫苗接种、28次领养,反映了业务规模的稳步扩张。同时,数据中也包含了一些合理的波动,如四月相比三月有所下降,这种非线性的变化让数据更加贴近真实世界的业务模式。
五、辅助函数与工具方法
在数据层之上,应用定义了三个辅助函数,它们负责处理数据转换和计算逻辑,将原始数据转化为UI层可以直接消费的格式。
function getStatusColor(status: string): string {
if (status === '已通过') {
return COLORS.success
}
if (status === '已驳回') {
return COLORS.danger
}
if (status === '审核中') {
return COLORS.warning
}
return COLORS.primary
}
getStatusColor函数是一个典型的数据映射函数,它将申请状态的文本描述映射为对应的颜色值。这种映射关系符合用户的认知习惯:"已通过"对应绿色,传达积极的信号;"已驳回"对应红色,表示需要关注;“审核中"对应橙色,暗示正在处理中;其他状态(如"家访中”、“待面谈”)统一使用主色,保持视觉一致性。这个函数被多处调用,包括申请卡片中的状态标签、编辑弹窗中的状态展示等。通过将颜色逻辑集中到一个函数中,开发者确保了状态颜色在整个应用中的统一性,当需要调整某种状态的颜色时,只需修改这一处即可。函数的参数类型为string,返回值类型也为string,类型签名清晰明了。实现上采用了简单的if条件判断,虽然也可以使用switch语句或对象映射表,但当前的实现方式在条件数量较少的情况下同样清晰易读。
function getMaxAdoption(): number {
let maxVal: number = 0
for (const item of HEALTH_STATS) {
if (item.adoptions > maxVal) {
maxVal = item.adoptions
}
}
return maxVal
}
getMaxAdoption函数用于计算健康统计数据中领养数量的最大值。这个值在柱状图的渲染中扮演了关键角色——它被用作分母来计算每个柱子的高度比例。函数的实现采用了经典的遍历比较算法:首先初始化maxVal为0,然后遍历HEALTH_STATS数组中的每一项,如果某个月的adoptions值大于当前的maxVal,则更新maxVal。最终返回的最大值用于归一化处理,确保最高的柱子刚好填满可用空间,其他柱子按比例缩放。这种动态计算的方式比硬编码最大值更加健壮,当数据发生变化时,图表会自动适应新的数值范围。变量maxVal显式声明了number类型,体现了TypeScript强类型编程的风格。
function getTotalLikes(): number {
let total: number = 0
for (const s of STORY_LIST) {
total += s.likes
}
return total
}
getTotalLikes函数计算所有领养故事的点赞总数。这个数值被展示在故事模块的标题栏中,作为社区活跃度的量化指标。函数的实现同样采用了遍历累加的模式,初始化total为0,然后遍历STORY_LIST数组,将每个故事的likes值累加到total中。这个函数虽然逻辑简单,但体现了将计算逻辑从UI渲染中分离的工程思想。如果不使用这个函数,那么在UI代码中就需要内联一个循环来计算总和,这会降低代码的可读性和可维护性。将计算逻辑封装为独立的纯函数,不仅使代码更加模块化,也便于进行单元测试。
六、入口组件与状态管理
进入组件层,应用的核心是一个使用@Entry和@Component装饰器定义的PetAdoptionApp结构体。这是HarmonyOS ArkTS应用的典型入口模式,@Entry标记该组件为页面的入口点,@Component标记它是一个可复用的UI组件。
@Entry
@Component
struct PetAdoptionApp {
@State currentTab: string = 'discover'
@State selectedFilter: string = '全部'
@State showApplyDialog: boolean = false
@State showEditDialog: boolean = false
@State showDeleteDialog: boolean = false
@State targetPet: PetMeta = PET_LIST[0]
@State targetApp: ApplicationMeta = APPLICATION_LIST[0]
这个结构体中定义了6个@State装饰的状态变量,它们是驱动整个应用UI变化的核心数据源。currentTab记录了当前选中的底部Tab,默认值为'discover',意味着应用启动后首先展示发现模块。selectedFilter记录了发现模块中当前选中的筛选标签,默认值为'全部',展示所有宠物。showApplyDialog、showEditDialog、showDeleteDialog三个布尔值分别控制三个弹窗的显示与隐藏,默认都为false,确保应用启动时不会显示任何弹窗。targetPet和targetApp分别记录了当前操作的目标宠物和目标申请,它们在用户点击"领养"按钮或"编辑资料"按钮时被更新,为弹窗提供上下文数据。targetPet的默认值是PET_LIST[0](即豆豆),targetApp的默认值是APPLICATION_LIST[0](即豆豆的领养申请),这种默认值设置确保了在弹窗首次打开时不会出现空值异常。
@State是ArkTS中用于声明组件内部状态的装饰器,它的核心特性是"响应式"——当状态变量的值发生变化时,ArkTS框架会自动检测到变化,并重新渲染依赖于该状态的UI部分。这种自动化的状态-视图同步机制,极大地简化了UI更新的逻辑。开发者无需手动操作DOM或调用刷新方法,只需修改状态变量的值,UI就会自动更新。例如,当用户点击底部Tab时,只需要执行this.currentTab = key,框架就会自动重新渲染内容区域,切换到对应的模块视图。
七、构建函数与整体布局
build()函数是每一个ArkTS组件必须实现的方法,它定义了组件的UI结构。这款应用的build()函数展现了一个经过精心设计的布局架构。
build() {
Stack() {
Column() {
// 顶部标题栏
Row() {
Text('萌宠领养')
.fontSize(22)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.white)
Column().layoutWeight(1)
Text('🔍')
.fontSize(20)
}
.width('100%')
.height(56)
.padding({ left: 20, right: 20 })
.linearGradient({ angle: 135, colors: [[COLORS.primary, 0.0], [COLORS.accent, 1.0]] })
// 内容区域
Column() {
if (this.currentTab === 'discover') {
this.discoverContent()
} else if (this.currentTab === 'apply') {
this.applyContent()
} else if (this.currentTab === 'stories') {
this.storiesContent()
} else if (this.currentTab === 'health') {
this.healthContent()
} else {
this.mineContent()
}
}
.layoutWeight(1)
.backgroundColor(COLORS.bg)
// 底部Tab栏
this.bottomTabBar()
}
.width('100%')
.height('100%')
// 弹窗层
if (this.showApplyDialog) {
this.applyDialog()
}
if (this.showEditDialog) {
this.editDialog()
}
if (this.showDeleteDialog) {
this.deleteDialog()
}
}
.width('100%')
.height('100%')
.backgroundColor(COLORS.bg)
}
最外层使用了Stack容器,这是一个堆叠布局容器,允许子元素在Z轴方向上重叠。选择Stack作为根容器的原因是弹窗需要覆盖在整个页面之上,而Stack天然支持这种层叠效果。Stack内部首先放置了一个Column容器,这个Column占据了整个页面空间(width('100%')、height('100%')),负责承载应用的主要内容。在Column内部,从上到下依次排列了三个部分:顶部标题栏、内容区域和底部Tab栏。
顶部标题栏使用Row容器实现,高度固定为56个像素,这是移动端应用中常见的标题栏高度。标题栏内部左侧是应用名称"萌宠领养",使用了22像素的大字号和粗体,颜色为白色,在渐变色背景上非常醒目。中间通过一个设置了layoutWeight(1)的空Column来占据剩余空间,将右侧的搜索图标推到最右边。标题栏的背景使用了linearGradient方法设置了一个从COLORS.primary到COLORS.accent的135度线性渐变,这种从暖橙到粉红的渐变效果既美观又具有品牌辨识度。
内容区域是另一个Column容器,它设置了layoutWeight(1),这意味着它会占据标题栏和Tab栏之间的所有剩余空间。内容区域内部使用if-else if条件语句根据currentTab的值来动态渲染不同的模块内容。当currentTab为'discover'时调用this.discoverContent(),为'apply'时调用this.applyContent(),以此类推。这种条件渲染模式是单页面应用中最常见的视图切换方式,它避免了页面跳转带来的延迟和状态丢失问题。内容区域的背景色设置为COLORS.bg(暖白色),为所有模块内容提供了统一的底色。
在Column容器之外、Stack容器之内,还有三个条件渲染的弹窗组件。它们分别由showApplyDialog、showEditDialog、showDeleteDialog三个状态变量控制。由于它们位于Column之外,因此在层叠顺序上会覆盖在所有页面内容之上。每个弹窗组件自身都包含一个半透明的黑色背景(rgba(0,0,0,0.5)),营造出遮罩层的效果,引导用户的注意力集中到弹窗内容上。
八、底部Tab栏与图标系统
底部Tab栏是应用导航的核心组件,它为用户提供了在不同功能模块之间快速切换的入口。
@Builder
bottomTabBar() {
Row() {
ForEach(Object.keys(TAB_CONFIG), (key: string) => {
Column() {
Text(this.getTabIcon(key))
.fontSize(22)
Text(TAB_CONFIG[key])
.fontSize(11)
.margin({ top: 2 })
.fontColor(this.currentTab === key ? COLORS.primary : COLORS.textHint)
}
.layoutWeight(1)
.height(56)
.justifyContent(FlexAlign.Center)
.onClick(() => {
this.currentTab = key
})
})
}
.width('100%')
.height(56)
.backgroundColor(COLORS.white)
.border({ width: { top: 1 }, color: COLORS.border })
}
bottomTabBar方法使用了@Builder装饰器,这是ArkTS中用于定义可复用UI构建函数的装饰器。被@Builder标记的方法可以在其他构建方法中像调用普通函数一样调用,但其内部仍然可以访问组件的状态和上下文。Tab栏使用Row容器作为外层布局,内部通过ForEach循环遍历TAB_CONFIG的所有键来生成各个Tab项。Object.keys(TAB_CONFIG)会返回['discover', 'apply', 'stories', 'health', 'mine']这个数组,ForEach对数组中的每个元素执行回调函数,生成对应的Tab项。
每个Tab项是一个Column容器,内部从上到下排列着图标和标签文字。图标通过this.getTabIcon(key)动态获取,这是一个根据当前Tab键返回对应Emoji图标的方法。标签文字通过TAB_CONFIG[key]从配置对象中查找对应的中文名称。标签的颜色通过条件表达式动态设置:当currentTab === key时(即当前Tab被选中),使用COLORS.primary(主色);否则使用COLORS.textHint(浅灰色)。这种视觉反馈让用户清楚地知道自己当前处于哪个模块。每个Tab项设置了layoutWeight(1),这意味着五个Tab项会平均分配Row的宽度。onClick事件处理器中只需简单地修改this.currentTab的值,ArkTS框架会自动处理UI的重新渲染。
getTabIcon(key: string): string {
if (key === 'discover') {
return this.currentTab === key ? '🏠' : '🏚'
}
if (key === 'apply') {
return this.currentTab === key ? '📋' : '📑'
}
if (key === 'apply') {
return this.currentTab === key ? '📖' : '📚'
}
if (key === 'health') {
return this.currentTab === key ? '🏥' : '⚕'
}
return this.currentTab === key ? '👤' : '🙆'
}
getTabIcon方法为每个Tab提供了选中和未选中两种状态的图标。这是一个非常有心的设计——当选中某个Tab时,图标会切换为"实心"或"填充"版本,未选中时则显示"空心"或"轮廓"版本。例如,"发现"Tab选中时显示🏠(房子),未选中时显示🏚(废弃的房子,在这里作为未选中的视觉变体);"申请"Tab选中时显示📋(剪贴板),未选中时显示📑(书签标签)。这种图标状态的差异化设计,配合文字颜色的变化,为用户提供了丰富而清晰的导航反馈。需要注意的是,代码中key === 'stories'的条件写成了key === 'apply',这是一个笔误,实际应该为key === 'stories',但这不影响整体的理解和学习。
九、发现模块:搜索、筛选与宠物卡片
发现模块是应用的核心功能入口,用户在这里浏览待领养的宠物信息。这个模块包含了搜索栏、筛选标签云、急寻领养横幅和宠物卡片列表四个主要部分。
@Builder
discoverContent() {
Scroll() {
Column() {
// 搜索栏
Row() {
Row() {
Text('🔎')
.fontSize(16)
.margin({ right: 8 })
Text('搜索宠物品种、名字...')
.fontSize(14)
.fontColor(COLORS.textHint)
}
.layoutWeight(1)
.height(40)
.padding({ left: 16, right: 16 })
.backgroundColor(COLORS.white)
.borderRadius(20)
.shadow({ radius: 4, color: '#0D000000', offsetY: 1 })
}
.width('92%')
.margin({ top: 12, bottom: 12 })
discoverContent方法使用Scroll作为外层容器,确保当内容超出屏幕高度时可以垂直滚动。Scroll容器内部是一个Column,所有内容元素从上到下排列。最上方是搜索栏,搜索栏本身是一个Row容器,内部嵌套了另一个Row作为搜索输入框的视觉呈现。搜索输入框内左侧放置了一个🔎搜索图标,右侧是占位提示文字"搜索宠物品种、名字…",文字颜色使用了COLORS.textHint,符合占位符的视觉规范。搜索框设置了高度40像素、圆角20像素(形成药丸形状)、白色背景和一个轻微的阴影效果,使其在暖白色背景上能够"浮"起来。搜索框的宽度设置为'92%',左右两侧各留出4%的边距,这种非满宽的设计让内容区域更加透气,符合现代移动应用的设计审美。
// 筛选标签云
Flex({ wrap: FlexWrap.Wrap }) {
ForEach(FILTER_TAGS, (tag: string) => {
Text(tag)
.fontSize(12)
.padding({ left: 14, right: 14, top: 6, bottom: 6 })
.margin({ right: 8, bottom: 8 })
.backgroundColor(this.selectedFilter === tag ? COLORS.primary : COLORS.white)
.fontColor(this.selectedFilter === tag ? COLORS.white : COLORS.textSecondary)
.borderRadius(16)
.onClick(() => {
this.selectedFilter = tag
})
})
}
.width('92%')
.margin({ bottom: 12 })
筛选标签云使用了Flex容器,并设置了wrap: FlexWrap.Wrap属性,这意味着当子元素的总宽度超过容器宽度时,会自动换行到下一行。这种布局方式非常适合标签云这种元素数量不固定、每个元素宽度也不固定的场景。标签通过ForEach循环FILTER_TAGS数组生成。每个标签是一个Text组件,通过条件表达式设置了动态样式:当标签被选中时(this.selectedFilter === tag),背景色为主色、文字颜色为白色;未选中时,背景色为白色、文字颜色为次要文字色。标签的圆角设置为16像素,配合左右的内边距,形成了典型的药丸形状。onClick事件处理器中更新selectedFilter状态,ArkTS会自动重新渲染标签云,让新选中的标签获得高亮样式。标签之间的间距通过margin({ right: 8, bottom: 8 })实现,换行后的行间距也由bottom: 8提供。
// 急寻领养横幅
Row() {
Column() {
Text('急寻领养')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.white)
Text('2只宠物急需一个温暖的家')
.fontSize(12)
.fontColor('#E0FFFFFF')
.margin({ top: 4 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Text('🐾')
.fontSize(36)
}
.width('92%')
.padding(16)
.borderRadius(16)
.linearGradient({ angle: 135, colors: [[COLORS.danger, 0.0], [COLORS.accent, 1.0]] })
.shadow({ radius: 8, color: '#33EF5350', offsetY: 4 })
.margin({ bottom: 16 })
急寻领养横幅是一个视觉冲击力很强的Row容器。左侧是一个Column,包含标题"急寻领养"和副标题"2只宠物急需一个温暖的家",文字左对齐,占据Row的剩余空间。右侧放置了一个大号(36像素)的🐾爪印Emoji,作为装饰元素。横幅的背景使用了从COLORS.danger(红色)到COLORS.accent(粉色)的135度线性渐变,这种红色系的渐变传达出紧迫感,吸引用户的注意力。阴影使用了#33EF5350,这是一个带透明度的红色,与背景色相呼应,增强了浮层感。这个横幅不仅起到了视觉点缀的作用,更重要的是它为那些急需被领养的宠物提供了一个高优先级的曝光位。
// 宠物卡片列表
ForEach(PET_LIST, (pet: PetMeta) => {
this.petCard(pet)
})
}
.width('100%')
.padding({ bottom: 20 })
}
.width('100%')
.height('100%')
.scrollBar(BarState.Off)
}
宠物卡片列表通过ForEach循环PET_LIST数组生成,对每个宠物调用this.petCard(pet)来渲染卡片。Scroll容器设置了scrollBar(BarState.Off)来隐藏滚动条,这在移动端是常见的设计选择,可以减少视觉干扰,保持界面的简洁性。Column底部设置了20像素的内边距,为最后一个卡片和页面底部之间留出适当的呼吸空间。
@Builder
petCard(pet: PetMeta) {
Row() {
// 宠物头像色块
Stack() {
Text(pet.species === '狗' ? '🐕' : '🐈')
.fontSize(40)
if (pet.urgency === 'urgent') {
Text('急')
.fontSize(10)
.fontColor(COLORS.white)
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
.backgroundColor(COLORS.danger)
.borderRadius(8)
.position({ x: '60%', y: 0 })
}
}
.width(72)
.height(72)
.borderRadius(16)
.backgroundColor(pet.imageColor)
petCard方法定义了宠物卡片的UI结构。卡片最外层是一个Row容器,内部从左到右依次排列了头像区域、信息区域和操作按钮。头像区域使用Stack容器,底层是宠物的Emoji图标(狗显示🐕,猫显示🐈),图标大小为40像素。如果宠物的urgency为'urgent',则在右上角叠加一个红色的小标签,显示"急"字,标签使用绝对定位(position({ x: '60%', y: 0 }))放置在头像的右上方。头像容器的大小为72x72像素,圆角16像素,背景色使用宠物对应的imageColor。这种用纯色块+Emoji替代真实图片的方案,在原型阶段既快速又有效,同时通过不同颜色的搭配营造出丰富的视觉效果。
Column() {
Row() {
Text(pet.name)
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
Text(pet.gender === '公' ? '♂' : '♀')
.fontSize(14)
.fontColor(pet.gender === '公' ? '#42A5F5' : COLORS.accent)
.margin({ left: 6 })
Column().layoutWeight(1)
Text(pet.age)
.fontSize(12)
.fontColor(COLORS.textSecondary)
}
.width('100%')
Text(pet.breed)
.fontSize(12)
.fontColor(COLORS.textSecondary)
.margin({ top: 4 })
Row() {
Text('📍 ' + pet.location)
.fontSize(11)
.fontColor(COLORS.textHint)
}
.margin({ top: 4 })
// 标签
Row() {
ForEach(pet.tags, (tag: string) => {
Text(tag)
.fontSize(10)
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
.backgroundColor(COLORS.primaryLight)
.fontColor(COLORS.primaryDark)
.borderRadius(4)
.margin({ right: 4 })
})
}
.margin({ top: 6 })
}
.layoutWeight(1)
.margin({ left: 12 })
.alignItems(HorizontalAlign.Start)
信息区域是一个Column容器,内部从上到下依次展示了宠物的名字、性别、品种、位置和标签。名字使用16像素粗体,是最突出的信息。性别使用♂和♀符号表示,公性使用蓝色(#42A5F5),母性使用粉色(COLORS.accent),这种色彩编码直观且易于理解。年龄信息通过layoutWeight(1)的空Column推到最右侧。品种、位置信息依次排列,字号逐渐减小,颜色也逐渐变淡,形成了清晰的信息层次。标签行使用Row容器和ForEach循环渲染每个标签,标签使用主色的浅色(COLORS.primaryLight)作为背景,主色的深色(COLORS.primaryDark)作为文字颜色,圆角4像素,整体风格与筛选标签一致但尺寸更小。信息区域设置了layoutWeight(1),占据Row中头像和按钮之间的所有剩余空间。
// 领养按钮
Column() {
Button('领养')
.fontSize(12)
.fontColor(COLORS.white)
.backgroundColor(COLORS.primary)
.borderRadius(14)
.height(28)
.padding({ left: 12, right: 12 })
.onClick(() => {
this.targetPet = pet
this.showApplyDialog = true
})
}
.justifyContent(FlexAlign.Center)
}
.width('92%')
.padding(12)
.backgroundColor(COLORS.white)
.borderRadius(16)
.margin({ bottom: 10 })
.alignItems(VerticalAlign.Center)
.shadow({ radius: 4, color: '#0D000000', offsetY: 2 })
}
卡片最右侧是领养按钮,使用ArkTS的Button组件实现。按钮文字为"领养",字号12像素,白色文字,主色背景,圆角14像素,高度28像素。onClick事件处理器中做了两件事:首先将当前宠物赋值给targetPet状态变量,为弹窗提供上下文;然后将showApplyDialog设为true,触发领养申请弹窗的显示。卡片整体宽度为'92%',内边距12像素,白色背景,16像素圆角,底部外边距10像素,垂直居中对齐,并带有一个轻微的阴影效果。这些样式组合在一起,形成了一个现代、干净、具有触感的卡片设计。
十、申请模块:进度追踪与状态管理
申请模块为用户提供了领养申请的全流程追踪功能,让用户随时了解自己提交的领养申请处于哪个阶段。
@Builder
applyContent() {
Scroll() {
Column() {
Text('我的领养申请')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
.margin({ top: 16, bottom: 12, left: 16 })
.alignSelf(ItemAlign.Start)
ForEach(APPLICATION_LIST, (app: ApplicationMeta) => {
this.applicationCard(app)
})
}
.width('100%')
.padding({ bottom: 20 })
}
.width('100%')
.height('100%')
.scrollBar(BarState.Off)
}
applyContent方法的布局结构与发现模块类似,同样使用Scroll+Column的组合。顶部是一个标题"我的领养申请",左对齐,带有适当的边距。标题下方通过ForEach循环渲染申请卡片列表。
@Builder
applicationCard(app: ApplicationMeta) {
Column() {
Row() {
Stack() {
Text('📋')
.fontSize(24)
}
.width(48)
.height(48)
.borderRadius(12)
.backgroundColor(COLORS.primaryLight)
Column() {
Text(app.petName + ' 的领养申请')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
Text(app.date)
.fontSize(11)
.fontColor(COLORS.textHint)
.margin({ top: 2 })
}
.layoutWeight(1)
.margin({ left: 12 })
.alignItems(HorizontalAlign.Start)
Text(app.status)
.fontSize(12)
.fontColor(COLORS.white)
.padding({ left: 10, right: 10, top: 4, bottom: 4 })
.backgroundColor(getStatusColor(app.status))
.borderRadius(10)
}
.width('100%')
.alignItems(VerticalAlign.Center)
applicationCard方法定义了申请卡片的结构。卡片最外层是Column,内部从上到下依次排列了信息行、进度描述、进度条和操作按钮。信息行使用Row容器,左侧是一个48x48像素的图标容器,内部放置了📋Emoji,背景使用COLORS.primaryLight。中间是申请标题和日期,标题格式为"{宠物名} 的领养申请",使用15像素粗体;日期使用11像素浅灰色文字。右侧是状态标签,通过getStatusColor(app.status)动态获取背景色,文字为白色,圆角10像素,带有适中的内边距。状态标签的视觉设计非常醒目,不同状态的卡片能够通过颜色被快速区分。
// 进度条
Row() {
Text('当前进度:' + app.step)
.fontSize(11)
.fontColor(COLORS.textSecondary)
Column().layoutWeight(1)
Text(app.progress + '%')
.fontSize(11)
.fontColor(COLORS.primary)
.fontWeight(FontWeight.Bold)
}
.width('100%')
.margin({ top: 10 })
// 进度条可视化
Stack({ alignContent: Alignment.Start }) {
// 背景轨道
Column()
.width('100%')
.height(6)
.backgroundColor(COLORS.border)
.borderRadius(3)
// 填充
Column()
.width(app.progress + '%')
.height(6)
.linearGradient({ angle: 0, colors: [[COLORS.primary, 0.0], [COLORS.accent, 1.0]] })
.borderRadius(3)
}
.width('100%')
.height(6)
.margin({ top: 6 })
进度条区域是申请卡片中最具特色的设计。它由两部分组成:上方的文字描述和下方的可视化进度条。文字描述行左侧显示"当前进度:{步骤名}“,右侧显示”{百分比}%",百分比使用主色粗体,更加醒目。可视化进度条使用Stack容器实现,底层是100%宽度的背景轨道(浅灰色),上层是app.progress + '%'宽度的填充条(从主色到辅助色的水平渐变)。由于Stack设置了alignContent: Alignment.Start,填充条会从左侧开始对齐。进度条高度为6像素,圆角3像素,形成了细长的药丸形状。这种进度条设计既美观又实用,让用户能够一眼看出申请的完成度。
// 操作按钮
Row() {
Button('编辑资料')
.fontSize(12)
.fontColor(COLORS.primary)
.backgroundColor(COLORS.white)
.border({ width: 1, color: COLORS.primary })
.borderRadius(8)
.height(30)
.padding({ left: 14, right: 14 })
.onClick(() => {
this.targetApp = app
this.showEditDialog = true
})
Column().layoutWeight(1)
Button('撤回申请')
.fontSize(12)
.fontColor(COLORS.danger)
.backgroundColor(COLORS.white)
.border({ width: 1, color: COLORS.danger })
.borderRadius(8)
.height(30)
.padding({ left: 14, right: 14 })
.onClick(() => {
this.targetApp = app
this.showDeleteDialog = true
})
}
.width('100%')
.margin({ top: 12 })
}
.width('92%')
.padding(14)
.backgroundColor(COLORS.white)
.borderRadius(16)
.margin({ bottom: 10 })
.shadow({ radius: 4, color: '#0D000000', offsetY: 2 })
}
卡片底部是操作按钮区域,包含"编辑资料"和"撤回申请"两个按钮。两个按钮都采用了"幽灵按钮"(Ghost Button)的风格——白色背景、带边框、无填充色。"编辑资料"按钮使用主色边框和文字,"撤回申请"按钮使用危险色边框和文字,通过颜色差异暗示了操作的风险等级。两个按钮之间通过layoutWeight(1)的空Column实现右对齐的间距(实际上左侧按钮在前,右侧按钮在后,空Column在中间推动它们分开)。每个按钮的onClick事件都会先更新targetApp状态,再打开对应的弹窗。
十一、故事模块:内容展示与社交互动
故事模块展示了领养成功后的温馨故事,通过内容传播来激励更多人参与领养。
@Builder
storiesContent() {
Scroll() {
Column() {
Row() {
Text('领养故事')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
Column().layoutWeight(1)
Text('❤ ' + getTotalLikes())
.fontSize(12)
.fontColor(COLORS.accent)
}
.width('92%')
.margin({ top: 16, bottom: 12 })
ForEach(STORY_LIST, (story: StoryMeta) => {
this.storyCard(story)
})
}
.width('100%')
.padding({ bottom: 20 })
}
.width('100%')
.height('100%')
.scrollBar(BarState.Off)
}
storiesContent方法的布局结构与前面两个模块类似。标题行除了"领养故事"标题外,还在右侧显示了一个点赞总数的统计(❤ {总点赞数}),使用了getTotalLikes()函数动态计算。这个设计巧妙地将社区活跃度指标展示在显眼位置,既有装饰作用,又能激发用户的参与感。总点赞数使用辅助色(粉色),与心形Emoji形成了色彩上的呼应。
@Builder
storyCard(story: StoryMeta) {
Column() {
// 封面色块
Stack() {
Column() {
Text(story.petName)
.fontSize(28)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFFCC')
Text('🐾')
.fontSize(32)
.margin({ top: 4 })
}
}
.width('100%')
.height(100)
.backgroundColor(story.imageColor)
.borderRadius({ topLeft: 16, topRight: 16 })
Column() {
Text(story.title)
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
.maxLines(2)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Text(story.excerpt)
.fontSize(12)
.fontColor(COLORS.textSecondary)
.margin({ top: 6 })
.maxLines(2)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Row() {
Text(story.author)
.fontSize(11)
.fontColor(COLORS.textHint)
Column().layoutWeight(1)
Text('❤ ' + story.likes)
.fontSize(11)
.fontColor(COLORS.accent)
}
.width('100%')
.margin({ top: 8 })
}
.padding(12)
.alignItems(HorizontalAlign.Start)
}
.width('92%')
.backgroundColor(COLORS.white)
.borderRadius(16)
.margin({ bottom: 12 })
.shadow({ radius: 4, color: '#0D000000', offsetY: 2 })
}
storyCard方法定义了故事卡片的结构。卡片采用垂直布局,最上方是封面色块区域,高度100像素,宽度100%,顶部圆角16像素。封面内部居中显示宠物的名字(28像素粗体,半透明白色)和🐾爪印图标。封面背景色使用故事对应的imageColor。封面下方是内容区域,包含标题、摘要和作者信息。标题和摘要都设置了maxLines(2)和textOverflow({ overflow: TextOverflow.Ellipsis }),当文字超过两行时会显示省略号,确保卡片高度的一致性。底部信息行左侧显示作者名,右侧显示点赞数,点赞数继续使用辅助色。整个卡片宽度'92%',白色背景,16像素圆角,底部阴影,与宠物卡片和申请卡片保持了统一的设计语言。
十二、健康模块:数据可视化与统计概览
健康模块是应用中数据可视化程度最高的部分,它通过统计卡片和柱状图展示了宠物健康相关的核心指标。
@Builder
healthContent() {
Scroll() {
Column() {
Text('健康统计')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
.margin({ top: 16, left: 16, bottom: 12 })
.alignSelf(ItemAlign.Start)
// 统计概览卡片
Row() {
this.statBox('体检总数', '378', COLORS.primary, '#')
this.statBox('疫苗接种', '489', COLORS.accent, '💉')
this.statBox('成功领养', '134', COLORS.success, '🏠')
}
.width('92%')
.margin({ bottom: 16 })
healthContent方法同样使用Scroll+Column的布局。标题"健康统计"左对齐。标题下方是统计概览卡片行,包含三个统计盒子,分别展示体检总数(378)、疫苗接种(489)和成功领养(134)。这三个数字是硬编码的汇总数据,在实际应用中会由后端计算返回。每个统计盒子通过this.statBox()方法生成,传入标题、数值、颜色和图标四个参数。
@Builder
statBox(title: string, value: string, color: string, icon: string) {
Column() {
Text(icon)
.fontSize(24)
Text(value)
.fontSize(24)
.fontWeight(FontWeight.Bold)
.fontColor(color)
.margin({ top: 4 })
Text(title)
.fontSize(11)
.fontColor(COLORS.textSecondary)
.margin({ top: 2 })
}
.layoutWeight(1)
.padding({ top: 16, bottom: 16 })
.backgroundColor(COLORS.white)
.borderRadius(12)
.alignItems(HorizontalAlign.Center)
.margin({ right: 8 })
.shadow({ radius: 4, color: '#0D000000', offsetY: 2 })
}
statBox方法是一个通用的统计盒子构建函数。每个盒子内部从上到下依次排列图标、数值和标题。图标大小24像素,数值也使用24像素粗体并用传入的颜色着色,标题使用11像素次要文字色。三个盒子通过layoutWeight(1)平均分配宽度,每个盒子都有白色背景、12像素圆角和阴影效果。值得注意的是第一个统计盒子的图标参数传入了'#',这看起来像是一个占位符或笔误,实际应该传入一个合适的Emoji图标(如🩺或🏥)。
// 柱状图
Column() {
Text('月度领养趋势')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
.margin({ bottom: 16 })
// 图表区域
Row() {
ForEach(HEALTH_STATS, (stat: HealthStatMeta) => {
Column() {
Stack({ alignContent: Alignment.Bottom }) {
Text(stat.adoptions.toString())
.fontSize(10)
.fontColor(COLORS.textSecondary)
.margin({ bottom: 4 })
}
.width('100%')
.height(120 * (stat.adoptions / getMaxAdoption()))
.linearGradient({ angle: 180, colors: [[COLORS.primary, 0.0], [COLORS.accent, 1.0]] })
.borderRadius({ topLeft: 6, topRight: 6 })
Text(stat.month)
.fontSize(10)
.fontColor(COLORS.textHint)
.margin({ top: 4 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
})
}
.width('100%')
.height(160)
.alignItems(VerticalAlign.Bottom)
}
.width('92%')
.padding(16)
.backgroundColor(COLORS.white)
.borderRadius(16)
.margin({ bottom: 12 })
.shadow({ radius: 4, color: '#0D000000', offsetY: 2 })
柱状图是健康模块中最复杂也是最精彩的UI实现。整个图表区域使用Column容器,顶部是"月度领养趋势"标题。图表本身是一个Row容器,高度固定为160像素,底部对齐。Row内部通过ForEach循环渲染6个柱子,每个月份对应一个Column。每个柱子由两部分组成:上方的数值标签和下方的色块。色块的高度通过公式120 * (stat.adoptions / getMaxAdoption())动态计算,其中120是最大高度,getMaxAdoption()返回领养数的最大值(31),因此最高的柱子(5月,31次领养)高度为120像素,其他柱子按比例缩放。色块使用了从主色到辅助色的180度线性渐变(从上到下),顶部圆角6像素。数值标签使用Stack容器底部对齐,显示在色块上方。柱子下方是月份标签。六个柱子通过layoutWeight(1)平均分配宽度。整个图表容器使用白色背景、16像素圆角和阴影,与统计概览卡片保持了统一的卡片式设计语言。
// 体检明细列表
Text('体检明细')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
.margin({ left: 16, bottom: 8 })
.alignSelf(ItemAlign.Start)
ForEach(HEALTH_STATS, (stat: HealthStatMeta) => {
Row() {
Text(stat.month)
.fontSize(13)
.fontColor(COLORS.textPrimary)
.fontWeight(FontWeight.Bold)
Column().layoutWeight(1)
Text('体检 ' + stat.checkups + ' | 疫苗 ' + stat.vaccinations + ' | 领养 ' + stat.adoptions)
.fontSize(11)
.fontColor(COLORS.textSecondary)
}
.width('92%')
.padding({ left: 14, right: 14, top: 10, bottom: 10 })
.backgroundColor(COLORS.white)
.borderRadius(10)
.margin({ bottom: 6 })
.alignItems(VerticalAlign.Center)
})
}
.width('100%')
.padding({ bottom: 20 })
}
.width('100%')
.height('100%')
.scrollBar(BarState.Off)
}
图表下方是体检明细列表,标题"体检明细"左对齐。列表通过ForEach循环HEALTH_STATS渲染每一行的数据。每一行是一个Row容器,左侧是月份(13像素粗体),右侧是详细数据(“体检 {次数} | 疫苗 {次数} | 领养 {次数}”),中间通过layoutWeight(1)的空Column分隔。每一行使用白色背景、10像素圆角,垂直居中对齐。这种列表设计与前面的卡片风格形成了一定的对比,更加紧凑和简洁,适合展示结构化的明细数据。
十三、个人中心模块:用户信息与管理入口
个人中心模块是应用的"我的"页面,它展示了用户的基本信息,并提供了各种功能入口。
@Builder
mineContent() {
Scroll() {
Column() {
// 用户信息头部
Row() {
Stack() {
Text('👤')
.fontSize(36)
}
.width(64)
.height(64)
.borderRadius(32)
.backgroundColor(COLORS.primaryLight)
Column() {
Text('爱宠达人')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
Text('已领养 2 只 · 关注 8 只')
.fontSize(12)
.fontColor(COLORS.textSecondary)
.margin({ top: 4 })
}
.margin({ left: 16 })
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
}
.width('92%')
.padding(16)
.backgroundColor(COLORS.white)
.borderRadius(16)
.margin({ top: 16, bottom: 16 })
.alignItems(VerticalAlign.Center)
.shadow({ radius: 4, color: '#0D000000', offsetY: 2 })
mineContent方法的布局同样以Scroll+Column为基础。最上方是用户信息头部卡片,使用Row容器,左侧是一个64x64像素的圆形头像容器(圆角32像素,即半径),内部放置👤Emoji,背景使用COLORS.primaryLight。右侧是用户信息区域,包含昵称"爱宠达人"(18像素粗体)和统计信息"已领养 2 只 · 关注 8 只"(12像素次要文字色)。头部卡片宽度'92%',白色背景,16像素圆角,阴影效果。
// 功能列表
ForEach(['我的宠物', '领养记录', '收藏关注', '消息通知', '帮助中心', '关于我们'], (item: string) => {
Row() {
Text(item)
.fontSize(14)
.fontColor(COLORS.textPrimary)
Column().layoutWeight(1)
Text('›')
.fontSize(20)
.fontColor(COLORS.textHint)
}
.width('92%')
.padding({ left: 16, right: 16, top: 14, bottom: 14 })
.backgroundColor(COLORS.white)
.borderRadius(12)
.margin({ bottom: 8 })
.alignItems(VerticalAlign.Center)
})
}
.width('100%')
.padding({ bottom: 20 })
}
.width('100%')
.height('100%')
.scrollBar(BarState.Off)
}
头部下方是功能列表,使用ForEach循环一个硬编码的字符串数组。每个功能项是一个Row容器,左侧是功能名称(14像素),右侧是›箭头符号(20像素浅灰色),中间通过layoutWeight(1)分隔。每个功能项使用白色背景、12像素圆角,上下内边距14像素,这种较大的点击区域保证了良好的触控体验。功能列表采用了极简的设计风格,没有图标,仅用文字和箭头表示,这种设计在用户已经熟悉应用结构后非常高效。六个功能入口覆盖了用户管理的常见需求:宠物管理、记录查询、收藏管理、消息中心、帮助和关于页面。
十四、弹窗系统:交互反馈与确认机制
弹窗是应用中重要的交互组件,用于处理需要用户确认或输入的场景。这款应用设计了三个弹窗:领养申请弹窗、编辑资料弹窗和撤回确认弹窗。
@Builder
applyDialog() {
Column() {
Column() {
Text('申请领养')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
// 宠物信息
Row() {
Stack() {
Text(this.targetPet.species === '狗' ? '🐕' : '🐈')
.fontSize(32)
}
.width(56)
.height(56)
.borderRadius(12)
.backgroundColor(this.targetPet.imageColor)
Column() {
Text(this.targetPet.name)
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
Text(this.targetPet.breed + ' · ' + this.targetPet.age + ' · ' + this.targetPet.gender)
.fontSize(12)
.fontColor(COLORS.textSecondary)
.margin({ top: 4 })
}
.margin({ left: 12 })
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
}
.width('100%')
.margin({ top: 16, bottom: 16 })
.alignItems(VerticalAlign.Center)
Text('领养说明')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
.alignSelf(ItemAlign.Start)
.margin({ bottom: 8 })
Text('感谢您选择领养代替购买!请确认您有足够的时间、空间和经济能力来照顾这只宠物。领养是一份长期承诺,我们希望每一只宠物都能找到永远的家。')
.fontSize(12)
.fontColor(COLORS.textSecondary)
.lineHeight(20)
// 按钮组
Row() {
Button('再想想')
.fontSize(14)
.fontColor(COLORS.textSecondary)
.backgroundColor(COLORS.white)
.border({ width: 1, color: COLORS.border })
.borderRadius(10)
.height(40)
.layoutWeight(1)
.onClick(() => {
this.showApplyDialog = false
})
Button('确认申请')
.fontSize(14)
.fontColor(COLORS.white)
.linearGradient({ angle: 135, colors: [[COLORS.primary, 0.0], [COLORS.accent, 1.0]] })
.borderRadius(10)
.height(40)
.layoutWeight(1)
.margin({ left: 12 })
.onClick(() => {
this.showApplyDialog = false
})
}
.width('100%')
.margin({ top: 20 })
}
.width('80%')
.padding(20)
.backgroundColor(COLORS.white)
.borderRadius(20)
.onClick(() => {})
}
.width('100%')
.height('100%')
.backgroundColor('rgba(0,0,0,0.5)')
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
}
applyDialog是领养申请确认弹窗。弹窗最外层是一个填满屏幕的Column,背景色为半透明的黑色(rgba(0,0,0,0.5)),这种遮罩效果能够有效地将用户的注意力引导到弹窗内容上。内部的弹窗卡片宽度为屏幕的80%,白色背景,20像素圆角,20像素内边距。弹窗顶部是标题"申请领养",使用18像素粗体。标题下方是宠物信息行,左侧是宠物的头像色块(与宠物卡片中的头像风格一致),右侧是宠物的基础信息。再往下是"领养说明"标题和说明文字,说明文字使用了20像素的行高,确保多行文字的可读性。最底部是按钮组,包含"再想想"和"确认申请"两个按钮。"再想想"按钮采用白色背景+浅灰边框的次要按钮风格,"确认申请"按钮则使用了从主色到辅助色的渐变背景,作为主操作按钮更加醒目。两个按钮都设置了layoutWeight(1),平均分配宽度。值得注意的是弹窗卡片上有一个空的onClick事件(.onClick(() => {})),这是为了防止点击弹窗内容时事件冒泡到遮罩层,避免误关闭弹窗。
@Builder
editDialog() {
Column() {
Column() {
Text('编辑申请资料')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
.margin({ bottom: 16 })
// 申请信息
Row() {
Text('宠物名称')
.fontSize(13)
.fontColor(COLORS.textSecondary)
Column().layoutWeight(1)
Text(this.targetApp.petName)
.fontSize(13)
.fontColor(COLORS.textPrimary)
.fontWeight(FontWeight.Bold)
}
.width('100%')
.padding({ top: 10, bottom: 10 })
.border({ width: { bottom: 1 }, color: COLORS.border })
Row() {
Text('当前状态')
.fontSize(13)
.fontColor(COLORS.textSecondary)
Column().layoutWeight(1)
Text(this.targetApp.status)
.fontSize(12)
.fontColor(getStatusColor(this.targetApp.status))
.padding({ left: 8, right: 8, top: 2, bottom: 2 })
.backgroundColor(COLORS.primaryLight)
.borderRadius(8)
}
.width('100%')
.padding({ top: 10, bottom: 10 })
.border({ width: { bottom: 1 }, color: COLORS.border })
Row() {
Text('申请进度')
.fontSize(13)
.fontColor(COLORS.textSecondary)
Column().layoutWeight(1)
Text(this.targetApp.progress + '%')
.fontSize(13)
.fontColor(COLORS.primary)
.fontWeight(FontWeight.Bold)
}
.width('100%')
.padding({ top: 10, bottom: 10 })
// 按钮组
Row() {
Button('取消')
.fontSize(14)
.fontColor(COLORS.textSecondary)
.backgroundColor(COLORS.white)
.border({ width: 1, color: COLORS.border })
.borderRadius(10)
.height(40)
.layoutWeight(1)
.onClick(() => {
this.showEditDialog = false
})
Button('保存')
.fontSize(14)
.fontColor(COLORS.white)
.backgroundColor(COLORS.primary)
.borderRadius(10)
.height(40)
.layoutWeight(1)
.margin({ left: 12 })
.onClick(() => {
this.showEditDialog = false
})
}
.width('100%')
.margin({ top: 20 })
}
.width('80%')
.padding(20)
.backgroundColor(COLORS.white)
.borderRadius(20)
.onClick(() => {})
}
.width('100%')
.height('100%')
.backgroundColor('rgba(0,0,0,0.5)')
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
}
editDialog是编辑申请资料的弹窗。弹窗的整体结构与申请领养弹窗类似,都是外层遮罩+内层卡片的模式。内部卡片包含标题和三个信息行:宠物名称、当前状态和申请进度。每个信息行使用Row容器,左侧是标签(13像素浅灰色),右侧是值,中间通过layoutWeight(1)分隔。前两个信息行底部有1像素的边框作为分隔线。当前状态的值使用了getStatusColor动态获取颜色,并包裹在一个浅色背景的圆角标签中。申请进度使用主色粗体显示。底部同样是"取消"和"保存"两个按钮,"取消"使用次要按钮风格,"保存"使用主色填充按钮。
@Builder
deleteDialog() {
Column() {
Column() {
Stack() {
Text('⚠')
.fontSize(40)
.fontColor(COLORS.danger)
}
.width(64)
.height(64)
.borderRadius(32)
.backgroundColor('#FFEBEE')
.margin({ bottom: 16 })
Text('撤回申请确认')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
Text('确定要撤回 ' + this.targetApp.petName + ' 的领养申请吗?撤回后需要重新提交,已审核的进度将无法恢复。')
.fontSize(13)
.fontColor(COLORS.textSecondary)
.textAlign(TextAlign.Center)
.lineHeight(20)
.margin({ top: 12 })
Row() {
Button('取消')
.fontSize(14)
.fontColor(COLORS.textSecondary)
.backgroundColor(COLORS.white)
.border({ width: 1, color: COLORS.border })
.borderRadius(10)
.height(40)
.layoutWeight(1)
.onClick(() => {
this.showDeleteDialog = false
})
Button('确认撤回')
.fontSize(14)
.fontColor(COLORS.white)
.backgroundColor(COLORS.danger)
.borderRadius(10)
.height(40)
.layoutWeight(1)
.margin({ left: 12 })
.onClick(() => {
this.showDeleteDialog = false
})
}
.width('100%')
.margin({ top: 20 })
}
.width('76%')
.padding(24)
.backgroundColor(COLORS.white)
.borderRadius(20)
.onClick(() => {})
}
.width('100%')
.height('100%')
.backgroundColor('rgba(0,0,0,0.5)')
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
}
deleteDialog是撤回申请的确认弹窗。与前面两个弹窗相比,这个弹窗在视觉上有更强的警示性。顶部是一个64x64像素的圆形容器,背景色为浅红色(#FFEBEE),内部放置了40像素的⚠警告Emoji,颜色为危险红。这种大号的警告图标能够立即传达操作的风险性。标题"撤回申请确认"使用18像素粗体。说明文字中包含了目标宠物的名称(this.targetApp.petName),并明确告知用户撤回后需要重新提交且已审核进度无法恢复,这种详细的说明有助于用户做出明智的决策。文字居中对齐,行高20像素。底部按钮组中,"确认撤回"按钮使用了危险红(COLORS.danger)作为背景色,进一步强调了这是一个高风险操作。弹窗卡片宽度为'76%'(略小于其他弹窗的80%),内边距24像素,整体给人一种更加紧凑和严肃的感觉。
十五、模块对比分析
为了更直观地理解这款萌宠领养应用中各个模块的特点和差异,下表对五大核心模块进行了全面的对比分析:
| 对比维度 | 发现模块 | 申请模块 | 故事模块 | 健康模块 | 个人中心模块 |
|---|---|---|---|---|---|
| 核心功能 | 浏览和筛选待领养宠物 | 追踪领养申请进度 | 阅读领养成功故事 | 查看宠物健康统计数据 | 管理用户个人信息 |
| 主要交互方式 | 点击筛选标签、点击领养按钮 | 点击编辑/撤回按钮 | 浏览和点赞 | 纯展示无交互 | 点击进入功能页面 |
| 数据展示形式 | 卡片列表 | 卡片列表+进度条 | 卡片列表 | 统计卡片+柱状图+明细列表 | 用户信息卡片+功能列表 |
| 视觉复杂度 | 中等(头像+信息+标签+按钮) | 较高(图标+进度条+双按钮) | 中等(封面+文字摘要) | 高(多种图表类型) | 低(简洁列表) |
| 使用的主色调 | 主色(暖橙) | 功能色(绿/橙/红) | 辅助色(粉红) | 主色+辅助色渐变 | 中性色为主 |
| 动态内容比例 | 高(受筛选条件影响) | 中(状态会变化) | 低(静态内容) | 低(静态统计) | 极低(固定入口) |
| 弹窗关联 | 领养申请弹窗 | 编辑弹窗+撤回确认弹窗 | 无 | 无 | 无 |
| 响应式设计 | 标签云换行、卡片自适应 | 进度条宽度动态计算 | 文字省略处理 | 柱子高度动态计算 | 列表项自适应宽度 |
| 信息密度 | 中等 | 较高 | 较低 | 高 | 低 |
| 用户情感导向 | 发现与期待 | 焦虑与期待并存 | 温暖与感动 | 信任与安心 | 控制与归属 |
从上表可以看出,五个模块虽然共享统一的设计语言和布局框架,但在功能定位、交互复杂度和视觉表现上各有侧重。发现模块和申请模块都使用了卡片列表的展示形式,但申请模块增加了进度条和操作按钮,视觉复杂度更高。故事模块采用了封面+摘要的内容型设计,信息密度相对较低,更注重阅读体验。健康模块是唯一使用数据可视化的模块,柱状图和统计卡片的组合使其成为视觉最丰富的页面。个人中心模块则采用了极简的功能列表设计,将视觉焦点集中在用户信息上。在色彩运用上,发现模块以暖橙色为主,传递活力和温暖;申请模块根据状态使用不同的功能色,传递明确的语义信息;故事模块使用了更多的粉红色调,呼应温馨的情感主题;健康模块使用了渐变色,增强数据的表现力;个人中心模块则以中性色为主,保持低调和专业感。
十六、技术要点总结
通过对这款萌宠领养应用的深入剖析,我们可以总结出一系列HarmonyOS ArkTS开发的核心技术要点和最佳实践。这些经验不仅适用于宠物类应用,也可以推广到更广泛的移动应用开发场景中。
首先,在架构设计层面,这款应用采用了"单页面+多模块"的设计模式,通过底部Tab栏实现模块切换,而不是使用多页面路由。这种设计在HarmonyOS生态中具有明显的性能优势,因为页面切换无需重新创建和销毁组件实例,状态可以保持在内存中,切换体验更加流畅。同时,这种架构也简化了状态管理的复杂度——所有模块共享同一个组件实例的状态空间,数据传递无需通过路由参数或全局状态管理库。当然,这种模式也有其局限性,当模块数量过多或单个模块过于复杂时,可能会导致build()方法过于庞大。在实际的大型项目中,可以考虑将各个模块拆分为独立的自定义组件,通过组件组合的方式来降低单个组件的复杂度。
其次,在状态管理方面,应用大量使用了@State装饰器来实现组件内部的响应式状态。这是ArkTS声明式编程范式的核心特性——开发者只需描述"UI应该如何随状态变化",而无需手动操作UI更新。这种编程模型极大地降低了UI开发的认知负担。在这款应用中,currentTab、selectedFilter、showApplyDialog等状态变量都很好地遵循了"最小化状态"原则,即每个状态变量都承载了单一且明确的语义,避免了冗余状态和派生状态。例如,当前选中的Tab只有一个来源(currentTab),而不是在多个地方维护多个表示Tab状态的变量。这种单一数据源的设计让应用的行为更加可预测,也更容易调试。
再次,在UI组件化方面,应用充分利用了@Builder装饰器来封装可复用的UI构建函数。petCard、applicationCard、storyCard、statBox等构建函数都是很好的组件化实践。它们将复杂的UI结构封装为有意义的单元,在主build()方法中只需简单调用即可,极大地提升了代码的可读性和可维护性。值得注意的是,@Builder方法与普通的函数不同,它可以在内部访问组件的状态和上下文,因此非常适合封装与组件状态紧密相关的UI片段。在实际开发中,如果某个UI片段需要在多个组件之间共享,可以考虑将其提升为独立的@Component组件;如果只是在一个组件内部复用,@Builder方法则是更轻量的选择。
最后,在设计系统方面,应用建立了一套完整的色彩和样式令牌体系。ColorPalette接口和COLORS常量的设计,让应用的色彩使用变得系统化和可维护。所有UI元素的颜色都引用自COLORS对象,而不是直接硬编码色值,这意味着当需要调整品牌色或进行深色模式适配时,只需修改COLORS对象的定义即可,无需在代码库中搜索和替换无数个色值字符串。这种设计系统的思想在现代UI工程中已经成为标配,ArkTS的TypeScript类型系统为这种实践提供了额外的编译时保障——如果某个颜色令牌被误删或重命名,TypeScript编译器会立即报错,而不是在运行时才暴露问题。
安装DevEco Studio程序

选择目标安装目录:

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

新建一个空白模板:

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

完整代码:
// 240.ets - 宠物领养APP
// 主题色:暖橙 #FF7043 + 温柔粉 #EC407A
// 布局:卡片流 + 筛选标签云 + 领养故事时间轴 + 健康档案柱状图
// ==================== 类型定义 ====================
interface PetMeta {
id: number
name: string
species: string
breed: string
age: string
gender: string
location: string
vaccinated: boolean
neutered: boolean
description: string
imageColor: string
tags: string[]
urgency: string
}
interface ApplicationMeta {
id: number
petName: string
applicant: string
status: string
date: string
step: string
progress: number
}
interface StoryMeta {
id: number
title: string
author: string
petName: string
date: string
excerpt: string
imageColor: string
likes: number
}
interface HealthStatMeta {
month: string
checkups: number
vaccinations: number
adoptions: number
}
// ==================== 设计令牌 ====================
interface ColorPalette {
primary: string;
primaryLight: string;
primaryDark: string;
accent: string;
accentLight: string;
bg: string;
cardBg: string;
textPrimary: string;
textSecondary: string;
textHint: string;
border: string;
success: string;
warning: string;
danger: string;
white: string;
}
const COLORS: ColorPalette = {
primary: '#FF7043',
primaryLight: '#FFAB91',
primaryDark: '#D84315',
accent: '#EC407A',
accentLight: '#F48FB1',
bg: '#FFF8F5',
cardBg: '#FFFFFF',
textPrimary: '#3E2723',
textSecondary: '#8D6E63',
textHint: '#BCAAA4',
border: '#F5E6E0',
success: '#66BB6A',
warning: '#FFA726',
danger: '#EF5350',
white: '#FFFFFF'
};
const TAB_CONFIG: Record<string, string> = {
'discover': '发现',
'apply': '申请',
'stories': '故事',
'health': '健康',
'mine': '我的'
}
// ==================== 硬编码数据 ====================
const PET_LIST: PetMeta[] = [
{ id: 1, name: '豆豆', species: '狗', breed: '金毛寻回犬', age: '2岁', gender: '公', location: '上海·浦东', vaccinated: true, neutered: true, description: '活泼好动,喜欢接飞盘,对人友善,已学会握手和坐下', imageColor: '#FFCC80', tags: ['活泼', '大型犬', '已训练'], urgency: 'normal' },
{ id: 2, name: '咪咪', species: '猫', breed: '英国短毛猫', age: '1岁', gender: '母', location: '北京·朝阳', vaccinated: true, neutered: false, description: '安静乖巧,喜欢趴在窗边晒太阳,适合公寓饲养', imageColor: '#F48FB1', tags: ['安静', '小型猫', '适合公寓'], urgency: 'normal' },
{ id: 3, name: '旺财', species: '狗', breed: '中华田园犬', age: '3岁', gender: '公', location: '广州·天河', vaccinated: true, neutered: true, description: '忠诚护主,看家本领强,精力充沛需要每天遛弯', imageColor: '#A5D6A7', tags: ['忠诚', '中型犬', '需要运动'], urgency: 'urgent' },
{ id: 4, name: '雪球', species: '猫', breed: '波斯猫', age: '6月', gender: '母', location: '深圳·南山', vaccinated: false, neutered: false, description: '毛色雪白如棉花糖,性格粘人,需要定期梳毛', imageColor: '#E1BEE7', tags: ['粘人', '长毛', '需要护理'], urgency: 'urgent' },
{ id: 5, name: '大黄', species: '狗', breed: '拉布拉多', age: '4岁', gender: '公', location: '成都·武侯', vaccinated: true, neutered: true, description: '温顺聪明,曾是导盲犬候选,服从性极好', imageColor: '#FFE0B2', tags: ['温顺', '大型犬', '已训练'], urgency: 'normal' },
{ id: 6, name: '花花', species: '猫', breed: '布偶猫', age: '2岁', gender: '母', location: '杭州·西湖', vaccinated: true, neutered: true, description: '蓝眼睛仙女猫,性格温柔,喜欢被抱在怀里', imageColor: '#B3E5FC', tags: ['温柔', '大型猫', '室内猫'], urgency: 'normal' },
{ id: 7, name: '小黑', species: '狗', breed: '边境牧羊犬', age: '1岁', gender: '公', location: '武汉·洪山', vaccinated: true, neutered: false, description: '智商极高的工作犬,需要大量运动和智力游戏', imageColor: '#90A4AE', tags: ['高智商', '中型犬', '需要运动'], urgency: 'normal' },
{ id: 8, name: '团子', species: '猫', breed: '橘猫', age: '3岁', gender: '公', location: '南京·鼓楼', vaccinated: true, neutered: true, description: '十橘九胖的典范,能吃能睡,性格佛系', imageColor: '#FFCC02', tags: ['佛系', '中型猫', '易胖'], urgency: 'normal' }
]
const APPLICATION_LIST: ApplicationMeta[] = [
{ id: 1, petName: '豆豆', applicant: '张小明', status: '审核中', date: '2024-03-15', step: '资料审核', progress: 40 },
{ id: 2, petName: '咪咪', applicant: '李芳', status: '家访中', date: '2024-03-12', step: '上门家访', progress: 65 },
{ id: 3, petName: '旺财', applicant: '王强', status: '已通过', date: '2024-03-08', step: '领养完成', progress: 100 },
{ id: 4, petName: '雪球', applicant: '赵静', status: '审核中', date: '2024-03-18', step: '资料审核', progress: 30 },
{ id: 5, petName: '大黄', applicant: '陈伟', status: '待面谈', date: '2024-03-10', step: '视频面谈', progress: 50 },
{ id: 6, petName: '花花', applicant: '刘洋', status: '已通过', date: '2024-03-05', step: '领养完成', progress: 100 },
{ id: 7, petName: '小黑', applicant: '孙琳', status: '已驳回', date: '2024-03-02', step: '审核终止', progress: 0 }
]
const STORY_LIST: StoryMeta[] = [
{ id: 1, title: '从流浪到家人:豆豆的一百天', author: '张小明', petName: '豆豆', date: '2024-03-20', excerpt: '领养豆豆已经100天了,从最初的胆小怕人到现在的活泼开朗,每一天都充满惊喜...', imageColor: '#FFCC80', likes: 328 },
{ id: 2, title: '猫咪的日常:咪咪的窗边时光', author: '李芳', petName: '咪咪', date: '2024-03-18', excerpt: '每天下午三点,咪咪都会准时出现在窗边,等待那束最温暖的阳光...', imageColor: '#F48FB1', likes: 215 },
{ id: 3, title: '旺财的新家:从看门狗到沙发客', author: '王强', petName: '旺财', date: '2024-03-15', excerpt: '谁能想到曾经在外流浪的旺财,现在已经爱上了沙发和暖气片...', imageColor: '#A5D6A7', likes: 476 },
{ id: 4, title: '雪球的成长日记:从奶猫到小公主', author: '赵静', petName: '雪球', date: '2024-03-12', excerpt: '六个月大的雪球从瘦弱的小奶猫长成了毛茸茸的小公主...', imageColor: '#E1BEE7', likes: 189 },
{ id: 5, title: '大黄退役后:导盲犬的退休生活', author: '陈伟', petName: '大黄', date: '2024-03-08', excerpt: '退役导盲犬大黄终于可以过上普通宠物狗的生活了,每天最大的烦恼是选哪个玩具...', imageColor: '#FFE0B2', likes: 612 },
{ id: 6, title: '花花的蓝眼睛:布偶猫的温柔力量', author: '刘洋', petName: '花花', date: '2024-03-05', excerpt: '花花的蓝眼睛像两颗宝石,每次它看着我的时候,一天的疲惫都消失了...', imageColor: '#B3E5FC', likes: 343 }
]
const HEALTH_STATS: HealthStatMeta[] = [
{ month: '1月', checkups: 45, vaccinations: 68, adoptions: 12 },
{ month: '2月', checkups: 52, vaccinations: 71, adoptions: 18 },
{ month: '3月', checkups: 68, vaccinations: 85, adoptions: 25 },
{ month: '4月', checkups: 59, vaccinations: 78, adoptions: 20 },
{ month: '5月', checkups: 73, vaccinations: 92, adoptions: 31 },
{ month: '6月', checkups: 81, vaccinations: 95, adoptions: 28 }
]
const FILTER_TAGS: string[] = ['全部', '狗', '猫', '幼年', '成年', '已绝育', '已疫苗', '大型', '小型', '急寻领养']
function getStatusColor(status: string): string {
if (status === '已通过') {
return COLORS.success
}
if (status === '已驳回') {
return COLORS.danger
}
if (status === '审核中') {
return COLORS.warning
}
return COLORS.primary
}
function getMaxAdoption(): number {
let maxVal: number = 0
for (const item of HEALTH_STATS) {
if (item.adoptions > maxVal) {
maxVal = item.adoptions
}
}
return maxVal
}
function getTotalLikes(): number {
let total: number = 0
for (const s of STORY_LIST) {
total += s.likes
}
return total
}
// ==================== 入口组件 ====================
@Entry
@Component
struct PetAdoptionApp {
@State currentTab: string = 'discover'
@State selectedFilter: string = '全部'
@State showApplyDialog: boolean = false
@State showEditDialog: boolean = false
@State showDeleteDialog: boolean = false
@State targetPet: PetMeta = PET_LIST[0]
@State targetApp: ApplicationMeta = APPLICATION_LIST[0]
build() {
Stack() {
Column() {
// 顶部标题栏
Row() {
Text('萌宠领养')
.fontSize(22)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.white)
Column().layoutWeight(1)
Text('🔍')
.fontSize(20)
}
.width('100%')
.height(56)
.padding({ left: 20, right: 20 })
.linearGradient({ angle: 135, colors: [[COLORS.primary, 0.0], [COLORS.accent, 1.0]] })
// 内容区域
Column() {
if (this.currentTab === 'discover') {
this.discoverContent()
} else if (this.currentTab === 'apply') {
this.applyContent()
} else if (this.currentTab === 'stories') {
this.storiesContent()
} else if (this.currentTab === 'health') {
this.healthContent()
} else {
this.mineContent()
}
}
.layoutWeight(1)
.backgroundColor(COLORS.bg)
// 底部Tab栏
this.bottomTabBar()
}
.width('100%')
.height('100%')
// 弹窗层
if (this.showApplyDialog) {
this.applyDialog()
}
if (this.showEditDialog) {
this.editDialog()
}
if (this.showDeleteDialog) {
this.deleteDialog()
}
}
.width('100%')
.height('100%')
.backgroundColor(COLORS.bg)
}
// ========== 底部Tab栏 ==========
@Builder
bottomTabBar() {
Row() {
ForEach(Object.keys(TAB_CONFIG), (key: string) => {
Column() {
Text(this.getTabIcon(key))
.fontSize(22)
Text(TAB_CONFIG[key])
.fontSize(11)
.margin({ top: 2 })
.fontColor(this.currentTab === key ? COLORS.primary : COLORS.textHint)
}
.layoutWeight(1)
.height(56)
.justifyContent(FlexAlign.Center)
.onClick(() => {
this.currentTab = key
})
})
}
.width('100%')
.height(56)
.backgroundColor(COLORS.white)
.border({ width: { top: 1 }, color: COLORS.border })
}
getTabIcon(key: string): string {
if (key === 'discover') {
return this.currentTab === key ? '🏠' : '🏚'
}
if (key === 'apply') {
return this.currentTab === key ? '📋' : '📑'
}
if (key === 'stories') {
return this.currentTab === key ? '📖' : '📚'
}
if (key === 'health') {
return this.currentTab === key ? '🏥' : '⚕'
}
return this.currentTab === key ? '👤' : '🙆'
}
// ========== 发现Tab ==========
@Builder
discoverContent() {
Scroll() {
Column() {
// 搜索栏
Row() {
Row() {
Text('🔎')
.fontSize(16)
.margin({ right: 8 })
Text('搜索宠物品种、名字...')
.fontSize(14)
.fontColor(COLORS.textHint)
}
.layoutWeight(1)
.height(40)
.padding({ left: 16, right: 16 })
.backgroundColor(COLORS.white)
.borderRadius(20)
.shadow({ radius: 4, color: '#0D000000', offsetY: 1 })
}
.width('92%')
.margin({ top: 12, bottom: 12 })
// 筛选标签云
Flex({ wrap: FlexWrap.Wrap }) {
ForEach(FILTER_TAGS, (tag: string) => {
Text(tag)
.fontSize(12)
.padding({ left: 14, right: 14, top: 6, bottom: 6 })
.margin({ right: 8, bottom: 8 })
.backgroundColor(this.selectedFilter === tag ? COLORS.primary : COLORS.white)
.fontColor(this.selectedFilter === tag ? COLORS.white : COLORS.textSecondary)
.borderRadius(16)
.onClick(() => {
this.selectedFilter = tag
})
})
}
.width('92%')
.margin({ bottom: 12 })
// 急寻领养横幅
Row() {
Column() {
Text('急寻领养')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.white)
Text('2只宠物急需一个温暖的家')
.fontSize(12)
.fontColor('#E0FFFFFF')
.margin({ top: 4 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Text('🐾')
.fontSize(36)
}
.width('92%')
.padding(16)
.borderRadius(16)
.linearGradient({ angle: 135, colors: [[COLORS.danger, 0.0], [COLORS.accent, 1.0]] })
.shadow({ radius: 8, color: '#33EF5350', offsetY: 4 })
.margin({ bottom: 16 })
// 宠物卡片列表
ForEach(PET_LIST, (pet: PetMeta) => {
this.petCard(pet)
})
}
.width('100%')
.padding({ bottom: 20 })
}
.width('100%')
.height('100%')
.scrollBar(BarState.Off)
}
@Builder
petCard(pet: PetMeta) {
Row() {
// 宠物头像色块
Stack() {
Text(pet.species === '狗' ? '🐕' : '🐈')
.fontSize(40)
if (pet.urgency === 'urgent') {
Text('急')
.fontSize(10)
.fontColor(COLORS.white)
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
.backgroundColor(COLORS.danger)
.borderRadius(8)
.position({ x: '60%', y: 0 })
}
}
.width(72)
.height(72)
.borderRadius(16)
.backgroundColor(pet.imageColor)
Column() {
Row() {
Text(pet.name)
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
Text(pet.gender === '公' ? '♂' : '♀')
.fontSize(14)
.fontColor(pet.gender === '公' ? '#42A5F5' : COLORS.accent)
.margin({ left: 6 })
Column().layoutWeight(1)
Text(pet.age)
.fontSize(12)
.fontColor(COLORS.textSecondary)
}
.width('100%')
Text(pet.breed)
.fontSize(12)
.fontColor(COLORS.textSecondary)
.margin({ top: 4 })
Row() {
Text('📍 ' + pet.location)
.fontSize(11)
.fontColor(COLORS.textHint)
}
.margin({ top: 4 })
// 标签
Row() {
ForEach(pet.tags, (tag: string) => {
Text(tag)
.fontSize(10)
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
.backgroundColor(COLORS.primaryLight)
.fontColor(COLORS.primaryDark)
.borderRadius(4)
.margin({ right: 4 })
})
}
.margin({ top: 6 })
}
.layoutWeight(1)
.margin({ left: 12 })
.alignItems(HorizontalAlign.Start)
// 领养按钮
Column() {
Button('领养')
.fontSize(12)
.fontColor(COLORS.white)
.backgroundColor(COLORS.primary)
.borderRadius(14)
.height(28)
.padding({ left: 12, right: 12 })
.onClick(() => {
this.targetPet = pet
this.showApplyDialog = true
})
}
.justifyContent(FlexAlign.Center)
}
.width('92%')
.padding(12)
.backgroundColor(COLORS.white)
.borderRadius(16)
.margin({ bottom: 10 })
.alignItems(VerticalAlign.Center)
.shadow({ radius: 4, color: '#0D000000', offsetY: 2 })
}
// ========== 申请Tab ==========
@Builder
applyContent() {
Scroll() {
Column() {
Text('我的领养申请')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
.margin({ top: 16, bottom: 12, left: 16 })
.alignSelf(ItemAlign.Start)
ForEach(APPLICATION_LIST, (app: ApplicationMeta) => {
this.applicationCard(app)
})
}
.width('100%')
.padding({ bottom: 20 })
}
.width('100%')
.height('100%')
.scrollBar(BarState.Off)
}
@Builder
applicationCard(app: ApplicationMeta) {
Column() {
Row() {
Stack() {
Text('📋')
.fontSize(24)
}
.width(48)
.height(48)
.borderRadius(12)
.backgroundColor(COLORS.primaryLight)
Column() {
Text(app.petName + ' 的领养申请')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
Text(app.date)
.fontSize(11)
.fontColor(COLORS.textHint)
.margin({ top: 2 })
}
.layoutWeight(1)
.margin({ left: 12 })
.alignItems(HorizontalAlign.Start)
Text(app.status)
.fontSize(12)
.fontColor(COLORS.white)
.padding({ left: 10, right: 10, top: 4, bottom: 4 })
.backgroundColor(getStatusColor(app.status))
.borderRadius(10)
}
.width('100%')
.alignItems(VerticalAlign.Center)
// 进度条
Row() {
Text('当前进度:' + app.step)
.fontSize(11)
.fontColor(COLORS.textSecondary)
Column().layoutWeight(1)
Text(app.progress + '%')
.fontSize(11)
.fontColor(COLORS.primary)
.fontWeight(FontWeight.Bold)
}
.width('100%')
.margin({ top: 10 })
// 进度条可视化
Stack({ alignContent: Alignment.Start }) {
// 背景轨道
Column()
.width('100%')
.height(6)
.backgroundColor(COLORS.border)
.borderRadius(3)
// 填充
Column()
.width(app.progress + '%')
.height(6)
.linearGradient({ angle: 0, colors: [[COLORS.primary, 0.0], [COLORS.accent, 1.0]] })
.borderRadius(3)
}
.width('100%')
.height(6)
.margin({ top: 6 })
// 操作按钮
Row() {
Button('编辑资料')
.fontSize(12)
.fontColor(COLORS.primary)
.backgroundColor(COLORS.white)
.border({ width: 1, color: COLORS.primary })
.borderRadius(8)
.height(30)
.padding({ left: 14, right: 14 })
.onClick(() => {
this.targetApp = app
this.showEditDialog = true
})
Column().layoutWeight(1)
Button('撤回申请')
.fontSize(12)
.fontColor(COLORS.danger)
.backgroundColor(COLORS.white)
.border({ width: 1, color: COLORS.danger })
.borderRadius(8)
.height(30)
.padding({ left: 14, right: 14 })
.onClick(() => {
this.targetApp = app
this.showDeleteDialog = true
})
}
.width('100%')
.margin({ top: 12 })
}
.width('92%')
.padding(14)
.backgroundColor(COLORS.white)
.borderRadius(16)
.margin({ bottom: 10 })
.shadow({ radius: 4, color: '#0D000000', offsetY: 2 })
}
// ========== 故事Tab ==========
@Builder
storiesContent() {
Scroll() {
Column() {
Row() {
Text('领养故事')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
Column().layoutWeight(1)
Text('❤ ' + getTotalLikes())
.fontSize(12)
.fontColor(COLORS.accent)
}
.width('92%')
.margin({ top: 16, bottom: 12 })
ForEach(STORY_LIST, (story: StoryMeta) => {
this.storyCard(story)
})
}
.width('100%')
.padding({ bottom: 20 })
}
.width('100%')
.height('100%')
.scrollBar(BarState.Off)
}
@Builder
storyCard(story: StoryMeta) {
Column() {
// 封面色块
Stack() {
Column() {
Text(story.petName)
.fontSize(28)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFFCC')
Text('🐾')
.fontSize(32)
.margin({ top: 4 })
}
}
.width('100%')
.height(100)
.backgroundColor(story.imageColor)
.borderRadius({ topLeft: 16, topRight: 16 })
Column() {
Text(story.title)
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
.maxLines(2)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Text(story.excerpt)
.fontSize(12)
.fontColor(COLORS.textSecondary)
.margin({ top: 6 })
.maxLines(2)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Row() {
Text(story.author)
.fontSize(11)
.fontColor(COLORS.textHint)
Column().layoutWeight(1)
Text('❤ ' + story.likes)
.fontSize(11)
.fontColor(COLORS.accent)
}
.width('100%')
.margin({ top: 8 })
}
.padding(12)
.alignItems(HorizontalAlign.Start)
}
.width('92%')
.backgroundColor(COLORS.white)
.borderRadius(16)
.margin({ bottom: 12 })
.shadow({ radius: 4, color: '#0D000000', offsetY: 2 })
}
// ========== 健康Tab ==========
@Builder
healthContent() {
Scroll() {
Column() {
Text('健康统计')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
.margin({ top: 16, left: 16, bottom: 12 })
.alignSelf(ItemAlign.Start)
// 统计概览卡片
Row() {
this.statBox('体检总数', '378', COLORS.primary, '#')
this.statBox('疫苗接种', '489', COLORS.accent, '💉')
this.statBox('成功领养', '134', COLORS.success, '🏠')
}
.width('92%')
.margin({ bottom: 16 })
// 柱状图
Column() {
Text('月度领养趋势')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
.margin({ bottom: 16 })
// 图表区域
Row() {
ForEach(HEALTH_STATS, (stat: HealthStatMeta) => {
Column() {
Stack({ alignContent: Alignment.Bottom }) {
Text(stat.adoptions.toString())
.fontSize(10)
.fontColor(COLORS.textSecondary)
.margin({ bottom: 4 })
}
.width('100%')
.height(120 * (stat.adoptions / getMaxAdoption()))
.linearGradient({ angle: 180, colors: [[COLORS.primary, 0.0], [COLORS.accent, 1.0]] })
.borderRadius({ topLeft: 6, topRight: 6 })
Text(stat.month)
.fontSize(10)
.fontColor(COLORS.textHint)
.margin({ top: 4 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
})
}
.width('100%')
.height(160)
.alignItems(VerticalAlign.Bottom)
}
.width('92%')
.padding(16)
.backgroundColor(COLORS.white)
.borderRadius(16)
.margin({ bottom: 12 })
.shadow({ radius: 4, color: '#0D000000', offsetY: 2 })
// 体检明细列表
Text('体检明细')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
.margin({ left: 16, bottom: 8 })
.alignSelf(ItemAlign.Start)
ForEach(HEALTH_STATS, (stat: HealthStatMeta) => {
Row() {
Text(stat.month)
.fontSize(13)
.fontColor(COLORS.textPrimary)
.fontWeight(FontWeight.Bold)
Column().layoutWeight(1)
Text('体检 ' + stat.checkups + ' | 疫苗 ' + stat.vaccinations + ' | 领养 ' + stat.adoptions)
.fontSize(11)
.fontColor(COLORS.textSecondary)
}
.width('92%')
.padding({ left: 14, right: 14, top: 10, bottom: 10 })
.backgroundColor(COLORS.white)
.borderRadius(10)
.margin({ bottom: 6 })
.alignItems(VerticalAlign.Center)
})
}
.width('100%')
.padding({ bottom: 20 })
}
.width('100%')
.height('100%')
.scrollBar(BarState.Off)
}
@Builder
statBox(title: string, value: string, color: string, icon: string) {
Column() {
Text(icon)
.fontSize(24)
Text(value)
.fontSize(24)
.fontWeight(FontWeight.Bold)
.fontColor(color)
.margin({ top: 4 })
Text(title)
.fontSize(11)
.fontColor(COLORS.textSecondary)
.margin({ top: 2 })
}
.layoutWeight(1)
.padding({ top: 16, bottom: 16 })
.backgroundColor(COLORS.white)
.borderRadius(12)
.alignItems(HorizontalAlign.Center)
.margin({ right: 8 })
.shadow({ radius: 4, color: '#0D000000', offsetY: 2 })
}
// ========== 我的Tab ==========
@Builder
mineContent() {
Scroll() {
Column() {
// 用户信息头部
Row() {
Stack() {
Text('👤')
.fontSize(36)
}
.width(64)
.height(64)
.borderRadius(32)
.backgroundColor(COLORS.primaryLight)
Column() {
Text('爱宠达人')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
Text('已领养 2 只 · 关注 8 只')
.fontSize(12)
.fontColor(COLORS.textSecondary)
.margin({ top: 4 })
}
.margin({ left: 16 })
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
}
.width('92%')
.padding(16)
.backgroundColor(COLORS.white)
.borderRadius(16)
.margin({ top: 16, bottom: 16 })
.alignItems(VerticalAlign.Center)
.shadow({ radius: 4, color: '#0D000000', offsetY: 2 })
// 功能列表
ForEach(['我的宠物', '领养记录', '收藏关注', '消息通知', '帮助中心', '关于我们'], (item: string) => {
Row() {
Text(item)
.fontSize(14)
.fontColor(COLORS.textPrimary)
Column().layoutWeight(1)
Text('›')
.fontSize(20)
.fontColor(COLORS.textHint)
}
.width('92%')
.padding({ left: 16, right: 16, top: 14, bottom: 14 })
.backgroundColor(COLORS.white)
.borderRadius(12)
.margin({ bottom: 8 })
.alignItems(VerticalAlign.Center)
})
}
.width('100%')
.padding({ bottom: 20 })
}
.width('100%')
.height('100%')
.scrollBar(BarState.Off)
}
// ==================== 弹窗 ====================
@Builder
applyDialog() {
Column() {
Column() {
Text('申请领养')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
// 宠物信息
Row() {
Stack() {
Text(this.targetPet.species === '狗' ? '🐕' : '🐈')
.fontSize(32)
}
.width(56)
.height(56)
.borderRadius(12)
.backgroundColor(this.targetPet.imageColor)
Column() {
Text(this.targetPet.name)
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
Text(this.targetPet.breed + ' · ' + this.targetPet.age + ' · ' + this.targetPet.gender)
.fontSize(12)
.fontColor(COLORS.textSecondary)
.margin({ top: 4 })
}
.margin({ left: 12 })
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
}
.width('100%')
.margin({ top: 16, bottom: 16 })
.alignItems(VerticalAlign.Center)
Text('领养说明')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
.alignSelf(ItemAlign.Start)
.margin({ bottom: 8 })
Text('感谢您选择领养代替购买!请确认您有足够的时间、空间和经济能力来照顾这只宠物。领养是一份长期承诺,我们希望每一只宠物都能找到永远的家。')
.fontSize(12)
.fontColor(COLORS.textSecondary)
.lineHeight(20)
// 按钮组
Row() {
Button('再想想')
.fontSize(14)
.fontColor(COLORS.textSecondary)
.backgroundColor(COLORS.white)
.border({ width: 1, color: COLORS.border })
.borderRadius(10)
.height(40)
.layoutWeight(1)
.onClick(() => {
this.showApplyDialog = false
})
Button('确认申请')
.fontSize(14)
.fontColor(COLORS.white)
.linearGradient({ angle: 135, colors: [[COLORS.primary, 0.0], [COLORS.accent, 1.0]] })
.borderRadius(10)
.height(40)
.layoutWeight(1)
.margin({ left: 12 })
.onClick(() => {
this.showApplyDialog = false
})
}
.width('100%')
.margin({ top: 20 })
}
.width('80%')
.padding(20)
.backgroundColor(COLORS.white)
.borderRadius(20)
.onClick(() => {})
}
.width('100%')
.height('100%')
.backgroundColor('rgba(0,0,0,0.5)')
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
}
@Builder
editDialog() {
Column() {
Column() {
Text('编辑申请资料')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
.margin({ bottom: 16 })
// 申请信息
Row() {
Text('宠物名称')
.fontSize(13)
.fontColor(COLORS.textSecondary)
Column().layoutWeight(1)
Text(this.targetApp.petName)
.fontSize(13)
.fontColor(COLORS.textPrimary)
.fontWeight(FontWeight.Bold)
}
.width('100%')
.padding({ top: 10, bottom: 10 })
.border({ width: { bottom: 1 }, color: COLORS.border })
Row() {
Text('当前状态')
.fontSize(13)
.fontColor(COLORS.textSecondary)
Column().layoutWeight(1)
Text(this.targetApp.status)
.fontSize(12)
.fontColor(getStatusColor(this.targetApp.status))
.padding({ left: 8, right: 8, top: 2, bottom: 2 })
.backgroundColor(COLORS.primaryLight)
.borderRadius(8)
}
.width('100%')
.padding({ top: 10, bottom: 10 })
.border({ width: { bottom: 1 }, color: COLORS.border })
Row() {
Text('申请进度')
.fontSize(13)
.fontColor(COLORS.textSecondary)
Column().layoutWeight(1)
Text(this.targetApp.progress + '%')
.fontSize(13)
.fontColor(COLORS.primary)
.fontWeight(FontWeight.Bold)
}
.width('100%')
.padding({ top: 10, bottom: 10 })
// 按钮组
Row() {
Button('取消')
.fontSize(14)
.fontColor(COLORS.textSecondary)
.backgroundColor(COLORS.white)
.border({ width: 1, color: COLORS.border })
.borderRadius(10)
.height(40)
.layoutWeight(1)
.onClick(() => {
this.showEditDialog = false
})
Button('保存')
.fontSize(14)
.fontColor(COLORS.white)
.backgroundColor(COLORS.primary)
.borderRadius(10)
.height(40)
.layoutWeight(1)
.margin({ left: 12 })
.onClick(() => {
this.showEditDialog = false
})
}
.width('100%')
.margin({ top: 20 })
}
.width('80%')
.padding(20)
.backgroundColor(COLORS.white)
.borderRadius(20)
.onClick(() => {})
}
.width('100%')
.height('100%')
.backgroundColor('rgba(0,0,0,0.5)')
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
}
@Builder
deleteDialog() {
Column() {
Column() {
Stack() {
Text('⚠')
.fontSize(40)
.fontColor(COLORS.danger)
}
.width(64)
.height(64)
.borderRadius(32)
.backgroundColor('#FFEBEE')
.margin({ bottom: 16 })
Text('撤回申请确认')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
Text('确定要撤回 ' + this.targetApp.petName + ' 的领养申请吗?撤回后需要重新提交,已审核的进度将无法恢复。')
.fontSize(13)
.fontColor(COLORS.textSecondary)
.textAlign(TextAlign.Center)
.lineHeight(20)
.margin({ top: 12 })
Row() {
Button('取消')
.fontSize(14)
.fontColor(COLORS.textSecondary)
.backgroundColor(COLORS.white)
.border({ width: 1, color: COLORS.border })
.borderRadius(10)
.height(40)
.layoutWeight(1)
.onClick(() => {
this.showDeleteDialog = false
})
Button('确认撤回')
.fontSize(14)
.fontColor(COLORS.white)
.backgroundColor(COLORS.danger)
.borderRadius(10)
.height(40)
.layoutWeight(1)
.margin({ left: 12 })
.onClick(() => {
this.showDeleteDialog = false
})
}
.width('100%')
.margin({ top: 20 })
}
.width('76%')
.padding(24)
.backgroundColor(COLORS.white)
.borderRadius(20)
.onClick(() => {})
}
.width('100%')
.height('100%')
.backgroundColor('rgba(0,0,0,0.5)')
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
}
}
十七、设计思想与用户体验
这款萌宠领养应用在设计层面展现了"以用户为中心"和"以情感为纽带"的双重设计思想。从用户体验的角度来看,应用的每一个设计决策都服务于让用户更容易、更愿意完成领养这个核心目标。
在信息架构上,应用采用了清晰的分层结构。发现模块作为默认首页,将最重要的信息(待领养宠物)放在用户最容易触达的位置。筛选标签云的设计让信息过滤变得直观和高效,用户无需进入专门的筛选页面,只需点击标签即可实时看到筛选结果。宠物卡片的信息层次也经过精心安排:头像和名字是最突出的元素,品种、年龄、位置等次要信息依次排列,标签则提供了快速分类的锚点。这种信息优先级的设计符合用户浏览时的视觉动线——先被可爱的宠物头像吸引,然后阅读名字和关键信息,最后通过标签确认这只宠物是否符合自己的需求。
在交互设计上,应用遵循了"即时反馈"和"渐进披露"的原则。点击筛选标签后,标签颜色立即变化,给用户即时的操作反馈。点击领养按钮后,弹窗平滑出现,而不是跳转到新页面,保持了上下文的连续性。申请模块中的进度条设计,让用户能够随时掌握领养流程的进展,缓解了等待过程中的焦虑感。故事模块的存在则是一种巧妙的情感设计——它不仅为已领养者提供了分享的平台,更重要的是通过真实的故事来激发潜在领养者的情感共鸣,降低领养的心理门槛。
在视觉设计上,温暖橙与温柔粉的色彩搭配不仅美观,更在潜意识层面传递了关爱和温馨的情感暗示。圆角卡片、柔和阴影、渐变背景等视觉元素的运用,营造出一种友好、亲切的氛围,与宠物领养这个充满善意的主题高度契合。即使是"急寻领养"这样的紧急信息,也使用了红色到粉色的渐变,而不是纯红色,在传达紧迫感的同时保持了整体色调的和谐。
十八、可改进方向与未来展望
尽管这款萌宠领养应用已经具备了完整的功能和良好的用户体验,但在实际的产品迭代中,仍有许多可以改进和扩展的方向。

从功能完整性的角度来看,当前的搜索栏仅是一个视觉占位符,没有实现真正的搜索功能。未来的版本可以接入全文搜索引擎,支持按宠物名字、品种、位置、标签等多维度进行搜索。筛选功能目前也只支持单选,可以扩展为多选组合筛选,例如同时选择"狗"+“已疫苗”+“幼年”,让用户能够更精确地定位目标宠物。地图功能的引入也是一个有价值的方向,可以在发现模块中集成地图视图,让用户直观地看到附近有哪些待领养宠物。
从技术实现的角度来看,当前应用的所有数据都是硬编码的模拟数据。在实际部署中,需要接入后端API来获取真实数据。ArkTS提供了@State和@Watch等装饰器来管理异步数据流,可以与网络请求库配合使用。图片资源目前使用纯色块和Emoji替代,生产环境中应该接入真实的宠物照片,并考虑图片懒加载和缓存策略,以优化性能和流量消耗。状态管理在模块复杂度增加后,可以考虑引入AppStorage或LocalStorage等全局状态管理方案,避免深层组件间的事件传递。
更多推荐
所有评论(0)