本篇案例将介绍如何使用@State、@Prop、@Link、@Watch、@Provide、@Consume管理页面级变量的状态,实现对页面数据的增加、删除、修改。要求完成以下功能:

  • 实现一个自定义弹窗,完成添加子目标的功能。
  • 实现一个可编辑列表,可点击指定行展开调节工作目标进度,可多选、全选删除指定行。

1. 案例效果截图

 

2. 案例运用到的知识点

2.1. 核心知识点

  • V1状态管理:@State、@Prop、@Link、@Watch、@Provide、@Consume。
  • 自定义弹窗: 通过CustomDialogController类显示自定义弹窗。
  • List列表:列表包含一系列相同宽度的列表项。
  • 自定义构建函数:@Builder
  • 样式扩展:@Extend
  • 渲染控制:ForEach、if

2.2. 其他知识点

  • ArkTS 语言基础
  • 自定义组件和组件生命周期
  • 内置组件:Column/Text/Row/Stack/Blank/Button
  • 日志管理类的编写
  • 常量与资源分类的访问
  • MVVM模式

3. 代码结构

├──entry/src/main/ets                   // ArkTS代码区
│  ├──common
│  │  ├──constants
│  │  │  └──CommonConstant.ets          // 公共常量类
│  │  └──utils
│  │     ├──DateUtil.ets                // 获取格式化日期工具
│  │     └──Logger.ets                  // 日志打印工具类
│  ├──entryability
│  │  └──EntryAbility.ts                // 程序入口类
│  ├──pages
│  │  └──MainPage.ets                   // 主页面
│  ├──view
│  │  ├──AddTargetDialog.ets            // 自定义弹窗
│  │  ├──ProgressEditPanel.ets          // 进展调节自定义组件
│  │  ├──TargetInformation.ets          // 整体目标详情自定义组件
│  │  ├──TargetList.ets                 // 工作目标列表
│  │  └──TargetListItem.ets             // 工作目标列表子项
│  └──viewmodel
│     ├──DataModel.ets                  // 工作目标数据操作类
│     └──TaskItemModel.ets              // 任务享实体类
└──entry/src/main/resources	            // 资源文件目录

4. 公共文件与资源

本案例涉及到的常量类和工具类代码如下:

4.1. 通用日志类

  • entry/src/main/ets/common/utils/Logger.ets

4.2. 常量类

  • entry/src/main/ets/common/constant/CommonConstant.ets

本案例涉及到的资源文件如下:

  1. string.json
  • entry/src/main/resources/base/element/string.json
  1. float.json
  • entry/src/main/resources/base/element/float.json
  1. color.json
  • entry/src/main/resources/base/element/color.json

资源的具体内容及其他相关文件,请参考随书配套源码。

5. 静态展示页面

5.1. 首页架构与页面标题

// entry/src/main/ets/pages/Index.ets
import { CommonConstants } from '../common/constant/CommonConstant'

@Entry
@Component
struct Index {
  build() {
    Column() {
      this.titleBar()
    }
    .width(CommonConstants.FULL_WIDTH).height(CommonConstants.FULL_HEIGHT)
    .backgroundColor($r('app.color.index_background'))
  }

  @Builder
  titleBar() {
    Text($r('app.string.title'))
      .width(CommonConstants.TITLE_WIDTH).height($r('app.float.title_height'))
      .fontSize($r('app.float.title_font'))
      .fontWeight(CommonConstants.FONT_WEIGHT_LARGE).textAlign(TextAlign.Start)
      .margin({
        top: $r('app.float.title_margin'),
        bottom: $r('app.float.title_margin')
      })
  }
}

5.2. 目标展示

  • 实现TargetInformation 组件
// entry/src/main/ets/view/TargetInformation.ets
import { CommonConstants } from '../common/constant/CommonConstant'

@Component
export default struct TargetInformation {
  @Prop latestUpdateDate: string = ''
  @Prop totalTasksNumber: number = 0
  @Prop completedTasksNumber: number = 0

  build() {
    Column() {
      this.TargetItem()
      this.OverallProgress()
    }
    .padding($r('app.float.target_padding'))
    .width(CommonConstants.MAIN_BOARD_WIDTH)
    .height($r('app.float.target_info_height'))
    .backgroundColor(Color.White)
    .borderRadius(CommonConstants.TARGET_BORDER_RADIUS)
  }

  @Builder
  TargetItem() {
    Row() {
      Image($r("app.media.goals"))
        .width($r('app.float.target_image_length'))
        .height($r('app.float.target_image_length'))
        .objectFit(ImageFit.Fill)
        .borderRadius(CommonConstants.IMAGE_BORDER_RADIUS)
      Column() {
        Text($r('app.string.target_name'))
          .fontSize($r('app.float.target_name_font'))
          .fontWeight(CommonConstants.FONT_WEIGHT_LARGE)
          .width(CommonConstants.TITLE_WIDTH)
        Text($r('app.string.target_info'))
          .opacityTextStyle()
          .fontSize($r('app.float.target_desc_font'))
          .margin({ top: $r('app.float.title_margin') })
      }
      .layoutWeight(1)
      .margin({ left: CommonConstants.TARGET_MARGIN_LEFT })
      .alignItems(HorizontalAlign.Start)
    }
    .width(CommonConstants.FULL_WIDTH)
  }

  @Builder
  OverallProgress() {
    Row() {
      Column() {
        Text($r('app.string.overall_progress'))
          .fontSize($r('app.float.button_font'))
          .fontColor($r('app.color.title_black_color'))
          .fontWeight(CommonConstants.FONT_WEIGHT)
        Row() {
          Text($r('app.string.latest_updateTime'))
            .opacityTextStyle()
          Text(this.latestUpdateDate)
            .opacityTextStyle()
        }
        .margin({ top: $r('app.float.text_margin') })
      }
      .alignItems(HorizontalAlign.Start)

      Blank()
      Stack() {
        Row() {
          Text(this.completedTasksNumber.toString())
            .fontSize($r('app.float.progress_font'))
            .fontWeight(CommonConstants.FONT_WEIGHT)
            .fontColor($r('app.color.main_blue'))
          Text(`/${this.totalTasksNumber}`)
            .fontSize($r('app.float.progress_font'))
            .fontWeight(CommonConstants.FONT_WEIGHT)
        }

        Progress({
          value: this.completedTasksNumber,
          total: this.totalTasksNumber,
          type: ProgressType.Ring
        })
          .color($r('app.color.main_blue'))
          .style({
            strokeWidth: CommonConstants.STROKE_WIDTH
          })
          .width($r('app.float.progress_length'))
          .height($r('app.float.progress_length'))
      }
    }
    .width(CommonConstants.FULL_WIDTH)
    .height($r('app.float.progress_length'))
    .margin({ top: $r('app.float.progress_margin_top') })
  }
}

@Extend(Text)
function opacityTextStyle() {
  .fontSize($r('app.float.text_font'))
  .fontColor($r('app.color.title_black_color'))
  .opacity(CommonConstants.OPACITY)
  .fontWeight(CommonConstants.FONT_WEIGHT)
}
  • 首页引入TargetInfomation
// entry/src/main/ets/pages/Index.ets
import { CommonConstants } from '../common/constant/CommonConstant'

// 1.引入TargetInformation模块
import TargetInformation from '../view/TargetInformation'

@Entry
@Component
struct Index {
  // 2.定义相关变量
  @State totalTasksNumber: number = 0
  @State completedTasksNumber: number = 0
  @State latestUpdateDate: string = CommonConstants.DEFAULT_PROGRESS_VALUE

  build() {
    Column() {
      this.titleBar()

      // 3.调用 TargetInformation 模块
      TargetInformation({
        latestUpdateDate: this.latestUpdateDate,
        totalTasksNumber: this.totalTasksNumber,
        completedTasksNumber: this.completedTasksNumber
      })
    }
    .width(CommonConstants.FULL_WIDTH)
    .height(CommonConstants.FULL_HEIGHT)
    .backgroundColor($r('app.color.index_background'))
  }

