修改时间

This commit is contained in:
zpc 2026-01-01 17:03:57 +08:00
parent e76590f61e
commit 193b9a1e0f
6 changed files with 1150 additions and 347 deletions

View File

@ -2,8 +2,8 @@
## Bug统计
- **总数量**: 9个
- **已修复**: 1个 ✅
- **未修改**: 8
- **已修复**: 2个 ✅
- **未修改**: 7
- **分类**: 后端问题 6个前端问题 2个前后端问题 3个
---
@ -40,7 +40,7 @@
---
### cs110_24 - 已评价组局消息未消失
**状态**: 未修改
**状态**: ✅ 已修复
**类型**: 后端
**优先级**: 中
@ -48,11 +48,32 @@
我的页面中,已结束的牌局在给牌友评价后,该组局消息没有消失。
**解决方案**:
需要明确消失逻辑:
- 所有参与者都完成评价后消息消失
- 或者预约结束后7天自动消失
- 需要产品确认具体的消失逻辑
- uniapp\mahjong_group\pages\me\me-page.vue 我的页面
用户只要评价过一次(不管评价了谁),这个预约就应该从"我的页面"中消失。如果用户还想继续评价其他人,可以去"预约记录"页面找到这个预约继续评价。
**修复内容**:
1. **后端接口修改** (`server/CoreCms.Net.Web.WebApi/Controllers/SQController.cs`)
- 修改`GetMyUseReservation`接口的SQL查询条件
- 添加`NOT EXISTS`子查询,排除当前用户已经评价过的预约
- 查询逻辑:如果用户对某个预约有评价记录,该预约就不会显示在"我的页面"中
2. **前端评价组件** (`uniapp/mahjong_group/components/com/page/reservation-evaluate.vue`)
- 修改`submitEvaluate`方法
- 评价成功后触发全局事件`evaluateSuccess`
- 延迟1.5秒后关闭弹窗并通知刷新数据
3. **我的页面** (`uniapp/mahjong_group/pages/me/me-page.vue`)
- 添加`onLoad`、`onUnload`生命周期导入
- 在`onLoad`中监听`evaluateSuccess`事件
- 收到事件后重新调用`loadCurrentAppointment()`刷新数据
- 在`onUnload`中移除事件监听,避免内存泄漏
**修复逻辑**:
- 用户评价任意一个人后,该预约立即从"我的页面"消失
- 如需继续评价其他人,用户可前往"预约记录"页面查找
- 保持页面简洁,避免已处理预约长期占用显示空间
**修复时间**: 2025-01-01
**测试状态**: 待测试
---
### cs120_1 - 时间段预约逻辑错误
@ -238,7 +259,7 @@
- cs120_9 - 鸽子费审核功能缺失
### 🟡 中优先级(影响用户体验)
- cs110_24 - 已评价组局消息未消失
- cs110_24 - 已评价组局消息未消失(已修复)
- cs120_3 - 首页高度显示异常
- cs120_4 - 房间卡片文字显示不全
- cs120_5 - 签到后页面状态未刷新

View File

@ -232,9 +232,6 @@ namespace CoreCms.Net.Model.Entities
[Display(Name = "状态0=待开始1=锁定中2=进行中3=已结束,4=取消")]
[Required(ErrorMessage = "请输入{0}")]
public System.Int32 status { get; set; }

View File

