vue3+ts 时间选择器

vue3+ts 时间选择器

最近写了一些小项目,有用到时间选择器,但是系统自带有点丑,网上也没有找到合适的轮子,于是自己写了一个给大家分享一下。
文末有完整代码
在这里插入图片描述
功能

  • 出场动画
  • 选择时间
  • 数据双向绑定

使用方式:

<template>
<div class="box">
<!--  <input id="dateTimePicker" placeholder="请选择时间"  type="text" @focus="show=true" @blur="show=false">-->
  <p>{{time}}</p>
  <input type="text" v-model="time" >
  <div>
  <p style="display: flex;width: 30vw;justify-content: space-between"><button @click="show=true">展示</button><button  @click="show=false" >隐藏</button></p>
  </div>
  <DateTimePicker  v-model="time" v-if="show" :show="show"></DateTimePicker>
</div>
</template>

<script setup lang="ts">
import DateTimePicker from './components/dateTimePicker/dateTimePicker.vue'
import {ref} from "vue";
const time=ref("2020/03/15 13:56:15")
const show=ref(true)
</script>

主要html代码:

     <!--      日期-->
      <div class="date_box" v-show="type==0">
        <template v-for="(dateValue,key,index) in pickerData.date">
          <div class="date_time_item" :id="key" @touchstart="isStop(true,key)"
               @touchend="isStop(false,key)"
               @scroll="scrolle($event,key)">
            <p id="scynull1"></p>
            <p :class="[key+item,className(key,formatTime(item))]" v-for="(item,index) in dateValue.data" :key="item">
              {{ item }}</p>
            <p id="scynul2l" style="height: 70px"></p>
          </div>
          <div class="unit">
            <div class="unit_item">
              {{ dateValue.unit }}
            </div>
          </div>
        </template>
      </div>

这里给盒子添加了 @scroll 事件的监听,通过判断滚动条的距离来判断当前选中的时间, @touchstart @touchend使用来判断用户手指是否离开屏幕,避免反复触发事件
js:

//触摸事件
const isStop = (val: boolean, key: string) => {
  stop.value = val
  if (!stop.value && refScrollTarget) {
    setTimeout(() => {
      scrolle(refScrollTarget, key)
    })
  }
}
//滚动事件
const scrolle = (event: any, key: string) => {
  refScrollTarget = event
  //滑动状态中就不执行后面代码
  if (stop.value == true) {
    return
  }
  if (timer) {
    clearTimeout(timer)
  }
  setTimeout(() => {
    timer = setTimeout(() => {
      //元素高度
      let itemHeight = 50
      //滚动结余量
      let scollValue = event.target.scrollTop % itemHeight
      //如果高度对应就返回
      if (scollValue - itemHeight == 0) {
        return
      }
      if (itemHeight - scollValue > itemHeight / 2) {
        event.target.scrollTop = event.target.scrollTop - scollValue
      }
      if (itemHeight - scollValue <= itemHeight / 2) {
        event.target.scrollTop = event.target.scrollTop - scollValue + itemHeight
      }
      const val = Math.round(event.target.scrollTop / itemHeight) || 0
      setDateTime(key, val.toString())
    }, 50)
  })
}

这里对绑定的数据进行了监听,和数据的初始化操作,因为写Ts时间不长,这个类型让我很是苦恼,为了清楚编译器报错,所以给了很多的any

//时间改变
const timeChanged = () => {
  if (props.modelValue) {
    DateTime.value = props.modelValue
  }
  ((document.getElementById('year') as any).scrollTop as any) = (Number(DateTime.value.slice(0, 4)) - 1980) * 50;
  ((document.getElementById('month') as any).scrollTop as any) = (Number(DateTime.value.slice(5, 7)) - 1) * 50;
  ((document.getElementById('day') as any).scrollTop as any) = (Number(DateTime.value.slice(8, 10)) - 1) * 50;
  ((document.getElementById('hour') as any).scrollTop as any) = (Number(DateTime.value.slice(11, 13)) - 1) * 50;
  ((document.getElementById('minute') as any).scrollTop as any) = (Number(DateTime.value.slice(14, 16)) - 1) * 50;
  ((document.getElementById('second') as any).scrollTop as any) = (Number(DateTime.value.slice(17, 19)) - 1) * 50;
}
onMounted(() => {
  timeChanged()
})
watch(
    () => props.modelValue,
    (newValue, oldValue) => {
      timeChanged()
    }
)
init()

