You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

345 lines
8.8 KiB
Vue

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.

<template>
<el-dialog :model-value="modelValue" @update:model-value="onDialogUpdate" :show-close="false"
:close-on-click-modal="true" align-center append-to-body class="lottery-dialog" @closed="onClosed">
<div class="lottery-main">
<div class="lottery-header">
<h2 class="lottery-title">积分抽奖</h2>
<p class="lottery-subtitle">顺时针转动高亮 · 停在哪格就领对应奖励</p>
</div>
<div class="lottery-grid">
<template v-for="(item, idx) in gridItems" :key="idx">
<!-- -->
<div v-if="item.type === 'draw'" class="lottery-cell lottery-cell--draw"
:class="{ 'is-disabled': isDrawing || !drawable, 'is-drawed': !drawable }" @click="startDraw">
</div>
<!-- 普通积分格 -->
<div v-else class="lottery-cell" :class="{
'is-active': activeIndex === idx && isDrawing,
'is-winner': winnerIndex === idx && !isDrawing,
}">
<span class="points-value">{{ item.points }}积分</span>
</div>
</template>
</div>
</div>
</el-dialog>
</template>
<script setup>
import { computed, ref, watch } from "vue";
import { ElMessage } from "element-plus";
import { draw, getPoints } from "@/service/modular/hospital";
import { useRewardResultStore } from "@/stores/rewardResult";
import iconLottery from "@/assets/image/hospital/ponits/icon-lottery-large.svg";
const props = defineProps({
modelValue: { type: Boolean, default: false },
// 是否允许抽奖(外部判断是否已抽过等)
drawable: { type: Boolean, default: true },
// 批量任务相关(后端字段未确定,使用 props 传入)
batchGoal: { type: Number, default: 50 },
batchCurrent: { type: Number, default: 5 },
});
const emit = defineEmits([
"update:modelValue",
"success",
"failed",
"viewAccount",
]);
const unwrap = (res) => res?.data ?? res ?? {};
const rewardResultStore = useRewardResultStore();
// ============ 网格布局(中心是抽奖按钮) ============
const gridItems = [
{ type: "points", points: 1 },
{ type: "points", points: 2 },
{ type: "points", points: 3 },
{ type: "points", points: 4 },
{ type: "draw" }, // 中心
{ type: "points", points: 5 },
{ type: "points", points: 6 },
{ type: "points", points: 7 },
{ type: "points", points: 8 },
];
// 可抽奖格子的索引(顺时针顺序,不含中心)
const drawableIndices = [0, 1, 2, 5, 8, 7, 6, 3];
const isDrawing = ref(false);
const activeIndex = ref(-1);
const winnerIndex = ref(-1);
const winningPoints = ref(0);
const wait = (ms) => new Promise((r) => setTimeout(r, ms));
// ============ 找到积分对应的格子索引 ============
const findIndexByPoints = (points) => {
return gridItems.findIndex(
(item) => item.type === "points" && item.points === points,
);
};
// ============ 取最新积分(用于结果区进度条) ============
const fetchLatestPoints = async () => {
try {
const res = unwrap(await getPoints());
return Number(res?.totalPoints) || 0;
} catch (_) {
return 0;
}
};
// ============ 开始抽奖 ============
const startDraw = async () => {
if (isDrawing.value) return;
if (!props.drawable) {
ElMessage.info("今日已抽奖,明天再来吧!");
return;
}
// 重置状态
isDrawing.value = true;
winnerIndex.value = -1;
winningPoints.value = 0;
activeIndex.value = -1;
// 启动接口请求
const apiPromise = draw();
let apiResult = null;
let apiError = null;
apiPromise
.then((r) => {
apiResult = unwrap(r);
})
.catch((e) => {
apiError = e;
});
const startTime = Date.now();
const MIN_DURATION = 2000; // 至少 2 秒
let step = 0;
// ========== 第一阶段:高速循环,至少 2 秒 ==========
while (Date.now() - startTime < MIN_DURATION) {
activeIndex.value = drawableIndices[step % drawableIndices.length];
step++;
const elapsed = Date.now() - startTime;
// 渐进减速
let delay = 70;
if (elapsed > 600) delay = 100;
if (elapsed > 1100) delay = 140;
if (elapsed > 1600) delay = 180;
await wait(delay);
}
// ========== 第二阶段:等待接口 + 慢速循环 ==========
let waited = 0;
while (apiResult === null && apiError === null && waited < 3000) {
activeIndex.value = drawableIndices[step % drawableIndices.length];
step++;
waited += 250;
await wait(250);
}
if (apiError || apiResult === null || apiResult === undefined) {
isDrawing.value = false;
ElMessage.error(apiError?.message || "抽奖失败");
emit("failed", apiError);
return;
}
// 解析中奖分数(兼容多种返回结构)
const points = Number(
apiResult.points ?? apiResult.score ?? apiResult ?? 0,
);
// 找积分对应的格子
const targetIdx = findIndexByPoints(points);
// ========== 第三阶段:从当前位置逐步走到目标 ==========
let safety = 0;
while (activeIndex.value !== targetIdx && safety < 20) {
activeIndex.value = drawableIndices[step % drawableIndices.length];
step++;
safety++;
await wait(150);
}
// 命中目标
activeIndex.value = targetIdx;
winnerIndex.value = targetIdx;
winningPoints.value = points;
// 短暂停顿后:关闭抽奖弹窗 -> 打开全局"奖励结果"弹窗
await wait(500);
isDrawing.value = false;
// 取最新积分用于结果区进度条
const totalPoints = await fetchLatestPoints();
// 关闭抽奖弹窗,再开全局结果弹窗(放在外面不会被 overflow 隐藏)
emit("update:modelValue", false);
// 下一帧再 open 避免和抽奖弹窗关闭动画叠加
setTimeout(() => {
rewardResultStore.open({
title: "恭喜抽中",
icon: iconLottery,
points,
batchGoal: 500,
batchCurrent: totalPoints,
rewardName: "康小虎 AI 吉祥物",
});
}, 100);
emit("success", { points });
};
const onDialogUpdate = (val) => {
if (!val && isDrawing.value) {
ElMessage.warning("抽奖进行中,请稍候...");
return;
}
emit("update:modelValue", val);
};
const onClosed = () => {
// 关闭后重置所有状态
isDrawing.value = false;
activeIndex.value = -1;
winnerIndex.value = -1;
winningPoints.value = 0;
};
</script>
<style lang="scss">
// keyframes
@keyframes winnerPulse {
0% {
transform: scale(0.95);
}
50% {
transform: scale(1.08);
}
100% {
transform: scale(1);
}
}
/* ============ .lottery-dialog ============ */
.lottery-dialog {
// ===== =====
overflow: hidden !important;
padding: 0 !important;
width: 638px !important;
height: 766px !important;
background: transparent !important;
box-shadow: none !important;
transform: scale(0.75) !important;
.el-dialog__header {
display: none;
}
.el-dialog__body {
padding: 0;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
// ===== =====
.lottery-main {
padding: 86px 82px 54px 74px;
width: 100%;
height: 100%;
background: url(@/assets/image/hospital/ponits/card-bg-decoration.svg) no-repeat center center;
background-size: 100% 100%;
}
.lottery-header {
padding: 18px 2px;
text-align: center;
margin-bottom: 28px;
height: 115px;
.lottery-title {
font-weight: 900;
font-size: 38px;
color: #FF3950;
}
.lottery-subtitle {
font-weight: 400;
font-size: 19px;
color: #666666;
}
}
.lottery-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 7px;
}
.lottery-cell {
aspect-ratio: 1 / 1;
border-radius: 20px;
background: #FFFFFF;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
font-size: 20px;
color: #1D2129;
transition: all 0.18s ease;
user-select: none;
&.is-active {
background: url(@/assets/image/hospital/ponits/reward-purple-frame.svg) no-repeat center center;
background-size: 100% 100%;
color: #FFFFFF;
transform: scale(1.04);
}
&.is-winner {
background: url(@/assets/image/hospital/ponits/reward-purple-frame.svg) no-repeat center 10px;
background-size: 100% 100%;
color: #FFFFFF;
animation: winnerPulse 0.6s ease;
}
&--draw {
background: url(@/assets/image/hospital/ponits/reward-fudai-active.svg) no-repeat;
background-size: 104% 104%;
background-position: center 3px;
&:hover:not(.is-disabled) {
transform: translateY(-2px);
}
&.is-disabled {
cursor: not-allowed;
transform: none;
}
&.is-drawed {
background: url(@/assets/image/hospital/ponits/reward-fudai-pending.svg) no-repeat;
background-size: 104% 104%;
background-position: center 3px;
}
}
}
}
</style>