You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
shop-pc/pages/seckill/index.vue

456 lines
12 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<!--
* @Author: ch
* @Date: 2022-05-04 17:25:41
* @LastEditors: ch
* @LastEditTime: 2022-05-04 17:26:08
* @Description: file content
-->
<template>
<div class="seckill">
<div v-show="isSticky" class="seckill-header-sticky">
<TabBar
:tab-id="query.activityTimeId"
:list="tabList"
@tab-change="handleTabChange"
/>
</div>
<!-- 秒杀时间段 -->
<div
class="seckill-header"
:style="{ backgroundImage: `url(${bkgSckill})` }"
>
<div class="seckill-header-tabbar">
<TabBar
:tab-id="query.activityTimeId"
:list="tabList"
@tab-change="handleTabChange"
/>
</div>
</div>
<!-- 倒计时展示 -->
<div
v-if="tabList.length > 0"
class="seckill-bar flex flex-middle flex-center"
>
<p>{{ seckillTip }}</p>
<div class="seckill-bar-countdown flex flex-middle">
<div class="seckill-bar-countdown__time">{{ countdown.hour }}</div>
<span class="seckill-bar-countdown--mark">:</span>
<div class="seckill-bar-countdown__time">{{ countdown.minute }}</div>
<span class="seckill-bar-countdown--mark">:</span>
<div class="seckill-bar-countdown__time">{{ countdown.second }}</div>
</div>
</div>
<!-- 秒杀商品列表 -->
<div v-loading="loading">
<div class="seckill-products flex flex-wrap">
<div
v-for="item in goodsList"
:key="item.productId"
@click="onJumpGoodsDetail(item.productId)"
class="seckill-products-wrap"
>
<div class="seckill-products-wrap__cover">
<img :src="item.productMainPicture" />
<div
v-if="item.percentage === 100"
class="products-wrap-cover__sold flex flex-middle flex-center"
>
已售罄
</div>
</div>
<div class="seckill-products-wrap__content">
<p class="products-wrap-content__title">{{ item.productName }}</p>
<div class="products-wrap-content__price">
<strong>¥{{ item.activityPrice }}</strong>
<del>¥{{ item.originalPrice }}</del>
</div>
<div
class="products-wrap-content__footer flex flex-middle flex-between"
>
<div class="wrap-content-footer__stock">
<el-progress
:percentage="item.percentage"
:show-text="false"
></el-progress>
<p>仅剩{{ item.stock }}件</p>
</div>
<div
v-if="item.percentage === 100"
class="wrap-content-footer__btn wrap-content-footer__btn--gray"
>
已售罄
</div>
<div v-else class="wrap-content-footer__btn">
{{ goodsBtnText }}
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 分页 -->
<div class="seckill-pagination flex flex-right">
<UiPagination
:current-page="currentPage"
:page-size="query.length"
:total="total"
@current-change="handleCurrentChange"
>
<el-button class="btn-confirm"></el-button>
</UiPagination>
</div>
<DialogEnd :visible="seckillEndVisible" @close="handleClose" />
</div>
</template>
<script>
import UiPagination from "@/components/UiPagination.vue";
import {
ApiGetSeckillTimes,
ApiGetSeckillGoods,
ApiGetCurrentTime,
} from "@/plugins/api/seckill";
import { SECKILL_STATUS } from "@/constants";
import TabBar from "./module/TabBar.vue";
import DialogEnd from "./module/DialogEnd.vue";
const PAGE_SIZE = 16;
export default {
name: "Sckill",
components: { TabBar, DialogEnd, UiPagination },
data() {
return {
bkgSckill: require("~/assets/img/sckill/bkg-large.png"),
tabList: [], // 秒杀时间段
goodsList: [], // 商品列表
total: 0,
currentPage: 0,
loading: false,
query: {
pageIndex: 1,
length: PAGE_SIZE,
activityTimeId: 0,
},
isSticky: false,
timeInterval: null, // 倒计时定时器
seckillTip: "", // 秒杀提示文案
seckillTime: 0, // 秒杀时间
seckillEndVisible: false, //秒杀结束弹窗
seckillStatus: SECKILL_STATUS.NOT_START, // 当前秒杀时间段状态
inProgressId: 0, // 秒杀中的活动id
};
},
computed: {
countdown() {
const data = parseInt(this.seckillTime / 1e3);
const hour = Math.floor(data / 60 / 60);
const minute = Math.floor((data / 60) % 60);
const second = Math.floor(data % 60);
return {
hour: hour < 10 ? `0${hour}` : `${hour}`,
minute: minute < 10 ? `0${minute}` : `${minute}`,
second: second < 10 ? `0${second}` : `${second}`,
};
},
goodsBtnText() {
return this.query.activityTimeId === this.inProgressId
? "立即开抢"
: "即将开抢";
},
},
watch: {
// 切换秒杀时间段
"query.activityTimeId": {
immediate: true,
handler(val) {
if (val === 0) {
return;
}
clearImmediate(this.timeInterval);
const tabItem = this.tabList.find((item) => item.id === val);
if (tabItem) {
const { activityStartTime, activityEndTime } = tabItem;
this.setTimerInterval({
startTime: activityStartTime,
endTime: activityEndTime,
});
}
},
},
},
created() {
this.getSeckillTimes();
},
mounted() {
// 监听滚动事件
window.addEventListener("scroll", this.scrollEventMethod);
},
destroyed() {
window.removeEventListener("scroll", this.scrollEventMethod);
clearInterval(this.timeInterval);
},
methods: {
scrollEventMethod() {
this.isSticky = window.scrollY > 400;
},
async getSeckillTimes() {
const { result } = await ApiGetSeckillTimes();
if (result && result.length > 0) {
this.tabList = result;
const inProgressItem = result.find((item) => item.isInProgress);
if (inProgressItem) {
this.inProgressId = inProgressItem.id;
this.query.activityTimeId = inProgressItem.id;
} else {
this.query.activityTimeId = result[0].id;
}
this.getGoodsList();
return;
}
this.seckillEndVisible = true;
},
async getGoodsList() {
this.loading = true;
const { result } = await ApiGetSeckillGoods({ ...this.query });
this.loading = false;
if (result) {
this.total = result.total;
this.goodsList = result.records.map((item) => {
// 售出数量
const saleNumber = item.number - item.stock;
return {
...item,
percentage:
saleNumber === 0
? 0
: (saleNumber / item.number).toFixed(3) * 100,
};
});
}
},
// 设置定时器
async setTimerInterval({ startTime, endTime }) {
clearInterval(this.timeInterval);
// 获取服务器当前时间
const { result } = await ApiGetCurrentTime();
const current = result || Date.now();
startTime = new Date(startTime).getTime();
endTime = new Date(endTime).getTime();
if (current > endTime) {
// 秒杀结束
this.getSeckillTimes();
this.queryDataReset();
return;
}
if (startTime < current && endTime > current) {
// 秒杀进行中
this.seckillTime = endTime - current;
this.seckillTip = "本场正在秒杀中,好物转瞬即逝,不要错过哦~距结束仅剩";
this.seckillStatus = SECKILL_STATUS.GOING;
} else {
// 秒杀未开始
this.seckillTime = startTime - current;
this.seckillTip = "本场秒杀即将开抢,距开始还剩";
this.seckillStatus = SECKILL_STATUS.NOT_START;
}
this.timeInterval = setInterval(() => {
if (this.seckillTime <= 0) {
clearInterval(this.timeInterval);
// 仅有一个进行中的秒杀时间段,倒计时完成展示结束弹窗
if (
this.tabList.length === 1 &&
this.seckillStatus === SECKILL_STATUS.GOING
) {
this.seckillEndVisible = true;
this.$store.commit("setSeckillTabVisible", false);
return;
}
// 秒杀开始activityTimeId重置触发watch
this.query.activityTimeId = 0;
this.getSeckillTimes();
return;
}
this.seckillTime -= 1e3;
}, 1e3);
},
// 重置分页等数据
queryDataReset() {
Object.assign(this.query, {
pageIndex: 1,
length: PAGE_SIZE,
});
},
onJumpGoodsDetail(id) {
window.open(`${location.origin}/goods/detail/${id}`);
},
handleCurrentChange(page) {
this.query.pageIndex = page;
this.getGoodsList();
},
handleTabChange(id) {
this.query.activityTimeId = id;
this.queryDataReset();
this.getGoodsList();
},
// 离开活动页面
handleClose() {
this.$router.push("/");
},
},
};
</script>
<style lang="scss" scoped>
.seckill {
background: #f8f8f8;
padding-bottom: 60px;
.seckill-header-sticky {
position: fixed;
left: 50%;
transform: translate(-50%);
top: 50px;
z-index: 1;
@include layout-box;
}
.seckill-header {
position: relative;
width: 100%;
height: 156px;
background-size: 100% 100%;
.seckill-header-tabbar {
@include layout-box;
position: absolute;
left: 50%;
bottom: 0;
transform: translate(-50%);
}
}
.seckill-bar {
height: 60px;
line-height: 60px;
text-align: center;
font-size: 14px;
color: #666666;
.seckill-bar-countdown {
margin-left: 25px;
font-weight: bold;
&__time {
width: 22px;
height: 22px;
line-height: 22px;
text-align: center;
font-size: 14px;
color: #ffffff;
background: #2f3430;
}
&--mark {
display: block;
margin: 0 7px;
color: #2f3430;
}
}
}
.seckill-products,
.seckill-pagination {
@include layout-box;
}
.seckill-products {
.seckill-products-wrap {
width: 24%;
background: #ffffff;
margin-bottom: 16px;
cursor: pointer;
&:not(:nth-child(4n)) {
margin-right: calc(4% / 3);
}
&:hover {
box-shadow: 0px 0px 10px 1px rgba(0, 0, 0, 0.1);
}
.seckill-products-wrap__cover {
position: relative;
width: 100%;
height: 288px;
padding: 20px;
img {
width: 100%;
height: 100%;
}
.products-wrap-cover__sold {
position: absolute;
width: 110px;
height: 110px;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
background: rgba(0, 0, 0, 0.5);
font-size: 24px;
color: #ffffff;
border-radius: 50%;
}
}
.seckill-products-wrap__content {
padding: 20px 16px;
.products-wrap-content__title {
@include ellipsis;
color: #333333;
font-size: 16px;
margin-bottom: 10px;
}
.products-wrap-content__price {
font-size: 12px;
color: #999999;
margin-bottom: 7px;
strong {
font-size: 18px;
color: #ff512b;
margin-right: 14px;
}
}
.products-wrap-content__footer {
.wrap-content-footer__stock {
font-size: 12px;
color: #666666;
/deep/.el-progress {
width: 100px;
height: 6px;
background: #dddddd;
border-radius: 4px;
margin-bottom: 8px;
}
}
.wrap-content-footer__btn {
width: 100px;
height: 36px;
line-height: 36px;
text-align: center;
background: #ff512b;
font-size: 14px;
color: #ffffff;
&--gray {
color: #999999;
background: #eeeeee;
}
}
}
}
}
}
.seckill-pagination {
.btn-confirm {
width: 81px;
height: 32px;
border-radius: 2px;
margin-left: 14px;
}
}
}
</style>