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.

443 lines
12 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="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-1000 表示未上榜接口字段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>