rk
2026-06-11 8caa1157044d2229e56a288cc5665fadf526dd45
app/pages/order-detail/order-detail.vue
@@ -57,7 +57,7 @@
                  </view>
                  <view class="order-detail-page__summary-right">
                     <text class="order-detail-page__price">¥{{ orderDetail.platformRewardAmount ? (orderDetail.driverFee + orderDetail.platformRewardAmount) / 100 : (orderDetail.driverFee / 100).toFixed(2) }}</text>
                     <text class="order-detail-page__price">¥{{ orderDetail.platformRewardAmount ? ((orderDetail.driverFee + orderDetail.platformRewardAmount) / 100).toFixed(2) : (orderDetail.driverFee / 100).toFixed(2) }}</text>
                     <text v-if="orderDetail.platformRewardAmount" class="order-detail-page__extra">含加急¥{{ (orderDetail.platformRewardAmount / 100).toFixed(2) }}</text>
                  </view>
               </view>
@@ -73,9 +73,9 @@
                  </view>
                  <view class="order-detail-page__done-summary-right">
                     <view class="order-detail-page__done-price-row">
                        <text class="order-detail-page__price">¥{{ (orderDetail.driverFee / 100).toFixed(1) }}</text>
                        <text class="order-detail-page__price">¥{{ (orderDetail.driverFee / 100).toFixed(2) }}</text>
                     </view>
                     <text v-if="orderDetail.isUrgent === 1" class="order-detail-page__extra">含加急¥{{ orderDetail.urgentAmount / 100 }}</text>
                     <text v-if="orderDetail.isUrgent === 1" class="order-detail-page__extra">含加急¥{{ (orderDetail.urgentAmount / 100).toFixed(2) }}</text>
                  </view>
               </view>
@@ -119,7 +119,7 @@
                     <image class="order-detail-page__qrcode-image" :src="'https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=' + orderDetail.driverVerifyCode" mode="aspectFit"></image>
                  </view>
                  <text class="order-detail-page__qrcode-value">{{ orderDetail.driverVerifyCode }}</text>
                  <text class="order-detail-page__qrcode-label">取货码</text>
                  <text class="order-detail-page__qrcode-label">{{ orderDetail.status === 3 ? '取货码' : '存件码' }}</text>
               </view>
            </view>
@@ -280,11 +280,83 @@
            <button class="photo-popup__submit" hover-class="photo-popup__submit--hover" @click="submitPhotoPopup">{{ photoPopupSubmitText }}</button>
         </view>
      </u-popup>
      <u-popup :show="showTimelinePopup" round="20" mode="bottom" @close="closeTimelinePopup">
         <view class="track-popup">
            <view class="track-popup__header">
               <text class="track-popup__title">订单轨迹</text>
               <view class="track-popup__close" @click="closeTimelinePopup">×</view>
            </view>
            <scroll-view scroll-y class="track-popup__body">
               <view class="track-empty" v-if="!timelineList.length">暂无轨迹信息</view>
               <view class="track-list" v-else>
                  <view class="track-item" v-for="(track, index) in timelineList" :key="track.key || index">
                     <view class="track-item__content">
                        <view class="track-item__rail">
                           <image v-if="index === 0" class="track-item__dot track-item__dot--active" src="/static/image/dian.png" mode="aspectFit"></image>
                           <view v-else class="track-item__dot"></view>
                           <view class="track-item__line" v-if="index !== timelineList.length - 1"></view>
                        </view>
                        <view class="track-item__body">
                           <text class="track-item__time">{{ track.time }}</text>
                           <text class="track-item__desc">{{ track.title }}</text>
                           <view class="track-item__images" v-if="track.images && track.images.length">
                              <image
                                 v-for="(img, imgIndex) in track.images"
                                 :key="track.key + '-' + imgIndex"
                                 :src="img"
                                 mode="aspectFill"
                                 @click="previewTimelineImages(track.images, imgIndex)"
                              ></image>
                           </view>
                        </view>
                     </view>
                  </view>
               </view>
            </scroll-view>
         </view>
         <!-- <view class="timeline-popup">
            <view class="timeline-popup__header">
               <view class="timeline-popup__placeholder"></view>
               <text class="timeline-popup__title">订单轨迹</text>
               <u-icon name="close" color="#A7ACB5" size="26" @click="closeTimelinePopup"></u-icon>
            </view>
            <scroll-view class="timeline-popup__scroll" scroll-y>
               <view v-if="timelineList.length" class="timeline-popup__list">
                  <view v-for="(item, index) in timelineList" :key="index" class="timeline-popup__item">
                     <view class="timeline-popup__axis">
                        <view class="timeline-popup__dot" :class="index === 0 ? 'timeline-popup__dot--active' : ''"></view>
                        <view v-if="index !== timelineList.length - 1" class="timeline-popup__line"></view>
                     </view>
                     <view class="timeline-popup__content">
                        <text class="timeline-popup__time">{{ item.time || '-' }}</text>
                        <text class="timeline-popup__desc">{{ item.title || '-' }}</text>
                        <view v-if="item.images.length" class="timeline-popup__images">
                           <image
                              v-for="(image, imageIndex) in item.images"
                              :key="imageIndex"
                              class="timeline-popup__image"
                              :src="image"
                              mode="aspectFill"
                              @click="previewTimelineImages(item.images, imageIndex)"
                           ></image>
                        </view>
                     </view>
                  </view>
               </view>
               <view v-else class="timeline-popup__empty">暂无订单轨迹</view>
            </scroll-view>
         </view> -->
      </u-popup>
   </view>
