Commit 0519d87a by lijiabin

【需求 20520】 feat: 完成前台大转盘功能

parent feb96717
<template>
<div class="lucky-wheel-wrapper">
<div class="lucky-wheel-wrapper" :class="{ 'scale-80': smallerThanXl }">
<LuckyWheel
ref="myLucky"
width="260px"
height="260px"
:blocks="blocks"
:buttons="buttons"
:prizes="prizes"
:prizes="computedPrizes"
:default-config="defaultConfig"
:default-style="defaultStyle"
@start="startCallback"
@end="endCallback"
/>
<!-- 自定义指针 -->
......@@ -21,7 +20,7 @@
class="go-btn"
:class="{ 'is-spinning': isSpinning }"
:disabled="isSpinning"
@click="startCallback"
@click="popClick"
>
<span class="go-text">GO</span>
</button>
......@@ -29,11 +28,22 @@
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import { LuckyWheel } from '@lucky-canvas/vue'
import ringTexture from '@/assets/img/lucky-wheel-outer-ring.svg'
import type { WheelPrizeItemDto, LuckWheelResultDto } from '@/api'
import { participateLuckyWheel, getWheelPrizeList } from '@/api'
import { breakpointsTailwind, useBreakpoints } from '@vueuse/core'
import { useMessageBox } from '@/hooks'
const { confirm } = useMessageBox()
const breakpoints = useBreakpoints(breakpointsTailwind)
const smallerThanXl = breakpoints.smaller('xl')
const wheelPrizeList = ref<WheelPrizeItemDto[]>([])
const emit = defineEmits<{
(e: 'prize', prize: Record<string, unknown>): void
handlePrizeResult: [LuckWheelResultDto]
}>()
const myLucky = ref<InstanceType<typeof LuckyWheel>>()
......@@ -47,9 +57,9 @@ const defaultConfig = {
const defaultStyle = {
fontColor: '#333',
fontSize: '12px',
fontWeight: 'bold',
lineHeight: '14px',
lengthLimit: '60%',
}
const blocks = [
......@@ -69,102 +79,37 @@ const blocks = [
{ padding: '3px', background: '#7b6fef' },
]
const prizes = [
{
background: '#ffffff',
fonts: [{ text: '谢谢参与', top: '12%', fontSize: '11px', fontColor: '#6858ec' }],
imgs: [
{
src: 'https://cdn.jsdelivr.net/gh/twitter/twemoji@14.0.2/assets/72x72/1f60a.png',
width: '30%',
top: '38%',
},
],
},
{
background: '#f3f0ff',
fonts: [{ text: '10个京豆', top: '12%', fontSize: '11px', fontColor: '#e5a012' }],
imgs: [
{
src: 'https://cdn.jsdelivr.net/gh/twitter/twemoji@14.0.2/assets/72x72/1f61d.png',
width: '30%',
top: '38%',
},
],
},
{
background: '#ffffff',
fonts: [{ text: '5个京豆', top: '12%', fontSize: '11px', fontColor: '#e5a012' }],
imgs: [
{
src: 'https://cdn.jsdelivr.net/gh/twitter/twemoji@14.0.2/assets/72x72/1f929.png',
width: '30%',
top: '38%',
},
],
},
{
background: '#f3f0ff',
fonts: [{ text: '1个京豆', top: '12%', fontSize: '11px', fontColor: '#e5a012' }],
imgs: [
{
src: 'https://cdn.jsdelivr.net/gh/twitter/twemoji@14.0.2/assets/72x72/1f60e.png',
width: '30%',
top: '38%',
},
],
},
{
background: '#ffffff',
fonts: [{ text: '谢谢参与', top: '12%', fontSize: '11px', fontColor: '#6858ec' }],
imgs: [
{
src: 'https://cdn.jsdelivr.net/gh/twitter/twemoji@14.0.2/assets/72x72/1f385.png',
width: '30%',
top: '38%',
},
],
},
{
background: '#f3f0ff',
fonts: [{ text: '10个京豆', top: '12%', fontSize: '11px', fontColor: '#e5a012' }],
imgs: [
{
src: 'https://cdn.jsdelivr.net/gh/twitter/twemoji@14.0.2/assets/72x72/1f917.png',
width: '30%',
top: '38%',
},
],
},
{
background: '#ffffff',
fonts: [{ text: '5个京豆', top: '12%', fontSize: '11px', fontColor: '#e5a012' }],
imgs: [
{
src: 'https://cdn.jsdelivr.net/gh/twitter/twemoji@14.0.2/assets/72x72/1f618.png',
width: '30%',
top: '38%',
},
],
},
{
background: '#f3f0ff',
fonts: [{ text: '1个京豆', top: '12%', fontSize: '11px', fontColor: '#e5a012' }],
imgs: [
{
src: 'https://cdn.jsdelivr.net/gh/twitter/twemoji@14.0.2/assets/72x72/1f970.png',
width: '30%',
top: '38%',
},
],
},
]
const computedPrizes = computed(() => {
const width = wheelPrizeList.value.length >= 6 ? '37%' : '30%'
return wheelPrizeList.value.map((item: WheelPrizeItemDto, index: number) => {
return {
background: index % 2 === 0 ? '#ffffff' : '#f3f0ff',
fonts: [
{
text: item.name,
top: '8%',
fontSize: '10px',
fontColor: index % 2 === 0 ? '#6858ec' : '#e5a012',
lineClamp: 1,
},
],
imgs: [
{
src: item.imageUrl,
width,
top: '35%',
},
],
id: item.id,
}
})
})
const buttons = [
{ radius: '30%', background: '#fce4e4' },
{ radius: '25%', background: '#f75a5a' },
{ radius: '25%', background: '#fce4e4' },
{ radius: '20%', background: '#f75a5a' },
{
radius: '22%',
radius: '18%',
background: '#e63939',
pointer: false,
fonts: [{ text: '', top: '-14px' }],
......@@ -173,20 +118,70 @@ const buttons = [
const isSpinning = ref(false)
const startCallback = () => {
let resultPrize: null | LuckWheelResultDto = null
const startCallback = async () => {
if (isSpinning.value) return
isSpinning.value = true
myLucky.value?.play()
setTimeout(async () => {
const { data } = await participateLuckyWheel()
const idx = computedPrizes.value.findIndex((item) => item.id === data.prizeId)
myLucky.value?.stop(idx)
resultPrize = data
}, 1000)
}
// 获取最新的奖品列表 对比 是否更新了
const popClick = async () => {
if (isSpinning.value) return
const { data } = await getWheelPrizeList()
const newWheelPrizeList = data
const oldWheelPrizeList = wheelPrizeList.value
// 暂时只需要对比 1长度不一致 需要更新 2如果长度一样 如果有一组的名字或者图片 不一样 需要更新
let shouldUpdate = false
if (newWheelPrizeList.length !== oldWheelPrizeList.length) {
shouldUpdate = true
} else {
newWheelPrizeList.forEach((item: WheelPrizeItemDto, index: number) => {
if (
item.name !== oldWheelPrizeList[index]?.name ||
item.imageUrl !== oldWheelPrizeList[index]?.imageUrl
) {
shouldUpdate = true
}
})
}
if (shouldUpdate) {
// 给用户提示 奖池更新了 请重新点击
await confirm({
title: '检测到后台奖池有更新',
message: '请重新点击按钮开始抽奖',
type: 'warning',
showCancelButton: false,
})
wheelPrizeList.value = newWheelPrizeList
} else {
startCallback()
}
}
const endCallback = () => {
setTimeout(() => {
const index = Math.floor(Math.random() * prizes.length)
myLucky.value?.stop(index)
}, 3000)
isSpinning.value = false
emit('handlePrizeResult', resultPrize as LuckWheelResultDto)
}, 500)
}
const endCallback = (prize: Record<string, unknown>) => {
isSpinning.value = false
emit('prize', prize)
const initWheelPrizeList = async () => {
const { data } = await getWheelPrizeList()
wheelPrizeList.value = data
}
onMounted(() => {
initWheelPrizeList()
})
</script>
<style scoped>
......@@ -206,8 +201,8 @@ const endCallback = (prize: Record<string, unknown>) => {
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 60px;
height: 60px;
width: 50px;
height: 50px;
border-radius: 50%;
border: 3px solid #ff8a80;
background: linear-gradient(145deg, #ff5252, #d32f2f);
......
......@@ -66,7 +66,10 @@
</p>
<!-- Actions -->
<div class="mt-5 grid grid-cols-2 gap-2.5">
<div
class="mt-5 grid grid-cols-2 gap-2.5"
:class="{ 'grid-cols-1!': !showCancelButton || !showConfirmButton }"
>
<button
v-if="showCancelButton"
type="button"
......
......@@ -71,7 +71,7 @@
</div>
</div>
</div>
<div class="right flex-col gap-3 flex basis-1/4 xl:basis-1/4 min-w-0">
<div class="right flex-col gap-3 basis-1/4 xl:basis-1/4 min-w-0 hidden sm:flex">
<!-- 等级等相关信息 -->
<div
ref="levelContainerRef"
......@@ -350,14 +350,20 @@
</div>
<!-- 大转盘 -->
<div class="lottery-container common-box rounded-lg bg-#F5F0FF">
<div class="flex items-center gap-2 mb-4">
<div
v-if="wheelConfig?.isActivityActive"
class="lottery-container common-box rounded-lg bg-#F5F0FF"
>
<div class="flex items-center gap-2 xl:mb-4">
<div class="w-1 h-4 bg-gradient-to-b from-violet-500 to-purple-500 rounded-full"></div>
<h1 class="text-sm sm:text-base font-bold">大转盘</h1>
</div>
<div class="flex justify-center">
<LuckyWheel />
</div>
<div class="flex items-center justify-center text-sm text-gray-500 xl:mt-4 px-1 truncate">
每次抽奖{{ wheelConfig?.costYaCoin }} YA币
</div>
</div>
</div>
</div>
......@@ -384,6 +390,7 @@ import {
getRecordData,
getUserDailyLotteryInfo,
userJoinLottery,
getWheelConfig,
} from '@/api'
import { TaskTypeEnum, TaskDateLimitTypeText, ArticleTypeEnum } from '@/constants'
import type {
......@@ -392,6 +399,7 @@ import type {
UserAccountDataDto,
UserRecordDataDto,
DailyLotteryInfo,
WheelConfigDto,
} from '@/api'
import { TABS_REF_KEY, levelListOptions } from '@/constants'
import { useScrollTop } from '@/hooks'
......@@ -532,6 +540,11 @@ const handleLottery = async () => {
push.success('参与每日抽奖成功!')
}
/**
* 大转盘
*/
const wheelConfig = ref<WheelConfigDto>(null)
const handleTask = async (item: TaskItemDto) => {
if (item.currentCount === item.limitCount) return
// 先暂时写死
......@@ -584,7 +597,8 @@ const initPage = () => {
getUserAccountData(),
getRecordData(),
getUserDailyLotteryInfo(),
]).then(([r1, r2, r3, r4]) => {
getWheelConfig(),
]).then(([r1, r2, r3, r4, r5]) => {
if (r1.status === 'fulfilled') {
carouselList.value = r1.value.data
}
......@@ -598,6 +612,9 @@ const initPage = () => {
if (r4.status === 'fulfilled') {
lotteryPrizesDetail.value = r4.value.data
}
if (r5.status === 'fulfilled') {
wheelConfig.value = r5.value.data
}
})
}
......@@ -623,6 +640,7 @@ onActivated(async () => {
refreshTaskData(false)
refreshUserAccountData()
getLotteryPrizesDetail()
if (route.fullPath.includes('#levelContainerRef')) {
await handleBackTop()
open.value = true
......
......@@ -66,10 +66,10 @@
</template>
<script lang="tsx" setup>
import { getSelfAuctionRecord, getUserLotteryRecordList } from '@/api'
import { getSelfAuctionRecord, getUserLotteryRecordList, getUserWheelRecordList } from '@/api'
import { usePageSearch } from '@/hooks'
import { ActivityTypeEnum } from '@/constants/enums'
import type { UserLotteryRecordItemDto } from '@/api/dailyLottery/types'
import type { UserLotteryRecordItemDto, UserWheelRecordItemDto } from '@/api'
const EmptyComp = () => (
<div class="flex flex-col items-center justify-center h-64">
......@@ -182,6 +182,56 @@ const activityTypeListOptions = [
),
refresh: () => refresh2(),
},
{
label: '大转盘',
value: ActivityTypeEnum.WHEEL,
component: () => (
<>
{!list3.value.length ? (
<EmptyComp />
) : (
<>
<div class="space-y-4">
<el-table height="500" data={list3.value} stripe border loading={loading3.value}>
<el-table-column prop="prizeName" label="名称">
{({ row }: { row: UserWheelRecordItemDto }) => (
<div>{row.prizeName ?? `谢谢参与(${row.blessingText})`}</div>
)}
</el-table-column>
<el-table-column prop="createdAtStr" label="参与时间" />
<el-table-column prop="isLotteryDone" label="是否中奖">
{({ row }: { row: UserWheelRecordItemDto }) => (
<div>
{row.isWin ? (
<span class="text-green-500"></span>
) : (
<span class="text-red-500"></span>
)}
</div>
)}
</el-table-column>
</el-table>
</div>
<div class="flex items-center justify-end px-6 py-4 border-t border-gray-200">
<div class="pagination-wrapper bg-white rounded-lg shadow-sm border border-gray-100 p-2">
<el-pagination
v-model:current-page={searchParams3.value.current}
v-model:page-size={searchParams3.value.size}
onSizeChange={changePageSize3}
onCurrentChange={goToPage3}
page-sizes={[10, 20, 30, 40]}
layout="prev, pager, next, jumper, total"
total={total3.value}
class="custom-pagination"
/>
</div>
</div>
</>
)}
</>
),
refresh: () => refresh3(),
},
]
const tab = ref(activityTypeListOptions[0]!.value)
......@@ -212,7 +262,17 @@ const {
} = usePageSearch(getUserLotteryRecordList, {
immediate: false,
})
const {
list: list3,
loading: loading3,
searchParams: searchParams3,
total: total3,
refresh: refresh3,
goToPage: goToPage3,
changePageSize: changePageSize3,
} = usePageSearch(getUserWheelRecordList, {
immediate: false,
})
const refresh = () => {
activityTypeListOptions.find((item) => item.value === tab.value)?.refresh?.()
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment