619 lines
14 KiB
Vue
619 lines
14 KiB
Vue
<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>
|
||
<!-- 催单弹框(uView Plus up-popup) -->
|
||
<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
|
||
});
|
||
}
|
||
|
||
// 催单弹框控制(使用 uView Plus 的 up-popup)
|
||
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> |