鸿蒙next 自定义日历组件

效果图预览

20250124-113957

使用说明

1.选择日期左右箭头,实现每月日历切换,示例中超出当前月份,禁止进入下一月,可在代码更改

2.日历中显示当前选择的日期,选中的日期颜色可自定义

3.日历中可展示历史记录作为数据存储标志

4.当前页面选择的日期可在本页面保存状态

复制代码
可根据自己需求,对日历组件进行更改,将代码拷贝到DevEco Studio 可直接运行使用

日历组件使用代码

TypeScript 复制代码
@Entry
@Component
struct TEST {
  @State currentSelectDate: string = calendarUtils.getCurrentDay(); // 当前读取数据日期,用户数据获取
  private historyDateArray: string[] =
    [calendarUtils.getPreviousDay(calendarUtils.getCurrentDay()), calendarUtils.getCurrentDay()];
  private calendarController: CustomDialogController | null = new CustomDialogController({
    builder: CustomCalendar({
      currentSelectDate: this.currentSelectDate,
      defaultSelectDates: this.historyDateArray,
      colors: ["#fed2cf", "#ed553c", "#f74d33", "#fcfaff"],
      cancel: () => {
        this.calendarController?.close();
      },
      confirm: (date: Date) => {
        this.selectCalendarConfirm(date)
      }
    }),
    autoCancel: true,
    alignment: DialogAlignment.BottomEnd,
    offset: { dx: -8, dy: -20 },
    gridCount: 4,
    showInSubWindow: true,
    width: 359,
    isModal: true,
    customStyle: false,
    cornerRadius: 16,
  })

  // 选择日历确认框
  selectCalendarConfirm(date: Date) {
    let dateTime = new Date(date);
    let year = dateTime.getFullYear();
    let month = dateTime.getMonth() + 1;
    let day = dateTime.getDate();
    let strDate = `${year}-${month}-${day}`;
    this.currentSelectDate = strDate;
    let selectIndex = calendarUtils.getDaysFromDate(strDate);
    this.calendarController?.close();
  }

  build() {
    Column() {

      Button("日历组件").onClick(() => {
        this.calendarController?.open();
      })
    }.width('100%').height('100%')
  }
}

日历组件代码

TypeScript 复制代码
import { calendarUtils } from "../../utlis/calendarUtils";


interface monthType {
  defaultData: boolean,
  value: number
}

// 日历
@CustomDialog
export struct CustomCalendar {
  controller?: CustomDialogController
  @State selectedDate: Date = new Date();
  @State isDateSelected: boolean = false;
  @State currentMonth: number = new Date().getMonth(); // 当前选择的月数
  @State defaultMonth: number = new Date().getMonth(); // 默认月数
  @State currentYear: number = new Date().getFullYear();
  @Prop defaultSelectDates: string[] = []; // 默认历史数据
  @State defaultYear: number = new Date().getFullYear();
  @Prop currentSelectDate: string = calendarUtils.getCurrentDay();
  @Prop colors: string[] = ["#fed2cf", "#ed553c", "#f74d33", "#fcfaff"]; // 0 默认背景 1 背景默认字体颜色 2 选中背景 3 选中字体颜色
  @State monthDays: monthType[] = []; // 月份天数
  cancel: () => void = () => {
  }
  confirm: (date: Date) => void = () => {
  }

  aboutToAppear() {
    console.log("currentSelectDate===>", this.currentSelectDate)
    this.onDefaultDataSelect(this.currentSelectDate);
    let days = this.getDaysInMonth();
    console.log("days===>", JSON.stringify(days))
    this.monthDays = [...days]
  }

