用日历数据写一个 vue 自定义日历组件

说明

本文语法使用的是组合式,需要vue3或者 vue2.7

组件样式使用的是 tailwindcss,这里只是简单介绍下我的日历组件是如何构成的。

本来打算开源的,后来想想也不好看,也只有一个组件,就算了。

日历数据来源用 dayjs 得到一个日历的数据

思路

日历头

html 复制代码
<div class="calendar" :style="{ 'background-color': backgroundColor }">
  <div v-if="showHeader" class="header flex justify-between items-center px-3 py-2 border-b">
    <div class="title">
      <span v-if="showHeaderYear">{{ selectDate.year() }} 年 </span>
      {{ selectDate.month() + 1 }} 月
    </div>
    <div class="button-group flex items-center justify-end">
      <button
        class="button border rounded-l py-1 px-4 text-center text-xs lg:hover:text-sky-600 lg:hover:bg-sky-100 lg:hover:border-sky-200"
        style="margin-right: -1px" @click="changeMonth(-1)">上个月</button>
      <button
        class="button border py-1 px-4 text-center text-xs lg:hover:text-sky-600 lg:hover:bg-sky-100 lg:hover:border-sky-200"
        @click="today">今天</button>
      <button
        class="button border rounded-r py-1 px-4 text-center text-xs lg:hover:text-sky-600 lg:hover:bg-sky-100 lg:hover:border-sky-200"
        style="margin-left: -1px" @click="changeMonth(1)">下个月</button>
    </div>
  </div>

日历头很简单,只有左边一个标题显示了当前的年和月,右边是三个控制跳转的按钮,分别是上个月今天下个月

其中有几个是传入的可选 props

go 复制代码
// 日历背景颜色
backgroundColor: {
  type: String,
  default: '#fff',
},
// 是否显示日历头
showHeader: {
  type: Boolean,
  default: true
},

这里面的selectDate 是当前选择的日期经过 dayjs 格式化后的对象

js 复制代码
// 传入的日期,使用时通常传入今天
date: {
  type: String,
  default: dayjs().format('YYYY-MM-DD')
},
// 传入日期的 dayjs 格式化对象
const selectDate = computed(() => dayjs(props.date))

此处的 changeMonth和 today 方法稍后再说

日历 body 部分

背景显示月份

其实这个有没有都行,也很少有需求会点名要这个功能

html 复制代码
<div class="body relative p-2">
  <div v-if="showMonthOnBackground"
    :class="['month-shadow', 'absolute', 'text-9xl', 'top-1/2', 'left-1/2', 'text-gray-100/80', '-translate-x-2/4', '-translate-y-2/4', 'pointer-events-none']">
    {{ month.split('-')[1] }}
  </div>

它的作用就是在日历的背景中显示一个月份的虚影

js 复制代码
// 是否背景显示月份虚影
showMonthOnBackground: {
  type: Boolean,
  default: false
},
// 当前选中日期的年-月
const month = computed(() => {
  return `${selectDate.value.year()}-${selectDate.value.month() + 1}`
})

日历周标题

考虑到可能会有定制周标题的需求,于是开放成了 props

js 复制代码
// 星期的显示文字
weekDays: {
  type: Array,
  default: () => ['一', '二', '三', '四', '五', '六', '日']
},
// 一周是否开始于周一
weekStartsOnMonday: {
  type: Boolean,
  default: true
},
// 星期的样式
weekDayStyle: {
  type: Object,
  default: () => ({})
},

const _weekDays = computed(() => {
  let weekDays = [...props.weekDays];
  if (!props.weekStartsOnMonday) {
    const lastDay = weekDays.pop()
    weekDays.unshift(lastDay)
  }
  return weekDays;
})

配置 weekDays 时不用管一周开始于周几,按周一至周日排,如果将 weekStartsOnMonday 配置成 false 时会自动将周日放到第一位的

html 复制代码
<div class="weekdays grid grid-cols-7">
  <div v-for="weekday in _weekDays" :key="weekday" class="weekday text-center text-base py-3"
    :style="weekDayStyle">{{ weekday }}
  </div>
</div>

日历日期部分

html 复制代码
<div class="week relative grid grid-cols-7" v-for="(week, weekIndex) in calendar">
  <div v-for="(day, dayIndex) in week" :key="day.fullDate" :class="[
'day',
'text-center',
'text-base',
'cursor-pointer',
'lg:hover:bg-blue-200',
'transition-colors',
{
'border-r': bordered,
'border-b': bordered,
'border-t': !weekIndex && bordered,
'border-l': !dayIndex && bordered,
'pointer-events-none': isCellDisabled(day),
'opacity-50': isCellDisabled(day),
'invisible': isHiddenCell(day),
}]" @click="onSelect(day)" :style="dateCellStyle">
    <div
      :class="['day-inner', 'py-2', { 'text-gray-400': !day.isCurrentMonth, 'text-sky-500': day.isToday || selectDate.isSame(day.fullDate, 'day'), 'bg-blue-200': selectDate.isSame(day.fullDate, 'day') }]">
      <slot :day="day" name="date-cell"><span>{{ day.date }}</span></slot>
    </div>
  </div>
</div>

这里要说明的比较多,首先日历数据 calendar

js 复制代码
const calendar = ref([])
// 每当选中日期的月份变化了之后,就重新计算日历数据
watch(month, () => {
  calendar.value = generateCalendar(selectDate.value, {
    weekStartsOnMonday: props.weekStartsOnMonday
  })
}, { immediate: true })

得到的是一个二维数组,所以在画UI 时要循环两次,一次是周,一次是日期。

样式部分没什么好说的,只有一点要解释一下,为什么在加 hover 时要加上个前缀 lg,是因为这个组件我是移动端和桌面端同时使用的,在移动端时会有 hover 残留的现象,所以为了解决就加上了lg。

