Przeglądaj źródła

feat(cfc-frontend): tongue diagnosis as health report type

- report-upload.vue: add tongue type selector, photo capture & AI analysis
- report-confirm.vue: add tongue indicators card, route confirm to confirmTongue
- report-list.vue: add tongue badge display
- body/index.vue: redirect tongue entry to report-upload?reportType=tongue
- tongue-index.vue: deprecate — redirect to new report flow
- api.js: parseReportPreview accepts optional type param
Sisyphus 3 miesięcy temu
rodzic
commit
b024dd7913

+ 1 - 1
cfc-frontend/pages/body/index.vue

@@ -282,7 +282,7 @@ export default {
         { icon: '\u{1F957}', label: '饮食', needLogin: true, page: '/pages/body/meal-record' },
         { icon: '\u{1F634}', label: '作息', needLogin: true, page: '/pages/body/sleep-record' },
         { icon: '\u{1F9D8}', label: '冥想', needLogin: true, page: '/pages/health/meditation-index' },
-        { icon: '\u{1F4CA}', label: '舌诊', needLogin: true, page: '/pages/health/tongue-index' }
+        { icon: '\u{1F4CA}', label: '舌诊', needLogin: true, page: '/pages/health/report-upload?reportType=tongue' }
       ],
       contactList: [],
       healthAlerts: [],

+ 82 - 7
cfc-frontend/pages/health/report-confirm.vue

@@ -40,11 +40,11 @@
           <text class="field-label">年龄</text>
           <text class="field-value">{{ extractedAge }}</text>
         </view>
-        <view class="field-row" v-if="payload.summary">
+        <view class="field-row" v-if="payload.summary && payload.type !== 'tongue'">
           <text class="field-label">综合评分</text>
           <input class="field-input" type="number" v-model="payload.summary.overallScore" />
         </view>
-        <view class="field-row" v-if="payload.summary">
+        <view class="field-row" v-if="payload.summary && payload.type !== 'tongue'">
           <text class="field-label">报告摘要</text>
           <textarea class="field-textarea" v-model="payload.summary.summary" maxlength="500" />
         </view>
@@ -126,8 +126,23 @@
         </view>
       </view>
 
-      <!-- 健康指标 -->
-      <view class="card" v-if="payload.indicators && payload.indicators.length">
+      <!-- 舌诊指标(只读展示) -->
+      <view class="card" v-if="payload.type === 'tongue' && payload.indicators && payload.indicators.length">
+        <view class="card-title">舌象指标</view>
+        <view class="tongue-indicator-grid">
+          <view class="tongue-ind-item" v-for="(ind, idx) in payload.indicators" :key="idx">
+            <text class="tongue-ind-label">{{ indicatorLabel(ind.code) }}</text>
+            <text class="tongue-ind-value">{{ ind.value }}</text>
+          </view>
+        </view>
+        <view class="tongue-summary" v-if="payload.summary && payload.summary.overallAssessment">
+          <text class="tongue-summary-title">综合评估</text>
+          <text class="tongue-summary-text">{{ payload.summary.overallAssessment }}</text>
+        </view>
+      </view>
+
+      <!-- 健康指标(舌诊时隐藏,使用上方舌象指标卡片) -->
+      <view class="card" v-if="payload.type !== 'tongue' && payload.indicators && payload.indicators.length">
         <view class="card-title">健康指标 ({{ payload.indicators.length }})</view>
         <view class="indicator-item" v-for="(ind, idx) in payload.indicators" :key="idx">
           <view class="ind-header">
@@ -199,7 +214,7 @@
 </template>
 
 <script>