  build() {
    Column() {
      Row({ space: 30 }) {
        Image($r('app.media.rightArrow'))
          .width('24vp')
          .height('24vp')
          .padding({
            left: 8,
            right: 8,
            top: 4,
            bottom: 4
          })
          .rotate({
            x: 0,
            y: 0,
            z: 90,
            centerX: '50%',
            centerY: '50%',
            angle: 180
          })
          .onClick(() => this.onMonthChange(-1))
        Text(`${this.currentYear}年${this.currentMonth + 1 >= 10 ? this.currentMonth + 1 :
          '0' + (this.currentMonth + 1)}月`)
          .fontSize(20)
          .fontWeight(FontWeight.Bold)
          .margin({ left: 15, right: 15 })

        if (this.defaultYear != this.currentYear || this.currentMonth != this.defaultMonth) {
          Image($r('app.media.rightArrow'))
            .width('24vp')
            .height('24vp')
            .padding({
              left: 8,
              right: 8,
              top: 4,
              bottom: 4
            })
            .onClick(() => this.onMonthChange(1))
        } else {
          Image($r('app.media.rightArrowGray'))
            .width('24vp')
            .height('24vp')
            .padding({
              left: 8,
              right: 8,
              top: 4,
              bottom: 4
            })
        }

      }
      .width('100%')
      .justifyContent(FlexAlign.Center)
      .margin({ top: 20, bottom: 30 })

      // 星期标题
      Row() {
        ForEach(['日', '一', '二', '三', '四', '五', '六'], (day: string) => {
          Text(day)
            .textAlign(TextAlign.Center)
            .fontSize(18)
            .fontColor('#999999')
        }, (day: string) => day)
      }
      .width('93%')
      .margin({ bottom: 10 })
      .justifyContent(FlexAlign.SpaceBetween)

      Grid() {
        ForEach(this.monthDays, (item: monthType, index: number) => {
          GridItem() {
            Column() {
              Text(item.value.toString())
                .fontSize(18)
                .fontWeight(this.isSelectedDate(item.value) ? FontWeight.Bold : FontWeight.Normal)
                .fontColor(this.isSelectedDate(item.value) ? this.colors[3] :
                  item.defaultData ? this.colors[1] : this.getDateColor(item.value))
            }
            .width('100%')
            .height('100%')
            .borderRadius(25)
            .backgroundColor(this.isSelectedDate(item.value) ? this.colors[2] :
              (item.defaultData ? this.colors[0] : Color.Transparent))
            .justifyContent(FlexAlign.Center)
          }
          .aspectRatio(1)
          .onClick(() => this.onDateSelected(item.value))
        })
      }
      .width('100%')
      .columnsTemplate('1fr 1fr 1fr 1fr 1fr 1fr 1fr')
      .rowsGap(8)
      .columnsGap(8)
      .height('260vp')

      // 按钮
      Row() {
        Button('取消', { type: ButtonType.Normal })
          .width('140vp')
          .height('50vp')
          .backgroundColor("#f6f6f6")
          .fontColor("#171d29")
          .borderRadius(12)
          .onClick(() => {
            this.cancel();
          })
        Button('确定', { type: ButtonType.Normal })
          .width('140vp')
          .height('50vp')
          .backgroundColor("#171d29")
          .borderRadius(12)
          .onClick(() => {
            console.log("this.selectedDate==>", this.selectedDate);
            this.confirm(this.selectedDate)
            console.log("this.monthDays=>", JSON.stringify(this.monthDays))
          })
      }
      .width('100%')
      .height('50vp')
      .justifyContent(FlexAlign.SpaceBetween)
      .padding({ left: 18, right: 18 })
      .margin({ bottom: 10 });

    }
    .width('100%')
    .padding({
      left: 16,
      right: 16,
      top: 16,
      bottom: 16
    })
    .backgroundColor('#ffffff')

  }

  private onMonthChange(increment: number) {
    let newMonth = this.currentMonth + increment
    let newYear = this.currentYear

    if (newMonth > 11) {
      newMonth = 0
      newYear++
    } else if (newMonth < 0) {
      newMonth = 11
      newYear--
    }

    this.currentMonth = newMonth;
    this.currentYear = newYear;

    let result = this.getDaysInMonth();


    this.monthDays = [...result];
  }

  private onDateSelected(day: number) {
    const newSelectedDate = new Date(this.currentYear, this.currentMonth, day);
    const currentDate = new Date(this.currentYear, this.currentMonth, day)
    const today = new Date()

    // 如果点击的值大于今天值,不选中
    if (currentDate > today) {
      return
    }

    if (this.isDateSelected &&
      this.selectedDate.getDate() === day &&
      this.selectedDate.getMonth() === this.currentMonth &&
      this.selectedDate.getFullYear() === this.currentYear) {
      // 如果点击的是已选中的日期,取消选中
      // this.isDateSelected = false
      this.isDateSelected = true
    } else {
      // 否则,选中新的日期
      this.selectedDate = newSelectedDate
      this.isDateSelected = true
    }
  }

  // 默认选中
  private onDefaultDataSelect(value: string) {
    let date = value.split("-");
    let year = Number(date[0]); // 获取当前年
    let month = Number(date[1]) - 1; // 获取当前月,如果需要渲染到页面,需要+1,不渲染默认进行计算
    let day = Number(date[2]); // 获取当前天数

    this.selectedDate = new Date(year, month, day);
    this.isDateSelected = true;

  }

