2025/2/8第一次更新

This commit is contained in:
爱吃咸鱼小猫咪
2025-02-08 18:50:38 +08:00
commit d7af560866
26519 changed files with 5046029 additions and 0 deletions
@@ -0,0 +1,109 @@
import { CHANGE_EVENT, UPDATE_MODEL_EVENT } from '../../../constants'
import {
buildProps,
definePropType,
isArray,
isNumber,
isString,
} from '../../../utils'
import type { ExtractPropTypes } from 'vue'
import type { Arrayable } from '../../../utils'
export const calendarModes = ['date', 'multi', 'range'] as const
export type CalendarModelValueType = Arrayable<string>
export const calendarProps = buildProps({
/**
* @description 日期绑定的值,支持的格式为 YYYY/MM/DD 2023/01/01 或者 YYYY-MM-DD 2023-01-01 [2023/01/01, 2023/01/02] [2023-01-01, 2023-01-02]
*/
modelValue: {
type: definePropType<CalendarModelValueType>([String, Array]),
default: '',
},
/**
* @description 选中时的背景颜色,以tn开头使用图鸟内置的颜色
*/
activeBgColor: String,
/**
* @description 选中时的文字颜色,以tn开头使用图鸟内置的颜色
*/
activeTextColor: String,
/**
* @description 在日期范围内的背景颜色,以tn开头使用图鸟内置的颜色
*/
rangeBgColor: String,
/**
* @description 在日期范围内的文字颜色,以tn开头使用图鸟内置的颜色
*/
rangeTextColor: String,
/**
* @description 日期模式
*/
mode: {
type: String,
values: calendarModes,
default: 'date',
},
/**
* @description 日期最小值,支持的格式为 YYYY/MM/DD 或者 YYYY-MM-DD,如果没有设置最小值则以当前日期作为最小值
*/
minDate: String,
/**
* @description 日期最大值,支持的格式为 YYYY/MM/DD 或者 YYYY-MM-DD,如果没有设置最大值则以当前年份最后一天作为最大值
*/
maxDate: String,
/**
* @description 是否允许切换年份
*/
allowChangeYear: {
type: Boolean,
default: true,
},
/**
* @description 是否允许切换月份
*/
allowChangeMonth: {
type: Boolean,
default: true,
},
/**
* @description 是否显示农历
*/
showLunar: Boolean,
/**
* @description 当 mode 为 range 时,开始时间的提示信息
*/
rangeStartDesc: {
type: String,
default: '开始',
},
/**
* @description 当 mode 为 range 时,结束时间的提示信息
*/
rangeEndDesc: {
type: String,
default: '结束',
},
})
export const calendarEmits = {
[UPDATE_MODEL_EVENT]: (value: CalendarModelValueType) =>
isArray(value) || isString(value),
[CHANGE_EVENT]: (value: CalendarModelValueType) =>
isArray(value) || isString(value),
/**
* @description 年份切换事件
*/
'change-year': (year: number) => isNumber(year),
/**
* @description 月份切换事件
*/
'change-month': (month: number) => isNumber(month),
}
export type CalendarProps = ExtractPropTypes<typeof calendarProps>
export type CalendarEmits = typeof calendarEmits
export type CalendarMode = (typeof calendarModes)[number]
@@ -0,0 +1,150 @@
<script lang="ts" setup>
import TnIcon from '../../icon/src/icon.vue'
import { calendarEmits, calendarProps } from './calendar'
import { useCalendar, useCalendarCustomStyle } from './composables'
const props = defineProps(calendarProps)
const emit = defineEmits(calendarEmits)
const {
reloadMonthSwiper,
calendarId,
dateContainerHeight,
calendarData,
weekText,
currentMonthIndex,
currentSelectedDate,
minDate,
maxDate,
swiperSwitchMonthEvent,
swiperSwitchMonthAnimationFinishEvent,
dateItemClickEvent,
switchMonth,
switchYear,
} = useCalendar(props, emit)
const { ns, itemClass, itemStyle } = useCalendarCustomStyle(props)
</script>
<template>
<view :id="calendarId" :class="[ns.b(), ns.m(mode)]">
<!-- 操作区域 -->
<view :class="[ns.e('operation')]">
<!-- 年切换按钮 -->
<view
v-if="allowChangeYear && currentSelectedDate.year !== minDate.year"
:class="[ns.e('operation__year-btn')]"
@tap.stop="switchYear('prev')"
>
<TnIcon name="left-triangle" />
</view>
<!-- 年月显示 -->
<view :class="[ns.e('operation__value')]">
{{ currentSelectedDate.year }}
</view>
<!-- 年切换按钮 -->
<view
v-if="allowChangeYear && currentSelectedDate.year !== maxDate.year"
:class="[ns.e('operation__year-btn')]"
@tap.stop="switchYear('next')"
>
<TnIcon name="right-triangle" />
</view>
</view>
<!-- 星期文字提示 -->
<view :class="[ns.e('week-text')]">
<view
v-for="(item, index) in weekText"
:key="index"
:class="[ns.e('week-text__item')]"
>
{{ item }}
</view>
</view>
<!-- 数据展示区域 -->
<view
:class="[ns.e('data')]"
:style="{
height: `${dateContainerHeight ? `${dateContainerHeight}px` : 'auto'}`,
}"
>
<!-- 月份背景 -->
<view :class="[ns.e('data__month-bg')]">
{{ currentSelectedDate.month }}
</view>
<!-- 月份切换按钮 -->
<view
v-if="
allowChangeMonth &&
!(
(currentSelectedDate.year === minDate.year &&
currentSelectedDate.month === minDate.month) ||
currentSelectedDate.month === 1
)
"
class="left"
:class="[ns.e('data__month-btn')]"
@tap.stop="switchMonth('prev')"
>
<TnIcon name="left" />
</view>
<view
v-if="
allowChangeMonth &&
!(
(currentSelectedDate.year === maxDate.year &&
currentSelectedDate.month === maxDate.month) ||
currentSelectedDate.month === 12
)
"
class="right"
:class="[ns.e('data__month-btn')]"
@tap.stop="switchMonth('next')"
>
<TnIcon name="right" />
</view>
<swiper
v-if="!reloadMonthSwiper"
:class="[ns.e('data__swiper')]"
:current="currentMonthIndex"
:indicator-dots="false"
:autoplay="false"
:circular="false"
adjust-height="none"
@change="swiperSwitchMonthEvent"
@animationfinish="swiperSwitchMonthAnimationFinishEvent"
>
<swiper-item
v-for="(item, index) in calendarData"
:key="index"
:class="[ns.e('data__swiper-item')]"
>
<view :class="[ns.e('data__dates')]">
<view
v-for="(dateItem, dateIndex) in item.data"
:key="dateIndex"
:class="[ns.e('data__date'), itemClass(dateItem.status)]"
:style="itemStyle(dateItem.status)"
>
<view
v-if="dateItem.date != 0"
:class="[ns.e('data__date__content')]"
@tap.stop="dateItemClickEvent(dateItem)"
>
<view class="date">{{ dateItem.date }}</view>
<view v-if="dateItem.desc" class="desc">
{{ dateItem.desc }}
</view>
</view>
</view>
</view>
</swiper-item>
</swiper>
</view>
</view>
</template>
<style lang="scss" scoped>
@import '../../../theme-chalk/src/calendar.scss';
</style>
@@ -0,0 +1,94 @@
import { computed, toRef } from 'vue'
import { useComponentColor, useNamespace } from '../../../../hooks'
import type { CSSProperties } from 'vue'
import type { CalendarProps } from '../calendar'
import type { CalendarItemDateStatus } from '../types'
type itemClassType = (status: CalendarItemDateStatus) => string
type itemStyleType = (status: CalendarItemDateStatus) => CSSProperties
export const useCalendarCustomStyle = (props: CalendarProps) => {
const ns = useNamespace('calendar')
// 解析颜色
const [activeBgColorClass, activeBgColorStyle] = useComponentColor(
toRef(props, 'activeBgColor'),
'bg'
)
const [activeTextColorClass, activeTextColorStyle] = useComponentColor(
toRef(props, 'activeTextColor'),
'text'
)
const [rangeBgColorClass, rangeBgColorStyle] = useComponentColor(
toRef(props, 'rangeBgColor'),
'bg'
)
const [rangeTextColorClass, rangeTextColorStyle] = useComponentColor(
toRef(props, 'rangeTextColor'),
'text'
)
// dateItem对应的类
const itemClass = computed<itemClassType>(() => {
return (status: CalendarItemDateStatus) => {
const cls: string[] = [ns.is(status)]
if (status === 'active') {
if (activeBgColorClass.value) {
cls.push(activeBgColorClass.value)
}
if (activeTextColorClass.value) {
cls.push(activeTextColorClass.value)
}
} else if (status === 'range') {
if (rangeBgColorClass.value) {
cls.push(rangeBgColorClass.value)
}
if (rangeTextColorClass.value) {
cls.push(rangeTextColorClass.value)
}
}
return cls.join(' ')
}
})
// dateItem对应的样式
const itemStyle = computed<itemStyleType>(() => {
return (status: CalendarItemDateStatus) => {
const style: CSSProperties = {}
if (status === 'active') {
if (!activeBgColorClass.value) {
style.backgroundColor =
activeBgColorStyle.value || 'var(--tn-color-primary)'
}
if (activeTextColorStyle.value) {
style.color = activeTextColorStyle.value
} else if (!activeBgColorClass.value && !activeTextColorClass.value) {
style.color = 'var(--tn-color-white)'
}
} else if (status === 'range') {
if (!rangeBgColorClass.value) {
style.backgroundColor =
rangeBgColorStyle.value || 'var(--tn-color-primary-light-7)'
}
if (rangeTextColorStyle.value) {
style.color = rangeTextColorStyle.value
} else if (!rangeBgColorClass.value && !rangeTextColorClass.value) {
style.color = 'var(--tn-color-primary)'
}
}
return style
}
})
return {
ns,
itemClass,
itemStyle,
}
}
@@ -0,0 +1,2 @@
export * from './calendar-custom'
export * from './use-calendar'
@@ -0,0 +1,67 @@
import { computed, getCurrentInstance, nextTick, onMounted, ref } from 'vue'
import { useSelectorQuery } from '../../../../hooks'
import { debugWarn, generateId } from '../../../../utils'
import type { Ref } from 'vue'
import type { CalendarMode } from '../calendar'
export const useCalendarSelector = (
currentDateCount: Ref<number>,
mode: CalendarMode
) => {
const instance = getCurrentInstance()
if (!instance) {
debugWarn('TnCalendar', '请在 setup 函数中使用 useCalendarSelector')
}
const calendarId = `tc-${generateId()}`
const { getSelectorNodeInfo } = useSelectorQuery(instance)
// 单个日期的高度
const singleDateItemHeight = ref<number>(0)
// 日期容器的高度
const dateContainerHeight = computed<number>(
() => Math.ceil(currentDateCount.value / 7) * singleDateItemHeight.value
)
let initCount = 0
// 获取单个日期容器的信息
const getDateItemComponentRectInfo = async () => {
try {
const rectInfo = await getSelectorNodeInfo(
`#${calendarId} .tn-calendar__data__date`
)
initCount = 0
singleDateItemHeight.value = rectInfo.width || 0
if (mode === 'date' || mode === 'multi') {
singleDateItemHeight.value += uni.upx2px(12)
}
} catch (err) {
if (initCount > 10) {
initCount = 0
debugWarn('TnCalendar', `获取单个日期容器信息失败: ${err}`)
return
}
initCount++
setTimeout(() => {
getDateItemComponentRectInfo()
}, 150)
}
}
onMounted(() => {
nextTick(() => {
setTimeout(() => {
getDateItemComponentRectInfo()
}, 50)
})
})
return {
calendarId,
dateContainerHeight,
}
}
@@ -0,0 +1,643 @@
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,
}
}
@@ -0,0 +1,3 @@
import type Calendar from './calendar.vue'
export type TnCalendarInstance = InstanceType<typeof Calendar>
+43
View File
@@ -0,0 +1,43 @@
import type { Dayjs } from '../../../libs/dayjs'
export type CalendarItemDateStatus = 'normal' | 'active' | 'range' | 'disabled'
export interface CalendarItem {
/**
* @description 日期
*/
date: number
/**
* @description 日期状态
*/
status: CalendarItemDateStatus
/**
* @description 日期描述,可以存放农历、开始结束提示信息
*/
desc?: string
}
export interface CalendarMonthData {
/**
* @description 月份
*/
month: number
/**
* @description 月份的数据
*/
data: CalendarItem[]
}
export type CalendarData = CalendarMonthData[]
export interface DateData {
year: number
month: number
date: number
}
export type CalendarSelectDataMap = Map<number, number[]>
export type CalendarRangeSelectData = {
start?: Dayjs
end?: Dayjs
}