<template>
|
<view class="guide-page">
|
<app-nav title="名导精讲" />
|
|
<scroll-view class="guide-scroll" scroll-y="true">
|
<view class="map-preview">
|
<map
|
v-if="hasMeetingLocation"
|
id="meetingMap"
|
class="map-preview__map"
|
:longitude="meetingPoint.longitude"
|
:latitude="meetingPoint.latitude"
|
:scale="15"
|
:markers="mapMarkers"
|
:circles="mapCircles"
|
:enable-scroll="false"
|
:enable-zoom="false"
|
:enable-rotate="false"
|
:enable-overlooking="false"
|
:show-location="false"
|
/>
|
<cover-view v-if="hasMeetingLocation" class="meeting-pin">
|
<cover-view class="meeting-bubble">
|
<cover-view class="meeting-bubble__text">{{ meetingPoint.name }}</cover-view>
|
</cover-view>
|
<cover-view class="meeting-triangle">▼</cover-view>
|
</cover-view>
|
</view>
|
|
<view class="arrival-panel">
|
<text class="arrival-title">请尽快前往汇合点</text>
|
<view v-if="order.familyNo" class="team-badge" :style="familyBadgeStyle">
|
<text class="team-badge__text">家庭号 {{ order.familyNo }}</text>
|
</view>
|
</view>
|
|
<view class="detail-card">
|
<view class="guide-row">
|
<view class="guide-avatar">
|
<image class="guide-avatar__icon" :src="order.guideAvatar || '/static/ui-icons/user-blue.svg'" mode="aspectFill" />
|
</view>
|
<view class="guide-main">
|
<text class="guide-name">导游 · {{ order.guideName || '待接单' }}</text>
|
<view class="guide-stars">
|
<text v-for="item in 5" :key="item" class="guide-star" :class="{ 'guide-star--inactive': item > guideStarCount }">★</text>
|
</view>
|
</view>
|
<view class="phone-action" @tap="handleCallGuide">
|
<image class="phone-action__icon" src="/static/ui-icons/phone-black.svg" mode="aspectFit" />
|
<text class="phone-action__text">电话联系</text>
|
</view>
|
</view>
|
|
<view class="divider" />
|
|
<view class="order-summary">
|
<view class="order-title-row">
|
<view class="order-title-main">
|
<text class="order-scenic">{{ order.scenic }}</text>
|
<text class="order-service">{{ order.service }}</text>
|
<text class="order-duration">{{ order.duration }}</text>
|
</view>
|
<text class="order-meeting-point">{{ order.meetingPoint }}</text>
|
</view>
|
|
<view class="price-row">
|
<view class="unit-block">
|
<text class="unit-price">¥{{ order.unitPrice }}{{ order.unit }}</text>
|
<text class="quantity">x{{ order.quantity }}</text>
|
</view>
|
<text class="total-price">¥{{ order.total }}</text>
|
</view>
|
</view>
|
|
<view class="divider divider--compact" />
|
|
<view class="order-detail">
|
<text class="order-detail__title">订单详情</text>
|
<view
|
v-for="item in detailRows"
|
:key="item.label"
|
class="detail-row"
|
>
|
<text class="detail-row__label">{{ item.label }}</text>
|
<text class="detail-row__value">{{ item.value }}</text>
|
</view>
|
</view>
|
</view>
|
</scroll-view>
|
|
<view class="bottom-bar">
|
<app-action-button
|
class="bottom-action bottom-action--ghost"
|
variant="ghost"
|
text="取消订单"
|
:height="76"
|
:radius="18"
|
:font-size="26"
|
@tap="handleCancel"
|
/>
|
<app-action-button
|
class="bottom-action bottom-action--primary"
|
text="地图导航"
|
loading-text="打开中..."
|
:height="76"
|
:radius="18"
|
:font-size="26"
|
:loading="navigating"
|
@tap="handleNavigate"
|
/>
|
</view>
|
|
<cancel-order-sheet
|
:visible="cancelSheetVisible"
|
@close="closeCancelSheet"
|
@confirm="confirmCancelOrder"
|
/>
|
</view>
|
</template>
|
|
<script setup>
|
import { computed, ref } from 'vue'
|
import { onHide, onLoad, onShow, onUnload } from '@dcloudio/uni-app'
|
import AppNav from '@/components/app-nav/app-nav.vue'
|
import CancelOrderSheet from '@/components/cancel-order-sheet/cancel-order-sheet.vue'
|
import { cancelTourOrder, getTourOrder } from '@/services/order'
|
import { resolveProfileAssetUrl } from '@/services/profile'
|
import { useLoginGuard } from '@/utils/auth-guard'
|
|
useLoginGuard('/pages/order-pending-guide/index')
|
|
const ORDER_POLL_INTERVAL = 3000
|
|
const orderId = ref('')
|
const orderDetail = ref({})
|
let orderPollTimer = null
|
let orderRefreshing = false
|
let pageActive = false
|
let redirecting = false
|
let loadErrorShown = false
|
|
const meetingPoint = computed(() => ({
|
longitude: Number(orderDetail.value.meetingPointLongitude || 0),
|
latitude: Number(orderDetail.value.meetingPointLatitude || 0),
|
name: orderDetail.value.meetingPointName || '汇合点',
|
address: orderDetail.value.meetingPointName || ''
|
}))
|
|
const hasMeetingLocation = computed(() => Boolean(meetingPoint.value.longitude && meetingPoint.value.latitude))
|
|
const order = computed(() => ({
|
id: orderDetail.value.id || orderId.value,
|
scenic: orderDetail.value.scenicName || '-',
|
service: formatServiceTitle(orderDetail.value.packageName),
|
duration: orderDetail.value.durationMinutes ? `${orderDetail.value.durationMinutes}分钟` : '-',
|
meetingPoint: orderDetail.value.meetingPointName || '-',
|
unitPrice: formatAmount(orderDetail.value.unitPrice),
|
unit: String(orderDetail.value.orderType || '').startsWith('GROUP_') ? '/人' : '/团',
|
quantity: orderDetail.value.peopleCount || 1,
|
total: formatAmount(orderDetail.value.amount),
|
familyNo: orderDetail.value.familyNo || '',
|
guideName: orderDetail.value.guideName || '',
|
guideMobile: orderDetail.value.guideMobile || '',
|
guideAvatar: resolveProfileAssetUrl(orderDetail.value.guideAvatar)
|
}))
|
|
const guideStarCount = computed(() => Math.max(0, Math.min(5, Math.round(Number(orderDetail.value.guideRating || 0)))))
|
|
const familyBadgeStyle = computed(() => ({
|
backgroundColor: '#278BFF'
|
}))
|
|
const cancelSheetVisible = ref(false)
|
const navigating = ref(false)
|
|
const detailRows = computed(() => [
|
{ label: '下单时间', value: formatDateTime(orderDetail.value.placedAt) },
|
{ label: '接单时间', value: formatDateTime(orderDetail.value.acceptedAt) },
|
{ label: '支付金额', value: `¥${formatAmount(orderDetail.value.paymentAmount ?? orderDetail.value.amount)}` },
|
{ label: '支付流水号', value: orderDetail.value.paymentOrderNo || '-' }
|
])
|
|
const mapMarkers = []
|
|
const mapCircles = computed(() => hasMeetingLocation.value ? [{
|
longitude: meetingPoint.value.longitude,
|
latitude: meetingPoint.value.latitude,
|
color: '#278BFFFF',
|
fillColor: '#278BFF26',
|
radius: 180,
|
strokeWidth: 2
|
}] : [])
|
|
onLoad((options = {}) => {
|
orderId.value = options.orderId || options.id || ''
|
})
|
|
onShow(() => {
|
pageActive = true
|
redirecting = false
|
startOrderPolling()
|
})
|
|
onHide(() => {
|
pageActive = false
|
stopOrderPolling()
|
})
|
|
onUnload(() => {
|
pageActive = false
|
stopOrderPolling()
|
})
|
|
function startOrderPolling() {
|
stopOrderPolling()
|
if (!orderId.value) {
|
uni.showToast({ title: '订单参数缺失', icon: 'none' })
|
return
|
}
|
|
refreshOrderStatus()
|
orderPollTimer = setInterval(refreshOrderStatus, ORDER_POLL_INTERVAL)
|
}
|
|
function stopOrderPolling() {
|
if (orderPollTimer) {
|
clearInterval(orderPollTimer)
|
orderPollTimer = null
|
}
|
}
|
|
async function refreshOrderStatus() {
|
if (!pageActive || !orderId.value || orderRefreshing || redirecting) return
|
|
orderRefreshing = true
|
try {
|
const detail = await getTourOrder(orderId.value)
|
if (!pageActive) return
|
orderDetail.value = detail
|
loadErrorShown = false
|
redirectForOrderStatus(detail && detail.status)
|
} catch (error) {
|
if (!loadErrorShown) {
|
loadErrorShown = true
|
uni.showToast({ title: error.message || '订单状态更新失败', icon: 'none' })
|
}
|
console.warn('待讲解订单状态查询失败', error)
|
} finally {
|
orderRefreshing = false
|
}
|
}
|
|
function redirectForOrderStatus(status) {
|
const routeMap = {
|
PENDING_PAYMENT: '/pages/order-pending-pay/index',
|
PENDING_ACCEPT: '/pages/order-waiting/index',
|
IN_SERVICE: '/pages/order-serving/index',
|
COMPLETED: '/pages/order-pending-review/index',
|
CANCELLED: '/pages/order-canceled/index',
|
REFUNDED: '/pages/order-canceled/index'
|
}
|
const target = routeMap[status]
|
if (!target || redirecting) return
|
|
redirecting = true
|
stopOrderPolling()
|
uni.redirectTo({
|
url: `${target}?orderId=${orderId.value}`,
|
fail() {
|
redirecting = false
|
if (pageActive) startOrderPolling()
|
}
|
})
|
}
|
|
function formatAmount(value) {
|
const amount = Number(value || 0)
|
return Number.isFinite(amount) ? amount.toFixed(2) : '0.00'
|
}
|
|
function formatDateTime(value) {
|
return value ? String(value).replace('T', ' ').slice(0, 19) : '-'
|
}
|
|
function formatServiceTitle(value) {
|
return value ? String(value).replace(/\s*·\s*/g, '·') : '-'
|
}
|
|
function handleCallGuide() {
|
if (!order.value.guideMobile) {
|
uni.showToast({ title: '暂无导游联系电话', icon: 'none' })
|
return
|
}
|
uni.makePhoneCall({ phoneNumber: order.value.guideMobile })
|
}
|
|
function handleCancel() {
|
cancelSheetVisible.value = true
|
}
|
|
function closeCancelSheet() {
|
cancelSheetVisible.value = false
|
}
|
|
async function confirmCancelOrder() {
|
cancelSheetVisible.value = false
|
try {
|
await cancelTourOrder(order.value.id)
|
} catch (error) {
|
uni.showToast({ title: error.message || '取消订单失败', icon: 'none' })
|
return
|
}
|
uni.redirectTo({
|
url: `/pages/order-canceled/index?id=${order.value.id}`
|
})
|
}
|
|
function handleNavigate() {
|
if (navigating.value) return
|
if (!hasMeetingLocation.value) {
|
uni.showToast({ title: '暂无汇合点坐标', icon: 'none' })
|
return
|
}
|
|
navigating.value = true
|
uni.openLocation({
|
longitude: meetingPoint.value.longitude,
|
latitude: meetingPoint.value.latitude,
|
name: meetingPoint.value.name,
|
address: meetingPoint.value.address,
|
scale: 18,
|
success() {
|
setTimeout(() => {
|
navigating.value = false
|
}, 180)
|
},
|
fail() {
|
navigating.value = false
|
uni.showToast({
|
title: '地图导航暂不可用',
|
icon: 'none'
|
})
|
}
|
})
|
}
|
</script>
|
|
<style lang="scss" scoped>
|
.guide-page {
|
height: 100vh;
|
display: flex;
|
flex-direction: column;
|
overflow: hidden;
|
background: #f4f6fa;
|
}
|
|
.guide-scroll {
|
flex: 1;
|
min-height: 0;
|
}
|
|
.map-preview {
|
position: relative;
|
height: 320rpx;
|
overflow: hidden;
|
background: #e5e7eb;
|
}
|
|
.map-preview__map {
|
width: 100%;
|
height: 100%;
|
}
|
|
.meeting-pin {
|
position: absolute;
|
left: 50%;
|
top: 84rpx;
|
z-index: 4;
|
display: flex;
|
flex-direction: column;
|
align-items: center;
|
justify-content: center;
|
width: 260rpx;
|
transform: translateX(-50%);
|
}
|
|
.meeting-bubble {
|
width: 248rpx;
|
height: 76rpx;
|
border-radius: 38rpx;
|
background: #278bff;
|
overflow: hidden;
|
box-sizing: border-box;
|
box-shadow: 0 8rpx 18rpx rgba(39, 139, 255, 0.18);
|
}
|
|
.meeting-bubble__text {
|
width: 248rpx;
|
height: 76rpx;
|
color: #ffffff;
|
font-size: 27rpx;
|
font-weight: 900;
|
line-height: 76rpx;
|
text-align: center;
|
white-space: nowrap;
|
}
|
|
.meeting-triangle {
|
width: 40rpx;
|
height: 28rpx;
|
margin-top: -2rpx;
|
color: #278bff;
|
font-size: 30rpx;
|
font-weight: 900;
|
line-height: 28rpx;
|
text-align: center;
|
}
|
|
.arrival-panel {
|
display: flex;
|
flex-direction: column;
|
align-items: center;
|
padding: 42rpx 28rpx 34rpx;
|
background: #ffffff;
|
box-shadow: 0 4rpx 14rpx rgba(17, 24, 39, 0.04);
|
}
|
|
.arrival-title {
|
color: #111827;
|
font-size: 34rpx;
|
font-weight: 850;
|
line-height: 1;
|
}
|
|
.team-badge {
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
min-width: 132rpx;
|
height: 58rpx;
|
margin-top: 30rpx;
|
padding: 0 24rpx;
|
border-radius: 29rpx;
|
box-sizing: border-box;
|
box-shadow: 0 6rpx 14rpx rgba(39, 139, 255, 0.15);
|
}
|
|
.team-badge__text {
|
color: #ffffff;
|
font-size: 25rpx;
|
font-weight: 900;
|
line-height: 1;
|
white-space: nowrap;
|
}
|
|
.detail-card {
|
margin: 24rpx 28rpx 142rpx;
|
padding: 30rpx;
|
border-radius: 28rpx;
|
background: #ffffff;
|
box-shadow: 0 6rpx 18rpx rgba(17, 24, 39, 0.04);
|
}
|
|
.guide-row {
|
display: flex;
|
align-items: center;
|
}
|
|
.guide-avatar {
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
width: 78rpx;
|
height: 78rpx;
|
border: 2rpx solid #d6dce6;
|
border-radius: 50%;
|
background: #ffffff;
|
}
|
|
.guide-avatar__icon {
|
width: 46rpx;
|
height: 46rpx;
|
}
|
|
.guide-main {
|
display: flex;
|
flex: 1;
|
flex-direction: column;
|
gap: 12rpx;
|
min-width: 0;
|
margin-left: 28rpx;
|
}
|
|
.guide-name {
|
color: #111827;
|
font-size: 26rpx;
|
font-weight: 850;
|
line-height: 1;
|
}
|
|
.guide-stars {
|
display: flex;
|
align-items: center;
|
gap: 4rpx;
|
}
|
|
.guide-star {
|
color: #ffb020;
|
font-size: 22rpx;
|
font-weight: 900;
|
line-height: 1;
|
}
|
|
.guide-star--inactive {
|
color: #d8dde6;
|
}
|
|
.phone-action {
|
display: flex;
|
flex-direction: column;
|
align-items: center;
|
justify-content: center;
|
gap: 10rpx;
|
width: 112rpx;
|
flex: 0 0 auto;
|
}
|
|
.phone-action__icon {
|
width: 34rpx;
|
height: 34rpx;
|
}
|
|
.phone-action__text {
|
color: #111827;
|
font-size: 23rpx;
|
font-weight: 750;
|
line-height: 1;
|
}
|
|
.divider {
|
height: 2rpx;
|
margin: 30rpx 0;
|
background: #edf1f6;
|
}
|
|
.divider--compact {
|
margin: 30rpx 0 22rpx;
|
}
|
|
.order-title-row {
|
display: flex;
|
align-items: center;
|
justify-content: space-between;
|
gap: 24rpx;
|
}
|
|
.order-title-main {
|
display: flex;
|
align-items: center;
|
flex: 1;
|
min-width: 0;
|
}
|
|
.order-scenic,
|
.order-service {
|
color: #111827;
|
font-size: 27rpx;
|
font-weight: 900;
|
line-height: 1;
|
white-space: nowrap;
|
}
|
|
.order-service {
|
margin-left: 10rpx;
|
}
|
|
.order-duration {
|
margin-left: 12rpx;
|
color: #8a93a2;
|
font-size: 24rpx;
|
font-weight: 750;
|
line-height: 1;
|
}
|
|
.order-meeting-point {
|
flex: 0 1 auto;
|
max-width: 220rpx;
|
color: #8a93a2;
|
font-size: 23rpx;
|
font-weight: 750;
|
line-height: 1;
|
overflow: hidden;
|
text-align: right;
|
text-overflow: ellipsis;
|
white-space: nowrap;
|
}
|
|
.price-row {
|
display: flex;
|
align-items: flex-end;
|
justify-content: space-between;
|
margin-top: 34rpx;
|
}
|
|
.unit-block {
|
display: flex;
|
align-items: baseline;
|
gap: 34rpx;
|
}
|
|
.unit-price {
|
color: #8a93a2;
|
font-size: 31rpx;
|
font-weight: 850;
|
line-height: 1;
|
}
|
|
.quantity {
|
color: #8a93a2;
|
font-size: 22rpx;
|
font-weight: 800;
|
line-height: 1;
|
}
|
|
.total-price {
|
color: #111827;
|
font-size: 34rpx;
|
font-weight: 900;
|
line-height: 1;
|
}
|
|
.order-detail__title {
|
display: block;
|
color: #111827;
|
font-size: 27rpx;
|
font-weight: 900;
|
line-height: 1;
|
}
|
|
.detail-row {
|
display: flex;
|
align-items: center;
|
justify-content: space-between;
|
margin-top: 32rpx;
|
}
|
|
.detail-row__label {
|
color: #8a93a2;
|
font-size: 25rpx;
|
font-weight: 700;
|
line-height: 1;
|
}
|
|
.detail-row__value {
|
color: #8a93a2;
|
font-size: 25rpx;
|
font-weight: 750;
|
line-height: 1;
|
}
|
|
.bottom-bar {
|
position: fixed;
|
left: 0;
|
right: 0;
|
bottom: 0;
|
z-index: 30;
|
display: flex;
|
align-items: center;
|
height: calc(112rpx + env(safe-area-inset-bottom));
|
padding: 0 28rpx env(safe-area-inset-bottom);
|
border-top: 2rpx solid #edf1f6;
|
background: rgba(255, 255, 255, 0.98);
|
box-sizing: border-box;
|
}
|
|
.bottom-action {
|
flex: 1;
|
}
|
|
.bottom-action--ghost {
|
color: #111827;
|
}
|
|
.bottom-action--primary {
|
margin-left: 22rpx;
|
}
|
</style>
|