This commit is contained in:
zpc 2025-09-14 00:07:26 +08:00
parent 35ee8ea96c
commit 3648e6247f
9 changed files with 349 additions and 127 deletions

View File

@ -80,7 +80,7 @@ function calculateTimeRange(time1, time2) {
* @returns {string} - 返回 '今天' | '明天' | '后天' | '其他'
*/
function getDayDescription(input) {
if (input == null || input == "" || input == 0) {
return "请选择时间";
@ -103,4 +103,30 @@ function getDayDescription(input) {
return target.format('MM-DD ' + tips + ' HH:mm')
}
export { parseTimeString, formatTime, calculateTimeRange, getDayDescription }
/**
* 将传入的时间的分钟数 向上取整到最近的 5 的倍数
* 比如1 56 1058 00且进位到下一小时59 00进位
* @param {Date|number|string|dayjs} inputTime - 可以是时间戳Date 对象ISO 字符串dayjs 对象
* @returns {dayjs} 返回调整后的 dayjs 对象分钟是 5 的倍数且进位正确
*/
function ceilMinuteToNext5(inputTime) {
const t = dayjs(inputTime) // 支持多种时间格式
const m = t.minute()
const s = t.second()
// 向上取整到最近的 5 的倍数
const nextMin = Math.ceil(m / 5) * 5
if (nextMin === 60) {
// 59 → 60 → 变成下一小时的 00 分
return t.add(1, 'hour').minute(0).second(0)
}
// 否则正常设置分钟,并清空秒
return t.minute(nextMin).second(0)
}
export { parseTimeString, formatTime, calculateTimeRange, getDayDescription, ceilMinuteToNext5 }

3
components.d.ts vendored
View File

@ -8,9 +8,11 @@ export {}
/* prettier-ignore */
declare module 'vue' {
export interface GlobalComponents {
CardContainer: typeof import('./components/com/appointment/card-container.vue')['default']
Container: typeof import('./components/com/page/container.vue')['default']
ContainerBase: typeof import('./components/com/page/container-base.vue')['default']
LabelField: typeof import('./components/com/appointment/label-field.vue')['default']
LabelSlectField: typeof import('./components/com/appointment/label-slect-field.vue')['default']
MahjongCard: typeof import('./components/index/MahjongCard.vue')['default']
NoData: typeof import('./components/com/page/no-data.vue')['default']
NoEmpty: typeof import('./components/com/index/NoEmpty.vue')['default']
@ -19,6 +21,7 @@ declare module 'vue' {
ReservationPopup: typeof import('./components/com/index/ReservationPopup.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
TagSelect: typeof import('./components/com/appointment/tag-select.vue')['default']
TimeSelectCell: typeof import('./components/com/appointment/time-select-cell.vue')['default']
UniNavBar: typeof import('./components/uni-nav-bar/uni-nav-bar.vue')['default']
UniStatusBar: typeof import('./components/uni-nav-bar/uni-status-bar.vue')['default']

View File

@ -0,0 +1,41 @@
<template>
<view class="card column" :style="cardStyle">
<slot />
</view>
</template>
<script setup>
import { computed } from 'vue'
const props = defineProps({
width: { type: String, default: '90%' },
bgColor: { type: String, default: '#FFFFFF' },
radius: { type: String, default: '10rpx' },
marginTop: { type: String, default: '20rpx' },
padding: { type: String, default: '20rpx 0rpx 30rpx 0rpx' },
boxShadow: { type: Boolean, default: true },
justify: { type: String, default: 'center' }
})
const cardStyle = computed(() => {
const style = {
width: props.width,
backgroundColor: props.bgColor,
borderRadius: props.radius,
margin: `${props.marginTop} auto 0`,
display: 'flex',
justifyContent: props.justify
}
if (props.padding) {
style.padding = props.padding
}
if (props.boxShadow) {
style.boxShadow = '0 0 10px 3px rgba(0, 0, 0, 0.1)'
}
return style
})
</script>
<style scoped lang="scss"></style>

View File

@ -0,0 +1,65 @@
<template>
<view class="label-field">
<view class="spacer-20"></view>
<view class="label-wrap">
<text class="label-text">{{ label }}</text>
</view>
<view class="flex-1">
<view class="label-field__body">
<slot />
</view>
</view>
<view class="spacer-20"></view>
</view>
</template>
<script setup>
const props = defineProps({
label: { type: String, default: '' }
})
</script>
<style scoped lang="scss">
.label-field {
width: 100%;
display: flex;
}
.spacer-20 {
width: 20rpx;
}
.flex-1 {
flex: 1;
}
.label-wrap {
width: 130rpx;
line-height: 60rpx;
height: 60rpx;
}
.label-field__label {
font-size: 26rpx;
margin-top: 20rpx;
font-family: PingFang SC, PingFang SC;
font-weight: 500;
color: #515151;
text-align: left;
font-style: normal;
text-transform: none;
}
.label-text {
font-size: 28rpx;
color: #515151;
font-weight: 500;
text-transform: none;
font-style: normal;
font-family: PingFang SC, PingFang SC;
}
.label-field__body {
width: 100%;
}
</style>

View File

@ -0,0 +1,54 @@
<template>
<view class="tag-select">
<view class="tags-row">
<view v-for="opt in options" :key="opt" class="tag" :class="{ selected: isSelected(opt) }"
@click="onClick(opt)">
{{ opt }}
</view>
</view>
</view>
</template>
<script setup>
const props = defineProps({
options: { type: Array, default: () => [] },
modelValue: { type: String, default: '' }
})
const emit = defineEmits(['update:modelValue', 'change'])
const isSelected = (opt) => props.modelValue === opt
const onClick = (opt) => {
if (opt === props.modelValue) return
emit('update:modelValue', opt)
emit('change', opt)
}
</script>
<style scoped lang="scss">
.tags-row {
display: flex;
align-items: center;
flex-wrap: wrap;
}
.tag {
margin-right: 16rpx;
margin-bottom: 12rpx;
padding: 8rpx 18rpx;
font-size: 24rpx;
line-height: 30rpx;
height: 30rpx;
border-radius: 8rpx;
background-color: #DADADA;
color: #5B5B5B;
border: 1rpx solid #DADADA;
}
.tag.selected {
background-color: #00AC4E;
color: #FFFFFF;
border-color: #00AC4E;
}
</style>

View File

@ -1,22 +1,19 @@
<template>
<view class="row time-row">
<view class="label-wrap">
<text class="label-text">{{ label }}</text>
</view>
<view class="value-wrap">
<view class="cell-box" @click="onSelect">
<view class="content-flex">
<slot />
</view>
<view class="icon-wrap">
<image :src="icon" class="icon-img"></image>
</view>
<label-slect-field :label="label">
<view class="cell-box" @click="onSelect">
<view class="content-flex">
<slot />
</view>
<view class="icon-wrap">
<image :src="icon" class="icon-img"></image>
</view>
</view>
</view>
</label-slect-field>
</template>
<script setup>
import { labelSlectField } from '@/components/com/appointment/label-slect-field.vue'
const props = defineProps({
label: { type: String, default: '' },
icon: { type: String, default: '' }
@ -32,7 +29,7 @@ const onSelect = () => emit('select')
.label-wrap {
width: 130rpx;
line-height: 50rpx;
line-height: 60rpx;
height: 60rpx;
}

View File

@ -4,10 +4,11 @@
<reservation-evaluate ref="_baseEvaluatePop" />
<!-- 预约信息弹窗组件 -->
<ReservationPopup ref="_reservationPopup" />
<up-datetime-picker hasInput :show="_upDatesTimePicker.show" :filter="_upDatesTimePicker.filter"
<up-datetime-picker :show="_upDatesTimePicker.show" :filter="_upDatesTimePicker.filter"
:formatter="_upDatesTimePicker.formatter" v-model="_upDatesTimePicker.value" mode="datetime"
:minDate="_upDatesTimePicker.minDate" @cancel="_upDatesTimePicker.onCancel"
@confirm="_upDatesTimePicker.onConfirm"></up-datetime-picker>
@confirm="_upDatesTimePicker.onConfirm" :title="_upDatesTimePicker.title"
:hasInput="false"></up-datetime-picker>
</view>
</template>
@ -30,6 +31,7 @@ const _upDatesTimePicker = ref({
refId: null,
show: false,
value: Date.now(),
title: '',
minDate: Date.now(),
filter: (mode, options) => {
// console.log("filter", mode, options)
@ -73,7 +75,7 @@ const _upDatesTimePicker = ref({
onError: null
});
const openUpDatesTimePicker = (value, minDate) => {
const openUpDatesTimePicker = (value, minDate, title) => {
return new Promise((resolve, reject) => {
// 1.
const now = dayjs();
@ -94,6 +96,11 @@ const openUpDatesTimePicker = (value, minDate) => {
} else {
_upDatesTimePicker.value.minDate = timestamp;
}
if (title != null) {
_upDatesTimePicker.value.title = title;
} else {
_upDatesTimePicker.value.title = '';
}
_upDatesTimePicker.value.show = true;
})
}

View File

@ -4,147 +4,127 @@
<text style="margin-top: 100rpx; text-align: center;">发起预约</text>
<view class=""
style="width: 90%; background-color: #FFFFFF; border-radius: 10rpx; margin: 30rpx auto 0; display: flex; justify-content: center; box-shadow: 0 0 10px 3px rgba(0, 0, 0, 0.1);">
<view class="column" style="width: 95%; margin-bottom: 20rpx; margin-top: 25rpx;">
<time-select-cell label="开始时间" icon="@@:app/static/time_start.png" @select="openUpDatesTimePicker">
{{ getDayDescription(startTimeStr) }}
</time-select-cell>
<view style="height:20rpx;"></view>
<time-select-cell label="结束时间" icon="@@:app/static/time_end.png" @select="openUpDatesTimePickerEnd">
{{ getDayDescription(endTimeStr) }}
</time-select-cell>
</view>
</view>
<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="openUpDatesTimePickerEnd">
<up-picker-data v-model="reservationInfo.room_id" title="请选择房间" :options="[]" valueKey="id"
labelKey="name">
</up-picker-data>
</time-select-cell>
</card-container>
<view class="column"
style="width: 90%; border-radius: 10rpx; margin: 40rpx auto 0; background-color: #FFFFFF; box-shadow: 0 0 10px 3px rgba(0, 0, 0, 0.1);">
<card-container marginTop="30rpx">
<label-field label="组局名称">
<view class="" style="width:100%;margin-top: 10rpx;">
<view style="width:100%; ">
<input class="uni-input" placeholder="请输入组局名称" style="font-size: 24rpx; width: 89%;" />
</view>
</label-field>
<label-field label="房间">
<uni-data-select class="custom-select" v-model="roomValue" :localdata="range" @change="change"></uni-data-select>
</label-field>
<view :style="{ height: lineHeight }"></view>
<label-field label="人数">
<uni-data-select v-model="peopleValue" :localdata="peopleRange" @change="change"></uni-data-select>
<uni-data-select v-model="peopleValue" :localdata="peopleRange"
@change="changeLog"></uni-data-select>
</label-field>
<view :style="{ height: lineHeight }"></view>
<label-field label="玩法类型">
<uni-data-select v-model="peopleValue" :localdata="peopleRange" @change="change"></uni-data-select>
<uni-data-select v-model="peopleValue" :localdata="peopleRange"
@change="changeLog"></uni-data-select>
</label-field>
<view :style="{ height: lineHeight }"></view>
<label-field label="具体规则">
<uni-data-select v-model="peopleValue" :localdata="peopleRange" @change="change"></uni-data-select>
<uni-data-select v-model="peopleValue" :localdata="peopleRange"
@change="changeLog"></uni-data-select>
</label-field>
<view :style="{ height: lineHeight }"></view>
<label-field label="其他补充">
<view class="" style="width: 89%; background-color: white; border-radius: 10rpx; height: 70rpx; display: flex; align-items: center; padding-left: 20rpx; border: 1rpx solid #515151;">
<view
style="width: 89%; background-color: white; border-radius: 10rpx; height: 70rpx; display: flex; align-items: center; padding-left: 20rpx; border: 1rpx solid #515151;">
<input class="uni-input" placeholder="请输入补充信息" style="font-size: 24rpx; width: 100%;" />
</view>
</label-field>
</view>
</card-container>
<view class="column"
style="width: 90%; border-radius: 10rpx; margin: 40rpx auto 0; background-color: #FFFFFF; box-shadow: 0 0 10px 3px rgba(0, 0, 0, 0.1);">
<view class="row" style="margin-top: 20rpx; margin-left: 20rpx;">
<view class="" style="width: 120rpx; font-size: 26rpx; align-items: center;">
是否禁烟
</view>
<card-container marginTop="30rpx">
<radio-group class="" style="margin-left: 50rpx;">
<label class="" style="font-size: 24rpx;">
<label-slect-field label="是否禁烟">
<radio-group @change="() => { }">
<label style="font-size: 24rpx;margin-right:16rpx;">
<radio value="r1" color="#00AC4E" style="transform:scale(0.7);" :checked="true" />禁烟
</label>
<label class="" style="font-size: 24rpx; margin-left: 90rpx;">
<label style="font-size: 24rpx; margin-right:16rpx;">
<radio value="r2" color="#00AC4E" style="transform:scale(0.7);" />不禁烟
</label>
</radio-group>
</view>
<view class="row" style="margin-top: 20rpx; margin-left: 20rpx;">
<view class="" style="width: 120rpx; font-size: 26rpx; align-items: center;">
性别
</view>
<radio-group class="" style="margin-left: 50rpx;">
<label class="" style="font-size: 24rpx;">
</label-slect-field>
<view :style="{ height: lineHeight }"></view>
<label-slect-field label="性别">
<radio-group @change="() => { }">
<label style="font-size: 24rpx; margin-right:16rpx;">
<radio value="r1" color="#00AC4E" style="transform:scale(0.7);" :checked="true" />不限
</label>
<label class="" style="font-size: 24rpx; margin-left: 90rpx;">
<label style="font-size: 24rpx; margin-right:16rpx;">
<radio value="r2" color="#00AC4E" style="transform:scale(0.7);" />
</label>
<label class="" style="font-size: 24rpx; margin-left: 90rpx;">
<label style="font-size: 24rpx; margin-right:16rpx;">
<radio value="r3" color="#00AC4E" style="transform:scale(0.7);" />
</label>
</radio-group>
</view>
<view class="row" style="margin-top: 20rpx; margin-left: 20rpx; margin-bottom: 20rpx;">
<view class="" style="width: 120rpx; font-size: 26rpx; align-items: center;">
信誉
</view>
<text style="font-size: 25.86rpx; margin-left: 55rpx;">大于等于</text>
<view class="counter-container">
<!-- 减号按钮 -->
<view class="" @click="decrement" :disabled="currentValue <= 0">
<uni-icons type="minus" size="24" color="#00AC4E" />
</view>
<!-- 数值显示 -->
<view class="counter-value">
{{ currentValue.toFixed(1) }}
</view>
<!-- 加号按钮 -->
<view class="" @click="increment" :disabled="currentValue >= 5">
<uni-icons type="plus" size="24" color="#00AC4E" />
</label-slect-field>
<view :style="{ height: lineHeight }"></view>
<label-slect-field label="信誉">
<view style="display:flex;align-items:center;">
<text style="font-size: 25.86rpx;width: 120rpx;">大于等于</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>
</view>
<card-container marginTop="30rpx">
</view>
<view class="column"
style="width: 90%; border-radius: 10rpx; margin: 20rpx auto 0; background-color: #FFFFFF; box-shadow: 0 0 10px 3px rgba(0, 0, 0, 0.1);">
<view class="row" style="margin-top: 20rpx; margin-left: 20rpx;">
<view class="" style=" font-size: 26rpx; align-items: center;">
鸽子费保证金
</view>
<radio-group class="" style="margin-left: 50rpx;">
<label class="" style="font-size: 24rpx;">
<label-slect-field label="鸽子费">
<radio-group @change="() => { }">
<label style="font-size: 24rpx;margin-right:16rpx;">
<radio value="r1" color="#00AC4E" style="transform:scale(0.7);" :checked="true" />0
</label>
<label class="" style="font-size: 24rpx;">
<label style="font-size: 24rpx;margin-right:16rpx;">
<radio value="r2" color="#00AC4E" style="transform:scale(0.7);" />5
</label>
<label class="" style="font-size: 24rpx;">
<label style="font-size: 24rpx;margin-right:16rpx;">
<radio value="r3" color="#00AC4E" style="transform:scale(0.7);" />10
</label>
</radio-group>
</view>
</label-slect-field>
<view :style="{ height: lineHeight }"></view>
<text
style="font-size: 24rpx; margin-left: 20rpx; margin-bottom: 20rpx;">除发起者外其他参与人需缴纳鸽子费若有参与者在预约后没有赴约其鸽子费由在场的所有人平分组局成功或失败后鸽子费将全额返还</text>
</view>
style="font-size: 24rpx; margin-left: 20rpx; margin-bottom: 20rpx;">鸽子费保证金参与人需缴纳鸽子费若有参与者在预约后没有赴约其鸽子费由在场的所有人平分组局成功或失败后鸽子费将全额返还</text>
</card-container>
<view class="center"
style="width: 90%; border-radius: 10rpx; margin: 20rpx auto 0; background-color: #00AC4E;">
style="width: 90%; border-radius: 10rpx; margin: 30rpx auto 0; background-color: #00AC4E;">
<text style="margin: 20rpx; color: white;">发起预约</text>
</view>
@ -160,11 +140,13 @@
<script setup>
import { ref } from 'vue';
import { getDayDescription } from '@/common/system/timeUtile';
import { getDayDescription, ceilMinuteToNext5 } from '@/common/system/timeUtile';
import { 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'
const _containerBase = ref(null)
const startTimeStr = ref(0)
const endTimeStr = ref(0)
@ -172,14 +154,54 @@ const hours = ref("")
const minutes = ref("")
const roomValue = ref("")
const peopleValue = ref("")
const lineHeight = ref("15rpx")
const timeRange = ref(["2小时", "3小时", "4小时", "自定义"])
const timeRangeValue = ref("")
//
const reservationInfo = ref({
room_id: 0,//id
start_time: 0,//
end_time: 0,//
max_age: 0,//
min_age: 0,//
title: '',//
extra_info: '',//
game_rule: '',//
game_type: '',//
gender_limit: 1,//
is_smoking: 0,//
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();
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();
}
}
}
}
const openUpDatesTimePicker = async () => {
var now = startTimeStr.value;
if (startTimeStr.value == 0) {
now = Date.now();
now += 1000 * 60 * 30;
now = ceilMinuteToNext5(now).valueOf();
}
const startTime = await _containerBase.value.openUpDatesTimePicker(now, now)
const startTime = await _containerBase.value.openUpDatesTimePicker(now, now, "预约开始时间")
startTimeStr.value = startTime
}
const openUpDatesTimePickerEnd = async () => {
@ -190,9 +212,12 @@ const openUpDatesTimePickerEnd = async () => {
})
return;
}
var now = (startTimeStr.value + 1000 * 60 * 30);
var now = endTimeStr.value;
if (now == 0) {
now = (startTimeStr.value + 1000 * 60 * 30);
}
//minDate+1000*60*30 30
const endTime = await _containerBase.value.openUpDatesTimePicker(now, now)
const endTime = await _containerBase.value.openUpDatesTimePicker(now, now, "预约结束时间")
endTimeStr.value = endTime
}
@ -266,7 +291,6 @@ const setTime = () => {
display: flex;
align-items: center;
justify-content: center;
margin-left: 90rpx;
}
.counter-btn {

View File

@ -371,17 +371,22 @@ const myUseReservation = ref([]);
// -
const loadCurrentAppointment = async () => {
var res = await sqInterface.getMyUseReservation();
console.log("getMyUseReservation", res);
myUseReservation.value = myUseReservation.value.splice(0, myUseReservation.value.length);
myUseReservation.value.push(...res);
var _isLogin = await isLogin();
if (!_isLogin) {
return;
}
try {
//
currentAppointment.value = null
var res = await sqInterface.getMyUseReservation();
console.log("getMyUseReservation", res);
if (res != null) {
myUseReservation.value = myUseReservation.value.splice(0, myUseReservation.value.length);
myUseReservation.value.push(...res);
}
} catch (error) {
console.error('加载预约信息失败:', error)
currentAppointment.value = null
}
}
@ -397,7 +402,7 @@ onShow(() => {
})
onLoad(async () => {
await loadUserInfo();
});
</script>