需求:

web端 想要跟手机端一样选择年月日时分,通过滚动选择

实现效果图:

理念:

1.年月日时分 分别为单个输入框,用来做输入修改

2.div把输入框拼接起来,显示出一个日期框的样子

3.年月日时分 下拉给默认list数据,

  • 年:给100个选项    20 - 现在年 - 80
  • 月:12个选项
  • 日:根据所选中年月来判断出多少个日期
  • 时:24
  • 分:60

4.打开日期框时,使用 appendChild(div) 动态插入年月日时分

5.获取到默认日期,给对应下拉数据 add('active') 加选中class名

6.注意一点:日期格式转换 月 日 时 分  小于10补0

7.当输入修改时,下面的年月日会滚动到修改后的日期上面,输入框 加 @input 事件

 8.当输入的值不在下拉列表中 默认第一个月 01,日01,时01,分01

9.添加滚动 事件 handleScroll

10.添加选中事件 handleClick

11.添加打开,关闭(取消/确定) 日期框事件

示例:默认值为当时间 (可使用组件传参方式给与默认值)

完整代码:

<template>
  <div class="main-container">
    <div class="datetime-input" @click="togglePicker(true)">
      <span class="el-input__prefix">
        <span class="el-input__prefix-inner">
          <i class="el-icon el-input__icon">
            <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
              <path fill="currentColor"
                    d="M512 896a384 384 0 1 0 0-768 384 384 0 0 0 0 768m0 64a448 448 0 1 1 0-896 448 448 0 0 1 0 896"></path>
              <path fill="currentColor"
                    d="M480 256a32 32 0 0 1 32 32v256a32 32 0 0 1-64 0V288a32 32 0 0 1 32-32"></path>
              <path fill="currentColor" d="M480 512h256q32 0 32 32t-32 32H480q-32 0-32-32t32-32"></path>
            </svg>
          </i>
        </span>
      </span>
      <input class="no" style="min-width: 56px;width: 56px;" type="text" maxlength="4" placeholder="YYYY"
             v-model="selectedDate.year"
             @input="updateFromInputFields('year')"/>
      <span class="separator">-</span>
      <input class="no" style="min-width: 28px;width: 28px;" type="text" maxlength="2" placeholder="MM"
             v-model="selectedDate.month"
             @input="updateFromInputFields('month')"/>
      <span class="separator">-</span>
      <input class="no" style="min-width: 28px;width: 28px;" type="text" maxlength="2" placeholder="DD"
             v-model="selectedDate.day"
             @input="updateFromInputFields('day')"/>
      <span style="width: 3px;"></span>
      <input class="no" style="min-width: 28px;width: 28px;" type="text" maxlength="2" placeholder="HH"
             v-model="selectedDate.hour"
             @input="updateFromInputFields('hour')"/>
      <span class="separator">:</span>
      <input class="no" style="min-width: 28px;width: 28px;" type="text" maxlength="2" placeholder="MM"
             v-model="selectedDate.minute"
             @input="updateFromInputFields('minute')"/>
    </div>
    <div class="picker-container" ref="pickerContainerRef">
      <div class="wheel-container">
        <div class="wheel-group">
          <div class="wheel-label">年</div>
          <div class="wheel" ref="yearsWheel" @scroll="handleScroll(yearsWheel,'year')" @click="handleClick(yearsWheel)"
               @wheel="handleWheel(yearsWheel)"></div>
        </div>
        <div class="wheel-group">
          <div class="wheel-label">月</div>
          <div class="wheel" ref="monthWheel" @scroll="handleScroll(monthWheel,'month')"
               @click="handleClick(monthWheel)" @wheel="handleWheel(monthWheel)"></div>
        </div>
        <div class="wheel-group">
          <div class="wheel-label">日</div>
          <div class="wheel" ref="dayWheel" @scroll="handleScroll(dayWheel,'')" @click="handleClick(dayWheel)"
               @wheel="handleWheel(dayWheel)"></div>
        </div>
        <div class="wheel-group" style="margin-left: 8px;">
          <div class="wheel-label">时</div>
          <div class="wheel" ref="hourWheel" @scroll="handleScroll(hourWheel,'')" @click="handleClick(hourWheel)"
               @wheel="handleWheel(hourWheel)"></div>
        </div>
        <div class="wheel-group">
          <div class="wheel-label">分</div>
          <div class="wheel" ref="minuteWheel" @scroll="handleScroll(minuteWheel,'')" @click="handleClick(minuteWheel)"
               @wheel="handleWheel(minuteWheel)"></div>
        </div>
      </div>
      <div class="buttons">
        <button class="now-btn" @click="nowSelection($event)">此刻</button>
        <button class="cancel-btn" @click="cancelSelection($event)">取消</button>
        <button class="confirm-btn" @click="confirmSelection($event)">确定</button>
      </div>
    </div>
  </div>
