appointment_system/miniprogram/src/pages/appointment/airport-transfer-page.vue
2025-12-22 16:57:26 +08:00

512 lines
15 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>
<view class="page">
<!-- 固定头部 -->
<view class="header">
<view class="center" style="width: 50rpx; height: 50rpx; margin-left: 32rpx;">
<image src="/static/ic_back.png" @click="back" style="width: 48rpx; height: 48rpx;" mode=""></image>
</view>
<text style="font-size: 30rpx;">{{ $t('infoEntry.title') }}</text>
<view style="width: 50rpx;margin-right: 32rpx;"></view>
</view>
<!-- 可滚动内容区域 -->
<view class="scroll-content">
<view class="content">
<!-- 预约登记规则区域 -->
<view class="booking-rules-box" v-if="bookingRules">
<view class="rules-title">预约登记规则</view>
<view class="rules-content">{{ bookingRules }}</view>
</view>
<view class="" style="width: 100%; font-size: 40rpx; padding-left: 54rpx; margin-top: 38rpx;">
{{ $t('infoEntry.personalInfo') }}
</view>
<!-- 真实姓名 -->
<view class="column" :class="{ 'flash-animation': flashingField === 'userName' }"
style="width: 680rpx; margin-top: 38rpx;" id="fieldUserName">
<text style="font-size: 30rpx;"><text
style="color: #FF0000;">*</text>{{ $t('infoEntry.realName') }}</text>
<up-input :placeholder="$t('infoEntry.realNamePlaceholder')" border="surround"
v-model="userName"></up-input>
</view>
<view class="" style="width: 680rpx; height: 2rpx; background-color: #EAEAEA;"></view>
<!-- 微信号 -->
<view class="column" :class="{ 'flash-animation': flashingField === 'contact' }"
style="width: 680rpx; margin-top: 14rpx;" id="fieldContact">
<text style="font-size: 30rpx;"><text style="color: #FF0000;">*</text>{{ $t('infoEntry.wechat') }}
({{ $t('infoEntry.contactMethod') }})</text>
<up-input :placeholder="$t('infoEntry.wechatPlaceholder')" border="surround"
v-model="userWechat"></up-input>
</view>
<view class="" style="width: 680rpx; height: 2rpx; background-color: #EAEAEA;"></view>
<!-- 手机号 -->
<view class="column" :class="{ 'flash-animation': flashingField === 'contact' }"
style="width: 680rpx; margin-top: 14rpx;">
<text style="font-size: 30rpx;"><text style="color: #FF0000;">*</text>{{ $t('infoEntry.phone') }}
({{ $t('infoEntry.contactMethod') }})</text>
<view class="row" style="margin-top: 10rpx; margin-bottom: 10rpx;">
<aure-country-picker v-model="selectedDialCode" :title="$t('infoEntry.selectCountry')"
:height="'70%'" :width="'60vw'" :duration="350" :position="'bottom'" :round="true"
:radius="'24rpx'" :mask-closable="true"></aure-country-picker>
<up-input :placeholder="$t('infoEntry.phonePlaceholder')" border="surround"
v-model="userPhone"></up-input>
</view>
</view>
<view class="" style="width: 680rpx; height: 2rpx; background-color: #EAEAEA;"></view>
<!-- whatsApp -->
<view class="column" :class="{ 'flash-animation': flashingField === 'contact' }"
style="width: 680rpx; margin-top: 14rpx;">
<text style="font-size: 30rpx;"><text style="color: #FF0000;">*</text>{{ $t('infoEntry.whatsapp') }}
({{ $t('infoEntry.contactMethod') }})</text>
<up-input :placeholder="$t('infoEntry.whatsappPlaceholder')" border="surround"
v-model="userWhats"></up-input>
</view>
<view class="" style="width: 680rpx; height: 2rpx; background-color: #EAEAEA;"></view>
<!-- 备注 -->
<view class="column" style="width: 680rpx; margin-top: 14rpx;">
<text style="font-size: 30rpx;">{{ $t('infoEntry.remark') }}</text>
<up-input :placeholder="$t('infoEntry.remarkPlaceholder')" border="surround"
v-model="remark"></up-input>
</view>
<view class="" style="width: 680rpx; height: 2rpx; background-color: #EAEAEA;"></view>
<!-- 服务预约信息 -->
<view class="" style="width: 100%; font-size: 40rpx; padding-left: 54rpx; margin-top: 38rpx;">
{{ $t('infoEntry.serviceInfo') }}
</view>
<!-- 预约日期 -->
<view class="column" :class="{ 'flash-animation': flashingField === 'reservationDate' }"
style="width: 680rpx; margin-top: 14rpx;" id="fieldReservationDate">
<text style="font-size: 30rpx;"><text style="color: #FF0000;">*</text>预约日期</text>
<view class="date-item" @click="openCalendar">
<text class="date-text" :class="{ 'date-placeholder': !reservationDate }">
{{ reservationDate || '请选择预约日期' }}
</text>
<image src="/static/arrow_right2.png" style="width: 32rpx; height: 32rpx;" mode="aspectFit">
</image>
</view>
</view>
<view class="" style="width: 680rpx; height: 2rpx; background-color: #EAEAEA;"></view>
<!-- 机场名称 -->
<view class="column" :class="{ 'flash-animation': flashingField === 'airportName' }"
style="width: 680rpx; margin-top: 14rpx;" id="fieldAirportName">
<text style="font-size: 30rpx;"><text style="color: #FF0000;">*</text>机场名称</text>
<up-input placeholder="请输入机场名称" border="surround" v-model="airportName"></up-input>
</view>
<view class="" style="width: 680rpx; height: 2rpx; background-color: #EAEAEA;"></view>
<!-- 送达地址 -->
<view class="column" :class="{ 'flash-animation': flashingField === 'deliveryAddress' }"
style="width: 680rpx; margin-top: 14rpx;" id="fieldDeliveryAddress">
<text style="font-size: 30rpx;"><text style="color: #FF0000;">*</text>送达地址</text>
<up-input placeholder="请输入送达地址" border="surround" v-model="deliveryAddress"></up-input>
</view>
<view class="" style="width: 680rpx; height: 2rpx; background-color: #EAEAEA;"></view>
<!-- 乘客人数 -->
<view class="column" :class="{ 'flash-animation': flashingField === 'passengerCount' }"
style="width: 680rpx; margin-top: 14rpx;" id="fieldPassengerCount">
<text style="font-size: 30rpx;"><text style="color: #FF0000;">*</text>乘客人数</text>
<view class="counter-row">
<view class="counter-btn" @click="decreaseCount('passenger')">
<text class="counter-icon"></text>
</view>
<text class="counter-value">{{ passengerCount }}人</text>
<view class="counter-btn" @click="increaseCount('passenger')">
<text class="counter-icon"></text>
</view>
</view>
</view>
<view class="" style="width: 680rpx; height: 2rpx; background-color: #EAEAEA;"></view>
<!-- 行李件数 -->
<view class="column" :class="{ 'flash-animation': flashingField === 'luggageCount' }"
style="width: 680rpx; margin-top: 14rpx;" id="fieldLuggageCount">
<text style="font-size: 30rpx;"><text style="color: #FF0000;">*</text>行李件数</text>
<up-input placeholder="请输入行李件数" border="surround" v-model="luggageCount" type="number"></up-input>
</view>
<view class="" style="width: 680rpx; height: 2rpx; background-color: #EAEAEA;"></view>
<view class="center" @click="checkData()"
style="width: 642rpx; height: 72rpx; background-color: #57C9DD; border-radius: 16rpx; box-shadow: 0 0 10rpx 10rpx rgba(0, 0, 0, 0.1); margin-top: 50rpx; margin-bottom: 100rpx;">
{{ $t('common.submit') }}
</view>
</view>
</view>
<!-- 日历选择器 -->
<up-calendar :show="showCalendar" mode="single" :minDate="minDate" :maxDate="maxDate"
@confirm="onCalendarConfirm" @close="closeCalendar" :confirmText="$t('common.confirm')" color="#57C9DD">
</up-calendar>
</view>
</template>
<script>
import { AppServer } from '@/modules/api/AppServer.js'
import bookingRulesMixin from '@/mixins/bookingRulesMixin.js'
const appServer = new AppServer()
export default {
mixins: [bookingRulesMixin],
data() {
return {
serviceId: "",
hotServiceId: "",
source: "",
serviceTitle: "",
userName: "",
userWechat: "",
userPhone: "",
userWhats: "",
remark: "",
reservationDate: "",
airportName: "",
deliveryAddress: "",
passengerCount: 0,
luggageCount: "",
submitting: false,
flashingField: '',
selectedDialCode: '86',
showCalendar: false,
minDate: '',
maxDate: ''
}
},
onLoad(options) {
this.initDateRange()
if (options.id) {
this.source = options.source || ''
if (this.source === 'hot') {
this.hotServiceId = options.id
} else {
this.serviceId = options.id
}
}
if (options.title) {
this.serviceTitle = decodeURIComponent(options.title)
}
// 加载预约规则
this.loadBookingRules('airport_transfer')
},
methods: {
initDateRange() {
const now = new Date()
const year = now.getFullYear()
const month = String(now.getMonth() + 1).padStart(2, '0')
const day = String(now.getDate()).padStart(2, '0')
this.minDate = `${year}-${month}-${day}`
this.maxDate = `${year + 2}-12-31`
},
openCalendar() {
this.initDateRange()
this.$nextTick(() => {
this.showCalendar = true
})
},
closeCalendar() {
this.showCalendar = false
},
onCalendarConfirm(dates) {
if (dates && dates.length > 0) {
this.reservationDate = dates[0]
}
this.showCalendar = false
},
checkData() {
const validations = [{
field: 'userName',
selector: '#fieldUserName',
check: () => !this.userName.trim(),
message: '请输入真实姓名'
},
{
field: 'contact',
selector: '#fieldContact',
check: () => !this.userWechat.trim() && !this.userPhone.trim() && !this.userWhats.trim(),
message: '请至少填写一种联系方式(微信号/手机号/WhatsApp'
},
{
field: 'reservationDate',
selector: '#fieldReservationDate',
check: () => !this.reservationDate,
message: '请选择预约日期'
},
{
field: 'airportName',
selector: '#fieldAirportName',
check: () => !this.airportName.trim(),
message: '请输入机场名称'
},
{
field: 'deliveryAddress',
selector: '#fieldDeliveryAddress',
check: () => !this.deliveryAddress.trim(),
message: '请输入送达地址'
},
{
field: 'passengerCount',
selector: '#fieldPassengerCount',
check: () => this.passengerCount === 0,
message: '请至少选择一位乘客'
},
{
field: 'luggageCount',
selector: '#fieldLuggageCount',
check: () => !this.luggageCount || this.luggageCount === '',
message: '请输入行李件数'
}
]
for (const validation of validations) {
if (validation.check()) {
uni.showToast({
title: validation.message,
icon: 'none'
})
this.scrollToElement(validation.selector)
this.flashingField = validation.field
setTimeout(() => {
this.flashingField = ''
}, 1500)
return
}
}
this.submitAppointment()
},
async submitAppointment() {
if (this.submitting) return
this.submitting = true
uni.showLoading({
title: '提交中...',
mask: true
})
try {
const appointmentData = {
serviceId: this.serviceId || null,
hotServiceId: this.hotServiceId ? parseInt(this.hotServiceId) : null,
serviceType: 'airport_transfer',
realName: this.userName.trim(),
wechatId: this.userWechat.trim() || null,
phone: this.userPhone.trim() || null,
phoneCountryCode: this.userPhone.trim() ? this.selectedDialCode : null,
whatsapp: this.userWhats.trim() || null,
notes: this.remark.trim() || null,
appointmentDate: this.reservationDate,
airportTerminal: this.airportName.trim(),
deliveryAddress: this.deliveryAddress.trim(),
passengerCount: this.passengerCount,
luggageCount: parseInt(this.luggageCount) || 0,
}
const result = await appServer.CreateAppointment(appointmentData)
uni.hideLoading()
if (result.success || result.code === 0) {
uni.showToast({
title: '预约提交成功',
icon: 'success'
})
setTimeout(() => {
uni.navigateBack({
delta: 1
})
}, 1500)
} else {
uni.showToast({
title: result.error?.message || '提交失败,请重试',
icon: 'none'
})
}
} catch (error) {
uni.hideLoading()
console.error('提交预约失败:', error)
uni.showToast({
title: '网络错误,请重试',
icon: 'none'
})
} finally {
this.submitting = false
}
},
scrollToElement(selector) {
const systemInfo = uni.getSystemInfoSync()
const screenHeight = systemInfo.windowHeight
const query = uni.createSelectorQuery().in(this)
query.select(selector).boundingClientRect()
query.selectViewport().scrollOffset()
query.exec((res) => {
if (res[0] && res[1]) {
const rect = res[0]
const scrollInfo = res[1]
const targetScrollTop = scrollInfo.scrollTop + rect.top - (screenHeight / 2) + (rect.height / 2)
uni.pageScrollTo({
scrollTop: Math.max(0, targetScrollTop),
duration: 300
})
}
})
},
back() {
uni.navigateBack({
delta: 1
});
},
increaseCount(type) {
if (type === 'passenger') {
this.passengerCount++
}
},
decreaseCount(type) {
if (type === 'passenger' && this.passengerCount > 0) {
this.passengerCount--
}
}
}
}
</script>
<style lang="scss">
.page {
min-height: 100vh;
background-color: #F3F3F3;
}
.header {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
width: 100%;
padding-top: 88rpx;
padding-bottom: 20rpx;
background-color: #F3F3F3;
position: fixed;
top: 0;
left: 0;
z-index: 100;
}
.scroll-content {
padding-top: 140rpx;
background-color: #F3F3F3;
min-height: 100vh;
}
.content {
display: flex;
flex-direction: column;
align-items: center;
background-color: #F3F3F3;
min-height: 100%;
}
.date-item {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
width: 100%;
height: 80rpx;
padding: 0 10rpx;
box-sizing: border-box;
}
.date-text {
font-size: 28rpx;
color: #333;
}
.date-placeholder {
color: #c0c4cc;
}
@keyframes flash {
0% {
background-color: #F3F3F3;
}
50% {
background-color: #ff6666;
}
100% {
background-color: #F3F3F3;
}
}
.flash-animation {
animation: flash 0.5s ease-in-out 3;
}
.counter-row {
display: flex;
flex-direction: row;
align-items: center;
padding: 20rpx 10rpx;
}
.counter-btn {
width: 50rpx;
height: 50rpx;
display: flex;
align-items: center;
justify-content: center;
background-color: #BFBFBF;
border-radius: 8rpx;
}
.counter-icon {
font-size: 28rpx;
color: #fff;
font-weight: bold;
line-height: 50rpx;
text-align: center;
color: #FFFFFF;
}
.counter-value {
font-size: 28rpx;
color: #333;
min-width: 80rpx;
text-align: center;
}
/* 预约登记规则样式 */
.booking-rules-box {
width: 680rpx;
height: 396rpx;
margin-top: 32rpx;
padding: 30rpx;
background-image: linear-gradient(-45deg, #60D7FF, #68BBD7);
border-radius: 20rpx;
box-shadow: 0 0 10rpx 10rpx rgba(0, 0, 0, 0.1);
box-sizing: border-box;
overflow-y: auto;
}
.rules-title {
font-size: 32rpx;
font-weight: bold;
color: #fff;
margin-bottom: 20rpx;
}
.rules-content {
font-size: 26rpx;
color: #fff;
line-height: 1.6;
white-space: pre-wrap;
}
</style>