小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%')
  }
}
相关推荐
世界宇宙超级无敌究极特级顶级第一非常谱尼2 小时前
RF Power Amplifiers for Wireless Communications 第三章学习笔记
笔记·学习·功放·pa·mmic
tc&2 小时前
Kamailio的学习
学习·kamailio
龘龍龙2 小时前
Python基础学习(二)
开发语言·python·学习
xiaoxiaoxiaolll2 小时前
机器学习材料性能预测与材料基因工程
深度学习·学习
Rousson2 小时前
硬件学习笔记--92 关于UWB的介绍
笔记·学习
玩具猴_wjh2 小时前
12.12 学习笔记
笔记·学习
炽烈小老头2 小时前
【每天学习一点算法 2025/12/10】反转链表
学习·算法·链表
QiZhang | UESTC2 小时前
学习日记day47
学习
●VON2 小时前
AI辅助学习如何避免依赖陷阱?
人工智能·学习