522 lines
18 KiB
Plaintext
522 lines
18 KiB
Plaintext
<template>
|
||
<view v-if="visible" class="drawer-mask" :class="{ closing: isClosing }" @click="close">
|
||
<view class="drawer-container" :class="{ closing: isClosing }" @click.stop>
|
||
<view class="drawer-header">
|
||
<view class="header-left">
|
||
<text class="title">订单详情</text>
|
||
</view>
|
||
<view class="close-btn" @click="close">
|
||
<text class="close-icon">×</text>
|
||
</view>
|
||
</view>
|
||
|
||
<scroll-view class="drawer-body" scroll-y>
|
||
<!-- 订单概况 KPIS -->
|
||
<view class="order-summary-card">
|
||
<view class="order-type-icon">
|
||
<image src="/static/icons/order_blue.png" mode="aspectFit" class="type-icon" />
|
||
</view>
|
||
<view class="summary-info">
|
||
<view class="top-row">
|
||
<text class="order-type-text">{{ orderInfo['typeName'] || '普通订单' }}</text>
|
||
<text class="order-sn-text">订单号:{{ orderInfo['sn'] }}</text>
|
||
<text class="shop-tag" v-if="orderInfo['store_name'] != '--'">{{ orderInfo['store_name'] }}</text>
|
||
</view>
|
||
<view class="bottom-grids">
|
||
<view class="summary-grid">
|
||
<text class="label">订单状态</text>
|
||
<text class="value status-val">{{ orderInfo['statusName'] }}</text>
|
||
</view>
|
||
<view class="summary-grid">
|
||
<text class="label">总金额</text>
|
||
<text class="value price-val">¥ {{ orderInfo['actualPrice'] }}</text>
|
||
</view>
|
||
<view class="summary-grid">
|
||
<text class="label">已支付</text>
|
||
<text class="value status-val">¥ {{ orderInfo['paidAmount'] }}</text>
|
||
</view>
|
||
<view class="summary-grid">
|
||
<text class="label">支付状态</text>
|
||
<text class="value">{{ orderInfo['payMethod'] }}</text>
|
||
</view>
|
||
<view class="summary-grid">
|
||
<text class="label">支付时间</text>
|
||
<text class="value">{{ orderInfo['payTime'] || '--' }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- Tabs -->
|
||
<view class="drawer-tabs">
|
||
<view v-for="(tab, index) in tabs" :key="index" class="tab-item" :class="{ active: activeTab === index }" @click="activeTab = index">
|
||
<text class="tab-text">{{ tab }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- Tab Content -->
|
||
<view class="tab-content">
|
||
<!-- 订单信息 -->
|
||
<view v-if="activeTab === 0" class="info-section">
|
||
<view class="section-block">
|
||
<view class="section-title">
|
||
<view class="blue-bar"></view>
|
||
<text>用户信息</text>
|
||
</view>
|
||
<view class="info-grid">
|
||
<view class="info-item">
|
||
<text class="label">用户名称:</text>
|
||
<text class="value">{{ orderInfo['user']['name'] }}</text>
|
||
</view>
|
||
<view class="info-item">
|
||
<text class="label">绑定电话:</text>
|
||
<text class="value">{{ orderInfo['user']['phone'] }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="section-block">
|
||
<view class="section-title">
|
||
<view class="blue-bar"></view>
|
||
<text>收货信息</text>
|
||
</view>
|
||
<view class="info-grid">
|
||
<view class="info-item">
|
||
<text class="label">收货人:</text>
|
||
<text class="value">{{ orderInfo['real_name'] || orderInfo['user']['name'] }}</text>
|
||
</view>
|
||
<view class="info-item">
|
||
<text class="label">收货电话:</text>
|
||
<text class="value">{{ orderInfo['user_phone'] || orderInfo['user']['phone'] }}</text>
|
||
</view>
|
||
<view class="info-item full">
|
||
<text class="label">收货地址:</text>
|
||
<text class="value">{{ orderInfo['user_address'] || '暂无地址信息' }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="section-block">
|
||
<view class="section-title">
|
||
<view class="blue-bar"></view>
|
||
<text>订单信息</text>
|
||
</view>
|
||
<view class="info-grid">
|
||
<view class="info-item">
|
||
<text class="label">创建时间:</text>
|
||
<text class="value">{{ orderInfo['created_at'] || '--' }}</text>
|
||
</view>
|
||
<view class="info-item">
|
||
<text class="label">商品总数:</text>
|
||
<text class="value">{{ totalNum > 0 ? totalNum : (orderInfo['total_num'] || '0') }}</text>
|
||
</view>
|
||
<view class="info-item">
|
||
<text class="label">产品金额:</text>
|
||
<text class="value">¥ {{ orderInfo['total_price'] || '0.00' }}</text>
|
||
</view>
|
||
<view class="info-item">
|
||
<text class="label">运费:</text>
|
||
<text class="value">¥ {{ orderInfo['shipping_fee'] || '0.00' }}</text>
|
||
</view>
|
||
<view class="info-item">
|
||
<text class="label">折扣金额:</text>
|
||
<text class="value">- ¥ {{ orderInfo['coupon_price'] || '0.00' }}</text>
|
||
</view>
|
||
<view class="info-item">
|
||
<text class="label">总金额:</text>
|
||
<text class="value">¥ {{ orderInfo['actualPrice'] }}</text>
|
||
</view>
|
||
<view class="info-item">
|
||
<text class="label">支付金额:</text>
|
||
<text class="value price-red">¥ {{ orderInfo['paidAmount'] }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="section-block">
|
||
<view class="section-title">
|
||
<view class="blue-bar"></view>
|
||
<text>买家留言</text>
|
||
</view>
|
||
<view class="info-grid">
|
||
<view class="info-item full">
|
||
<text class="value">{{ orderInfo['mark'] || '-' }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="section-block">
|
||
<view class="section-title">
|
||
<view class="blue-bar"></view>
|
||
<text>订单备注</text>
|
||
</view>
|
||
<view class="info-grid">
|
||
<view class="info-item full">
|
||
<text class="value">{{ orderInfo['remark'] || '-' }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 商品信息 -->
|
||
<view v-if="activeTab === 1" class="info-section">
|
||
<view class="product-table">
|
||
<view class="product-thead">
|
||
<text class="p-th p-info">商品信息</text>
|
||
<text class="p-th p-sku">规格</text>
|
||
<text class="p-th p-price">单价</text>
|
||
<text class="p-th p-num">数量</text>
|
||
<text class="p-th p-total">小计</text>
|
||
</view>
|
||
<view class="product-tbody">
|
||
<view v-for="(p, pi) in productItems" :key="pi" class="p-tr">
|
||
<view class="p-td p-info">
|
||
<image :src="p['image'] || '/static/logo.png'" mode="aspectFill" class="p-img" />
|
||
<text class="p-name">{{ p['name'] }}</text>
|
||
</view>
|
||
<view class="p-td p-sku">
|
||
<text class="p-sku-txt">{{ p['sku_info'] || '-' }}</text>
|
||
</view>
|
||
<view class="p-td p-price">¥{{ p['price'] }}</view>
|
||
<view class="p-td p-num">{{ p['quantity'] }}</view>
|
||
<view class="p-td p-total">¥{{ ((p['price'] as number) * (p['quantity'] as number)).toFixed(2) }}</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 订单记录 -->
|
||
<view v-if="activeTab === 2" class="info-section">
|
||
<view v-if="logs.length === 0" class="empty-logs">
|
||
<text class="empty-logs-text">暂无订单记录</text>
|
||
</view>
|
||
<view v-else class="timeline">
|
||
<view v-for="(log, li) in logs" :key="li" class="timeline-item">
|
||
<view class="dot"></view>
|
||
<view class="line" v-if="li !== logs.length - 1"></view>
|
||
<view class="log-content">
|
||
<text class="log-title">{{ log['title'] }}</text>
|
||
<text class="log-time">{{ log['time'] }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</scroll-view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup lang="uts">
|
||
import { ref, computed, watch } from 'vue'
|
||
import { supabase } from '@/components/supadb/aksupainstance.uts'
|
||
|
||
const props = defineProps({
|
||
visible: { type: Boolean, default: false },
|
||
orderInfo: { type: Object, default: () : UTSJSONObject => ({}) as UTSJSONObject }
|
||
})
|
||
|
||
const emit = defineEmits(['update:visible'])
|
||
|
||
const activeTab = ref(0)
|
||
const tabs = ['订单信息', '商品信息', '订单记录']
|
||
|
||
const fetchedItems = ref<UTSJSONObject[]>([])
|
||
const itemsLoading = ref(false)
|
||
const isClosing = ref(false)
|
||
|
||
const productItems = computed<UTSJSONObject[]>(() => fetchedItems.value)
|
||
|
||
const totalNum = computed<number>(() => {
|
||
return fetchedItems.value.reduce((sum: number, p: UTSJSONObject) : number => {
|
||
const qty = p['quantity'] != null ? (p['quantity'] as number) : 0
|
||
return sum + qty
|
||
}, 0)
|
||
})
|
||
|
||
const fmtTime = (timeStr : string | null) : string => {
|
||
if (timeStr == null || timeStr == '') return ''
|
||
if (timeStr.indexOf('T') > -1) {
|
||
const parts = timeStr.split('T')
|
||
const timePart = parts[1].split('.')[0]
|
||
return parts[0] + ' ' + timePart
|
||
}
|
||
return timeStr
|
||
}
|
||
|
||
const logs = computed<UTSJSONObject[]>(() => {
|
||
const result: UTSJSONObject[] = []
|
||
const createdAt = props.orderInfo['created_at'] as string | null
|
||
if (createdAt != null && createdAt !== '' && createdAt !== '--') {
|
||
result.push({ title: '订单创建', time: createdAt } as UTSJSONObject)
|
||
}
|
||
const paidAt = props.orderInfo['payTime'] as string | null
|
||
if (paidAt != null && paidAt !== '' && paidAt !== '--') {
|
||
result.push({ title: '支付成功', time: paidAt } as UTSJSONObject)
|
||
}
|
||
const shippedAt = fmtTime(props.orderInfo['shipped_at'] as string | null)
|
||
if (shippedAt !== '') {
|
||
result.push({ title: '商家发货', time: shippedAt } as UTSJSONObject)
|
||
}
|
||
const deliveredAt = fmtTime(props.orderInfo['delivered_at'] as string | null)
|
||
if (deliveredAt !== '') {
|
||
result.push({ title: '确认收货', time: deliveredAt } as UTSJSONObject)
|
||
}
|
||
const completedAt = fmtTime(props.orderInfo['completed_at'] as string | null)
|
||
if (completedAt !== '') {
|
||
result.push({ title: '交易完成', time: completedAt } as UTSJSONObject)
|
||
}
|
||
return result
|
||
})
|
||
|
||
const fetchItems = async () => {
|
||
const orderId = props.orderInfo['id']
|
||
if (orderId == null || orderId === '') return
|
||
itemsLoading.value = true
|
||
fetchedItems.value = []
|
||
try {
|
||
const res = await supabase
|
||
.from('ml_order_items')
|
||
.select('*')
|
||
.eq('order_id', orderId as string)
|
||
.execute()
|
||
if (res.error == null && res.data != null) {
|
||
fetchedItems.value = (res.data as UTSJSONObject[]).map((p: UTSJSONObject) : UTSJSONObject => {
|
||
return {
|
||
image: p['image_url'] != null ? (p['image_url'] as string) : '/static/logo.png',
|
||
name: p['product_name'] != null ? (p['product_name'] as string) : '--',
|
||
sku_info: p['sku_name'] != null ? (p['sku_name'] as string) : (p['specifications'] != null ? (p['specifications'] as string) : '-'),
|
||
price: p['price'] != null ? p['price'] : 0,
|
||
quantity: p['quantity'] != null ? p['quantity'] : 0
|
||
} as UTSJSONObject
|
||
})
|
||
}
|
||
} catch (e) {
|
||
console.error('fetchItems error:', e)
|
||
} finally {
|
||
itemsLoading.value = false
|
||
}
|
||
}
|
||
|
||
const close = () => {
|
||
isClosing.value = true
|
||
setTimeout(() => {
|
||
isClosing.value = false
|
||
emit('update:visible', false)
|
||
}, 280)
|
||
}
|
||
|
||
watch(() => props.visible, (newVal) => {
|
||
if (newVal) {
|
||
isClosing.value = false
|
||
activeTab.value = 0
|
||
fetchedItems.value = []
|
||
fetchItems()
|
||
}
|
||
})
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
.drawer-mask {
|
||
position: fixed;
|
||
top: 0; left: 0; right: 0; bottom: 0;
|
||
background-color: rgba(0,0,0,0.5);
|
||
z-index: 2000;
|
||
display: flex;
|
||
flex-direction: row;
|
||
justify-content: flex-end;
|
||
animation: drawerMaskFadeIn 0.3s ease-out;
|
||
&.closing {
|
||
animation: drawerMaskFadeOut 0.28s ease-in forwards;
|
||
}
|
||
}
|
||
|
||
.drawer-container {
|
||
width: 800px;
|
||
max-width: 90%;
|
||
height: 100vh;
|
||
background-color: #fff;
|
||
box-shadow: -2px 0 8px rgba(0,0,0,0.1);
|
||
display: flex;
|
||
flex-direction: column;
|
||
animation: drawerSlideIn 0.3s cubic-bezier(0.23, 1, 0.32, 1);
|
||
&.closing {
|
||
animation: drawerSlideOut 0.28s cubic-bezier(0.755, 0.05, 0.855, 0.06) forwards;
|
||
}
|
||
}
|
||
|
||
@keyframes drawerSlideIn {
|
||
from { transform: translateX(100%); }
|
||
to { transform: translateX(0); }
|
||
}
|
||
@keyframes drawerSlideOut {
|
||
from { transform: translateX(0); }
|
||
to { transform: translateX(100%); }
|
||
}
|
||
@keyframes drawerMaskFadeIn {
|
||
from { opacity: 0; }
|
||
to { opacity: 1; }
|
||
}
|
||
@keyframes drawerMaskFadeOut {
|
||
from { opacity: 1; }
|
||
to { opacity: 0; }
|
||
}
|
||
|
||
.drawer-header {
|
||
height: 56px;
|
||
padding: 0 24px;
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
border-bottom: 1px solid #f0f0f0;
|
||
}
|
||
|
||
.title { font-size: 16px; font-weight: 600; color: #333; }
|
||
.close-btn {
|
||
width: 32px; height: 32px;
|
||
display: flex; align-items: center; justify-content: center;
|
||
cursor: pointer;
|
||
}
|
||
.close-icon { font-size: 24px; color: #999; line-height: 1; }
|
||
|
||
.drawer-body {
|
||
flex: 1;
|
||
background-color: #f5f7f9;
|
||
}
|
||
|
||
.order-summary-card {
|
||
background-color: #fff;
|
||
padding: 24px;
|
||
margin-bottom: 12px;
|
||
display: flex;
|
||
flex-direction: row;
|
||
gap: 16px;
|
||
}
|
||
|
||
.type-icon { width: 48px; height: 48px; }
|
||
|
||
.summary-info {
|
||
flex: 1;
|
||
}
|
||
|
||
.top-row {
|
||
margin-bottom: 16px;
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
gap: 12px;
|
||
}
|
||
|
||
.order-type-text { font-size: 16px; font-weight: 600; color: #333; }
|
||
.order-sn-text { font-size: 14px; color: #666; }
|
||
.shop-tag {
|
||
font-size: 12px; color: #1890ff; background: #e6f7ff;
|
||
border: 1px solid #91d5ff; padding: 2px 8px; border-radius: 2px;
|
||
}
|
||
|
||
.bottom-grids {
|
||
display: flex;
|
||
flex-direction: row;
|
||
gap: 40px;
|
||
}
|
||
|
||
.summary-grid {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
.label { font-size: 12px; color: #999; }
|
||
.value { font-size: 14px; color: #333; }
|
||
.status-val { font-weight: 600; }
|
||
.price-val { color: #f5222d; font-weight: 600; }
|
||
}
|
||
|
||
.drawer-tabs {
|
||
background-color: #fff;
|
||
display: flex;
|
||
flex-direction: row;
|
||
border-bottom: 1px solid #f0f0f0;
|
||
padding: 0 24px;
|
||
}
|
||
|
||
.tab-item {
|
||
padding: 12px 20px;
|
||
margin-right: 12px;
|
||
position: relative;
|
||
cursor: pointer;
|
||
.tab-text { font-size: 14px; color: #595959; }
|
||
&.active {
|
||
.tab-text { color: #1890ff; font-weight: 500; }
|
||
&::after {
|
||
content: '';
|
||
position: absolute; bottom: 0; left: 0; right: 0; height: 2px; background: #1890ff;
|
||
}
|
||
}
|
||
}
|
||
|
||
.tab-content {
|
||
padding: 16px;
|
||
}
|
||
|
||
.section-block {
|
||
background-color: #fff;
|
||
padding: 24px;
|
||
margin-bottom: 16px;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
.section-title {
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
gap: 8px;
|
||
margin-bottom: 20px;
|
||
text { font-size: 14px; font-weight: 600; color: #333; }
|
||
}
|
||
|
||
.blue-bar { width: 3px; height: 14px; background-color: #1890ff; }
|
||
|
||
.info-grid {
|
||
display: flex;
|
||
flex-direction: row;
|
||
flex-wrap: wrap;
|
||
gap: y;
|
||
}
|
||
|
||
.info-item {
|
||
width: 33.33%;
|
||
margin-bottom: 12px;
|
||
display: flex;
|
||
flex-direction: row;
|
||
.label { font-size: 13px; color: #666; width: 80px; flex-shrink: 0; }
|
||
.value { font-size: 13px; color: #333; line-height: 1.4; word-break: break-all; }
|
||
.price-red { color: #f5222d; font-weight: 600; }
|
||
&.full { width: 100%; }
|
||
}
|
||
|
||
/* 商品表格 */
|
||
.product-table { padding: 8px; background-color: #fff; }
|
||
.product-thead { display: flex; flex-direction: row; background-color: #fafafa; border-bottom: 1px solid #f0f0f0; }
|
||
.p-th { padding: 12px 8px; font-size: 13px; font-weight: 500; color: #333; text-align: left; }
|
||
.p-tr { display: flex; flex-direction: row; border-bottom: 1px solid #f0f0f0; }
|
||
.p-td { padding: 12px 8px; font-size: 13px; color: #595959; display: flex; align-items: center; }
|
||
|
||
.p-info { flex: 1; display: flex; flex-direction: row; align-items: center; gap: 8px; }
|
||
.p-img { width: 40px; height: 40px; border-radius: 4px; }
|
||
.p-sku { width: 120px; }
|
||
.p-price { width: 100px; }
|
||
.p-num { width: 80px; }
|
||
.p-total { width: 100px; }
|
||
|
||
/* 记录 */
|
||
.timeline { padding: 24px; background-color: #fff; }
|
||
.empty-logs { padding: 40px 24px; background-color: #fff; display: flex; align-items: center; justify-content: center; }
|
||
.empty-logs-text { font-size: 13px; color: #bfbfbf; }
|
||
.timeline-item { position: relative; padding-left: 24px; padding-bottom: 24px; }
|
||
.dot { position: absolute; left: 0; top: 4px; width: 10px; height: 10px; border-radius: 5px; background-color: #1890ff; z-index: 2; }
|
||
.line { position: absolute; left: 4.5px; top: 14px; bottom: -4px; width: 1px; background-color: #e8e8e8; }
|
||
.log-content { display: flex; flex-direction: column; gap: 4px; }
|
||
.log-title { font-size: 14px; color: #333; }
|
||
.log-time { font-size: 12px; color: #999; }
|
||
</style>
|