508 lines
13 KiB
Vue
508 lines
13 KiB
Vue
<template>
|
||
<DefaultLayout>
|
||
<div class="incentive-center">
|
||
<div class="container">
|
||
<!-- 页面标题 -->
|
||
<div class="page-header">
|
||
<h1 class="page-title">
|
||
<Icon name="sparkles" size="xl" color="#FFD700" style="margin-right: 12px;" />
|
||
激励中心
|
||
</h1>
|
||
<p class="page-desc">参与活动、达成目标、赢取奖励!</p>
|
||
</div>
|
||
|
||
<!-- 我的积分卡片 -->
|
||
<div class="score-card">
|
||
<div class="score-main">
|
||
<div class="score-label">我的积分</div>
|
||
<div class="score-value">{{ userScore }}</div>
|
||
</div>
|
||
<div class="score-actions">
|
||
<div class="score-stats">
|
||
<div class="stat">
|
||
<span class="stat-value">{{ continuousCheckIn }}</span>
|
||
<span class="stat-label">连续签到</span>
|
||
</div>
|
||
<div class="stat">
|
||
<span class="stat-value">{{ achievementCount }}</span>
|
||
<span class="stat-label">已获成就</span>
|
||
</div>
|
||
<div class="stat">
|
||
<span class="stat-value">{{ activeGoalCount }}</span>
|
||
<span class="stat-label">进行中目标</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 功能模块入口 -->
|
||
<div class="module-grid">
|
||
<!-- 每日签到 -->
|
||
<div class="module-card checkin" @click="router.push('/checkin')">
|
||
<div class="module-icon">
|
||
<Icon name="calendar" size="xl" color="#31f08b" />
|
||
</div>
|
||
<div class="module-info">
|
||
<h3 class="module-title">每日签到</h3>
|
||
<p class="module-desc">坚持签到,累计积分</p>
|
||
</div>
|
||
<div class="module-status" v-if="hasCheckedIn">
|
||
<a-tag color="green">今日已签</a-tag>
|
||
</div>
|
||
<div class="module-status" v-else>
|
||
<a-tag color="orange">待签到</a-tag>
|
||
</div>
|
||
<Icon name="chevronRight" size="md" color="#6B6B6B" class="module-arrow" />
|
||
</div>
|
||
|
||
<!-- 成就中心 -->
|
||
<div class="module-card achievement" @click="router.push('/achievements')">
|
||
<div class="module-icon">
|
||
<Icon name="trophy" size="xl" color="#FFD700" />
|
||
</div>
|
||
<div class="module-info">
|
||
<h3 class="module-title">成就中心</h3>
|
||
<p class="module-desc">解锁成就,展示荣誉</p>
|
||
</div>
|
||
<div class="module-badge" v-if="unclaimedAchievements > 0">
|
||
<span class="badge-count">{{ unclaimedAchievements }}</span>
|
||
</div>
|
||
<Icon name="chevronRight" size="md" color="#6B6B6B" class="module-arrow" />
|
||
</div>
|
||
|
||
<!-- 我的目标 -->
|
||
<div class="module-card goals" @click="router.push('/goals')">
|
||
<div class="module-icon">
|
||
<Icon name="flag" size="xl" color="#3B82F6" />
|
||
</div>
|
||
<div class="module-info">
|
||
<h3 class="module-title">我的目标</h3>
|
||
<p class="module-desc">设定目标,持续成长</p>
|
||
</div>
|
||
<div class="module-progress" v-if="activeGoalCount > 0">
|
||
<span>{{ activeGoalCount }} 个进行中</span>
|
||
</div>
|
||
<Icon name="chevronRight" size="md" color="#6B6B6B" class="module-arrow" />
|
||
</div>
|
||
|
||
<!-- 竞赛活动 -->
|
||
<div class="module-card contest" @click="router.push('/contests')">
|
||
<div class="module-icon">
|
||
<Icon name="fire" size="xl" color="#EF4444" />
|
||
</div>
|
||
<div class="module-info">
|
||
<h3 class="module-title">竞赛活动</h3>
|
||
<p class="module-desc">参与竞赛,争夺荣耀</p>
|
||
</div>
|
||
<div class="module-status" v-if="activeContestCount > 0">
|
||
<a-tag color="red">{{ activeContestCount }} 场进行中</a-tag>
|
||
</div>
|
||
<Icon name="chevronRight" size="md" color="#6B6B6B" class="module-arrow" />
|
||
</div>
|
||
|
||
<!-- 团队PK -->
|
||
<div class="module-card team-pk" @click="router.push('/team-pk')">
|
||
<div class="module-icon">
|
||
<Icon name="bolt" size="xl" color="#F59E0B" />
|
||
</div>
|
||
<div class="module-info">
|
||
<h3 class="module-title">团队 PK</h3>
|
||
<p class="module-desc">部门对战,为团队而战</p>
|
||
</div>
|
||
<div class="module-status" v-if="activePKCount > 0">
|
||
<a-tag color="orange">{{ activePKCount }} 场对战中</a-tag>
|
||
</div>
|
||
<Icon name="chevronRight" size="md" color="#6B6B6B" class="module-arrow" />
|
||
</div>
|
||
|
||
<!-- 排行榜 -->
|
||
<div class="module-card leaderboard" @click="router.push('/designer-hall')">
|
||
<div class="module-icon">
|
||
<Icon name="chartBar" size="xl" color="#8B5CF6" />
|
||
</div>
|
||
<div class="module-info">
|
||
<h3 class="module-title">排行榜</h3>
|
||
<p class="module-desc">查看榜单,了解排名</p>
|
||
</div>
|
||
<div class="module-rank" v-if="myRank > 0">
|
||
<span>当前 #{{ myRank }}</span>
|
||
</div>
|
||
<Icon name="chevronRight" size="md" color="#6B6B6B" class="module-arrow" />
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 积分记录 -->
|
||
<div class="section">
|
||
<div class="section-header">
|
||
<h2 class="section-title">
|
||
<Icon name="clock" size="lg" color="#AFAFAF" style="margin-right: 8px;" />
|
||
积分记录
|
||
</h2>
|
||
<a-button type="link" @click="viewAllRecords">查看全部</a-button>
|
||
</div>
|
||
<div class="record-list">
|
||
<div v-for="record in recentRecords" :key="record.id" class="record-item">
|
||
<div class="record-info">
|
||
<Icon :name="getRecordIcon(record.type)" size="md" :color="record.score > 0 ? '#31f08b' : '#EF4444'" />
|
||
<div class="record-detail">
|
||
<span class="record-title">{{ record.title }}</span>
|
||
<span class="record-time">{{ formatTime(record.time) }}</span>
|
||
</div>
|
||
</div>
|
||
<div :class="['record-score', record.score > 0 ? 'positive' : 'negative']">
|
||
{{ record.score > 0 ? '+' : '' }}{{ record.score }}
|
||
</div>
|
||
</div>
|
||
<div v-if="recentRecords.length === 0" class="empty-records">
|
||
暂无积分记录
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</DefaultLayout>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, computed, onMounted } from 'vue'
|
||
import { useRouter } from 'vue-router'
|
||
import DefaultLayout from '@/layouts/DefaultLayout.vue'
|
||
import Icon from '@/components/Icon.vue'
|
||
import { useAuthStore } from '@/stores/auth'
|
||
import dayjs from 'dayjs'
|
||
import { hasCheckedInToday, getCheckInStats } from '@/api/incentive'
|
||
import { getMyGoals } from '@/api/goal'
|
||
import { getContestList } from '@/api/contest'
|
||
import { getTeamPKList } from '@/api/teamPK'
|
||
|
||
const router = useRouter()
|
||
const authStore = useAuthStore()
|
||
|
||
// 用户数据
|
||
const userScore = computed(() => authStore.userInfo?.score || 0)
|
||
const continuousCheckIn = ref(0)
|
||
const achievementCount = ref(0)
|
||
const activeGoalCount = ref(0)
|
||
const activeContestCount = ref(0)
|
||
const activePKCount = ref(0)
|
||
const myRank = ref(0)
|
||
const unclaimedAchievements = ref(0)
|
||
const hasCheckedIn = ref(false)
|
||
|
||
// 积分记录
|
||
const recentRecords = ref<Array<{
|
||
id: number
|
||
type: string
|
||
title: string
|
||
score: number
|
||
time: string
|
||
}>>([
|
||
{ id: 1, type: 'checkin', title: '每日签到奖励', score: 10, time: new Date().toISOString() },
|
||
{ id: 2, type: 'task', title: '完成任务获得积分', score: 50, time: new Date(Date.now() - 86400000).toISOString() },
|
||
{ id: 3, type: 'achievement', title: '解锁成就「初出茅庐」', score: 100, time: new Date(Date.now() - 172800000).toISOString() },
|
||
])
|
||
|
||
const loadData = async () => {
|
||
try {
|
||
// 加载签到状态
|
||
hasCheckedIn.value = await hasCheckedInToday()
|
||
|
||
// 加载签到统计
|
||
const stats = await getCheckInStats()
|
||
continuousCheckIn.value = stats.continuousDays || 0
|
||
|
||
// 加载活跃目标数
|
||
const goals = await getMyGoals('active')
|
||
activeGoalCount.value = goals.length
|
||
|
||
// 加载进行中的竞赛数
|
||
const contests = await getContestList({ status: 'active' })
|
||
activeContestCount.value = contests.records?.length || 0
|
||
|
||
// 加载进行中的PK数
|
||
const pks = await getTeamPKList({ status: 'active' })
|
||
activePKCount.value = pks.records?.length || 0
|
||
|
||
} catch (error) {
|
||
console.error('加载数据失败:', error)
|
||
}
|
||
}
|
||
|
||
const getRecordIcon = (type: string) => {
|
||
const iconMap: Record<string, string> = {
|
||
checkin: 'calendar',
|
||
task: 'document',
|
||
achievement: 'trophy',
|
||
contest: 'fire',
|
||
pk: 'bolt',
|
||
goal: 'flag'
|
||
}
|
||
return iconMap[type] || 'star'
|
||
}
|
||
|
||
const formatTime = (time: string) => {
|
||
return dayjs(time).format('MM-DD HH:mm')
|
||
}
|
||
|
||
const viewAllRecords = () => {
|
||
// TODO: 跳转到积分记录详情页
|
||
}
|
||
|
||
onMounted(() => {
|
||
loadData()
|
||
})
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
.incentive-center {
|
||
min-height: 100vh;
|
||
padding: 24px;
|
||
}
|
||
|
||
.container {
|
||
max-width: 1000px;
|
||
margin: 0 auto;
|
||
}
|
||
|
||
.page-header {
|
||
margin-bottom: 24px;
|
||
}
|
||
|
||
.page-title {
|
||
font-size: 28px;
|
||
font-weight: 600;
|
||
color: #ECECEC;
|
||
display: flex;
|
||
align-items: center;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.page-desc {
|
||
color: #AFAFAF;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.score-card {
|
||
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
||
border: 1px solid #3A3A3A;
|
||
border-radius: 16px;
|
||
padding: 32px;
|
||
margin-bottom: 32px;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
|
||
.score-main {
|
||
.score-label {
|
||
color: #AFAFAF;
|
||
font-size: 14px;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.score-value {
|
||
font-size: 48px;
|
||
font-weight: 700;
|
||
color: #FFD700;
|
||
text-shadow: 0 0 20px rgba(255, 215, 0, 0.3);
|
||
}
|
||
}
|
||
|
||
.score-stats {
|
||
display: flex;
|
||
gap: 40px;
|
||
}
|
||
|
||
.stat {
|
||
text-align: center;
|
||
|
||
.stat-value {
|
||
display: block;
|
||
font-size: 24px;
|
||
font-weight: 600;
|
||
color: #ECECEC;
|
||
}
|
||
|
||
.stat-label {
|
||
display: block;
|
||
font-size: 12px;
|
||
color: #6B6B6B;
|
||
margin-top: 4px;
|
||
}
|
||
}
|
||
|
||
.module-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(2, 1fr);
|
||
gap: 16px;
|
||
margin-bottom: 32px;
|
||
}
|
||
|
||
.module-card {
|
||
background: #2A2A2A;
|
||
border: 1px solid #3A3A3A;
|
||
border-radius: 12px;
|
||
padding: 20px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 16px;
|
||
cursor: pointer;
|
||
transition: all 0.3s ease;
|
||
position: relative;
|
||
|
||
&:hover {
|
||
border-color: #31f08b;
|
||
transform: translateX(4px);
|
||
}
|
||
|
||
&.checkin:hover { border-color: #31f08b; }
|
||
&.achievement:hover { border-color: #FFD700; }
|
||
&.goals:hover { border-color: #3B82F6; }
|
||
&.contest:hover { border-color: #EF4444; }
|
||
&.team-pk:hover { border-color: #F59E0B; }
|
||
&.leaderboard:hover { border-color: #8B5CF6; }
|
||
}
|
||
|
||
.module-icon {
|
||
width: 48px;
|
||
height: 48px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background: rgba(255, 255, 255, 0.05);
|
||
border-radius: 12px;
|
||
}
|
||
|
||
.module-info {
|
||
flex: 1;
|
||
}
|
||
|
||
.module-title {
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
color: #ECECEC;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.module-desc {
|
||
font-size: 13px;
|
||
color: #6B6B6B;
|
||
}
|
||
|
||
.module-status, .module-progress, .module-rank {
|
||
margin-right: 8px;
|
||
font-size: 13px;
|
||
color: #AFAFAF;
|
||
}
|
||
|
||
.module-badge {
|
||
position: absolute;
|
||
top: 12px;
|
||
right: 40px;
|
||
|
||
.badge-count {
|
||
background: #EF4444;
|
||
color: white;
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
padding: 2px 8px;
|
||
border-radius: 10px;
|
||
}
|
||
}
|
||
|
||
.module-arrow {
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.section {
|
||
margin-bottom: 32px;
|
||
}
|
||
|
||
.section-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.section-title {
|
||
font-size: 18px;
|
||
font-weight: 600;
|
||
color: #ECECEC;
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
|
||
.record-list {
|
||
background: #2A2A2A;
|
||
border: 1px solid #3A3A3A;
|
||
border-radius: 12px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.record-item {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 16px 20px;
|
||
border-bottom: 1px solid #3A3A3A;
|
||
|
||
&:last-child {
|
||
border-bottom: none;
|
||
}
|
||
}
|
||
|
||
.record-info {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
}
|
||
|
||
.record-detail {
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.record-title {
|
||
color: #ECECEC;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.record-time {
|
||
color: #6B6B6B;
|
||
font-size: 12px;
|
||
margin-top: 2px;
|
||
}
|
||
|
||
.record-score {
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
|
||
&.positive {
|
||
color: #31f08b;
|
||
}
|
||
|
||
&.negative {
|
||
color: #EF4444;
|
||
}
|
||
}
|
||
|
||
.empty-records {
|
||
padding: 32px;
|
||
text-align: center;
|
||
color: #6B6B6B;
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
.module-grid {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.score-card {
|
||
flex-direction: column;
|
||
gap: 24px;
|
||
text-align: center;
|
||
}
|
||
}
|
||
</style>
|