</template>
<script setup lang="ts" name="index">
import {ref, onMounted, onActivated} from 'vue';

const props = defineProps({
  localDateTimeNow: {
    type: String,
    default: ''
  }
});

const emit = defineEmits(['timeUpdateClick']);

const selectedDate = ref({
  day: new Date().getDate(),
  month: new Date().getMonth() + 1,
  year: new Date().getFullYear(),
  hour: new Date().getHours(),
  minute: new Date().getMinutes(),
});


const years = Array.from({length: 100}, (_, i) => new Date().getFullYear() - 20 + i);
const months = Array.from({length: 12}, (_, i) => i + 1);
const hours = Array.from({length: 24}, (_, i) => i);
const minutes = Array.from({length: 60}, (_, i) => i);

const yearsWheel = ref(null)
const monthWheel = ref(null)
const dayWheel = ref(null)
const hourWheel = ref(null)
const minuteWheel = ref(null)
const pickerContainerRef = ref(null)
const itemHeight = 21;
// 方法  可使用页面传参方式 date传过来的参数
// const timeupdate = (date) => {
//  const [datePart, timePart] = date.split(' ')
//  const [year, month, day] = datePart.split('-')
//  const [hour, minute] = timePart.split(':')
//  selectedDate.value.year = year
//  selectedDate.value.month = month
//  selectedDate.value.day = day
//  selectedDate.value.hour = hour
//  selectedDate.value.minute = minute
//};

const getDaysInMonth = (year: number, month: number) => {
  const days = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
  if ((year % 4 === 0) && (year % 100 !== 0 || year % 400 === 0)) {
    days[1] = 29;
  }
  return days[month - 1];
};

const generateDaysArray = (year: number, month: number) => {
  const daysInMonth = getDaysInMonth(year, month);
  return Array.from({length: daysInMonth}, (_, i) => i + 1);
};

const initWheel = (wheelElement, data, initialValue, addPlaceholders = false) => {
  wheelElement.innerHTML = '';
  data.forEach(item => {
    const div = document.createElement('div');
    div.textContent = item < 10 ? '0' + item : item;
    div.className = 'divItem';
    div.dataset.value = item.toString();
    wheelElement.appendChild(div);
  });
  if (addPlaceholders) {
    for (let i = 0; i < 4; i++) {
      const div = document.createElement('div');
      div.textContent = '';
      div.classList.add('placeholder');
      wheelElement.appendChild(div);
    }
  }
  const initialIndex = data.indexOf(Number(initialValue));
  nextTick(() => {
    wheelElement.scrollTop = initialIndex * itemHeight
    updateActiveItem(wheelElement);
    wheelElement.addEventListener('scroll', () => {
      updateActiveItem(wheelElement);
      updateSelectedDate();
      updateInputFields();
    });

    wheelElement.addEventListener('click', (e: MouseEvent) => {
      if (e.target instanceof HTMLElement && e.target.tagName === 'DIV' && !e.target.classList.contains('placeholder')) {
        const index = Array.from(wheelElement.children).indexOf(e.target);
        wheelElement.scrollTop = index * itemHeight;
      }
    });

    wheelElement.addEventListener('wheel', (e: WheelEvent) => {
      e.preventDefault();
      const delta = Math.sign(e.deltaY) * 1;
      const currentScroll = wheelElement.scrollTop;
      const newScroll = Math.max(0, Math.min(currentScroll + delta * itemHeight, (data.length - 1) * itemHeight));
      wheelElement.scrollTop = newScroll;
    });
  })

};

const updateActiveItem = (wheelElement: HTMLElement) => {
  const selectedIndex = Math.round(wheelElement.scrollTop / itemHeight);
  const items = wheelElement.querySelectorAll('div');
  items.forEach(item => item.classList.remove('active'));
  let activeIndex = selectedIndex;
  while (activeIndex >= 0 && items[activeIndex] && items[activeIndex].classList.contains('placeholder')) {
    activeIndex--;
  }
  if (activeIndex >= 0 && items[activeIndex]) {
    items[activeIndex].classList.add('active');
  }
};

const updateSelectedDate = () => {
  const activeYear = yearsWheel.value.querySelector('.active')
  const activeMonth = monthWheel.value.querySelector('.active')
  const activeDay = dayWheel.value.querySelector('.active')
  const activeHour = hourWheel.value.querySelector('.active')
  const activeMinute = minuteWheel.value.querySelector('.active')
  if (activeDay && activeMonth && activeYear && activeHour && activeMinute) {
    selectedDate.value = {
      year: parseInt(activeYear.dataset.value),
      month: parseInt(activeMonth.dataset.value),
      day: parseInt(activeDay.dataset.value),
      hour: parseInt(activeHour.dataset.value),
      minute: parseInt(activeMinute.dataset.value),
    };
    validateDate();
  }
};