完整代码:

<<template>
    <div id="date_time_picker_box">
      <div class="date_chose_box">
        <div class="chose">
          <span :class="type==0?'active':''" @click="type=0">日期</span>
          <span :class="type==1?'active':''" @click="type=1">时间</span>
        </div>
        <div class="ok">
          <span style="float: right" @click="show=false">取消</span>
          <span style="float: right" @click="backTime">确认</span>
        </div>
      </div>
      <!--      日期-->
      <div class="date_box" v-show="type==0">
        <template v-for="(dateValue,key,index) in pickerData.date">
          <div class="date_time_item" :id="key" @touchstart="isStop(true,key)"
               @touchend="isStop(false,key)"
               @scroll="scrolle($event,key)">
            <p id="scynull1"></p>
            <p :class="[key+item,className(key,formatTime(item))]" v-for="(item,index) in dateValue.data" :key="item">
              {{ item }}</p>
            <p id="scynul2l" style="height: 70px"></p>
          </div>
          <div class="unit">
            <div class="unit_item">
              {{ dateValue.unit }}
            </div>
          </div>
        </template>
      </div>
      <!--      时间-->
      <div class="time_box date_box" v-show="type==1">
        <template v-for="(dateValue,key,index) in pickerData.time">
          <div class="date_time_item"
               :id="key"
               @touchstart="isStop(true,key)"
               @touchend="isStop(false,key)"
               @scroll="scrolle($event,key)">
            <p></p>
            <p :class="[key+item,className(key,formatTime(item))]" v-for="(item,index) in dateValue.data" :key="item">
              {{ formatTime(item) }}</p>
            <p style="height: 70px"></p>
          </div>
          <div class="unit">
            <div class="unit_item">
              {{ dateValue.unit }}
            </div>
          </div>
        </template>
      </div>
    </div>
</template>

<script lang="ts" setup>
import {ref, reactive, onMounted, defineProps, watch} from "vue"