  @Builder
  titleBar() {
    Text($r('app.string.title'))
      .width(CommonConstants.TITLE_WIDTH)
      .height($r('app.float.title_height'))
      .fontSize($r('app.float.title_font'))
      .fontWeight(CommonConstants.FONT_WEIGHT_LARGE)
      .textAlign(TextAlign.Start)
      .margin({
        top: $r('app.float.title_margin'),
        bottom: $r('app.float.title_margin')
      })
  }
}

5.3. 子任务列表

  • 定义子任务项数据结构
// entry/src/main/ets/viewModel/TaskItemModel.ets
export default class TaskItemModel {
  taskName: string
  updateDate: string
  progressValue: number

  constructor(taskName: string, progressValue: number, updateDate: string) {
    this.taskName = taskName
    this.progressValue = progressValue
    this.updateDate = updateDate
  }
}
  • 定义数据模型
// entry/src/main/ets/viewModel/DataModel.ets
import TaskItemModel from './TaskItemModel'
import getCurrentTime from '../common/utils/DateUtil'

export class DataModel {

  private targetData: Array<TaskItemModel> = [
    new TaskItemModel('私域流量提升10%', 0, getCurrentTime()),
    new TaskItemModel('公域域流量提升10%', 10, getCurrentTime()),
    new TaskItemModel('活跃度提升20%', 20, getCurrentTime())
  ]

  getData(): Array<TaskItemModel> {
    return this.targetData
  }
}

export default new DataModel()
  • 实现日期构建函数
// entry/src/main/ets/common/utils/DateUtil.ets
import { CommonConstants } from '../constant/CommonConstant'

export default function getCurrentTime(): string {
  let date = new Date()
  let year = date.getFullYear()
  let month = date.getMonth() + CommonConstants.PLUS_ONE
  let day = date.getDate()
  let hours = date.getHours()
  let minutes = date.getMinutes().toString()
  if (Number.parseInt(minutes) < CommonConstants.TEN) {
    minutes = `0${minutes}`
  }
  let second = date.getSeconds().toString();
  if (Number.parseInt(second) < CommonConstants.TEN) {
    second = `0${second}`
  }
  return `${year}/${month}/${day} ${hours}:${minutes}:${second}`
}
  • 实现子任务列表项组件
// entry/src/main/ets/pages/view/TargetListItem.ets

import TaskItemModel from '../viewModel/TaskItemModel'
import { CommonConstants } from '../common/constant/CommonConstant'

@Component
export default struct TargetListItem {
  private taskItem?: TaskItemModel

  build() {
    Stack({ alignContent: Alignment.Start }) {
      Column() {
        this.TargetItem()
      }
      .padding({
        left: $r('app.float.list_padding'),
        top: $r('app.float.list_padding_top'),
        bottom: $r('app.float.list_padding_bottom'),
        right: false 
          ? $r('app.float.list_edit_padding') 
          : $r('app.float.list_padding')
      })
      .height(false 
              ? $r('app.float.expanded_item_height') 
              : $r('app.float.list_item_height'))
      .width(CommonConstants.FULL_WIDTH)
      .opacity(
        10 === CommonConstants.SLIDER_MAX_VALUE ?
        CommonConstants.OPACITY : CommonConstants.NO_OPACITY
      )
      .borderRadius(CommonConstants.LIST_RADIUS)
      .animation({ duration: CommonConstants.DURATION })
      .backgroundColor(false ? $r('app.color.edit_blue') : Color.White)
    }
    .width(CommonConstants.FULL_WIDTH)
  }

  @Builder TargetItem() {
    Row() {
      Text(this.taskItem?.taskName)
        .fontSize($r('app.float.list_font'))
        .fontWeight(CommonConstants.FONT_WEIGHT)
        .fontColor($r('app.color.title_black_color'))
        .width(CommonConstants.TASK_NAME_WIDTH)
        .textAlign(TextAlign.Start)
        .maxLines(CommonConstants.MAX_LINES)
      Blank()
      Column() {
        Text(`10%`)
          .fontSize($r('app.float.list_font'))
          .fontWeight(CommonConstants.FONT_WEIGHT)
          .fontColor($r('app.color.title_black_color'))
        Row() {
          Text($r('app.string.latest_updateTime'))
            .opacityTextStyle()
          Text('2024-10-10 12:12:10')
            .opacityTextStyle()
        }
        .margin({ top: $r('app.float.text_margin') })
      }
      .alignItems(HorizontalAlign.End)
    }
    .width(CommonConstants.FULL_WIDTH)
  }
}

@Extend(Text) function opacityTextStyle() {
  .fontSize($r('app.float.text_font'))
  .fontColor($r('app.color.title_black_color'))
  .opacity(CommonConstants.OPACITY)
  .fontWeight(CommonConstants.FONT_WEIGHT)
}
  • 首页中渲染 TargetList 组件
// entry/src/main/ets/pages/Index.ets
import { CommonConstants } from '../common/constant/CommonConstant'
import TargetInformation from '../view/TargetInformation'

// 1.引入TargetList 相关模块
import TaskItemModel from '../viewModel/TaskItemModel'
import DataModel from '../viewModel/DataModel'
import TargetList from '../view/TargetList'

@Entry
@Component
struct Index {
  @State totalTasksNumber: number = 0
  @State completedTasksNumber: number = 0
  @State latestUpdateDate: string = CommonConstants.DEFAULT_PROGRESS_VALUE

  // 2.定义数据源
  @State targetData: Array<TaskItemModel> = DataModel.getData()

  build() {
    Column() {
      this.titleBar()
      TargetInformation({
        latestUpdateDate: this.latestUpdateDate,
        totalTasksNumber: this.totalTasksNumber,
        completedTasksNumber: this.completedTasksNumber
      })

      // 3.引入 TargetList 组件,传入数据
      TargetList({
        targetData: $targetData
      })
      .height(CommonConstants.LIST_BOARD_HEIGHT)
    }
    .width(CommonConstants.FULL_WIDTH)
    .height(CommonConstants.FULL_HEIGHT)
    .backgroundColor($r('app.color.index_background'))
  }

  @Builder
  titleBar() {
    Text($r('app.string.title'))
      .width(CommonConstants.TITLE_WIDTH)
      .height($r('app.float.title_height'))
      .fontSize($r('app.float.title_font'))
      .fontWeight(CommonConstants.FONT_WEIGHT_LARGE)
      .textAlign(TextAlign.Start)
      .margin({
        top: $r('app.float.title_margin'),
        bottom: $r('app.float.title_margin')
      })
  }
}
  • 实现TargetList 组件
// entry/src/main/ets/pages/view/TargetList.ets
import { CommonConstants } from "../common/constant/CommonConstant"
import TaskItemModel from '../viewModel/TaskItemModel'
import TargetListItem from "./TargetListItem"

@Component
export default struct TargetList {
  @Link targetData: Array<TaskItemModel>

  build() {
    Column() {
      Row() {
        Text($r('app.string.sub_goals'))
          .fontSize($r('app.float.secondary_title'))
          .fontWeight(CommonConstants.FONT_WEIGHT_LARGE)
          .fontColor($r('app.color.title_black_color'))
      }
      .width(CommonConstants.FULL_WIDTH)
      .height($r('app.float.history_line_height'))
      .padding({
        left: $r('app.float.list_padding'),
        right: $r('app.float.list_padding_right')
      })

      List({ space: CommonConstants.LIST_SPACE }) {
        ForEach(
          this.targetData, 
          (item: TaskItemModel, index: number | undefined) => {
            ListItem() {
              TargetListItem({
                taskItem: item
              })
            }
          }, (item: TaskItemModel) => JSON.stringify(item))
      }
      .edgeEffect(EdgeEffect.None)
      .margin({ top: $r('app.float.list_margin_top') })
      .width(CommonConstants.FULL_WIDTH)
      .height(CommonConstants.LIST_HEIGHT)
    }
    .width(CommonConstants.MAIN_BOARD_WIDTH)
    .height(CommonConstants.FULL_HEIGHT)
    .padding({ top: $r('app.float.operate_row_margin') })
  }
}

