700 lines
18 KiB
Vue
700 lines
18 KiB
Vue
<template>
|
||
<view class="date-range-picker">
|
||
<u-popup :show="show" @close="handleClose" mode="center" round="16" closeable>
|
||
<view class="picker-container">
|
||
<view class="picker-header">
|
||
<text class="title">查看可预约日期</text>
|
||
</view>
|
||
<view class="v12-align-center v12-justify-between">
|
||
<view class="v12-my-3">
|
||
<text class="v12-font-24 v12-dark-text v12-mr-3">请选择入住日期</text>
|
||
<text class="v12-font-24 v12-primary-text">{{ ` 共计${minBookingDays}天 ` }}</text>
|
||
</view>
|
||
<view v-if="originDate" class="v12-font-24 v12-primary-text">
|
||
原定时间:{{ originDate }}
|
||
</view>
|
||
</view>
|
||
<view class="calendar">
|
||
<view class="calendar-header">
|
||
<view class="month-nav">
|
||
<text class="nav-btn" @click="changeMonth(-1)">〈</text>
|
||
<text class="month-text">{{ currentYear }}年{{ currentMonth + 1 }}月</text>
|
||
<text class="nav-btn" @click="changeMonth(1)">〉</text>
|
||
</view>
|
||
<view class="weekdays">
|
||
<text v-for="day in ['日', '一', '二', '三', '四', '五', '六']" :key="day" class="weekday v12-dark-text">
|
||
{{ day }}
|
||
</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="days">
|
||
<view
|
||
v-for="(day, index) in calendarDays"
|
||
:key="index"
|
||
class="day"
|
||
:class="{
|
||
'empty': !day,
|
||
'selected': isSelected(day),
|
||
'in-range': isInRange(day),
|
||
'start-date': isStartDate(day),
|
||
'end-date': isEndDate(day),
|
||
'selecting-start': isSelectingStart(day),
|
||
'disabled': isDisabled(day),
|
||
'booked': getDateStatus(day) === 1,
|
||
'unavailable': getDateStatus(day) === 2,
|
||
'available': getDateStatus(day) === 0
|
||
}"
|
||
@click="selectDate(day)"
|
||
>
|
||
<text>{{ day || '' }}</text>
|
||
<view class="v12-font-20" v-if="day && getDateStatus(day) !== null">{{ getDateStatus(day) }}</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="picker-footer">
|
||
<button class="btn btn-confirm" @click="confirmSelect">立即预订</button>
|
||
</view>
|
||
</view>
|
||
</u-popup>
|
||
</view>
|
||
</template>
|
||
|
||
<script>
|
||
import { sojoumOrderCalendar, sojoumOrderCheckDate } from "@/api/sojoumOrder";
|
||
export default {
|
||
name: 'DateRangePicker',
|
||
|
||
props: {
|
||
originDate: {
|
||
type: String,
|
||
default: ''
|
||
},
|
||
show: {
|
||
type: Boolean,
|
||
default: false
|
||
},
|
||
value: {
|
||
type: Array,
|
||
default: () => [null, null]
|
||
},
|
||
minDate: {
|
||
type: [String, Date],
|
||
default: null
|
||
},
|
||
maxDate: {
|
||
type: [String, Date],
|
||
default: null
|
||
},
|
||
format: {
|
||
type: String,
|
||
default: 'YYYY-MM-DD'
|
||
},
|
||
travelGroupProductId: {
|
||
type: String,
|
||
default: ''
|
||
},
|
||
minBookingDays: {
|
||
type: Number,
|
||
default: 0
|
||
},
|
||
},
|
||
|
||
data() {
|
||
return {
|
||
currentYear: new Date().getFullYear(),
|
||
currentMonth: new Date().getMonth(),
|
||
startDate: null,
|
||
endDate: null,
|
||
activeCalendar: 'start', // 'start' or 'end'
|
||
tempStartDate: null,
|
||
tempEndDate: null,
|
||
dateStatusMap: {}, // 存储日期状态
|
||
selectionState: 'initial' // 'initial' | 'selecting' | 'completed'
|
||
}
|
||
},
|
||
|
||
computed: {
|
||
calendarDays() {
|
||
const days = []
|
||
const firstDay = new Date(this.currentYear, this.currentMonth, 1)
|
||
const lastDay = new Date(this.currentYear, this.currentMonth + 1, 0)
|
||
|
||
// 填充月初空白天数
|
||
for (let i = 0; i < firstDay.getDay(); i++) {
|
||
days.push(null)
|
||
}
|
||
|
||
// 填充当月天数
|
||
for (let i = 1; i <= lastDay.getDate(); i++) {
|
||
days.push(i)
|
||
}
|
||
|
||
// 填充月末空白天数
|
||
const remainingDays = 42 - days.length // 保持6行固定高度
|
||
for (let i = 0; i < remainingDays; i++) {
|
||
days.push(null)
|
||
}
|
||
|
||
return days
|
||
}
|
||
},
|
||
|
||
watch: {
|
||
show(newVal) {
|
||
if (newVal) {
|
||
this.initDates()
|
||
this.getDays()
|
||
}
|
||
},
|
||
currentMonth() {
|
||
this.getDays()
|
||
},
|
||
currentYear() {
|
||
this.getDays()
|
||
},
|
||
value: {
|
||
handler(newVal) {
|
||
if (newVal && newVal.length === 2) {
|
||
this.startDate = newVal[0] ? new Date(newVal[0]) : null
|
||
this.endDate = newVal[1] ? new Date(newVal[1]) : null
|
||
this.tempStartDate = this.startDate
|
||
this.tempEndDate = this.endDate
|
||
}
|
||
},
|
||
immediate: true
|
||
}
|
||
},
|
||
|
||
methods: {
|
||
getDays(){
|
||
uni.showLoading({
|
||
title: '加载中...',
|
||
mask: true,
|
||
})
|
||
sojoumOrderCalendar({
|
||
travelGroupProductId: this.travelGroupProductId,
|
||
month: `${this.currentYear}-${String(this.currentMonth + 1).padStart(2, '0')}`
|
||
}).then(res => {
|
||
if (res.data) {
|
||
// 更新日期状态映射
|
||
this.dateStatusMap = {}
|
||
res.data.forEach(day => {
|
||
// inventoryStatus int 状态 (0:可预约, 1:已被预约, 2:不可预约)
|
||
this.dateStatusMap[day.date] = day.inventoryStatus === 0 ? '可预约' : day.inventoryStatus === 1 ? '已被预约' : '不可预约'
|
||
})
|
||
}
|
||
}).finally(() => {
|
||
uni.hideLoading()
|
||
})
|
||
},
|
||
|
||
getDateStatus(day) {
|
||
if (!day) return null
|
||
const date = this.formatDate(this.getDateFromDay(day))
|
||
return this.dateStatusMap[date]
|
||
},
|
||
|
||
isDisabled(day) {
|
||
|
||
if (!day) return true
|
||
const date = this.getDateFromDay(day)
|
||
// 禁用过去的日期(今天之前的日期)
|
||
const today = new Date()
|
||
today.setHours(0, 0, 0, 0)
|
||
if (date < today) return true
|
||
|
||
// 检查最小和最大日期限制
|
||
if (this.minDate && date < new Date(this.minDate)) return true
|
||
if (this.maxDate && date > new Date(this.maxDate)) return true
|
||
|
||
// 检查预约状态
|
||
const status = this.getDateStatus(day)
|
||
// 只允许选择可预约的日期(状态为"可预约"),其他状态都禁用
|
||
|
||
return status !== '可预约'
|
||
},
|
||
initDates() {
|
||
this.tempStartDate = this.startDate
|
||
this.tempEndDate = this.endDate
|
||
|
||
// 根据当前选择状态设置selectionState
|
||
if (this.tempStartDate && this.tempEndDate) {
|
||
this.selectionState = 'completed'
|
||
this.activeCalendar = 'start'
|
||
} else if (this.tempStartDate) {
|
||
this.selectionState = 'selecting'
|
||
this.activeCalendar = 'end'
|
||
} else {
|
||
this.selectionState = 'initial'
|
||
this.activeCalendar = 'start'
|
||
}
|
||
|
||
if (this.startDate) {
|
||
this.currentYear = this.startDate.getFullYear()
|
||
this.currentMonth = this.startDate.getMonth()
|
||
} else {
|
||
const now = new Date()
|
||
this.currentYear = now.getFullYear()
|
||
this.currentMonth = now.getMonth()
|
||
}
|
||
},
|
||
|
||
changeMonth(delta) {
|
||
let newMonth = this.currentMonth + delta
|
||
if (newMonth < 0) {
|
||
this.currentYear--
|
||
newMonth = 11
|
||
} else if (newMonth > 11) {
|
||
this.currentYear++
|
||
newMonth = 0
|
||
}
|
||
this.currentMonth = newMonth
|
||
},
|
||
|
||
formatDate(date) {
|
||
if (!date) return ''
|
||
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}`
|
||
},
|
||
|
||
getDateFromDay(day) {
|
||
return day ? new Date(this.currentYear, this.currentMonth, day) : null
|
||
},
|
||
|
||
// isDisabled(day) {
|
||
// if (!day) return true
|
||
// const date = this.getDateFromDay(day)
|
||
|
||
// // 禁用过去的日期(今天之前的日期)
|
||
// const today = new Date()
|
||
// today.setHours(0, 0, 0, 0)
|
||
// if (date < today) return true
|
||
|
||
// // 检查最小和最大日期限制
|
||
// if (this.minDate && date < new Date(this.minDate)) return true
|
||
// if (this.maxDate && date > new Date(this.maxDate)) return true
|
||
|
||
// return false
|
||
// },
|
||
|
||
isSelected(day) {
|
||
if (!day) return false
|
||
const date = this.getDateFromDay(day)
|
||
return this.isStartDate(day) || this.isEndDate(day)
|
||
},
|
||
|
||
isStartDate(day) {
|
||
if (!day || !this.tempStartDate) return false
|
||
const date = this.getDateFromDay(day)
|
||
return date.getTime() === this.tempStartDate.getTime()
|
||
},
|
||
|
||
isEndDate(day) {
|
||
if (!day || !this.tempEndDate) return false
|
||
const date = this.getDateFromDay(day)
|
||
return date.getTime() === this.tempEndDate.getTime()
|
||
},
|
||
|
||
isInRange(day) {
|
||
if (!day || !this.tempStartDate || !this.tempEndDate) return false
|
||
const date = this.getDateFromDay(day)
|
||
return date > this.tempStartDate && date < this.tempEndDate
|
||
},
|
||
|
||
isSelectingStart(day) {
|
||
if (!day || !this.tempStartDate || this.tempEndDate) return false
|
||
// 只有在选择状态且只有开始日期时才显示特殊样式
|
||
if (this.selectionState !== 'selecting') return false
|
||
const date = this.getDateFromDay(day)
|
||
return date.getTime() === this.tempStartDate.getTime()
|
||
},
|
||
|
||
selectDate(day) {
|
||
if (!day || this.isDisabled(day)) return
|
||
|
||
const selectedDate = this.getDateFromDay(day)
|
||
|
||
// 如果设置了最小预订天数,则自动计算结束日期
|
||
if (this.minBookingDays > 0) {
|
||
this.tempStartDate = selectedDate
|
||
const endDate = new Date(selectedDate)
|
||
endDate.setDate(selectedDate.getDate() + this.minBookingDays - 1)
|
||
this.tempEndDate = endDate
|
||
this.selectionState = 'completed'
|
||
return
|
||
}
|
||
|
||
// 如果已有完整的日期区间(开始日期和结束日期都存在),重置选择
|
||
if (this.tempStartDate && this.tempEndDate && this.selectionState === 'completed') {
|
||
// 重置选择状态,将点击的日期作为新的开始日期
|
||
this.tempStartDate = selectedDate
|
||
this.tempEndDate = null
|
||
this.activeCalendar = 'end'
|
||
this.selectionState = 'selecting'
|
||
return
|
||
}
|
||
|
||
// 如果点击已选择的开始日期,则取消选择
|
||
if (this.tempStartDate && selectedDate.getTime() === this.tempStartDate.getTime()) {
|
||
this.tempStartDate = null
|
||
this.tempEndDate = null
|
||
this.activeCalendar = 'start'
|
||
this.selectionState = 'initial'
|
||
return
|
||
}
|
||
|
||
// 如果点击已选择的结束日期,则只清除结束日期
|
||
if (this.tempEndDate && selectedDate.getTime() === this.tempEndDate.getTime()) {
|
||
this.tempEndDate = null
|
||
this.activeCalendar = 'end'
|
||
this.selectionState = 'selecting'
|
||
return
|
||
}
|
||
|
||
if (this.activeCalendar === 'start' || this.selectionState === 'initial') {
|
||
// 选择开始日期
|
||
this.tempStartDate = selectedDate
|
||
this.tempEndDate = null
|
||
this.activeCalendar = 'end'
|
||
this.selectionState = 'selecting'
|
||
} else if (this.activeCalendar === 'end') {
|
||
if (selectedDate < this.tempStartDate) {
|
||
// 如果选择的结束日期小于开始日期,将其设为开始日期
|
||
this.tempStartDate = selectedDate
|
||
this.tempEndDate = null
|
||
this.selectionState = 'selecting'
|
||
} else {
|
||
// 设置结束日期,完成选择
|
||
this.tempEndDate = selectedDate
|
||
this.selectionState = 'completed'
|
||
}
|
||
}
|
||
},
|
||
|
||
clearDates() {
|
||
this.tempStartDate = null
|
||
this.tempEndDate = null
|
||
this.activeCalendar = 'start'
|
||
this.selectionState = 'initial'
|
||
},
|
||
|
||
confirmSelect() {
|
||
// 验证选择的日期数量
|
||
if (!this.tempStartDate || !this.tempEndDate) {
|
||
uni.showToast({
|
||
title: '请选择起始和结束日期',
|
||
icon: 'none'
|
||
})
|
||
return
|
||
}
|
||
|
||
// 计算选择的天数
|
||
const daysDiff = Math.floor((this.tempEndDate - this.tempStartDate) / (1000 * 60 * 60 * 24)) + 1
|
||
|
||
// 必须要选够天数
|
||
if (daysDiff !== this.minBookingDays) {
|
||
uni.showToast({
|
||
title: `此套餐预约需连续选择${this.minBookingDays}天`,
|
||
icon: 'none'
|
||
})
|
||
return
|
||
}
|
||
|
||
// 验证选择的日期范围内是否包含禁用日期
|
||
// const currentDate = new Date(this.tempStartDate)
|
||
// while (currentDate <= this.tempEndDate) {
|
||
// const day = currentDate.getDate()
|
||
// if (this.isDisabled(day)) {
|
||
// uni.showToast({
|
||
// title: '所选日期范围包含不可预约日期,请重新选择',
|
||
// icon: 'none'
|
||
// })
|
||
// return
|
||
// }
|
||
// currentDate.setDate(currentDate.getDate() + 1)
|
||
// }
|
||
sojoumOrderCheckDate({
|
||
travelGroupProductId: this.travelGroupProductId,
|
||
orderStartDate: this.formatDate(this.tempStartDate),
|
||
orderEndDate: this.formatDate(this.tempEndDate)
|
||
}).then(res => {
|
||
if (res.status === 200) {
|
||
// 验证通过,更新日期并关闭弹窗
|
||
this.startDate = this.tempStartDate
|
||
this.endDate = this.tempEndDate
|
||
this.selectionState = 'completed'
|
||
this.$emit('input', [
|
||
this.startDate ? this.formatDate(this.startDate) : null,
|
||
this.endDate ? this.formatDate(this.endDate) : null
|
||
])
|
||
this.handleClose(false)
|
||
} else {
|
||
uni.showToast({
|
||
title: res.msg || '所选日期范围包含不可预约日期,请重新选择',
|
||
icon: 'none'
|
||
})
|
||
}
|
||
})
|
||
},
|
||
|
||
handleClose(shouldClear = false) {
|
||
if (shouldClear) {
|
||
this.clearDates()
|
||
this.startDate = null
|
||
this.endDate = null
|
||
this.$emit('input', [null, null])
|
||
} else {
|
||
// 恢复到原始状态
|
||
this.tempStartDate = this.startDate
|
||
this.tempEndDate = this.endDate
|
||
if (this.startDate && this.endDate) {
|
||
this.selectionState = 'completed'
|
||
} else if (this.startDate) {
|
||
this.selectionState = 'selecting'
|
||
} else {
|
||
this.selectionState = 'initial'
|
||
}
|
||
}
|
||
this.$emit('update:show', false)
|
||
this.$emit('close')
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.date-range-picker {
|
||
.picker-container {
|
||
width: 690rpx;
|
||
background: #FFFFFF;
|
||
border-radius: 24rpx;
|
||
padding: 30rpx;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.picker-header {
|
||
text-align: center;
|
||
margin-bottom: 30rpx;
|
||
|
||
.title {
|
||
font-size: 32rpx;
|
||
font-weight: bold;
|
||
color: #333333;
|
||
}
|
||
}
|
||
|
||
.date-display {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-bottom: 30rpx;
|
||
padding: 20rpx;
|
||
background: #F8F8F8;
|
||
border-radius: 12rpx;
|
||
|
||
.date-input {
|
||
flex: 1;
|
||
|
||
.label {
|
||
font-size: 24rpx;
|
||
color: #999999;
|
||
margin-bottom: 8rpx;
|
||
display: block;
|
||
}
|
||
|
||
.value {
|
||
font-size: 28rpx;
|
||
color: #333333;
|
||
|
||
&.placeholder {
|
||
color: #999999;
|
||
}
|
||
}
|
||
}
|
||
|
||
.separator {
|
||
margin: 0 20rpx;
|
||
color: #999999;
|
||
font-size: 28rpx;
|
||
}
|
||
}
|
||
|
||
.calendar {
|
||
.calendar-header {
|
||
margin-bottom: 20rpx;
|
||
|
||
.month-nav {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-evenly;
|
||
margin-bottom: 20rpx;
|
||
padding: 0 20rpx;
|
||
|
||
.month-text {
|
||
font-size: 28rpx;
|
||
color: #333333;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.nav-btn {
|
||
padding: 10rpx 20rpx;
|
||
color: #666666;
|
||
font-size: 32rpx;
|
||
}
|
||
}
|
||
|
||
.weekdays {
|
||
display: flex;
|
||
justify-content: space-around;
|
||
|
||
.weekday {
|
||
width: 14.28%;
|
||
text-align: center;
|
||
font-size: 24rpx;
|
||
color: #999999;
|
||
}
|
||
}
|
||
}
|
||
|
||
.days {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
|
||
.day {
|
||
width: 14.28%;
|
||
height: 80rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 28rpx;
|
||
color: #333333;
|
||
position: relative;
|
||
flex-direction: column;
|
||
|
||
&:not(.selected):not(.disabled) {
|
||
color: #4B97EB;
|
||
}
|
||
|
||
.status-dot {
|
||
width: 8rpx;
|
||
height: 8rpx;
|
||
border-radius: 50%;
|
||
margin-top: 4rpx;
|
||
}
|
||
|
||
&.empty {
|
||
pointer-events: none;
|
||
}
|
||
|
||
&.disabled {
|
||
color: #CCCCCC;
|
||
pointer-events: none;
|
||
}
|
||
|
||
&.selected {
|
||
background: #C52733;
|
||
color: #FFFFFF;
|
||
.status-dot {
|
||
background: #FFFFFF;
|
||
}
|
||
}
|
||
|
||
&.selecting-start {
|
||
background: #4B97EB;
|
||
color: #FFFFFF;
|
||
border-radius: 8rpx;
|
||
position: relative;
|
||
|
||
&::after {
|
||
content: '';
|
||
position: absolute;
|
||
top: -2rpx;
|
||
left: -2rpx;
|
||
right: -2rpx;
|
||
bottom: -2rpx;
|
||
border: 2rpx solid #4B97EB;
|
||
border-radius: 10rpx;
|
||
animation: pulse 1.5s infinite;
|
||
}
|
||
|
||
.status-dot {
|
||
background: #FFFFFF;
|
||
}
|
||
}
|
||
|
||
&.in-range {
|
||
background: rgba(197, 39, 51, 0.1);
|
||
}
|
||
|
||
&.start-date {
|
||
border-top-left-radius: 8rpx;
|
||
border-bottom-left-radius: 8rpx;
|
||
}
|
||
|
||
&.end-date {
|
||
border-top-right-radius: 8rpx;
|
||
border-bottom-right-radius: 8rpx;
|
||
}
|
||
|
||
&.available .status-dot {
|
||
background: #4CAF50;
|
||
}
|
||
|
||
&.booked .status-dot {
|
||
background: #FF9800;
|
||
}
|
||
|
||
&.unavailable .status-dot {
|
||
background: #F44336;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.picker-footer {
|
||
margin-top: 30rpx;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
padding: 0 20rpx;
|
||
|
||
.btn {
|
||
width: 100%;
|
||
height: 80rpx;
|
||
border-radius: 40rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 28rpx;
|
||
|
||
&.btn-clear {
|
||
background: #F8F8F8;
|
||
color: #999999;
|
||
}
|
||
|
||
&.btn-confirm {
|
||
background: #C52733;
|
||
color: #FFFFFF;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
@keyframes pulse {
|
||
0% {
|
||
opacity: 1;
|
||
transform: scale(1);
|
||
}
|
||
50% {
|
||
opacity: 0.7;
|
||
transform: scale(1.05);
|
||
}
|
||
100% {
|
||
opacity: 1;
|
||
transform: scale(1);
|
||
}
|
||
}
|
||
</style> |