|
|
@@ -0,0 +1,193 @@
|
|
|
+<template>
|
|
|
+ <view class="page">
|
|
|
+ <view class="tabs">
|
|
|
+ <view class="tab" :class="{ active: tab === 'plan' }" @click="switchTab('plan')">方案</view>
|
|
|
+ <view class="tab" :class="{ active: tab === 'task' }" @click="switchTab('task')">任务</view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- Tab 1 方案 -->
|
|
|
+ <view v-if="tab === 'plan'">
|
|
|
+ <view v-if="plans.length === 0" class="empty">暂无待审核方案</view>
|
|
|
+ <view
|
|
|
+ v-for="p in plans"
|
|
|
+ :key="getPlanKey(p)"
|
|
|
+ class="card"
|
|
|
+ @click="goPlan(p)"
|
|
|
+ >
|
|
|
+ <view class="row">
|
|
|
+ <text class="name">{{ planTitle(p) }}</text>
|
|
|
+ <text class="badge" v-if="isMine(p)">已分配给我</text>
|
|
|
+ <text class="badge grey" v-else>我的客户</text>
|
|
|
+ </view>
|
|
|
+ <text class="meta">创建于 {{ formatTime(p.createdAt) }}</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- Tab 2 任务 -->
|
|
|
+ <view v-else>
|
|
|
+ <view class="member-bar">
|
|
|
+ <picker
|
|
|
+ mode="selector"
|
|
|
+ :range="memberNames"
|
|
|
+ :value="memberIndex"
|
|
|
+ @change="onMemberChange"
|
|
|
+ >
|
|
|
+ <view class="picker">成员:{{ memberNames[memberIndex] || '请选择' }}</view>
|
|
|
+ </picker>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view v-if="!selectedMemberId" class="empty">请先选择成员</view>
|
|
|
+ <view v-else-if="tasks.length === 0" class="empty">暂无待审核任务</view>
|
|
|
+
|
|
|
+ <view v-else>
|
|
|
+ <view v-for="t in tasks" :key="getTaskKey(t)" class="card">
|
|
|
+ <text class="name">{{ t.title }}</text>
|
|
|
+ <text class="meta">截止 {{ formatTime(t.deadline) }}</text>
|
|
|
+ <view class="actions">
|
|
|
+ <view class="btn ok" @click="reviewTask(t, true)">通过</view>
|
|
|
+ <view class="btn no" @click="reviewTask(t, false)">驳回</view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script>
|
|
|
+import {
|
|
|
+ getGuideFamilyMembers,
|
|
|
+ getGuidePendingPlans,
|
|
|
+ getGuideChildPendingTasks,
|
|
|
+ reviewGuideTask
|
|
|
+} from '@/utils/api.js'
|
|
|
+
|
|
|
+export default {
|
|
|
+ data() {
|
|
|
+ return {
|
|
|
+ familyId: null,
|
|
|
+ myUserId: null,
|
|
|
+ tab: 'plan',
|
|
|
+ plans: [],
|
|
|
+ members: [],
|
|
|
+ memberIndex: 0,
|
|
|
+ selectedMemberId: null,
|
|
|
+ tasks: []
|
|
|
+ }
|
|
|
+ },
|
|
|
+ computed: {
|
|
|
+ memberNames() {
|
|
|
+ return this.members.map(m => m.nickname || ('成员' + m.id))
|
|
|
+ }
|
|
|
+ },
|
|
|
+ onLoad(options) {
|
|
|
+ this.familyId = options.familyId
|
|
|
+ // 项目既有取值方式:uni.getStorageSync('userId'),全局并无 userId 可直接取
|
|
|
+ this.myUserId = uni.getStorageSync('userId') || null
|
|
|
+ this.fetchMembers()
|
|
|
+ this.fetchPlans()
|
|
|
+ },
|
|
|
+ methods: {
|
|
|
+ async fetchMembers() {
|
|
|
+ try {
|
|
|
+ const res = await getGuideFamilyMembers(this.familyId)
|
|
|
+ if (res && res.code === 200) {
|
|
|
+ this.members = res.data || []
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ uni.showToast({ title: '成员加载失败', icon: 'none' })
|
|
|
+ }
|
|
|
+ },
|
|
|
+ async fetchPlans() {
|
|
|
+ try {
|
|
|
+ const res = await getGuidePendingPlans(this.familyId)
|
|
|
+ if (res && res.code === 200) {
|
|
|
+ this.plans = res.data || []
|
|
|
+ } else {
|
|
|
+ uni.showToast({ title: (res && res.message) || '方案加载失败', icon: 'none' })
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ uni.showToast({ title: '方案加载失败', icon: 'none' })
|
|
|
+ }
|
|
|
+ },
|
|
|
+ switchTab(t) {
|
|
|
+ this.tab = t
|
|
|
+ if (t === 'task' && this.selectedMemberId) {
|
|
|
+ this.fetchTasks()
|
|
|
+ }
|
|
|
+ },
|
|
|
+ onMemberChange(e) {
|
|
|
+ this.memberIndex = Number(e.detail.value)
|
|
|
+ const m = this.members[this.memberIndex]
|
|
|
+ this.selectedMemberId = m ? m.id : null
|
|
|
+ if (this.selectedMemberId) {
|
|
|
+ this.fetchTasks()
|
|
|
+ }
|
|
|
+ },
|
|
|
+ async fetchTasks() {
|
|
|
+ try {
|
|
|
+ const res = await getGuideChildPendingTasks(this.familyId, this.selectedMemberId)
|
|
|
+ if (res && res.code === 200) {
|
|
|
+ this.tasks = res.data || []
|
|
|
+ } else {
|
|
|
+ this.tasks = []
|
|
|
+ uni.showToast({ title: (res && res.message) || '无权访问或暂无数据', icon: 'none' })
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ uni.showToast({ title: '任务加载失败', icon: 'none' })
|
|
|
+ }
|
|
|
+ },
|
|
|
+ async reviewTask(t, approved) {
|
|
|
+ try {
|
|
|
+ const res = await reviewGuideTask(this.familyId, t.id, approved, '')
|
|
|
+ if (res && res.code === 200) {
|
|
|
+ uni.showToast({ title: approved ? '已通过' : '已驳回', icon: 'none' })
|
|
|
+ this.fetchTasks()
|
|
|
+ } else {
|
|
|
+ uni.showToast({ title: (res && res.message) || '操作失败', icon: 'none' })
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ uni.showToast({ title: '网络异常', icon: 'none' })
|
|
|
+ }
|
|
|
+ },
|
|
|
+ // HealthPlan 实体无 title 字段,故标题依次取 goal / memberName / id
|
|
|
+ planTitle(p) {
|
|
|
+ return p.goal || p.memberName || ('健康方案 #' + p.id)
|
|
|
+ },
|
|
|
+ isMine(p) {
|
|
|
+ return !!p.teacherId && String(p.teacherId) === String(this.myUserId)
|
|
|
+ },
|
|
|
+ getPlanKey(p) { return 'gp-' + p.id },
|
|
|
+ getTaskKey(t) { return 'gt-' + t.id },
|
|
|
+ formatTime(t) {
|
|
|
+ if (!t) return '-'
|
|
|
+ // 项目统一时间格式:取前19位并把 ISO 的 T 换成空格,避免 toLocaleString 的浏览器差异
|
|
|
+ return String(t).substring(0, 19).replace('T', ' ')
|
|
|
+ },
|
|
|
+ goPlan(p) {
|
|
|
+ uni.navigateTo({
|
|
|
+ url: '/pages/teacher/plan-detail?familyId=' + this.familyId + '&planId=' + p.id
|
|
|
+ })
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.page { padding: 24rpx; background: #F5FAFE; min-height: 100vh; }
|
|
|
+.tabs { display: flex; flex-direction: row; background: #fff; border-radius: 12rpx; margin-bottom: 20rpx; }
|
|
|
+.tab { flex: 1; text-align: center; padding: 20rpx 0; font-size: 28rpx; color: #8A94A6; }
|
|
|
+.tab.active { color: #4A9BD7; font-weight: 600; }
|
|
|
+.card { background: #fff; border-radius: 16rpx; padding: 24rpx; margin-bottom: 20rpx; }
|
|
|
+.row { display: flex; flex-direction: row; align-items: center; justify-content: space-between; margin-bottom: 12rpx; }
|
|
|
+.name { font-size: 30rpx; color: #1F2D3D; font-weight: 500; }
|
|
|
+.badge { font-size: 22rpx; color: #4A9BD7; background: #EAF4FC; padding: 4rpx 12rpx; border-radius: 8rpx; }
|
|
|
+.badge.grey { color: #8A94A6; background: #F2F4F7; }
|
|
|
+.meta { font-size: 24rpx; color: #8A94A6; }
|
|
|
+.member-bar { background: #fff; border-radius: 12rpx; padding: 20rpx 24rpx; margin-bottom: 20rpx; }
|
|
|
+.picker { font-size: 28rpx; color: #1F2D3D; }
|
|
|
+.actions { display: flex; flex-direction: row; margin-top: 16rpx; }
|
|
|
+.btn { flex: 1; text-align: center; padding: 16rpx 0; border-radius: 12rpx; font-size: 26rpx; margin-right: 16rpx; }
|
|
|
+.btn.ok { background: #4A9BD7; color: #fff; }
|
|
|
+.btn.no { background: #F2F4F7; color: #5A6478; margin-right: 0; }
|
|
|
+.empty { text-align: center; color: #8A94A6; font-size: 28rpx; padding: 80rpx 0; }
|
|
|
+</style>
|