轻量级vue日历组件

<template>

  <div

    v-click-outside="bindEvent"

    ref="agm-datepicker"

    class="agm-datepicker">

    <div

      :class="{active: show}"

      class="agm-input"

      @click.stop="show=!show">

      <input

        :value="current | dateFormat"

        type="text"

        readonly>

    </div>

    <div

      v-if="show"

      :class="{positionTop:positionTop}"

      class="agm-datepicker-wrap">

      <div

        class="agm-datepicker-header"

        @click.stop="">

        <span

          v-if="!selectMonth"

          class="agm-datepicker-header-btn agm-datepicker-header-btn-pre"

          @click.stop="switchYear(-1)">&lt;&lt;</span>

        <span

          v-if="!selectMonth && !selectYear"

          class="agm-datepicker-header-btn agm-datepicker-header-btn-pre"

          @click.stop="switchMonth(-1)">&lt;</span>

        <span class="agm-datepicker-header-btn-day">

          <span

            class="agm-datepicker-header-btn"

            @click.stop="openSelectYear">{{ select.year }}年 </span>

          <span

            class="agm-datepicker-header-btn"

            @click.stop="openSelectMonth">{{ select.month }}月</span>

        </span>

        <span

          v-if="!selectMonth"

          class="agm-datepicker-header-btn agm-datepicker-header-btn-next"

          @click.stop="switchYear(1)">&gt;&gt;</span>

        <span

          v-if="!selectMonth && !selectYear"

          class="agm-datepicker-header-btn agm-datepicker-header-btn-next"

          @click.stop="switchMonth(1)">&gt;</span>

      </div>

      <div class="agm-datepicker-content">

        <table>

          <thead>

            <th>日</th>

            <th>一</th>

            <th>二</th>

            <th>三</th>

            <th>四</th>

            <th>五</th>

            <th>六</th>

          </thead>

          <tbody>

            <tr

              v-for="(week,index) of list"

              :key="'week_'+index">

              <td

                v-for="(weekday,idx) of week"

                :key="'weekday_'+index+idx"

                :class="{

                  'flag': weekday.flag,

                  'active': !weekday.flag && weekday.text == current.date

                && select.month == current.month && select.year == current.year}"

                @click="pick(weekday)">

                {{ weekday.text }}

              </td>

            </tr>

          </tbody>

        </table>

        <div

          v-if="selectYear"

          class="agm-date-picker-year-panel">

          <div class="panel-list">

            <div

              v-for="(y, index) of years"

              :key="'year_'+index"

              class="panel-item">

              <div

                :class="{active: y===select.year}"

                class="panel-item-inner"

                @click.stop="pickYear(y)">{{ y }}</div> 

            </div>

          </div>

        </div>

        <div

          v-if="selectMonth"

          class="agm-date-picker-year-panel">

          <div class="panel-list">

            <div

              v-for="(m, index) of months"

              :key="'month_'+index"

              class="panel-item">

              <div

                :class="{active: (index+1)===select.month}"

                class="panel-item-inner"

                @click.stop="pickMonth(index+1)">{{ m }}</div> 

            </div>

          </div>

        </div>

      </div>

    </div>

  </div>

</template>

<script>

