| | |
| | | <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"> |
| | |
| | | <!-- 订单详情 --> |
| | | <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"> |
| | |
| | | </view> |
| | | </view> |
| | | </view> |
| | | |
| | | <scroll-view class="order-detail__scroll" scroll-y> |
| | | |
| | | <view class="order-detail__content"> |
| | | <view class="order-detail__head"> |
| | |
| | | <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> |
| | |
| | | tts: null, |
| | | show: false, |
| | | show1: false, |
| | | showPhotoDeliverPopup: false, |
| | | showOrderDetail: false, |
| | | detailPopupType: 'hall', |
| | | routeInfo: null, |
| | | statusBarHeight: 0, |
| | | headerHeight: 0, |
| | | tabbarHeight: 0, |
| | |
| | | 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' } |
| | |
| | | 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' } |
| | |
| | | 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' } |
| | |
| | | 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' } |
| | |
| | | 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', |
| | |
| | | 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', |
| | |
| | | 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, |
| | |
| | | }, |
| | | |
| | | 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, |
| | |
| | | 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) { |
| | |
| | | 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; |
| | |
| | | } |
| | | } |
| | | |
| | | .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; |
| | |
| | | &__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 { |
| | |
| | | } |
| | | |
| | | &__confirm-icon { |
| | | width: 32rpx; |
| | | height: 32rpx; |
| | | width: 44rpx; |
| | | height: 44rpx; |
| | | flex-shrink: 0; |
| | | } |
| | | } |