文章目录
- 前言
- 药品模型的关键是 `times` 和 `taken`
- 今日总剂次和已服剂次都是派生值
- 标记服药只允许从未服用变成已服用
- 剂次按钮承担了状态反馈
- 这个案例适合继续补哪些能力
- 完整代码
- 最后总结
前言
用药提醒页最怕写成“看起来像提醒,实际上只是列表”。真正有价值的部分,是用户标记某一剂已经服用后,今日进度、药品剩余量、剂次状态都能一起更新。这个案例没有接系统通知,也没有做新增药品,但它把剂次标记和进度统计这条主线写出来了。
当前页面包含三块:顶部今日服药进度,下方固定的“下次服药提醒”,以及药品列表。右上角+没有绑定新增逻辑;showAlert状态也声明了,但没有参与任何渲染或交互。
药品模型的关键是times和taken
每个药品用Medication表示。字段不少,但真正影响今日进度的是times和taken。
interfaceMedication{id:numbername:stringdosage:stringfrequency:stringtimes:string[]remaining:numbertotal:numberemoji:stringtaken:boolean[]}times表示今天有哪些服药时间点,taken表示这些时间点是否已经完成。两者靠下标对应,比如阿莫西林有三个时间点08:00、14:00、20:00,taken: [true, true, false]就表示前两次已服用,晚上那次还没服。
这种结构适合做演示,因为它直接表达“一种药有多个剂次”。真实项目里我不会只用下标绑定,至少会把每次服药拆成对象,带上时间、状态、实际服用时间和提醒 ID,不然后面补签、延迟服用、漏服统计都会比较难做。
今日总剂次和已服剂次都是派生值
顶部进度没有单独保存百分比,而是从meds实时计算。
gettodayDoses():number{returnthis.meds.reduce((s,m)=>s+m.times.length,0)}gettakenDoses():number{returnthis.meds.reduce((s,m)=>s+m.taken.filter(t=>t).length,0)}todayDoses统计所有药品今天一共有多少个服药槽位,takenDoses统计这些槽位里已经完成了多少个。顶部文案、百分比和Progress都依赖这两个 getter。
Text(`今日服药进度:${this.takenDoses}/${this.todayDoses}`)Text(`${Math.round(this.takenDoses/this.todayDoses*100)}%`)Progress({value:this.takenDoses,total:this.todayDoses})这里同样有一个边界:如果没有任何药品,todayDoses会是 0,百分比计算就不安全。当前示例写死了 4 个药品,所以不会出问题;正式页面要补空状态。
标记服药只允许从未服用变成已服用
每个时间槽都绑定了点击事件,但只有未服用时才会调用takeMed()。
.onClick(()=>{if(!med.taken[i])this.takeMed(med.id,i)})takeMed()做了两件事:把对应剂次的taken[timeIdx]改成true,同时把剩余药量减一。
takeMed(medId:number,timeIdx:number){this.meds=this.meds.map(m=>{if(m.id!==medId)returnmconsttaken=m.taken.slice()taken[timeIdx]=trueconstremaining=Math.max(0,m.remaining-1)m.taken=taken;m.remaining=remaining;returnm})}Math.max(0, m.remaining - 1)保证剩余药量不会减成负数。这个细节挺必要,尤其是用药这种场景,数字显示错了会让用户非常不信任。
不过当前逻辑不支持取消已服用,也没有防止重复扣减之外的更多校验。由于 UI 层已经禁止已服用槽位再次调用takeMed(),正常点击不会重复减库存;但如果后续别处也调用这个函数,最好在函数内部再判断一次taken[timeIdx]是否已经为true。
剂次按钮承担了状态反馈
药品卡片底部的每个时间点会根据med.taken[i]改变颜色和文字。
Text(med.taken[i]?'✓':'')Text(time).fontColor(med.taken[i]?'#FFF':'#555').backgroundColor(med.taken[i]?'#4CAF50':'#F0F0F0')这个处理让状态反馈很直接:已服用的剂次变成绿色并显示对勾,未服用的保持灰色。列表里的剩余药量进度条也会跟着remaining变化。
“下次服药提醒”这一块目前是静态文案:维生素C 09:00、7分钟后都没有从meds里计算。正式做提醒功能时,应该根据当前时间和未完成剂次动态找出最近的一次,并接入系统通知或后台提醒能力。
这个案例适合继续补哪些能力
如果要把它往真实用药助手扩展,我会先补三件事:新增/编辑药品、动态下一次提醒、服药记录持久化。新增药品需要处理剂量、频次和时间列表;动态提醒需要和系统时间、通知权限打交道;持久化则决定用户关闭应用后记录还在不在。
当前代码最值得保留的是状态来源清楚:meds是主数据,今日进度和剂次完成数都从它推导出来,点击剂次只改对应药品的taken和remaining。这条线稳住以后,再加提醒服务和历史记录会容易很多。
完整代码
下面保留案例完整代码,方便你直接对照学习、复制和重构。
import{router}from'@kit.ArkUI'interfaceMedication{id:numbername:stringdosage:stringfrequency:stringtimes:string[]remaining:numbertotal:numberemoji:stringtaken:boolean[]}@Entry@Componentstruct MedicationReminderBoard{@Statemeds:Medication[]=[{id:1,name:'阿莫西林',dosage:'500mg',frequency:'每天3次',times:['08:00','14:00','20:00'],remaining:24,total:30,emoji:'💊',taken:[true,true,false]},{id:2,name:'维生素C',dosage:'200mg',frequency:'每天1次',times:['09:00'],remaining:18,total:30,emoji:'🍊',taken:[false]},{id:3,name:'布洛芬',dosage:'400mg',frequency:'需要时',times:['随需'],remaining:10,total:12,emoji:'🔴',taken:[false]},{id:4,name:'叶酸',dosage:'0.4mg',frequency:'每天1次',times:['07:30'],remaining:60,total:90,emoji:'🌿',taken:[true]},]@StateshowAlert:string=''takeMed(medId:number,timeIdx:number){this.meds=this.meds.map(m=>{if(m.id!==medId)returnmconsttaken=m.taken.slice()taken[timeIdx]=trueconstremaining=Math.max(0,m.remaining-1)m.taken=taken;m.remaining=remaining;returnm})}gettodayDoses():number{returnthis.meds.reduce((s,m)=>s+m.times.length,0)}gettakenDoses():number{returnthis.meds.reduce((s,m)=>s+m.taken.filter(t=>t).length,0)}build(){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('#4CAF50')}.width('100%').padding({left:16,right:16,top:12,bottom:12})// Progress summaryColumn(){Row(){Text(\`今日服药进度:\${this.takenDoses}/\${this.todayDoses}\`).fontSize(15).fontColor('#FFF').layoutWeight(1)Text(\`\${Math.round(this.takenDoses/this.todayDoses*100)}%\`).fontSize(18).fontWeight(FontWeight.Bold).fontColor('#FFF')}.width('100%').margin({bottom:8})Progress({value:this.takenDoses,total:this.todayDoses}).color('#FFF').backgroundColor('#FFFFFF40').height(8).borderRadius(4)}.width('calc(100% - 32vp)').backgroundColor('#4CAF50').borderRadius(16).padding(20).margin({bottom:16})// Next dose reminderRow(){Text('⏰').fontSize(20)Column(){Text('下次服药提醒').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#333')Text('维生素C 09:00').fontSize(13).fontColor('#4CAF50')}.layoutWeight(1).alignItems(HorizontalAlign.Start).margin({left:12})Text('7分钟后').fontSize(13).fontColor('#FF9500').fontWeight(FontWeight.Bold)}.width('calc(100% - 32vp)').backgroundColor('#FFF').borderRadius(12).padding(14).margin({bottom:12}).border({width:1,color:'#E8F5E9'})// MedicationsList({space:10}){ForEach(this.meds,(med:Medication)=>{ListItem(){Column(){Row(){Text(med.emoji).fontSize(36).margin({right:12})Column(){Row(){Text(med.name).fontSize(16).fontWeight(FontWeight.Bold).fontColor('#333')Text(med.dosage).fontSize(12).fontColor('#888').margin({left:8})}Text(med.frequency).fontSize(13).fontColor('#666').margin({top:4})Row(){Progress({value:med.remaining,total:med.total}).color('#4CAF50').backgroundColor('#EEE').height(4).width(80).borderRadius(2)Text(\` 剩余 \${med.remaining}/\${med.total}\`).fontSize(11).fontColor('#AAA').margin({left:4})}.margin({top:6})}.layoutWeight(1).alignItems(HorizontalAlign.Start)}.width('100%').margin({bottom:10})// Dose slotsRow(){ForEach(med.times,(time:string,i:number)=>{Row(){Text(med.taken[i]?'✓':'').fontSize(12).fontColor('#FFF')Text(time).fontSize(12).fontColor(med.taken[i]?'#FFF':'#555').margin({left:2})}.backgroundColor(med.taken[i]?'#4CAF50':'#F0F0F0').borderRadius(16).padding({left:10,right:10,top:5,bottom:5}).margin({right:8}).onClick(()=>{if(!med.taken[i])this.takeMed(med.id,i)})})}}.width('100%').backgroundColor('#FFF').borderRadius(14).padding(14).shadow({radius:4,color:'#0000001A',offsetY:2})}})}.layoutWeight(1).padding({left:16,right:16})}.width('100%').height('100%').backgroundColor('#F1F8F4')}}最后总结
用药提醒面板 这个案例并不靠复杂技巧取胜,它更像一个结构扎实、逻辑完整的业务页面样板。把这篇文章里的状态设计、函数组织和页面分层吃透之后,你再去写同类型功能,速度会明显快很多。