6. 添加任务子目标

6.1. 给 TargetList 添加 Button

// entry/src/main/ets/pages/view/TargetList.ets
import { CommonConstants } from "../common/constant/CommonConstant"
import TaskItemModel from '../viewModel/TaskItemModel'
import TargetListItem from "./TargetListItem"

@Component
export default struct TargetList {
  @Link targetData: Array<TaskItemModel>

  // 3. 接收父组件的添加事件
  onAddClick?: () => void

  build() {
    Column() {
      Row() {
        Text($r('app.string.sub_goals'))
          .fontSize($r('app.float.secondary_title'))
          .fontWeight(CommonConstants.FONT_WEIGHT_LARGE)
          .fontColor($r('app.color.title_black_color'))
      }
      .width(CommonConstants.FULL_WIDTH)
      .height($r('app.float.history_line_height'))
      .padding({
        left: $r('app.float.list_padding'),
        right: $r('app.float.list_padding_right')
      })

      List({ space: CommonConstants.LIST_SPACE }) {
        ForEach(this.targetData, (item: TaskItemModel, index: number | undefined) => {
          ListItem() {
            TargetListItem({
              taskItem: item
            })
          }
        }, (item: TaskItemModel) => JSON.stringify(item))
      }
      .edgeEffect(EdgeEffect.None)
      .margin({ top: $r('app.float.list_margin_top') })
      .width(CommonConstants.FULL_WIDTH)
      .height(CommonConstants.LIST_HEIGHT)

      // 1. "添加子目标"按钮
      Blank()

      Button($r('app.string.add_task'))
        .operateButtonStyle($r('app.color.main_blue'))
        .onClick(() => {
          if (this.onAddClick !== undefined) {
            this.onAddClick()
          }
        })
    }
    .width(CommonConstants.MAIN_BOARD_WIDTH)
    .height(CommonConstants.FULL_HEIGHT)
    .padding({ top: $r('app.float.operate_row_margin') })
  }
}

// 2. "添加子目标"按钮样式
@Extend(Button) function operateButtonStyle(color: Resource) {
  .width($r('app.float.button_width'))
  .height($r('app.float.button_height'))
  .fontSize($r('app.float.button_font'))
  .fontWeight(CommonConstants.FONT_WEIGHT)
  .fontColor(color)
  .backgroundColor($r('app.color.button_background'))
}

6.2. 首页处理

// entry/src/main/ets/pages/Index.ets
import { CommonConstants } from '../common/constant/CommonConstant'
import TargetInformation from '../view/TargetInformation'

import TaskItemModel from '../viewModel/TaskItemModel'
import DataModel from '../viewModel/DataModel'
import TargetList from '../view/TargetList'

// 3.实现 AddTargetDialog 并导入
import AddTargetDialog from '../view/AddTargetDialog'

// 5.导入 promptAction 模块
import { promptAction } from '@kit.ArkUI'

// 7. 导入 getCurrentTime
import getCurrentTime from '../common/utils/DateUtil'

@Entry
@Component
struct Index {
  @State totalTasksNumber: number = 0
  @State completedTasksNumber: number = 0
  @State latestUpdateDate: string = CommonConstants.DEFAULT_PROGRESS_VALUE

  @State targetData: Array<TaskItemModel> = DataModel.getData()

  // 2.定义 dialogController
  dialogController: CustomDialogController = new CustomDialogController({
    builder: AddTargetDialog({
      onClickOk: (value: string): void => this.saveTask(value)
    }),
    alignment: DialogAlignment.Bottom,
    offset: {
      dx: CommonConstants.DIALOG_OFFSET_X,
      dy: $r('app.float.dialog_offset_y')
    },
    customStyle: true,
    autoCancel: false
  })

  // 9. 定义 overAllProgressChanged
  @Provide @Watch('onProgressChanged') overAllProgressChanged: boolean = false

  build() {
    Column() {
      this.titleBar()
      TargetInformation({
        latestUpdateDate: this.latestUpdateDate,
        totalTasksNumber: this.totalTasksNumber,
        completedTasksNumber: this.completedTasksNumber
      })
      TargetList({
        targetData: $targetData,

        // 1. 传入 onAddClick 事件
        onAddClick: (): void => this.dialogController.open()
      })
        .height(CommonConstants.LIST_BOARD_HEIGHT)
    }
    .width(CommonConstants.FULL_WIDTH)
    .height(CommonConstants.FULL_HEIGHT)
    .backgroundColor($r('app.color.index_background'))
  }

  @Builder
  titleBar() {
    Text($r('app.string.title'))
      .width(CommonConstants.TITLE_WIDTH)
      .height($r('app.float.title_height'))
      .fontSize($r('app.float.title_font'))
      .fontWeight(CommonConstants.FONT_WEIGHT_LARGE)
      .textAlign(TextAlign.Start)
      .margin({
        top: $r('app.float.title_margin'),
        bottom: $r('app.float.title_margin')
      })
  }

  // 4. 实现保存功能
  saveTask(taskName: string) {
    if (taskName === '') {
      promptAction.showToast({
        message: $r('app.string.cannot_input_empty'),
        duration: CommonConstants.TOAST_TIME,
        bottom: CommonConstants.TOAST_MARGIN_BOTTOM
      })
      return
    }

    // 6. 在 DateModel 里实现 addData 方法(见下文),并调用
    DataModel.addData(new TaskItemModel(taskName, 0, getCurrentTime()))
    this.targetData = DataModel.getData()

    // 11. 设置 overAllProgressChanged 的值
    this.overAllProgressChanged = !this.overAllProgressChanged

    this.dialogController.close()
  }

  // 10. 实现 onProgressChanged
  onProgressChanged() {
    this.totalTasksNumber = this.targetData.length
    this.completedTasksNumber = this.targetData.filter((item: TaskItemModel) => {
      return item.progressValue === CommonConstants.SLIDER_MAX_VALUE
    }).length
    this.latestUpdateDate = getCurrentTime()
  }
}

6.3. 实现AddTargetDialog

// entry/src/main/ets/pages/view/AddTargetDialog.ets
import { CommonConstants } from '../common/constant/CommonConstant'

@CustomDialog
export default struct AddTargetDialog {
  @State subtaskName: string = ''
  private controller?: CustomDialogController
  onClickOk?: (value: string) => void

  build() {
    Column() {
      Text($r('app.string.add_task_dialog'))
        .width(CommonConstants.FULL_WIDTH)
        .fontSize($r('app.float.secondary_title'))
        .fontWeight(CommonConstants.FONT_WEIGHT)
        .fontColor($r('app.color.title_black_color'))
        .textAlign(TextAlign.Start)
      
      TextInput({ placeholder: $r('app.string.input_target_name')})
        .placeholderColor(Color.Grey)
        .placeholderFont({ size: $r('app.float.list_font')})
        .caretColor(Color.Blue)
        .backgroundColor($r('app.color.input_background'))
        .width(CommonConstants.FULL_WIDTH)
        .height(CommonConstants.DIALOG_INPUT_HEIGHT)
        .margin({ top: CommonConstants.DIALOG_INPUT_MARGIN })
        .fontSize($r('app.float.list_font'))
        .fontColor($r('app.color.title_black_color'))
        .onChange((value: string) => {
          this.subtaskName = value
        })
      Blank()
      Row() {
        Button($r('app.string.cancel_button'))
          .dialogButtonStyle()
          .onClick(() => {
            this.controller?.close()
          })
        Divider()
          .vertical(true)
        Button($r('app.string.confirm_button'))
          .dialogButtonStyle()
          .onClick(() => {
            if (this.onClickOk !== undefined) {
              this.onClickOk(this.subtaskName)
            }
          })
      }
      .width(CommonConstants.DIALOG_OPERATION_WIDTH)
      .height(CommonConstants.DIALOG_OPERATION_HEIGHT)
      .justifyContent(FlexAlign.SpaceBetween)
    }
    .padding($r('app.float.dialog_padding'))
    .height($r('app.float.dialog_height'))
    .width(CommonConstants.DIALOG_WIDTH)
    .borderRadius(CommonConstants.DIALOG_BORDER_RADIUS)
    .backgroundColor(Color.White)
  }
}

