560 lines
14 KiB
Vue
560 lines
14 KiB
Vue
<template>
|
||
<DefaultLayout>
|
||
<div class="checkin-page">
|
||
<div class="container">
|
||
<a-row :gutter="[24, 24]">
|
||
<!-- 签到卡片 -->
|
||
<a-col :xs="24" :lg="12">
|
||
<div class="checkin-card chart-card">
|
||
<div class="checkin-header">
|
||
<Icon name="calendar" size="lg" class="header-icon" />
|
||
<span class="header-title">打卡签到</span>
|
||
</div>
|
||
|
||
<div class="checkin-body">
|
||
<!-- 今日签到状态 -->
|
||
<div class="checkin-status">
|
||
<div v-if="!hasCheckedIn" class="status-waiting">
|
||
<div class="status-icon">
|
||
<Icon name="checkCircle" size="xl" />
|
||
</div>
|
||
<div class="status-text">
|
||
<h3>准备签到</h3>
|
||
<p>点击下方按钮完成今日签到</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-else class="status-checked">
|
||
<div class="status-icon">
|
||
<Icon name="checkCircle" size="xl" />
|
||
</div>
|
||
<div class="status-text">
|
||
<h3>今日已签到</h3>
|
||
<p>明天继续保持哦!</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 签到按钮 -->
|
||
<a-button
|
||
v-if="!hasCheckedIn"
|
||
type="primary"
|
||
size="large"
|
||
block
|
||
:loading="checkingIn"
|
||
@click="handleCheckIn"
|
||
class="checkin-button"
|
||
>
|
||
<template #icon><Icon name="check" size="sm" /></template>
|
||
立即签到
|
||
</a-button>
|
||
|
||
<a-button
|
||
v-else
|
||
size="large"
|
||
block
|
||
disabled
|
||
class="checkin-button checked"
|
||
>
|
||
<template #icon><Icon name="checkCircle" size="sm" /></template>
|
||
今日已签到
|
||
</a-button>
|
||
|
||
<!-- 签到统计 -->
|
||
<div class="checkin-stats">
|
||
<div class="stat-item">
|
||
<div class="stat-value">{{ stats.continuousDays }}</div>
|
||
<div class="stat-label">连续签到(天)</div>
|
||
</div>
|
||
<div class="stat-divider"></div>
|
||
<div class="stat-item">
|
||
<div class="stat-value">{{ stats.totalDays }}</div>
|
||
<div class="stat-label">累计签到(天)</div>
|
||
</div>
|
||
<div class="stat-divider"></div>
|
||
<div class="stat-item">
|
||
<div class="stat-value">{{ calculateReward(stats.continuousDays) }}</div>
|
||
<div class="stat-label">今日奖励(积分)</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</a-col>
|
||
|
||
<!-- 奖励规则 -->
|
||
<a-col :xs="24" :lg="12">
|
||
<div class="reward-card chart-card">
|
||
<div class="reward-header">
|
||
<Icon name="trophy" size="lg" class="header-icon" />
|
||
<span class="header-title">奖励规则</span>
|
||
</div>
|
||
|
||
<div class="reward-body">
|
||
<div class="reward-item">
|
||
<div class="reward-icon">
|
||
<Icon name="fire" size="md" />
|
||
</div>
|
||
<div class="reward-content">
|
||
<div class="reward-title">基础奖励</div>
|
||
<div class="reward-desc">每日签到可获得 5 积分</div>
|
||
</div>
|
||
<div class="reward-badge">5 分</div>
|
||
</div>
|
||
|
||
<div class="reward-item highlight">
|
||
<div class="reward-icon">
|
||
<Icon name="rise" size="md" />
|
||
</div>
|
||
<div class="reward-content">
|
||
<div class="reward-title">连续3天</div>
|
||
<div class="reward-desc">奖励提升至 1.5倍</div>
|
||
</div>
|
||
<div class="reward-badge">7-8 分</div>
|
||
</div>
|
||
|
||
<div class="reward-item highlight">
|
||
<div class="reward-icon">
|
||
<Icon name="bolt" size="md" />
|
||
</div>
|
||
<div class="reward-content">
|
||
<div class="reward-title">连续7天</div>
|
||
<div class="reward-desc">奖励提升至 2倍</div>
|
||
</div>
|
||
<div class="reward-badge">10 分</div>
|
||
</div>
|
||
|
||
<div class="reward-item highlight">
|
||
<div class="reward-icon">
|
||
<Icon name="crown" size="md" />
|
||
</div>
|
||
<div class="reward-content">
|
||
<div class="reward-title">连续30天</div>
|
||
<div class="reward-desc">奖励提升至 3倍</div>
|
||
</div>
|
||
<div class="reward-badge">15 分</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</a-col>
|
||
</a-row>
|
||
|
||
<!-- 签到历史 -->
|
||
<div class="history-section">
|
||
<h2 class="section-title">
|
||
<Icon name="history" size="md" />
|
||
签到历史
|
||
</h2>
|
||
|
||
<a-spin :spinning="loadingHistory">
|
||
<div v-if="history.length > 0" class="history-list">
|
||
<div v-for="record in history" :key="record.id" class="history-item">
|
||
<div class="history-date">
|
||
<Icon name="calendar" size="sm" />
|
||
{{ formatDate(record.checkInDate) }}
|
||
</div>
|
||
<div class="history-continuous">
|
||
连续 {{ record.continuousDays }} 天
|
||
</div>
|
||
<div class="history-reward">
|
||
+{{ record.rewardScore }} 积分
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-else class="empty-history">
|
||
<a-empty description="暂无签到记录" />
|
||
</div>
|
||
</a-spin>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</DefaultLayout>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, onMounted } from 'vue'
|
||
import DefaultLayout from '@/layouts/DefaultLayout.vue'
|
||
import { message } from 'ant-design-vue'
|
||
import Icon from '@/components/Icon.vue'
|
||
import {
|
||
checkIn,
|
||
hasCheckedInToday,
|
||
getCheckInHistory,
|
||
getCheckInStats,
|
||
type CheckIn as CheckInRecord,
|
||
type CheckInStats
|
||
} from '@/api/incentive'
|
||
|
||
const checkingIn = ref(false)
|
||
const loadingHistory = ref(false)
|
||
const hasCheckedIn = ref(false)
|
||
const stats = ref<CheckInStats>({
|
||
continuousDays: 0,
|
||
totalDays: 0,
|
||
hasCheckedInToday: false
|
||
})
|
||
const history = ref<CheckInRecord[]>([])
|
||
|
||
// 加载签到状态
|
||
const loadCheckInStatus = async () => {
|
||
try {
|
||
const statsData = await getCheckInStats()
|
||
stats.value = statsData
|
||
hasCheckedIn.value = statsData.hasCheckedInToday
|
||
} catch (error) {
|
||
console.error('加载签到状态失败', error)
|
||
}
|
||
}
|
||
|
||
// 加载签到历史
|
||
const loadHistory = async () => {
|
||
loadingHistory.value = true
|
||
try {
|
||
history.value = await getCheckInHistory(30)
|
||
} catch (error) {
|
||
console.error('加载签到历史失败', error)
|
||
} finally {
|
||
loadingHistory.value = false
|
||
}
|
||
}
|
||
|
||
// 处理签到
|
||
const handleCheckIn = async () => {
|
||
checkingIn.value = true
|
||
try {
|
||
const result = await checkIn()
|
||
|
||
// 显示签到成功消息
|
||
const rewardMsg = `签到成功!连续签到${result.continuousDays}天,获得${result.rewardScore}积分`
|
||
message.success(rewardMsg)
|
||
|
||
// 如果是特殊连续天数,额外提示
|
||
if (result.continuousDays === 7) {
|
||
message.info('连续签到7天,真棒!继续保持!')
|
||
} else if (result.continuousDays === 30) {
|
||
message.info('连续签到30天,你太厉害了!')
|
||
}
|
||
|
||
// 刷新数据
|
||
await Promise.all([loadCheckInStatus(), loadHistory()])
|
||
} catch (error: any) {
|
||
if (error.message) {
|
||
message.error(error.message)
|
||
} else {
|
||
message.error('签到失败')
|
||
}
|
||
} finally {
|
||
checkingIn.value = false
|
||
}
|
||
}
|
||
|
||
// 计算奖励积分
|
||
const calculateReward = (continuousDays: number): number => {
|
||
const baseScore = 5
|
||
if (continuousDays >= 30) {
|
||
return baseScore * 3
|
||
} else if (continuousDays >= 14) {
|
||
return Math.floor(baseScore * 2.5)
|
||
} else if (continuousDays >= 7) {
|
||
return baseScore * 2
|
||
} else if (continuousDays >= 3) {
|
||
return Math.floor(baseScore * 1.5)
|
||
} else {
|
||
return baseScore
|
||
}
|
||
}
|
||
|
||
// 格式化日期
|
||
const formatDate = (dateStr: string) => {
|
||
const date = new Date(dateStr)
|
||
return date.toLocaleDateString('zh-CN', {
|
||
year: 'numeric',
|
||
month: '2-digit',
|
||
day: '2-digit',
|
||
weekday: 'short'
|
||
})
|
||
}
|
||
|
||
onMounted(() => {
|
||
loadCheckInStatus()
|
||
loadHistory()
|
||
})
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.checkin-page {
|
||
min-height: 100vh;
|
||
padding: $space-xl 0;
|
||
background: $bg-primary;
|
||
}
|
||
|
||
.container {
|
||
max-width: $container-max-width;
|
||
margin: 0 auto;
|
||
padding: 0 $space-xl;
|
||
|
||
@media (max-width: $breakpoint-tablet) {
|
||
padding: 0 $space-lg;
|
||
}
|
||
|
||
@media (max-width: $breakpoint-mobile) {
|
||
padding: 0 $space-md;
|
||
}
|
||
}
|
||
|
||
.checkin-card,
|
||
.reward-card {
|
||
background: $bg-card;
|
||
border: 1px solid $border-primary;
|
||
border-radius: $radius-lg;
|
||
overflow: hidden;
|
||
transition: all $transition-base $ease-out;
|
||
|
||
&:hover {
|
||
border-color: $border-color;
|
||
background: $bg-card-hover;
|
||
}
|
||
}
|
||
|
||
.checkin-header,
|
||
.reward-header {
|
||
padding: $space-lg;
|
||
background: rgba(#31f08b, 0.1);
|
||
border-bottom: 1px solid rgba(#31f08b, 0.2);
|
||
color: $text-primary;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: $space-md;
|
||
|
||
.header-icon {
|
||
font-size: 24px;
|
||
}
|
||
|
||
.header-title {
|
||
font-size: $font-size-xl;
|
||
font-weight: $font-weight-bold;
|
||
}
|
||
}
|
||
|
||
.checkin-body {
|
||
padding: $space-2xl;
|
||
}
|
||
|
||
.checkin-status {
|
||
margin-bottom: $space-2xl;
|
||
|
||
.status-waiting,
|
||
.status-checked {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: $space-lg;
|
||
}
|
||
|
||
.status-icon {
|
||
width: 80px;
|
||
height: 80px;
|
||
border-radius: 50%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 40px;
|
||
|
||
.status-waiting & {
|
||
background: rgba(24, 144, 255, 0.1);
|
||
color: $primary-color;
|
||
}
|
||
|
||
.status-checked & {
|
||
background: rgba(82, 196, 26, 0.1);
|
||
color: $color-success;
|
||
}
|
||
}
|
||
|
||
.status-text {
|
||
text-align: center;
|
||
|
||
h3 {
|
||
font-size: $font-size-2xl;
|
||
font-weight: $font-weight-bold;
|
||
color: $text-primary;
|
||
margin-bottom: $space-xs;
|
||
}
|
||
|
||
p {
|
||
font-size: $font-size-base;
|
||
color: $text-secondary;
|
||
}
|
||
}
|
||
}
|
||
|
||
.checkin-button {
|
||
height: 56px;
|
||
font-size: $font-size-lg;
|
||
font-weight: $font-weight-bold;
|
||
border-radius: $radius-md;
|
||
margin-bottom: $space-2xl;
|
||
|
||
&.checked {
|
||
background: #f0f0f0;
|
||
}
|
||
}
|
||
|
||
.checkin-stats {
|
||
display: flex;
|
||
justify-content: space-around;
|
||
align-items: center;
|
||
padding: $space-lg 0;
|
||
|
||
.stat-item {
|
||
text-align: center;
|
||
|
||
.stat-value {
|
||
font-size: $font-size-3xl;
|
||
font-weight: $font-weight-bold;
|
||
color: $primary-color;
|
||
margin-bottom: $space-xs;
|
||
}
|
||
|
||
.stat-label {
|
||
font-size: $font-size-sm;
|
||
color: $text-secondary;
|
||
}
|
||
}
|
||
|
||
.stat-divider {
|
||
width: 1px;
|
||
height: 40px;
|
||
background: $border-color;
|
||
}
|
||
}
|
||
|
||
.reward-body {
|
||
padding: $space-lg;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: $space-md;
|
||
}
|
||
|
||
.reward-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: $space-md;
|
||
padding: $space-lg;
|
||
border-radius: $radius-md;
|
||
background: $bg-tertiary;
|
||
transition: all 0.2s ease-out;
|
||
|
||
&.highlight {
|
||
background: linear-gradient(135deg, rgba(255, 215, 0, 0.1) 0%, rgba(255, 140, 0, 0.1) 100%);
|
||
|
||
&:hover {
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 4px 12px rgba(255, 140, 0, 0.2);
|
||
}
|
||
}
|
||
|
||
.reward-icon {
|
||
width: 40px;
|
||
height: 40px;
|
||
border-radius: 50%;
|
||
background: rgba(#31f08b, 0.1);
|
||
border: 1px solid rgba(#31f08b, 0.3);
|
||
color: #31f08b;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 20px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.reward-content {
|
||
flex: 1;
|
||
|
||
.reward-title {
|
||
font-size: $font-size-base;
|
||
font-weight: $font-weight-bold;
|
||
color: $text-primary;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.reward-desc {
|
||
font-size: $font-size-sm;
|
||
color: $text-secondary;
|
||
}
|
||
}
|
||
|
||
.reward-badge {
|
||
padding: 4px 12px;
|
||
border-radius: $radius-sm;
|
||
background: rgba(#31f08b, 0.1);
|
||
border: 1px solid rgba(#31f08b, 0.3);
|
||
color: #31f08b;
|
||
font-size: $font-size-sm;
|
||
font-weight: $font-weight-bold;
|
||
flex-shrink: 0;
|
||
}
|
||
}
|
||
|
||
.history-section {
|
||
margin-top: $space-3xl;
|
||
}
|
||
|
||
.section-title {
|
||
font-size: $font-size-xl;
|
||
font-weight: $font-weight-semibold;
|
||
color: $text-primary;
|
||
margin-bottom: $space-lg;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: $space-sm;
|
||
}
|
||
|
||
.history-list {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||
gap: $space-md;
|
||
}
|
||
|
||
.history-item {
|
||
background: $bg-secondary;
|
||
border: 1px solid $border-color;
|
||
border-radius: $radius-md;
|
||
padding: $space-lg;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: $space-sm;
|
||
transition: all 0.2s ease-out;
|
||
|
||
&:hover {
|
||
border-color: $primary-color;
|
||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
.history-date {
|
||
font-size: $font-size-base;
|
||
color: $text-primary;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: $space-xs;
|
||
}
|
||
|
||
.history-continuous {
|
||
font-size: $font-size-sm;
|
||
color: $text-secondary;
|
||
}
|
||
|
||
.history-reward {
|
||
font-size: $font-size-base;
|
||
font-weight: $font-weight-bold;
|
||
color: $primary-color;
|
||
}
|
||
}
|
||
|
||
.empty-history {
|
||
padding: $space-3xl 0;
|
||
text-align: center;
|
||
}
|
||
</style>
|
||
|