export default {

  name: 'AgmDatePicker',

  filters: {

    // 日期格式过滤器

    dateFormat(val) {

      if (!val) {

        return ''

      }

      return `${val.year}-${val.month}-${val.date}`.replace(/\d+/g, a => {

        return a.length === 4 ? a : a.length === 2 ? a : '0' + a

      })

    }

  },

  inheritAttrs: false,

  props: {

    moment: {

      type: Number,

      default() {

        return new Date().getTime()

      }

    }

  },

  data() {

    return {

      show: false, // 控制日历面板的显示与隐藏

      selectYear: false, // 控制年份的面板的显示和隐藏

      selectMonth: false, // 控制月份的面板的显示和隐藏

      positionTop: false, // 控制面板显示方向

      current: '', // 已选择的日期时间。yyyy-MM-dd

      select: {

        // 已选择的日期对象

        year: '',

        month: '',

        date: '',

        day: ''

      },

      currentMonthFirstDay: null, // 当前月的1号属于星期几

      currentMonthEndDate: null, // 当前月的最后一天是几号

      currentMonthEndDay: null, // 当前月的最后一天属于星期几

      lastMonthEndDate: null, // 上个月的最后一天是几号

      list: [], // 日历的二维数组

      years: [], // 1900-2100

      months: [

        '1月',

        '2月',

        '3月',

        '4月',

        '5月',

        '6月',

        '7月',

        '8月',

        '9月',

        '10月',

        '11月',

        '12月'

      ]

    }

  },

  watch: {

    select: {

      handler(newVal) {

        let pre

        if (newVal.month == 1) {

          pre = new Date(newVal.year - 1, 12, 0)

        } else {

          pre = new Date(newVal.year, newVal.month - 1, 0)

        }

        this.lastMonthEndDate = pre.getDate()

        // 获取日历排表

        this.getDateList()

      },

      deep: true

    },

    // 打开年份选择器的时候使当前年份、月份出现在窗口顶部

    selectYear(newVal) {

      if (newVal) {

        this.years = []

        this.$nextTick(() => {

          let year = this.select.year

          let startYear = ((year / 10) | 0) * 10

          // 获得年份列表: 1900-2100

          for (let i = startYear; i < startYear + 10; i++) {

            this.years.push(i)

          }

        })

      }

    },

    show(newVal) {

      if (newVal) {

        this.checkOffsetBottom()

      }

    }

  },

  created() {

    this.transform(this.moment)

    this.complete()

    const date = new Date()

    let year = date.getFullYear()

    let startYear = ((year / 10) | 0) * 10

    // 获得年份列表: 1900-2100

    for (let i = startYear; i < startYear + 10; i++) {

      this.years.push(i)

    }

  },

  methods: {

    /**

    * 将时间转化为具体的 年、月、日、星期

    **/

    transform(time) {

      const date = new Date(time)

      this.select.year = date.getFullYear()

      this.select.month = date.getMonth() + 1

      this.select.date = date.getDate()

      this.select.day = date.getDay()

      this.currentMonthFirstDay = new Date(

        this.select.year,

        this.select.month - 1,

        1,

        0

      ).getDay()

      this.currentMonthEndDate = new Date(

        this.select.year,

        this.select.month,

        0

      ).getDate()

      this.currentMonthEndDay = new Date(

        this.select.year,

        this.select.month,

        0

      ).getDay()

    },

    /*

            * 计算出日历列表,二维数组

            * 第一层为星期,第二层为每星期的第几天

            */

    getDateList() {

      this.list = []

      // 获取日历第一行的数据(需加上第一个星期中所包含上个月的几天)

      let temp = this.lastMonthEndDate - (this.currentMonthFirstDay - 1)

      let list = this.numberList(temp, this.lastMonthEndDate, true).concat(

        this.numberList(1, 7 - this.currentMonthFirstDay)

      )

      this.list.push(list)

      temp = 7 - this.currentMonthFirstDay + 1

      /*

                * 剩下的行数

                */

      // 计算除了第一行剩下的天数

      const leftDays =

        this.currentMonthEndDate - (7 - this.currentMonthFirstDay)

      // 剩下的星期数

      const lineNumber = Math.ceil(leftDays / 7)

      // 包含下个月日历的天数

      const nextDays = 7 - (leftDays % 7)

      for (let i = 0; i < lineNumber; i++) {

        if (i === lineNumber - 1 && nextDays > 0 && nextDays !== 7) {

          this.list[lineNumber] = this.numberList(

            temp,

            this.currentMonthEndDate

          ).concat(this.numberList(1, nextDays, true))

        } else {

          this.list.push(this.numberList(temp, temp + 6))

        }

        temp = temp + 7

      }

      // 补齐6排

      if (lineNumber < 5) {

        this.list.push(this.numberList(nextDays + 1, nextDays + 7, true))

      }

    },

    /*

            * 获得日历数组

            * start: 开始日

            * end: 结束日

            * flag: boolean值,判断是否属于本月

            */

    numberList(start, end, flag) {

      let list = []

      for (let i = start; i <= end; i++) {

        list.push({

          text: i,

          flag: !!flag

        })

      }

      return list

    },

    /*

            * 切换月份, -1为当前月份-1,+1为当前月份+1

            */

    switchMonth(n) {

      let year = this.select.year

      if (n === -1) {

        const pre = this.select.month === 1 ? 12 : this.select.month - 1

        if (pre === 12) {

          this.transform(new Date(year - 1, pre - 1, this.select.date))

        } else {

          this.transform(new Date(year, pre - 1, this.select.date))

        }

      } else {

        const next = this.select.month === 12 ? 1 : this.select.month + 1

        if (next === 1) {

          this.transform(new Date(year + 1, next - 1, this.select.date))

        } else {

          this.transform(new Date(year, next - 1, this.select.date))

        }

      }

    },

    /*

            * 切换年份, -1为当前年份-1,+1为当前年份+1

            */

    switchYear(n) {

      if (this.selectYear) {

        let year = this.years[0]

        this.years = []

        if (n === -1) {

          for (var i = year - 10; i < year; i++) {

            this.years.push(i)

          }

        } else {

          for (var i = year + 10; i < year + 20; i++) {

            this.years.push(i)

          }

        }

      } else {

        let year = this.select.year

        if (n === -1) {

          this.transform(

            new Date(year - 1, this.select.month - 1, this.select.date)

          )

        } else {

          this.transform(

            new Date(year + 1, this.select.month - 1, this.select.date)

          )

        }

      }

    },

    pick(day) {

      if (!!day.flag) {

        // 当页日历上可能还会显示部分上个月或者下个月的部分天数,根据标识来做判断

        // 并对月份作出相应的处理

        if (parseInt(day.text) > 15) {

          this.transform(

            new Date(

              this.select.year,

              this.select.month - 2,

              parseInt(day.text)

            )

          )

        } else {

          this.transform(

            new Date(this.select.year, this.select.month, parseInt(day.text))

          )

        }

      } else {

        this.transform(

          new Date(this.select.year, this.select.month - 1, parseInt(day.text))

        )

      }

      this.complete()

      this.bindEvent()

    },

    // 绑定事件:点击关闭日历面板

    bindEvent() {

      this.show = false

      this.selectYear = false

      this.selectMonth = false

    },

    // 打开选择年份

    openSelectYear() {

      this.selectYear = !this.selectYear

      this.selectMonth = false

    },

    // 打开选择月份

    openSelectMonth() {

      this.selectMonth = !this.selectMonth

      this.selectYear = false

    },

    // 选取年

    pickYear(n) {

      // this.transform(new Date(n, this.select.month - 1, this.select.date))

      this.select.year = n

      this.selectYear = !this.selectYear

      this.selectMonth = true

      // this.complete()

    },

    // 选取月

    pickMonth(n) {

      // this.transform(new Date(this.select.year, n - 1, this.select.date))

      this.select.month = n

      this.selectMonth = !this.selectMonth

      // this.complete()

    },

    // 更改选中时间并向父组件派发事件

    complete() {

      // 触发父组件的传过来的picked事件。三个参数: 年,月,日

      this.$emit(

        'picked',

        this.select.year,

        this.select.month,

        this.select.date

      )

      this.current = {

        year: this.select.year,

        month: this.select.month,

        date: this.select.date

      }

    },

    checkOffsetBottom() {

      // console.log(this.$refs['agm-datepicker'].screenTop)

      // console.log(this.$refs['agm-datepicker'].offsetTop)

      // console.log(this.$refs['agm-datepicker'].offsetHeight)

      // console.log(this.$refs['agm-datepicker'].getBoundingClientRect())

      let scrollTop =

        (document.documentElement && document.documentElement.scrollTop) ||

        document.body.scrollTop

      let clientHeight =

        (document.documentElement && document.documentElement.clientHeight) ||

        document.body.clientHeight

      let offsetBottom =

        clientHeight -

        (this.$refs['agm-datepicker'].offsetTop -

          scrollTop +

          this.$refs['agm-datepicker'].offsetHeight)

      offsetBottom < 250

        ? (this.positionTop = true)

        : (this.positionTop = false)

    }

  }

}

