Эх сурвалжийг харах

feat(frontend): 新增单家庭方案/任务双 Tab 审核页

iwt 2 өдөр өмнө
parent
commit
3e4ae77b82

+ 193 - 0
cfc-frontend/pages/teacher/review-family.vue

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