  private isSelectedDate(day: number): boolean {
    return this.isDateSelected &&
      this.selectedDate.getDate() === day &&
      this.selectedDate.getMonth() === this.currentMonth &&
      this.selectedDate.getFullYear() === this.currentYear
  }

  private getDaysInMonth(): monthType[] {
    const daysInMonth = new Date(this.currentYear, this.currentMonth + 1, 0).getDate();
    const selectMonth = new Date(this.currentYear, this.currentMonth + 1, 0).getMonth() + 1;
    const selectYear = new Date(this.currentYear, this.currentMonth + 1, 0).getFullYear();

    let array = Array.from<number, number>({ length: daysInMonth }, (_, i) => i + 1);

    let result: monthType[] = [];
    for (let i = 0; i < array.length; i++) {
      let obj: monthType = {
        value: array[i],
        defaultData: false,
      }

      let selectData = this.defaultSelectDates.find((item: string) => {
        let date = item.split("-");
        let year = date[0];
        let month = date[1];
        let day = date[2];

        if (selectYear == Number(year) && selectMonth == Number(month) && Number(day) == array[i]) {
          return item
        } else {
          return undefined
        }
      });


      if (selectData) {
        obj.defaultData = true;
      } else {
        obj.defaultData = false;
      }
      result.push(obj);
    }

    return result;
  }

  private getDateColor(day: number): string {
    const currentDate = new Date(this.currentYear, this.currentMonth, day)
    const today = new Date()
    today.setHours(0, 0, 0, 0)

    if (currentDate > today) {
      return '#CCCCCC' // 灰色显示过去的日期
    } else if (this.isSelectedDate(day)) {
      return '#ffffff' // 选中日期的文字颜色
    } else {
      return '#000000' // 未选中日期的文字颜色
    }
  }

  private getLunarDate(day: number): string {
    return LunarDate.solarToLunar(this.currentYear, this.currentMonth + 1, day);
  }
}

class LunarDate {
  private static lunarInfo: number[] = [
    0x04bd8, 0x04ae0, 0x0a570, 0x054d5, 0x0d260, 0x0d950, 0x16554, 0x056a0, 0x09ad0, 0x055d2,
    0x04ae0, 0x0a5b6, 0x0a4d0, 0x0d250, 0x1d255, 0x0b540, 0x0d6a0, 0x0ada2, 0x095b0, 0x14977,
    0x04970, 0x0a4b0, 0x0b4b5, 0x06a50, 0x06d40, 0x1ab54, 0x02b60, 0x09570, 0x052f2, 0x04970,
    0x06566, 0x0d4a0, 0x0ea50, 0x06e95, 0x05ad0, 0x02b60, 0x186e3, 0x092e0, 0x1c8d7, 0x0c950,
    0x0d4a0, 0x1d8a6, 0x0b550, 0x056a0, 0x1a5b4, 0x025d0, 0x092d0, 0x0d2b2, 0x0a950, 0x0b557,
    0x06ca0, 0x0b550, 0x15355, 0x04da0, 0x0a5d0, 0x14573, 0x052d0, 0x0a9a8, 0x0e950, 0x06aa0,
    0x0aea6, 0x0ab50, 0x04b60, 0x0aae4, 0x0a570, 0x05260, 0x0f263, 0x0d950, 0x05b57, 0x056a0,
    0x096d0, 0x04dd5, 0x04ad0, 0x0a4d0, 0x0d4d4, 0x0d250, 0x0d558, 0x0b540, 0x0b5a0, 0x195a6,
    0x095b0, 0x049b0, 0x0a974, 0x0a4b0, 0x0b27a, 0x06a50, 0x06d40, 0x0af46, 0x0ab60, 0x09570,
    0x04af5, 0x04970, 0x064b0, 0x074a3, 0x0ea50, 0x06b58, 0x055c0, 0x0ab60, 0x096d5, 0x092e0,
    0x0c960, 0x0d954, 0x0d4a0, 0x0da50, 0x07552, 0x056a0, 0x0abb7, 0x025d0, 0x092d0, 0x0cab5,
    0x0a950, 0x0b4a0, 0x0baa4, 0x0ad50, 0x055d9, 0x04ba0, 0x0a5b0, 0x15176, 0x052b0, 0x0a930,
    0x07954, 0x06aa0, 0x0ad50, 0x05b52, 0x04b60, 0x0a6e6, 0x0a4e0, 0x0d260, 0x0ea65, 0x0d530,
    0x05aa0, 0x076a3, 0x096d0, 0x04bd7, 0x04ad0, 0x0a4d0, 0x1d0b6, 0x0d250, 0x0d520, 0x0dd45,
    0x0b5a0, 0x056d0, 0x055b2, 0x049b0, 0x0a577, 0x0a4b0, 0x0aa50, 0x1b255, 0x06d20, 0x0ada0
  ];
  private static Gan = ["甲", "乙", "丙", "丁", "戊", "己", "庚", "辛", "壬", "癸"];
  private static Zhi = ["子", "丑", "寅", "卯", "辰", "巳", "午", "未", "申", "酉", "戌", "亥"];
  private static Animals = ["鼠", "牛", "虎", "兔", "龙", "蛇", "马", "羊", "猴", "鸡", "狗", "猪"];
  private static lunarMonths = ["正", "二", "三", "四", "五", "六", "七", "八", "九", "十", "冬", "腊"];
  private static lunarDays = ["初一", "初二", "初三", "初四", "初五", "初六", "初七", "初八", "初九", "初十",
    "十一", "十二", "十三", "十四", "十五", "十六", "十七", "十八", "十九", "二十",
    "廿一", "廿二", "廿三", "廿四", "廿五", "廿六", "廿七", "廿八", "廿九", "三十"];