</script>

<style lang="scss" rel="stylesheet/scss" scoped>

@import '~/assets/scss/element.scss';

.agm-datepicker {

  position: relative;

  width: 200px;

  height: 36px;

  color: #515a6e;

  .agm-input {

    height: 100%;

    width: 100%;

    box-sizing: border-box;

    border: 1px solid #bfcbd7;

    border-radius: 3px;

    cursor: pointer;

    input {

      height: 100%;

      width: 100%;

      box-sizing: border-box;

      padding: 5px 10px;

      border: none;

      background: none;

      font-size: 14px;

      outline: none;

    }

    &.active {

      box-shadow: 0 0 0 2px rgba(45, 140, 240, 0.2);

      border: 1px solid #20a0ff;

    }

  }

  .agm-datepicker-wrap {

    width: 240px;

    box-shadow: 0 1px 6px rgba(0, 0, 0, 0.2);

    border-radius: 4px;

    z-index: 10;

    position: absolute;

    top: 100%;

    left: 0;

    margin: 5px 0;

    background-color: #fff;

    &.positionTop {

      top: -250px;

    }

    .agm-datepicker-header {

      padding: 0px 10px;

      font-size: 14px;

      text-align: center;

      line-height: 36px;

      border-bottom: 1px solid #e8eaec;

      .agm-datepicker-header-btn {

        cursor: pointer;

        transition: color 0.2s ease-in-out;

        user-select: none;

        &:hover {

          color: #008afe;

        }

      }

      .agm-datepicker-header-btn-pre {

        float: left;

        padding-right: 5px;

        font-size: 18px;

        transform: scale(0.5, 1);

      }

      .agm-datepicker-header-btn-day {

      }

      .agm-datepicker-header-btn-next {

        padding-left: 5px;

        float: right;

        font-size: 18px;

        transform: scale(0.5, 1);

      }

    }

    .agm-datepicker-content {

      position: relative;

      table {

        width: 100%;

        border-collapse: collapse;

        thead {

          line-height: 30px;

          font-size: 12px;

          color: #999;

        }

        tbody {

          tr {

            line-height: 28px;

            td {

              font-size: 12px;

              text-align: center;

              cursor: pointer;

              border-radius: 3px;

              transition: all 0.2s ease-in-out;

              &.active,

              &.active:hover {

                color: #fff;

                background: #008afe;

              }

              &.flag {

                color: #999;

              }

            }

            td:hover {

              background: #eee;

            }

          }

        }

      }

      .agm-date-picker-year-panel {

        position: absolute;

        top: 0;

        left: 0;

        width: 100%;

        height: 100%;

        background: #fff;

        font-size: 14px;

        .panel-list {

          @include list(row);

          height: 100%;

          .panel-item {

            @include list(row);

            justify-content: center;

            align-items: center;

            width: 33.33333%;

            height: 25%;

            .panel-item-inner {

              width: 80%;

              text-align: center;

              padding: 10px 0;

              border-radius: 3px;

              transition: all 0.2s ease-in-out;

              cursor: pointer;

              &:hover {

                background-color: #e1f0fe;

              }

              &.active {

                background-color: #e1f0fe;

              }

            }

          }

        }

      }

    }

  }

}

</style>

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容