mahjong_group/pages/appointment/appointment-page.vue
2025-12-07 21:17:34 +08:00

1525 lines
44 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<com-page-container-base ref="_containerBase">
<view class="content column">
<view class="header-row">
<image src="/static/back.png" class="back-icon" @click="goBack" mode="aspectFit"></image>
<text class="page-title">发起预约</text>
<view class="spacer-40"></view>
</view>
<view class="column" style="overflow-y: auto;">
<card-container marginTop="30rpx">
<label-field label="日期">
<view class="input-wrapper" style="padding: 15rpx 20rpx;" @click="openDatePicker">
<text>{{ getDateDisplayText() }}</text>
</view>
</label-field>
<view :style="{ height: lineHeight }"></view>
<label-field label="时间段">
<view v-if="roomDetailLoading" class="center" style="padding: 30rpx 0;">
<text style="color: #999; font-size: 24rpx;">加载时段信息中...</text>
</view>
<uni-data-select v-else v-model="selectedTimeSlot" placeholder="请选择时间段"
:localdata="timeSlotOptions" @change="onTimeSlotChange"></uni-data-select>
<view v-if="!roomDetailLoading && timeSlotOptions.length === 0" style="padding: 20rpx 0;">
<text style="color: #FF0000; font-size: 24rpx;">当前日期该房间暂无可预约时段</text>
</view>
</label-field>
<view :style="{ height: lineHeight }"></view>
<label-field label="最晚到店时间">
<view class="input-wrapper" style="padding: 15rpx 20rpx;" @click="openLatestDateTimePicker">
<text>{{ getLatestDateTimeDisplayText() }}</text>
</view>
</label-field>
</card-container>
<card-container marginTop="30rpx">
<label-field label="组局名称">
<view class="input-wrapper">
<up-input placeholder="请输入内容" border="surround" v-model="reservationInfo.title"></up-input>
</view>
</label-field>
<view :style="{ height: lineHeight }"></view>
<label-field label="人数">
<uni-data-select v-model="reservationInfo.player_count" :placeholder="peopleText"
:localdata="peopleRange"></uni-data-select>
</label-field>
<view :style="{ height: lineHeight }"></view>
<label-field label="玩法类型">
<uni-data-select v-model="reservationInfo.game_type" placeholder="请选择玩法类型"
:localdata="gameTypeRange" @change="gameTypeRangeChange"></uni-data-select>
</label-field>
<view :style="{ height: lineHeight }"></view>
<label-field label="具体规则">
<uni-data-select v-model="reservationInfo.game_rule" :placeholder="gameRuleText"
:localdata="gameRuleRange" @change="gameRuleRangeChange"></uni-data-select>
</label-field>
<view :style="{ height: lineHeight }"></view>
<label-field label="其他补充">
<view class="input-wrapper">
<up-input placeholder="请输入其他补充内容" v-model="reservationInfo.extra_info"></up-input>
</view>
</label-field>
</card-container>
<card-container marginTop="30rpx">
<label-slect-field label="是否禁烟">
<view style="height: 61rpx;display: flex;justify-content: left;align-items: center;">
<com-appointment-radio-select :options="smokingOptions"
v-model="reservationInfo.is_smoking" />
</view>
</label-slect-field>
<view :style="{ height: lineHeight }"></view>
<label-slect-field label="性别">
<view style="height: 61rpx;display: flex;justify-content: left;align-items: center;">
<com-appointment-radio-select :options="genderOptions"
v-model="reservationInfo.gender_limit" />
</view>
</label-slect-field>
<view :style="{ height: lineHeight }"></view>
<label-slect-field label="年龄范围">
<view @click="openAgePicker"
style="height: 61rpx;display: flex;justify-content: left;align-items: center;"
class="clickable-row">
{{ getAgeRangeText() }}
</view>
</label-slect-field>
<view :style="{ height: lineHeight }"></view>
<label-slect-field label="信誉">
<view class="flex-center-row" style="height: 61rpx;">
<text class="inline-label" style="margin-top: 12rpx;">大于等于</text>
<view class="counter-container">
<view @click="decrement" :disabled="currentValue <= 0">
<uni-icons type="minus" size="24" color="#00AC4E" />
</view>
<view class="counter-value">
{{ currentValue.toFixed(1) }}
</view>
<view @click="increment" :disabled="currentValue >= 5">
<uni-icons type="plus" size="24" color="#00AC4E" />
</view>
</view>
</view>
</label-slect-field>
</card-container>
<card-container marginTop="30rpx">
<label-slect-field label="鸽子费">
<com-appointment-radio-select :options="depositOptions" v-model="reservationInfo.deposit_fee" />
</label-slect-field>
<view v-if="reservationInfo.deposit_fee === -1" :style="{ height: lineHeight }"></view>
<label-slect-field v-if="reservationInfo.deposit_fee === -1" label="金额">
<view class="input-wrapper">
<up-input type="number" placeholder="请输入0-50" v-model="customDeposit" @input="onCustomDepositInput"></up-input>
</view>
</label-slect-field>
<view :style="{ height: lineHeight }"></view>
<text class="note-text">鸽子费(保证金),参与人需缴纳鸽子费。若有参与者在预约后没有赴约,其鸽子费由在场的所有人平分。组局成功或失败后鸽子费将全额返还。</text>
</card-container>
<view class="action-row">
<view class="center reset-button" @click="resetForm">
<text style="margin: 20rpx;">重置</text>
</view>
<view class="center submit-button action-submit" @click="submitReservation">
<text style="margin: 20rpx; color: white;">发起预约</text>
</view>
</view>
<view class="center note-container" @click="tipsShow">
<!-- <text class="muted-text">组局成功后,发起者可通过店员领取线下红包</text> -->
</view>
</view>
</view>
<up-datetime-picker :show="datePickerVisible" v-model="datePickerValue" mode="date"
:minDate="datePickerMinDate" :maxDate="datePickerMaxDate" title="选择日期" @confirm="onDatePickerConfirm"
@cancel="() => datePickerVisible = false" @close="() => datePickerVisible = false"></up-datetime-picker>
<up-datetime-picker :show="latestDateTimePickerVisible" v-model="latestDateTimePickerValue" mode="time"
:minHour="latestDateTimeMinHour" :maxHour="latestDateTimeMaxHour"
:minMinute="latestDateTimeMinMinute" :maxMinute="latestDateTimeMaxMinute"
title="选择最晚到店时间" @confirm="onLatestDateTimePickerConfirm"
@cancel="() => latestDateTimePickerVisible = false" @close="() => latestDateTimePickerVisible = false"></up-datetime-picker>
<up-picker title="年龄范围选择" :show="agePickerVisible" :columns="agePickerColumns" :keyName="'text'"
:defaultIndex="agePickerDefaultIndex" @confirm="onAgePickerConfirm" @cancel="() => agePickerVisible = false"
@close="() => agePickerVisible = false"></up-picker>
<uni-popup ref="submitPopupRef" type="center">
<view style="width: 90vw;height:300rpx;background-color: #fff;border-radius: 20rpx;">
<view>
<view style="height: 80rpx;"></view>
<view style="font-size: 32rpx;font-weight: 500;color: #000;text-align: center;">发起预约成功!</view>
<view style="height:100rpx;"></view>
<view style="display: flex;width: 100%;height:100rpx;">
<button @click.stop open-type="share"
style=" background-color:#00AC4E;color: #fff;width: 50%;height: 100%; background-color:#00AC4E;"
class="center evaluate-btn" :data-item="reservation">
<text class="evaluate-btn-text">分享给好友</text>
</button>
<view
style="width: 50%;height: 100%;background-color: #f7f7f7;display: flex;align-items: center;justify-content: center;"
@click="submitPopupRef.close();">
<text>关闭</text>
</view>
</view>
</view>
</view>
</uni-popup>
</com-page-container-base>
</template>
<script setup>
import {
ref,
watch,
nextTick
} from 'vue';
import {
getConfigData,
getSubscribeMessage
} from '@/common/server/config'
import {
usePay
} from '@/common/server/interface/user'
import {
requestSubscribeMessage,
requestPayment
} from '@/common/utils'
import {
isLogin
} from '@/common/server/user'
import {
getDetail
} from '@/common/server/index'
import LabelField from '@/components/com/appointment/label-field.vue'
import LabelSlectField from '@/components/com/appointment/label-slect-field.vue'
import CardContainer from '@/components/com/appointment/card-container.vue'
import ComAppointmentRadioSelect from '@/components/com/appointment/radio-select.vue'
import {
addSQReservation,
cancelReservation,
canCreateSQReservation,
getRoomDetail
} from '@/common/server/interface/sq'
const submitPopupRef = ref(null)
// 年龄选择器状态
const agePickerVisible = ref(false)
const agePickerColumns = ref([
[],
[]
])
const agePickerDefaultIndex = ref([0, 0])
const reservationData = ref(null)
const lineHeight = ref("15rpx")
// 房间页跳转相关
const selectedDate = ref(null) // 选中的日期时间戳(秒级)
const selectedTimeSlot = ref(null) // 选中的时段类型 0-3null表示未选择
const roomDetail = ref(null) // 房间详情数据
const roomDetailLoading = ref(false) // 房间详情加载状态
const timeSlotOptions = ref([]) // 时间段选项,从房间详情动态生成
// 日期选择器状态
const datePickerVisible = ref(false)
const datePickerValue = ref(Date.now())
const datePickerMinDate = ref(Date.now()) // 最小日期为今天
// 最大日期为未来7天今天+未来6天
const datePickerMaxDate = ref(new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).getTime())
// 最晚到店时间选择器状态
const latestDateTimePickerVisible = ref(false)
const latestDateTimePickerValue = ref('00:00') // 时间选择器值HH:MM格式字符串
const latestDateTime = ref(null) // 最晚到店时间(秒级时间戳)
// 时间选择器范围限制
const latestDateTimeMinHour = ref(0)
const latestDateTimeMaxHour = ref(23)
const latestDateTimeMinMinute = ref(0)
const latestDateTimeMaxMinute = ref(59)
//提交表单数据
const reservationInfo = ref({
room_id: 0, //房间id 非空
room_name: '请选择房间', //房间名称
start_time: 0, //开始时间 时间戳 非空
end_time: 0, //结束时间 时间戳 非空
max_age: 0, //最大年龄 默认0
min_age: 0, //最小年龄 非空
title: '', //组局名称 非空
extra_info: '', //其它说明 可为空
game_rule: '', //具体规则 非空
game_type: '', //玩法类型 非空
gender_limit: 0, //性别限制
is_smoking: 2, //是否禁烟
credit_limit: 0, //信誉限制
deposit_fee: 0, //鸽子费
player_count: 0, //人数
});
const getRoomPickerName = () => {
return reservationInfo.value.room_name;
}
/**
* 获取日期显示文本
*/
const getDateDisplayText = () => {
if (!selectedDate.value) {
return '请选择日期';
}
const date = new Date(selectedDate.value * 1000);
const today = new Date();
today.setHours(0, 0, 0, 0);
const targetDate = new Date(date.getFullYear(), date.getMonth(), date.getDate());
const tomorrow = new Date(today);
tomorrow.setDate(tomorrow.getDate() + 1);
const dayAfterTomorrow = new Date(today);
dayAfterTomorrow.setDate(dayAfterTomorrow.getDate() + 2);
let dateText = '';
if (targetDate.getTime() === today.getTime()) {
dateText = '今天';
} else if (targetDate.getTime() === tomorrow.getTime()) {
dateText = '明天';
} else if (targetDate.getTime() === dayAfterTomorrow.getTime()) {
dateText = '后天';
} else {
const month = date.getMonth() + 1;
const day = date.getDate();
dateText = `${month}月${day}日`;
}
// 获取星期
const weekdays = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
const weekday = weekdays[date.getDay()];
return `${dateText} ${weekday}`;
}
/**
* 打开日期选择器
*/
const openDatePicker = () => {
if (selectedDate.value) {
datePickerValue.value = selectedDate.value * 1000; // 转换为毫秒
} else {
datePickerValue.value = Date.now();
}
datePickerVisible.value = true;
}
/**
* 日期选择器确认
*/
const onDatePickerConfirm = async (e) => {
console.log('日期选择器确认事件:', e);
console.log('datePickerValue.value:', datePickerValue.value);
// up-datetime-picker 的 confirm 事件返回的是对象 { value: 毫秒级时间戳, mode: 'date' }
// 优先使用 v-model 绑定的 datePickerValue.value因为它会在用户选择后自动更新
let timestampMs = datePickerValue.value;
// 如果 v-model 的值无效,尝试从事件参数获取
if (!timestampMs || isNaN(timestampMs) || timestampMs <= 0) {
if (e && typeof e === 'object' && e.value !== undefined) {
timestampMs = e.value;
} else if (typeof e === 'number' && !isNaN(e) && e > 0) {
timestampMs = e;
}
}
console.log('提取的时间戳(毫秒):', timestampMs);
// 确保是数字类型且有效
if (typeof timestampMs !== 'number' || isNaN(timestampMs) || timestampMs <= 0) {
console.error('日期选择器返回的时间戳无效:', e, 'datePickerValue:', datePickerValue.value);
uni.showToast({
title: '日期选择失败,请重试',
icon: 'none'
});
datePickerVisible.value = false;
return;
}
// 将日期转换为当天的 0 点(只取日期,不取时间)
const date = new Date(timestampMs);
if (isNaN(date.getTime())) {
console.error('日期转换失败:', timestampMs);
uni.showToast({
title: '日期格式错误',
icon: 'none'
});
datePickerVisible.value = false;
return;
}
const year = date.getFullYear();
const month = date.getMonth();
const day = date.getDate();
const dateAtMidnight = new Date(year, month, day, 0, 0, 0);
// 转换为秒级时间戳
const selectedTimestamp = Math.floor(dateAtMidnight.getTime() / 1000);
console.log('转换后的秒级时间戳:', selectedTimestamp);
// 验证转换后的时间戳是否有效
if (isNaN(selectedTimestamp) || selectedTimestamp <= 0) {
console.error('时间戳转换失败:', dateAtMidnight, selectedTimestamp);
uni.showToast({
title: '日期处理失败',
icon: 'none'
});
datePickerVisible.value = false;
return;
}
// 如果是同一天,不需要重新加载
if (selectedDate.value && selectedDate.value === selectedTimestamp) {
datePickerVisible.value = false;
return;
}
// 更新选中的日期
selectedDate.value = selectedTimestamp;
// 重置时段选择
selectedTimeSlot.value = null;
reservationInfo.value.start_time = 0;
reservationInfo.value.end_time = 0;
// 重置最晚到店时间
latestDateTime.value = null;
// 重新加载房间详情
if (reservationInfo.value.room_id) {
console.log('重新加载房间详情, roomId:', reservationInfo.value.room_id, 'date:', selectedTimestamp);
await loadRoomDetailForReservation(reservationInfo.value.room_id, selectedTimestamp);
}
datePickerVisible.value = false;
}
/**
* 获取最晚到店时间显示文本
*/
const getLatestDateTimeDisplayText = () => {
if (!latestDateTime.value) {
return '请选择最晚到店时间';
}
const date = new Date(latestDateTime.value * 1000);
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 minutes = String(date.getMinutes()).padStart(2, '0');
return `${year}-${month}-${day} ${hours}:${minutes}`;
}
/**
* 获取时间段的时间范围限制
*/
const getTimeSlotRange = () => {
if (!selectedTimeSlot.value || selectedTimeSlot.value === null || selectedTimeSlot.value === undefined) {
return { minHour: 0, maxHour: 23, minMinute: 0, maxMinute: 59 };
}
let minHour = 0, maxHour = 23, minMinute = 0, maxMinute = 59;
switch (selectedTimeSlot.value) {
case 0: // 凌晨 00:00-05:59
minHour = 0;
maxHour = 5;
minMinute = 0;
maxMinute = 59;
break;
case 1: // 上午 06:00-11:59
minHour = 6;
maxHour = 11;
minMinute = 0;
maxMinute = 59;
break;
case 2: // 下午 12:00-17:59
minHour = 12;
maxHour = 17;
minMinute = 0;
maxMinute = 59;
break;
case 3: // 晚上 18:00-23:59
minHour = 18;
maxHour = 23;
minMinute = 0;
maxMinute = 59;
break;
}
return { minHour, maxHour, minMinute, maxMinute };
}
/**
* 打开最晚到店时间选择器
*/
const openLatestDateTimePicker = () => {
if (!selectedDate.value) {
uni.showToast({
title: '请先选择日期',
icon: 'none'
});
return;
}
if (!selectedTimeSlot.value || selectedTimeSlot.value === null || selectedTimeSlot.value === undefined) {
uni.showToast({
title: '请先选择时间段',
icon: 'none'
});
return;
}
// 设置时间段范围限制
const timeRange = getTimeSlotRange();
latestDateTimeMinHour.value = timeRange.minHour;
latestDateTimeMaxHour.value = timeRange.maxHour;
latestDateTimeMinMinute.value = timeRange.minMinute;
latestDateTimeMaxMinute.value = timeRange.maxMinute;
// 如果有选择时间段,使用自动计算的时间
updateLatestDateTimeFromSlot();
// 将时间戳转换为 HH:MM 格式字符串
let timeString = null;
if (latestDateTime.value) {
const date = new Date(latestDateTime.value * 1000);
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
timeString = `${hours}:${minutes}`;
} else {
// 默认使用时间段开始时间+30分钟
const timeRangeForDefault = getTimeSlotRange();
let defaultHour = timeRangeForDefault.minHour;
let defaultMinute = 30;
if (defaultMinute > timeRangeForDefault.maxMinute) {
defaultMinute = timeRangeForDefault.maxMinute;
}
timeString = `${String(defaultHour).padStart(2, '0')}:${String(defaultMinute).padStart(2, '0')}`;
}
// 确保时间字符串格式正确(必须是 "HH:MM" 格式)
if (timeString && typeof timeString === 'string' && timeString.match(/^\d{2}:\d{2}$/)) {
latestDateTimePickerValue.value = timeString;
} else {
// 如果格式不正确,使用时间段开始时间作为默认值
const timeRangeForFallback = getTimeSlotRange();
latestDateTimePickerValue.value = `${String(timeRangeForFallback.minHour).padStart(2, '0')}:00`;
}
// 确保值设置完成后再打开选择器
nextTick(() => {
latestDateTimePickerVisible.value = true;
});
}
/**
* 最晚到店时间选择器确认
*/
const onLatestDateTimePickerConfirm = async (e) => {
// up-datetime-picker mode="time" 时v-model 绑定的值应该是 "HH:MM" 格式字符串
// 事件参数 e 可能是对象 { value: "HH:MM", mode: 'time' } 或直接是字符串 "HH:MM"
let timeString = latestDateTimePickerValue.value;
// 如果 v-model 的值无效,尝试从事件参数获取
if (!timeString || typeof timeString !== 'string') {
if (e && typeof e === 'object' && e.value !== undefined) {
timeString = e.value;
} else if (typeof e === 'string') {
timeString = e;
}
}
// 验证时间字符串格式
if (!timeString || typeof timeString !== 'string' || !timeString.includes(':')) {
console.error('时间选择器返回的值格式无效:', e, 'latestDateTimePickerValue:', latestDateTimePickerValue.value);
uni.showToast({
title: '时间选择失败,请重试',
icon: 'none'
});
latestDateTimePickerVisible.value = false;
return;
}
// 解析时间字符串 "HH:MM"
const timeParts = timeString.split(':');
if (timeParts.length !== 2) {
console.error('时间格式错误:', timeString);
uni.showToast({
title: '时间格式错误',
icon: 'none'
});
latestDateTimePickerVisible.value = false;
return;
}
const hours = parseInt(timeParts[0], 10);
const minutes = parseInt(timeParts[1], 10);
if (isNaN(hours) || isNaN(minutes) || hours < 0 || hours > 23 || minutes < 0 || minutes > 59) {
console.error('时间值无效:', hours, minutes);
uni.showToast({
title: '时间值无效',
icon: 'none'
});
latestDateTimePickerVisible.value = false;
return;
}
// 验证选择的时间是否在时间段范围内
if (selectedTimeSlot.value !== null && selectedTimeSlot.value !== undefined) {
const timeRange = getTimeSlotRange();
if (hours < timeRange.minHour || hours > timeRange.maxHour) {
uni.showToast({
title: `时间必须在 ${timeRange.minHour}:00-${timeRange.maxHour}:59 范围内`,
icon: 'none'
});
latestDateTimePickerVisible.value = false;
return;
}
if (hours === timeRange.minHour && minutes < timeRange.minMinute) {
uni.showToast({
title: `时间必须在 ${timeRange.minHour}:${String(timeRange.minMinute).padStart(2, '0')}-${timeRange.maxHour}:59 范围内`,
icon: 'none'
});
latestDateTimePickerVisible.value = false;
return;
}
if (hours === timeRange.maxHour && minutes > timeRange.maxMinute) {
uni.showToast({
title: `时间必须在 ${timeRange.minHour}:00-${timeRange.maxHour}:${String(timeRange.maxMinute).padStart(2, '0')} 范围内`,
icon: 'none'
});
latestDateTimePickerVisible.value = false;
return;
}
}
// 如果没有选择日期,使用当前日期
if (!selectedDate.value) {
uni.showToast({
title: '请先选择日期',
icon: 'none'
});
latestDateTimePickerVisible.value = false;
return;
}
// 将选择的日期和时间组合
const date = new Date(selectedDate.value * 1000);
const year = date.getFullYear();
const month = date.getMonth();
const day = date.getDate();
const dateTime = new Date(year, month, day, hours, minutes, 0);
// 转换为秒级时间戳
const selectedTimestamp = Math.floor(dateTime.getTime() / 1000);
// 验证转换后的时间戳是否有效
if (isNaN(selectedTimestamp) || selectedTimestamp <= 0) {
console.error('时间戳转换失败:', dateTime, selectedTimestamp);
uni.showToast({
title: '时间处理失败',
icon: 'none'
});
latestDateTimePickerVisible.value = false;
return;
}
// 更新最晚到店时间
latestDateTime.value = selectedTimestamp;
latestDateTimePickerVisible.value = false;
}
/**
* 根据时间段自动更新最晚到店时间(时间段开始时间+30分钟
*/
const updateLatestDateTimeFromSlot = () => {
if (!selectedDate.value || selectedTimeSlot.value === null || selectedTimeSlot.value === undefined) {
return;
}
// 将日期时间戳转换为日期对象
const date = new Date(selectedDate.value * 1000);
const year = date.getFullYear();
const month = date.getMonth();
const day = date.getDate();
// 根据时段类型设置开始时间
let startHour = 0;
let startMinute = 30; // 默认加30分钟
switch (selectedTimeSlot.value) {
case 0: // 凌晨 00:00-05:59
startHour = 0;
startMinute = 30;
break;
case 1: // 上午 06:00-11:59
startHour = 6;
startMinute = 30;
break;
case 2: // 下午 12:00-17:59
startHour = 12;
startMinute = 30;
break;
case 3: // 晚上 18:00-23:59
startHour = 18;
startMinute = 30;
break;
}
// 创建最晚到店时间(开始时间+30分钟
const latestDateTimeObj = new Date(year, month, day, startHour, startMinute, 0);
// 转换为秒级时间戳
latestDateTime.value = Math.floor(latestDateTimeObj.getTime() / 1000);
}
const tipsShow = () => {
submitPopupRef.value.open()
}
const maxPlayerCount = ref(0)
const peopleRange = ref([])
const peopleText = ref("请先选择房间");
const gameTypeRange = ref([])
const gameRuleRange = ref([])
const gameRuleText = ref("请先选择玩法类型");
const gameTypeRangeChange = (e) => {
console.log('gameTypeRangeChange:', e)
// gameRuleRange.value
if (e == 0) {
gameRuleRange.value = [];
reservationInfo.value.game_rule = 0;
gameRuleText.value = "请先选择玩法类型";
} else {
var f = gameTypeRange.value.find(it => it.value == e);
console.log('f', f);
gameRuleText.value = "请选择具体规则";
var temp = [];
if (f.children != null && f.children.length > 0) {
f.children.forEach(e => {
temp.push({
value: e.id,
text: e.name
})
})
}
gameRuleRange.value = temp;
}
}
const gameRuleRangeChange = (e) => {
console.log('gameRuleRangeChange:', e)
}
// 获取玩法类型文本
const getGameTypeText = (gameTypeValue) => {
if (!gameTypeValue || gameTypeValue === 0) {
return '';
}
const gameType = gameTypeRange.value.find(item => item.value === gameTypeValue);
return gameType ? gameType.text : '';
}
// 获取具体规则文本
const getGameRuleText = (gameRuleValue) => {
if (!gameRuleValue || gameRuleValue === 0) {
return '';
}
const gameRule = gameRuleRange.value.find(item => item.value === gameRuleValue);
return gameRule ? gameRule.text : '';
}
const smokingOptions = ref([
{ value: 2, text: '不禁烟' },
{ value: 1, text: '禁烟' },
])
const genderOptions = ref([
{ value: 0, text: '不限' },
{ value: 1, text: '男' },
{ value: 2, text: '女' },
])
const depositOptions = ref([
{ value: 0, text: '0元' },
{ value: 5, text: '5元' },
{ value: 10, text: '10元' },
{ value: -1, text: '自定义' },
])
const currentValue = ref(0)
const customDeposit = ref('')
const onCustomDepositInput = (val) => {
// 仅保留数字与小数点,但需求是整数金额,按只允许数字处理
let v = String(val).replace(/[^0-9]/g, '')
if (v === '') v = '0'
let n = Number(v)
if (n > 50) n = 50
if (n < 0) n = 0
customDeposit.value = String(n)
}
const increment = () => {
if (currentValue.value < 5) {
currentValue.value += 0.5
if (currentValue.value > 5) {
currentValue.value = 5
}
// 同步到表单对象
reservationInfo.value.credit_limit = currentValue.value
}
}
const decrement = () => {
if (currentValue.value > 0) {
currentValue.value -= 0.5
if (currentValue.value < 0) {
currentValue.value = 0
}
// 同步到表单对象
reservationInfo.value.credit_limit = currentValue.value
}
}
// 返回上一页
const goBack = () => {
uni.navigateBack({
delta: 1
})
}
// 表单验证方法
const validateForm = async () => {
const info = reservationInfo.value
// 必填字段验证
if (!info.room_id || info.room_id === 0) {
uni.showToast({
title: '请选择房间',
icon: 'none'
})
return false
}
// 检查是否选择了时段
if (selectedTimeSlot.value === null || selectedTimeSlot.value === undefined) {
uni.showToast({
title: '请选择时间段',
icon: 'none'
})
return false
}
// 验证选择的时段是否仍然可预约
if (roomDetail.value && roomDetail.value.time_slots) {
const selectedSlot = roomDetail.value.time_slots.find(
slot => slot.slot_type === selectedTimeSlot.value
);
if (!selectedSlot || selectedSlot.status !== 'available') {
uni.showToast({
title: '该时段已不可预约,请重新选择',
icon: 'none',
duration: 2000
});
// 重新加载房间详情,更新可预约时段
if (reservationInfo.value.room_id && selectedDate.value) {
await loadRoomDetailForReservation(reservationInfo.value.room_id, selectedDate.value);
}
return false;
}
}
if (!info.start_time || info.start_time === 0) {
uni.showToast({
title: '请选择时间段',
icon: 'none'
})
return false
}
if (!info.end_time || info.end_time === 0) {
uni.showToast({
title: '请选择时间段',
icon: 'none'
})
return false
}
if (info.end_time <= info.start_time) {
uni.showToast({
title: '结束时间需晚于开始时间',
icon: 'none'
})
return false
}
if (!info.title || info.title.trim() === '') {
uni.showToast({
title: '请输入组局名称',
icon: 'none'
})
return false
}
if (!info.player_count || info.player_count === 0) {
uni.showToast({
title: '请选择游玩人数',
icon: 'none'
})
return false
}
if (!info.game_type || info.game_type === 0) {
uni.showToast({
title: '请选择玩法类型',
icon: 'none'
})
return false
}
if (!info.game_rule || info.game_rule === 0) {
uni.showToast({
title: '请选择具体规则',
icon: 'none'
})
return false
}
// 年龄范围验证(如果设置了年龄限制)
if (info.min_age > 0 && info.max_age > 0 && info.min_age > info.max_age) {
uni.showToast({
title: '最小年龄不能大于最大年龄',
icon: 'none'
})
return false
}
return true
}
// 提交预约方法
const submitReservation = async () => {
var isLoginSucces = await isLogin();
if (!isLoginSucces) {
uni.navigateTo({
url: '/pages/me/login'
});
return;
}
// 验证表单
if (!(await validateForm())) {
return
}
try {
var messageId = await getSubscribeMessage(reservationInfo.value.deposit_fee);
console.log("messageId", messageId);
var subscribeMessage = await requestSubscribeMessage(messageId);
console.log("message", subscribeMessage);
// 显示加载状态
uni.showLoading({
title: '提交中...'
})
// 处理自定义鸽子费
let finalDeposit = reservationInfo.value.deposit_fee
if (finalDeposit === -1) {
const n = Number(customDeposit.value || 0)
if (isNaN(n) || n < 0 || n > 50) {
uni.showToast({ title: '自定义金额需在0-50之间', icon: 'none' })
return
}
finalDeposit = n
}
// 准备提交数据
const submitData = {
...reservationInfo.value,
// 确保信誉限制同步
credit_limit: currentValue.value,
deposit_fee: finalDeposit,
important_data: {},
// 添加最晚到店时间参数
latestDateTime: latestDateTime.value || null
}
// 将玩法类型和具体规则从数字转换为字符串
submitData.game_type = getGameTypeText(submitData.game_type);
submitData.game_rule = getGameRuleText(submitData.game_rule);
var important_data = "";
if (subscribeMessage.result != null) {
important_data = JSON.stringify(subscribeMessage.result);
}
submitData.important_data = important_data;
// 检测是否能创建预约 ,如果不能创建则提示并中断
const canCreateRes = await canCreateSQReservation(submitData)
if (!canCreateRes || canCreateRes.canCreate !== true) {
uni.hideLoading()
uni.showToast({
title: (canCreateRes && canCreateRes.message) ? canCreateRes.message : '当前条件不可创建预约',
icon: 'none'
})
return
}
var resPay = null;
if (submitData.deposit_fee > 0) {
resPay = await usePay(submitData.deposit_fee);
if (resPay == null) {
uni.showToast({
title: '鸽子费订单创建失败,请重新提交预约数据!',
icon: 'none'
})
return;
}
subscribeMessage.result.paymentId = resPay.paymentId;
if (subscribeMessage.result != null) {
important_data = JSON.stringify(subscribeMessage.result);
}
submitData.important_data = important_data;
}
// TODO: 调用后端API提交预约
// 调用后端API提交预约
const result = await addSQReservation(submitData)
uni.hideLoading()
console.log("result", result);
if (!result.success) {
uni.showToast({
title: result.message,
icon: 'none'
})
return;
}
if (submitData.deposit_fee > 0 && resPay != null) {
var payRes = await requestPayment(resPay);
if (payRes.success) {
uni.showToast({
title: '鸽子费支付成功',
icon: 'success'
})
} else {
await cancelReservation(result.data, "鸽子费支付失败,请重新预约!");
uni.showToast({
title: '鸽子费支付失败,请重新预约!',
icon: 'none'
})
return;
}
console.log("payRes", payRes);
}
var share_id = result.data;
var detailData = await getDetail(share_id);
if (detailData != null) {
reservationData.value = detailData;
tipsShow();
} else {
// 提交成功
uni.showToast({
title: '预约提交成功',
icon: 'success'
})
}
// 可以跳转到其他页面或重置表单
// uni.navigateBack() // 返回上一页
// 或者重置表单
resetForm()
} catch (error) {
uni.hideLoading()
console.error('提交预约失败:', error)
uni.showToast({
title: '提交失败,请重试',
icon: 'none'
})
}
}
// 重置表单方法
const resetForm = () => {
// 保存房间信息(不重置房间号)
const savedRoomId = reservationInfo.value.room_id;
const savedRoomName = reservationInfo.value.room_name;
const savedMaxPlayerCount = maxPlayerCount.value;
const savedPeopleRange = [...peopleRange.value];
const savedPeopleText = peopleText.value;
reservationInfo.value = {
room_id: savedRoomId,
room_name: savedRoomName,
start_time: 0,
end_time: 0,
max_age: 0,
min_age: 0,
title: '',
extra_info: '',
game_rule: '',
game_type: '',
gender_limit: 0,
is_smoking: 2,
credit_limit: 0,
deposit_fee: 0,
player_count: 0,
}
currentValue.value = 0
gameRuleRange.value = []
customDeposit.value = '' // 重置自定义鸽子费
// 保留人数范围
maxPlayerCount.value = savedMaxPlayerCount;
peopleRange.value = savedPeopleRange;
peopleText.value = savedPeopleText;
// 重置时段选择
selectedTimeSlot.value = null;
reservationInfo.value.start_time = 0;
reservationInfo.value.end_time = 0;
// 重置最晚到店时间
latestDateTime.value = null;
gameRuleText.value = "请先选择玩法类型"
}
/**
* 加载房间详情,用于预约页面
*/
const loadRoomDetailForReservation = async (roomId, date) => {
console.log('loadRoomDetailForReservation 调用参数:', { roomId, date, dateType: typeof date });
// 验证参数
if (!roomId || !date) {
console.error('loadRoomDetailForReservation 参数无效:', { roomId, date });
uni.showToast({
title: '参数错误',
icon: 'none'
});
roomDetailLoading.value = false;
return;
}
// 确保 date 是数字类型
const dateTimestamp = Number(date);
if (isNaN(dateTimestamp) || dateTimestamp <= 0) {
console.error('日期时间戳无效:', date, '转换后:', dateTimestamp);
uni.showToast({
title: '日期格式错误',
icon: 'none'
});
roomDetailLoading.value = false;
return;
}
roomDetailLoading.value = true;
try {
console.log('调用 getRoomDetail, roomId:', roomId, 'date:', dateTimestamp);
const detail = await getRoomDetail(roomId, dateTimestamp);
if (detail) {
roomDetail.value = detail;
// 设置容量和人数范围
if (detail.capacity && detail.capacity > 0) {
maxPlayerCount.value = detail.capacity;
const t = [];
peopleText.value = "请选择游玩人数";
for (let i = 2; i <= detail.capacity; i++) {
t.push({
value: i,
text: i + '人'
});
}
peopleRange.value = t;
} else {
peopleText.value = "请选择游玩人数";
}
// 从 time_slots 中过滤出可预约的时段,动态生成时间段选项
generateTimeSlotOptions(detail.time_slots);
} else {
uni.showToast({
title: '获取房间详情失败',
icon: 'none'
});
}
} catch (error) {
console.error('加载房间详情失败', error);
uni.showToast({
title: '加载房间详情失败',
icon: 'none'
});
} finally {
roomDetailLoading.value = false;
}
}
/**
* 根据房间的 time_slots 动态生成时间段选项
* 只显示可预约的时段status === 'available'
*/
const generateTimeSlotOptions = (timeSlots) => {
if (!timeSlots || !Array.isArray(timeSlots)) {
timeSlotOptions.value = [];
return;
}
// 时段时间范围映射
const timeRangeMap = {
0: { text: '凌晨', range: '00:00-05:59' },
1: { text: '上午', range: '06:00-11:59' },
2: { text: '下午', range: '12:00-17:59' },
3: { text: '晚上', range: '18:00-23:59' }
};
// 过滤出可预约的时段并生成选项
const availableSlots = timeSlots.filter(slot => slot.status === 'available');
timeSlotOptions.value = availableSlots.map(slot => {
const slotInfo = timeRangeMap[slot.slot_type] || { text: slot.slot_name, range: '' };
// 生成显示文本
let displayText = slotInfo.text;
if (slotInfo.range) {
displayText += ` (${slotInfo.range})`;
}
// 如果有价格信息,可以追加显示
if (slot.price_desc_standard) {
displayText += ` ${slot.price_desc_standard}`;
}
return {
value: slot.slot_type,
text: displayText
};
});
}
/**
* 时段选择改变事件
*/
const onTimeSlotChange = (val) => {
if (!selectedDate.value) return;
selectedTimeSlot.value = val;
// 根据日期和时段计算开始时间和结束时间
calculateTimeFromSlot();
// 自动更新最晚到店时间(时间段开始时间+30分钟
updateLatestDateTimeFromSlot();
}
/**
* 根据选择的日期和时段计算开始时间和结束时间
*/
const calculateTimeFromSlot = () => {
if (!selectedDate.value || selectedTimeSlot.value === null || selectedTimeSlot.value === undefined) return;
// 将日期时间戳转换为日期对象
const date = new Date(selectedDate.value * 1000);
const year = date.getFullYear();
const month = date.getMonth();
const day = date.getDate();
// 根据时段类型设置开始时间
let startHour = 0;
let endHour = 0;
switch (selectedTimeSlot.value) {
case 0: // 凌晨 00:00-05:59
startHour = 0;
endHour = 6;
break;
case 1: // 上午 06:00-11:59
startHour = 6;
endHour = 12;
break;
case 2: // 下午 12:00-17:59
startHour = 12;
endHour = 18;
break;
case 3: // 晚上 18:00-23:59
startHour = 18;
endHour = 24;
break;
}
// 创建开始时间和结束时间
const startTime = new Date(year, month, day, startHour, 0, 0);
const endTime = new Date(year, month, day, endHour, 0, 0);
// 转换为秒级时间戳
reservationInfo.value.start_time = Math.floor(startTime.getTime() / 1000);
reservationInfo.value.end_time = Math.floor(endTime.getTime() / 1000);
}
onLoad(async (options) => {
const config = await getConfigData();
console.log('config', config);
if (config != null && config.config != null) {
gameTypeRange.value = [...config.config.playingMethodOptions];
}
// 必须从房间页跳转,需要传入房间信息
if (!options || !options.roomId) {
uni.showToast({
title: '参数错误,请从房间页进入',
icon: 'none'
});
setTimeout(() => {
uni.navigateBack();
}, 1500);
return;
}
// 接收房间信息
const roomId = Number(options.roomId);
const roomName = decodeURIComponent(options.roomName || '未知房间');
const date = Number(options.date);
if (!roomId || !date) {
uni.showToast({
title: '房间信息不完整',
icon: 'none'
});
setTimeout(() => {
uni.navigateBack();
}, 1500);
return;
}
// 自动填充房间信息
reservationInfo.value.room_id = roomId;
reservationInfo.value.room_name = roomName;
selectedDate.value = date;
// 加载房间详情,获取可预约时段和容量信息
await loadRoomDetailForReservation(roomId, date);
})
onShow(async () => {
// resetForm();
})
// 年龄列构建与显示/文案
const buildAgeColumns = () => {
const minList = [{
value: 0,
text: '不限'
}]
for (let i = 18; i <= 80; i++) minList.push({
value: i,
text: String(i)
})
const maxList = [{
value: 0,
text: '不限'
}]
for (let i = 18; i <= 80; i++) maxList.push({
value: i,
text: String(i)
})
agePickerColumns.value = [minList, maxList]
}
const getAgeRangeText = () => {
const {
min_age,
max_age
} = reservationInfo.value
if (min_age == 0 && max_age == 0) {
return '不限'
}
const minText = min_age === 0 ? '不限' : min_age + '岁'
const maxText = max_age === 0 ? '不限' : max_age + '岁'
return minText + ' - ' + maxText
}
const openAgePicker = () => {
buildAgeColumns()
// 计算默认索引
const {
min_age,
max_age
} = reservationInfo.value
const [mins, maxs] = agePickerColumns.value
const findIndexByValue = (arr, val) => {
const idx = arr.findIndex(it => Number(it.value) === Number(val))
return idx >= 0 ? idx : 0
}
agePickerDefaultIndex.value = [
findIndexByValue(mins, min_age ?? 0),
findIndexByValue(maxs, max_age ?? 0),
]
agePickerVisible.value = true
}
const onAgePickerConfirm = (e) => {
// uview-plus up-picker confirm 回调 e.value 为两列所选对象数组
const selected = e && e.value ? e.value : []
const min = selected[0] ? Number(selected[0].value) : 0
const max = selected[1] ? Number(selected[1].value) : 0
// 若最小值大于最大值,自动交换或归零“不限”优先
let minAge = min
let maxAge = max
if (minAge !== 0 && maxAge !== 0 && minAge > maxAge) {
[minAge, maxAge] = [maxAge, minAge]
}
reservationInfo.value.min_age = minAge
reservationInfo.value.max_age = maxAge
agePickerVisible.value = false
}
</script>
<style lang="scss">
.content {
width: 100%;
height: 100vh;
background-color: #F7F7F7;
}
/* 标题行布局 */
.header-row {
width: 90%;
margin: 100rpx auto 0;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
}
/* 返回图标 */
.back-icon {
width: 40rpx;
height: 40rpx;
}
/* 页面标题 */
.page-title {
font-size: 30rpx;
text-align: center;
flex: 1;
}
/* 占位元素保持标题居中 */
.spacer-40 {
width: 40rpx;
}
/* 输入外层统一样式 */
.input-wrapper {
border: 1px solid #515151;
border-radius: 4px;
}
/* 可点击的行年龄范围显示 */
.clickable-row {
font-size: 28rpx;
width: 100%;
display: flex;
align-items: center;
height: 60rpx;
}
/* 内联标签信誉左侧于等于 */
.inline-label {
font-size: 25.86rpx;
width: 120rpx;
}
/* 通用居中容器 */
.center {
display: flex;
align-items: center;
justify-content: center;
}
/* 提交按钮容器样式 */
.submit-button {
width: 90%;
border-radius: 10rpx;
margin: 30rpx auto 0;
background-color: #00AC4E;
}
/* 操作区一行布局重置 + 发起 */
.action-row {
width: 90%;
margin: 30rpx auto 0;
display: flex;
gap: 20rpx;
}
.reset-button {
flex: 0 0 30%;
height: 88rpx;
border-radius: 10rpx;
background-color: #FFFFFF;
color: #333;
}
.action-submit {
flex: 1 1 auto;
height: 88rpx;
margin: 0;
/* 覆盖原有 submit-button 的外边距使其与 reset 同行 */
}
/* 备注容器及文本 */
.note-container {
width: 90%;
margin: 20rpx auto 20rpx;
}
.muted-text {
color: #979797;
font-size: 24rpx;
}
.note-text {
font-size: 24rpx;
margin-left: 20rpx;
margin-bottom: 20rpx;
}
.counter-container {
display: flex;
align-items: center;
justify-content: center;
}
.counter-btn {
width: 80rpx;
height: 80rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
padding: 0;
line-height: 1;
}
.minus-btn {
background-color: #f5f5f5;
border: 1px solid #eee;
}
.counter-value {
width: 90rpx;
text-align: center;
font-size: 28rpx;
font-weight: 500;
color: #333;
}
.custom-select ::v-deep .uni-input-input {
/* 正常状态下的边框颜色 */
border: 1px solid #007aff !important;
border-radius: 6px;
}
.flex-center-row {
display: flex;
}
</style>