uniapp 日期时间选择器

1,067 阅读6分钟

封装理由

image.png

如上图,项目需要这样选择时间

但是uniapp没有满足需求的组件

而使用的UI组件库功能满足,但是元素的样式不满足。导致出现:样式不好看、某些机型无法选中最后一个元素,如下图:可以明显看出由于组件的样式兼容问题,导致出现的样式和无法选中问题

image.png

参数

属性名描述示例
v-model:timeShow用于控制选择器的弹出与收起Boolean:true
v-model:currentTime双向绑定的时间值字符串或数字:'2024-01-01 12:08'、1704082080000(如果是数字的话注意时间单位得是毫秒数)
type展示格式字符串:'1',详细值请在下方查看
itemHeight各列中单个选项的高度数字:40(注意单位是px)
visibleItemCount每列中可见选项的数量数字:5
yearRange年份的选择范围数字:5(5的意思就是:年份的选择范围是当前时间的年份的前五年到后五年)
customYearRange自定义年份的选择范围数组:['1970','2024']
closeOnClickOverlay是否允许点击遮罩关闭选择器Boolean:true
showToolbar是否显示顶部的操作栏Boolean:true
cancelText取消按钮的文字字符串:'取消'
cancelColor取消按钮的颜色字符串:'#909193'
confirmText确认按钮的文字字符串:'确认'
confirmColor确认按钮的颜色字符串:'#3c9cff'
title顶部标题字符串:'请选择日期时间'
showDescription是否显示说明栏Boolean:true
immediateChange是否在手指松开时立即触发 change 事件。若不开启则会在滚动动画结束后触发 change 事件Boolean:true
resultType返回的结果类型字符串:'timestamp'
zIndex组件层级数字:99

参数值详细解释

type :

  • 1:年月日时分
  • 2:年月日
  • 3:年月
  • 4:月日
  • 5:月日时分
  • 6:日时分
  • 7:年
  • 8:月
  • 9:日
  • 10:时分
  • 11:时
  • 12:分

( 别问为什么要分这么细,问就是总有特殊的非常规需求 )

特别注意一下:当你使用iOS设备并且当type为7时,也就是只选择年的时候,如果你传入了字符串形式的currentTime

类似于:'2025/01'这样的时间时会出现获取不到年列表数据的情况

这可能是由于iOS中new Date('2025/01').getFullYear()无法识别'2025/01'具体是什么时间导致返回的是NaN

所以你需要传入完整一点的时间,例如:'2025/01/01'

showToolbar :

如果不显示顶部的操作栏,想要关闭组件就需要开启点击遮罩关闭选择器的功能,并且只要在选择器中改变了日期时间就会立即同步更改双向绑定的时间值(正常来说,应该是点击操作栏的确定按钮后才会更改双向绑定的时间值)

customYearRange : 数组中只有两个参数:第一个是起始年份,第二个是结束年份,例如传入值是['1970','2024'],那么可选择的年份就是1970-2024年

注意:

  • customYearRange参数和yearRange参数只会生效一个,customYearRange启用后yearRange就失效

resultType :

  • timestamp(返回的结果是:时间戳,毫秒级)
  • formattedDate(返回的结果是:格式化之后的时间字符串)

封装代码

