qiangdanxitong/frontend/src/views/Achievements.vue

534 lines
13 KiB
Vue

<template>
<DefaultLayout>
<div class="achievements-page">
<div class="container">
<div class="header-stats">
<div class="stat-box">
<div class="stat-value">{{ userAchievementsCount }}</div>
<div class="stat-label">已获得</div>
</div>
<div class="stat-box">
<div class="stat-value">{{ totalAchievementsCount }}</div>
<div class="stat-label">总成就</div>
</div>
<div class="stat-box">
<div class="stat-value">{{ completionRate }}%</div>
<div class="stat-label">完成度</div>
</div>
</div>
<!-- 筛选器 -->
<div class="filter-section">
<a-radio-group v-model:value="filterType" button-style="solid" @change="handleFilterChange">
<a-radio-button value="all">全部成就</a-radio-button>
<a-radio-button value="unlocked">已解锁</a-radio-button>
<a-radio-button value="locked">未解锁</a-radio-button>
</a-radio-group>
<a-select v-model:value="filterCategory" style="width: 150px" @change="handleFilterChange">
<a-select-option value="all">全部类型</a-select-option>
<a-select-option value="task">任务类</a-select-option>
<a-select-option value="score">积分类</a-select-option>
<a-select-option value="quality">质量类</a-select-option>
<a-select-option value="speed">速度类</a-select-option>
</a-select>
</div>
<!-- 成就列表 -->
<a-spin :spinning="loading">
<div v-if="filteredAchievements.length > 0" class="achievements-grid">
<div
v-for="achievement in filteredAchievements"
:key="achievement.id"
class="achievement-card"
:class="{
'unlocked': isUnlocked(achievement.id),
[`level-${achievement.level}`]: true
}"
>
<!-- 徽章图标 -->
<div class="achievement-badge">
<div class="badge-icon">
<Icon name="trophy" size="lg" v-if="achievement.level === 'diamond'" />
<Icon name="crown" size="lg" v-else-if="achievement.level === 'gold'" />
<Icon name="star" size="lg" v-else-if="achievement.level === 'silver'" />
<Icon name="trophy" size="lg" v-else />
</div>
<div v-if="isUnlocked(achievement.id)" class="badge-check">
<Icon name="checkCircle" size="sm" />
</div>
<div v-if="!isUnlocked(achievement.id)" class="badge-lock">
<Icon name="locked" size="sm" />
</div>
</div>
<!-- 成就信息 -->
<div class="achievement-info">
<div class="achievement-name">{{ achievement.achievementName }}</div>
<div class="achievement-desc">{{ achievement.description }}</div>
<div class="achievement-meta">
<a-tag :color="getLevelColor(achievement.level)" size="small">
{{ getLevelText(achievement.level) }}
</a-tag>
<a-tag color="blue" size="small">
{{ getTypeText(achievement.achievementType) }}
</a-tag>
</div>
<!-- 奖励信息 -->
<div class="achievement-reward">
<Icon name="trophy" size="sm" />
奖励 {{ achievement.rewardScore }} 积分
</div>
<!-- 已解锁信息 -->
<div v-if="isUnlocked(achievement.id)" class="achievement-unlocked">
<div class="unlock-info">
<Icon name="checkCircle" size="sm" />
已于 {{ getUnlockTime(achievement.id) }} 解锁
</div>
<a-button
v-if="!isRewardClaimed(achievement.id)"
type="primary"
size="small"
@click="handleClaimReward(achievement.id)"
>
领取奖励
</a-button>
<a-tag v-else color="success" size="small">
已领取
</a-tag>
</div>
</div>
</div>
</div>
<div v-else class="empty-state">
<a-empty description="暂无成就数据" />
</div>
</a-spin>
</div>
</div>
</DefaultLayout>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import DefaultLayout from '@/layouts/DefaultLayout.vue'
import { message } from 'ant-design-vue'
import Icon from '@/components/Icon.vue'
import {
getAllAchievements,
getUserAchievements,
claimAchievementReward,
type Achievement,
type UserAchievement
} from '@/api/incentive'
const loading = ref(false)
const achievements = ref<Achievement[]>([])
const userAchievements = ref<UserAchievement[]>([])
const filterType = ref('all')
const filterCategory = ref('all')
// 模拟当前用户ID
const currentUserId = ref(1)
// 计算属性
const userAchievementsCount = computed(() => userAchievements.value.length)
const totalAchievementsCount = computed(() => achievements.value.length)
const completionRate = computed(() => {
if (totalAchievementsCount.value === 0) return 0
return Math.round((userAchievementsCount.value / totalAchievementsCount.value) * 100)
})
// 过滤后的成就列表
const filteredAchievements = computed(() => {
let result = achievements.value
// 按解锁状态筛选
if (filterType.value === 'unlocked') {
result = result.filter(a => isUnlocked(a.id))
} else if (filterType.value === 'locked') {
result = result.filter(a => !isUnlocked(a.id))
}
// 按类型筛选
if (filterCategory.value !== 'all') {
result = result.filter(a => a.achievementType === filterCategory.value)
}
return result
})
// 加载所有成就
const loadAchievements = async () => {
loading.value = true
try {
achievements.value = await getAllAchievements()
} catch (error) {
message.error('加载成就列表失败')
console.error(error)
} finally {
loading.value = false
}
}
// 加载用户成就
const loadUserAchievements = async () => {
try {
userAchievements.value = await getUserAchievements(currentUserId.value)
} catch (error) {
console.error('加载用户成就失败', error)
}
}
// 判断成就是否已解锁
const isUnlocked = (achievementId: number) => {
return userAchievements.value.some(ua => ua.achievementId === achievementId)
}
// 判断奖励是否已领取
const isRewardClaimed = (achievementId: number) => {
const userAch = userAchievements.value.find(ua => ua.achievementId === achievementId)
return userAch?.rewardClaimed === 1
}
// 获取解锁时间
const getUnlockTime = (achievementId: number) => {
const userAch = userAchievements.value.find(ua => ua.achievementId === achievementId)
if (!userAch) return ''
const date = new Date(userAch.achievedTime)
return date.toLocaleDateString('zh-CN')
}
// 领取奖励
const handleClaimReward = async (achievementId: number) => {
try {
await claimAchievementReward(achievementId, currentUserId.value)
message.success('奖励领取成功!')
await loadUserAchievements()
} catch (error) {
message.error('领取失败')
console.error(error)
}
}
// 筛选改变
const handleFilterChange = () => {
// 触发重新计算
}
// 获取等级颜色
const getLevelColor = (level: string) => {
switch (level) {
case 'diamond':
return 'purple'
case 'gold':
return 'gold'
case 'silver':
return 'default'
case 'bronze':
return 'orange'
default:
return 'default'
}
}
// 获取等级文本
const getLevelText = (level: string) => {
switch (level) {
case 'diamond':
return '钻石'
case 'gold':
return '金牌'
case 'silver':
return '银牌'
case 'bronze':
return '铜牌'
default:
return level
}
}
// 获取类型文本
const getTypeText = (type: string) => {
switch (type) {
case 'task':
return '任务类'
case 'score':
return '积分类'
case 'quality':
return '质量类'
case 'speed':
return '速度类'
default:
return type
}
}
onMounted(() => {
loadAchievements()
loadUserAchievements()
})
</script>
<style lang="scss" scoped>
.achievements-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;
}
}
.header-stats {
display: flex;
gap: $space-lg;
margin-bottom: $space-2xl;
.stat-box {
background: $bg-secondary;
border: 1px solid $border-color;
border-radius: $radius-md;
padding: $space-lg;
min-width: 100px;
text-align: center;
.stat-value {
font-size: $font-size-2xl;
font-weight: $font-weight-bold;
color: $primary-color;
margin-bottom: $space-xs;
}
.stat-label {
font-size: $font-size-sm;
color: $text-secondary;
}
}
}
.filter-section {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: $space-xl;
}
// 图表卡片 - Cursor 风格
.chart-card {
background: $bg-card;
border: 1px solid $border-primary;
border-radius: $radius-lg;
padding: $space-xl;
transition: all $transition-base $ease-out;
&:hover {
border-color: $border-color;
background: $bg-card-hover;
}
}
.achievements-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: $space-lg;
}
.achievement-card {
background: $bg-card;
border: 2px solid $border-primary;
border-radius: $radius-lg;
padding: $space-xl;
display: flex;
flex-direction: column;
align-items: center;
gap: $space-lg;
transition: all $transition-base $ease-out;
position: relative;
overflow: hidden;
&:hover {
border-color: $border-color;
background: $bg-card-hover;
box-shadow: $shadow-base;
}
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4px;
background: linear-gradient(90deg, #ddd, #ddd);
}
&.level-bronze::before {
background: rgba(#31f08b, 0.3);
}
&.level-silver::before {
background: rgba(#31f08b, 0.4);
}
&.level-gold::before {
background: rgba(#31f08b, 0.5);
}
&.level-diamond::before {
background: rgba(#31f08b, 0.6);
}
&.unlocked {
border-color: $brand-primary;
background: rgba(#31f08b, 0.05);
.achievement-badge {
filter: grayscale(0);
}
}
&:hover {
transform: translateY(-4px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
}
}
.achievement-badge {
width: 120px;
height: 120px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
position: relative;
filter: grayscale(1);
transition: all 0.3s ease-out;
.level-bronze & {
background: rgba(#31f08b, 0.2);
}
.level-silver & {
background: rgba(#31f08b, 0.3);
}
.level-gold & {
background: rgba(#31f08b, 0.4);
}
.level-diamond & {
background: rgba(#31f08b, 0.5);
}
.badge-icon {
font-size: 60px;
color: white;
}
.badge-check,
.badge-lock {
position: absolute;
bottom: -5px;
right: -5px;
width: 36px;
height: 36px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
border: 3px solid $bg-secondary;
}
.badge-check {
background: #31f08b;
color: white;
}
.badge-lock {
background: $bg-tertiary;
color: $text-tertiary;
border: 1px solid $border-primary;
}
}
.achievement-info {
width: 100%;
text-align: center;
}
.achievement-name {
font-size: $font-size-xl;
font-weight: $font-weight-bold;
color: $text-primary;
margin-bottom: $space-sm;
}
.achievement-desc {
font-size: $font-size-base;
color: $text-secondary;
margin-bottom: $space-md;
line-height: 1.6;
}
.achievement-meta {
display: flex;
justify-content: center;
gap: $space-sm;
margin-bottom: $space-md;
}
.achievement-reward {
display: inline-flex;
align-items: center;
gap: $space-xs;
padding: $space-sm $space-lg;
background: rgba(#31f08b, 0.1);
border: 1px solid rgba(#31f08b, 0.3);
border-radius: $radius-md;
color: #31f08b;
font-weight: $font-weight-bold;
margin-bottom: $space-md;
}
.achievement-unlocked {
width: 100%;
padding-top: $space-md;
border-top: 1px solid $border-color;
display: flex;
flex-direction: column;
gap: $space-sm;
align-items: center;
.unlock-info {
font-size: $font-size-sm;
color: $text-tertiary;
display: flex;
align-items: center;
gap: $space-xs;
}
}
.empty-state {
padding: $space-3xl 0;
text-align: center;
}
</style>