isCellDisabled 是一个判断该日期是否禁止的方法

js 复制代码
// 是否禁止选择非当前月份的日期
disableNonCurrentMonth: {
  type: Boolean,
  default: false
},
// 禁止选中的日期
disabledDate: {
  type: Function,
  default: () => false
},

function isCellDisabled(day) {
  if (props.disableNonCurrentMonth && !day.isCurrentMonth) {
    return true
  }
  return props.disabledDate(day)
}

除了可以配置禁止选择当前月份的日期外,还可以自行配置禁止的日期,例如:

js 复制代码
<Calendar :disabledDate="handleDisabledDate" />

function handleDisabledDate (day) {
 if ([2, 3, 4, 5, 6].includes(day.date)){
   return true
 }
  return false
}

isHiddenCell 是一个判断日期是否隐藏的方法,用于判断这一天是否需要隐藏不显示

js 复制代码
// 是否显示非本月的日期
showOtherMonth: {
  type: Boolean,
  default: true
},
// 是否显示上个月的日期
showPrevMonth: {
  type: Boolean,
  default: true
},
// 是否显示下个月的日期
showNextMonth: {
  type: Boolean,
  default: true
},

function isHiddenCell(day) {
  if (day.isCurrentMonth) return false;
  if (!props.showOtherMonth) return true;
  if (!props.showPrevMonth && day.isPrevMonth) return true;
  if (!props.showNextMonth && day.isNextMonth) return true;
}

显示日期

因为有自定义日期内容的需求,所以日期部分写成了个插槽,将日期的数据传递出去,如何显示自己决定,默认只是一个日期

html 复制代码
<Calendar>
  <template #date-cell="{ day }">
    <span>自定义内容{{ day.fullDate }}</span>
  </template>
</Calendar>

切换日期

下面说下在点击切换日期时 还有之前提到的上个月 今天 下个月三个按钮都做了什么

其实他们做的都是同一个事,就是触发一个 onSelect 的 emit,由父组件来改变当前选中的日期

js 复制代码
// 点击上/下个月选择的日期,first: 上/下个月第一天,last: 上/下个月最后一天,none: 当前选择日期的上/下个月
monthSelectType: {
  type: String,
  default: 'none',
},

const emit = defineEmits(['onSelect'])
const FORMAT = 'YYYY-MM-DD';

function today() {
  const today = dayjs();
  emit('onSelect', today.format(FORMAT), {
    from: 'today',
    isOverMonth: !selectDate.value.isSame(today, 'month')
  })
}

function onSelect(newDate) {
  emit('onSelect', newDate.fullDate, {
    from: 'select',
    isOverMonth: !selectDate.value.isSame(newDate.fullDate, 'month')
  })
}

function changeMonth(delta) {
  const newMonthDay = selectDate.value.add(delta, 'month');

  let targetDate = null;

  if (props.monthSelectType === 'first') {
    targetDate = newMonthDay.startOf('month');
  } else if (props.monthSelectType === 'last') {
    targetDate = newMonthDay.endOf('month');
  } else {
    targetDate = newMonthDay;
  }

  emit('onSelect', targetDate.format(FORMAT), {
    from: delta > 0 ? 'next' : 'prev',
    isOverMonth: true,
  })
}

除了向父组件传递了新的日期之外,还传了一个数据,实际上目前的使用过程中只有 isOverMonth 这个参数用到了

向外暴露选择上下月份的方法

有时候并不想使用自带的 header,而是在页面上其他位置自定义了上下月份的按钮, 所以组件要将这两个方法暴露出去

js 复制代码
function prevMonth() {
  changeMonth(-1)
}
function nextMonth() {
  changeMonth(1)
}
defineExpose({
  prevMonth,
  nextMonth,
})

使用时:

js 复制代码
<Calendar ref="calendarRef" />

const calendarRef = ref(null)

function handleNextMonth() {
  calendarRef.value.nextMonth()
}

使用

使用这个日历组件时要传入选择的日期和改变日期的方法

js 复制代码
<Calendar :date="date" @onSelect="handleSelectDate" />

const date = ref(dayjs().format('YYYY-MM-DD'))

function handleSelectDate(newDate, { isOverMonth }) {
  date.value = newDate;
  // 可以在这获取这日的数据
  if (isOverMonth) {
    // 可以在这当跨月时获取月份所需的数据
  }
}
相关推荐
独泪了无痕1 小时前
CryptoJS:数据安全的JavaScript加密利器
前端·vue.js·node.js
熊猫_豆豆2 小时前
一个模拟四轴飞行器在随机气流扰动下悬停飞行的交互式3D仿真网页,包含飞行器建模与PID控制算法
javascript·3d·html·四轴无人机模拟飞行
来恩10033 小时前
jQuery选择器
前端·javascript·jquery
前端繁华如梦3 小时前
树上挂苹果还是挂玻璃球?Three.js 程序化果实的完整实现指南
前端·javascript
CDwenhuohuo4 小时前
优惠券组件直接用 uview plus
前端·javascript·vue.js
川冰ICE5 小时前
TypeScript装饰器与元编程实战
前端·javascript·typescript
AI砖家5 小时前
Vue3组件传参大全,各种传参方式的对比
前端·javascript·vue.js
希望永不加班5 小时前
var局部变量类型推断的利弊
java·服务器·前端·javascript·html
threelab5 小时前
Three.js 3D 地图可视化 | 三维可视化 / AI 提示词
前端·javascript·人工智能·3d·着色器
爱怪笑的小杰杰5 小时前
Leaflet 高性能大数据量图圆:彻底解决缩放/拖拽偏移问题
大数据·前端·vue.js·贴图