Explorar el Código

feat(health-plan): rewrite plan creation flow to 3 steps with embedded questionnaire

- Redesign flow: 选人+目标 → 健康问卷(内嵌) → 生成方案+修改确认/选择规划师
- Embed health-status-form fields inline in step 2 (体征/血脂/疾病史/药物/补充)
- Auto-load existing health data via getHealthStatus on page load
- Add saveAndGenerate to save form then call generateHealthPlan
- Step 3 shows plan preview with two paths: edit/confirm OR select planner service
- Create teacher-service.vue: planner service selection page (纯线上/可联系 types)
- Register teacher-service.vue in pages.json
- Preserve viewMode/planDetail for viewing existing plans
- Preserve all section editing (regenerate/confirm) and plan submission logic
- Follow WeChat Mini-Program constraints: no optional chaining, no CSS Grid, no :key expressions

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
liaoxg hace 3 semanas
padre
commit
ed938ca851

+ 6 - 0
cfc-frontend/pages.json

@@ -932,6 +932,12 @@
             "navigationBarTitleText": "方案制定"
           }
         },
+        {
+          "path": "teacher-service",
+          "style": {
+            "navigationBarTitleText": "选择规划师服务"
+          }
+        },
         {
           "path": "health-status-form",
           "style": {

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 700 - 113
cfc-frontend/pages/health/health-plan-summary.vue


+ 377 - 0
cfc-frontend/pages/health/teacher-service.vue

@@ -0,0 +1,377 @@
+<template>
+  <view class="ts-page">
+    <!-- 提示 -->
+    <view class="ts-tip">
+      <text class="ts-tip-icon">👩‍🏫</text>
+      <text class="ts-tip-text">选择规划师服务,专业团队为您审核和定制健康方案</text>
+    </view>
+
+    <!-- 服务类型切换 -->
+    <view class="ts-type-bar">
+      <view class="ts-type-item" :class="{ active: serviceType === 'online' }" @click="serviceType = 'online'">
+        <text class="ts-type-icon">💻</text>
+        <text class="ts-type-label">纯线上服务</text>
+      </view>
+      <view class="ts-type-item" :class="{ active: serviceType === 'contact' }" @click="serviceType = 'contact'">
+        <text class="ts-type-icon">📱</text>
+        <text class="ts-type-label">可联系服务</text>
+      </view>
+    </view>
+
+    <!-- 服务说明 -->
+    <view class="ts-info-card" v-if="serviceType === 'online'">
+      <text class="ts-info-title">纯线上服务</text>
+      <text class="ts-info-desc">• 规划师通过小程序内消息与您沟通</text>
+      <text class="ts-info-desc">• 双方不见面,不单独微信/电话联系</text>
+      <text class="ts-info-desc">• 所有沟通记录可追溯</text>
+      <text class="ts-info-price">费用包含:方案审核、定制调整、30天在线咨询</text>
+      <text class="ts-info-refund">退费说明:规划师审核前可全额退款;方案确认后不支持退款</text>
+    </view>
+    <view class="ts-info-card" v-else>
+      <text class="ts-info-title">可联系服务</text>
+      <text class="ts-info-desc">• 规划师可通过微信、电话与您联系</text>
+      <text class="ts-info-desc">• 购买后可查看规划师联系方式</text>
+      <text class="ts-info-desc">• 支持一对一深度沟通</text>
+      <text class="ts-info-price">费用包含:方案审核、定制调整、90天专属顾问、微信/电话沟通</text>
+      <text class="ts-info-refund">退费说明:规划师联系前可全额退款;首次沟通后不支持退款</text>
+    </view>
+
+    <!-- 规划师列表 -->
+    <view class="ts-list" v-if="guides.length > 0">
+      <view v-for="(g, idx) in guides" :key="getGuideKey(g, idx)"
+            class="ts-card" :class="{ active: selectedGuideId === g.id }"
+            @click="selectGuide(g)">
+        <view class="ts-card-top">
+          <view class="ts-avatar">{{ (g.name || '规划师').charAt(0) }}</view>
+          <view class="ts-card-info">
+            <text class="ts-card-name">{{ g.name || '规划师' }}</text>
+            <text class="ts-card-spec" v-if="g.specialty">{{ g.specialty }}</text>
+            <text class="ts-card-tag" v-if="g.isBound">已绑定家庭</text>
+          </view>
+          <view class="ts-card-check">{{ selectedGuideId === g.id ? '✓' : '' }}</view>
+        </view>
+        <view class="ts-card-desc" v-if="g.description">
+          <text>{{ g.description }}</text>
+        </view>
+        <view class="ts-card-price" v-if="g.price">
+          <text class="ts-price-val">¥{{ g.price }}</text>
+          <text class="ts-price-unit">/ {{ serviceType === 'online' ? '30天' : '90天' }}</text>
+        </view>
+      </view>
+    </view>
+
+    <!-- 空状态 -->
+    <view class="ts-empty" v-if="!loading && guides.length === 0">
+      <text class="ts-empty-icon">🔍</text>
+      <text class="ts-empty-text">暂无可用规划师</text>
+      <text class="ts-empty-hint">请联系管理员为您分配规划师</text>
+    </view>
+
+    <!-- 加载中 -->
+    <view class="ts-loading" v-if="loading">
+      <text class="ts-loading-text">加载中...</text>
+    </view>
+
+    <!-- 底部按钮 -->
+    <view class="ts-bottom-bar">
+      <button class="ts-btn-submit" :disabled="!selectedGuideId || submitting"
+              @click="confirmSelection">
+        {{ submitting ? '提交中...' : (selectedGuideId ? '确认选择并支付' : '请选择规划师') }}
+      </button>
+    </view>
+  </view>
+</template>
+
+<script>
+import { getAvailableGuides, getGuidePackage } from '../../utils/api'
+
+export default {
+  data: function() {
+    return {
+      guides: [],
+      selectedGuideId: null,
+      serviceType: 'online',
+      familyId: null,
+      planId: null,
+      memberIds: '',
+      goal: '',
+      loading: false,
+      submitting: false
+    }
+  },
+  onLoad: function(options) {
+    this.familyId = options.familyId ? parseInt(options.familyId) : null
+    this.planId = options.planId ? parseInt(options.planId) : null
+    this.memberIds = options.memberIds || ''
+    this.goal = options.goal || ''
+    if (!this.familyId) {
+      this.familyId = uni.getStorageSync('familyId') ? parseInt(uni.getStorageSync('familyId')) : null
+    }
+    if (!this.familyId) {
+      uni.showToast({ title: '缺少家庭信息', icon: 'none' })
+      setTimeout(function() { uni.navigateBack() }, 1000)
+      return
+    }
+    this.loadGuides()
+  },
+  methods: {
+    getGuideKey: function(g, idx) {
+      return g.id || ('g' + idx)
+    },
+    loadGuides: function() {
+      var self = this
+      self.loading = true
+      getAvailableGuides({}).then(function(res) {
+        self.loading = false
+        if (res && res.code === 200 && res.data) {
+          var list = res.data instanceof Array ? res.data : []
+          self.guides = list
+        } else {
+          self.guides = []
+        }
+      }).catch(function() {
+        self.loading = false
+        self.guides = []
+      })
+    },
+    selectGuide: function(g) {
+      this.selectedGuideId = g.id
+    },
+    confirmSelection: function() {
+      var self = this
+      if (!self.selectedGuideId || self.submitting) return
+      self.submitting = true
+      // 将选择保存到本地,返回上一页时 health-plan-summary 会读取
+      uni.setStorageSync('healthPlan_teacherId', self.selectedGuideId)
+      uni.setStorageSync('healthPlan_serviceType', self.serviceType)
+      uni.showToast({ title: '已选择规划师', icon: 'success' })
+      setTimeout(function() {
+        self.submitting = false
+        uni.navigateBack()
+      }, 800)
+    }
+  }
+}
+</script>
+
+<style scoped>
+.ts-page {
+  min-height: 100vh;
+  background: #F5F7FA;
+  padding-bottom: 160rpx;
+}
+.ts-tip {
+  margin: 24rpx 30rpx;
+  padding: 20rpx 24rpx;
+  background: #FFF7E6;
+  border-radius: 12rpx;
+  display: flex;
+  align-items: flex-start;
+  gap: 12rpx;
+}
+.ts-tip-icon { font-size: 36rpx; }
+.ts-tip-text { font-size: 26rpx; color: #8A6D3B; line-height: 1.6; flex: 1; }
+
+/* 服务类型切换 */
+.ts-type-bar {
+  display: flex;
+  gap: 20rpx;
+  margin: 24rpx 30rpx;
+}
+.ts-type-item {
+  flex: 1;
+  background: #fff;
+  border-radius: 16rpx;
+  padding: 24rpx 16rpx;
+  text-align: center;
+  border: 3rpx solid #E5E7EB;
+}
+.ts-type-item.active {
+  border-color: #F97316;
+  background: #FFFBF0;
+}
+.ts-type-icon { font-size: 48rpx; display: block; margin-bottom: 8rpx; }
+.ts-type-label { font-size: 26rpx; font-weight: 600; color: #333; }
+.ts-type-item.active .ts-type-label { color: #F97316; }
+
+/* 服务说明 */
+.ts-info-card {
+  margin: 0 30rpx 24rpx;
+  background: #fff;
+  border-radius: 16rpx;
+  padding: 24rpx;
+  border-left: 6rpx solid #F97316;
+}
+.ts-info-title {
+  font-size: 28rpx;
+  font-weight: bold;
+  color: #333;
+  display: block;
+  margin-bottom: 12rpx;
+}
+.ts-info-desc {
+  font-size: 24rpx;
+  color: #666;
+  line-height: 1.8;
+  display: block;
+}
+.ts-info-price {
+  font-size: 24rpx;
+  color: #F97316;
+  font-weight: 600;
+  display: block;
+  margin-top: 12rpx;
+  padding-top: 12rpx;
+  border-top: 1rpx solid #F0F0F0;
+}
+.ts-info-refund {
+  font-size: 22rpx;
+  color: #999;
+  display: block;
+  margin-top: 8rpx;
+}
+
+/* 规划师列表 */
+.ts-list {
+  margin: 0 30rpx;
+  display: flex;
+  flex-direction: column;
+  gap: 16rpx;
+}
+.ts-card {
+  background: #fff;
+  border-radius: 16rpx;
+  padding: 24rpx;
+  border: 3rpx solid #E5E7EB;
+  transition: all 0.2s;
+}
+.ts-card.active {
+  border-color: #F97316;
+  background: #FFFBF0;
+}
+.ts-card-top {
+  display: flex;
+  align-items: center;
+  gap: 16rpx;
+}
+.ts-avatar {
+  width: 72rpx;
+  height: 72rpx;
+  border-radius: 36rpx;
+  background: #ececec;
+  border: 2rpx solid #ddd;
+  color: #888;
+  font-size: 32rpx;
+  font-weight: bold;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  flex-shrink: 0;
+}
+.ts-card.active .ts-avatar {
+  background: #F97316;
+  border-color: #F97316;
+  color: #fff;
+}
+.ts-card-info { flex: 1; }
+.ts-card-name {
+  font-size: 30rpx;
+  font-weight: 600;
+  color: #333;
+  display: block;
+}
+.ts-card.active .ts-card-name { color: #F97316; }
+.ts-card-spec {
+  font-size: 24rpx;
+  color: #888;
+  display: block;
+  margin-top: 4rpx;
+}
+.ts-card-tag {
+  font-size: 22rpx;
+  color: #4CAF50;
+  display: block;
+  margin-top: 4rpx;
+}
+.ts-card-check {
+  width: 48rpx;
+  height: 48rpx;
+  border-radius: 24rpx;
+  border: 3rpx solid #d5d5d5;
+  background: #fff;
+  color: transparent;
+  font-size: 26rpx;
+  line-height: 42rpx;
+  text-align: center;
+  flex-shrink: 0;
+}
+.ts-card.active .ts-card-check {
+  background: #F97316;
+  border-color: #F97316;
+  color: #fff;
+}
+.ts-card-desc {
+  margin-top: 12rpx;
+  font-size: 24rpx;
+  color: #888;
+  line-height: 1.6;
+}
+.ts-card-price {
+  margin-top: 12rpx;
+  display: flex;
+  align-items: baseline;
+  gap: 4rpx;
+}
+.ts-price-val {
+  font-size: 36rpx;
+  font-weight: bold;
+  color: #F97316;
+}
+.ts-price-unit {
+  font-size: 24rpx;
+  color: #999;
+}
+
+/* 空状态 */
+.ts-empty {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  padding: 80rpx 0;
+}
+.ts-empty-icon { font-size: 80rpx; margin-bottom: 16rpx; }
+.ts-empty-text { font-size: 28rpx; color: #bbb; }
+.ts-empty-hint { font-size: 24rpx; color: #ddd; margin-top: 8rpx; }
+
+/* 加载中 */
+.ts-loading {
+  display: flex;
+  justify-content: center;
+  padding: 60rpx 0;
+}
+.ts-loading-text { font-size: 26rpx; color: #999; }
+
+/* 底部按钮 */
+.ts-bottom-bar {
+  position: fixed;
+  bottom: 0;
+  left: 0;
+  right: 0;
+  background: #fff;
+  padding: 20rpx 30rpx;
+  padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
+  box-shadow: 0 -2rpx 12rpx rgba(0,0,0,0.06);
+}
+.ts-btn-submit {
+  width: 100%;
+  height: 88rpx;
+  line-height: 88rpx;
+  border-radius: 44rpx;
+  border: none;
+  font-size: 30rpx;
+  text-align: center;
+  background: linear-gradient(135deg, #F97316, #FB923C);
+  color: #fff;
+}
+.ts-btn-submit[disabled] { opacity: 0.4; }
+</style>

Algunos archivos no se mostraron porque demasiados archivos cambiaron en este cambio