Forráskód Böngészése

feat(health-report): 上传后自动跳转确认页按 draftId 解析,移除本页预览/成员选择逻辑

asus 1 hónapja
szülő
commit
2cb7cd5ee9

+ 84 - 1
cfc-frontend/pages/health/report-confirm.vue

@@ -11,6 +11,12 @@
       <text class="tip-text">请核对以下解析内容,可修改后确认入库</text>
     </view>
 
+    <!-- 解析中状态(仅 draftId 模式,从上传页跳转后自动解析) -->
+    <view class="parsing-banner" v-if="parsing">
+      <text class="parsing-icon">⏳</text>
+      <text class="parsing-text">正在解析报告,请稍候...</text>
+    </view>
+
     <!-- 匹配可信度 -->
     <view class="confidence-card" v-if="confidenceLevelClass">
       <view class="confidence-header">
@@ -345,7 +351,7 @@
 </template>
 
 <script>
-import { confirmReportPreview, discardReportDraft, confirmTongue, discardTongue } from '../../utils/api.js'
+import { confirmReportPreview, discardReportDraft, confirmTongue, discardTongue, parseReportDraft } from '../../utils/api.js'
 import { addFamilyMember, getFamilyMembers } from '../../utils/api.js'
 
 export default {
@@ -377,6 +383,8 @@ export default {
       selectedMemberId: null,
       familyMembers: [],
       confirming: false,
+      // 仅 draftId 模式(从上传页跳转)下正在解析
+      parsing: false,
       // Phase 2: 匹配可信度与候选
       matchConfidence: null,
       matchLabel: '',
@@ -403,6 +411,7 @@ export default {
   },
   computed: {
     canConfirm() {
+      if (this.parsing) return false
       if (this.needBind && !this.selectedMemberId && !this.createdMemberId) return false
       return this.draftId && !this.confirming
     },
@@ -483,6 +492,12 @@ export default {
     this.memberId = options.memberId ? parseInt(options.memberId) : null
     this.familyId = options.familyId ? parseInt(options.familyId) : null
 
+    // 仅 draftId 模式(从上传页自动跳转):无 payload 参数 → 调用后端按草稿解析并自动填充
+    if (this.draftId && !options.payload) {
+      this.parseByDraftId()
+      return
+    }
+
     var payloadStr = decodeURIComponent(options.payload || '{}')
     try {
       var parsed = typeof payloadStr === 'string' ? JSON.parse(payloadStr) : payloadStr
@@ -545,6 +560,62 @@ export default {
     }
   },
   methods: {
+    // 仅 draftId 模式:调用后端按草稿解析,成功后自动填充页面
+    parseByDraftId() {
+      var self = this
+      self.parsing = true
+      parseReportDraft(self.draftId).then(function(res) {
+        var detail = res && res.data
+        if (!detail) {
+          self.parsing = false
+          uni.showToast({ title: (res && res.message) || '解析失败,请重试', icon: 'none' })
+          return
+        }
+        self.applyParseResult(detail)
+      }).catch(function(err) {
+        self.parsing = false
+        var msg = (err && err.message) || '解析失败,请重试'
+        uni.showToast({ title: msg, icon: 'none' })
+      })
+    },
+    // 将后端解析结果应用到页面
+    applyParseResult(detail) {
+      this.parsing = false
+      if (detail.payload) {
+        this.payload = Object.assign({
+          summary: {},
+          indicators: [],
+          gutFlora: [],
+          probioticSpecies: [],
+          taxonomyClass: [],
+          taxonomyOrder: [],
+          taxonomyFamily: [],
+          taxonomyGenus: [],
+          taxonomySpecies: [],
+          pathogenGenus: [],
+          pathogenDetection: [],
+          foods: [],
+          diseaseRisks: []
+        }, detail.payload)
+      }
+      this.extractedName = detail.extractedName || ''
+      this.extractedGender = detail.extractedGender || ''
+      this.extractedAge = detail.extractedAge || ''
+      this.matchedMemberId = detail.matchedMemberId ? parseInt(detail.matchedMemberId) : null
+      this.needBind = !!detail.needBind
+      this.matchConfidence = detail.confidence !== undefined && detail.confidence !== null ? parseFloat(detail.confidence) : null
+      this.matchLabel = detail.matchLabel || ''
+      this.candidates = detail.candidates || []
+      this.showCandidatesOnly = this.candidates.length > 0
+
+      // 始终加载家庭成员
+      this.loadFamilyMembers()
+
+      // 高置信度且已匹配 → 直接选中
+      if (this.needBind && this.matchedMemberId && this.matchConfidence >= 0.8) {
+        this.selectedMemberId = this.matchedMemberId
+      }
+    },
     loadFamilyMembers() {
       var self = this
       // 从后端获取真实家庭成员(家长 + 孩子),storage 中并无该数据
@@ -841,6 +912,18 @@ export default {
 .tip-icon { font-size: 32rpx; margin-right: 16rpx; flex-shrink: 0; }
 .tip-text { font-size: 24rpx; color: #2E7D32; line-height: 1.5; }
 
+.parsing-banner {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  background: #FFF8E1;
+  margin: 0 30rpx 20rpx;
+  padding: 20rpx 24rpx;
+  border-radius: 16rpx;
+}
+.parsing-icon { font-size: 32rpx; margin-right: 16rpx; flex-shrink: 0; }
+.parsing-text { font-size: 24rpx; color: #F57F17; line-height: 1.5; }
+
 .form-section { margin: 0 30rpx; }
 
 .card {

+ 31 - 685
cfc-frontend/pages/health/report-upload.vue

@@ -13,52 +13,6 @@
       <text class="tip-text">上传体检报告或肠道检测报告,以便为您解读并提供个性化营养方案</text>
     </view>
 
-    <!-- PDF解析预览(提取到信息时显示) -->
-    <view class="parsed-preview" v-if="parsedInfo">
-      <view class="parsed-title">PDF解析结果</view>
-      <view class="parsed-row" v-if="parsedInfo.personName">
-        <text class="parsed-label">被检测人</text>
-        <text class="parsed-value">{{ parsedInfo.personName }}</text>
-      </view>
-      <view class="parsed-row" v-if="parsedInfo.reportNumber">
-        <text class="parsed-label">报告编号</text>
-        <text class="parsed-value">{{ parsedInfo.reportNumber }}</text>
-      </view>
-      <view class="parsed-row" v-if="parsedInfo.reportDate">
-        <text class="parsed-label">报告日期</text>
-        <text class="parsed-value">{{ parsedInfo.reportDate }}</text>
-      </view>
-      <!-- 匹配状态 -->
-      <view class="match-status" v-if="parsedInfo.needManualSelect">
-        <text class="match-icon">⚠️</text>
-        <text class="match-text">无法自动匹配到家庭成员,请手动选择</text>
-      </view>
-      <view class="match-status success" v-else-if="parsedInfo.matchedMemberName">
-        <text class="match-icon">✅</text>
-        <text class="match-text">已自动匹配: {{ parsedInfo.matchedMemberName }}</text>
-      </view>
-    </view>
-
-    <!-- 家庭成员选择器(需要手动选择时显示) -->
-    <view class="form-item member-selector" v-if="showMemberSelector">
-      <text class="form-label">选择成员</text>
-      <view class="member-list">
-        <view
-          class="member-item"
-          v-for="member in familyMembers"
-          :key="member.id"
-          :class="{ 'member-selected': selectedMemberId === (member.userId || member.id) }"
-          @click="selectMember(member)"
-        >
-          <text class="member-avatar">{{ member.nickname ? member.nickname.substring(0, 1) : '?' }}</text>
-          <text class="member-name">{{ member.nickname || '未知' }}</text>
-        </view>
-      </view>
-      <view class="add-member-btn" v-if="parsedInfo && parsedInfo.personName" @click="chooseRelationAndAdd">
-        <text class="add-member-text">+ 将「{{ parsedInfo.personName }}」添加为新成员</text>
-      </view>
-    </view>
-
     <!-- 表单 -->
     <view class="form-section">
       <!-- 上传方式:并排卡片 -->
@@ -75,41 +29,13 @@
         </view>
       </view>
 
-      <!-- 已选文件 -->
-      <view class="form-item" v-if="selectedFile || photoPath">
-        <text class="form-label">已选文件</text>
-        <view class="file-info">
-          <text class="file-name">{{ selectedFile ? (selectedFile.name || '文件') : '照片' }}</text>
-          <text class="file-size" v-if="selectedFile && selectedFile.size">{{ formatFileSize(selectedFile.size) }}</text>
-          <text class="file-remove" @click.stop="removeFile">移除</text>
-        </view>
-      </view>
-
-      <!-- 上传中/分析中 -->
-      <view class="form-item" v-if="parsingPreview">
-        <text class="form-label">AI分析中...</text>
+      <!-- 上传中 -->
+      <view class="form-item" v-if="uploading">
+        <text class="form-label">上传中...</text>
         <view class="analyzing-status">
-          <text class="analyzing-text">正在解析文件内容,请稍候...</text>
+          <text class="analyzing-text">正在上传文件,即将进入分析页面...</text>
         </view>
       </view>
-
-      <!-- 备注 -->
-      <view class="form-item">
-        <text class="form-label">备注(选填)</text>
-        <textarea
-          class="note-input"
-          v-model="form.description"
-          placeholder="请输入报告摘要或备注信息..."
-          maxlength="500"
-        />
-      </view>
-
-      <!-- 提交按钮 -->
-      <view class="submit-btn" :class="{ 'btn-disabled': !canSubmit }" @click="doSubmit">
-        <text v-if="parsingPreview">解析中...</text>
-        <text v-else-if="submitting">提交中...</text>
-        <text v-else>提交报告</text>
-      </view>
     </view>
 
     <!-- 底部占位 -->
@@ -141,7 +67,7 @@
 </template>
 
 <script>
-import { getFamilyMembers, addFamilyMember, getProductsByDomain } from '../../utils/api.js'
+import { getProductsByDomain, uploadReportOnly } from '../../utils/api.js'
 import config from '@/config.js'
 
 export default {
@@ -149,61 +75,30 @@ export default {
     return {
       memberId: null,
       familyId: null,
-      form: {
-        reportDate: '',
-        overallScore: 0,
-        description: ''
-      },
-      submitting: false,
+      uploading: false,
       selectedFile: null,
-      parsingPreview: false,
-      parsedInfo: null,
-      showMemberSelector: false,
-      selectedMemberId: null,
-      familyMembers: [],
-      // 拍照
       photoPath: null,
       // 购买推荐
       recommendedProducts: [],
       loadingProducts: false
     }
   },
-  computed: {
-    canSubmit() {
-      if (this.parsingPreview) return false
-      if (this.selectedFile || this.photoPath) {
-        // 文件已选且解析成功(有draftId且不需要手动选成员)
-        return this.parsedInfo && this.parsedInfo.draftId && !this.parsedInfo.needManualSelect
-      }
-      return false
-    }
-  },
   onLoad: function(options) {
     this.memberId = options.memberId ? parseInt(options.memberId) : null
     this.loadRecommendProducts()
   },
   onShow: function() {
-    // 每次显示时从store获取familyId和家庭成员
+    // 每次显示时从store获取familyId
     this.loadFamilyInfo()
   },
   methods: {
     loadFamilyInfo() {
-      var self = this
-      // 从Vuex获取familyId和家庭成员
+      // 从本地存储获取familyId
       var userInfo = uni.getStorageSync('userInfo')
       if (userInfo) {
         try {
           var info = typeof userInfo === 'string' ? JSON.parse(userInfo) : userInfo
           this.familyId = info.familyId || null
-          // 加载家庭成员列表(从本地存储或请求)
-          var storedMembers = uni.getStorageSync('familyMembers')
-          if (storedMembers) {
-            try {
-              this.familyMembers = typeof storedMembers === 'string' ? JSON.parse(storedMembers) : storedMembers
-            } catch (e) {
-              this.familyMembers = []
-            }
-          }
         } catch (e) {
           console.log('解析userInfo失败', e)
         }
@@ -217,25 +112,14 @@ export default {
         sourceType: ['camera', 'album'],
         success: function(res) {
           self.photoPath = res.tempFilePaths[0]
-          // 拍照后直接解析
-          if (self.familyId) {
-            self.previewParse()
-          }
+          // 拍照后直接上传并进入确认页(familyId 为可选)
+          self.uploadAndGoConfirm(self.photoPath, 'image')
         }
       })
     },
     // ===== 文件上传 =====
     pickFile() {
       var self = this
-      // 重置解析状态
-      this.parsedInfo = null
-      this.showMemberSelector = false
-      this.selectedMemberId = null
-      this.form.reportDate = ''
-      this.form.overallScore = 0
-      this.photoPath = null
-
-      // 从微信聊天记录中选择文件
       uni.chooseMessageFile({
         count: 1,
         type: 'file',
@@ -244,9 +128,8 @@ export default {
           var files = res.tempFiles
           if (files && files.length > 0) {
             self.selectedFile = files[0]
-            if (self.familyId) {
-              self.previewParse()
-            }
+            // 直接上传并进入确认页(familyId 为可选,后端会自行处理成员匹配)
+            self.uploadAndGoConfirm(files[0].path, '')
           }
         },
         fail: function(e) {
@@ -254,210 +137,29 @@ export default {
         }
       })
     },
-    previewParse() {
+    // 上传文件 → 自动跳转确认页(确认页按 draftId 触发解析并自动刷新显示结果)
+    uploadAndGoConfirm(filePath, type) {
       var self = this
-      if (!this.familyId) return
-      var filePath = this.selectedFile ? this.selectedFile.path : this.photoPath
       if (!filePath) return
-      this.parsingPreview = true
-
-      var apiPath = config.API_BASE_URL + '/api/health/report/parse-preview'
-      if (self.selectedFile) {
-        // PDF文件上传
-        uni.uploadFile({
-          url: apiPath + (self.memberId ? '?memberId=' + self.memberId : ''),
-          filePath: filePath,
-          name: 'file',
-          header: {
-            'Authorization': 'Bearer ' + (uni.getStorageSync('token') || '')
-          },
-          success: function(uploadRes) {
-            var data = JSON.parse(uploadRes.data)
-            if (data.code === 200 && data.data) {
-              self.handleParsedResult(data.data)
-            } else {
-              self.parsingPreview = false
-              uni.showToast({ title: data.message || '解析失败', icon: 'none' })
-            }
-          },
-          fail: function(err) {
-            self.parsingPreview = false
-            console.error('PDF上传失败', err)
-            uni.showToast({ title: '上传失败', icon: 'none' })
-          }
-        })
-      } else if (self.photoPath) {
-        // 图片上传
-        uni.uploadFile({
-          url: apiPath + '?type=image' + (self.memberId ? '&memberId=' + self.memberId : ''),
-          filePath: filePath,
-          name: 'file',
-          header: {
-            'Authorization': 'Bearer ' + (uni.getStorageSync('token') || '')
-          },
-          success: function(uploadRes) {
-            var data = JSON.parse(uploadRes.data)
-            if (data.code === 200 && data.data) {
-              self.handleParsedResult(data.data)
-            } else {
-              self.parsingPreview = false
-              uni.showToast({ title: data.message || '解析失败', icon: 'none' })
-            }
-          },
-          fail: function(err) {
-            self.parsingPreview = false
-            console.error('图片上传失败', err)
-            uni.showToast({ title: '上传失败', icon: 'none' })
-          }
-        })
-      }
-    },
-    handleParsedResult: function(detail) {
-      this.parsingPreview = false
-      this.parsedInfo = {
-        draftId: detail.draftId,
-        personName: detail.extractedName || '',
-        extractedGender: detail.extractedGender || '',
-        extractedAge: detail.extractedAge || '',
-        reportNumber: detail.extractedReportNumber,
-        reportDate: detail.extractedReportDate,
-        needManualSelect: detail.needBind,
-        payload: detail.payload || {},
-        matchedMemberId: detail.matchedMemberId,
-        confidence: detail.confidence,
-        matchLabel: detail.matchLabel,
-        candidates: detail.candidates || []
-      }
-      if (detail.extractedReportDate) {
-        this.form.reportDate = detail.extractedReportDate
-      }
-      if (detail.needBind) {
-        this.showMemberSelector = true
-        this.loadFamilyMembers()
-        if (this.parsedInfo.personName) {
-          this.promptAddParsedMember()
-        }
-      }
-    },
-    loadFamilyMembers() {
-      var self = this
-      // 从后端获取真实家庭成员(家长 + 孩子),storage 中并无该数据
-      getFamilyMembers().then(function(res) {
-        var data = (res && res.data) || {}
-        var parents = (data.parents || []).map(function(p) {
-          return { id: p.id, nickname: p.realName || p.nickname, gender: p.gender, role: 'parent' }
-        })
-        var children = (data.children || []).map(function(c) {
-          return { id: c.id, nickname: c.nickname, gender: c.gender, age: c.age, role: 'child' }
-        })
-        self.familyMembers = parents.concat(children)
-      }).catch(function(e) {
-        console.log('加载家庭成员失败', e)
-        self.familyMembers = []
-      })
-    },
-    selectMember(member) {
-      this.selectedMemberId = member.id || member.userId
-      this.parsedInfo = this.parsedInfo || {}
-      // 手动选择的成员通过 matchedMemberId 传递给确认页
-      this.parsedInfo.matchedMemberId = this.selectedMemberId
-      this.parsedInfo.needManualSelect = false
-      this.showMemberSelector = false
-    },
-    genderText(gender) {
-      if (gender === 'male') return '男'
-      if (gender === 'female') return '女'
-      return ''
-    },
-    // 询问是否将解析到的人添加为家庭成员
-    promptAddParsedMember() {
-      var self = this
-      if (!this.parsedInfo || !this.parsedInfo.personName) return
-      var name = this.parsedInfo.personName
-      var desc = name
-      var extra = []
-      var g = this.genderText(this.parsedInfo.extractedGender)
-      if (g) extra.push(g)
-      if (this.parsedInfo.extractedAge) extra.push(this.parsedInfo.extractedAge + '岁')
-      if (extra.length > 0) desc = name + '(' + extra.join(',') + ')'
-      uni.showModal({
-        title: '未匹配到家庭成员',
-        content: '是否将解析到的「' + desc + '」添加为家庭成员?',
-        confirmText: '添加',
-        cancelText: '手动选择',
-        success: function(res) {
-          if (res.confirm) {
-            self.chooseRelationAndAdd()
-          }
-          // 取消则保留下方的手动选择列表
-        }
-      })
-    },
-    // 选择新成员与家庭的关系(后端要求辈分必填)后添加
-    chooseRelationAndAdd() {
-      var self = this
-      var relOptions = [
-        { label: '孩子', generationLevel: 'child' },
-        { label: '配偶', generationLevel: 'peer', peerType: 'spouse' },
-        { label: '父母', generationLevel: 'parent' },
-        { label: '兄弟姐妹', generationLevel: 'peer', peerType: 'sibling' },
-        { label: '祖父母', generationLevel: 'grandparent' }
-      ]
-      uni.showActionSheet({
-        itemList: relOptions.map(function(o) { return o.label }),
-        success: function(res) {
-          var rel = relOptions[res.tapIndex]
-          if (rel) {
-            self.doAddParsedMember(rel)
-          }
-        }
-      })
-    },
-    // 调用后端添加成员,并将其作为本次报告对象
-    doAddParsedMember(rel) {
-      var self = this
-      var name = this.parsedInfo.personName
-      var dto = {
-        nickname: name,
-        gender: this.parsedInfo.extractedGender || '',
-        generationLevel: rel.generationLevel
-      }
-      if (rel.peerType) dto.peerType = rel.peerType
-      // 由年龄推算大致出生年份(用于展示,用户后续可修改)
-      var age = parseInt(this.parsedInfo.extractedAge)
-      if (!isNaN(age) && age > 0 && age < 130) {
-        dto.birthday = (new Date().getFullYear() - age) + '-01-01'
-      }
-      uni.showLoading({ title: '添加成员...' })
-      addFamilyMember(dto).then(function(res) {
+      self.uploading = true
+      uni.showLoading({ title: '上传中...' })
+      uploadReportOnly(filePath, type).then(function(res) {
         uni.hideLoading()
-        if (res && res.code === 200) {
-          var newId = res.data && (res.data.id || res.data.userId)
-          self.selectedMemberId = newId
-          self.parsedInfo.matchedMemberId = newId
-          self.parsedInfo.matchedMemberName = name
-          self.parsedInfo.needManualSelect = false
-          self.showMemberSelector = false
-          self.loadFamilyMembers()
-          uni.showToast({ title: '已添加并选中', icon: 'success' })
-        } else {
-          uni.showToast({ title: (res && res.message) || '添加失败', icon: 'none' })
+        self.uploading = false
+        var draftId = res.data && res.data.draftId
+        if (!draftId) {
+          uni.showToast({ title: (res && res.message) || '上传失败', icon: 'none' })
+          return
         }
-      }).catch(function(e) {
+        // 只传 draftId,确认页自行解析
+        uni.redirectTo({ url: '/pages/health/report-confirm?draftId=' + draftId })
+      }).catch(function(err) {
         uni.hideLoading()
-        console.log('添加成员失败', e)
-        uni.showToast({ title: '添加失败,请重试', icon: 'none' })
+        self.uploading = false
+        var msg = (err && err.message) || '上传失败,请重试'
+        uni.showToast({ title: msg, icon: 'none' })
       })
     },
-    removeFile() {
-      this.selectedFile = null
-      this.photoPath = null
-      this.parsedInfo = null
-      this.showMemberSelector = false
-      this.selectedMemberId = null
-      this.form.reportDate = ''
-      this.form.overallScore = 0
-    },
     loadRecommendProducts: function() {
       var self = this
       self.loadingProducts = true
@@ -495,54 +197,9 @@ export default {
       if (cents === null || cents === undefined) return '¥0'
       return '¥' + (cents / 100).toFixed(2)
     },
-    formatFileSize(bytes) {
-      if (!bytes) return ''
-      if (bytes < 1024) return bytes + 'B'
-      if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + 'KB'
-      return (bytes / 1024 / 1024).toFixed(1) + 'MB'
-    },
     goBack() {
       uni.navigateBack()
     },
-    doSubmit() {
-      if (this.submitting) return
-      var self = this
-
-      if (!self.parsedInfo || !self.parsedInfo.draftId) {
-        uni.showToast({ title: '请先上传文件并等待解析完成', icon: 'none' })
-        return
-      }
-      if (self.parsedInfo.needManualSelect && !self.selectedMemberId) {
-        uni.showToast({ title: '请先选择成员', icon: 'none' })
-        return
-      }
-
-      var payloadStr = encodeURIComponent(JSON.stringify(self.parsedInfo.payload || {}))
-      var candidatesStr = (self.parsedInfo.candidates && self.parsedInfo.candidates.length > 0)
-        ? encodeURIComponent(JSON.stringify(self.parsedInfo.candidates))
-        : ''
-      var params = 'draftId=' + self.parsedInfo.draftId
-        + '&extractedName=' + encodeURIComponent(self.parsedInfo.personName || '')
-        + '&extractedGender=' + encodeURIComponent(self.parsedInfo.extractedGender || '')
-        + '&extractedAge=' + (self.parsedInfo.extractedAge || '')
-        + '&payload=' + payloadStr
-        + '&needBind=' + (self.parsedInfo.needManualSelect ? 'true' : 'false')
-        + '&matchConfidence=' + (self.parsedInfo.confidence || '')
-        + '&matchLabel=' + encodeURIComponent(self.parsedInfo.matchLabel || '')
-        + '&candidates=' + candidatesStr
-        + '&extractedReportDate=' + encodeURIComponent(self.parsedInfo.reportDate || '')
-      if (self.parsedInfo.matchedMemberId) {
-        params += '&matchedMemberId=' + self.parsedInfo.matchedMemberId
-      }
-      if (self.memberId) {
-        params += '&memberId=' + self.memberId
-      }
-      if (self.familyId) {
-        params += '&familyId=' + self.familyId
-      }
-
-      uni.navigateTo({ url: '/pages/health/report-confirm?' + params })
-    },
     getImageUrl: function(path) {
       return config.API_BASE_URL + path
     }
@@ -621,326 +278,15 @@ export default {
   margin-bottom: 16rpx;
 }
 
-/* 类型选择 */
-.type-selector {
-  display: flex;
-  flex-direction: row;
-  gap: 20rpx;
-}
-.type-option {
-  flex: 1;
-  display: flex;
-  flex-direction: column;
-  align-items: center;
-  padding: 24rpx;
-  border-radius: 16rpx;
-  background: #F5F7FA;
-  border: 2rpx solid transparent;
-}
-.type-selected {
-  background: #E3F2FD;
-  border-color: #5B9BD5;
-}
-.type-icon {
-  font-size: 40rpx;
-  margin-bottom: 8rpx;
-}
-.type-name {
-  font-size: 26rpx;
-  color: #333;
-  font-weight: 500;
-}
-
-/* 文件上传 */
-.file-upload-area {
-  border: 2rpx dashed #CBD5E1;
-  border-radius: 16rpx;
-  padding: 32rpx 20rpx;
-  text-align: center;
-  background: #FAFBFC;
-  cursor: pointer;
-}
-.file-upload-area:active {
-  background: #F1F5F9;
-}
-.file-upload-placeholder {
-  display: flex;
-  flex-direction: column;
-  align-items: center;
-}
-.file-upload-icon {
-  font-size: 48rpx;
-  margin-bottom: 12rpx;
-}
-.file-upload-text {
-  font-size: 28rpx;
-  color: #5B9BD5;
-  font-weight: 500;
-}
-.file-upload-hint {
-  font-size: 22rpx;
-  color: #94A3B8;
-  margin-top: 8rpx;
-}
-.file-info {
-  display: flex;
-  flex-direction: row;
-  align-items: center;
-  background: #E3F2FD;
-  border-radius: 12rpx;
-  padding: 16rpx 20rpx;
-  margin-top: 16rpx;
-}
-.file-name {
-  flex: 1;
-  font-size: 24rpx;
-  color: #1565C0;
-  overflow: hidden;
-  text-overflow: ellipsis;
-  white-space: nowrap;
-}
-.file-size {
-  font-size: 22rpx;
-  color: #64748B;
-  margin: 0 16rpx;
-  flex-shrink: 0;
-}
-.file-remove {
-  font-size: 24rpx;
-  color: #EF4444;
-  flex-shrink: 0;
-  padding: 4rpx 8rpx;
-}
-
-/* 日期 */
-.date-display {
+/* 上传中状态 */
+.analyzing-status {
   display: flex;
   flex-direction: row;
   align-items: center;
-  justify-content: space-between;
-  padding: 16rpx 20rpx;
-  background: #F5F7FA;
-  border-radius: 12rpx;
-  font-size: 28rpx;
-  color: #333;
-}
-.date-arrow {
-  font-size: 32rpx;
-  color: #999;
-}
-
-/* 评分滑块 */
-.score-input-wrap {
-  display: flex;
-  flex-direction: row;
-  align-items: center;
-}
-.score-slider {
-  flex: 1;
-}
-.score-value {
-  font-size: 28rpx;
-  font-weight: bold;
-  color: #5B9BD5;
-  margin-left: 16rpx;
-  min-width: 80rpx;
-  text-align: center;
-}
-
-/* 备注 */
-.note-input {
-  width: 100%;
-  min-height: 160rpx;
-  background: #F5F7FA;
-  border-radius: 12rpx;
-  padding: 20rpx;
-  font-size: 26rpx;
-  color: #333;
-  box-sizing: border-box;
-}
-
-/* 提交按钮 */
-.submit-btn {
-  background: linear-gradient(135deg, #5B9BD5, #3A7CC4);
-  color: #fff;
-  font-size: 30rpx;
-  font-weight: bold;
-  text-align: center;
-  padding: 28rpx;
-  border-radius: 16rpx;
-  margin-top: 10rpx;
-}
-.submit-btn:active {
-  opacity: 0.85;
-}
-.btn-disabled {
-  opacity: 0.5;
-}
-
-/* ===== PDF解析预览 ===== */
-.parsed-preview {
-  background: #E8F5E9;
-  border-radius: 16rpx;
-  margin: 0 30rpx 20rpx;
-  padding: 20rpx 24rpx;
-  border: 2rpx solid #A5D6A7;
-}
-.parsed-title {
-  font-size: 24rpx;
-  font-weight: bold;
-  color: #2E7D32;
-  margin-bottom: 12rpx;
-}
-.parsed-row {
-  display: flex;
-  flex-direction: row;
-  align-items: center;
-  margin-bottom: 8rpx;
-}
-.parsed-label {
-  font-size: 24rpx;
-  color: #388E3C;
-  width: 140rpx;
-  flex-shrink: 0;
-}
-.parsed-value {
-  font-size: 24rpx;
-  color: #1B5E20;
-  font-weight: 500;
-}
-.match-status {
-  display: flex;
-  flex-direction: row;
-  align-items: center;
-  margin-top: 12rpx;
-  padding: 12rpx 16rpx;
-  background: #FFF3E0;
-  border-radius: 8rpx;
-}
-.match-status.success {
-  background: #E8F5E9;
-}
-.match-icon {
-  font-size: 24rpx;
-  margin-right: 8rpx;
-}
-.match-text {
-  font-size: 24rpx;
-  color: #E65100;
-}
-.match-status.success .match-text {
-  color: #2E7D32;
-}
-
-/* ===== 家庭成员选择器 ===== */
-.member-selector {
-  margin: 0 30rpx 20rpx;
-}
-.member-list {
-  display: flex;
-  flex-direction: row;
-  flex-wrap: wrap;
-  gap: 16rpx;
-}
-.member-item {
-  display: flex;
-  flex-direction: column;
-  align-items: center;
-  padding: 20rpx 24rpx;
-  background: #F5F7FA;
-  border-radius: 16rpx;
-  border: 2rpx solid transparent;
-  min-width: 120rpx;
-}
-.member-selected {
-  background: #E3F2FD;
-  border-color: #5B9BD5;
-}
-.member-avatar {
-  width: 64rpx;
-  height: 64rpx;
-  border-radius: 50%;
-  background: #5B9BD5;
-  color: #fff;
-  font-size: 28rpx;
-  font-weight: bold;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  margin-bottom: 8rpx;
-}
-.member-name {
-  font-size: 24rpx;
-  color: #333;
-}
-.add-member-btn {
-  margin-top: 16rpx;
-  padding: 20rpx;
-  border: 1rpx dashed #4caf50;
-  border-radius: 12rpx;
-  text-align: center;
-  background: #f6fbf6;
-}
-.add-member-text {
-  font-size: 26rpx;
-  color: #4caf50;
-}
-
-/* ===== 舌诊拍照区 ===== */
-.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;
+  font-size: 26rpx;
   color: #5B9BD5;
-  margin-top: 8rpx;
 }
 
 /* ===== 底部 ===== */

+ 32 - 0
cfc-frontend/utils/api.js

@@ -2114,6 +2114,38 @@ export const parseReportPreview = (filePath, memberId, type) => {
 
 // 阶段2:确认落库(可选传修改后的payload和subjectId)
 
+// 阶段0:仅上传报告文件(不解析),返回 draftId
+export const uploadReportOnly = (filePath, type) => {
+  return new Promise((resolve, reject) => {
+    const token = uni.getStorageSync('token')
+    uni.uploadFile({
+      url: BASE_URL + '/api/health/report/upload' + (type ? '?type=' + type : ''),
+      filePath: filePath,
+      name: 'file',
+      header: {
+        'Authorization': token ? 'Bearer ' + token : ''
+      },
+      success: (res) => {
+        var data
+        try { data = JSON.parse(res.data) } catch (e) { data = res.data }
+        if (data && data.code === 200) {
+          resolve(data)
+        } else {
+          console.warn('[API] 上传失败 ' + BASE_URL + '/api/health/report/upload' + ' code=' + (data && data.code || '') + ' msg=' + (data && data.message || ''))
+          reject(data)
+        }
+      },
+      fail: (err) => {
+        console.error('[API] 上传网络错误 ' + BASE_URL + '/api/health/report/upload', err)
+        reject(err)
+      }
+    })
+  })
+}
+
+// 阶段1:按 draftId 触发解析,返回完整 payload + 成员匹配
+export const parseReportDraft = (draftId) => request('/api/health/report/parse', 'POST', { draftId })
+
 export const confirmReportPreview = (data) => request('/api/health/report/confirm', 'POST', data)
 
 // 鏀惧純鑽夌