<template>
	<div
		class="date-time-box"
		:style="{ zIndex: props.zIndex }"
		v-if="mainShow"
	>
		<div
			class="mask-box"
			@click="clickMask"
		></div>
		<div
			class="date-time-picker"
			:class="{ 'animation-show': props.timeShow, 'animation-hide': !props.timeShow }"
		>
			<div
				class="control-box"
				v-if="props.showToolbar"
			>
				<div
					class="cancel"
					:style="{ color: props.cancelColor }"
					@click.stop="cancel"
					>{{ props.cancelText }}</div
				>
				<div class="title">
					<slot name="title">{{ props.title }}</slot>
				</div>
				<div
					class="confirm"
					:style="{ color: props.confirmColor }"
					@click.stop="confirm"
					>{{ props.confirmText }}</div
				>
			</div>
			<div
				class="description-box"
				v-if="props.showDescription"
			>
				<template v-if="showYear"><div class="description-item">年</div></template>
				<template v-if="showMonth"><div class="description-item">月</div></template>
				<template v-if="showDay"><div class="description-item">日</div></template>
				<template v-if="showHour"><div class="description-item">时</div></template>
				<template v-if="showMinute"><div class="description-item">分</div></template>
			</div>
			<picker-view
				:indicator-style="indicatorStyle"
				:value="selectDate"
				:immediate-change="props.immediateChange"
				@change="bindChange"
				class="picker-view"
				:style="pickerStyle"
			>
				<picker-view-column v-if="showYear">
					<view
						class="item"
						v-for="(item, index) in years"
						:key="index"
						>{{ item }}</view
					>
				</picker-view-column>
				<picker-view-column v-if="showMonth">
					<view
						class="item"
						v-for="(item, index) in formattedMonths"
						:key="index"
						>{{ item }}</view
					>
				</picker-view-column>
				<picker-view-column v-if="showDay">
					<view
						class="item"
						v-for="(item, index) in formattedDays"
						:key="index"
						>{{ item }}</view
					>
				</picker-view-column>
				<picker-view-column v-if="showHour">
					<view
						class="item"
						v-for="(item, index) in formattedHours"
						:key="index"
						>{{ item }}</view
					>
				</picker-view-column>
				<picker-view-column v-if="showMinute">
					<view
						class="item"
						v-for="(item, index) in formattedMinutes"
						:key="index"
						>{{ item }}</view
					>
				</picker-view-column>
			</picker-view>
		</div>
	</div>
</template>

