Files
medical-mall/pages/mall/merchant/order-detail.uvue

952 lines
23 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="order-detail-page">
<!-- #ifdef MP-WEIXIN -->
<view style="padding-top: var(--status-bar-height); background-color: #ffffff; display: flex; flex-direction: row; align-items: flex-end; border-bottom: 1rpx solid #eeeeee; box-sizing: border-box; height: calc(88rpx + var(--status-bar-height));">
<view style="display: flex; flex-direction: row; align-items: center; padding: 0 30rpx; height: 88rpx;" @click="uni.navigateBack()">
<text style="font-size: 44rpx; color: #333333; line-height: 1; margin-right: 6rpx;"></text>
<text style="font-size: 28rpx; color: #333333;">返回</text>
</view>
</view>
<!-- #endif -->
<!-- 订单状态头部 -->
<view class="status-header" :class="getStatusBgClass(order.order_status)">
<text class="status-icon">{{ getStatusIcon(order.order_status) }}</text>
<text class="status-text">{{ getStatusText(order.order_status) }}</text>
<text class="status-desc">{{ getStatusDesc(order.order_status) }}</text>
</view>
<!-- 物流信息 -->
<view v-if="order.order_status >= 3" class="section logistics-section">
<view class="section-title">物流信息</view>
<view class="logistics-info">
<view class="logistics-company">
<text class="label">物流公司:</text>
<text class="value">{{ order.shipping_company || '待填写' }}</text>
</view>
<view class="logistics-number">
<text class="label">物流单号:</text>
<text class="value">{{ order.tracking_number || '待填写' }}</text>
<text v-if="order.tracking_number" class="copy-btn" @click="copyTrackingNumber">复制</text>
</view>
</view>
</view>
<!-- 收货地址 -->
<view class="section address-section">
<view class="section-title">收货信息</view>
<view class="address-info">
<view class="address-user">
<text class="name">{{ addressData.recipient_name || '未知' }}</text>
<text class="phone">{{ addressData.phone || '未知' }}</text>
</view>
<view class="address-detail">
{{ addressData.province || '' }}{{ addressData.city || '' }}{{ addressData.district || '' }}{{ addressData.detail_address || '' }}
</view>
</view>
</view>
<!-- 订单信息 -->
<view class="section order-info-section">
<view class="section-title">订单信息</view>
<view class="info-list">
<view class="info-item">
<text class="label">订单编号:</text>
<text class="value">{{ order.order_no }}</text>
<text class="copy-btn" @click="copyOrderNo">复制</text>
</view>
<view class="info-item">
<text class="label">下单时间:</text>
<text class="value">{{ formatTime(order.created_at) }}</text>
</view>
<view v-if="order.paid_at" class="info-item">
<text class="label">付款时间:</text>
<text class="value">{{ formatTime(order.paid_at) }}</text>
</view>
<view v-if="order.shipped_at" class="info-item">
<text class="label">发货时间:</text>
<text class="value">{{ formatTime(order.shipped_at) }}</text>
</view>
<view v-if="order.remark" class="info-item">
<text class="label">订单备注:</text>
<text class="value">{{ order.remark }}</text>
</view>
</view>
</view>
<!-- 商品列表 -->
<view class="section products-section">
<view class="section-title">商品信息</view>
<view class="products-list">
<view v-for="item in order.items" :key="item.id" class="product-item">
<image
:src="item.image_url || '/static/images/default-product.png'"
class="product-image"
mode="aspectFill"
/>
<view class="product-info">
<text class="product-name">{{ item.product_name }}</text>
<text class="product-spec">{{ item.sku_name || '标准规格' }}</text>
</view>
<view class="product-right">
<text class="product-price">¥{{ item.price }}</text>
<text class="product-quantity">x{{ item.quantity }}</text>
</view>
</view>
</view>
</view>
<!-- 费用明细 -->
<view class="section fees-section">
<view class="section-title">费用明细</view>
<view class="fees-list">
<view class="fee-item">
<text class="label">商品金额:</text>
<text class="value">¥{{ order.product_amount }}</text>
</view>
<view class="fee-item">
<text class="label">运费:</text>
<text class="value">¥{{ order.shipping_fee }}</text>
</view>
<view v-if="order.discount_amount > 0" class="fee-item">
<text class="label">优惠:</text>
<text class="value discount">-¥{{ order.discount_amount }}</text>
</view>
<view class="fee-item total">
<text class="label">实付金额:</text>
<text class="value">¥{{ order.total_amount }}</text>
</view>
</view>
</view>
<!-- 操作按钮 -->
<view class="action-buttons">
<view
v-if="order.order_status === 2"
class="action-btn primary"
@click="shipOrder"
>
去发货
</view>
<view
v-if="order.order_status === 3"
class="action-btn primary"
@click="viewLogistics"
>
查看物流
</view>
<view
v-if="order.order_status === 3"
class="action-btn primary"
@click="confirmDelivery"
>
确认收货
</view>
<view
v-if="order.order_status === -1 || order.order_status === 5"
class="action-btn danger"
@click="deleteOrder"
>
删除订单
</view>
<view class="action-btn default" @click="contactBuyer">
联系买家
</view>
</view>
<!-- 发货弹窗 -->
<view v-if="showShipModal" class="modal-mask" @click="closeShipModal">
<view class="modal-content" @click.stop>
<view class="modal-header">
<text class="modal-title">发货</text>
<text class="modal-close" @click="closeShipModal">×</text>
</view>
<view class="modal-body">
<view class="form-item">
<text class="form-label">物流公司</text>
<picker
class="form-picker"
:range="logisticsCompanies"
range-key="name"
@change="onLogisticsChange"
>
<view class="picker-value">
{{ selectedLogistics?.name || '请选择物流公司' }}
</view>
</picker>
</view>
<view class="form-item">
<text class="form-label">物流单号</text>
<input
class="form-input"
v-model="trackingNumber"
placeholder="请输入物流单号"
/>
</view>
</view>
<view class="modal-footer">
<view class="modal-btn cancel" @click="closeShipModal">取消</view>
<view class="modal-btn confirm" @click="confirmShip">确认发货</view>
</view>
</view>
</view>
</view>
</template>
<script lang="uts">
import supa from '@/components/supadb/aksupainstance.uts'
type OrderItemType = {
id: string
order_id: string
product_id: string
sku_id: string
product_name: string
sku_name: string
price: number
quantity: number
image_url: string
sku_snapshot: string
}
type AddressType = {
recipient_name: string
phone: string
province: string
city: string
district: string
detail_address: string
}
type LogisticsType = {
name: string
code: string
}
export default {
data() {
return {
orderId: '',
order: {
id: '',
order_no: '',
user_id: '',
merchant_id: '',
order_status: 1,
total_amount: 0,
product_amount: 0,
shipping_fee: 0,
discount_amount: 0,
paid_amount: 0,
shipping_address: '',
remark: '',
shipping_company: '',
tracking_number: '',
paid_at: '',
shipped_at: '',
created_at: '',
updated_at: '',
items: [] as OrderItemType[]
},
addressData: {
recipient_name: '',
phone: '',
province: '',
city: '',
district: '',
detail_address: ''
} as AddressType,
showShipModal: false,
logisticsCompanies: [
{ name: '顺丰速运', code: 'SF' },
{ name: '圆通速递', code: 'YTO' },
{ name: '中通快递', code: 'ZTO' },
{ name: '韵达快递', code: 'YD' },
{ name: '申通快递', code: 'STO' },
{ name: 'EMS', code: 'EMS' },
{ name: '京东物流', code: 'JD' }
] as LogisticsType[],
selectedLogistics: { name: '', code: '' } as LogisticsType,
trackingNumber: ''
}
},
onLoad(options: any) { console.log('--- DEBUG ON LOAD ---', options)
let id = ''
if (options['id'] != null) {
id = options['id'] as string
} else if (options.id != null) {
id = options.id as string
}
if (id !== '') {
this.orderId = id
this.loadOrderDetail()
}
},
methods: {
async loadOrderDetail() { console.log('--- DEBUG LOAD ORDER DETAIL ---', this.orderId); try {
const response = await supa
.from('ml_orders')
.select(`
*,
ml_order_items (
id,
order_id,
product_id,
sku_id,
product_name,
sku_name,
price,
quantity,
image_url,
sku_snapshot
)
`)
.eq('id', this.orderId)
.single()
.execute()
if (response.error != null || (response.status ?? 200) >= 400) {
console.error('获取订单详情失败:', response.error)
uni.showToast({ title: '加载失败', icon: 'none' })
return
}
console.log('--- DEBUG RAW ORDER DATA ---', response.data); let realData = response.data;
let isArrLike = false;
if (response.data != null && (response.data as any)['0'] != null) {
realData = (response.data as any)['0'];
isArrLike = true;
}
console.log('--- EXTRACTED realData ---', isArrLike);
const rawData = realData as UTSJSONObject
if (rawData == null) return
this.order = {
id: String(rawData['id'] ?? '') || '',
order_no: String(rawData['order_no'] ?? '') || '',
user_id: String(rawData['user_id'] ?? '') || '',
merchant_id: String(rawData['merchant_id'] ?? '') || '',
order_status: Number(rawData['order_status'] ?? 0) || 1,
total_amount: Number(rawData['total_amount'] ?? 0) || 0,
product_amount: Number(rawData['product_amount'] ?? 0) || 0,
shipping_fee: Number(rawData['shipping_fee'] ?? 0) || 0,
discount_amount: Number(rawData['discount_amount'] ?? 0) || 0,
paid_amount: Number(rawData['paid_amount'] ?? 0) || 0,
shipping_address: String(rawData['shipping_address'] ?? '') || '{}',
remark: String(rawData['remark'] ?? '') || '',
shipping_company: String(rawData['carrier_name'] ?? rawData['shipping_company'] ?? '') || '',
tracking_number: String(rawData['tracking_no'] ?? rawData['tracking_number'] ?? '') || '',
paid_at: String(rawData['paid_at'] ?? '') || '',
shipped_at: String(rawData['shipped_at'] ?? '') || '',
created_at: String(rawData['created_at'] ?? '') || '',
updated_at: String(rawData['updated_at'] ?? '') || '',
items: []
}
const itemsObj = rawData['ml_order_items']
if (itemsObj != null && Array.isArray(itemsObj)) {
const itemsArray = itemsObj as any[]
for (let i = 0; i < itemsArray.length; i++) {
const orderItem = itemsArray[i] as UTSJSONObject
this.order.items.push({
id: String(orderItem['id'] ?? '') || '',
order_id: String(orderItem['order_id'] ?? '') || '',
product_id: String(orderItem['product_id'] ?? '') || '',
sku_id: String(orderItem['sku_id'] ?? '') || '',
product_name: String(orderItem['product_name'] ?? '') || '',
sku_name: String(orderItem['sku_name'] ?? '') || '',
price: Number(orderItem['price'] ?? 0) || 0,
quantity: Number(orderItem['quantity'] ?? 0) || 0,
image_url: String(orderItem['image_url'] ?? '') || '',
sku_snapshot: ''
} as OrderItemType)
}
}
this.parseAddress()
} catch (e) {
console.error('获取订单详情异常:', e)
}
},
parseAddress() {
try {
const addrStr = this.order.shipping_address
if (addrStr && addrStr !== '{}') {
const addrObj = JSON.parse(addrStr) as AddressType
this.addressData = addrObj
}
} catch (e) {
console.error('解析地址失败:', e)
}
},
getStatusIcon(status: number): string {
if (status === 1) return '💰'
if (status === 2) return '📦'
if (status === 3) return '🚚'
if (status === 4) return '✅'
if (status === 0) return '↩️'
if (status === 5 || status === -1) return '❌'
return '📋'
},
getStatusText(status: number): string {
if (status === 1) return '待付款'
if (status === 2) return '待发货'
if (status === 3) return '待收货'
if (status === 4) return '已完成'
if (status === 0) return '退款中'
if (status === 5 || status === -1) return '已取消'
return '未知'
},
getStatusDesc(status: number): string {
if (status === 1) return '买家已下单,请尽快发货'
if (status === 2) return '等待商家发货'
if (status === 3) return '商品运输中,请关注物流'
if (status === 4) return '订单已完成'
if (status === 0) return '买家申请退款,请处理'
if (status === 5 || status === -1) return '订单已取消'
return ''
},
getStatusBgClass(status: number): string {
if (status === 1) return 'status-bg-1'
if (status === 2) return 'status-bg-2'
if (status === 3) return 'status-bg-3'
if (status === 4) return 'status-bg-4'
if (status === 0 || status === 5 || status === -1) return 'status-bg-0'
return 'status-bg-1'
},
formatTime(timeStr: string): string {
if (!timeStr) return '-'
const date = new Date(timeStr)
const year = date.getFullYear()
const month = (date.getMonth() + 1).toString().padStart(2, '0')
const day = date.getDate().toString().padStart(2, '0')
const hour = date.getHours().toString().padStart(2, '0')
const minute = date.getMinutes().toString().padStart(2, '0')
return `${year}-${month}-${day} ${hour}:${minute}`
},
copyOrderNo() {
uni.setClipboardData({
data: this.order.order_no,
success: () => {
uni.showToast({ title: '复制成功', icon: 'success' })
}
})
},
copyTrackingNumber() {
uni.setClipboardData({
data: this.order.tracking_number,
success: () => {
uni.showToast({ title: '复制成功', icon: 'success' })
}
})
},
shipOrder() {
this.showShipModal = true
},
closeShipModal() {
this.showShipModal = false
this.selectedLogistics = {} as LogisticsType
this.trackingNumber = ''
},
onLogisticsChange(e: any) {
const index = e.detail.value as number
this.selectedLogistics = this.logisticsCompanies[index]
},
async confirmShip() {
if (this.selectedLogistics == null || !this.selectedLogistics?.name) {
uni.showToast({ title: '请选择物流公司', icon: 'none' })
return
}
if (!this.trackingNumber) {
uni.showToast({ title: '请输入物流单号', icon: 'none' })
return
}
try {
const response = await supa
.from('ml_orders')
.update({
order_status: 3,
shipping_status: 2,
carrier_name: this.selectedLogistics?.name, tracking_no: this.trackingNumber,
shipped_at: new Date().toISOString(),
updated_at: new Date().toISOString()
})
.eq('id', this.order.id)
.execute()
if (response.error != null || (response.status ?? 200) >= 400) {
let msg = response.error?.message ?? (response.data != null ? JSON.stringify(response.data) : '请检查网络或登录状态'); uni.showToast({ title: '发货被拦截: ' + msg, icon: 'none', duration: 4500 }); console.error('SUPABASE API ERR:', response)
return
}
uni.showToast({ title: '发货成功', icon: 'success' })
this.closeShipModal()
this.loadOrderDetail()
} catch (e) { uni.showToast({ title: '发货发生异常', icon: 'none' }); console.error(e) }
},
viewLogistics() {
uni.navigateTo({
url: `/pages/mall/merchant/logistics?orderId=${this.order.id}`
})
},
async confirmDelivery() {
uni.showModal({
title: '确认收货',
content: '确认买家已收到货物吗?',
success: async (res) => {
if (res.confirm) {
try {
const response = await supa
.from('ml_orders')
.update({
order_status: 4,
delivered_at: new Date().toISOString(),
completed_at: new Date().toISOString(),
updated_at: new Date().toISOString()
})
.eq('id', this.order.id)
.execute()
if (response.error != null || (response.status ?? 200) >= 400) {
uni.showToast({ title: '操作失败', icon: 'none' })
return
}
uni.showToast({ title: '操作成功', icon: 'success' })
this.loadOrderDetail()
} catch (e) {
uni.showToast({ title: '操作失败', icon: 'none' })
}
}
}
})
},
async deleteOrder() {
uni.showModal({
title: '确认删除',
content: '确定要删除该订单吗?',
success: async (res) => {
if (res.confirm) {
try {
const response = await supa
.from('ml_orders')
.delete()
.eq('id', this.order.id)
.execute()
if (response.error != null || (response.status ?? 200) >= 400) {
uni.showToast({ title: '删除失败', icon: 'none' })
return
}
uni.showToast({ title: '删除成功', icon: 'success' })
setTimeout(() => {
uni.navigateBack()
}, 1500)
} catch (e) {
uni.showToast({ title: '删除失败', icon: 'none' })
}
}
}
})
},
contactBuyer() {
uni.navigateTo({
url: `/pages/mall/merchant/chat?userId=${this.order.user_id}`
})
}
}
}
</script>
<style>
.order-detail-page {
background-color: #f5f5f5;
min-height: 100vh;
padding-bottom: 180rpx;
}
.status-header {
padding: 50rpx 30rpx;
text-align: center;
}
.status-bg-1 {
background: linear-gradient(135deg, #FF9800 0%, #FF5722 100%);
}
.status-bg-2 {
background: linear-gradient(135deg, #2196F3 0%, #1976D2 100%);
}
.status-bg-3 {
background: linear-gradient(135deg, #4CAF50 0%, #388E3C 100%);
}
.status-bg-4 {
background: linear-gradient(135deg, #9C27B0 0%, #7B1FA2 100%);
}
.status-bg-0, .status-bg-5 {
background: linear-gradient(135deg, #607D8B 0%, #455A64 100%);
}
.status-icon {
font-size: 60rpx;
display: block;
margin-bottom: 20rpx;
}
.status-text {
font-size: 36rpx;
font-weight: bold;
color: #fff;
display: block;
margin-bottom: 10rpx;
}
.status-desc {
font-size: 26rpx;
color: rgba(255, 255, 255, 0.8);
}
.section {
background-color: #fff;
margin-bottom: 20rpx;
padding: 30rpx;
}
.section-title {
font-size: 30rpx;
font-weight: bold;
color: #333;
margin-bottom: 24rpx;
}
.logistics-info {
font-size: 28rpx;
}
.logistics-company, .logistics-number {
display: flex;
align-items: center;
margin-bottom: 16rpx;
}
.logistics-number .label {
margin-right: 16rpx;
}
.logistics-number .value {
flex: 1;
}
.label {
color: #999;
margin-right: 16rpx;
}
.value {
color: #333;
}
.address-info {
font-size: 28rpx;
}
.address-user {
display: flex;
align-items: center;
margin-bottom: 16rpx;
}
.address-user .name {
font-weight: bold;
margin-right: 20rpx;
}
.address-user .phone {
color: #666;
}
.address-detail {
color: #666;
line-height: 1.5;
}
.info-list {
font-size: 28rpx;
}
.info-item {
display: flex;
align-items: center;
margin-bottom: 16rpx;
}
.info-item .label {
min-width: 140rpx;
}
.info-item .value {
flex: 1;
}
.copy-btn {
color: #007AFF;
font-size: 24rpx;
margin-left: 16rpx;
}
.products-list {
display: flex;
flex-direction: column;
}
.product-item {
display: flex;
align-items: center;
padding: 20rpx 0;
border-bottom: 1rpx solid #f5f5f5;
}
.product-item:last-child {
border-bottom: none;
}
.product-image {
width: 140rpx;
height: 140rpx;
border-radius: 8rpx;
margin-right: 20rpx;
background-color: #f5f5f5;
}
.product-info {
flex: 1;
display: flex;
flex-direction: column;
}
.product-name {
font-size: 28rpx;
color: #333;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.product-spec {
font-size: 24rpx;
color: #999;
margin-top: 8rpx;
}
.product-right {
display: flex;
flex-direction: column;
align-items: flex-end;
}
.product-price {
font-size: 28rpx;
color: #333;
font-weight: 500;
}
.product-quantity {
font-size: 24rpx;
color: #999;
margin-top: 8rpx;
}
.fees-list {
font-size: 28rpx;
}
.fee-item {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16rpx;
}
.fee-item .value {
color: #333;
}
.fee-item .value.discount {
color: #FF5722;
}
.fee-item.total {
margin-top: 20rpx;
padding-top: 20rpx;
border-top: 1rpx solid #f5f5f5;
}
.fee-item.total .label {
font-weight: bold;
color: #333;
}
.fee-item.total .value {
font-size: 32rpx;
font-weight: bold;
color: #FF3B30;
}
.action-buttons {
position: fixed;
bottom: 0;
left: 0;
right: 0;
display: flex;
gap: 20rpx;
padding: 20rpx 30rpx;
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
background-color: #fff;
box-shadow: 0 -2rpx 10rpx rgba(0, 0, 0, 0.05);
}
.action-btn {
flex: 1;
height: 80rpx;
line-height: 80rpx;
text-align: center;
font-size: 28rpx;
border-radius: 40rpx;
}
.action-btn.primary {
background-color: #007AFF;
color: #fff;
}
.action-btn.default {
background-color: #f5f5f5;
color: #333;
}
.action-btn.danger {
background-color: #FF3B30;
color: #fff;
}
.modal-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: flex-end;
justify-content: center;
z-index: 99;
}
.modal-content {
width: 100%;
background-color: #fff;
border-radius: 24rpx 24rpx 0 0;
padding-bottom: env(safe-area-inset-bottom);
position: relative;
z-index: 99;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 30rpx;
border-bottom: 1rpx solid #f5f5f5;
}
.modal-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
}
.modal-close {
font-size: 44rpx;
color: #999;
line-height: 1;
}
.modal-body {
padding: 30rpx;
}
.form-item {
margin-bottom: 30rpx;
}
.form-label {
font-size: 28rpx;
color: #333;
display: block;
margin-bottom: 16rpx;
}
.form-picker, .form-input {
height: 72rpx;
border: 1rpx solid #e5e5e5;
border-radius: 8rpx;
padding: 0 20rpx;
font-size: 28rpx;
}
.picker-value {
height: 72rpx;
line-height: 72rpx;
color: #333;
}
.modal-footer {
display: flex;
border-top: 1rpx solid #f5f5f5;
}
.modal-btn {
flex: 1;
height: 88rpx;
line-height: 88rpx;
text-align: center;
font-size: 28rpx;
}
.modal-btn.cancel {
color: #666;
border-right: 1rpx solid #f5f5f5;
}
.modal-btn.confirm {
color: #007AFF;
font-weight: bold;
}
</style>