Files
akmon/pages/sport/student/achievements.uvue
2026-01-20 08:04:15 +08:00

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>