534 lines
13 KiB
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>
|
|
|