Browse Source

fix(frontend): 健康方案查看页+报告日期格式

- health-plan-summary: 新增 planId 参数支持查看已有方案,展示目标/成员/制定时间,含重新制定按钮
- 会员限制:付费年度家庭会员不限次,非付费会员每月仅可重新制定一次(localStorage 记录)
- health-main: goPlanSummary 传 planId;报告日期格式化为 yyyy-MM-dd

Ultraworked with [Sisyphus](https://github.com/OhMyOpenCode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
E2E Test Bot 1 month ago
parent
commit
c0f3eed2b8

+ 19 - 4
cfc-frontend/pages/health-main/index.vue

@@ -215,7 +215,7 @@
             <view class="report-type-badge" :class="'badge-' + _reportTypeClass(report.reportType)">{{ _reportTypeLabel(report.reportType) }}</view>
             <view class="report-info">
               <text class="report-name">{{ report.childName || report.personName || '健康报告' }}</text>
-              <text class="report-meta">{{ report.reportDate || '' }}{{ report.gutType ? ' · ' + report.gutType : '' }}</text>
+              <text class="report-meta">{{ _formatReportDate(report.reportDate) }}{{ report.gutType ? ' · ' + report.gutType : '' }}</text>
             </view>
             <view class="report-score-wrap">
               <text v-if="report.overallScore != null" class="report-score">{{ report.overallScore }}</text>
@@ -250,6 +250,7 @@
 import LoginGuideCard from '../../components/LoginGuideCard.vue'
 import RadarChart from '../../components/RadarChart.vue'
 import ArticleBookshelf from '../../components/ArticleBookshelf.vue'
+import { parseDate } from '../../utils/format.js'
 
 export default {
   components: { LoginGuideCard, RadarChart, ArticleBookshelf },
@@ -261,6 +262,7 @@ export default {
       selectedMemberId: '',
       reports: [],
       lastPlan: '',
+      lastPlanId: null,
       healthArticles: [],
       hasAiQHistory: false,
       tabs: [
@@ -507,7 +509,8 @@ export default {
     goSelfTest: function() { uni.navigateTo({ url: '/pages/health/survey-questionnaire?dim=all' }) },
     goPlanSummary: function() {
       var url = '/pages/health/health-plan-summary'
-      if (this.selectedMemberId) url += '?memberId=' + this.selectedMemberId
+      if (this.lastPlanId) url += '?planId=' + this.lastPlanId
+      if (this.selectedMemberId) url += (url.indexOf('?') >= 0 ? '&' : '?') + 'memberId=' + this.selectedMemberId
       uni.navigateTo({ url: url })
     },
     loadHealthData: function() {
@@ -578,7 +581,13 @@ export default {
       api.getHealthPlanList({}).then(function(res) {
         if (res && res.code === 200 && res.data) {
           var list = res.data.list || res.data
-          if (Array.isArray(list) && list.length > 0) self.lastPlan = list[0].goal || ''
+          if (Array.isArray(list) && list.length > 0) {
+            self.lastPlan = list[0].goal || ''
+            self.lastPlanId = list[0].id || null
+          } else {
+            self.lastPlan = ''
+            self.lastPlanId = null
+          }
         }
       }).catch(function() {})
     },
@@ -588,7 +597,13 @@ export default {
     },
     goMoreArticles: function() {
       uni.navigateTo({ url: '/pages/article-center/index?dimension=body' })
-    }
+    },
+    _formatReportDate: function(dateStr) {
+      if (!dateStr) return ''
+      // 后端返回 ISO 8601 (2026-08-15T14:30:00) 或 yyyy-MM-dd,统一截取前10位
+      var s = String(dateStr)
+      return s.length >= 10 ? s.substring(0, 10) : s
+    },
   }
 }
 </script>

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

@@ -171,8 +171,8 @@
       <view class="back-link" @click="step = 3">← 返回健康问卷</view>
     </view>
 
-    <!-- 成功 -->
-    <view v-if="taskSubmitted" class="success-overlay">
+    <!-- 成功(非查看模式) -->
+    <view v-if="taskSubmitted && !viewMode" class="success-overlay">
       <view class="success-card">
         <text class="success-icon">&#9989;</text>
         <text class="success-title">方案已确认</text>
@@ -180,6 +180,26 @@
         <button class="btn-done" @click="goBack">完成</button>
       </view>
     </view>
+
+    <!-- 查看已有方案(有 planId 时显示) -->
+    <view class="section plan-view-section" v-if="viewMode && planDetail">
+      <view class="section-header">
+        <text class="section-title">📋 健康方案</text>
+      </view>
+      <view class="plan-meta">
+        <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>
+      </view>
+      <view class="plan-preview" v-if="planDetail.planContent">
+        <view class="plan-preview-label">方案详情</view>
+        <text class="plan-preview-text">{{ planDetail.planContent }}</text>
+      </view>
+      <view class="plan-actions">
+        <button class="btn-edit" @click="reCreate">重新制定方案</button>
+        <button class="btn-done plan-done-btn" @click="goBack">返回</button>
+      </view>
+    </view>
   </view>
 </template>
 
@@ -203,7 +223,12 @@ export default {
       taskSubmittedCount: 0,
       healthStatus: null,
       dietPrefs: null,
-      autoExecute: false
+      autoExecute: false,
+      // 查看模式
+      planId: null,
+      planDetail: null,
+      isPaidAnnualFamily: false,
+      recreateUsed: false
     }
   },
   computed: {
@@ -213,6 +238,17 @@ export default {
     canConfirmTasks: function() {
       return this.parsedTasks.length > 0 && this.parsedTasks.every(function(t) { return t.executorId })
     },
+    /** 是否处于查看模式(有 planId 参数) */
+    viewMode: function() {
+      return !!this.planId
+    },
+    /** 是否付费年度家庭会员(不受重做次数限制) */
+    canRecreate: function() {
+      if (this.isPaidAnnualFamily) return true
+      // 非付费会员:仅可重新制定一次(通过 localStorage 记录)
+      var key = 'health_plan_recreate_used_' + (this.planDetail && this.planDetail.familyId ? this.planDetail.familyId : '')
+      return !uni.getStorageSync(key)
+    },
     healthStatusSummary: function() {
       var d = this.healthStatus || {}
       var list = []
@@ -237,8 +273,14 @@ export default {
     }
     this.subjectId = options.subjectId || ''
     this.autoExecute = options.autoExecute === '1'
+    this.planId = options.planId ? parseInt(options.planId) : null
     this.loadExecutors()
     this.loadHealthContext()
+    // 查看已有方案模式
+    if (this.planId) {
+      this.loadPlanDetail()
+      this.loadMembership()
+    }
     // 自动出方案模式:等待异步加载完成后执行
     if (this.autoExecute) {
       var self = this
@@ -249,6 +291,8 @@ export default {
   },
   onShow: function() {
     this.loadHealthContext()
+    // 每次显示时刷新会员状态(可能刚续费)
+    if (this.planId) this.loadMembership()
   },
   methods: {
     getMemberKey: function(m, idx) { return m.memberId || idx },
@@ -278,6 +322,63 @@ export default {
         }
       }).catch(function() { self.dietPrefs = null })
     },
+    /** 查看模式:加载已有方案详情 */
+    loadPlanDetail: function() {
+      var self = this
+      var api = require('../../utils/api.js')
+      api.getHealthPlanDetail({ planId: this.planId }).then(function(res) {
+        if (res && res.code === 200 && res.data) {
+          self.planDetail = res.data
+          // 还原目标
+          self.userGoal = res.data.goal || ''
+          // 还原执行成员
+          if (res.data.memberIds) {
+            self.selectedMemberIds = res.data.memberIds.split(',').filter(function(id) { return id })
+          }
+          // 解析方案内容为任务(只读展示)
+          self.parseTasksFromPlan(res.data.planContent)
+          self.planContent = res.data.planContent
+        }
+      }).catch(function() {
+        uni.showToast({ title: '方案加载失败', icon: 'none' })
+        setTimeout(function() { uni.navigateBack() }, 1500)
+      })
+    },
+    /** 加载会员信息判断是否付费年度家庭会员 */
+    loadMembership: function() {
+      var self = this
+      var api = require('../../utils/api.js')
+      api.getMyMembership().then(function(res) {
+        if (res && res.code === 200 && res.data && res.data.membership) {
+          var m = res.data.membership
+          self.isPaidAnnualFamily = !!(m.isActive && (m.levelCode === 'ANNUAL_FAMILY' || m.levelCode === 'ANNUAL'))
+        }
+      }).catch(function() {})
+    },
+    /** 重新制定:检查会员限制后进入新建流程 */
+    reCreate: function() {
+      if (!this.canRecreate) {
+        uni.showModal({
+          title: '提示',
+          content: '非付费会员每月仅限重新制定一次,升级年度家庭会员可无限次重做',
+          showCancel: false,
+          confirmText: '知道了'
+        })
+        return
+      }
+      // 标记已使用一次(非付费会员)
+      if (!this.isPaidAnnualFamily) {
+        var key = 'health_plan_recreate_used_' + (this.planDetail && this.planDetail.familyId ? this.planDetail.familyId : '')
+        if (key) uni.setStorageSync(key, true)
+      }
+      // 重置为新建模式
+      this.step = 1
+      this.planId = null
+      this.planDetail = null
+      this.taskSubmitted = false
+      this.parsedTasks = []
+      this.planContent = ''
+    },
     toggleMember: function(memberId) {
       var idx = this.selectedMemberIds.indexOf(memberId)
       if (idx >= 0) this.selectedMemberIds.splice(idx, 1)
@@ -927,4 +1028,36 @@ export default {
 .btn-step-next {
   width: 100%;
 }
+
+/* ===== 方案查看模式 ===== */
+.plan-view-section { margin-top: 0; }
+.plan-meta {
+  display: flex;
+  flex-direction: column;
+  gap: 6rpx;
+  margin-bottom: 20rpx;
+  padding: 16rpx 20rpx;
+  background: #FFF7ED;
+  border-radius: 12rpx;
+}
+.plan-meta-item {
+  font-size: 24rpx;
+  color: #92400E;
+}
+.plan-actions {
+  display: flex;
+  gap: 16rpx;
+  margin-top: 24rpx;
+}
+.plan-done-btn {
+  flex: 1;
+  height: 80rpx;
+  line-height: 80rpx;
+  background: #F97316;
+  color: #fff;
+  font-size: 28rpx;
+  border-radius: 40rpx;
+  border: none;
+  text-align: center;
+}
 </style>