const props = defineProps({
  show: {
    type: Boolean,
    required: true,
    default: () => false
  },
  modelValue: {
    type: String,
    required: true,
    default: () => "2000/01/01 00:00:00"
  }
})
// YYYY-mm-dd
const DateTime = ref("1980/01/01 00:00:00")
// dom数据
const pickerData = reactive({
  date: {
    year: {
      unit: "年",
      data: [] as number[]
    },
    month: {
      unit: "月",
      data: [] as number[]
    },
    day: {
      unit: "日",
      data: [] as number[]
    }
  },
  time: {
    hour: {
      unit: "时",
      data: [] as number[]
    },
    minute: {
      unit: "分",
      data: [] as number[]
    },
    second: {
      unit: "秒",
      data: [] as number[]
    }
  }
})
const emit = defineEmits(['update:modelValue'])
const type = ref(0)
const stop = ref(false)
// 存放dom元素
let refScrollTarget: { scrollTop: any } | null = null
//延时器
let timer: any = null
//判断闰年和月份天数
const backYearOrDay = (val: number, year: number) => {
  const Days = [1, 3, 5, 7, 8, 10, 12]
  if (val == 2) {
    if (year % 4 == 0) {
      return 29
    }
    return 28
  }
  if (Days.includes(val)) {
    return 31
  }
  return 30
}
//触摸事件
const isStop = (val: boolean, key: string) => {
  stop.value = val
  if (!stop.value && refScrollTarget) {
    setTimeout(() => {
      scrolle(refScrollTarget, key)
    })
  }
}
//滚动事件
const scrolle = (event: any, key: string) => {
  refScrollTarget = event
  //滑动状态中就不执行后面代码
  if (stop.value == true) {
    return
  }
  if (timer) {
    clearTimeout(timer)
  }
  setTimeout(() => {
    timer = setTimeout(() => {
      //元素高度
      let itemHeight = 50
      //滚动结余量
      let scollValue = event.target.scrollTop % itemHeight
      //如果高度对应就返回
      if (scollValue - itemHeight == 0) {
        return
      }
      if (itemHeight - scollValue > itemHeight / 2) {
        event.target.scrollTop = event.target.scrollTop - scollValue
      }
      if (itemHeight - scollValue <= itemHeight / 2) {
        event.target.scrollTop = event.target.scrollTop - scollValue + itemHeight
      }
      const val = Math.round(event.target.scrollTop / itemHeight) || 0
      setDateTime(key, val.toString())
    }, 50)
  })
}
//过滤器
const formatTime = (val: number | string) => {
  if (val < 10) {
    return "0" + val
  }
  return val + ""
}
//设置时间
const setDateTime = (key: string, val: string) => {
  let newTimeData = DateTime.value.split('')
  switch (key) {
    case 'year':
      newTimeData.splice(0, 4, ...(Number(val) + 1980).toString().split(''))
      DateTime.value = newTimeData.join('')
      break;
    case 'month':
      newTimeData.splice(5, 2, ...formatTime(Number(val) + 1).split(''))
      pickerData.date.day.data = []
      //  月份改变日期改变
      for (let i = 1; i <= backYearOrDay(Number(newTimeData[1]), Number(newTimeData[0])); i++) {
        pickerData.date.day.data.push(i);
      }
      DateTime.value = newTimeData.join('')
      break;
    case 'day':
      // newTimeData[2] = (Number(val) + 1).toString()
      newTimeData.splice(8, 2, ...formatTime(Number(val) + 1).split(''))
      DateTime.value = newTimeData.join('')
      break;
    case 'hour':
      // newTimeData[3] = (Number(val)).toString()
      newTimeData.splice(11, 2, ...formatTime(Number(val)).split(''))
      DateTime.value = newTimeData.join('')
      break;
    case 'minute':
      // newTimeData[4] = (Number(val)).toString()
      newTimeData.splice(14, 2, ...formatTime(Number(val)).split(''))
      DateTime.value = newTimeData.join('')
      break;
    case 'second':
      // newTimeData[5] = (Number(val)).toString()
      newTimeData.splice(17, 2, ...formatTime(Number(val)).split(''))
      DateTime.value = newTimeData.join('')
      break;
    default:
      break;
  }
}
//返回时间
const backTime = () => {
  emit('update:modelValue', DateTime.value)
}
//返回类名
const className = (key: string, value: number) => {
  let newTimeData = DateTime.value.split('-')
  switch (key) {
    case 'year':
      return DateTime.value.slice(0, 4) == value.toString() ? 'active_time' : ''
    case 'month':
      return DateTime.value.slice(5, 7) == value.toString() ? 'active_time' : ''
    case 'day':
      return DateTime.value.slice(8, 10) == value.toString() ? 'active_time' : ''
    case 'hour':
      return DateTime.value.slice(11, 13) == value.toString() ? 'active_time' : ''
    case 'minute':
      return DateTime.value.slice(14, 16) == value.toString() ? 'active_time' : ''
    case 'second':
      return DateTime.value.slice(17, 19) == value.toString() ? 'active_time' : ''
    default:
      return null
      break;
  }
}
//数据初始化
const init = () => {
  const year = new Date().getFullYear()
  const month = new Date().getMonth() + 1
  const day = new Date().getDate()
  const hour = new Date().getHours()
  const minute = new Date().getMinutes()
  const second = new Date().getSeconds()
  const time = DateTime.value.split("-")
  // time[0] = year.toString()
  // time[1] = month.toString()
  // time[2] = day.toString()
  // time[3] = hour.toString()
  // time[4] = minute.toString()
  // time[5] = second.toString()
  for (let i = 1980; i < 2100; i++) {
    pickerData.date.year.data.push(i);
  }
  for (let i = 1; i < 13; i++) {
    pickerData.date.month.data.push(i);
  }
  for (let i = 1; i <= backYearOrDay(month, Number(time[0])); i++) {
    pickerData.date.day.data.push(i);
  }
  for (let i = 0; i < 24; i++) {
    pickerData.time.hour.data.push(i);
  }
  for (let i = 0; i < 60; i++) {
    pickerData.time.minute.data.push(i);
    pickerData.time.second.data.push(i);
  }
  DateTime.value = time.join('-');
}
//时间改变
const timeChanged = () => {
  if (props.modelValue) {
    DateTime.value = props.modelValue
  }
  ((document.getElementById('year') as any).scrollTop as any) = (Number(DateTime.value.slice(0, 4)) - 1980) * 50;
  ((document.getElementById('month') as any).scrollTop as any) = (Number(DateTime.value.slice(5, 7)) - 1) * 50;
  ((document.getElementById('day') as any).scrollTop as any) = (Number(DateTime.value.slice(8, 10)) - 1) * 50;
  ((document.getElementById('hour') as any).scrollTop as any) = (Number(DateTime.value.slice(11, 13)) ) * 50;
  ((document.getElementById('minute') as any).scrollTop as any) = (Number(DateTime.value.slice(14, 16)) ) * 50;
  ((document.getElementById('second') as any).scrollTop as any) = (Number(DateTime.value.slice(17, 19)) ) * 50;
}
onMounted(() => {
  timeChanged()
})
watch(
    () => props.modelValue,
    (newValue, oldValue) => {
      timeChanged()
    }
)
init()
</script>