@Extend(Button) function dialogButtonStyle() {
  .fontSize($r('app.float.button_font'))
  .height($r('app.float.dialog_btn_height'))
  .width($r('app.float.dialog_btn_width'))
  .backgroundColor(Color.White)
  .fontColor($r('app.color.main_blue'))
}

6.4. 在 DateModel里实现addData方法

// entry/src/main/ets/viewModel/DataModel.ets
import TaskItemModel from './TaskItemModel'
import getCurrentTime from '../common/utils/DateUtil'
import Logger from '../common/utils/Logger'

// 2.定义 TAG
const TAG = '[DataModel]'

export class DataModel {

  private targetData: Array<TaskItemModel> = [
    new TaskItemModel('私域流量提升10%', 0, getCurrentTime()),
    new TaskItemModel('公域域流量提升10%', 10, getCurrentTime()),
    new TaskItemModel('活跃度提升20%', 20, getCurrentTime())
  ]

  getData(): Array<TaskItemModel> {
    return this.targetData
  }

  // 1. 添加 addData 函数
  addData(data: TaskItemModel) {
    if (!data) {
      Logger.error(TAG, 'addData error because data is: ' + data)
      return
    }
    this.targetData.push(data)
  }
}

export default new DataModel()

7. 更新任务子目标进展

7.1. 渲染列表内容

// entry/src/main/ets/pages/view/TargetListItem.ets
import TaskItemModel from '../viewModel/TaskItemModel'
import { CommonConstants } from '../common/constant/CommonConstant'

@Component
export default struct TargetListItem {
  private taskItem?: TaskItemModel

  // 1.定义进度和时间两个变量
  @State latestProgress?: number = 0
  @State updateDate?: string = ''

  // 2.通过 aboutToAppear 再次重写 进度和时间两个变量的值
  aboutToAppear(): void {
    this.latestProgress = this.taskItem?.progressValue
    this.updateDate = this.taskItem?.updateDate
  }

  build() {
    Stack({ alignContent: Alignment.Start }) {
      Column() {
        this.TargetItem()
      }
      .padding({
        left: $r('app.float.list_padding'),
        top: $r('app.float.list_padding_top'),
        bottom: $r('app.float.list_padding_bottom'),
        right: false 
          ? $r('app.float.list_edit_padding') : $r('app.float.list_padding')
      })
      .height(false 
              ? $r('app.float.expanded_item_height') 
              : $r('app.float.list_item_height'))
      .width(CommonConstants.FULL_WIDTH)
      .opacity(
        10 === CommonConstants.SLIDER_MAX_VALUE ?
        CommonConstants.OPACITY : CommonConstants.NO_OPACITY
      )
      .borderRadius(CommonConstants.LIST_RADIUS)
      .animation({ duration: CommonConstants.DURATION })
      .backgroundColor(false ? $r('app.color.edit_blue') : Color.White)
    }
    .width(CommonConstants.FULL_WIDTH)
  }

  @Builder TargetItem() {
    Row() {
      Text(this.taskItem?.taskName)
        .fontSize($r('app.float.list_font'))
        .fontWeight(CommonConstants.FONT_WEIGHT)
        .fontColor($r('app.color.title_black_color'))
        .width(CommonConstants.TASK_NAME_WIDTH)
        .textAlign(TextAlign.Start)
        .maxLines(CommonConstants.MAX_LINES)
      Blank()
      Column() {
        // 3.渲染进度和更新时间
        Text(`${this.latestProgress}%`)
          .fontSize($r('app.float.list_font'))
          .fontWeight(CommonConstants.FONT_WEIGHT)
          .fontColor($r('app.color.title_black_color'))
        Row() {
          Text($r('app.string.latest_updateTime'))
            .opacityTextStyle()
          Text(this.updateDate)
            .opacityTextStyle()
        }
        .margin({ top: $r('app.float.text_margin') })
      }
      .alignItems(HorizontalAlign.End)
    }
    .width(CommonConstants.FULL_WIDTH)
  }
}

@Extend(Text) function opacityTextStyle() {
  .fontSize($r('app.float.text_font'))
  .fontColor($r('app.color.title_black_color'))
  .opacity(CommonConstants.OPACITY)
  .fontWeight(CommonConstants.FONT_WEIGHT)
}

7.2. 实现单击子目标项伸缩效果

// entry/src/main/ets/view/TargetList.ets
import { CommonConstants } from "../common/constant/CommonConstant"
import TaskItemModel from '../viewModel/TaskItemModel'
import TargetListItem from "./TargetListItem"

@Component
export default struct TargetList {
  @Link targetData: Array<TaskItemModel>

  onAddClick?: () => void

  // 1.定义与编辑子目标进度有关的变量
  @State isEditMode: boolean = false
  @State clickIndex: number = CommonConstants.DEFAULT_CLICK_INDEX

  build() {
    Column() {
      Row() {
        Text($r('app.string.sub_goals'))
          .fontSize($r('app.float.secondary_title'))
          .fontWeight(CommonConstants.FONT_WEIGHT_LARGE)
          .fontColor($r('app.color.title_black_color'))
      }
      .width(CommonConstants.FULL_WIDTH)
      .height($r('app.float.history_line_height'))
      .padding({
        left: $r('app.float.list_padding'),
        right: $r('app.float.list_padding_right')
      })

      List({ space: CommonConstants.LIST_SPACE }) {
        ForEach(
          this.targetData, 
          (item: TaskItemModel, index: number | undefined) => {
            ListItem() {
              TargetListItem({
                taskItem: item,
  
                // 2.传递与编辑子目标进度相关的属性
                index: index,
                isEditMode: this.isEditMode,
                clickIndex: $clickIndex
              })
            }
          }, (item: TaskItemModel) => JSON.stringify(item))
      }
      .edgeEffect(EdgeEffect.None)
      .margin({ top: $r('app.float.list_margin_top') })
      .width(CommonConstants.FULL_WIDTH)
      .height(CommonConstants.LIST_HEIGHT)

      Blank()

      Button($r('app.string.add_task'))
        .operateButtonStyle($r('app.color.main_blue'))
        .onClick(() => {
          if (this.onAddClick !== undefined) {
            this.onAddClick()
          }
        })
    }
    .width(CommonConstants.MAIN_BOARD_WIDTH)
    .height(CommonConstants.FULL_HEIGHT)
    .padding({ top: $r('app.float.operate_row_margin') })
  }
}

@Extend(Button) function operateButtonStyle(color: Resource) {
  .width($r('app.float.button_width'))
  .height($r('app.float.button_height'))
  .fontSize($r('app.float.button_font'))
  .fontWeight(CommonConstants.FONT_WEIGHT)
  .fontColor(color)
  .backgroundColor($r('app.color.button_background'))
}
// entry/src/main/ets/view/TargetListItem.ets

import TaskItemModel from '../viewModel/TaskItemModel'
import { CommonConstants } from '../common/constant/CommonConstant'

@Component
export default struct TargetListItem {
  private taskItem?: TaskItemModel

  @State latestProgress?: number = 0
  @State updateDate?: string = ''

  // 3.定义与编辑子目标进度有关的变量
  @State isExpanded: boolean = false
  public index: number = 0
  @Prop isEditMode: boolean = false
  @Link @Watch('onClickIndexChanged') clickIndex: number

  aboutToAppear(): void {
    this.latestProgress = this.taskItem?.progressValue
    this.updateDate = this.taskItem?.updateDate
  }

  // 4.实现改变clickIndex后的回调
  onClickIndexChanged() {
    if (this.clickIndex !== this.index) {
      this.isExpanded = false
    }
  }

