feat: 前端页面和组件全面完善

- 优化通用组件:导航栏、页脚、图片上传、搜索
- 完善业务组件:商品卡片、秒杀卡片
- 更新用户端页面:首页、商品、秒杀、订单、购物车、个人中心
- 新增用户收藏页面
- 完善管理后台:仪表盘、商品/订单/用户/秒杀管理
- 新增管理后台:收藏管理、评价管理、系统监控页面
This commit is contained in:
2026-03-10 23:21:53 +08:00
parent abba469a20
commit c52d9c52e3
25 changed files with 3409 additions and 1467 deletions

View File

@@ -1,167 +1,95 @@
<template>
<div class="order-detail-page">
<div class="container mx-auto px-4 py-8">
<!-- 面包屑 -->
<el-breadcrumb separator="/" class="mb-6">
<el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item>
<el-breadcrumb-item :to="{ path: '/orders' }">我的订单</el-breadcrumb-item>
<el-breadcrumb-item>订单详情</el-breadcrumb-item>
</el-breadcrumb>
<div v-if="loading" class="text-center py-12">
<el-icon :size="40" class="animate-spin"><Loading /></el-icon>
<p class="mt-2 text-gray-500">加载中...</p>
</div>
<div v-else-if="!order" class="text-center py-12">
<el-empty description="订单不存在" />
<el-button type="primary" @click="router.push('/orders')">
返回订单列表
</el-button>
<el-button type="primary" @click="router.push('/orders')">返回订单列表</el-button>
</div>
<div v-else>
<!-- 订单状态 -->
<div class="bg-white rounded-lg shadow-sm p-6 mb-6">
<div class="flex justify-between items-center mb-4">
<h2 class="text-xl font-semibold">订单状态</h2>
<el-tag :type="getStatusType(order.status)" size="large">
{{ getStatusText(order.status) }}
</el-tag>
<el-tag :type="getStatusType(order.status)" size="large">{{ getStatusText(order.status) }}</el-tag>
</div>
<!-- 订单进度 -->
<el-steps :active="getActiveStep()" finish-status="success">
<el-step title="提交订单" :description="formatTime(order.createdAt)" />
<el-step title="支付订单" :description="order.paidAt ? formatTime(order.paidAt) : ''" />
<el-step title="商家发货" :description="order.shippedAt ? formatTime(order.shippedAt) : ''" />
<el-step title="确认收货" :description="order.completedAt ? formatTime(order.completedAt) : ''" />
</el-steps>
<!-- 操作按钮 -->
<div class="mt-6 flex gap-2">
<template v-if="order.status === 'PENDING'">
<el-button type="primary" @click="handlePay">立即付款</el-button>
<el-button @click="handleCancel">取消订单</el-button>
</template>
<template v-else-if="order.status === 'SHIPPED'">
<el-button type="primary" @click="handleConfirm">确认收货</el-button>
</template>
<template v-else-if="order.status === 'COMPLETED'">
<el-button @click="handleReview">评价</el-button>
<el-button @click="handleRebuy">再次购买</el-button>
<el-button text type="danger" @click="handleDelete">删除订单</el-button>
</template>
<template v-else-if="order.status === 'CANCELLED'">
<el-button text type="danger" @click="handleDelete">删除订单</el-button>
</template>
</div>
</div>
<!-- 收货信息 -->
<div class="bg-white rounded-lg shadow-sm p-6 mb-6">
<h3 class="text-lg font-semibold mb-4">收货信息</h3>
<div v-if="order.address" class="text-sm space-y-2">
<div>
<span class="text-gray-500">收货</span>
<span>{{ order.address.name }} {{ order.address.phone }}</span>
</div>
<div>
<span class="text-gray-500">收货地址</span>
<span>
{{ order.address.province }}
{{ order.address.city }}
{{ order.address.district }}
{{ order.address.address }}
</span>
</div>
</div>
<div v-else class="text-gray-500">
暂无收货信息
<div><span class="text-gray-500">收货人</span><span>{{ order.address.name }} {{ order.address.phone }}</span></div>
<div><span class="text-gray-500">收货地址</span><span>{{ order.address.province }} {{ order.address.city }} {{ order.address.district }} {{ order.address.address }}</span></div>
</div>
<div v-else class="text-gray-500">暂无收货信息</div>
</div>
<!-- 商品信息 -->
<div class="bg-white rounded-lg shadow-sm p-6 mb-6">
<h3 class="text-lg font-semibold mb-4">商品信息</h3>
<div class="space-y-4">
<div
v-for="item in order.items"
:key="item.id"
class="flex gap-4 pb-4 border-b last:border-0"
>
<img
:src="item.productImage || '/default-product.png'"
:alt="item.productName"
class="w-24 h-24 object-cover rounded"
@error="handleImageError"
>
<div v-for="item in order.items" :key="item.id" class="flex gap-4 pb-4 border-b last:border-0">
<SafeImage :src="item.productImage" :alt="item.productName" wrapper-class="w-24 h-24 rounded" img-class="w-24 h-24 object-cover rounded" />
<div class="flex-1">
<h4 class="font-semibold mb-2">{{ item.productName }}</h4>
<div class="text-sm text-gray-500">
单价¥{{ item.price }} × {{ item.quantity }}
</div>
</div>
<div class="text-right">
<div class="font-semibold text-lg">¥{{ item.subtotal }}</div>
<div class="text-sm text-gray-500">单价¥{{ item.price }} × {{ item.quantity }}</div>
</div>
<div class="text-right"><div class="font-semibold text-lg">¥{{ item.subtotal }}</div></div>
</div>
</div>
<!-- 费用明细 -->
<div class="border-t pt-4 mt-4 space-y-2">
<div class="flex justify-between text-sm">
<span class="text-gray-500">商品总额</span>
<span>¥{{ order.totalAmount }}</span>
</div>
<div class="flex justify-between text-sm">
<span class="text-gray-500">运费</span>
<span>¥0.00</span>
</div>
<div class="flex justify-between text-sm">
<span class="text-gray-500">优惠</span>
<span class="text-red-500">-¥{{ (order.totalAmount - order.paymentAmount).toFixed(2) }}</span>
</div>
<div class="flex justify-between pt-2 border-t">
<span class="font-semibold">实付金额</span>
<span class="text-xl font-bold text-red-500">¥{{ order.paymentAmount }}</span>
</div>
<div class="flex justify-between text-sm"><span class="text-gray-500">商品总额</span><span>¥{{ order.totalAmount }}</span></div>
<div class="flex justify-between text-sm"><span class="text-gray-500">运费</span><span>¥0.00</span></div>
<div class="flex justify-between text-sm"><span class="text-gray-500">优惠</span><span class="text-red-500">-¥0.00</span></div>
<div class="flex justify-between text-lg font-semibold pt-2 border-t"><span>实付金额</span><span class="text-red-500">¥{{ order.paymentAmount }}</span></div>
</div>
</div>
<!-- 订单信息 -->
<div class="bg-white rounded-lg shadow-sm p-6">
<h3 class="text-lg font-semibold mb-4">订单信息</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm">
<div>
<span class="text-gray-500">订单编号</span>
<span>{{ order.orderNo }}</span>
<el-button text type="primary" size="small" @click="copyOrderNo">
复制
</el-button>
</div>
<div>
<span class="text-gray-500">创建时间</span>
<span>{{ formatTime(order.createdAt) }}</span>
</div>
<div v-if="order.paidAt">
<span class="text-gray-500">付款时间</span>
<span>{{ formatTime(order.paidAt) }}</span>
</div>
<div v-if="order.paymentMethod">
<span class="text-gray-500">支付方式</span>
<span>{{ getPaymentMethodText(order.paymentMethod) }}</span>
</div>
<div v-if="order.shippedAt">
<span class="text-gray-500">发货时间</span>
<span>{{ formatTime(order.shippedAt) }}</span>
</div>
<div v-if="order.completedAt">
<span class="text-gray-500">完成时间</span>
<span>{{ formatTime(order.completedAt) }}</span>
</div>
<div v-if="order.remark" class="md:col-span-2">
<span class="text-gray-500">订单备注</span>
<span>{{ order.remark }}</span>
</div>
<div><span class="text-gray-500">订单编号</span><span>{{ order.orderNo }}</span><el-button text type="primary" size="small" @click="copyOrderNo">复制</el-button></div>
<div><span class="text-gray-500">创建时间</span><span>{{ formatTime(order.createdAt) }}</span></div>
<div v-if="order.paidAt"><span class="text-gray-500">付款时间</span><span>{{ formatTime(order.paidAt) }}</span></div>
<div v-if="order.paymentMethod"><span class="text-gray-500">支付方式</span><span>{{ getPaymentMethodText(order.paymentMethod) }}</span></div>
<div v-if="order.shippedAt"><span class="text-gray-500">发货时间</span><span>{{ formatTime(order.shippedAt) }}</span></div>
<div v-if="order.completedAt"><span class="text-gray-500">完成时间</span><span>{{ formatTime(order.completedAt) }}</span></div>
<div v-if="order.remark" class="md:col-span-2"><span class="text-gray-500">订单备注</span><span>{{ order.remark }}</span></div>
</div>
</div>
</div>
@@ -174,88 +102,40 @@ import { ref, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import { orderApi } from '@/api/modules/order'
import { reviewApi } from '@/api/modules/review'
import { useCartStore } from '@/stores/cart'
import type { Order } from '@/types/api'
import dayjs from 'dayjs'
import SafeImage from '@/components/common/SafeImage.vue'
const route = useRoute()
const router = useRouter()
const cartStore = useCartStore()
const loading = ref(false)
const order = ref<Order | null>(null)
// 格式化时间
const formatTime = (time: string) => {
return dayjs(time).format('YYYY-MM-DD HH:mm:ss')
}
const formatTime = (time: string) => dayjs(time).format('YYYY-MM-DD HH:mm:ss')
const getStatusType = (status: string) => ({ PENDING: 'warning', PAID: 'primary', SHIPPED: 'primary', COMPLETED: 'success', CANCELLED: 'info', REFUNDED: 'danger' }[status] || 'info')
const getStatusText = (status: string) => ({ PENDING: '待付款', PAID: '待发货', SHIPPED: '待收货', COMPLETED: '已完成', CANCELLED: '已取消', REFUNDED: '已退款' }[status] || status)
const getPaymentMethodText = (method: string) => ({ ONLINE: '在线支付', ALIPAY: '支付宝', WECHAT: '微信支付', CASH: '货到付款', default: '默认支付' }[method] || method)
// 处理图片错误
const handleImageError = (e: Event) => {
const target = e.target as HTMLImageElement
target.src = '/default-product.png'
}
// 获取状态类型
const getStatusType = (status: string) => {
const map: Record<string, string> = {
'PENDING': 'warning',
'PAID': 'primary',
'SHIPPED': 'primary',
'COMPLETED': 'success',
'CANCELLED': 'info',
'REFUNDED': 'danger'
}
return map[status] || 'info'
}
// 获取状态文本
const getStatusText = (status: string) => {
const map: Record<string, string> = {
'PENDING': '待付款',
'PAID': '待发货',
'SHIPPED': '待收货',
'COMPLETED': '已完成',
'CANCELLED': '已取消',
'REFUNDED': '已退款'
}
return map[status] || status
}
// 获取支付方式文本
const getPaymentMethodText = (method: string) => {
const map: Record<string, string> = {
'ONLINE': '在线支付',
'ALIPAY': '支付宝',
'WECHAT': '微信支付',
'CASH': '货到付款'
}
return map[method] || method
}
// 获取当前步骤
const getActiveStep = () => {
if (!order.value) return 0
switch (order.value.status) {
case 'PENDING': return 1
case 'PAID': return 2
case 'SHIPPED': return 3
case 'COMPLETED': return 4
case 'CANCELLED':
case 'REFUNDED': return -1
default: return 0
}
}
// 加载订单详情
const loadOrderDetail = async () => {
loading.value = true
try {
const id = Number(route.params.id)
const res = await orderApi.getDetail(id)
if (res.success) {
order.value = res.data
}
const res = await orderApi.getDetail(Number(route.params.id))
if (res.success) order.value = res.data
} catch (error) {
console.error('加载订单详情失败:', error)
ElMessage.error('加载失败')
@@ -264,89 +144,58 @@ const loadOrderDetail = async () => {
}
}
// 复制订单号
const copyOrderNo = () => {
if (order.value) {
navigator.clipboard.writeText(order.value.orderNo)
ElMessage.success('订单号已复制')
}
if (!order.value) return
navigator.clipboard.writeText(order.value.orderNo)
ElMessage.success('订单号已复制')
}
// 付款
const handlePay = async () => {
if (!order.value) return
await ElMessageBox.confirm(
`订单金额:¥${order.value.paymentAmount},确认付款?`,
'付款确认',
{
confirmButtonText: '确认付款',
cancelButtonText: '取消',
type: 'warning',
}
)
try {
await orderApi.pay(order.value.id, 'ONLINE')
ElMessage.success('付款成功')
loadOrderDetail()
} catch (error) {
console.error('付款失败:', error)
}
await ElMessageBox.confirm(`订单金额:¥${order.value.paymentAmount},确认付款?`, '付款确认', { confirmButtonText: '确认付款', cancelButtonText: '取消', type: 'warning' })
try { await orderApi.pay(order.value.id, 'ONLINE'); ElMessage.success('付款成功'); loadOrderDetail() } catch (error) { console.error('付款失败:', error) }
}
// 取消订单
const handleCancel = async () => {
if (!order.value) return
await ElMessageBox.confirm('确定要取消订单吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
try {
await orderApi.cancel(order.value.id)
ElMessage.success('订单已取消')
loadOrderDetail()
} catch (error) {
console.error('取消订单失败:', error)
}
await ElMessageBox.confirm('确定要取消该订单吗?', '提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' })
try { await orderApi.cancel(order.value.id); ElMessage.success('订单已取消'); loadOrderDetail() } catch (error) { console.error('取消订单失败:', error) }
}
// 确认收货
const handleConfirm = async () => {
if (!order.value) return
await ElMessageBox.confirm('确定已收到商品?', '确认收货', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
await ElMessageBox.confirm('确定已收到商品?', '确认收货', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' })
try { await orderApi.confirm(order.value.id); ElMessage.success('已确认收货'); loadOrderDetail() } catch (error) { console.error('确认收货失败:', error) }
}
const handleReview = async () => {
if (!order.value) return
const firstItem = order.value.items[0]
if (!firstItem) return
try {
await orderApi.confirm(order.value.id)
ElMessage.success('已确认收货')
loadOrderDetail()
const { value } = await ElMessageBox.prompt('请输入本次购物评价', '商品评价', { inputType: 'textarea', inputPlaceholder: '分享一下你的使用感受吧', confirmButtonText: '提交评价', cancelButtonText: '取消' })
await reviewApi.create({ orderId: order.value.id, productId: firstItem.productId, rating: 5, content: value })
ElMessage.success('评价提交成功')
} catch (error) {
console.error('确认收货失败:', error)
if (error) console.error('提交评价失败:', error)
}
}
// 评价
const handleReview = () => {
ElMessage.info('评价功能开发中...')
}
// 再次购买
const handleRebuy = () => {
ElMessage.success('商品已加入购物车')
const handleRebuy = async () => {
if (!order.value) return
const firstItem = order.value.items[0]
if (!firstItem) return
await cartStore.addToCart(firstItem.productId, firstItem.quantity)
router.push('/cart')
}
onMounted(() => {
loadOrderDetail()
})
const handleDelete = async () => {
if (!order.value) return
await ElMessageBox.confirm('确定删除该订单吗?删除后不可恢复。', '删除确认', { confirmButtonText: '删除', cancelButtonText: '取消', type: 'warning' })
try { await orderApi.delete(order.value.id); ElMessage.success('订单已删除'); router.push('/orders') } catch (error) { console.error('删除订单失败:', error) }
}
onMounted(() => { loadOrderDetail() })
</script>
<style scoped lang="scss">
@@ -354,4 +203,4 @@ onMounted(() => {
min-height: calc(100vh - 60px);
background-color: #f5f5f5;
}
</style>
</style>