560 lines
14 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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