  build() {
    Stack({ alignContent: Alignment.Start }) {
      Column() {
        this.TargetItem()
      }
      .padding({
        left: $r('app.float.list_padding'),
        top: $r('app.float.list_padding_top'),
        bottom: $r('app.float.list_padding_bottom'),
        right: $r('app.float.list_padding')
      })

      // 6.编辑高度
      .height(this.isExpanded ? $r('app.float.expanded_item_height') : $r('app.float.list_item_height'))
      .width(CommonConstants.FULL_WIDTH)
      .opacity(
        10 === CommonConstants.SLIDER_MAX_VALUE ?
        CommonConstants.OPACITY : CommonConstants.NO_OPACITY
      )
      .borderRadius(CommonConstants.LIST_RADIUS)
      .animation({ duration: CommonConstants.DURATION })
      .backgroundColor(Color.White)

      // 5.实现点击Item展开子目标编辑slider
      .onClick(() => {
        if (!this.isEditMode) {
          animateTo({ duration: CommonConstants.DURATION }, () => {
            this.isExpanded = !this.isExpanded
          })
          this.clickIndex = this.index
        }
      })
    }
    .width(CommonConstants.FULL_WIDTH)
  }

  @Builder TargetItem() {
    Row() {
      Text(this.taskItem?.taskName)
        .fontSize($r('app.float.list_font'))
        .fontWeight(CommonConstants.FONT_WEIGHT)
        .fontColor($r('app.color.title_black_color'))
        .width(CommonConstants.TASK_NAME_WIDTH)
        .textAlign(TextAlign.Start)
        .maxLines(CommonConstants.MAX_LINES)
      Blank()
      Column() {
        Text(`${this.latestProgress}%`)
          .fontSize($r('app.float.list_font'))
          .fontWeight(CommonConstants.FONT_WEIGHT)
          .fontColor($r('app.color.title_black_color'))
        Row() {
          Text($r('app.string.latest_updateTime'))
            .opacityTextStyle()
          Text(this.updateDate)
            .opacityTextStyle()
        }
        .margin({ top: $r('app.float.text_margin') })
      }
      .alignItems(HorizontalAlign.End)
    }
    .width(CommonConstants.FULL_WIDTH)
  }
}

@Extend(Text) function opacityTextStyle() {
  .fontSize($r('app.float.text_font'))
  .fontColor($r('app.color.title_black_color'))
  .opacity(CommonConstants.OPACITY)
  .fontWeight(CommonConstants.FONT_WEIGHT)
}

7.3. 实现和引入ProgressEditPanel组件

// entry/src/main/ets/view/TargetListItem.ets=
import TaskItemModel from '../viewModel/TaskItemModel'
import { CommonConstants } from '../common/constant/CommonConstant'

// 2.实现和导入编辑子目标进度的相关的模块
import ProgressEditPanel from './ProgressEditPanel'

@Component
export default struct TargetListItem {
  private taskItem?: TaskItemModel

  @State latestProgress?: number = 0
  @State updateDate?: string = ''

  @State isExpanded: boolean = false
  public index: number = 0
  @Prop isEditMode: boolean = false
  @Link @Watch('onClickIndexChanged') clickIndex: number

  // 3.定义相关变量
  @Consume overAllProgressChanged: boolean
  @State sliderMode: number = CommonConstants.DEFAULT_SLIDER_MODE

  aboutToAppear(): void {
    this.latestProgress = this.taskItem?.progressValue
    this.updateDate = this.taskItem?.updateDate
  }

  onClickIndexChanged() {
    if (this.clickIndex !== this.index) {
      this.isExpanded = false
    }
  }

  build() {
    Stack({ alignContent: Alignment.Start }) {
      Column() {
        this.TargetItem()

        // 1.展开,显示slider
        if (this.isExpanded) {
          Blank()
          ProgressEditPanel({
            slidingProgress: this.latestProgress,
            onCancel: () => this.isExpanded = false,
            onClickOK: () => {},
            sliderMode: $sliderMode
          })
            // 4.添加动画
            .transition({
              scale: {
                x: CommonConstants.TRANSITION_ANIMATION_X,
                y: CommonConstants.TRANSITION_ANIMATION_Y
              }
            })
        }
      }
      .padding({
        left: $r('app.float.list_padding'),
        top: $r('app.float.list_padding_top'),
        bottom: $r('app.float.list_padding_bottom'),
        right: $r('app.float.list_padding')
      })

      .height(this.isExpanded 
              ? $r('app.float.expanded_item_height') 
              : $r('app.float.list_item_height'))
      .width(CommonConstants.FULL_WIDTH)
      .opacity(CommonConstants.NO_OPACITY)
      .borderRadius(CommonConstants.LIST_RADIUS)
      .animation({ duration: CommonConstants.DURATION })
      .backgroundColor(Color.White)

      .onClick(() => {
        if (!this.isEditMode) {
          animateTo({ duration: CommonConstants.DURATION }, () => {
            this.isExpanded = !this.isExpanded
          })
          this.clickIndex = this.index
        }
      })
    }
    .width(CommonConstants.FULL_WIDTH)
  }

  @Builder TargetItem() {
    Row() {
      Text(this.taskItem?.taskName)
        .fontSize($r('app.float.list_font'))
        .fontWeight(CommonConstants.FONT_WEIGHT)
        .fontColor($r('app.color.title_black_color'))
        .width(CommonConstants.TASK_NAME_WIDTH)
        .textAlign(TextAlign.Start)
        .maxLines(CommonConstants.MAX_LINES)
      Blank()
      Column() {
        Text(`${this.latestProgress}%`)
          .fontSize($r('app.float.list_font'))
          .fontWeight(CommonConstants.FONT_WEIGHT)
          .fontColor($r('app.color.title_black_color'))
        Row() {
          Text($r('app.string.latest_updateTime'))
            .opacityTextStyle()
          Text(this.updateDate)
            .opacityTextStyle()
        }
        .margin({ top: $r('app.float.text_margin') })
      }
      .alignItems(HorizontalAlign.End)
    }
    .width(CommonConstants.FULL_WIDTH)
  }
}

@Extend(Text) function opacityTextStyle() {
  .fontSize($r('app.float.text_font'))
  .fontColor($r('app.color.title_black_color'))
  .opacity(CommonConstants.OPACITY)
  .fontWeight(CommonConstants.FONT_WEIGHT)
}
// entry/src/main/ets/view/ProgressEditPanel.ets
import { CommonConstants } from '../common/constant/CommonConstant'

@Component
export default struct ProgressEditPanel {
  @Link sliderMode: number
  @Prop slidingProgress: number = 0
  onCancel?: () => void
  onClickOK?: (progress: number) => void

  build() {
    Column() {
      Row() {
        Slider({
          value: this.slidingProgress,
          min: CommonConstants.SLIDER_MIN_VALUE,
          max: CommonConstants.SLIDER_MAX_VALUE,
          style: SliderStyle.InSet,
          step: CommonConstants.SLIDER_STEP
        })
          .width(CommonConstants.SLIDER_INNER_WIDTH)
          .onChange((value: number, mode: SliderChangeMode) => {
            this.slidingProgress = Math.floor(value)
            this.sliderMode = mode
          })
        Text(`${this.slidingProgress}%`)
          .fontSize($r('app.float.progress_font'))
          .fontWeight(CommonConstants.FONT_WEIGHT)
          .fontColor($r('app.color.dialog_progress'))
          .textAlign(TextAlign.Center)
          .margin({ left: $r('app.float.slider_margin_left') })
      }
      .width(CommonConstants.SLIDER_WIDTH)
      .height(CommonConstants.SLIDER_HEIGHT)

      Row() {
        CustomButton({
          buttonText: $r('app.string.cancel_button')
        })
          .onClick(() => {
            if (this.onCancel !== undefined) {
              this.onCancel()
            }
          })
        CustomButton({
          buttonText: $r('app.string.confirm_button')
        })
          .onClick(() => {
            if (this.onClickOK !== undefined) {
              this.onClickOK(this.slidingProgress)
            }
          })
      }
      .margin({ top: CommonConstants.SLIDER_BUTTON_MARGIN })
      .width(CommonConstants.DIALOG_OPERATION_WIDTH)
      .justifyContent(FlexAlign.SpaceBetween)
    }
    .height($r('app.float.edit_panel_height'))
    .width(CommonConstants.FULL_WIDTH)
    .justifyContent(FlexAlign.End)
  }
}

