Browse Source

feat: 新增日历组件

YuJing 2 tháng trước cách đây
mục cha
commit
6f17ffc3bd

+ 335 - 0
commons/basic/src/main/ets/components/generalComp/YTCalendarPicker.ets

@@ -0,0 +1,335 @@
+import { BasicType, DateInfo } from "../../models";
+
+@Component
+@CustomDialog
+export struct YTCalendarPicker {
+  // 年月
+  @State year: number = 2025;
+  @State month: number = 8;
+  // 开始是星期几
+  @State weekStart: number = 1;
+  // 本月的天数
+  @State daysInMonth: number = 31;
+  // 渲染用的月份数组
+  @State monthArray: DateInfo[] = [];
+  @State showDatePickerMenu: boolean = false
+  @State selectDay: Date = new Date()
+  private declare range: TextCascadePickerRangeContent[]
+  private years =
+    Array.from<number, number>({ length: new Date().getFullYear() - 1970 + 1 }, (_: number, i: number) => 1970 + i)
+  //当前年月选择的下标
+  private selectedIndex: number[] = [new Date().getFullYear() - 1970, new Date().getMonth()]
+  private linearInfo: LinearGradientOptions = {
+    colors: [['#B9FD2A', 0.01], ['#F5FD6D', 1]],
+    angle: 110
+  }
+  // 星期中文映射表
+  private readonly WEEK_MAP = ['日', '一', '二', '三', '四', '五', '六'];
+  // 取消
+  onCancel: () => void = () => {
+  }
+  // 确定
+  onConfirm: (date: Date) => void = () => {
+  }
+  // 右上角的结构
+  @BuilderParam rightTopBuild: () => void = this.buttonRow;
+  // yy-mm-dd 格式的数组, 符合数组内格式的日期下会有标记
+  @Require dateList: string[] = []
+
+  diaLogControl?: CustomDialogController
+
+  /**
+   * 回调
+   */
+
+  // 更改月份
+  changeMonth(year: number, month: number) {
+    // 1. 获取当前月份的天数
+    this.daysInMonth = this.getDaysInMonth(year, month);
+
+    // 2. 获取当月的第一天是星期几
+    this.weekStart = new Date(year, month - 1, 1).getDay() + 1;
+
+    // 3. 初始化月份数组
+    this.monthArray = [...new Array(this.weekStart - 1).fill({} as DateInfo),
+      ...this.generateBackwardDateArray(new Date(year, month - 1, 1), this.daysInMonth, false)];
+
+    return this.monthArray
+  }
+
+  // 点击单个日期
+  clickItem(item: DateInfo){
+    this.selectDay = item.id
+  }
+
+  /**
+   * 获取指定月份的天数
+   * @param year 年
+   * @param month 月份
+   * @returns 当月的天数
+   */
+  getDaysInMonth(year: number, month: number): number {
+    return new Date(year, month, 0).getDate();
+  }
+
+  /**
+   * 判断两个 Date 对象是否为同一天
+   * @param date1
+   * @param date2
+   * @returns
+   */
+  isSameDay(date1: Date, date2: Date): boolean {
+    return (
+      date1.getFullYear() === date2.getFullYear() &&
+        date1.getMonth() === date2.getMonth() &&
+        date1.getDate() === date2.getDate()
+    );
+  }
+
+  /**
+   * 格式化日期对象为自定义字符串
+   * @param date 日期对象
+   * @param needTime 是否需要时间 ( 时、分、秒 )
+   * @param needSecond 是否需要秒
+   * @returns
+   */
+  formatDateToCustomString(date: Date, needTime: boolean = true, needSecond: boolean = true): string {
+    // 转换为 YY-MM-DD HH:mm:ss 格式
+    const year = date.getFullYear().toString();
+    const month = (date.getMonth() + 1).toString().padStart(2, '0');
+    const day = date.getDate().toString().padStart(2, '0');
+    const hours = date.getHours().toString().padStart(2, '0');
+    const minutes = date.getMinutes().toString().padStart(2, '0');
+    const seconds = date.getSeconds().toString().padStart(2, '0');
+
+    const result =
+      `${year}-${month}-${day}` + (needTime ? ` ${hours}:${minutes}` + (needSecond ? `:${seconds}` : '') : '');
+    return result;
+  }
+
+  /**
+   * 生成从指定日期开始向后的连续日期数组,可以包含今天但不能超过今天
+   * @param startDate 起始日期(默认当前日期)
+   * @param count 生成的日期数量(默认7天)
+   * @returns 日期对象数组,包含今天但不包含超过今天的日期
+   */
+  generateBackwardDateArray(
+    startDate: Date = new Date(),
+    count: number = 7,
+    isCheck: boolean = true
+  ): DateInfo[] {
+    const dateArray: DateInfo[] = [];
+
+    // 复制起始日期,避免修改原对象
+    const currentDate = new Date(startDate);
+
+    // 获取今天的日期
+    // 获取今天的日期并设置时间为23:59:59:999,便于比较
+    const today = new Date();
+    today.setHours(0, 0, 0, 0);
+    today.setDate(today.getDate() + 1);
+    today.setTime(today.getTime() - 1); // 设置为今天的最后一毫秒
+
+    for (let i = 0; i < count; i++) {
+      // 检查当前日期是否超过今天
+      if (isCheck && currentDate > today) {
+        break;
+      }
+
+      // 添加日期信息到数组
+      dateArray.push(this.createDateInfo(currentDate));
+
+      // 日期加1天(向后)
+      currentDate.setDate(currentDate.getDate() + 1);
+    }
+
+    return dateArray;
+  }
+
+  /**
+   * 创建一个日期信息对象
+   * @param date 日期对象
+   * @returns DateInfo 对象
+   */
+  createDateInfo(date: Date): DateInfo {
+    const year = date.getFullYear();
+    const month = date.getMonth() + 1;
+    const day = date.getDate();
+    const week = this.WEEK_MAP[date.getDay()];
+
+    return {
+      year: year,
+      month: month,
+      day: day,
+      week: week,
+      id: new Date(date)
+    };
+  }
+
+  aboutToAppear(): void {
+    this.changeMonth(2025, 8);
+
+    const monthsRange =
+      Array.from<number, number>({ length: 12 }, (_: number, i: number) => 1 + i).map(month => {
+        return { text: month.toString() } as TextCascadePickerRangeContent
+      })
+    this.range = this.years.map(year => {
+      return { text: year.toString(), children: monthsRange } as TextCascadePickerRangeContent
+    })
+  }
+
+  build() {
+    Column({ space: 20 }) {
+      // 月份切换和头部组件
+      Row() {
+        // 月份切换
+        Row({ space: 8 }) {
+          Text(`${this.year}-${this.month.toString().padStart(2, '0')}`)
+            .fontSize(16)
+            .fontWeight(600)
+
+          Image($r('app.media.ic_back'))
+            .width(14)
+            .height(8)
+        }
+        .onClick(() => {
+          this.showDatePickerMenu = true
+        })
+        .bindMenu(this.showDatePickerMenu, this.dateSelectMenu, {
+          onDisappear: () => {
+            this.showDatePickerMenu = false
+            this.year = this.range[this.selectedIndex[0]].text.valueOf() as number
+            this.month = this.range[this.selectedIndex[0]].children![this.selectedIndex[1]].text.valueOf() as number
+            this.changeMonth(this.year, this.month)
+          },
+          placement: Placement.Bottom
+        })
+
+        // 右上角的结构
+        this.rightTopBuild()
+      }
+      .width("100%")
+      .alignItems(VerticalAlign.Center)
+      .justifyContent(FlexAlign.SpaceBetween)
+
+      // 周 title 的显示
+      Row() {
+        ForEach(this.WEEK_MAP, (item: string, index: number) => {
+          Text(item)
+        })
+      }
+      .width("100%")
+      .justifyContent(FlexAlign.SpaceBetween)
+
+      // 日历主体
+      Grid() {
+        ForEach(this.monthArray, (item: DateInfo, index: number) => {
+          GridItem() {
+            Column({ space: 3 }) {
+              if (item.id) {
+                // 日历-单个日期的组件
+                Row() {
+                  Text(item.day + '')
+                    .fontSize(12)
+                    .fontColor(this.isSameDay(item.id, this.selectDay) ? Color.Black : '#979797')
+                }
+                .width(32)
+                .aspectRatio(1)
+                .borderRadius(8)
+                .linearGradient(this.isSameDay(item.id, this.selectDay) ? this.linearInfo : {
+                  colors: [['#F6F6F6', 1]],
+                })
+                .alignItems(VerticalAlign.Center)
+                .justifyContent(FlexAlign.Center)
+                .onClick(() => {
+                  this.clickItem(item)
+                })
+
+                // 日期标记 - 如无需求,可删除
+                Text()
+                  .width(4)
+                  .aspectRatio(1)
+                  .borderRadius(2)
+                  .linearGradient(this.dateList.indexOf(this.formatDateToCustomString(item.id, false)) !== -1 ?
+                  this.linearInfo : null)
+              } else {
+                // 空白占位符
+                Text('')
+              }
+            }
+            .width(49)
+            .aspectRatio(1)
+            .alignItems(HorizontalAlign.Center)
+            .justifyContent(FlexAlign.Center)
+          }
+        })
+      }
+      .columnsTemplate('repeat(7, 1fr)')
+      .width("100%")
+    }
+    .width("100%")
+    .height(400)
+    .backgroundColor(Color.White)
+    .padding({
+      left: 15,
+      right: 15,
+      top: 20,
+      bottom: 4
+    })
+  }
+
+  @Builder
+  private dateSelectMenu() {
+    Stack({ alignContent: Alignment.Center }) {
+      TextPicker({ range: this.range, selected: this.selectedIndex })
+        .selectedTextStyle({ color: '#FF353C46', font: { weight: 500, size: 16 } })
+        .defaultPickerItemHeight(36)
+        .divider(null)
+        .onScrollStop((value, index) => {
+          console.log(`testLog ${value} ${index}`)
+          this.selectedIndex = index as number[]
+        })
+
+      //自定义选择遮罩
+      Column() {
+
+      }
+      .width('100%')
+      .height(36)
+      .backgroundColor('#52D9D9D9')
+      .borderRadius(8)
+    }
+    .height(140)
+    .width(160)
+    .padding(12)
+    .borderRadius(8)
+  }
+
+  @Builder
+  private buttonRow() {
+    Row({ space: 14 }) {
+      Text("取消")
+        .borderRadius(36)
+        .backgroundColor('#F6F6F6')
+        .padding({
+          left: 20,
+          top: 5,
+          right: 20,
+          bottom: 5
+        })
+        .onClick(this.onCancel)
+      Text("确认")
+        .borderRadius(36)
+        .linearGradient(this.linearInfo)
+        .padding({
+          left: 20,
+          top: 5,
+          right: 20,
+          bottom: 5
+        })
+        .onClick(() => {
+          this.onConfirm(this.selectDay)
+        })
+    }
+  }
+}

+ 10 - 1
commons/basic/src/main/ets/models/index.ets

@@ -51,4 +51,13 @@ export type ResultCallBack<T> = (res?: T, err?: Error) => void
 /**
  * @description 断点字符串类型
  */
-export type BreakPointString = 'xs' | 'sm' | 'md' | 'lg'
+export type BreakPointString = 'xs' | 'sm' | 'md' | 'lg'
+
+// 日期信息接口定义
+export interface DateInfo {
+  year: number;   // 年份
+  month: number;  // 月份(1-12)
+  day: number;    // 日期(1-31)
+  week: string;   // 星期(中文)
+  id: Date ;    // 唯一标识
+}