<script setup>
	import { ref, onMounted, watch, nextTick, computed } from 'vue';

	const props = defineProps({
		timeShow: {
			type: Boolean,
			required: true,
		},
		currentTime: {
			type: [Number, String],
		},
		type: {
			type: String,
			default: '1',
		},
		itemHeight: {
			type: Number,
			default: 40,
		},
		visibleItemCount: {
			type: Number,
			default: 5,
		},
		yearRange: {
			type: Number,
			default: 5,
		},
		customYearRange: {
			type: Array,
			default: [],
		},
		closeOnClickOverlay: {
			type: Boolean,
			default: true,
		},
		showToolbar: {
			type: Boolean,
			default: true,
		},
		cancelText: {
			type: String,
			default: '取消',
		},
		cancelColor: {
			type: String,
			default: '#909193',
		},
		confirmText: {
			type: String,
			default: '确认',
		},
		confirmColor: {
			type: String,
			default: '#3c9cff',
		},
		title: {
			type: String,
			default: '',
		},
		showDescription: {
			type: Boolean,
			default: true,
		},
		immediateChange: {
			type: Boolean,
			default: true,
		},
		resultType: {
			type: String,
			default: 'formattedDate',
		},
		zIndex: {
			type: Number,
			default: 9999,
		},
	});
	const emit = defineEmits(['update:timeShow', 'update:currentTime', 'confirm']);

	onMounted(() => {
		indicatorStyle.value = `height: ${props.itemHeight}px;`;
		pickerStyle.value = `height: ${props.visibleItemCount * props.itemHeight}px;`;
	});

	const date = new Date();
	const years = ref([]);
	const months = ref([]);
	const days = ref([]);
	const hours = ref([]);
	const minutes = ref([]);
	const year = ref('');
	const month = ref('');
	const day = ref('');
	const hour = ref('');
	const minute = ref('');
	const selectDate = ref([]);
	const indicatorStyle = ref('');
	const pickerStyle = ref('');

	const showYear = computed(() => ['1', '2', '3', '7'].includes(props.type));
	const showMonth = computed(() => ['1', '2', '3', '4', '5', '8'].includes(props.type));
	const showDay = computed(() => ['1', '2', '4', '5', '6', '9'].includes(props.type));
	const showHour = computed(() => ['1', '5', '6', '10', '11'].includes(props.type));
	const showMinute = computed(() => ['1', '5', '6', '10', '12'].includes(props.type));

	const mainShow = ref(false);
	watch(
		() => props.timeShow,
		(val) => {
			if (val) {
				init();
				mainShow.value = val;
			} else {
				setTimeout(() => {
					mainShow.value = val;
				}, 500);
			}
		},
		{ immediate: true }
	);

	async function init() {
		let currentTime = props.currentTime || null;
		if (currentTime) {
			year.value = new Date(currentTime).getFullYear();
			month.value = new Date(currentTime).getMonth() + 1;
			day.value = new Date(currentTime).getDate();
			hour.value = new Date(currentTime).getHours();
			minute.value = new Date(currentTime).getMinutes();
		} else {
			year.value = date.getFullYear();
			month.value = date.getMonth() + 1;
			day.value = date.getDate();
			hour.value = date.getHours();
			minute.value = date.getMinutes();
		}

		years.value = [];
		months.value = [];
		days.value = [];
		hours.value = [];
		minutes.value = [];
		if (props.customYearRange.length == 0) {
			for (let i = year.value - props.yearRange; i <= year.value + props.yearRange; i++) {
				years.value.push(i);
			}
		} else {
			let Difference = props.customYearRange[1] * 1 - props.customYearRange[0] * 1;
			for (let i = 0; i <= Difference; i++) {
				years.value.push(props.customYearRange[0] * 1 + i);
			}
		}
		for (let i = 1; i <= 12; i++) {
			months.value.push(i);
		}
		updateDays();
		for (let i = 0; i < 24; i++) {
			hours.value.push(i);
		}
		for (let i = 0; i < 60; i++) {
			minutes.value.push(i);
		}

		const currentYearIndex = years.value.indexOf(year.value);
		const currentMonthIndex = month.value - 1;
		const currentDayIndex = day.value - 1;
		const currentHourIndex = hour.value;
		const currentMinuteIndex = minute.value;

		await nextTick();
		selectDate.value = [
			showYear.value ? currentYearIndex : 0,
			showMonth.value ? currentMonthIndex : 0,
			showDay.value ? currentDayIndex : 0,
			showHour.value ? currentHourIndex : 0,
			showMinute.value ? currentMinuteIndex : 0,
		].filter((_, idx) => {
			if (idx === 0 && !showYear.value) return false;
			if (idx === 1 && !showMonth.value) return false;
			if (idx === 2 && !showDay.value) return false;
			if (idx === 3 && !showHour.value) return false;
			if (idx === 4 && !showMinute.value) return false;
			return true;
		});
	}

	function updateDays() {
		const daysInMonth = new Date(year.value, month.value, 0).getDate();
		days.value = Array.from({ length: daysInMonth }, (_, i) => i + 1);
		if (day.value > daysInMonth) day.value = daysInMonth;
	}

	function bindChange(e) {
		const val = e.detail.value;
		const selectedValues = [
			showYear.value ? years.value[val.shift()] : year.value,
			showMonth.value ? months.value[val.shift()] : month.value,
			showDay.value ? days.value[val.shift()] : day.value,
			showHour.value ? hours.value[val.shift()] : hour.value,
			showMinute.value ? minutes.value[val.shift()] : minute.value,
		];
		[year.value, month.value, day.value, hour.value, minute.value] = selectedValues;
		updateDays();
		if (!props.showToolbar) {
			let { timestamp, formattedDate } = getSelectedTime();
			if (props.resultType == 'timestamp') {
				emit('update:currentTime', timestamp);
			} else {
				emit('update:currentTime', formattedDate);
			}
		}
	}

	function formatNumber(n) {
		return n < 10 ? `0${n}` : n;
	}

	const formattedMonths = computed(() => months.value.map(formatNumber));
	const formattedDays = computed(() => days.value.map(formatNumber));
	const formattedHours = computed(() => hours.value.map(formatNumber));
	const formattedMinutes = computed(() => minutes.value.map(formatNumber));

	function clickMask() {
		if (props.closeOnClickOverlay) {
			emit('update:timeShow', false);
		}
	}
	function cancel() {
		emit('update:timeShow', false);
	}
	function confirm() {
		let { timestamp, formattedDate } = getSelectedTime();
		emit('confirm', timestamp, formattedDate);
		if (props.resultType == 'timestamp') {
			emit('update:currentTime', timestamp);
		} else {
			emit('update:currentTime', formattedDate);
		}
		emit('update:timeShow', false);
	}
	function getSelectedTime() {
		const selectedDate = new Date(year.value, month.value - 1, day.value, hour.value, minute.value);
		const timestamp = selectedDate.getTime();
		const data_year = formatNumber(year.value);
		const data_month = formatNumber(month.value);
		const data_day = formatNumber(day.value);
		const data_hours = formatNumber(hour.value);
		const data_minutes = formatNumber(minute.value);

		let formattedDate;
		switch (props.type) {
			case '1':
				formattedDate = `${data_year}/${data_month}/${data_day} ${data_hours}:${data_minutes}`;
				break;
			case '2':
				formattedDate = `${data_year}/${data_month}/${data_day}`;
				break;
			case '3':
				formattedDate = `${data_year}/${data_month}`;
				break;
			case '4':
				formattedDate = `${data_month}/${data_day}`;
				break;
			case '5':
				formattedDate = `${data_month}/${data_day} ${data_hours}:${data_minutes}`;
				break;
			case '6':
				formattedDate = `${data_day} ${data_hours}:${data_minutes}`;
				break;
			case '7':
				formattedDate = `${data_year}`;
				break;
			case '8':
				formattedDate = `${data_month}`;
				break;
			case '9':
				formattedDate = `${data_day}`;
				break;
			case '10':
				formattedDate = `${data_hours}:${data_minutes}`;
				break;
			case '11':
				formattedDate = `${data_hours}`;
				break;
			case '12':
				formattedDate = `${data_minutes}`;
				break;
			default:
				formattedDate = `${data_year}/${data_month}/${data_day} ${data_hours}:${data_minutes}`;
		}

		return { timestamp, formattedDate };
	}