@Component
struct CustomButton {
  @State buttonColor: Resource = $r('app.color.start_window_background')
  buttonText?: Resource

  build() {
    Text(this.buttonText)
      .dialogButtonStyle()
      .backgroundColor(this.buttonColor)
      .borderRadius(CommonConstants.LIST_RADIUS)
      .textAlign(TextAlign.Center)
      .onTouch((event?: TouchEvent) => {
        if (event !== undefined && event.type === TouchType.Down) {
          this.buttonColor = $r('app.color.custom_button_color')
        }
        if (event !== undefined && event.type === TouchType.Up) {
          this.buttonColor = $r('app.color.start_window_background')
        }
      })
  }
}

@Extend(Text) function dialogButtonStyle() {
  .fontSize($r('app.float.button_font'))
  .height($r('app.float.dialog_btn_height'))
  .width($r('app.float.dialog_btn_width'))
  .fontColor($r('app.color.main_blue'))
}

7.4. 存储子目标进度

// entry/src/main/ets/view/TargetListItem.ets
import TaskItemModel from '../viewModel/TaskItemModel'
import { CommonConstants } from '../common/constant/CommonConstant'

import ProgressEditPanel from './ProgressEditPanel'

// 1.导入相关模块
import getCurrentTime from '../common/utils/DateUtil'
import DataModel from '../viewModel/DataModel'

@Component
export default struct TargetListItem {
  private taskItem?: TaskItemModel

  @State latestProgress?: number = 0
  @State updateDate?: string = ''

  @State isExpanded: boolean = false
  public index: number = 0
  @Prop isEditMode: boolean = false
  @Link @Watch('onClickIndexChanged') clickIndex: number

  @State sliderMode: number = CommonConstants.DEFAULT_SLIDER_MODE

  // 3.定义 overAllProgressChanged 变量
  @Consume overAllProgressChanged: boolean

  aboutToAppear(): void {
    this.latestProgress = this.taskItem?.progressValue
    this.updateDate = this.taskItem?.updateDate
  }

  onClickIndexChanged() {
    if (this.clickIndex !== this.index) {
      this.isExpanded = false
    }
  }

  build() {
    Stack({ alignContent: Alignment.Start }) {
      Column() {
        this.TargetItem()

        if (this.isExpanded) {
          Blank()
          ProgressEditPanel({
            slidingProgress: this.latestProgress,
            onCancel: () => this.isExpanded = false,
            onClickOK:  (progress: number): void => {
              this.latestProgress = progress
              this.updateDate = getCurrentTime()

              //2.调用 updateProgress 方法
              let result = DataModel.updateProgress(
                this.index, 
                this.latestProgress, 
                this.updateDate
              )
              if (result) {
                this.overAllProgressChanged = !this.overAllProgressChanged
              }
              this.isExpanded = false
            },
            sliderMode: $sliderMode
          })
            .transition({
              scale: {
                x: CommonConstants.TRANSITION_ANIMATION_X,
                y: CommonConstants.TRANSITION_ANIMATION_Y
              }
            })
        }
      }
      .padding({
        left: $r('app.float.list_padding'),
        top: $r('app.float.list_padding_top'),
        bottom: $r('app.float.list_padding_bottom'),
        right: $r('app.float.list_padding')
      })

      .height(this.isExpanded 
              ? $r('app.float.expanded_item_height') 
              : $r('app.float.list_item_height'))
      .width(CommonConstants.FULL_WIDTH)
      .opacity(CommonConstants.NO_OPACITY)
      .borderRadius(CommonConstants.LIST_RADIUS)
      .animation({ duration: CommonConstants.DURATION })
      .backgroundColor(Color.White)

      .onClick(() => {
        if (!this.isEditMode) {
          animateTo({ duration: CommonConstants.DURATION }, () => {
            this.isExpanded = !this.isExpanded
          })
          this.clickIndex = this.index
        }
      })
    }
    .width(CommonConstants.FULL_WIDTH)
  }

  @Builder TargetItem() {
    Row() {
      Text(this.taskItem?.taskName)
        .fontSize($r('app.float.list_font'))
        .fontWeight(CommonConstants.FONT_WEIGHT)
        .fontColor($r('app.color.title_black_color'))
        .width(CommonConstants.TASK_NAME_WIDTH)
        .textAlign(TextAlign.Start)
        .maxLines(CommonConstants.MAX_LINES)
      Blank()
      Column() {
        Text(`${this.latestProgress}%`)
          .fontSize($r('app.float.list_font'))
          .fontWeight(CommonConstants.FONT_WEIGHT)
          .fontColor($r('app.color.title_black_color'))
        Row() {
          Text($r('app.string.latest_updateTime'))
            .opacityTextStyle()
          Text(this.updateDate)
            .opacityTextStyle()
        }
        .margin({ top: $r('app.float.text_margin') })
      }
      .alignItems(HorizontalAlign.End)
    }
    .width(CommonConstants.FULL_WIDTH)
  }
}

@Extend(Text) function opacityTextStyle() {
  .fontSize($r('app.float.text_font'))
  .fontColor($r('app.color.title_black_color'))
  .opacity(CommonConstants.OPACITY)
  .fontWeight(CommonConstants.FONT_WEIGHT)
}
// entry/src/main/ets/viewModel/DataModel.ets
import TaskItemModel from './TaskItemModel'
import getCurrentTime from '../common/utils/DateUtil'
import Logger from '../common/utils/Logger'

const TAG = '[DataModel]'

export class DataModel {

  private targetData: Array<TaskItemModel> = [
    new TaskItemModel('私域流量提升10%', 0, getCurrentTime()),
    new TaskItemModel('公域域流量提升10%', 10, getCurrentTime()),
    new TaskItemModel('活跃度提升20%', 20, getCurrentTime())
  ]

  getData(): Array<TaskItemModel> {
    return this.targetData
  }

  addData(data: TaskItemModel) {
    if (!data) {
      Logger.error(TAG, 'addData error because data is: ' + data)
      return
    }
    this.targetData.push(data)
  }

  updateProgress(
    index: number, 
    updateValue: number, 
    updateDate: string
  ): boolean {
    if (!this.targetData[index]) {
      return false
    }
    this.targetData[index].progressValue = updateValue
    this.targetData[index].updateDate = updateDate
    return true
  }
}

export default new DataModel()

8. 编辑子目标列表

8.1. 编辑、全选和取消全选

// entry/src/main/ets/view/TargetList.ets
import { CommonConstants } from "../common/constant/CommonConstant"
import TaskItemModel from '../viewModel/TaskItemModel'
import TargetListItem from "./TargetListItem"

@Component
export default struct TargetList {
  @Link targetData: Array<TaskItemModel>

  onAddClick?: () => void

  @State isEditMode: boolean = false
  @State clickIndex: number = CommonConstants.DEFAULT_CLICK_INDEX

  // 3.定义变量
  @State selectAll: boolean = false
  @State selectArray: Array<boolean> = []

