Files
lilishop-ui/buyer/src/pages/promotion/seckill.vue
2026-11-04 14:51:13 +08:00

399 lines
9.1 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="seckill-list">
<BaseHeader></BaseHeader>
<Search />
<!-- 秒杀时间段 -->
<div class="promotion-decorate">限时秒杀</div>
<ul class="time-line">
<template v-for="(time, index) in list">
<li v-if="index < 5" @click="currIndex = index" :key="index" :class="{ currTimeline: currIndex === index }">
<div>{{ time.timeLine + ":00" }}</div>
<div v-if="currIndex === index">
<p>{{ nowHour >= time.timeLine ? "秒杀中" : "即将开始" }}</p>
<p>
{{ nowHour >= time.timeLine ? "距结束" : "距开始" }}&nbsp;{{
currTime
}}
</p>
</div>
<div v-else class="not-curr">
{{ nowHour >= time.timeLine ? "秒杀中" : "即将开始" }}
</div>
</li>
</template>
</ul>
<!-- 秒杀商品列表 -->
<div class="seckill-goods-wrap">
<div class="goods-list">
<empty v-if="goodsList.length === 0" />
<div v-else class="goods-show-info1" v-for="(item, index) in goodsList" :key="index"
@click="goGoodsDetail(item.skuId, item.goodsId)">
<div class="goods-show-img">
<img :src="item.goodsImage" loading="lazy" decoding="async" :alt="item.goodsName" />
</div>
<div class="goods-show-price">
<span class="seckill-price text-danger">{{ $filters.unitPrice(item.price, "¥") }}</span>
<span class="origin-price">{{ $filters.unitPrice(item.originalPrice, "¥") }}</span>
</div>
<div class="goods-show-detail">
<span>{{ item.goodsName }}</span>
</div>
<div v-if="nowHour >= list[currIndex].timeLine" class="goods-show-num1">
<span>已售{{ salePercent(item) }}%</span>
<el-progress
:show-text="false"
:color="'var(--theme-color, #ff3c2a)'"
:percentage="salePercent(item)"
/>
</div>
<div class="goods-show-seller">
<span>{{ item.storeName }}</span>
</div>
<div
class="goods-seckill-btn"
:class="{ 'is-disabled': !canBuy(item) }"
>
{{ buyText(item) }}
</div>
</div>
</div>
</div>
<BaseFooter />
</div>
</template>
<script setup>
import { onBeforeUnmount, onMounted, ref, watch } from "vue";
import { useRouter } from "vue-router";
import { seckillByDay } from "@/api/promotion";
const router = useRouter();
const list = ref([]); // 秒杀时段列表
const goodsList = ref([]); // 商品列表
let interval = null; // 定时器
const currIndex = ref(0); // 当前时间段的下标
const currTime = ref(0); // 当前显示的倒计时
const diffSeconds = ref(0); // 倒计时时间戳
const nowHour = ref(new Date().getHours()); // 当前小时数
watch(currIndex, (val) => {
clearInterval(interval);
interval = null;
nowHour.value = new Date().getHours();
countDown(val);
goodsList.value = list.value[val]?.seckillGoodsList || [];
});
watch(diffSeconds, (val) => {
const hours = Math.floor(val / 3600);
// 当前秒数 / 60,向下取整
// 获取到所有分钟数 3600 / 60 = 60分钟
// 对60取模,超过小时数的分钟数
const minutes = Math.floor(val / 60) % 60;
// 当前的秒数 % 60,获取到 超过小时数、分钟数的秒数(秒数)
const seconds = val % 60;
currTime.value =
filteTime(hours) + ":" + filteTime(minutes) + ":" + filteTime(seconds);
if (val <= 0) {
clearInterval(interval);
interval = null;
}
function filteTime (time) {
if (time < 10) {
return "0" + time;
} else {
return time;
}
}
});
function getListByDay () {
// 当天秒杀活动;result 可能为空数组(当天无活动),需判空避免取下标 0 报错
seckillByDay().then((res) => {
if (res.success && res.result && res.result.length) {
list.value = res.result;
goodsList.value = list.value[0].seckillGoodsList || [];
countDown(currIndex.value);
}
});
}
function goGoodsDetail (skuId, goodsId) {
// 跳转商品详情
let routeUrl = router.resolve({
path: "/goodsDetail",
query: { skuId, goodsId },
});
window.open(routeUrl.href, "_blank");
}
// 已售占比。没有库存时按售罄展示
function salePercent (item) {
if (!item.quantity || item.quantity <= 0) return 100;
return Math.min(100, Math.ceil((item.salesNum / item.quantity) * 100));
}
// 场次已开始,且销量达到库存
function isSoldOut (item) {
return nowHour.value >= list.value[currIndex.value].timeLine && item.quantity <= item.salesNum;
}
// 场次已开始且未售罄才可抢
function canBuy (item) {
return nowHour.value >= list.value[currIndex.value].timeLine && !isSoldOut(item);
}
// 按钮文案:未开始 / 已售罄 / 立即抢购
function buyText (item) {
if (nowHour.value < list.value[currIndex.value].timeLine) return "即将开始";
if (isSoldOut(item)) return "已售罄";
return "立即抢购";
}
function countDown (currIndex) {
// 倒计时
// 0点时间戳
let zeroTime = new Date(new Date().toLocaleDateString()).getTime();
let currTime = new Date().getTime();
let actTime = 0;
let nowHour = new Date().getHours(); // 当前小时数
if (list.value[currIndex].timeLine > nowHour) {
// 活动未开始
actTime = zeroTime + list.value[currIndex].timeLine * 3600 * 1000;
} else if (list.value[currIndex].timeLine <= nowHour) {
// 活动进行中
if (currIndex === list.value.length - 1) {
// 如果是最后一个活动,直到24点结束
actTime = zeroTime + 24 * 3600 * 1000;
} else {
actTime = zeroTime + list.value[currIndex + 1].timeLine * 3600 * 1000;
}
}
diffSeconds.value = Math.floor((actTime - currTime) / 1000);
interval = setInterval(() => {
diffSeconds.value--;
}, 1000);
}
onMounted(() => {
getListByDay();
});
onBeforeUnmount(() => {
clearInterval(interval);
});
</script>
<style lang="scss" scoped>
@import "../../assets/styles/goodsList.scss";
.seckill-list {
background: #f5f5f5;
}
.promotion-decorate {
width: auto;
margin: 28px auto 16px;
color: #171A26;
font-size: 22px;
font-weight: 600;
&::before,
&::after {
display: none;
}
}
.time-line {
width: 1200px;
max-width: 1200px;
height: auto;
margin: 0 auto;
background: #fff;
display: flex;
box-sizing: border-box;
border-radius: 8px;
overflow: hidden;
li {
flex: 1;
width: auto;
min-height: 78px;
padding: 12px 20px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
gap: 4px;
font-size: 18px;
font-weight: 600;
color: #171A26;
box-sizing: border-box;
& + li {
box-shadow: inset 1px 0 0 #f2f2f2;
}
&:hover {
cursor: pointer;
background: #fafafa;
}
p {
margin: 0;
font-size: 13px;
font-weight: 400;
line-height: 18px;
}
.not-curr {
border: none;
border-radius: 0;
padding: 0;
margin: 0;
font-size: 13px;
font-weight: 400;
color: #999;
}
}
.currTimeline {
background: $theme_color;
color: #fff;
&:hover {
background: $theme_color;
}
> div:nth-child(1) {
font-size: 22px;
line-height: 28px;
}
> div:nth-child(2) {
margin-left: 0;
}
.not-curr {
color: #fff;
}
}
}
.seckill-goods-wrap {
width: 1200px;
max-width: 1200px;
margin: 16px auto 40px;
padding: 16px;
box-sizing: border-box;
background: #fff;
border-radius: 8px;
min-height: 320px;
.goods-list {
width: 100%;
margin: 0;
column-gap: 16px;
}
:deep(.empty) {
margin: 48px 0;
color: #999;
}
}
.goods-show-info1 {
width: calc(25% - 12px);
min-width: 0;
padding: 12px;
margin: 0 0 16px;
position: relative;
border: 1px solid #f0f0f0;
border-radius: 8px;
cursor: pointer;
background-color: #fff;
box-sizing: border-box;
display: flex;
flex-direction: column;
&:hover {
border-color: #eee;
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.06);
}
}
.goods-show-img {
display: flex;
justify-content: center;
border-radius: 6px;
overflow: hidden;
background: #fafafa;
img {
width: 100%;
height: auto;
aspect-ratio: 1;
object-fit: cover;
}
}
.goods-show-price {
display: flex;
align-items: baseline;
gap: 8px;
margin-top: 10px;
}
.origin-price {
color: #999;
font-size: 13px;
text-decoration: line-through;
}
.goods-show-detail {
height: 40px;
margin: 8px 0;
color: #333;
font-size: 14px;
line-height: 20px;
}
.goods-show-num1 {
display: flex;
align-items: center;
gap: 8px;
font-size: 12px;
margin-bottom: 8px;
color: #666;
span {
flex-shrink: 0;
}
:deep(.el-progress) {
flex: 1;
}
}
.goods-show-seller {
margin-bottom: 10px;
color: #999;
font-size: 13px;
}
.goods-seckill-btn {
margin-top: auto;
height: 34px;
border-radius: 4px;
color: #fff;
text-align: center;
line-height: 34px;
font-size: 14px;
background-color: $theme_color;
&.is-disabled {
background: #c8c8c8;
}
}
</style>