|
|
@@ -0,0 +1,388 @@
|
|
|
+<script setup lang="ts">
|
|
|
+import dayjs from 'dayjs'
|
|
|
+import { reactive, ref, watch } from 'vue'
|
|
|
+
|
|
|
+// 定义组件属性
|
|
|
+const props = defineProps<{
|
|
|
+ // 初始时间区间,格式为[开始时间, 结束时间]
|
|
|
+ value?: [string, string]
|
|
|
+ // 占位符文本
|
|
|
+ placeholder?: string
|
|
|
+ // 是否禁用
|
|
|
+ disabled?: boolean
|
|
|
+}>()
|
|
|
+
|
|
|
+// 定义事件
|
|
|
+const emit = defineEmits<{
|
|
|
+ // 选择时间后触发的事件
|
|
|
+ (e: 'update:value', value: [string, string]): void
|
|
|
+ // 确定按钮点击事件
|
|
|
+ (e: 'confirm', value: [string, string]): void
|
|
|
+ // 重置按钮点击事件
|
|
|
+ (e: 'reset'): void
|
|
|
+}>()
|
|
|
+
|
|
|
+// 内部状态管理
|
|
|
+const state = reactive({
|
|
|
+ // 时间选择器弹窗是否显示
|
|
|
+ popupVisible: false,
|
|
|
+ // 日期选择器弹窗是否显示
|
|
|
+ datePopupVisible: false,
|
|
|
+ // 当前正在选择的时间类型(start或end)
|
|
|
+ currentDateType: 'start' as 'start' | 'end',
|
|
|
+ // 开始时间
|
|
|
+ startTime: '',
|
|
|
+ // 结束时间
|
|
|
+ endTime: '',
|
|
|
+ // 格式化后的开始时间显示
|
|
|
+ startText: '开始时间',
|
|
|
+ // 格式化后的结束时间显示
|
|
|
+ endText: '结束时间',
|
|
|
+ // 选择器显示文本
|
|
|
+ displayText: props.placeholder || '选择时间区间',
|
|
|
+})
|
|
|
+
|
|
|
+// 日期选择器配置
|
|
|
+const datePickerConfig = reactive({
|
|
|
+ // 日期格式
|
|
|
+ format: 'yyyy-MM-dd',
|
|
|
+ // 当前选择的日期(时间戳)
|
|
|
+ value: new Date().getTime(),
|
|
|
+ // 最小日期(可选,时间戳)
|
|
|
+ minDate: 0,
|
|
|
+ // 最大日期(可选,时间戳)
|
|
|
+ maxDate: 0,
|
|
|
+})
|
|
|
+
|
|
|
+// 监听传入的value变化,更新内部状态
|
|
|
+watch(() => props.value, (newValue) => {
|
|
|
+ if (newValue && Array.isArray(newValue) && newValue.length === 2) {
|
|
|
+ state.startTime = newValue[0]
|
|
|
+ state.endTime = newValue[1]
|
|
|
+ updateDisplayText()
|
|
|
+ }
|
|
|
+}, { immediate: true })
|
|
|
+
|
|
|
+// 打开时间选择器弹窗
|
|
|
+function openPopup() {
|
|
|
+ if (props.disabled) {
|
|
|
+ return
|
|
|
+ }
|
|
|
+ state.popupVisible = true
|
|
|
+}
|
|
|
+
|
|
|
+// 关闭时间选择器弹窗
|
|
|
+function closePopup() {
|
|
|
+ state.popupVisible = false
|
|
|
+}
|
|
|
+
|
|
|
+// 打开日期选择器
|
|
|
+function openDatePicker(type: 'start' | 'end') {
|
|
|
+ state.currentDateType = type
|
|
|
+ // 设置当前选择的日期
|
|
|
+ if (type === 'start') {
|
|
|
+ // 确保日期格式正确,无论是字符串还是时间戳
|
|
|
+ datePickerConfig.value = state.startTime ? new Date(state.startTime).getTime() : new Date().getTime()
|
|
|
+ // 结束时间之后的日期不可选
|
|
|
+ datePickerConfig.maxDate = state.endTime ? new Date(state.endTime).getTime() : 0
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ // 确保日期格式正确,无论是字符串还是时间戳
|
|
|
+ datePickerConfig.value = state.endTime ? new Date(state.endTime).getTime() : new Date().getTime()
|
|
|
+ // 开始时间之前的日期不可选
|
|
|
+ datePickerConfig.minDate = state.startTime ? new Date(state.startTime).getTime() : 0
|
|
|
+ }
|
|
|
+ state.datePopupVisible = true
|
|
|
+}
|
|
|
+
|
|
|
+// 关闭日期选择器
|
|
|
+function closeDatePicker() {
|
|
|
+ state.datePopupVisible = false
|
|
|
+}
|
|
|
+
|
|
|
+// 选择日期确认
|
|
|
+function confirmDate() {
|
|
|
+ // 将时间戳转换为 YYYY-MM-DD 格式
|
|
|
+ const selectedDate = dayjs(datePickerConfig.value).format('YYYY-MM-DD')
|
|
|
+
|
|
|
+ if (state.currentDateType === 'start') {
|
|
|
+ state.startTime = selectedDate
|
|
|
+ state.startText = selectedDate
|
|
|
+ // 如果结束时间早于开始时间,自动调整结束时间
|
|
|
+ if (state.endTime && dayjs(state.endTime).isBefore(dayjs(selectedDate))) {
|
|
|
+ state.endTime = ''
|
|
|
+ state.endText = '结束时间'
|
|
|
+ }
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ state.endTime = selectedDate
|
|
|
+ state.endText = selectedDate
|
|
|
+ // 如果开始时间晚于结束时间,自动调整开始时间
|
|
|
+ if (state.startTime && dayjs(state.startTime).isAfter(dayjs(selectedDate))) {
|
|
|
+ state.startTime = ''
|
|
|
+ state.startText = '开始时间'
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ closeDatePicker()
|
|
|
+ updateDisplayText()
|
|
|
+}
|
|
|
+
|
|
|
+// 更新显示文本
|
|
|
+function updateDisplayText() {
|
|
|
+ console.log('zx1111', state.startTime, state.endTime)
|
|
|
+ if (state.startTime && state.endTime) {
|
|
|
+ state.displayText = `${state.startTime} 至 ${state.endTime}`
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ state.displayText = props.placeholder || '选择时间区间'
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 确认选择
|
|
|
+function confirmSelection() {
|
|
|
+ if (!state.startTime || !state.endTime) {
|
|
|
+ uni.showToast({
|
|
|
+ title: '请选择完整的时间区间',
|
|
|
+ icon: 'none',
|
|
|
+ })
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ const result: [string, string] = [state.startTime, state.endTime]
|
|
|
+ emit('update:value', result)
|
|
|
+ emit('confirm', result)
|
|
|
+ closePopup()
|
|
|
+}
|
|
|
+
|
|
|
+// 重置选择
|
|
|
+// function resetSelection() {
|
|
|
+// state.startTime = ''
|
|
|
+// state.endTime = ''
|
|
|
+// state.startText = '开始时间'
|
|
|
+// state.endText = '结束时间'
|
|
|
+// state.displayText = props.placeholder || '选择时间区间'
|
|
|
+// emit('update:value', ['', ''])
|
|
|
+// emit('reset')
|
|
|
+// }
|
|
|
+
|
|
|
+// 暴露方法给父组件
|
|
|
+defineExpose({
|
|
|
+ openPopup
|
|
|
+})
|
|
|
+</script>
|
|
|
+
|
|
|
+<template>
|
|
|
+ <view class="filter-time-component">
|
|
|
+ <!-- 时间选择器触发按钮 -->
|
|
|
+ <!-- <view
|
|
|
+ class="filter-time-trigger"
|
|
|
+ :class="{ disabled }"
|
|
|
+ @click="openPopup"
|
|
|
+ >
|
|
|
+ <text>{{ state.displayText }}</text>
|
|
|
+ <text class="iconfont icon-down" />
|
|
|
+ </view> -->
|
|
|
+
|
|
|
+ <!-- 时间选择器弹窗(顶部弹出) -->
|
|
|
+ <up-popup
|
|
|
+ v-model:show="state.popupVisible"
|
|
|
+ mode="top"
|
|
|
+ @close="closePopup"
|
|
|
+ >
|
|
|
+ <view class="time-picker-container">
|
|
|
+ <!-- 标题栏 -->
|
|
|
+ <view class="time-picker-header">
|
|
|
+ <text class="title">时间筛选</text>
|
|
|
+ <!-- <text class="reset-btn" @click="resetSelection">重置</text> -->
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 时间选择区域 -->
|
|
|
+ <view class="time-picker-body">
|
|
|
+ <view class="time-item">
|
|
|
+ <text class="label">开始时间</text>
|
|
|
+ <view class="time-selector" @click="openDatePicker('start')">
|
|
|
+ <text class="time-text">{{ state.startText }}</text>
|
|
|
+ <text class="iconfont icon-down" />
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view class="time-item">
|
|
|
+ <text class="label">结束时间</text>
|
|
|
+ <view class="time-selector" @click="openDatePicker('end')">
|
|
|
+ <text class="time-text">{{ state.endText }}</text>
|
|
|
+ <text class="iconfont icon-down" />
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 操作按钮 -->
|
|
|
+ <view class="time-picker-footer">
|
|
|
+ <view class="btn cancel-btn" @click="closePopup">
|
|
|
+ 取消
|
|
|
+ </view>
|
|
|
+ <view class="btn confirm-btn" @click="confirmSelection">
|
|
|
+ 确定
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </up-popup>
|
|
|
+
|
|
|
+ <!-- 日期选择器 -->
|
|
|
+ <up-datetime-picker
|
|
|
+ v-model="datePickerConfig.value"
|
|
|
+ mode="date"
|
|
|
+ :show="state.datePopupVisible"
|
|
|
+ @confirm="confirmDate"
|
|
|
+ @cancel="closeDatePicker"
|
|
|
+ />
|
|
|
+ </view>
|
|
|
+</template>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.filter-time-component {
|
|
|
+ position: relative;
|
|
|
+ display: inline-block;
|
|
|
+}
|
|
|
+
|
|
|
+.filter-time-trigger {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ height: 80rpx;
|
|
|
+ padding: 0 30rpx;
|
|
|
+ background-color: #f5f5f5;
|
|
|
+ border-radius: 10rpx;
|
|
|
+ font-size: 28rpx;
|
|
|
+ color: #333333;
|
|
|
+ cursor: pointer;
|
|
|
+}
|
|
|
+
|
|
|
+.filter-time-trigger.disabled {
|
|
|
+ opacity: 0.5;
|
|
|
+ pointer-events: none;
|
|
|
+}
|
|
|
+
|
|
|
+.iconfont {
|
|
|
+ margin-left: 10rpx;
|
|
|
+ font-size: 24rpx;
|
|
|
+ color: #999999;
|
|
|
+}
|
|
|
+
|
|
|
+/* 时间选择器弹窗样式 */
|
|
|
+.time-picker-container {
|
|
|
+ background-color: #ffffff;
|
|
|
+ border-radius: 0 0 20rpx 20rpx;
|
|
|
+ padding-bottom: 30rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.time-picker-header {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ padding: 30rpx;
|
|
|
+ border-bottom: 1rpx solid #eeeeee;
|
|
|
+}
|
|
|
+
|
|
|
+.time-picker-header .title {
|
|
|
+ font-size: 32rpx;
|
|
|
+ font-weight: 500;
|
|
|
+ color: #333333;
|
|
|
+}
|
|
|
+
|
|
|
+.time-picker-header .reset-btn {
|
|
|
+ font-size: 28rpx;
|
|
|
+ color: #64a6ff;
|
|
|
+ cursor: pointer;
|
|
|
+}
|
|
|
+
|
|
|
+.time-picker-body {
|
|
|
+ padding: 30rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.time-item {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ margin-bottom: 30rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.time-item:last-child {
|
|
|
+ margin-bottom: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.time-item .label {
|
|
|
+ font-size: 30rpx;
|
|
|
+ color: #333333;
|
|
|
+}
|
|
|
+
|
|
|
+.time-selector {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: flex-end;
|
|
|
+ padding: 20rpx 30rpx;
|
|
|
+ background-color: #f5f5f5;
|
|
|
+ border-radius: 10rpx;
|
|
|
+ cursor: pointer;
|
|
|
+}
|
|
|
+
|
|
|
+.time-selector .time-text {
|
|
|
+ font-size: 28rpx;
|
|
|
+ color: #666666;
|
|
|
+ margin-right: 10rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.time-picker-footer {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ padding: 0 30rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.btn {
|
|
|
+ flex: 1;
|
|
|
+ height: 80rpx;
|
|
|
+ line-height: 80rpx;
|
|
|
+ text-align: center;
|
|
|
+ border-radius: 10rpx;
|
|
|
+ font-size: 30rpx;
|
|
|
+ font-weight: 500;
|
|
|
+ cursor: pointer;
|
|
|
+}
|
|
|
+
|
|
|
+.btn.cancel-btn {
|
|
|
+ background-color: #f5f5f5;
|
|
|
+ color: #666666;
|
|
|
+ margin-right: 20rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.btn.confirm-btn {
|
|
|
+ background-color: #64a6ff;
|
|
|
+ color: #ffffff;
|
|
|
+ margin-left: 20rpx;
|
|
|
+}
|
|
|
+
|
|
|
+/* 日期选择器弹窗样式 */
|
|
|
+.date-picker-container {
|
|
|
+ background-color: #ffffff;
|
|
|
+ border-radius: 20rpx 20rpx 0 0;
|
|
|
+}
|
|
|
+
|
|
|
+.date-picker-header {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ padding: 30rpx;
|
|
|
+ border-bottom: 1rpx solid #eeeeee;
|
|
|
+}
|
|
|
+
|
|
|
+.date-picker-header .title {
|
|
|
+ font-size: 32rpx;
|
|
|
+ font-weight: 500;
|
|
|
+ color: #333333;
|
|
|
+}
|
|
|
+
|
|
|
+.date-picker-body {
|
|
|
+ padding: 20rpx 0;
|
|
|
+}
|
|
|
+</style>
|