需求:
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">此刻</button><button class="cancel-btn" @click="cancelSelection">取消</button><button class="confirm-btn" @click="confirmSelection">確定</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 dayFather = ref(0)
const dayInputNum = ref(0)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 itemHeight = 21;const initialIndex = data.indexOf(Number(initialValue));nextTick(() => {wheelElement.scrollTop = initialIndex * itemHeightupdateActiveItem(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) * 3;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 itemHeight = 21;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 = () => {if (dayInputNum.value == 1) {selectedDate.value.day = dayFather.value}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.yearselectedDate.value.month = Number(selectedDate.value.month) < 10 ? '0' + Number(selectedDate.value.month) : selectedDate.value.monthselectedDate.value.day = Number(selectedDate.value.day) < 10 ? '0' + Number(selectedDate.value.day) : selectedDate.value.dayselectedDate.value.hour = Number(selectedDate.value.hour) < 10 ? '0' + Number(selectedDate.value.hour) : selectedDate.value.hourselectedDate.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)}if (show) {dayInputNum.value++}}
};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];}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) => {dayInputNum.value++const index = Array.from(wheelElement.children).findIndex(item => item.className == 'active');wheelElement.scrollTop = index * 21;
};const handleWheel = (wheelElement) => {const delta = Math.sign(event.deltaY) * 3;const currentScroll = wheelElement.scrollTop;const itemHeight = 21;const newScroll = Math.max(0, Math.min(currentScroll + delta * itemHeight, (wheelElement.children.length - 1) * itemHeight));wheelElement.scrollTop = newScroll;
};const confirmSelection = () => {togglePicker(false);
};const nowSelection = () => {const now = new Date();selectedDate.value = {day: now.getDate(),month: now.getMonth() + 1,year: now.getFullYear(),hour: now.getHours(),minute: now.getMinutes(),};initAllWheels();
};const cancelSelection = () => {const now = new Date();selectedDate.value = {day: now.getDate(),month: now.getMonth() + 1,year: now.getFullYear(),hour: now.getHours(),minute: now.getMinutes(),};initAllWheels();togglePicker(false);
};
// 點擊頁面其他地方關閉選擇器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>