手摸手之日历组件

673 阅读5分钟

开始

  • 本文纯靠自己思路来写,如有雷同,也不是抄袭。
  • 阅读本文前,需要了解一点ts,vue3,jsx语法。😯 还有moment相关语法。

预想效果 or 实际效果 简直是一模一样😄

  • 预想效果 element-ui 日历组件 截屏2021-08-14 下午4.08.31.png
  • 实际效果 截屏2021-08-14 下午4.10.33.png

实现

  • 日历组件的实现,借助于 moement.js 大大的简化了计算流程,也减少了很多代码量。可以说极大的减少了实现难度。
  • 本文仅是作为练习,如果有错误的地方,烦请指正。
  • 日历这个大家知道,费劲吧啦的介绍使用场景个人觉得是没什么用的,你遇到了自然就能想到。

计算日期-页面显示那些日期-最主要的函数

  • 这个其实有两种 一种是页面只显示当前月,一种就是上边图片里的这种,按照每行一周显示,这个稍稍复杂了一点。为什么说复杂一点呢?
    • 获取整月的话,直接获取当月多少天然后从一号开始累加就能获取整月的所有天数。
    • 按照每周一行显示的话,你需要获取当前月的第一天是周几然后计算往前推几天,进行展示。

interface obj {
  [propName: string]: string | number;
}

// 周几转换成中文 虽然没有用到😁
const weekList = ['周日', '周一', '周二', '周三', '周四', '周五', '周六']
let listOfDatesOfTheMonth: Array<obj> = reactive([])

// 计算日历所需要展示的天数
const generatedDate = (val?: string) => {
  // 获取是几月
  const month = moment(val || new Date()).format('YYYY-MM')
  // 获取当月第一天是周几
  const whichDay = moment(month).startOf('month').weekday()
  // 计算开始开始日期 7 减去当月第一天是周几(获取到的周几是 0123456, 其中0代表周日)
  const startDate = moment(moment(month).subtract(7 - whichDay, 'days')).format('YYYY-MM-DD')

  // 整理数据
  // [...Array(42).keys()] 这个是生成了长度42的数组,为什么是42呢? 因为有的月份是28 或者30 31, 35 会导致某些月份展示不全。
  listOfDatesOfTheMonth = [...Array(42).keys()].reduce((acc:Array<obj>, val) => {
    // 获取展示的日期
    const date = moment(startDate).add(val + 1, 'days').format('YYYY-MM-DD')
    // 分割日期 用于获取是 几号 如 123456
    const dateList = date.split('-')
    // 获取周几
    const week = moment(date).weekday()

    // 将需要的数据 放进数组
    acc.push({ index: val + 1, date: date, day: dateList[dateList.length - 1], week: weekList[week] })
    // 返回数组 为啥要返回 可以去mdn 看下reduce
    return acc
  }, [])
  console.log(listOfDatesOfTheMonth)

  getDateItemList()
}

上一月 or 当天 or 下一月

  • 这个的实现 我们需要记录下当前是那个月,然后加一个月 减去一月,当天直接 new Date() ?
// 当前天 选中天
const currentDate = ref(moment().format('YYYY-MM-DD'))
// 当前月
const currentMonth = ref(moment().format('YYYY-MM'))

// 上一月 下一月 当前月
const changeMonth = (type: string) => {
  // 设置日期 这个用于 点击月不是当前月 重新生成展示数据
  if (type === 'setDate') {
    currentMonth.value = moment(currentDate.value).format('YYYY-MM')
  }

  // 今日
  if (type === 'today') {
    currentMonth.value = moment().format('YYYY-MM')
  }

  // 下个月
  if (type === 'nextMonth') {
    currentMonth.value = moment(currentMonth.value).add(1, 'month').format('YYYY-MM')
  }

  // 上个月
  if (type === 'lastMonth') {
    currentMonth.value = moment(currentMonth.value).subtract(1, 'month').format('YYYY-MM')
  }

  currentDate.value = moment(currentMonth.value).format('YYYY-MM-DD')
  generatedDate(currentMonth.value)
}

点击日历的日期

  • 这个有两个效果 一个选中日期增加背景色,判断当前日期是否等于选中日期 增加class实现
  • 点击日期不是当月,重新生成数据。
// 设置当前天 选中天
const setCurrentDate = (date:string) => {
  currentDate.value = date
  // 月份不同 重新生成日历
  if (date.indexOf(currentMonth.value) === -1) {
    changeMonth('setDate')
  }
}

// 获取每天 设置样式及操作
let dateItemList: JSX.Element[] = []
const getDateItemList = () => {
  dateItemList = listOfDatesOfTheMonth.map(item => {
    // item.date 的类型可以是字符串或数组 详见obj的定义
    const isCurrentMonth = String(item.date).indexOf(currentMonth.value) === -1
    // 计算 绑定的class
    const className = `dateItem ${isCurrentMonth ? '' : 'currentMonth'} ${currentDate.value === item.date ? 'currentDate' : ''}`
    return <div onClick={() => { setCurrentDate(item.date + '') }} class={className}>{parseInt(item.day + '') }</div>
  })
}

最终代码

import { defineComponent, ref, reactive, onMounted, watch } from 'vue'
import './indenx.scss'
import moment from 'moment'