  build() {
    Column() {
      Row() {
        Text($r('app.string.sub_goals'))
          .fontSize($r('app.float.secondary_title'))
          .fontWeight(CommonConstants.FONT_WEIGHT_LARGE)
          .fontColor($r('app.color.title_black_color'))

        // 1.添加编辑按钮
        Blank()
        if (this.targetData.length > 0) {
          if (this.isEditMode) {
            Text($r('app.string.cancel_button'))
              // 2.添加 operateTextStyle
              .operateTextStyle($r('app.color.main_blue'))
              .margin({ left: $r('app.float.operate_button_margin') })
              .onClick(() => {
                // 3.设置变量
                this.selectAll = false
                this.isEditMode = false

                // 4.设置全选或取消
                this.selectAllOrCancel(false)
              })
            Text($r('app.string.select_all_button'))
              .operateTextStyle($r('app.color.main_blue'))
              .margin({
                left: $r('app.float.operate_button_margin')
              })
            Checkbox()
              // 5.判断是否全选
              .select(this.isSelectAll())
              .selectedColor($r('app.color.main_blue'))
              .width(CommonConstants.CHECKBOX_WIDTH)
              .onClick(() => {
                this.selectAll = !this.selectAll
                this.selectAllOrCancel(this.selectAll)
              })
          } else {
            Text($r('app.string.edit_button'))
              .operateTextStyle($r('app.color.main_blue'))
              .onClick(() => {
                this.isEditMode = true
                this.selectAllOrCancel(false)
              })
          }
        }
      }
      .width(CommonConstants.FULL_WIDTH)
      .height($r('app.float.history_line_height'))
      .padding({
        left: $r('app.float.list_padding'),
        right: $r('app.float.list_padding_right')
      })

      List({ space: CommonConstants.LIST_SPACE }) {
        ForEach(
          this.targetData, 
          (item: TaskItemModel, index: number | undefined) => {
            ListItem() {
              TargetListItem({
                taskItem: item,
                index: index,
                isEditMode: this.isEditMode,
                clickIndex: $clickIndex
  
                // 6.传递数组
                selectArr: $selectArray
              })
            }
          }, (item: TaskItemModel) => JSON.stringify(item))
      }
      .edgeEffect(EdgeEffect.None)
      .margin({ top: $r('app.float.list_margin_top') })
      .width(CommonConstants.FULL_WIDTH)
      .height(CommonConstants.LIST_HEIGHT)

      Blank()

      Button($r('app.string.add_task'))
        .operateButtonStyle($r('app.color.main_blue'))
        .onClick(() => {
          if (this.onAddClick !== undefined) {
            this.onAddClick()
          }
        })
    }
    .width(CommonConstants.MAIN_BOARD_WIDTH)
    .height(CommonConstants.FULL_HEIGHT)
    .padding({ top: $r('app.float.operate_row_margin') })
  }

  // 4.设置全选或取消
  selectAllOrCancel(selectStatus: boolean) {
    let newSelectArray: Array<boolean> = []
    this.targetData.forEach(() => {
      newSelectArray.push(selectStatus)
    })
    this.selectArray = newSelectArray
  }


  // 5.判断是否全选
  isSelectAll(): boolean {
    if (this.selectArray.length === 0) {
      return false
    }
    let deSelectCount: Length = this.selectArray.filter(
      (selected: boolean) => selected === false
    ).length
    if (deSelectCount === 0) {
      this.selectAll = true
      return true
    }
    this.selectAll = false
    return false
  }
}

@Extend(Button) function operateButtonStyle(color: Resource) {
  .width($r('app.float.button_width'))
  .height($r('app.float.button_height'))
  .fontSize($r('app.float.button_font'))
  .fontWeight(CommonConstants.FONT_WEIGHT)
  .fontColor(color)
  .backgroundColor($r('app.color.button_background'))
}

@Extend(Text) function operateTextStyle(color: Resource) {
  .fontSize($r('app.float.text_button_font'))
  .fontColor(color)
  .lineHeight($r('app.float.text_line_height'))
  .fontWeight(CommonConstants.FONT_WEIGHT)
}
// entry/src/main/ets/view/TargetListItem.ets
import TaskItemModel from '../viewModel/TaskItemModel'
import { CommonConstants } from '../common/constant/CommonConstant'

import ProgressEditPanel from './ProgressEditPanel'

import getCurrentTime from '../common/utils/DateUtil'
import DataModel from '../viewModel/DataModel'

@Component
export default struct TargetListItem {
  private taskItem?: TaskItemModel

  @State latestProgress?: number = 0
  @State updateDate?: string = ''

  @State isExpanded: boolean = false
  public index: number = 0
  @Prop isEditMode: boolean = false
  @Link @Watch('onClickIndexChanged') clickIndex: number

  @State sliderMode: number = CommonConstants.DEFAULT_SLIDER_MODE

  @Consume overAllProgressChanged: boolean

  // 1.定义selectArr
  @Link selectArr: Array<boolean>

  aboutToAppear(): void {
    this.latestProgress = this.taskItem?.progressValue
    this.updateDate = this.taskItem?.updateDate
  }

  onClickIndexChanged() {
    if (this.clickIndex !== this.index) {
      this.isExpanded = false
    }
  }

  build() {
    Stack({ alignContent: Alignment.Start }) {
      Column() {
        this.TargetItem()

        if (this.isExpanded) {
          Blank()
          ProgressEditPanel({
            slidingProgress: this.latestProgress,
            onCancel: () => this.isExpanded = false,
            onClickOK:  (progress: number): void => {
              this.latestProgress = progress
              this.updateDate = getCurrentTime()
              let result = DataModel.updateProgress(
                this.index, 
                this.latestProgress, 
                this.updateDate
              )
              if (result) {
                this.overAllProgressChanged = !this.overAllProgressChanged
              }
              this.isExpanded = false
            },
            sliderMode: $sliderMode
          })
            .transition({
              scale: {
                x: CommonConstants.TRANSITION_ANIMATION_X,
                y: CommonConstants.TRANSITION_ANIMATION_Y
              }
            })
        }
      }
      .padding({
        left: $r('app.float.list_padding'),
        top: $r('app.float.list_padding_top'),
        bottom: $r('app.float.list_padding_bottom'),

        // 2.修改 right
        right: this.isEditMode 
          ? $r('app.float.list_edit_padding') 
          : $r('app.float.list_padding')
      })
      .height(this.isExpanded 
              ? $r('app.float.expanded_item_height') 
              : $r('app.float.list_item_height'))
      .width(CommonConstants.FULL_WIDTH)

      // 3.修改 opacity
      .opacity(
        this.latestProgress === CommonConstants.SLIDER_MAX_VALUE ?
        CommonConstants.OPACITY : CommonConstants.NO_OPACITY
      )
      .borderRadius(CommonConstants.LIST_RADIUS)
      .animation({ duration: CommonConstants.DURATION })

      //4.修改 backgroundColor
      .backgroundColor(this.selectArr[this.index] 
                       ? $r('app.color.edit_blue') 
                       : Color.White)
      .onClick(() => {
        if (!this.isEditMode) {
          animateTo({ duration: CommonConstants.DURATION }, () => {
            this.isExpanded = !this.isExpanded
          })
          this.clickIndex = this.index
        }
      })

      // 5.添加编辑功能
      if (this.isEditMode) {
        Row() {
          Checkbox()
            .select(this.selectArr[this.index])
            .selectedColor($r('app.color.main_blue'))
            .width(CommonConstants.CHECKBOX_WIDTH)
            .margin({ right: $r('app.float.list_padding') })
            .onChange((isCheck: boolean) => {
              this.selectArr[this.index] = isCheck;
            })
        }
        .width(CommonConstants.FULL_WIDTH)
        .justifyContent(FlexAlign.End)
      }
    }
    .width(CommonConstants.FULL_WIDTH)
  }

  @Builder TargetItem() {
    Row() {
      Text(this.taskItem?.taskName)
        .fontSize($r('app.float.list_font'))
        .fontWeight(CommonConstants.FONT_WEIGHT)
        .fontColor($r('app.color.title_black_color'))
        .width(CommonConstants.TASK_NAME_WIDTH)
        .textAlign(TextAlign.Start)
        .maxLines(CommonConstants.MAX_LINES)
      Blank()
      Column() {
        Text(`${this.latestProgress}%`)
          .fontSize($r('app.float.list_font'))
          .fontWeight(CommonConstants.FONT_WEIGHT)
          .fontColor($r('app.color.title_black_color'))
        Row() {
          Text($r('app.string.latest_updateTime'))
            .opacityTextStyle()
          Text(this.updateDate)
            .opacityTextStyle()
        }
        .margin({ top: $r('app.float.text_margin') })
      }
      .alignItems(HorizontalAlign.End)
    }
    .width(CommonConstants.FULL_WIDTH)
  }
}

