669 lines
17 KiB
Vue
669 lines
17 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-my-3">
|
|
<text class="v12-font-28 v12-dark-text v12-mr-3">请选择入住日期</text>
|
|
<text class="v12-font-28 v12-primary-text">{{ ` 共计${minBookingDays}天 ` }}</text>
|
|
</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 } from "@/api/sojoumOrder";
|
|
export default {
|
|
name: 'DateRangePicker',
|
|
|
|
props: {
|
|
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.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)
|
|
}
|
|
|
|
// 验证通过,更新日期并关闭弹窗
|
|
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)
|
|
},
|
|
|
|
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> |