小V健身助手开发手记(二):从数据输入到任务管理——构建动态运动记录系统

小V健身助手开发手记(二)

  • 从数据输入到任务管理------构建动态运动记录系统
    • [🧩 项目结构概览](#🧩 项目结构概览)
    • [⏳ 功能一:日期选择弹窗 ------ `DateDialog`](#⏳ 功能一:日期选择弹窗 —— DateDialog)
      • [🔍 核心要点:](#🔍 核心要点:)
    • [💡 功能二:任务添加弹窗 ------ `TaskAddDialog`](#💡 功能二:任务添加弹窗 —— TaskAddDialog)
      • [🎯 设计亮点:](#🎯 设计亮点:)
      • [📱 UI 优化:](#📱 UI 优化:)
    • [🧱 功能三:首页任务列表 ------ `HomeContent`](#🧱 功能三:首页任务列表 —— HomeContent)
      • [✨ 关键逻辑:](#✨ 关键逻辑:)
    • [🔄 数据流转与状态同步](#🔄 数据流转与状态同步)
    • [✅ 总结](#✅ 总结)
    • 代码总结

从数据输入到任务管理------构建动态运动记录系统

在上一篇文章中,我们实现了「小V健身助手」的启动流程与用户隐私授权机制,确保应用在合规的前提下为用户提供服务。本篇将深入运动任务录入与管理模块,讲解如何通过自定义弹窗、本地状态共享与动态数据渲染,构建一个流畅、直观的健康数据录入体验。

我们将围绕以下三大功能展开:

  1. 日期选择弹窗:让用户自由设定运动日期;
  2. 任务添加弹窗:支持数字键盘输入、实时卡路里计算;
  3. 首页任务列表动态更新:实现数据联动与可视化反馈。

整个系统采用 HarmonyOS ArkTS + Stage 模型 构建,充分体现了组件化、状态驱动与端侧轻量化设计思想。


🧩 项目结构概览

复制代码
ets/
├── dialog/
│   ├── DateDialog.ets        // 日期选择弹窗
│   └── TaskAddDialog.ets     // 任务添加弹窗
├── pages/
│   ├── AddTaskPage.ets       // 添加任务页面
│   └── MainIndexPage.ets     // 主页
├── util/
│   └── DateUtil.ets          // 日期工具类
└── view/
    └── home/
        ├── HomeContent.ets   // 首页内容组件
        └── Addbtn.ets        // 浮动添加按钮

各组件职责清晰,便于维护与扩展。


⏳ 功能一:日期选择弹窗 ------ DateDialog

用户常需记录非当天的运动数据(如补录昨日训练),因此我们需要一个独立的日期选择器。

ts 复制代码
@CustomDialog
export default struct DateDialog {
  controller: CustomDialogController
  date: Date = new Date()

  build() {
    Column() {
      DatePicker({
        start: new Date('2020-01-01'),
        end: new Date(),
        selected: this.date
      })
        .onChange((value: DatePickerResult) => {
          const year = Number(value.year) || new Date().getFullYear();
          const month = Number(value.month) || new Date().getMonth();
          const day = Number(value.day) || new Date().getDate();
          this.date.setFullYear(year, month, day);
        })

      Row({ space: 20 }) {
        Button('取消')
          .width(120)
          .backgroundColor('#ff3e3a3a')
          .onClick(() => this.controller.close())

        Button('确定')
          .width(120)
          .backgroundColor('#ff3e3a3a')
          .onClick(() => {
            AppStorage.SetOrCreate('date', this.date.getTime())
            this.controller.close()
          })
      }
    }
    .padding(12)
  }
}

🔍 核心要点:

  • 使用 DatePicker 组件提供标准日期选择界面;
  • onChange 回调处理用户选择,避免 undefined 值;
  • 点击"确定"后,将时间戳写入全局 AppStorage,供其他页面读取;
  • AppStorage.SetOrCreate 是跨页面共享状态的最佳实践。

最佳实践 :避免使用 @State@Link 共享全局状态,应优先使用 AppStorage 实现跨组件通信。


💡 功能二:任务添加弹窗 ------ TaskAddDialog

这是本模块的核心交互组件,支持用户输入运动时长并自动计算卡路里消耗。

ts 复制代码
@CustomDialog
export default struct TaskAddDialog {
  @StorageProp('date') date: number = DateUtil.beginTimeOfDay(new Date()) // 从全局获取日期
  @State show: boolean = true
  @State value: string = ''
  @State num: number = 0
  @State calorie: number = 500 // 每小时消耗卡路里

  @Builder
  saveBtn(text: string, onClick: () => void) {
    Button() {
      Text(text)
        .fontSize(20)
        .fontWeight(800)
        .opacity(0.9)
    }
    .width(80)
    .height(50)
    .type(ButtonType.Normal)
    .backgroundColor('#bfdefd')
    .borderRadius(5)
    .padding({ left: 3, right: 3 })
    .onClick(onClick)
  }

  numArr: string[] = ['1','2','3','4','5','6','7','8','9','0','.']

  clickNumber(num: string) {
    let val = this.value + num
    if (val.includes('.') && val.lastIndexOf('.') !== val.length - 1 && val.indexOf('.') !== val.lastIndexOf('.')) return
    let amount = this.parseFloat(val)
    if (amount >= 999.9) {
      this.num = 999.0
      this.value = '999'
    } else {
      this.num = amount
      this.value = val
    }
  }

  clickDel() {
    if (this.value.length <= 0) return
    this.value = this.value.substring(0, this.value.length - 1)
    this.num = this.parseFloat(this.value)
  }

  parseFloat(str: string): number {
    if (!str) return 0
    if (str.endsWith('.')) str = str.slice(0, -1)
    return parseFloat(str)
  }
}

🎯 设计亮点:

  • 数字键盘模拟 :通过 Grid + ForEach 实现九宫格数字输入;
  • 小数点校验:防止输入多个小数点或非法格式;
  • 数值限制:最大值设为 999.9,避免误操作;
  • 实时卡路里计算this.calorie * this.num 自动更新预估消耗。

📱 UI 优化:

  • 使用 Panel 实现半屏滑动键盘,提升移动端体验;
  • mode(PanelMode.Half) + halfHeight(1050) 控制面板高度;
  • dragBar(false) 隐藏拖拽条,保持简洁。
ts 复制代码
Panel(this.show) {
  Column() { ... }
}
.type(PanelType.Temporary)
.dragBar(false)
.width('100%')

🧱 功能三:首页任务列表 ------ HomeContent

首页是用户查看运动成果的主要入口,需要动态加载并展示任务数据。

ts 复制代码
@Component
export default struct HomeContent {
  @StorageProp('date') date: number = DateUtil.beginTimeOfDay(new Date())

  controller: CustomDialogController = new CustomDialogController({
    builder: DateDialog({ date: new Date(this.date) })
  })

  addTask() {
    router.pushUrl({ url: 'pages/AddTaskPage' })
  }

  @State arr: SportDate[] = [
    { name: '游泳', icon: $r('app.media.home_ic_swimming'), consume: 60, num: 10, target: 10, pre: '分钟' },
    // 更多运动项...
  ]

  build() {
    Column() {
      // 顶部日期选择区域
      Row() {
        Text(DateUtil.formatDate(this.date))
          .fontSize(15)
          .fontWeight(500)
        Image($r('app.media.arrow_down'))
          .width(20)
      }
      .width('90%')
      .height(50)
      .backgroundColor(Color.White)
      .margin({ left: 19, top: 90 })
      .borderRadius(20)
      .justifyContent(FlexAlign.Center)
      .onClick(() => this.controller.open())

      // 任务列表
      Column() {
        Text('任务列表')
          .fontSize(13)
          .fontWeight(700)
          .margin({ left: 20, top: 20, bottom: 10 })

        if (this.arr.length !== 0) {
          List() {
            ForEach(this.arr, (item: SportDate) => {
              ListItem() {
                Row() {
                  Image(item.icon).width(50).height(50)
                  Text(item.name).fontSize(13).fontWeight(600).opacity(0.8)
                  Blank()
                  if (item.num === item.target) {
                    Text(`消耗${item.consume * item.num}卡路里`)
                      .fontSize(13)
                      .fontColor('#3385d8')
                  } else {
                    Text(`${item.num}:${item.target}/${item.pre}`)
                      .fontSize(13)
                      .fontWeight(600)
                  }
                }
                .width('100%')
                .backgroundColor(Color.White)
                .borderRadius(15)
              }
              .width('90%')
            })
          }
          .width('100%')
          .alignListItem(ListItemAlign.Center)
        } else {
          Column({ space: 8 }) {
            Image($r('app.media.ic_no_data')).width(350).height(200)
            Text('暂无任务,请添加任务').fontSize(20).opacity(0.4).margin({ top: 20 })
          }
          .margin({ top: 50, left: 10 })
        }

        Addbtn({ clickAction: () => this.addTask() })
      }
      .width('100%')
      .height('100%')
      .alignItems(HorizontalAlign.Start)
    }
    .backgroundColor('#efefef')
    .width('100%')
    .height('100%')
  }
}

✨ 关键逻辑:

  • 使用 @StorageProp 实时监听 date 变化,确保页面刷新;
  • 任务完成状态判断:item.num === item.target 时显示"已完成"提示;
  • 空数据友好提示:无任务时显示"暂无任务"图标与文字;
  • 浮动添加按钮 Addbtn 支持点击跳转至添加页面。

🔄 数据流转与状态同步

整个系统的数据流如下:

复制代码
[用户选择日期] → [DateDialog] → [AppStorage] → [HomeContent]
                        ↑
                        |
                 [TaskAddDialog] → [AddTaskPage]

所有组件均通过 AppStorage 共享 date 状态,实现跨页面联动。未来可扩展为:

  • 将任务数据存入 data_preferences 或数据库;
  • 支持多天历史记录切换;
  • 添加图表展示每日卡路里趋势。

✅ 总结

通过本次开发,我们成功构建了「小V健身助手」的运动任务管理闭环:

模块 技术实现 用户价值
日期选择 DatePicker + AppStorage 支持补录历史运动
任务输入 数字键盘 + 实时计算 快速准确记录数据
首页展示 List + 状态联动 清晰呈现运动成果

这不仅是功能的实现,更是对 用户体验一致性数据驱动设计 的践行。

代码已通过 HarmonyOS SDK API Version 10+ 验证,适用于 Stage 模型项目。

代码总结

dialgo

DateDialog

ts 复制代码
@CustomDialog
export default struct DateDialog{
  controller: CustomDialogController
  date:Date = new Date()

  build() {
    Column(){
      DatePicker({
        start:new Date('2020-01-01'),
        end:new Date(),
        selected:this.date
      })
        .onChange((value: DatePickerResult)=>{
          // 给year/month/day加默认值,避免undefined
          const year = Number(value.year) || new Date().getFullYear();
          const month = Number(value.month) || new Date().getMonth();
          const day = Number(value.day) || new Date().getDate();
          this.date.setFullYear(year, month, day);
        })
      Row({space:20}){
        Button('取消')
          .width(120)
          .backgroundColor('#ff3e3a3a')
          .onClick(()=>{
            this.controller.close()
          })
        Button('确定')
          .width(120)
          .backgroundColor('#ff3e3a3a')
          .onClick(()=>{
            // 将日期保存到全局
            AppStorage.SetOrCreate('date',this.date.getTime())
            this.controller.close()// 关闭弹窗
          })

      }
    }
    .padding(12)
  }

}

TaskAddDialog

ts 复制代码
import DateUtil from "../util/DateUtil"

@Extend(GridItem)
function btnStyle(){
  .backgroundColor(Color.White)
  .borderRadius(15)
  .opacity(0.7)
  .height(50)
}


interface SaveBtnFace {}

@CustomDialog
export default struct TaskAddDialog  {

  // 获取到日期毫秒值
  @StorageProp('date') date:number = DateUtil.beginTimeOfDay(new Date())// 从全局获取日期

  @State show:boolean = true
  @State value : string = ''
  @State num : number = 0
  @State calorie : number = 500;

  // 复用确认按钮
  @Builder
  saveBtn(text:string,onClick:()=> SaveBtnFace){
    Button(){
      Text(text)
        .fontSize(20)
        .fontWeight(800)
        .opacity(0.9)
    }
    .width(80)
    .height(50)
    .type(ButtonType.Normal)
    .backgroundColor('#bfdefd')
    .borderRadius(5)
    .padding({left:3,right:3})
    .onClick(onClick)
  }

  // 键盘数字
  numArr:string[] = ['1','2','3','4','5','6','7','8','9','0','.']

  controller : CustomDialogController

  // 数字点击事件逻辑
  clickNumber(num:string){
    let val = this.value + num
    // 检查小数点
    let firstIndex = val.indexOf('.')
    let lastIndex = val.lastIndexOf('.')
    if(firstIndex !== lastIndex || (lastIndex!=-1 && lastIndex < val.length - 2)){// 校验逻辑
      return
    }

    let amount = this.parseFloat(val)

    if(amount >= 999.9){// 限制最大数
      this.num = 999.0
      this.value = '999'
    }else{
      this.num = amount
      this.value = val
    }
  }

  // 删除事件
  clickDel(){
    if(this.value.length <= 0){
      this.value = ''
      this.num = 0
      return
    }
    this.value = this.value.substring(0,this.value.length-1)
    this.num = this.parseFloat(this.value)
  }

  // 字符串转小数
  parseFloat(str:string){
    if(!str){
      return 0
    }
    if(str.endsWith('.')){
      str = str.substring(0,str.length-1)
    }
    return parseFloat(str)
  }


  build() {
    Column(){
      // 弹窗头部
      Row(){
        Text(DateUtil.formatDate(this.date))
          .fontSize(15)
          .fontWeight(800)
        Blank(10)
        Button(){
          Text('x')
            .fontSize(15)
            .fontColor(Color.White)
            .fontWeight(800)
        }
        .width(20)
        .height(20)
        .backgroundColor(Color.Red)
        .padding({bottom:5})
        .onClick(()=>{
          this.controller.close()
        })
      }
      .width('95%')
      .justifyContent(FlexAlign.End)

      // 中间部分
      Column({space:10}){
        Image($r('app.media.home_ic_swimming'))
          .width(90)
          .height(90)
        Text('游泳')
          .fontSize(20)
          .fontWeight(700)
        Row(){
          TextInput({text:this.num.toFixed(1)})
            .width('35%')
            .fontSize(30)
            .fontColor('#a6c1db')
            .caretColor(Color.Transparent)
            .textAlign(TextAlign.Center)
            .copyOption(CopyOptions.None)
          Text('/小时')
            .fontSize(30)
            .opacity(0.7)
            .fontWeight(800)
        }
        // 小键盘
        Panel(this.show){
          Column(){
            Grid(){
              ForEach(this.numArr,(item:string)=>{
                GridItem(){
                  Text(item)
                    .fontSize(20)
                    .fontWeight(500)
                }
                .btnStyle()
                .onClick(()=>{
                  this.clickNumber(item)
                })

              })
              // 删除按钮
              GridItem(){
                Text('删除')
                  .fontSize(20)
                  .fontWeight(500)
              }
              .btnStyle()
              .onClick(()=>{
                this.clickDel()
              })
              // 确定按钮
              GridItem(){
                this.saveBtn('确定',() => this.show = false)
              }
              // 从1开始到3结束,也就是占一整行
              .columnStart(1)
              .columnEnd(3)
              .btnStyle()

            }
            .columnsTemplate('1fr 1fr 1fr')
            .columnsGap(5)
            .rowsGap(8)
            .width('95%')
            .padding({top:15})
          }
        }
        .mode(PanelMode.Half)
        .halfHeight(1050)
        .type(PanelType.Temporary)
        .dragBar(false)
        .width('100%')
        Row(){
          Text('预计消耗' + this.calorie * this.num + '卡路里')
            .fontSize(20)
            .fontWeight(700)
            .opacity(0.7)
        }
        Row({space:20}){
          this.saveBtn('修改',() => this.show = true)
          Button(){
            Text('确定')
              .fontSize(20)
              .fontWeight(800)
              .opacity(0.9)
          }
          .width(80)
          .height(50)
          .type(ButtonType.Normal)
          .backgroundColor('#bfdefd')
          .borderRadius(5)
          .padding({left:3,right:3})
          .onClick(()=>{
            this.controller.close()
          })
        }
      }
    }
    .width('95%')
    .height('95%')
    .alignItems(HorizontalAlign.Center)
  }
}

UserPrivacyDialog

ts 复制代码
@CustomDialog
export default struct UserPrivacyDialog{
  controller: CustomDialogController = new CustomDialogController({
    builder:''
  })
  cancel:Function = () =>{}  // 不同意
  confirm:Function = () =>{} // 同意
  build() {
    Column({space:10}){
      Text('欢迎使用小V健身')
      Button('同意')
        .fontColor(Color.White)
        .backgroundColor('#ff06ae27')
        .width(150)
        .onClick(()=>{
          this.confirm()
          this.controller.close()
        })
      Button('不同意')
        .fontColor(Color.Gray)
        .backgroundColor('#c8fcd0')
        .width(150)
        .onClick(()=>{
          this.cancel()
          this.controller.close()
        })
    }
    .width('80%')
    .height('75%')
  }
}

pages

AddTaskPage

ts 复制代码
import { router } from '@kit.ArkUI'
import TaskAddDialog from '../dialog/TaskAddDialog'

interface AddSportDate{
  name:String,
  icon:ResourceStr,
  consume:number,
  pre:String
}
@Entry
@Component
struct AddTaskPage{

  controller : CustomDialogController = new CustomDialogController({
    builder:TaskAddDialog()
  })

  @State arr:AddSportDate[]=[
    {
      name:'游泳',
      icon:$r('app.media.home_ic_swimming'),
      consume:60,
      pre:'分钟',
    },
    {
      name:'游泳',
      icon:$r('app.media.home_ic_swimming'),
      consume:60,
      pre:'分钟',
    },
    {
      name:'游泳',
      icon:$r('app.media.home_ic_swimming'),
      consume:60,
      pre:'分钟',
    },
    {
      name:'游泳',
      icon:$r('app.media.home_ic_swimming'),
      consume:60,
      pre:'分钟',
    },
  ]

  build(){
    Column(){
      Row(){
        Image($r('app.media.ic_back'))
          .width(25)
      }
      .margin({top:10,left:10,bottom:10})
      .onClick(()=>{
        router.back()
      })
      List({space:10}){
        ForEach(this.arr,(item:AddSportDate) => {
          ListItem(){
            Row(){
              Image(item.icon)
                .width(60)
                .height(60)
                .margin({right:15})
              Column(){
                Text(item.name+'')
                  .fontSize(15)
                  .fontWeight(500)
                Text(item.consume + '卡路里/' + item.pre)
                  .fontSize(10)
                  .fontWeight(600)
                  .opacity(0.7)
              }
              .alignItems(HorizontalAlign.Start)
              Blank()
              Button(){
                Image($r('app.media.ic_list_add'))
                  .width(20)
              }
              .onClick(()=>{
                this.controller.open()
              })
            }
            .width('100%')
            .justifyContent(FlexAlign.SpaceBetween)
          }
          .width('90%')
          .backgroundColor(Color.White)
          .padding(5)
          .borderRadius(15)

        })
      }
      .width('100%')
      .alignListItem(ListItemAlign.Center)

    }
    .width('100%')
    .height('100%')
    .backgroundColor('#efefef')
    .alignItems(HorizontalAlign.Start)
  }
}

Index

ts 复制代码
import UserPrivacyDialog from '../dialog/UserPrivacyDialog'
import { common } from '@kit.AbilityKit'
import data_preferences from '@ohos.data.preferences'
import { router } from '@kit.ArkUI'

// 定义常量存储首选项中的键
const H_STORE:string = 'V_health'
const IS_PRIVACY:string = 'isPrivacy'

@Entry
@Component
struct Index {
  // 生命周期
  contest: common.UIAbilityContext = getContext(this) as common.UIAbilityContext;
  dialogController: CustomDialogController = new CustomDialogController({
    builder:UserPrivacyDialog({
      cancel:()=>{this.exitAPP()},
      confirm:()=>{this.onConfirm()}
    })
  })

  // 点击同意后的逻辑
  onConfirm(){
    // 定义首选项
    let preferences = data_preferences.getPreferences(this.contest,H_STORE)
    // 异步处理首选项中的数据
    preferences.then((res)=>{
      res.put(IS_PRIVACY,true).then(()=>{
        res.flush();
        // 记录日志
        console.log('Index','isPrivacy记录成功');
        this.jumpToMain()
      }).catch((err:Error)=>{
        console.log('Index','isPrivacy记录失败,原因'+err);
      })
    })
  }
  // 点击不同意时的逻辑
  exitAPP(){
    this.contest.terminateSelf()
  }

  // 页面加载开始执行逻辑
  aboutToAppear(): void {
    let preferences = data_preferences.getPreferences(this.contest,H_STORE)
    preferences.then((res)=>{
      res.get(IS_PRIVACY,false).then((isPrivate)=>{
        // 判断传入的参数
        if(isPrivate==true){
          // 点击同意跳转到首页
          this.jumpToMain()
        }
        else{
          this.dialogController.open()
        }
      })
    })
  }

  // 页面结束时的执行逻辑
  aboutToDisappear(): void {
    clearTimeout()
  }

  // 跳转到首页
  jumpToMain(){
    setTimeout(()=>{
      router.replaceUrl({url:'pages/MainIndexPage'})
    },2000)
  }

  build() {
    Column(){

    }
    .width('100%')
    .height('100%')
    .backgroundImage($r('app.media.backgroundBegin'))
    .backgroundImageSize({width:'100%',height:'100%'})
  }
}

MainIndexPage

ts 复制代码
import HomeContent from '../view/home/HomeContent'

@Entry
@Component
struct MainIndexPage {
  @State selectIndex:number = 0

  @Builder TabBarBuilder(index:number,selIcon:ResourceStr,normalIcon:ResourceStr,text:ResourceStr){
    Column(){
      Image(this.selectIndex === index ? selIcon : normalIcon)
        .width(20)
      Text(text)
        .fontSize(10)
        .fontColor(this.selectIndex === index ? '#3385d8' : '#c4c4c4')
    }
  }
  build() {
    Column(){
      Tabs({
        barPosition:BarPosition.End,
        index:this.selectIndex
      }){
        // 主页
        TabContent(){
          HomeContent()
        }
          .tabBar(this.TabBarBuilder(
            0,
            $r('app.media.tabs_home_sel'),
            $r('app.media.tabs_home_normal'),
            '主页'
          ))
        // 成就页
        TabContent()
          .tabBar(this.TabBarBuilder(
            1,
            $r('app.media.tabs_achieve_sel'),
            $r('app.media.tabs_achieve_normal'),
            '成就'
          ))
        // 个人页
        TabContent()
          .tabBar(this.TabBarBuilder(
            2,
            $r('app.media.tabs_per_sel'),
            $r('app.media.tabs_per_normal'),
            '个人'
          ))
      }
      .onChange((num:number)=>{
        this.selectIndex = num
      })
    }
  }
}

util

DateUtil

ts 复制代码
class DateUtil {
  formatDate(num:number){
    let date = new Date(num)
    let year = date.getFullYear()
    let month = date.getMonth() + 1
    let day = date.getDate()
    let m = month < 10 ? '0' + month : month
    let d = day < 10 ? '0' + day : day
    return `${year}年${m}月${d}日`
  }

  beginTimeOfDay(date:Date){
    let d = new Date(date.getFullYear(),date.getMonth(),date.getDate())
    return d.getTime()
  }
}

let dateUtil = new DateUtil()

export default dateUtil as DateUtil

view/home

Addbtn

ts 复制代码
@Component
export default struct Addbtn{
  clickAction: Function = () => {}
  build() {
    Button({type:ButtonType.Circle,stateEffect:false}){
      Image($r('app.media.ic_add'))
        .borderRadius('50%')
        .width('100%')
        .height('100%')
        .fillColor('#c9f2fd')
    }
    .zIndex(2)
    .position({x:'78%',y:'48%'})
    .width(48)
    .height(48)
    .onClick(()=>{this.clickAction()})
  }
}

HomeContent

ts 复制代码
import DateDialog from "../../dialog/DateDialog"
import DateUtil from "../../util/DateUtil"
import Addbtn from "./Addbtn"
import { router } from "@kit.ArkUI"

// 首页运动数据类型接口
interface SportDate{
  name:String,
  icon:ResourceStr,
  consume:number,
  num:number,
  target:number,
  pre:String
}

@Component
export default struct HomeContent {


  // 获取到日期毫秒值
  @StorageProp('date') date:number = DateUtil.beginTimeOfDay(new Date())// 从全局获取日期

  controller: CustomDialogController = new CustomDialogController({
    builder:DateDialog({date:new Date(this.date)})
  })

  addTask(){
    router.pushUrl({url:'pages/AddTaskPage'})
    console.log('跳转到添加任务页面')
  }

  // 运动数据
  @State arr: SportDate[] = [
    {
      name:'游泳',
      icon:$r('app.media.home_ic_swimming'),
      consume:60,
      num:10,
      target:10,
      pre:'分钟',
    },
    {
      name:'跳绳',
      icon:$r('app.media.home_ic_swimming'),
      consume:60,
      num:10,
      target:10,
      pre:'分钟',
    },
    {
      name:'跳绳',
      icon:$r('app.media.home_ic_swimming'),
      consume:60,
      num:10,
      target:10,
      pre:'分钟',
    },
    {
      name:'跳绳',
      icon:$r('app.media.home_ic_swimming'),
      consume:60,
      num:10,
      target:10,
      pre:'分钟',
    },
  ]
  build() {
    Column(){
      // 上半部分
      Column(){
        Row(){
          Text(DateUtil.formatDate(this.date))
            .fontSize(15)
            .fontWeight(500)
          Image($r('app.media.arrow_down'))
            .width(20)
        }
        .width('90%')
        .height(50)
        .backgroundColor(Color.White)
        .margin({left:19,top:90})
        .borderRadius(20)
        .justifyContent(FlexAlign.Center)
        .onClick(()=>{
          this.controller.open()
        })
      }
      .backgroundImage($r('app.media.home_bg'))
      .backgroundImageSize({width:'100%',height:'100%'})
      .width('100%')
      .height('40%')
      .alignItems(HorizontalAlign.Start)
      .borderRadius({bottomLeft:20,bottomRight:20})

      // 下半部分
      Column(){
        Text('任务列表')
          .fontSize(13)
          .fontWeight(700)
          .margin({left:20,top:20,bottom:10})
        if(this.arr.length!==0){
          Column(){
            List({space:10}){
              ForEach(this.arr,(item:SportDate)=>{
                ListItem(){
                  Row(){
                    Image(item.icon)
                      .width(50)
                      .height(50)
                    Text(item.name+'')
                      .fontSize(13)
                      .fontWeight(600)
                      .opacity(0.8)
                    Blank()
                    if(item.num === item.target){// 任务已经完成
                      Text('消耗' + item.consume * item.num + '卡路里')
                        .fontSize(13)
                        .fontWeight(600)
                        .margin({right:10})
                        .fontColor('#3385d8')
                    }else{// 任务还没有完成
                      Text(item.num + ':' + item.target + '/' + item.pre)
                        .fontSize(13)
                        .fontWeight(600)
                        .margin({right:10})
                    }
                  }
                  .width('100%')
                  .backgroundColor(Color.White)
                  .borderRadius(15)
                }
                .width('90%')
              })
            }
            .width('100%')
            .alignListItem(ListItemAlign.Center)
          }
          .width('100%')
        }else{// 如果没有数据
          Column({space:8}){
            Image($r('app.media.ic_no_data'))
              .width(350)
              .height(200)
            Text('暂无任务,请添加任务')
              .fontSize(20)
              .opacity(0.4)
              .margin({top:20})
          }
          .margin({top:50,left:10})
        }
        Addbtn({clickAction:()=>{this.addTask()}})
      }
      .width('100%')
      .height('100%')
      .alignItems(HorizontalAlign.Start)
    }
    .backgroundColor('#efefef')
    .width('100%')
    .height('100%')
  }
}
相关推荐
西岸行者5 天前
学习笔记:SKILLS 能帮助更好的vibe coding
笔记·学习
悠哉悠哉愿意5 天前
【单片机学习笔记】串口、超声波、NE555的同时使用
笔记·单片机·学习
别催小唐敲代码5 天前
嵌入式学习路线
学习
毛小茛5 天前
计算机系统概论——校验码
学习
babe小鑫5 天前
大专经济信息管理专业学习数据分析的必要性
学习·数据挖掘·数据分析
winfreedoms5 天前
ROS2知识大白话
笔记·学习·ros2
在这habit之下5 天前
Linux Virtual Server(LVS)学习总结
linux·学习·lvs
我想我不够好。5 天前
2026.2.25监控学习
学习
im_AMBER5 天前
Leetcode 127 删除有序数组中的重复项 | 删除有序数组中的重复项 II
数据结构·学习·算法·leetcode
CodeJourney_J5 天前
从“Hello World“ 开始 C++
c语言·c++·学习