|
|
@@ -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;
|
|
|
}
|