周范围选择器

周范围选择器

原因:

在翻找了不少大佬造的轮子后始终没有找到一个符合我需求的,于是决定自己造一个

方案:

使用date-fns来实现获取周总数,第几周等数据,其他逻辑自行处理(其实可以依托element-ui部分组件,但是当时脑子没转过来)
话不多上代码

<template>
  <div class="week_picker flex">
    <div class="week_box">
      <div class="flex flex_column_center">
        <div ref="startWeekSelect" :class="['week_block',{'week_open': weekIsOpen.start}]">
          <div class="week_top flex flex-content-center" @click="weekIsOpen.start = !weekIsOpen.start">
            <div class="week_txt">{{ `${yearData.start}年 第${weekData.startWeek}周` }}</div>
            <div class="icon el-icon-arrow-down"></div>
          </div>
          <div class="week_select_box">
            <div class="week_select">
              <div class="year_box flex flex_column_center flex_row_justify">
                <i class="year_btn el-icon-d-arrow-left" @click="yearIconClick('start','down')"></i>
                <div class="year_txt">{{ yearData.start }}</div>
                <i class="year_btn el-icon-d-arrow-right" @click="yearIconClick('start','up')"></i>
              </div>
              <div class="week_option_box">
                <div :class="['week_option',{'active':item.value === weekData.startWeek}]"
                     v-for="item in startWeekList"
                     :value="item.value"
                     @click="weekOptionClick(item.value)"
                >{{ item.label }}
                </div>

              </div>
              <div class="arrow_icon"></div>
            </div>
          </div>
        </div>
        <p style="margin: 0 15px">至</p>
        <div ref="endWeekSelect" :class="['week_block',{'week_open': weekIsOpen.end}]">
          <div class="week_top flex flex-content-center" @click="weekIsOpen.end = !weekIsOpen.end">
            <div class="week_txt">{{ `${yearData.end}年 第${weekData.endWeek}周` }}</div>
            <div class="icon el-icon-arrow-down"></div>
          </div>
          <div class="week_select_box">
            <div class="week_select">
              <div class="year_box flex flex_column_center flex_row_justify">
                <i :class="['year_btn el-icon-d-arrow-left',{'disabled':this.yearData.end <= this.yearData.start}]"
                   @click="yearIconClick('end','down')"
                ></i>
                <p class="year_txt">{{ yearData.end }}</p>
                <i class="year_btn el-icon-d-arrow-right" @click="yearIconClick('end','up')"></i>
              </div>
              <div class="week_option_box">
                <div
                  :class="['week_option',{'active':item.value === weekData.endWeek},{'disabled':endWeekDisabled(item.value)}]"
                  v-for="item in endWeekList"
                  :value="item.value"
                  @click="weekOptionClick(item.value,'end')"
                >{{ item.label }}
                </div>
              </div>
              <div class="arrow_icon"></div>
            </div>
          </div>
        </div>
      </div>
      <div class="txt">{{ dateScope }}</div>
    </div>
  </div>
</template>

<script>

import { getWeekStartAndEnd } from '@/utils/ruoyi'
import { getISOWeek, getISOWeeksInYear, getYear } from 'date-fns'