<style scoped>
@keyframes show {
  0% {
    transform: translateY(100%);
  }
  100% {
    transform: translateY(0);
  }
}

#date_time_picker_box {
  width: 100vw;
  background: white;
  position: fixed;
  transition: all linear .2s;
  bottom: 0;
  animation: show .2s linear;
}

.date_chose_box {
  display: flex;
  background: #0088ff;
  position: relative;
  z-index: 5;
  font-size: 16px;
  height: 30px;
  justify-content: space-between;
}

.date_chose_box .chose {
  display: flex;
  align-items: center;
}

.date_chose_box .ok {
  display: flex;
  align-items: center;
}

.date_chose_box span {
  margin: 5px;
  color: rgba(232, 243, 255, 0.8);
}

.date_chose_box .active {
  margin: 5px;
  color: #ffffff;
}

.date_box {
  display: flex;
  height: 169px;
  padding: 0 20px;
  overflow-y: scroll;
}

.date_box::after {
  content: '';
  width: 100%;
  height: 1px;
  position: absolute;
  top: 132px;
  left: 0;
  background: #1d212954;
}

.date_box::before {
  content: '';
  width: 100%;
  height: 1px;
  position: absolute;
  top: 80px;
  left: 0;
  background: #1d212954;
}

.date_box > .date_time_item > p {
  font-size: 16px;
  height: 50px;
  line-height: 50px;
  box-sizing: border-box;
  margin: 0;
}

.date_box > .date_time_item > .active_time {
  color: #0088ff;
}

.date_box > .date_time_item > active_item {
  color: #0088ff;
}

.date_box > .unit {
  height: 150px;
  display: flex;
  align-items: center;
  justify-content: center;
}

.date_box > .unit > .unit_item {
  height: 50px;
  line-height: 50px;
  box-sizing: border-box;
}

.date_time_item {
  flex: 1;
  overflow-y: scroll;
  height: 150px;
  user-select: none;
  position: relative;
}

#date_time_picker_box::before {
  content: '';
  display: block;
  position: absolute;
  width: 100%;
  height: 40px;
  top: 30px;
  background: red;
  background: linear-gradient(to bottom, #ffffff, rgba(255, 255, 255, .62));
  z-index: 99;
}

#date_time_picker_box::after {
  content: '';
  display: block;
  position: absolute;
  width: 100%;
  height: 50px;
  bottom: 6px;
  background: red;
  background: linear-gradient(to top, #ffffff, rgba(255, 255, 255, .62));
  z-index: 99;
}

.date_time_item::-webkit-scrollbar {
  display: none !important;
}
</style>


  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值