|
|
@@ -1,5 +1,4 @@
|
|
|
<template>
|
|
|
- <!-- 使用 u-popup 作为弹窗容器 -->
|
|
|
<u-popup
|
|
|
:show="visible"
|
|
|
mode="bottom"
|
|
|
@@ -31,7 +30,11 @@
|
|
|
@tap="selectDate(date.value)"
|
|
|
>
|
|
|
<view class="date-content">
|
|
|
- <text class="date-name">{{ date.label }}</text>
|
|
|
+ <text class="date-name"
|
|
|
+ :class="{
|
|
|
+ active: selectedDate === date.value,
|
|
|
+ today: date.isToday
|
|
|
+ }">{{ date.label }}</text>
|
|
|
<text class="date-info">{{ date.month }}月{{ date.day }}日</text>
|
|
|
<text class="date-week">{{ date.week }}</text>
|
|
|
</view>
|
|
|
@@ -44,21 +47,20 @@
|
|
|
<view v-if="selectedDate === todayDate && withinOneHourSlot" class="recommend-section">
|
|
|
<view class="recommend-header">
|
|
|
<text class="recommend-title">一小时内</text>
|
|
|
- <!-- <text class="recommend-desc">推荐时间:{{ withinOneHourSlot.label }}</text> -->
|
|
|
</view>
|
|
|
- <view class="recommend-time"
|
|
|
- @tap="selectTime(withinOneHourSlot.value, true)">
|
|
|
+ <view
|
|
|
+ class="recommend-time"
|
|
|
+ :class="{ active: selectedTime === withinOneHourSlot.value }"
|
|
|
+ @tap="selectTime(withinOneHourSlot.value, true)"
|
|
|
+ >
|
|
|
<text class="time-value">{{ withinOneHourSlot.label }}</text>
|
|
|
- <!-- <text class="time-tag">推荐</text> -->
|
|
|
</view>
|
|
|
</view>
|
|
|
|
|
|
- <!-- 时间段列表 -->
|
|
|
+ <!-- 普通时间段列表 -->
|
|
|
<view class="time-section">
|
|
|
<view class="section-title">可选时间段</view>
|
|
|
-
|
|
|
<view v-if="loading" class="loading">加载中...</view>
|
|
|
-
|
|
|
<view v-else class="time-list">
|
|
|
<view
|
|
|
v-for="timeSlot in timeSlots"
|
|
|
@@ -80,7 +82,7 @@
|
|
|
<!-- 提示信息 -->
|
|
|
<view class="notice-section">
|
|
|
<text class="notice-text">
|
|
|
- 部分区域支持夜间上门,具体以快递员联系为准
|
|
|
+ <!-- 部分区域支持夜间上门,具体以快递员联系为准 -->
|
|
|
</text>
|
|
|
</view>
|
|
|
</scroll-view>
|
|
|
@@ -104,245 +106,155 @@
|
|
|
import { ref, computed, watch, onMounted, onUnmounted } from 'vue'
|
|
|
|
|
|
const props = defineProps({
|
|
|
- visible: {
|
|
|
- type: Boolean,
|
|
|
- default: false
|
|
|
- },
|
|
|
- // 显示天数
|
|
|
- days: {
|
|
|
- type: Number,
|
|
|
- default: 3
|
|
|
- },
|
|
|
- // 开始时间
|
|
|
- startTime: {
|
|
|
- type: Number,
|
|
|
- default: 9
|
|
|
- },
|
|
|
- // 结束时间
|
|
|
- endTime: {
|
|
|
- type: Number,
|
|
|
- default: 21
|
|
|
- },
|
|
|
- // 是否自动选择推荐时间
|
|
|
- autoSelect: {
|
|
|
- type: Boolean,
|
|
|
- default: true
|
|
|
- }
|
|
|
+ visible: { type: Boolean, default: false },
|
|
|
+ days: { type: Number, default: 3 },
|
|
|
+ startTime: { type: Number, default: 8 },
|
|
|
+ endTime: { type: Number, default: 20 },
|
|
|
+ autoSelect: { type: Boolean, default: true }
|
|
|
})
|
|
|
|
|
|
const emit = defineEmits(['update:visible', 'close', 'confirm'])
|
|
|
|
|
|
-// 当前时间
|
|
|
+// 当前时间(每分钟更新)
|
|
|
const currentTime = ref(new Date())
|
|
|
const selectedDate = ref('')
|
|
|
const selectedTime = ref('')
|
|
|
const loading = ref(false)
|
|
|
const isWithinOneHour = ref(false)
|
|
|
|
|
|
-// 计算当前时间20分钟后的时间
|
|
|
+// ----- 精确时间计算 -----
|
|
|
+const oneHourLater = computed(() => {
|
|
|
+ const now = currentTime.value
|
|
|
+ return now.getHours() + now.getMinutes() / 60 + 1
|
|
|
+})
|
|
|
+
|
|
|
const twentyMinutesLater = computed(() => {
|
|
|
- const now = new Date()
|
|
|
+ const now = new Date(currentTime.value)
|
|
|
now.setMinutes(now.getMinutes() + 10)
|
|
|
return now
|
|
|
})
|
|
|
|
|
|
-// 计算一小时后的小时(向上取整)
|
|
|
-const oneHourLaterHour = computed(() => {
|
|
|
- const now = new Date()
|
|
|
- now.setHours(now.getHours() + 1)
|
|
|
- return Math.ceil(now.getHours())
|
|
|
-})
|
|
|
+// ----- 推荐时段(唯一 value 前缀,不与普通时段冲突)-----
|
|
|
+const withinOneHourSlot = computed(() => {
|
|
|
+ if (!dateList.value.length || !dateList.value[0].isToday) return null
|
|
|
+
|
|
|
+ const later = twentyMinutesLater.value
|
|
|
+ let startHour = later.getHours()
|
|
|
+ let startMinute = Math.ceil(later.getMinutes() / 10) * 10
|
|
|
|
|
|
-// 获取今天日期字符串
|
|
|
-const todayDate = computed(() => {
|
|
|
- const today = new Date()
|
|
|
- return formatDate(today)
|
|
|
+ if (startMinute >= 60) {
|
|
|
+ startHour += 1
|
|
|
+ startMinute = 0
|
|
|
+ }
|
|
|
+ if (startHour >= props.endTime) return null
|
|
|
+
|
|
|
+ let endHour = startHour + 1
|
|
|
+ let endMinute = startMinute
|
|
|
+ if (endHour > props.endTime) {
|
|
|
+ endHour = props.endTime
|
|
|
+ endMinute = 0
|
|
|
+ }
|
|
|
+ const duration = (endHour - startHour) * 60 + (endMinute - startMinute)
|
|
|
+ if (duration < 30) return null
|
|
|
+
|
|
|
+ const startStr = formatTime(startHour, startMinute)
|
|
|
+ const endStr = formatTime(endHour, endMinute)
|
|
|
+ return {
|
|
|
+ start: startHour + startMinute / 60,
|
|
|
+ end: endHour + endMinute / 60,
|
|
|
+ label: `(${startStr}-${endStr})`,
|
|
|
+ value: `immediate:${startStr}-${endStr}`, // ✅ 唯一标识
|
|
|
+ startHour, startMinute, endHour, endMinute
|
|
|
+ }
|
|
|
})
|
|
|
|
|
|
-// 生成日期列表
|
|
|
+// ----- 日期列表 -----
|
|
|
+const todayDate = computed(() => formatDate(new Date()))
|
|
|
const dateList = computed(() => {
|
|
|
const days = ['周日', '周一', '周二', '周三', '周四', '周五', '周六']
|
|
|
const list = []
|
|
|
const today = new Date()
|
|
|
-
|
|
|
for (let i = 0; i < props.days; i++) {
|
|
|
const date = new Date(today)
|
|
|
date.setDate(date.getDate() + i)
|
|
|
-
|
|
|
const month = date.getMonth() + 1
|
|
|
const day = date.getDate()
|
|
|
const week = days[date.getDay()]
|
|
|
const dateStr = formatDate(date)
|
|
|
-
|
|
|
- let label = ''
|
|
|
- if (i === 0) {
|
|
|
- label = '今天'
|
|
|
- } else if (i === 1) {
|
|
|
- label = '明天'
|
|
|
- } else {
|
|
|
- label = '后天'
|
|
|
- }
|
|
|
-
|
|
|
list.push({
|
|
|
value: dateStr,
|
|
|
- label: label,
|
|
|
- week: week,
|
|
|
- month: month,
|
|
|
- day: day,
|
|
|
- fullLabel: `${month}月${day}日 ${week}`,
|
|
|
+ label: i === 0 ? '今天' : i === 1 ? '明天' : '后天',
|
|
|
+ week, month, day,
|
|
|
isToday: i === 0,
|
|
|
- isTomorrow: i === 1,
|
|
|
- isDayAfterTomorrow: i === 2,
|
|
|
- displayDate: `${date.getFullYear()}-${month.toString().padStart(2, '0')}-${day.toString().padStart(2, '0')}`
|
|
|
+ displayDate: dateStr
|
|
|
})
|
|
|
}
|
|
|
-
|
|
|
return list
|
|
|
})
|
|
|
|
|
|
-// 基础时间段配置
|
|
|
+// ----- 基础整点时段 -----
|
|
|
const baseTimeSlots = computed(() => {
|
|
|
const slots = []
|
|
|
- const startHour = props.startTime
|
|
|
- const endHour = props.endTime
|
|
|
- const interval = 1 // 小时间隔
|
|
|
-
|
|
|
- for (let hour = startHour; hour < endHour; hour += interval) {
|
|
|
- const start = hour
|
|
|
- const end = hour + interval
|
|
|
- const startStr = start.toString().padStart(2, '0')
|
|
|
- const endStr = end.toString().padStart(2, '0')
|
|
|
-
|
|
|
+ for (let hour = props.startTime; hour < props.endTime; hour++) {
|
|
|
slots.push({
|
|
|
- start: start,
|
|
|
- end: end,
|
|
|
- label: `${startStr}:00-${endStr}:00`,
|
|
|
- value: `${startStr}:00-${endStr}:00`,
|
|
|
- startHour: start,
|
|
|
- endHour: end
|
|
|
+ start: hour,
|
|
|
+ end: hour + 1,
|
|
|
+ label: `${padZero(hour)}:00-${padZero(hour + 1)}:00`,
|
|
|
+ value: `${padZero(hour)}:00-${padZero(hour + 1)}:00`,
|
|
|
+ startHour: hour,
|
|
|
+ endHour: hour + 1
|
|
|
})
|
|
|
}
|
|
|
-
|
|
|
return slots
|
|
|
})
|
|
|
|
|
|
-// 计算一小时内时间段
|
|
|
-const withinOneHourSlot = computed(() => {
|
|
|
- if (!dateList.value.length || !dateList.value[0].isToday) return null
|
|
|
-
|
|
|
- const now = currentTime.value
|
|
|
- const later20min = twentyMinutesLater.value
|
|
|
-
|
|
|
- // 计算开始时间:当前时间 + 20分钟,分钟向上取整到10的倍数
|
|
|
- const startMinutes = later20min.getMinutes()
|
|
|
- const roundedMinutes = Math.ceil(startMinutes / 10) * 10
|
|
|
- let startHour = later20min.getHours()
|
|
|
- let startMinute = roundedMinutes
|
|
|
-
|
|
|
- // 如果分钟超过60,小时加1,分钟归零
|
|
|
- if (startMinute >= 60) {
|
|
|
- startHour += 1
|
|
|
- startMinute = 0
|
|
|
- }
|
|
|
-
|
|
|
- // 计算结束时间:开始时间 + 1小时
|
|
|
- let endHour = startHour + 1
|
|
|
- let endMinute = startMinute
|
|
|
-
|
|
|
- // 如果开始时间已经超过结束时间限制,则不显示
|
|
|
- if (startHour >= props.endTime) return null
|
|
|
-
|
|
|
- // 如果结束时间超过结束时间限制,则调整
|
|
|
- if (endHour > props.endTime) {
|
|
|
- endHour = props.endTime
|
|
|
- endMinute = 0
|
|
|
- }
|
|
|
-
|
|
|
- // 如果开始时间小于开始时间限制,则调整
|
|
|
- if (startHour < props.startTime) {
|
|
|
- startHour = props.startTime
|
|
|
- startMinute = 0
|
|
|
- }
|
|
|
-
|
|
|
- // 检查时间段是否有效(至少30分钟)
|
|
|
- const slotDuration = (endHour - startHour) * 60 + (endMinute - startMinute)
|
|
|
- if (slotDuration < 30) return null
|
|
|
-
|
|
|
- const startStr = formatTime(startHour, startMinute)
|
|
|
- const endStr = formatTime(endHour, endMinute)
|
|
|
-
|
|
|
- return {
|
|
|
- start: startHour + startMinute / 60,
|
|
|
- end: endHour + endMinute / 60,
|
|
|
- label: `(${startStr}-${endStr})`,
|
|
|
- value: `${startStr}-${endStr}`,
|
|
|
- isWithinOneHour: true,
|
|
|
- startHour: startHour,
|
|
|
- startMinute: startMinute,
|
|
|
- endHour: endHour,
|
|
|
- endMinute: endMinute
|
|
|
- }
|
|
|
-})
|
|
|
-
|
|
|
-// 是否是夜间时间段
|
|
|
-const isNightTime = (hour) => {
|
|
|
- return hour >= 18
|
|
|
-}
|
|
|
-
|
|
|
-// 根据选择的日期获取时间段
|
|
|
+// ----- 根据所选日期过滤可用时段(精确到小时+分钟)-----
|
|
|
const timeSlots = computed(() => {
|
|
|
const slots = []
|
|
|
const isToday = selectedDate.value === todayDate.value
|
|
|
-
|
|
|
+
|
|
|
if (isToday) {
|
|
|
- const oneHourLater = oneHourLaterHour.value
|
|
|
-
|
|
|
- // 添加普通时间段(开始时间从一小时后开始)
|
|
|
+ const threshold = oneHourLater.value
|
|
|
baseTimeSlots.value.forEach(slot => {
|
|
|
- // 时间段开始时间必须在当前时间+1小时之后
|
|
|
- const isDisabled = slot.start < oneHourLater
|
|
|
-
|
|
|
- if (!isDisabled) {
|
|
|
+ if (slot.start >= threshold) {
|
|
|
slots.push({
|
|
|
...slot,
|
|
|
disabled: false,
|
|
|
- night: false,
|
|
|
+ night: slot.start >= 22,
|
|
|
tag: ''
|
|
|
- })
|
|
|
- // night: isNightTime(slot.start),
|
|
|
+ })
|
|
|
}
|
|
|
})
|
|
|
} else {
|
|
|
- // 明天及以后显示所有时间段
|
|
|
baseTimeSlots.value.forEach(slot => {
|
|
|
slots.push({
|
|
|
...slot,
|
|
|
disabled: false,
|
|
|
- night: isNightTime(slot.start),
|
|
|
+ night: slot.start >= 22,
|
|
|
tag: ''
|
|
|
})
|
|
|
})
|
|
|
}
|
|
|
-
|
|
|
return slots
|
|
|
})
|
|
|
|
|
|
-// 按钮文本
|
|
|
-const buttonText = computed(() => {
|
|
|
- if (!selectedTime.value) return '请选择上门时间'
|
|
|
- return '确定'
|
|
|
-})
|
|
|
+// ----- 按钮文字 -----
|
|
|
+const buttonText = computed(() => selectedTime.value ? '确定' : '请选择上门时间')
|
|
|
|
|
|
-// 初始化选择
|
|
|
-const initSelection = () => {
|
|
|
- if (dateList.value.length > 0) {
|
|
|
- selectedDate.value = dateList.value[0].value
|
|
|
-
|
|
|
- // 自动选择推荐时间或第一个可用时间
|
|
|
- if (withinOneHourSlot.value && props.autoSelect) {
|
|
|
+// ----- 核心:切换日期(自动根据 autoSelect 选中第一个可用时段)-----
|
|
|
+const selectDate = (date) => {
|
|
|
+ selectedDate.value = date
|
|
|
+ selectedTime.value = ''
|
|
|
+ isWithinOneHour.value = false
|
|
|
+
|
|
|
+ // 若开启自动选择,立即为该日期选择一个合适的时间
|
|
|
+ if (props.autoSelect) {
|
|
|
+ // 1. 如果是今天且有推荐时段,优先选中推荐
|
|
|
+ if (date === todayDate.value && withinOneHourSlot.value) {
|
|
|
selectedTime.value = withinOneHourSlot.value.value
|
|
|
isWithinOneHour.value = true
|
|
|
} else {
|
|
|
+ // 2. 否则选中该日期下第一个可用普通时段
|
|
|
const firstSlot = timeSlots.value[0]
|
|
|
if (firstSlot) {
|
|
|
selectedTime.value = firstSlot.value
|
|
|
@@ -352,121 +264,98 @@ const initSelection = () => {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-// 选择日期
|
|
|
-const selectDate = (date) => {
|
|
|
- selectedDate.value = date
|
|
|
- selectedTime.value = '' // 重置时间选择
|
|
|
- isWithinOneHour.value = false
|
|
|
-}
|
|
|
-
|
|
|
-// 选择时间
|
|
|
-const selectTime = (time, isWithinOneHourFlag = false) => {
|
|
|
+// ----- 选择时间(普通时段 / 推荐时段)-----
|
|
|
+const selectTime = (time, isImmediate = false) => {
|
|
|
selectedTime.value = time
|
|
|
- isWithinOneHour.value = isWithinOneHourFlag
|
|
|
+ isWithinOneHour.value = isImmediate
|
|
|
}
|
|
|
|
|
|
-// 格式化日期时间
|
|
|
-const formatDateTime = (dateStr, hour) => {
|
|
|
- const date = new Date(dateStr)
|
|
|
- const wholeHour = Math.floor(hour)
|
|
|
- const minutes = Math.round((hour - wholeHour) * 60)
|
|
|
- date.setHours(wholeHour, minutes, 0, 0)
|
|
|
-
|
|
|
- const year = date.getFullYear()
|
|
|
- const month = String(date.getMonth() + 1).padStart(2, '0')
|
|
|
- const day = String(date.getDate()).padStart(2, '0')
|
|
|
- const hours = String(date.getHours()).padStart(2, '0')
|
|
|
- const mins = String(date.getMinutes()).padStart(2, '0')
|
|
|
- const seconds = String(date.getSeconds()).padStart(2, '0')
|
|
|
-
|
|
|
- return `${year}-${month}-${day} ${hours}:${mins}:${seconds}`
|
|
|
-}
|
|
|
-
|
|
|
-// 获取开始小时
|
|
|
-const getStartHour = (timeLabel) => {
|
|
|
- if (isWithinOneHour.value) {
|
|
|
- return withinOneHourSlot.value.startHour + withinOneHourSlot.value.startMinute / 60
|
|
|
- }
|
|
|
-
|
|
|
- const match = timeLabel.match(/(\d{2}):/)
|
|
|
- return match ? parseInt(match[1]) : 0
|
|
|
-}
|
|
|
-
|
|
|
-// 获取结束小时
|
|
|
-const getEndHour = (timeLabel) => {
|
|
|
- if (isWithinOneHour.value) {
|
|
|
- return withinOneHourSlot.value.endHour + withinOneHourSlot.value.endMinute / 60
|
|
|
+// ----- 初始化弹窗(打开时)-----
|
|
|
+const initSelection = () => {
|
|
|
+ if (dateList.value.length) {
|
|
|
+ // 直接调用 selectDate,复用自动选择逻辑
|
|
|
+ selectDate(dateList.value[0].value)
|
|
|
}
|
|
|
-
|
|
|
- const match = timeLabel.match(/-(\d{2}):/)
|
|
|
- return match ? parseInt(match[1]) : 0
|
|
|
}
|
|
|
|
|
|
-// 确认选择
|
|
|
+// ----- 确认选择 -----
|
|
|
const confirm = () => {
|
|
|
if (!selectedTime.value) return
|
|
|
-
|
|
|
+
|
|
|
const selectedDateObj = dateList.value.find(d => d.value === selectedDate.value)
|
|
|
- const selectedTimeSlot = isWithinOneHour.value ? withinOneHourSlot.value :
|
|
|
- baseTimeSlots.value.find(s => s.value === selectedTime.value)
|
|
|
-
|
|
|
- if (!selectedDateObj || !selectedTimeSlot) return
|
|
|
-
|
|
|
- // 计算开始和结束时间
|
|
|
- const startHour = getStartHour(selectedTime.value)
|
|
|
- const endHour = getEndHour(selectedTime.value)
|
|
|
-
|
|
|
- // 计算具体的开始和结束时间字符串
|
|
|
- const startTimeStr = formatDateTime(selectedDate.value, startHour)
|
|
|
- const endTimeStr = formatDateTime(selectedDate.value, endHour)
|
|
|
-
|
|
|
- // 生成显示文本
|
|
|
- let displayText = ''
|
|
|
+ let selectedTimeSlot
|
|
|
+
|
|
|
if (isWithinOneHour.value) {
|
|
|
- displayText = `${selectedDateObj.label} 一小时内`
|
|
|
+ selectedTimeSlot = withinOneHourSlot.value
|
|
|
} else {
|
|
|
- displayText = `${selectedDateObj.label} ${selectedTime.value}`
|
|
|
+ selectedTimeSlot = baseTimeSlots.value.find(s => s.value === selectedTime.value)
|
|
|
}
|
|
|
-
|
|
|
- const timeData = {
|
|
|
+
|
|
|
+ if (!selectedDateObj || !selectedTimeSlot) return
|
|
|
+
|
|
|
+ const startHour = isWithinOneHour.value
|
|
|
+ ? withinOneHourSlot.value.start
|
|
|
+ : selectedTimeSlot.start
|
|
|
+ const endHour = isWithinOneHour.value
|
|
|
+ ? withinOneHourSlot.value.end
|
|
|
+ : selectedTimeSlot.end
|
|
|
+
|
|
|
+ const startTimeStr = formatDateTime(selectedDate.value, startHour)
|
|
|
+ const endTimeStr = formatDateTime(selectedDate.value, endHour)
|
|
|
+
|
|
|
+ const displayText = isWithinOneHour.value
|
|
|
+ ? `${selectedDateObj.label} 一小时内`
|
|
|
+ : `${selectedDateObj.label} ${selectedTime.value}`
|
|
|
+
|
|
|
+ emit('confirm', {
|
|
|
date: selectedDate.value,
|
|
|
time: selectedTime.value,
|
|
|
dateLabel: selectedDateObj.label,
|
|
|
timeLabel: selectedTime.value,
|
|
|
fullLabel: displayText,
|
|
|
- displayText: displayText,
|
|
|
+ displayText,
|
|
|
startTime: startTimeStr,
|
|
|
endTime: endTimeStr,
|
|
|
isToday: selectedDateObj.isToday,
|
|
|
isImmediate: isWithinOneHour.value,
|
|
|
- isNight: isNightTime(selectedTimeSlot.startHour),
|
|
|
- startHour: startHour,
|
|
|
- endHour: endHour
|
|
|
- }
|
|
|
-
|
|
|
- emit('confirm', timeData)
|
|
|
+ isNight: selectedTimeSlot.start >= 18,
|
|
|
+ startHour,
|
|
|
+ endHour
|
|
|
+ })
|
|
|
close()
|
|
|
}
|
|
|
|
|
|
-// 关闭弹框
|
|
|
+// ----- 关闭弹窗 -----
|
|
|
const close = () => {
|
|
|
emit('close')
|
|
|
emit('update:visible', false)
|
|
|
}
|
|
|
|
|
|
-// 工具函数
|
|
|
+// ----- 工具函数 -----
|
|
|
const formatDate = (date) => {
|
|
|
- const year = date.getFullYear()
|
|
|
- const month = String(date.getMonth() + 1).padStart(2, '0')
|
|
|
- const day = String(date.getDate()).padStart(2, '0')
|
|
|
- return `${year}-${month}-${day}`
|
|
|
+ const y = date.getFullYear()
|
|
|
+ const m = String(date.getMonth() + 1).padStart(2, '0')
|
|
|
+ const d = String(date.getDate()).padStart(2, '0')
|
|
|
+ return `${y}-${m}-${d}`
|
|
|
}
|
|
|
-
|
|
|
const formatTime = (hour, minute) => {
|
|
|
- return `${hour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}`
|
|
|
+ return `${String(hour).padStart(2, '0')}:${String(minute).padStart(2, '0')}`
|
|
|
+}
|
|
|
+const padZero = (num) => String(num).padStart(2, '0')
|
|
|
+const formatDateTime = (dateStr, hourDecimal) => {
|
|
|
+ const date = new Date(dateStr)
|
|
|
+ const wholeHour = Math.floor(hourDecimal)
|
|
|
+ const minutes = Math.round((hourDecimal - wholeHour) * 60)
|
|
|
+ date.setHours(wholeHour, minutes, 0, 0)
|
|
|
+ const y = date.getFullYear()
|
|
|
+ const m = String(date.getMonth() + 1).padStart(2, '0')
|
|
|
+ const d = String(date.getDate()).padStart(2, '0')
|
|
|
+ const hh = String(date.getHours()).padStart(2, '0')
|
|
|
+ const mm = String(date.getMinutes()).padStart(2, '0')
|
|
|
+ return `${y}-${m}-${d} ${hh}:${mm}:00`
|
|
|
}
|
|
|
|
|
|
-// 监听可见性变化
|
|
|
+// ----- 监听弹窗显示,初始化 -----
|
|
|
watch(() => props.visible, (newVal) => {
|
|
|
if (newVal) {
|
|
|
currentTime.value = new Date()
|
|
|
@@ -474,32 +363,26 @@ watch(() => props.visible, (newVal) => {
|
|
|
}
|
|
|
})
|
|
|
|
|
|
-// 定时更新当前时间
|
|
|
+// ----- 每分钟刷新当前时间(仅弹窗可见时)-----
|
|
|
let timer = null
|
|
|
onMounted(() => {
|
|
|
timer = setInterval(() => {
|
|
|
if (props.visible) {
|
|
|
currentTime.value = new Date()
|
|
|
}
|
|
|
- }, 60000) // 每分钟更新一次
|
|
|
+ }, 60000)
|
|
|
})
|
|
|
-
|
|
|
-// 组件销毁时清除定时器
|
|
|
onUnmounted(() => {
|
|
|
- if (timer) {
|
|
|
- clearInterval(timer)
|
|
|
- }
|
|
|
+ if (timer) clearInterval(timer)
|
|
|
})
|
|
|
</script>
|
|
|
|
|
|
<style scoped lang="less">
|
|
|
-/* 弹窗内部容器 */
|
|
|
.time-picker-container {
|
|
|
height: 80vh;
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
}
|
|
|
-
|
|
|
.picker-header {
|
|
|
display: flex;
|
|
|
justify-content: space-between;
|
|
|
@@ -508,44 +391,35 @@ onUnmounted(() => {
|
|
|
border-bottom: 1rpx solid #f0f0f0;
|
|
|
flex-shrink: 0;
|
|
|
}
|
|
|
-
|
|
|
.picker-title {
|
|
|
font-size: 36rpx;
|
|
|
font-weight: 600;
|
|
|
color: #333;
|
|
|
}
|
|
|
-
|
|
|
.close-btn {
|
|
|
font-size: 50rpx;
|
|
|
color: #999;
|
|
|
line-height: 40rpx;
|
|
|
padding: 10rpx;
|
|
|
}
|
|
|
-
|
|
|
-/* 主要内容区域 */
|
|
|
.main-content {
|
|
|
flex: 1;
|
|
|
display: flex;
|
|
|
overflow: hidden;
|
|
|
}
|
|
|
-
|
|
|
-/* 左侧日期选择 */
|
|
|
.date-sidebar {
|
|
|
width: 220rpx;
|
|
|
background-color: #f8f8f8;
|
|
|
border-right: 1rpx solid #f0f0f0;
|
|
|
}
|
|
|
-
|
|
|
.date-item {
|
|
|
padding: 32rpx 24rpx;
|
|
|
border-bottom: 1rpx solid #f0f0f0;
|
|
|
}
|
|
|
-
|
|
|
.date-item.active {
|
|
|
background-color: #fff;
|
|
|
position: relative;
|
|
|
}
|
|
|
-
|
|
|
.date-item.active::before {
|
|
|
content: '';
|
|
|
position: absolute;
|
|
|
@@ -557,114 +431,96 @@ onUnmounted(() => {
|
|
|
background-color: #1B64F0;
|
|
|
border-radius: 0 4rpx 4rpx 0;
|
|
|
}
|
|
|
-
|
|
|
.date-item.today .date-name {
|
|
|
- color: #1B64F0;
|
|
|
+ // color: #1B64F0;
|
|
|
+ color: #333;
|
|
|
}
|
|
|
-
|
|
|
.date-item.active .date-content {
|
|
|
color: #1B64F0;
|
|
|
+ font-weight: 600;
|
|
|
}
|
|
|
-
|
|
|
.date-content {
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
align-items: center;
|
|
|
text-align: center;
|
|
|
}
|
|
|
-
|
|
|
.date-name {
|
|
|
font-size: 32rpx;
|
|
|
- font-weight: 600;
|
|
|
color: #333;
|
|
|
margin-bottom: 8rpx;
|
|
|
+ &.active{
|
|
|
+ color: #1B64F0;
|
|
|
+ }
|
|
|
}
|
|
|
-
|
|
|
.date-info {
|
|
|
font-size: 24rpx;
|
|
|
color: #666;
|
|
|
margin-bottom: 4rpx;
|
|
|
}
|
|
|
-
|
|
|
.date-week {
|
|
|
font-size: 22rpx;
|
|
|
color: #999;
|
|
|
}
|
|
|
-
|
|
|
-/* 右侧时间选择 */
|
|
|
.time-content {
|
|
|
flex: 1;
|
|
|
padding: 0 32rpx;
|
|
|
overflow-y: auto;
|
|
|
}
|
|
|
-
|
|
|
.recommend-section {
|
|
|
margin-top: 32rpx;
|
|
|
padding-bottom: 24rpx;
|
|
|
border-bottom: 1rpx solid #f0f0f0;
|
|
|
}
|
|
|
-
|
|
|
.recommend-header {
|
|
|
display: flex;
|
|
|
justify-content: space-between;
|
|
|
align-items: center;
|
|
|
margin-bottom: 20rpx;
|
|
|
}
|
|
|
-
|
|
|
.recommend-title {
|
|
|
font-size: 28rpx;
|
|
|
font-weight: 500;
|
|
|
color: #333;
|
|
|
}
|
|
|
-
|
|
|
-.recommend-desc {
|
|
|
- font-size: 24rpx;
|
|
|
- color: #1B64F0;
|
|
|
-}
|
|
|
-
|
|
|
+/* 推荐时段:未选中时灰色边框,无背景 */
|
|
|
.recommend-time {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
justify-content: space-between;
|
|
|
padding: 28rpx 24rpx;
|
|
|
- // background: linear-gradient(135deg, #fff2f0 0%, #ffe6e6 100%);
|
|
|
border-radius: 16rpx;
|
|
|
- border: 2rpx solid #1B64F0;
|
|
|
+ border: 2rpx solid #e8e8e8;
|
|
|
+ transition: all 0.2s;
|
|
|
}
|
|
|
-
|
|
|
-.time-value {
|
|
|
+.recommend-time .time-value {
|
|
|
font-size: 32rpx;
|
|
|
font-weight: 500;
|
|
|
- color: #1B64F0;
|
|
|
+ color: #333;
|
|
|
}
|
|
|
-
|
|
|
-.recommend-time .time-tag {
|
|
|
- font-size: 24rpx;
|
|
|
- padding: 8rpx 16rpx;
|
|
|
- background: #1B64F0;
|
|
|
- color: white;
|
|
|
- border-radius: 20rpx;
|
|
|
+/* 推荐时段:选中时蓝色背景 + 白色文字 */
|
|
|
+.recommend-time.active {
|
|
|
+ // background-color: #1B64F0;
|
|
|
+ border-color: #1B64F0;
|
|
|
+}
|
|
|
+.recommend-time.active .time-value {
|
|
|
+ // color: white;
|
|
|
}
|
|
|
-
|
|
|
-/* 时间段列表 */
|
|
|
.time-section {
|
|
|
margin-top: 32rpx;
|
|
|
margin-bottom: 32rpx;
|
|
|
}
|
|
|
-
|
|
|
.section-title {
|
|
|
font-size: 28rpx;
|
|
|
font-weight: 500;
|
|
|
color: #333;
|
|
|
margin-bottom: 24rpx;
|
|
|
}
|
|
|
-
|
|
|
.time-list {
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
gap: 20rpx;
|
|
|
}
|
|
|
-
|
|
|
.time-item {
|
|
|
padding: 28rpx 24rpx;
|
|
|
border: 2rpx solid #e8e8e8;
|
|
|
@@ -672,42 +528,33 @@ onUnmounted(() => {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
justify-content: space-between;
|
|
|
- position: relative;
|
|
|
transition: all 0.2s ease;
|
|
|
}
|
|
|
-
|
|
|
.time-item.active {
|
|
|
border-color: #1B64F0;
|
|
|
- // background: #fff2f0;
|
|
|
+ background-color: #fff;
|
|
|
}
|
|
|
-
|
|
|
.time-item.night {
|
|
|
border-color: #4a5cff;
|
|
|
}
|
|
|
-
|
|
|
.time-item:not(.disabled):active {
|
|
|
transform: scale(0.98);
|
|
|
opacity: 0.8;
|
|
|
}
|
|
|
-
|
|
|
.time-item.disabled {
|
|
|
opacity: 0.5;
|
|
|
}
|
|
|
-
|
|
|
.time-text {
|
|
|
font-size: 28rpx;
|
|
|
color: #333;
|
|
|
}
|
|
|
-
|
|
|
.time-item.active .time-text {
|
|
|
color: #1B64F0;
|
|
|
font-weight: 500;
|
|
|
}
|
|
|
-
|
|
|
.time-item.night .time-text {
|
|
|
color: #4a5cff;
|
|
|
}
|
|
|
-
|
|
|
.time-item .time-tag {
|
|
|
font-size: 20rpx;
|
|
|
padding: 4rpx 12rpx;
|
|
|
@@ -715,32 +562,25 @@ onUnmounted(() => {
|
|
|
background: #f5f5f5;
|
|
|
color: #666;
|
|
|
}
|
|
|
-
|
|
|
.time-item.active .time-tag {
|
|
|
background: #1B64F0;
|
|
|
color: white;
|
|
|
}
|
|
|
-
|
|
|
.time-item.night .time-tag {
|
|
|
background: #4a5cff;
|
|
|
color: white;
|
|
|
}
|
|
|
-
|
|
|
-/* 提示信息 */
|
|
|
.notice-section {
|
|
|
padding: 24rpx;
|
|
|
margin-bottom: 32rpx;
|
|
|
background: #f8f8f8;
|
|
|
border-radius: 12rpx;
|
|
|
}
|
|
|
-
|
|
|
.notice-text {
|
|
|
font-size: 24rpx;
|
|
|
color: #666;
|
|
|
line-height: 1.4;
|
|
|
}
|
|
|
-
|
|
|
-/* 操作按钮 */
|
|
|
.action-bar {
|
|
|
padding: 24rpx 32rpx;
|
|
|
padding-top: 0;
|
|
|
@@ -748,7 +588,6 @@ onUnmounted(() => {
|
|
|
border-top: 1rpx solid #f0f0f0;
|
|
|
flex-shrink: 0;
|
|
|
}
|
|
|
-
|
|
|
.confirm-btn {
|
|
|
width: 100%;
|
|
|
height: 88rpx;
|
|
|
@@ -763,17 +602,14 @@ onUnmounted(() => {
|
|
|
border: none;
|
|
|
transition: all 0.2s ease;
|
|
|
}
|
|
|
-
|
|
|
.confirm-btn.active {
|
|
|
background: #1B64F0;
|
|
|
color: white;
|
|
|
}
|
|
|
-
|
|
|
.confirm-btn.active:active {
|
|
|
opacity: 0.9;
|
|
|
transform: scale(0.99);
|
|
|
}
|
|
|
-
|
|
|
.loading {
|
|
|
text-align: center;
|
|
|
padding: 60rpx;
|