Просмотр исходного кода

feat: 重构心愿页

Ultraworked with Sisyphus
User 3 месяцев назад
Родитель
Сommit
387eeef9e3
1 измененных файлов с 706 добавлено и 163 удалено
  1. 706 163
      cfc-frontend/pages/child/tasks.vue

+ 706 - 163
cfc-frontend/pages/child/tasks.vue

@@ -1,163 +1,706 @@
-<template>
-  <PlayfulCard elevation="1">
-    <view class="container">
-      <!-- 退出切换按钮 -->
-      <view class="exit-switch" v-if="isSwitchedChild" @click="exitSwitch">
-        <text>🔄 退出切换</text>
-      </view>
-      
-      <view class="task-list" v-if="tasks.length > 0">
-        <view v-for="task in tasks" :key="task.id" class="task-item" :class="{ completed: task.status === 'completed' }">
-            <view class="task-left">
-              <view class="task-title">
-                {{ task.title }}
-                <text v-if="task.category === '小游戏类'" class="minigame-badge">🎮</text>
-              </view>
-              <view class="task-meta">
-                <text class="points">+{{ task.points }}分</text>
-                <text class="deadline">截止 {{ formatTime(task.deadline) }}</text>
-              </view>
-            </view>
-            <view class="task-right">
-              <button 
-                v-if="task.status === 'pending'" 
-                class="btn-complete" 
-                @click="handleTaskClick(task)"
-              >
-                {{ task.category === '小游戏类' ? '开始游戏' : '完成' }}
-              </button>
-            <view v-else class="completed-badge">已完成</view>
-          </view>
-        </view>
-      </view>
-
-      <view class="empty" v-else>
-        <text>暂无今日任务</text>
-      </view>
-    </view>
-  </PlayfulCard>
-</template>
-
-<script>
-import PlayfulCard from '@/components/PlayfulCard.vue'
-import { getTodayTasks, completeTask as completeTaskApi, getChildren, switchBackToParent, verifyPassword } from '../../utils/api.js'
-
-export default {
-  components: { PlayfulCard },
-  data() {
-    return {
-      tasks: [],
-      currentChildId: '',
-      isSwitchedChild: false // 家长切换到孩子状态的标记
-    }
-  },
-  onShow() {
-    this.isSwitchedChild = uni.getStorageSync('isSwitchedChild') || false
-    this.loadData()
-  },
-  methods: {
-    async loadData() {
-      try {
-        const childrenRes = await getChildren()
-        const children = childrenRes.data || []
-        const storedChildId = uni.getStorageSync('currentChildId')
-        const currentChild = children.find(c => c.id == storedChildId) || children[0]
-        if (!currentChild) {
-          this.tasks = []
-          this.currentChildId = ''
-          return
-        }
-        this.currentChildId = currentChild.id
-        const res = await getTodayTasks(currentChild.id)
-        this.tasks = res.data || []
-      } catch (e) {
-        console.error('加载孩子任务失败', e)
-      }
-    },
-    async completeChildTask(taskId) {
-      try {
-        const res = await completeTaskApi(taskId, this.currentChildId, '')
-        uni.showToast({ title: `获得 ${res.data.pointsEarned} 积分`, icon: 'success' })
-        this.loadData()
-      } catch (e) {
-        console.error('完成任务失败', e)
-      }
-    },
-    handleTaskClick(task) {
-      // 如果是小游戏任务,跳转到对应的小游戏页面
-      if (task.category === '小游戏类' && task.minigameCode) {
-        const gamePages = {
-          'schulte': '/pages/games/schulte',
-          '1a2b': '/pages/games/1a2b',
-          'sudoku': '/pages/games/sudoku'
-        }
-        const gamePage = gamePages[task.minigameCode]
-        if (gamePage) {
-          uni.navigateTo({
-            url: `${gamePage}?taskId=${task.id}&childId=${this.currentChildId}`
-          })
-        }
-      } else {
-        // 普通任务,直接完成
-        this.completeChildTask(task.id)
-      }
-    },
-    formatTime(dateStr) {
-      if (!dateStr) return ''
-      const date = new Date(dateStr)
-      return `${date.getHours()}:${String(date.getMinutes()).padStart(2, '0')}`
-    },
-    exitSwitch() {
-      // 只有家长切换到孩子状态时才显示退出切换功能
-      if (!this.isSwitchedChild) return
-      
-      uni.showModal({
-        title: '退出切换',
-        content: '请输入家长密码以确认退出',
-        editable: true,
-        success: async (res) => {
-          if (res.confirm && res.content) {
-            try {
-              const verifyResult = await verifyPassword(res.content)
-              if (verifyResult.code === 200) {
-                await this.switchBackToParentMode()
-              } else {
-                uni.showToast({ title: '密码错误', icon: 'none' })
-              }
-            } catch (e) {
-              uni.showToast({ title: '密码验证失败', icon: 'none' })
-            }
-          }
-        }
-      })
-    },
-    async switchBackToParentMode() {
-      try {
-        const result = await switchBackToParent()
-        if (result.data) {
-          this.$store.commit('switchBackToParent')
-          uni.showToast({ title: '已切换回家长模式', icon: 'success' })
-          uni.reLaunch({ url: '/pages/index/parent-index' })
-        }
-      } catch (e) { 
-        uni.showToast({ title: '切换失败', icon: 'none' }) 
-      }
-    }
-  }
-}
-</script>
-
-<style scoped>
-.container { padding: 30rpx; }
-.exit-switch { background: #f5f5f5; padding: 20rpx; border-radius: 10rpx; margin-bottom: 20rpx; text-align: center; color: #666; font-size: 28rpx; }
-.task-item { background: #fff; border-radius: 16rpx; padding: 30rpx; margin-bottom: 20rpx; display: flex; justify-content: space-between; align-items: center; }
-.task-item.completed { opacity: 0.6; }
-.task-title { font-size: 30rpx; color: #333; margin-bottom: 10rpx; }
-.task-meta { display: flex; gap: 20rpx; }
-.points { color: #FF6B6B; font-size: 24rpx; }
-.deadline { color: #999; font-size: 24rpx; }
-.btn-complete { background: #FF6B6B; color: #fff; font-size: 26rpx; padding: 10rpx 30rpx; border-radius: 30rpx; }
-.completed-badge { color: #52C41A; font-size: 26rpx; }
-.minigame-badge { font-size: 28rpx; margin-left: 10rpx; }
-.empty { text-align: center; color: #999; padding: 100rpx; }
-</style>
+<template>
+  <view class="page-bg">
+    <!-- 退出切换(家长切换到孩子状态时显示) -->
+    <view class="exit-switch" v-if="isSwitchedChild" @click="exitSwitch">
+      <text class="exit-switch__icon">🔄</text>
+      <text class="exit-switch__text">退出切换</text>
+    </view>
+
+    <!-- 顶部标签栏 -->
+    <view class="tab-bar clay-card">
+      <view
+        v-for="tab in tabs"
+        :key="tab.key"
+        class="tab-item"
+        :class="{ 'tab-item--active': activeTab === tab.key }"
+        hover-class="tab-item--hover"
+        @click="switchTab(tab.key)"
+      >
+        <text class="tab-item__label">{{ tab.label }}</text>
+        <text class="tab-item__count" v-if="tab.count > 0">{{ tab.count }}</text>
+      </view>
+    </view>
+
+    <!-- 加载状态 -->
+    <BaseLoading :loading="loading" text="小助理正在准备任务..." />
+
+    <!-- 任务列表 -->
+    <view class="task-list" v-if="!loading && filteredTasks.length > 0">
+      <view
+        v-for="(task, index) in filteredTasks"
+        :key="task.id"
+        class="animate-slide-up"
+        :class="'animate-stagger-' + ((index % 20) + 1)"
+      >
+        <view
+          class="task-card clay-card"
+          :class="{
+            'task-card--completed': task.status === 'completed',
+            'task-card--overdue': getIsOverdue(task),
+            'task-card--completing': completingTaskId === task.id
+          }"
+        >
+          <!-- 完成时庆祝遮罩 -->
+          <view class="task-card__celebrate" v-if="completingTaskId === task.id">
+            <text class="task-card__celebrate-icon">🎉</text>
+          </view>
+
+          <view class="task-card__inner">
+            <!-- 左侧图标 -->
+            <view class="task-icon">
+              <text class="task-icon__emoji">{{ getCategoryIcon(task.category) }}</text>
+            </view>
+
+            <!-- 中间内容 -->
+            <view class="task-content">
+              <view class="task-title">{{ task.title }}</view>
+              <view class="task-meta">
+                <BaseBadge variant="warning" size="xs">
+                  <text class="points-text">+{{ task.points }} ⭐</text>
+                </BaseBadge>
+                <text
+                  class="task-time"
+                  v-if="task.status === 'pending' && !getIsOverdue(task)"
+                >
+                  ⏱ {{ getTimeLeft(task) }}
+                </text>
+                <text
+                  class="task-time task-time--overdue"
+                  v-if="getIsOverdue(task)"
+                >
+                  ⏰ 已超时{{ getOverdueDays(task) }}
+                </text>
+              </view>
+            </view>
+
+            <!-- 右侧操作 -->
+            <view class="task-action">
+              <!-- 待完成任务 -->
+              <template v-if="task.status === 'pending' && !getIsOverdue(task)">
+                <PlayfulButton
+                  variant="clay"
+                  size="sm"
+                  :disabled="completingTaskId === task.id"
+                  @click="handleTaskClick(task)"
+                >
+                  <text v-if="task.category === '小游戏类'">🎮 开始游戏</text>
+                  <text v-else>✅ 完成了!</text>
+                </PlayfulButton>
+              </template>
+
+              <!-- 已超时且未完成的任务,显示已超时状态 -->
+              <view
+                class="status-badge status-badge--overdue"
+                v-if="getIsOverdue(task) && task.status !== 'completed'"
+              >
+                <text class="status-badge__text">⏰ 已超时</text>
+              </view>
+
+              <!-- 已完成任务 -->
+              <view
+                class="status-badge status-badge--completed"
+                v-if="task.status === 'completed'"
+              >
+                <text class="status-badge__text">✅ 已完成</text>
+              </view>
+            </view>
+          </view>
+        </view>
+      </view>
+    </view>
+
+    <!-- 空状态 -->
+    <view class="empty-wrapper" v-if="!loading && filteredTasks.length === 0">
+      <BaseEmpty
+        :icon="emptyIcon"
+        :title="emptyTitle"
+        :description="emptyDescription"
+      >
+      </BaseEmpty>
+    </view>
+  </view>
+</template>
+
+<script>
+import PlayfulButton from '@/components/PlayfulButton.vue'
+import BaseBadge from '@/components/BaseBadge.vue'
+import BaseEmpty from '@/components/BaseEmpty.vue'
+import BaseLoading from '@/components/BaseLoading.vue'
+import { getTodayTasks, completeTask as completeTaskApi, getChildren, switchBackToParent, verifyPassword } from '../../utils/api.js'
+
+export default {
+  components: { PlayfulButton, BaseBadge, BaseEmpty, BaseLoading },
+  data() {
+    return {
+      tasks: [],
+      currentChildId: '',
+      isSwitchedChild: false,
+      loading: false,
+      activeTab: 'pending',
+      completingTaskId: null
+    }
+  },
+  computed: {
+    tabs() {
+      var pending = 0
+      var completed = 0
+      var overdue = 0
+      var task, i
+      for (i = 0; i < this.tasks.length; i++) {
+        task = this.tasks[i]
+        if (task.status === 'completed') {
+          completed++
+        } else if (this.checkIsOverdue(task)) {
+          overdue++
+        } else {
+          pending++
+        }
+      }
+      return [
+        { key: 'pending', label: '📋 待完成', count: pending },
+        { key: 'completed', label: '✅ 已完成', count: completed },
+        { key: 'overdue', label: '⏰ 已超时', count: overdue }
+      ]
+    },
+    filteredTasks() {
+      var result = []
+      var task, i
+      for (i = 0; i < this.tasks.length; i++) {
+        task = this.tasks[i]
+        if (this.activeTab === 'pending') {
+          if (task.status === 'pending' && !this.checkIsOverdue(task)) {
+            result.push(task)
+          }
+        } else if (this.activeTab === 'completed') {
+          if (task.status === 'completed') {
+            result.push(task)
+          }
+        } else if (this.activeTab === 'overdue') {
+          if (this.checkIsOverdue(task)) {
+            result.push(task)
+          }
+        }
+      }
+      return result
+    },
+    emptyIcon() {
+      var icons = {
+        pending: '📝',
+        completed: '🎉',
+        overdue: '😴'
+      }
+      return icons[this.activeTab] || '📋'
+    },
+    emptyTitle() {
+      var titles = {
+        pending: '暂无待完成任务',
+        completed: '还没有已完成的任务',
+        overdue: '太棒了,没有超时任务!'
+      }
+      return titles[this.activeTab] || ''
+    },
+    emptyDescription() {
+      var descs = {
+        pending: '让家长布置一些任务吧~\n完成后可以获得积分奖励哦',
+        completed: '完成的任务都会出现在这里,\n继续保持好习惯!',
+        overdue: '继续保持良好的完成习惯,\n按时完成任务吧!'
+      }
+      return descs[this.activeTab] || ''
+    }
+  },
+  onShow() {
+    this.isSwitchedChild = uni.getStorageSync('isSwitchedChild') || false
+    this.loadData()
+  },
+  methods: {
+    async loadData() {
+      var childrenRes, children, storedChildId, currentChild, i, res
+      this.loading = true
+      try {
+        childrenRes = await getChildren()
+        children = childrenRes.data || []
+        storedChildId = uni.getStorageSync('currentChildId')
+        currentChild = null
+        for (i = 0; i < children.length; i++) {
+          if (String(children[i].id) === String(storedChildId)) {
+            currentChild = children[i]
+            break
+          }
+        }
+        if (!currentChild) {
+          currentChild = children[0]
+        }
+        if (!currentChild) {
+          this.tasks = []
+          this.currentChildId = ''
+          return
+        }
+        this.currentChildId = currentChild.id
+        res = await getTodayTasks(currentChild.id)
+        this.tasks = res.data || []
+      } catch (e) {
+        console.error('加载孩子任务失败', e)
+      } finally {
+        this.loading = false
+      }
+    },
+    async completeChildTask(taskId) {
+      var res, points
+      this.completingTaskId = taskId
+      try {
+        res = await completeTaskApi(taskId, this.currentChildId, '')
+        points = res.data.pointsEarned
+        uni.showToast({ title: '获得 ' + points + ' 积分', icon: 'success' })
+        setTimeout(() => {
+          this.completingTaskId = null
+          this.loadData()
+        }, 600)
+      } catch (e) {
+        this.completingTaskId = null
+        console.error('完成任务失败', e)
+      }
+    },
+    handleTaskClick(task) {
+      var gamePages, gamePage
+      if (this.completingTaskId === task.id) return
+      if (task.category === '小游戏类' && task.minigameCode) {
+        gamePages = {
+          'schulte': '/pages/games/schulte',
+          '1a2b': '/pages/games/1a2b',
+          'sudoku': '/pages/games/sudoku'
+        }
+        gamePage = gamePages[task.minigameCode]
+        if (gamePage) {
+          uni.navigateTo({
+            url: gamePage + '?taskId=' + task.id + '&childId=' + this.currentChildId
+          })
+        }
+      } else {
+        this.completeChildTask(task.id)
+      }
+    },
+    getCategoryIcon(category) {
+      var icons = {
+        '学习类': '📚',
+        '生活类': '🏠',
+        '运动类': '⚽',
+        '小游戏类': '🎮',
+        '其他': '⭐'
+      }
+      return icons[category] || '📝'
+    },
+    getTimeLeft(task) {
+      var deadline, now, diff, hours, minutes, days
+      if (!task.deadline) return '无截止时间'
+      deadline = new Date(task.deadline)
+      now = new Date()
+      diff = deadline - now
+      if (diff < 0) return ''
+      hours = Math.floor(diff / (1000 * 60 * 60))
+      minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60))
+      if (hours > 24) {
+        days = Math.floor(hours / 24)
+        return '还剩 ' + days + ' 天'
+      }
+      if (hours > 0) return '还剩 ' + hours + ' 小时'
+      if (minutes > 0) return '还剩 ' + minutes + ' 分钟'
+      return '即将截止'
+    },
+    checkIsOverdue(task) {
+      var deadline, now
+      if (task.status === 'completed') return false
+      if (!task.deadline) return false
+      deadline = new Date(task.deadline)
+      now = new Date()
+      return deadline < now
+    },
+    getOverdueDays(task) {
+      var deadline, now, diff, days, hours
+      if (!task.deadline) return ''
+      deadline = new Date(task.deadline)
+      now = new Date()
+      diff = now - deadline
+      days = Math.floor(diff / (1000 * 60 * 60 * 24))
+      if (days > 0) return ' ' + days + '天'
+      hours = Math.floor(diff / (1000 * 60 * 60))
+      if (hours > 0) return ' ' + hours + '小时'
+      return ''
+    },
+    switchTab(tabKey) {
+      this.activeTab = tabKey
+    },
+    exitSwitch() {
+      var self = this
+      if (!self.isSwitchedChild) return
+      uni.showModal({
+        title: '退出切换',
+        content: '请输入家长密码以确认退出',
+        editable: true,
+        success: function(res) {
+          if (res.confirm && res.content) {
+            verifyPassword(res.content).then(function(verifyResult) {
+              if (verifyResult.code === 200) {
+                self.switchBackToParentMode()
+              } else {
+                uni.showToast({ title: '密码错误', icon: 'none' })
+              }
+            }).catch(function() {
+              uni.showToast({ title: '密码验证失败', icon: 'none' })
+            })
+          }
+        }
+      })
+    },
+    async switchBackToParentMode() {
+      var result
+      try {
+        result = await switchBackToParent()
+        if (result.data) {
+          this.$store.commit('switchBackToParent')
+          uni.showToast({ title: '已切换回家长模式', icon: 'success' })
+          uni.reLaunch({ url: '/pages/index/parent-index' })
+        }
+      } catch (_) {
+        uni.showToast({ title: '切换失败', icon: 'none' })
+      }
+    }
+  }
+}
+</script>
+
+<style scoped>
+/* =============================================
+   页面基础
+   ============================================= */
+.page-bg {
+  min-height: 100vh;
+  background: var(--bg, #FFF7ED);
+  padding-bottom: 40rpx;
+}
+
+/* =============================================
+   退出切换横幅
+   ============================================= */
+.exit-switch {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  gap: 12rpx;
+  padding: 24rpx;
+  margin: 24rpx 32rpx 0;
+  background: var(--surface, #FFFFFF);
+  border: 2rpx solid var(--border, #FED7AA);
+  border-radius: var(--radius-lg, 32rpx);
+  box-shadow: var(--shadow-sm);
+}
+
+.exit-switch:active {
+  opacity: 0.7;
+  transform: scale(0.98);
+}
+
+.exit-switch__icon {
+  font-size: 32rpx;
+}
+
+.exit-switch__text {
+  font-size: 28rpx;
+  color: var(--text-secondary, #6B5A4A);
+  font-weight: var(--font-weight-medium, 500);
+}
+
+/* =============================================
+   顶部标签栏
+   ============================================= */
+.tab-bar {
+  display: flex;
+  flex-direction: row;
+  margin: 24rpx 32rpx 0;
+  padding: 8rpx;
+  border-radius: var(--radius-xl, 40rpx);
+  background: var(--surface, #FFFFFF);
+  position: relative;
+}
+
+.tab-item {
+  flex: 1;
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  justify-content: center;
+  gap: 8rpx;
+  padding: 20rpx 12rpx;
+  border-radius: var(--radius-lg, 32rpx);
+  transition: all var(--transition-base, 0.25s ease);
+  position: relative;
+}
+
+.tab-item--hover {
+  background: rgba(249, 115, 22, 0.06);
+}
+
+.tab-item--active {
+  background: linear-gradient(145deg, var(--color-primary-light, #FB923C), var(--color-primary, #F97316));
+  box-shadow: inset -2rpx -2rpx 8rpx rgba(255, 255, 255, 0.3),
+    4rpx 4rpx 12rpx rgba(249, 115, 22, 0.2);
+}
+
+.tab-item__label {
+  font-size: 26rpx;
+  font-weight: var(--font-weight-medium, 500);
+  color: var(--text-secondary, #6B5A4A);
+  transition: color var(--transition-fast, 0.15s ease);
+}
+
+.tab-item--active .tab-item__label {
+  color: var(--text-color-inverse, #FFFFFF);
+  font-weight: var(--font-weight-bold, 600);
+}
+
+.tab-item__count {
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  min-width: 36rpx;
+  height: 36rpx;
+  padding: 0 8rpx;
+  border-radius: 999rpx;
+  background: rgba(249, 115, 22, 0.12);
+  color: var(--color-primary, #F97316);
+  font-size: 20rpx;
+  font-weight: var(--font-weight-bold, 600);
+  line-height: 1;
+}
+
+.tab-item--active .tab-item__count {
+  background: rgba(255, 255, 255, 0.25);
+  color: #FFFFFF;
+}
+
+/* =============================================
+   任务列表
+   ============================================= */
+.task-list {
+  padding: 24rpx 32rpx;
+}
+
+/* =============================================
+   任务卡片(Claymorphism 风格)
+   ============================================= */
+.task-card {
+  position: relative;
+  overflow: hidden;
+  margin-bottom: 24rpx;
+  transition: transform var(--transition-fast, 0.15s ease),
+    opacity var(--transition-base, 0.25s ease);
+}
+
+.task-card:active {
+  transform: scale(0.98);
+}
+
+/* --- 已完成状态 --- */
+.task-card--completed {
+  opacity: 0.7;
+  border-color: var(--color-success, #22C55E);
+  background: linear-gradient(145deg, #F0FDF4, #ECFDF5);
+  box-shadow: inset -2rpx -2rpx 6rpx rgba(34, 197, 94, 0.05),
+    3rpx 3rpx 10rpx rgba(34, 197, 94, 0.08);
+}
+
+/* --- 超时状态 --- */
+.task-card--overdue {
+  border-color: var(--color-error, #EF4444);
+  background: linear-gradient(145deg, #FEF2F2, #FEE2E2);
+  box-shadow: inset -2rpx -2rpx 6rpx rgba(239, 68, 68, 0.05),
+    3rpx 3rpx 10rpx rgba(239, 68, 68, 0.08);
+}
+
+/* --- 完成中动画 --- */
+.task-card--completing {
+  animation: cardComplete 0.6s ease forwards;
+  pointer-events: none;
+}
+
+.task-card__celebrate {
+  position: absolute;
+  inset: 0;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  background: rgba(255, 255, 255, 0.6);
+  border-radius: var(--radius-md, 20rpx);
+  z-index: 10;
+}
+
+.task-card__celebrate-icon {
+  font-size: 80rpx;
+  animation: celebrateBounce 0.6s ease both;
+}
+
+/* --- 内部布局 --- */
+.task-card__inner {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  gap: 20rpx;
+}
+
+/* =============================================
+   任务图标
+   ============================================= */
+.task-icon {
+  width: 88rpx;
+  height: 88rpx;
+  border-radius: var(--radius-md, 20rpx);
+  background: linear-gradient(145deg, #FFF7ED, #FFEDD5);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  flex-shrink: 0;
+  border: 2rpx solid var(--border-light, #FFEDD5);
+}
+
+.task-card--completed .task-icon {
+  background: linear-gradient(145deg, #F0FDF4, #DCFCE7);
+  border-color: #BBF7D0;
+}
+
+.task-card--overdue .task-icon {
+  background: linear-gradient(145deg, #FEF2F2, #FECACA);
+  border-color: #FECACA;
+}
+
+.task-icon__emoji {
+  font-size: 40rpx;
+  line-height: 1;
+}
+
+/* =============================================
+   任务内容区
+   ============================================= */
+.task-content {
+  flex: 1;
+  min-width: 0;
+}
+
+.task-title {
+  font-size: 32rpx;
+  font-weight: var(--font-weight-bold, 600);
+  color: var(--text, #3D2E1E);
+  line-height: 1.4;
+  margin-bottom: 10rpx;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.task-card--completed .task-title {
+  text-decoration: line-through;
+  color: var(--text-secondary, #6B5A4A);
+}
+
+.task-meta {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  gap: 16rpx;
+  flex-wrap: wrap;
+}
+
+.points-text {
+  font-size: 22rpx;
+  line-height: 1.4;
+}
+
+.task-time {
+  font-size: 22rpx;
+  color: var(--text-secondary, #6B5A4A);
+  line-height: 1.4;
+}
+
+.task-time--overdue {
+  color: var(--color-error, #EF4444);
+  font-weight: var(--font-weight-medium, 500);
+}
+
+/* =============================================
+   右侧操作区
+   ============================================= */
+.task-action {
+  flex-shrink: 0;
+  margin-left: 8rpx;
+}
+
+/* =============================================
+   状态徽章
+   ============================================= */
+.status-badge {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  padding: 16rpx 24rpx;
+  border-radius: 999rpx;
+  font-weight: var(--font-weight-bold, 600);
+}
+
+.status-badge--completed {
+  background: rgba(34, 197, 94, 0.12);
+  color: var(--color-success, #22C55E);
+  border: 2rpx solid rgba(34, 197, 94, 0.2);
+}
+
+.status-badge--overdue {
+  background: rgba(239, 68, 68, 0.12);
+  color: var(--color-error, #EF4444);
+  border: 2rpx solid rgba(239, 68, 68, 0.2);
+}
+
+.status-badge__text {
+  font-size: 24rpx;
+  line-height: 1;
+}
+
+/* =============================================
+   空状态
+   ============================================= */
+.empty-wrapper {
+  padding-top: 60rpx;
+}
+
+/* =============================================
+   关键帧动画
+   ============================================= */
+@keyframes cardComplete {
+  0% {
+    transform: scale(1);
+    opacity: 1;
+  }
+  30% {
+    transform: scale(1.05);
+    opacity: 0.9;
+  }
+  100% {
+    transform: scale(0.95);
+    opacity: 0.6;
+  }
+}
+
+@keyframes celebrateBounce {
+  0% {
+    transform: scale(0) rotate(-180deg);
+    opacity: 0;
+  }
+  50% {
+    transform: scale(1.3) rotate(10deg);
+    opacity: 1;
+  }
+  70% {
+    transform: scale(0.9) rotate(-5deg);
+  }
+  100% {
+    transform: scale(1) rotate(0deg);
+    opacity: 1;
+  }
+}
+</style>