  static solarToLunar(year: number, month: number, day: number): string {
    if (year < 1900 || year > 2100) {
      return "无效年份";
    }

    const baseDate = new Date(1900, 0, 31);
    const objDate = new Date(year, month - 1, day);
    let offset = Math.floor((objDate.getTime() - baseDate.getTime()) / 86400000);

    let i: number, leap = 0, temp = 0;
    for (i = 1900; i < 2101 && offset > 0; i++) {
      temp = LunarDate.getLunarYearDays(i);
      offset -= temp;
    }

    if (offset < 0) {
      offset += temp;
      i--;
    }

    const lunarYear = i;
    leap = LunarDate.getLeapMonth(i);
    let isLeap = false;

    for (i = 1; i < 13 && offset > 0; i++) {
      if (leap > 0 && i === (leap + 1) && isLeap === false) {
        --i;
        isLeap = true;
        temp = LunarDate.getLeapDays(lunarYear);
      } else {
        temp = LunarDate.getLunarMonthDays(lunarYear, i);
      }

      if (isLeap === true && i === (leap + 1)) {
        isLeap = false;
      }
      offset -= temp;
    }

    if (offset === 0 && leap > 0 && i === leap + 1) {
      if (isLeap) {
        isLeap = false;
      } else {
        isLeap = true;
        --i;
      }
    }

    if (offset < 0) {
      offset += temp;
      --i;
    }
    const lunarMonth = i;
    const lunarDay = offset + 1;
    const monthStr = (isLeap ? "闰" : "") + LunarDate.lunarMonths[lunarMonth - 1];
    const dayStr = LunarDate.lunarDays[lunarDay - 1];
    return dayStr === '初一' ? monthStr + "月" : dayStr;
  }

  private static getLunarYearDays(year: number): number {
    let i = 0, sum = 348;
    for (i = 0x8000; i > 0x8; i >>= 1) {
      sum += (LunarDate.lunarInfo[year - 1900] & i) ? 1 : 0;
    }
    return sum + LunarDate.getLeapDays(year);
  }

  private static getLeapMonth(year: number): number {
    return LunarDate.lunarInfo[year - 1900] & 0xf;
  }

  private static getLeapDays(year: number): number {
    if (LunarDate.getLeapMonth(year)) {
      return (LunarDate.lunarInfo[year - 1900] & 0x10000) ? 30 : 29;
    }
    return 0;
  }

  private static getLunarMonthDays(year: number, month: number): number {
    return (LunarDate.lunarInfo[year - 1900] & (0x10000 >> month)) ? 30 : 29;
  }
}

日期处理utils代码

TypeScript 复制代码
class CalendarUtils {
  private totalDays: number = 0;

  // 获取日历全部时间列表值
  public getCalendarListCount() {
    let list: number[] = []
    for (let i = 1; i <= this.getDaysInLastTenYears(); i++) {
      list.push(i);
    }
    return list;
  }

  // 获取前10年的总共天数
  public getDaysInLastTenYears(): number {
    const now = new Date();
    const tenYearsAgo = new Date(now.getFullYear() - 10, now.getMonth(), now.getDate());
    const millisecondsInDay = 1000 * 60 * 60 * 24;
    const diffInMilliseconds = now.getTime() - tenYearsAgo.getTime();

    this.totalDays = Math.ceil(diffInMilliseconds / millisecondsInDay);
    return Math.ceil(diffInMilliseconds / millisecondsInDay);
  }