-import { confirmReportPreview, discardReportDraft } from '../../utils/api.js'
+import { confirmReportPreview, discardReportDraft, confirmTongue, discardTongue } from '../../utils/api.js'
 import { addFamilyMember } from '../../utils/api.js'
 
 export default {
@@ -418,7 +433,8 @@ export default {
         postData.subjectId = self.childId
       }
 
-      confirmReportPreview(postData).then(function(res) {
+      var confirmFn = self.payload.type === 'tongue' ? confirmTongue : confirmReportPreview
+      confirmFn(postData).then(function(res) {
         uni.hideLoading()
         if (res.code === 200) {
           uni.showToast({ title: '入库成功', icon: 'success' })
@@ -453,7 +469,8 @@ export default {
         content: '放弃后解析数据将被删除,需要重新上传',
         success: function(res) {
           if (res.confirm) {
-            discardReportDraft(self.draftId).then(function() {
+            var discardFn = self.payload.type === 'tongue' ? discardTongue : discardReportDraft
+            discardFn({ draftId: self.draftId }).then(function() {
               uni.showToast({ title: '已放弃', icon: 'none' })
               setTimeout(function() { uni.navigateBack() }, 500)
             }).catch(function() {
@@ -465,6 +482,19 @@ export default {
     },
     goBack() {
       uni.navigateBack()
+    },
+    // 舌诊指标标签映射
+    indicatorLabel(code) {
+      var map = {
+        'tongue_color': '舌色',
+        'coating_color': '苔色',
+        'coating_texture': '苔质',
+        'fissure': '裂纹',
+        'teeth_mark': '齿痕',
+        'sublingual_vein': '舌下静脉',
+        'constitution': '体质'
+      }
+      return map[code] || code
     }
   }
 }
@@ -1008,4 +1038,49 @@ export default {
   margin-top: 10rpx;
 }
 .btn-create-submit:active { opacity: 0.85; }
+
+/* ===== 舌诊指标 ===== */
+.tongue-indicator-grid {
+  display: flex;
+  flex-direction: row;
+  flex-wrap: wrap;
+  gap: 12rpx;
+}
+.tongue-ind-item {
+  width: calc(50% - 6rpx);
+  background: #F8FAFC;
+  border-radius: 16rpx;
+  padding: 20rpx 24rpx;
+  box-sizing: border-box;
+}
+.tongue-ind-label {
+  display: block;
+  font-size: 24rpx;
+  color: #94A3B8;
+  margin-bottom: 8rpx;
+}
+.tongue-ind-value {
+  display: block;
+  font-size: 28rpx;
+  color: #1E293B;
+  font-weight: 500;
+}
+.tongue-summary {
+  margin-top: 20rpx;
+  padding: 20rpx 24rpx;
+  background: #FFF8E1;
+  border-radius: 16rpx;
+}
+.tongue-summary-title {
+  font-size: 24rpx;
+  color: #F59E0B;
+  font-weight: 600;
+  display: block;
+  margin-bottom: 8rpx;
+}
+.tongue-summary-text {
+  font-size: 26rpx;
+  color: #333;
+  line-height: 1.6;
+}
 </style>

+ 6 - 2
cfc-frontend/pages/health/report-list.vue

@@ -21,8 +21,8 @@
         @click="goToDetail(item.id)"
       >
         <view class="report-card-left">
-          <view class="report-type-badge" :class="'badge-' + (item.reportType === 'gut_flora' ? 'gut' : 'exam')">
-            <text>{{ item.reportType === 'gut_flora' ? '肠道检测' : '体检报告' }}</text>
+          <view class="report-type-badge" :class="'badge-' + (item.reportType === 'gut_flora' ? 'gut' : item.reportType === 'tongue' ? 'tongue' : 'exam')">
+            <text>{{ item.reportType === 'gut_flora' ? '肠道检测' : item.reportType === 'tongue' ? '舌诊' : '体检报告' }}</text>
           </view>
           <text class="report-date">{{ formatDate(item.reportDate) }}</text>
         </view>
@@ -180,6 +180,10 @@ export default {
   background: #E8F5E9;
   color: #2E7D32;
 }
+.badge-tongue {
+  background: #FFF3E0;
+  color: #E65100;
+}
 .report-date {
   font-size: 24rpx;
   color: #888;

+ 180 - 6
cfc-frontend/pages/health/report-upload.vue

@@ -65,7 +65,7 @@
           <view
             class="type-option"
             :class="{ 'type-selected': form.reportType === 'physical_exam' }"
-            @click="form.reportType = 'physical_exam'"
+            @click="selectType('physical_exam')"
           >
             <text class="type-icon">🏥</text>
             <text class="type-name">体检报告</text>
@@ -73,16 +73,43 @@
           <view
             class="type-option"
             :class="{ 'type-selected': form.reportType === 'gut_flora' }"
-            @click="form.reportType = 'gut_flora'"
+            @click="selectType('gut_flora')"
           >
             <text class="type-icon">🔬</text>
             <text class="type-name">肠道检测</text>
           </view>
+          <view
+            class="type-option"
+            :class="{ 'type-selected': form.reportType === 'tongue' }"
+            @click="selectType('tongue')"
+          >
+            <text class="type-icon">👅</text>
+            <text class="type-name">舌诊拍照</text>
+          </view>
         </view>
       </view>
 
-      <!-- 文件上传 -->
-      <view class="form-item">
+      <!-- 舌诊拍照(仅 tongue 类型显示) -->
+      <view class="form-item" v-if="isTongue">
+        <text class="form-label">拍摄舌象</text>
+        <view class="photo-area">
+          <view class="photo-placeholder" v-if="!photoPath && !analyzing" @click="takePhoto">
+            <text class="photo-icon">📷</text>
+            <text class="photo-text">点击拍摄舌象</text>
+            <text class="photo-sub">自然光线下拍摄舌面,避免有色光线</text>
+          </view>
+          <image class="photo-preview" v-else-if="photoPath && !analyzing" :src="photoPath" mode="aspectFit" @click="takePhoto"></image>
+          <view class="photo-analyzing" v-else>
+            <text class="analyzing-text">AI 舌象分析中...</text>
+          </view>
+        </view>
+        <view class="photo-retake" v-if="photoPath && !analyzing">
+          <text @click="takePhoto">点击重新拍摄</text>
+        </view>
+      </view>
+
+      <!-- PDF文件上传(非舌诊类型显示) -->
+      <view class="form-item" v-if="!isTongue">
         <text class="form-label">上传PDF文件(选填)</text>
         <view class="file-upload-area" @click="pickFile">
           <view class="file-upload-placeholder" v-if="!selectedFile">
@@ -146,7 +173,9 @@
 
       <!-- 提交按钮 -->
       <view class="submit-btn" :class="{ 'btn-disabled': !canSubmit }" @click="doSubmit">
-        <text v-if="submitting">提交中...</text>
+        <text v-if="analyzing">分析中...</text>
+        <text v-else-if="isTongue">开始分析</text>
+        <text v-else-if="submitting">提交中...</text>
         <text v-else>提交报告</text>
       </view>
     </view>
@@ -173,16 +202,24 @@ export default {
       },
       showDatePicker: false,
       submitting: false,
+      analyzing: false,
       selectedFile: null,
       parsingPreview: false,
       parsedInfo: null,
       showMemberSelector: false,
       selectedMemberId: null,
-      familyMembers: []
+      familyMembers: [],
+      // 舌诊
+      photoPath: null,
+      tongueResult: null
     }
   },
   computed: {
+    isTongue() {
+      return this.form.reportType === 'tongue'
+    },
     canSubmit() {
+      if (this.isTongue) return this.photoPath && !this.analyzing
       if (this.selectedFile) {
         return this.parsedInfo && this.parsedInfo.draftId && !this.parsedInfo.needManualSelect
       }
@@ -191,6 +228,9 @@ export default {
   },
   onLoad: function(options) {
     this.childId = options.childId ? parseInt(options.childId) : null
+    if (options.reportType && ['physical_exam', 'gut_flora', 'tongue'].indexOf(options.reportType) !== -1) {
+      this.form.reportType = options.reportType
+    }
     // 默认设为今天
     var now = new Date()
     var y = now.getFullYear()
@@ -203,6 +243,19 @@ export default {
     this.loadFamilyInfo()
   },
   methods: {
+    selectType(type) {
+      if (this.form.reportType === type) return
+      this.form.reportType = type
+      // 切类型时清理其他类型的状态
+      if (type === 'tongue') {
+        this.selectedFile = null
+        this.parsedInfo = null
+      } else {
+        this.photoPath = null
+        this.tongueResult = null
+        this.analyzing = false
+      }
+    },
     loadFamilyInfo() {
       var self = this
       // 从Vuex获取familyId和家庭成员
@@ -225,6 +278,60 @@ export default {
         }
       }
     },
+    // ===== 舌诊拍照 =====
+    takePhoto() {
+      var self = this
+      uni.chooseImage({
+        count: 1,
+        sourceType: ['camera', 'album'],
+        success: function(res) {
+          self.photoPath = res.tempFilePaths[0]
+          self.tongueResult = null
+        }
+      })
+    },
+    analyzeTongue() {
+      if (!this.photoPath || this.analyzing) return
+      var self = this
+      this.analyzing = true
+      var memberId = this.childId || ''
+      uni.uploadFile({
+        url: getApp().globalData.baseUrl + '/api/health/report/parse-preview?type=tongue' + (memberId ? '&memberId=' + memberId : ''),
+        filePath: this.photoPath,
+        name: 'file',
+        header: {
+          'Authorization': 'Bearer ' + (uni.getStorageSync('token') || '')
+        },
+        success: function(res) {
+          var data = JSON.parse(res.data)
+          if (data.code === 200 && data.data) {
+            self.tongueResult = data.data
+            // 跳转到确认页
+            var payload = {
+              type: 'tongue',
+              indicators: (data.data.indicators || []).map(function(ind) {
+                return { code: ind.code, name: ind.name, value: ind.value }
+              }),
+              summary: { overallAssessment: data.data.overallAssessment || '' }
+            }
+            var params = 'draftId=' + data.data.recordId
+              + '&payload=' + encodeURIComponent(JSON.stringify(payload))
+              + '&reportType=tongue'
+              + (self.childId ? '&childId=' + self.childId : '')
+              + (self.familyId ? '&familyId=' + self.familyId : '')
+            uni.navigateTo({ url: '/pages/health/report-confirm?' + params })
+          } else {
+            uni.showToast({ title: data.message || '分析失败', icon: 'none' })
+          }
+        },
+        fail: function() {
+          uni.showToast({ title: '上传失败', icon: 'none' })
+        },
+        complete: function() {
+          self.analyzing = false
+        }
+      })
+    },
     onScoreChanging(e) {
       this.form.overallScore = e.detail.value
     },
@@ -327,6 +434,16 @@ export default {
       if (this.submitting) return
       var self = this
 
+      // 舌诊:拍照后分析
+      if (self.isTongue) {
+        if (!self.photoPath) {
+          uni.showToast({ title: '请先拍摄舌象', icon: 'none' })
+          return
+        }
+        self.analyzeTongue()
+        return
+      }
+
       if (self.selectedFile) {
         if (self.parsedInfo && self.parsedInfo.needManualSelect && !self.selectedMemberId) {
           uni.showToast({ title: '请先选择成员', icon: 'none' })
@@ -730,6 +847,63 @@ export default {
   color: #333;
 }
 
+/* ===== 舌诊拍照区 ===== */
+.photo-area {
+  display: flex;
+  justify-content: center;
+  margin-bottom: 16rpx;
+}
+.photo-placeholder {
+  width: 400rpx;
+  height: 400rpx;
+  border: 4rpx dashed #CBD5E1;
+  border-radius: 24rpx;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  background: #fff;
+}
+.photo-icon {
+  font-size: 80rpx;
+  margin-bottom: 16rpx;
+}
+.photo-text {
+  font-size: 28rpx;
+  color: #64748B;
+}
+.photo-sub {
+  font-size: 22rpx;
+  color: #94A3B8;
+  margin-top: 12rpx;
+  text-align: center;
+  padding: 0 20rpx;
+}
+.photo-preview {
+  width: 400rpx;
+  height: 400rpx;
+  border-radius: 24rpx;
+}
+.photo-analyzing {
+  width: 400rpx;
+  height: 400rpx;
+  border-radius: 24rpx;
+  background: #f5f5f5;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+.analyzing-text {
+  font-size: 28rpx;
+  color: #5B9BD5;
+}
+.photo-retake {
+  text-align: center;
+  font-size: 24rpx;
+  color: #5B9BD5;
+  margin-top: 8rpx;
+}
+
 /* ===== 底部 ===== */
 .bottom-spacer {
   height: 60rpx;

+ 3 - 277
cfc-frontend/pages/health/tongue-index.vue

@@ -1,286 +1,12 @@
 <template>
-  <view class="tongue-container">
-    <!-- 步骤条 -->
-    <view class="step-bar">
-      <view class="step-item" :class="{ active: step >= 1, done: step > 1 }">
-        <text class="step-num">{{ step > 1 ? '✓' : '1' }}</text>
-        <text class="step-label">选择成员</text>
-      </view>
-      <view class="step-line" :class="{ active: step > 1 }"></view>
-      <view class="step-item" :class="{ active: step >= 2, done: step > 2 }">
-        <text class="step-num">{{ step > 2 ? '✓' : '2' }}</text>
-        <text class="step-label">舌象拍照</text>
-      </view>
-      <view class="step-line" :class="{ active: step > 2 }"></view>
-      <view class="step-item" :class="{ active: step >= 3 }">
-        <text class="step-num">3</text>
-        <text class="step-label">预览确认</text>
-      </view>
-    </view>
-
-    <!-- Step 1: 选成员(可选,默认我自己) -->
-    <view class="step-content" v-if="step === 1">
-      <view class="section-title">选择家庭成员(可选)</view>
-      <view class="member-item self-first" @click="selectSelf">
-        <text class="member-avatar self-avatar">我</text>
-        <text class="member-name">我自己</text>
-      </view>
-      <view class="member-list">
-        <view class="member-item"
-          v-for="m in familyMembers"
-          :key="m.id"
-          :class="{ selected: selectedMember && selectedMember.id === m.id }"
-          @click="selectMember(m)">
-          <text class="member-avatar">{{ m.name && m.name.slice(0, 1) }}</text>
-          <text class="member-name">{{ m.name }}</text>
-        </view>
-      </view>
-      <button class="btn-primary" @click="step = 2" :disabled="!canProceed">下一步</button>
-    </view>
-
-    <!-- Step 2: 拍照 -->
-    <view class="step-content" v-if="step === 2">
-      <view class="section-title">拍摄舌象</view>
-      <view class="photo-area">
-        <view class="photo-placeholder" v-if="!photoPath" @click="takePhoto">
-          <text class="photo-icon">📷</text>
-          <text class="photo-text">点击拍摄舌象</text>
-          <text class="photo-sub">提示:自然光下拍摄,避免有色光线</text>
-        </view>
-        <image class="photo-preview" v-else :src="photoPath" mode="aspectFit" @click="takePhoto"></image>
-      </view>
-      <view class="btn-row">
-        <button class="btn-secondary" @click="step = 1">上一步</button>
-        <button class="btn-primary" @click="uploadAndAnalyze" :disabled="!photoPath || uploading">
-          {{ uploading ? '分析中...' : '开始分析' }}
-        </button>
-      </view>
-    </view>
-
-    <!-- Step 3: 预览确认 -->
-    <view class="step-content" v-if="step === 3">
-      <view class="result-header">
-        <image class="result-img" :src="tongueResult.imageUrl" mode="aspectFit"></image>
-        <text class="result-assessment">{{ tongueResult.overallAssessment }}</text>
-      </view>
-
-      <view class="indicator-list">
-        <view class="indicator-item" v-for="(ind, idx) in indicators" :key="idx">
-          <text class="indicator-label">{{ ind.name }}</text>
-          <picker mode="selector" :range="ind.options" :value="ind.selectedIndex" @change="onIndicatorChange($event, idx)">
-            <view class="indicator-value">
-              <text>{{ ind.value }}</text>
-              <text class="indicator-arrow">▼</text>
-            </view>
-          </picker>
-        </view>
-      </view>
-
-      <view class="btn-row">
-        <button class="btn-secondary" @click="discardAndBack">放弃</button>
-        <button class="btn-primary" @click="confirmSave" :disabled="saving">
-          {{ saving ? '保存中...' : '确认入库' }}
-        </button>
-      </view>
-    </view>
-  </view>
+  <view></view>
 </template>
 
 <script>
+// 废弃 — 舌诊功能已迁移到 report-upload?reportType=tongue
 export default {
-  data: function() {
-    return {
-      step: 1,
-      familyMembers: [],
-      selectedMember: null,
-      selectedSelf: true,
-      photoPath: null,
-      uploading: false,
-      saving: false,
-      tongueResult: {
-        recordId: null,
-        imageUrl: '',
-        overallAssessment: ''
-      },
-      indicators: []
-    }
-  },
-  computed: {
-    canProceed: function() {
-      return this.selectedMember || this.selectedSelf
-    }
-  },
   onLoad: function() {
-    this.loadFamilyMembers()
-  },
-  methods: {
-    selectSelf: function() {
-      this.selectedSelf = true
-      this.selectedMember = null
-    },
-    loadFamilyMembers: function() {
-      var members = uni.getStorageSync('familyChildren') || []
-      if (members.length > 0) {
-        this.familyMembers = members
-      }
-    },
-    selectMember: function(m) {
-      this.selectedMember = m
-    },
-    takePhoto: function() {
-      var self = this
-      uni.chooseImage({
-        count: 1,
-        sourceType: ['camera', 'album'],
-        success: function(res) {
-          self.photoPath = res.tempFilePaths[0]
-        }
-      })
-    },
-    uploadAndAnalyze: function() {
-      if (!this.photoPath) return
-      this.uploading = true
-
-      var self = this
-      var memberId = this.selectedMember ? this.selectedMember.id : ''
-      uni.uploadFile({
-        url: getApp().globalData.baseUrl + '/api/health/report/parse-preview?type=tongue' + (memberId ? '&memberId=' + memberId : ''),
-        filePath: this.photoPath,
-        name: 'file',
-        success: function(res) {
-          var data = JSON.parse(res.data)
-          if (data.code === 0 && data.data) {
-            self.tongueResult.recordId = data.data.recordId
-            self.tongueResult.imageUrl = data.data.imageUrl
-            self.tongueResult.overallAssessment = data.data.overallAssessment
-            var inds = (data.data.indicators || []).map(function(ind) {
-              var opts = []
-              try { opts = JSON.parse(ind.options) } catch(e) {}
-              return {
-                code: ind.code,
-                name: ind.name,
-                value: ind.value,
-                options: opts,
-                selectedIndex: opts.indexOf(ind.value)
-              }
-            })
-            self.indicators = inds
-            self.step = 3
-          } else {
-            uni.showToast({ title: data.message || '分析失败', icon: 'none' })
-          }
-        },
-        fail: function() {
-          uni.showToast({ title: '上传失败', icon: 'none' })
-        },
-        complete: function() {
-          self.uploading = false
-        }
-      })
-    },
-    onIndicatorChange: function(e, idx) {
-      var i = parseInt(e.detail.value)
-      this.indicators[idx].selectedIndex = i
-      this.indicators[idx].value = this.indicators[idx].options[i]
-    },
-    confirmSave: function() {
-      this.saving = true
-      var self = this
-      var modifiedIndicators = this.indicators.map(function(ind) {
-        return { code: ind.code, value: ind.value }
-      })
-      var memberId = this.selectedMember ? this.selectedMember.id : ''
-      uni.request({
-        url: getApp().globalData.baseUrl + '/api/health/report/confirm?type=tongue',
-        method: 'POST',
-        data: {
-          recordId: this.tongueResult.recordId,
-          memberId: memberId || undefined,
-          indicators: modifiedIndicators
-        },
-        success: function(res) {
-          if (res.data.code === 0) {
-            uni.showToast({ title: '舌诊结果已保存' })
-            setTimeout(function() {
-              uni.navigateBack()
-            }, 1500)
-          } else {
-            uni.showToast({ title: res.data.message || '保存失败', icon: 'none' })
-          }
-        },
-        fail: function() {
-          uni.showToast({ title: '网络错误', icon: 'none' })
-        },
-        complete: function() {
-          self.saving = false
-        }
-      })
-    },
-    discardAndBack: function() {
-      var self = this
-      uni.showModal({
-        title: '提示',
-        content: '确定放弃本次舌诊分析吗?',
-        success: function(res) {
-          if (res.confirm) {
-      var memberId = this.selectedMember ? this.selectedMember.id : ''
-      uni.request({
-        url: getApp().globalData.baseUrl + '/api/health/report/discard?type=tongue',
-        method: 'POST',
-        data: { recordId: self.tongueResult.recordId, memberId: memberId || undefined }
-            })
-            uni.navigateBack()
-          }
-        }
-      })
-    }
+    uni.redirectTo({ url: '/pages/health/report-upload?reportType=tongue' })
   }
 }
 </script>
-
-<style scoped>
-.tongue-container { padding: 30rpx; background: #F5FAFE; min-height: 100vh; }
-.step-bar { display: flex; align-items: center; margin-bottom: 40rpx; }
-.step-item { display: flex; flex-direction: column; align-items: center; }
-.step-num { width: 48rpx; height: 48rpx; border-radius: 50%; background: #E2E8F0; color: #94A3B8; text-align: center; line-height: 48rpx; font-size: 24rpx; font-weight: bold; }
-.step-item.active .step-num { background: #F97316; color: #fff; }
-.step-item.done .step-num { background: #10B981; color: #fff; }
-.step-label { font-size: 22rpx; color: #94A3B8; margin-top: 8rpx; }
-.step-item.active .step-label { color: #F97316; font-weight: bold; }
-.step-line { flex: 1; height: 2rpx; background: #E2E8F0; margin: 0 16rpx; margin-bottom: 40rpx; }
-.step-line.active { background: #10B981; }
-
-.section-title { font-size: 32rpx; font-weight: bold; color: #1E293B; margin-bottom: 24rpx; }
-.self-first { width: 140rpx; height: 160rpx; display: flex; flex-direction: column; align-items: center; justify-content: center; background: #FFF7ED; border-radius: 16rpx; border: 2rpx solid #F97316; margin-bottom: 20rpx; }
-.self-avatar { width: 64rpx; height: 64rpx; border-radius: 50%; background: #FED7AA; text-align: center; line-height: 64rpx; font-size: 28rpx; color: #9A3412; margin-bottom: 8rpx; }
-.member-list { display: flex; flex-wrap: wrap; gap: 20rpx; margin-bottom: 40rpx; }
-.member-item { width: 140rpx; height: 160rpx; display: flex; flex-direction: column; align-items: center; justify-content: center; background: #fff; border-radius: 16rpx; border: 2rpx solid #E2E8F0; }
-.member-item.selected { border-color: #F97316; background: #FFF7ED; }
-.member-avatar { width: 64rpx; height: 64rpx; border-radius: 50%; background: #FED7AA; text-align: center; line-height: 64rpx; font-size: 28rpx; color: #9A3412; margin-bottom: 8rpx; }
-.member-name { font-size: 24rpx; color: #1E293B; }
-
-.photo-area { display: flex; justify-content: center; margin-bottom: 40rpx; }
-.photo-placeholder { width: 400rpx; height: 400rpx; border: 4rpx dashed #CBD5E1; border-radius: 24rpx; display: flex; flex-direction: column; align-items: center; justify-content: center; background: #fff; }
-.photo-icon { font-size: 80rpx; margin-bottom: 16rpx; }
-.photo-text { font-size: 28rpx; color: #64748B; }
-.photo-sub { font-size: 22rpx; color: #94A3B8; margin-top: 12rpx; }
-.photo-preview { width: 400rpx; height: 400rpx; border-radius: 24rpx; }
-
-.result-header { display: flex; flex-direction: column; align-items: center; margin-bottom: 30rpx; }
-.result-img { width: 300rpx; height: 300rpx; border-radius: 16rpx; margin-bottom: 16rpx; }
-.result-assessment { font-size: 26rpx; color: #475569; text-align: center; line-height: 1.6; }
-
-.indicator-list { margin-bottom: 40rpx; }
-.indicator-item { display: flex; justify-content: space-between; align-items: center; padding: 24rpx 20rpx; background: #fff; border-radius: 12rpx; margin-bottom: 12rpx; }
-.indicator-label { font-size: 28rpx; color: #1E293B; }
-.indicator-value { display: flex; align-items: center; color: #F97316; font-size: 28rpx; }
-.indicator-arrow { font-size: 20rpx; margin-left: 8rpx; }
-
-.btn-row { display: flex; gap: 20rpx; }
-.btn-primary { flex: 1; background: #F97316; color: #fff; border-radius: 40rpx; height: 88rpx; line-height: 88rpx; text-align: center; font-size: 30rpx; }
-.btn-primary:disabled { opacity: 0.5; }
-.btn-secondary { flex: 1; background: #fff; color: #64748B; border-radius: 40rpx; height: 88rpx; line-height: 88rpx; text-align: center; font-size: 30rpx; border: 2rpx solid #E2E8F0; }
-
-.loading-wrap { display: flex; justify-content: center; align-items: center; min-height: 200rpx; }
-.loading-text { color: #94A3B8; font-size: 28rpx; }
-</style>

+ 2 - 2
cfc-frontend/utils/api.js

@@ -1427,11 +1427,11 @@ export const getReportList = (childId) => request('/api/health/report/list', 'PO
 export const getReportDetail = (reportId) => request('/api/health/report/detail', 'POST', { reportId })
 
 // 闃舵1锛氫笂浼燩DF鈫掕В鏋愨啋瀛榙raft鈫掕繑鍥瀌raftId+payload+鎴愬憳鍖归厤锛堜笉钀藉簱锛?
-export const parseReportPreview = (filePath, childId, familyId) => {
+export const parseReportPreview = (filePath, childId, familyId, type) => {
   return new Promise((resolve, reject) => {
     const token = uni.getStorageSync('token')
     uni.uploadFile({
-      url: BASE_URL + '/api/health/report/parse-preview',
+      url: BASE_URL + '/api/health/report/parse-preview' + (type ? '?type=' + type : ''),
       filePath: filePath,
       name: 'file',
       formData: {