export default defineComponent({
  name: 'calender',
  setup () {
    interface obj {
      [propName: string]: string | number;
    }

    // 当前天 选中天
    const currentDate = ref(moment().format('YYYY-MM-DD'))
    // 当前月
    const currentMonth = ref(moment().format('YYYY-MM'))

    const weekList = ['周日', '周一', '周二', '周三', '周四', '周五', '周六']
    let listOfDatesOfTheMonth: Array<obj> = reactive([])

    // 计算日历所需要展示的天数
    const generatedDate = (val?: string) => {
      // 获取是几月
      const month = moment(val || new Date()).format('YYYY-MM')
      // 获取当月第一天是周几
      const whichDay = moment(month).startOf('month').weekday()
      // 计算开始开始日期 7 减去当月第一天是周几(获取到的周几是 0123456, 其中0代表周日)
      const startDate = moment(moment(month).subtract(7 - whichDay, 'days')).format('YYYY-MM-DD')

      // 整理数据
      // [...Array(42).keys()] 这个是生成了长度42的数组,为什么是42呢? 因为有的月份是28 或者30 31, 35 会导致某些月份展示不全。
      listOfDatesOfTheMonth = [...Array(42).keys()].reduce((acc:Array<obj>, val) => {
        // 获取展示的日期
        const date = moment(startDate).add(val + 1, 'days').format('YYYY-MM-DD')
        // 分割日期 用于获取是 几号 如 123456
        const dateList = date.split('-')
        // 获取周几
        const week = moment(date).weekday()

        // 将需要的数据 放进数组
        acc.push({ index: val + 1, date: date, day: dateList[dateList.length - 1], week: weekList[week] })
        // 返回数组 为啥要返回 可以去mdn 看下reduce
        return acc
      }, [])
      console.log(listOfDatesOfTheMonth)

      getDateItemList()
    }

    onMounted(() => {
      // 初始化日历默认当前月
      changeMonth('today')
    })

    // 监听当前选中日期 更新视图
    watch(currentDate, () => {
      getDateItemList()
    })

    // 获取每天 设置样式及操作
    let dateItemList: JSX.Element[] = []
    const getDateItemList = () => {
      dateItemList = listOfDatesOfTheMonth.map(item => {
        // item.date 的类型可以是字符串或数组 详见obj的定义
        const isCurrentMonth = String(item.date).indexOf(currentMonth.value) === -1
        // 计算 绑定的class
        const className = `dateItem ${isCurrentMonth ? '' : 'currentMonth'} ${currentDate.value === item.date ? 'currentDate' : ''}`
        return <div onClick={() => { setCurrentDate(item.date + '') }} class={className}>{parseInt(item.day + '') }</div>
      })
    }

    // 上一月 下一月 当前月
    const changeMonth = (type: string) => {
      // 今日
      if (type === 'setDate') {
        currentMonth.value = moment(currentDate.value).format('YYYY-MM')
      }

      // 今日
      if (type === 'today') {
        currentMonth.value = moment().format('YYYY-MM')
      }

      // 下个月
      if (type === 'nextMonth') {
        currentMonth.value = moment(currentMonth.value).add(1, 'month').format('YYYY-MM')
      }

      // 上个月
      if (type === 'lastMonth') {
        currentMonth.value = moment(currentMonth.value).subtract(1, 'month').format('YYYY-MM')
      }

      currentDate.value = moment(currentMonth.value).format('YYYY-MM-DD')
      generatedDate(currentMonth.value)
    }

    // 设置当前天 选中天
    const setCurrentDate = (date:string) => {
      currentDate.value = date
      // 月份不同 重新生成日历
      if (date.indexOf(currentMonth.value) === -1) {
        changeMonth('setDate')
      }
    }

    // header 周 列表
    const weekItemList = weekList.map(item => {
      return <div class='dateItem'>{item}</div>
    })

    return () => (
      <div class='calendar-box'>
        {currentDate.value}
        <div class='header'>
          <p>{currentMonth.value}</p>
          <div class='operate'>
            <p onClick={() => {
              changeMonth('lastMonth')
            }}>上个月</p>
            <p onClick={() => {
              changeMonth('today')
            }}>今天</p>
            <p onClick={() => {
              changeMonth('nextMonth')
            }}>下个月</p>
          </div>
        </div>
        <div class='weekBox'>{weekItemList}</div>
        <div class='dateItemBox'>{dateItemList}</div>
      </div>
    )
  }
})
// indenx.scss
// calendar 日历组件
.calendar-box {
  width: 430px;
  border: 1px solid #ebeef5;

  .header {
    display: flex;
    justify-content: space-between;
    align-content: center;
    padding: 5px;
    border-bottom: 1px solid #ebeef5;

    .operate {
      display: flex;
      justify-content: flex-end;

      p {
        padding: 5px 10px;
        margin-left: 5px;
        border-radius: 2px;
        border: 1px solid #ebeef5;
        cursor: pointer;
      }

      p:hover {
        background: #f2f8fe;
      }
    }
  }



  .dateItemBox , .weekBox {
    width: 100%;
    display: flex;
    justify-content: space-between;
    flex-wrap: wrap;
    .dateItem {
      color: gray;
      width: calc(100% / 7);
      height: 40px;
      border: 1px solid #ebeef5;
      cursor: default;
    }

    .dateItem:nth-of-type(7n) {
      margin-right: 0;
    }

    .dateItem:hover{
      background: #f2f8fe;
    }


    .currentMonth{
      color: #303133;
    }

    .currentDate{
      background: #f2f8fe;
    }
  }

  .weekBox{
    .dateItem{
      text-align: center;
      line-height: 40px;
      border: none;
    }
  }
}