Bladeren bron

feat: 互动记录支持照片/视频/音频附件上传及多项优化

- 互动记录:新增照片/视频/音频上传(前端附件UI + /api/media/upload 复用,
  后端 interaction_logs 加 photo_urls/video_url/audio_url 列,迁移303)
- 互动记录:修复保存失败(fromMemberId==toMemberId 后端拒绝,改为
  当前用户发起/toMemberId=选定成员,互动对象列表过滤本人)
- 家庭成员页:列表隐藏当前登录用户(filteredMembers)
- 成员选择进入记录互动时隐藏互动对象选择器,默认选定的成员
- relation-detail 互动记录入口修正路由(family→action-detail)
- 报告上传:合并 DAN 与健康报告上传页,指纹自动识别类型,取消手动
  选择报告类型/维度,新增从聊天/拍照/文件夹三种上传方式
- 推荐人绑定:bindReferralIfNotSelf 防自邀打扰(缓存本人邀请码,
  邀请码==本人时静默跳过),登录/分享/新用户流程接入
- 家庭邀请冷启动识别 token/familyInviteCode 参数,放行落地页
- user-edit 新用户流程:家庭身份选择置顶、提交申请/跳过悬浮底部
- invite/join 先初始化登录状态再校验家庭邀请
- 录入页面 & 深层分享参数兜底 & 会员升级等文案微调
liaoxg 1 week geleden
bovenliggende
commit
8ab8c94c09

+ 4 - 0
.gitignore

@@ -71,6 +71,10 @@ venv/
 
 # Codegraph
 .codegraph/
+.codegraph-backup/
+
+# 一次性解析调试脚本(不入库)
+/cfc-frontend/test-parse.js
 
 # Skills temp directory
 .superpowers/sdd/

+ 5 - 0
cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java

@@ -10502,5 +10502,10 @@ public class DatabaseInitializer implements CommandLineRunner {
         } catch (Exception e) {
             log.warn("迁移301: platform_balance_log 唯一索引处理失败: {}", e.getMessage());
         }
+
+        // 迁移303: interaction_logs表添加附件字段(照片、视频、音频URL)
+        ensureColumn("interaction_logs", "photo_urls", "TEXT COMMENT '照片URL列表(逗号分隔)'");
+        ensureColumn("interaction_logs", "video_url", "VARCHAR(500) COMMENT '视频URL'");
+        ensureColumn("interaction_logs", "audio_url", "VARCHAR(500) COMMENT '音频URL'");
     }
 }

+ 9 - 0
cfc-backend/src/main/java/com/etotem/cfc/dto/AddInteractionDTO.java

@@ -17,4 +17,13 @@ public class AddInteractionDTO {
     private String description;
 
     private String happenedAt;
+
+    /** 照片URL列表(逗号分隔) */
+    private String photoUrls;
+
+    /** 视频URL */
+    private String videoUrl;
+
+    /** 音频URL */
+    private String audioUrl;
 }

+ 9 - 0
cfc-backend/src/main/java/com/etotem/cfc/dto/InteractionLogVO.java

@@ -27,4 +27,13 @@ public class InteractionLogVO {
     private Date happenedAt;
 
     private Date createdAt;
+
+    /** 照片URL列表(逗号分隔) */
+    private String photoUrls;
+
+    /** 视频URL */
+    private String videoUrl;
+
+    /** 音频URL */
+    private String audioUrl;
 }

+ 9 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/InteractionLog.java

@@ -33,5 +33,14 @@ public class InteractionLog implements Serializable {
     /** 互动发生时间 */
     private Date happenedAt;
 
+    /** 照片URL列表(逗号分隔) */
+    private String photoUrls;
+
+    /** 视频URL */
+    private String videoUrl;
+
+    /** 音频URL */
+    private String audioUrl;
+
     private Date createdAt;
 }

+ 6 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/InteractionLogService.java

