REFACTOR:礼品功能重构

This commit is contained in:
LinCyJang
2025-10-24 01:31:50 +08:00
parent 705d64d238
commit d1bbea6e9b
8 changed files with 986 additions and 58 deletions
@@ -0,0 +1,569 @@
<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="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">
{{ 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),
'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: {} // 存储日期状态
}
},
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
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
},
selectDate(day) {
if (!day || this.isDisabled(day)) return
const selectedDate = this.getDateFromDay(day)
// 如果点击已选择的日期,则取消选择
if (this.tempStartDate && selectedDate.getTime() === this.tempStartDate.getTime()) {
this.tempStartDate = null
this.tempEndDate = null
this.activeCalendar = 'start'
return
}
if (this.tempEndDate && selectedDate.getTime() === this.tempEndDate.getTime()) {
this.tempEndDate = null
return
}
if (this.activeCalendar === 'start') {
if (this.tempEndDate && selectedDate > this.tempEndDate) {
// 如果选择的开始日期大于结束日期,清空结束日期
this.tempEndDate = null
}
this.tempStartDate = selectedDate
this.activeCalendar = 'end'
} else {
if (selectedDate < this.tempStartDate) {
// 如果选择的结束日期小于开始日期,将其设为开始日期
this.tempStartDate = selectedDate
} else {
this.tempEndDate = selectedDate
}
}
},
clearDates() {
this.tempStartDate = null
this.tempEndDate = null
this.activeCalendar = 'start'
},
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.$emit('input', [
this.startDate ? this.formatDate(this.startDate) : null,
this.endDate ? this.formatDate(this.endDate) : null
])
this.handleClose()
},
handleClose() {
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-between;
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;
}
}
&.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;
}
}
}
}
</style>