appointment_system/pages/appointment/info-entry-page.vue
2025-12-15 23:42:31 +08:00

625 lines
19 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=""
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>
export default {
data() {
return {
userName: "",
userWechat: "",
userPhone: "",
userWhats: "",
remark: "",
departureDate: "",
returnDate: "",
departureCity: "",
arrivalCity: "",
luggageCount: "",
adultCount: 0,
childCount: 0,
infantCount: 0,
calendarType: "departure",
tripType: "single",
tripTypeIndex: 0,
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() {
this.initDateRange()
},
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
}
}
// 所有验证通过
uni.showToast({
title: this.$t('infoEntry.hasData'),
icon: 'none'
})
// TODO: 提交数据逻辑
},
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;
}
</style>