Files
2024-02-22 18:37:23 +08:00

609 lines
15 KiB
Vue
Raw Permalink 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.

<!--
@name: detail
@author: kahu4
@date: 2024-01-16 16:35
@description拼团详情
@update: 2024-01-16 16:35
-->
<script setup>
import Header from "@/components/Header/index.vue";
import { useScroll } from "@/hooks/useScroll";
import Goods from "@/components/goodsComponents/Goods.vue";
import { onLoad, onShareAppMessage, onShareTimeline } from "@dcloudio/uni-app";
import { computed, onBeforeUnmount, ref } from "vue";
import InviteFriends from "@/components/Share/InviteFriends.vue";
import GroupByPoster from "@/components/Poster/GroupBy.vue";
import { SharePathKey, useShare } from "@/hooks/useShare";
import { useRouter } from "@/hooks/useRouter";
import { getGroupByDetailTeamworkId } from "@/api/goods";
import { getTimeAfterNow } from "@/utils/utils";
import { useMainStore } from "@/store/modules/useMainStore";
import GoodAttrSelect from "@/components/good-attr-select/good-attr-select.vue";
import { getProductDetail } from "@/api/product";
import { useInterface } from "@/hooks/useInterface";
import { getCartAdd } from "@/api/cart";
const {getParams, push} = useRouter();
const {scrollTop} = useScroll()
const mainStore = useMainStore();
const teamworkId = ref()
const {toast} = useInterface();
onLoad((option) => {
const params = getParams(option);
// 邀请进来的
if (params.t && params.t === SharePathKey.GROUP_BY) {
teamworkId.value = params.id
} else {
teamworkId.value = params.teamworkId
}
doGetGroupByDetailTeamworkId()
})
onBeforeUnmount(() => {
stopTimePlay()
})
// 拼团详情
const groupDetail = ref()
// 拼团状态 0 进行中 1 成功 2 失败
const groupState = ref(0)
// 拼团title
const groupTitle = computed(() => {
if (!groupDetail.value) return
if (groupState.value === 0) {
return `再邀${ residuePersonNum.value }位即可拼团成功`
}
if (groupState.value === 1) {
return '拼团成功,请等待商家发货'
}
if (groupState.value === 2) {
return '拼团失败,可以再次开团啊~'
}
})
/**
* 获取拼团详情
* @return {Promise<void>}
*/
async function doGetGroupByDetailTeamworkId() {
const res = await getGroupByDetailTeamworkId({id: teamworkId.value});
groupDetail.value = res
groupDetail.value.teamworkId = teamworkId.value
// 校验状态 state 0 进行中 1 成功 2 失败
groupState.value = res.state
await doGetGoodsDetail()
if (res.state > 0) return
// 设置倒计时
setTimePlay(res.closeTime)
}
// ========================= sku 商品相关 ===========================================
const selectAttrPanel = ref()
const canDoOrder = ref(true) // 是否能立即下单
const goodsDetail = ref() // 商品详情
const selectSku = ref() // 选中的sku
const skuNum = ref(1) // 选中sku下单数量
/**
* 获取商品详情
* 请求回来给sku选择器使用
*/
async function doGetGoodsDetail() {
goodsDetail.value = await getProductDetail({productId: groupDetail.value.id, skuId: groupDetail.value.skuId});
canDoOrder.value = true
groupByInvitationShare({...groupDetail.value, cartInfo: [{productInfo: {image: groupDetail.value.image}}]});
const {productValue} = goodsDetail.value
for (const skuName in productValue) {
const sku = productValue[skuName]
if (sku.id !== groupDetail.value.skuId) continue;
selectSku.value = sku
break;
}
// 没有sku
if (!selectSku.value) {
toast({title: '此规格下架了~看看其他商品吧~'})
canDoOrder.value = false
return
}
// sku没有库存
if (!selectSku.value.campaignStock || selectSku.value <= 0) {
toast({title: '此规格库存不足~看看其他商品吧~'})
canDoOrder.value = false
return
}
canDoOrder.value = true
}
/**
* sku选择器confirm
* @param attr
*/
function handleSelectAttr(attr) {
const {store, num} = attr
// 与拼团商品sku不一致
if (store.id !== groupDetail.value.skuId) {
toast({title: '检测到您选择的规格和好友下单拼团规格不一致~请重新选择', time: 3000})
return
}
// 下单逻辑
selectSku.value = store
skuNum.value = num
handleBuy()
}
/**
* 下单
* @param orderType 1、普通下单2、商品活动下单
* @return {Promise<void>}
*/
const handleBuy = async (orderType = 2) => {
uni.showLoading({
title: "加载中",
});
let res = await getCartAdd({
orderType,
cartNum: skuNum.value,
productId: selectSku.value.productId,
uniqueId: selectSku.value.unique,
new: 1,
teamworkId: groupDetail.value.teamworkId,
});
uni.hideLoading();
const data = handleSubmitParams(orderType, res)
push(
{url: "/pages/submitOrder/submitOrder"},
{data}
);
};
/**
* 处理跳转参数
* @param orderType
* @param cardRes 下单购物车返回信息
* @return {{orderType, campaignDetailId: any, campaignType: any, cartId, teamworkType: number}|{orderType, cartId}}
*/
const handleSubmitParams = (orderType, cardRes) => {
let data = {
cartId: cardRes.cartId,
orderType
}
// 活动
if (orderType === 2) {
data.campaignType = selectSku.value.campaignType
data.campaignDetailId = selectSku.value.campaignDetailId
data.teamworkType = 2 // 1开团 2拼团
data.teamworkId = groupDetail.value.teamworkId
}
return data
}
// ========================= 倒计时相关 ===========================================
let timeObj = ref({}), time
const timestamp = ref(0)
/**
* 开始倒计时
* @param closeTime
*/
function setTimePlay(closeTime) {
const nowTime = Date.now()
timestamp.value = closeTime
if (timestamp.value === 0 || closeTime - nowTime <= 0) return
const setTime = () => {
timeObj.value = getTimeAfterNow(timestamp.value)
if (closeTime - nowTime <= 0) {
// 倒计时结束
stopTimePlay()
timeOver()
}
}
setTime()
time = setInterval(() => setTime(), 1000)
}
/**
* 停止倒计时
*/
function stopTimePlay() {
time && clearInterval(time)
time = undefined
timestamp.value = 0
}
/**
* 倒计时结束
*/
function timeOver() {
doGetGroupByDetailTeamworkId()
}
/**
* 还差多少人成团
* @type {ComputedRef<number|*>}
*/
const residuePersonNum = computed(() => {
if (!groupDetail.value) return 1
return groupDetail.value.person - groupDetail.value.users.length
})
/**
* 当前用户是否已经购买
* @type {ComputedRef<unknown>}
*/
const isBuy = computed(() => {
if (!groupDetail.value) return false
return groupDetail.value.users.findIndex(item => item.uid === mainStore.user.id) > -1
})
const groupRules = [
{
index: 1,
info: '开团或成团享团购价'
},
{
index: 2,
info: '邀请好友参与优惠多'
},
{
index: 3,
info: '人满发货不满退款'
}
]
/**
* 立即拼团
*/
function takePartInAGroup() {
if (!canDoOrder.value) return toast({title: "此规格不能下单,可以看看其他上哦~"})
selectAttrPanel.value.open()
}
// ============================ 邀请 ==================================================
const {shareInfo, groupByInvitationShare, shareH5, shareAppMessage, shareTimeline} = useShare()
onShareAppMessage(shareAppMessage)
onShareTimeline(shareTimeline)
const groupByPosterRef = ref()
const inviteFriendShareRef = ref()
function inviteFriend() {
if (groupState.value === 2) return
inviteFriendShareRef.value.open()
}
function againGroup() {
push({
url: '/pages/goodsDetail/goodsDetail',
}, {
data: {id: groupDetail.value.id, "skuId": groupDetail.value.skuId},
type: 'redirectTo'
})
}
function handleShareConfirm(shareItem) {
if (shareItem.value === 'wechat') {
shareH5()
} else {
groupByPosterRef.value.open(groupDetail.value, shareInfo.value)
}
}
</script>
<template>
<view
class="group-by-detail"
>
<Header :scroll-top="scrollTop"> 拼团详情</Header>
<view
class="main"
v-if="groupDetail">
<!-- 商品信息 -->
<view
class="goods-row row">
<Goods
:goods="groupDetail"
:ratio="true"
imgWidth="200rpx"
infoPadding="20rpx 20rpx"
row
>
<template #options="{goods}">
<span class="price">
{{ goods.price }}
</span>
</template>
</Goods>
</view>
<!-- 拼团信息 -->
<view class="row group-info">
<view class="title">
{{ groupTitle }}
</view>
<view
class="time"
v-if="timeObj && groupState===0">
剩余
<view class="time-group">
<text class="time-item">{{ timeObj.hours }}</text>
<text class="primary-color">:</text>
<text class="time-item">{{ timeObj.minutes }}</text>
<text class="primary-color">:</text>
<text class="time-item">{{ timeObj.seconds }}</text>
</view>
结束
</view>
<view class="users">
<view
v-for="user in groupDetail.users"
:key="user.id"
class="user-item">
<image :src="user.avatar" />
<view
class="first-group"
v-if="user.isHead === '1'">
团长
</view>
</view>
<!-- 该团还需要几个人 -->
<view
class="user-item plus"
v-for="item in residuePersonNum"
:key="item"
@click="inviteFriend">
<uv-icon name="plus" />
</view>
</view>
<!-- 如果自己已经在团内 -->
<template v-if="isBuy">
<view
v-if="groupState === 0"
class="btn animation-button"
@click="inviteFriend">
邀请好友拼团
</view>
<view
v-if="[1,2].includes(groupState)"
class="btn animation-button"
@click="againGroup">
再次拼团
</view>
</template>
<!-- 如果自己不在团内 -->
<template v-else>
<view
v-if="[1,2].includes(groupState)"
class="btn animation-button"
@click="againGroup">
再次拼团
</view>
<view
v-else
class="btn animation-button"
@click="takePartInAGroup">
立即参团
</view>
</template>
</view>
<!-- 拼团玩法 -->
<view class="group-rule group-info row">
<view class="title">
拼团玩法
</view>
<view class="rule-box">
<view
v-for="item in groupRules"
:key="item.index"
class="rule-item">
<view class="circle-number">
{{ item.index }}
</view>
<view class="info">
{{ item.info }}
</view>
<view class="line"></view>
</view>
</view>
</view>
</view>
<!-- SKU选择器 -->
<GoodAttrSelect
v-if="goodsDetail && selectSku"
ref="selectAttrPanel"
:goods-detail="goodsDetail"
:sku-id="selectSku.id"
@select="handleSelectAttr" />
<!-- 邀请好友 -->
<InviteFriends
ref="inviteFriendShareRef"
@share="handleShareConfirm" />
<GroupByPoster ref="groupByPosterRef" />
</view>
</template>
<style lang="scss">
page {
background: $page-bg-color;
}
</style>
<style
lang="scss"
scoped>
.group-by-detail {
width: 100%;
.main {
width: 100%;
@include usePadding(30, 30);
.row {
width: 100%;
background: $white-color;
border-radius: 20rpx;
margin-bottom: 25rpx;
}
.price {
color: $primary-color;
font-size: 30rpx;
}
.group-info {
@include usePadding(30, 30);
width: 100%;
.title, .time {
@include useFlex(center, center)
}
.title {
font-weight: bold;
font-size: 32rpx;
}
.time {
margin-top: 15rpx;
font-size: 24rpx;
@include useFlex(center, center);
.time-group {
margin: 0 15rpx;
@include useFlex(center, center);
.time-item {
@include usePadding(10, 6);
background: #FDEFEA;
border: 1rpx solid #E85A2B;
color: #E85A2B;
border-radius: 5rpx;
}
.primary-color {
color: #E85A2B;
padding: 0 5rpx;
font-size: 32rpx;
}
}
}
.users {
margin: 40rpx 0;
@include useFlex(center, center, row, wrap, 20rpx);
width: 100%;
.user-item {
width: 130rpx;
height: 130rpx;
border-radius: 50%;
position: relative;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 0 10rpx rgba(220, 220, 220, 0.7);
image {
width: 100%;
height: 100%;
border-radius: 50%;
background: #e5e5e5;
}
.first-group {
@include usePadding(16, 5);
background: $primary-color;
color: $white-color;
white-space: nowrap;
border-radius: 34rpx;
display: flex;
align-items: center;
justify-content: center;
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
font-size: 24rpx;
}
}
}
.plus {
transition: all .3s;
&:active {
scale: 1.1;
}
}
.btn {
margin-top: 10rpx;
display: flex;
align-items: center;
justify-content: center;
height: 80rpx;
border-radius: 10rpx;
}
}
.group-rule {
width: 100%;
.rule-box {
@include usePadding(0, 30);
@include useFlex(space-between, center);
font-size: 20rpx;
color: $tips-color;
.rule-item {
position: relative;
@include useFlex(center, center, column);
.circle-number {
@include useFlex(center, center);
width: 50rpx;
height: 50rpx;
color: #000;
border-radius: 50%;
background: #f5f5f5;
margin: 16rpx 0;
}
&:after {
content: '';
position: absolute;
width: 100%;
height: 5rpx;
background: #f5f5f5;
top: 35%;
left: 65%;
}
&:last-child:after {
content: '';
position: absolute;
width: 100%;
height: 0;
background: #f5f5f5;
top: 35%;
left: 60%;
}
}
}
}
}
}
.success-color {
color: #E85A2B;
}
</style>