feat: 更新订单和用户视图,增强订单信息展示,支持充值类型订单

This commit is contained in:
2025-12-31 12:19:01 +08:00
parent 95bc5bdb5d
commit c133169c7b
5 changed files with 161 additions and 105 deletions

View File

@@ -10,8 +10,9 @@
<p class="text-sm text-slate-500">订单号: {{ order.id }}</p>
</div>
<div class="ml-auto">
<span class="px-3 py-1 rounded-full text-sm font-bold bg-green-50 text-green-600 border border-green-100" v-if="order.status === 'completed'">交易成功</span>
<span class="px-3 py-1 rounded-full text-sm font-bold bg-orange-50 text-orange-600 border border-orange-100" v-else-if="order.status === 'unpaid'">待支付</span>
<span class="px-3 py-1 rounded-full text-sm font-bold border" :class="statusBadgeClass(order.status)">
{{ order.status_description }}
</span>
</div>
</div>
@@ -29,22 +30,22 @@
<div class="w-9 h-9 rounded-full bg-green-500 text-white flex items-center justify-center font-bold text-sm border-4 border-slate-50"><i class="pi pi-check"></i></div>
<div class="text-center">
<div class="text-sm font-bold text-slate-900">提交订单</div>
<div class="text-xs text-slate-500 mt-1">{{ order.createTime }}</div>
<div class="text-xs text-slate-500 mt-1">{{ order.create_time }}</div>
</div>
</div>
<!-- Step 2 -->
<div class="flex flex-col items-center gap-2 w-24">
<div class="w-9 h-9 rounded-full flex items-center justify-center font-bold text-sm border-4 border-slate-50 transition-colors" :class="order.payTime ? 'bg-green-500 text-white' : 'bg-slate-200 text-slate-500'"><i class="pi pi-wallet"></i></div>
<div class="w-9 h-9 rounded-full flex items-center justify-center font-bold text-sm border-4 border-slate-50 transition-colors" :class="order.pay_time ? 'bg-green-500 text-white' : 'bg-slate-200 text-slate-500'"><i class="pi pi-wallet"></i></div>
<div class="text-center">
<div class="text-sm font-bold" :class="order.payTime ? 'text-slate-900' : 'text-slate-500'">付款成功</div>
<div class="text-xs text-slate-500 mt-1" v-if="order.payTime">{{ order.payTime }}</div>
<div class="text-sm font-bold" :class="order.pay_time ? 'text-slate-900' : 'text-slate-500'">付款成功</div>
<div class="text-xs text-slate-500 mt-1" v-if="order.pay_time">{{ order.pay_time }}</div>
</div>
</div>
<!-- Step 3 -->
<div class="flex flex-col items-center gap-2 w-24">
<div class="w-9 h-9 rounded-full flex items-center justify-center font-bold text-sm border-4 border-slate-50 transition-colors" :class="order.status === 'completed' ? 'bg-green-500 text-white' : 'bg-slate-200 text-slate-500'"><i class="pi pi-box"></i></div>
<div class="text-center">
<div class="text-sm font-bold" :class="order.status === 'completed' ? 'text-slate-900' : 'text-slate-500'">{{ order.isVirtual ? '自动发货' : '商家发货' }}</div>
<div class="text-sm font-bold" :class="order.status === 'completed' ? 'text-slate-900' : 'text-slate-500'">{{ order.is_virtual ? '自动发货' : '商家发货' }}</div>
</div>
</div>
<!-- Step 4 -->
@@ -61,18 +62,38 @@
<!-- Info Stack (Single Column Layout) -->
<div class="space-y-8">
<!-- 1. Product Info -->
<div class="border border-slate-200 rounded-lg overflow-hidden">
<div class="bg-slate-50 px-6 py-3 border-b border-slate-200 font-bold text-slate-900 text-lg">商品信息</div>
<div class="p-6 flex flex-col sm:flex-row gap-6">
<img :src="order.cover" class="w-32 h-20 object-cover rounded bg-slate-100 flex-shrink-0">
<div class="flex-1 min-w-0">
<h3 class="font-bold text-slate-900 text-xl mb-2">{{ order.title }}</h3>
<div class="text-base text-slate-500 mb-3">{{ order.sku || '默认规格' }}</div>
<div v-if="order.isVirtual" class="inline-flex items-center px-2.5 py-1 rounded text-sm font-medium bg-blue-50 text-blue-600">虚拟商品</div>
<div class="border border-slate-200 rounded-lg overflow-hidden" v-if="order.id">
<div class="bg-slate-50 px-6 py-3 border-b border-slate-200 font-bold text-slate-900 text-lg">
{{ order.type === 'recharge' ? '服务信息' : '商品信息' }}
</div>
<!-- Recharge View -->
<div v-if="order.type === 'recharge'" class="p-6 flex items-center gap-6">
<div class="w-20 h-20 rounded-full bg-slate-50 flex items-center justify-center text-primary-500">
<i class="pi pi-wallet text-3xl"></i>
</div>
<div class="text-right sm:text-right text-left">
<div class="font-bold text-slate-900 text-xl">¥ {{ order.price }}</div>
<div class="text-slate-500 text-base mt-1">x {{ order.quantity }}</div>
<div>
<h3 class="font-bold text-slate-900 text-xl mb-2">账户余额充值</h3>
<div class="text-slate-500">充值金额将会即时到账</div>
</div>
<div class="ml-auto text-right">
<div class="font-bold text-slate-900 text-xl">¥ {{ order.amount }}</div>
</div>
</div>
<!-- Content Items View -->
<div v-else class="divide-y divide-slate-100">
<div v-for="(item, idx) in order.items" :key="idx" class="p-6 flex flex-col sm:flex-row gap-6">
<img :src="item.cover" class="w-32 h-20 object-cover rounded bg-slate-100 flex-shrink-0">
<div class="flex-1 min-w-0">
<h3 class="font-bold text-slate-900 text-xl mb-2">{{ item.title }}</h3>
<div class="text-base text-slate-500 mb-3">{{ item.sku || '默认规格' }}</div>
<div v-if="order.is_virtual" class="inline-flex items-center px-2.5 py-1 rounded text-sm font-medium bg-blue-50 text-blue-600">虚拟商品</div>
</div>
<div class="text-right sm:text-right text-left">
<div class="font-bold text-slate-900 text-xl">¥ {{ item.price }}</div>
<!-- Quantity is on order level usually for single-SKU, or item level? Assuming item price is unit price * quantity or just price paid -->
</div>
</div>
</div>
@@ -81,16 +102,12 @@
<div class="flex flex-col gap-2 ml-auto max-w-sm">
<div class="flex justify-between text-base text-slate-600">
<span>商品总额</span>
<span>¥ {{ (order.price * order.quantity).toFixed(2) }}</span>
<span>¥ {{ order.amount }}</span>
</div>
<div class="flex justify-between text-base text-slate-600">
<span>运费</span>
<span>¥ 0.00</span>
</div>
<div class="flex justify-between text-base text-slate-600">
<span>优惠券</span>
<span class="text-red-600">- ¥ 0.00</span>
</div>
<div class="border-t border-slate-200 my-2 pt-4 flex justify-between items-center">
<span class="font-bold text-slate-900 text-lg">实付金额</span>
<span class="text-3xl font-bold text-red-600">¥ {{ order.amount }}</span>
@@ -108,12 +125,12 @@
<h3 class="font-bold text-slate-900 text-lg mb-4 border-l-4 border-primary-500 pl-3">订单信息</h3>
<div class="space-y-3 text-slate-600">
<p>订单编号: <span class="text-slate-900 select-all">{{ order.id }}</span> <button class="text-primary-600 ml-2 hover:underline font-medium cursor-pointer">复制</button></p>
<p>创建时间: {{ order.createTime }}</p>
<p v-if="order.payTime">付款时间: {{ order.payTime }}</p>
<p>创建时间: {{ order.create_time }}</p>
<p v-if="order.pay_time">付款时间: {{ order.pay_time }}</p>
</div>
</div>
<div v-if="!order.isVirtual">
<div v-if="!order.is_virtual && order.type !== 'recharge'">
<h3 class="font-bold text-slate-900 text-lg mb-4 border-l-4 border-primary-500 pl-3">收货信息</h3>
<div class="space-y-2 text-slate-600">
<p class="font-bold text-slate-900 text-lg">张三 138****8888</p>
@@ -123,13 +140,13 @@
</div>
<!-- Right Column: Merchant & Actions -->
<div class="space-y-6">
<div class="space-y-6" v-if="order.tenant_id !== '0' && order.type !== 'recharge'">
<div>
<h3 class="font-bold text-slate-900 text-lg mb-4 border-l-4 border-primary-500 pl-3">商家信息</h3>
<div class="flex items-center gap-4 p-4 bg-slate-50 rounded-lg">
<img :src="order.tenantAvatar" class="w-12 h-12 rounded-full">
<img :src="order.tenant_avatar || 'https://api.dicebear.com/7.x/initials/svg?seed=' + order.tenant_name" class="w-12 h-12 rounded-full">
<div>
<div class="font-bold text-slate-900">{{ order.tenantName }}</div>
<div class="font-bold text-slate-900">{{ order.tenant_name }}</div>
<button class="text-primary-600 text-sm mt-1 hover:underline cursor-pointer">联系商家</button>
</div>
</div>
@@ -142,32 +159,41 @@
</template>
<script setup>
import { ref, computed } from 'vue';
import { ref, computed, onMounted } from 'vue';
import { useRoute } from 'vue-router';
import { userApi } from '../../api/user';
const route = useRoute();
const orderId = route.params.id;
const order = ref({});
const loading = ref(true);
// Mock Data logic based on ID logic or just static for demo
const order = ref({
id: orderId || '82934712',
createTime: '2025-12-24 14:30:00',
payTime: '2025-12-24 14:30:05',
status: 'completed',
isVirtual: true,
cover: 'https://images.unsplash.com/photo-1514306191717-452ec28c7f31?ixlib=rb-1.2.1&auto=format&fit=crop&w=100&q=60',
title: '《霸王别姬》全本实录珍藏版',
sku: '高清数字版',
price: 9.90,
quantity: 1,
amount: '9.90',
tenantName: '梅派传人小林',
tenantAvatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Master1'
onMounted(async () => {
try {
const res = await userApi.getOrder(orderId);
order.value = res || {};
} catch (e) {
console.error(e);
} finally {
loading.value = false;
}
});
const progressWidth = computed(() => {
if (order.value.status === 'completed') return '100%';
if (order.value.status === 'paid') return '66%';
if (order.value.status === 'completed' || order.value.status === 'paid') return '100%';
if (order.value.status === 'paid') return '66%'; // 'paid' can be 100% for virtual/recharge
return '33%';
});
const statusBadgeClass = (status) => {
const map = {
created: 'bg-orange-50 text-orange-600 border-orange-100',
paid: 'bg-blue-50 text-blue-600 border-blue-100',
completed: 'bg-green-50 text-green-600 border-green-100',
refunding: 'bg-purple-50 text-purple-600 border-purple-100',
refunded: 'bg-slate-100 text-slate-500 border-slate-200',
cancelled: 'bg-slate-100 text-slate-400 border-slate-200'
};
return map[status] || 'bg-slate-50 text-slate-500 border-slate-100';
};
</script>

View File

@@ -50,19 +50,24 @@
<div class="space-y-4">
<div v-for="order in recentOrders" :key="order.id" @click="$router.push(`/me/orders/${order.id}`)"
class="flex items-center gap-4 p-4 border border-slate-100 rounded-lg hover:border-primary-100 hover:shadow-sm transition-all cursor-pointer active:scale-[0.99] group">
<div class="w-16 h-16 bg-slate-100 rounded object-cover flex-shrink-0">
<img v-if="order.items && order.items.length > 0"
<div class="w-16 h-16 bg-slate-100 rounded flex-shrink-0 flex items-center justify-center relative overflow-hidden">
<template v-if="order.type === 'recharge' || !order.items?.length">
<i class="pi pi-wallet text-2xl text-primary-500"></i>
</template>
<img v-else-if="order.items && order.items.length > 0"
:src="order.items[0].cover"
class="w-full h-full object-cover rounded transition-transform group-hover:scale-105">
<i v-else class="pi pi-box text-2xl text-slate-400"></i>
</div>
<div class="flex-1 min-w-0">
<h3 class="font-bold text-slate-900 truncate group-hover:text-primary-600 transition-colors">
{{ order.items && order.items.length > 0 ? order.items[0].title : '未知商品' }}</h3>
{{ (order.type === 'recharge' || !order.items?.length) ? '账户充值' : (order.items?.[0]?.title || '未知商品') }}
</h3>
<div class="text-sm text-slate-500 mt-1">{{ order.create_time }} · 订单号: {{ order.id }}</div>
</div>
<div class="text-right">
<div class="font-bold text-slate-900">¥ {{ order.amount }}</div>
<div class="text-sm text-green-600 mt-1">{{ order.status }}</div>
<div class="text-sm mt-1 font-medium" :class="statusColor(order.status)">{{ order.status_description }}</div>
</div>
</div>
<div v-if="recentOrders.length === 0" class="text-center text-slate-400 py-4">暂无订单</div>
@@ -90,22 +95,38 @@ const wallet = ref({ balance: 0, points: 0 });
const couponCount = ref(0);
const recentOrders = ref([]);
const statusColor = (status) => {
const map = {
created: 'text-orange-600',
paid: 'text-blue-600',
completed: 'text-green-600',
refunding: 'text-purple-600',
refunded: 'text-slate-500',
cancelled: 'text-slate-400'
};
return map[status] || 'text-slate-500';
};
const fetchData = async () => {
try {
const w = await userApi.getWallet();
wallet.value.balance = w.balance || 0;
const u = await userApi.getMe();
wallet.value.points = u.points || 0;
const c = await userApi.getCoupons('unused');
couponCount.value = c.length;
const o = await userApi.getOrders('all');
recentOrders.value = (o || []).slice(0, 3);
} catch (e) {
console.error(e);
}
// Fetch Wallet
userApi.getWallet()
.then(w => { wallet.value.balance = w.balance || 0; })
.catch(e => console.error("Wallet fetch failed:", e));
// Fetch User Info (Points)
userApi.getMe()
.then(u => { wallet.value.points = u.points || 0; })
.catch(e => console.error("Me fetch failed:", e));
// Fetch Coupons
userApi.getCoupons('unused')
.then(c => { couponCount.value = (c || []).length; })
.catch(e => console.error("Coupons fetch failed:", e));
// Fetch Recent Orders
userApi.getOrders('all')
.then(o => { recentOrders.value = (o || []).slice(0, 3); })
.catch(e => console.error("Orders fetch failed:", e));
};
onMounted(() => {

View File

@@ -32,25 +32,40 @@
<!-- Order Header -->
<div class="bg-slate-50 px-4 py-3 flex items-center justify-between text-sm text-slate-500">
<div class="flex items-center gap-4">
<span class="font-medium text-slate-900">{{ order.date }}</span>
<span class="font-medium text-slate-900">{{ order.create_time }}</span>
<span class="select-all">订单号: {{ order.id }}</span>
<span class="hover:text-primary-600 cursor-pointer transition-colors">{{ order.tenantName }} <i class="pi pi-angle-right text-xs"></i></span>
<span class="hover:text-primary-600 cursor-pointer transition-colors" v-if="order.tenant_id !== '0'">{{ order.tenant_name }} <i class="pi pi-angle-right text-xs"></i></span>
<span v-else class="text-slate-500">平台自营</span>
</div>
<div class="font-bold" :class="statusColor(order.status)">{{ statusText(order.status) }}</div>
<div class="font-bold" :class="statusColor(order.status)">{{ order.status_description }}</div>
</div>
<!-- Order Body -->
<div class="p-4 flex flex-col sm:flex-row gap-6">
<!-- Product Info (Clickable Area) -->
<div class="flex-1 flex gap-4 cursor-pointer" @click="$router.push(`/me/orders/${order.id}`)">
<div class="w-24 h-16 bg-slate-100 rounded object-cover flex-shrink-0 relative overflow-hidden group-hover:opacity-90 transition-opacity">
<img :src="order.cover" class="w-full h-full object-cover">
<div v-if="order.type === 'video'" class="absolute inset-0 flex items-center justify-center bg-black/20 text-white"><i class="pi pi-play-circle"></i></div>
<div class="w-24 h-16 bg-slate-100 rounded flex-shrink-0 relative overflow-hidden group-hover:opacity-90 transition-opacity flex items-center justify-center">
<template v-if="order.type === 'recharge' || !order.items?.length">
<i class="pi pi-wallet text-3xl text-primary-500"></i>
</template>
<template v-else-if="order.items && order.items.length > 0">
<img :src="order.items[0].cover" class="w-full h-full object-cover">
<!-- Assuming genre is on item, not order.type -->
</template>
<template v-else>
<i class="pi pi-box text-2xl text-slate-400"></i>
</template>
</div>
<div>
<h3 class="font-bold text-slate-900 line-clamp-1 mb-1 group-hover:text-primary-600 transition-colors">{{ order.title }}</h3>
<div class="text-xs text-slate-500 mb-2">{{ order.typeLabel }}</div>
<div v-if="order.isVirtual" class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-blue-50 text-blue-600">虚拟发货</div>
<h3 class="font-bold text-slate-900 line-clamp-1 mb-1 group-hover:text-primary-600 transition-colors">
{{ (order.type === 'recharge' || !order.items?.length) ? '账户充值' : (order.items?.[0]?.title || '未知商品') }}
</h3>
<div class="text-xs text-slate-500 mb-2">
{{ (order.type === 'recharge' || !order.items?.length) ? '在线充值' : (order.items?.[0]?.genre || '数字内容') }}
</div>
<div v-if="order.is_virtual || order.type === 'recharge' || !order.items?.length" class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-blue-50 text-blue-600">
{{ (order.type === 'recharge' || !order.items?.length) ? '即时到账' : '虚拟发货' }}
</div>
</div>
</div>
@@ -61,10 +76,10 @@
<div class="text-xs text-slate-400">在线支付</div>
</div>
<div class="flex flex-col gap-2">
<button v-if="order.status === 'unpaid'" class="px-4 py-1.5 bg-primary-600 text-white text-sm font-medium rounded-lg hover:bg-primary-700 transition-colors shadow-sm active:scale-95 cursor-pointer">去支付</button>
<button v-if="order.status === 'created'" class="px-4 py-1.5 bg-primary-600 text-white text-sm font-medium rounded-lg hover:bg-primary-700 transition-colors shadow-sm active:scale-95 cursor-pointer">去支付</button>
<router-link :to="`/me/orders/${order.id}`" v-if="order.status === 'paid' || order.status === 'completed'" class="px-4 py-1.5 border border-slate-300 text-slate-700 text-sm font-medium rounded-lg hover:bg-slate-50 hover:border-slate-400 transition-colors inline-block text-center cursor-pointer active:scale-95">查看详情</router-link>
<button v-if="order.status === 'completed'" class="px-4 py-1.5 text-primary-600 text-sm hover:underline cursor-pointer">申请售后</button>
<button v-if="order.status === 'unpaid'" class="text-xs text-slate-400 hover:text-slate-600 cursor-pointer">取消订单</button>
<button v-if="order.status === 'completed' && order.type !== 'recharge'" class="px-4 py-1.5 text-primary-600 text-sm hover:underline cursor-pointer">申请售后</button>
<button v-if="order.status === 'created'" class="text-xs text-slate-400 hover:text-slate-600 cursor-pointer">取消订单</button>
</div>
</div>
</div>
@@ -124,18 +139,6 @@ watch(currentTab, () => {
// Use orders directly (filtered by backend)
const filteredOrders = computed(() => orders.value);
const statusText = (status) => {
const map = {
created: '待支付',
paid: '已支付',
completed: '交易成功',
refunding: '退款中',
refunded: '已退款',
cancelled: '已取消'
};
return map[status] || status;
};
const statusColor = (status) => {
const map = {
created: 'text-orange-600',