  // 根据当前天数获取年月日
  public getDateFromDaysAgo(days: number): string {
    const now = new Date();
    let dayCount = this.totalDays - days;
    const millisecondsPerDay = 1000 * 60 * 60 * 24;
    const targetDate = new Date(now.getTime() - dayCount * millisecondsPerDay);
    const year = targetDate.getFullYear();
    const month = String(targetDate.getMonth() + 1).padStart(2, '0'); // Months are zero-based
    const day = String(targetDate.getDate()).padStart(2, '0');
    return `${year}-${month}-${day}`;
  }

  // 根据年月日获取天数下标 反值
  public getDaysFromDate(dateString: string): number {
    const dateParts = dateString.split('-');
    const targetDate = new Date(Number(dateParts[0]), Number(dateParts[1]) - 1, Number(dateParts[2]));
    const now = new Date();
    const diffInMilliseconds = now.getTime() - targetDate.getTime();
    const millisecondsPerDay = 1000 * 60 * 60 * 24;
    let days = Math.floor(Math.abs(diffInMilliseconds) / millisecondsPerDay);

    return (this.totalDays - 1) - days;
  }

  // 获取上一天年月日
  public getPreviousDay(value: string | Date): string {
    const now = new Date(value);
    now.setDate(now.getDate() - 1); // 获取前一天
    const year = now.getFullYear();
    const month = String(now.getMonth() + 1).padStart(2, '0'); // 月份从0开始,所以需要+1,并补0
    const day = String(now.getDate()).padStart(2, '0'); // 补0
    return `${year}-${month}-${day}`;
  }

  // 获取下一天年月日
  public getNextDay(value: string | Date): string {
    const now = new Date(value);
    now.setDate(now.getDate() + 1); // 获取后一天
    const year = now.getFullYear();
    const month = String(now.getMonth() + 1).padStart(2, '0'); // 月份从0开始,所以需要+1,并补0
    const day = String(now.getDate()).padStart(2, '0'); // 补0
    return `${year}-${month}-${day}`;
  }

  // 获取当前年月日
  public getCurrentDay() {
    const now = new Date();
    const year = now.getFullYear();
    const month = String(now.getMonth() + 1).padStart(2, '0'); // 月份从0开始,加一,补0
    const day = String(now.getDate()).padStart(2, '0'); // 补0
    return `${year}-${month}-${day}`;
  }

  // 获取前10年的年月茹
  public getTheFirstYearsDate(value: number) {
    const currentDate = new Date();
    const currentYear = currentDate.getFullYear();
    // 获取前10年的日期
    const previousTenYearsDate = new Date(currentDate.setFullYear(currentYear - value));
    const Year = previousTenYearsDate.getFullYear();
    const Month = String(previousTenYearsDate.getMonth() + 1).padStart(2, '0');
    const Day = String(previousTenYearsDate.getDate()).padStart(2, '0');
    return `${Year}-${Month}-${Day}`;
  }
}


// 获取数据预加载
export class MyDataSource implements IDataSource {
  private list: number[] = []

  constructor(list: number[]) {
    this.list = list
  }

  totalCount(): number {
    return this.list.length
  }

  getData(index: number): number {
    return this.list[index]
  }

  registerDataChangeListener(listener: DataChangeListener): void {
  }

  unregisterDataChangeListener() {
  }
}

export const calendarUtils = new CalendarUtils();
相关推荐
行十万里人生4 小时前
Qt 控件与布局管理
数据库·qt·microsoft·华为od·华为·华为云·harmonyos
ChinaDragonDreamer4 小时前
HarmonyOS:创建应用静态快捷方式
harmonyos·鸿蒙
徐_三岁1 天前
TypeScript 中的 object 和Object的区别
前端·javascript·typescript
ThomasChan1231 天前
Typesrcipt泛型约束详细解读
前端·javascript·vue.js·react.js·typescript·vue·jquery
yg_小小程序员2 天前
鸿蒙开发(32)arkTS、通过关系型数据库实现数据持久化封装
数据库·华为·typescript·harmonyos
源之缘-OFD先行者2 天前
在 VS Code 中使用 TypeScript 进行开发和打包的几种方法
linux·服务器·typescript
Li_Ning213 天前
vue3+uniapp开发鸿蒙初体验
华为·uni-app·harmonyos
特立独行的猫a3 天前
HarmonyOS NEXT边学边玩:从零实现一个影视App(七、今日票房页面的设计与实现)
华为·harmonyos
李洋-蛟龙腾飞公司3 天前
华为支付-(可选)特定场景配置操作
华为·harmonyos