mahjong_group/pages/appointment/appointment-page.vue
2025-09-15 00:28:33 +08:00

571 lines
16 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">
<text class="page-title">发起预约</text>
<card-container marginTop="30rpx">
<label-field label="预定时长">
<tag-select :options="timeRange" v-model="timeRangeValue" @change="onTimeRangeChange" />
</label-field>
<view :style="{ height: lineHeight }"></view>
<time-select-cell label="开始时间" icon="@@:app/static/time_start.png" @select="openUpDatesTimePicker">
{{ getDayDescription(startTimeStr) }}
</time-select-cell>
<view :style="{ height: lineHeight }"></view>
<time-select-cell label="结束时间" icon="@@:app/static/time_end.png" @select="openUpDatesTimePickerEnd">
{{ getDayDescription(endTimeStr) }}
</time-select-cell>
<view :style="{ height: lineHeight }"></view>
<time-select-cell label="房间" @select="openRoomPicker">
{{ getRoomPickerName() }}
</time-select-cell>
</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="是否禁烟">
<com-appointment-radio-select :options="smokingOptions" v-model="reservationInfo.is_smoking" />
</label-slect-field>
<view :style="{ height: lineHeight }"></view>
<label-slect-field label="性别">
<com-appointment-radio-select :options="genderOptions" v-model="reservationInfo.gender_limit" />
</label-slect-field>
<view :style="{ height: lineHeight }"></view>
<label-slect-field label="年龄范围">
<view @click="openAgePicker" class="clickable-row">
{{ getAgeRangeText() }}
</view>
</label-slect-field>
<view :style="{ height: lineHeight }"></view>
<label-slect-field label="信誉">
<view class="flex-center-row">
<text class="inline-label">大于等于</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 :style="{ height: lineHeight }"></view>
<text class="note-text">鸽子费(保证金),参与人需缴纳鸽子费。若有参与者在预约后没有赴约,其鸽子费由在场的所有人平分。组局成功或失败后鸽子费将全额返还。</text>
</card-container>
<view class="center submit-button">
<text style="margin: 20rpx; color: white;">发起预约</text>
</view>
<view class="center note-container">
<text class="muted-text">组局成功后,发起者可通过店员领取线下红包</text>
</view>
</view>
<com-appointment-picker-data ref="roomPickerRef" />
<up-picker title="年龄范围选择" :show="agePickerVisible" :columns="agePickerColumns" :keyName="'text'"
:defaultIndex="agePickerDefaultIndex" @confirm="onAgePickerConfirm" @cancel="() => agePickerVisible = false"
@close="() => agePickerVisible = false"></up-picker>
</com-page-container-base>
</template>
<script setup>
import {
ref,
watch
} from 'vue';
import {
getConfigData
} from '@/common/server/config'
import {
getDayDescription,
ceilMinuteToNext5
} from '@/common/system/timeUtile';
import {
forEach,
union
} from 'lodash';
import TimeSelectCell from '@/components/com/appointment/time-select-cell.vue'
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 TagSelect from '@/components/com/appointment/tag-select.vue'
import ComAppointmentPickerData from '@/components/com/appointment/picker-data.vue'
import ComAppointmentRadioSelect from '@/components/com/appointment/radio-select.vue'
import {
getReservationRoomList
} from '@/common/server/interface/sq'
const _containerBase = ref(null)
// 年龄选择器状态
const agePickerVisible = ref(false)
const agePickerColumns = ref([[], []])
const agePickerDefaultIndex = ref([0, 0])
const startTimeStr = ref(0)
const endTimeStr = ref(0)
const lineHeight = ref("15rpx")
const timeRange = ref(["2小时", "3小时", "4小时", "自定义"])
const timeRangeValue = ref("")
// 房间选项
const roomOptions = ref([])
const roomPickerRef = ref(null)
//提交表单数据
const reservationInfo = ref({
room_id: 0, //房间id
room_name: '请选择房间', //房间名称
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, //人数
});
//自动计算结束时间
const onTimeRangeChange = async (val) => {
timeRangeValue.value = val;
console.log('timeRange change:', val)
if (val != "") {
await openUpDatesTimePicker(false);
if (startTimeStr.value > 0) {
var str = val;
if (str == "2小时") {
endTimeStr.value = startTimeStr.value + 1000 * 60 * 60 * 2;
} else if (str == "3小时") {
endTimeStr.value = startTimeStr.value + 1000 * 60 * 60 * 3;
} else if (str == "4小时") {
endTimeStr.value = startTimeStr.value + 1000 * 60 * 60 * 4;
} else {
await openUpDatesTimePickerEnd(false);
}
}
}
}
const getRoomPickerName = () => {
return reservationInfo.value.room_name;
}
const openUpDatesTimePicker = async (isManual = true) => {
var now = startTimeStr.value;
var min = Date.now();
min += 1000 * 60 * 30;
min = ceilMinuteToNext5(min).valueOf();
if (startTimeStr.value == 0) {
now = Date.now();
now += 1000 * 60 * 30;
now = ceilMinuteToNext5(now).valueOf();
}
const startTime = await _containerBase.value.openUpDatesTimePicker(now, min, "预约开始时间")
startTimeStr.value = startTime
// 同步到表单对象,毫秒转秒
reservationInfo.value.start_time = Math.floor(startTimeStr.value / 1000)
// 手动选择时间则切换为“自定义”
if (isManual) {
timeRangeValue.value = "自定义"
}
}
const openUpDatesTimePickerEnd = async (isManual = true) => {
if (startTimeStr.value == 0) {
uni.showToast({
title: '请先选择开始时间',
icon: 'none'
})
return;
}
var now = endTimeStr.value;
var min = (startTimeStr.value + 1000 * 60 * 30);
if (now == 0) {
now = (startTimeStr.value + 1000 * 60 * 30);
}
//minDate+1000*60*30 最小间隔30分钟
const endTime = await _containerBase.value.openUpDatesTimePicker(now, min, "预约结束时间")
endTimeStr.value = endTime
// 同步到表单对象,毫秒转秒
reservationInfo.value.end_time = Math.floor(endTimeStr.value / 1000)
// 手动选择时间则切换为“自定义”
if (isManual) {
timeRangeValue.value = "自定义"
}
}
const maxPlayerCount = ref(0)
const openRoomPicker = async () => {
// 需先选择有效的开始/结束时间
if (!reservationInfo.value.start_time || !reservationInfo.value.end_time) {
uni.showToast({
title: '请先选择开始和结束时间',
icon: 'none'
})
return
}
if (reservationInfo.value.end_time <= reservationInfo.value.start_time) {
uni.showToast({
title: '结束时间需晚于开始时间',
icon: 'none'
})
return
}
// 无数据则尝试拉取
if (!roomOptions.value || roomOptions.value.length === 0) {
await tryLoadRooms()
}
if (!roomOptions.value || roomOptions.value.length === 0) {
uni.showToast({
title: '暂无可预约房间',
icon: 'none'
})
return
}
const selected = await roomPickerRef.value.show({
options: roomOptions.value,
valueKey: 'id',
labelKey: 'name',
modelValue: reservationInfo.value.room_id
})
if (selected) {
reservationInfo.value.room_id = selected.id
reservationInfo.value.room_name = selected.name
maxPlayerCount.value = selected.capacity
}
}
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 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元' },
])
const currentValue = ref(0)
const increment = () => {
if (currentValue.value < 5) {
currentValue.value += 0.5
if (currentValue.value > 5) {
currentValue.value = 5
}
}
}
const decrement = () => {
if (currentValue.value > 0) {
currentValue.value -= 0.5
if (currentValue.value < 0) {
currentValue.value = 0
}
}
}
const changeLog = (e) => {
console.log('change事件:', e)
}
// 当开始或结束时间变化(通过其它方式)时也自动刷新房间
watch([startTimeStr, endTimeStr], async ([s, e]) => {
reservationInfo.value.start_time = s ? Math.floor(s / 1000) : 0
reservationInfo.value.end_time = e ? Math.floor(e / 1000) : 0
await tryLoadRooms()
})
watch(() => reservationInfo.value.room_id, async (newId, oldId) => {
console.log('room_id changed:', oldId, '->', newId);
if (newId == 0) {
reservationInfo.value.player_count = 0;
peopleRange.value = [];
peopleText.value = "请先选择房间";
} else {
var t = [];
peopleText.value = "请选择游玩人数";
for (let i = 2; i <= maxPlayerCount.value; i++) {
t.push({
value: i,
text: i + '人'
});
}
console.log("peopleRange.value ", maxPlayerCount.value, t);
if (reservationInfo.value.player_count > maxPlayerCount.value) {
reservationInfo.value.player_count = 0;
}
peopleRange.value = t;
}
});
// 拉取可预约房间
const tryLoadRooms = async () => {
if (!reservationInfo.value.start_time || !reservationInfo.value.end_time) return
if (reservationInfo.value.end_time <= reservationInfo.value.start_time) return
const list = await getReservationRoomList(reservationInfo.value.start_time, reservationInfo.value.end_time)
// list.push()
if (Array.isArray(list)) {
roomOptions.value = Array.isArray(list) ? list : []
} else {
roomOptions.value = []
}
// 若当前选中房间不在可选列表中,则清空
console.log("房间id==>", reservationInfo.value.room_id, "房间列表==>", roomOptions.value);
if (!roomOptions.value.some(it => it.id === reservationInfo.value.room_id)) {
reservationInfo.value.room_id = 0;
reservationInfo.value.room_name = '请选择房间';
}
}
onLoad(async () => {
const config = await getConfigData();
console.log('config', config);
if (config != null && config.config != null) {
gameTypeRange.value = [...config.config.playingMethodOptions];
}
})
// 年龄列构建与显示/文案
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;
overflow-y: auto;
background-color: #F7F7F7;
}
/* 页面标题 */
.page-title {
margin-top: 100rpx;
text-align: center;
}
/* 输入外层统一样式 */
.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;
}
/* 备注容器及文本 */
.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>