Files
FlashSaleSystem/flash-sale-frontend/src/pages/flashsale/detail.vue
YoVinchen c52d9c52e3 feat: 前端页面和组件全面完善
- 优化通用组件:导航栏、页脚、图片上传、搜索
- 完善业务组件:商品卡片、秒杀卡片
- 更新用户端页面:首页、商品、秒杀、订单、购物车、个人中心
- 新增用户收藏页面
- 完善管理后台:仪表盘、商品/订单/用户/秒杀管理
- 新增管理后台:收藏管理、评价管理、系统监控页面
2026-03-10 23:21:53 +08:00

249 lines
8.8 KiB
Vue
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>
<div class="flashsale-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: '/flashsale' }">秒杀活动</el-breadcrumb-item>
<el-breadcrumb-item>{{ flashSale?.productName || '详情' }}</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="!flashSale" class="text-center py-12">
<el-empty description="秒杀活动不存在" />
<el-button type="primary" @click="router.push('/flashsale')">返回秒杀列表</el-button>
</div>
<div v-else class="bg-white rounded-lg shadow-lg overflow-hidden">
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8 p-8">
<div>
<div class="relative">
<SafeImage
:src="flashSale.productImageUrl"
:alt="flashSale.productName"
wrapper-class="w-full rounded-lg overflow-hidden bg-gray-100"
img-class="w-full rounded-lg object-cover"
/>
<div class="absolute top-4 left-4">
<el-tag :type="statusType" size="large" effect="dark">
<el-icon class="mr-1"><Lightning /></el-icon>
{{ statusText }}
</el-tag>
</div>
</div>
</div>
<div>
<h1 class="text-3xl font-bold mb-4">{{ flashSale.productName }}</h1>
<div class="bg-red-50 rounded-lg p-6 mb-6">
<div class="flex items-end mb-2">
<span class="text-sm text-gray-500 mr-2">秒杀价</span>
<span class="text-4xl font-bold text-red-500">¥{{ flashSale.flashPrice }}</span>
<span class="ml-4 text-lg text-gray-400 line-through">¥{{ flashSale.originalPrice }}</span>
<span class="ml-2 px-2 py-1 bg-red-500 text-white text-sm rounded">{{ discountPercent }}% OFF</span>
</div>
<div class="text-sm text-gray-600 mt-4">
<p>开始时间{{ formatTime(flashSale.startTime) }}</p>
<p>结束时间{{ formatTime(flashSale.endTime) }}</p>
</div>
</div>
<div class="mb-6">
<div class="flex justify-between items-center mb-2">
<span class="text-gray-600">库存情况</span>
<span class="text-sm text-gray-500">剩余 {{ flashSale.remainingStock }} / {{ flashSale.flashStock }} </span>
</div>
<el-progress :percentage="stockPercent" :stroke-width="10" :color="progressColor" />
</div>
<div v-if="flashSale.status === 'ACTIVE'" class="mb-6">
<div class="text-center p-4 bg-gray-50 rounded-lg">
<p class="text-sm text-gray-600 mb-2">距离结束还有</p>
<CountDown :end-time="endTime" @finish="handleFinish" />
</div>
</div>
<div class="mb-6 p-4 bg-blue-50 rounded-lg">
<div class="flex items-center text-blue-700">
<el-icon class="mr-2"><InfoFilled /></el-icon>
<span>每人限购 {{ flashSale.limitPerUser }} </span>
</div>
</div>
<div class="space-y-4">
<el-button type="danger" size="large" class="w-full" :disabled="!canParticipate" :loading="participating" @click="handleParticipate">
<el-icon class="mr-2"><Lightning /></el-icon>
{{ buttonText }}
</el-button>
<div class="flex gap-4">
<el-button size="large" class="flex-1" @click="handleViewProduct">查看商品详情</el-button>
<el-button size="large" class="flex-1" @click="handleShare"><el-icon class="mr-1"><Share /></el-icon>分享活动</el-button>
</div>
</div>
<div class="mt-8 p-4 bg-yellow-50 rounded-lg">
<h3 class="font-semibold mb-2">抢购说明</h3>
<ul class="text-sm text-gray-600 space-y-1">
<li> 秒杀商品数量有限先到先得</li>
<li> 每个用户限购{{ flashSale.limitPerUser }}</li>
<li> 下单后请在30分钟内完成支付</li>
<li> 商品一经售出非质量问题不支持退换</li>
</ul>
</div>
</div>
</div>
<div v-if="flashSale.description" class="border-t px-8 py-6">
<h3 class="text-xl font-semibold mb-4">活动说明</h3>
<div class="text-gray-600" v-html="flashSale.description"></div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import CountDown from '@/components/business/CountDown.vue'
import SafeImage from '@/components/common/SafeImage.vue'
import { flashsaleApi } from '@/api/modules/flashsale'
import { useUserStore } from '@/stores/user'
import type { FlashSale } from '@/types/api'
import dayjs from 'dayjs'
const route = useRoute()
const router = useRouter()
const userStore = useUserStore()
const loading = ref(false)
const participating = ref(false)
const flashSale = ref<FlashSale | null>(null)
const formatTime = (time: string) => dayjs(time).format('YYYY-MM-DD HH:mm:ss')
const statusType = computed(() => {
if (!flashSale.value) return 'info'
switch (flashSale.value.status) {
case 'UPCOMING': return 'warning'
case 'ACTIVE': return 'danger'
case 'ENDED': return 'info'
default: return 'info'
}
})
const statusText = computed(() => {
if (!flashSale.value) return ''
switch (flashSale.value.status) {
case 'UPCOMING': return '即将开始'
case 'ACTIVE': return '秒杀中'
case 'ENDED': return '已结束'
default: return ''
}
})
const discountPercent = computed(() => {
if (!flashSale.value) return 0
return Math.round((1 - flashSale.value.flashPrice / flashSale.value.originalPrice) * 100)
})
const stockPercent = computed(() => {
if (!flashSale.value || flashSale.value.flashStock === 0) return 0
return Math.round((flashSale.value.remainingStock / flashSale.value.flashStock) * 100)
})
const progressColor = computed(() => {
if (stockPercent.value > 50) return '#67c23a'
if (stockPercent.value > 20) return '#e6a23c'
return '#f56c6c'
})
const endTime = computed(() => {
if (!flashSale.value) return 0
return new Date(flashSale.value.endTime).getTime()
})
const canParticipate = computed(() => {
if (!flashSale.value) return false
return flashSale.value.status === 'ACTIVE' && flashSale.value.remainingStock > 0
})
const buttonText = computed(() => {
if (!flashSale.value) return '立即抢购'
if (flashSale.value.status === 'UPCOMING') return '即将开始'
if (flashSale.value.status === 'ENDED') return '已结束'
if (flashSale.value.remainingStock === 0) return '已售罄'
return '立即抢购'
})
const loadDetail = async () => {
loading.value = true
try {
const res = await flashsaleApi.getDetail(Number(route.params.id))
if (res.success) flashSale.value = res.data
} catch (error) {
console.error('加载秒杀详情失败:', error)
ElMessage.error('加载失败')
} finally {
loading.value = false
}
}
const handleParticipate = async () => {
if (!userStore.isLoggedIn) {
ElMessage.warning('请先登录')
router.push({ path: '/login', query: { redirect: route.fullPath } })
return
}
if (!flashSale.value || !canParticipate.value) return
await ElMessageBox.confirm(`确定要抢购该商品吗?\n秒杀价¥${flashSale.value.flashPrice}`, '抢购确认', {
confirmButtonText: '立即抢购',
cancelButtonText: '取消',
type: 'warning',
})
participating.value = true
try {
const res = await flashsaleApi.participate({ flashSaleId: flashSale.value.id, quantity: 1 })
if (res.success) {
ElMessage.success('抢购成功')
router.push(`/order/${res.data.orderId}`)
}
} catch (error) {
console.error('参与秒杀失败:', error)
} finally {
participating.value = false
}
}
const handleViewProduct = () => {
if (flashSale.value) router.push(`/product/${flashSale.value.productId}`)
}
const handleShare = async () => {
await navigator.clipboard.writeText(window.location.href)
ElMessage.success('链接已复制,快去分享吧')
}
const handleFinish = () => {
loadDetail()
}
onMounted(() => {
loadDetail()
})
</script>
<style scoped lang="scss">
.flashsale-detail-page {
min-height: calc(100vh - 60px);
background-color: #f5f5f5;
}
</style>