UNPKG

@tuniao/tnui-vue3-uniapp

Version:
644 lines (593 loc) 20.7 kB
import { computed, nextTick, reactive, ref, watch } from 'vue' import dayjs from '../../../../libs/dayjs' import { CHANGE_EVENT, UPDATE_MODEL_EVENT } from '../../../../constants' import { debugWarn, isString } from '../../../../utils' import { solar2lunar } from '../../libs/lunar-calendar' import { useCalendarSelector } from './use-calendar-selector' import type { SetupContext } from 'vue' import type { Dayjs } from '../../../../libs/dayjs' import type { CalendarEmits, CalendarModelValueType, CalendarProps, } from '../calendar' import type { CalendarData, CalendarItem, CalendarItemDateStatus, CalendarMonthData, CalendarRangeSelectData, CalendarSelectDataMap, DateData, } from '../types' // 默认的日期格式 const DEFAULT_DATE_FORMAT = 'YYYY/MM/DD' // 格式化日期数据,将-替换为/ const formatDate = <T extends CalendarModelValueType>(date: T): T => { if (!date || !date.length) return date if (isString(date)) { return date.replace(/-/g, '/') as T } return date.map((item) => item.replace(/-/g, '/')) as T } // 填充0 const fillDateZero = (date: number) => String(date).padStart(2, '0') // 根据年月日生成日期字符串 const generateDateStr = (date: DateData) => `${date.year}/${fillDateZero(date.month)}/${fillDateZero(date.date)}` export const useCalendar = ( props: CalendarProps, emits: SetupContext<CalendarEmits>['emit'] ) => { // 重新加载月份swiper const reloadMonthSwiper = ref<boolean>(false) // 日历的数据 const calendarData = ref<CalendarData>([]) // 星期提示文字 const weekText = ref<string[]>(['日', '一', '二', '三', '四', '五', '六']) // 最小年月日 const minDate = reactive<DateData>({ year: 0, month: 0, date: 0, }) // 最大年月日 const maxDate = reactive<DateData>({ year: 0, month: 0, date: 0, }) // 当前年月日 const currentDate = reactive<DateData>({ year: 0, month: 0, date: 0, }) // 当前选中的年月日 const currentSelectedDate = reactive<DateData>({ year: 0, month: 0, date: 0, }) // 日期范围内数据 const rangeDate = reactive<CalendarRangeSelectData>({}) // 更新modelValue // 标记是否需要重新渲染日历 let needRenderCalendar = false const updateModelValue = (changeEmit = true) => { const { modelValue, mode } = props const formatModelValue = formatDate(modelValue) let value: CalendarModelValueType = '' // 根据不同的模式,返回不同的值 switch (mode) { case 'date': // 如果当前选中的日期不为空,则先将之前选中日期的选中状态修改为normal if (modelValue) { const dateDayjs = dayjs( formatModelValue as string, DEFAULT_DATE_FORMAT ) updateDateStatus(dateDayjs, 'normal') } // 设置当前选中日期为激活状态 updateDateStatus(currentSelectedDate, 'active') value = generateDateStr(currentSelectedDate) break case 'multi': // 判断当前选中的日期是否已经存在于modelValue中,如果存在,则将其从modelValue中移除 // eslint-disable-next-line no-case-declarations const activeIndex = (formatModelValue as string[]).indexOf( generateDateStr(currentSelectedDate) ) if (formatModelValue.length && activeIndex !== -1) { updateDateStatus(currentSelectedDate, 'normal') ;(formatModelValue as string[]).splice(activeIndex, 1) value = [...(formatModelValue as string[])] } else { updateDateStatus(currentSelectedDate, 'active') value = [ ...(formatModelValue as string[]), generateDateStr(currentSelectedDate), ] } break case 'range': // 如果已选的值为空或者长度为2,则将当前选中的日期设置为开始日期 if ( !formatModelValue || formatModelValue.length === 0 || formatModelValue.length === 2 ) { if (formatModelValue.length === 2) { // 清空原来设置的时间 const startDateDayjs = dayjs( (formatModelValue as string[])[0], DEFAULT_DATE_FORMAT ) const endDateDayjs = dayjs( (formatModelValue as string[])[1], DEFAULT_DATE_FORMAT ) updateDateStatus(startDateDayjs, 'normal') updateDateStatus(endDateDayjs, 'normal') } value = [generateDateStr(currentSelectedDate)] } else { // 判断当前选中的日期是否比开始日期还要着,如果是则重新设置开始日期 const currentDate = generateDateStr(currentSelectedDate) if (rangeDate.start) { if (rangeDate.start.isAfter(generateDateStr(currentSelectedDate))) { value = [currentDate] } else { // 将选择后的日添加为结束日期 value = [ ...(formatModelValue as string[]), generateDateStr(currentSelectedDate), ] } } } needRenderCalendar = true break } emits(UPDATE_MODEL_EVENT, value) if (changeEmit) { nextTick(() => { emits(CHANGE_EVENT, value) }) } } // 当前月份在中数据中的索引 const currentMonthIndex = computed(() => { return calendarData.value.findIndex( (item) => item.month === currentSelectedDate.month ) }) // 当前月份中日期数据的长度 const currentMonthDateLength = computed(() => { return calendarData.value[currentMonthIndex.value]?.data?.length || 0 }) // 处理容器信息 const { calendarId, dateContainerHeight } = useCalendarSelector( currentMonthDateLength, props.mode ) // 保存当前设置的日期信息 const activeDateValueMap = new Map<number, CalendarSelectDataMap>() // 生成日历数据 const generateCalendarData = () => { const data: CalendarData = [] // 填充日期数据 const _fillDateData = ( month: number, minDisabledDate = 0, maxDisabledDate = 0 ) => { const monthData: CalendarMonthData = { month, data: [], } // 获取对应年份和月份 const activeDates = activeDateValueMap .get(currentSelectedDate.year) ?.get(month) // 获取当前月份的天数 const days = new Date(currentSelectedDate.year, month, 0).getDate() // 获取当前月份的第一天是星期几 const firstDayWeek = new Date( `${currentSelectedDate.year}/${month}/1` ).getDay() // 填充空白数据 for (let i = 0; i < firstDayWeek; i++) { monthData.data.push({ date: 0, status: 'disabled', }) } // 填充日期数据 for (let i = 1; i <= days; i++) { let status: CalendarItemDateStatus = i < minDisabledDate || (maxDisabledDate && i > maxDisabledDate) ? 'disabled' : 'normal' if (activeDates?.includes(i)) status = 'active' let desc = '' if (props.showLunar) { const lunarValue = solar2lunar(currentSelectedDate.year, month, i) if (lunarValue !== -1) { desc = lunarValue.IDayCn === '初一' ? lunarValue.IMonthCn : lunarValue.IDayCn } } // 设置日期范围数据 if (props.mode === 'range') { const { start, end } = rangeDate if ( start && start.isSame(`${currentSelectedDate.year}/${month}/${i}`) ) { status = 'active' desc = props.rangeStartDesc } if (end && end.isSame(`${currentSelectedDate.year}/${month}/${i}`)) { status = 'active' desc = props.rangeEndDesc } if (start && end) { // 判断是否在范围内 const currentDateDayjs = dayjs( `${currentSelectedDate.year}/${month}/${i}`, DEFAULT_DATE_FORMAT ) if ( currentDateDayjs.isAfter(start) && currentDateDayjs.isBefore(end) ) { status = 'range' } } } monthData.data.push({ date: i, status, desc, }) } return monthData } // 填充月份数据 let minMonth = 1 let maxMonth = 12 // 如果当前选中的年份等于最小年份,那么最小月份就是最小年份的月份 if (currentSelectedDate.year === minDate.year) { minMonth = minDate.month } if (currentSelectedDate.year === maxDate.year) { maxMonth = maxDate.month } if (minMonth === 0 || maxMonth === 0) return for (let i = minMonth; i <= maxMonth; i++) { let minDisabledDate = 0 let maxDisabledDate = 0 // 如果当前选中的年份等于最小年份,那么最小月份就是最小年份的月份 if (currentSelectedDate.year === minDate.year && i === minMonth) { minDisabledDate = minDate.date maxDisabledDate = 0 } if (currentSelectedDate.year === maxDate.year && i === maxMonth) { minDisabledDate = 0 maxDisabledDate = maxDate.date } data.push(_fillDateData(i, minDisabledDate, maxDisabledDate)) } calendarData.value = data } watch( () => props.modelValue, (val: CalendarModelValueType) => { if (val || val.length) { const { mode } = props let modelValue: string[] = [] if (mode === 'date') modelValue = [val as string] else modelValue = val as string[] activeDateValueMap.clear() // 遍历获取对应已设置的日期信息 modelValue.forEach((item) => { const dateDayjs = dayjs(formatDate(item), DEFAULT_DATE_FORMAT) const year = dateDayjs.year() const month = dateDayjs.month() + 1 const date = dateDayjs.date() // 设置当前激活的年份和月份 if (currentSelectedDate.year === 0) { currentSelectedDate.year = year currentSelectedDate.month = month } if (activeDateValueMap.has(year)) { let monthDates = activeDateValueMap.get(year)?.get(month) if (!monthDates) monthDates = [date] else monthDates.push(date) activeDateValueMap.get(year)?.set(month, monthDates) } else { activeDateValueMap.set(year, new Map([[month, [date]]])) } }) // 如果是选择日期范围,则设置开始和结束时间 if (mode === 'range') { rangeDate.start = undefined rangeDate.end = undefined if (modelValue?.[0]) { // 开始时间 rangeDate.start = dayjs(modelValue[0], DEFAULT_DATE_FORMAT) } if (modelValue?.[1]) { // 结束时间 rangeDate.end = dayjs(modelValue[1], DEFAULT_DATE_FORMAT) } } if (needRenderCalendar) { needRenderCalendar = false nextTick(() => { generateCalendarData() }) } } }, { immediate: true, deep: true, } ) // 根据传递的日期,设置当前日期的状态 const updateDateStatus = ( dateData: DateData | Dayjs, status: CalendarItemDateStatus ) => { let month = 0 let date = 0 if (dateData.toString() === '[object Object]') { month = (dateData as DateData).month date = (dateData as DateData).date } else { month = (dateData as Dayjs).month() + 1 date = (dateData as Dayjs).date() } const monthIndex = calendarData.value.findIndex( (item) => item.month === month ) if (monthIndex === -1) return const dateIndex = calendarData.value[monthIndex].data.findIndex( (item) => item.date === date ) if (dateIndex === -1) return calendarData.value[monthIndex].data[dateIndex].status = status } // 初始化最小、最大年月日 const initDateData = () => { let { minDate: _minDate, maxDate: _maxDate, modelValue, mode } = props // 当前时间的 dayjs 对象 const currentDayjs = dayjs() currentDate.year = currentDayjs.year() currentDate.month = currentDayjs.month() + 1 currentDate.date = currentDayjs.date() // 如果没有设置最小值,那么最小值就是当前年月日 if (!_minDate) { _minDate = currentDayjs.format(DEFAULT_DATE_FORMAT) } else { _minDate = formatDate(_minDate) } const minDateDayjs = dayjs(_minDate, DEFAULT_DATE_FORMAT) minDate.year = minDateDayjs.year() minDate.month = minDateDayjs.month() + 1 minDate.date = minDateDayjs.date() // 如果没有设置最大值,那么最大值就是当前年份最后一个月的最后一天 if (!_maxDate) { _maxDate = currentDayjs.endOf('year').format(DEFAULT_DATE_FORMAT) } else { _maxDate = formatDate(_maxDate) } const maxDateDayjs = dayjs(_maxDate, DEFAULT_DATE_FORMAT) maxDate.year = maxDateDayjs.year() maxDate.month = maxDateDayjs.month() + 1 maxDate.date = maxDateDayjs.date() let noGenerateAfterInit = false // 如果没有设置默认值,那么默认值就是当前年月 if (!modelValue || !modelValue?.length) { currentSelectedDate.year = Math.max(currentDate.year, minDate.year) currentSelectedDate.month = currentDate.year === minDate.year ? Math.max(currentDate.month, minDate.month) : currentDate.month // 如果当前是单选日期模式,那么默认选中当前日期 if ( mode === 'date' && currentSelectedDate.year === currentDate.year && currentSelectedDate.month === currentDate.month ) { currentSelectedDate.date = currentDate.date emits(UPDATE_MODEL_EVENT, generateDateStr(currentSelectedDate)) } } else { let initDefaultDate = '' switch (mode) { case 'date': initDefaultDate = formatDate(modelValue as string) // 判断设置的默认值是否在最小、最大年月日范围内,如果不在,那么默认值就是最小、最大年月日 if (minDateDayjs.isAfter(initDefaultDate)) { initDefaultDate = minDateDayjs.format(DEFAULT_DATE_FORMAT) emits(UPDATE_MODEL_EVENT, initDefaultDate) } if (maxDateDayjs.isBefore(initDefaultDate)) { initDefaultDate = maxDateDayjs.format(DEFAULT_DATE_FORMAT) emits(UPDATE_MODEL_EVENT, initDefaultDate) } break case 'multi': // 筛选出在最小、最大年月日范围内的日期 // eslint-disable-next-line no-case-declarations const multiDefaultModelValue = (modelValue as string[]).filter( (date) => { const dateDayjs = dayjs(date) return ( (dateDayjs.isAfter(minDateDayjs) && dateDayjs.isBefore(maxDateDayjs)) || dateDayjs.isSame(minDateDayjs) || dateDayjs.isSame(maxDateDayjs) ) } ) if ( multiDefaultModelValue.length !== (modelValue as string[]).length ) { emits(UPDATE_MODEL_EVENT, multiDefaultModelValue) } initDefaultDate = multiDefaultModelValue[0] break case 'range': if (modelValue.length !== 2) { debugWarn('TnCalendar', '在 range 模式下 modelValue 长度必须为 2') return } // eslint-disable-next-line no-case-declarations const rangeMinDateDayjs = dayjs((modelValue as string[])[0]) // eslint-disable-next-line no-case-declarations const rangeMaxDateDayjs = dayjs((modelValue as string[])[1]) // 判断是否超过最大范围 if ( rangeMinDateDayjs.isAfter(maxDateDayjs) || rangeMaxDateDayjs.isBefore(minDateDayjs) ) { debugWarn('TnCalendar', '在 range 模式下 modelValue 超过最大范围') return } // eslint-disable-next-line no-case-declarations const rangeDefaultModelValue = modelValue as string[] if (rangeMinDateDayjs.isBefore(minDateDayjs)) { rangeDefaultModelValue[0] = minDateDayjs.format(DEFAULT_DATE_FORMAT) initDefaultDate = rangeDefaultModelValue[0] } if (rangeMaxDateDayjs.isAfter(maxDateDayjs)) { rangeDefaultModelValue[1] = maxDateDayjs.format(DEFAULT_DATE_FORMAT) } emits(UPDATE_MODEL_EVENT, rangeDefaultModelValue) noGenerateAfterInit = true break } if (initDefaultDate) { const initDefaultDateDayjs = dayjs(initDefaultDate, DEFAULT_DATE_FORMAT) currentSelectedDate.year = initDefaultDateDayjs.year() currentSelectedDate.month = initDefaultDateDayjs.month() + 1 currentSelectedDate.date = initDefaultDateDayjs.date() } } if (!noGenerateAfterInit || calendarData.value.length === 0) { nextTick(() => { // 生成日历数据 generateCalendarData() }) } } initDateData() // 如果修改了最小、最大年月日,那么重新初始化最小、最大年月日 watch( () => [props.minDate, props.maxDate], () => { initDateData() } ) // dateItem点击事件 const dateItemClickEvent = (date: CalendarItem) => { if ( (date.status === 'active' && props.mode === 'date') || date.status === 'disabled' || date.date === 0 ) return currentSelectedDate.date = date.date updateModelValue() } // 滑动切换月份事件 const swiperSwitchMonthEvent = (event: any) => { const { current } = event.detail currentSelectedDate.month = calendarData.value[current].month clearSwiperAnimationFlagTimer = setTimeout(() => { swiperSwitchAnimationFinish = true if (clearSwiperAnimationFlagTimer) { clearTimeout(clearSwiperAnimationFlagTimer) clearSwiperAnimationFlagTimer = null } }, 300) emits('change-month', currentSelectedDate.month) } // 月份切换动画结束 let swiperSwitchAnimationFinish = true let clearSwiperAnimationFlagTimer: ReturnType<typeof setTimeout> | null = null const swiperSwitchMonthAnimationFinishEvent = () => { swiperSwitchAnimationFinish = true if (clearSwiperAnimationFlagTimer) { clearTimeout(clearSwiperAnimationFlagTimer) clearSwiperAnimationFlagTimer = null } } // 切换月份 const switchMonth = (type: 'prev' | 'next') => { if (!props.allowChangeMonth || !swiperSwitchAnimationFinish) return swiperSwitchAnimationFinish = false if (type === 'prev') { if ( (currentSelectedDate.year === minDate.year && currentSelectedDate.month === minDate.month) || currentSelectedDate.month === 1 ) return currentSelectedDate.month-- } else { if ( (currentSelectedDate.year === maxDate.year && currentSelectedDate.month === maxDate.month) || currentSelectedDate.month === 12 ) return currentSelectedDate.month++ } } // 切换年份 const switchYear = (type: 'prev' | 'next') => { if (!props.allowChangeYear) return if (type === 'prev') { if (currentSelectedDate.year === minDate.year) return currentSelectedDate.year-- currentSelectedDate.month = 12 currentSelectedDate.date = 31 } else { if (currentSelectedDate.year === maxDate.year) return currentSelectedDate.year++ currentSelectedDate.month = 1 currentSelectedDate.date = 1 } emits('change-year', currentSelectedDate.year) reloadMonthSwiper.value = true setTimeout(() => { reloadMonthSwiper.value = false }, 0) // 重新生产日历数据 generateCalendarData() } return { reloadMonthSwiper, calendarId, weekText, calendarData, currentMonthIndex, dateContainerHeight, minDate, maxDate, currentSelectedDate, swiperSwitchMonthEvent, swiperSwitchMonthAnimationFinishEvent, dateItemClickEvent, switchMonth, switchYear, } }