Files
ai-dimension/client/src/pages/achievements/achievements.vue
T
txai-dev 2a111d21c0 feat: v1.3.0 差异化重构——星图点亮体系
P0 修复:
- 详情页正文渲染(content 分段),71 条知识不再空白
- 问答页去登录墙:游客每日 3 次(IP 限流),登录后每日 5 次
- 修正问答配额为真正的每日重置语义

点亮闭环:
- KnowledgeLit 模型 + 服务端判题(POST /:id/quiz,答案不下发)
- /api/knowledge/stats 星域统计 + 点亮进度 + streak + litIds
- AI 批量生成 71 条读后检测题(scripts/generate-quiz.js)

设计系统 2.0:
- 70 个统一线性图标替换全部 emoji 图标
- 主题 token mixin 化,修复 H5 暗色级联 bug,MP 跟随系统(darkmode)
- 首页星域导航(进度环)、我的页学习者档案、星域列表页重写
- 硬编码色清理,误植 map 页移除

服务端加固:
- trust proxy 真实 IP、错误信息脱敏、ask 独立限流、无效 ObjectId 404
- seo.js ESM→CJS(修复容器启动)、AI 导师"维度精灵"人格化
2026-09-12 23:39:05 +08:00

364 lines
7.9 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 { onShareAppMessage, onShareTimeline } from '@dcloudio/uni-app'
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) {
loading.value = false
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}`
}
// 微信小程序分享
// #ifdef MP-WEIXIN
onShareAppMessage(() => ({
title: 'AI 维度 - 你的AI知识助手',
path: '/pages/index/index'
}))
// #endif
</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>