@@ -66,6 +66,9 @@ public class InteractionLogService {
         log.setInteractionType(dto.getInteractionType());
         log.setDescription(dto.getDescription());
         log.setCreatedAt(new Date());
+        log.setPhotoUrls(dto.getPhotoUrls());
+        log.setVideoUrl(dto.getVideoUrl());
+        log.setAudioUrl(dto.getAudioUrl());
 
         if (dto.getHappenedAt() != null && !dto.getHappenedAt().isEmpty()) {
             try {
@@ -140,6 +143,9 @@ public class InteractionLogService {
         vo.setDescription(il.getDescription());
         vo.setHappenedAt(il.getHappenedAt());
         vo.setCreatedAt(il.getCreatedAt());
+        vo.setPhotoUrls(il.getPhotoUrls());
+        vo.setVideoUrl(il.getVideoUrl());
+        vo.setAudioUrl(il.getAudioUrl());
 
         // 填充成员名称
         FamilyMember from = familyMemberMapper.selectById(il.getFromMemberId());

+ 3 - 0
cfc-backend/src/main/resources/schema.sql

@@ -4402,6 +4402,9 @@ CREATE TABLE IF NOT EXISTS interaction_logs (
     interaction_type VARCHAR(20) NOT NULL COMMENT '互动类型: dining/outgoing/call/video/chat/play/gift',
     description VARCHAR(500) COMMENT '互动描述',
     happened_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '互动发生时间',
+    photo_urls TEXT COMMENT '照片URL列表(逗号分隔)',
+    video_url VARCHAR(500) COMMENT '视频URL',
+    audio_url VARCHAR(500) COMMENT '音频URL',
     created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
     INDEX idx_ilog_family (family_id),
     INDEX idx_ilog_to_member (to_member_id)

+ 1 - 1
cfc-frontend/AGENTS.md

@@ -45,7 +45,7 @@ uni-app 微信小程序前端,Vue 2 语法,家庭教育任务管理。
 | `pages/rewards` | 心愿单/勋章墙(2 页) |
 | `pages/games` | 小游戏(5 页) |
 | `pages/assessment` | 测评申请/报告(5 页) |
-| `pages/dan-assessment` | DAN 测评上传(1 页) |
+| `pages/dan-assessment` | DAN 测评报告(1 页) |
 | `pages/growth` | 成长记录(6 页) |
 | `pages/shop` | 商城(15 页) |
 | `pages/vendor` | 服务商入驻/管理(5 页) |

+ 6 - 4
cfc-frontend/App.vue

@@ -1,7 +1,7 @@
 <script>
 import config from '@/config.js'
 import Vue from 'vue'
-import { checkUserFamily, verifyToken, bindReferral } from './utils/api.js'
+import { checkUserFamily, verifyToken, bindReferralIfNotSelf } from './utils/api.js'
 Vue.prototype.$config = config
 
 export default {
@@ -9,8 +9,10 @@ export default {
     console.log('App Launch', options)
     // 捕获分享/邀请参数(普通分享链接 ?inviteCode=xxx):未登录写入 storage 供注册登录后绑定推荐人,已登录直接绑定
     this.captureReferralParams(options)
-    // 分享/深链冷启动(携带分享参数)放行到落地页,不套 splash,避免分享的文章/小程序页面打不开
-    var hasDeepLink = options && options.query && (options.query.inviteCode || options.query.id)
+    // 分享/深链冷启动(携带分享参数)放行到落地页,不套 splash,避免分享的文章/小程序页面打不开。
+    // 家庭邀请分享卡为 ?token=xxx(family-members 分享)或 ?familyInviteCode=xxx(user-edit 直达),
+    // 必须一并识别,否则冷启动会被 redirectTo splash 劫持导致邀请页打不开(空白/闪到首页)
+    var hasDeepLink = options && options.query && (options.query.inviteCode || options.query.id || options.query.token || options.query.familyInviteCode)
     // 冷启动:跳转到 splash 页展示过渡动画,2秒后自动进入首页
     if (options.scene !== 1047 && options.scene !== 1048 && options.scene !== 1049 && !hasDeepLink) {
       uni.setStorageSync('_isFirstLaunch', '1')
@@ -67,7 +69,7 @@ export default {
       if (uni.getStorageSync('token')) {
         var bound = uni.getStorageSync('boundInviteCode')
         if (bound === code) return
-        bindReferral(code).then(function(res) {
+        bindReferralIfNotSelf(code).then(function(res) {
           if (res && res.code === 200) {
             uni.setStorageSync('boundInviteCode', code)
             console.log('分享参数推荐人绑定成功', code)

+ 2 - 2
cfc-frontend/components/share-mixin.js

@@ -7,7 +7,7 @@
  *   mounted/after data load:
  *     this.setShareInfo('推荐阅读: ' + title, '/pages/article-center/article-detail?id=' + id)
  */
-import { getReferralCode, bindReferral, extractReferralCode } from '../utils/api.js'
+import { getReferralCode, bindReferralIfNotSelf, extractReferralCode } from '../utils/api.js'
 
 export default {
   data() {
@@ -64,7 +64,7 @@ export default {
       if (bound === code) return
 
       try {
-        var res = await bindReferral(code)
+        var res = await bindReferralIfNotSelf(code)
         if (res && res.code === 200) {
           uni.setStorageSync('boundInviteCode', code)
           console.log('邀请码绑定成功', code)

+ 1 - 8
cfc-frontend/pages.json

@@ -404,13 +404,6 @@
     {
       "root": "pages/dan-assessment",
       "pages": [
-        {
-          "path": "report-upload",
-          "style": {
-            "navigationBarTitleText": "DAN 报告上传",
-            "navigationStyle": "custom"
-          }
-        },
         {
           "path": "report-result",
           "style": {
@@ -825,7 +818,7 @@
         {
           "path": "report-upload",
           "style": {
-            "navigationBarTitleText": "上传健康报告"
+            "navigationBarTitleText": "上传报告"
           }
         },
         {

+ 354 - 10
cfc-frontend/pages/action-detail/interaction-log.vue

@@ -5,20 +5,20 @@
     </view>
 
     <!-- 家庭数据区 -->
-    <!-- 选择互动对象 -->
-    <view class="form-section">
+    <!-- 选择互动对象(仅在未通过URL传入memberId时显示) -->
+    <view class="form-section" v-if="!preselectedMemberId">
       <text class="section-label">互动对象</text>
       <view class="member-select">
         <view class="member-item" v-for="m in members" :key="m.id"
               :class="{selected: selectedMemberId === m.id}"
               @click="selectMember(m)">
           <text class="member-name">{{ m.nickname }}</text>
-          <text class="member-rel">{{ m.relationshipTypeName || m.relationshipType }}</text>
+          <text class="member-rel">{{ m.relationship || m.relativeLabel || m.roleLabel }}</text>
         </view>
       </view>
     </view>
 
-    <!-- 选择互动类型 -->
+    <!-- 互动类型 -->
     <view class="form-section">
       <text class="section-label">互动类型</text>
       <view class="type-grid">
@@ -52,25 +52,96 @@
       </view>
     </view>
 
+    <!-- 附件上传区域 -->
+    <view class="form-section">
+      <text class="section-label">添加附件(选填)</text>
+
+      <!-- 照片区域 -->
+      <view class="media-section">
+        <text class="media-subtitle">📷 照片</text>
+        <view class="media-grid">
+          <view class="media-item" v-for="(photo, idx) in photos" :key="idx">
+            <image class="media-preview" :src="photo" mode="aspectFill" @click="previewPhoto(idx)"></image>
+            <view class="media-delete" @click.stop="removePhoto(idx)">×</view>
+          </view>
+          <view class="media-add" v-if="photos.length < 9" @click="choosePhotos">
+            <text class="media-add-icon">+</text>
+            <text class="media-add-text">{{ photos.length }}/9</text>
+          </view>
+        </view>
+      </view>
+
+      <!-- 视频区域 -->
+      <view class="media-section" style="margin-top: 20rpx;">
+        <text class="media-subtitle">🎬 视频</text>
+        <view class="media-grid" v-if="video">
+          <view class="media-item video-item">
+            <view class="video-placeholder">
+              <text class="video-icon">🎬</text>
+              <text class="video-name">{{ videoName || '视频已选择' }}</text>
+            </view>
+            <view class="media-delete" @click.stop="removeVideo">×</view>
+          </view>
+        </view>
+        <view class="media-add" v-if="!video" @click="chooseVideo">
+          <text class="media-add-icon">+</text>
+          <text class="media-add-text">选择视频</text>
+        </view>
+      </view>
+
+      <!-- 音频区域 -->
+      <view class="media-section" style="margin-top: 20rpx;">
+        <text class="media-subtitle">🎵 音频</text>
+        <view class="media-grid" v-if="audio">
+          <view class="media-item audio-item">
+            <view class="audio-placeholder">
+              <text class="audio-icon">🎵</text>
+              <text class="audio-name">{{ audioName || '音频已选择' }}</text>
+            </view>
+            <view class="media-delete" @click.stop="removeAudio">×</view>
+          </view>
+        </view>
+        <view class="media-add" v-if="!audio" @click="chooseAudio">
+          <text class="media-add-icon">+</text>
+          <text class="media-add-text">选择音频</text>
+        </view>
+      </view>
+    </view>
+
     <!-- 提交按钮 -->
     <view class="submit-bar">
-      <button class="btn-submit" @click="onSubmit" :disabled="!canSubmit">保存记录</button>
+      <button class="btn-submit" @click="onSubmit" :disabled="!canSubmit || submitting">
+        {{ submitting ? '保存中...' : '保存记录' }}
+      </button>
     </view>
   </view>
 </template>
 
 <script>
-import { getFamilyMemberList, addInteraction, getInteractionTypes } from '../../utils/api.js'
+import { getFamilyMemberList, addInteraction } from '../../utils/api.js'
+import config from '../../config.js'
 
 export default {
   data() {
     return {
       members: [],
       selectedMemberId: null,
+      preselectedMemberId: null, // URL传入的memberId,不显示成员选择器
+      currentMemberId: null,     // 当前登录用户的家庭成员ID(fromMemberId)
       selectedType: '',
       description: '',
       timeType: 'now',
       customTime: '',
+      submitting: false,
+
+      // 附件相关
+      photos: [],      // 照片临时路径列表(最多9张)
+      video: '',        // 视频临时路径
+      videoName: '',
+      audio: '',        // 音频临时路径
+      audioName: '',
+      uploading: false,
+
       interactionTypes: {
         dining: '一起用餐',
         outgoing: '外出活动',
@@ -98,6 +169,7 @@ export default {
   },
   onLoad(options) {
     if (options.memberId) {
+      this.preselectedMemberId = Number(options.memberId)
       this.selectedMemberId = Number(options.memberId)
     }
     this.loadMembers()
@@ -107,7 +179,26 @@ export default {
       try {
         var res = await getFamilyMemberList()
         if (res.code === 200 && res.data) {
-          this.members = res.data || []
+          var all = res.data || []
+          // 确定"我"的成员ID:优先从成员列表中匹配当前登录用户
+          var userInfo = uni.getStorageSync('userInfo')
+          var uid = (userInfo && userInfo.id) || 0
+          var self = this
+          var found = null
+          if (uid) {
+            for (var i = 0; i < all.length; i++) {
+              var m = all[i]
+              if (m && m.userId && Number(m.userId) === Number(uid)) {
+                found = m
+                break
+              }
+            }
+          }
+          this.currentMemberId = found ? found.id : null
+          // 互动对象列表过滤掉"我"(from==to 后端会拒绝)
+          this.members = all.filter(function(m) {
+            return !(m && m.userId && found && Number(m.userId) === Number(found.userId))
+          })
         }
       } catch (e) {
         uni.showToast({ title: '加载成员失败', icon: 'none' })
@@ -119,22 +210,179 @@ export default {
     onTimeChange(e) {
       this.customTime = e.detail.value
     },
+
+    // ===== 照片选择 =====
+    choosePhotos() {
+      var self = this
+      var remaining = 9 - self.photos.length
+      if (remaining <= 0) return
+
+      uni.chooseImage({
+        count: remaining,
+        sizeType: ['compressed'],
+        sourceType: ['album', 'camera'],
+        success: function(res) {
+          res.tempFilePaths.forEach(function(path) {
+            if (self.photos.length < 9) {
+              self.photos.push(path)
+            }
+          })
+        }
+      })
+    },
+    previewPhoto(idx) {
+      uni.previewImage({
+        current: idx,
+        urls: this.photos
+      })
+    },
+    removePhoto(idx) {
+      this.photos.splice(idx, 1)
+    },
+
+    // ===== 视频选择 =====
+    chooseVideo() {
+      var self = this
+      uni.chooseVideo({
+        sourceType: ['album', 'camera'],
+        maxDuration: 60,
+        compressed: true,
+        success: function(res) {
+          self.video = res.tempFilePath
+          self.videoName = res.name || '视频'
+        }
+      })
+    },
+    removeVideo() {
+      this.video = ''
+      this.videoName = ''
+    },
+
+    // ===== 音频选择 =====
+    chooseAudio() {
+      var self = this
+      uni.chooseMessageFile({
+        count: 1,
+        type: 'file',
+        extension: ['mp3', 'wav', 'm4a', 'aac', 'ogg'],
+        success: function(res) {
+          if (res.tempFiles && res.tempFiles.length > 0) {
+            self.audio = res.tempFiles[0].path
+            self.audioName = res.tempFiles[0].name || '音频'
+          }
+        },
+        fail: function() {
+          // 用户取消或不支持,尝试录音
+          uni.chooseVideo({
+            sourceType: ['album'],
+            maxDuration: 60,
+            compressed: true,
+            success: function(res) {
+              self.audio = res.tempFilePath
+              self.audioName = res.name || '音频'
+            }
+          })
+        }
+      })
+    },
+    removeAudio() {
+      this.audio = ''
+      this.audioName = ''
+    },
+
+    // ===== 上传文件 =====
+    uploadFile(filePath) {
+      var self = this
+      return new Promise(function(resolve, reject) {
+        uni.uploadFile({
+          url: config.API_BASE_URL + '/api/media/upload',
+          filePath: filePath,
+          name: 'file',
+          header: { 'Authorization': 'Bearer ' + uni.getStorageSync('token') },
+          success: function(res) {
+            try {
+              var data = JSON.parse(res.data)
+              if (data && data.data && data.data.url) {
+                resolve(data.data.url)
+              } else if (data && data.url) {
+                resolve(data.url)
+              } else {
+                resolve('')
+              }
+            } catch (e) {
+              resolve(res.data || '')
+            }
+          },
+          fail: function() { reject() }
+        })
+      })
+    },
+
+    // ===== 提交 =====
     async onSubmit() {
       if (!this.canSubmit) {
         uni.showToast({ title: '请选择对象和类型', icon: 'none' })
         return
       }
+      if (this.submitting) return
+
+      // fromMemberId 必须与 toMemberId 不同(后端校验"互动双方不能是同一人")
+      var fromId = this.currentMemberId || this.selectedMemberId
+      if (!fromId || Number(fromId) === Number(this.selectedMemberId)) {
+        uni.showToast({ title: '无法确定当前成员,请返回后重试', icon: 'none' })
+        return
+      }
+
+      this.submitting = true
+      this.uploading = true
+
       try {
         var happenedAt = ''
         if (this.timeType === 'custom' && this.customTime) {
           happenedAt = this.customTime
         }
+
+        // 上传附件
+        var photoUrls = []
+        if (this.photos && this.photos.length > 0) {
+          for (var i = 0; i < this.photos.length; i++) {
+            try {
+              var url = await this.uploadFile(this.photos[i])
+              if (url) photoUrls.push(url)
+            } catch (e) {
+              // 单个上传失败不阻塞整个流程
+            }
+          }
+        }
+        var videoUrl = ''
+        if (this.video) {
+          try {
+            videoUrl = await this.uploadFile(this.video) || ''
+          } catch (e) {
+            videoUrl = ''
+          }
+        }
+        var audioUrl = ''
+        if (this.audio) {
+          try {
+            audioUrl = await this.uploadFile(this.audio) || ''
+          } catch (e) {
+            audioUrl = ''
+          }
+        }
+
+        this.uploading = false
+
+        // 保存互动记录
         var res = await addInteraction({
-          fromMemberId: this.selectedMemberId,
+          fromMemberId: fromId,
           toMemberId: this.selectedMemberId,
           interactionType: this.selectedType,
           description: this.description,
-          happenedAt: happenedAt
+          happenedAt: happenedAt,
+          photoUrls: photoUrls.join(','),
+          videoUrl: videoUrl,
+          audioUrl: audioUrl
         })
         if (res.code === 200) {
           uni.showToast({ title: '记录成功', icon: 'success' })
@@ -146,6 +394,9 @@ export default {
         }
       } catch (e) {
         uni.showToast({ title: '保存失败', icon: 'none' })
+      } finally {
+        this.submitting = false
+        this.uploading = false
       }
     }
   }
@@ -265,6 +516,99 @@ export default {
   font-size: 28rpx;
   color: #333;
 }
+
+/* ===== 附件上传样式 ===== */
+.media-section {
+  margin-bottom: 16rpx;
+}
+.media-subtitle {
+  font-size: 24rpx;
+  color: #666;
+  display: block;
+  margin-bottom: 12rpx;
+}
+.media-grid {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 12rpx;
+}
+.media-item {
+  width: 150rpx;
+  height: 150rpx;
+  border-radius: 8rpx;
+  position: relative;
+  overflow: hidden;
+}
+.media-preview {
+  width: 100%;
+  height: 100%;
+}
+.media-delete {
+  position: absolute;
+  top: 0;
+  right: 0;
+  width: 40rpx;
+  height: 40rpx;
+  background: rgba(0, 0, 0, 0.6);
+  color: #fff;
+  font-size: 24rpx;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  border-radius: 0 8rpx 0 8rpx;
+}
+.video-item,
+.audio-item {
+  width: 200rpx;
+  height: 100rpx;
+  background: #f0f0f0;
+  border-radius: 8rpx;
+}
+.video-placeholder,
+.audio-placeholder {
+  width: 100%;
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+}
+.video-icon,
+.audio-icon {
+  font-size: 32rpx;
+}
+.video-name,
+.audio-name {
+  font-size: 20rpx;
+  color: #666;
+  margin-top: 8rpx;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  max-width: 180rpx;
+}
+.media-add {
+  width: 150rpx;
+  height: 150rpx;
+  background: #fafafa;
+  border: 2rpx dashed #ddd;
+  border-radius: 8rpx;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+}
+.media-add-icon {
+  font-size: 48rpx;
+  color: #999;
+}
+.media-add-text {
+  font-size: 20rpx;
+  color: #999;
+  margin-top: 8rpx;
+}
+
+/* ===== 提交按钮 ===== */
 .submit-bar {
   margin-top: 40rpx;
 }
@@ -280,4 +624,4 @@ export default {
 .btn-submit[disabled] {
   background: #ccc;
 }
-</style>
+</style>

+ 0 - 743
cfc-frontend/pages/dan-assessment/report-upload.vue

@@ -1,743 +0,0 @@
-<template>
-  <view class="page">
-    <view class="header">
-      <view class="back-btn" @click="goBack">
-        <text class="back-icon">&#x2190;</text>
-        <text class="back-text">返回</text>
-      </view>
-      <text class="header-title">DAN 报告上传</text>
-    </view>
-
-    <!-- Step 1: 选择维度 + 选择文件 -->
-    <view class="section" v-if="step === 1">
-      <view class="dimension-select">
-        <text class="dimension-label">报告类型</text>
-        <view class="dimension-options">
-          <view
-            v-for="item in dimensionOptions"
-            :key="item.code"
-            :class="['dimension-option', dimension === item.code ? 'active' : '']"
-            @click="selectDimension(item.code)">
-            <text class="dimension-name">{{ item.label }}</text>
-          </view>
-        </view>
-      </view>
-
-      <!-- 购买推荐 -->
-      <view class="purchase-section" v-if="recommendedProducts.length > 0">
-        <view class="purchase-header">
-          <text class="purchase-icon">📦</text>
-          <text class="purchase-title">为家人购买检测服务</text>
-        </view>
-        <scroll-view class="purchase-scroll" scroll-x enable-flex show-scrollbar="false">
-          <view
-            class="purchase-card"
-            v-for="item in recommendedProducts"
-            :key="item.id"
-            @click="goToProduct(item.id)"
-          >
-            <image class="purchase-cover" :src="getImageUrl(item.coverImage) || '/static/default-product.png'" mode="aspectFill" />
-            <text class="purchase-name">{{ item.name }}</text>
-            <text class="purchase-price">{{ formatPriceWithSymbol(item.price) }}</text>
-            <view class="purchase-btn">
-              <text class="purchase-btn-text">去购买</text>
-            </view>
-          </view>
-        </scroll-view>
-      </view>
-
-      <view class="upload-area" @click="chooseFile">
-        <view class="upload-icon" v-if="!selectedFile">&#x1F4C4;</view>
-        <view class="upload-icon" v-else>&#x2705;</view>
-        <text class="upload-text" v-if="!selectedFile">点击上传 PDF 文件</text>
-        <text class="upload-text" v-else>{{ selectedFile.name }}</text>
-        <text class="upload-hint">支持 PDF 格式的 DAN 测评报告</text>
-      </view>
-
-      <view class="action-bar">
-        <view class="upload-btn" @click="startUpload" v-if="selectedFile">
-          <text class="upload-btn-text">上传并解析</text>
-        </view>
-      </view>
-    </view>
-
-    <!-- Step 2: 预览解析结果 -->
-    <view class="section" v-if="step === 2">
-      <view class="preview-header">
-        <text class="preview-title">解析预览</text>
-        <text class="preview-desc">请确认以下数据是否准确</text>
-      </view>
-
-      <view class="preview-card" v-if="previewData">
-        <view class="preview-row">
-          <text class="preview-label">维度</text>
-          <text class="preview-value">{{ dimensionLabel }}</text>
-        </view>
-        <view class="preview-row">
-          <text class="preview-label">解析概要</text>
-          <text class="preview-value">{{ previewData.summary || '无' }}</text>
-        </view>
-      </view>
-
-      <!-- 解析项列表 -->
-      <view class="items-section" v-if="previewData && previewData.items && previewData.items.length > 0">
-        <text class="items-title">数据项 ({{ previewData.items.length }})</text>
-        <view class="item-card" v-for="(item, idx) in previewData.items" :key="idx">
-          <view class="item-header">
-            <text class="item-name">{{ item.name || item.code }}</text>
-            <text class="item-category">{{ item.category }}</text>
-          </view>
-          <view class="item-value-bar">
-            <view class="item-value-fill" :style="'width:' + item.value + '%'"></view>
-          </view>
-          <text class="item-value-text">{{ item.value }}/100</text>
-        </view>
-      </view>
-
-      <!-- 建议 -->
-      <view class="suggestions-card" v-if="previewData && previewData.suggestions">
-        <text class="suggestions-title">成长建议</text>
-        <text class="suggestions-text">{{ previewData.suggestions }}</text>
-      </view>
-
-      <view class="action-bar">
-        <view class="confirm-btn" @click="doConfirm" v-if="!confirming">
-          <text class="confirm-btn-text">确认并保存</text>
-        </view>
-        <view class="confirm-btn disabled" v-else>
-          <text class="confirm-btn-text">保存中...</text>
-        </view>
-        <view class="retry-btn" @click="resetUpload">
-          <text class="retry-btn-text">重新上传</text>
-        </view>
-      </view>
-    </view>
-
-    <!-- Step 3: 完成 -->
-    <view class="section" v-if="step === 3">
-      <view class="success-card">
-        <text class="success-icon">&#x2705;</text>
-        <text class="success-title">报告已保存</text>
-        <text class="success-desc">DAN 测评数据已更新至五维能量体系</text>
-        <text class="success-desc" v-if="planId">成长方案已自动生成,请确认后分解为每日任务</text>
-      </view>
-      <view class="action-bar">
-        <view class="upload-btn" @click="goToPlan" v-if="planId">
-          <text class="upload-btn-text">查看成长方案</text>
-        </view>
-        <view class="retry-btn" @click="goToResult" v-if="linkedResultId">
-          <text class="retry-btn-text">查看测评结果</text>
-        </view>
-        <view class="retry-btn" @click="goBack">
-          <text class="retry-btn-text">返回</text>
-        </view>
-      </view>
-    </view>
-
-    <!-- Loading -->
-    <view class="loading-overlay" v-if="loading">
-      <view class="loading-box">
-        <text class="loading-text">解析中...</text>
-      </view>
-    </view>
-  </view>
-</template>
-
-<script>
-import { danParsePreview, danConfirmReport, productList } from '../../utils/api.js'
-import config from '@/config.js'
-
-var REPORT_DOMAIN_MAP = {
-  mind: 'mind',
-  wisdom: 'wisdom',
-  cognition: 'wisdom',
-  learning: 'wisdom',
-  behavior: 'mind',
-  career: 'wisdom',
-  campus: 'wisdom',
-  adolescent: 'mind'
-}
-
-var DIMENSION_OPTIONS = [
-  { code: 'mind', label: '心维度 (A2)' },
-  { code: 'wisdom', label: '智维度 (B4)' },
-  { code: 'cognition', label: '认知能力' },
-  { code: 'learning', label: '学习力' },
-  { code: 'behavior', label: '行为' },
-  { code: 'career', label: '生涯' },
-  { code: 'campus', label: '校园' },
-  { code: 'adolescent', label: '青春期' }
-]
-
-export default {
-  data() {
-    return {
-      step: 1,
-      dimension: 'mind',
-      dimensionOptions: DIMENSION_OPTIONS,
-      selectedFile: null,
-      previewData: null,
-      draftId: null,
-      planId: null,
-      linkedResultId: null,
-      loading: false,
-      confirming: false,
-      recommendedProducts: [],
-      loadingProducts: false
-    }
-  },
-  computed: {
-    dimensionLabel: function() {
-      for (var i = 0; i < this.dimensionOptions.length; i++) {
-        if (this.dimensionOptions[i].code === this.dimension) {
-          return this.dimensionOptions[i].label
-        }
-      }
-      return this.dimension || 'DAN 测评'
-    }
-  },
-  onLoad: function(options) {
-    var validCodes = DIMENSION_OPTIONS.map(function(d) { return d.code })
-    if (options && options.dimension && validCodes.indexOf(options.dimension) >= 0) {
-      this.dimension = options.dimension
-    }
-    if (options && options.resume === '1') {
-      this.resumeFromUnifiedUpload()
-    }
-    this.loadRecommendProducts(this.dimension)
-  },
-  methods: {
-    resumeFromUnifiedUpload: function() {
-      var cached = null
-      try {
-        cached = uni.getStorageSync('danUploadPreview')
-        uni.removeStorageSync('danUploadPreview')
-      } catch (e) {}
-      if (!cached || !cached.draftId) {
-        uni.showToast({ title: '预览数据失效,请重新上传', icon: 'none' })
-        return
-      }
-      if (cached.dimension) {
-        this.dimension = cached.dimension
-      }
-      this.draftId = cached.draftId
-      this.previewData = cached.previewData || {}
-      if (!this.previewData.items && this.previewData.itemsJson) {
-        try {
-          this.previewData.items = typeof this.previewData.itemsJson === 'string'
-            ? JSON.parse(this.previewData.itemsJson)
-            : this.previewData.itemsJson
-        } catch (e) {
-          this.previewData.items = []
-        }
-      }
-      this.step = 2
-    },
-    goBack: function() {
-      uni.navigateBack()
-    },
-    chooseFile: function() {
-      var self = this
-      uni.chooseMessageFile({
-        count: 1,
-        type: 'file',
-        extension: ['pdf'],
-        success: function(res) {
-          if (res.tempFiles && res.tempFiles.length > 0) {
-            self.selectedFile = res.tempFiles[0]
-          }
-        },
-        fail: function() {
-          // 降级:使用相册/相机
-          uni.chooseImage({
-            count: 1,
-            success: function(res) {
-              if (res.tempFiles && res.tempFiles.length > 0) {
-                self.selectedFile = res.tempFiles[0]
-              }
-            }
-          })
-        }
-      })
-    },
-    startUpload: function() {
-      if (this.loading) return
-      if (!this.selectedFile) return
-      var self = this
-      self.loading = true
-
-      var memberId = this.$store.state && this.$store.state.currentChildId
-          ? this.$store.state.currentChildId
-          : uni.getStorageSync('currentChildId')
-
-      danParsePreview(self.selectedFile.path || self.selectedFile.tempFilePath, self.dimension, memberId)
-        .then(function(res) {
-          self.loading = false
-          if (res.code === 200 && res.data) {
-            self.previewData = res.data
-            self.draftId = res.data.draftId
-            self.step = 2
-          } else {
-            uni.showToast({ title: res.message || '解析失败', icon: 'none' })
-          }
-        })
-        .catch(function(err) {
-          self.loading = false
-          uni.showToast({ title: err && err.message ? err.message : '上传失败', icon: 'none' })
-        })
-    },
-    doConfirm: function() {
-      if (this.confirming) return
-      var self = this
-      self.confirming = true
-
-      var memberId = this.$store.state && this.$store.state.currentChildId
-          ? this.$store.state.currentChildId
-          : uni.getStorageSync('currentChildId')
-
-      danConfirmReport({
-        draftId: self.draftId,
-        dimension: self.dimension,
-        memberId: memberId
-      })
-        .then(function(res) {
-          self.confirming = false
-          if (res.code === 200) {
-            var data = res.data || {}
-            self.planId = data.planId || null
-            self.linkedResultId = data.linkedResultId || null
-            self.step = 3
-            if (self.planId) {
-              uni.showModal({
-                title: '报告已入库',
-                content: '成长方案已自动生成,是否立即查看并确认任务?',
-                confirmText: '查看方案',
-                cancelText: '稍后',
-                success: function(modalRes) {
-                  if (modalRes.confirm) {
-                    self.goToPlan()
-                  }
-                }
-              })
-            }
-          } else {
-            uni.showToast({ title: res.message || '保存失败', icon: 'none' })
-          }
-        })
-        .catch(function(err) {
-          self.confirming = false
-          uni.showToast({ title: '保存失败', icon: 'none' })
-        })
-    },
-    goToPlan: function() {
-      if (!this.planId) return
-      uni.redirectTo({
-        url: '/pages/health/health-plan-summary?planId=' + this.planId
-      })
-    },
-    goToResult: function() {
-      if (!this.linkedResultId) return
-      uni.redirectTo({
-        url: '/pages/dan-assessment/report-result?reportId=' + this.linkedResultId
-      })
-    },
-    resetUpload: function() {
-      this.step = 1
-      this.selectedFile = null
-      this.previewData = null
-      this.draftId = null
-      this.planId = null
-      this.linkedResultId = null
-    },
-    selectDimension: function(dim) {
-      this.dimension = dim
-      this.loadRecommendProducts(dim)
-    },
-    loadRecommendProducts: function(dimension) {
-      var domain = REPORT_DOMAIN_MAP[dimension]
-      if (!domain) {
-        this.recommendedProducts = []
-        return
-      }
-      var self = this
-      self.loadingProducts = true
-      productList({ domain: domain, productType: 'assessment', size: 4 }).then(function(res) {
-        if (res && res.data && res.data.records) {
-          self.recommendedProducts = res.data.records
-        } else {
-          self.recommendedProducts = []
-        }
-      }).catch(function() {
-        self.recommendedProducts = []
-      }).finally(function() {
-        self.loadingProducts = false
-      })
-    },
-    goToProduct: function(productId) {
-      uni.navigateTo({ url: '/pages/discover-detail/product-detail/product-detail?id=' + productId })
-    },
-    formatPriceWithSymbol: function(cents) {
-      if (cents === null || cents === undefined) return '¥0'
-      return '¥' + (cents / 100).toFixed(2)
-    },
-    getImageUrl: function(path) {
-      return config.API_BASE_URL + path
-    }
-  }
-}
-</script>
-
-<style>
-.page {
-  min-height: 100vh;
-  background: #F5F5F7;
-  padding-bottom: 40rpx;
-}
-.header {
-  display: flex;
-  align-items: center;
-  padding: 88rpx 32rpx 24rpx;
-  background: linear-gradient(135deg, #667EEA, #764BA2);
-}
-.back-btn {
-  display: flex;
-  align-items: center;
-  margin-right: 24rpx;
-  padding: 8rpx 16rpx;
-}
-.back-icon {
-  font-size: 36rpx;
-  color: #fff;
-  margin-right: 8rpx;
-}
-
-.header-title {
-  font-size: 36rpx;
-  font-weight: 700;
-  color: #fff;
-}
-.section {
-  margin: 24rpx 24rpx 0;
-}
-.dimension-select {
-  background: #fff;
-  border-radius: 16rpx;
-  padding: 32rpx;
-}
-.dimension-label {
-  font-size: 28rpx;
-  font-weight: 600;
-  color: #333;
-  margin-bottom: 20rpx;
-  display: block;
-}
-.dimension-options {
-  display: flex;
-  flex-direction: row;
-  flex-wrap: wrap;
-}
-.dimension-option {
-  width: 48%;
-  margin-right: 2%;
-  margin-bottom: 16rpx;
-  padding: 20rpx 12rpx;
-  border-radius: 12rpx;
-  background: #F5F5F7;
-  text-align: center;
-  border: 2rpx solid transparent;
-  box-sizing: border-box;
-}
-.dimension-option.active {
-  background: #EEF2FF;
-  border-color: #6366F1;
-}
-.dimension-name {
-  font-size: 24rpx;
-  color: #333;
-  font-weight: 500;
-}
-.dimension-option.active .dimension-name {
-  color: #6366F1;
-}
-.upload-area {
-  background: #fff;
-  border-radius: 16rpx;
-  padding: 60rpx 32rpx;
-  text-align: center;
-  margin-top: 24rpx;
-  border: 2rpx dashed #D1D5DB;
-}
-.upload-icon {
-  font-size: 64rpx;
-  margin-bottom: 16rpx;
-}
-.upload-text {
-  font-size: 30rpx;
-  color: #333;
-  display: block;
-  margin-bottom: 8rpx;
-}
-.upload-hint {
-  font-size: 24rpx;
-  color: #999;
-}
-.action-bar {
-  margin-top: 32rpx;
-  display: flex;
-  gap: 20rpx;
-}
-.upload-btn {
-  flex: 1;
-  background: #6366F1;
-  border-radius: 12rpx;
-  padding: 24rpx;
-  text-align: center;
-}
-.upload-btn-text {
-  font-size: 30rpx;
-  color: #fff;
-  font-weight: 600;
-}
-.preview-header {
-  margin-bottom: 24rpx;
-}
-.preview-title {
-  font-size: 32rpx;
-  font-weight: 700;
-  color: #333;
-  display: block;
-}
-.preview-desc {
-  font-size: 24rpx;
-  color: #999;
-  margin-top: 8rpx;
-}
-.preview-card {
-  background: #fff;
-  border-radius: 16rpx;
-  padding: 24rpx;
-}
-.preview-row {
-  display: flex;
-  justify-content: space-between;
-  padding: 12rpx 0;
-  border-bottom: 1rpx solid #F3F4F6;
-}
-.preview-row:last-child {
-  border-bottom: none;
-}
-.preview-label {
-  font-size: 26rpx;
-  color: #666;
-}
-.preview-value {
-  font-size: 26rpx;
-  color: #333;
-  font-weight: 500;
-}
-.items-section {
-  margin-top: 24rpx;
-}
-.items-title {
-  font-size: 28rpx;
-  font-weight: 600;
-  color: #333;
-  margin-bottom: 16rpx;
-  display: block;
-}
-.item-card {
-  background: #fff;
-  border-radius: 12rpx;
-  padding: 20rpx;
-  margin-bottom: 12rpx;
-}
-.item-header {
-  display: flex;
-  justify-content: space-between;
-  margin-bottom: 12rpx;
-}
-.item-name {
-  font-size: 26rpx;
-  color: #333;
-  font-weight: 500;
-}
-.item-category {
-  font-size: 22rpx;
-  color: #999;
-  background: #F3F4F6;
-  padding: 4rpx 12rpx;
-  border-radius: 8rpx;
-}
-.item-value-bar {
-  height: 16rpx;
-  background: #F3F4F6;
-  border-radius: 8rpx;
-  overflow: hidden;
-}
-.item-value-fill {
-  height: 100%;
-  background: linear-gradient(90deg, #667EEA, #764BA2);
-  border-radius: 8rpx;
-}
-.item-value-text {
-  font-size: 24rpx;
-  color: #666;
-  margin-top: 8rpx;
-  display: block;
-  text-align: right;
-}
-.suggestions-card {
-  background: #FFFBEB;
-  border-radius: 16rpx;
-  padding: 24rpx;
-  margin-top: 24rpx;
-}
-.suggestions-title {
-  font-size: 28rpx;
-  font-weight: 600;
-  color: #92400E;
-  display: block;
-  margin-bottom: 12rpx;
-}
-.suggestions-text {
-  font-size: 26rpx;
-  color: #78350F;
-  line-height: 1.6;
-}
-.confirm-btn {
-  flex: 1;
-  background: #10B981;
-  border-radius: 12rpx;
-  padding: 24rpx;
-  text-align: center;
-}
-.confirm-btn.disabled {
-  opacity: 0.6;
-}
-.confirm-btn-text {
-  font-size: 30rpx;
-  color: #fff;
-  font-weight: 600;
-}
-.retry-btn {
-  padding: 24rpx 32rpx;
-  border-radius: 12rpx;
-  border: 2rpx solid #D1D5DB;
-  text-align: center;
-}
-.retry-btn-text {
-  font-size: 28rpx;
-  color: #666;
-}
-.success-card {
-  background: #fff;
-  border-radius: 16rpx;
-  padding: 60rpx 32rpx;
-  text-align: center;
-  margin-top: 120rpx;
-}
-.success-icon {
-  font-size: 80rpx;
-  display: block;
-  margin-bottom: 24rpx;
-}
-.success-title {
-  font-size: 36rpx;
-  font-weight: 700;
-  color: #10B981;
-  display: block;
-  margin-bottom: 12rpx;
-}
-.success-desc {
-  font-size: 28rpx;
-  color: #666;
-}
-.loading-overlay {
-  position: fixed;
-  top: 0; left: 0; right: 0; bottom: 0;
-  background: rgba(0,0,0,0.5);
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  z-index: 999;
-}
-.loading-box {
-  background: #fff;
-  border-radius: 16rpx;
-  padding: 48rpx 64rpx;
-}
-.loading-text {
-  font-size: 30rpx;
-  color: #333;
-}
-.purchase-section {
-  background: #F0F9FF;
-  border-radius: 16rpx;
-  padding: 24rpx;
-  margin-top: 24rpx;
-  border: 2rpx solid #BAE6FD;
-}
-.purchase-header {
-  display: flex;
-  flex-direction: row;
-  align-items: center;
-  margin-bottom: 16rpx;
-}
-.purchase-icon {
-  font-size: 28rpx;
-  margin-right: 8rpx;
-}
-.purchase-title {
-  font-size: 26rpx;
-  font-weight: bold;
-  color: #0369A1;
-}
-.purchase-scroll {
-  display: flex;
-  flex-direction: row;
-  white-space: nowrap;
-}
-.purchase-card {
-  display: inline-flex;
-  flex-direction: column;
-  width: 220rpx;
-  background: #fff;
-  border-radius: 12rpx;
-  padding: 16rpx;
-  margin-right: 16rpx;
-  flex-shrink: 0;
-}
-.purchase-cover {
-  width: 188rpx;
-  height: 120rpx;
-  border-radius: 8rpx;
-  background: #F1F5F9;
-  margin-bottom: 8rpx;
-}
-.purchase-name {
-  font-size: 22rpx;
-  color: #333;
-  line-height: 1.3;
-  overflow: hidden;
-  text-overflow: ellipsis;
-  white-space: nowrap;
-  margin-bottom: 4rpx;
-}
-.purchase-price {
-  font-size: 24rpx;
-  color: #EF4444;
-  font-weight: bold;
-  margin-bottom: 8rpx;
-}
-.purchase-btn {
-  background: #3B82F6;
-  border-radius: 8rpx;
-  padding: 8rpx 0;
-  text-align: center;
-}
-.purchase-btn-text {
-  font-size: 22rpx;
-  color: #fff;
-  font-weight: 500;
-}
-</style>

+ 205 - 552
cfc-frontend/pages/health/report-upload.vue

@@ -3,47 +3,30 @@
     <!-- 说明提示 -->
     <view class="tip-banner">
       <text class="tip-icon">📋</text>
-      <text class="tip-text">{{ tipText }}</text>
+      <text class="tip-text">上传体检/菌群/DAN测评等报告,后台自动识别类型并解析</text>
     </view>
 
-    <!-- 表单 -->
+    <!-- 上传方式 -->
     <view class="form-section">
-      <!-- 步骤1:报告大类 -->
-      <view class="form-item category-box">
-        <text class="form-label">选择报告类型</text>
-        <view class="category-row">
-          <view
-            :class="['category-card', reportKind === 'health' ? 'category-active' : '']"
-            @click="selectReportKind('health')">
-            <text class="category-icon">🏥</text>
-            <text class="category-title">健康报告</text>
-            <text class="category-desc">体检 / 菌群 / 舌诊 / 体脂</text>
-          </view>
-          <view
-            :class="['category-card', reportKind === 'dan' ? 'category-active category-dan' : '']"
-            @click="selectReportKind('dan')">
-            <text class="category-icon">🧠</text>
-            <text class="category-title">DAN测评</text>
-            <text class="category-desc">A2 / B4 等成长测评</text>
-          </view>
+      <view class="upload-methods">
+        <view class="upload-card card-chat" @click="pickFromChat">
+          <view class="upload-card-icon">💬</view>
+          <text class="upload-card-title">从聊天选取</text>
+          <text class="upload-card-desc">微信聊天中的 PDF/图片</text>
         </view>
-      </view>
-
-      <!-- 步骤2:DAN 维度(仅 DAN) -->
-      <view class="form-item" v-if="reportKind === 'dan'">
-        <text class="form-label">选择 DAN 测评维度</text>
-        <view class="dim-grid">
-          <view
-            v-for="item in danDimensions"
-            :key="item.code"
-            :class="['dim-chip', danDimension === item.code ? 'dim-active' : '']"
-            @click="selectDanDimension(item.code)">
-            <text class="dim-chip-text">{{ item.label }}</text>
-          </view>
+        <view class="upload-card card-photo" @click="takePhoto">
+          <view class="upload-card-icon">📷</view>
+          <text class="upload-card-title">拍照上传</text>
+          <text class="upload-card-desc">拍摄纸质报告或舌象</text>
+        </view>
+        <view class="upload-card card-folder" @click="pickFromFolder">
+          <view class="upload-card-icon">📁</view>
+          <text class="upload-card-title">从文件夹选取</text>
+          <text class="upload-card-desc">手机本地文件</text>
         </view>
       </view>
 
-      <!-- 采集中:上传成功后后台解析,页面需展示进度,避免看起来像卡住 -->
+      <!-- 采集中面板 -->
       <view class="collecting-panel" v-if="collecting">
         <view class="collecting-card">
           <view class="collecting-icon">
@@ -52,35 +35,22 @@
           </view>
           <view class="collecting-info">
             <text class="collecting-title">{{ collectError ? '采集失败' : '数据采集中' }}</text>
-            <text class="collecting-file">{{ collectFileName || '健康报告' }}</text>
-            <text class="collecting-hint">{{ collectError || '后台正在解析报告,完成后自动入库,无需重复上传' }}</text>
+            <text class="collecting-file">{{ collectFileName || '报告文件' }}</text>
+            <text class="collecting-hint">{{ collectError || '后台正在识别报告类型并解析,完成后自动入库' }}</text>
           </view>
         </view>
-        <view class="collecting-btn" @click="goReportList">去报告列表查看</view>
-      </view>
-
-      <!-- 上传方式 -->
-      <view class="upload-methods" v-if="reportKind && !collecting">
-        <view class="upload-card card-file" @click="pickFile">
-          <view class="upload-card-icon">📄</view>
-          <text class="upload-card-title">从聊天选择文件</text>
-          <text class="upload-card-desc" v-if="reportKind === 'health'">PDF / 图片<br/>体检、菌群等健康报告</text>
-          <text class="upload-card-desc" v-else>PDF<br/>DAN 测评报告(推荐)</text>
-        </view>
-        <view class="upload-card card-photo" @click="takePhoto" v-if="reportKind === 'health'">
-          <view class="upload-card-icon">📷</view>
-          <text class="upload-card-title">拍照上传</text>
-          <text class="upload-card-desc">拍摄舌象照片<br/>或单页纸质报告</text>
-        </view>
-        <view class="upload-card card-photo" @click="goBodyFatScale" v-if="reportKind === 'health'">
-          <view class="upload-card-icon">⚖️</view>
-          <text class="upload-card-title">体脂秤</text>
-          <text class="upload-card-desc">拍摄体脂秤<br/>显示屏照片</text>
+        <view class="collecting-actions">
+          <view class="collecting-btn" @click="goReportList">去报告列表查看</view>
+          <view class="collecting-cancel" @click="cancelCollect">取消</view>
         </view>
       </view>
 
-      <view class="kind-hint" v-if="!reportKind">
-        <text class="kind-hint-text">请先选择「健康报告」或「DAN测评」,再上传文件</text>
+      <!-- 上传中 -->
+      <view class="form-item" v-if="uploading">
+        <text class="form-label">上传中...</text>
+        <view class="analyzing-status">
+          <text class="analyzing-text">{{ uploadingText }}</text>
+        </view>
       </view>
 
       <!-- 责任声明 -->
@@ -95,23 +65,15 @@
           <text class="disclaimer-text">我同意平台使用本报告数据进行分析,为我提供个性化健康解决方案;未经我授权,平台不会以任何形式将我的报告内容提供给其他任何个人或机构。</text>
         </view>
       </view>
-
-      <!-- 上传中 -->
-      <view class="form-item" v-if="uploading">
-        <text class="form-label">上传中...</text>
-        <view class="analyzing-status">
-          <text class="analyzing-text">{{ uploadingText }}</text>
-        </view>
-      </view>
     </view>
 
-    <!-- 上传说明:随类型切换 -->
+    <!-- 上传说明 -->
     <view class="guide-section">
       <view class="guide-header">
         <text class="guide-header-icon">📖</text>
-        <text class="guide-header-title">{{ reportKind === 'dan' ? 'DAN 测评说明' : '健康报告说明' }}</text>
+        <text class="guide-header-title">支持的报告类型</text>
       </view>
-      <view class="guide-list" v-if="reportKind !== 'dan'">
+      <view class="guide-list">
         <view class="guide-item">
           <view class="guide-icon-box guide-icon-blue"><text class="guide-icon">📄</text></view>
           <view class="guide-info">
@@ -126,6 +88,13 @@
             <text class="guide-desc">支持 PDF 或清晰照片,AI 自动解读菌群健康</text>
           </view>
         </view>
+        <view class="guide-item">
+          <view class="guide-icon-box guide-icon-purple"><text class="guide-icon">🧠</text></view>
+          <view class="guide-info">
+            <text class="guide-name">DAN 测评报告</text>
+            <text class="guide-desc">A2/B4 等成长测评 PDF,自动解析维度数据</text>
+          </view>
+        </view>
         <view class="guide-item">
           <view class="guide-icon-box guide-icon-orange"><text class="guide-icon">👅</text></view>
           <view class="guide-info">
@@ -141,87 +110,26 @@
           </view>
         </view>
       </view>
-      <view class="guide-list" v-else>
-        <view class="guide-item">
-          <view class="guide-icon-box guide-icon-teal"><text class="guide-icon">💖</text></view>
-          <view class="guide-info">
-            <text class="guide-name">心维度 A2</text>
-            <text class="guide-desc">人格、情绪、自我概念与自驱力等心理测评报告</text>
-          </view>
-        </view>
-        <view class="guide-item">
-          <view class="guide-icon-box guide-icon-purple"><text class="guide-icon">🧠</text></view>
-          <view class="guide-info">
-            <text class="guide-name">智维度 B4</text>
-            <text class="guide-desc">认知能力、学习力等双培强基 DAN 智维度报告</text>
-          </view>
-        </view>
-        <view class="guide-item">
-          <view class="guide-icon-box guide-icon-blue"><text class="guide-icon">📚</text></view>
-          <view class="guide-info">
-            <text class="guide-name">其他 DAN 维度</text>
-            <text class="guide-desc">认知 / 学习 / 行为 / 生涯 / 校园 / 青春期等专项报告</text>
-          </view>
-        </view>
-      </view>
       <view class="guide-tips">
         <text class="guide-tips-title">温馨提示</text>
-        <text class="guide-tips-item">· 请先选择报告类型,再选择文件上传</text>
-        <text class="guide-tips-item">· 健康报告走菌群/体检解析;DAN 走测评维度解析</text>
+        <text class="guide-tips-item">· 上传后系统自动识别报告类型,无需手动选择</text>
+        <text class="guide-tips-item">· 请确保报告清晰完整,以便准确解析</text>
         <text class="guide-tips-item">· 确认入库后将自动生成成长方案,可再分解为每日任务</text>
       </view>
     </view>
-
-    <!-- 点击上传后的类型选择弹层(未预选时兜底) -->
-    <view class="sheet-mask" v-if="showKindSheet" @click="closeKindSheet">
-      <view class="sheet-panel" @click.stop>
-        <text class="sheet-title">请选择要上传的报告</text>
-        <view class="sheet-item" @click="onKindSheetPick('health')">
-          <text class="sheet-item-title">健康报告</text>
-          <text class="sheet-item-desc">体检、肠道菌群、舌诊、体脂等</text>
-        </view>
-        <view class="sheet-item" @click="onKindSheetPick('dan')">
-          <text class="sheet-item-title">DAN测评</text>
-          <text class="sheet-item-desc">A2 / B4 等成长测评 PDF</text>
-        </view>
-        <view class="sheet-cancel" @click="closeKindSheet">
-          <text class="sheet-cancel-text">取消</text>
-        </view>
-      </view>
-    </view>
   </view>
 </template>
 
 <script>
-import { uploadReportOnly, danParsePreview, getReportCollectStatus } from '../../utils/api.js'
-import config from '@/config.js'
-
-var DAN_DIMENSIONS = [
-  { code: 'mind', label: '心 A2' },
-  { code: 'wisdom', label: '智 B4' },
-  { code: 'cognition', label: '认知' },
-  { code: 'learning', label: '学习' },
-  { code: 'behavior', label: '行为' },
-  { code: 'career', label: '生涯' },
-  { code: 'campus', label: '校园' },
-  { code: 'adolescent', label: '青春期' }
-]
+import { uploadReportOnly, getReportCollectStatus } from '../../utils/api.js'
 
 export default {
   data() {
     return {
       memberId: null,
       uploading: false,
-      selectedFile: null,
-      photoPath: null,
       agreedToTerms: false,
-      currentMemberId: null,
-      extractedMetrics: [],
-      reportKind: '',
-      danDimension: 'mind',
-      danDimensions: DAN_DIMENSIONS,
-      showKindSheet: false,
-      pendingIsPhoto: false,
+      uploadingText: '正在上传文件...',
       collectDraftId: null,
       collectTimer: null,
       collecting: false,
@@ -230,53 +138,13 @@ export default {
       collectPollCount: 0
     }
   },
-  computed: {
-    tipText: function() {
-      if (this.reportKind === 'dan') {
-        return '上传 DAN 测评 PDF(A2/B4 等),解析确认后生成个性化成长方案'
-      }
-      if (this.reportKind === 'health') {
-        return '上传体检/菌群/舌诊等健康报告,解析确认后生成个性化健康方案'
-      }
-      return '请先选择「健康报告」或「DAN测评」,再上传文件生成方案'
-    },
-    uploadingText: function() {
-      return this.reportKind === 'dan'
-        ? '正在解析 DAN 测评报告...'
-        : '正在上传文件,即将进入分析页面...'
-    }
-  },
   onLoad: function(options) {
     this.memberId = options.memberId ? parseInt(options.memberId) : null
-    if (options.reportKind === 'health' || options.reportKind === 'dan') {
-      this.reportKind = options.reportKind
-    }
-    if (options.dimension) {
-      var codes = DAN_DIMENSIONS.map(function(d) { return d.code })
-      if (codes.indexOf(options.dimension) >= 0) {
-        this.danDimension = options.dimension
-        this.reportKind = 'dan'
-      }
-    }
-    if (options.bodyFatMetrics) {
-      try {
-        this.extractedMetrics = JSON.parse(decodeURIComponent(options.bodyFatMetrics))
-      } catch (e) {}
-    }
   },
   onUnload: function() {
     this.stopCollectPolling()
   },
   methods: {
-    selectReportKind: function(kind) {
-      this.reportKind = kind
-      if (kind === 'dan' && !this.danDimension) {
-        this.danDimension = 'mind'
-      }
-    },
-    selectDanDimension: function(code) {
-      this.danDimension = code
-    },
     toggleAgree: function() {
       this.agreedToTerms = !this.agreedToTerms
     },
@@ -299,115 +167,75 @@ export default {
         }
       })
     },
-    // 点击上传:未选类型时弹层;已选则直接走对应链路
-    pickFile: function() {
+    pickFromChat: function() {
       var self = this
       this.ensureAgreed(function() {
-        self.beginUpload(false)
+        uni.chooseMessageFile({
+          count: 1,
+          type: 'file',
+          extension: ['pdf'],
+          success: function(res) {
+            var files = res.tempFiles
+            if (files && files.length > 0) {
+              self.uploadReport(files[0].path, files[0].name || '聊天文件')
+            }
+          },
+          fail: function() {
+            // 用户取消或失败,静默处理
+          }
+        })
       })
     },
     takePhoto: function() {
       var self = this
       this.ensureAgreed(function() {
-        self.beginUpload(true)
-      })
-    },
-    beginUpload: function(isPhoto) {
-      if (!this.reportKind) {
-        this.pendingIsPhoto = isPhoto
-        this.showKindSheet = true
-        return
-      }
-      if (this.reportKind === 'dan') {
-        if (!this.danDimension) {
-          uni.showToast({ title: '请选择 DAN 测评维度', icon: 'none' })
-          return
-        }
-        this.startPickDan()
-        return
-      }
-      this.startPickHealth(isPhoto)
-    },
-    closeKindSheet: function() {
-      this.showKindSheet = false
-    },
-    onKindSheetPick: function(kind) {
-      this.reportKind = kind
-      this.showKindSheet = false
-      var isPhoto = this.pendingIsPhoto
-      this.pendingIsPhoto = false
-      if (kind === 'dan') {
-        if (isPhoto) {
-          uni.showToast({ title: 'DAN 请上传 PDF 文件', icon: 'none' })
-        }
-        this.startPickDan()
-        return
-      }
-      this.startPickHealth(isPhoto)
-    },
-    startPickHealth: function(isPhoto) {
-      var self = this
-      if (isPhoto) {
         uni.chooseImage({
           count: 1,
           sourceType: ['camera', 'album'],
           success: function(res) {
-            self.photoPath = res.tempFilePaths[0]
-            self.uploadHealthAndGoConfirm(self.photoPath, 'image')
+            if (res.tempFilePaths && res.tempFilePaths.length > 0) {
+              self.uploadReport(res.tempFilePaths[0], '照片')
+            }
           }
         })
-      } else {
+      })
+    },
+    pickFromFolder: function() {
+      var self = this
+      this.ensureAgreed(function() {
         uni.chooseMessageFile({
           count: 1,
           type: 'file',
-          extension: ['pdf'],
           success: function(res) {
             var files = res.tempFiles
             if (files && files.length > 0) {
-              self.selectedFile = files[0]
-              self.uploadHealthAndGoConfirm(files[0].path, '')
+              self.uploadReport(files[0].path, files[0].name || '本地文件')
             }
           },
-          fail: function(e) {
-            console.log('选择文件取消或失败', e)
+          fail: function() {
+            // 降级到相册
+            uni.chooseImage({
+              count: 1,
+              sourceType: ['album'],
+              success: function(res) {
+                if (res.tempFilePaths && res.tempFilePaths.length > 0) {
+                  self.uploadReport(res.tempFilePaths[0], '相册文件')
+                }
+              }
+            })
           }
         })
-      }
-    },
-    startPickDan: function() {
-      var self = this
-      uni.chooseMessageFile({
-        count: 1,
-        type: 'file',
-        extension: ['pdf'],
-        success: function(res) {
-          var files = res.tempFiles
-          if (files && files.length > 0) {
-            self.selectedFile = files[0]
-            self.uploadDanAndGoPreview(files[0].path)
-          }
-        },
-        fail: function() {
-          uni.chooseImage({
-            count: 1,
-            success: function(res) {
-              if (res.tempFiles && res.tempFiles.length > 0) {
-                self.selectedFile = res.tempFiles[0]
-                var path = res.tempFiles[0].path || (res.tempFilePaths && res.tempFilePaths[0])
-                self.uploadDanAndGoPreview(path)
-              }
-            }
-          })
-        }
       })
     },
-    // 健康报告:upload-only → 后台异步采集(指纹判定→脚本/opencode),完成后自动入库
-    uploadHealthAndGoConfirm: function(filePath, type) {
+    uploadReport: function(filePath, fileName) {
       if (!filePath) return
       var self = this
       self.uploading = true
+      self.uploadingText = '正在上传文件...'
       uni.showLoading({ title: '上传中...' })
-      uploadReportOnly(filePath, type).then(function(res) {
+      
+      // 不传 type,让后端通过指纹自动判断报告类型
+      uploadReportOnly(filePath).then(function(res) {
         uni.hideLoading()
         self.uploading = false
         var draftId = res.data && res.data.draftId
@@ -418,7 +246,7 @@ export default {
         self.collectDraftId = draftId
         self.collecting = true
         self.collectError = ''
-        self.collectFileName = (self.selectedFile && (self.selectedFile.name || self.selectedFile.path)) || '健康报告'
+        self.collectFileName = fileName || '报告文件'
         self.startCollectPolling(draftId)
       }).catch(function(err) {
         uni.hideLoading()
@@ -433,7 +261,6 @@ export default {
         this.collectTimer = null
       }
     },
-    // 轮询采集状态:立即查一次,再每 4 秒查,完成后跳转详情/列表
     startCollectPolling: function(draftId) {
       var self = this
       self.stopCollectPolling()
@@ -446,7 +273,7 @@ export default {
     pollCollectStatus: function(draftId) {
       var self = this
       self.collectPollCount = (self.collectPollCount || 0) + 1
-      // 约 6 分钟仍未完成:停轮询,提示去列表看进度(后台任务可能仍在跑)
+      // 约 6 分钟仍未完成:停轮询,提示去列表看进度
       if (self.collectPollCount > 90) {
         self.stopCollectPolling()
         self.collectError = '解析耗时较长,可去报告列表查看进度,无需重复上传'
@@ -463,10 +290,12 @@ export default {
           var reportType = data.reportType || 'gut_flora'
           setTimeout(function() {
             if (reportId) {
-              var url = reportType === 'gut_flora'
-                ? '/pages/health/gut-flora-detail?reportId=' + reportId
-                : '/pages/health/report-detail?reportType=' + reportType + '&reportId=' + reportId
-              uni.redirectTo({ url: url })
+              // 统一跳转到健康报告详情页(后端指纹识别后统一入库到 health_report 表)
+              if (reportType === 'gut_flora') {
+                uni.redirectTo({ url: '/pages/health/gut-flora-detail?reportId=' + reportId })
+              } else {
+                uni.redirectTo({ url: '/pages/health/report-detail?reportType=' + reportType + '&reportId=' + reportId })
+              }
             } else {
               uni.redirectTo({ url: '/pages/health/report-list' })
             }
@@ -484,91 +313,12 @@ export default {
       this.stopCollectPolling()
       uni.redirectTo({ url: '/pages/health/report-list' })
     },
-    // DAN测评:parse-preview → 预览确认页
-    uploadDanAndGoPreview: function(filePath) {
-      if (!filePath) return
-      var self = this
-      var memberId = self.memberId
-          || (self.$store.state && self.$store.state.currentChildId)
-          || uni.getStorageSync('currentChildId')
-          || uni.getStorageSync('currentMemberId')
-      if (!memberId) {
-        uni.showToast({ title: '请先选择家庭成员', icon: 'none' })
-        return
-      }
-      self.uploading = true
-      uni.showLoading({ title: '解析中...' })
-      danParsePreview(filePath, self.danDimension, memberId).then(function(res) {
-        uni.hideLoading()
-        self.uploading = false
-        if (!res || res.code !== 200 || !res.data) {
-          uni.showToast({ title: (res && res.message) || '解析失败', icon: 'none' })
-          return
-        }
-        try {
-          uni.setStorageSync('danUploadPreview', {
-            draftId: res.data.draftId || res.data.uploadId,
-            previewData: res.data,
-            dimension: self.danDimension,
-            memberId: memberId
-          })
-        } catch (e) {}
-        uni.redirectTo({
-          url: '/pages/dan-assessment/report-upload?resume=1&dimension=' + self.danDimension
-        })
-      }).catch(function(err) {
-        uni.hideLoading()
-        self.uploading = false
-        uni.showToast({ title: (err && err.message) || '上传失败', icon: 'none' })
-      })
-    },
-    goBodyFatScale: function() {
-      var self = this
-      this.ensureAgreed(function() {
-        self.reportKind = 'health'
-        self.startBodyFatUpload()
-      })
-    },
-    startBodyFatUpload: function() {
-      var self = this
-      uni.chooseImage({
-        count: 1,
-        sourceType: ['camera', 'album'],
-        success: function(res) {
-          self.photoPath = res.tempFilePaths[0]
-          self.uploadBodyFat(self.photoPath)
-        }
-      })
-    },
-    uploadBodyFat: function(filePath) {
-      var self = this
-      self.uploading = true
-      uni.showLoading({ title: 'AI 识别中...' })
-      var token = uni.getStorageSync('token')
-      uni.uploadFile({
-        url: config.API_BASE_URL + '/api/health/report/bodyfat-upload',
-        filePath: filePath,
-        name: 'file',
-        header: { 'Authorization': 'Bearer ' + token },
-        success: function(uploadRes) {
-          var data
-          try { data = JSON.parse(uploadRes.data) } catch (e) { data = uploadRes.data }
-          self.uploading = false
-          uni.hideLoading()
-          if (data && data.code === 200) {
-            self.currentMemberId = data.data && data.data.memberId
-            self.extractedMetrics = data.data && data.data.metrics || []
-            uni.navigateTo({ url: '/pages/health/bodyfat-confirm' })
-          } else {
-            uni.showToast({ title: (data && data.message) || '上传失败', icon: 'none' })
-          }
-        },
-        fail: function() {
-          self.uploading = false
-          uni.hideLoading()
-          uni.showToast({ title: '网络错误', icon: 'none' })
-        }
-      })
+    cancelCollect: function() {
+      this.stopCollectPolling()
+      this.collecting = false
+      this.collectDraftId = null
+      this.collectFileName = ''
+      this.collectError = ''
     }
   }
 }
@@ -578,6 +328,7 @@ export default {
 .upload-container {
   min-height: 100vh;
   background: #F5F7FA;
+  padding-bottom: env(safe-area-inset-bottom);
 }
 
 .tip-banner {
@@ -617,140 +368,77 @@ export default {
   margin-bottom: 16rpx;
 }
 
-.category-row {
+/* 上传方式卡片 */
+.upload-methods {
   display: flex;
   flex-direction: row;
   gap: 16rpx;
+  margin-bottom: 20rpx;
 }
-.category-card {
+.upload-card {
   flex: 1;
   display: flex;
   flex-direction: column;
   align-items: center;
-  padding: 28rpx 16rpx;
-  border-radius: 16rpx;
-  background: #F8FAFC;
-  border: 2rpx solid #E2E8F0;
-}
-.category-active {
-  background: #EFF6FF;
-  border-color: #3B82F6;
+  justify-content: center;
+  padding: 32rpx 12rpx;
+  border-radius: 20rpx;
+  text-align: center;
+  min-height: 200rpx;
 }
-.category-dan.category-active {
-  background: #F5F3FF;
-  border-color: #8B5CF6;
+.upload-card:active {
+  opacity: 0.85;
 }
-.category-icon {
+.upload-card-icon {
+  width: 80rpx;
+  height: 80rpx;
+  border-radius: 20rpx;
   font-size: 40rpx;
-  margin-bottom: 10rpx;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  margin-bottom: 16rpx;
 }
-.category-title {
-  font-size: 28rpx;
+.upload-card-title {
+  font-size: 26rpx;
   font-weight: 600;
-  color: #1E293B;
-  margin-bottom: 6rpx;
+  color: #333;
+  margin-bottom: 8rpx;
 }
-.category-desc {
+.upload-card-desc {
   font-size: 20rpx;
   color: #94A3B8;
-  text-align: center;
-}
-
-.dim-grid {
-  display: flex;
-  flex-direction: row;
-  flex-wrap: wrap;
-}
-.dim-chip {
-  width: 23%;
-  margin-right: 2%;
-  margin-bottom: 12rpx;
-  padding: 16rpx 0;
-  text-align: center;
-  border-radius: 12rpx;
-  background: #F1F5F9;
-  border: 2rpx solid transparent;
-  box-sizing: border-box;
-}
-.dim-active {
-  background: #EDE9FE;
-  border-color: #8B5CF6;
-}
-.dim-chip-text {
-  font-size: 22rpx;
-  color: #334155;
+  line-height: 1.5;
 }
 
-.kind-hint {
-  background: #FFFBEB;
-  border-radius: 12rpx;
-  padding: 20rpx;
-  margin-bottom: 20rpx;
-  border: 2rpx dashed #FCD34D;
+.card-chat {
+  background: #F0F7FF;
+  border: 2rpx dashed #93C5FD;
 }
-.kind-hint-text {
-  font-size: 24rpx;
-  color: #92400E;
+.card-chat .upload-card-icon {
+  background: linear-gradient(135deg, #3B82F6, #1D4ED8);
+  color: #fff;
 }
 
-.analyzing-status {
-  display: flex;
-  flex-direction: row;
-  align-items: center;
+.card-photo {
+  background: #FFF7ED;
+  border: 2rpx solid #FED7AA;
 }
-.analyzing-text {
-  font-size: 26rpx;
-  color: #5B9BD5;
+.card-photo .upload-card-icon {
+  background: linear-gradient(135deg, #FB923C, #EA580C);
+  color: #fff;
 }
 
-.disclaimer-box {
-  background: #FFF8F0;
-  border: 2rpx solid #FDE68A;
-}
-.disclaimer-check {
-  display: flex;
-  flex-direction: row;
-  align-items: center;
-  margin-bottom: 12rpx;
+.card-folder {
+  background: #F0FDF4;
+  border: 2rpx dashed #86EFAC;
 }
-.disclaimer-checkbox {
-  width: 36rpx;
-  height: 36rpx;
-  border-radius: 6rpx;
-  border: 2rpx solid #CBD5E1;
-  background: #fff;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  flex-shrink: 0;
-  margin-right: 12rpx;
-}
-.disclaimer-checked {
-  background: #5B9BD5;
-  border-color: #5B9BD5;
-}
-.disclaimer-checkmark {
-  font-size: 24rpx;
+.card-folder .upload-card-icon {
+  background: linear-gradient(135deg, #22C55E, #16A34A);
   color: #fff;
-  font-weight: bold;
-}
-.disclaimer-label {
-  font-size: 24rpx;
-  color: #333;
-  font-weight: 500;
-}
-.disclaimer-content {
-  background: #FFFDF5;
-  border-radius: 10rpx;
-  padding: 16rpx;
-  margin-left: 48rpx;
-}
-.disclaimer-text {
-  font-size: 22rpx;
-  color: #92400E;
-  line-height: 1.6;
 }
 
+/* 采集中面板 */
 .collecting-panel {
   margin-bottom: 20rpx;
 }
@@ -816,75 +504,95 @@ export default {
   margin-top: 8rpx;
   line-height: 1.5;
 }
+.collecting-actions {
+  display: flex;
+  flex-direction: row;
+  gap: 16rpx;
+  margin-top: 16rpx;
+}
 .collecting-btn {
-  margin-top: 20rpx;
-  height: 80rpx;
-  line-height: 80rpx;
+  flex: 1;
+  height: 72rpx;
+  line-height: 72rpx;
   text-align: center;
   background: #5B9BD5;
   color: #fff;
   font-size: 28rpx;
   border-radius: 12rpx;
 }
+.collecting-cancel {
+  padding: 0 24rpx;
+  height: 72rpx;
+  line-height: 72rpx;
+  text-align: center;
+  background: #fff;
+  border: 1rpx solid #D1D5DB;
+  color: #666;
+  font-size: 28rpx;
+  border-radius: 12rpx;
+}
 
-.upload-methods {
+/* 上传中状态 */
+.analyzing-status {
   display: flex;
   flex-direction: row;
-  gap: 16rpx;
-  margin-bottom: 20rpx;
+  align-items: center;
 }
-.upload-card {
-  flex: 1;
+.analyzing-text {
+  font-size: 26rpx;
+  color: #5B9BD5;
+}
+
+/* 责任声明 */
+.disclaimer-box {
+  background: #FFF8F0;
+  border: 2rpx solid #FDE68A;
+}
+.disclaimer-check {
   display: flex;
-  flex-direction: column;
+  flex-direction: row;
   align-items: center;
-  justify-content: center;
-  padding: 32rpx 12rpx;
-  border-radius: 20rpx;
-  text-align: center;
-  min-height: 220rpx;
-}
-.upload-card:active {
-  opacity: 0.85;
+  margin-bottom: 12rpx;
 }
-.upload-card-icon {
-  width: 80rpx;
-  height: 80rpx;
-  border-radius: 20rpx;
-  font-size: 40rpx;
+.disclaimer-checkbox {
+  width: 36rpx;
+  height: 36rpx;
+  border-radius: 6rpx;
+  border: 2rpx solid #CBD5E1;
+  background: #fff;
   display: flex;
   align-items: center;
   justify-content: center;
-  margin-bottom: 16rpx;
-}
-.upload-card-title {
-  font-size: 26rpx;
-  font-weight: 600;
-  color: #333;
-  margin-bottom: 8rpx;
-}
-.upload-card-desc {
-  font-size: 20rpx;
-  color: #94A3B8;
-  line-height: 1.5;
+  flex-shrink: 0;
+  margin-right: 12rpx;
 }
-.card-file {
-  background: #F0F7FF;
-  border: 2rpx dashed #93C5FD;
+.disclaimer-checked {
+  background: #5B9BD5;
+  border-color: #5B9BD5;
 }
-.card-file .upload-card-icon {
-  background: linear-gradient(135deg, #3B82F6, #1D4ED8);
+.disclaimer-checkmark {
+  font-size: 24rpx;
   color: #fff;
+  font-weight: bold;
 }
-.card-photo {
-  background: #FFF7ED;
-  border: 2rpx solid #FED7AA;
+.disclaimer-label {
+  font-size: 24rpx;
+  color: #333;
+  font-weight: 500;
 }
-.card-photo .upload-card-icon {
-  background: linear-gradient(135deg, #FB923C, #EA580C);
-  color: #fff;
+.disclaimer-content {
+  background: #FFFDF5;
+  border-radius: 10rpx;
+  padding: 16rpx;
+  margin-left: 48rpx;
+}
+.disclaimer-text {
+  font-size: 22rpx;
+  color: #92400E;
+  line-height: 1.6;
 }
 
+/* 上传说明 */
 .guide-section {
   margin: 0 30rpx 30rpx;
   background: #fff;
@@ -964,59 +672,4 @@ export default {
   display: block;
   line-height: 1.7;
 }
-
-.sheet-mask {
-  position: fixed;
-  left: 0;
-  right: 0;
-  top: 0;
-  bottom: 0;
-  background: rgba(15, 23, 42, 0.45);
-  z-index: 1000;
-  display: flex;
-  flex-direction: column;
-  justify-content: flex-end;
-}
-.sheet-panel {
-  background: #fff;
-  border-radius: 24rpx 24rpx 0 0;
-  padding: 32rpx 28rpx calc(32rpx + env(safe-area-inset-bottom));
-}
-.sheet-title {
-  font-size: 30rpx;
-  font-weight: 600;
-  color: #0F172A;
-  display: block;
-  text-align: center;
-  margin-bottom: 24rpx;
-}
-.sheet-item {
-  background: #F8FAFC;
-  border-radius: 16rpx;
-  padding: 28rpx 24rpx;
-  margin-bottom: 16rpx;
-}
-.sheet-item:active {
-  background: #EFF6FF;
-}
-.sheet-item-title {
-  font-size: 28rpx;
-  font-weight: 600;
-  color: #1E293B;
-  display: block;
-  margin-bottom: 6rpx;
-}
-.sheet-item-desc {
-  font-size: 22rpx;
-  color: #94A3B8;
-}
-.sheet-cancel {
-  margin-top: 8rpx;
-  padding: 24rpx;
-  text-align: center;
-}
-.sheet-cancel-text {
-  font-size: 28rpx;
-  color: #64748B;
-}
 </style>

+ 2 - 0
cfc-frontend/pages/invite/join.vue

@@ -153,6 +153,8 @@ export default {
       this.familyInviteCode = familyInviteCode
       // 存入本地,供登录页/个人信息页使用
       uni.setStorageSync('pendingFamilyInviteCode', familyInviteCode)
+      // 必须先初始化登录状态:已登录用户走"去完善信息"直达个人信息页,未登录才引导去登录
+      this.checkLoginStatus()
       this.validateFamilyInvite(familyInviteCode)
       return
     }

+ 2 - 2
cfc-frontend/pages/login/login.vue

@@ -66,7 +66,7 @@
 </template>
 
 <script>
-import { wechatPhoneLogin, checkTeamInviteStatus, joinTeamByInvite, bindReferral } from '../../utils/api.js'
+import { wechatPhoneLogin, checkTeamInviteStatus, joinTeamByInvite, bindReferralIfNotSelf } from '../../utils/api.js'
 
 export default {
   data() {
@@ -192,7 +192,7 @@ export default {
     },
     async handleReferralBind(inviteCode) {
       try {
-        var res = await bindReferral(inviteCode)
+        var res = await bindReferralIfNotSelf(inviteCode)
         if (res && res.code === 200) {
           console.log('登录后绑定邀请码成功', inviteCode)
         }

+ 1 - 1
cfc-frontend/pages/mind-detail/index.vue

@@ -964,7 +964,7 @@ var descList = descMap[this.dominantElement] || descMap.wood
       }).catch(function() {})
     },
     goDanUpload: function(dimension) {
-      uni.navigateTo({ url: '/pages/dan-assessment/report-upload' })
+      uni.navigateTo({ url: '/pages/health/report-upload' })
     },
     viewDanReport: function(report) {
       var reportId = report.sourceReportId || report.id

+ 9 - 5
cfc-frontend/pages/profile-extra/family-members.vue

@@ -6,7 +6,7 @@
         <text class="family-header-icon">🏠</text>
         <view class="family-header-text">
           <text class="family-name">{{ familyName || '我的家庭' }}</text>
-          <text class="family-member-count">{{ members.length }} 位成员</text>
+          <text class="family-member-count">{{ filteredMembers.length }} 位成员</text>
         </view>
       </view>
     </view>
@@ -31,8 +31,8 @@
     </view>
 
     <!-- ===== 成员卡片列表 ===== -->
-    <view class="member-list" v-if="members.length > 0">
-      <view class="member-card" v-for="m in members" :key="m.id" @click="showMemberDetail(m)">
+    <view class="member-list" v-if="filteredMembers.length > 0">
+      <view class="member-card" v-for="m in filteredMembers" :key="m.id" @click="showMemberDetail(m)">
         <image class="member-avatar" :src="m.avatar || '/static/default-avatar.png'" mode="aspectFill"></image>
         <view class="member-info">
           <text class="member-name">{{ m.nickname || '未设置' }}</text>
@@ -43,7 +43,7 @@
     </view>
 
     <!-- ===== 空状态 ===== -->
-    <view v-if="members.length === 0" class="empty-state">
+    <view v-if="filteredMembers.length === 0" class="empty-state">
       <text class="empty-icon">👨‍👩‍👧‍👦</text>
       <text class="empty-title">还没有家庭成员</text>
       <text class="empty-desc">邀请家人加入,一起记录成长</text>
@@ -156,8 +156,12 @@ export default {
     }
   },
   computed: {
+    // 列表过滤掉"我"(当前登录用户对应的成员),避免自己和自己互动
     filteredMembers() {
-      return this.members
+      var self = this
+      return (this.members || []).filter(function(m) {
+        return !(m && m.userId && m.userId === self.userId)
+      })
     },
     detailMemberScores() {
       if (!this.detailMember) return {}

+ 1 - 1
cfc-frontend/pages/tianpan/relation-detail.vue

@@ -402,7 +402,7 @@ export default {
 
     viewInteractionLog: function() {
       uni.navigateTo({
-        url: '/pages/family/interaction-log?memberId=' + this.memberId
+        url: '/pages/action-detail/interaction-log?memberId=' + this.memberId
       })
     }
   }

+ 42 - 25
cfc-frontend/pages/user-edit/user-edit.vue

@@ -43,7 +43,18 @@
 		</view>
 
 		<!-- 详细信息填写(选择角色后显示) -->
-		<view v-if="isEdit && (!isNewUserFlow || hasFamily)">
+		<view v-if="isEdit && (!isNewUserFlow || hasFamily)" :class="isEdit && form.role !== 'teacher' && isNewUserFlow ? 'user-form-wrap' : ''">
+			<!-- 家庭身份(接受家庭邀请时置顶显示,方便第一时间选择家庭身份) -->
+			<view class="family-section" v-if="isEdit && form.role === 'parent' && isNewUserFlow && hasPendingFamilyInvite">
+				<view class="section-title">👨‍👩‍👧‍👦 家庭身份(可选)</view>
+				<view class="form-item">
+					<text class="label">您在家庭中的身份</text>
+					<picker mode="selector" :range="familyRoleOptions" :value="familyRoleIndex" @change="onFamilyRoleChange">
+						<view class="picker-input">{{ form.familyRole || '请选择(可跳过)' }}</view>
+					</picker>
+				</view>
+				<text class="hint-text">不选也可先保存,后续在家庭页面发起申请</text>
+			</view>
 		<view class="form-section">
 			<!-- 头像 -->
 			<view class="form-item avatar-item">
@@ -219,18 +230,17 @@
 				</view>
 			</view>
 		</view>
-		<!-- 家庭角色选择(新用户流程、角色为家长、有家庭邀请码时显示,可选) -->
-		<view class="family-section" v-if="isEdit && form.role === 'parent' && isNewUserFlow && hasPendingFamilyInvite">
-			<view class="section-title">👨‍👩‍👧‍👦 家庭身份(可选)</view>
-			<view class="form-item">
-				<text class="label">您在家庭中的身份</text>
-				<picker mode="selector" :range="familyRoleOptions" :value="familyRoleIndex" @change="onFamilyRoleChange">
-					<view class="picker-input">{{ form.familyRole || '请选择(可跳过)' }}</view>
-				</picker>
+		<!-- 新用户流程:提交申请 / 跳过(接受邀请时按钮固定在页面底部悬浮) -->
+		<view class="family-section" v-if="isEdit && form.role !== 'teacher' && isNewUserFlow">
+			<view class="family-actions family-actions-fixed">
+				<button class="btn btn-primary" @click="saveUserInfo" :loading="loading">
+					{{ hasPendingFamilyInvite ? '提交申请' : '保存资料' }}
+				</button>
+				<button class="btn btn-skip" @click="skip">
+					跳过
+				</button>
 			</view>
-			<text class="hint-text">不选也可先保存,后续在家庭页面发起申请</text>
 		</view>
-
 		<!-- 免费用户提示(新用户首次注册信息完善时不展示,注册流程不推送会员) -->
 		<view class="family-section" v-if="isEdit && !isNewUserFlow && memberLevel === 'FREE'">
 			<view class="mascot-locked-hint" @click="goUpgrade">
@@ -240,18 +250,6 @@
 			</view>
 		</view>
 
-		<!-- 新用户流程:保存资料 / 跳过 -->
-		<view class="family-section" v-if="isEdit && form.role !== 'teacher' && isNewUserFlow">
-			<view class="family-actions">
-				<button class="btn btn-primary" @click="saveUserInfo" :loading="loading">
-					保存资料
-				</button>
-				<button class="btn btn-skip" @click="skip">
-					跳过
-				</button>
-			</view>
-		</view>
-
 		<!-- 修改个人资料保存按钮(非新用户流程时显示) -->
 		<view class="family-section" v-if="isEdit && !isNewUserFlow && form.role !== 'teacher'">
 			<view class="family-actions">
@@ -311,7 +309,7 @@
 </template>
 
 <script>
-import { updateUserInfo, createFamily, directRegister, getUserInfo, requestJoinByCode, getMyMembership, bindReferral } from '../../utils/api.js'
+import { updateUserInfo, createFamily, directRegister, getUserInfo, requestJoinByCode, getMyMembership, bindReferralIfNotSelf } from '../../utils/api.js'
 import config from '@/config.js'
 
 export default {
@@ -942,7 +940,7 @@ export default {
 			var inviteType = uni.getStorageSync('inviteType')
 			if (inviteCode && inviteType !== 'team') {
 				var self = this
-				bindReferral(inviteCode).then(function(res) {
+				bindReferralIfNotSelf(inviteCode).then(function(res) {
 					if (res && res.code === 200) {
 						console.log('新用户绑定推荐人成功', inviteCode)
 					}
@@ -1217,6 +1215,25 @@ export default {
 	margin-top: 30rpx;
 }
 
+/* 新用户流程(接受家庭邀请):提交申请/跳过固定悬浮页面底部,不随滚动消失 */
+.family-actions-fixed {
+	position: fixed;
+	left: 0;
+	right: 0;
+	bottom: 0;
+	z-index: 100;
+	margin-top: 0;
+	padding: 20rpx 32rpx;
+	padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
+	background: #ffffff;
+	box-shadow: 0 -4rpx 16rpx rgba(0, 0, 0, 0.06);
+}
+
+/* 新用户流程表单底部留白,避免内容被固定悬浮按钮遮挡 */
+.user-form-wrap {
+	padding-bottom: 340rpx;
+}
+
 .btn {
 	border-radius: 40rpx;
 	font-size: 30rpx;

+ 1 - 1
cfc-frontend/pages/wisdom-detail/index.vue

@@ -570,7 +570,7 @@ export default {
       uni.navigateTo({ url: '/pages/health/report-upload' })
     },
     goDanUpload: function() {
-      uni.navigateTo({ url: '/pages/dan-assessment/report-upload' })
+      uni.navigateTo({ url: '/pages/health/report-upload' })
     },
     viewDanReport: function(report) {
       var reportId = report.sourceReportId || report.id

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

@@ -1674,6 +1674,36 @@ export const bindReferral = (referralCode) => {
   return request('/api/invite/bind', 'POST', { referralCode })
 }
 
+// —— 自己的邀请码缓存(模块级,供 bindReferralIfNotSelf 使用)——
+let _myReferralCode = ''
+let _myReferralCodeReq = null
+
+// 获取/缓存自己的邀请码;请求失败允许下次重试
+function getMyReferralCodeCached() {
+  if (_myReferralCode) return Promise.resolve(_myReferralCode)
+  if (!_myReferralCodeReq) {
+    _myReferralCodeReq = getReferralCode().then(function(res) {
+      _myReferralCode = extractReferralCode(res)
+      return _myReferralCode
+    }).catch(function(e) {
+      _myReferralCodeReq = null
+      throw e
+    })
+  }
+  return _myReferralCodeReq
+}
+
+// 绑定推荐人(防"不能邀请自己"打扰):邀请码是自己的(如点击自己分享的文章/商品/活动链接、自己扫自己的推广码)
+// 则静默跳过,返回 { code: 200, skipped: true },不调后端、不弹业务提示;否则走真实 bindReferral
+export const bindReferralIfNotSelf = (referralCode) => {
+  return getMyReferralCodeCached().then(function(myCode) {
+    if (myCode && String(myCode).toLowerCase() === String(referralCode).toLowerCase()) {
+      return { code: 200, skipped: true }
+    }
+    return bindReferral(referralCode)
+  })
+}
+
 // 绑定家庭邀请人(首次注册时关联邀请家庭,仅新用户调用)
 export const bindFamilyInviter = (familyInviteCode) => {
   return request('/api/family/invite/bind-inviter', 'POST', { familyInviteCode })