Files
delivery-uniapp/pages/order/detail.vue

699 lines
16 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<s-layout title="订单详情" class="set-userinfo-wrap">
<view class="order-detail-page">
<!-- 地图区域 -->
<view class="map-area">
<map class="map-native" :latitude="order?.pickupLat" :longitude="order?.pickupLng" show-location
enable-3D enable-zoom :scale="16"></map>
<cover-view class="map-overlay">
<cover-view class="eta">距离商家{{ distanceText }}预计{{ etaText }}到达</cover-view>
<cover-view class="nav-btn" @click="navigateToShop">导航到商家</cover-view>
</cover-view>
</view>
<!-- 地址块 -->
<scroll-view class="content" scroll-y="true">
<view class="address-block">
<view class="addr-row">
<view class="badge pickup"></view>
<view class="addr-body">
<text class="addr-title">{{ order?.shopName || '店铺名称' }}</text>
<text class="addr-sub">{{ order?.pickupAddress }}</text>
</view>
<view class="nav-icon" @click="openMap(order?.pickupLat, order?.pickupLng, order?.pickupAddress)">导航</view>
</view>
<view class="addr-row">
<view class="badge deliver"></view>
<view class="addr-body">
<text class="addr-title">{{ order?.deliveryAddress }}</text>
<text
class="addr-sub">收货人{{ order?.receiverName }} {{ order?.receiverPhone ? ('尾号' + (order.receiverPhone + '').slice(-4)) : ''}}</text>
</view>
<view class="nav-icon" @click="openMap(order?.deliveryLat, order?.deliveryLng, order?.deliveryAddress)">导航
</view>
</view>
</view>
<!-- 顾客备注 -->
<view class="note" v-if="order?.note">
<text>顾客{{ order.note }}</text>
</view>
<!-- 商品清单 -->
<view class="goods-list">
<view class="goods-header">
<text>商品清单</text>
<text class="item-count">{{ totalCount }}</text>
<text class="total-price">¥{{ totalPrice.toFixed(2) }}</text>
</view>
<view class="goods-item" v-for="(g, idx) in order?.items || []" :key="idx">
<view class="g-left">
<text class="g-name">{{ g.name }}{{ g.spec ? ('·' + g.spec) : '' }}</text>
<text class="g-qty">×{{ g.quantity }}</text>
</view>
<view class="g-right">¥{{ (g.price || 0).toFixed(2) }}</view>
</view>
</view>
<!-- 联系/记录等 -->
<view class="action-record">
<!-- <view class="call" @click="callPhone(order?.receiverPhone)">
<text>电话联系</text>
</view> -->
<view class="record" @click="toRecord">交接记录</view>
</view>
<!-- 占位底部高度避免内容被底部按钮遮挡 -->
<view style="height:200rpx;"></view>
</scroll-view>
<!-- 底部操作 -->
<view class="fixed-actions">
<view class="left-actions">
<view class="icon-phone" @click="callPhone(order?.receiverPhone)"></view>
</view>
<view class="right-actions">
<view class="btn remind"
@click="sheep.$helper.toast('功能开发中ing')">催单</view>
<view class="btn remind" @click="openRemindPopup">电话联系</view>
<view class="btn confirm" v-if="order?.deliveryStatus == 2" @click="confirmArrive">确认到店</view>
<view class="btn confirm" v-if="order?.deliveryStatus == 3" @click="confirmPickup">确认取餐</view>
<view class="btn handover" v-if="order?.deliveryStatus == 4" @click="deliveryHandover">送达交接点</view>
<view class="btn handover" v-if="order?.deliveryStatus == 5" @click="openDeliveryPopup">确认送达顾客</view>
</view>
</view>
<!-- 催单弹框 -->
<up-popup v-model:show="showRemind" mode="bottom" :closeable="false" border-radius="12">
<view class="remind-popup">
<view class="remind-row" @click="callShopPhone">
<text class="remind-title">联系商家</text>
<view class="remind-btn" @click.stop="callShopPhone">拨打电话</view>
</view>
<view class="remind-row" @click="callCustomerPhone">
<text class="remind-title">联系顾客</text>
<view class="remind-btn" @click.stop="callCustomerPhone">拨打电话</view>
</view>
</view>
<template #bottom>
<view class="remind-cancel" @click="showRemind = false">取消</view>
</template>
</up-popup>
<!-- 确认送达顾客弹框 -->
<DeliveryPopup
:show="showDeliveryPopup"
:receiverPhone="order?.receiverPhone || ''"
@update:show="showDeliveryPopup = $event"
@submit="handleDeliveryConfirm"
@close="showDeliveryPopup = false"
/>
</view>
</s-layout>
</template>
<script setup>
import {
ref,
computed
} from 'vue';
import {
onLoad
} from '@dcloudio/uni-app';
import sheep from '@/sheep';
import DeliveryOrderApi from '@/sheep/api/member/deliveryOrder';
import DeliveryPopup from '@/pages/index/components/delivery-popup.vue';
const orderId = ref(null);
const order = ref(null);
const loading = ref(false);
const mapAvailable = ref(true); // 如果需要使用 map 组件,置为 true
// 入口:从页面参数取 orderId然后加载数据
onLoad((options = {}) => {
orderId.value = options.id || options.orderId || null;
fetchOrder();
});
async function fetchOrder() {
loading.value = true;
try {
// 使用 DeliveryOrderApi 获取配送单详情
const res = await DeliveryOrderApi.getDetail(orderId.value);
// 根据封装不同,这里兼容 res.data 或 res
const resData = (res && res.data) ? res.data : res;
console.log("res数据", res);
if (resData) {
// 字段映射:将接口返回字段映射到页面使用字段
order.value = {
...resData,
// 店铺坐标 (取货点)
pickupLat: resData.shopLatitude,
pickupLng: resData.shopLongitude,
pickupAddress: resData.shopAddress,
// 收货地址
deliveryLat: resData.receiverLatitude,
deliveryLng: resData.receiverLongitude,
deliveryAddress: resData.receiverAddress,
// 商品列表
items: resData.orderItemDataList?.map(item => ({
name: item.productName,
quantity: item.quantity,
price: parseFloat(item.unitPrice) || 0
})) || [],
// 备注
note: resData.orderRemark,
deliveryStatus: resData.deliveryStatus
};
}
console.log("order.value数据", order.value);
} catch (e) {
console.error('fetchOrder error', e);
// 友好提示
sheep.$helper && sheep.$helper.toast && sheep.$helper.toast('获取订单失败,请稍后重试');
} finally {
loading.value = false;
}
}
const totalCount = computed(() => {
if (!order.value || !order.value.items) return 0;
return order.value.items.reduce((s, it) => s + (it.quantity || 0), 0);
});
const totalPrice = computed(() => {
if (!order.value || !order.value.items) return 0;
return order.value.items.reduce((s, it) => s + ((it.price || 0) * (it.quantity || 0)), 0);
});
// 显示距离与预计时间(示例,真实项目可使用服务端或高德/百度 SDK 计算)
const distanceText = computed(() => {
// 此处为示例固定值,后续接入定位/路程计算替换
return order.value ? '873m' : '--';
});
const etaText = computed(() => {
return order.value ? '预计十分钟到达' : '--';
});
function goBack() {
uni.navigateBack();
}
function openMap(lat, lng, name) {
if (!lat || !lng) {
sheep.$helper && sheep.$helper.toast && sheep.$helper.toast('无法获取坐标');
return;
}
uni.openLocation({
latitude: Number(lat),
longitude: Number(lng),
name: name || '',
scale: 18
});
}
function navigateToShop() {
openMap(order.value?.pickupLat, order.value?.pickupLng, order.value?.pickupAddress);
}
function callPhone(phone) {
if (!phone) {
sheep.$helper && sheep.$helper.toast && sheep.$helper.toast('未找到联系电话');
return;
}
uni.makePhoneCall({
phoneNumber: phone
});
}
// 催单弹框控制
const showRemind = ref(false);
function openRemindPopup() {
showRemind.value = true;
}
function closeRemindPopup() {
showRemind.value = false;
}
// 确认送达顾客弹框控制
const showDeliveryPopup = ref(false);
function openDeliveryPopup() {
showDeliveryPopup.value = true;
}
// 确认送达顾客
async function handleDeliveryConfirm(imageUrl) {
console.log('送达照片URL:', imageUrl);
if (!orderId.value) {
sheep.$helper.toast('订单信息异常');
return;
}
try {
const res = await DeliveryOrderApi.riderConfirmDelivery({
deliveryOrderId: orderId.value,
imageUrl: imageUrl
});
if (res.code === 0 && res.data === true) {
sheep.$helper.toast('已提交送达照片');
showDeliveryPopup.value = false;
// 刷新订单状态
fetchOrder();
} else {
sheep.$helper.toast(res.msg || '提交失败');
}
} catch (error) {
console.error('确认送达异常:', error);
sheep.$helper.toast('提交失败,请重试');
}
}
function callShopPhone() {
// 商家电话优先使用 order.shopPhone否则尝试 fallback
const phone = order.value?.shopPhone || order.value?.shopPhoneNumber || order.value?.receiverPhone || '';
if (!phone) {
sheep.$helper && sheep.$helper.toast && sheep.$helper.toast('未找到商家电话');
return;
}
callPhone(phone);
closeRemindPopup();
}
function callCustomerPhone() {
const phone = order.value?.receiverPhone || '';
if (!phone) {
sheep.$helper && sheep.$helper.toast && sheep.$helper.toast('未找到顾客电话');
return;
}
callPhone(phone);
closeRemindPopup();
}
// 确认到店
async function confirmArrive() {
if (!order.value || !orderId.value) return;
try {
uni.showLoading({ title: '提交中...' });
const res = await DeliveryOrderApi.riderConfirmArrival(orderId.value);
uni.hideLoading();
if (res.code === 0 && res.data === true) {
sheep.$helper.toast('已确认到店');
// 刷新订单状态
fetchOrder();
} else {
sheep.$helper.toast(res.msg || '确认到店失败');
}
} catch (e) {
uni.hideLoading();
console.error('confirmArrive error', e);
sheep.$helper.toast('确认失败,请重试');
}
}
// 确认取餐
async function confirmPickup() {
if (!order.value || !orderId.value) return;
try {
uni.showLoading({ title: '提交中...' });
const res = await DeliveryOrderApi.riderConfirmPickup(orderId.value);
uni.hideLoading();
if (res.code === 0 && res.data === true) {
sheep.$helper.toast('已确认取餐');
// 刷新订单状态
fetchOrder();
} else {
sheep.$helper.toast(res.msg || '确认取餐失败');
}
} catch (e) {
uni.hideLoading();
console.error('confirmPickup error', e);
sheep.$helper.toast('确认失败,请重试');
}
}
// 送达交接点
async function deliveryHandover() {
if (!order.value || !orderId.value) return;
try {
uni.showLoading({ title: '提交中...' });
await DeliveryOrderApi.riderDeliveryHandover(orderId.value);
uni.hideLoading();
sheep.$helper && sheep.$helper.toast && sheep.$helper.toast('已送达交接点');
// 刷新订单状态
fetchOrder();
} catch (e) {
console.error('deliveryHandover error', e);
uni.hideLoading();
sheep.$helper && sheep.$helper.toast && sheep.$helper.toast('提交失败,请重试');
}
}
//跳转交接记录
function toRecord(id) {
uni.navigateTo({
url: `/pages/order/handoverRecord?orderId=${orderId.value}`
})
}
</script>
<style scoped lang="scss">
.order-detail-page {
// background: #fff;
background: #f7f7f7;
min-height: 100vh;
position: relative;
overflow-x: hidden;
box-sizing: border-box;
-webkit-overflow-scrolling: touch;
}
.header {
height: 88rpx;
display: flex;
align-items: center;
padding: 0 20rpx;
border-bottom: 1rpx solid #eee;
}
.back {
width: 44rpx;
font-size: 40rpx;
color: #333;
}
.title {
flex: 1;
text-align: center;
font-size: 32rpx;
font-weight: 700;
color: #333;
margin-right: 44rpx;
}
.map-area {
height: 560rpx;
background: #f3f3f3;
position: relative;
overflow: hidden;
}
.map-image {
width: 100%;
height: 100%;
display: block;
box-sizing: border-box;
}
.map-native {
width: 100%;
height: 100%;
}
.map-overlay {
position: absolute;
left: 20rpx;
right: 20rpx;
bottom: 45rpx;
display: flex;
justify-content: space-between;
align-items: center;
flex-direction: row;
}
.eta {
background: rgba(255, 255, 255, 0.95);
padding: 0rpx 14rpx;
border-radius: 20rpx;
font-size: 24rpx;
color: #333;
text-align: center;
// line-height: 32rpx;
}
.nav-btn {
background: #fff;
padding: 0rpx 20rpx;
border-radius: 20rpx;
color: #1e9fff;
border: 1rpx solid #dbeeff;
font-size: 24rpx;
text-align: center;
// line-height: 32rpx;
}
.content {
padding: 20rpx;
padding-left: 20rpx;
padding-right: 20rpx;
background: #f7f7f7;
min-height: 200rpx;
box-sizing: border-box;
overflow-x: hidden;
}
.address-block {
background: #fff;
padding: 16rpx;
border-radius: 12rpx;
box-shadow: 0 2rpx 6rpx rgba(0, 0, 0, 0.04);
}
.addr-row {
display: flex;
align-items: flex-start;
padding: 12rpx 0;
border-bottom: 1rpx solid #f0f0f0;
}
.addr-row:last-child {
border-bottom: none;
}
.badge {
width: 46rpx;
height: 46rpx;
border-radius: 23rpx;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-weight: 700;
font-size: 20rpx;
margin-right: 12rpx;
}
.badge.pickup {
background: #87d6ff;
}
.badge.deliver {
background: #ffd591;
color: #333;
}
.addr-body {
flex: 1;
min-width: 0;
}
.addr-title {
display: block;
font-size: 28rpx;
font-weight: 600;
word-break: break-word;
}
.addr-sub {
display: block;
font-size: 22rpx;
color: #888;
margin-top: 6rpx;
}
.nav-icon {
color: #1e9fff;
padding: 6rpx 8rpx;
}
.note {
margin-top: 12rpx;
background: #fff;
padding: 12rpx;
border-radius: 8rpx;
color: #666;
}
.goods-list {
margin-top: 14rpx;
}
.goods-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12rpx;
background: #fff;
border-radius: 8rpx;
font-weight: 700;
color: #333;
}
.goods-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12rpx;
background: #fff;
margin-top: 8rpx;
border-radius: 8rpx;
box-sizing: border-box;
}
.g-left {
display: flex;
gap: 8rpx;
align-items: center;
}
.g-left {
min-width: 0;
}
.g-name {
font-size: 26rpx;
color: #333;
max-width: 62%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.g-qty {
color: #888;
margin-left: 8rpx;
}
.g-right {
color: #333;
font-weight: 600;
}
.action-record {
display: flex;
gap: 12rpx;
margin-top: 14rpx;
}
.call {
flex: 1;
background: #fff;
padding: 12rpx;
border-radius: 8rpx;
text-align: center;
border: 1rpx solid #ddd;
}
.record {
flex: 2;
background: #fff;
padding: 12rpx;
border-radius: 8rpx;
text-align: center;
border: 1rpx solid #ddd;
}
.fixed-actions {
position: fixed;
left: 0;
right: 0;
bottom: 0;
height: 140rpx;
display: flex;
align-items: center;
padding: 20rpx;
gap: 12rpx;
background: rgba(255, 255, 255, 0.98);
box-shadow: 0 -2rpx 8rpx rgba(0, 0, 0, 0.04);
}
.left-actions {
width: 72rpx;
display: flex;
align-items: center;
justify-content: center;
}
.right-actions {
flex: 1;
display: flex;
gap: 12rpx;
justify-content: flex-end;
align-items: center;
}
.btn {
padding: 16rpx 18rpx;
border-radius: 12rpx;
color: #fff;
font-weight: 700;
}
.btn.remind {
background: #f39c12;
}
.btn.confirm {
background: #1e9fff;
}
.btn.handover {
background: #2ecc71;
}
.remind-popup {
padding: 20rpx 0;
}
.remind-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20rpx;
background: #f6f6f6;
margin: 10rpx 16rpx;
border-radius: 8rpx;
}
.remind-title {
font-size: 28rpx;
font-weight: 700;
color: #333;
}
.remind-btn {
background: #1e9fff;
color: #fff;
padding: 10rpx 18rpx;
border-radius: 8rpx;
font-weight: 700;
}
.remind-cancel {
text-align: center;
padding: 18rpx 0 70rpx;
color: #666;
font-size: 26rpx;
background: #fff;
margin-top: 10rpx;
}
</style>