Browse Source

feat(frontend): 报告详情页新增快速分析浮层(免费版单次/付费版多轮)

- import reportQuickAnalyze + getMyMembership from api.js
- data quickAnalyze 状态对象(show/sending/messages/inputText/conversationId/usedToday)
- computed quickQuestions(3 个预设快捷问题)
- methods: openQuickAnalyze/closeQuickAnalyze/sendQuickQuestion/sendQuickAnalyze
- 模板:悬浮 FAB 按钮(仅 view 模式)+ 浮层(消息区/chips/输入框)
- 样式:fixed 定位 FAB + overlay + panel,flexbox 布局
Xiaogang Liao 1 month ago
parent
commit
80e5cf1006
1 changed files with 210 additions and 2 deletions
  1. 210 2
      cfc-frontend/pages/health/report-detail.vue

+ 210 - 2
cfc-frontend/pages/health/report-detail.vue

@@ -392,12 +392,48 @@
         </view>
       </view>
     </view>
+
+    <!-- 报告快速分析入口(仅查看模式) -->
+    <view class="quick-analyze-fab" v-if="pageMode==='view' && reportId" @tap="openQuickAnalyze">
+      <text class="quick-analyze-fab-icon">🎯</text>
+      <text class="quick-analyze-fab-text">快速分析</text>
+    </view>
+
+    <!-- 快速分析浮层 -->
+    <view class="quick-analyze-overlay" v-if="quickAnalyze.show" @tap="closeQuickAnalyze"></view>
+    <view class="quick-analyze-panel" v-if="quickAnalyze.show">
+      <view class="quick-analyze-header">
+        <text class="quick-analyze-title">报告快速分析</text>
+        <text class="quick-analyze-close" @tap="closeQuickAnalyze">✕</text>
+      </view>
+      <view class="quick-analyze-hint" v-if="quickAnalyze.usedToday">免费版今日次数已用完,升级会员解锁更多分析</view>
+      <scroll-view class="quick-analyze-messages" scroll-y>
+        <view class="quick-analyze-message" v-for="(msg, qidx) in quickAnalyze.messages" :key="qidx" :class="'msg-' + msg.role">
+          <text class="quick-analyze-msg-text">{{ msg.content }}</text>
+        </view>
+        <view class="quick-analyze-loading" v-if="quickAnalyze.sending">
+          <text class="quick-analyze-loading-text">AI 正在分析中...</text>
+        </view>
+      </scroll-view>
+      <view class="quick-analyze-chips" v-if="quickAnalyze.messages.length === 0">
+        <view class="quick-analyze-chip" v-for="(q, qi) in quickQuestions" :key="qi" @tap="sendQuickQuestion(q)">
+          <text class="quick-analyze-chip-text">{{ q }}</text>
+        </view>
+      </view>
+      <view class="quick-analyze-input-bar">
+        <input class="quick-analyze-input" v-model="quickAnalyze.inputText" placeholder="输入你想问的问题..."
+          :disabled="quickAnalyze.sending || quickAnalyze.usedToday" confirm-type="send" @confirm="sendQuickAnalyze" />
+        <view class="quick-analyze-send" :class="{ 'send-disabled': quickAnalyze.sending || quickAnalyze.usedToday }" @tap="sendQuickAnalyze">
+          <text class="quick-analyze-send-text">发送</text>
+        </view>
+      </view>
+    </view>
   </view>
 </template>
 
 <script>
 import ReportBlocksRenderer from '../../components/report-blocks-renderer.vue'
