- 优化通用组件:导航栏、页脚、图片上传、搜索 - 完善业务组件:商品卡片、秒杀卡片 - 更新用户端页面:首页、商品、秒杀、订单、购物车、个人中心 - 新增用户收藏页面 - 完善管理后台:仪表盘、商品/订单/用户/秒杀管理 - 新增管理后台:收藏管理、评价管理、系统监控页面
249 lines
8.8 KiB
Vue
249 lines
8.8 KiB
Vue
<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>
|