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

783 lines
22 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 != null && (order?.order_status ?? 0) >= 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 as any) }}</text>
</view>
</view>
<!-- 如果有物流信息显示 -->
<view v-if="deliveryInfo != null && 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 != null && item.image_url != '' ? 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" v-if="order != null">
<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 != null && order?.payment_method != ''">
<text class="info-label">支付方式</text>
<text class="info-value">{{ getPaymentMethodText(order?.payment_method as any) }}</text>
</view>
<view class="info-row" v-if="order?.paid_at != null && 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 != null && 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 != null && order?.completed_at != ''">
<text class="info-label">完成时间</text>
<text class="info-value">{{ formatTime(order?.completed_at ?? '') }}</text>
</view>
</view>
<!-- 费用明细 -->
<view class="cost-detail card" v-if="order != null">
<view class="cost-row">
<text class="cost-label">商品总额</text>
<text class="cost-value">¥{{ order?.product_amount ?? 0 }}</text>
</view>
<view class="cost-row">
<text class="cost-label">运费</text>
<text class="cost-value">+¥{{ order?.shipping_fee != null ? order?.shipping_fee : 0 }}</text>
</view>
<view class="cost-row" v-if="(order?.discount_amount ?? 0) > 0">
<text class="cost-label">优惠金额</text>
<text class="cost-value">-¥{{ order?.discount_amount ?? 0 }}</text>
</view>
<view class="cost-row total">
<text class="cost-label">实付金额</text>
<text class="cost-value price">¥{{ order?.total_amount ?? 0 }}</text>
</view>
</view>
</scroll-view>
<!-- 底部操作 -->
<view class="bottom-actions" v-if="order != null">
<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'
// 定义订单类型
type OrderType = {
order_no: string,
order_status: number,
total_amount: number,
product_amount: number,
shipping_fee: number,
discount_amount: number,
payment_method: string,
created_at: string,
paid_at: string,
shipped_at: string,
completed_at: string,
merchant_id: string,
shipping_address: any
}
type OrderItemType = {
id: string,
product_id: string,
product_name: string,
image_url: string,
price: number,
quantity: number,
specifications: any
}
type AddressType = {
name: string,
phone: string,
province: string,
city: string,
district: string,
detail: string,
address: string
}
type DeliveryInfoType = {
tracking_no: string
}
const orderId = ref('')
const order = ref<OrderType | null>(null)
const orderItems = ref<OrderItemType[]>([])
const shopName = ref('店铺名称')
const deliveryAddress = ref<AddressType | null>(null)
const deliveryInfo = ref<DeliveryInfoType | null>(null)
// 辅助函数 - 必须在调用前定义
const getStatusText = (): string => {
const status = order.value?.order_status ?? 0
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 = (): string => {
const status = order.value?.order_status ?? 0
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 = (): string => {
const status = order.value?.order_status ?? 0
if (status === 1) return '💳'
if (status === 2) return '📦'
if (status === 3) return '🚚'
if (status === 4) return '✅'
return '📝'
}
const getStatusClass = (): string => {
const status = order.value?.order_status ?? 0
return `status-${status}`
}
const getFullAddress = (addr: any): string => {
if (addr == null) return ''
// 兼容简单的字符串地址和对象地址
if (typeof addr === 'string') return addr
const addrObj = addr as Record<string, any>
if (addrObj['address'] != null) return addrObj['address'] as string
return ((addrObj['province'] as string) ?? '') + ((addrObj['city'] as string) ?? '') + ((addrObj['district'] as string) ?? '') + ((addrObj['detail'] as string) ?? (addrObj['address_detail'] as string) ?? '')
}
const getSpecText = (specs: any): string => {
if (specs == null) return ''
if (typeof specs === 'string') return specs
// 简化处理:直接返回字符串形式
return JSON.stringify(specs)
}
const formatTime = (iso: string): 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): string => {
return '在线支付'
}
const copyText = (text: string) => {
if(text == '') return
uni.setClipboardData({
data: text,
success: () => uni.showToast({ title: '已复制' })
})
}
const loadShopInfo = async (merchantId: string) => {
const result = await supa
.from('ml_shops')
.select('shop_name')
.eq('merchant_id', merchantId)
.single()
const resultObj = result as Record<string, any>
const resultData = resultObj['data']
if (resultData != null) {
const dataObj = resultData as Record<string, any>
shopName.value = dataObj['shop_name'] as string
}
}
const loadOrderDetail = async () => {
uni.showLoading({ title: '加载中' })
try {
const data = await supabaseService.getOrderDetail(orderId.value)
if (data != null) {
// 使用 JSON.parse(JSON.stringify()) 转换数据
const dataObj = JSON.parse(JSON.stringify(data)) as UTSJSONObject
order.value = data as OrderType
// 解析订单商品
const itemsRaw = dataObj.get('ml_order_items')
if (itemsRaw != null && Array.isArray(itemsRaw)) {
const items = itemsRaw as any[]
orderItems.value = []
for (let i = 0; i < items.length; i++) {
const item = items[i]
const itemObj = JSON.parse(JSON.stringify(item)) as UTSJSONObject
const orderItem: OrderItemType = {
id: (itemObj.get('id') ?? '') as string,
product_id: (itemObj.get('product_id') ?? '') as string,
product_name: (itemObj.get('product_name') ?? '') as string,
price: (itemObj.get('price') ?? 0) as number,
quantity: (itemObj.get('quantity') ?? 1) as number,
image_url: (itemObj.get('image_url') ?? '') as string,
specifications: (itemObj.get('specifications') ?? '') as string
}
orderItems.value.push(orderItem)
}
}
// 解析收货地址
const addressRaw = dataObj.get('shipping_address')
if (addressRaw != null) {
const addressObj = JSON.parse(JSON.stringify(addressRaw)) as UTSJSONObject
const province = (addressObj.get('province') ?? '') as string
const city = (addressObj.get('city') ?? '') as string
const district = (addressObj.get('district') ?? '') as string
const detail = (addressObj.get('detail') ?? '') as string
deliveryAddress.value = {
name: (addressObj.get('name') ?? '') as string,
phone: (addressObj.get('phone') ?? '') as string,
province: province,
city: city,
district: district,
detail: detail,
address: province + city + district + detail
} as AddressType
}
// 获取店铺信息
const merchantId = dataObj.get('merchant_id') as string
if (merchantId != null && merchantId != '') {
loadShopInfo(merchantId)
}
console.log('订单详情加载成功,商品数量:', orderItems.value.length)
} else {
uni.showToast({ title: '订单不存在', icon: 'none' })
}
} catch (e) {
console.error('加载订单详情失败:', e)
uni.showToast({ title: '加载失败', icon: 'none' })
} finally {
uni.hideLoading()
}
}
// 动作函数
const contactService = () => {
uni.showActionSheet({
itemList: ['在线客服', '拨打电话'],
success: (res) => {
if (res.tapIndex === 1) {
// 模拟拨打电话
uni.makePhoneCall({ phoneNumber: '400-123-4567' })
} else {
uni.showToast({ title: '连接到了商家客服' })
// 这里可以跳转到聊天页面
}
}
})
}
const payOrder = () => {
const totalAmount = order.value?.total_amount ?? 0
uni.navigateTo({
url: `/pages/mall/consumer/payment?orderId=${orderId.value}&amount=${totalAmount}`
})
}
const doCancelOrder = async () => {
const result = await supa.from('ml_orders').update({ order_status: 5 }).eq('id', orderId.value)
const resultObj = result as Record<string, any>
const resultError = resultObj['error']
if(resultError == null) {
if (order.value != null) {
order.value.order_status = 5
}
uni.showToast({ title: '订单已取消' })
}
}
const cancelOrder = () => {
uni.showModal({
title: '提示',
content: '确定要取消订单吗?',
success: (res) => {
if (res.confirm) {
doCancelOrder()
}
}
})
}
const remindDelivery = () => {
uni.showToast({ title: '已提醒商家尽快发货' })
}
const viewLogistics = () => {
uni.navigateTo({ url: `/pages/mall/consumer/logistics?orderId=${orderId.value}` })
}
const goToReview = () => {
uni.navigateTo({ url: `/pages/mall/consumer/review?orderId=${orderId.value}` })
}
const doConfirmReceive = async () => {
const result = await supabaseService.confirmReceipt(orderId.value)
if (result.success) {
if (order.value != null) {
order.value.order_status = 4
}
uni.showToast({ title: '收货成功' })
setTimeout(() => goToReview(), 1500)
} else {
uni.showToast({ title: result.error ?? '失败', icon: 'none' })
}
}
const confirmReceive = () => {
uni.showModal({
title: '确认收货',
content: '确保您已收到货物',
success: (res) => {
if (res.confirm) {
doConfirmReceive()
}
}
})
}
const rePurchase = async () => {
uni.showLoading({ title: '处理中' })
const orderData = order.value as any
const success = await supabaseService.rePurchase(orderData)
uni.hideLoading()
if (success) {
uni.showToast({ title: '已加入购物车' })
setTimeout(() => {
uni.switchTab({ url: '/pages/mall/consumer/cart' })
}, 1000)
} else {
uni.showToast({ title: '操作失败', icon: 'none' })
}
}
const doApplyRefund = async (reason: string) => {
const success = await supabaseService.applyRefund(orderId.value, reason)
if (success) {
if (order.value != null) {
order.value.order_status = 6
}
uni.showToast({ title: '申请已提交' })
} else {
uni.showToast({ title: '提交失败', icon: 'none' })
}
}
const applyRefund = () => {
uni.showModal({
title: '申请退款',
editable: true,
placeholderText: '请输入退款原因',
success: (res) => {
if (res.confirm) {
const reason = res.content ?? '用户主动申请'
doApplyRefund(reason)
}
}
})
}
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}` })
}
// 生命周期 - 在所有函数定义之后
onLoad((options) => {
const id = options['id']
const orderIdParam = options['orderId']
if (id != null && id != '') {
orderId.value = id as string
loadOrderDetail()
} else if (orderIdParam != null && orderIdParam != '') {
orderId.value = orderIdParam as string
loadOrderDetail()
}
})
</script>
<style scoped>
.order-detail-page {
display: flex;
flex-direction: column;
flex: 1;
background-color: #f5f5f5;
}
.scroll-content {
flex: 1;
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;
}
.status-desc {
font-size: 12px;
opacity: 0.9;
margin-top: 5px;
}
/* 配送信息 */
.section-title {
font-weight: bold;
font-size: 16px;
margin-bottom: 10px;
}
.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;
overflow: hidden;
text-overflow: ellipsis;
lines: 2; /* uvue specific */
}
.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: 30px;
}
.action-left {
display: flex;
}
.service-btn {
display: flex;
flex-direction: column;
align-items: center;
font-size: 10px;
color: #666;
background-color: transparent;
line-height: 1.2;
}
.service-icon {
font-size: 18px;
margin-bottom: 2px;
}
.action-right {
display: flex;
}
/* Add margin to buttons inside action-right for spacing */
.action-right .btn {
margin-left: 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>