feat: order page
This commit is contained in:
@@ -65,7 +65,12 @@ const router = createRouter({
|
||||
{
|
||||
path: 'orders',
|
||||
name: 'user-orders',
|
||||
component: () => import('../views/user/OrdersView.vue') // Placeholder
|
||||
component: () => import('../views/user/OrdersView.vue')
|
||||
},
|
||||
{
|
||||
path: 'orders/:id',
|
||||
name: 'user-order-detail',
|
||||
component: () => import('../views/order/DetailView.vue')
|
||||
},
|
||||
{
|
||||
path: 'wallet',
|
||||
|
||||
173
frontend/portal/src/views/order/DetailView.vue
Normal file
173
frontend/portal/src/views/order/DetailView.vue
Normal file
@@ -0,0 +1,173 @@
|
||||
<template>
|
||||
<div class="bg-white rounded-xl shadow-sm border border-slate-100 min-h-[600px] p-8">
|
||||
<!-- Header -->
|
||||
<div class="flex items-center gap-4 mb-8">
|
||||
<button @click="$router.back()" class="w-10 h-10 flex items-center justify-center rounded-full hover:bg-slate-100 text-slate-500 transition-colors">
|
||||
<i class="pi pi-arrow-left text-lg"></i>
|
||||
</button>
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-slate-900">订单详情</h1>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Status Progress (Fixed Alignment) -->
|
||||
<div class="mb-10 p-8 bg-slate-50 rounded-xl border border-slate-100">
|
||||
<div class="relative mx-12"> <!-- Added margin for text overflow safety -->
|
||||
<!-- Background Line -->
|
||||
<div class="absolute top-4 left-0 w-full h-1 bg-slate-200 -z-0"></div>
|
||||
<!-- Active Line -->
|
||||
<div class="absolute top-4 left-0 h-1 bg-green-500 -z-0 transition-all duration-500" :style="{ width: progressWidth }"></div>
|
||||
|
||||
<div class="flex justify-between relative z-10">
|
||||
<!-- Step 1 -->
|
||||
<div class="flex flex-col items-center gap-2 w-24 -ml-12"> <!-- Fixed width and negative margin to center on point -->
|
||||
<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>
|
||||
</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="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>
|
||||
</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>
|
||||
</div>
|
||||
<!-- Step 4 -->
|
||||
<div class="flex flex-col items-center gap-2 w-24 -mr-12">
|
||||
<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-thumbs-up"></i></div>
|
||||
<div class="text-center">
|
||||
<div class="text-sm font-bold" :class="order.status === 'completed' ? 'text-slate-900' : 'text-slate-500'">交易完成</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 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>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<!-- Price Calculation -->
|
||||
<div class="border-t border-slate-100 p-6 bg-slate-50/50">
|
||||
<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>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 2. Order Meta Info (Full Width) -->
|
||||
<div class="bg-white border border-slate-200 rounded-xl p-8 text-base">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-12">
|
||||
<!-- Left Column: Basic Info -->
|
||||
<div class="space-y-6">
|
||||
<div>
|
||||
<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">复制</button></p>
|
||||
<p>创建时间: {{ order.createTime }}</p>
|
||||
<p v-if="order.payTime">付款时间: {{ order.payTime }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="!order.isVirtual">
|
||||
<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>
|
||||
<p>北京市 朝阳区 建国路88号</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right Column: Merchant & Actions -->
|
||||
<div class="space-y-6">
|
||||
<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">
|
||||
<div>
|
||||
<div class="font-bold text-slate-900">{{ order.tenantName }}</div>
|
||||
<button class="text-primary-600 text-sm mt-1 hover:underline">联系商家</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
const route = useRoute();
|
||||
const orderId = route.params.id;
|
||||
|
||||
// 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'
|
||||
});
|
||||
|
||||
const progressWidth = computed(() => {
|
||||
if (order.value.status === 'completed') return '100%';
|
||||
if (order.value.status === 'paid') return '66%';
|
||||
return '33%';
|
||||
});
|
||||
</script>
|
||||
@@ -1,6 +1,165 @@
|
||||
<template>
|
||||
<div class="p-8">
|
||||
<h1 class="text-2xl font-bold mb-4">My Orders</h1>
|
||||
<p class="text-slate-400">(Order list)</p>
|
||||
<div class="bg-white rounded-xl shadow-sm border border-slate-100 min-h-[600px]">
|
||||
<!-- Header & Tabs -->
|
||||
<div class="px-6 pt-6 border-b border-slate-100">
|
||||
<h1 class="text-2xl font-bold text-slate-900 mb-6">我的订单</h1>
|
||||
<div class="flex items-center gap-8">
|
||||
<button
|
||||
v-for="tab in tabs"
|
||||
:key="tab.value"
|
||||
@click="currentTab = tab.value"
|
||||
class="pb-4 text-sm font-medium transition-colors border-b-2"
|
||||
:class="currentTab === tab.value ? 'text-primary-600 border-primary-600' : 'text-slate-500 border-transparent hover:text-slate-700'"
|
||||
>
|
||||
{{ tab.label }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Order List -->
|
||||
<div class="p-6 space-y-6">
|
||||
<!-- Search/Filter (Optional) -->
|
||||
<div class="flex justify-end mb-4">
|
||||
<div class="relative w-64">
|
||||
<i class="pi pi-search absolute left-3 top-1/2 -translate-y-1/2 text-slate-400"></i>
|
||||
<input type="text" placeholder="搜索订单号或商品..." class="w-full pl-10 pr-4 py-2 border border-slate-200 rounded-lg text-sm focus:outline-none focus:border-primary-500">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- List Items -->
|
||||
<div v-if="filteredOrders.length > 0" class="space-y-6">
|
||||
<div v-for="order in filteredOrders" :key="order.id" class="border border-slate-200 rounded-lg overflow-hidden hover:border-slate-300 transition-colors">
|
||||
<!-- 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>订单号: {{ order.id }}</span>
|
||||
<span>{{ order.tenantName }}</span>
|
||||
</div>
|
||||
<div class="font-bold" :class="statusColor(order.status)">{{ statusText(order.status) }}</div>
|
||||
</div>
|
||||
|
||||
<!-- Order Body -->
|
||||
<div class="p-4 flex flex-col sm:flex-row gap-6">
|
||||
<!-- Product Info -->
|
||||
<div class="flex-1 flex gap-4">
|
||||
<div class="w-24 h-16 bg-slate-100 rounded object-cover flex-shrink-0 relative overflow-hidden">
|
||||
<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>
|
||||
<div>
|
||||
<h3 class="font-bold text-slate-900 line-clamp-1 mb-1">{{ 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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Price & Actions -->
|
||||
<div class="flex items-center justify-between sm:w-1/3 sm:border-l sm:border-slate-100 sm:pl-6">
|
||||
<div class="text-right sm:text-left">
|
||||
<div class="font-bold text-slate-900 text-lg">¥ {{ order.amount }}</div>
|
||||
<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">去支付</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 inline-block text-center">查看详情</router-link>
|
||||
<button v-if="order.status === 'completed'" class="px-4 py-1.5 text-primary-600 text-sm hover:underline">申请售后</button>
|
||||
<button v-if="order.status === 'unpaid'" class="text-xs text-slate-400 hover:text-slate-600">取消订单</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div v-else class="text-center py-20">
|
||||
<div class="inline-flex items-center justify-center w-16 h-16 rounded-full bg-slate-50 mb-4">
|
||||
<i class="pi pi-shopping-bag text-2xl text-slate-300"></i>
|
||||
</div>
|
||||
<p class="text-slate-500 text-lg">暂无相关订单</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue';
|
||||
|
||||
const currentTab = ref('all');
|
||||
const tabs = [
|
||||
{ label: '全部订单', value: 'all' },
|
||||
{ label: '待支付', value: 'unpaid' },
|
||||
{ label: '已完成', value: 'completed' },
|
||||
{ label: '退款/售后', value: 'refund' }
|
||||
];
|
||||
|
||||
// Mock Data
|
||||
const orders = ref([
|
||||
{
|
||||
id: '82934712',
|
||||
date: '2025-12-24 14:30',
|
||||
tenantName: '梅派传人小林',
|
||||
cover: 'https://images.unsplash.com/photo-1514306191717-452ec28c7f31?ixlib=rb-1.2.1&auto=format&fit=crop&w=100&q=60',
|
||||
title: '《霸王别姬》全本实录珍藏版',
|
||||
type: 'video',
|
||||
typeLabel: '戏曲视频',
|
||||
isVirtual: true,
|
||||
amount: '9.90',
|
||||
status: 'completed'
|
||||
},
|
||||
{
|
||||
id: '82934713',
|
||||
date: '2025-12-23 09:15',
|
||||
tenantName: '戏曲周边商城',
|
||||
cover: 'https://images.unsplash.com/photo-1557683316-973673baf926?ixlib=rb-1.2.1&auto=format&fit=crop&w=100&q=60',
|
||||
title: '京剧脸谱纪念书签 (一套4张)',
|
||||
type: 'product',
|
||||
typeLabel: '实体商品',
|
||||
isVirtual: false,
|
||||
amount: '45.00',
|
||||
status: 'unpaid'
|
||||
},
|
||||
{
|
||||
id: '82934711',
|
||||
date: '2025-12-20 18:20',
|
||||
tenantName: '豫剧李大师',
|
||||
cover: 'https://images.unsplash.com/photo-1469571486292-0ba58a3f068b?ixlib=rb-1.2.1&auto=format&fit=crop&w=100&q=60',
|
||||
title: '豫剧唱腔发音技巧专栏',
|
||||
type: 'article',
|
||||
typeLabel: '付费专栏',
|
||||
isVirtual: true,
|
||||
amount: '99.00',
|
||||
status: 'refunded' // or refunding
|
||||
}
|
||||
]);
|
||||
|
||||
const filteredOrders = computed(() => {
|
||||
if (currentTab.value === 'all') return orders.value;
|
||||
if (currentTab.value === 'refund') return orders.value.filter(o => ['refunded', 'refunding'].includes(o.status));
|
||||
return orders.value.filter(o => o.status === currentTab.value);
|
||||
});
|
||||
|
||||
const statusText = (status) => {
|
||||
const map = {
|
||||
unpaid: '待支付',
|
||||
paid: '已支付',
|
||||
completed: '交易成功',
|
||||
refunding: '退款中',
|
||||
refunded: '已退款',
|
||||
cancelled: '已取消'
|
||||
};
|
||||
return map[status] || status;
|
||||
};
|
||||
|
||||
const statusColor = (status) => {
|
||||
const map = {
|
||||
unpaid: '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';
|
||||
};
|
||||
</script>
|
||||
Reference in New Issue
Block a user