Files
medical-mall/pages/mall/delivery/order-detail.uvue
2026-02-02 18:20:22 +08:00

937 lines
27 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
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>
<view class="delivery-order-detail">
<!-- 返回按钮 -->
<view class="back-header">
<view class="back-box" @click="goBack">
<text class="back-icon"></text>
<text class="back-text">返回</text>
</view>
</view>
<!-- 订单状态 -->
<view class="order-status">
<view class="status-progress">
<view class="progress-item" :class="{ active: order.status >= 1 }">
<view class="progress-dot"></view>
<text class="progress-text">待接单</text>
</view>
<view class="progress-line" :class="{ active: order.status >= 2 }"></view>
<view class="progress-item" :class="{ active: order.status >= 2 }">
<view class="progress-dot"></view>
<text class="progress-text">已接单</text>
</view>
<view class="progress-line" :class="{ active: order.status >= 3 }"></view>
<view class="progress-item" :class="{ active: order.status >= 3 }">
<view class="progress-dot"></view>
<text class="progress-text">取货中</text>
</view>
<view class="progress-line" :class="{ active: order.status >= 4 }"></view>
<view class="progress-item" :class="{ active: order.status >= 4 }">
<view class="progress-dot"></view>
<text class="progress-text">已取货</text>
</view>
<view class="progress-line" :class="{ active: order.status >= 5 }"></view>
<view class="progress-item" :class="{ active: order.status >= 5 }">
<view class="progress-dot"></view>
<text class="progress-text">已送达</text>
</view>
</view>
<text class="status-desc">{{ getStatusDesc() }}</text>
</view>
<!-- 配送信息 -->
<view class="delivery-info">
<view class="section-title">配送信息</view>
<view class="delivery-route">
<view class="route-item pickup">
<view class="route-icon">📦</view>
<view class="route-content">
<text class="route-title">取货地址</text>
<text class="route-address">{{ merchant.contact_name }} · {{ merchant.contact_phone }}</text>
<text class="route-detail">{{ pickupAddress }}</text>
</view>
<!-- 只在取货中状态且订单未完成时显示按钮 -->
<button v-if="order.status === 3 && order.status < 5" class="route-action" @click="confirmPickup">确认取货</button>
</view>
<view class="route-line"></view>
<view class="route-item delivery">
<view class="route-icon">🏠</view>
<view class="route-content">
<text class="route-title">送货地址</text>
<text class="route-address">{{ getDeliveryAddress().name }} · {{ getDeliveryAddress().phone }}</text>
<text class="route-detail">{{ getDeliveryAddress().detail }}</text>
</view>
<!-- 只在已取货状态且订单未完成时显示按钮 -->
<button v-if="order.status === 4 && order.status < 5" class="route-action" @click="confirmDelivery">确认送达</button>
</view>
</view>
<view class="delivery-distance">
<text class="distance-label">配送距离:</text>
<text class="distance-value">{{ deliveryInfo.distance }}km</text>
<text class="time-label">预计时长:</text>
<text class="time-value">{{ deliveryInfo.estimated_time }}分钟</text>
</view>
</view>
<!-- 订单商品 -->
<view class="order-products">
<view class="section-title">订单商品</view>
<view class="shop-info">
<text class="shop-name">{{ merchant.shop_name }}</text>
<text class="order-no">订单号:{{ order.order_no }}</text>
</view>
<view v-for="item in orderItems" :key="item.id" class="product-item">
<image :src="item.product_image || '/static/default-product.png'" class="product-image" />
<view class="product-info">
<text class="product-name">{{ item.product_name }}</text>
<text v-if="item.sku_specifications" class="product-spec">{{ getSpecText(item.sku_specifications) }}</text>
<view class="price-quantity">
<text class="product-price">¥{{ item.price }}</text>
<text class="product-quantity">×{{ item.quantity }}</text>
</view>
</view>
</view>
<view class="order-summary">
<view class="summary-item">
<text class="summary-label">商品总额</text>
<text class="summary-value">¥{{ order.total_amount }}</text>
</view>
<view class="summary-item">
<text class="summary-label">配送费</text>
<text class="summary-value delivery-fee">¥{{ order.delivery_fee }}</text>
</view>
<view class="summary-item total">
<text class="summary-label">实付金额</text>
<text class="summary-value">¥{{ order.actual_amount }}</text>
</view>
</view>
</view>
<!-- 配送备注 -->
<view class="delivery-notes">
<view class="section-title">配送备注</view>
<view class="note-item">
<text class="note-label">顾客备注:</text>
<text class="note-content">{{ customerNote || '无备注' }}</text>
</view>
<view class="note-item">
<text class="note-label">商家备注:</text>
<text class="note-content">{{ merchantNote || '无备注' }}</text>
</view>
<!-- 只在订单未完成时显示输入框 -->
<view v-if="order.status < 5" class="note-item">
<text class="note-label">配送备注:</text>
<input v-model="deliveryNote" class="note-input" placeholder="请输入配送备注" />
</view>
<!-- 如果订单已完成,显示存储的备注 -->
<view v-else-if="deliveryNote" class="note-item">
<text class="note-label">配送备注:</text>
<text class="note-content">{{ deliveryNote }}</text>
</view>
</view>
<!-- 联系方式 -->
<view class="contact-section">
<view class="section-title">联系方式</view>
<view class="contact-list">
<view class="contact-item" @click="callCustomer">
<view class="contact-icon">📞</view>
<view class="contact-info">
<text class="contact-name">联系顾客</text>
<text class="contact-phone">{{ getDeliveryAddress().phone }}</text>
</view>
</view>
<view class="contact-item" @click="callMerchant">
<view class="contact-icon">🏪</view>
<view class="contact-info">
<text class="contact-name">联系商家</text>
<text class="contact-phone">{{ merchant.contact_phone }}</text>
</view>
</view>
</view>
</view>
<!-- 底部操作 -->
<view class="bottom-actions">
<!-- 只在待接单状态且订单未完成时显示接受/拒绝订单按钮 -->
<button v-if="order.status === 1" class="action-btn accept" @click="acceptOrder">接受订单</button>
<button v-if="order.status === 1" class="action-btn reject" @click="rejectOrder">拒绝订单</button>
<!-- 只在已接单状态且订单未完成时显示“前往取货”和“正在取货”按钮 -->
<button v-if="order.status === 2" class="action-btn navigate" @click="startNavigation">前往取货</button>
<button v-if="order.status === 2" class="action-btn complete" @click="confirmArrivedAtPickup">正在取货</button>
<!-- 只在取货中状态且订单未完成时显示“确认取货”按钮 -->
<button v-if="order.status === 3" class="action-btn complete" @click="confirmPickup">确认取货</button>
<!-- 只在已取货状态且订单未完成时显示“确认送达”按钮 -->
<button v-if="order.status === 4" class="action-btn complete" @click="confirmDelivery">确认送达</button>
<!-- 始终显示联系客服按钮 -->
<button class="action-btn contact" @click="contactService">联系客服</button>
</view>
</view>
</template>
<script lang="uts">
import supa, { supaReady } from '@/components/supadb/aksupainstance.uts'
import { getCurrentUserId } from '@/utils/store.uts'
export default {
data() {
return {
order: {
id: '',
order_no: '',
user_id: '',
merchant_id: '',
status: 0, // 👈 从 URL 参数获取
total_amount: 0,
discount_amount: 0,
delivery_fee: 0,
actual_amount: 0,
payment_method: 0,
payment_status: 0,
delivery_address: {},
created_at: ''
},
orderItems: [],
merchant: {
id: '',
user_id: '',
shop_name: '',
shop_logo: '',
shop_banner: '',
shop_description: '',
contact_name: '',
contact_phone: '',
shop_status: 0,
rating: 0,
total_sales: 0,
created_at: ''
},
deliveryInfo: {
distance: 0,
estimated_time: 0,
courier_id: '',
pickup_time: '',
delivery_time: ''
},
pickupAddress: '',
customerNote: '',
merchantNote: '',
deliveryNote: '',
}
},
onLoad(options: any) {
const orderId = options.id as string
// ✅ 从 URL 参数获取 status
const status = parseInt(options.status as string) || 0
if (orderId) {
this.order.id = orderId
this.order.status = status // ✅ 设置传入的状态
this.loadOrderDetail(orderId)
}
},
methods: {
goBack() {
uni.navigateBack()
},
async loadOrderDetail(orderId: string) {
const originalStatus = this.order.status
try {
await supaReady
console.log('loadOrderDetail called', { orderId })
// 获取订单主表:按 id(UUID) / cid(数字) / order_no 三种可能匹配
let orderRes: any = null
const isUuid = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(orderId)
const isNumber = /^\d+$/.test(orderId)
console.log('loadOrderDetail: supa session=', supa.getSession && supa.getSession())
console.log('loadOrderDetail: detect id format', { isUuid, isNumber })
if (isUuid) {
console.log('loadOrderDetail: querying by id')
orderRes = await supa.from('ml_orders').select('*').eq('id', orderId).limit(1).execute()
} else if (isNumber) {
console.log('loadOrderDetail: querying by cid')
orderRes = await supa.from('ml_orders').select('*').eq('cid', Number(orderId)).limit(1).execute()
} else {
console.log('loadOrderDetail: querying by order_no')
orderRes = await supa.from('ml_orders').select('*').eq('order_no', orderId).limit(1).execute()
}
console.log('loadOrderDetail: orderRes=', orderRes)
if (orderRes && Array.isArray(orderRes.data) && orderRes.data.length > 0) {
const row = orderRes.data[0]
const shipping = row.shipping_address || {}
this.order = Object.assign(this.order, {
id: row.id,
order_no: row.order_no || '',
user_id: row.user_id || '',
merchant_id: row.merchant_id || '',
status: Number(row.order_status) || originalStatus,
total_amount: Number(row.total_amount || row.product_amount || 0),
discount_amount: Number(row.discount_amount || 0),
delivery_fee: Number(row.shipping_fee || row.shipping_fee || 0),
actual_amount: Number(row.paid_amount || row.actual_amount || 0),
payment_method: row.payment_method || 0,
payment_status: row.payment_status || 0,
delivery_address: {
name: shipping.name || shipping.recipient || '',
phone: shipping.phone || shipping.mobile || '',
detail: shipping.detail || shipping.address || JSON.stringify(shipping)
},
created_at: row.created_at || ''
})
this.customerNote = row.remark || ''
this.merchantNote = row.merchant_memo || ''
// 读取订单商品 - 使用从主表获得的真实 order id
const realOrderId = row.id
const itemsRes: any = await supa.from('ml_order_items').select('*').eq('order_id', realOrderId).execute()
console.log('loadOrderDetail: itemsRes=', itemsRes)
if (itemsRes && Array.isArray(itemsRes.data)) {
this.orderItems = itemsRes.data.map((it:any) => ({
id: it.id,
order_id: it.order_id,
product_id: it.product_id,
sku_id: it.sku_id,
product_name: it.product_name,
sku_specifications: it.specifications || it.sku_name || null,
price: Number(it.price) || 0,
quantity: Number(it.quantity) || 0,
total_amount: Number(it.total_amount) || 0,
product_image: it.image_url || it.product_image || ''
}))
}
// 读取商家店铺信息ml_shops
if (row.merchant_id) {
const shopRes: any = await supa.from('ml_shops').select('*').eq('merchant_id', row.merchant_id).limit(1).execute()
console.log('loadOrderDetail: shopRes=', shopRes)
if (shopRes && Array.isArray(shopRes.data) && shopRes.data.length > 0) {
const s = shopRes.data[0]
this.merchant = Object.assign(this.merchant, {
id: s.id || '',
user_id: s.merchant_id || '',
shop_name: s.shop_name || s.name || '',
shop_logo: s.shop_logo || '',
shop_banner: s.shop_banner || '',
shop_description: s.description || s.shop_description || '',
contact_name: s.contact_name || '',
contact_phone: s.contact_phone || '',
shop_status: s.status || 0,
rating: s.rating || 0,
total_sales: s.total_sales || 0,
created_at: s.created_at || ''
})
this.pickupAddress = s.address || ''
}
}
// deliveryInfo 从 ml_delivery_tasks 中读取(如果存在)
const dtRes: any = await supa.from('ml_delivery_tasks').select('*').eq('order_id', realOrderId).limit(1).execute()
console.log('loadOrderDetail: dtRes=', dtRes)
if (dtRes && Array.isArray(dtRes.data) && dtRes.data.length > 0) {
const dt = dtRes.data[0]
this.deliveryInfo.distance = Number(dt.distance) || this.deliveryInfo.distance
this.deliveryInfo.estimated_time = Number(dt.estimated_time) || this.deliveryInfo.estimated_time
this.deliveryInfo.courier_id = dt.driver_id || ''
this.deliveryInfo.pickup_time = dt.pickup_time || ''
this.deliveryInfo.delivery_time = dt.delivered_time || ''
}
}
} catch (e) {
console.error('loadOrderDetail db error', e)
}
},
getStatusDesc(): string {
if (this.order.status >= 5) {
return '订单已送达完成'
} else if (this.order.status === 4) {
return '已取货,正在前往送达'
} else if (this.order.status === 3) {
return '配送员正在前往取货'
} else if (this.order.status === 2) {
return '订单已接取'
} else if (this.order.status === 1) {
return '等待配送员接单'
} else {
return '订单状态未知'
}
},
getDeliveryAddress(): any {
return this.order.delivery_address as any
},
getSpecText(specifications: any): string {
if (!specifications) return ''
return Object.keys(specifications).map(key => `${key}: ${specifications[key]}`).join(', ')
},
confirmPickup() {
if (this.order.status < 5) {
uni.showModal({
title: '确认取货',
content: '确认已从商家处取到商品?',
success: async (res) => {
if (res.confirm) {
try {
await supaReady
// 更新 delivery task
const dtRes: any = await supa.from('ml_delivery_tasks').update({ status: 4, pickup_time: new Date().toISOString() }).eq('order_id', this.order.id).execute()
// 同步订单状态
await supa.from('ml_orders').update({ order_status: 4 }).eq('id', this.order.id).execute()
this.order.status = 4
uni.showToast({ title: '取货确认成功', icon: 'success' })
} catch (e) {
console.error('confirmPickup db error', e)
uni.showToast({ title: '取货确认失败', icon: 'none' })
}
}
}
})
}
},
confirmDelivery() {
if (this.order.status < 5) {
uni.showModal({
title: '确认送达',
content: '确认商品已送达到顾客手中?',
success: async (res) => {
if (res.confirm) {
try {
await supaReady
// 更新 delivery task
await supa.from('ml_delivery_tasks').update({ status: 6, delivered_time: new Date().toISOString() }).eq('order_id', this.order.id).execute()
// 更新订单状态为已送达
await supa.from('ml_orders').update({ order_status: 5 }).eq('id', this.order.id).execute()
this.order.status = 5
uni.showToast({ title: '送达确认成功', icon: 'success' })
} catch (e) {
console.error('confirmDelivery db error', e)
uni.showToast({ title: '送达确认失败', icon: 'none' })
}
}
}
})
}
},
acceptOrder() {
if (this.order.status < 5) {
uni.showModal({
title: '接受订单',
content: '确定接受这个配送订单吗?',
success: async (res) => {
if (res.confirm) {
try {
await supaReady
const userId = getCurrentUserId()
let driverId = null
if (userId) {
const dRes: any = await supa.from('ml_delivery_drivers').select('id').eq('user_id', userId).limit(1).execute()
if (dRes && Array.isArray(dRes.data) && dRes.data.length > 0) driverId = dRes.data[0].id
}
// 尝试更新已有 delivery task
const updateRes: any = await supa.from('ml_delivery_tasks').update({ driver_id: driverId, status: 2 }).eq('order_id', this.order.id).execute()
if (!(updateRes && Array.isArray(updateRes.data) && updateRes.data.length > 0)) {
// 若不存在,则插入新的 task
await supa.from('ml_delivery_tasks').insert({ order_id: this.order.id, driver_id: driverId, status: 2, created_at: new Date().toISOString() }).execute()
}
// 更新订单状态
await supa.from('ml_orders').update({ order_status: 2 }).eq('id', this.order.id).execute()
this.order.status = 2
uni.showToast({ title: '订单接受成功', icon: 'success' })
} catch (e) {
console.error('acceptOrder db error', e)
uni.showToast({ title: '接受订单失败', icon: 'none' })
}
}
}
})
}
},
rejectOrder() {
if (this.order.status < 5) {
uni.showModal({
title: '拒绝订单',
content: '确定拒绝这个配送订单吗?',
success: async (res) => {
if (res.confirm) {
try {
await supaReady
// 标记订单为已拒绝order_status = 7
await supa.from('ml_orders').update({ order_status: 7 }).eq('id', this.order.id).execute()
uni.showToast({ title: '订单已拒绝', icon: 'success' })
uni.navigateBack()
} catch (e) {
console.error('rejectOrder db error', e)
uni.showToast({ title: '拒绝订单失败', icon: 'none' })
}
}
}
})
}
},
startNavigation() {
if (this.order.status < 5) {
uni.showToast({
title: '正在启动导航',
icon: 'loading'
})
setTimeout(() => {
uni.showToast({
title: '导航已启动',
icon: 'success'
})
}, 1000)
}
},
callCustomer() {
const phone = this.getDeliveryAddress().phone
uni.makePhoneCall({ phoneNumber: phone })
},
callMerchant() {
uni.makePhoneCall({ phoneNumber: this.merchant.contact_phone })
},
contactService() {
uni.navigateTo({
url: `/pages/mall/service/chat?orderId=${this.order.id}&type=delivery`
})
}
}
}
</script>
<style>
/* 保持原有样式不变 */
.delivery-order-detail {
background-color: #f5f5f5;
min-height: 100vh;
padding-bottom: 160rpx;
}
/* ... 其余样式保持原样 ... */
/* 返回按钮头部 */
.back-header {
background-color: #fff;
padding: 20rpx 30rpx;
position: relative;
height: 80rpx;
display: flex;
align-items: center;
}
.back-box {
position: absolute;
left: 30rpx;
top: 50%;
transform: translateY(-50%);
display: flex;
align-items: center;
cursor: pointer;
padding: 10rpx;
border-radius: 8rpx;
transition: background-color 0.2s ease;
}
.back-box:active {
background-color: #f0f0f0;
}
.back-icon {
font-size: 36rpx;
color: #333;
margin-right: 5rpx;
}
.back-text {
font-size: 28rpx;
color: #333;
font-weight: 500;
}
.order-status {
background-color: #fff;
padding: 40rpx 30rpx;
margin-bottom: 20rpx;
}
.status-progress {
display: flex;
align-items: center;
margin-bottom: 20rpx;
}
.progress-item {
display: flex;
flex-direction: column;
align-items: center;
flex: 1;
}
.progress-dot {
width: 24rpx;
height: 24rpx;
border-radius: 50%;
background-color: #ddd;
margin-bottom: 10rpx;
}
.progress-item.active .progress-dot {
background-color: #4caf50;
}
.progress-text {
font-size: 22rpx;
color: #666;
}
.progress-item.active .progress-text {
color: #4caf50;
font-weight: bold;
}
.progress-line {
height: 2rpx;
background-color: #ddd;
flex: 1;
margin: 0 20rpx;
margin-bottom: 32rpx;
}
.progress-line.active {
background-color: #4caf50;
}
.status-desc {
font-size: 26rpx;
color: #666;
text-align: center;
}
.delivery-info, .order-products, .delivery-notes, .contact-section {
background-color: #fff;
padding: 30rpx;
margin-bottom: 20rpx;
}
.section-title {
font-size: 30rpx;
font-weight: bold;
color: #333;
margin-bottom: 25rpx;
}
.delivery-route {
margin-bottom: 30rpx;
}
.route-item {
display: flex;
align-items: flex-start;
padding: 25rpx 0;
}
.route-icon {
font-size: 36rpx;
margin-right: 20rpx;
margin-top: 10rpx;
}
.route-content {
flex: 1;
}
.route-title {
font-size: 28rpx;
font-weight: bold;
color: #333;
margin-bottom: 10rpx;
}
.route-address {
font-size: 26rpx;
color: #666;
margin-bottom: 8rpx;
}
.route-detail {
font-size: 24rpx;
color: #999;
line-height: 1.4;
}
.route-action {
padding: 15rpx 30rpx;
background-color: #4caf50;
color: #fff;
border-radius: 25rpx;
font-size: 24rpx;
border: none;
}
.route-line {
width: 2rpx;
height: 40rpx;
background-color: #ddd;
margin-left: 38rpx;
}
.delivery-distance {
display: flex;
align-items: center;
padding: 20rpx;
background-color: #f8f9fa;
border-radius: 10rpx;
}
.distance-label, .time-label {
font-size: 24rpx;
color: #666;
margin-right: 10rpx;
}
.distance-value, .time-value {
font-size: 24rpx;
color: #333;
font-weight: bold;
margin-right: 30rpx;
}
.shop-info {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20rpx 0;
border-bottom: 1rpx solid #f5f5f5;
margin-bottom: 25rpx;
}
.shop-name {
font-size: 28rpx;
color: #333;
font-weight: bold;
}
.order-no {
font-size: 24rpx;
color: #666;
}
.product-item {
display: flex;
padding: 25rpx 0;
border-bottom: 1rpx solid #f5f5f5;
}
.product-item:last-child {
border-bottom: none;
}
.product-image {
width: 100rpx;
height: 100rpx;
border-radius: 8rpx;
margin-right: 20rpx;
}
.product-info {
flex: 1;
}
.product-name {
font-size: 26rpx;
color: #333;
margin-bottom: 8rpx;
line-height: 1.3;
}
.product-spec {
font-size: 22rpx;
color: #999;
margin-bottom: 12rpx;
}
.price-quantity {
display: flex;
justify-content: space-between;
align-items: center;
}
.product-price {
font-size: 26rpx;
color: #ff4444;
font-weight: bold;
}
.product-quantity {
font-size: 24rpx;
color: #666;
}
.order-summary {
margin-top: 25rpx;
padding-top: 20rpx;
border-top: 1rpx solid #f5f5f5;
}
.summary-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8rpx 0;
}
.summary-label {
font-size: 26rpx;
color: #666;
}
.summary-value {
font-size: 26rpx;
color: #333;
}
.summary-value.delivery-fee {
color: #4caf50;
}
.summary-item.total {
border-top: 1rpx solid #f5f5f5;
margin-top: 10rpx;
padding-top: 15rpx;
}
.summary-item.total .summary-label,
.summary-item.total .summary-value {
font-weight: bold;
font-size: 28rpx;
color: #ff4444;
}
.note-item {
margin-bottom: 20rpx;
}
.note-label {
font-size: 26rpx;
color: #666;
margin-bottom: 8rpx;
}
.note-content {
font-size: 26rpx;
color: #333;
line-height: 1.4;
}
.note-input {
width: 100%;
padding: 15rpx;
border: 1rpx solid #ddd;
border-radius: 8rpx;
font-size: 26rpx;
background-color: #f8f9fa;
}
.contact-list {
display: flex;
gap: 30rpx;
}
.contact-item {
flex: 1;
display: flex;
align-items: center;
padding: 25rpx;
background-color: #f8f9fa;
border-radius: 10rpx;
}
.contact-icon {
font-size: 32rpx;
margin-right: 15rpx;
}
.contact-info {
flex: 1;
}
.contact-name {
font-size: 26rpx;
color: #333;
margin-bottom: 5rpx;
}
.contact-phone {
font-size: 24rpx;
color: #007aff;
}
.bottom-actions {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background-color: #fff;
padding: 25rpx 30rpx;
box-shadow: 0 -2rpx 10rpx rgba(0, 0, 0, 0.1);
display: flex;
gap: 15rpx;
flex-wrap: wrap;
}
.action-btn {
flex: 1;
min-width: 120rpx;
height: 70rpx;
border-radius: 35rpx;
font-size: 26rpx;
border: none;
margin: 5rpx;
box-sizing: border-box;
}
.action-btn.accept, .action-btn.complete {
background-color: #4caf50;
color: #fff;
}
.action-btn.reject {
background-color: #ff4444;
color: #fff;
}
.action-btn.navigate {
background-color: #2196f3;
color: #fff;
}
.action-btn.contact {
background-color: #ffa726;
color: #fff;
}
</style>