Преглед изворни кода

feat(health): 规划师方案维护功能 — 前端审核页面 + 路由 + API

- cfc-web: 新增 teacher/HealthPlanReview.vue 健康方案审核页面(编辑/通过/驳回)
- cfc-web: 新增 api/healthPlan.js 接口封装
- cfc-web: TeacherDashboard 快捷入口新增「健康方案审核」
- cfc-web: router 新增 /teacher-health-plan-review 路由
- cfc-frontend: health-plan-summary.vue 更新状态展示和成功提示

后端已有 c4744618,本次为前端配套
iwt пре 1 месец
родитељ
комит
1da715277e

+ 6 - 3
cfc-frontend/pages/health/health-plan-summary.vue

@@ -115,13 +115,16 @@
         <text class="plan-meta-item">目标:{{ planDetail.goal || '—' }}</text>
         <text class="plan-meta-item" v-if="planDetail.memberName">成员:{{ planDetail.memberName }}</text>
         <text class="plan-meta-item" v-if="planDetail.createdAt">制定于 {{ formatTime(planDetail.createdAt) }}</text>
-        <text class="plan-meta-item" v-if="planDetail.status === 'confirmed'">状态:已确认</text>
+        <text class="plan-meta-item" v-if="planDetail.status === 'confirmed' || planDetail.status === 'published'">状态:已发布</text>
+      <text class="plan-meta-item" v-if="planDetail.status === 'pending_review'">状态:待规划师审核</text>
+      <text class="plan-meta-item" v-if="planDetail.status === 'rejected'">状态:已驳回</text>
+      <text class="plan-meta-item" v-if="planDetail.reviewComment && planDetail.status !== 'draft'">审核备注:{{ planDetail.reviewComment }}</text>
       </view>
       <view v-if="planDetail.planContent" class="plan-preview">
         <text class="plan-preview-text">{{ planDetail.planContent }}</text>
       </view>
       <view class="plan-actions">
-        <button class="btn-edit" @click="reCreate" v-if="planDetail.status !== 'confirmed'">重新制定</button>
+        <button class="btn-edit" @click="reCreate" v-if="planDetail.status !== 'confirmed' && planDetail.status !== 'published' && planDetail.status !== 'pending_review'">重新制定</button>
         <button class="btn-done plan-done-btn" @click="goBack">返回</button>
       </view>
     </view>
@@ -130,7 +133,7 @@
     <view v-if="taskSubmitted" class="success-overlay">
       <view class="success-card">
         <text class="success-icon">✓</text>
-        <text class="success-title">方案已确认</text>
+        <text class="success-title">{{ planDetail.status === 'published' || planDetail.status === 'confirmed' ? '方案已发布' : '方案已提交审核' }}</text>
         <text class="success-sub">已为您创建日常任务</text>
         <button class="btn-done" @click="goBack">完成</button>
       </view>

+ 41 - 0
cfc-web/src/api/healthPlan.js

@@ -0,0 +1,41 @@
+import request from '@/utils/request'
+
+export function getPendingReviewPlans(params) {
+  return request({
+    url: '/api/health/plan/pending-review/list',
+    method: 'post',
+    data: params
+  })
+}
+
+export function updatePlanContent(data) {
+  return request({
+    url: '/api/health/plan/pending-review/update',
+    method: 'post',
+    data
+  })
+}
+
+export function approvePlan(data) {
+  return request({
+    url: '/api/health/plan/pending-review/approve',
+    method: 'post',
+    data
+  })
+}
+
+export function rejectPlan(data) {
+  return request({
+    url: '/api/health/plan/pending-review/reject',
+    method: 'post',
+    data
+  })
+}
+
+export function getHealthPlanDetail(data) {
+  return request({
+    url: '/api/health/plan/detail',
+    method: 'post',
+    data
+  })
+}

+ 6 - 0
cfc-web/src/router/index.js

@@ -198,6 +198,12 @@ const routes = [
         component: () => import('@/views/teacher/ProxyReport.vue'),
         meta: { title: '代上传报告', requiresTeacher: true, perm: 'health:reports' }
       },
