vue3+原生javascript 手写日期时间选择框 滚动选择可输入
·
需求:
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>
更多推荐
所有评论(0)