|
|
@@ -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)
|
|
|
+ })
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|