doum
2026-04-23 7d242579a0923e7639876797e738a22c45d6e2d0
app/pages/index/index.vue
@@ -1,5 +1,19 @@
<template>
   <view class="hall-page">
         <view v-if="showOrderDetail && isStatusDetail" class="order-detail-map-layer" :style="{ top: statusBarHeight + 'px' }">
            <map
               class="order-detail-map-layer__map"
               :latitude="detailMap.center.latitude"
               :longitude="detailMap.center.longitude"
               :markers="detailMap.markers"
               :polyline="detailMap.polyline"
               :include-points="detailMap.includePoints"
               :scale="detailMap.scale"
               :enable-zoom="true"
               :enable-scroll="true"
            ></map>
         </view>
      <view class="hall-page__header" :style="{ paddingTop: statusBarHeight + 'px' }">
         <view class="hall-page__user-row">
            <view class="hall-page__user">
@@ -186,11 +200,10 @@
      <!-- 订单详情 -->
      <u-popup :show="showOrderDetail" round="20" mode="bottom" :overlayStyle="{ background: 'rgba(0, 0, 0, 0.32)' }" @close="showOrderDetail = false">
         <view class="order-detail" :style="{ height: 'calc(100vh - ' + statusBarHeight + 'px)' }">
            <scroll-view class="order-detail__scroll" scroll-y>
               <view v-if="isStatusDetail" class="order-detail__map-section">
                  <view class="order-detail__map">
                     <image class="order-detail__map-image" mode="aspectFill"></image>
                     <view class="order-detail__map-bubble">{{ detailOrder.mapTips }}</view>
                  <view class="order-detail__map-placeholder"></view>
                  <view class="order-detail__map-bubble">{{ detailMap.tips }}</view>
                  </view>
                  <view class="order-detail__status-bar">
                     <view class="order-detail__status-left">
@@ -203,6 +216,8 @@
                     </view>
                  </view>
               </view>
            <scroll-view class="order-detail__scroll" scroll-y>
               <view class="order-detail__content">
                  <view class="order-detail__head">
@@ -301,11 +316,50 @@
                  <image class="order-detail__cancel-icon" src="/static/image/ic_close2@2x.png" mode="aspectFit"></image>
               </view>
               <button v-if="!isStatusDetail" class="order-detail__confirm" hover-class="order-detail__confirm--hover">确认抢单</button>
               <button v-else class="order-detail__confirm order-detail__confirm--status" hover-class="order-detail__confirm--hover">
                  <image class="order-detail__confirm-icon" mode="aspectFit"></image>
               <button v-else class="order-detail__confirm order-detail__confirm--status" hover-class="order-detail__confirm--hover" @click="handleStatusAction">
                  <image class="order-detail__confirm-icon" src="/static/image/ic_photo@2x.png" mode="aspectFit"></image>
                  <text>{{ detailPopupType === 'pickup' ? '拍照取货' : '拍照送达' }}</text>
               </button>
            </view>
         </view>
      </u-popup>
      <u-popup :show="showPhotoDeliverPopup" round="20" mode="bottom">
         <view class="photo-deliver">
            <view class="photo-deliver__header">
               <image class="photo-deliver__close-placeholder" mode="aspectFit"></image>
               <text class="photo-deliver__title">拍照送达</text>
               <image class="photo-deliver__close" mode="aspectFit" @click="showPhotoDeliverPopup = false"></image>
            </view>
            <view class="photo-deliver__section">
               <view class="photo-deliver__label-row">
                  <text class="photo-deliver__label">拍摄送达照片</text>
                  <text class="photo-deliver__required">*</text>
                  <text class="photo-deliver__hint">最多3张照片</text>
               </view>
               <view class="photo-deliver__photos">
                  <view class="photo-deliver__upload-card">
                     <image class="photo-deliver__upload-icon" mode="aspectFit"></image>
                     <text class="photo-deliver__upload-text">点击拍照</text>
                  </view>
                  <view class="photo-deliver__preview-card">
                     <image class="photo-deliver__preview-image" mode="aspectFill"></image>
                     <view class="photo-deliver__preview-mask">
                        <text class="photo-deliver__preview-delete">删除</text>
                     </view>
                  </view>
               </view>
            </view>
            <view class="photo-deliver__section photo-deliver__section--remark">
               <text class="photo-deliver__remark-title">备注信息</text>
               <textarea class="photo-deliver__textarea" maxlength="200" placeholder="请输入" placeholder-style="color: #c7cbd3;" />
            </view>
            <button class="photo-deliver__submit" hover-class="photo-deliver__submit--hover" @click="showPhotoDeliverPopup = false">确认送达</button>
         </view>
      </u-popup>
   </view>
