Procházet zdrojové kódy

fix(task): 任务模块只认本人任务,创建页输入框与按钮样式修复

- tasks.vue: 移除家庭成员切换条改为「当前账号」提示条;今日/待接受任务按 isSelf
  解析出的本人 memberId 拉取,complete/start/accept/reject 一律传本人 memberId 并加
  isOwnTask 防呆提示;「我创建的」改用 familyWide 拉取后按 creatorId 过滤(原实现把
  userId 当 memberId 传给 today,查错表且可能串号)
- index-home.vue: 「我的任务」不再按家庭聚合,改由后端 JWT currentMemberId 兜底拉
  本人任务;快速完成返回空数据时提示「只能完成自己的任务」
- create-task.vue: 输入框统一 108rpx 同高 line-height,picker 同高 flex 居中,按钮
  显式 height/line-height 并清除 button 默认 ::after 边框
- api.js: createTask 端点修正为 /api/tasks/create(根映射 /api/tasks 已删除)
liaoxg před 1 týdnem
rodič
revize
f38bcd2ac2

+ 10 - 21
cfc-frontend/pages/index-home/index.vue

@@ -1102,25 +1102,10 @@ title: '添加家庭成员',
     },
     loadTodayTasks: function() {
       var self = this
-      var role = uni.getStorageSync('currentRole') || uni.getStorageSync('role') || 'parent'
-      // 家长:家庭维度拉取(方案任务可能挂在任意成员,不只是当前孩子)
-      if (role === 'parent') {
-        getTodayTasks(null, { familyWide: true }).then(function(res) {
-          if (res.code === 200 && Array.isArray(res.data)) {
-            self.pendingTasks = res.data.filter(function(t) {
-              return t.status === 'pending'
-            })
-          }
-        }).catch(function() {
-          self.pendingTasks = []
-        })
-        return
-      }
-      if (!self.currentChildId) {
-        self.pendingTasks = []
-        return
-      }
-      getTodayTasks(self.currentChildId).then(function(res) {
+      // 「我的任务」只展示当前登录用户自己的任务:
+      // 不传 memberId,由后端按 JWT 的 currentMemberId(始终是登录账号自己)返回,
+      // 不再按家庭聚合(familyWide),避免混入其他家庭成员的任务
+      getTodayTasks().then(function(res) {
         if (res.code === 200 && Array.isArray(res.data)) {
           self.pendingTasks = res.data.filter(function(t) {
             return t.status === 'pending'
@@ -1510,13 +1495,17 @@ title: '添加家庭成员',
     },
     quickCompleteTask: function(task) {
       var self = this
-      var memberId = (task && task.familyMemberId) || self.currentChildId || uni.getStorageSync('currentMemberId')
+      // 首页「我的任务」只展示本人任务:memberId 取任务归属成员,后端亦会按任务
+      // 实际归属校验(不属于本人时返回空数据),不会替其他家庭成员完成任务
+      var memberId = (task && task.familyMemberId) || null
       completeTaskApi(task.id, memberId, '').then(function(res) {
         if (res.code === 200) {
           if (res.data && res.data.needReview) {
             uni.showToast({ title: '已提交,待审核', icon: 'none' })
+          } else if (res.data) {
+            uni.showToast({ title: '获得 ' + (res.data.pointsEarned || 0) + ' CF', icon: 'success' })
           } else {
-            uni.showToast({ title: '获得 ' + (res.data && res.data.pointsEarned || 0) + ' CF', icon: 'success' })
+            uni.showToast({ title: '只能完成自己的任务', icon: 'none' })
           }
           self.loadTodayTasks()
         } else {

+ 9 - 5
cfc-frontend/pages/tasks/create-task.vue

@@ -480,13 +480,17 @@ onStartRequiredChange(e) {
 .form-container { background: #fff; border-radius: 20rpx; padding: 30rpx; }
 .form-item { margin-bottom: 30rpx; }
 .label { display: block; font-size: 28rpx; color: #333; margin-bottom: 10rpx; }
-.input, .textarea { border: 1rpx solid #ddd; border-radius: 10rpx; padding: 20rpx; font-size: 28rpx; width: 100%; box-sizing: border-box; }
-.input { height: 96rpx; }
-.textarea { height: 150rpx; }
-.picker { border: 1rpx solid #ddd; border-radius: 10rpx; padding: 20rpx; font-size: 28rpx; color: #666; }
+.input, .textarea { border: 1rpx solid #ddd; border-radius: 10rpx; font-size: 28rpx; width: 100%; box-sizing: border-box; }
+/* 输入框高度统一 108rpx;line-height 与高度一致,保证微信 input 文字垂直居中 */
+.input { height: 108rpx; line-height: 108rpx; padding: 0 24rpx; }
+.textarea { height: 150rpx; line-height: 1.5; padding: 20rpx 24rpx; }
+.picker { border: 1rpx solid #ddd; border-radius: 10rpx; font-size: 28rpx; color: #666; box-sizing: border-box; height: 108rpx; display: flex; align-items: center; padding: 0 24rpx; }
 .form-hint { display: block; font-size: 22rpx; color: #999; margin-top: 8rpx; }
 .child-checkbox { display: flex; align-items: center; padding: 16rpx 0; border-bottom: 1rpx solid #f0f0f0; }
 .child-checkbox:last-child { border-bottom: none; }
 .child-name { font-size: 28rpx; color: #333; margin-left: 10rpx; }
-.btn-primary { background: #F97316; color: #fff; border-radius: 20rpx; margin-top: 40rpx; }
+/* 创建按钮:显式 height + line-height 同高,微信 button 文字才能上下居中 */
+.btn-primary { background: #F97316; color: #fff; border-radius: 20rpx; margin-top: 40rpx; width: 100%; height: 96rpx; line-height: 96rpx; font-size: 32rpx; border: none; padding: 0; }
+/* 清除微信 button 组件默认的 ::after 1px 边框(否则按钮文字与边框视觉不居中) */
+.btn-primary::after { border: none; }
 </style>

+ 130 - 95
cfc-frontend/pages/tasks/tasks.vue

@@ -1,13 +1,11 @@
 <template>
   <view class="container">
-    <!-- 顶部:家庭成员选择(选择谁就展示谁的任务) -->
-    <FamilyMemberStrip
-      v-if="familyMembers.length > 0"
-      :members="familyMembers"
-      :selectedMemberId="currentMemberId"
-      :isParent="isParent"
-      @select="onFamilyMemberSelect"
-    />
+    <!-- 顶部:任务归属说明(只显示/操作当前登录用户本人的任务) -->
+    <view class="owner-bar">
+      <text class="owner-label">当前账号</text>
+      <text class="owner-name">{{ selfMember ? selfMember.nickname : '我' }}</text>
+      <text class="owner-tip">仅显示并可操作本人的任务</text>
+    </view>
 
     <!-- Tab 切换 -->
     <view class="tab-bar">
@@ -207,10 +205,8 @@
 import { getMyPendingTasks, acceptTask as acceptTaskApi, rejectTask as rejectTaskApi, getTodayTasks, completeTask as completeTaskApi, startTask as startTaskApi, createTask as createTaskApi, getFamilyMemberList, getMyMembership, reportAppEnter
 } from '../../utils/api.js'
 import { parseDate } from '../../utils/format.js'
-import FamilyMemberStrip from '../../components/FamilyMemberStrip.vue'
 
 export default {
-  components: { FamilyMemberStrip },
   data() {
     return {
       activeTab: 'today',
@@ -220,7 +216,7 @@ export default {
       createdTasks: [],
       isParent: false,
       isMember: false,
-      currentMemberId: null,
+      selfMemberId: null,
       currentUserId: null,
       familyMembers: [],
       showAddModal: false,
@@ -240,51 +236,49 @@ export default {
       categories: ['学习类', '生活类', '运动类', '其他']
     }
   },
+  computed: {
+    // 家庭列表中 isSelf=1 的成员即当前登录账号本人(后端按 userId 反查,不受切换视角影响)
+    selfMember: function() {
+      var members = this.familyMembers || []
+      for (var i = 0; i < members.length; i++) {
+        if (members[i] && members[i].isSelf) return members[i]
+      }
+      return null
+    }
+  },
   onLoad(options) {
     reportAppEnter('task');
-    
+
     if (options && options.tab) {
       this.activeTab = options.tab
     }
-    if (options && options.memberId) {
-      var mid = parseInt(options.memberId, 10)
-      if (!isNaN(mid)) {
-        this.currentMemberId = mid
-        uni.setStorageSync('currentMemberId', mid)
-        uni.setStorageSync('currentChildId', mid)
-      }
-    }
+    // 注意:不再支持通过 memberId 参数切换成员视角,任务列表只展示当前登录用户自己的任务
   },
   onShow() {
     const role = uni.getStorageSync('currentRole') || uni.getStorageSync('role') || 'parent'
     this.isParent = role === 'parent'
     this.currentUserId = uni.getStorageSync('userId')
-    if (this.currentMemberId == null) {
-      var cached = uni.getStorageSync('currentMemberId') || uni.getStorageSync('currentChildId')
-      if (cached !== '' && cached != null) {
-        var n = parseInt(cached, 10)
-        this.currentMemberId = isNaN(n) ? cached : n
-      }
-    }
     this.checkMembership()
     this.loadInitialData()
   },
   methods: {
-    onFamilyMemberSelect(member) {
-      if (!member || member.id == null) return
-      var id = typeof member.id === 'number' ? member.id : parseInt(member.id, 10)
-      if (isNaN(id)) id = member.id
-      if (this.currentMemberId === id) return
-      this.currentMemberId = id
-      uni.setStorageSync('currentMemberId', id)
-      uni.setStorageSync('currentChildId', id)
-      this.loadTodayTasks()
-      this.loadPendingTasks()
+    // 从家庭列表中解析「当前登录账号本人」的成员ID(isSelf 标记)
+    // 解析不到时不传 memberId,由后端按 JWT 的 currentMemberId 兜底(始终是登录账号自己)
+    resolveSelfMemberId() {
+      var members = this.familyMembers || []
+      for (var i = 0; i < members.length; i++) {
+        if (members[i] && members[i].isSelf) {
+          var n = parseInt(members[i].id, 10)
+          this.selfMemberId = isNaN(n) ? members[i].id : n
+          return
+        }
+      }
+      this.selfMemberId = null
     },
-    resolveTaskMemberId(task) {
-      if (task && task.familyMemberId) return task.familyMemberId
-      if (task && task.childId) return task.childId
-      return this.currentMemberId
+    // 是否本人的任务:兜底防呆(列表本身已按本人拉取,防御脏数据)
+    isOwnTask(task) {
+      if (!task || this.selfMemberId == null || task.familyMemberId == null) return true
+      return String(this.selfMemberId) === String(task.familyMemberId)
     },
     checkMembership() {
       getMyMembership().then(res => {
@@ -307,68 +301,30 @@ export default {
         const membersRes = await getFamilyMemberList({ visibleOnly: true })
         var raw = (membersRes && membersRes.data) || []
         this.familyMembers = Array.isArray(raw) ? raw : []
-        this.ensureSelectedMemberFromRaw(this.familyMembers)
-        if (!this.currentMemberId) {
-          this.todayTasks = []
-          this.pendingTasks = []
-        } else {
-          this.loadTodayTasks()
-          this.loadPendingTasks()
-        }
+        this.resolveSelfMemberId()
+        this.loadTodayTasks()
+        this.loadPendingTasks()
         this.loadCreatedTasks()
       } catch (e) {
         console.error('加载数据失败', e)
+        this.familyMembers = []
       } finally {
         this.loading = false
       }
     },
-    ensureSelectedMemberFromRaw(raw) {
-      var members = raw || []
-      if (members.length === 0) {
-        this.currentMemberId = null
-        return
-      }
-      if (this.currentMemberId != null) {
-        var cur = String(this.currentMemberId)
-        for (var i = 0; i < members.length; i++) {
-          if (members[i] && String(members[i].id) === cur) {
-            // 统一成数字,保证 FamilyMemberStrip 选中态 === 比较成立
-            var n = parseInt(members[i].id, 10)
-            this.currentMemberId = isNaN(n) ? members[i].id : n
-            return
-          }
-        }
-      }
-      var first = members[0]
-      var fid = first && first.id != null ? first.id : null
-      if (fid != null) {
-        var pn = parseInt(fid, 10)
-        this.currentMemberId = isNaN(pn) ? fid : pn
-        uni.setStorageSync('currentMemberId', this.currentMemberId)
-        uni.setStorageSync('currentChildId', this.currentMemberId)
-      } else {
-        this.currentMemberId = null
-      }
-    },
     async loadPendingTasks() {
-      if (!this.currentMemberId) {
-        this.pendingTasks = []
-        return
-      }
       try {
-        const res = await getMyPendingTasks(this.currentMemberId)
+        // 只拉取本人收到的待接受任务(不传 memberId 时后端按 JWT 兜底)
+        const res = await getMyPendingTasks(this.selfMemberId)
         this.pendingTasks = res.data || []
       } catch (e) {
         console.error('加载待接受任务失败', e)
       }
     },
     async loadTodayTasks() {
-      if (!this.currentMemberId) {
-        this.todayTasks = []
-        return
-      }
       try {
-        const res = await getTodayTasks(this.currentMemberId)
+        // 只拉取本人的今日任务(不传 memberId 时后端按 JWT 兜底)
+        const res = await getTodayTasks(this.selfMemberId)
         this.todayTasks = res.data || []
       } catch (e) {
         console.error('加载今日任务失败', e)
@@ -376,8 +332,13 @@ export default {
     },
     async loadCreatedTasks() {
       try {
-        const res = await getTodayTasks(this.currentUserId)
-        this.createdTasks = res.data || []
+        // 我创建的:家庭今日任务按 creatorId 过滤,只看当前登录用户创建的任务
+        const res = await getTodayTasks(null, { familyWide: true })
+        var all = (res && res.data) || []
+        var uid = this.currentUserId
+        this.createdTasks = all.filter(function(t) {
+          return t.creatorId != null && String(t.creatorId) === String(uid)
+        })
       } catch (e) {
         console.error('加载创建的任务失败', e)
       }
@@ -396,9 +357,13 @@ export default {
         })
         return
       }
+      // 只允许操作当前登录用户本人的任务
+      if (!this.isOwnTask(task)) {
+        uni.showToast({ title: '只能完成自己的任务', icon: 'none' })
+        return
+      }
       try {
-        var memberId = this.resolveTaskMemberId(task)
-        const res = await completeTaskApi(task.id, memberId, '')
+        const res = await completeTaskApi(task.id, this.selfMemberId, '')
         if (res.data && res.data.needReview) {
           uni.showToast({ title: '已提交,待审核', icon: 'none' })
         } else {
@@ -428,6 +393,11 @@ export default {
     /** 统一任务动作入口:开始/联动跳转/填写/直接完成 */
     handleTaskAction(task) {
       var self = this
+      // 只允许操作当前登录用户本人的任务
+      if (!this.isOwnTask(task)) {
+        uni.showToast({ title: '只能操作自己的任务', icon: 'none' })
+        return
+      }
       // 前置锁定任务不可操作
       if (task.status === 'locked') {
         uni.showToast({ title: '需先完成前置任务', icon: 'none' })
@@ -436,7 +406,7 @@ export default {
       // 两阶段:先开始,再按 action_type 跳转联动页面
       if (task.startRequired === 1 && task.status !== 'in_progress') {
         uni.showLoading({ title: '开始中...' })
-        startTaskApi(task.id, this.resolveTaskMemberId(task)).then(function(res) {
+        startTaskApi(task.id, this.selfMemberId).then(function(res) {
           uni.hideLoading()
           if (res && res.code === 200 && res.data) {
             uni.showToast({ title: '已开始', icon: 'success' })
@@ -502,8 +472,14 @@ export default {
     async submitWithInput() {
       var task = this.activeInputTask
       if (!task || !this.inputContent) return
+      // 只允许操作当前登录用户本人的任务
+      if (!this.isOwnTask(task)) {
+        uni.showToast({ title: '只能完成自己的任务', icon: 'none' })
+        this.closeInputModal()
+        return
+      }
       try {
-        const res = await completeTaskApi(task.id, this.resolveTaskMemberId(task), '', this.inputContent, 'text')
+        const res = await completeTaskApi(task.id, this.selfMemberId, '', this.inputContent, 'text')
         if (res.data && res.data.needReview) {
           uni.showToast({ title: '已提交,待审核', icon: 'none' })
         } else {
@@ -531,7 +507,8 @@ export default {
         return
       }
       uni.showLoading({ title: '接受中...' })
-      acceptTaskApi(task.id, this.currentMemberId).then(res => {
+      // 接受接口只认本人:memberId 一律传当前登录账号自己的成员ID
+      acceptTaskApi(task.id, this.selfMemberId).then(res => {
         uni.hideLoading()
         uni.showToast({ title: '已接受', icon: 'success' })
         this.loadPendingTasks()
@@ -548,7 +525,8 @@ export default {
     },
     confirmReject() {
       uni.showLoading({ title: '拒绝中...' })
-      rejectTaskApi(this.rejectTaskId, this.currentMemberId).then(res => {
+      // 拒绝接口只认本人:memberId 一律传当前登录账号自己的成员ID
+      rejectTaskApi(this.rejectTaskId, this.selfMemberId).then(res => {
         uni.hideLoading()
         this.showRejectModalFlag = false
         uni.showToast({ title: '已拒绝', icon: 'success' })
@@ -654,6 +632,41 @@ export default {
   min-height: 100vh;
 }
 
+/* 当前账号提示条(任务只归属当前登录用户本人) */
+.owner-bar {
+  display: flex;
+  align-items: center;
+  margin: 8rpx 0 16rpx;
+  padding: 14rpx 24rpx;
+  background: #fff;
+  border-radius: 16rpx;
+  box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.04);
+}
+
+.owner-label {
+  font-size: 22rpx;
+  color: #fff;
+  background: linear-gradient(135deg, #F97316, #FB923C);
+  border-radius: 8rpx;
+  padding: 4rpx 12rpx;
+  margin-right: 16rpx;
+}
+
+.owner-name {
+  font-size: 28rpx;
+  font-weight: 600;
+  color: #333;
+  flex: 1;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.owner-tip {
+  font-size: 22rpx;
+  color: #999;
+}
+
 .tab-bar {
   margin-top: 16rpx;
   display: flex;
@@ -912,8 +925,10 @@ export default {
   font-size: 30rpx;
   border-radius: 44rpx;
   border: none;
+  padding: 0;
   box-shadow: 0 8rpx 24rpx rgba(249, 115, 22, 0.35);
 }
+.add-btn::after { border: none; }
 
 .orchestration-btn {
   position: fixed;
@@ -927,8 +942,10 @@ export default {
   font-size: 30rpx;
   border-radius: 44rpx;
   border: none;
+  padding: 0;
   box-shadow: 0 8rpx 24rpx rgba(99, 102, 241, 0.35);
 }
+.orchestration-btn::after { border: none; }
 
 .modal-mask {
   position: fixed;
@@ -1003,9 +1020,13 @@ export default {
   border-radius: 44rpx;
   font-size: 28rpx;
   border: none;
+  padding: 0;
   box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.06);
 }
 
+/* 清除微信 button 组件默认的 ::after 1px 边框(保证文字与按钮上下居中) */
+.modal-btns button::after { border: none; }
+
 .modal-btns button:first-child {
   background: #f5f5f5;
   color: #666;
@@ -1042,6 +1063,20 @@ export default {
   box-sizing: border-box;
 }
 
+/* 输入框高度补齐(任务标题/积分):与 picker 同高,文字垂直居中 */
+.form-item input {
+  height: 108rpx;
+  line-height: 108rpx;
+  padding: 0 24rpx;
+}
+
+.form-item .picker {
+  height: 108rpx;
+  display: flex;
+  align-items: center;
+  padding: 0 24rpx;
+}
+
 .form-item .textarea {
   min-height: 140rpx;
 }

+ 1 - 1
cfc-frontend/utils/api.js

@@ -692,7 +692,7 @@ export const updateFamilyMember = (data) => {
 
 // 任务模块
 export const createTask = (data) => {
-  return request('/api/tasks', 'POST', data)
+  return request('/api/tasks/create', 'POST', data)
 }
 
 export const getTaskListForPicker = (params) => {