HarmonyOS Next 办公应用:可变日历组件的开发实现(一)

93 阅读2分钟

HarmonyOS Next 办公应用:可变日历组件的开发实现

概述

在 HarmonyOS Next 办公类应用开发中,实现一个灵活且功能丰富的日历组件是常见需求。下面将介绍如何构建一个可变日历组件,支持日期选择、月份切换等功能。

核心代码功能及对应代码段

1. 日期数据类定义

定义了 LunarDateCJDateItem 类,用于处理阴历日期和日期项。

@Observed
class LunarDate {
  public year?: number
  public month?: number
  public day?: number
  public isLeapMonth?: boolean
}

@Observed
export class CJDateItem {
  fullYear: number = 0
  month: number = 0
  date: number = 0
  week?: number
  time?: number
  lunarDate?: LunarDate
  isPre?: boolean
  isNext?: boolean
  isToday?: boolean
  constructor(date: Date, isPre?: boolean, isNext?: boolean) {
    if (date) {
      this.fullYear = date.getFullYear()
      this.month = date.getMonth()
      this.date = date.getDate()
      this.week = date.getDay()
      this.time = date.getTime()
    }
    this.isPre = isPre
    this.isNext = isNext
  }
  // 日期比较方法
  equalDay(other: CJDateItem | number): boolean {... }
  compareDay(other: CJDateItem | number): number {... }
  bigThan(other: CJDateItem | number, hasEqual: boolean = false) {... }
  smallThan(other: CJDateItem | number, hasEqual: boolean = false) {... }
}
2. 可变日历组件

VariableCalendar 组件是核心组件,负责日历的渲染和交互。

@Entry
@Component
export struct VariableCalendar {
  @State @Watch('WatchTitle') title: string = ''
  @State hasPre: boolean = false
  @State hasNext: boolean = false
  @State @Watch('WatchSelectItem') selectItem: CJDateItem = new CJDateItem(new Date())
  // 其他状态变量...

  aboutToAppear() {
    this.initAttr()
    if (!this.selectItem) {
      let temp = new CJDateItem(this.today)
      this.selectItem = temp
    }
    this.calcDatas()
  }

  initAttr() {
    // 初始化日期范围
    this.today = new Date(
      this.today.getFullYear(),
      this.today.getMonth(),
      this.today.getDate(),
    )
    if (!this.startDate) {
      this.startDate = new Date(1970, 0, 1)
    } else {
      this.startDate = new Date(this.startDate.getFullYear(), this.startDate.getMonth(), this.startDate.getDate())
    }
    if (!this.endDate) {
      this.endDate = new Date(this.today.getFullYear() + 10, 11, 31)
    } else {
      this.endDate = new Date(this.endDate.getFullYear(), this.endDate.getMonth(), this.endDate.getDate())
    }
  }

  preMonth() {
    // 切换到上一个月
    this.dates = []
    this.startDay.setMonth(this.startDay.getMonth() - 1)
    this.calcDatas()
  }

  nextMonth() {
    // 切换到下一个月
    this.dates = []
    this.startDay.setMonth(this.startDay.getMonth() + 1)
    this.calcDatas()
  }

  calcDatas() {
    // 计算当前月份的日期数据
    const startDay = this.startDay
    this.currMonth = startDay.getMonth()
    this.currYear = startDay.getFullYear()
    this.title = `${startDay.getFullYear()}${startDay.getMonth() + 1}月`
    startDay.setDate(1)
    // 判断是否有上一个月和下一个月
    this.hasPre = startDay.getFullYear() > this.startDate.getFullYear() ||
      (startDay.getFullYear() === this.startDate.getFullYear() && startDay.getMonth() > this.startDate.getMonth())
    this.hasNext = startDay.getFullYear() < this.endDate.getFullYear() ||
      (startDay.getFullYear() === this.endDate.getFullYear() && startDay.getMonth() < this.endDate.getMonth())
    // 计算日期数据
    let endDay: Date = new Date(
      startDay.getFullYear(),
      startDay.getMonth() + 1,
      0, 23, 59, 59)
    let tempDate: Date = new Date(
      startDay.getFullYear(),
      startDay.getMonth(),
      startDay.getDate()
    )
    const count = endDay.getDate()
    const preCount = startDay.getDay()
    const nextCount = 6 - endDay.getDay()
    const finilCount = count + preCount + nextCount
    tempDate.setDate(tempDate.getDate() - preCount)
    for (let index = 0; index < finilCount; index++) {
      let item = new CJDateItem(
        tempDate,
        (index < preCount ? true : false) || this.startDate.getTime() > tempDate.getTime(),
        (index >= preCount + count ? true : false) || this.endDate.getTime() < tempDate.getTime()
      )
      this.dates.push(item)
      tempDate.setDate(tempDate.getDate() + 1)
    }
  }