@@ -318,8 +372,10 @@
            tts: null,
            show: false,
            show1: false,
            showPhotoDeliverPopup: false,
            showOrderDetail: false,
            detailPopupType: 'hall',
            routeInfo: null,
            statusBarHeight: 0,
            headerHeight: 0,
            tabbarHeight: 0,
@@ -344,8 +400,16 @@
               statusText: '抢单大厅',
               qrcodeValue: '767889',
               qrcodeLabel: '取货码',
               mapTips: '剩余3.2km,约4分钟',
               mapTips: '',
               showCancelTag: false,
               startPoint: {
                  latitude: 31.8269,
                  longitude: 117.2334
               },
               endPoint: {
                  latitude: 31.8435,
                  longitude: 117.2852
               },
               tags: [
                  { text: '标速达', type: 'blue' },
                  { text: '贵重物品', type: 'orange' }
@@ -385,6 +449,14 @@
                  time: '45分钟内',
                  price: '¥20.5',
                  extra: '3.0',
                  startPoint: {
                     latitude: 31.829512,
                     longitude: 117.239211
                  },
                  endPoint: {
                     latitude: 31.841268,
                     longitude: 117.278695
                  },
                  tags: [
                     { text: '极速达', type: 'blue' },
                     { text: '贵重物品', type: 'orange' }
@@ -402,6 +474,14 @@
                  time: '45分钟内',
                  price: '¥20.5',
                  extra: '3.0',
                  startPoint: {
                     latitude: 31.827106,
                     longitude: 117.232884
                  },
                  endPoint: {
                     latitude: 31.847331,
                     longitude: 117.289762
                  },
                  tags: [
                     { text: '极速达', type: 'red' },
                     { text: '大件物品', type: 'blue-light' }
@@ -419,6 +499,14 @@
                  time: '45分钟内',
                  price: '¥20.5',
                  extra: '3.0',
                  startPoint: {
                     latitude: 31.823761,
                     longitude: 117.228947
                  },
                  endPoint: {
                     latitude: 31.838473,
                     longitude: 117.272513
                  },
                  tags: [
                     { text: '极速达', type: 'red' },
                     { text: '大件物品', type: 'blue-light' }
@@ -439,8 +527,16 @@
                  statusText: '待取货',
                  qrcodeValue: '767889',
                  qrcodeLabel: '取货码',
                  mapTips: '剩余3.2km,约4分钟',
                  mapTips: '',
                  showCancelTag: true,
                  startPoint: {
                     latitude: 31.829512,
                     longitude: 117.239211
                  },
                  endPoint: {
                     latitude: 31.841268,
                     longitude: 117.278695
                  },
                  time: '45分钟内',
                  price: '¥20.5',
                  extra: '3.0',
@@ -464,8 +560,16 @@
                  statusText: '配送中',
                  qrcodeValue: '767889',
                  qrcodeLabel: '存件码',
                  mapTips: '剩余3.2km,约4分钟',
                  mapTips: '',
                  showCancelTag: false,
                  startPoint: {
                     latitude: 31.827106,
                     longitude: 117.232884
                  },
                  endPoint: {
                     latitude: 31.847331,
                     longitude: 117.289762
                  },
                  time: '45分钟内',
                  price: '¥20.5',
                  extra: '3.0',
@@ -500,6 +604,65 @@
            return this.detailPopupType === 'pickup' || this.detailPopupType === 'delivering'
         },
         detailMap() {
            const fallbackPoint = {
               latitude: 31.8269,
               longitude: 117.2334
            }
            const startPoint = this.detailOrder.startPoint || fallbackPoint
            const endPoint = this.detailOrder.endPoint || fallbackPoint
            const routePoints = this.routeInfo && this.routeInfo.points && this.routeInfo.points.length ? this.routeInfo.points : []
            const center = {
               latitude: (startPoint.latitude + endPoint.latitude) / 2,
               longitude: (startPoint.longitude + endPoint.longitude) / 2
            }
            const distanceKm = this.routeInfo && this.routeInfo.distanceKm ? this.routeInfo.distanceKm : 0
            const durationMinutes = this.routeInfo && this.routeInfo.durationMinutes ? this.routeInfo.durationMinutes : 0
            const tips = this.detailOrder.mapTips || (distanceKm ? `剩余${distanceKm.toFixed(1)}km,约${durationMinutes}分钟` : '路线规划中...')
            return {
               center,
               markers: [
                  {
                     id: 1,
                     latitude: startPoint.latitude,
                     longitude: startPoint.longitude,
                     iconPath: '/static/image/map_marker_start.svg',
                     width: 32,
                     height: 38,
                     anchor: {
                        x: 0.5,
                        y: 1
                     }
                  },
                  {
                     id: 2,
                     latitude: endPoint.latitude,
                     longitude: endPoint.longitude,
                     iconPath: '/static/image/map_marker_end.svg',
                     width: 32,
                     height: 38,
                     anchor: {
                        x: 0.5,
                        y: 1
                     }
                  }
               ],
               polyline: [
                  ...(routePoints.length ? [{
                     points: routePoints,
                     color: '#2A7FFF',
                     width: 8,
                     dottedLine: false,
                     arrowLine: true
                  }] : [])
               ],
               includePoints: routePoints.length ? routePoints : [startPoint, endPoint],
               scale: 12,
               tips
            }
         },
         currentOrderList() {
            const orderMap = {
               hall: this.orderList,
@@ -523,8 +686,42 @@
      },
      methods: {
         calculateDistance(startPoint, endPoint) {
            if (!startPoint || !endPoint) {
               return 0
            }
            const toRad = (value) => value * Math.PI / 180
            const earthRadius = 6371
            const deltaLat = toRad(endPoint.latitude - startPoint.latitude)
            const deltaLng = toRad(endPoint.longitude - startPoint.longitude)
            const lat1 = toRad(startPoint.latitude)
            const lat2 = toRad(endPoint.latitude)
            const a = Math.sin(deltaLat / 2) * Math.sin(deltaLat / 2) + Math.cos(lat1) * Math.cos(lat2) * Math.sin(deltaLng / 2) * Math.sin(deltaLng / 2)
            const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a))
            return earthRadius * c
         },
         estimateDuration(distanceKm) {
            if (!distanceKm) {
               return 1
            }
            const averageSpeedKmPerHour = 35
            return Math.max(1, Math.round(distanceKm / averageSpeedKmPerHour * 60))
         },
         handleStatusAction() {
            if (this.detailPopupType === 'delivering') {
               this.showOrderDetail = false
               this.showPhotoDeliverPopup = true
            }
         },
         openDetailPopup(item) {
            this.detailPopupType = this.activeTab
            this.routeInfo = null
            this.detailOrder = {
               ...this.detailOrder,
               ...item,
@@ -534,6 +731,114 @@
               photos: item.photos || this.detailOrder.photos
            }
            this.showOrderDetail = true
            if (this.activeTab === 'pickup' || this.activeTab === 'delivering') {
               this.fetchDrivingRoute(this.detailOrder)
            }
         },
         fetchDrivingRoute(order) {
            if (!order || !order.startPoint || !order.endPoint) {
               return
            }
            const origin = `${order.startPoint.longitude},${order.startPoint.latitude}`
            const destination = `${order.endPoint.longitude},${order.endPoint.latitude}`
            uni.request({
               url: 'https://restapi.amap.com/v3/direction/driving',
               method: 'GET',
               data: {
                  key: 'e4d46c87adf151dca20060317592b1b6',
                  origin,
                  destination,
                  extensions: 'all',
                  strategy: 0,
                  output: 'json'
               },
               success: (response) => {
                  const path = response.data && response.data.route && response.data.route.paths && response.data.route.paths[0]
                  if (!path) {
                     this.routeInfo = this.buildMockRouteInfo(order.startPoint, order.endPoint)
                     return
                  }
                  const points = this.parseRoutePoints(path.steps || [], order.startPoint, order.endPoint)
                  const distanceKm = Number(path.distance || 0) / 1000
                  const durationMinutes = Math.max(1, Math.round(Number(path.duration || 0) / 60))
                  this.routeInfo = {
                     points,
                     distanceKm: distanceKm || this.calculateDistance(order.startPoint, order.endPoint),
                     durationMinutes
                  }
               },
               fail: () => {
                  this.routeInfo = this.buildMockRouteInfo(order.startPoint, order.endPoint)
               }
            })
         },
         buildMockRouteInfo(startPoint, endPoint) {
            const middleLatitude = (startPoint.latitude + endPoint.latitude) / 2
            const middleLongitude = (startPoint.longitude + endPoint.longitude) / 2
            const points = [
               startPoint,
               {
                  latitude: startPoint.latitude + (middleLatitude - startPoint.latitude) * 0.65,
                  longitude: startPoint.longitude + 0.008
               },
               {
                  latitude: middleLatitude + 0.004,
                  longitude: middleLongitude + 0.012
               },
               {
                  latitude: endPoint.latitude - 0.003,
                  longitude: endPoint.longitude - 0.008
               },
               endPoint
            ]
            const distanceKm = this.calculatePathDistance(points)
            const durationMinutes = this.estimateDuration(distanceKm)
            return {
               points,
               distanceKm,
               durationMinutes
            }
         },
         parseRoutePoints(steps, startPoint, endPoint) {
            const points = []
            steps.forEach((step) => {
               const polyline = step.polyline || ''
               polyline.split(';').forEach((pointText) => {
                  const [longitude, latitude] = pointText.split(',').map(Number)
                  if (!Number.isNaN(latitude) && !Number.isNaN(longitude)) {
                     points.push({ latitude, longitude })
                  }
               })
            })
            if (!points.length) {
               return [startPoint, endPoint]
            }
            return points
         },
         calculatePathDistance(points) {
            if (!points || points.length < 2) {
               return 0
            }
            let totalDistance = 0
            for (let index = 1; index < points.length; index += 1) {
               totalDistance += this.calculateDistance(points[index - 1], points[index])
            }
            return totalDistance
         },
         toggleFilterPopup(show) {
@@ -632,6 +937,22 @@
      background: #f5f6f8;
      overflow: hidden;
      
      .order-detail-map-layer {
         position: fixed;
         left: 0;
         right: 0;
         height: 330rpx;
         z-index: 10081;
         overflow: hidden;
         border-top-left-radius: 28rpx;
         border-top-right-radius: 28rpx;
         &__map {
            width: 100%;
            height: 100%;
         }
      }
      .qrcode {
         padding: 36rpx 30rpx;
         box-sizing: border-box;
@@ -680,6 +1001,168 @@
         }
      }
      .photo-deliver {
         padding: 32rpx 28rpx calc(env(safe-area-inset-bottom) + 28rpx);
         background: #ffffff;
         box-sizing: border-box;
         border-top-left-radius: 20rpx;
         border-top-right-radius: 20rpx;
         overflow: hidden;
         &__header {
            display: flex;
            align-items: center;
            justify-content: space-between;
         }
         &__title {
            font-size: 34rpx;
            font-weight: 700;
            color: #111111;
         }
         &__close,
         &__close-placeholder {
            width: 36rpx;
            height: 36rpx;
            flex-shrink: 0;
         }
         &__close-placeholder {
            opacity: 0;
         }
         &__section {
            margin-top: 56rpx;
            &--remark {
               margin-top: 46rpx;
            }
         }
         &__label-row {
            display: flex;
            align-items: center;
            flex-wrap: wrap;
         }
         &__label,
         &__remark-title {
            font-size: 28rpx;
            font-weight: 700;
            color: #23262d;
         }
         &__required {
            margin-left: 4rpx;
            font-size: 28rpx;
            font-weight: 700;
            color: #ff3b30;
         }
         &__hint {
            margin-left: 12rpx;
            font-size: 24rpx;
            color: #a8adb7;
         }
         &__photos {
            display: flex;
            gap: 18rpx;
            margin-top: 30rpx;
         }
         &__upload-card,
         &__preview-card {
            position: relative;
            width: 160rpx;
            height: 160rpx;
            border-radius: 8rpx;
            overflow: hidden;
         }
         &__upload-card {
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
            border: 2rpx dashed #c9ced6;
            background: #ffffff;
            box-sizing: border-box;
         }
         &__upload-icon {
            width: 52rpx;
            height: 52rpx;
         }
         &__upload-text {
            margin-top: 14rpx;
            font-size: 26rpx;
            color: #9da3ae;
         }
         &__preview-card {
            background: #eef1f5;
         }
         &__preview-image {
            width: 100%;
            height: 100%;
         }
         &__preview-mask {
            position: absolute;
            left: 0;
            right: 0;
            bottom: 0;
            display: flex;
            justify-content: center;
            align-items: center;
            height: 48rpx;
            background: rgba(0, 0, 0, 0.46);
         }
         &__preview-delete {
            font-size: 26rpx;
            color: #ffffff;
         }
         &__textarea {
            width: 100%;
            height: 110rpx;
            margin-top: 24rpx;
            padding: 28rpx 24rpx;
            border-radius: 12rpx;
            background: #f7f8fa;
            box-sizing: border-box;
            font-size: 30rpx;
            color: #2c3139;
         }
         &__submit {
            width: 100%;
            height: 88rpx;
            line-height: 88rpx;
            margin-top: 86rpx;
            border-radius: 50rpx;
            background: #106efa;
            font-size: 32rpx;
            font-weight: 700;
            color: #ffffff;
            border: 0;
            padding: 0;
            &::after {
               border: 0;
            }
            &--hover {
               opacity: 0.92;
            }
         }
      }
      .order-detail {
         display: flex;
         flex-direction: column;
@@ -700,14 +1183,18 @@
         &__map {
            position: relative;
            height: 330rpx;
            background: linear-gradient(180deg, #eef5ff 0%, #dbe9ff 100%);
            background: #eef5ff;
            overflow: hidden;
         }
         &__map-image {
         &__map-view {
            width: 100%;
            height: 100%;
            opacity: 0.2;
         }
         &__map-placeholder {
            width: 100%;
            height: 100%;
         }
         &__map-bubble {
@@ -1110,8 +1597,8 @@
         }
         &__confirm-icon {
            width: 32rpx;
            height: 32rpx;
            width: 44rpx;
            height: 44rpx;
            flex-shrink: 0;
         }
      }