|
|
<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>
|