-import { getReportDetail, getDanReportDetail, getReportPayload, editHealthReport, parseReportDraft, confirmReportPreview, discardReportDraft, confirmTongue, discardTongue, getFamilyMemberList, addFamilyMember, queryIndicatorKnowledge, queryBacteriaKnowledge } from '../../utils/api.js'
+import { getReportDetail, getDanReportDetail, getReportPayload, editHealthReport, parseReportDraft, confirmReportPreview, discardReportDraft, confirmTongue, discardTongue, getFamilyMemberList, addFamilyMember, queryIndicatorKnowledge, queryBacteriaKnowledge, reportQuickAnalyze, getMyMembership } from '../../utils/api.js'
 import { parseDate } from '../../utils/format.js'
 
 export default {
@@ -455,7 +491,16 @@ export default {
       collapseState: { gutFlora: false, diseaseRisk: false, probiotic: false, taxonomy: false, pathogen: false },
       showHelpPopup: false,
       helpData: {},
-      _kbCache: {}
+      _kbCache: {},
+      // 报告快速分析浮层
+      quickAnalyze: {
+        show: false,
+        sending: false,
+        messages: [],      // [{role:'user'|'ai', content}]
+        inputText: '',
+        conversationId: '',
+        usedToday: false    // 免费版今日已用(前端预判,最终以后端为准)
+      }
     }
   },
   computed: {
@@ -463,6 +508,13 @@ export default {
       var map = { gut_flora: '肠道菌群报告', dan: 'DAN测评报告', physical_exam: '体检报告', tongue: '舌诊报告' }
       return map[this.reportType] || '健康报告'
     },
+    quickQuestions: function() {
+      return [
+        '这份报告最需要关注哪些异常指标?',
+        '报告中提示了哪些健康风险?',
+        '有哪些改善建议?'
+      ]
+    },
     isGutFlora() {
       return this.reportType === 'gut_flora'
     },
@@ -585,6 +637,53 @@ export default {
     }
   },
   methods: {
+    // ===== 报告快速分析 =====
+    openQuickAnalyze: function() {
+      this.quickAnalyze.show = true
+      var self = this
+      // 预判免费版今日额度(仅用于 UI 展示,最终以后端校验为准)
+      getMyMembership().then(function(res) {
+        var level = res.data && res.data.memberLevel
+        if (level && level !== 'FREE') {
+          self.quickAnalyze.usedToday = false
+        }
+      }).catch(function() {})
+    },
+    closeQuickAnalyze: function() {
+      this.quickAnalyze.show = false
+    },
+    sendQuickQuestion: function(q) {
+      if (q && q.trim()) {
+        this.quickAnalyze.inputText = q
+        this.sendQuickAnalyze()
+      }
+    },
+    sendQuickAnalyze: function() {
+      var self = this
+      var text = (this.quickAnalyze.inputText || '').trim()
+      if (!text || this.quickAnalyze.sending) return
+      this.quickAnalyze.messages.push({ role: 'user', content: text })
+      this.quickAnalyze.inputText = ''
+      this.quickAnalyze.sending = true
+      reportQuickAnalyze(text, this.reportId, this.quickAnalyze.conversationId).then(function(res) {
+        if (res.code === 200 && res.data) {
+          self.quickAnalyze.messages.push({ role: 'ai', content: res.data.answer || '(无回答)' })
+          if (res.data.conversationId) {
+            self.quickAnalyze.conversationId = res.data.conversationId
+          }
+        } else {
+          var msg = res.message || '分析失败'
+          if (msg.indexOf('每日仅限') !== -1) {
+            self.quickAnalyze.usedToday = true
+          }
+          self.quickAnalyze.messages.push({ role: 'ai', content: msg })
+        }
+      }).catch(function() {
+        self.quickAnalyze.messages.push({ role: 'ai', content: 'AI 服务暂不可用,请稍后重试' })
+      }).finally(function() {
+        self.quickAnalyze.sending = false
+      })
+    },
     loadData() {
       var self = this
       uni.showLoading({ title: '加载中...' })
@@ -2188,4 +2287,113 @@ export default {
   margin-top: 6rpx;
   opacity: 0.8;
 }
+
+/* ===== 报告快速分析 ===== */
+.quick-analyze-fab {
+  position: fixed;
+  right: 30rpx;
+  bottom: 60rpx;
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  background: linear-gradient(135deg, #5B9BD5, #3B82F6);
+  border-radius: 44rpx;
+  padding: 18rpx 30rpx;
+  box-shadow: 0 4rpx 16rpx rgba(59, 130, 246, 0.4);
+  z-index: 90;
+}
+.quick-analyze-fab-icon { font-size: 32rpx; margin-right: 10rpx; }
+.quick-analyze-fab-text { font-size: 28rpx; color: #fff; font-weight: 500; }
+.quick-analyze-fab:active { opacity: 0.85; }
+
+.quick-analyze-overlay {
+  position: fixed;
+  top: 0; left: 0; right: 0; bottom: 0;
+  background: rgba(0, 0, 0, 0.45);
+  z-index: 98;
+}
+.quick-analyze-panel {
+  position: fixed;
+  left: 30rpx; right: 30rpx;
+  bottom: 40rpx;
+  background: #fff;
+  border-radius: 20rpx;
+  padding: 24rpx;
+  z-index: 99;
+  display: flex;
+  flex-direction: column;
+  max-height: 70vh;
+}
+.quick-analyze-header {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  justify-content: space-between;
+  margin-bottom: 16rpx;
+}
+.quick-analyze-title { font-size: 30rpx; font-weight: bold; color: #333; }
+.quick-analyze-close { font-size: 32rpx; color: #999; padding: 8rpx; }
+.quick-analyze-hint {
+  font-size: 24rpx;
+  color: #E65100;
+  background: #FFF3E0;
+  border-radius: 12rpx;
+  padding: 12rpx 16rpx;
+  margin-bottom: 12rpx;
+}
+.quick-analyze-messages {
+  max-height: 50vh;
+  margin-bottom: 16rpx;
+}
+.quick-analyze-message {
+  margin-bottom: 12rpx;
+  display: flex;
+  flex-direction: column;
+}
+.quick-analyze-message.msg-user { align-items: flex-end; }
+.quick-analyze-message.msg-ai { align-items: flex-start; }
+.quick-analyze-msg-text {
+  font-size: 26rpx;
+  color: #333;
+  background: #F3F4F6;
+  border-radius: 14rpx;
+  padding: 14rpx 18rpx;
+  max-width: 80%;
+  line-height: 1.5;
+}
+.msg-user .quick-analyze-msg-text { background: #5B9BD5; color: #fff; }
+.quick-analyze-loading { display: flex; justify-content: center; padding: 12rpx 0; }
+.quick-analyze-loading-text { font-size: 24rpx; color: #999; }
+.quick-analyze-chips { display: flex; flex-direction: column; margin-bottom: 16rpx; }
+.quick-analyze-chip {
+  background: #EFF6FF;
+  border: 1rpx solid #BFDBFE;
+  border-radius: 12rpx;
+  padding: 14rpx 18rpx;
+  margin-bottom: 10rpx;
+}
+.quick-analyze-chip:active { opacity: 0.8; }
+.quick-analyze-chip-text { font-size: 26rpx; color: #1D4ED8; }
+.quick-analyze-input-bar {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+}
+.quick-analyze-input {
+  flex: 1;
+  height: 72rpx;
+  background: #F3F4F6;
+  border-radius: 36rpx;
+  padding: 0 28rpx;
+  font-size: 26rpx;
+  color: #333;
+  margin-right: 16rpx;
+}
+.quick-analyze-send {
+  background: #5B9BD5;
+  border-radius: 36rpx;
+  padding: 16rpx 34rpx;
+}
+.quick-analyze-send.send-disabled { opacity: 0.5; }
+.quick-analyze-send-text { font-size: 26rpx; color: #fff; }
 </style>