效果图:

弹框:

组件:
<!-- components/DateTimePicker.vue -->
<template>
<view class="datetime-picker">
<!-- 选择框 -->
<view class="picker-field" :class="{ active: showPicker }" @click="openPicker">
<view class="field-value">
<text class="value-text" :class="{ placeholder: !modelValue }">
{{ displayValue || placeholder }}
</text>
</view>
</view>
<!-- 选择器弹窗 -->
<view v-if="showPicker" class="picker-modal">
<view class="picker-mask" @click="closePicker" />
<view class="picker-content">
<view class="picker-header">
<text class="header-btn cancel" @click="closePicker">取消</text>
<text class="header-title">{{ label }}</text>
<text class="header-btn confirm" @click="confirmSelection">确定</text>
</view>
<view class="picker-body">
<view class="picker-section">
<text class="section-label">日期</text>
<picker mode="date" :value="tempDate" :start="minDate" :end="maxDate" @change="onDateChange">
<view class="picker-display">
<text class="picker-text">{{ tempDate || '请选择日期' }}</text>
</view>
</picker>
</view>
<view class="picker-section">
<text class="section-label">时间</text>
<picker mode="time" :value="tempTime" @change="onTimeChange">
<view class="picker-display">
<text class="picker-text">{{ tempTime || '请选择时间' }}</text>
</view>
</picker>
</view>
<view class="preview-section">
<text class="preview-label">预览</text>
<text class="preview-value">{{ previewValue }}</text>
</view>
</view>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { ref, computed, watch, withDefaults, defineProps, defineEmits } from 'vue';
interface Props {
modelValue?: string;
label?: string;
placeholder?: string;
minDate?: string;
maxDate?: string;
}
const props = withDefaults(defineProps<Props>(), {
label: '选择时间',
placeholder: '请选择日期时间',
minDate: '2020-01-01',
maxDate: '2030-12-31',
});
const emit = defineEmits<{
'update:modelValue': [value: string];
}>();
// 响应式数据
const showPicker = ref(false);
const tempDate = ref('');
const tempTime = ref('');
// 计算属性
const displayValue = computed(() => {
return props.modelValue;
});
const previewValue = computed(() => {
if (tempDate.value && tempTime.value) {
return `${tempDate.value} ${tempTime.value}:00`;
}
return '请选择日期和时间';
});
const closePicker = () => {
showPicker.value = false;
};
const confirmSelection = () => {
if (tempDate.value && tempTime.value) {
const value = `${tempDate.value} ${tempTime.value}:00`;
emit('update:modelValue', value);
}
closePicker();
};
const onDateChange = (e: any) => {
tempDate.value = e.detail.value;
};
const onTimeChange = (e: any) => {
tempTime.value = e.detail.value;
};
// 工具函数
const getCurrentDate = (): string => {
const now = new Date();
const year = now.getFullYear();
const month = (now.getMonth() + 1).toString().padStart(2, '0');
const day = now.getDate().toString().padStart(2, '0');
return `${year}-${month}-${day}`;
};
const getCurrentTime = (): string => {
const now = new Date();
const hours = now.getHours().toString().padStart(2, '0');
const minutes = now.getMinutes().toString().padStart(2, '0');
return `${hours}:${minutes}`;
};
// 方法
const openPicker = () => {
if (props.modelValue) {
// 回显已选值
const [datePart, timePart] = props.modelValue.split(' ');
tempDate.value = datePart;
tempTime.value = timePart ? timePart.slice(0, 5) : getCurrentTime();
} else {
// 使用当前时间
tempDate.value = getCurrentDate();
tempTime.value = getCurrentTime();
}
showPicker.value = true;
};
</script>
<style scoped>
.datetime-picker {
width: 100%;
}
.picker-field {
background: #ffffff;
border: 2rpx solid #e5e5e5;
border-radius: 16rpx;
padding: 24rpx 30rpx;
transition: all 0.3s ease;
}
.picker-field.active {
border-color: #007aff;
}
.field-label {
font-size: 28rpx;
color: #666;
font-weight: 500;
}
.field-value {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 16rpx;
}
.value-text {
font-size: 32rpx;
color: #333;
font-weight: 500;
}
.value-text.placeholder {
color: #999;
}
.field-icon {
color: #999;
font-size: 24rpx;
}
/* 弹窗样式同上面示例 */
.picker-modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 9999;
}
.picker-mask {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
}
.picker-content {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
background: #ffffff;
border-top-left-radius: 24rpx;
border-top-right-radius: 24rpx;
max-height: 70vh;
overflow: hidden;
}
.picker-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 30rpx;
border-bottom: 1rpx solid #f0f0f0;
}
.header-title {
font-size: 36rpx;
font-weight: 600;
color: #333;
}
.header-btn {
font-size: 32rpx;
padding: 16rpx 24rpx;
}
.cancel {
color: #666;
}
.confirm {
color: #007aff;
font-weight: 500;
}
.picker-body {
padding: 30rpx;
}
.picker-section {
margin-bottom: 40rpx;
}
.section-label {
display: block;
font-size: 28rpx;
color: #666;
margin-bottom: 20rpx;
font-weight: 500;
}
.picker-display {
display: flex;
justify-content: space-between;
align-items: center;
padding: 24rpx 30rpx;
background: #f8f9fa;
border-radius: 12rpx;
border: 2rpx solid #e9ecef;
}
.picker-text {
font-size: 32rpx;
color: #333;
font-weight: 500;
}
.picker-arrow {
color: #999;
font-size: 24rpx;
transform: rotate(90deg);
}
.preview-section {
background: #e8f4ff;
border-radius: 12rpx;
padding: 30rpx;
margin-top: 20rpx;
}
.preview-label {
display: block;
font-size: 26rpx;
color: #007aff;
margin-bottom: 16rpx;
font-weight: 500;
}
.preview-value {
font-size: 32rpx;
color: #333;
font-weight: 600;
text-align: center;
display: block;
}
</style>
页面:
<template>
<date-picker
v-model="departTime"
class="time-input"
label="开始时间"
placeholder="请选择开始时间" />
</template>
<script setup lang="ts">
import { ref } from 'vue';
const departTime = ref('');
</script>