+      {
+        path: 'teacher-health-plan-review',
+        name: 'TeacherHealthPlanReview',
+        component: () => import('@/views/teacher/HealthPlanReview.vue'),
+        meta: { title: '健康方案审核', requiresTeacher: true, perm: 'health:plan:review' }
+      },
       {
         path: 'assessment-admin',
         name: 'AssessmentAdmin',

+ 6 - 0
cfc-web/src/views/TeacherDashboard.vue

@@ -80,6 +80,12 @@
               </div>
               <span>DAN测评</span>
             </div>
+            <div class="td-action-item" @click="$router.push('/teacher-health-plan-review')">
+              <div class="td-action-icon" style="background: #FEF3C7">
+                <i class="el-icon-document-checked" style="color: #D97706"></i>
+              </div>
+              <span>健康方案审核</span>
+            </div>
           </div>
         </el-card>
       </el-col>

+ 214 - 0
cfc-web/src/views/teacher/HealthPlanReview.vue

@@ -0,0 +1,214 @@
+<template>
+  <div class="health-plan-review">
+    <div class="page-header">
+      <h2>健康方案审核</h2>
+      <div class="filters">
+        <el-select v-model="familyFilter" placeholder="选择家庭" clearable @change="loadPlans" style="width:180px">
+          <el-option v-for="f in families" :key="f.id" :label="f.familyName || '家庭'+f.id" :value="f.id" />
+        </el-select>
+        <el-select v-model="statusFilter" placeholder="状态筛选" clearable @change="loadPlans" style="width:140px;margin-left:10px">
+          <el-option label="待审核" value="pending_review" />
+          <el-option label="草稿" value="draft" />
+          <el-option label="已驳回" value="rejected" />
+        </el-select>
+        <el-button type="primary" @click="loadPlans" style="margin-left:10px">查询</el-button>
+      </div>
+    </div>
+
+    <el-table :data="plans" border stripe style="width:100%" v-loading="loading">
+      <el-table-column prop="id" label="方案ID" width="80" />
+      <el-table-column label="家庭成员" width="150">
+        <template slot-scope="{ row }">{{ row.memberName || row.memberIds }}</template>
+      </el-table-column>
+      <el-table-column label="目标" min-width="180">
+        <template slot-scope="{ row }">{{ row.goal ? row.goal.substring(0,60) + (row.goal.length>60?'...':'') : '-' }}</template>
+      </el-table-column>
+      <el-table-column label="维度" width="120">
+        <template slot-scope="{ row }">
+          <el-tag v-for="(d,i) in (row.dimensions||'').split(',')" :key="i" size="mini" style="margin-right:4px">{{ d }}</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="状态" width="100">
+        <template slot-scope="{ row }">
+          <el-tag :type="statusType(row.status)">{{ statusLabel(row.status) }}</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column prop="reviewComment" label="审核意见" width="150">
+        <template slot-scope="{ row }">{{ row.reviewComment || '-' }}</template>
+      </el-table-column>
+      <el-table-column prop="createdAt" label="创建时间" width="160" />
+      <el-table-column label="操作" width="220">
+        <template slot-scope="{ row }">
+          <el-button size="mini" type="primary" @click="showEdit(row)">编辑</el-button>
+          <el-button v-if="row.status==='pending_review'||row.status==='draft'" size="mini" type="success" @click="handleApprove(row)">通过</el-button>
+          <el-button v-if="row.status==='pending_review'||row.status==='draft'" size="mini" type="danger" @click="showReject(row)">驳回</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <!-- 编辑弹窗 -->
+    <el-dialog title="编辑健康方案" :visible.sync="editVisible" width="700px" destroy-on-close>
+      <el-form label-width="90px" v-if="currentPlan">
+        <el-form-item label="方案内容">
+          <el-input v-model="editForm.planContent" type="textarea" :rows="18" placeholder="编辑方案内容..." />
+        </el-form-item>
+        <el-form-item label="审核意见">
+          <el-input v-model="editForm.reviewComment" type="textarea" :rows="3" placeholder="可选:添加审核备注" />
+        </el-form-item>
+      </el-form>
+      <div slot="footer">
+        <el-button @click="editVisible=false">取消</el-button>
+        <el-button type="primary" :loading="saving" @click="saveEdit">保存修改</el-button>
+      </div>
+    </el-dialog>
+
+    <!-- 驳回弹窗 -->
+    <el-dialog title="驳回方案" :visible.sync="rejectVisible" width="420px">
+      <el-form label-width="80px">
+        <el-form-item label="驳回原因">
+          <el-input v-model="rejectForm.comment" type="textarea" :rows="4" placeholder="请输入驳回原因" />
+        </el-form-item>
+      </el-form>
+      <div slot="footer">
+        <el-button @click="rejectVisible=false">取消</el-button>
+        <el-button type="danger" :loading="rejecting" @click="confirmReject">确认驳回</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import { getPendingReviewPlans, updatePlanContent, approvePlan, rejectPlan, getHealthPlanDetail } from '@/api/healthPlan'
+
+export default {
+  name: 'HealthPlanReview',
+  data() {
+    return {
+      plans: [],
+      families: [],
+      familyFilter: '',
+      statusFilter: '',
+      loading: false,
+      editVisible: false,
+      rejectVisible: false,
+      saving: false,
+      rejecting: false,
+      currentPlan: null,
+      editForm: { planContent: '', reviewComment: '' },
+      rejectForm: { planId: null, comment: '' }
+    }
+  },
+  created() {
+    this.loadFamilies()
+    this.loadPlans()
+  },
+  methods: {
+    async loadFamilies() {
+      try {
+        const res = await fetch('/api/family/list', {
+          method: 'POST',
+          headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + localStorage.getItem('token') },
+          body: JSON.stringify({})
+        })
+        const json = await res.json()
+        if (json.code === 200) this.families = json.data || []
+      } catch(e) {}
+    },
+    async loadPlans() {
+      this.loading = true
+      try {
+        const res = await getPendingReviewPlans({
+          familyId: this.familyFilter || null,
+          status: this.statusFilter || null
+        })
+        if (res.code === 200) this.plans = res.data || []
+      } catch(e) {
+        this.$message.error('加载失败')
+      } finally {
+        this.loading = false
+      }
+    },
+    statusType(s) {
+      const map = { pending_review: 'warning', draft: 'info', published: 'success', rejected: 'danger' }
+      return map[s] || 'info'
+    },
+    statusLabel(s) {
+      const map = { pending_review: '待审核', draft: '草稿', published: '已发布', rejected: '已驳回' }
+      return map[s] || s
+    },
+    showEdit(row) {
+      this.currentPlan = row
+      this.editForm = {
+        planContent: row.planContent || '',
+        reviewComment: row.reviewComment || ''
+      }
+      this.editVisible = true
+    },
+    async saveEdit() {
+      this.saving = true
+      try {
+        const res = await updatePlanContent({
+          planId: this.currentPlan.id,
+          planContent: this.editForm.planContent,
+          planJson: this.currentPlan.planJson
+        })
+        if (res.code === 200) {
+          this.$message.success('保存成功')
+          this.editVisible = false
+          this.loadPlans()
+        } else {
+          this.$message.error(res.message || '保存失败')
+        }
+      } catch(e) {
+        this.$message.error('请求失败')
+      } finally {
+        this.saving = false
+      }
+    },
+    showReject(row) {
+      this.rejectForm = { planId: row.id, comment: '' }
+      this.rejectVisible = true
+    },
+    async confirmReject() {
+      this.rejecting = true
+      try {
+        const res = await rejectPlan({
+          planId: this.rejectForm.planId,
+          comment: this.rejectForm.comment
+        })
+        if (res.code === 200) {
+          this.$message.success('已驳回')
+          this.rejectVisible = false
+          this.loadPlans()
+        } else {
+          this.$message.error(res.message || '操作失败')
+        }
+      } catch(e) {
+        this.$message.error('请求失败')
+      } finally {
+        this.rejecting = false
+      }
+    },
+    async handleApprove(row) {
+      try {
+        const res = await approvePlan({ planId: row.id })
+        if (res.code === 200) {
+          this.$message.success('方案已发布,任务自动生成')
+          this.loadPlans()
+        } else {
+          this.$message.error(res.message || '操作失败')
+        }
+      } catch(e) {
+        this.$message.error('请求失败')
+      }
+    }
+  }
+}
+</script>
+
+<style scoped>
+.health-plan-review { padding: 20px; }
+.page-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
+.page-header h2 { margin: 0; font-size: 20px; }
+.filters { display: flex; align-items: center; }
+</style>