</template>
<script>
import image from 'uview-ui/libs/config/props/image';
   import { mapState } from 'vuex'
   import { chooseImageWithNotice, getLocationWithNotice } from '@/utils/utils'
   export default {
      data() {
@@ -301,10 +373,14 @@
            showCancelModal: false,
            cancelRemain: 0,
            showGrabModal: false,
            showTimelinePopup: false,
            timelineList: [],
            currentLocation: null,
            routePoints: [],
            locationTimer: null,
            distance: 0,
            duration: 0,
            isWithinOperationRadius: true,
            statusTextMap: {
               2: '待接单',
               3: '待取货',
@@ -318,6 +394,7 @@
         }
      },
      computed: {
         ...mapState(['userInfo']),
         formattedRemainTime() {
            const minutes = this.orderDetail.remainMinutes
            if (!minutes) return null
@@ -332,11 +409,9 @@
            return this.orderDetail.status === 3 || this.orderDetail.status === 4
         },
         mapData() {
            const startPoint = this.currentLocation || { latitude: 31.829512, longitude: 117.239211 }
            const startPoint = this.currentLocation || { latitude: this.orderDetail.navigateLat, longitude: this.orderDetail.navigateLng }
            const hasEndPoint = this.orderDetail.navigateLat && this.orderDetail.navigateLng
            const endPoint = hasEndPoint
               ? { latitude: this.orderDetail.navigateLng, longitude: this.orderDetail.navigateLat }
               : { latitude: 31.841268, longitude: 117.278695 }
            const endPoint = { latitude: this.orderDetail.navigateLng, longitude: this.orderDetail.navigateLat }
            let center
            let scale = 12
@@ -370,7 +445,7 @@
            } else if (this.currentLocation) {
               center = this.currentLocation
            } else {
               center = { latitude: 31.83539, longitude: 117.258953 }
               center = { latitude: this.orderDetail.navigateLat, longitude: this.orderDetail.navigateLng }
            }
            const markers = [
@@ -381,10 +456,6 @@
            const routePoints = this.routePoints.length > 0 ? this.routePoints : [
               startPoint,
               { latitude: 31.831624, longitude: 117.247836 },
               { latitude: 31.834918, longitude: 117.255467 },
               { latitude: 31.838214, longitude: 117.265358 },
               { latitude: 31.840126, longitude: 117.272481 },
               endPoint
            ]
@@ -412,26 +483,27 @@
         footerButtons() {
            const status = this.orderDetail.status
            const takeShopId = this.orderDetail.takeShopId
            const buttons = [{ text: '订单轨迹', primary: false, action: 'timeline' }]
            if (status === 2) {
               return [{ text: '立即抢单', primary: true, action: 'grab' }]
               return buttons.concat([{ text: '立即抢单', primary: true, action: 'grab' }])
            }
            if (status === 3) {
               return [
               return buttons.concat([
                  { text: '取消订单', primary: false, action: 'cancel' },
                  { text: '拍照取货', primary: true, action: 'pickup' }
               ]
               ])
            }
            if (status === 4) {
               if (!takeShopId) {
                  return [{ text: '拍照送达', primary: true, action: 'deliver' }]
                  return buttons.concat([{ text: '拍照送达', primary: true, action: 'deliver' }])
               }
               return []
               return buttons
            }
            return []
            return buttons
         },
         photoPopupTitle() {
            return this.photoPopupMode === 'deliver' ? '拍照送达' : '拍照取货'
@@ -456,10 +528,62 @@
            this.getOrderDetail()
         }
      },
      onUnload() {
         if (this.locationTimer) {
            clearInterval(this.locationTimer)
            this.locationTimer = null
         }
      },
      methods: {
         handleBack() {
            uni.navigateBack({ delta: 1 });
         },
         initOperationRadius() {
            console.log('initOperationRadius')
            return new Promise((resolve) => {
               getLocationWithNotice({
                  type: 'gcj02',
                  success: (res) => {
                     this.$u.api.checkDriverOperationRadius({
                        lat: res.latitude,
                        lng: res.longitude,
                        orderId: this.orderId
                     }).then(res => {
                        if (res.code === 200) {
                           this.isWithinOperationRadius = res.data
                           console.log(res.data)
                           if (!this.isWithinOperationRadius) {
                              uni.showToast({
                                 title: '您当前位置与收货地址距离超出范围,请在地址附近重新拍照',
                                 icon: 'none'
                              })
                              resolve(false)
                           } else {
                              resolve(true)
                           }
                        } else {
                           resolve(false)
                        }
                     }).catch(() => {
                        resolve(false)
                     })
                  },
                  fail: () => {
                     this.isWithinOperationRadius = false
                     uni.showToast({
                        title: '无法获取您的位置信息,请前往设置开启定位权限',
                        icon: 'none'
                     })
                     resolve(false)
                  }
               }).catch(() => {
                  resolve(false)
               })
            })
         },
         getOrderDetail() {
            this.$u.api.orderDetail({ orderId: this.orderId }).then(res => {
               if (res.code === 200) {
@@ -477,7 +601,14 @@
         },
         getCurrentLocation() {
            uni.getLocation({
            this.fetchLocation()
            this.locationTimer = setInterval(() => {
               this.fetchLocation()
            }, 60000)
         },
         fetchLocation() {
            getLocationWithNotice({
               type: 'gcj02',
               success: (res) => {
                  this.currentLocation = {
@@ -489,7 +620,7 @@
               fail: (err) => {
                  console.log('获取位置失败', err)
               }
            })
            }).catch(() => {})
         },
         getRoutePlan() {
@@ -499,14 +630,15 @@
            }
            const from = `${this.currentLocation.latitude},${this.currentLocation.longitude}`
            const to = `${this.orderDetail.navigateLng},${this.orderDetail.navigateLat}`
            console.log('driverType', this.userInfo.driverType)
            this.$u.api.directionInfo({
               from,
               to,
               mode: 'driving'
               mode: this.userInfo.driverType
            }).then(res => {
               console.log('directionInfo success:', res)
               if (res && res.paths && res.paths.length > 0) {
                  const path = res.paths[0]
               console.log('paths success:', res.data.route.paths[0])
               if (res.code === 200) {
                  const path = res.data.route.paths[0]
                  this.distance = path.distance
                  this.duration = path.duration
                  const points = []
@@ -523,11 +655,7 @@
                  })
                  this.routePoints = points
                  this.$forceUpdate()
               } else {
                  console.log('No route data returned:', res)
               }
            }).catch(err => {
               console.log('路径规划失败', err)
            })
         },
@@ -585,6 +713,59 @@
            })
         },
         normalizeTimelineImages(images) {
            if (Array.isArray(images)) {
               return images.filter(item => !!item)
            }
            if (typeof images === 'string') {
               return images.split(',').map(item => item.trim()).filter(item => !!item)
            }
            return []
         },
         openTimelinePopup() {
            if (!this.orderId) {
               uni.showToast({ title: '订单ID不存在', icon: 'none' })
               return
            }
            uni.showLoading({ title: '加载中...' })
            this.$u.api.timeline(this.orderId).then(res => {
               if (res.code === 200) {
                  const list = Array.isArray(res.data) ? res.data : []
                  this.timelineList = list.map(item => ({
                     time: item.time || '',
                     title: item.title || '',
                     images: this.normalizeTimelineImages(item.images)
                  }))
                  this.showTimelinePopup = true
               } else {
                  uni.showToast({ title: res.msg || '获取订单轨迹失败', icon: 'none' })
               }
            }).catch(() => {
               uni.showToast({ title: '获取订单轨迹失败', icon: 'none' })
            }).finally(() => {
               uni.hideLoading()
            })
         },
         closeTimelinePopup() {
            this.showTimelinePopup = false
         },
         previewTimelineImages(images, currentIndex) {
            if (!images || !images.length) {
               return
            }
            uni.previewImage({
               current: images[currentIndex],
               urls: images
            })
         },
         handleFooterAction(button) {
            const action = button.action
@@ -594,16 +775,24 @@
            }
            if (action === 'pickup' || action === 'deliver') {
               this.uploadedPhotos = []
               this.photoRemark = ''
               this.photoPopupMode = action
               this.showPhotoPopup = true
               this.initOperationRadius().then((isValid) => {
                  console.log(isValid)
                  if (!isValid) return
                  this.uploadedPhotos = []
                  this.photoRemark = ''
                  this.photoPopupMode = action
                  this.showPhotoPopup = true
               })
               return
            }
            if (action === 'grab') {
               this.handleGrabOrder()
               return
            }
            if (action === 'timeline') {
               this.openTimelinePopup()
            }
         },
         handleCancelOrder() {
@@ -635,6 +824,7 @@
               if (res.code === 200) {
                  uni.showToast({ title: '接单成功', icon: 'success' })
                  this.getOrderDetail()
                  uni.$emit('jiedanSuccess')
                  setTimeout(() => {
                     uni.navigateBack()
                  }, 1500)
@@ -650,33 +840,51 @@
         },
         chooseImage() {
            const count = 3 - this.uploadedPhotos.length
            uni.chooseImage({
            chooseImageWithNotice({
               count: count,
               sourceType: ['camera', 'album'],
               success: (res) => {
                  const tempFilePaths = res.tempFilePaths
                  this.uploadedPhotos = this.uploadedPhotos.concat(tempFilePaths)
               }
            })
            }).catch(() => {})
         },
         deletePhoto(index) {
            this.uploadedPhotos.splice(index, 1)
         },
         submitPhotoPopup() {
            submitPhotoPopup() {
            if (this.uploadedPhotos.length === 0) {
               uni.showToast({ title: '请上传照片', icon: 'none' })
               return
            }
            uni.showLoading({ title: '上传中...' })
            getLocationWithNotice({
               type: 'gcj02',
               success: (locationRes) => {
                  this.doUploadPhotos(locationRes.latitude, locationRes.longitude)
               },
               fail: () => {
                  this.doUploadPhotos(null, null)
               }
            }).catch(() => {
               this.doUploadPhotos(null, null)
            })
         },
         doUploadPhotos(latitude, longitude) {
            const uploadTasks = this.uploadedPhotos.map(path => {
               return new Promise((resolve, reject) => {
                  const formData = { folder: 'orders' }
                  if (latitude && longitude) {
                     formData.latitude = latitude
                     formData.longitude = longitude
                  }
                  console.log('formData:', formData)
                  uni.uploadFile({
                     url: this.$baseUrl + 'web/public/upload',
                     filePath: path,
                     name: 'file',
                     formData: {
                        folder: 'order'
                     },
                     formData: formData,
                     success: (uploadRes) => {
                        const data = JSON.parse(uploadRes.data)
                        if (data.code === 200) {
@@ -698,6 +906,10 @@
                  images: images.map(img => img.imgaddr),
                  orderId: this.orderId,
                  remark: this.photoRemark
               }
               if (latitude && longitude) {
                  params.latitude = latitude
                  params.longitude = longitude
               }
               return this.$u.api[api](params)
            }).then(res => {
@@ -1250,6 +1462,128 @@
         }
      }
   }
   .track-popup {
      background: #FFFFFF;
      border-radius: 24rpx 24rpx 0 0;
      padding: 24rpx 24rpx calc(24rpx + env(safe-area-inset-bottom));
      box-sizing: border-box;
      .track-popup__header {
         height: 88rpx;
         display: flex;
         align-items: center;
         justify-content: center;
         position: relative;
      }
      .track-popup__title {
         font-weight: 600;
         font-size: 32rpx;
         color: #222222;
      }
      .track-popup__close {
         position: absolute;
         right: 0;
         top: 50%;
         transform: translateY(-50%);
         width: 56rpx;
         height: 56rpx;
         display: flex;
         align-items: center;
         justify-content: center;
         font-size: 56rpx;
         line-height: 1;
         color: #A5A7AD;
      }
      .track-popup__body {
         height: 50vh;
      }
      .track-empty {
         line-height: 50vh;
         text-align: center;
         font-size: 28rpx;
         color: #999999;
      }
      .track-list {
         padding: 12rpx 0 0;
      }
      .track-item {
         position: relative;
      }
      .track-item__rail {
         width: 34rpx;
         flex-shrink: 0;
         display: flex;
         flex-direction: column;
         align-items: center;
         align-self: stretch;
         position: relative;
      }
      .track-item__dot {
         width: 16rpx;
         height: 16rpx;
         flex-shrink: 0;
         position: relative;
         z-index: 1;
         border-radius: 50%;
         background: #CCCCCC;
         margin-top: 12rpx;
      }
      .track-item__dot--active {
         width: 32rpx;
         height: 32rpx;
         flex-shrink: 0;
         display: block;
         position: relative;
         z-index: 1;
         margin-top: 4rpx;
         background: transparent;
      }
      .track-item__line {
         width: 2rpx;
         position: absolute;
         left: 50%;
         top: 28rpx;
         bottom: -30rpx;
         transform: translateX(-50%);
         background: #E6EAF0;
      }
      .track-item__content {
         display: flex;
         align-items: flex-start;
         column-gap: 22rpx;
         padding: 0 0 20rpx;
      }
      .track-item__body {
         flex: 1;
         min-width: 0;
      }
      .track-item__time {
         display: block;
         font-weight: 400;
         font-size: 26rpx;
         color: #666666;
         line-height: 1.4;
      }
      .track-item__desc {
         display: block;
         margin-top: 22rpx;
         font-weight: 500;
         font-size: 30rpx;
         color: #666666;
      }
      .track-item__images {
         display: flex;
         flex-wrap: wrap;
         gap: 18rpx;
         margin-top: 24rpx;
         image {
            width: 130rpx;
            height: 130rpx;
            border-radius: 12rpx;
            background: #F3F5F8;
         }
      }
   }
   .photo-popup {
      padding: 30rpx 28rpx calc(env(safe-area-inset-bottom) + 28rpx);
@@ -1408,4 +1742,117 @@
         }
      }
   }
   .timeline-popup {
      padding: 26rpx 28rpx calc(env(safe-area-inset-bottom) + 28rpx);
      background: #ffffff;
      border-top-left-radius: 20rpx;
      border-top-right-radius: 20rpx;
      &__header {
         display: flex;
         align-items: center;
         justify-content: space-between;
      }
      &__placeholder {
         width: 52rpx;
         height: 52rpx;
         flex-shrink: 0;
         opacity: 0;
      }
      &__title {
         font-size: 40rpx;
         font-weight: 700;
         color: #222222;
      }
      &__scroll {
         max-height: 50vh;
         margin-top: 30rpx;
      }
      &__list {
         padding-bottom: 12rpx;
      }
      &__item {
         display: flex;
         align-items: stretch;
      }
      &__axis {
         width: 40rpx;
         display: flex;
         flex-direction: column;
         align-items: center;
         flex-shrink: 0;
      }
      &__dot {
         width: 18rpx;
         height: 18rpx;
         margin-top: 8rpx;
         border-radius: 50%;
         background: #D5D9E2;
         box-shadow: 0 0 0 8rpx #ffffff;
         z-index: 1;
         &--active {
            background: #2F80FF;
            box-shadow: 0 0 0 8rpx rgba(47, 128, 255, 0.14);
         }
      }
      &__line {
         flex: 1;
         width: 2rpx;
         margin-top: 8rpx;
         background: #E5EAF2;
      }
      &__content {
         flex: 1;
         padding: 0 0 40rpx 16rpx;
      }
      &__time {
         display: block;
         font-size: 26rpx;
         font-weight: 400;
         color: #808692;
         line-height: 1.4;
      }
      &__desc {
         display: block;
         margin-top: 18rpx;
         font-size: 40rpx;
         font-weight: 600;
         color: #4A4A4A;
         line-height: 1.45;
      }
      &__images {
         display: flex;
         flex-wrap: wrap;
         gap: 16rpx;
         margin-top: 22rpx;
      }
      &__image {
         width: 124rpx;
         height: 124rpx;
         border-radius: 12rpx;
         background: #F2F4F7;
      }
      &__empty {
         line-height: 50vh;
         text-align: center;
         font-size: 28rpx;
         color: #A0A6B0;
      }
   }
</style>