mahjong_group/pages/appointment/appointment-page.vue
2025-12-29 22:40:36 +08:00

1431 lines
38 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="开始时间">
<picker mode="time" :value="startTime" @change="onStartTimeChange">
<view class="picker-value">{{ startTime || '请选择开始时间' }}</view>
</picker>
</label-field>
<view :style="{ height: lineHeight }"></view>
<label-field label="结束时间">
<view class="end-time-row">
<picker mode="time" :value="endTime" @change="onEndTimeChange">
<view class="picker-value">{{ endTime || '请选择结束时间' }}</view>
</picker>
<view class="next-day-toggle" v-if="startTime && endTime">
<text class="next-day-label" :class="{ active: !isNextDay }" @click="setNextDay(false)">当天</text>
<text class="next-day-divider">/</text>
<text class="next-day-label" :class="{ active: isNextDay }" @click="setNextDay(true)">次日</text>
</view>
</view>
</label-field>
<!-- 时长和跨时段信息显示 -->
<view class="time-info" v-if="startTime && endTime && !timeError">
<view class="time-info-item">
<text class="time-info-label">预计时长:</text>
<text class="time-info-value">{{ calculateDuration() }}</text>
</view>
<view class="time-info-item">
<text class="time-info-label">跨越时段:</text>
<text class="time-info-value">{{ calculateCrossSlots() }}</text>
</view>
</view>
<!-- 时间错误提示 -->
<view class="time-error" v-if="timeError">
<text>{{ timeError }}</text>
</view>
</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-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 roomDetail = ref(null) // 房间详情数据
const roomDetailLoading = ref(false) // 房间详情加载状态
// 自由时间选择相关
const startTime = ref('') // 开始时间 "HH:mm"
const endTime = ref('') // 结束时间 "HH:mm"
const timeError = ref('') // 时间错误提示
const isNextDay = ref(false) // 结束时间是否为次日(用于通宵预约)
// 日期选择器状态
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 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;
// 重置时间选择
startTime.value = '';
endTime.value = '';
timeError.value = '';
isNextDay.value = false; // 重置跨天标记
reservationInfo.value.start_time = 0;
reservationInfo.value.end_time = 0;
// 重新加载房间详情
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 onStartTimeChange = (e) => {
startTime.value = e.detail.value;
validateTimeRange();
buildTimeFromPicker();
}
/**
* 结束时间变更处理
*/
const onEndTimeChange = (e) => {
endTime.value = e.detail.value;
// 自动判断是否需要切换到次日
autoDetectNextDay();
validateTimeRange();
buildTimeFromPicker();
}
/**
* 自动检测是否需要切换到次日(当结束时间小于开始时间时)
*/
const autoDetectNextDay = () => {
if (!startTime.value || !endTime.value) return;
const [startH, startM] = startTime.value.split(':').map(Number);
const [endH, endM] = endTime.value.split(':').map(Number);
const startMinutes = startH * 60 + startM;
const endMinutes = endH * 60 + endM;
// 如果结束时间小于开始时间,自动切换到次日
if (endMinutes <= startMinutes) {
isNextDay.value = true;
}
}
/**
* 设置结束时间是否为次日
*/
const setNextDay = (value) => {
isNextDay.value = value;
validateTimeRange();
buildTimeFromPicker();
}
/**
* 验证时间范围(支持跨天预约)
*/
const validateTimeRange = () => {
timeError.value = '';
if (!startTime.value || !endTime.value) {
return true;
}
const [startH, startM] = startTime.value.split(':').map(Number);
const [endH, endM] = endTime.value.split(':').map(Number);
const startMinutes = startH * 60 + startM;
let endMinutes = endH * 60 + endM;
// 如果是次日结束时间加24小时
if (isNextDay.value) {
endMinutes += 24 * 60;
}
// 计算时长(分钟)
const durationMinutes = endMinutes - startMinutes;
if (durationMinutes <= 0) {
timeError.value = '结束时间必须晚于开始时间';
return false;
}
if (durationMinutes < 60) {
timeError.value = '预约时长不能少于1小时';
return false;
}
if (durationMinutes > 720) {
timeError.value = '预约时长不能超过12小时';
return false;
}
return true;
}
/**
* 构建时间戳(从时间选择器值,支持跨天预约)
*/
const buildTimeFromPicker = () => {
if (!selectedDate.value || !startTime.value || !endTime.value) {
return;
}
if (!validateTimeRange()) {
// 清空时间戳
reservationInfo.value.start_time = 0;
reservationInfo.value.end_time = 0;
return;
}
const date = new Date(selectedDate.value * 1000);
const [startH, startM] = startTime.value.split(':').map(Number);
const [endH, endM] = endTime.value.split(':').map(Number);
const startDateTime = new Date(date.getFullYear(), date.getMonth(), date.getDate(), startH, startM, 0);
// 如果是次日结束时间加1天
let endDateTime;
if (isNextDay.value) {
const nextDay = new Date(date.getFullYear(), date.getMonth(), date.getDate() + 1, endH, endM, 0);
endDateTime = nextDay;
} else {
endDateTime = new Date(date.getFullYear(), date.getMonth(), date.getDate(), endH, endM, 0);
}
reservationInfo.value.start_time = Math.floor(startDateTime.getTime() / 1000);
reservationInfo.value.end_time = Math.floor(endDateTime.getTime() / 1000);
}
/**
* 计算时长显示(支持跨天预约)
*/
const calculateDuration = () => {
if (!startTime.value || !endTime.value) return '-';
const [startH, startM] = startTime.value.split(':').map(Number);
const [endH, endM] = endTime.value.split(':').map(Number);
let endMinutes = endH * 60 + endM;
// 如果是次日结束时间加24小时
if (isNextDay.value) {
endMinutes += 24 * 60;
}
const diffMinutes = endMinutes - (startH * 60 + startM);
if (diffMinutes <= 0) return '-';
const hours = Math.floor(diffMinutes / 60);
const mins = diffMinutes % 60;
if (mins === 0) {
return `${hours}小时`;
}
return `${hours}小时${mins}分钟`;
}
/**
* 计算跨越时段(支持跨天预约)
*/
const calculateCrossSlots = () => {
if (!startTime.value || !endTime.value) return '-';
const [startH] = startTime.value.split(':').map(Number);
const [endH] = endTime.value.split(':').map(Number);
const slots = [];
const ranges = [
{ name: '凌晨', start: 0, end: 6 },
{ name: '上午', start: 6, end: 12 },
{ name: '下午', start: 12, end: 18 },
{ name: '晚上', start: 18, end: 24 }
];
if (isNextDay.value) {
// 跨天预约:当天的时段 + 次日的时段
// 当天从开始时间到24点
for (const r of ranges) {
if (startH < r.end && 24 > r.start) {
if (!slots.includes(r.name)) {
slots.push(r.name);
}
}
}
// 次日从0点到结束时间
for (const r of ranges) {
if (0 < r.end && endH > r.start) {
if (!slots.includes(r.name)) {
slots.push(r.name);
}
}
}
} else {
// 同一天:正常判断
for (const r of ranges) {
// 重叠判断:预约开始 < 时段结束 AND 预约结束 > 时段开始
if (startH < r.end && endH > r.start) {
slots.push(r.name);
}
}
}
return slots.join('、') || '-';
}
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 (!startTime.value || !endTime.value) {
uni.showToast({
title: '请选择开始和结束时间',
icon: 'none'
})
return false
}
// 检查时间是否有效
if (timeError.value) {
uni.showToast({
title: timeError.value,
icon: 'none'
})
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: {}
}
// 将玩法类型和具体规则从数字转换为字符串
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;
// 重置时间选择
startTime.value = '';
endTime.value = '';
timeError.value = '';
isNextDay.value = false; // 重置跨天标记
reservationInfo.value.start_time = 0;
reservationInfo.value.end_time = 0;
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 = "请选择游玩人数";
t.push({
value: 1,
text:'无需组局'
});
for (let i = 2; i <= detail.capacity; i++) {
t.push({
value: i,
text: i + '人'
});
}
peopleRange.value = t;
} else {
peopleText.value = "请选择游玩人数";
}
} else {
uni.showToast({
title: '获取房间详情失败',
icon: 'none'
});
}
} catch (error) {
console.error('加载房间详情失败', error);
uni.showToast({
title: '加载房间详情失败',
icon: 'none'
});
} finally {
roomDetailLoading.value = false;
}
}
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;
}
/* 时间选择器样式 */
.picker-value {
padding: 15rpx 20rpx;
background-color: #f5f5f5;
border-radius: 8rpx;
font-size: 28rpx;
color: #333;
}
/* 结束时间行布局时间选择器 + 当天/次日切换 */
.end-time-row {
display: flex;
align-items: center;
gap: 20rpx;
}
/* 当天/次日切换样式 */
.next-day-toggle {
display: flex;
align-items: center;
background-color: #f0f0f0;
border-radius: 8rpx;
padding: 8rpx 16rpx;
}
.next-day-label {
font-size: 24rpx;
color: #999;
padding: 4rpx 12rpx;
border-radius: 6rpx;
transition: all 0.2s;
&.active {
color: #fff;
background-color: #00AC4E;
}
}
.next-day-divider {
font-size: 24rpx;
color: #ccc;
margin: 0 4rpx;
}
/* 时间信息显示 */
.time-info {
margin: 20rpx 0;
padding: 20rpx;
background-color: #f8f8f8;
border-radius: 12rpx;
}
.time-info-item {
display: flex;
justify-content: space-between;
margin-bottom: 10rpx;
&:last-child {
margin-bottom: 0;
}
}
.time-info-label {
font-size: 24rpx;
color: #999;
}
.time-info-value {
font-size: 24rpx;
color: #333;
font-weight: 500;
}
/* 时间错误提示 */
.time-error {
margin: 16rpx 0;
padding: 16rpx;
background-color: #fff2f0;
border-radius: 8rpx;
text {
font-size: 24rpx;
color: #ff4d4f;
}
}
/* 可点击的行年龄范围显示 */
.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>