Files
medical-mall/pages/mall/admin/order/order-management/components/OrderDetailDrawer.uvue

522 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 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>