HarmonyOS7习惯打卡面板实战:每日勾选与连续天数反馈

HarmonyOS7习惯打卡面板实战:每日勾选与连续天数反馈
文章目录前言Habit 模型里藏着两类信息今日完成数不需要单独保存一周视图是二维关系不只是装饰toggleToday() 同时处理打卡和取消打卡顶部进度区只读状态不直接改状态这页还没做到完整习惯管理完整代码最后总结前言习惯打卡页看起来很轻但它其实很考验状态设计。用户点的是一个小圆点页面要同步更新今日完成数、环形进度、剩余待完成文案、连续天数和这一行的颜色。如果这些数据各存一份很快就会出现“列表已打卡但顶部统计没变”的问题。这个案例做的是一周习惯面板。每个习惯展示过去 7 天的完成情况只有最右侧的今日方块可以点击。右上角的只是一个静态入口源码里没有绑定新增习惯逻辑。Habit模型里藏着两类信息每个习惯都用Habit表示。它既保存展示信息也保存打卡状态。interfaceHabit{id:numbername:stringicon:stringcolor:stringtarget:numberunit:stringchecks:boolean[]streak:number}name、icon、color用来画列表项target和unit当前没有展示出来但可以理解成后续扩展目标值的预留字段比如喝水 8 杯、阅读 30 分钟。真正参与交互的是checks和streak。checks是长度为 7 的布尔数组对应周一到周日streak是连续打卡天数列表里直接展示成“连续 x 天”。今日完成数不需要单独保存源码里有一个todayProgress状态但当前页面没有使用它。真正的今日完成数来自completedToday这个 gettergetcompletedToday():number{returnthis.habits.filter(hh.checks[6]).length}这里固定读取checks[6]也就是周日这一列。页面底部提示也写了“点击最右侧方块标记今日完成”所以这个案例默认把数组最后一项当成今天。顶部数字、环形进度、剩余待完成文案都依赖completedToday只要某个习惯的今日状态变化顶部统计会自动重新计算。这种写法比维护一个todayProgress更稳。todayProgress如果手动加减很容易和habits里的真实打卡状态不同步。要继续整理这段代码我会直接删掉未使用的todayProgress让状态源更单一。一周视图是二维关系不只是装饰页面顶部先渲染weekDays列表里每个习惯再渲染自己的checks。这两组数据是按下标对齐的。StateweekDays:string[][周一,周二,周三,周四,周五,周六,周日]列表项中每个checked会被画成一个圆点。已完成时使用习惯自己的主题色未完成则用浅灰如果是第 7 列也就是i 6还会加边框强调“今天”。ForEach(habit.checks,(checked:boolean,i:number){Column().width(28).height(28).borderRadius(14).backgroundColor(checked?habit.color:(i6?#F0F0F0:#F8F8F8)).border({width:i6?2:0,color:habit.color}).onClick((){if(i6)this.toggleToday(habit.id)})})注意这里的点击条件只有i 6才会调用toggleToday()。历史日期只是展示不能回补打卡。这个限制在教学页面里很清楚但真实习惯应用通常会允许补签或者至少提供编辑历史记录的入口。toggleToday()同时处理打卡和取消打卡toggleToday()的输入只有习惯id。函数会遍历habits找到对应习惯后复制一份checks反转最后一项再根据新的今日状态调整streak。toggleToday(id:number){this.habitsthis.habits.map(h{if(h.id!id)returnhconstchecksh.checks.slice()checks[6]!checks[6]conststreakchecks[6]?h.streak1:Math.max(0,h.streak-1)h.checkschecks;h.streakstreak;returnh})}这里有个工程上需要留意的点外层数组通过map()重新赋值了但命中的h对象本身还是原对象只是改了它的字段。这个案例能表达状态更新思路但如果习惯数据后面变复杂我更倾向于返回一个新对象return { ...h, checks, streak }。这样引用变化更完整也更符合不可变更新的习惯。连续天数的计算也只是演示版。当前逻辑是打卡就1取消就-1并没有真的从历史checks倒推连续天数。比如前一天没打卡、今天打卡严格来说连续天数应该是 1而不是在旧streak基础上加一。正式项目里streak最好由完整日期记录计算出来不要靠单次点击累加。顶部进度区只读状态不直接改状态顶部卡片里的数字和环形进度都来自completedToday和habits.length。Progress({value:this.completedToday,total:this.habits.length,type:ProgressType.Ring})Text(${Math.round(this.completedToday/this.habits.length*100)}%)Text(今日加油还有${this.habits.length-this.completedToday}个习惯待完成)这部分没有自己的业务逻辑它只是把状态翻译成用户能理解的信息。这样的分工很重要列表负责触发状态变化顶部负责展示聚合结果。只要habits是可信的顶部就不用关心用户刚才点了哪一行。不过这里也缺一个边界处理。如果habits.length变成 0百分比计算会出现除以 0。当前数据是写死的 5 条习惯所以不会触发真实页面支持删除全部习惯时需要补空状态。这页还没做到完整习惯管理这个页面已经能说明“今日打卡如何驱动全页刷新”但它还不是完整的习惯管理功能。顶部没有新增逻辑target和unit没有展示历史日期不能点击连续天数也不是从真实日期序列计算出来的。如果继续扩展我会先把checks从简单布尔数组改成带日期的记录比如{ date, checked }。这样才能处理跨周、补签、漏签、连续天数重算这些真实问题。当前写法的价值在于足够小适合先把 ArkUI 的列表渲染、派生统计和点击更新这条链路吃透。完整代码下面保留案例完整代码方便你直接对照学习、复制和重构。import{router}fromkit.ArkUIinterfaceHabit{id:numbername:stringicon:stringcolor:stringtarget:numberunit:stringchecks:boolean[]streak:number}EntryComponentstruct HabitCheckinBoard{Statehabits:Habit[][{id:1,name:喝水,icon:,color:#5BC4F5,target:8,unit:杯,checks:[true,true,true,true,true,false,false],streak:5},{id:2,name:阅读,icon:,color:#F5A623,target:30,unit:分钟,checks:[true,true,false,true,true,true,false],streak:4},{id:3,name:冥想,icon:,color:#9B59B6,target:10,unit:分钟,checks:[true,false,true,true,false,false,false],streak:1},{id:4,name:跑步,icon:,color:#2ED573,target:5,unit:公里,checks:[false,true,false,true,false,true,false],streak:2},{id:5,name:早睡,icon:,color:#5A67D8,target:23,unit:点前,checks:[true,true,true,false,true,true,false],streak:4},]StatetodayProgress:number0StateweekDays:string[][周一,周二,周三,周四,周五,周六,周日]getcompletedToday():number{returnthis.habits.filter(hh.checks[6]).length}toggleToday(id:number){this.habitsthis.habits.map(h{if(h.id!id)returnhconstchecksh.checks.slice()checks[6]!checks[6]conststreakchecks[6]?h.streak1:Math.max(0,h.streak-1)h.checkschecks;h.streakstreak;returnh})}build(){Column(){Row(){Image($r(app.media.startIcon)).width(24).height(24).onClick(()router.back())Text(习惯打卡).fontSize(18).fontWeight(FontWeight.Bold).layoutWeight(1).textAlign(TextAlign.Center)Text().fontSize(22).fontColor(#5A67D8)}.width(100%).padding({left:16,right:16,top:12,bottom:12})// Today summaryColumn(){Row(){Column(){Text(\\${this.completedToday}\).fontSize(36).fontWeight(FontWeight.Bold).fontColor(#5A67D8)Text(\/\${this.habits.length}已完成\).fontSize(14).fontColor(#FFFFFFAA)}.layoutWeight(1)Stack(){Progress({value:this.completedToday,total:this.habits.length,type:ProgressType.Ring}).width(80).height(80).color(#FFF).backgroundColor(#FFFFFF40).borderRadius(16)Text(\\${Math.round(this.completedToday/this.habits.length*100)}%\).fontSize(16).fontWeight(FontWeight.Bold).fontColor(#FFF)}}.width(100%)Text(\今日加油还有 \${this.habits.length-this.completedToday}个习惯待完成\).fontSize(12).fontColor(#FFFFFFAA).margin({top:8})}.width(calc(100% - 32vp)).borderRadius(16).padding(20).margin({bottom:16}).linearGradient({colors:[[#5A67D8,0.0],[#7B68EE,1.0]],angle:135})// Week headerRow(){Text( ).width(48)ForEach(this.weekDays,(d:string){Text(d.substring(1)).fontSize(10).fontColor(#AAA).layoutWeight(1).textAlign(TextAlign.Center)})}.width(100%).padding({left:16,right:16,bottom:4})// Habit listList({space:10}){ForEach(this.habits,(habit:Habit){ListItem(){Column(){Row(){// IconText(habit.icon).fontSize(24).width(40).textAlign(TextAlign.Center)Column(){Text(habit.name).fontSize(14).fontWeight(FontWeight.Bold).fontColor(#333)Row(){Text().fontSize(11)Text(\ 连续 \${habit.streak}天\).fontSize(11).fontColor(#FF6B35)}.margin({top:2})}.width(72).alignItems(HorizontalAlign.Start)// 7-day checksForEach(habit.checks,(checked:boolean,i:number){Column().width(28).height(28).borderRadius(14).backgroundColor(checked?habit.color:(i6?#F0F0F0:#F8F8F8)).border({width:i6?2:0,color:habit.color}).layoutWeight(1).onClick((){if(i6)this.toggleToday(habit.id)})})}.width(100%)}.width(100%).backgroundColor(#FFF).borderRadius(12).padding({left:12,right:12,top:10,bottom:10}).shadow({radius:3,color:#0000001A,offsetY:2})}})}.layoutWeight(1).padding({left:16,right:16})Text(✓ 点击最右侧方块标记今日完成).fontSize(12).fontColor(#BBBBBB).padding(12)}.width(100%).height(100%).backgroundColor(#F0F4FF)}}最后总结习惯打卡面板 这个案例并不靠复杂技巧取胜它更像一个结构扎实、逻辑完整的业务页面样板。把这篇文章里的状态设计、函数组织和页面分层吃透之后你再去写同类型功能速度会明显快很多。