Files
ai-dimension/client/src/pages/achievements/achievements.vue
T

355 lines
7.7 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>
<view class="page-achievements page-layout">
<view class="status-bar-placeholder"></view>
<!-- 顶部导航 -->
<view class="header">
<text class="title">🏆 成就</text>
<text class="unlock-count">{{ unlockedCount }}/{{ totalCount }}</text>
</view>
<!-- 总览卡片 -->
<view class="overview-card" v-if="categories.length > 0">
<view class="overview-bar">
<view class="overview-fill" :style="{ width: progressPercent + '%' }"></view>
</view>
<text class="overview-text">
已解锁 {{ unlockedCount }} / {{ totalCount }} 个成就
</text>
</view>
<!-- 成就分类 -->
<view class="categories" v-if="categories.length > 0">
<view
v-for="cat in categories"
:key="cat.name"
class="category"
>
<view class="category-header">
<text class="category-icon">{{ catIcon(cat.name) }}</text>
<text class="category-name">{{ cat.name }}</text>
<text class="category-count">
{{ cat.unlocked }}/{{ cat.achievements.length }}
</text>
</view>
<view class="achievement-list">
<view
v-for="ach in cat.achievements"
:key="ach.id"
class="achievement-item"
:class="{ locked: !ach.unlocked }"
>
<view
class="ach-icon"
:style="ach.unlocked ? { color: achColor(ach.icon) } : {}"
>
{{ ach.icon }}
</view>
<view class="ach-info">
<text class="ach-name">{{ ach.name }}</text>
<text class="ach-desc">{{ ach.description }}</text>
<text class="ach-date" v-if="ach.unlocked && ach.date">
{{ formatDate(ach.date) }}
</text>
<text class="ach-date" v-else>
未解锁
</text>
</view>
</view>
</view>
</view>
</view>
<!-- 空状态 -->
<view class="empty-state" v-else-if="!loading">
<text class="empty-icon">🏆</text>
<text class="empty-text">还没有获得任何成就</text>
<text class="empty-sub">继续学习解锁属于你的成就吧</text>
</view>
<!-- 加载状态 -->
<view class="empty-state" v-else>
<text class="empty-icon" style="opacity:0.4"></text>
<text class="empty-text">加载中...</text>
</view>
</view>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { userApi } from '@/utils/api'
import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
const categories = ref([])
const loading = ref(true)
// 成就分类的默认配置
const CATEGORY_CONFIG = [
{ name: '知识探索', icon: '📖', color: 'var(--dim1-color)' },
{ name: '收藏成就', icon: '⭐', color: 'var(--dim2-color)' },
{ name: 'AI 问答', icon: '🤖', color: 'var(--dim3-color)' },
{ name: '社交成就', icon: '👥', color: 'var(--dim4-color)' }
]
const unlockedCount = computed(() => {
return categories.value.reduce(
(sum, cat) => sum + (cat.unlocked || 0),
0
)
})
const totalCount = computed(() => {
return categories.value.reduce(
(sum, cat) => sum + cat.achievements.length,
0
)
})
const progressPercent = computed(() => {
if (totalCount.value === 0) return 0
return Math.round((unlockedCount.value / totalCount.value) * 100)
})
onMounted(() => {
if (!userStore.isLoggedIn) {
uni.showToast({ title: '请先登录', icon: 'none' })
uni.navigateTo({ url: '/pages/login/login' })
return
}
loadAchievements()
})
async function loadAchievements() {
loading.value = true
try {
const data = await userApi.achievements(userStore.token)
// 数据格式:[{name, achievements:[{id,name,description,icon,unlocked,date}]}]
const raw = Array.isArray(data) ? data : data.categories || []
categories.value = raw.map((cat) => ({
...cat,
unlocked: cat.achievements.filter((a) => a.unlocked).length
}))
} catch (e) {
console.error('[achievements] load failed:', e)
categories.value = []
} finally {
loading.value = false
}
}
function catIcon(name) {
const config = CATEGORY_CONFIG.find((c) => c.name === name)
return config ? config.icon : '🏆'
}
function achColor(icon) {
// 根据成就图标或默认分类给彩色
return 'var(--dim1-color)'
}
function formatDate(dateStr) {
if (!dateStr) return ''
const date = new Date(dateStr)
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
return `${year}-${month}-${day}`
}
</script>
<style lang="scss" scoped>
.page-achievements {
min-height: 100vh;
padding: 0 20px 40px;
background: var(--bg-primary);
}
.status-bar-placeholder { height: 44px; }
// 顶部导航
.header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 0 16px;
}
.title {
font-size: 22px;
font-weight: 600;
color: var(--text-primary);
}
.unlock-count {
font-size: 13px;
padding: 3px 12px;
border-radius: 12px;
background: rgba(77, 182, 172, 0.12);
color: var(--dim3-color);
}
// 总览卡片
.overview-card {
padding: 16px 18px;
background: var(--glass-bg);
border: 1px solid var(--glass-border);
border-radius: 14px;
margin-bottom: 20px;
}
.overview-bar {
height: 4px;
background: var(--glass-bg);
border-radius: 2px;
overflow: hidden;
margin-bottom: 10px;
}
.overview-fill {
height: 100%;
background: linear-gradient(90deg, var(--dim1-color), var(--dim3-color));
border-radius: 2px;
transition: width 0.5s ease;
}
.overview-text {
font-size: 12px;
color: var(--text-tertiary);
}
// 成就分类
.categories {
display: flex;
flex-direction: column;
gap: 16px;
}
.category-header {
display: flex;
align-items: center;
gap: 10px;
padding: 0 0 10px;
border-bottom: 1px solid var(--glass-border);
margin-bottom: 12px;
}
.category-icon {
font-size: 18px;
}
.category-name {
font-size: 15px;
color: var(--text-primary);
font-weight: 500;
flex: 1;
}
.category-count {
font-size: 11px;
color: var(--text-muted);
}
// 成就列表
.achievement-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.achievement-item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px;
background: var(--glass-bg);
border: 1px solid var(--glass-border);
border-radius: 12px;
}
.achievement-item.locked {
opacity: 0.4;
}
.ach-icon {
width: 42px;
height: 42px;
border-radius: 10px;
background: rgba(179, 136, 255, 0.1);
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
flex-shrink: 0;
}
.achievement-item.locked .ach-icon {
background: var(--glass-bg);
color: var(--text-muted);
}
.ach-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 3px;
min-width: 0;
}
.ach-name {
font-size: 14px;
color: var(--text-primary);
font-weight: 500;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.ach-desc {
font-size: 12px;
color: var(--text-muted);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.ach-date {
font-size: 10px;
color: rgba(179, 136, 255, 0.6);
margin-top: 2px;
}
.achievement-item.locked .ach-date {
color: var(--text-muted);
}
// 空状态
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 80px 0;
text-align: center;
}
.empty-icon {
font-size: 56px;
margin-bottom: 16px;
opacity: 0.5;
}
.empty-text {
font-size: 16px;
color: var(--text-tertiary);
display: block;
margin-bottom: 6px;
}
.empty-sub {
font-size: 13px;
color: var(--text-muted);
display: block;
}
</style>