前提
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-based
const 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(); // 2025
const month = date.getMonth() + 1; // 5 (getMonth() 返回 0-based)
const day = date.getDate(); // 5
const hours = date.getHours(); // 23
const minutes = date.getMinutes(); // 59
const seconds = date.getSeconds(); // 59
return [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>