const validateDate = () => {
  const lastDayOfMonth = getDaysInMonth(Number(selectedDate.value.year), selectedDate.value.month);
  if (selectedDate.value.day > lastDayOfMonth) {
    selectedDate.value.day = lastDayOfMonth;
    const days = generateDaysArray(Number(selectedDate.value.year), selectedDate.value.month);
    initWheel(dayWheel.value, days, selectedDate.value.day, true);
  }
};

const updateInputFields = () => {
  selectedDate.value.year = selectedDate.value.year
  selectedDate.value.month = Number(selectedDate.value.month) < 10 ? '0' + Number(selectedDate.value.month) : selectedDate.value.month
  selectedDate.value.day = Number(selectedDate.value.day) < 10 ? '0' + Number(selectedDate.value.day) : selectedDate.value.day
  selectedDate.value.hour = Number(selectedDate.value.hour) < 10 ? '0' + Number(selectedDate.value.hour) : selectedDate.value.hour
  selectedDate.value.minute = Number(selectedDate.value.minute) < 10 ? '0' + Number(selectedDate.value.minute) : selectedDate.value.minute
};

const updateFromInputFields = (type) => {
  if (type == 'year' && selectedDate.value[type].length == 4 || selectedDate.value[type].length == 2) {
    const year = selectedDate.value.year;
    const month = selectedDate.value.month;
    const day = selectedDate.value.day;
    const hour = selectedDate.value.hour;
    const minute = selectedDate.value.minute;
    const lastDayOfMonth = getDaysInMonth(year, month) || 1;
    const validDay = Math.min(day, lastDayOfMonth);
    selectedDate.value = {
      year,
      month: Math.max(1, Math.min(12, month)),
      day: Math.max(1, Math.min(lastDayOfMonth, validDay)),
      hour: Math.max(0, Math.min(23, hour)),
      minute: Math.max(0, Math.min(59, minute)),
    };
    initAllWheels();
    updateInputFields();
  }
};

const togglePicker = (show) => {
  if (pickerContainerRef.value) {
    pickerContainerRef.value.style.display = show ? 'block' : 'none';
    if (show) {
      initAllWheels();
    } else {
      emit('timeUpdateClick', {...selectedDate.value, second: props.localDateTimeNow.slice(-2)})
    }
  }
};

const initAllWheels = () => {
  initWheel(yearsWheel.value, years, selectedDate.value.year)
  initWheel(monthWheel.value, months, selectedDate.value.month, true);
  const days = generateDaysArray(selectedDate.value.year, selectedDate.value.month);
  const lastDayOfMonth = getDaysInMonth(selectedDate.value.year, selectedDate.value.month);
  if (selectedDate.value.day > lastDayOfMonth) {
    selectedDate.value.day = lastDayOfMonth;
  }
  initWheel(dayWheel.value, days, selectedDate.value.day, true);
  initWheel(hourWheel.value, hours, selectedDate.value.hour, true);
  initWheel(minuteWheel.value, minutes, selectedDate.value.minute, true);
  updateInputFields();
};

const updateDayWheelOnMonthOrYearChange = () => {
  const activeYear = yearsWheel.value.querySelector('.active')
  const activeMonth = monthWheel.value.querySelector('.active')
  if (activeYear && activeMonth) {
    const year = parseInt(activeYear.dataset.value);
    const month = parseInt(activeMonth.dataset.value);
    const days = generateDaysArray(year, month);
    if (Number(selectedDate.value.day) > days[days.length - 1]) {
      selectedDate.value.day = days[days.length - 1];
    }
    dayWheel.value.innerHTML = '';
    days.forEach(item => {
      const div = document.createElement('div');
      div.textContent = item < 10 ? '0' + item : item;
      div.className = 'divItem';
      div.dataset.value = item.toString();
      dayWheel.value.appendChild(div);
    });
    for (let i = 0; i < 4; i++) {
      const div = document.createElement('div');
      div.textContent = '';
      div.classList.add('placeholder');
      dayWheel.value.appendChild(div);
    }
    updateActiveItem(dayWheel.value);
    // initWheel(dayWheel.value, days, selectedDate.value.day, true);
    updateInputFields();
  }
};


const handleScroll = (wheelElement, type: string) => {
  updateActiveItem(wheelElement);
  updateSelectedDate();
  if (type === 'year' || type === 'month') {
    updateDayWheelOnMonthOrYearChange();
  }
};

