@tuniao/tnui-vue3-uniapp
Version:
TuniaoUI for vue3 uniapp
644 lines (593 loc) • 20.7 kB
text/typescript
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,
}
}