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

669 lines
18 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">
<scroll-view scroll-y="true" class="scroll-content">
<!-- 订单状态 -->
<view class="order-status" :class="getStatusClass()">
<view class="status-icon">
<text class="status-emoji">{{ getStatusIcon() }}</text>
</view>
<view class="status-info">
<text class="status-text">{{ getStatusText() }}</text>
<text class="status-desc">{{ getStatusDesc() }}</text>
</view>
</view>
<!-- 配送信息 -->
<view v-if="order.order_status >= 2" class="delivery-info card">
<view class="delivery-header">
<text class="section-title">配送信息</text>
</view>
<view class="delivery-address">
<view class="address-icon">📍</view>
<view class="address-content">
<view class="address-user">
<text class="recipient">{{ deliveryAddress.name }}</text>
<text class="phone">{{ deliveryAddress.phone }}</text>
</view>
<text class="address-detail">{{ getFullAddress(deliveryAddress) }}</text>
</view>
</view>
<!-- 如果有物流信息显示 -->
<view v-if="deliveryInfo.tracking_no" class="courier-info">
<text class="courier-label">物流单号:</text>
<text class="courier-value">{{ deliveryInfo.tracking_no }}</text>
<text class="copy-btn" @click="copyText(deliveryInfo.tracking_no)">复制</text>
</view>
</view>
<!-- 商品信息 -->
<view class="order-products card">
<view class="shop-header" @click="goToShop">
<text class="shop-icon">🏪</text>
<text class="shop-name">{{ shopName }}</text>
<text class="arrow-right">></text>
</view>
<view v-for="item in orderItems" :key="item.id" class="product-item" @click="goToProduct(item.product_id)">
<image :src="item.image_url || '/static/default-product.png'" class="product-image" mode="aspectFill"/>
<view class="product-info">
<text class="product-name">{{ item.product_name }}</text>
<text v-if="item.specifications" class="product-spec">{{ getSpecText(item.specifications) }}</text>
<view class="price-quantity">
<text class="product-price">¥{{ item.price }}</text>
<text class="product-quantity">×{{ item.quantity }}</text>
</view>
</view>
</view>
</view>
<!-- 订单信息 -->
<view class="order-info card">
<view class="info-row">
<text class="info-label">订单编号</text>
<text class="info-value copyable" @click="copyText(order.order_no)">{{ order.order_no }} <text class="copy-icon">📄</text></text>
</view>
<view class="info-row">
<text class="info-label">下单时间</text>
<text class="info-value">{{ formatTime(order.created_at) }}</text>
</view>
<view class="info-row" v-if="order.payment_method">
<text class="info-label">支付方式</text>
<text class="info-value">{{ getPaymentMethodText(order.payment_method) }}</text>
</view>
<view class="info-row" v-if="order.paid_at">
<text class="info-label">支付时间</text>
<text class="info-value">{{ formatTime(order.paid_at) }}</text>
</view>
<view class="info-row" v-if="order.shipped_at">
<text class="info-label">发货时间</text>
<text class="info-value">{{ formatTime(order.shipped_at) }}</text>
</view>
<view class="info-row" v-if="order.completed_at">
<text class="info-label">完成时间</text>
<text class="info-value">{{ formatTime(order.completed_at) }}</text>
</view>
</view>
<!-- 费用明细 -->
<view class="cost-detail card">
<view class="cost-row">
<text class="cost-label">商品总额</text>
<text class="cost-value">¥{{ order.product_amount }}</text>
</view>
<view class="cost-row">
<text class="cost-label">运费</text>
<text class="cost-value">+¥{{ order.shipping_fee || 0 }}</text>
</view>
<view class="cost-row" v-if="order.discount_amount > 0">
<text class="cost-label">优惠金额</text>
<text class="cost-value">-¥{{ order.discount_amount }}</text>
</view>
<view class="cost-row total">
<text class="cost-label">实付金额</text>
<text class="cost-value price">¥{{ order.total_amount }}</text>
</view>
</view>
</scroll-view>
<!-- 底部操作 -->
<view class="bottom-actions">
<view class="action-left">
<view class="service-btn" @click="contactService">
<text class="service-icon">🎧</text>
<text>客服</text>
</view>
</view>
<view class="action-right">
<button v-if="order.order_status === 1" class="btn primary" @click="payOrder">立即支付</button>
<button v-if="order.order_status === 1" class="btn" @click="cancelOrder">取消订单</button>
<button v-if="order.order_status === 2" class="btn" @click="remindDelivery">提醒发货</button>
<button v-if="order.order_status === 2" class="btn" @click="applyRefund">申请退款</button>
<button v-if="order.order_status === 3" class="btn primary" @click="confirmReceive">确认收货</button>
<button v-if="order.order_status === 3" class="btn" @click="viewLogistics">查看物流</button>
<button v-if="order.order_status === 4" class="btn primary" @click="goToReview">评价</button>
<button v-if="order.order_status === 4" class="btn" @click="rePurchase">再次购买</button>
<button v-if="order.order_status === 4" class="btn" @click="applyAfterSales">申请售后</button>
<button v-if="order.order_status === 5" class="btn" @click="rePurchase">重新购买</button>
</view>
</view>
</view>
</template>
<script setup lang="uts">
import { ref, onMounted, computed } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { supabaseService } from '@/utils/supabaseService.uts'
import supa from '@/components/supadb/aksupainstance.uts'
const orderId = ref('')
const order = ref<any>({})
const orderItems = ref<any[]>([])
const shopName = ref('店铺名称')
const deliveryAddress = ref<any>({})
const deliveryInfo = ref<any>({})
onLoad((options) => {
if (options['id']) {
orderId.value = options['id'] as string
loadOrderDetail()
} else if (options['orderId']) {
orderId.value = options['orderId'] as string
loadOrderDetail()
}
})
const loadOrderDetail = async () => {
uni.showLoading({ title: '加载中' })
try {
const data = await supabaseService.getOrderDetail(orderId.value)
if (data) {
order.value = data
orderItems.value = data.ml_order_items || []
deliveryAddress.value = data.shipping_address || {}
// 获取店铺信息
if (data.merchant_id) {
loadShopInfo(data.merchant_id)
}
} else {
uni.showToast({ title: '订单不存在', icon: 'none' })
}
} catch (e) {
uni.showToast({ title: '加载失败', icon: 'none' })
} finally {
uni.hideLoading()
}
}
const loadShopInfo = async (merchantId: string) => {
const { data } = await supa
.from('ml_shops')
.select('shop_name')
.eq('merchant_id', merchantId)
.single()
if (data) {
shopName.value = data['shop_name'] as string
}
}
// 辅助函数
const getStatusText = () => {
const status = order.value.order_status
if (status == 1) return '待付款'
if (status == 2) return '待发货'
if (status == 3) return '待收货'
if (status == 4) return '已完成'
if (status == 5) return '已取消'
if (status == 6) return '退款中'
if (status == 7) return '已退款'
return '未知状态'
}
const getStatusDesc = () => {
const status = order.value.order_status
if (status == 1) return '请尽快完成支付'
if (status == 2) return '商家正在打包商品'
if (status == 3) return '商品正在赶往您的地址'
if (status == 4) return '订单已完成,感谢支持'
if (status == 5) return '订单已取消'
if (status == 6) return '售后处理中'
if (status == 7) return '钱款已退回'
return ''
}
const getStatusIcon = () => {
const status = order.value.order_status
if (status === 1) return '💳'
if (status === 2) return '📦'
if (status === 3) return '🚚'
if (status === 4) return '✅'
return '📝'
}
const getStatusClass = () => {
const status = order.value.order_status
return `status-${status}`
}
const getFullAddress = (addr: any) => {
if (!addr) return ''
// 兼容简单的字符串地址和对象地址
if (typeof addr === 'string') return addr
if (addr.address) return addr.address
return (addr.province || '') + (addr.city || '') + (addr.district || '') + (addr.detail || addr.address_detail || '')
}
const getSpecText = (specs: any) => {
if (!specs) return ''
if (typeof specs === 'string') return specs
return Object.keys(specs).map(k => `${k}:${specs[k]}`).join(' ')
}
const formatTime = (iso: string) => {
if (!iso) return ''
const d = new Date(iso)
return `${d.getFullYear()}-${d.getMonth()+1}-${d.getDate()} ${d.getHours()}:${d.getMinutes()}`
}
const getPaymentMethodText = (method: any) => {
return '在线支付'
}
const copyText = (text: string) => {
if(!text) return
uni.setClipboardData({
data: text,
success: () => uni.showToast({ title: '已复制' })
})
}
// 动作函数
const contactService = () => {
uni.showActionSheet({
itemList: ['在线客服', '拨打电话'],
success: (res) => {
if (res.tapIndex === 1) {
// 模拟拨打电话
uni.makePhoneCall({ phoneNumber: '400-123-4567' })
} else {
uni.showToast({ title: '连接到了商家客服' })
// 这里可以跳转到聊天页面
}
}
})
}
const payOrder = () => {
uni.navigateTo({
url: `/pages/mall/consumer/payment?orderId=${orderId.value}&amount=${order.value.total_amount}`
})
}
const cancelOrder = () => {
uni.showModal({
title: '提示',
content: '确定要取消订单吗?',
success: async (res) => {
if (res.confirm) {
const { error } = await supa.from('ml_orders').update({ order_status: 5 }).eq('id', orderId.value)
if(!error) {
order.value.order_status = 5
uni.showToast({ title: '订单已取消' })
}
}
}
})
}
const remindDelivery = () => {
uni.showToast({ title: '已提醒商家尽快发货' })
}
const viewLogistics = () => {
uni.navigateTo({ url: `/pages/mall/consumer/logistics?orderId=${orderId.value}` })
}
const confirmReceive = async () => {
uni.showModal({
title: '确认收货',
content: '确保您已收到货物',
success: async (res) => {
if (res.confirm) {
const result = await supabaseService.confirmReceipt(orderId.value)
if (result.success) {
order.value.order_status = 4
uni.showToast({ title: '收货成功' })
setTimeout(() => goToReview(), 1500)
} else {
uni.showToast({ title: result.error || '失败', icon: 'none' })
}
}
}
})
}
const goToReview = () => {
uni.navigateTo({ url: `/pages/mall/consumer/review?orderId=${orderId.value}` })
}
const rePurchase = async () => {
uni.showLoading({ title: '处理中' })
const success = await supabaseService.rePurchase(order.value)
uni.hideLoading()
if (success) {
uni.showToast({ title: '已加入购物车' })
setTimeout(() => {
uni.switchTab({ url: '/pages/mall/consumer/cart' })
}, 1000)
} else {
uni.showToast({ title: '操作失败', icon: 'none' })
}
}
const applyRefund = () => {
uni.showModal({
title: '申请退款',
editable: true,
placeholderText: '请输入退款原因',
success: async (res) => {
if (res.confirm) {
const reason = res.content || '用户主动申请'
const success = await supabaseService.applyRefund(orderId.value, reason)
if (success) {
order.value.order_status = 6
uni.showToast({ title: '申请已提交' })
} else {
uni.showToast({ title: '提交失败', icon: 'none' })
}
}
}
})
}
const applyAfterSales = () => {
// 售后逻辑类似退款,或者是跳转到专门的售后单页面
applyRefund()
}
const goToShop = () => {
// 跳转到店铺详情
// uni.navigateTo({ url: `/pages/mall/shop/index?id=${order.value.merchant_id}` })
uni.showToast({ title: '进入店铺: ' + shopName.value })
}
const goToProduct = (pid: string) => {
uni.navigateTo({ url: `/pages/mall/consumer/product-detail?id=${pid}` })
}
</script>
<style scoped>
.order-detail-page {
display: flex;
flex-direction: column;
height: 100vh;
background-color: #f5f5f5;
}
.scroll-content {
flex: 1;
overflow-y: auto;
padding-bottom: 20px;
}
.card {
background-color: #ffffff;
margin: 10px;
padding: 15px;
border-radius: 10px;
}
/* 状态栏 */
.order-status {
background: linear-gradient(to right, #ff9000, #ff5000);
padding: 20px 25px;
color: white;
display: flex;
align-items: center;
}
.status-emoji {
font-size: 32px;
margin-right: 15px;
}
.status-text {
font-size: 18px;
font-weight: bold;
display: block;
}
.status-desc {
font-size: 12px;
opacity: 0.9;
margin-top: 5px;
display: block;
}
/* 配送信息 */
.section-title {
font-weight: bold;
font-size: 16px;
margin-bottom: 10px;
display: block;
}
.delivery-address {
display: flex;
align-items: flex-start;
}
.address-icon {
font-size: 20px;
margin-right: 10px;
color: #666;
}
.address-user {
margin-bottom: 5px;
font-weight: bold;
font-size: 14px;
}
.phone {
margin-left: 10px;
color: #666;
font-weight: normal;
}
.address-detail {
font-size: 13px;
color: #333;
line-height: 1.4;
}
.courier-info {
margin-top: 10px;
padding-top: 10px;
border-top: 1px solid #eee;
font-size: 13px;
display: flex;
align-items: center;
}
.copy-btn {
margin-left: 10px;
color: #ff5000;
font-size: 12px;
border: 1px solid #ff5000;
padding: 1px 6px;
border-radius: 10px;
}
/* 店铺与商品 */
.shop-header {
display: flex;
align-items: center;
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 1px solid #f5f5f5;
}
.shop-icon {
margin-right: 5px;
}
.shop-name {
font-size: 14px;
font-weight: bold;
flex: 1;
}
.arrow-right {
color: #999;
}
.product-item {
display: flex;
margin-bottom: 15px;
}
.product-item:last-child {
margin-bottom: 0;
}
.product-image {
width: 80px;
height: 80px;
border-radius: 6px;
margin-right: 10px;
background-color: #f9f9f9;
}
.product-info {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.product-name {
font-size: 14px;
line-height: 1.4;
color: #333;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.product-spec {
font-size: 12px;
color: #999;
background-color: #f5f5f5;
padding: 2px 5px;
border-radius: 4px;
align-self: flex-start;
margin-top: 5px;
}
.price-quantity {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 5px;
}
.product-price {
font-size: 16px;
font-weight: bold;
color: #333;
}
.product-quantity {
color: #999;
font-size: 12px;
}
/* 订单详情 */
.info-row {
display: flex;
justify-content: space-between;
margin-bottom: 10px;
font-size: 13px;
}
.info-label {
color: #999;
}
.info-value {
color: #333;
}
.copy-icon {
font-size: 12px;
}
/* 费用明细 */
.cost-row {
display: flex;
justify-content: space-between;
margin-bottom: 8px;
font-size: 13px;
}
.cost-row.total {
margin-top: 15px;
padding-top: 15px;
border-top: 1px solid #f5f5f5;
align-items: center;
}
.cost-value.price {
color: #ff5000;
font-size: 18px;
font-weight: bold;
}
/* 底部按钮 */
.bottom-actions {
background-color: #ffffff;
padding: 10px 15px;
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: 0 -2px 10px rgba(0,0,0,0.05);
padding-bottom: calc(10px + env(safe-area-inset-bottom));
}
.action-left {
display: flex;
}
.service-btn {
display: flex;
flex-direction: column;
align-items: center;
font-size: 10px;
color: #666;
background: none;
line-height: 1.2;
}
.service-icon {
font-size: 18px;
margin-bottom: 2px;
}
.action-right {
display: flex;
gap: 10px;
}
.btn {
margin: 0;
padding: 0 15px;
height: 32px;
line-height: 30px;
font-size: 13px;
border-radius: 16px;
background: #ffffff;
border: 1px solid #cccccc;
color: #666;
}
.btn.primary {
border-color: #ff5000;
color: #ff5000;
background-color: #fff0ec;
}
/* 状态样式 */
.status-4 .status-text { /* Completed */ }
</style>