用日历数据写一个 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) {
    // 可以在这当跨月时获取月份所需的数据
  }
}
相关推荐
沐土Arvin28 分钟前
探索原生JS的力量:自定义实现类似于React的useState功能
前端·javascript·react.js
JYeontu1 小时前
语音指令怎么避免同音词干扰?
前端·javascript
逆袭的小黄鸭1 小时前
JavaScript 异步操作入门指南与基础实践
前端·javascript
意桉1 小时前
Element Plus 去掉表格外边框
前端·vue.js·elementui·element plus
反复的大魔王1 小时前
Ant Design Vue的日历组件(Calendar)在中文语言包环境下设置以周日开始?
vue.js·ant design
逾明1 小时前
使用js创建img加载阿里云oss图片跨域的问题
前端·javascript·浏览器
umigreen1 小时前
element-plus组件Upload 上传图片(超详细)
vue.js
码觉客1 小时前
在 PDF.js 的 viewer.html 基础上进行改造,实现同一个 PDF 文件在网页中上下拆分显示,并且两部分的标注数据能够实时同步
前端·javascript
sunn。1 小时前
V-SHOW和箭头函数在VUE项目的踩坑点
前端·javascript·vue.js