@Extend(Text) function opacityTextStyle() {
  .fontSize($r('app.float.text_font'))
  .fontColor($r('app.color.title_black_color'))
  .opacity(CommonConstants.OPACITY)
  .fontWeight(CommonConstants.FONT_WEIGHT)
}

8.2. 删除

// entry/src/main/ets/view/TargetList.ets
import { CommonConstants } from "../common/constant/CommonConstant"
import TaskItemModel from '../viewModel/TaskItemModel'
import TargetListItem from "./TargetListItem"

// 3. 删除操作, 导入模块
import DataModel from '../viewModel/DataModel'

@Component
export default struct TargetList {
  @Link targetData: Array<TaskItemModel>
  onAddClick?: () => void
  @State isEditMode: boolean = false
  @State clickIndex: number = CommonConstants.DEFAULT_CLICK_INDEX
  @State selectAll: boolean = false
  @State selectArray: Array<boolean> = []

  // 5.更新总体进度,消费 overAllProgressChanged
  @Consume overAllProgressChanged: boolean

  build() {
    Column() {
      Row() {
        Text($r('app.string.sub_goals'))
          .fontSize($r('app.float.secondary_title'))
          .fontWeight(CommonConstants.FONT_WEIGHT_LARGE)
          .fontColor($r('app.color.title_black_color'))

        Blank()
        if (this.targetData.length > 0) {
          if (this.isEditMode) {
            Text($r('app.string.cancel_button'))
              .operateTextStyle($r('app.color.main_blue'))
              .margin({ left: $r('app.float.operate_button_margin') })
              .onClick(() => {
                this.selectAll = false
                this.isEditMode = false
                this.selectAllOrCancel(false)
              })
            Text($r('app.string.select_all_button'))
              .operateTextStyle($r('app.color.main_blue'))
              .margin({
                left: $r('app.float.operate_button_margin')
              })
            Checkbox()
              .select(this.isSelectAll())
              .selectedColor($r('app.color.main_blue'))
              .width(CommonConstants.CHECKBOX_WIDTH)
              .onClick(() => {
                this.selectAll = !this.selectAll
                this.selectAllOrCancel(this.selectAll)
              })
          } else {
            Text($r('app.string.edit_button'))
              .operateTextStyle($r('app.color.main_blue'))
              .onClick(() => {
                this.isEditMode = true
                this.selectAllOrCancel(false)
              })
          }
        }
      }
      .width(CommonConstants.FULL_WIDTH)
      .height($r('app.float.history_line_height'))
      .padding({
        left: $r('app.float.list_padding'),
        right: $r('app.float.list_padding_right')
      })

      List({ space: CommonConstants.LIST_SPACE }) {
        ForEach(this.targetData, (item: TaskItemModel, index: number | undefined) => {
          ListItem() {
            TargetListItem({
              taskItem: item,
              index: index,
              isEditMode: this.isEditMode,
              clickIndex: $clickIndex,
              selectArr: $selectArray
            })
          }
        }, (item: TaskItemModel) => JSON.stringify(item))
      }
      .edgeEffect(EdgeEffect.None)
      .margin({ top: $r('app.float.list_margin_top') })
      .width(CommonConstants.FULL_WIDTH)
      .height(CommonConstants.LIST_HEIGHT)

      Blank()

      // 1.添加删除按钮
      if (this.isEditMode) {
        Button($r('app.string.delete_button'))
          // 2.删除是否可用(opacity & enabled)
          .opacity(this.isSelectRows() 
                   ? CommonConstants.NO_OPACITY 
                   : CommonConstants.OPACITY)
          .enabled(this.isSelectRows() ? true : false)

          .operateButtonStyle($r('app.color.main_red'))
          .onClick(() => {
            // 3.删除操作
            this.deleteSelected()
            this.selectAllOrCancel(false)
            this.selectAll = false
          })
      } else {
        Button($r('app.string.add_task'))
          .operateButtonStyle($r('app.color.main_blue'))
          .onClick(() => {
            if (this.onAddClick !== undefined) {
              this.onAddClick()
            }
          })
      }
    }
    .width(CommonConstants.MAIN_BOARD_WIDTH)
    .height(CommonConstants.FULL_HEIGHT)
    .padding({ top: $r('app.float.operate_row_margin') })
  }

  selectAllOrCancel(selectStatus: boolean) {
    let newSelectArray: Array<boolean> = []
    this.targetData.forEach(() => {
      newSelectArray.push(selectStatus)
    })
    this.selectArray = newSelectArray
  }

  isSelectAll(): boolean {
    if (this.selectArray.length === 0) {
      return false
    }
    let deSelectCount: Length = this.selectArray.filter(
      (selected: boolean) => selected === false
    ).length
    if (deSelectCount === 0) {
      this.selectAll = true
      return true
    }
    this.selectAll = false
    return false
  }

  // 2.删除是否可用opacity
  isSelectRows(): boolean {
    return this.selectArray.filter(
      (selected: boolean) => selected === true
    ).length !== 0
  }

  // 3.删除操作
  deleteSelected() {
    // 4.实现 deleteData
    DataModel.deleteData(this.selectArray)
    this.targetData = DataModel.getData()

    // 5.更新总体进度
    this.overAllProgressChanged = !this.overAllProgressChanged
    this.isEditMode = false
  }
}

@Extend(Button)
function operateButtonStyle(color: Resource) {
  .width($r('app.float.button_width'))
  .height($r('app.float.button_height'))
  .fontSize($r('app.float.button_font'))
  .fontWeight(CommonConstants.FONT_WEIGHT)
  .fontColor(color)
  .backgroundColor($r('app.color.button_background'))
}

@Extend(Text)
function operateTextStyle(color: Resource) {
  .fontSize($r('app.float.text_button_font'))
  .fontColor(color)
  .lineHeight($r('app.float.text_line_height'))
  .fontWeight(CommonConstants.FONT_WEIGHT)
}
// entry/src/main/ets/viewModel/DataModel.ets

import TaskItemModel from './TaskItemModel'
import getCurrentTime from '../common/utils/DateUtil'
import Logger from '../common/utils/Logger'
import { CommonConstants } from '../common/constant/CommonConstant'

const TAG = '[DataModel]'

export class DataModel {

  private targetData: Array<TaskItemModel> = [
    new TaskItemModel('私域流量提升10%', 0, getCurrentTime()),
    new TaskItemModel('公域域流量提升10%', 10, getCurrentTime()),
    new TaskItemModel('活跃度提升20%', 20, getCurrentTime())
  ]

  getData(): Array<TaskItemModel> {
    return this.targetData
  }

  addData(data: TaskItemModel) {
    if (!data) {
      Logger.error(TAG, 'addData error because data is: ' + data)
      return
    }
    this.targetData.push(data)
  }

  updateProgress(
    index: number, 
    updateValue: number, 
    updateDate: string
  ): boolean {
    if (!this.targetData[index]) {
      return false
    }
    this.targetData[index].progressValue = updateValue
    this.targetData[index].updateDate = updateDate
    return true
  }

  // 4.实现 deleteData
  deleteData(selectArr: Array<boolean>) {
    if (!selectArr) {
      Logger.error(TAG, 'Failed to delete data because selectArr is ' 
                   + selectArr)
    }
    let dataLen = this.targetData.length - CommonConstants.ONE_TASK
    for (let i = dataLen; i >= 0; i--) {
      if (selectArr[i]) {
        this.targetData.splice(i, CommonConstants.ONE_TASK)
      }
    }
  }
}

export default new DataModel()

9. 代码与视频教程

完整案例代码与视频教程请参见:

代码:Code-01.zip。

视频:《制作目标管理工具》。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