appointment_system/miniprogram/src/pages/appointment/airfare-info-entry-page.vue
2025-12-20 23:19:40 +08:00

744 lines
23 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 v-else class=""
style="width: 680rpx; height: 396rpx; background-image: linear-gradient(-45deg, #60D7FF, #68BBD7); margin-top: 32rpx; border-radius: 20rpx; box-shadow: 0 0 10rpx 10rpx rgba(0, 0, 0, 0.1);">
</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" style="width: 680rpx; margin-top: 14rpx;">
<text style="font-size: 30rpx;"><text style="color: #FF0000;">*</text>单程 / 往返</text>
<view class="date-item" @click="showTripPicker = true">
<text class="date-text">{{ tripTypeText }}</text>
<image src="/static/arrow_down.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 === 'departureDate' }"
style="width: 680rpx; margin-top: 14rpx;" id="fieldDepartureDate">
<text style="font-size: 30rpx;"><text
style="color: #FF0000;">*</text>{{ $t('infoEntry.departureDate') }}</text>
<view class="date-item" @click="openDepartureCalendar">
<text class="date-text" :class="{ 'date-placeholder': !departureDate }">
{{ departureDate || $t('infoEntry.departureDatePlaceholder') }}
</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 v-if="tripType === 'round'" class="column"
:class="{ 'flash-animation': flashingField === 'returnDate' }"
style="width: 680rpx; margin-top: 14rpx;" id="fieldReturnDate">
<text style="font-size: 30rpx;"><text style="color: #FF0000;">*</text>返程日期</text>
<view class="date-item" @click="openReturnCalendar">
<text class="date-text" :class="{ 'date-placeholder': !returnDate }">
{{ returnDate || '请选择返程日期' }}
</text>
<image src="/static/arrow_right2.png" style="width: 32rpx; height: 32rpx;" mode="aspectFit">
</image>
</view>
</view>
<view v-if="tripType === 'round'" class=""
style="width: 680rpx; height: 2rpx; background-color: #EAEAEA;"></view>
<!-- 出发城市 -->
<view class="column" :class="{ 'flash-animation': flashingField === 'departureCity' }"
style="width: 680rpx; margin-top: 14rpx;" id="fieldDepartureCity">
<text style="font-size: 30rpx;"><text style="color: #FF0000;">*</text>出发城市</text>
<up-input placeholder="请输入出发城市" border="surround" v-model="departureCity"></up-input>
</view>
<view class="" style="width: 680rpx; height: 2rpx; background-color: #EAEAEA;"></view>
<!-- 到达城市 -->
<view class="column" :class="{ 'flash-animation': flashingField === 'arrivalCity' }"
style="width: 680rpx; margin-top: 14rpx;" id="fieldArrivalCity">
<text style="font-size: 30rpx;"><text style="color: #FF0000;">*</text>到达城市</text>
<up-input placeholder="请输入到达城市" border="surround" v-model="arrivalCity"></up-input>
</view>
<view class="" style="width: 680rpx; height: 2rpx; background-color: #EAEAEA;"></view>
<!-- 人数 -->
<view class="column" :class="{ 'flash-animation': flashingField === 'personCount' }"
style="width: 680rpx; margin-top: 14rpx;" id="fieldPersonCount">
<text style="font-size: 30rpx;"><text style="color: #FF0000;">*</text>人数</text>
<!-- 成人 -->
<view class="person-row">
<view class="person-info">
<text class="person-title">成人</text>
<text class="person-desc">13岁或以上</text>
</view>
<view class="person-counter">
<view class="counter-btn" @click="decreaseCount('adult')">
<text class="counter-icon"></text>
</view>
<text class="counter-value">{{ adultCount }}人</text>
<view class="counter-btn" @click="increaseCount('adult')">
<text class="counter-icon"></text>
</view>
</view>
</view>
<!-- 儿童 -->
<view class="person-row">
<view class="person-info">
<text class="person-title">儿童</text>
<text class="person-desc">2~12岁</text>
</view>
<view class="person-counter">
<view class="counter-btn" @click="decreaseCount('child')">
<text class="counter-icon"></text>
</view>
<text class="counter-value">{{ childCount }}人</text>
<view class="counter-btn" @click="increaseCount('child')">
<text class="counter-icon"></text>
</view>
</view>
</view>
<!-- 婴儿 -->
<view class="person-row">
<view class="person-info">
<text class="person-title">婴儿</text>
<text class="person-desc">2岁以下</text>
</view>
<view class="person-counter">
<view class="counter-btn" @click="decreaseCount('infant')">
<text class="counter-icon"></text>
</view>
<text class="counter-value">{{ infantCount }}人</text>
<view class="counter-btn" @click="increaseCount('infant')">
<text class="counter-icon"></text>
</view>
</view>
</view>
</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;"><text style="color: #FF0000;">*</text>舱位选择</text>
<view class="date-item" @click="showCabinPicker = true">
<text class="date-text">{{ cabinTypeText }}</text>
<image src="/static/arrow_down.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 === '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-picker :show="showTripPicker" :columns="tripTypeColumns" @confirm="onTripTypeConfirm"
@cancel="showTripPicker = false" @close="showTripPicker = false" :defaultIndex="[tripTypeIndex]"
keyName="label">
</up-picker>
<!-- 舱位选择器 -->
<up-picker :show="showCabinPicker" :columns="cabinTypeColumns" @confirm="onCabinTypeConfirm"
@cancel="showCabinPicker = false" @close="showCabinPicker = false" :defaultIndex="[cabinTypeIndex]"
keyName="label">
</up-picker>
<!-- 日历选择器 -->
<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: "", // 服务ID来自分类页
hotServiceId: "", // 热门服务ID来自热门服务
source: "", // 来源hot=热门服务,其他=分类页
serviceTitle: "", // 服务标题
userName: "",
userWechat: "",
userPhone: "",
userWhats: "",
remark: "",
departureDate: "",
returnDate: "",
departureCity: "",
arrivalCity: "",
luggageCount: "",
adultCount: 0,
childCount: 0,
infantCount: 0,
calendarType: "departure",
tripType: "single",
tripTypeIndex: 0,
submitting: false, // 提交状态
tripTypeColumns: [
[{
label: '单程',
value: 'single'
},
{
label: '往返',
value: 'round'
}
]
],
cabinType: "economy",
cabinTypeIndex: 0,
cabinTypeColumns: [
[{
label: '经济舱',
value: 'economy'
},
{
label: '超级经济舱',
value: 'premium_economy'
},
{
label: '商务舱',
value: 'business'
}
]
],
flashingField: '',
selectedDialCode: '86',
showCalendar: false,
showTripPicker: false,
showCabinPicker: false,
minDate: '',
maxDate: ''
}
},
computed: {
tripTypeText() {
const item = this.tripTypeColumns[0].find(t => t.value === this.tripType)
return item ? item.label : '单程'
},
cabinTypeText() {
const item = this.cabinTypeColumns[0].find(t => t.value === this.cabinType)
return item ? item.label : '经济舱'
}
},
onLoad(options) {
this.initDateRange()
// 获取页面参数
if (options.id) {
this.source = options.source || ''
// 根据来源判断是 serviceId 还是 hotServiceId
if (this.source === 'hot') {
this.hotServiceId = options.id
} else {
this.serviceId = options.id
}
}
if (options.title) {
this.serviceTitle = decodeURIComponent(options.title)
}
// 加载预约规则
this.loadBookingRules('flight')
},
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`
},
openDepartureCalendar() {
console.log('打开出发日期选择器')
this.calendarType = 'departure'
this.initDateRange()
this.$nextTick(() => {
this.showCalendar = true
})
},
openReturnCalendar() {
console.log('打开返程日期选择器')
this.calendarType = 'return'
// 返程日期最小值为出发日期
if (this.departureDate) {
this.minDate = this.departureDate
} else {
this.initDateRange()
}
this.$nextTick(() => {
this.showCalendar = true
})
},
closeCalendar() {
this.showCalendar = false
this.initDateRange()
},
onCalendarConfirm(dates) {
console.log('日历确认:', dates)
if (dates && dates.length > 0) {
if (this.calendarType === 'departure') {
this.departureDate = dates[0]
// 如果返程日期早于出发日期,清空返程日期
if (this.returnDate && this.returnDate < dates[0]) {
this.returnDate = ''
}
} else {
this.returnDate = dates[0]
}
}
this.showCalendar = false
this.initDateRange()
},
onTripTypeConfirm(e) {
const selected = e.value[0]
this.tripType = selected.value
this.tripTypeIndex = this.tripTypeColumns[0].findIndex(t => t.value === selected.value)
this.showTripPicker = false
},
onCabinTypeConfirm(e) {
const selected = e.value[0]
this.cabinType = selected.value
this.cabinTypeIndex = this.cabinTypeColumns[0].findIndex(t => t.value === selected.value)
this.showCabinPicker = 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: 'departureDate',
selector: '#fieldDepartureDate',
check: () => !this.departureDate,
message: '请选择出发日期'
},
{
field: 'returnDate',
selector: '#fieldReturnDate',
check: () => this.tripType === 'round' && !this.returnDate,
message: '请选择返程日期'
},
{
field: 'departureCity',
selector: '#fieldDepartureCity',
check: () => !this.departureCity.trim(),
message: '请输入出发城市'
},
{
field: 'arrivalCity',
selector: '#fieldArrivalCity',
check: () => !this.arrivalCity.trim(),
message: '请输入到达城市'
},
{
field: 'personCount',
selector: '#fieldPersonCount',
check: () => this.adultCount === 0 && this.childCount === 0 && this.infantCount === 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: 'flight', // 机票类型
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,
// 机票专用字段
tripType: this.tripType,
departureDate: this.departureDate,
returnDate: this.tripType === 'round' ? this.returnDate : null,
departureCity: this.departureCity.trim(),
arrivalCity: this.arrivalCity.trim(),
adultCount: this.adultCount,
childCount: this.childCount,
infantCount: this.infantCount,
cabinType: this.cabinType,
luggageCount: parseInt(this.luggageCount) || 0,
}
const result = await appServer.CreateAppointment(appointmentData)
uni.hideLoading()
// 兼容两种返回格式: { success: true } 或 { code: 0 }
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]
// 当前滚动位置 + 元素相对视口的top - 屏幕高度的一半 + 元素高度的一半
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 === 'adult') {
this.adultCount++
} else if (type === 'child') {
this.childCount++
} else if (type === 'infant') {
this.infantCount++
}
},
decreaseCount(type) {
if (type === 'adult' && this.adultCount > 0) {
this.adultCount--
} else if (type === 'child' && this.childCount > 0) {
this.childCount--
} else if (type === 'infant' && this.infantCount > 0) {
this.infantCount--
}
}
}
}
</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;
}
.person-row {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
width: 100%;
padding: 20rpx 10rpx;
}
.person-info {
display: flex;
flex-direction: column;
}
.person-title {
font-size: 28rpx;
color: #333;
font-weight: 500;
}
.person-desc {
font-size: 24rpx;
color: #999;
margin-top: 6rpx;
}
.person-counter {
display: flex;
flex-direction: row;
align-items: center;
}
.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;
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;
}
.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>