1157 lines
29 KiB
Plaintext
1157 lines
29 KiB
Plaintext
<template>
|
|
<scroll-view direction="vertical" class="achievements" :scroll-y="true" :enable-back-to-top="true">
|
|
<!-- Header Section -->
|
|
<view class="header-section">
|
|
<view class="profile-summary">
|
|
<view class="avatar-section">
|
|
<view class="avatar">{{ getInitials() }}</view>
|
|
</view>
|
|
<view class="profile-info">
|
|
<text class="user-name">{{ getUserName() }}</text>
|
|
<view class="user-stats">
|
|
<text class="stat">等级 {{ getCurrentLevel() }}</text>
|
|
<text class="stat">经验 {{ getCurrentExp() }}/{{ getRequiredExp() }}</text>
|
|
<text class="stat">成就 {{ getTotalAchievements() }}</text>
|
|
<text class="stat">积分 {{ getTotalPoints() }}</text>
|
|
<text class="stat">完成率 {{ getCompletionRate() }}%</text>
|
|
<text class="stat">连续 {{ getStreakDays() }} 天</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
|
|
<!-- Achievement Categories -->
|
|
<view class="categories-section">
|
|
<view class="section-header">
|
|
<text class="section-title">成就分类</text>
|
|
</view>
|
|
<scroll-view class="categories-scroll" direction="horizontal" show-scrollbar="false">
|
|
<view class="categories-list">
|
|
<view
|
|
class="category-item"
|
|
v-for="(category, index) in achievementCategories"
|
|
:key="index"
|
|
:class="{ active: currentCategory == category.value }"
|
|
@click="setCategory(category.value as string)"
|
|
>
|
|
<text class="category-icon">{{ category.icon }}</text>
|
|
<text class="category-name">{{ category.name }}</text>
|
|
<view class="category-badge">
|
|
<text class="badge-text">{{ getCategoryCount(category.value as string) }}</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</scroll-view>
|
|
</view>
|
|
|
|
<!-- Achievements List -->
|
|
<view class="achievements-section">
|
|
<view class="achievements-list">
|
|
<view
|
|
class="achievement-card"
|
|
v-for="(achievement, index) in filteredAchievements"
|
|
:key="index"
|
|
:class="{ unlocked: getAchievementUnlocked(achievement), featured: getAchievementFeatured(achievement) }"
|
|
@click="viewAchievementDetail(achievement)"
|
|
>
|
|
<view class="achievement-icon">
|
|
<text class="icon">{{ getAchievementIcon(achievement) }}</text>
|
|
<view class="unlock-badge" v-if="getAchievementUnlocked(achievement)"><text class="unlock-text">✓</text></view>
|
|
</view>
|
|
<view class="achievement-content">
|
|
<text class="achievement-title">{{ getAchievementTitle(achievement) }}</text>
|
|
<text class="achievement-desc">{{ getAchievementDescription(achievement) }}</text>
|
|
<view class="achievement-meta">
|
|
<view class="points-badge"><text class="points-text">+{{ getAchievementPoints(achievement) }}分</text></view>
|
|
<text class="category-text">{{ getCategoryName(getAchievementCategory(achievement)) }}</text>
|
|
</view>
|
|
<view class="progress-section" v-if="!getAchievementUnlocked(achievement) && getAchievementProgressObj(achievement) != null">
|
|
<view class="progress-bar">
|
|
<view class="progress-fill" :style="`width: ${getAchievementProgress(achievement)}%`"></view>
|
|
</view>
|
|
<text class="progress-text">{{ getAchievementProgressCurrent(achievement) }}/{{ getAchievementProgressTarget(achievement) }}</text>
|
|
</view>
|
|
<view class="unlock-date" v-if="getAchievementUnlocked(achievement)"><text class="date-text">{{ formatDate(getAchievementUnlockedAt(achievement)) }}获得</text></view>
|
|
</view>
|
|
<view class="difficulty-indicator" :class="`difficulty-${getAchievementDifficulty(achievement)}`">
|
|
<text class="difficulty-text">{{ formatDifficulty(getAchievementDifficulty(achievement)) }}</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
|
|
<!-- Recent Achievements -->
|
|
<view class="recent-section" v-if="getRecentAchievements().length > 0">
|
|
<view class="section-header"><text class="section-title">最近获得</text></view>
|
|
<view class="recent-achievements">
|
|
<view class="recent-item" v-for="(achievement, index) in getRecentAchievements()" :key="index" @click="viewAchievementDetail(achievement)">
|
|
<view class="recent-icon"><text class="icon">{{ getAchievementIcon(achievement) }}</text></view>
|
|
<view class="recent-content">
|
|
<text class="recent-title">{{ getAchievementTitle(achievement) }}</text>
|
|
<text class="recent-date">{{ formatDate(getAchievementUnlockedAt(achievement)) }}</text>
|
|
</view>
|
|
<view class="recent-points"><text class="points">+{{ getAchievementPoints(achievement) }}</text></view>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
|
|
<!-- Achievement Detail Modal -->
|
|
<view class="modal-overlay" v-if="selectedAchievement != null && getAchievementTitle(selectedAchievement) !== ''">
|
|
<view class="achievement-detail-modal" @click.stop>
|
|
<view class="modal-header">
|
|
<view class="achievement-detail-icon" :class="{ unlocked: getAchievementUnlocked(selectedAchievement) }">
|
|
<text class="detail-icon">{{ getAchievementIcon(selectedAchievement) }}</text>
|
|
</view>
|
|
<button class="modal-close-btn" @click="closeDetailModal">✕</button>
|
|
</view>
|
|
<view class="modal-content">
|
|
<text class="detail-title">{{ getSelectedAchievementTitle() }}</text>
|
|
<text class="detail-description">{{ getSelectedAchievementDescription() }}</text>
|
|
<view class="detail-requirements">
|
|
<text class="requirements-title">获得条件:</text>
|
|
<text class="requirements-text">{{ getSelectedAchievementRequirements() }}</text>
|
|
</view>
|
|
<view class="detail-reward">
|
|
<text class="reward-label">奖励:</text>
|
|
<text class="reward-points">{{ getSelectedAchievementPoints() }}积分</text>
|
|
</view>
|
|
<view class="detail-progress" v-if="!getSelectedAchievementUnlocked() && getAchievementProgressObj(selectedAchievement) != null">
|
|
<text class="progress-label">进度:</text>
|
|
<view class="detail-progress-bar">
|
|
<view class="detail-progress-fill" :style="`width: ${getAchievementProgress(selectedAchievement)}%`"></view>
|
|
</view>
|
|
<text class="detail-progress-text">{{ getSelectedAchievementProgressCurrent() }}/{{ getSelectedAchievementProgressTarget() }}</text>
|
|
</view>
|
|
<view class="detail-unlock-info" v-if="getSelectedAchievementUnlocked()">
|
|
<text class="unlock-label">获得时间:</text>
|
|
<text class="unlock-time">{{ formatDateTime(getSelectedAchievementUnlockedAt()) }}</text>
|
|
</view>
|
|
<view class="detail-difficulty">
|
|
<text class="difficulty-label">难度:</text>
|
|
<text class="difficulty-value">{{ formatDifficulty(getSelectedAchievementDifficulty()) }}</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</scroll-view>
|
|
</template>
|
|
|
|
<script setup lang="uts"> import { ref, watch, computed, onMounted, onUnmounted } from 'vue'
|
|
import { onLoad, onResize } from '@dcloudio/uni-app'
|
|
import { getCurrentUserId } from '@/utils/store.uts'
|
|
// Unified user ID handling
|
|
const userId = ref('')
|
|
|
|
// Responsive state - using onResize for dynamic updates
|
|
const screenWidth = ref<number>(uni.getSystemInfoSync().windowWidth)
|
|
|
|
// Reactive data
|
|
const achievements = ref<Array<UTSJSONObject>>([])
|
|
const filteredAchievements = ref<Array<UTSJSONObject>>([])
|
|
const currentCategory = ref('all')
|
|
const selectedAchievement = ref<UTSJSONObject>({})
|
|
const userStats = ref<UTSJSONObject>({})
|
|
const loading = ref(true)
|
|
const achievementCategories = [
|
|
{ name: '全部', value: 'all', icon: '' },
|
|
{ name: '训练', value: 'training', icon: '' },
|
|
{ name: '技能', value: 'skill', icon: '⚡' },
|
|
{ name: '进步', value: 'progress', icon: '' },
|
|
{ name: '坚持', value: 'persistence', icon: '' },
|
|
{ name: '特殊', value: 'special', icon: '' }
|
|
]
|
|
|
|
// Computed properties for responsive design
|
|
const isLargeScreen = computed(() : boolean => {
|
|
return screenWidth.value >= 768
|
|
})
|
|
|
|
// Methods
|
|
function setCategory(category: string) {
|
|
currentCategory.value = category
|
|
}
|
|
|
|
function formatDate(dateString: string): string {
|
|
if (dateString == null || dateString === "") return ""
|
|
const date = new Date(dateString)
|
|
return `${date.getFullYear()}年${date.getMonth() + 1}月${date.getDate()}日`
|
|
}
|
|
|
|
function filterAchievements() {
|
|
if (currentCategory.value === 'all') {
|
|
filteredAchievements.value = achievements.value
|
|
} else {
|
|
filteredAchievements.value = achievements.value.filter(
|
|
(achievement: UTSJSONObject) => achievement instanceof UTSJSONObject && achievement.getString('category') === currentCategory.value
|
|
)
|
|
}
|
|
}
|
|
|
|
function loadAchievements() {
|
|
loading.value = true
|
|
|
|
// Mock data - replace with actual API call
|
|
setTimeout(() => {
|
|
achievements.value = [
|
|
{
|
|
id: '1',
|
|
title: '初出茅庐',
|
|
description: '完成第一次训练记录',
|
|
icon: '',
|
|
category: 'training',
|
|
difficulty: 'easy',
|
|
points: 10,
|
|
unlocked: true,
|
|
unlocked_at: '2024-01-10T10:30:00',
|
|
requirements: '完成任意一次训练记录',
|
|
featured: false
|
|
},
|
|
{
|
|
id: '2',
|
|
title: '坚持不懈',
|
|
description: '连续训练7天',
|
|
icon: '',
|
|
category: 'persistence',
|
|
difficulty: 'medium',
|
|
points: 50,
|
|
unlocked: true,
|
|
unlocked_at: '2024-01-15T14:20:00',
|
|
requirements: '连续7天都有训练记录',
|
|
featured: true
|
|
},
|
|
{
|
|
id: '3',
|
|
title: '技术精进',
|
|
description: '单项技能评分达到90分以上',
|
|
icon: '⚡',
|
|
category: 'skill',
|
|
difficulty: 'hard',
|
|
points: 100,
|
|
unlocked: false,
|
|
progress: { current: 2, target: 3 },
|
|
requirements: '在任意技能项目中获得90分以上评分',
|
|
featured: false
|
|
},
|
|
{
|
|
id: '4',
|
|
title: '全能选手',
|
|
description: '完成所有训练项目',
|
|
icon: '',
|
|
category: 'training',
|
|
difficulty: 'expert',
|
|
points: 200,
|
|
unlocked: false,
|
|
progress: { current: 5, target: 8 },
|
|
requirements: '完成系统中所有的训练项目',
|
|
featured: true
|
|
},
|
|
{
|
|
id: '5',
|
|
title: '进步之星',
|
|
description: '单月内平均分提升10分以上',
|
|
icon: '',
|
|
category: 'progress',
|
|
difficulty: 'medium',
|
|
points: 75,
|
|
unlocked: false,
|
|
progress: { current: 6, target: 10 },
|
|
requirements: '在一个月内平均成绩提升10分或以上',
|
|
featured: false
|
|
},
|
|
{
|
|
id: '6',
|
|
title: '月度之王',
|
|
description: '成为月度训练积分第一名',
|
|
icon: '',
|
|
category: 'special',
|
|
difficulty: 'expert',
|
|
points: 300,
|
|
unlocked: false,
|
|
requirements: '在月度排行榜中获得第一名',
|
|
featured: true
|
|
}
|
|
]
|
|
|
|
filterAchievements()
|
|
loading.value = false
|
|
}, 1000)
|
|
}
|
|
|
|
function loadUserStats() {
|
|
// Mock user statistics
|
|
userStats.value = {
|
|
name: '张同学',
|
|
level: 5,
|
|
current_exp: 450,
|
|
required_exp: 600,
|
|
total_points: 285,
|
|
completion_rate: 68,
|
|
streak_days: 12,
|
|
unlocked_achievements: 2
|
|
}
|
|
}
|
|
|
|
function getCategoryCount(category: string): number {
|
|
if (category == 'all') {
|
|
return achievements.value.filter(
|
|
(a: UTSJSONObject) => a instanceof UTSJSONObject && a.getBoolean('unlocked') === true
|
|
).length
|
|
}
|
|
return achievements.value.filter(
|
|
(a: UTSJSONObject) => a instanceof UTSJSONObject && a.getString('category') === category && a.getBoolean('unlocked') === true
|
|
).length
|
|
}
|
|
|
|
function getCategoryName(category: string): string {
|
|
const categoryObj = achievementCategories.find(c => c.value === category)
|
|
return (categoryObj != null && typeof categoryObj.name === 'string' ? categoryObj.name :category) as string
|
|
}
|
|
|
|
function getAchievementProgress(achievement: UTSJSONObject): number {
|
|
if (!(achievement instanceof UTSJSONObject)) return 0
|
|
const progress = achievement.get('progress')
|
|
if (progress == null || !(progress instanceof UTSJSONObject)) return 0
|
|
const current = progress.getNumber('current')
|
|
const target = progress.getNumber('target')
|
|
if (current == null || target == null || target === 0) return 0
|
|
return Math.round((current / target) * 100)
|
|
}
|
|
|
|
function formatDifficulty(difficulty: string): string {
|
|
const difficultyMap = {
|
|
'easy': '简单',
|
|
'medium': '中等',
|
|
'hard': '困难',
|
|
'expert': '专家'
|
|
}
|
|
return difficultyMap[difficulty] as string|null ?? difficulty
|
|
}
|
|
|
|
function formatDateTime(dateString: string): string {
|
|
const date = new Date(dateString)
|
|
return `${date.getFullYear()}年${date.getMonth() + 1}月${date.getDate()}日 ${date.getHours()}:${date.getMinutes().toString().padStart(2, '0')}`
|
|
}
|
|
|
|
function getRecentAchievements(): Array<UTSJSONObject> {
|
|
return (achievements.value as Array<UTSJSONObject>)
|
|
.filter((a: UTSJSONObject) => a instanceof UTSJSONObject && a.getBoolean('unlocked') === true)
|
|
.sort((a: UTSJSONObject, b: UTSJSONObject) => {
|
|
const aDate = a.getString('unlocked_at')
|
|
const bDate = b.getString('unlocked_at')
|
|
if (aDate == null || bDate == null) return 0
|
|
return new Date(bDate).getTime() - new Date(aDate).getTime()
|
|
})
|
|
.slice(0, 3)
|
|
}
|
|
|
|
function viewAchievementDetail(achievement: UTSJSONObject) {
|
|
selectedAchievement.value = achievement
|
|
}
|
|
|
|
function closeDetailModal() {
|
|
selectedAchievement.value = {} as UTSJSONObject
|
|
}
|
|
|
|
// Getter helpers for UTSJSONObject achievement fields
|
|
function getAchievementTitle(achievement: UTSJSONObject): string {
|
|
return achievement instanceof UTSJSONObject ? achievement.getString('title') ?? '' : ''
|
|
}
|
|
function getAchievementDescription(achievement: UTSJSONObject): string {
|
|
return achievement instanceof UTSJSONObject ? achievement.getString('description') ?? '' : ''
|
|
}
|
|
function getAchievementIcon(achievement: UTSJSONObject): string {
|
|
return achievement instanceof UTSJSONObject ? achievement.getString('icon') ?? '' : ''
|
|
}
|
|
function getAchievementCategory(achievement: UTSJSONObject): string {
|
|
return achievement instanceof UTSJSONObject ? achievement.getString('category') ?? '' : ''
|
|
}
|
|
function getAchievementDifficulty(achievement: UTSJSONObject): string {
|
|
return achievement instanceof UTSJSONObject ? achievement.getString('difficulty') ?? '' : ''
|
|
}
|
|
function getAchievementPoints(achievement: UTSJSONObject): number {
|
|
return achievement instanceof UTSJSONObject ? achievement.getNumber('points') ?? 0 : 0
|
|
}
|
|
function getAchievementUnlocked(achievement: UTSJSONObject): boolean {
|
|
return achievement instanceof UTSJSONObject ? achievement.getBoolean('unlocked') === true : false
|
|
}
|
|
function getAchievementUnlockedAt(achievement: UTSJSONObject): string {
|
|
return achievement instanceof UTSJSONObject ? achievement.getString('unlocked_at') ?? '' : ''
|
|
}
|
|
function getAchievementRequirements(achievement: UTSJSONObject): string {
|
|
return achievement instanceof UTSJSONObject ? achievement.getString('requirements') ?? '' : ''
|
|
}
|
|
function getAchievementFeatured(achievement: UTSJSONObject): boolean {
|
|
return achievement instanceof UTSJSONObject ? achievement.getBoolean('featured') === true : false
|
|
}
|
|
function getAchievementProgressObj(achievement: UTSJSONObject): UTSJSONObject|null {
|
|
if (!(achievement instanceof UTSJSONObject)) return null
|
|
const progress = achievement.get('progress')
|
|
return progress instanceof UTSJSONObject ? progress : null
|
|
}
|
|
function getAchievementProgressCurrent(achievement: UTSJSONObject): number {
|
|
const progress = getAchievementProgressObj(achievement)
|
|
if (progress != null) {
|
|
const val = progress.getNumber('current')
|
|
return val != null ? val : 0
|
|
}
|
|
return 0
|
|
}
|
|
function getAchievementProgressTarget(achievement: UTSJSONObject): number {
|
|
const progress = getAchievementProgressObj(achievement)
|
|
if (progress != null) {
|
|
const val = progress.getNumber('target')
|
|
return val != null ? val : 0
|
|
}
|
|
return 0
|
|
}
|
|
|
|
// selectedAchievement getters
|
|
function getSelectedAchievementTitle(): string {
|
|
return getAchievementTitle(selectedAchievement.value)
|
|
}
|
|
function getSelectedAchievementDescription(): string {
|
|
return getAchievementDescription(selectedAchievement.value)
|
|
}
|
|
function getSelectedAchievementIcon(): string {
|
|
return getAchievementIcon(selectedAchievement.value)
|
|
}
|
|
function getSelectedAchievementUnlocked(): boolean {
|
|
return getAchievementUnlocked(selectedAchievement.value)
|
|
}
|
|
function getSelectedAchievementRequirements(): string {
|
|
return getAchievementRequirements(selectedAchievement.value)
|
|
}
|
|
function getSelectedAchievementPoints(): number {
|
|
return getAchievementPoints(selectedAchievement.value)
|
|
}
|
|
function getSelectedAchievementProgressCurrent(): number {
|
|
return getAchievementProgressCurrent(selectedAchievement.value)
|
|
}
|
|
function getSelectedAchievementProgressTarget(): number {
|
|
return getAchievementProgressTarget(selectedAchievement.value)
|
|
}
|
|
function getSelectedAchievementUnlockedAt(): string {
|
|
return getAchievementUnlockedAt(selectedAchievement.value)
|
|
}
|
|
function getSelectedAchievementDifficulty(): string {
|
|
return getAchievementDifficulty(selectedAchievement.value)
|
|
}
|
|
|
|
// userStats getters (全部用 getString/getNumber)
|
|
function getCurrentLevel(): number {
|
|
return userStats.value instanceof UTSJSONObject ? userStats.value.getNumber('level') ?? 1 : 1
|
|
}
|
|
function getCurrentExp(): number {
|
|
return userStats.value instanceof UTSJSONObject ? userStats.value.getNumber('current_exp') ?? 0 : 0
|
|
}
|
|
function getRequiredExp(): number {
|
|
return userStats.value instanceof UTSJSONObject ? userStats.value.getNumber('required_exp') ?? 100 : 100
|
|
}
|
|
function getTotalAchievements(): number {
|
|
return userStats.value instanceof UTSJSONObject ? userStats.value.getNumber('unlocked_achievements') ?? 0 : 0
|
|
}
|
|
function getTotalPoints(): number {
|
|
return userStats.value instanceof UTSJSONObject ? userStats.value.getNumber('total_points') ?? 0 : 0
|
|
}
|
|
function getCompletionRate(): number {
|
|
return userStats.value instanceof UTSJSONObject ? userStats.value.getNumber('completion_rate') ?? 0 : 0
|
|
}
|
|
function getStreakDays(): number {
|
|
return userStats.value instanceof UTSJSONObject ? userStats.value.getNumber('streak_days') ?? 0 : 0
|
|
}
|
|
function getUserName(): string {
|
|
return userStats.value instanceof UTSJSONObject ? (userStats.value.getString('name') ?? '用户') : '用户'
|
|
}
|
|
function getInitials(): string {
|
|
const name = getUserName()
|
|
return name.length > 0 ? name.charAt(0) : 'U'
|
|
} // Lifecycle
|
|
onLoad((options: OnLoadOptions) => {
|
|
userId.value = options['id'] ?? getCurrentUserId()
|
|
loadAchievements()
|
|
loadUserStats()
|
|
})
|
|
|
|
onMounted(() => {
|
|
// Initialize screen width
|
|
screenWidth.value = uni.getSystemInfoSync().windowWidth
|
|
})
|
|
|
|
// Handle resize events for responsive design
|
|
onResize((size) => {
|
|
screenWidth.value = size.size.windowWidth
|
|
})
|
|
|
|
// Watch
|
|
watch(currentCategory, () => {
|
|
filterAchievements()
|
|
})
|
|
</script>
|
|
|
|
<style>
|
|
.achievements {
|
|
display: flex;
|
|
flex:1;
|
|
background-color: #f5f5f5;
|
|
padding: 20rpx;
|
|
padding-bottom: 40rpx;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
/* Header Section */
|
|
.header-section {
|
|
margin-bottom: 25rpx;
|
|
}
|
|
|
|
.profile-summary {
|
|
background-image: linear-gradient(to bottom right, #667eea, #764ba2);
|
|
border-radius: 20rpx;
|
|
padding: 30rpx;
|
|
color: white;
|
|
display: flex;
|
|
align-items: center;
|
|
}
|
|
|
|
.avatar-section {
|
|
position: relative;
|
|
margin-right: 25rpx;
|
|
}
|
|
|
|
.avatar {
|
|
width: 120rpx;
|
|
height: 120rpx;
|
|
border-radius: 60rpx;
|
|
background-color: rgba(255, 255, 255, 0.2);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
border: 3rpx solid rgba(255, 255, 255, 0.3);
|
|
}
|
|
|
|
.avatar-text {
|
|
font-size: 48rpx;
|
|
font-weight: bold;
|
|
color: white;
|
|
}
|
|
|
|
.level-badge {
|
|
position: absolute;
|
|
bottom: -5rpx;
|
|
right: -5rpx;
|
|
background-color: #ffc107;
|
|
border-radius: 15rpx;
|
|
padding: 5rpx 12rpx;
|
|
border: 2rpx solid white;
|
|
}
|
|
|
|
.level-text {
|
|
font-size: 20rpx;
|
|
font-weight: bold;
|
|
color: #333;
|
|
}
|
|
|
|
.profile-info {
|
|
flex: 1;
|
|
}
|
|
|
|
.user-name {
|
|
font-size: 36rpx;
|
|
font-weight: bold;
|
|
margin-bottom: 8rpx;
|
|
display: block;
|
|
}
|
|
|
|
.user-title {
|
|
font-size: 26rpx;
|
|
opacity: 0.9;
|
|
margin-bottom: 15rpx;
|
|
display: block;
|
|
}
|
|
|
|
.exp-bar {
|
|
height: 10rpx;
|
|
background-color: rgba(255, 255, 255, 0.3);
|
|
border-radius: 5rpx;
|
|
overflow: hidden;
|
|
margin-bottom: 8rpx;
|
|
}
|
|
|
|
.exp-fill {
|
|
height: 100%;
|
|
background-color: #ffc107;
|
|
border-radius: 5rpx;
|
|
transition: width 0.3s ease;
|
|
}
|
|
|
|
.exp-text {
|
|
font-size: 24rpx;
|
|
opacity: 0.9;
|
|
}
|
|
.user-stats {
|
|
display: flex;
|
|
flex-direction: row;
|
|
margin-top: 10rpx;
|
|
}
|
|
.user-stats .stat {
|
|
flex: 1;
|
|
margin-right: 10rpx;
|
|
}
|
|
|
|
.stat {
|
|
font-size: 22rpx;
|
|
color: #f0f0f0;
|
|
text-align: center;
|
|
}
|
|
|
|
/* Statistics Section */
|
|
.stats-section {
|
|
margin-bottom: 25rpx;
|
|
}
|
|
.stats-grid {
|
|
display: flex;
|
|
flex-direction: row;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.stats-grid .stat-card {
|
|
width: calc(50% - 7.5rpx);
|
|
margin-right: 15rpx;
|
|
margin-bottom: 15rpx;
|
|
}
|
|
.stat-card {
|
|
background-color: white;
|
|
border-radius: 16rpx;
|
|
padding: 25rpx;
|
|
text-align: center;
|
|
position: relative;
|
|
box-shadow: 0 2rpx 15rpx rgba(0, 0, 0, 0.05);
|
|
margin-right: 15rpx;
|
|
}
|
|
|
|
.stat-icon {
|
|
font-size: 40rpx;
|
|
margin-bottom: 15rpx;
|
|
display: block;
|
|
}
|
|
|
|
.stat-value {
|
|
font-size: 36rpx;
|
|
font-weight: bold;
|
|
color: #667eea;
|
|
margin-bottom: 8rpx;
|
|
display: block;
|
|
}
|
|
|
|
.stat-label {
|
|
font-size: 24rpx;
|
|
color: #666;
|
|
}
|
|
|
|
/* Categories Section */
|
|
.categories-section {
|
|
margin-bottom: 25rpx;
|
|
}
|
|
|
|
.section-header {
|
|
margin-bottom: 20rpx;
|
|
}
|
|
|
|
.section-title {
|
|
font-size: 32rpx;
|
|
font-weight: bold;
|
|
color: #333;
|
|
}
|
|
|
|
.categories-scroll {
|
|
white-space: nowrap;
|
|
}
|
|
.categories-list {
|
|
display: flex;
|
|
padding: 5rpx 0;
|
|
}
|
|
|
|
.categories-list .category-item {
|
|
margin-right: 15rpx;
|
|
}
|
|
.category-item {
|
|
min-width: 120rpx;
|
|
padding: 20rpx 15rpx;
|
|
background-color: white;
|
|
border-radius: 16rpx;
|
|
text-align: center;
|
|
position: relative;
|
|
border: 2rpx solid transparent;
|
|
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
|
|
margin-right: 15rpx;
|
|
}
|
|
|
|
.category-item.active {
|
|
border-color: #667eea;
|
|
background-image: linear-gradient(to bottom right, #f8f9ff, #e3f2fd);
|
|
}
|
|
|
|
.category-icon {
|
|
font-size: 32rpx;
|
|
margin-bottom: 8rpx;
|
|
display: block;
|
|
}
|
|
|
|
.category-name {
|
|
font-size: 24rpx;
|
|
color: #333;
|
|
font-weight: 400;
|
|
}
|
|
|
|
.category-badge {
|
|
position: absolute;
|
|
top: -5rpx;
|
|
right: -5rpx;
|
|
background-color: #667eea;
|
|
border-radius: 10rpx;
|
|
min-width: 20rpx;
|
|
height: 20rpx;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.badge-text {
|
|
font-size: 18rpx;
|
|
color: white;
|
|
font-weight: bold;
|
|
}
|
|
|
|
/* Achievements Section */
|
|
.achievements-section {
|
|
margin-bottom: 25rpx;
|
|
}
|
|
.achievements-list {
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.achievements-list .achievement-card {
|
|
margin-bottom: 15rpx;
|
|
}
|
|
.achievement-card {
|
|
background-color: white;
|
|
border-radius: 20rpx;
|
|
padding: 25rpx;
|
|
display: flex;
|
|
align-items: center;
|
|
position: relative;
|
|
box-shadow: 0 2rpx 15rpx rgba(0, 0, 0, 0.05);
|
|
opacity: 0.6;
|
|
margin-bottom: 20rpx;
|
|
}
|
|
|
|
.achievement-card.unlocked {
|
|
opacity: 1;
|
|
border-color: #28a745;
|
|
}
|
|
|
|
.achievement-card.featured {
|
|
background-image: linear-gradient(to bottom right, #fff9e6, #f0f9ff);
|
|
border-color: #ffc107;
|
|
}
|
|
|
|
.achievement-icon {
|
|
width: 80rpx;
|
|
height: 80rpx;
|
|
border-radius: 40rpx;
|
|
background-color: #f8f9ff;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
margin-right: 20rpx;
|
|
position: relative;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.achievement-card.unlocked .achievement-icon {
|
|
background-image: linear-gradient(to bottom right, #667eea, #764ba2);
|
|
}
|
|
|
|
.icon {
|
|
font-size: 40rpx;
|
|
color: #667eea;
|
|
}
|
|
|
|
.achievement-card.unlocked .icon {
|
|
color: white;
|
|
}
|
|
|
|
.unlock-badge {
|
|
position: absolute;
|
|
bottom: -5rpx;
|
|
right: -5rpx;
|
|
width: 24rpx;
|
|
height: 24rpx;
|
|
border-radius: 12rpx;
|
|
background-color: #28a745;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
border: 2rpx solid white;
|
|
}
|
|
|
|
.unlock-text {
|
|
font-size: 16rpx;
|
|
color: white;
|
|
font-weight: bold;
|
|
}
|
|
|
|
.achievement-content {
|
|
flex: 1;
|
|
}
|
|
|
|
.achievement-title {
|
|
font-size: 30rpx;
|
|
font-weight: bold;
|
|
color: #333;
|
|
margin-bottom: 8rpx;
|
|
display: block;
|
|
}
|
|
|
|
.achievement-desc {
|
|
font-size: 26rpx;
|
|
color: #666;
|
|
line-height: 1.4;
|
|
margin-bottom: 15rpx;
|
|
}
|
|
.achievement-meta {
|
|
display: flex;
|
|
align-items: center;
|
|
margin-bottom: 15rpx;
|
|
}
|
|
.achievement-meta > * {
|
|
margin-right: 15rpx;
|
|
}
|
|
|
|
.points-badge {
|
|
background-color: #ffc107;
|
|
padding: 5rpx 12rpx;
|
|
border-radius: 12rpx;
|
|
}
|
|
|
|
.points-text {
|
|
font-size: 22rpx;
|
|
font-weight: bold;
|
|
color: #333;
|
|
}
|
|
|
|
.category-text {
|
|
font-size: 22rpx;
|
|
color: #666;
|
|
}
|
|
|
|
.progress-section {
|
|
margin-bottom: 10rpx;
|
|
}
|
|
|
|
.progress-bar {
|
|
height: 6rpx;
|
|
background-color: #f0f0f0;
|
|
border-radius: 3rpx;
|
|
overflow: hidden;
|
|
margin-bottom: 5rpx;
|
|
}
|
|
|
|
.progress-fill {
|
|
height: 100%;
|
|
background-image: linear-gradient(to bottom, #667eea, #764ba2);
|
|
border-radius: 3rpx;
|
|
transition: width 0.3s ease;
|
|
}
|
|
|
|
.progress-text {
|
|
font-size: 22rpx;
|
|
color: #666;
|
|
}
|
|
|
|
.unlock-date {
|
|
margin-top: 5rpx;
|
|
}
|
|
|
|
.date-text {
|
|
font-size: 22rpx;
|
|
color: #28a745;
|
|
font-weight: 400;
|
|
}
|
|
|
|
.difficulty-indicator {
|
|
position: absolute;
|
|
top: 15rpx;
|
|
right: 15rpx;
|
|
padding: 5rpx 10rpx;
|
|
border-radius: 8rpx;
|
|
font-size: 20rpx;
|
|
}
|
|
|
|
.difficulty-easy {
|
|
background-color: rgba(40, 167, 69, 0.1);
|
|
color: #28a745;
|
|
}
|
|
|
|
.difficulty-medium {
|
|
background-color: rgba(255, 193, 7, 0.1);
|
|
color: #ffc107;
|
|
}
|
|
|
|
.difficulty-hard {
|
|
background-color: rgba(255, 87, 34, 0.1);
|
|
color: #ff5722;
|
|
}
|
|
|
|
.difficulty-expert {
|
|
background-color: rgba(156, 39, 176, 0.1);
|
|
color: #9c27b0;
|
|
}
|
|
|
|
.difficulty-text {
|
|
font-weight: 400;
|
|
}
|
|
|
|
/* Recent Section */
|
|
.recent-section {
|
|
margin-bottom: 25rpx;
|
|
}
|
|
|
|
.recent-achievements {
|
|
background-color: white;
|
|
border-radius: 20rpx;
|
|
padding: 25rpx;
|
|
box-shadow: 0 2rpx 15rpx rgba(0, 0, 0, 0.05);
|
|
}
|
|
.recent-item {
|
|
display: flex;
|
|
align-items: center;
|
|
padding: 15rpx 0;
|
|
border-bottom: 1rpx solid #f0f0f0;
|
|
}
|
|
|
|
.recent-icon {
|
|
width: 60rpx;
|
|
height: 60rpx;
|
|
border-radius: 30rpx;
|
|
background-image: linear-gradient(to bottom right, #667eea, #764ba2);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
margin-right: 15rpx;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.recent-icon .icon {
|
|
font-size: 28rpx;
|
|
color: white;
|
|
}
|
|
|
|
.recent-content {
|
|
flex: 1;
|
|
}
|
|
|
|
.recent-title {
|
|
font-size: 28rpx;
|
|
font-weight: bold;
|
|
color: #333;
|
|
margin-bottom: 5rpx;
|
|
display: block;
|
|
}
|
|
|
|
.recent-date {
|
|
font-size: 24rpx;
|
|
color: #666;
|
|
}
|
|
|
|
.recent-points {
|
|
text-align: right;
|
|
}
|
|
|
|
.points {
|
|
font-size: 26rpx;
|
|
font-weight: bold;
|
|
color: #ffc107;
|
|
}
|
|
|
|
/* Achievement Detail Modal */
|
|
.modal-overlay {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
background-color: rgba(0, 0, 0, 0.5);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
z-index: 1000;
|
|
}
|
|
|
|
.achievement-detail-modal {
|
|
width: 90%;
|
|
max-width: 600rpx;
|
|
background-color: white;
|
|
border-radius: 20rpx;
|
|
overflow: hidden;
|
|
max-height: 80vh;
|
|
}
|
|
|
|
.modal-header {
|
|
background-image: linear-gradient(to bottom right, #667eea, #764ba2);
|
|
padding: 30rpx;
|
|
text-align: center;
|
|
position: relative;
|
|
}
|
|
|
|
.achievement-detail-icon {
|
|
width: 120rpx;
|
|
height: 120rpx;
|
|
border-radius: 60rpx;
|
|
background-color: rgba(255, 255, 255, 0.2);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
margin: 0 auto;
|
|
opacity: 0.6;
|
|
}
|
|
|
|
.achievement-detail-icon.unlocked {
|
|
opacity: 1;
|
|
background-color: rgba(255, 255, 255, 0.3);
|
|
}
|
|
|
|
.detail-icon {
|
|
font-size: 60rpx;
|
|
color: white;
|
|
}
|
|
|
|
.modal-close-btn {
|
|
position: absolute;
|
|
top: 20rpx;
|
|
right: 20rpx;
|
|
width: 60rpx;
|
|
height: 60rpx;
|
|
border-radius: 30rpx;
|
|
background-color: rgba(255, 255, 255, 0.2);
|
|
border: none;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 28rpx;
|
|
color: white;
|
|
}
|
|
|
|
.modal-content {
|
|
padding: 30rpx;
|
|
}
|
|
|
|
.detail-title {
|
|
font-size: 36rpx;
|
|
font-weight: bold;
|
|
color: #333;
|
|
text-align: center;
|
|
margin-bottom: 15rpx;
|
|
display: block;
|
|
}
|
|
|
|
.detail-description {
|
|
font-size: 28rpx;
|
|
color: #666;
|
|
text-align: center;
|
|
line-height: 1.5;
|
|
margin-bottom: 25rpx;
|
|
}
|
|
|
|
.detail-requirements {
|
|
margin-bottom: 20rpx;
|
|
}
|
|
|
|
.requirements-title {
|
|
font-size: 26rpx;
|
|
font-weight: bold;
|
|
color: #333;
|
|
margin-bottom: 8rpx;
|
|
display: block;
|
|
}
|
|
|
|
.requirements-text {
|
|
font-size: 26rpx;
|
|
color: #666;
|
|
line-height: 1.5;
|
|
}
|
|
|
|
.detail-reward {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 20rpx;
|
|
}
|
|
|
|
.reward-label {
|
|
font-size: 26rpx;
|
|
color: #333;
|
|
font-weight: bold;
|
|
}
|
|
|
|
.reward-points {
|
|
font-size: 28rpx;
|
|
font-weight: bold;
|
|
color: #ffc107;
|
|
}
|
|
|
|
.detail-progress {
|
|
margin-bottom: 20rpx;
|
|
}
|
|
|
|
.progress-label {
|
|
font-size: 26rpx;
|
|
font-weight: bold;
|
|
color: #333;
|
|
margin-bottom: 10rpx;
|
|
display: block;
|
|
}
|
|
|
|
.detail-progress-bar {
|
|
height: 10rpx;
|
|
background-color: #f0f0f0;
|
|
border-radius: 5rpx;
|
|
overflow: hidden;
|
|
margin-bottom: 8rpx;
|
|
}
|
|
|
|
.detail-progress-fill {
|
|
height: 100%;
|
|
background-image: linear-gradient(to bottom, #667eea, #764ba2);
|
|
border-radius: 5rpx;
|
|
transition: width 0.3s ease;
|
|
}
|
|
|
|
.detail-progress-text {
|
|
font-size: 24rpx;
|
|
color: #666;
|
|
text-align: center;
|
|
}
|
|
|
|
.detail-unlock-info {
|
|
text-align: center;
|
|
padding: 20rpx;
|
|
background-color: #f8f9ff;
|
|
border-radius: 12rpx;
|
|
}
|
|
|
|
.unlock-label {
|
|
font-size: 26rpx;
|
|
color: #333;
|
|
font-weight: bold;
|
|
margin-bottom: 8rpx;
|
|
display: block;
|
|
}
|
|
|
|
.unlock-time {
|
|
font-size: 26rpx;
|
|
color: #28a745;
|
|
font-weight: 400;
|
|
}
|
|
</style>
|