@ -7,7 +7,7 @@
<ReservationPopup ref="_reservationPopup" />
<up-datetime-picker :show="_upDatesTimePicker.show" :filter="_upDatesTimePicker.filter"
:formatter="_upDatesTimePicker.formatter" v-model="_upDatesTimePicker.value" mode="datetime"
:minDate="_upDatesTimePicker.minDate" @cancel="_upDatesTimePicker.onCancel"
:minDate="_upDatesTimePicker.minDate" :maxDate="_upDatesTimePicker.maxDate" @cancel="_upDatesTimePicker.onCancel"
@confirm="_upDatesTimePicker.onConfirm" :title="_upDatesTimePicker.title"
:hasInput="false"></up-datetime-picker>
</view>
@ -34,12 +34,14 @@ const openQianDaoPop = async (reservation) => {
console.log("openQianDaoPop", reservation)
_qianDaoPopup.value && _qianDaoPopup.value.show(reservation)
}
let maxt=Date.now() + 6 * 24 * 60 * 60 * 1000;
const _upDatesTimePicker = ref({
refId: null,
show: false,
value: Date.now(),
title: '',
minDate: Date.now(),
maxDate: maxt, // +7
filter: (mode, options) => {
// console.log("filter", mode, options)
if (mode == "minute") {

View File

@ -248,7 +248,7 @@ const handleJoin = () => {
.grid-item {
width: 300rpx;
height: 405rpx;
height: 425rpx;
background: #FFFFFF;
box-shadow: -4rpx 12rpx 7rpx 0rpx rgba(0, 0, 0, 0.25);
border-radius: 27rpx;

View File

@ -0,0 +1,899 @@
<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(reservationInfo.start_time * 1000) }}
</time-select-cell>
<view :style="{ height: lineHeight }"></view>
<time-select-cell label="结束时间" icon="@@:app/static/time_end.png" @select="openUpDatesTimePickerEnd">
{{ getDayDescription(reservationInfo.end_time * 1000) }}
</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="是否禁烟">
<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 :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>
<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>
<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
} from 'vue';
import {
getConfigData, getSubscribeMessage
} from '@/common/server/config'
import { usePay } from '@/common/server/interface/user'
import {
requestSubscribeMessage,
requestPayment
} from '@/common/utils'
import {
getDayDescription,
ceilMinuteToNext5
} from '@/common/system/timeUtile';
import { isLogin } from '@/common/server/user'
import {
forEach,
union
} from 'lodash';
import {
getDetail
} from '@/common/server/index'
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, addSQReservation, cancelReservation, canCreateSQReservation
} from '@/common/server/interface/sq'
const _containerBase = ref(null)
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 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, // 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 (reservationInfo.value.start_time > 0) {
var str = val;
if (str == "2小时") {
reservationInfo.value.end_time = reservationInfo.value.start_time + 2 * 60 * 60;
} else if (str == "3小时") {
reservationInfo.value.end_time = reservationInfo.value.start_time + 3 * 60 * 60;
} else if (str == "4小时") {
reservationInfo.value.end_time = reservationInfo.value.start_time + 4 * 60 * 60;
} else {
await openUpDatesTimePickerEnd(false);
}
}
}
}
const getRoomPickerName = () => {
return reservationInfo.value.room_name;
}
const tipsShow = () => {
submitPopupRef.value.open()
}
const openUpDatesTimePicker = async (isManual = true) => {
var now = reservationInfo.value.start_time * 1000;
var min = Date.now();
min += 1000 * 60 * 30;
min = ceilMinuteToNext5(min).valueOf();
if (reservationInfo.value.start_time == 0) {
now = Date.now();
now += 1000 * 60 * 30;
now = ceilMinuteToNext5(now).valueOf();
}
const startTime = await _containerBase.value.openUpDatesTimePicker(now, min, "预约开始时间")
//
reservationInfo.value.start_time = Math.floor(startTime / 1000)
// ""
if (isManual) {
timeRangeValue.value = "自定义"
}
}
const openUpDatesTimePickerEnd = async (isManual = true) => {
if (reservationInfo.value.start_time == 0) {
uni.showToast({
title: '请先选择开始时间',
icon: 'none'
})
return;
}
var now = reservationInfo.value.end_time * 1000;
var min = (reservationInfo.value.start_time * 1000 + 1000 * 60 * 30);
if (now == 0) {
now = (reservationInfo.value.start_time * 1000 + 1000 * 60 * 30);
}
//minDate+1000*60*30 30
const endTime = await _containerBase.value.openUpDatesTimePicker(now, min, "预约结束时间")
//
reservationInfo.value.end_time = Math.floor(endTime / 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 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元' },
])
const currentValue = ref(0)
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 changeLog = (e) => {
console.log('change事件:', e)
}
//
const validateForm = () => {
const info = reservationInfo.value
//
if (!info.room_id || info.room_id === 0) {
uni.showToast({
title: '请选择房间',
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 (!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: '提交中...'
})
//
const submitData = {
...reservationInfo.value,
//
credit_limit: currentValue.value,
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 = () => {
reservationInfo.value = {
room_id: 0,
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,
}
currentValue.value = 0
timeRangeValue.value = ""
gameRuleRange.value = []
peopleRange.value = []
peopleText.value = "请先选择房间"
gameRuleText.value = "请先选择玩法类型"
}
//
watch([() => reservationInfo.value.start_time, () => reservationInfo.value.end_time], async ([s, e]) => {
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];
}
})
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;
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;
}
/* 操作区一行布局:重置 + 发起 */
.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>

View File

@ -8,46 +8,33 @@
</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 label="预定时长">
<tag-select :options="timeRange" v-model="timeRangeValue" @change="onTimeRangeChange" />
</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>
<time-select-cell label="开始时间" icon="@@:app/static/time_start.png" @select="openUpDatesTimePicker">
{{ getDayDescription(reservationInfo.start_time * 1000) }}
</time-select-cell>
<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>
<time-select-cell label="结束时间" icon="@@:app/static/time_end.png" @select="openUpDatesTimePickerEnd">
{{ getDayDescription(reservationInfo.end_time * 1000) }}
</time-select-cell>
<!-- 时长和跨时段信息显示 -->
<view class="time-info" v-if="startTime && endTime && !timeError">
<view class="time-info" v-if="reservationInfo.start_time && reservationInfo.end_time && !timeError">
<view class="time-info-item">
<text class="time-info-label">预计时长</text>
<text class="time-info-value">{{ calculateDuration() }}</text>
<text class="time-info-value">{{ calculateDurationFromTimestamp() }}</text>
</view>
<view class="time-info-item">
<text class="time-info-label">跨越时段</text>
<text class="time-info-value">{{ calculateCrossSlots() }}</text>
<text class="time-info-value">{{ calculateCrossSlotsFromTimestamp() }}</text>
</view>
</view>
<!-- 时间错误提示 -->
<view class="time-error" v-if="timeError">
<text>{{ timeError }}</text>
</view>
</card-container>
</card-container>
<card-container marginTop="30rpx">
@ -155,9 +142,6 @@
</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>
@ -218,10 +202,16 @@
import {
getDetail
} from '@/common/server/index'
import {
getDayDescription,
ceilMinuteToNext5
} from '@/common/system/timeUtile';
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 TimeSelectCell from '@/components/com/appointment/time-select-cell.vue'
import TagSelect from '@/components/com/appointment/tag-select.vue'
import ComAppointmentRadioSelect from '@/components/com/appointment/radio-select.vue'
import {
addSQReservation,
@ -229,6 +219,7 @@
canCreateSQReservation,
getRoomDetail
} from '@/common/server/interface/sq'
const _containerBase = ref(null)
const submitPopupRef = ref(null)
//
const agePickerVisible = ref(false)
@ -239,21 +230,14 @@
const agePickerDefaultIndex = ref([0, 0])
const reservationData = ref(null)
const lineHeight = ref("15rpx")
//
const timeRange = ref(["2小时", "3小时", "4小时", "自定义"])
const timeRangeValue = ref("")
//
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
@ -275,224 +259,103 @@
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;
//
const onTimeRangeChange = async (val) => {
timeRangeValue.value = val;
console.log('timeRange change:', val)
if (val != "") {
await openUpDatesTimePicker(false);
if (reservationInfo.value.start_time > 0) {
var str = val;
if (str == "2小时") {
reservationInfo.value.end_time = reservationInfo.value.start_time + 2 * 60 * 60;
} else if (str == "3小时") {
reservationInfo.value.end_time = reservationInfo.value.start_time + 3 * 60 * 60;
} else if (str == "4小时") {
reservationInfo.value.end_time = reservationInfo.value.start_time + 4 * 60 * 60;
} else {
await openUpDatesTimePickerEnd(false);
}
}
}
console.log('提取的时间戳(毫秒):', timestampMs);
//
if (typeof timestampMs !== 'number' || isNaN(timestampMs) || timestampMs <= 0) {
console.error('日期选择器返回的时间戳无效:', e, 'datePickerValue:', datePickerValue.value);
}
const openUpDatesTimePicker = async (isManual = true) => {
var now = reservationInfo.value.start_time * 1000;
var min = Date.now();
min += 1000 * 60 * 30;
min = ceilMinuteToNext5(min).valueOf();
if (reservationInfo.value.start_time == 0) {
now = Date.now();
now += 1000 * 60 * 30;
now = ceilMinuteToNext5(now).valueOf();
}
const startTime = await _containerBase.value.openUpDatesTimePicker(now, min, "预约开始时间")
//
reservationInfo.value.start_time = Math.floor(startTime / 1000)
// ""
if (isManual) {
timeRangeValue.value = "自定义"
}
//
timeError.value = ''
//
validateTimeFromTimestamp()
}
const openUpDatesTimePickerEnd = async (isManual = true) => {
if (reservationInfo.value.start_time == 0) {
uni.showToast({
title: '日期选择失败,请重试',
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;
var now = reservationInfo.value.end_time * 1000;
var min = (reservationInfo.value.start_time * 1000 + 1000 * 60 * 30);
if (now == 0) {
now = (reservationInfo.value.start_time * 1000 + 1000 * 60 * 30);
}
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;
//minDate+1000*60*30 30
const endTime = await _containerBase.value.openUpDatesTimePicker(now, min, "预约结束时间")
//
reservationInfo.value.end_time = Math.floor(endTime / 1000)
// ""
if (isManual) {
timeRangeValue.value = "自定义"
}
//
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;
//
validateTimeFromTimestamp()
}
/**
* 开始时间变更处理
* 验证时间范围基于时间戳
*/
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 = () => {
const validateTimeFromTimestamp = () => {
timeError.value = '';
if (!startTime.value || !endTime.value) {
if (!reservationInfo.value.start_time || !reservationInfo.value.end_time) {
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;
const startTime = reservationInfo.value.start_time;
const endTime = reservationInfo.value.end_time;
// 24
if (isNextDay.value) {
endMinutes += 24 * 60;
}
//
const durationSeconds = endTime - startTime;
//
const durationMinutes = endMinutes - startMinutes;
if (durationMinutes <= 0) {
if (durationSeconds <= 0) {
timeError.value = '结束时间必须晚于开始时间';
return false;
}
if (durationMinutes < 60) {
if (durationSeconds < 3600) {
timeError.value = '预约时长不能少于1小时';
return false;
}
if (durationMinutes > 720) {
if (durationSeconds > 43200) {
timeError.value = '预约时长不能超过12小时';
return false;
}
@ -501,60 +364,17 @@
}
/**
* 构建时间戳从时间选择器值支持跨天预约
* 计算时长显示基于时间戳
*/
const buildTimeFromPicker = () => {
if (!selectedDate.value || !startTime.value || !endTime.value) {
return;
}
const calculateDurationFromTimestamp = () => {
if (!reservationInfo.value.start_time || !reservationInfo.value.end_time) return '-';
if (!validateTimeRange()) {
//
reservationInfo.value.start_time = 0;
reservationInfo.value.end_time = 0;
return;
}
const diffSeconds = reservationInfo.value.end_time - reservationInfo.value.start_time;
const date = new Date(selectedDate.value * 1000);
const [startH, startM] = startTime.value.split(':').map(Number);
const [endH, endM] = endTime.value.split(':').map(Number);
if (diffSeconds <= 0) return '-';
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;
const hours = Math.floor(diffSeconds / 3600);
const mins = Math.floor((diffSeconds % 3600) / 60);
if (mins === 0) {
return `${hours}小时`;
@ -563,14 +383,17 @@
}
/**
* 计算跨越时段支持跨天预约
* 计算跨越时段基于时间戳
*/
const calculateCrossSlots = () => {
if (!startTime.value || !endTime.value) return '-';
const [startH] = startTime.value.split(':').map(Number);
const [endH] = endTime.value.split(':').map(Number);
const calculateCrossSlotsFromTimestamp = () => {
if (!reservationInfo.value.start_time || !reservationInfo.value.end_time) return '-';
const startDate = new Date(reservationInfo.value.start_time * 1000);
const endDate = new Date(reservationInfo.value.end_time * 1000);
const startH = startDate.getHours();
const endH = endDate.getHours();
const slots = [];
const ranges = [
{ name: '凌晨', start: 0, end: 6 },
@ -579,7 +402,10 @@
{ name: '晚上', start: 18, end: 24 }
];
if (isNextDay.value) {
//
const isNextDay = endDate.getDate() !== startDate.getDate();
if (isNextDay) {
// +
// 24
for (const r of ranges) {
@ -609,7 +435,58 @@
return slots.join('、') || '-';
}
/**
* 设置默认开始时间
* @param {number} dateTimestamp 日期时间戳秒级
*/
const setDefaultStartTime = async (dateTimestamp) => {
try {
//
const selectedDate = new Date(dateTimestamp * 1000);
const today = new Date();
//
const isToday = selectedDate.toDateString() === today.toDateString();
let defaultStartTime;
if (isToday) {
// 305
const now = new Date();
now.setMinutes(now.getMinutes() + 30); // 30
defaultStartTime = ceilMinuteToNext5(now.getTime());
} else {
// 10:00
const defaultTime = new Date(selectedDate);
defaultTime.setHours(10, 0, 0, 0);
defaultStartTime = defaultTime.getTime();
}
//
reservationInfo.value.start_time = Math.floor(defaultStartTime / 1000);
// 2
timeRangeValue.value = "2小时";
// 2
reservationInfo.value.end_time = reservationInfo.value.start_time + 2 * 60 * 60;
//
validateTimeFromTimestamp();
console.log('设置默认时间:', {
startTime: reservationInfo.value.start_time,
endTime: reservationInfo.value.end_time,
startTimeDisplay: getDayDescription(reservationInfo.value.start_time * 1000),
endTimeDisplay: getDayDescription(reservationInfo.value.end_time * 1000)
});
} catch (error) {
console.error('设置默认开始时间失败:', error);
}
}
const tipsShow = () => {
submitPopupRef.value.open()
}
@ -786,7 +663,7 @@
}
//
if (!startTime.value || !endTime.value) {
if (!reservationInfo.value.start_time || !reservationInfo.value.end_time) {
uni.showToast({
title: '请选择开始和结束时间',
icon: 'none'
@ -795,9 +672,9 @@
}
//
if (timeError.value) {
if (!validateTimeFromTimestamp()) {
uni.showToast({
title: timeError.value,
title: timeError.value || '时间设置有误',
icon: 'none'
})
return false
@ -1050,10 +927,8 @@
peopleText.value = savedPeopleText;
//
startTime.value = '';
endTime.value = '';
timeError.value = '';
isNextDay.value = false; //
timeRangeValue.value = ""
timeError.value = ''
reservationInfo.value.start_time = 0;
reservationInfo.value.end_time = 0;
@ -1063,7 +938,7 @@
/**
* 加载房间详情用于预约页面
*/
const loadRoomDetailForReservation = async (roomId, date) => {
const loadRoomDetailForReservation = async (roomId, date = null) => {
console.log('loadRoomDetailForReservation 调用参数:', {
roomId,
date,
@ -1071,19 +946,39 @@
});
//
if (!roomId || !date) {
if (!roomId) {
console.error('loadRoomDetailForReservation 参数无效:', {
roomId,
date
});
uni.showToast({
title: '参数错误',
title: '房间ID参数错误',
icon: 'none'
});
roomDetailLoading.value = false;
return;
}
//
if (!date) {
// 4
maxPlayerCount.value = 4;
const t = [];
peopleText.value = "请选择游玩人数";
t.push({
value: 1,
text: '无需组局'
});
for (let i = 2; i <= 4; i++) {
t.push({
value: i,
text: i + '人'
});
}
peopleRange.value = t;
return;
}
// date
const dateTimestamp = Number(date);
if (isNaN(dateTimestamp) || dateTimestamp <= 0) {
@ -1146,41 +1041,30 @@
gameTypeRange.value = [...config.config.playingMethodOptions];
}
//
if (!options || !options.roomId) {
uni.showToast({
title: '参数错误,请从房间页进入',
icon: 'none'
});
setTimeout(() => {
uni.navigateBack();
}, 1500);
return;
//
if (options && options.roomId) {
//
const roomId = Number(options.roomId);
const roomName = decodeURIComponent(options.roomName || '未知房间');
if (roomId) {
//
reservationInfo.value.room_id = roomId;
reservationInfo.value.room_name = roomName;
//
if (options.date) {
const date = Number(options.date);
if (date) {
//
await loadRoomDetailForReservation(roomId, date);
//
await setDefaultStartTime(date);
}
}
}
}
//
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();