van-picker实现日期时间选择器
前提
vue3项目 "vant": "^4.9.18"
vant4 官网 Vant 4 - A lightweight, customizable Vue UI library for mobile web apps.
时间选择组件 timePicker.vue
代码中只展示到分钟(我的业务需求),需要秒的话放开代码中second项目代码即可,需要各种属性自定义等,可自己加props传入设置,我没弄
<template><van-picker v-model="pickerVal" :columns="columns" @change="onChange" option-height="44px":show-toolbar="false"/>
</template><script setup lang="ts">
import { getTimeRange } from '@/utils/index';let pickerVal = ref(parseTimeToArray(getTimeRange(0).start));//getTimeRange(0).start获取的就是一个我的默认显示的时间 ,你可以跟着你的业务来 ${year}-${month}-${date} 00:00
const props = defineProps({modelValue: {type: String,default: '',},
});const emit = defineEmits(['update:modelValue']);// 时间列数据源
const columns = ref<any[]>([]);init();watch(() => props.modelValue, async (newVal, oldVal) => {if (newVal) {pickerVal.value = parseTimeToArray(newVal);}
},{immediate:true});
// 初始化日期
function init() {const dateStr = props.modelValue || getTimeRange(0).start;// const date = new Date(dateStr);const date = new Date(dateStr.replace(/-/g, '/'));const Y = date.getFullYear();const M = date.getMonth(); // 0-basedconst D = date.getDate();const h = date.getHours();const m = date.getMinutes();// const s = date.getSeconds();// 初始化列数据columns.value = [getYearColumns(Y),getMonthColumns(M),getDayColumns(Y, M + 1, D),getHourColumns(h),getMinuteColumns(m),// getSecondColumns(s),];
}// 获取年份列
function getYearColumns(currentYear: number) {const years = [];for (let i = currentYear - 10; i <= currentYear ; i++) {years.push({ text: `${i}年`, value: i });}return years;
}// 获取月份列
function getMonthColumns(month: number) {const months = Array.from({ length: 12 }, (_, i) => ({text: i + 1 < 10 ? `0${i + 1}月` : `${i + 1}月`,value: i + 1}));return months;
}// 获取某年某月的天数
function getDaysInMonth(year: number, month: number): number {return new Date(year, month, 0).getDate();
}// 获取天数列
function getDayColumns(year: number, month: number, day: number) {const days = Array.from({ length: getDaysInMonth(year, month) }, (_, i) => ({text: i + 1 < 10 ? `0${i + 1}日` : `${i + 1}日`,value: i+1}));return days;
}// 获取小时列
function getHourColumns(hour: number) {const hours = Array.from({ length: 24 }, (_, i) => ({text: i < 10 ? `0${i}时` : `${i}时`,value: i}));return hours;
}// 获取分钟列
function getMinuteColumns(minute: number) {const minutes = Array.from({ length: 60 }, (_, i) => ({text: i < 10 ? `0${i}分` : `${i}分`,value: i,}));return minutes;
}// 获取秒列
function getSecondColumns(second: number) {const seconds = Array.from({ length: 60 }, (_, i) => ({text: i < 10 ? `0${i}秒` : `${i}秒`,value: i}));return seconds;
}// 列改变监听// (picker: any, value: any, index: number)
function onChange({ selectedValues, selectedOptions, selectedIndexes, columnIndex }) {if (columnIndex === 0 || columnIndex === 1) {// 年或月改变 → 更新天数const selectedYear = selectedValues[0];const selectedMonth = selectedValues[1];const selectedDay = selectedValues[2];const totalDays = getDaysInMonth(selectedYear, selectedMonth);const newDays = Array.from({ length: totalDays }, (_, i) => ({text: i + 1 < 10 ? `0${i + 1}日` : `${i + 1}日`,value: i + 1,}));columns.value[2] = newDays; // 更新第3列(天)if (selectedDay > totalDays) {let newVal = [...pickerVal.value];newVal[2] = totalDays - 1;pickerVal.value = newVal;}}// 提交最终值submitTime();
}// 提交时间
function submitTime() {emit('update:modelValue', arrayToTimeString(pickerVal.value));
}function parseTimeToArray(timeStr: string): number[] {const date = new Date(timeStr);const year = date.getFullYear(); // 2025const month = date.getMonth() + 1; // 5 (getMonth() 返回 0-based)const day = date.getDate(); // 5const hours = date.getHours(); // 23const minutes = date.getMinutes(); // 59const seconds = date.getSeconds(); // 59return [year, month, day, hours, minutes];
}
function arrayToTimeString(arr: number[]): string {const [year, month, day, hours, minutes] = arr;// 补零处理const pad = (num: number) => num.toString().padStart(2, '0');return `${year}-${pad(month)}-${pad(day)} ${pad(hours)}:${pad(minutes)}`;//:${pad(seconds)}
}
</script>
如果需要跟van-popup配合使用
这里更多是根据个人业务写的组件,仅供参考
<template><van-popup v-model:show="show" position="bottom":style="{ height: '60%' }" round><div class="flexAlCt time-top"><span class="cancel-btn" @click="closePop">取消</span><span class="time-title">日期筛选</span><span class="sure-btn" @click="confirmPop">确定</span></div><div class="flexAlCt quick-col"><span v-for="v in list" :key="v.num" class="quick-row":class="v.num==timeNum?'active':''" @click="quickChooseTime(v)">{{ v.name }}</span></div><p class="sub-title">自定义日期</p><div class="flexAlCt choose-time"><label>开始日期</label><span :class="defineTime.start?'choosed-time':'no-time'"@click="showTimePicker('start')">{{ defineTime.start||'请选择' }}</span></div><timePicker v-model:model-value="defineTime.start" v-if="timeShow.start"/><div class="flexAlCt choose-time"><label>结束日期</label><span :class="defineTime.end?'choosed-time':'no-time'"@click="showTimePicker('end')">{{ defineTime.end||'请选择' }}</span></div><timePicker v-model:model-value="defineTime.end" v-if="timeShow.end"/></van-popup>
</template>
<script setup lang="ts">
import timePicker from "./timePicker.vue";
import { getTimeRange } from '@/utils/index';
import { showToast } from 'vant';
const emit = defineEmits(['close', 'confirm']);
const props = defineProps({popupShow: {type: Boolean,default: true,},time: {type: Object,default: () => {return {start: '',end: ''}},},
});let show = ref(false);//弹出框显示
let list = [//快捷时间列表{ name: '近一周', num: 7, time: getTimeRange(7) },{ name: '近一月', num: 30, time: getTimeRange(30) },{ name: '近三月', num: 90, time: getTimeRange(90) }
];
let timeNum = ref(7);//选中的快捷时间let timeShow = reactive({//自定义时间选择器显示start: false,end: false,
});
let defineTime = reactive({//自定义时间start: '',end: '',
})//监听popop显示
watch(()=>props.popupShow,() => {show.value = props.popupShow;if (props.time.start) {compareQuickTime(props.time);defineTime.start = props.time.start;defineTime.end = props.time.end;}
}, { immediate: true })//监听自定义时间变化
watch(() => defineTime, (nv) => {compareQuickTime(nv);
}, { deep: true });//比对快捷时间
function compareQuickTime(compare) {let obj = list.find(val => {return val.time.start === compare.start &&val.time.end === compare.end});if (obj) {timeNum.value = obj.num;} else {timeNum.value = -1;}
}
//快捷选择时间
function quickChooseTime(v) {if (timeNum.value == v.num) {timeNum.value = -1;} else {timeNum.value = v.num;defineTime.start = v.time.start;defineTime.end = v.time.end;}
}
//切换时间选择器显示
function showTimePicker(type) {if (!timeShow[type]) {let stype = type == 'start' ? 'end' : 'start';timeShow[stype] = false;}timeShow[type] = !timeShow[type];
}
//关闭弹出框
function closePop() {show.value = false;emit('close');
}
//确定
function confirmPop() {const startTime = new Date(defineTime.start.replace(/-/g, '/')).getTime();const endTime = new Date(defineTime.end.replace(/-/g, '/')).getTime();if (startTime > endTime) {showToast("开始时间不能大于结束时间");return;}emit('confirm', toRaw(defineTime));closePop();
}
</script>
<style lang="less" scoped>
.time-top{justify-content: space-between;padding:9px 16px;font-size:17px;.cancel-btn,.sure-btn{font-size:15px;line-height: 1;padding:8px 15px;}.sure-btn{background: var(--primary-yellow);border-radius: 8px;}.time-title{font-size:17px;line-height: 1;font-weight: 500;}
}
.quick-col{padding:12px;justify-content: space-between;.quick-row{width: 30%;height: 36px;border:1px solid #F5F5F5;background: #F5F5F5;border-radius: 4px;text-align: center;line-height: 34px;&.active{border-color: var(--primary-yellow);background: #FFFAED;position:relative;&::before{content:"✔";position:absolute;width:24px;height: 16px;line-height:16px;border-bottom-left-radius: 10px;background: var(--primary-yellow);top:0;right:0;}}}
}
.sub-title{color: #999;font-size:16px;padding-left:16px;line-height: 44px;
}
.choose-time{justify-content: space-between;line-height: 44px;font-size:16px;padding-left:16px;.choosed-time{color:var(--primary-yellow);padding:0 16px;}.no-time{padding:0 16px;color:#B3B3B3;}
}
</style>