HarmonyOS7宠物健康档案页实战:档案切换与健康信息展示
文章目录
- 前言
- 先把 Pet 模型看清楚
- 页面真正的状态只有三块
- 列表卡片承担的是“快速判断”
- 点击查看详情时发生了什么
- 这个页面目前还只是“查看档案”
- 完整代码
- 最后总结
前言
宠物健康档案这种页面,看起来像是几张卡片加一个详情弹窗,但真正要写顺,重点不在卡片样式,而在数据边界。列表里展示的是概览信息,点进去以后展示的是同一只宠物的完整档案;如果这里把状态拆乱,后面加疫苗提醒、体重曲线、就诊记录都会很难接。
这个示例的实现比较克制:只有一个Pet模型、一个pets列表、一个selectedPet和一个showDetail。它没有做新增、编辑、远程同步这些扩展能力,但正好适合拿来练 ArkUI 里的“列表项选择 + 弹层详情”这条交互链路。
先把 Pet 模型看清楚
Pet不是单纯用来渲染名字和头像的,它把健康档案里最常见的字段放在了一起:品种、年龄、体重、下次疫苗时间、最近体检时间和备注。
interfacePet{id:numbername:stringspecies:stringbreed:stringage:numberweight:numberavatar:stringnextVaccine:stringlastCheckup:stringnotes:string}我比较喜欢这个模型的一点是,它没有为了页面展示再拆出一堆临时字段。列表卡片和详情弹层都直接消费Pet,只是展示的信息密度不同。这样做的好处是数据来源很清楚:页面上看到的每个值,基本都能回到同一个对象里。
需要注意的是,当前avatar用的是字符,不是真实图片资源。教学示例里这样写很方便,但正式项目里通常会换成图片地址或资源引用,否则后续做头像上传、缓存、裁剪时会受限制。
页面真正的状态只有三块
这页没有复杂的计算属性,状态关系也很直白:pets是主数据源,selectedPet表示当前打开详情的是哪一只宠物,showDetail控制弹层是否显示。
@Statepets:Pet[]=[{id:1,name:'豆豆',species:'猫',breed:'英短',age:3,weight:4.2,avatar:'🐱',nextVaccine:'2026-09-01',lastCheckup:'2026-03-15',notes:'喜欢吃鸡肉罐头'},]@StateselectedPet:Pet|null=null@StateshowDetail:boolean=false这里有个小细节值得记住:详情弹层没有只保存selectedPetId,而是直接保存了完整的Pet对象。对静态示例来说没问题,代码也更短;但如果后面允许编辑宠物资料,我会更倾向于保存selectedPetId,详情展示时再从pets里找对象。这样能避免列表数据更新后,弹层里还拿着旧对象引用的问题。
列表卡片承担的是“快速判断”
ForEach(this.pets, ...)把每只宠物渲染成一张卡片。卡片上没有把所有字段都塞出来,只展示了名字、品种、年龄、体重、下次疫苗时间和最近体检时间。
ForEach(this.pets,(pet:Pet)=>{Column(){Row(){Text(pet.avatar).fontSize(48).margin({right:16})Column(){Row(){Text(pet.name).fontSize(20).fontWeight(FontWeight.Bold).fontColor('#333')Text(pet.breed).fontSize(13).fontColor('#FFF')}Text(`${pet.species}·${pet.age}岁 ·${pet.weight}kg`)}}}})这种取舍挺实际。宠物档案页的列表入口,用户通常先关心“是哪只宠物”和“有没有临近健康事项”。所以nextVaccine被放在卡片显眼位置,notes则留到详情里看,不然列表会很挤。
卡片里的体重、年龄、最近体检做成三列指标,也是在降低阅读成本。这里没有额外抽HealthIndicator组件,是因为示例规模不大;如果真实项目里还有心率、驱虫、绝育状态这些字段,就应该把这类指标行抽出去,否则页面文件会越写越长。
点击查看详情时发生了什么
这页最关键的交互其实只有一段:点击“查看详情”后,先把当前pet放进selectedPet,再把showDetail改成true。
Text('查看详情').fontSize(12).fontColor('#007AFF').onClick(()=>{this.selectedPet=petthis.showDetail=true})这个顺序不要反过来。虽然在这个案例里反过来也大概率能显示出来,但从状态语义上讲,应该先准备好弹层要用的数据,再打开弹层。尤其是后面如果弹层里加动画、异步加载、表单初始化,这个顺序会更重要。
弹层渲染也做了一层保护:只有showDetail为真,并且selectedPet不是null时才展示详情。
if(this.showDetail&&this.selectedPet!==null){Column(){Text(this.selectedPet!.name+'的档案')Text(this.selectedPet!.avatar).fontSize(60)}}selectedPet!是 ArkTS 里常见的非空断言。这里前面已经用selectedPet !== null判断过,所以写起来没问题。但它也提醒了一件事:不要在没有判空的地方随手加!,那只是把类型提示压下去,不是真的处理了空值。
这个页面目前还只是“查看档案”
顶部有一个+,但当前源码里它只是一个文本,没有绑定新增逻辑。健康小贴士也是固定文案,不会根据宠物种类或疫苗日期动态变化。把这些限制说清楚,比硬把它讲成完整宠物管理系统更诚实。
如果要继续扩展,我会先补两个方向:一个是新增和编辑宠物资料,让pets真的变成可维护的数据;另一个是基于nextVaccine做提醒状态,比如距离疫苗日期少于 30 天时高亮。到那一步,这个页面就不只是静态档案展示,而开始有健康管理的味道了。
完整代码
下面保留案例完整代码,方便你直接对照学习、复制和重构。
import{router}from'@kit.ArkUI'interfacePet{id:numbername:stringspecies:stringbreed:stringage:numberweight:numberavatar:stringnextVaccine:stringlastCheckup:stringnotes:string}@Entry@Componentstruct PetHealthProfile{@Statepets:Pet[]=[{id:1,name:'豆豆',species:'猫',breed:'英短',age:3,weight:4.2,avatar:'🐱',nextVaccine:'2026-09-01',lastCheckup:'2026-03-15',notes:'喜欢吃鸡肉罐头'},{id:2,name:'旺财',species:'狗',breed:'柴犬',age:2,weight:10.5,avatar:'🐶',nextVaccine:'2026-07-20',lastCheckup:'2026-05-10',notes:'每天需要运动1小时'},{id:3,name:'小黄',species:'猫',breed:'金渐层',age:1,weight:3.8,avatar:'😺',nextVaccine:'2026-08-15',lastCheckup:'2026-04-20',notes:'有点挑食'},]@StateselectedPet:Pet|null=null@StateshowDetail:boolean=falsebuild(){Stack(){Column(){Row(){Image($r('app.media.ic_back')).width(24).height(24).onClick(()=>router.back())Text('宠物健康档案').fontSize(18).fontWeight(FontWeight.Bold).layoutWeight(1).textAlign(TextAlign.Center)Text('+').fontSize(22).fontColor('#FF6B9D')}.width('100%').padding({left:16,right:16,top:12,bottom:16})// Pet cardsScroll(){Column({space:16}){ForEach(this.pets,(pet:Pet)=>{Column(){Row(){Text(pet.avatar).fontSize(48).margin({right:16})Column(){Row(){Text(pet.name).fontSize(20).fontWeight(FontWeight.Bold).fontColor('#333')Text(pet.breed).fontSize(13).fontColor('#FFF').backgroundColor('#FF6B9D').borderRadius(10).padding({left:8,right:8,top:3,bottom:3}).margin({left:8})}Text(\`\${pet.species}· \${pet.age}岁 · \${pet.weight}kg\`).fontSize(14).fontColor('#666').margin({top:4})Row(){Text('💉').fontSize(12)Text(\` 下次疫苗 \${pet.nextVaccine}\`).fontSize(12).fontColor('#FF9500')}.margin({top:6})}.layoutWeight(1).alignItems(HorizontalAlign.Start)Text('查看详情').fontSize(12).fontColor('#007AFF').onClick(()=>{this.selectedPet=petthis.showDetail=true})}.width('100%').margin({bottom:12})// Health indicatorsRow(){Column(){Text('体重').fontSize(11).fontColor('#888')Text(\`\${pet.weight}kg\`).fontSize(15).fontWeight(FontWeight.Bold).fontColor('#333')}.layoutWeight(1).alignItems(HorizontalAlign.Center)Column(){Text('年龄').fontSize(11).fontColor('#888')Text(\`\${pet.age}岁\`).fontSize(15).fontWeight(FontWeight.Bold).fontColor('#333')}.layoutWeight(1).alignItems(HorizontalAlign.Center)Column(){Text('最近体检').fontSize(11).fontColor('#888')Text(pet.lastCheckup).fontSize(13).fontWeight(FontWeight.Bold).fontColor('#2ED573')}.layoutWeight(1).alignItems(HorizontalAlign.Center)}.width('100%').backgroundColor('#F8F9FA').borderRadius(10).padding(12)}.width('100%').backgroundColor('#FFF').borderRadius(16).padding(16).shadow({radius:6,color:'#0000001A',offsetY:3})})// TipsColumn(){Text('🏥 健康小贴士').fontSize(14).fontWeight(FontWeight.Bold).fontColor('#333').margin({bottom:8})Text('• 定期接种疫苗,预防常见疾病').fontSize(13).fontColor('#666').margin({bottom:4})Text('• 每年至少进行一次全面体检').fontSize(13).fontColor('#666').margin({bottom:4})Text('• 保持适量运动,控制体重').fontSize(13).fontColor('#666')}.width('100%').backgroundColor('#FFF9E6').borderRadius(12).padding(16).borderWidth(1).borderColor('#FFD700')}.padding({left:16,right:16,bottom:20})}.layoutWeight(1)}.width('100%').height('100%').backgroundColor('#FFF5F8')// Detail modalif(this.showDetail&&this.selectedPet!==null){Column(){Column(){Row(){Text(this.selectedPet!.name+'的档案').fontSize(17).fontWeight(FontWeight.Bold).layoutWeight(1)Text('✕').fontSize(18).fontColor('#888').onClick(()=>this.showDetail=false)}.width('100%').margin({bottom:16})Text(this.selectedPet!.avatar).fontSize(60).margin({bottom:12})Column({space:8}){Row(){Text('品种:').fontSize(14).fontColor('#888').width(80);Text(this.selectedPet!.breed).fontSize(14).fontColor('#333')}Row(){Text('年龄:').fontSize(14).fontColor('#888').width(80);Text(\`\${this.selectedPet!.age}岁\`).fontSize(14).fontColor('#333')}Row(){Text('体重:').fontSize(14).fontColor('#888').width(80);Text(\`\${this.selectedPet!.weight}kg\`).fontSize(14).fontColor('#333')}Row(){Text('疫苗:').fontSize(14).fontColor('#888').width(80);Text(this.selectedPet!.nextVaccine).fontSize(14).fontColor('#FF9500')}Row(){Text('体检:').fontSize(14).fontColor('#888').width(80);Text(this.selectedPet!.lastCheckup).fontSize(14).fontColor('#2ED573')}Row(){Text('备注:').fontSize(14).fontColor('#888').width(80);Text(this.selectedPet!.notes).fontSize(14).fontColor('#666').layoutWeight(1)}}.width('100%').alignItems(HorizontalAlign.Start)}.backgroundColor('#FFF').borderRadius(20).padding(24).width('85%').alignItems(HorizontalAlign.Center)}.width('100%').height('100%').backgroundColor('#00000060').justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center)}}}}最后总结
宠物健康档案页 这个案例并不靠复杂技巧取胜,它更像一个结构扎实、逻辑完整的业务页面样板。把这篇文章里的状态设计、函数组织和页面分层吃透之后,你再去写同类型功能,速度会明显快很多。