</script>

<style lang="scss" scoped>
	.date-time-box {
		position: fixed;
		top: 0;
		left: 0;
		width: 100%;
		height: 100%;
		.mask-box {
			background: rgba(0, 0, 0, 0.5);
			position: absolute;
			top: 0;
			left: 0;
			width: 100%;
			height: 100%;
		}
		.date-time-picker {
			position: absolute;
			bottom: 0;
			left: 0;
			width: 100%;
			background: #fff;
			border-radius: 30rpx 30rpx 0 0;
			padding: 30rpx;
			box-sizing: border-box;
			.control-box {
				padding: 0 20rpx;
				display: flex;
				justify-content: space-between;
				align-items: center;
				margin-bottom: 30rpx;
				.title {
					font-size: 32rpx;
					font-weight: 800;
					color: #333;
				}
				.cancel {
					font-size: 32rpx;
					font-weight: 800;
				}
				.confirm {
					font-size: 32rpx;
					font-weight: 800;
				}
			}
			.description-box {
				display: flex;
				justify-content: space-around;
				margin-bottom: 10rpx;
				.description-item {
					font-size: 28rpx;
					font-weight: 800;
					color: #333;
				}
			}
			.picker-view {
				.item {
					display: flex;
					justify-content: center;
					align-items: center;
				}
			}
		}
		@keyframes show {
			0% {
				transform: translateY(100%);
			}
			100% {
				transform: translateY(0);
			}
		}
		.animation-show {
			animation: show 0.5s;
		}
		@keyframes hide {
			0% {
				transform: translateY(0);
			}
			100% {
				transform: translateY(100%);
			}
		}
		.animation-hide {
			animation: hide 0.5s;
		}
	}
</style>

页面使用

<myDateTimePicker
		v-model:timeShow="dateTimePickerShow"
		v-model:currentTime="currentTime"
		type="1"
		@confirm="confirmDateTimePicker"
	/>
        
        const dateTimePickerShow = ref(false);
	const currentTime = ref('');
	function confirmDateTimePicker(timestamp, formattedDate) {
		console.log(timestamp, formattedDate);
	}