const handleClick = (wheelElement) => {
  const index = Array.from(wheelElement.children).findIndex(item => item.className == 'active');
  wheelElement.scrollTop = index * 21;
};

const handleWheel = (wheelElement) => {
  const delta = Math.sign(event.deltaY) * 1;
  const currentScroll = wheelElement.scrollTop;
  const newScroll = Math.max(0, Math.min(currentScroll + delta * itemHeight, (wheelElement.children.length - 1) * itemHeight));
  wheelElement.scrollTop = newScroll;
};

const confirmSelection = (e) => {
  preventHandler(e)
  togglePicker(false);
};

const nowSelection = (e) => {
  preventHandler(e)
  const [datePart, timePart] = props.localDateTimeNow.split(' ')
  const [year, month, day] = datePart.split('-')
  const [hour, minute] = timePart.split(':')
  selectedDate.value = {
    year: year,
    month: month,
    day: day,
    hour: hour,
    minute: minute,
  };
  initAllWheels();
};

const cancelSelection = (e) => {
  preventHandler(e)
  const [datePart, timePart] = props.localDateTimeNow.split(' ')
  const [year, month, day] = datePart.split('-')
  const [hour, minute] = timePart.split(':')
  selectedDate.value = {
    year: year,
    month: month,
    day: day,
    hour: hour,
    minute: minute,
  };
  initAllWheels();
  togglePicker(false);
};

const preventHandler = (e) => {
  e.preventDefault()
  e.stopPropagation()
}

// 点击页面其他地方关闭选择器
document.addEventListener('click', (e) => {
  if (e.target && e.target.className != 'no' && e.target.className != 'datetime-input' && e.target.className != 'separator' && e.target.className != 'cancel-btn' && e.target.className != 'picker-container' && e.target.className != 'wheel-container' && e.target.className != 'wheel-group' && e.target.className != 'wheel' && e.target.className != 'buttons' && e.target.className != 'confirm-btn' && e.target.className != 'cancel-btn' && e.target.className != 'now-btn' && e.target.className != 'wheel-label' && e.target.className != 'active' && e.target.className != 'divItem') {
    togglePicker(false);
  }
})
// 暴露变量
defineExpose({
  timeupdate
});
onMounted(() => {
  initAllWheels();
});
</script>
<style scoped>
.main-container {
  width: 300px;
  position: relative;
  border: 1px solid #dcdfe6;
  margin-top: 1px;
  margin-bottom: 1px;
}

.datetime-input {
  height: 30px;
  line-height: 30px;
  display: flex;
  align-items: center;
  margin: 0 10px;
}

.datetime-input input {
  /*width: 100%;*/
  /*text-align: center;*/
}

.datetime-input .separator {
  margin: 0 0 2px 0;
  color: #999;
}

.picker-container {
  display: none;
  position: absolute;
  top: calc(100% + 2px);
  left: 0;
  width: 100%;
  background: white;
  border-radius: 0 0 5px 5px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  padding: 10px 0 5px;
  z-index: 1000;
  border: 1px solid #ddd;
  border-top: none;

}

.wheel-container {
  display: flex;
  overflow-x: auto;
  padding: 5px 0;
  scrollbar-width: none;
  justify-content: center;

}

.wheel-container::-webkit-scrollbar {
  display: none;
}

.wheel-group {
  display: flex;
  flex-direction: column;
  align-items: center;
  margin: 0 2px;
  min-width: 40px;
}

.wheel-label {
  font-size: 11px;
  color: #666;
  margin-bottom: 3px;
  height: 15px;
  line-height: 15px;
}

.wheel {
  height: 110px;
  width: 40px;
  overflow: hidden;
  text-align: center;
  border: 1px solid #eee;
  border-radius: 3px;
  background: #fff;
  scroll-behavior: smooth;
}

.wheel div {
  height: 25px;
  line-height: 25px;
  cursor: pointer;
  transition: all 0.2s;
  font-size: 13px;
}

.wheel div:hover {
  background: #f5f5f5;
}

:deep(.wheel div.active) {
  background: #4285f4;
  color: white;
  font-weight: bold;
}

:deep(.placeholder) {
  height: 21px;
  line-height: 21px;
}

:deep(.divItem) {
  font-size: 16px;
  height: 21px;
  line-height: 21px;
}

:deep(.wheel div.placeholder) {
  color: transparent;
  pointer-events: none;
}

.buttons {
  display: flex;
  justify-content: flex-end;
}

button {
  height: 30px;
  line-height: 30px;
  width: 50px;
  border: none;
  border-radius: 3px;
  cursor: pointer;
  font-size: 12px;
  margin-left: 6px;
}

.cancel-btn {
  background: #f0f0f0;
  color: #333;
}

.confirm-btn {
  background: #4285f4;
  color: white;
}
</style>

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