Sfoglia il codice sorgente

feat(frontend): 新增成就页面 achievement/index.vue

Sisyphus 1 mese fa
parent
commit
c991b876cf
1 ha cambiato i file con 265 aggiunte e 0 eliminazioni
  1. 265 0
      cfc-frontend/pages/achievement/index.vue

+ 265 - 0
cfc-frontend/pages/achievement/index.vue

@@ -0,0 +1,265 @@
+<template>
+  <view class="container">
+    <!-- 顶部导航 -->
+    <view class="nav-bar">
+      <view class="nav-back" @click="goBack">‹ 返回</view>
+      <text class="nav-title">我的成就</text>
+      <view class="nav-placeholder"></view>
+    </view>
+
+    <!-- 统计概览 -->
+    <view class="stats-card">
+      <view class="stat-item">
+        <text class="stat-num">{{ unlockedCount }}</text>
+        <text class="stat-label">已获得</text>
+      </view>
+      <view class="stat-item">
+        <text class="stat-num">{{ totalBadges }}</text>
+        <text class="stat-label">总勋章</text>
+      </view>
+      <view class="stat-item">
+        <text class="stat-num">{{ streakDays }}</text>
+        <text class="stat-label">连续打卡</text>
+      </view>
+    </view>
+
+    <!-- 勋章列表 -->
+    <view class="badge-section">
+      <view class="section-title">勋章墙</view>
+      <view class="badge-grid" v-if="badges.length > 0">
+        <view class="badge-item" v-for="badge in badges" :key="badge.id">
+          <view :class="['badge-icon-wrap', badge.unlocked ? 'unlocked' : 'locked']">
+            <text class="badge-icon">{{ badge.icon }}</text>
+          </view>
+          <text class="badge-name">{{ badge.name }}</text>
+          <text class="badge-desc" v-if="badge.unlocked">{{ badge.getDesc }}</text>
+        </view>
+      </view>
+      <view class="empty-state" v-else>
+        <text class="empty-icon">🏅</text>
+        <text class="empty-text">暂无勋章</text>
+        <text class="empty-hint">完成任务和打卡获得勋章</text>
+      </view>
+    </view>
+
+    <!-- 进度目标 -->
+    <view class="goals-section">
+      <view class="section-title">近期目标</view>
+      <view class="goal-item" v-for="goal in goals" :key="goal.id">
+        <view class="goal-header">
+          <text class="goal-name">{{ goal.name }}</text>
+          <text class="goal-progress">{{ goal.current }}/{{ goal.target }}</text>
+        </view>
+        <view class="progress-bar">
+          <view class="progress-fill" :style="{ width: goal.percent + '%' }"></view>
+        </view>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script>
+import { getChildBadges } from '@/utils/api'
+
+export default {
+  data() {
+    return {
+      badges: [],
+      totalBadges: 0,
+      unlockedCount: 0,
+      streakDays: 0,
+      goals: []
+    }
+  },
+  onLoad() {
+    this.loadBadges()
+    this.loadStreak()
+    this.loadGoals()
+  },
+  methods: {
+    loadBadges() {
+      const memberId = uni.getStorageSync('memberId') || null
+      getChildBadges(memberId).then(res => {
+        if (res.code === 200 && res.data) {
+          this.badges = res.data.map(item => ({
+            id: item.badge?.id,
+            icon: item.badge?.icon || '⭐',
+            name: item.badge?.name || '未知勋章',
+            unlocked: !!item.childBadge,
+            getDesc: item.badge?.description || ''
+          }))
+          this.totalBadges = res.data.length
+          this.unlockedCount = res.data.filter(item => item.childBadge).length
+        }
+      }).catch(() => {})
+    },
+    loadStreak() {
+      // 从本地存储获取连续打卡天数
+      this.streakDays = parseInt(uni.getStorageSync('streakDays') || 0)
+    },
+    loadGoals() {
+      this.goals = [
+        { id: 1, name: '连续打卡7天', current: this.streakDays, target: 7, percent: Math.min(100, (this.streakDays / 7) * 100) },
+        { id: 2, name: '获得5个勋章', current: this.unlockedCount, target: 5, percent: Math.min(100, (this.unlockedCount / 5) * 100) },
+        { id: 3, name: '累计积分1000', current: parseInt(uni.getStorageSync('totalPoints') || 0), target: 1000, percent: Math.min(100, (parseInt(uni.getStorageSync('totalPoints') || 0) / 1000) * 100) }
+      ]
+    },
+    goBack() {
+      uni.navigateBack()
+    }
+  }
+}
+</script>
+
+<style scoped>
+.container {
+  min-height: 100vh;
+  background: #f5f7fa;
+}
+.nav-bar {
+  display: flex;
+  align-items: center;
+  height: 88rpx;
+  background: #fff;
+  border-bottom: 1rpx solid #eee;
+  padding: 0 30rpx;
+}
+.nav-back {
+  font-size: 32rpx;
+  color: #666;
+  padding: 0 20rpx;
+}
+.nav-title {
+  flex: 1;
+  text-align: center;
+  font-size: 32rpx;
+  font-weight: 600;
+  color: #333;
+}
+.nav-placeholder {
+  width: 60rpx;
+}
+.stats-card {
+  display: flex;
+  background: #fff;
+  padding: 40rpx 0;
+  margin-bottom: 20rpx;
+}
+.stat-item {
+  flex: 1;
+  text-align: center;
+}
+.stat-num {
+  display: block;
+  font-size: 48rpx;
+  font-weight: 700;
+  color: #F97316;
+}
+.stat-label {
+  display: block;
+  font-size: 24rpx;
+  color: #999;
+  margin-top: 8rpx;
+}
+.badge-section, .goals-section {
+  background: #fff;
+  margin: 0 30rpx 20rpx;
+  border-radius: 20rpx;
+  padding: 30rpx;
+}
+.section-title {
+  font-size: 30rpx;
+  font-weight: 600;
+  color: #333;
+  margin-bottom: 24rpx;
+}
+.badge-grid {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 20rpx;
+}
+.badge-item {
+  width: calc(33.33% - 14rpx);
+  text-align: center;
+}
+.badge-icon-wrap {
+  width: 80rpx;
+  height: 80rpx;
+  border-radius: 50%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  margin: 0 auto 12rpx;
+}
+.badge-icon-wrap.unlocked {
+  background: linear-gradient(135deg, #FFD700, #FFA500);
+}
+.badge-icon-wrap.locked {
+  background: #e8e8e8;
+}
+.badge-icon {
+  font-size: 40rpx;
+}
+.badge-name {
+  display: block;
+  font-size: 22rpx;
+  color: #333;
+  margin-bottom: 4rpx;
+}
+.badge-desc {
+  display: block;
+  font-size: 18rpx;
+  color: #999;
+}
+.empty-state {
+  text-align: center;
+  padding: 60rpx 0;
+}
+.empty-icon {
+  font-size: 80rpx;
+  display: block;
+  margin-bottom: 20rpx;
+}
+.empty-text {
+  display: block;
+  font-size: 28rpx;
+  color: #999;
+  margin-bottom: 12rpx;
+}
+.empty-hint {
+  display: block;
+  font-size: 24rpx;
+  color: #ccc;
+}
+.goal-item {
+  margin-bottom: 24rpx;
+}
+.goal-item:last-child {
+  margin-bottom: 0;
+}
+.goal-header {
+  display: flex;
+  justify-content: space-between;
+  margin-bottom: 12rpx;
+}
+.goal-name {
+  font-size: 26rpx;
+  color: #333;
+}
+.goal-progress {
+  font-size: 24rpx;
+  color: #999;
+}
+.progress-bar {
+  height: 12rpx;
+  background: #f0f0f0;
+  border-radius: 6rpx;
+  overflow: hidden;
+}
+.progress-fill {
+  height: 100%;
+  background: linear-gradient(90deg, #F97316, #FB923C);
+  border-radius: 6rpx;
+  transition: width 0.3s;
+}
+</style>