|
|
<template>
|
|
|
<el-dialog :model-value="modelValue" @update:model-value="onDialogUpdate" :show-close="false"
|
|
|
:close-on-click-modal="true" align-center append-to-body class="signin-dialog" @closed="onClosed">
|
|
|
<div class="signin-card">
|
|
|
<!-- ============ 头部:标题 / 副标题 / 装饰图 ============ -->
|
|
|
<div class="signin-header">
|
|
|
<h3 class="signin-title" :class="{ 'is-signed': signedIn }">
|
|
|
<template v-if="signedIn">
|
|
|
连续签到<span class="title-num">{{ streakDays }}</span>天
|
|
|
<template v-if="rank > 0">排名第<span class="title-num">{{ rank }}名</span></template>
|
|
|
<template v-else>暂无排名</template>
|
|
|
</template>
|
|
|
<template v-else>
|
|
|
每日签到 <span class="title-num">领积分</span>
|
|
|
</template>
|
|
|
</h3>
|
|
|
<p class="signin-subtitle">
|
|
|
<template v-if="signedIn">
|
|
|
当前积分: <span class="title-num">{{ totalPoints }}</span>分
|
|
|
<template v-if="beatPercent > 0">您已击败全国{{ beatPercent }}%的医院</template>
|
|
|
<template v-else>暂未上榜</template>
|
|
|
</template>
|
|
|
<template v-else>
|
|
|
坚持签到,积分兑好礼
|
|
|
</template>
|
|
|
</p>
|
|
|
</div>
|
|
|
|
|
|
<div class="main-view">
|
|
|
<!-- ============ 7 天签到网格 ============ -->
|
|
|
<div class="signin-grid">
|
|
|
<div v-for="(day, idx) in days" :key="idx" class="signin-day" :class="{
|
|
|
'is-today': day.isToday,
|
|
|
'is-signed': day.signed,
|
|
|
'is-future': !day.signed && !day.isToday,
|
|
|
}">
|
|
|
<div class="day-top">
|
|
|
<!-- 上方:第N天 -->
|
|
|
<div class="day-label" v-if="!day.isToday">第{{ day.dayIdx }}天</div>
|
|
|
<div class="day-label" v-else>今天</div>
|
|
|
<!-- 中间:图标 -->
|
|
|
<div class="day-icon">
|
|
|
<img :src="checkIcon" class="day-check" alt="" />
|
|
|
</div>
|
|
|
<div class="day-desc">+1</div>
|
|
|
</div>
|
|
|
|
|
|
<!-- 下方:积分 / 日期 -->
|
|
|
<div class="day-bottom">
|
|
|
<img class="day-signedin" v-if="day.signed && !day.isToday"
|
|
|
src="@/assets/image/hospital/ponits/signedin.svg" alt="">
|
|
|
<img class="day-signedin-today" v-if="day.signed && day.isToday"
|
|
|
src="@/assets/image/hospital/ponits/sinedin-today.svg" alt="">
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<!-- ============ 底部按钮 ============ -->
|
|
|
<button class="signin-btn" :class="{ 'is-signed': signedIn, 'is-loading': submitting }"
|
|
|
:disabled="signedIn || submitting" @click="handleSignIn">
|
|
|
<template v-if="signedIn">签到成功</template>
|
|
|
<template v-else>签到</template>
|
|
|
</button>
|
|
|
</div>
|
|
|
</div>
|
|
|
</el-dialog>
|
|
|
|
|
|
<!-- 签到成功 → 今日未抽奖则弹出抽奖弹窗(抽过则不再弹,避免重复打扰) -->
|
|
|
<LotteryDialog
|
|
|
v-model="lotteryVisible"
|
|
|
:drawable="!hasDrawn"
|
|
|
:batch-goal="500"
|
|
|
:batch-current="0"
|
|
|
@success="onLotterySuccess"
|
|
|
/>
|
|
|
</template>
|
|
|
|
|
|
<script setup>
|
|
|
import { computed, ref, watch } from "vue";
|
|
|
import dayjs from "dayjs";
|
|
|
import { ElMessage } from "element-plus";
|
|
|
import { signIn, getPoints } from "@/service/modular/hospital";
|
|
|
import { useSignInDialogStore } from "@/stores/signInDialog";
|
|
|
import { useRewardResultStore } from "@/stores/rewardResult";
|
|
|
import LotteryDialog from "@/views/hospital/LotteryDialog.vue";
|
|
|
import checkIcon from "@/assets/image/hospital/ponits/coin.svg";
|
|
|
import iconSign from "@/assets/image/hospital/ponits/icon-sign-large.svg";
|
|
|
|
|
|
const props = defineProps({
|
|
|
modelValue: { type: Boolean, default: false },
|
|
|
// 是否已签到(控制顶部文案变化 + 按钮禁用)
|
|
|
signedIn: { type: Boolean, default: false },
|
|
|
// 当前可用积分(已签到时展示)
|
|
|
totalPoints: { type: Number, default: 0 },
|
|
|
// 连续签到天数(接口字段:consecutiveSignInDays)
|
|
|
consecutiveSignInDays: { type: Number, default: 0 },
|
|
|
// 连续签到排名(接口字段:signInRank)
|
|
|
signInRank: { type: Number, default: 0 },
|
|
|
// 击败医院百分比 0-100,0 表示未上榜(接口字段:beatPercent)
|
|
|
beatPercent: { type: Number, default: 0 },
|
|
|
// 今日是否已抽奖(来自 summary.isDraw);未抽过奖时签到成功会顺带弹抽奖
|
|
|
hasDrawn: { type: Boolean, default: false },
|
|
|
});
|
|
|
|
|
|
const emit = defineEmits(["update:modelValue", "success"]);
|
|
|
|
|
|
const unwrap = (res) => res?.data ?? res ?? {};
|
|
|
|
|
|
// 顶部展示用的本地变量(与 props 保持同步,方便响应式消费)
|
|
|
const streakDays = computed(() => Number(props.consecutiveSignInDays) || 0);
|
|
|
const rank = computed(() => Number(props.signInRank) || 0);
|
|
|
const beatPercent = computed(() => {
|
|
|
const n = Number(props.beatPercent) || 0;
|
|
|
// 0 表示未上榜
|
|
|
return n > 0 ? n : 0;
|
|
|
});
|
|
|
|
|
|
// ============ 根据连续签到天数动态生成 7 天 ============
|
|
|
// 规则:
|
|
|
// N = consecutiveSignInDays
|
|
|
// T = 是否今天已签到
|
|
|
// displayN = T ? N : N + 1 // 今天未签到时,把"今天"作为第 N+1 天拼到序列里
|
|
|
// startIdx = max(1, displayN - 6) // displayN <= 7 从第 1 天起排,> 7 截取最后 7 天
|
|
|
// 第 k 天 (k = startIdx .. startIdx+6) 已签到 ⇔ k <= N
|
|
|
const buildSignInDays = (consecutiveDays, signedToday) => {
|
|
|
const today = dayjs().startOf("day");
|
|
|
const N = Math.max(0, Number(consecutiveDays) || 0);
|
|
|
const displayN = signedToday ? N : N + 1;
|
|
|
const startIdx = Math.max(1, displayN - 6);
|
|
|
|
|
|
return Array.from({ length: 7 }, (_, i) => {
|
|
|
const dayIdx = startIdx + i;
|
|
|
const offset = displayN - dayIdx; // 距离今天的天数
|
|
|
const d = today.subtract(offset, "day");
|
|
|
const isToday = dayIdx === displayN;
|
|
|
const signed = dayIdx <= N;
|
|
|
return {
|
|
|
date: d.format("MM-DD"),
|
|
|
signed,
|
|
|
isToday,
|
|
|
dayIdx,
|
|
|
};
|
|
|
});
|
|
|
};
|
|
|
|
|
|
const days = ref(
|
|
|
buildSignInDays(props.consecutiveSignInDays, props.signedIn),
|
|
|
);
|
|
|
|
|
|
// 父组件更新 summary 后,重新生成 7 天
|
|
|
watch(
|
|
|
() => [props.consecutiveSignInDays, props.signedIn],
|
|
|
([n, signed]) => {
|
|
|
days.value = buildSignInDays(n, signed);
|
|
|
},
|
|
|
);
|
|
|
|
|
|
const submitting = ref(false);
|
|
|
const lotteryVisible = ref(false);
|
|
|
const signInDialogStore = useSignInDialogStore();
|
|
|
const rewardResultStore = useRewardResultStore();
|
|
|
|
|
|
const onDialogUpdate = (val) => {
|
|
|
if (submitting.value) return;
|
|
|
emit("update:modelValue", val);
|
|
|
};
|
|
|
|
|
|
const onClosed = () => {
|
|
|
submitting.value = false;
|
|
|
};
|
|
|
|
|
|
// 取最新积分(用于签到成功结果区进度条)
|
|
|
const fetchLatestPoints = async () => {
|
|
|
try {
|
|
|
const res = unwrap(await getPoints());
|
|
|
return Number(res?.totalPoints) || 0;
|
|
|
} catch (_) {
|
|
|
return 0;
|
|
|
}
|
|
|
};
|
|
|
|
|
|
// 抽奖成功后:刷新 summary(让 hasDrawn/isDraw 等状态同步)
|
|
|
const onLotterySuccess = async () => {
|
|
|
await signInDialogStore.refreshSummary();
|
|
|
};
|
|
|
|
|
|
// ============ 签到 ============
|
|
|
const handleSignIn = async () => {
|
|
|
if (props.signedIn) {
|
|
|
ElMessage.info("今日已签到,明天再来吧!");
|
|
|
return;
|
|
|
}
|
|
|
submitting.value = true;
|
|
|
try {
|
|
|
await signIn({ remark: "" });
|
|
|
ElMessage.success("签到成功! +1 积分 🎉");
|
|
|
// 重新生成 7 天(刷新签到状态)
|
|
|
days.value = buildSignInDays(props.consecutiveSignInDays, props.signedIn);
|
|
|
|
|
|
// 取最新积分,用于签到成功结果区进度条 / 后续场景
|
|
|
const totalPoints = await fetchLatestPoints();
|
|
|
|
|
|
// 先关闭签到弹窗(在父层),再下一帧按是否抽过奖选择弹哪个反馈
|
|
|
emit("update:modelValue", false);
|
|
|
setTimeout(() => {
|
|
|
if (!props.hasDrawn) {
|
|
|
// 未抽过奖 → 弹抽奖弹窗(抽完后内部会自己弹奖励结果)
|
|
|
lotteryVisible.value = true;
|
|
|
} else {
|
|
|
// 已抽过奖 → 弹签到成功的奖励结果(原注释里那块的复用)
|
|
|
// rewardResultStore.open({
|
|
|
// title: "签到成功",
|
|
|
// icon: iconSign,
|
|
|
// points: 1,
|
|
|
// batchGoal: 500,
|
|
|
// batchCurrent: totalPoints,
|
|
|
// rewardName: "康小虎 AI 吉祥物",
|
|
|
// });
|
|
|
}
|
|
|
}, 100);
|
|
|
|
|
|
// 通知外层 signInDialogStore 触发 onSigned 回调(同步刷新 summary)
|
|
|
signInDialogStore.fireSigned();
|
|
|
|
|
|
emit("success");
|
|
|
} catch (e) {
|
|
|
ElMessage.error(e?.message || "签到失败");
|
|
|
} finally {
|
|
|
submitting.value = false;
|
|
|
}
|
|
|
};
|
|
|
</script>
|
|
|
|
|
|
<style lang="scss">
|
|
|
/* ============ 所有样式都嵌套在 .signin-dialog 下 ============ */
|
|
|
.signin-dialog {
|
|
|
min-width: 502px !important;
|
|
|
padding: 0 !important;
|
|
|
background: transparent !important;
|
|
|
box-shadow: none !important;
|
|
|
overflow: hidden !important;
|
|
|
|
|
|
.el-dialog__header {
|
|
|
display: none;
|
|
|
}
|
|
|
|
|
|
.el-dialog__body {
|
|
|
padding: 0;
|
|
|
width: 100%;
|
|
|
height: 100%;
|
|
|
}
|
|
|
|
|
|
// ===== 卡片外壳 =====
|
|
|
.signin-card {
|
|
|
min-width: 502px;
|
|
|
height: 380px;
|
|
|
position: relative;
|
|
|
border-radius: 24px;
|
|
|
box-shadow: 0px 4px 4px 0px rgba(0, 0, 0, 0.06);
|
|
|
padding: 0px;
|
|
|
box-sizing: border-box;
|
|
|
background: url(@/assets/image/hospital/ponits/signin-bg.svg) no-repeat center center;
|
|
|
background-size: 103% auto;
|
|
|
}
|
|
|
|
|
|
// ===== 头部 =====
|
|
|
.signin-header {
|
|
|
height: 148px;
|
|
|
position: relative;
|
|
|
padding: 40px 24px 10px;
|
|
|
|
|
|
.signin-title {
|
|
|
font-weight: 600;
|
|
|
font-size: 24px;
|
|
|
color: #1D2129;
|
|
|
line-height: 36px;
|
|
|
|
|
|
&.is-signed {
|
|
|
font-size: 20px;
|
|
|
}
|
|
|
|
|
|
.title-num {
|
|
|
color: #FF8D28;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.signin-subtitle {
|
|
|
margin-top: 4px;
|
|
|
font-weight: normal;
|
|
|
font-size: 14px;
|
|
|
color: #666666;
|
|
|
|
|
|
.title-num {
|
|
|
color: #FF8D28;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.signin-deco {
|
|
|
position: absolute;
|
|
|
right: 0;
|
|
|
top: 0;
|
|
|
width: 84px;
|
|
|
height: 64px;
|
|
|
object-fit: contain;
|
|
|
pointer-events: none;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.main-view {
|
|
|
overflow: hidden;
|
|
|
width: 100%;
|
|
|
padding: 28px 18px;
|
|
|
background: #FFFFFF;
|
|
|
border-radius: 9px 9px 20px 20px;
|
|
|
transform: translateY(-15px);
|
|
|
}
|
|
|
|
|
|
// ===== 7 天网格 =====
|
|
|
.signin-grid {
|
|
|
display: grid;
|
|
|
grid-template-columns: repeat(7, 1fr);
|
|
|
gap: 10px;
|
|
|
margin-bottom: 36px;
|
|
|
}
|
|
|
|
|
|
.signin-day {
|
|
|
cursor: pointer;
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
align-items: center;
|
|
|
|
|
|
border-radius: 8px;
|
|
|
border: 1px solid #F3F3F3;
|
|
|
transition: all 0.2s;
|
|
|
min-height: 104px;
|
|
|
background: #F8F8FA;
|
|
|
font-weight: normal;
|
|
|
font-size: 12px;
|
|
|
color: #333333;
|
|
|
|
|
|
// 今日(未签到)
|
|
|
&.is-today {
|
|
|
background: #FF8D28;
|
|
|
color: #FFFFFF;
|
|
|
border: none;
|
|
|
|
|
|
.day-top {
|
|
|
border: none;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 已签到
|
|
|
&.is-signed {
|
|
|
.day-top {
|
|
|
border: none;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 未签到(不是今天)
|
|
|
&.is-future {
|
|
|
.day-icon {}
|
|
|
}
|
|
|
|
|
|
.day-top {
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
align-items: center;
|
|
|
width: 100%;
|
|
|
padding: 9px 13px 5px;
|
|
|
border-radius: 8px;
|
|
|
border-bottom: 1px solid #F3F3F3;
|
|
|
}
|
|
|
|
|
|
.day-label {
|
|
|
white-space: nowrap;
|
|
|
}
|
|
|
|
|
|
.day-icon {
|
|
|
width: 30px;
|
|
|
height: 30px;
|
|
|
margin: 1px auto 4px;
|
|
|
|
|
|
.day-check {
|
|
|
width: 30px;
|
|
|
height: 30px;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.day-desc {
|
|
|
margin-bottom: 6px;
|
|
|
}
|
|
|
|
|
|
.day-bottom {
|
|
|
width: 16px;
|
|
|
height: 16px;
|
|
|
margin-bottom: 5px;
|
|
|
|
|
|
img {
|
|
|
width: 16px;
|
|
|
height: 16px;
|
|
|
object-fit: contain;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.day-points-text {
|
|
|
color: #BCBCBC;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// ===== 底部按钮 =====
|
|
|
.signin-btn {
|
|
|
display: block;
|
|
|
width: 268px;
|
|
|
height: 42px;
|
|
|
border: none;
|
|
|
border-radius: 21px;
|
|
|
background: #FF8D28;
|
|
|
color: #FFFFFF;
|
|
|
font-weight: normal;
|
|
|
font-size: 16px;
|
|
|
color: #FFFFFF;
|
|
|
cursor: pointer;
|
|
|
transition: all 0.2s;
|
|
|
line-height: 42px;
|
|
|
text-align: center;
|
|
|
margin: 0 auto;
|
|
|
|
|
|
&:hover:not(:disabled) {
|
|
|
transform: translateY(-1px);
|
|
|
box-shadow: 0 6px 14px rgba(255, 138, 40, 0.4);
|
|
|
}
|
|
|
|
|
|
&:disabled {
|
|
|
cursor: not-allowed;
|
|
|
}
|
|
|
|
|
|
&.is-signed {
|
|
|
background: #e0e0e0;
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
</style>
|