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

fix(checkin): 修复 4 问题(formatDate/任务不关闭/计数/成员切换),9 文件

zxyj-web 2 недель назад
Родитель
Сommit
f71832d28f

+ 6 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/HealthExerciseController.java

@@ -94,6 +94,12 @@ public class HealthExerciseController {
             if (record.getTaskId() != null) {
                 Task linkedTask = taskMapper.selectById(record.getTaskId());
                 if (linkedTask != null) {
+                    // 以任务归属为准修正记录成员,避免打卡记录与任务串号(家长切孩子视角场景)
+                    if (linkedTask.getFamilyMemberId() != null &&
+                        record.getMemberId() != null &&
+                        !linkedTask.getFamilyMemberId().equals(record.getMemberId())) {
+                        record.setMemberId(linkedTask.getFamilyMemberId());
+                    }
                     if (linkedTask.getIsDailyProgress() != null && linkedTask.getIsDailyProgress() == 1) {
                         // 每日进度型任务:按【具体 taskId】累计进度并达标关闭(首页卡片精确消失)
                         com.etotem.cfc.entity.FamilyMember fm = familyMemberMapper.selectById(record.getMemberId());

+ 6 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/HealthMealController.java

@@ -254,6 +254,12 @@ public class HealthMealController {
                 if (record.getTaskId() != null) {
                     Task linkedTask = taskMapper.selectById(record.getTaskId());
                     if (linkedTask != null) {
+                        // 以任务归属为准修正记录成员,避免打卡记录与任务串号(家长切孩子视角场景)
+                        if (linkedTask.getFamilyMemberId() != null &&
+                            record.getMemberId() != null &&
+                            !linkedTask.getFamilyMemberId().equals(record.getMemberId())) {
+                            record.setMemberId(linkedTask.getFamilyMemberId());
+                        }
                         if (linkedTask.getIsDailyProgress() != null && linkedTask.getIsDailyProgress() == 1) {
                             // 每日进度型任务:按【具体 taskId】累计进度并达标关闭(首页卡片精确消失)
                             com.etotem.cfc.entity.FamilyMember fm = familyMemberMapper.selectById(record.getMemberId());

+ 6 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/HealthSleepController.java

@@ -72,6 +72,12 @@ public class HealthSleepController {
             if (record.getTaskId() != null) {
                 Task linkedTask = taskMapper.selectById(record.getTaskId());
                 if (linkedTask != null) {
+                    // 以任务归属为准修正记录成员,避免打卡记录与任务串号(家长切孩子视角场景)
+                    if (linkedTask.getFamilyMemberId() != null &&
+                        record.getMemberId() != null &&
+                        !linkedTask.getFamilyMemberId().equals(record.getMemberId())) {
+                        record.setMemberId(linkedTask.getFamilyMemberId());
+                    }
                     if (linkedTask.getIsDailyProgress() != null && linkedTask.getIsDailyProgress() == 1) {
                         // 每日进度型任务:按【具体 taskId】累计进度并达标关闭(首页卡片精确消失)
                         com.etotem.cfc.entity.FamilyMember fm = familyMemberMapper.selectById(record.getMemberId());

+ 4 - 1
cfc-backend/src/main/java/com/etotem/cfc/controller/task/TaskController.java

@@ -132,8 +132,11 @@ public class TaskController {
             } catch (Exception e) {
                 // 画像计算失败不影响任务完成
             }
+            return Result.success(result);
         }
-        return Result.success(result);
+        // taskService.completeTask 返回 null:任务不存在、不属于该成员、已完成/待审核不可重复提交,
+        // 或小游戏任务走专用链路。必须返回错误让前端提示用户,杜绝静默失败。
+        return Result.error("任务不存在、无权完成或状态已变更");
     }
 
     @Operation(summary = "开始任务(两阶段任务入口)")

+ 1 - 1
cfc-frontend/pages/growth/diet-checkin.vue

@@ -219,7 +219,7 @@ export default {
     this.setShareInfo('我在用浠艾福记录孩子的每日饮食,一起来打卡吧', '/pages/growth/diet-checkin')
     var role = uni.getStorageSync('currentRole') || uni.getStorageSync('role') || 'parent'
     if (role !== 'child') {
-      this.memberId = uni.getStorageSync('currentChildId') || uni.getStorageSync('currentMemberId') || uni.getStorageSync('userId') || null
+      this.memberId = options.memberId || uni.getStorageSync('currentMemberId') || null
     }
     // 支持从 records 页传 date 参数(补打)
     if (options.date) {

+ 1 - 1
cfc-frontend/pages/growth/exercise-checkin.vue

@@ -213,7 +213,7 @@ export default {
     this.todayText = this.getToday()
     var role = uni.getStorageSync('currentRole') || uni.getStorageSync('role') || 'parent'
     if (role !== 'child') {
-      this.memberId = uni.getStorageSync('currentChildId') || uni.getStorageSync('currentMemberId') || uni.getStorageSync('userId') || null
+      this.memberId = options.memberId || uni.getStorageSync('currentMemberId') || null
     }
     this.isSelfCheckin = uni.getStorageSync('currentView') !== 'member'
     this.recorderManager = uni.getRecorderManager()

+ 3 - 2
cfc-frontend/pages/growth/sleep-checkin.vue

@@ -158,7 +158,7 @@
 
 <script>
 import config from '../../config'
-import { formatDate } from '../../utils/format.js'
+import { formatDate as formatDateUtil } from '../../utils/format.js'
 import { BrandColors } from '../../utils/brand-colors.js'
 import { completeTask as completeTaskApi } from '../../utils/api.js'
 import fontScaleMixin from '@/mixins/font-scale.js'
@@ -300,7 +300,7 @@ export default {
     this.todayWeek = '周' + weekdays[d.getDay()]
     var role = uni.getStorageSync('currentRole') || uni.getStorageSync('role') || 'parent'
     if (role !== 'child') {
-      this.memberId = uni.getStorageSync('currentChildId') || uni.getStorageSync('currentMemberId') || uni.getStorageSync('userId') || null
+      this.memberId = options.memberId || uni.getStorageSync('currentMemberId') || null
     }
     this.loadHistory()
   },
@@ -318,6 +318,7 @@ export default {
     }).exec()
   },
   methods: {
+    formatDate: function(d) { return formatDateUtil(d) },
     durationFromMinutes: function (mins) {
       if (!mins || mins <= 0) return '--'
       var h = Math.floor(mins / 60)

+ 7 - 3
cfc-frontend/pages/index-home/index.vue

@@ -1581,11 +1581,15 @@ title: '添加家庭成员',
           uni.navigateTo({ url: '/pages/shop/cart?taskId=' + taskId })
         }
       } else if (at === 'diet') {
-        uni.navigateTo({ url: '/pages/growth/diet-checkin?taskId=' + taskId })
+        // 携带任务归属成员ID:打卡页据此写回,避免家长切过孩子视角后任务记错人
+        var mId = (task.familyMemberId || task.executorId || '')
+        uni.navigateTo({ url: '/pages/growth/diet-checkin?taskId=' + taskId + (mId ? '&memberId=' + mId : '') })
       } else if (at === 'sleep') {
-        uni.navigateTo({ url: '/pages/growth/sleep-checkin?taskId=' + taskId })
+        var mId2 = (task.familyMemberId || task.executorId || '')
+        uni.navigateTo({ url: '/pages/growth/sleep-checkin?taskId=' + taskId + (mId2 ? '&memberId=' + mId2 : '') })
       } else if (at === 'exercise') {
-        uni.navigateTo({ url: '/pages/growth/exercise-checkin?taskId=' + taskId })
+        var mId3 = (task.familyMemberId || task.executorId || '')
+        uni.navigateTo({ url: '/pages/growth/exercise-checkin?taskId=' + taskId + (mId3 ? '&memberId=' + mId3 : '') })
       }
     },
     onChallengeCheckin: function(payload) {

+ 380 - 183
cfc-frontend/pages/tasks/tasks.vue

@@ -1,10 +1,20 @@
 <template>
   <view class="container" :style="{ '--font-scale': fontScale }">
-    <!-- 顶部:任务归属说明(只显示/操作当前登录用户本人的任务) -->
-    <view class="owner-bar">
-      <text class="owner-label">当前账号</text>
-      <text class="owner-name">{{ selfMember ? selfMember.nickname : '我' }}</text>
-      <text class="owner-tip">仅显示并可操作本人的任务</text>
+    <!-- 页头:当前账号 + 成员选择横向滚动条 -->
+    <view class="page-header">
+      <view class="header-account">
+        <text class="account-label">当前账号</text>
+        <text class="account-name">{{ selfMember ? selfMember.nickname : '我' }}</text>
+      </view>
+      <view class="header-tip">可查看家人的任务,仅能完成自己的</view>
+      <scroll-view scroll-x class="member-scroll">
+        <view class="member-chips">
+          <view class="member-chip" :class="{ active: !selectedMemberId }" @click="selectMember(null)">本人</view>
+          <view class="member-chip" v-for="member in familyMembers" :key="member.id" :class="{ active: selectedMemberId && String(selectedMemberId) === String(member.id) }" @click="selectMember(member.id)">
+            {{ member.nickname }}
+          </view>
+        </view>
+      </scroll-view>
     </view>
 
     <!-- Tab 切换 -->
@@ -23,85 +33,119 @@
 
     <!-- 待接受任务 -->
     <view class="task-list" v-if="activeTab === 'pending'">
-      <view class="task-item assigned" v-for="task in pendingTasks" :key="task.id">
-        <view class="task-left">
-          <view class="task-title">
-            {{ task.title }}
-            <text v-if="task.memberOnly === 1" class="member-badge">会员任务</text>
+      <view class="task-card" v-for="task in pendingTasks" :key="task.id">
+        <view class="card-accent-line"></view>
+        <view class="card-body">
+          <view class="card-left">
+            <view class="card-top-row">
+              <text class="card-type-icon" v-if="task.category === '小游戏类'">🎮</text>
+              <text class="card-type-icon" v-else-if="task.actionType === 'cart' || task.actionType === 'order'">🛒</text>
+              <text class="card-type-icon" v-else-if="task.actionType === 'focus'">⏱️</text>
+              <text class="card-type-icon" v-else-if="task.actionType === 'survey'">📝</text>
+              <text class="card-type-icon" v-else-if="task.actionType === 'article'">📖</text>
+              <text class="card-type-icon" v-else-if="task.actionType === 'activity'">🎯</text>
+              <text class="card-type-icon" v-else-if="task.requireInput === 1">✍️</text>
+              <text class="card-title">{{ task.title }}</text>
+              <text class="member-pill" v-if="task.memberOnly === 1">会员</text>
+            </view>
+            <view class="card-meta">
+              <text class="points-pill">+{{ task.points }} CF</text>
+              <text class="meta-dot">·</text>
+              <text class="card-creator">创建人:{{ task.creatorName || '家人' }}</text>
+              <text class="meta-dot">·</text>
+              <text class="card-deadline">截止 {{ formatTime(task.deadline) }}</text>
+            </view>
           </view>
-          <view class="task-meta">
-            <text class="points">+{{ task.points }}CF</text>
-            <text class="creator">创建人:{{ task.creatorName || '家人' }}</text>
-            <text class="deadline">截止 {{ formatTime(task.deadline) }}</text>
+          <view class="card-actions">
+            <button class="btn-accept" @click="acceptTask(task)">接受</button>
+            <button class="btn-reject" @click="showRejectModal(task)">拒绝</button>
           </view>
         </view>
-        <view class="task-actions">
-          <button class="btn-accept" @click="acceptTask(task)">接受</button>
-          <button class="btn-reject" @click="showRejectModal(task)">拒绝</button>
-        </view>
       </view>
-      <view class="empty" v-if="!loading && pendingTasks.length === 0">
-        <text>暂无待接受任务</text>
+      <view class="empty-state" v-if="!loading && pendingTasks.length === 0">
+        <view class="empty-icon">📭</view>
+        <text class="empty-main">暂无待接受任务</text>
+        <text class="empty-sub">收到任务后会出现在这里</text>
       </view>
     </view>
 
     <!-- 今日任务 -->
     <view class="task-list" v-if="activeTab === 'today'">
-      <view class="task-item" v-for="task in todayTasks" :key="task.id" :class="{ completed: task.status === 'completed', locked: task.status === 'locked' }">
-        <view class="task-left">
-          <view class="task-title">
-            {{ task.title }}
-            <text v-if="task.memberOnly === 1" class="member-badge">会员任务</text>
-            <text v-if="task.category === '小游戏类'" class="minigame-badge">🎮</text>
-            <text v-if="task.actionType === 'cart' || task.actionType === 'order'" class="minigame-badge">🛒</text>
-            <text v-if="task.actionType === 'focus'" class="minigame-badge">⏱️</text>
-            <text v-if="task.actionType === 'survey'" class="minigame-badge">📝</text>
-            <text v-if="task.actionType === 'article'" class="minigame-badge">📖</text>
-            <text v-if="task.actionType === 'activity'" class="minigame-badge">🎯</text>
-            <text v-if="task.requireInput === 1" class="minigame-badge">✍️</text>
-          </view>
-          <view class="task-meta">
-            <text class="points">+{{ task.points }}CF</text>
-            <text class="deadline">截止 {{ formatTime(task.deadline) }}</text>
-          </view>
-          <!-- 进度型任务:展示进度条 -->
-          <view class="progress-wrap" v-if="task.isDailyProgress === 1">
-            <view class="progress-bar">
-              <view class="progress-fill" :style="{ width: getProgressPercent(task) + '%' }"></view>
+      <view class="task-card" v-for="task in todayTasks" :key="task.id" :class="{ completed: task.status === 'completed', locked: task.status === 'locked' }">
+        <view class="card-accent-line" :class="'line-' + task.status"></view>
+        <view class="card-body">
+          <view class="card-left">
+            <view class="card-top-row">
+              <text class="card-type-icon" v-if="task.category === '小游戏类'">🎮</text>
+              <text class="card-type-icon" v-else-if="task.actionType === 'cart' || task.actionType === 'order'">🛒</text>
+              <text class="card-type-icon" v-else-if="task.actionType === 'focus'">⏱️</text>
+              <text class="card-type-icon" v-else-if="task.actionType === 'survey'">📝</text>
+              <text class="card-type-icon" v-else-if="task.actionType === 'article'">📖</text>
+              <text class="card-type-icon" v-else-if="task.actionType === 'activity'">🎯</text>
+              <text class="card-type-icon" v-else-if="task.requireInput === 1">✍️</text>
+              <text class="card-title">{{ task.title }}</text>
+              <text class="member-pill" v-if="task.memberOnly === 1">会员</text>
+            </view>
+            <view class="card-meta">
+              <text class="points-pill">+{{ task.points }} CF</text>
+              <text class="meta-dot">·</text>
+              <text class="card-deadline">截止 {{ formatTime(task.deadline) }}</text>
+            </view>
+            <!-- 进度型任务:展示进度条 -->
+            <view class="progress-wrap" v-if="task.isDailyProgress === 1">
+              <view class="progress-track">
+                <view class="progress-fill" :style="{ width: getProgressPercent(task) + '%' }"></view>
+              </view>
+              <text class="progress-text">{{ task.progress || 0 }}/{{ task.targetValue || 1 }}</text>
             </view>
-            <text class="progress-text">{{ task.progress || 0 }}/{{ task.targetValue || 1 }}</text>
+            <text v-if="task.status === 'locked'" class="lock-tip">🔒 需先完成前置任务后解锁</text>
+          </view>
+          <view class="card-right">
+            <button v-if="task.status === 'pending' || task.status === 'in_progress'" class="btn-complete" @click="handleTaskAction(task)">{{ getActionLabel(task) }}</button>
+            <view v-else-if="task.status === 'completed'" class="badge badge-completed">已完成</view>
+            <view v-else-if="task.status === 'pending_review'" class="badge badge-review">待审核</view>
+            <view v-else class="badge badge-lock">🔒 未解锁</view>
           </view>
-          <text v-if="task.status === 'locked'" class="lock-tip">🔒 需先完成前置任务后解锁</text>
-        </view>
-        <view class="task-right" v-if="task.status === 'pending' || task.status === 'in_progress'">
-          <button class="btn-complete" @click="handleTaskAction(task)">{{ getActionLabel(task) }}</button>
         </view>
-        <view class="completed-badge" v-else-if="task.status === 'completed'">已完成</view>
-        <view class="review-badge" v-else-if="task.status === 'pending_review'">待审核</view>
-        <view class="lock-badge" v-else>🔒 未解锁</view>
       </view>
-      <view class="empty" v-if="!loading && todayTasks.length === 0">
-        <text>暂无今日任务</text>
+      <view class="empty-state" v-if="!loading && todayTasks.length === 0">
+        <view class="empty-icon">✅</view>
+        <text class="empty-main">今日任务已全部完成</text>
+        <text class="empty-sub">继续保持,明天继续加油!</text>
       </view>
     </view>
 
     <!-- 我创建的任务 -->
     <view class="task-list" v-if="activeTab === 'created'">
-      <view class="task-item" v-for="task in createdTasks" :key="task.id">
-        <view class="task-left">
-          <view class="task-title">
-            {{ task.title }}
-            <text v-if="task.memberOnly === 1" class="member-badge">会员任务</text>
-          </view>
-          <view class="task-meta">
-            <text class="points">+{{ task.points }}CF</text>
-            <text class="assignee">{{ task.executorName || '未分配' }}</text>
-            <text class="status-tag" :class="'status-' + task.status">{{ statusLabel(task.status) }}</text>
+      <view class="task-card" v-for="task in createdTasks" :key="task.id">
+        <view class="card-accent-line"></view>
+        <view class="card-body card-body-row">
+          <view class="card-left">
+            <view class="card-top-row">
+              <text class="card-type-icon" v-if="task.category === '小游戏类'">🎮</text>
+              <text class="card-type-icon" v-else-if="task.actionType === 'cart' || task.actionType === 'order'">🛒</text>
+              <text class="card-type-icon" v-else-if="task.actionType === 'focus'">⏱️</text>
+              <text class="card-type-icon" v-else-if="task.actionType === 'survey'">📝</text>
+              <text class="card-type-icon" v-else-if="task.actionType === 'article'">📖</text>
+              <text class="card-type-icon" v-else-if="task.actionType === 'activity'">🎯</text>
+              <text class="card-type-icon" v-else-if="task.requireInput === 1">✍️</text>
+              <text class="card-title">{{ task.title }}</text>
+              <text class="member-pill" v-if="task.memberOnly === 1">会员</text>
+            </view>
+            <view class="card-meta">
+              <text class="points-pill">+{{ task.points }} CF</text>
+              <text class="meta-dot">·</text>
+              <text class="card-assignee">{{ task.executorName || '未分配' }}</text>
+              <text class="meta-dot">·</text>
+              <text class="status-tag" :class="'status-' + task.status">{{ statusLabel(task.status) }}</text>
+            </view>
           </view>
         </view>
       </view>
-      <view class="empty" v-if="!loading && createdTasks.length === 0">
-        <text>暂无创建的任务</text>
+      <view class="empty-state" v-if="!loading && createdTasks.length === 0">
+        <view class="empty-icon">📋</view>
+        <text class="empty-main">暂无创建的任务</text>
+        <text class="empty-sub">点击底部按钮创建新任务</text>
       </view>
     </view>
 
@@ -219,6 +263,7 @@ export default {
       isParent: false,
       isMember: false,
       selfMemberId: null,
+      selectedMemberId: null,
       currentUserId: null,
       familyMembers: [],
       showAddModal: false,
@@ -277,6 +322,12 @@ export default {
       }
       this.selfMemberId = null
     },
+    // 切换今日任务的查看成员:选中成员的任务列表(选中本人/不传 memberId 由后端按 JWT 兜底)
+    selectMember(memberId) {
+      this.selectedMemberId = memberId || null
+      this.activeTab = 'today'
+      this.loadTodayTasks(this.selectedMemberId)
+    },
     // 是否本人的任务:兜底防呆(列表本身已按本人拉取,防御脏数据)
     isOwnTask(task) {
       if (!task || this.selfMemberId == null || task.familyMemberId == null) return true
@@ -323,10 +374,11 @@ export default {
         console.error('加载待接受任务失败', e)
       }
     },
-    async loadTodayTasks() {
+    async loadTodayTasks(memberId) {
       try {
-        // 只拉取本人的今日任务(不传 memberId 时后端按 JWT 兜底)
-        const res = await getTodayTasks(this.selfMemberId)
+        // 选中成员 → 传该成员 memberId;选中本人 → 传 selfMemberId(后端亦按 JWT currentMemberId 兜底)
+        var targetMemberId = arguments.length > 0 ? memberId : this.selectedMemberId
+        const res = await getTodayTasks(targetMemberId)
         this.todayTasks = res.data || []
       } catch (e) {
         console.error('加载今日任务失败', e)
@@ -366,14 +418,22 @@ export default {
       }
       try {
         const res = await completeTaskApi(task.id, this.selfMemberId, '')
+        if (res && res.code !== 200) {
+          // B2:后端明确返回错误(任务不存在/无权完成/状态已变更),杜绝静默失败
+          uni.showToast({ title: (res && res.message) || '完成失败', icon: 'none' })
+          return
+        }
         if (res.data && res.data.needReview) {
           uni.showToast({ title: '已提交,待审核', icon: 'none' })
-        } else {
+        } else if (res.data) {
           uni.showToast({ title: `获得 ${res.data.pointsEarned} CF`, icon: 'success' })
+        } else {
+          uni.showToast({ title: '只能完成自己的任务', icon: 'none' })
         }
         this.loadTodayTasks()
       } catch (e) {
         console.error('完成任务失败', e)
+        uni.showToast({ title: '完成失败,请重试', icon: 'none' })
       }
     },
     /** 按钮文案:按 action_type / start_required / require_input 分派 */
@@ -473,11 +533,14 @@ export default {
           uni.navigateTo({ url: '/pages/shop/cart?taskId=' + taskId })
         }
       } else if (at === 'diet') {
-        uni.navigateTo({ url: '/pages/growth/diet-checkin?taskId=' + taskId })
+        var mIdT = (task.familyMemberId || task.executorId || '')
+        uni.navigateTo({ url: '/pages/growth/diet-checkin?taskId=' + taskId + (mIdT ? '&memberId=' + mIdT : '') })
       } else if (at === 'sleep') {
-        uni.navigateTo({ url: '/pages/growth/sleep-checkin?taskId=' + taskId })
+        var mIdTs = (task.familyMemberId || task.executorId || '')
+        uni.navigateTo({ url: '/pages/growth/sleep-checkin?taskId=' + taskId + (mIdTs ? '&memberId=' + mIdTs : '') })
       } else if (at === 'exercise') {
-        uni.navigateTo({ url: '/pages/growth/exercise-checkin?taskId=' + taskId })
+        var mIdTe = (task.familyMemberId || task.executorId || '')
+        uni.navigateTo({ url: '/pages/growth/exercise-checkin?taskId=' + taskId + (mIdTe ? '&memberId=' + mIdTe : '') })
       }
     },
     closeInputModal() {
@@ -496,10 +559,17 @@ export default {
       }
       try {
         const res = await completeTaskApi(task.id, this.selfMemberId, '', this.inputContent, 'text')
+        if (res && res.code !== 200) {
+          uni.showToast({ title: (res && res.message) || '提交失败', icon: 'none' })
+          this.closeInputModal()
+          return
+        }
         if (res.data && res.data.needReview) {
           uni.showToast({ title: '已提交,待审核', icon: 'none' })
-        } else {
+        } else if (res.data) {
           uni.showToast({ title: `获得 ${res.data.pointsEarned} CF`, icon: 'success' })
+        } else {
+          uni.showToast({ title: '只能完成自己的任务', icon: 'none' })
         }
         this.closeInputModal()
         this.loadTodayTasks()
@@ -642,55 +712,93 @@ export default {
 </script>
 
 <style scoped lang="scss">
+/* ============ 间距节奏统一:所有区块间距为 8 的倍数 ============ */
 .container {
-  padding: 20rpx 30rpx 120rpx;
+  padding: 16rpx 32rpx 120rpx;
   background: $app-bg;
   min-height: 100vh;
 }
 
-/* 当前账号提示条(任务只归属当前登录用户本人) */
-.owner-bar {
+/* ============ 页头:当前账号 + 成员选择(置顶) ============ */
+.page-header {
+  background: $app-surface;
+  border-radius: $uni-radius-lg;
+  padding: $uni-spacing-md;
+  box-shadow: $uni-shadow-sm;
+}
+
+.header-account {
   display: flex;
   align-items: center;
-  margin: 8rpx 0 16rpx;
-  padding: 14rpx 24rpx;
-  background: $app-surface;
-  border-radius: 16rpx;
-  box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.04);
+  margin-bottom: $uni-spacing-sm;
 }
 
-.owner-label {
+.account-label {
   font-size: calc(22rpx * var(--font-scale, 1));
   color: $app-surface;
   background: linear-gradient(135deg, $brand-primary, $brand-primary-light);
   border-radius: 8rpx;
   padding: 4rpx 12rpx;
-  margin-right: 16rpx;
+  margin-right: $uni-spacing-sm;
+  font-weight: 500;
 }
 
-.owner-name {
-  font-size: calc(28rpx * var(--font-scale, 1));
+.account-name {
+  font-size: calc(30rpx * var(--font-scale, 1));
   font-weight: 600;
   color: $facade-text-strong;
   flex: 1;
+  min-width: 0;
   overflow: hidden;
   text-overflow: ellipsis;
   white-space: nowrap;
 }
 
-.owner-tip {
+.header-tip {
+  display: block;
   font-size: calc(22rpx * var(--font-scale, 1));
   color: $facade-text-muted;
+  margin-bottom: $uni-spacing-sm;
+}
+
+/* 成员选择横向滚动条 */
+.member-scroll {
+  width: 100%;
+  white-space: nowrap;
 }
 
+.member-chips {
+  display: flex;
+  flex-wrap: nowrap;
+  align-items: center;
+  padding-bottom: 4rpx;
+}
+
+.member-chip {
+  flex-shrink: 0;
+  font-size: calc(24rpx * var(--font-scale, 1));
+  color: $facade-text-body;
+  background: $facade-fill-gray-100;
+  border-radius: 999rpx;
+  padding: 10rpx 24rpx;
+  margin-right: $uni-spacing-sm;
+  transition: background $dur-state $ease-standard, color $dur-state $ease-standard;
+
+  &.active {
+    background: linear-gradient(135deg, $brand-primary, $brand-primary-light);
+    color: $app-surface;
+    font-weight: 600;
+  }
+}
+
+/* ============ Tab 栏:胶囊切换 ============ */
 .tab-bar {
-  margin-top: 16rpx;
   display: flex;
   background: $app-surface;
-  border-radius: 16rpx;
-  padding: 10rpx;
-  margin-bottom: 30rpx;
-  box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06);
+  border-radius: $uni-radius-lg;
+  padding: 8rpx;
+  margin: $uni-spacing-md 0;
+  box-shadow: $uni-shadow-sm;
 }
 
 .tab-item {
@@ -698,17 +806,19 @@ export default {
   display: flex;
   align-items: center;
   justify-content: center;
-  padding: 20rpx 0;
-  border-radius: 12rpx;
+  height: 64rpx;
+  border-radius: $uni-radius-sm;
   position: relative;
-}
+  transition: background $dur-state $ease-standard;
 
-.tab-item.active {
-  background: linear-gradient(135deg, $brand-primary, $brand-primary-light);
+  &.active {
+    background: linear-gradient(135deg, $brand-primary, $brand-primary-light);
+    box-shadow: 0 4rpx 12rpx rgba(249, 115, 22, 0.25);
+  }
 }
 
 .tab-text {
-  font-size: 28rpx;
+  font-size: calc(26rpx * var(--font-scale, 1));
   color: $facade-text-body;
 }
 
@@ -719,195 +829,266 @@ export default {
 
 .tab-badge {
   position: absolute;
-  top: 8rpx;
-  right: 20rpx;
+  top: 0;
+  right: 16rpx;
   background: $status-danger;
   color: $app-surface;
-  font-size: 20rpx;
+  font-size: 18rpx;
   min-width: 28rpx;
   height: 28rpx;
   line-height: 28rpx;
   text-align: center;
-  border-radius: 14rpx;
+  border-radius: 999rpx;
   padding: 0 6rpx;
+  transform: translateY(-8rpx);
 }
 
+/* ============ 任务列表统一容器 ============ */
 .task-list {
   display: flex;
   flex-direction: column;
-  gap: 20rpx;
+  gap: $uni-spacing-md;
 }
 
-.task-item {
-  display: flex;
-  align-items: center;
-  justify-content: space-between;
+/* ============ 任务卡片 ============ */
+.task-card {
+  position: relative;
   background: $app-surface;
-  border-radius: 20rpx;
-  padding: 28rpx;
-  box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.04);
+  border-radius: $uni-radius-lg;
+  box-shadow: $uni-shadow-sm;
+  overflow: hidden;
+
+  &.completed {
+    opacity: 0.65;
+  }
+
+  &.locked {
+    opacity: 0.5;
+  }
 }
 
-.task-item.completed {
-  opacity: 0.7;
+/* 左侧色条(按状态区分) */
+.card-accent-line {
+  position: absolute;
+  left: 0;
+  top: 0;
+  bottom: 0;
+  width: 5rpx;
+
+  &.line-completed { background: $status-success; }
+  &.line-locked { background: $facade-text-gray-400; }
+  &.line-pending_review { background: $status-warning; }
+  &.line-in_progress { background: $brand-primary; }
+  &.line-assigned { background: $brand-primary; }
+  &:not(.line-completed):not(.line-locked):not(.line-pending_review):not(.line-in_progress):not(.line-assigned) {
+    background: $brand-primary; /* pending / 默认 */
+  }
 }
 
-.task-item.locked {
-  opacity: 0.55;
+.card-body {
+  display: flex;
+  flex-direction: row;
+  align-items: flex-start;
+  justify-content: space-between;
+  padding: $uni-spacing-lg;
+  gap: $uni-spacing-lg;
 }
 
-.task-item.assigned {
-  border-left: 6rpx solid $brand-primary;
+.card-body-row {
+  flex-direction: row;
 }
 
-.task-left {
+.card-left {
   flex: 1;
   min-width: 0;
-  padding-right: 20rpx;
 }
 
-.task-title {
+/* 标题行:类型图标 + 标题 + 会员药丸 */
+.card-top-row {
+  display: flex;
+  align-items: center;
+  flex-wrap: wrap;
+  gap: $uni-spacing-xs;
+  margin-bottom: $uni-spacing-sm;
+}
+
+.card-type-icon {
+  font-size: calc(28rpx * var(--font-scale, 1));
+  flex-shrink: 0;
+}
+
+.card-title {
+  flex: 1;
+  min-width: 0;
   font-size: calc(30rpx * var(--font-scale, 1));
-  color: $facade-text-strong;
   font-weight: 500;
-  margin-bottom: 12rpx;
+  color: $facade-text-strong;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
 }
 
-.member-badge {
-  font-size: 20rpx;
+.member-pill {
+  font-size: calc(20rpx * var(--font-scale, 1));
   color: $brand-primary;
   background: $uni-primary-pale;
-  padding: 2rpx 10rpx;
-  border-radius: 8rpx;
-  margin-left: 8rpx;
-}
-
-.minigame-badge {
-  margin-left: 8rpx;
-  font-size: 24rpx;
+  padding: 4rpx 12rpx;
+  border-radius: 999rpx;
+  font-weight: 500;
 }
 
-.task-meta {
+/* 元信息:积分药丸 + 分隔点 + 创建人/执行人/截止 */
+.card-meta {
   display: flex;
   flex-wrap: wrap;
-  gap: 16rpx;
-  font-size: calc(24rpx * var(--font-scale, 1));
+  align-items: center;
+  gap: $uni-spacing-xs;
+  font-size: calc(22rpx * var(--font-scale, 1));
   color: $facade-text-muted;
 }
 
-.points {
+.points-pill {
+  display: inline-flex;
+  align-items: center;
+  font-size: calc(22rpx * var(--font-scale, 1));
+  font-weight: 600;
   color: $brand-primary;
-  font-weight: 500;
+  background: $brand-primary-soft;
+  padding: 4rpx 12rpx;
+  border-radius: 999rpx;
 }
 
-.deadline, .creator, .assignee {
-  color: $facade-text-muted;
+.meta-dot {
+  color: $facade-text-gray-400;
 }
 
-.lock-tip {
-  display: block;
-  margin-top: 8rpx;
-  font-size: calc(22rpx * var(--font-scale, 1));
-  color: $facade-text-gray-400;
+.card-creator,
+.card-assignee,
+.card-deadline {
+  color: $facade-text-muted;
 }
 
+/* 进度条 */
 .progress-wrap {
   display: flex;
   align-items: center;
-  margin-top: 12rpx;
+  margin-top: $uni-spacing-sm;
 }
 
-.progress-bar {
+.progress-track {
   flex: 1;
-  height: 12rpx;
+  height: 10rpx;
   background: $facade-fill-gray-100;
-  border-radius: 6rpx;
+  border-radius: 5rpx;
   overflow: hidden;
-  margin-right: 12rpx;
+  margin-right: $uni-spacing-sm;
 }
 
 .progress-fill {
   height: 100%;
   background: linear-gradient(135deg, $brand-primary, $brand-primary-light);
-  border-radius: 6rpx;
-  transition: width 0.3s ease;
+  border-radius: 5rpx;
+  transition: width $dur-structural $ease-standard;
 }
 
 .progress-text {
-  font-size: calc(22rpx * var(--font-scale, 1));
+  font-size: calc(20rpx * var(--font-scale, 1));
   color: $brand-primary;
   white-space: nowrap;
 }
 
-.task-right {
+.lock-tip {
+  display: block;
+  margin-top: $uni-spacing-xs;
+  font-size: calc(20rpx * var(--font-scale, 1));
+  color: $facade-text-gray-400;
+}
+
+/* ============ 卡片右侧:操作区 ============ */
+.card-right {
   flex-shrink: 0;
 }
 
+.card-actions {
+  display: flex;
+  flex-direction: column;
+  gap: $uni-spacing-sm;
+  flex-shrink: 0;
+}
+
+/* 主按钮:品牌渐变(完成/接受) */
 .btn-complete {
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
   background: linear-gradient(135deg, $brand-primary, $brand-primary-light);
   color: $app-surface;
-  font-size: 26rpx;
+  font-size: calc(24rpx * var(--font-scale, 1));
+  font-weight: 500;
   padding: 0 28rpx;
   height: 64rpx;
   line-height: 64rpx;
   border-radius: 32rpx;
   border: none;
+  box-shadow: 0 4rpx 12rpx rgba(249, 115, 22, 0.25);
 }
-
-.task-actions {
-  display: flex;
-  gap: 12rpx;
-  flex-shrink: 0;
-}
+.btn-complete::after { border: none; }
 
 .btn-accept {
-  background: $status-success;
+  background: linear-gradient(135deg, $status-success, $status-success-light-3);
   color: $app-surface;
-  font-size: 24rpx;
-  padding: 0 24rpx;
-  height: 56rpx;
-  line-height: 56rpx;
-  border-radius: 28rpx;
+  font-size: calc(24rpx * var(--font-scale, 1));
+  font-weight: 500;
+  padding: 0 32rpx;
+  height: 60rpx;
+  line-height: 60rpx;
+  border-radius: 30rpx;
   border: none;
 }
+.btn-accept::after { border: none; }
 
+/* 次按钮:描边(拒绝) */
 .btn-reject {
-  background: $facade-fill-gray-100;
+  background: $app-surface;
   color: $facade-text-body;
-  font-size: 24rpx;
-  padding: 0 24rpx;
-  height: 56rpx;
-  line-height: 56rpx;
-  border-radius: 28rpx;
-  border: none;
+  font-size: calc(24rpx * var(--font-scale, 1));
+  padding: 0 32rpx;
+  height: 60rpx;
+  line-height: 58rpx;
+  border-radius: 30rpx;
+  border: 2rpx solid $facade-gray-200;
 }
+.btn-reject::after { border: none; }
 
-.completed-badge, .review-badge, .lock-badge {
-  font-size: 24rpx;
-  padding: 8rpx 16rpx;
-  border-radius: 12rpx;
+/* ============ 状态徽章(统一色系与圆角) ============ */
+.badge {
+  font-size: calc(22rpx * var(--font-scale, 1));
+  padding: 8rpx 18rpx;
+  border-radius: 999rpx;
+  font-weight: 500;
   flex-shrink: 0;
 }
 
-.completed-badge {
+.badge-completed {
   background: $status-success-tint;
   color: $status-success;
 }
 
-.review-badge {
+.badge-review {
   background: $uni-primary-pale;
   color: $brand-primary;
 }
 
-.lock-badge {
+.badge-lock {
   background: $facade-fill-gray-100;
   color: $facade-text-gray-400;
 }
 
+/* 我创建的 tab 状态小标签 */
 .status-tag {
-  font-size: 22rpx;
-  padding: 2rpx 10rpx;
-  border-radius: 8rpx;
+  font-size: calc(20rpx * var(--font-scale, 1));
+  padding: 4rpx 12rpx;
+  border-radius: 999rpx;
   background: $facade-fill-gray-100;
   color: $facade-text-body;
 }
@@ -920,13 +1101,29 @@ export default {
 .status-rejected { background: $status-danger-soft; color: $status-danger; }
 .status-locked { background: $facade-fill-gray-100; color: $facade-text-gray-400; }
 
-.empty {
+/* ============ 空态 ============ */
+.empty-state {
   display: flex;
-  justify-content: center;
+  flex-direction: column;
   align-items: center;
-  padding: 80rpx 0;
-  color: $facade-text-gray-400;
+  justify-content: center;
+  padding: 96rpx 0;
+}
+
+.empty-icon {
+  font-size: calc(72rpx * var(--font-scale, 1));
+  margin-bottom: $uni-spacing-lg;
+}
+
+.empty-main {
   font-size: calc(28rpx * var(--font-scale, 1));
+  color: $facade-text-body;
+  margin-bottom: $uni-spacing-xs;
+}
+
+.empty-sub {
+  font-size: calc(22rpx * var(--font-scale, 1));
+  color: $facade-text-muted;
 }
 
 .add-btn {