  @Builder
  CusTopLayout() {
    // 日历顶部布局,包含月份切换按钮和标题
    Row() {
      Column() {
        Image($r('sys.media.ohos_ic_public_arrow_left'))
          .width(28)
          .fillColor(this.hasPre ? '#252a34' : '#9E9E9E')
          .aspectRatio(1)
      }
      .justifyContent(FlexAlign.Center)
      .height('100%')
      .aspectRatio(1)
      .onClick(() => {
        if (this.hasPre) {
          this.preMonth()
        }
      })
      Blank()
      Row() {
        Text(this.title)
          .fontSize(18)
          .fontColor('#252a34')
      }
      Blank()
      Column() {
        Image($r('sys.media.ohos_ic_public_arrow_right'))
          .fillColor(this.hasNext ? '#252a34' : '#9E9E9E')
          .width(28)
          .aspectRatio(1)
      }
      .justifyContent(FlexAlign.Center)
      .height('100%')
      .aspectRatio(1)
      .onClick(() => {
        if (this.hasNext) {
          this.nextMonth()
        }
      })
    }
    .width('100%')
    .height(50)
  }

  build() {
    Column() {
      Column() {
        this.CusTopLayout()
        Row() {
          ForEach(this.weeks, (item: string, index) => {
            Text(item)
              .textAlign(TextAlign.Center)
              .fontColor('#9E9E9E')
              .fontSize(13)
              .layoutWeight(1)
          })
        }
        .height(40)
      }
      .zIndex(99)
      .backgroundColor(Color.White)
      Column() {
        Flex({ wrap: FlexWrap.Wrap }) {
          ForEach(this.dates, (item: CJDateItem, index: number) => {
            CalenderCell({
              item: item,
              today: this.today.getTime(),
              selectItem: $selectItem,
              hasPre: this.hasPre,
              hasNext: this.hasNext,
              cellClick: (item: CJDateItem) => {
                if (item.isPre) {
                  this.preMonth()
                } else if (item.isNext) {
                  this.nextMonth()
                }
              }
            })
              .width(`14.28%`)
              .onAreaChange((oldValue: Area, newValue: Area) => {
                if (item.equalDay(this.selectItem)) {
                  this.currentYOffset = Number(newValue.position.y);
                }
              })
          }, (item: CJDateItem) => JSON.stringify(item))
        }
        .width('100%')
        .height(this.calendarHeight)
        .offset({ x: this.offsetX, y: this.offsetY })
        .backgroundColor(Color.White)
        .parallelGesture(
          PanGesture(this.panOption)
            .onActionUpdate((event: GestureEvent) => {
              if (event) {
                this.offsetX = event.offsetX;
              }
            })
            .onActionEnd((event: GestureEvent) => {
              if (event) {
                animateTo({
                  curve: Curve.Smooth
                }, () => {
                  if (Number(event.offsetX) > 160) {
                    this.preMonth();
                    this.offsetX = 0;
                  } else {
                    this.offsetX = 0;
                  }
                  if (event.offsetX < -160) {
                    this.nextMonth();
                    this.offsetX = 0;
                  } else {
                    this.offsetX = 0;
                  }
                })
              }
            })
        )
      }
      .zIndex(1)
    }
    .width('100%')
  }
}