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

617 lines
14 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">
<!-- 地图占位可替换为原生 map 组件或第三方地图组件 -->
<view class="map-area">
<!-- 真实项目建议使用 <map> 并渲染 polyline/markers -->
<image class="map-image" src="/static/img/map-placeholder.png" mode="widthFix" v-if="!mapAvailable" />
<map v-else class="map-native" :latitude="order?.pickupLat" :longitude="order?.pickupLng" show-location
enable-3D enable-zoom :scale="16"></map>
<view class="map-overlay">
<view class="eta">距离商家{{ distanceText }}预计{{ etaText }}到达</view>
<view class="nav-btn" @click="navigateToShop">导航到商家</view>
</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:140rpx;"></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">催单</view>
<view class="btn remind" @click="openRemindPopup">电话联系</view>
<!-- <view class="btn confirm">转单</view> -->
<view class="btn confirm" @click="confirmArrive">确认到店</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>
</view>
</s-layout>
</template>
<script setup>
import {
ref,
computed
} from 'vue';
import {
onLoad
} from '@dcloudio/uni-app';
import sheep from '@/sheep';
const orderId = ref(null);
const order = ref(null);
const loading = ref(false);
const mapAvailable = ref(false); // 如果需要使用 map 组件,置为 true
// 入口:从页面参数取 orderId然后加载数据
onLoad((options = {}) => {
orderId.value = options.id || options.orderId || null;
fetchOrder();
});
async function fetchOrder() {
loading.value = true;
try {
// 优先尝试平台统一 request项目内可能封装在 sheep.request 或 sheep.api
if (sheep && typeof sheep.request === 'function') {
const res = await sheep.request({
url: '/order/detail',
method: 'GET',
data: {
id: orderId.value
}
});
// 根据封装不同,这里兼容 res.data 或 res
order.value = (res && res.data) ? res.data : res;
} else if (sheep && sheep.$api && sheep.$api.trade && typeof sheep.$api.trade.detail === 'function') {
const res = await sheep.$api.trade.detail({
id: orderId.value
});
order.value = res?.data || res;
} else {
// 回退 mock 数据,避免界面空白,开发时替换为真实接口
order.value = {
id: orderId.value || 1001,
shopName: '店铺名(示例)',
pickupAddress: '广东省广州市天河区学院站荷光路118-121号',
pickupLat: 23.1005,
pickupLng: 113.3301,
deliveryAddress: '广东省广州市天河区华景新城软件园区B栋西梯501',
deliveryLat: 23.105,
deliveryLng: 113.335,
receiverName: '张先生',
receiverPhone: '13900001234',
note: '依据餐量提供餐具',
items: [{
name: '商品名称A',
spec: '规格1',
quantity: 2,
price: 23.89
},
{
name: '商品名称B',
spec: '规格2',
quantity: 1,
price: 45.00
}
]
};
}
} 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;
}
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();
}
function confirmArrive() {
// 确认到店:调用接口或本地改变状态
if (!order.value) return;
// 示例:调用后端接口(兼容性判断)
(async () => {
try {
if (sheep && typeof sheep.request === 'function') {
await sheep.request({
url: '/order/confirmArrive',
method: 'POST',
data: {
id: order.value.id
}
});
}
sheep.$helper && sheep.$helper.toast && sheep.$helper.toast('已确认到店');
// 可在此刷新订单状态
fetchOrder();
} catch (e) {
console.error(e);
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: 360rpx;
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: 20rpx;
display: flex;
justify-content: space-between;
align-items: center;
}
.eta {
background: rgba(255, 255, 255, 0.95);
padding: 10rpx 14rpx;
border-radius: 20rpx;
font-size: 24rpx;
color: #333;
}
.nav-btn {
background: #fff;
padding: 10rpx 14rpx;
border-radius: 20rpx;
color: #1e9fff;
border: 1rpx solid #dbeeff;
}
.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;
}
.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>