appointment_system/miniprogram/src/pages/me/my-appointments-page.vue

672 lines
25 KiB
Vue

<template>
<view class="content">
<!-- 顶部导航栏 -->
<view class="header">
<view class="status-bar" :style="{ height: statusBarHeight + 'px' }"></view>
<view class="header-content">
<view class="back-button" @click="goBack">
<image src="/static/ic_back.png" class="back-icon"></image>
</view>
<text class="header-title">{{ $t('myAppointment.title') || '我的预约单' }}</text>
<view class="header-placeholder"></view>
</view>
</view>
<!-- Tab 区域 -->
<view class="tab-container">
<view class="tab-item" :class="{ active: activeTab === 'all' }" @click="switchTab('all')">
<text class="tab-text" :class="{ active: activeTab === 'all' }">{{ $t('myAppointment.all') || '全部' }}</text>
</view>
<view class="tab-item" :class="{ active: activeTab === 'inProgress' }" @click="switchTab('inProgress')">
<text class="tab-text" :class="{ active: activeTab === 'inProgress' }">{{ $t('myAppointment.inProgress') || '进行中' }}</text>
</view>
<view class="tab-item" :class="{ active: activeTab === 'completed' }" @click="switchTab('completed')">
<text class="tab-text" :class="{ active: activeTab === 'completed' }">{{ $t('myAppointment.completed') || '已结束' }}</text>
</view>
</view>
<!-- 预约列表 -->
<scroll-view class="appointment-list" scroll-y @scrolltolower="loadMore" :show-scrollbar="false">
<view class="appointment-item" v-for="item in appointmentList" :key="item.id" @click="showDetail(item)">
<view class="item-title">{{ getServiceTitle(item) }}</view>
<view class="item-row">
<image src="/static/me_tiem.png" class="item-icon"></image>
<text class="item-text">{{ formatDate(item.createdAt) }}</text>
</view>
<view class="item-row">
<image src="/static/me_name.png" class="item-icon"></image>
<text class="item-text">{{ item.realName || '-' }}</text>
</view>
<view class="item-row">
<image src="/static/me_phone.png" class="item-icon"></image>
<text class="item-text">{{ getContactInfo(item) }}</text>
</view>
<view class="item-footer">
<text class="view-detail" @click.stop="showDetail(item)">{{ $t('myAppointment.viewDetail') || '查看详情' }}</text>
<text class="status-text" :class="getStatusClass(item.status)">{{ getStatusText(item.status) }}</text>
</view>
</view>
<view v-if="loading" class="loading-more">
<text class="loading-text">{{ $t('common.loading') }}</text>
</view>
<view v-if="!loading && !hasMore && appointmentList.length > 0" class="loading-more">
<text class="loading-text">{{ $t('common.noMore') || '没有更多了' }}</text>
</view>
<view v-if="appointmentList.length === 0 && !loading" class="empty-state">
<text class="empty-icon">📋</text>
<text class="no-data">{{ $t('myAppointment.noAppointment') || '暂无预约记录' }}</text>
</view>
</scroll-view>
<!-- 详情弹窗 -->
<view class="detail-mask" v-if="showDetailPopup" @click="closeDetail">
<view class="detail-popup" @click.stop>
<view class="popup-header">
<text class="popup-title">{{ $t('myAppointment.serviceDetail') || '服务详情' }}</text>
<view class="popup-close" @click="closeDetail">
<image src="/static/ic_colse.png" class="close-icon"></image>
</view>
</view>
<scroll-view class="popup-content" scroll-y>
<!-- 服务类型 -->
<view class="detail-item">
<text class="detail-label">服务类型</text>
<text class="detail-value">{{ getServiceTitle(currentDetail) }}</text>
</view>
<!-- 姓名 -->
<view class="detail-item">
<text class="detail-label">姓名</text>
<text class="detail-value">{{ currentDetail.realName || '-' }}</text>
</view>
<!-- 提交时间 -->
<view class="detail-item">
<text class="detail-label">提交时间</text>
<text class="detail-value">{{ formatDate(currentDetail.createdAt) }}</text>
</view>
<!-- 联系方式 -->
<view class="detail-item">
<text class="detail-label">联系方式</text>
<text class="detail-value">{{ getAllContactInfo(currentDetail) }}</text>
</view>
<!-- 预约日期(通用字段) -->
<view class="detail-item" v-if="currentDetail.appointmentDate">
<text class="detail-label">预约日期</text>
<text class="detail-value">{{ currentDetail.appointmentDate }}</text>
</view>
<!-- 机票相关字段 -->
<view class="detail-item" v-if="currentDetail.tripType">
<text class="detail-label">行程类型</text>
<text class="detail-value">{{ currentDetail.tripType === 'single' ? '单程' : '往返' }}</text>
</view>
<view class="detail-item" v-if="currentDetail.departureCity">
<text class="detail-label">出发城市</text>
<text class="detail-value">{{ currentDetail.departureCity }}</text>
</view>
<view class="detail-item" v-if="currentDetail.arrivalCity">
<text class="detail-label">到达城市</text>
<text class="detail-value">{{ currentDetail.arrivalCity }}</text>
</view>
<view class="detail-item" v-if="currentDetail.departureDate">
<text class="detail-label">出发日期</text>
<text class="detail-value">{{ currentDetail.departureDate }}</text>
</view>
<view class="detail-item" v-if="currentDetail.returnDate">
<text class="detail-label">返程日期</text>
<text class="detail-value">{{ currentDetail.returnDate }}</text>
</view>
<view class="detail-item" v-if="currentDetail.cabinType">
<text class="detail-label">舱位类型</text>
<text class="detail-value">{{ getCabinTypeText(currentDetail.cabinType) }}</text>
</view>
<view class="detail-item" v-if="currentDetail.luggageCount">
<text class="detail-label">行李件数</text>
<text class="detail-value">{{ currentDetail.luggageCount }}件</text>
</view>
<!-- 酒店相关字段 -->
<view class="detail-item" v-if="currentDetail.countryCity">
<text class="detail-label">国家/城市</text>
<text class="detail-value">{{ currentDetail.countryCity }}</text>
</view>
<view class="detail-item" v-if="currentDetail.hotelName">
<text class="detail-label">酒店名称</text>
<text class="detail-value">{{ currentDetail.hotelName }}</text>
</view>
<view class="detail-item" v-if="currentDetail.checkInDate">
<text class="detail-label">入住日期</text>
<text class="detail-value">{{ currentDetail.checkInDate }}</text>
</view>
<view class="detail-item" v-if="currentDetail.checkOutDate">
<text class="detail-label">退房日期</text>
<text class="detail-value">{{ currentDetail.checkOutDate }}</text>
</view>
<view class="detail-item" v-if="currentDetail.roomCount">
<text class="detail-label">房间数量</text>
<text class="detail-value">{{ currentDetail.roomCount }}间</text>
</view>
<view class="detail-item" v-if="currentDetail.roomType">
<text class="detail-label">房间类型</text>
<text class="detail-value">{{ currentDetail.roomType }}</text>
</view>
<view class="detail-item" v-if="currentDetail.needMeal !== null && currentDetail.needMeal !== undefined">
<text class="detail-label">是否需要餐食</text>
<text class="detail-value">{{ currentDetail.needMeal ? '是' : '否' }}</text>
</view>
<view class="detail-item" v-if="currentDetail.mealPlan">
<text class="detail-label">餐食计划</text>
<text class="detail-value">{{ getMealPlanText(currentDetail.mealPlan) }}</text>
</view>
<!-- 人数相关 -->
<view class="detail-item" v-if="currentDetail.adultCount">
<text class="detail-label">成人人数</text>
<text class="detail-value">{{ currentDetail.adultCount }}人</text>
</view>
<view class="detail-item" v-if="currentDetail.childCount">
<text class="detail-label">儿童人数</text>
<text class="detail-value">{{ currentDetail.childCount }}人</text>
</view>
<view class="detail-item" v-if="currentDetail.infantCount">
<text class="detail-label">婴儿人数</text>
<text class="detail-value">{{ currentDetail.infantCount }}人</text>
</view>
<view class="detail-item" v-if="currentDetail.passengerCount">
<text class="detail-label">乘客数量</text>
<text class="detail-value">{{ currentDetail.passengerCount }}人</text>
</view>
<!-- 机场/航班相关 -->
<view class="detail-item" v-if="currentDetail.airportTerminal">
<text class="detail-label">机场/航站楼</text>
<text class="detail-value">{{ currentDetail.airportTerminal }}</text>
</view>
<view class="detail-item" v-if="currentDetail.arrivalFlightNo">
<text class="detail-label">到达航班号</text>
<text class="detail-value">{{ currentDetail.arrivalFlightNo }}</text>
</view>
<view class="detail-item" v-if="currentDetail.departureFlightNo">
<text class="detail-label">出发航班号</text>
<text class="detail-value">{{ currentDetail.departureFlightNo }}</text>
</view>
<view class="detail-item" v-if="currentDetail.flightNo">
<text class="detail-label">航班号</text>
<text class="detail-value">{{ currentDetail.flightNo }}</text>
</view>
<view class="detail-item" v-if="currentDetail.deliveryAddress">
<text class="detail-label">送达地址</text>
<text class="detail-value">{{ currentDetail.deliveryAddress }}</text>
</view>
<!-- 高铁相关 -->
<view class="detail-item" v-if="currentDetail.originStation">
<text class="detail-label">出发站</text>
<text class="detail-value">{{ currentDetail.originStation }}</text>
</view>
<view class="detail-item" v-if="currentDetail.destinationStation">
<text class="detail-label">到达站</text>
<text class="detail-value">{{ currentDetail.destinationStation }}</text>
</view>
<view class="detail-item" v-if="currentDetail.seatClass">
<text class="detail-label">座位等级</text>
<text class="detail-value">{{ getSeatClassText(currentDetail.seatClass) }}</text>
</view>
<!-- 儿童相关 -->
<view class="detail-item" v-if="currentDetail.itinerary">
<text class="detail-label">行程</text>
<text class="detail-value">{{ currentDetail.itinerary }}</text>
</view>
<view class="detail-item" v-if="currentDetail.childAge">
<text class="detail-label">儿童年龄</text>
<text class="detail-value">{{ currentDetail.childAge }}岁</text>
</view>
<view class="detail-item" v-if="currentDetail.boyCount">
<text class="detail-label">男孩数量</text>
<text class="detail-value">{{ currentDetail.boyCount }}人</text>
</view>
<view class="detail-item" v-if="currentDetail.girlCount">
<text class="detail-label">女孩数量</text>
<text class="detail-value">{{ currentDetail.girlCount }}人</text>
</view>
<!-- 医疗相关 -->
<view class="detail-item" v-if="currentDetail.hospitalName">
<text class="detail-label">医院名称</text>
<text class="detail-value">{{ currentDetail.hospitalName }}</text>
</view>
<view class="detail-item" v-if="currentDetail.conditionDescription">
<text class="detail-label">病情描述</text>
<text class="detail-value">{{ currentDetail.conditionDescription }}</text>
</view>
<view class="detail-item" v-if="currentDetail.specialAssistanceReason">
<text class="detail-label">特殊协助原因</text>
<text class="detail-value">{{ currentDetail.specialAssistanceReason }}</text>
</view>
<!-- 宠物相关 -->
<view class="detail-item" v-if="currentDetail.origin">
<text class="detail-label">出发地</text>
<text class="detail-value">{{ currentDetail.origin }}</text>
</view>
<view class="detail-item" v-if="currentDetail.destination">
<text class="detail-label">目的地</text>
<text class="detail-value">{{ currentDetail.destination }}</text>
</view>
<view class="detail-item" v-if="currentDetail.petType">
<text class="detail-label">宠物类型</text>
<text class="detail-value">{{ currentDetail.petType }}</text>
</view>
<view class="detail-item" v-if="currentDetail.petName">
<text class="detail-label">宠物名称</text>
<text class="detail-value">{{ currentDetail.petName }}</text>
</view>
<view class="detail-item" v-if="currentDetail.hasQuarantineCert !== null && currentDetail.hasQuarantineCert !== undefined">
<text class="detail-label">检疫证明</text>
<text class="detail-value">{{ currentDetail.hasQuarantineCert ? '有' : '无' }}</text>
</view>
<!-- 导游/翻译相关 -->
<view class="detail-item" v-if="currentDetail.serviceDays">
<text class="detail-label">服务天数</text>
<text class="detail-value">{{ currentDetail.serviceDays }}天</text>
</view>
<!-- 物流相关 -->
<view class="detail-item" v-if="currentDetail.originPort">
<text class="detail-label">起运港</text>
<text class="detail-value">{{ currentDetail.originPort }}</text>
</view>
<view class="detail-item" v-if="currentDetail.destinationPort">
<text class="detail-label">目的港</text>
<text class="detail-value">{{ currentDetail.destinationPort }}</text>
</view>
<view class="detail-item" v-if="currentDetail.itemName">
<text class="detail-label">物品名称</text>
<text class="detail-value">{{ currentDetail.itemName }}</text>
</view>
<view class="detail-item" v-if="currentDetail.itemQuantity">
<text class="detail-label">物品数量</text>
<text class="detail-value">{{ currentDetail.itemQuantity }}</text>
</view>
<view class="detail-item" v-if="currentDetail.cargoName">
<text class="detail-label">货物名称</text>
<text class="detail-value">{{ currentDetail.cargoName }}</text>
</view>
<view class="detail-item" v-if="currentDetail.cargoQuantity">
<text class="detail-label">货物数量</text>
<text class="detail-value">{{ currentDetail.cargoQuantity }}</text>
</view>
<!-- 旅游相关 -->
<view class="detail-item" v-if="currentDetail.travelDestination">
<text class="detail-label">旅游目的地</text>
<text class="detail-value">{{ currentDetail.travelDestination }}</text>
</view>
<view class="detail-item" v-if="currentDetail.travelDate">
<text class="detail-label">出行日期</text>
<text class="detail-value">{{ currentDetail.travelDate }}</text>
</view>
<view class="detail-item" v-if="currentDetail.travelDays">
<text class="detail-label">旅游天数</text>
<text class="detail-value">{{ currentDetail.travelDays }}天</text>
</view>
<!-- 咨询相关 -->
<view class="detail-item" v-if="currentDetail.specificRequirements">
<text class="detail-label">具体需求</text>
<text class="detail-value">{{ currentDetail.specificRequirements }}</text>
</view>
<!-- 备注 -->
<view class="detail-item" v-if="currentDetail.notes">
<text class="detail-label">备注</text>
<text class="detail-value">{{ currentDetail.notes }}</text>
</view>
</scroll-view>
</view>
</view>
</view>
</template>
<script>
import { AppServer } from '@/modules/api/AppServer.js'
export default {
data() {
return {
statusBarHeight: 0,
activeTab: 'all',
loading: false,
appointmentList: [],
pagination: { page: 1, limit: 10, total: 0 },
hasMore: true,
showDetailPopup: false,
currentDetail: {}
}
},
onLoad(options) {
const systemInfo = uni.getSystemInfoSync()
this.statusBarHeight = systemInfo.statusBarHeight || 0
if (options.tab) this.activeTab = options.tab
this.fetchAppointments()
},
methods: {
goBack() {
uni.navigateBack({ delta: 1 })
},
switchTab(tab) {
if (this.activeTab === tab) return
this.activeTab = tab
this.pagination.page = 1
this.appointmentList = []
this.hasMore = true
this.fetchAppointments()
},
async fetchAppointments() {
if (this.loading) return
this.loading = true
try {
const appserver = new AppServer()
const params = { page: this.pagination.page, limit: this.pagination.limit }
if (this.activeTab === 'inProgress') params.status = 'in-progress'
else if (this.activeTab === 'completed') params.status = 'completed'
const res = await appserver.GetAppointments(params)
if (res.success || res.code === 0) {
const newList = res.data || []
if (this.pagination.page === 1) this.appointmentList = newList
else this.appointmentList = [...this.appointmentList, ...newList]
this.pagination.total = res.pagination?.total || 0
this.hasMore = newList.length >= this.pagination.limit
}
} catch (e) {
console.error('获取预约列表失败:', e)
} finally {
this.loading = false
}
},
loadMore() {
if (!this.hasMore || this.loading) return
this.pagination.page++
this.fetchAppointments()
},
showDetail(item) {
this.currentDetail = item
this.showDetailPopup = true
},
closeDetail() {
this.showDetailPopup = false
},
getServiceTitle(item) {
if (!item) return '-'
const locale = this.$i18n.locale
if (item.service) {
if (locale === 'zh') return item.service.titleZh || item.service.titleEn || '-'
else if (locale === 'es') return item.service.titleEs || item.service.titleEn || '-'
else return item.service.titleEn || item.service.titleZh || '-'
}
if (item.hotService) {
if (locale === 'zh') return item.hotService.name_zh || item.hotService.name_en || '-'
else if (locale === 'es') return item.hotService.name_es || item.hotService.name_en || '-'
else return item.hotService.name_en || item.hotService.name_zh || '-'
}
return this.getServiceTypeText(item.serviceType) || '-'
},
getServiceTypeText(type) {
const typeMap = {
'travel': '全球机票代理',
'flight': '全球机票代理',
'hotel': '全球酒店预定',
'vip_lounge': '全球机场贵宾室服务',
'lounge': '全球机场贵宾室服务',
'airport_transfer': '机场接/送机服务',
'unaccompanied_minor': '无成人陪伴儿童代办',
'rail_ticket': '高铁票代订',
'train': '高铁票代订',
'medical_consultation': '远程医疗问诊代理服务',
'telemedicine': '远程医疗问诊代理服务',
'special_needs': '特殊旅客定制服务代办',
'special_passenger': '特殊旅客定制服务代办',
'pet_transportation': '宠物托运代理',
'pet_transport': '宠物托运代理',
'guide_translation': '西班牙语专业导游/翻译服务',
'visa_consultation': '签证咨询',
'visa': '签证咨询',
'exhibition_service': '墨西哥展会咨询与协办服务',
'exhibition': '墨西哥展会咨询与协办服务',
'air_logistics': '跨境航空物流/快递一站式服务',
'sea_freight': '海运/清关一站式服务',
'travel_planning': '旅游线路规划/咨询',
'insurance_consultation': '跨境出行意外保险/国际财产保险咨询',
'insurance': '跨境出行意外保险/国际财产保险咨询'
}
return typeMap[type] || type
},
getContactInfo(item) {
if (!item) return '-'
return item.phone || item.wechatId || item.whatsapp || item.contactValue || '-'
},
getAllContactInfo(item) {
if (!item) return '-'
const contacts = []
if (item.phone) contacts.push((item.phoneCountryCode || '') + ' ' + item.phone)
if (item.wechatId) contacts.push('微信: ' + item.wechatId)
if (item.whatsapp) contacts.push('WhatsApp: ' + item.whatsapp)
return contacts.length > 0 ? contacts.join('\n') : '-'
},
formatDate(dateStr) {
if (!dateStr) return '-'
const date = new Date(dateStr)
return `${date.getFullYear()}/${date.getMonth() + 1}/${date.getDate()}`
},
getStatusText(status) {
const statusMap = {
'pending': this.$t('myAppointment.statusPending') || '待处理',
'confirmed': this.$t('myAppointment.statusConfirmed') || '已确认',
'in-progress': this.$t('myAppointment.statusInProgress') || '进行中',
'completed': this.$t('myAppointment.statusCompleted') || '已结束',
'cancelled': this.$t('myAppointment.statusCancelled') || '已取消'
}
return statusMap[status] || status
},
getStatusClass(status) {
return {
'status-in-progress': status === 'in-progress' || status === 'pending' || status === 'confirmed',
'status-completed': status === 'completed',
'status-cancelled': status === 'cancelled'
}
},
getCabinTypeText(type) {
const map = { 'economy': '经济舱', 'premium_economy': '超级经济舱', 'business': '商务舱' }
return map[type] || type
},
getMealPlanText(plan) {
const map = { 'breakfast': '早餐', 'three_meals': '三餐', 'all_inclusive': '全包' }
return map[plan] || plan
},
getSeatClassText(seatClass) {
const map = { 'first': '一等座', 'second': '二等座', 'third': '三等座' }
return map[seatClass] || seatClass
}
}
}
</script>
<style lang="scss">
.content {
height: 100vh;
display: flex;
flex-direction: column;
background-color: #F5F7FA;
}
.header {
background-color: #fff;
.status-bar { width: 100%; background-color: #fff; }
.header-content {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20rpx 32rpx;
}
.back-button { width: 60rpx; height: 60rpx; display: flex; align-items: center; justify-content: center; }
.back-icon { width: 40rpx; height: 40rpx; }
.header-title { font-size: 34rpx; font-weight: 600; color: #333; flex: 1; text-align: center; }
.header-placeholder { width: 60rpx; }
}
.tab-container {
display: flex;
flex-direction: row;
padding: 24rpx 32rpx;
background-color: #fff;
}
.tab-item {
padding: 14rpx 36rpx;
margin-right: 24rpx;
border-radius: 36rpx;
border: 2rpx solid #E0E0E0;
background-color: #fff;
&.active { background-color: #E5FBFF; border-color: #00A0BC; }
}
.tab-text {
font-size: 28rpx;
color: #666;
&.active { color: #00A0BC; font-weight: 500; }
}
.appointment-list {
flex: 1;
height: 0;
width: 686rpx;
margin: 24rpx auto 24rpx;
}
.appointment-item {
background-color: #fff;
border-radius: 24rpx;
padding: 32rpx;
margin-bottom: 24rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05);
}
.item-title { font-size: 32rpx; font-weight: 600; color: #333; margin-bottom: 24rpx; }
.item-row { display: flex; align-items: center; margin-bottom: 16rpx; }
.item-icon { width: 36rpx; height: 36rpx; margin-right: 20rpx; }
.item-text { font-size: 28rpx; color: #666; }
.item-footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 24rpx;
padding-top: 24rpx;
border-top: 1rpx solid #F0F0F0;
}
.view-detail { font-size: 28rpx; color: #00A0BC; }
.status-text { font-size: 28rpx; font-weight: 500; }
.status-in-progress { color: #00A0BC; }
.status-completed { color: #FF6B6B; }
.status-cancelled { color: #999; }
.loading-more { padding: 40rpx; display: flex; justify-content: center; }
.loading-text { font-size: 26rpx; color: #999; }
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding-top: 200rpx;
}
.empty-icon { font-size: 120rpx; opacity: 0.3; margin-bottom: 30rpx; }
.no-data { font-size: 28rpx; color: #999; }
// 详情弹窗 - 居中显示
.detail-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 999;
}
.detail-popup {
width: 85%;
max-height: 70vh;
background-color: #fff;
border-radius: 24rpx;
overflow: hidden;
}
.popup-header {
display: flex;
align-items: center;
justify-content: center;
padding: 36rpx 32rpx;
border-bottom: 1rpx solid #F0F0F0;
background-color: #fff;
position: relative;
}
.popup-title { font-size: 34rpx; font-weight: 600; color: #333; }
.popup-close {
position: absolute;
right: 32rpx;
width: 48rpx;
height: 48rpx;
display: flex;
align-items: center;
justify-content: center;
}
.close-icon { width: 36rpx; height: 36rpx; }
.popup-content {
max-height: 60vh;
padding: 0 40rpx 40rpx;
}
.detail-item {
padding: 28rpx 0;
border-bottom: 1rpx solid #F5F5F5;
&:last-child { border-bottom: none; }
}
.detail-label {
font-size: 30rpx;
font-weight: 600;
color: #333;
display: block;
margin-bottom: 12rpx;
}
.detail-value {
font-size: 28rpx;
color: #666;
display: block;
line-height: 1.6;
word-break: break-all;
white-space: pre-wrap;
}
</style>