1265 lines
33 KiB
Vue
1265 lines
33 KiB
Vue
<template>
|
|
<view class="growth-page" :style="{ background: themeConfig.bgColor }">
|
|
<view class="bg-decoration">
|
|
<view class="deco-blob deco-1" :style="{ background: `radial-gradient(circle, ${themeConfig.primaryColor}28 0%, transparent 70%)` }"></view>
|
|
<view class="deco-blob deco-2" :style="{ background: `radial-gradient(circle, ${themeConfig.secondaryColor}22 0%, transparent 70%)` }"></view>
|
|
<view class="deco-blob deco-3" :style="{ background: `radial-gradient(circle, ${themeConfig.accentColor}18 0%, transparent 70%)` }"></view>
|
|
</view>
|
|
|
|
<scroll-view class="scroll-container" scroll-y :show-scrollbar="false">
|
|
<view class="header-card" :style="{ background: themeConfig.gradientVibrant, boxShadow: `0 16rpx 48rpx ${themeConfig.shadowColor}` }">
|
|
<view class="card-bg">
|
|
<view class="bg-orb"></view>
|
|
<view class="bg-grid"></view>
|
|
<view class="bg-float-shape shape-1" :style="{ background: `linear-gradient(135deg, rgba(255,255,255,0.25) 0%, rgba(255,255,255,0.08) 100%)` }"></view>
|
|
<view class="bg-float-shape shape-2" :style="{ background: `linear-gradient(135deg, ${themeConfig.secondaryColor}40 0%, transparent 100%)` }"></view>
|
|
</view>
|
|
<view class="card-content">
|
|
<view class="user-card">
|
|
<view class="avatar-ring" :style="{ background: 'rgba(255, 255, 255, 0.25)' }">
|
|
<FaIcon v-if="growthInfo.levelIcon && growthInfo.levelIcon.startsWith && !growthInfo.levelIcon.startsWith('\\u')" :icon="growthInfo.levelIcon" :size="40" />
|
|
<FaIcon v-else icon="trophy" :size="40" color="#fff" />
|
|
</view>
|
|
<view class="user-info">
|
|
<text class="user-name">{{ growthInfo.levelTitle || '记录小白' }}</text>
|
|
<view class="level-badge" :style="{ background: 'rgba(255, 255, 255, 0.2)' }">
|
|
<text>Lv.{{ growthInfo.currentLevel }}</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
|
|
<view class="exp-section">
|
|
<view class="exp-header">
|
|
<text class="exp-label">当前经验</text>
|
|
<view class="exp-value">
|
|
<text class="exp-current">{{ animatedExp }}</text>
|
|
<text class="exp-divider">/</text>
|
|
<text class="exp-max">{{ growthInfo.nextLevelExp }} EXP</text>
|
|
</view>
|
|
</view>
|
|
<view class="progress-wrapper">
|
|
<view class="progress-bar" :style="{ background: 'rgba(255, 255, 255, 0.25)' }">
|
|
<view class="progress-fill" :style="{ width: animatedProgress + '%', background: 'linear-gradient(90deg, #fff 0%, rgba(255,255,255,0.8) 100%)' }">
|
|
<view class="progress-shine"></view>
|
|
</view>
|
|
</view>
|
|
<view class="progress-text">{{ animatedProgress }}%</view>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
|
|
<view class="content-area">
|
|
<view class="stats-row">
|
|
<view class="stat-item" :style="{ background: themeConfig.cardBgColor, boxShadow: `0 6rpx 24rpx ${themeConfig.shadowLight}` }">
|
|
<view class="stat-icon-wrap" :style="{ background: `${themeConfig.primaryColor}12` }">
|
|
<FaIcon icon="calendar-check" :size="22" :color="themeConfig.primaryColor" />
|
|
</view>
|
|
<text class="stat-num" :style="{ color: themeConfig.textPrimary }">{{ growthInfo.totalDays }}</text>
|
|
<text class="stat-label" :style="{ color: themeConfig.textSecondary }">累计天数</text>
|
|
</view>
|
|
<view class="stat-item" :style="{ background: themeConfig.cardBgColor, boxShadow: `0 6rpx 24rpx ${themeConfig.shadowLight}` }">
|
|
<view class="stat-icon-wrap" :style="{ background: `${themeConfig.warningColor}12` }">
|
|
<FaIcon icon="fire" :size="22" :color="themeConfig.warningColor" />
|
|
</view>
|
|
<text class="stat-num" :style="{ color: themeConfig.textPrimary }">{{ growthInfo.streakDays }}</text>
|
|
<text class="stat-label" :style="{ color: themeConfig.textSecondary }">连续打卡</text>
|
|
</view>
|
|
<view class="stat-item" :style="{ background: themeConfig.cardBgColor, boxShadow: `0 6rpx 24rpx ${themeConfig.shadowLight}` }">
|
|
<view class="stat-icon-wrap" :style="{ background: `${themeConfig.successColor}12` }">
|
|
<FaIcon icon="trophy" :size="22" :color="themeConfig.successColor" />
|
|
</view>
|
|
<text class="stat-num" :style="{ color: themeConfig.textPrimary }">{{ growthInfo.maxStreakDays }}</text>
|
|
<text class="stat-label" :style="{ color: themeConfig.textSecondary }">最高连续</text>
|
|
</view>
|
|
</view>
|
|
|
|
<view class="rules-entry" :style="{ background: themeConfig.cardBgColor, boxShadow: `0 8rpx 32rpx ${themeConfig.shadowLight}` }" @click="goToRules">
|
|
<view class="entry-icon" :style="{ background: `linear-gradient(135deg, ${themeConfig.primaryColor}15 0%, ${themeConfig.secondaryColor}10 100%)` }">
|
|
<FaIcon icon="book-open" :size="24" :color="themeConfig.primaryColor" />
|
|
</view>
|
|
<view class="entry-text">
|
|
<text class="entry-title" :style="{ color: themeConfig.textPrimary }">成长规则</text>
|
|
<text class="entry-desc" :style="{ color: themeConfig.textSecondary }">了解升级攻略和特权说明</text>
|
|
</view>
|
|
<view class="entry-arrow" :style="{ background: `${themeConfig.primaryColor}12` }">
|
|
<FaIcon icon="chevron-right" :size="14" :color="themeConfig.primaryColor" />
|
|
</view>
|
|
</view>
|
|
|
|
<view class="module-card" :style="{ background: themeConfig.cardBgColor, boxShadow: `0 8rpx 32rpx ${themeConfig.shadowLight}` }">
|
|
<view class="module-header">
|
|
<view class="module-icon-wrap" :style="{ background: `${themeConfig.primaryColor}15` }">
|
|
<FaIcon icon="chart-column" :size="22" :color="themeConfig.primaryColor" />
|
|
</view>
|
|
<view class="module-title-wrap">
|
|
<text class="module-title" :style="{ color: themeConfig.textPrimary }">今日进度</text>
|
|
<text class="module-subtitle" :style="{ color: themeConfig.textSecondary }">{{ todayDate }}</text>
|
|
</view>
|
|
</view>
|
|
|
|
<view class="today-exp">
|
|
<view class="exp-display">
|
|
<text class="exp-number" :style="{ background: themeConfig.gradientSoft, WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent' }">{{ growthInfo.todayExp }}</text>
|
|
<text class="exp-slash" :style="{ color: themeConfig.textSecondary }">/</text>
|
|
<text class="exp-total" :style="{ color: themeConfig.textSecondary }">{{ growthInfo.todayProgress.maxDailyExp || 120 }}</text>
|
|
<text class="exp-unit-text" :style="{ color: themeConfig.textSecondary }">EXP</text>
|
|
</view>
|
|
<view class="progress-bar-small" :style="{ background: themeConfig.borderLight }">
|
|
<view class="progress-fill-small" :style="{ width: dailyExpPercent + '%', background: themeConfig.gradientSoft }"></view>
|
|
</view>
|
|
</view>
|
|
|
|
<view class="task-list">
|
|
<view class="task-item" v-for="(item, index) in todayProgressItems" :key="index">
|
|
<view class="task-icon-box" :style="{ background: item.bgColor }">
|
|
<FaIcon :icon="item.iconName" :size="20" :color="item.color" />
|
|
</view>
|
|
<view class="task-info">
|
|
<text class="task-name" :style="{ color: themeConfig.textPrimary }">{{ item.name }}</text>
|
|
<text class="task-detail" :style="{ color: themeConfig.textSecondary }">{{ item.detail }}</text>
|
|
</view>
|
|
<text class="task-value" :style="{ color: item.color }">{{ item.value }}</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
|
|
<view class="module-card" :style="{ background: themeConfig.cardBgColor, boxShadow: `0 8rpx 32rpx ${themeConfig.shadowLight}` }">
|
|
<view class="module-header">
|
|
<view class="module-icon-wrap" :style="{ background: `${themeConfig.warningColor}15` }">
|
|
<FaIcon icon="medal" :size="22" :color="themeConfig.warningColor" />
|
|
</view>
|
|
<view class="module-title-wrap">
|
|
<text class="module-title" :style="{ color: themeConfig.textPrimary }">成就</text>
|
|
<text class="achievement-count" :style="{ color: themeConfig.primaryColor }">{{ achievementsData.unlocked.length }}/{{ achievementsData.unlocked.length + achievementsData.locked.length }}</text>
|
|
</view>
|
|
</view>
|
|
|
|
<view class="tab-group">
|
|
<view
|
|
class="tab-item"
|
|
:class="{ active: activeTab === 'unlocked' }"
|
|
:style="activeTab === 'unlocked' ? { background: themeConfig.gradientSoft, color: '#fff', boxShadow: `0 8rpx 24rpx ${themeConfig.shadowColor}` } : { background: `${themeConfig.textPrimary}05`, color: themeConfig.textSecondary }"
|
|
@click="activeTab = 'unlocked'"
|
|
>
|
|
已解锁
|
|
<view class="tab-badge" :style="activeTab === 'unlocked' ? { background: 'rgba(255,255,255,0.2)' } : {}">{{ achievementsData.unlocked.length }}</view>
|
|
</view>
|
|
<view
|
|
class="tab-item"
|
|
:class="{ active: activeTab === 'locked' }"
|
|
:style="activeTab === 'locked' ? { background: themeConfig.gradientSoft, color: '#fff', boxShadow: `0 8rpx 24rpx ${themeConfig.shadowColor}` } : { background: `${themeConfig.textPrimary}05`, color: themeConfig.textSecondary }"
|
|
@click="activeTab = 'locked'"
|
|
>
|
|
未解锁
|
|
<view class="tab-badge" :style="activeTab === 'locked' ? { background: 'rgba(255,255,255,0.2)' } : {}">{{ achievementsData.locked.length }}</view>
|
|
</view>
|
|
</view>
|
|
|
|
<view class="achievement-grid">
|
|
<view class="achievement-card"
|
|
v-for="(item, index) in (activeTab === 'unlocked' ? achievementsData.unlocked : achievementsData.locked)"
|
|
:key="item.id || index"
|
|
:style="{ animationDelay: index * 0.08 + 's' }"
|
|
>
|
|
<view class="achievement-top">
|
|
<view class="achievement-avatar" :style="activeTab === 'unlocked' ? { background: getCategoryBg(item.category) } : { background: themeConfig.borderLight }">
|
|
<text class="achievement-emoji" :class="{ 'is-locked': activeTab === 'locked' }">{{ item.icon }}</text>
|
|
<view class="achievement-glow" v-if="activeTab === 'unlocked'"></view>
|
|
</view>
|
|
<view class="achievement-category-tag" :style="{ background: getCategoryBg(item.category), color: getCategoryColor(item.category) }">
|
|
<text>{{ getCategoryName(item.category) }}</text>
|
|
</view>
|
|
</view>
|
|
<view class="achievement-text">
|
|
<text class="achievement-name" :style="{ color: activeTab === 'unlocked' ? themeConfig.textPrimary : themeConfig.textSecondary }">{{ item.name }}</text>
|
|
<text class="achievement-desc" :style="{ color: themeConfig.textSecondary }">{{ item.description }}</text>
|
|
</view>
|
|
<view class="achievement-reward" :style="activeTab === 'unlocked' ? { background: themeConfig.gradientSoft, color: '#fff' } : { background: themeConfig.borderLight, color: themeConfig.textSecondary }">
|
|
<text>+{{ item.expReward }} EXP</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
<EmptyState v-if="(activeTab === 'unlocked' ? achievementsData.unlocked : achievementsData.locked).length === 0" type="default" :title="activeTab === 'unlocked' ? '暂无已解锁成就' : '暂无未解锁成就'" :subtitle="activeTab === 'unlocked' ? '完成今日任务开始解锁成就吧' : '所有成就都已解锁,继续保持'" compact />
|
|
</view>
|
|
|
|
<view class="module-card" :style="{ background: themeConfig.cardBgColor, boxShadow: `0 8rpx 32rpx ${themeConfig.shadowLight}` }">
|
|
<view class="module-header">
|
|
<view class="module-icon-wrap" :style="{ background: `${themeConfig.dangerColor}15` }">
|
|
<FaIcon icon="gift" :size="22" :color="themeConfig.dangerColor" />
|
|
</view>
|
|
<text class="module-title" :style="{ color: themeConfig.textPrimary }">特权</text>
|
|
</view>
|
|
|
|
<view class="privilege-section">
|
|
<view class="section-label">
|
|
<text class="label-text" :style="{ color: themeConfig.textSecondary }">当前等级特权</text>
|
|
<view class="section-tag active" :style="{ background: `${themeConfig.successColor}15`, color: themeConfig.successColor }">
|
|
<text>已解锁</text>
|
|
</view>
|
|
</view>
|
|
<view class="privilege-tags">
|
|
<view class="privilege-tag" v-for="(privilege, index) in growthInfo.currentPrivileges" :key="index" :style="{ background: `${themeConfig.successColor}10` }">
|
|
<FaIcon icon="check" :size="14" :color="themeConfig.successColor" />
|
|
<text class="tag-text" :style="{ color: themeConfig.textPrimary }">{{ privilege }}</text>
|
|
</view>
|
|
<view class="privilege-tag empty" v-if="!growthInfo.currentPrivileges || growthInfo.currentPrivileges.length === 0" :style="{ background: themeConfig.borderLight }">
|
|
<text class="tag-text" :style="{ color: themeConfig.textSecondary }">暂无特权</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
|
|
<view class="privilege-section next-level" v-if="growthInfo.nextLevelPrivileges && growthInfo.nextLevelPrivileges.length > 0">
|
|
<view class="section-label">
|
|
<text class="label-text" :style="{ color: themeConfig.textSecondary }">下一级特权</text>
|
|
<view class="section-tag locked" :style="{ background: `${themeConfig.textPrimary}08`, color: themeConfig.textSecondary }">
|
|
<FaIcon icon="lock" :size="14" :color="themeConfig.textSecondary" />
|
|
<text style="margin-left: 6rpx;">即将解锁</text>
|
|
</view>
|
|
</view>
|
|
<view class="privilege-tags">
|
|
<view class="privilege-tag locked" v-for="(privilege, index) in growthInfo.nextLevelPrivileges" :key="index" :style="{ background: themeConfig.borderLight }">
|
|
<text class="tag-text" :style="{ color: themeConfig.textSecondary }">{{ privilege }}</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
|
|
<view class="bottom-padding"></view>
|
|
</scroll-view>
|
|
</view>
|
|
</template>
|
|
|
|
<script>
|
|
import { getThemeConfigFromStorage, THEME_PRESETS } from '@/utils/theme'
|
|
import { refreshNavBarTheme, forceApplyTabBarTheme } from '@/utils/themeGlobal'
|
|
import { getGrowthInfo, getGrowthAchievements } from '@/api/index'
|
|
import FaIcon from '@/components/FaIcon.vue'
|
|
import EmptyState from '@/components/EmptyState.vue'
|
|
|
|
export default {
|
|
components: { FaIcon, EmptyState },
|
|
data() {
|
|
return {
|
|
themeConfig: getThemeConfigFromStorage() || THEME_PRESETS['default'],
|
|
todayDate: '',
|
|
activeTab: 'unlocked',
|
|
achievementsData: {
|
|
unlocked: [],
|
|
locked: []
|
|
},
|
|
animatedProgress: 0,
|
|
animatedExp: 0,
|
|
growthInfo: {
|
|
currentLevel: 1,
|
|
levelTitle: '记录小白',
|
|
levelIcon: '',
|
|
levelColor: '#94A3B8',
|
|
totalExp: 0,
|
|
nextLevelExp: 200,
|
|
levelProgress: 0,
|
|
totalDays: 0,
|
|
streakDays: 0,
|
|
maxStreakDays: 0,
|
|
todayExp: 0,
|
|
todayProgress: {
|
|
tasksCompleted: 0,
|
|
taskExp: 0,
|
|
billsRecorded: 0,
|
|
billExp: 0,
|
|
moodRecorded: false,
|
|
moodExp: 0,
|
|
totalExp: 0,
|
|
maxDailyExp: 120
|
|
},
|
|
currentPrivileges: [],
|
|
nextLevelPrivileges: []
|
|
}
|
|
}
|
|
},
|
|
computed: {
|
|
expPercent() {
|
|
if (!this.growthInfo.nextLevelExp || this.growthInfo.nextLevelExp <= 0) return 0
|
|
return Math.min(100, Math.round((this.growthInfo.totalExp / this.growthInfo.nextLevelExp) * 100))
|
|
},
|
|
dailyExpPercent() {
|
|
return Math.min(100, Math.round((this.growthInfo.todayExp / 150) * 100))
|
|
},
|
|
todayProgressItems() {
|
|
return [
|
|
{
|
|
iconName: 'list-check',
|
|
name: '任务完成',
|
|
detail: `+${this.growthInfo.todayProgress.taskExp} EXP`,
|
|
value: `${this.growthInfo.todayProgress.tasksCompleted}个`,
|
|
color: this.themeConfig.primaryColor,
|
|
bgColor: `${this.themeConfig.primaryColor}08`
|
|
},
|
|
{
|
|
iconName: 'money-bill',
|
|
name: '账单记录',
|
|
detail: `+${this.growthInfo.todayProgress.billExp} EXP`,
|
|
value: `${this.growthInfo.todayProgress.billsRecorded}笔`,
|
|
color: '#f59e0b',
|
|
bgColor: 'rgba(245, 158, 11, 0.08)'
|
|
},
|
|
{
|
|
iconName: this.growthInfo.todayProgress.moodRecorded ? 'face-smile' : 'calendar-xmark',
|
|
name: '心情记录',
|
|
detail: `+${this.growthInfo.todayProgress.moodExp} EXP`,
|
|
value: this.growthInfo.todayProgress.moodRecorded ? '已完成' : '待记录',
|
|
color: '#ec4899',
|
|
bgColor: 'rgba(236, 72, 153, 0.08)'
|
|
},
|
|
{
|
|
iconName: this.growthInfo.todayProgress.reviewRecorded ? 'face-smile' : 'calendar-xmark',
|
|
name: '复盘记录',
|
|
detail: `+${this.growthInfo.todayProgress.reviewExp} EXP`,
|
|
value: this.growthInfo.todayProgress.reviewRecorded ? '已完成' : '待记录',
|
|
color: '#673ab7',
|
|
bgColor: 'rgba(153, 72, 153, 0.08)'
|
|
}
|
|
]
|
|
}
|
|
},
|
|
onLoad() {
|
|
this.setTodayDate()
|
|
this.loadGrowthInfo()
|
|
this.loadAchievements()
|
|
|
|
this.themeChangeHandler = (themeData) => {
|
|
let newTheme = null
|
|
if (themeData?.key && THEME_PRESETS[themeData.key]) {
|
|
newTheme = THEME_PRESETS[themeData.key]
|
|
} else if (themeData?.config) {
|
|
newTheme = themeData.config
|
|
}
|
|
if (newTheme) {
|
|
this.themeConfig = newTheme
|
|
}
|
|
}
|
|
uni.$on('themeChanged', this.themeChangeHandler)
|
|
},
|
|
onShow() {
|
|
const latestTheme = getThemeConfigFromStorage() || THEME_PRESETS['default']
|
|
if (JSON.stringify(latestTheme) !== JSON.stringify(this.themeConfig)) {
|
|
this.themeConfig = latestTheme
|
|
}
|
|
refreshNavBarTheme()
|
|
setTimeout(() => {
|
|
forceApplyTabBarTheme(latestTheme)
|
|
}, 100)
|
|
},
|
|
onUnload() {
|
|
uni.$off('themeChanged', this.themeChangeHandler)
|
|
},
|
|
methods: {
|
|
setTodayDate() {
|
|
const now = new Date()
|
|
const month = now.getMonth() + 1
|
|
const day = now.getDate()
|
|
const weekDays = ['周日', '周一', '周二', '周三', '周四', '周五', '周六']
|
|
const weekDay = weekDays[now.getDay()]
|
|
this.todayDate = `${month}月${day}日 ${weekDay}`
|
|
},
|
|
async loadGrowthInfo() {
|
|
try {
|
|
const res = await getGrowthInfo()
|
|
if (res) {
|
|
const data = res.data || res
|
|
this.growthInfo = {
|
|
currentLevel: data.level || data.currentLevel || 1,
|
|
levelTitle: data.title || data.levelTitle || '记录小白',
|
|
levelIcon: data.titleIcon || data.levelIcon || '',
|
|
levelColor: data.color || data.levelColor || '#94A3B8',
|
|
totalExp: data.experience || data.totalExp || 0,
|
|
nextLevelExp: data.nextLevelExp || 200,
|
|
levelProgress: data.levelProgress || 0,
|
|
totalDays: data.totalDays || 0,
|
|
streakDays: data.streakDays || 0,
|
|
maxStreakDays: data.maxStreakDays || 0,
|
|
todayExp: data.todayExp || (data.todayProgress ? data.todayProgress.billExp + data.todayProgress.taskExp + data.todayProgress.moodExp + data.todayProgress.reviewExp: 0),
|
|
todayProgress: {
|
|
tasksCompleted: data.todayProgress?.tasksCompleted || 0,
|
|
taskExp: data.todayProgress?.taskExp || 0,
|
|
billsRecorded: data.todayProgress?.billsRecorded || 0,
|
|
billExp: data.todayProgress?.billExp || 0,
|
|
moodRecorded: data.todayProgress?.moodRecorded || false,
|
|
moodExp: data.todayProgress?.moodExp || 0,
|
|
reviewRecorded: data.todayProgress?.reviewRecorded || false,
|
|
reviewExp: data.todayProgress?.reviewExp || 0,
|
|
totalExp: data.todayProgress?.totalExp || 0,
|
|
maxDailyExp: data.todayProgress?.maxDailyExp || 120
|
|
},
|
|
currentPrivileges: data.currentPrivileges || data.privileges || [],
|
|
nextLevelPrivileges: data.nextLevelPrivileges || []
|
|
}
|
|
|
|
this.animateProgress()
|
|
this.animateExp()
|
|
}
|
|
} catch (e) {
|
|
console.log('加载成长信息失败:', e)
|
|
}
|
|
},
|
|
animateProgress() {
|
|
const targetProgress = this.growthInfo.levelProgress || this.expPercent
|
|
const duration = 1000
|
|
const startTime = Date.now()
|
|
const startValue = 0
|
|
|
|
const animate = () => {
|
|
const elapsed = Date.now() - startTime
|
|
const progress = Math.min(elapsed / duration, 1)
|
|
const easeOut = 1 - Math.pow(1 - progress, 3)
|
|
this.animatedProgress = Math.round(startValue + (targetProgress - startValue) * easeOut)
|
|
|
|
if (progress < 1) {
|
|
setTimeout(animate, 16)
|
|
}
|
|
}
|
|
|
|
setTimeout(() => {
|
|
animate()
|
|
}, 300)
|
|
},
|
|
animateExp() {
|
|
const targetExp = this.growthInfo.totalExp
|
|
const duration = 800
|
|
const startTime = Date.now()
|
|
const startValue = 0
|
|
|
|
const animate = () => {
|
|
const elapsed = Date.now() - startTime
|
|
const progress = Math.min(elapsed / duration, 1)
|
|
const easeOut = 1 - Math.pow(1 - progress, 3)
|
|
this.animatedExp = Math.round(startValue + (targetExp - startValue) * easeOut)
|
|
|
|
if (progress < 1) {
|
|
setTimeout(animate, 16)
|
|
}
|
|
}
|
|
|
|
setTimeout(() => {
|
|
animate()
|
|
}, 300)
|
|
},
|
|
async loadAchievements() {
|
|
try {
|
|
const res = await getGrowthAchievements()
|
|
if (res) {
|
|
const data = res.data || res
|
|
this.achievementsData = {
|
|
unlocked: this.formatAchievements(data.unlockedList || data.unlocked || []),
|
|
locked: this.formatAchievements(data.lockedList || data.locked || [])
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.log('加载成就列表失败:', e)
|
|
}
|
|
},
|
|
formatAchievements(list) {
|
|
return list.map(item => {
|
|
// 后端直接返回 emoji 字符作为 icon 字段
|
|
const iconChar = item.icon && typeof item.icon === 'string' && item.icon.length > 0 ? item.icon : '🏆'
|
|
return {
|
|
id: item.id || (item.category || 'ach') + '_' + Math.random().toString(36).slice(2, 8),
|
|
icon: iconChar,
|
|
name: item.name || item.title || this.getCategoryName(item.category),
|
|
description: item.description || item.desc,
|
|
expReward: item.expReward || item.exp || item.reward || 0,
|
|
category: item.category,
|
|
rarity: item.rarity || '初级'
|
|
}
|
|
})
|
|
},
|
|
getCategoryIcon(category) {
|
|
// 返回 emoji 字符作为兜底分类图标
|
|
const icons = {
|
|
bill: '💰',
|
|
task: '📋',
|
|
mood: '🌈',
|
|
streak: '🔥',
|
|
review: '✨',
|
|
default: '🏆'
|
|
}
|
|
return icons[category] || icons.default
|
|
},
|
|
getCategoryName(category) {
|
|
const names = {
|
|
bill: '账单达人',
|
|
task: '任务能手',
|
|
mood: '心情记录',
|
|
streak: '连续之星',
|
|
review: '复盘感悟',
|
|
default: '成就'
|
|
}
|
|
return names[category] || names.default
|
|
},
|
|
getCategoryColor(category) {
|
|
const map = {
|
|
streak: '#F59E0B',
|
|
task: '#9B8AFB',
|
|
mood: '#EC4899',
|
|
bill: '#4ECDC4',
|
|
review: '#7B6CEB'
|
|
}
|
|
return map[category] || this.themeConfig.primaryColor
|
|
},
|
|
getCategoryBg(category) {
|
|
const map = {
|
|
streak: 'rgba(245, 158, 11, 0.15)',
|
|
task: 'rgba(155, 138, 251, 0.15)',
|
|
mood: 'rgba(236, 72, 153, 0.15)',
|
|
bill: 'rgba(78, 205, 196, 0.15)',
|
|
review: 'rgba(123, 108, 235, 0.15)'
|
|
}
|
|
return map[category] || 'rgba(155, 138, 251, 0.12)'
|
|
},
|
|
goToRules() {
|
|
uni.navigateTo({
|
|
url: '/pages/growth/rules'
|
|
})
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style lang="scss" scoped>
|
|
.growth-page {
|
|
min-height: 100vh;
|
|
position: relative;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.bg-decoration {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
pointer-events: none;
|
|
z-index: 0;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.deco-blob {
|
|
position: absolute;
|
|
border-radius: 50%;
|
|
filter: blur(120rpx);
|
|
opacity: 0.7;
|
|
}
|
|
|
|
.deco-1 {
|
|
width: 550rpx;
|
|
height: 550rpx;
|
|
top: -150rpx;
|
|
right: -120rpx;
|
|
}
|
|
|
|
.deco-2 {
|
|
width: 450rpx;
|
|
height: 450rpx;
|
|
bottom: 280rpx;
|
|
left: -100rpx;
|
|
}
|
|
|
|
.deco-3 {
|
|
width: 350rpx;
|
|
height: 350rpx;
|
|
top: 380rpx;
|
|
right: -80rpx;
|
|
}
|
|
|
|
.scroll-container {
|
|
height: 100vh;
|
|
position: relative;
|
|
z-index: 5;
|
|
}
|
|
|
|
.header-card {
|
|
margin: 20rpx 24rpx;
|
|
border-radius: 12rpx;
|
|
padding: 32rpx;
|
|
position: relative;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.card-bg {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.bg-orb {
|
|
position: absolute;
|
|
width: 350rpx;
|
|
height: 350rpx;
|
|
border-radius: 50%;
|
|
background: radial-gradient(circle at 40% 40%, rgba(255,255,255,0.3) 0%, rgba(255,255,255,0.1) 30%, transparent 70%);
|
|
top: -180rpx;
|
|
right: -100rpx;
|
|
filter: blur(20rpx);
|
|
animation: orbFloat 8s ease-in-out infinite;
|
|
}
|
|
|
|
@keyframes orbFloat {
|
|
0%, 100% { transform: translate(0, 0) scale(1); }
|
|
50% { transform: translate(-20rpx, 15rpx) scale(1.08); }
|
|
}
|
|
|
|
.bg-grid {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
background-image:
|
|
linear-gradient(rgba(255,255,255,0.03) 1px, transparent 1px),
|
|
linear-gradient(90deg, rgba(255,255,255,0.03) 1px, transparent 1px);
|
|
background-size: 32rpx 32rpx;
|
|
}
|
|
|
|
.bg-float-shape {
|
|
position: absolute;
|
|
border-radius: 50%;
|
|
opacity: 0.5;
|
|
}
|
|
|
|
.shape-1 {
|
|
width: 220rpx;
|
|
height: 220rpx;
|
|
top: -100rpx;
|
|
left: -80rpx;
|
|
animation: shapeFloat1 8s ease-in-out infinite;
|
|
}
|
|
|
|
.shape-2 {
|
|
width: 180rpx;
|
|
height: 180rpx;
|
|
top: 40rpx;
|
|
right: 80rpx;
|
|
animation: shapeFloat2 7s ease-in-out infinite;
|
|
}
|
|
|
|
@keyframes shapeFloat1 {
|
|
0%, 100% { transform: translate(0, 0) rotate(0deg); }
|
|
50% { transform: translate(15rpx, -12rpx) rotate(8deg); }
|
|
}
|
|
|
|
@keyframes shapeFloat2 {
|
|
0%, 100% { transform: translate(0, 0) scale(1); }
|
|
50% { transform: translate(-10rpx, 15rpx) scale(1.1); }
|
|
}
|
|
|
|
.card-content {
|
|
position: relative;
|
|
z-index: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 24rpx;
|
|
}
|
|
|
|
.user-card {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 22rpx;
|
|
}
|
|
|
|
.avatar-ring {
|
|
width: 96rpx;
|
|
height: 96rpx;
|
|
border-radius: 50%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
border: 3rpx solid rgba(255, 255, 255, 0.3);
|
|
transition: transform 0.3s ease;
|
|
}
|
|
|
|
.avatar-ring:active {
|
|
transform: scale(0.95);
|
|
}
|
|
|
|
.avatar-emoji {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.user-info {
|
|
flex: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 8rpx;
|
|
}
|
|
|
|
.user-name {
|
|
font-size: 32rpx;
|
|
font-weight: 700;
|
|
color: #fff;
|
|
}
|
|
|
|
.level-badge {
|
|
display: inline-flex;
|
|
padding: 4rpx 18rpx;
|
|
border-radius: 100rpx;
|
|
width: fit-content;
|
|
backdrop-filter: blur(10px);
|
|
}
|
|
|
|
.level-badge text {
|
|
font-size: 22rpx;
|
|
font-weight: 600;
|
|
color: #fff;
|
|
}
|
|
|
|
.exp-section {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 14rpx;
|
|
}
|
|
|
|
.exp-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
}
|
|
|
|
.exp-label {
|
|
font-size: 22rpx;
|
|
color: rgba(255, 255, 255, 0.85);
|
|
}
|
|
|
|
.exp-value {
|
|
display: flex;
|
|
align-items: baseline;
|
|
gap: 6rpx;
|
|
}
|
|
|
|
.exp-current {
|
|
font-size: 36rpx;
|
|
font-weight: 700;
|
|
color: #fff;
|
|
}
|
|
|
|
.exp-divider {
|
|
font-size: 24rpx;
|
|
color: rgba(255, 255, 255, 0.6);
|
|
}
|
|
|
|
.exp-max {
|
|
font-size: 24rpx;
|
|
color: rgba(255, 255, 255, 0.75);
|
|
}
|
|
|
|
.progress-wrapper {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 16rpx;
|
|
}
|
|
|
|
.progress-bar {
|
|
flex: 1;
|
|
height: 14rpx;
|
|
border-radius: 100rpx;
|
|
overflow: hidden;
|
|
position: relative;
|
|
}
|
|
|
|
.progress-fill {
|
|
height: 100%;
|
|
border-radius: 100rpx;
|
|
transition: width 1s cubic-bezier(0.34, 1.56, 0.64, 1);
|
|
position: relative;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.progress-shine {
|
|
position: absolute;
|
|
top: 0;
|
|
left: -100%;
|
|
width: 100%;
|
|
height: 100%;
|
|
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.4), transparent);
|
|
animation: progressShine 2s ease-in-out infinite;
|
|
}
|
|
|
|
@keyframes progressShine {
|
|
0% { left: -100%; }
|
|
100% { left: 100%; }
|
|
}
|
|
|
|
.progress-text {
|
|
font-size: 24rpx;
|
|
font-weight: 600;
|
|
color: #fff;
|
|
min-width: 60rpx;
|
|
text-align: right;
|
|
}
|
|
|
|
.content-area {
|
|
padding: 0 24rpx;
|
|
}
|
|
|
|
.stats-row {
|
|
display: flex;
|
|
gap: 14rpx;
|
|
margin-bottom: 20rpx;
|
|
}
|
|
|
|
.stat-item {
|
|
flex: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 10rpx;
|
|
padding: 24rpx 12rpx;
|
|
border-radius: 22rpx;
|
|
transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
|
|
}
|
|
|
|
.stat-item:active {
|
|
transform: scale(0.96);
|
|
}
|
|
|
|
.stat-icon-wrap {
|
|
width: 72rpx;
|
|
height: 72rpx;
|
|
border-radius: 20rpx;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
transition: transform 0.3s ease;
|
|
}
|
|
|
|
.stat-item:active .stat-icon-wrap {
|
|
transform: scale(1.1);
|
|
}
|
|
|
|
.stat-icon {
|
|
font-size: 36rpx;
|
|
}
|
|
|
|
.stat-num {
|
|
font-size: 36rpx;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.stat-label {
|
|
font-size: 22rpx;
|
|
}
|
|
|
|
.rules-entry {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 18rpx;
|
|
padding: 24rpx;
|
|
border-radius: 22rpx;
|
|
margin-bottom: 20rpx;
|
|
transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
|
|
}
|
|
|
|
.rules-entry:active {
|
|
transform: scale(0.98);
|
|
}
|
|
|
|
.entry-icon {
|
|
width: 72rpx;
|
|
height: 72rpx;
|
|
border-radius: 20rpx;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 34rpx;
|
|
}
|
|
|
|
.entry-text {
|
|
flex: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 4rpx;
|
|
}
|
|
|
|
.entry-title {
|
|
font-size: 30rpx;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.entry-desc {
|
|
font-size: 24rpx;
|
|
}
|
|
|
|
.entry-arrow {
|
|
width: 48rpx;
|
|
height: 48rpx;
|
|
border-radius: 50%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 28rpx;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.module-card {
|
|
border-radius: 12rpx;
|
|
padding: 28rpx;
|
|
margin-bottom: 20rpx;
|
|
}
|
|
|
|
.module-header {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 14rpx;
|
|
margin-bottom: 24rpx;
|
|
}
|
|
|
|
.module-icon-wrap {
|
|
width: 56rpx;
|
|
height: 56rpx;
|
|
border-radius: 16rpx;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.module-icon {
|
|
font-size: 28rpx;
|
|
}
|
|
|
|
.module-title-wrap {
|
|
flex: 1;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12rpx;
|
|
}
|
|
|
|
.module-title {
|
|
font-size: 30rpx;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.module-subtitle {
|
|
font-size: 24rpx;
|
|
}
|
|
|
|
.achievement-count {
|
|
font-size: 26rpx;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.today-exp {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 14rpx;
|
|
margin-bottom: 24rpx;
|
|
padding-bottom: 24rpx;
|
|
border-bottom: 1rpx solid rgba(0, 0, 0, 0.05);
|
|
}
|
|
|
|
.exp-display {
|
|
display: flex;
|
|
align-items: baseline;
|
|
gap: 8rpx;
|
|
}
|
|
|
|
.exp-number {
|
|
font-size: 48rpx;
|
|
font-weight: 700;
|
|
font-weight: 800;
|
|
}
|
|
|
|
.exp-slash {
|
|
font-size: 30rpx;
|
|
}
|
|
|
|
.exp-total {
|
|
font-size: 30rpx;
|
|
}
|
|
|
|
.exp-unit-text {
|
|
font-size: 24rpx;
|
|
margin-left: 6rpx;
|
|
}
|
|
|
|
.progress-bar-small {
|
|
height: 10rpx;
|
|
border-radius: 100rpx;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.progress-fill-small {
|
|
height: 100%;
|
|
border-radius: 100rpx;
|
|
transition: width 0.6s cubic-bezier(0.34, 1.56, 0.64, 1);
|
|
}
|
|
|
|
.task-list {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 18rpx;
|
|
}
|
|
|
|
.task-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 16rpx;
|
|
padding: 16rpx;
|
|
background: rgba(0, 0, 0, 0.02);
|
|
border-radius: 16rpx;
|
|
}
|
|
|
|
.task-icon-box {
|
|
width: 56rpx;
|
|
height: 56rpx;
|
|
border-radius: 14rpx;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 28rpx;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.task-info {
|
|
flex: 1;
|
|
}
|
|
|
|
.task-name {
|
|
font-size: 26rpx;
|
|
font-weight: 600;
|
|
display: block;
|
|
}
|
|
|
|
.task-detail {
|
|
font-size: 22rpx;
|
|
margin-top: 4rpx;
|
|
}
|
|
|
|
.task-value {
|
|
font-size: 26rpx;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.tab-group {
|
|
display: flex;
|
|
gap: 14rpx;
|
|
margin-bottom: 24rpx;
|
|
}
|
|
|
|
.tab-item {
|
|
flex: 1;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 8rpx;
|
|
padding: 18rpx;
|
|
border-radius: 16rpx;
|
|
font-size: 26rpx;
|
|
font-weight: 600;
|
|
transition: all 0.35s cubic-bezier(0.34, 1.56, 0.64, 1);
|
|
}
|
|
|
|
.tab-item:active {
|
|
transform: scale(0.96);
|
|
}
|
|
|
|
.tab-badge {
|
|
padding: 4rpx 12rpx;
|
|
border-radius: 100rpx;
|
|
font-size: 20rpx;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.achievement-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(2, 1fr);
|
|
gap: 16rpx;
|
|
}
|
|
|
|
.achievement-card {
|
|
position: relative;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 14rpx;
|
|
padding: 24rpx;
|
|
background: rgba(0, 0, 0, 0.03);
|
|
border-radius: 20rpx;
|
|
transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
|
|
animation: cardFadeIn 0.5s ease-out forwards;
|
|
opacity: 0;
|
|
transform: translateY(20rpx);
|
|
min-height: 200rpx;
|
|
}
|
|
|
|
@keyframes cardFadeIn {
|
|
to {
|
|
opacity: 1;
|
|
transform: translateY(0);
|
|
}
|
|
}
|
|
|
|
.achievement-card:active {
|
|
transform: scale(0.97);
|
|
background: rgba(0, 0, 0, 0.05);
|
|
}
|
|
|
|
.achievement-top {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 10rpx;
|
|
}
|
|
|
|
.achievement-avatar {
|
|
width: 72rpx;
|
|
height: 72rpx;
|
|
border-radius: 20rpx;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
position: relative;
|
|
overflow: hidden;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.achievement-emoji {
|
|
font-size: 44rpx;
|
|
line-height: 1;
|
|
display: block;
|
|
}
|
|
|
|
.achievement-emoji.is-locked {
|
|
filter: grayscale(0.85);
|
|
opacity: 0.6;
|
|
}
|
|
|
|
.achievement-glow {
|
|
position: absolute;
|
|
top: -50%;
|
|
left: -50%;
|
|
width: 200%;
|
|
height: 200%;
|
|
background: radial-gradient(circle, rgba(255, 255, 255, 0.3) 0%, transparent 70%);
|
|
animation: glowRotate 4s linear infinite;
|
|
pointer-events: none;
|
|
}
|
|
|
|
@keyframes glowRotate {
|
|
from { transform: rotate(0deg); }
|
|
to { transform: rotate(360deg); }
|
|
}
|
|
|
|
.achievement-category-tag {
|
|
padding: 4rpx 12rpx;
|
|
border-radius: 100rpx;
|
|
font-size: 18rpx;
|
|
font-weight: 600;
|
|
max-width: 60%;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.achievement-text {
|
|
flex: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 6rpx;
|
|
}
|
|
|
|
.achievement-name {
|
|
font-size: 26rpx;
|
|
font-weight: 700;
|
|
display: block;
|
|
line-height: 1.3;
|
|
}
|
|
|
|
.achievement-desc {
|
|
font-size: 22rpx;
|
|
line-height: 1.4;
|
|
display: -webkit-box;
|
|
-webkit-line-clamp: 2;
|
|
line-clamp: 2;
|
|
-webkit-box-orient: vertical;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.achievement-reward {
|
|
padding: 8rpx 18rpx;
|
|
border-radius: 100rpx;
|
|
font-size: 22rpx;
|
|
font-weight: 600;
|
|
text-align: center;
|
|
width: fit-content;
|
|
}
|
|
|
|
.privilege-section {
|
|
margin-bottom: 24rpx;
|
|
}
|
|
|
|
.privilege-section.next-level {
|
|
padding-top: 24rpx;
|
|
border-top: 1rpx solid rgba(0, 0, 0, 0.05);
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
.section-label {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12rpx;
|
|
margin-bottom: 18rpx;
|
|
}
|
|
|
|
.label-text {
|
|
font-size: 24rpx;
|
|
}
|
|
|
|
.section-tag {
|
|
padding: 6rpx 14rpx;
|
|
border-radius: 100rpx;
|
|
font-size: 22rpx;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.privilege-tags {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 12rpx;
|
|
}
|
|
|
|
.privilege-tag {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8rpx;
|
|
padding: 12rpx 20rpx;
|
|
border-radius: 100rpx;
|
|
}
|
|
|
|
.privilege-tag.empty,
|
|
.privilege-tag.locked {
|
|
background: rgba(0, 0, 0, 0.03);
|
|
}
|
|
|
|
.tag-check {
|
|
font-size: 22rpx;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.tag-text {
|
|
font-size: 24rpx;
|
|
}
|
|
|
|
.privilege-tag.empty .tag-text,
|
|
.privilege-tag.locked .tag-text {
|
|
color: #999;
|
|
}
|
|
|
|
.bottom-padding {
|
|
height: 60rpx;
|
|
}
|
|
</style> |