import { DateInfo } from "../../models"; @Component @CustomDialog export struct YTCalendarPicker { /** * 外部传入 */ // 主题色 linearInfo: LinearGradientOptions = { colors: [['#B9FD2A', 0.01], ['#F5FD6D', 1]], angle: 110 } // 边距 cPadding: Padding | Length | LocalizedPadding = { left: 0, right: 0, top: 20, bottom: 4 } // yyyy-mm-dd 格式的数组, 符合数组内格式的日期下会有标记 dateList: string[] = [] // 取消 onCancel: () => void = () => {} // 确定 onConfirm: (date: Date) => void = () => {} // 点击某一天的回调 onClickItem: (date: Date) => void = () => {} // 是否显示头部标题 showTitle: boolean = true /** * 内部方法 */ // 年月 @State year: number = new Date().getFullYear(); @State month: number = new Date().getMonth() + 1; // 开始是星期几 @State private weekStart: number = 1; // 本月的天数 @State private daysInMonth: number = 31; // 渲染用的月份数组 @State private monthArray: DateInfo[] = []; // 当前选中的日期 @State private selectDay: Date = new Date() // 显示选择日期的菜单 @State private showDatePickerMenu: boolean = false // 弹窗控制器 - diaLogControl?: CustomDialogController // 星期中文映射表 private readonly WEEK_MAP = ['日', '一', '二', '三', '四', '五', '六']; // 日期选择器的范围 private declare range: TextCascadePickerRangeContent[] // 年份 - 日期选择器使用 private years = Array.from({ length: new Date().getFullYear() - 1970 + 1 }, (_: number, i: number) => 1970 + i) //当前年月选择的下标 private selectedIndex: number[] = [new Date().getFullYear() - 1970, new Date().getMonth()] /** * 回调 */ // 更改月份 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 this.onClickItem(this.selectDay) } // 更改日期选择器的显示和隐藏 changeDatePickerMenu(show: boolean) { animateToImmediately({ duration: 200 }, () => { this.showDatePickerMenu = show }) } /** * 获取指定月份的天数 * @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(this.year, this.month); const monthsRange = Array.from({ 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 }) { // 月份切换和头部组件 if(this.showTitle) { Row() { // 月份切换 Row({ space: 8 }) { Text(`${this.year}-${this.month.toString().padStart(2, '0')}`) .fontSize(16) .fontWeight(600) .fontColor(Color.Black) Image($r('app.media.ic_back')) .width(14) .height(8) .rotate({angle: this.showDatePickerMenu ? 90 : 270}) } .onClick(() => { this.changeDatePickerMenu(true) }) .bindMenu(this.showDatePickerMenu, this.dateSelectMenu, { onDisappear: () => { this.changeDatePickerMenu(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.buttonRow() } .width("100%") .alignItems(VerticalAlign.Center) .justifyContent(FlexAlign.SpaceBetween) } // 周 title 的显示 Row() { ForEach(this.WEEK_MAP, (item: string, index: number) => { Row(){ Text(item) .fontColor(Color.Black) } .layoutWeight(1) .justifyContent(FlexAlign.Center) }) } .width("100%") .justifyContent(FlexAlign.SpaceBetween) // 日历主体 Grid() { ForEach(this.monthArray, (item: DateInfo, index: number) => { GridItem() { Column({ space: 5 }) { if (item.id) { // 日历-单个日期的组件 Row() { Text(item.day + '') .fontSize(12) .fontColor(this.isSameDay(item.id, this.selectDay) ? Color.Black : '#979797') } .width("100%") .aspectRatio(1) .borderRadius(8) .alignItems(VerticalAlign.Center) .justifyContent(FlexAlign.Center) .linearGradient(this.isSameDay(item.id, this.selectDay) ? this.linearInfo : { colors: [['#F6F6F6', 1]], }) .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(32) .alignItems(HorizontalAlign.Center) .justifyContent(FlexAlign.Center) } }) } .rowsGap(10) .maxCount(6) .width("100%") .columnsTemplate('repeat(7, 1fr)') } .width("100%") .padding(this.cPadding) .backgroundColor(Color.White) } // 日期选择菜单 @Builder private dateSelectMenu() { Stack({ alignContent: Alignment.Center }) { TextPicker({ range: this.range, selected: this.selectedIndex }) .divider(null) .defaultPickerItemHeight(36) .backgroundColor(Color.White) .selectedTextStyle({ color: '#FF353C46', font: { weight: 500, size: 16 } }) .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) .backgroundColor(Color.White) } // 右上角结构 - 确定和取消按钮 @Builder private buttonRow() { Row({ space: 14 }) { Text("取消") .borderRadius(36) .backgroundColor('#F6F6F6') .fontColor(Color.Black) .padding({ left: 20, top: 5, right: 20, bottom: 5 }) .onClick(this.onCancel) Text("确认") .borderRadius(36) .linearGradient(this.linearInfo) .fontColor(Color.Black) .padding({ left: 20, top: 5, right: 20, bottom: 5 }) .onClick(() => { this.onConfirm(this.selectDay) }) } } }