一、前言
目前
uni-ui
组件的日期选择组件,有日期选择、日期范围选择、日期时间选择、日期时间范围选择,就是没有时间段的选择,懒的去找第三方插件,而我的需求就是简单的取一个时间段的范围,就自己封装了。
二、最终效果

三、参数配置
1、代码示例
html
<TTimeRange v-model="time" v-model:show="showPicker" @confirm="onConfirm" @cancel="showPicker=false" />
2、组件使用
html
<TInput
title="每日可装车时段"
inputType="select"
v-model:select-value="state.orderMsg.loadTime"
@clickInput="showPicker=true"
/>
<TTimeRange v-model:show="showPicker" @confirm="onConfirm" @cancel="showPicker=false" />
四、组件源码
html
<template>
<view class="t_time-range-picker">
<!-- 遮罩层 -->
<view class="mask" v-if="show" @tap="cancel"></view>
<!-- 选择器容器 -->
<view class="picker-container" v-if="show">
<view class="picker-header">
<text class="btn cancel" @tap="cancel">取消</text>
<text class="btn clear" v-if="isShowClear" @tap="clearSelection">清空</text>
<text class="title">选择时间段</text>
<text class="btn confirm" @tap="confirm">确定</text>
</view>
<!-- 时间选择器 -->
<picker-view
class="picker-view"
:value="pickerValue"
@change="onPickerChange"
indicator-style="height: 60rpx;"
>
<!-- 开始时间-小时 -->
<picker-view-column>
<view class="picker-item" v-for="(h, index) in hours" :key="index">{{ h }}时</view>
</picker-view-column>
<!-- 开始时间-分钟 -->
<picker-view-column>
<view class="picker-item" v-for="(m, index) in minutes" :key="index">{{ m }}分</view>
</picker-view-column>
<view class="separator">至</view>
<!-- 结束时间-小时 -->
<picker-view-column>
<view class="picker-item" v-for="(h, index) in hours" :key="index">{{ h }}时</view>
</picker-view-column>
<!-- 结束时间-分钟 -->
<picker-view-column>
<view class="picker-item" v-for="(m, index) in minutes" :key="index">{{ m }}分</view>
</picker-view-column>
</picker-view>
</view>
</view>
</template>
<script setup lang="ts">
import { ref, computed, watch, reactive } from "vue";
const props = defineProps({
// 组件显示状态
show: {
type: Boolean,
default: false
},
isShowClear: {
type: Boolean,
default: false
},
// 双向绑定的时间段值
modelValue: {
type: Object as () => { start: string; end: string },
default: () => ({ start: "00:00", end: "00:00" })
}
});
const emit = defineEmits(["update:modelValue", "confirm", "cancel"]);
// 生成小时数组 (00-23)
const hours = Array.from({ length: 24 }, (_, i) => i.toString().padStart(2, "0"));
// 生成分钟数组 (00-59)
const minutes = Array.from({ length: 60 }, (_, i) => i.toString().padStart(2, "0"));
// picker-view的当前索引值 [开始小时, 开始分钟, 结束小时, 结束分钟]
const pickerValue = ref<number[]>([0, 0, 0, 0]);
// 当前选择的时间段
const timeRange = reactive({
start: props.modelValue.start,
end: props.modelValue.end
});
// 将时间字符串转换为picker索引
const timeToIndex = (time: string): number[] => {
const [hour, minute] = time.split(":").map(Number);
return [
hours.findIndex(h => h === hour?.toString().padStart(2, "0")),
minutes.findIndex(m => m === minute?.toString().padStart(2, "0"))
];
};
// 初始化picker值
const initPicker = () => {
const startIdx = timeToIndex(timeRange.start);
const endIdx = timeToIndex(timeRange.end);
pickerValue.value = [...startIdx, ...endIdx];
};
// 监听显示状态变化
watch(
() => props.show,
val => {
if (val) initPicker();
}
);
// 监听外部传入的值变化
watch(
() => props.modelValue,
val => {
timeRange.start = val.start;
timeRange.end = val.end;
}
);
// 选择器变化事件
const onPickerChange = (e: any) => {
const values: number[] = e.detail.value;
// 获取新选择的时间
const newStart = `${hours[values[0]]}:${minutes[values[1]]}`;
const newEnd = `${hours[values[2]]}:${minutes[values[3]]}`;
// 时间有效性校验
if (newStart > newEnd) {
// 如果开始时间晚于结束时间,自动交换
timeRange.start = newEnd;
timeRange.end = newStart;
pickerValue.value = [...timeToIndex(newEnd), ...timeToIndex(newStart)];
} else {
timeRange.start = newStart;
timeRange.end = newEnd;
pickerValue.value = values;
}
};
// 确认选择
const confirm = () => {
emit("update:modelValue", { ...timeRange });
emit("confirm", { ...timeRange });
};
// 取消选择
const cancel = () => {
initPicker(); // 重置为原始值
emit("cancel");
};
// 清空选择的方法
const clearSelection = () => {
// const defaultTime = { start: "00:00", end: "00:00" };
timeRange.start = "";
timeRange.end = "";
initPicker(); // 更新 pickerValue 到默认值
emit("update:modelValue", { ...timeRange });
};
defineExpose({ cancel, clearSelection });
</script>
<style lang="scss" scoped>
.t_time-range-picker {
position: relative;
z-index: 999;
.mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
}
.picker-container {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: #fff;
border-radius: 24rpx 24rpx 0 0;
padding: 20rpx 0;
.picker-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20rpx 30rpx;
border-bottom: 1rpx solid #eee;
.title {
font-size: 32rpx;
font-weight: bold;
}
.btn {
padding: 10rpx 30rpx;
font-size: 32rpx;
}
.cancel {
color: #101010;
}
.clear {
color: #ff3b30;
}
.confirm {
color: #007aff;
}
}
.picker-view {
height: 520rpx;
margin-top: 20rpx;
.picker-item {
line-height: 50rpx;
text-align: center;
font-size: 30rpx;
}
.separator {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
font-size: 32rpx;
color: #333;
}
}
}
}
</style>