export default {
  name: 'WeekPicker',
  data() {
    return {
      yearData: {// 年份
        start: '',
        end: ''
      },
      weekTal: { // 一年有多少周
        start: 0,
        end: 0
      },
      weekData: { // 周数
        startWeek: '',
        endWeek: ''
      },
      dateData: {
        start: '',
        end: ''
      },
      weekIsOpen: {
        start: false,
        end: false
      }
    }
  },
  computed: {
    // 开始周列表
    startWeekList() {
      console.log('初始化')
      let weekList = []
      const date = new Date()
      let yearType = this.yearData.start === date.getFullYear()
      for (let i = 1; i <= this.weekTal.start; i++) {
        weekList.push({ value: i, label: i === getISOWeek(new Date()) && yearType ? `第${i}周(本周)` : `第${i}周` })
      }
      return weekList
    },
    // 结束周列表
    endWeekList() {
      let weekList = []
      const date = new Date()
      let yearType = this.yearData.end === date.getFullYear()
      for (let i = 1; i <= this.weekTal.end; i++) {
        weekList.push({ value: i, label: i === getISOWeek(new Date()) && yearType ? `第${i}周(本周)` : `第${i}周` })
      }
      return weekList
    },
    // 日期范围
    dateScope() {
      let monday = ''
      let sunday = ''
      if (this.weekData.startWeek) {
        monday = getWeekStartAndEnd(this.yearData.start, this.weekData.startWeek, false).monday
        this.dateData.start = getWeekStartAndEnd(this.yearData.start, this.weekData.startWeek, true).monday
      }
      if (this.weekData.endWeek) {
        sunday = getWeekStartAndEnd(this.yearData.end, this.weekData.endWeek, false).sunday
        this.dateData.end = getWeekStartAndEnd(this.yearData.end, this.weekData.endWeek, true).sunday
      }

      if (this.weekData.startWeek && this.weekData.endWeek) {
        return `(${monday} 至 ${sunday})`
      }
      return ''
    }
  },
  created() {
    this.initDate()
    // 绑定全局点击事件
    document.addEventListener('click', this.weekInsideClick)
  },
  methods: {
    // 初始化时间
    initDate() {
      this.yearData.end = this.yearData.start = getYear(new Date(new Date().getFullYear(), 4, 1))
      this.weekTal.end = this.weekTal.start = getISOWeeksInYear(new Date(new Date().getFullYear(), 4, 1))
      this.weekData.startWeek = getISOWeek(new Date())
      this.weekData.endWeek = getISOWeek(new Date())
    },
    // 结束周禁用选项
    endWeekDisabled(data) {
      if (this.yearData.start === this.yearData.end) {
        return this.weekData.startWeek > data
      } else {
        return false
      }
    },

    // 周改变
    weekOptionClick(data, type = 'start') {
      let isClose = false
      if (type === 'start') {
        isClose = true
        this.weekData.startWeek = data
        if (this.yearData.start === this.yearData.end && this.weekData.startWeek > this.weekData.endWeek) {
          this.weekData.endWeek = data
        }
      } else {
        if (this.yearData.start === this.yearData.end) {
          if (this.weekData.startWeek <= data) {
            this.weekData.endWeek = data
            isClose = true
          }
        } else if (this.yearData.start < this.yearData.end) {
          this.weekData.endWeek = data
          isClose = true
        }
      }
      if (isClose) {
        this.weekIsOpen.start = false
        this.weekIsOpen.end = false
        this.$emit('change', this.dateData)
      }
    },

    // 点击外部触发
    weekInsideClick(event) {
      const isClickedInsideStart = this.$refs.startWeekSelect.contains(event.target)
      const isClickedInsideEnd = this.$refs.endWeekSelect.contains(event.target)
      if (!isClickedInsideStart) {
        this.weekIsOpen.start = false
      }
      if (!isClickedInsideEnd) {
        this.weekIsOpen.end = false
      }
    },

    /** 年份改变
     *
     * @param type // start | end 点击的是开始周还是结束周
     * @param direction // up | down 点击的是增加还是减少
     * @returns {boolean}
     */
    yearIconClick(type, direction) {

      // 结束年份小于等于开始年份并且点击减少结束年份,禁止点击
      if (type === 'end' && direction === 'down' && this.yearData.end <= this.yearData.start) {
        return false
      }

      if (direction === 'up') {
        this.yearData[type]++
      } else {
        if (this.yearData[type] >= 1950) {
          this.yearData[type]--
        }
      }
      if (type === 'start') {
        this.weekData.startWeek = 1
        if (this.yearData.start > this.yearData.end) {
          this.weekData.endWeek = 1
          this.yearData.end = this.yearData.start
        }
      }

    }
  },
  beforeDestroy() {
    // 移除全局点击事件
    document.removeEventListener('click', this.weekInsideClick)
  }
}
</script>

<style lang="scss">
.week_box {
  .txt {
    margin-top: 10px;
  }
}

.week_block {
  line-height: 34px;
  border-radius: 4px;
  border: 1px solid #DCDFE6;
  position: relative;
  white-space: nowrap;
  width: 150px;
  font-size: 14px;
  color: #606266;
  padding: 0 10px;

  &.week_open {
    .week_select_box {
      opacity: 1;
      pointer-events: auto;
      max-height: 400px;
    }
  }

  .week_txt {
    padding-right: 10px;
    flex: 1;
    cursor: pointer;
  }

  .icon {
    line-height: 34px;
    font-size: 14px;
    color: #C0C4CC;
    cursor: pointer;
  }

  .week_select_box {
    z-index: 50;
    position: absolute;
    top: 100%;
    left: -1px;
    right: -6px;
    white-space: nowrap;
    line-height: 34px;
    font-size: 14px;
    padding: 10px 5px 10px 0;
    overflow: hidden;
    max-height: 0;
    opacity: 0;
    pointer-events: none;
    transition: all .3s;

    .week_select {
      position: relative;
      border: solid 1px #dfe4ed;
      border-radius: 4px;
      background-color: #fff;
      box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
    }

    .year_box {
      padding: 0 5px;
      font-size: 18px;

      .year_btn {
        cursor: pointer;
        padding: 0 5px;
        line-height: 46px;

        &.disabled {
          cursor: no-drop;
          color: #c7cace !important;
        }

        &:hover {
          color: #1890ff;
        }
      }

      .year_txt {
        margin: 0 10px;
      }
    }

    .week_option_box {
      padding: 5px 0;
      height: 268px;
      overflow-y: auto;
      border-top: 1px solid #dfe4ed;
    }

    .week_option {
      padding: 0 15px;
      cursor: pointer;
      color: #606266;

      &.active {
        color: #1890ff;
        font-weight: 600;
      }

      &.disabled {
        cursor: no-drop;
        color: #c7cace !important;
        background-color: #fff !important;
      }

      &:hover {
        background-color: #f5f7fa;
      }
    }

    .arrow_icon {
      position: absolute;
      top: -6px;
      border-width: 6px;
      border-top-width: 0;
      display: block;
      width: 0;
      height: 0;
      left: 35px;
      border-color: transparent;
      border-style: solid;
      border-bottom-color: #e6ebf5;

      &:after {
        content: ' ';
        position: absolute;
        top: 1px;
        border-width: 5px;
        border-top-width: 0;
        display: block;
        width: 0;
        height: 0;
        left: 0;
        margin-left: -5px;
        border-color: transparent;
        border-style: solid;
        border-bottom-color: #FFFFFF;
      }
    }
  }

}
</style>

效果如图
image

posted @ 2026-04-15 09:18  异·类  阅读(10)  评论(0)    收藏  举报