gut-flora-detail.vue 62 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572
  1. <template>
  2. <view class="page">
  3. <scroll-view class="content" scroll-y>
  4. <!-- 报告头部 -->
  5. <view class="report-header">
  6. <text class="report-title">肠道菌群检测报告</text>
  7. <view class="report-meta">
  8. <text class="meta-item" v-if="report.personName">{{ report.personName }}</text>
  9. <text class="meta-item" v-if="report.gender">{{ report.gender }}</text>
  10. <text class="meta-item" v-if="report.age">{{ report.age }}岁</text>
  11. </view>
  12. <view class="report-meta">
  13. <text class="meta-item" v-if="report.reportDate">日期: {{ reportDateText }}</text>
  14. <text class="meta-item" v-if="report.reportNumber">编号: {{ report.reportNumber }}</text>
  15. </view>
  16. <view class="badge-row" v-if="report.gutAge || report.gutType || report.shannonIndex">
  17. <text class="badge" v-if="report.gutAge">肠龄: {{ report.gutAge }}</text>
  18. <text class="badge" v-if="report.gutType">肠型: {{ report.gutType }}</text>
  19. <text class="badge" v-if="report.shannonIndex">多样性指数: {{ report.shannonIndex }}</text>
  20. </view>
  21. <view class="rating-line" v-if="report.healthRating">
  22. <text class="rating-text">综合评级: {{ report.healthRating }}</text>
  23. </view>
  24. <!-- 报告解读 -->
  25. <view class="interpretation-box" v-if="reportInterpretation && reportInterpretation.gutAgeAssessment">
  26. <text class="interp-text">{{ reportInterpretation.gutAgeAssessment }}</text>
  27. </view>
  28. <!-- AI 解读 -->
  29. <view class="interpretation-box ai-box" v-if="aiAnalysisText">
  30. <text class="interp-label">🤖 AI 解读</text>
  31. <text class="interp-text">{{ aiAnalysisText }}</text>
  32. </view>
  33. <!-- 报告总结描述 -->
  34. <view class="summary-box" v-if="reportSummaryText">
  35. <text class="summary-title">报告总结</text>
  36. <text class="summary-text">{{ reportSummaryText }}</text>
  37. </view>
  38. </view>
  39. <!-- 评分圆环(折行显示,无横向滚动) -->
  40. <view class="scores-wrap">
  41. <view class="score-item" v-for="(score, idx) in scoreItems" :key="idx">
  42. <view class="score-ring" :style="'border-color: ' + score.color + '; color: ' + score.color">
  43. <text class="score-val">{{ score.value }}</text>
  44. </view>
  45. <text class="score-label">{{ score.label }}</text>
  46. </view>
  47. </view>
  48. <!-- 快速跳转 -->
  49. <view class="quick-nav">
  50. <view class="nav-card" @tap="goToSpecies">
  51. <text class="nav-icon">🦠</text>
  52. <text class="nav-label">菌群详情</text>
  53. <text class="nav-count">{{ floraAbnormalCount }}/{{ floraCount }}种</text>
  54. </view>
  55. <view class="nav-card" @tap="goToRisks">
  56. <text class="nav-icon">⚠️</text>
  57. <text class="nav-label">疾病风险</text>
  58. <text class="nav-count">{{ riskAttentionCount }}/{{ riskCount }}项</text>
  59. </view>
  60. <view class="nav-card" @tap="goToFoods">
  61. <text class="nav-icon">🍽️</text>
  62. <text class="nav-label">饮食推荐</text>
  63. <text class="nav-count">{{ foodCount }}项</text>
  64. </view>
  65. <view class="nav-card caution-card" @tap="goToCaution" v-if="cautionCount > 0">
  66. <text class="nav-icon">⚠️</text>
  67. <text class="nav-label">食材戒备</text>
  68. <text class="nav-count caution-count">{{ cautionCount }}项</text>
  69. </view>
  70. </view>
  71. <!-- 菌群功能分析 -->
  72. <view class="analysis-section" v-if="analysisData">
  73. <view class="section-title-bar">
  74. <text class="analysis-title">🧬 菌群功能分析</text>
  75. <text class="analysis-subtitle">基于知识库的功能维度解读</text>
  76. </view>
  77. <!-- 功能维度聚合 -->
  78. <view class="analysis-card">
  79. <text class="card-label">功能维度评估</text>
  80. <view class="func-dim" v-for="(dim, di) in analysisData.functionalAnalysis" :key="di">
  81. <view class="func-dim-head">
  82. <text class="func-dim-name">{{ dim.name }}</text>
  83. <text :class="'func-badge func-' + statusKey(dim.status)">{{ dim.status }}</text>
  84. </view>
  85. <view class="func-bar">
  86. <view class="func-bar-fill" :class="'fill-' + statusKey(dim.status)" :style="'width:' + dim.level + '%'"></view>
  87. </view>
  88. <text class="func-dim-summary">{{ dim.summary }}</text>
  89. </view>
  90. </view>
  91. <!-- P/B 比值 -->
  92. <view class="analysis-card" v-if="analysisData.pbRatio && analysisData.pbRatio.ratio !== undefined">
  93. <text class="card-label">饮食模式(P/B 比值)</text>
  94. <view class="pb-row">
  95. <text class="pb-label">Prevotella: {{ analysisData.pbRatio.prevotellaValue }}</text>
  96. <text class="pb-label">Bacteroides: {{ analysisData.pbRatio.bacteroidesValue }}</text>
  97. <text class="pb-ratio">比值 {{ analysisData.pbRatio.ratio }}</text>
  98. </view>
  99. <text class="func-dim-summary">{{ analysisData.pbRatio.interpretation }}</text>
  100. </view>
  101. <!-- 饮食模式推断 -->
  102. <view class="analysis-card" v-if="analysisData.dietaryPattern">
  103. <text class="card-label">饮食模式推断</text>
  104. <text class="pattern-name">{{ analysisData.dietaryPattern.pattern }}</text>
  105. <text class="func-dim-summary">{{ analysisData.dietaryPattern.detail }}</text>
  106. </view>
  107. <!-- 菌群风险分组手风琴(肥胖/抑郁/过敏等) -->
  108. <view class="accordion-section" v-if="analysisData && analysisData.riskIndicators && analysisData.riskIndicators.length > 0">
  109. <view class="accordion-header" @tap="toggleAccordion('riskGroup')">
  110. <text class="accordion-title">菌群风险分组</text>
  111. <text class="accordion-stats" v-if="analysisData.riskIndicators.length > 0">异常 <text class="abnormal-num">{{ riskGroupAbnormalCount }}</text> / 共 {{ analysisData.riskIndicators.length }} 组</text>
  112. <text class="accordion-arrow">{{ accordionOpen.riskGroup ? '收起' : '展开' }}</text>
  113. </view>
  114. <view class="accordion-body" v-if="accordionOpen.riskGroup">
  115. <view class="risk-group-item" v-for="(risk, ri) in analysisData.riskIndicators" :key="ri">
  116. <view class="risk-group-header" @tap="toggleRiskGroup(ri)">
  117. <text class="risk-group-name">{{ risk.name }}</text>
  118. <text :class="'risk-level risk-' + statusKey(risk.level)">{{ risk.level }}</text>
  119. <text class="risk-group-arrow">{{ riskGroupOpen[ri] ? '收起' : '展开' }}</text>
  120. </view>
  121. <view class="risk-group-body" v-if="riskGroupOpen[ri]">
  122. <text class="risk-bacteria" v-if="risk.relatedBacteria && risk.relatedBacteria.length > 0">
  123. 相关菌群:{{ risk.relatedBacteria.join('、') }}
  124. </text>
  125. <text class="risk-bacteria" v-else>暂无异常相关菌群</text>
  126. </view>
  127. </view>
  128. </view>
  129. </view>
  130. <!-- 益生菌指导 -->
  131. <view class="analysis-card" v-if="analysisData.probioticAdvice">
  132. <text class="card-label">益生菌指导</text>
  133. <view class="probiotic-item" v-for="(advice, key) in analysisData.probioticAdvice" :key="key">
  134. <text class="probiotic-name">{{ key === 'bifidobacterium' ? '双歧杆菌' : '乳杆菌' }}</text>
  135. <text :class="'func-badge func-' + statusKey(advice.status)">{{ advice.status }}</text>
  136. <text class="probiotic-advice">{{ advice.advice }}</text>
  137. </view>
  138. </view>
  139. </view>
  140. <!-- 肠道功能手风琴 -->
  141. <view class="accordion-section">
  142. <view class="accordion-header" @tap="toggleAccordion('gut')">
  143. <text class="accordion-title">肠道功能与代谢物</text>
  144. <text class="accordion-stats" v-if="gutStats.total > 0">异常 <text class="abnormal-num">{{ gutStats.abnormal }}</text> / 共 {{ gutStats.total }} 项</text>
  145. <text class="accordion-arrow">{{ accordionOpen.gut ? '收起' : '展开' }}</text>
  146. </view>
  147. <view class="accordion-body" v-if="accordionOpen.gut">
  148. <view class="indicator-card" v-for="(item, idx) in gutIndicators" :key="idx">
  149. <view class="indicator-row">
  150. <text class="ind-help-btn" @tap.stop="showKnowledge(indicatorType(item), item.indicatorName)">?</text>
  151. <text class="indicator-name" @tap="showKnowledge(indicatorType(item), item.indicatorName)">
  152. {{ item.indicatorName }}
  153. <text class="arrow-up" v-if="item.status === '偏高' || item.status === '过多'"> ↑</text>
  154. <text class="arrow-down" v-else-if="item.status === '偏低' || item.status === '缺乏' || item.status === '不足'"> ↓</text>
  155. <text class="arrow-warn" v-else-if="item.status === '异常'"> ⚠</text>
  156. </text>
  157. <view class="indicator-value" v-if="!isEditing">
  158. <text :class="'val-text val-' + (item._cssClass || 'normal')">{{ item.indicatorValue || '--' }}</text>
  159. <text class="indicator-unit" v-if="item.unit">{{ item.unit }}</text>
  160. </view>
  161. <view class="indicator-edit" v-if="isEditing">
  162. <input class="edit-input" type="text" v-model="item.indicatorValue" />
  163. <picker :value="indStatusIndex(item.status)" :range="indStatuses" @change="onIndStatusChange($event, item)">
  164. <text class="edit-picker">{{ item.status || '正常' }}</text>
  165. </picker>
  166. </view>
  167. </view>
  168. <text class="indicator-range" v-if="item.refRange">参考: {{ item.refRange }}</text>
  169. <text class="indicator-symptoms" v-if="item.symptoms">症状: {{ item.symptoms }}</text>
  170. </view>
  171. <view class="empty-hint" v-if="gutIndicators.length === 0">暂无数据</view>
  172. </view>
  173. </view>
  174. <!-- 营养分组手风琴(每个分组独立一级展示) -->
  175. <view class="nutrition-group-section" v-for="(group, gIdx) in nutritionGroups" :key="gIdx">
  176. <view class="accordion-section">
  177. <view class="accordion-header" @tap="toggleNutriGroup(gIdx)">
  178. <text class="accordion-title">{{ group.category }}</text>
  179. <text class="accordion-stats" v-if="group.items.length > 0">异常 <text class="abnormal-num">{{ group.abnormalCount }}</text> / 共 {{ group.items.length }} 项</text>
  180. <text class="accordion-arrow">{{ nutriGroupOpen[gIdx] ? '收起' : '展开' }}</text>
  181. </view>
  182. <view class="accordion-body" v-if="nutriGroupOpen[gIdx]">
  183. <view class="indicator-card" v-for="(item, idx) in group.items" :key="idx">
  184. <view class="indicator-row">
  185. <text class="ind-help-btn" @tap.stop="showKnowledge(indicatorType(item), item.indicatorName)">?</text>
  186. <text class="indicator-name" @tap="showKnowledge(indicatorType(item), item.indicatorName)">
  187. {{ item.indicatorName }}
  188. <text class="arrow-up" v-if="item.status === '偏高' || item.status === '过多'"> ↑</text>
  189. <text class="arrow-down" v-else-if="item.status === '偏低' || item.status === '缺乏' || item.status === '不足'"> ↓</text>
  190. <text class="arrow-warn" v-else-if="item.status === '异常'"> ⚠</text>
  191. </text>
  192. <view class="indicator-value" v-if="!isEditing">
  193. <text :class="'val-text val-' + (item._cssClass || 'normal')">{{ item.indicatorValue || '--' }}</text>
  194. <text class="indicator-unit" v-if="item.unit">{{ item.unit }}</text>
  195. </view>
  196. <view class="indicator-edit" v-if="isEditing">
  197. <input class="edit-input" type="text" v-model="item.indicatorValue" />
  198. <picker :value="indStatusIndex(item.status)" :range="indStatuses" @change="onIndStatusChange($event, item)">
  199. <text class="edit-picker">{{ item.status || '正常' }}</text>
  200. </picker>
  201. </view>
  202. </view>
  203. <text class="indicator-range" v-if="item.refRange">参考: {{ item.refRange }}</text>
  204. <text class="indicator-symptoms" v-if="item.symptoms">症状: {{ item.symptoms }}</text>
  205. </view>
  206. <view class="empty-hint" v-if="group.items.length === 0">暂无数据</view>
  207. </view>
  208. </view>
  209. </view>
  210. <view class="feedback-section" @tap="openQuickAnalyze">
  211. <text class="feedback-icon">💬</text>
  212. <text class="feedback-text">对这个报告有疑问或建议?告诉我们</text>
  213. <text class="feedback-arrow">›</text>
  214. </view>
  215. <view class="bottom-spacer"></view>
  216. </scroll-view>
  217. <!-- 快速分析浮层(由「有疑问或建议」入口唤起) -->
  218. <view class="quick-analyze-overlay" v-if="quickAnalyze.show" @tap="closeQuickAnalyze"></view>
  219. <view class="quick-analyze-panel" v-if="quickAnalyze.show">
  220. <view class="quick-analyze-header">
  221. <text class="quick-analyze-title">报告快速分析</text>
  222. <view class="quick-analyze-header-right">
  223. <text class="quick-analyze-feedback" @tap="openFeedbackFromAnalyze">提交反馈</text>
  224. <text class="quick-analyze-close" @tap="closeQuickAnalyze">✕</text>
  225. </view>
  226. </view>
  227. <view class="quick-analyze-hint" v-if="quickAnalyze.usedToday">免费版今日次数已用完,升级会员解锁更多分析</view>
  228. <scroll-view class="quick-analyze-messages" scroll-y>
  229. <view class="quick-analyze-message" v-for="(msg, qidx) in quickAnalyze.messages" :key="qidx" :class="'msg-' + msg.role">
  230. <text class="quick-analyze-msg-text">{{ msg.content }}</text>
  231. </view>
  232. <view class="quick-analyze-loading" v-if="quickAnalyze.sending">
  233. <text class="quick-analyze-loading-text">AI 正在分析中...</text>
  234. </view>
  235. </scroll-view>
  236. <view class="quick-analyze-chips" v-if="quickAnalyze.messages.length === 0">
  237. <view class="quick-analyze-chip" v-for="(q, qi) in quickQuestions" :key="qi" @tap="sendQuickQuestion(q)">
  238. <text class="quick-analyze-chip-text">{{ q }}</text>
  239. </view>
  240. </view>
  241. <view class="quick-analyze-input-bar">
  242. <input class="quick-analyze-input" v-model="quickAnalyze.inputText" placeholder="输入你想问的问题..."
  243. :disabled="quickAnalyze.sending || quickAnalyze.usedToday" confirm-type="send" @confirm="sendQuickAnalyze" />
  244. <view class="quick-analyze-send" :class="{ 'send-disabled': quickAnalyze.sending || quickAnalyze.usedToday }" @tap="sendQuickAnalyze">
  245. <text class="quick-analyze-send-text">发送</text>
  246. </view>
  247. </view>
  248. </view>
  249. <health-knowledge-popup :visible="knowledgeVisible" :data="knowledgeData" @close="closeKnowledge" />
  250. <feedback-popup :visible="feedbackVisible" :report-id="reportId" @close="closeFeedback" />
  251. <view class="fab" v-if="!isEditing && !isDraftMode" @tap="toggleEdit">
  252. <text class="fab-icon">✏️</text>
  253. </view>
  254. <view class="bottom-bar" v-if="isEditing">
  255. <view class="btn-cancel" @tap="cancelEdit">取消</view>
  256. <view class="btn-save" @tap="saveEdit">保存</view>
  257. </view>
  258. <!-- draft 模式:成员选择 -->
  259. <view class="member-select-wrap" v-if="isDraftMode && !confirming && !parsing">
  260. <view class="member-select-row" @tap="toggleMemberPanel">
  261. <text class="field-label">被检测人</text>
  262. <text class="field-value">{{ draftMemberText }}</text>
  263. <text class="member-select-arrow">{{ selectedMemberObj ? '修改 ›' : '选择 ›' }}</text>
  264. </view>
  265. <view class="member-panel" v-if="showMemberPanel">
  266. <!-- 候选匹配列表 -->
  267. <view class="candidate-section" v-if="candidates.length > 0">
  268. <text class="candidate-section-hint">以下成员与报告信息匹配,请选择:</text>
  269. <view
  270. class="candidate-item"
  271. v-for="(cand, idx) in candidates"
  272. :key="idx"
  273. :class="{ 'candidate-selected': selectedMemberId === (cand.id || cand.userId) }"
  274. @tap="selectAndCloseDraft(cand)"
  275. >
  276. <view class="candidate-left">
  277. <text class="candidate-avatar">{{ (cand.nickname || cand.name || '?').substring(0, 1) }}</text>
  278. <view class="candidate-info">
  279. <text class="candidate-name">{{ cand.nickname || cand.name || '未知' }}</text>
  280. <text class="candidate-detail" v-if="cand.gender || cand.age">{{ genderText(cand.gender) }}{{ cand.age ? ' · ' + cand.age + '岁' : '' }}</text>
  281. </view>
  282. </view>
  283. </view>
  284. </view>
  285. <!-- 全量成员 -->
  286. <view class="member-list" v-if="familyMembers.length > 0">
  287. <text class="member-list-hint">家庭成员</text>
  288. <view
  289. class="member-item"
  290. v-for="(member, mIdx) in familyMembers"
  291. :key="mIdx"
  292. :class="{ 'member-selected': selectedMemberId === (member.userId || member.id) }"
  293. @tap="selectAndCloseDraft(member)"
  294. >
  295. <text class="member-avatar">{{ (member.nickname || '?').substring(0, 1) }}</text>
  296. <text class="member-name">{{ member.nickname || '未知' }}</text>
  297. </view>
  298. </view>
  299. <!-- 不绑定成员 -->
  300. <view class="member-item unbind-member" @tap="unbindDraftMember">
  301. <text class="member-avatar unbind-avatar">✕</text>
  302. <text class="member-name">不绑定成员</text>
  303. </view>
  304. <!-- 新建成员入口 -->
  305. <view class="create-member-entry" @tap="toggleAutoCreateDraft">
  306. <text class="create-member-icon">+</text>
  307. <text class="create-member-text">{{ showAutoCreateForm ? '收起新建表单' : '不在列表中?新建家庭成员' }}</text>
  308. </view>
  309. <!-- 新建成员表单 -->
  310. <view class="auto-create-inline" v-if="showAutoCreateForm">
  311. <view class="form-item">
  312. <text class="form-item-label">姓名</text>
  313. <input class="form-item-input" v-model="newMember.name" placeholder="请输入姓名" />
  314. </view>
  315. <view class="form-item">
  316. <text class="form-item-label">性别</text>
  317. <view class="gender-selector">
  318. <text class="gender-option" :class="{ 'gender-selected': newMember.gender === 'male' }" @tap="newMember.gender = 'male'">男</text>
  319. <text class="gender-option" :class="{ 'gender-selected': newMember.gender === 'female' }" @tap="newMember.gender = 'female'">女</text>
  320. </view>
  321. </view>
  322. <view class="form-item">
  323. <text class="form-item-label">年龄</text>
  324. <input class="form-item-input" type="number" v-model="newMember.age" placeholder="请输入年龄" />
  325. </view>
  326. <view class="btn-create-submit" :class="{ 'btn-disabled': creatingMember }" @tap="doAutoCreateDraft">
  327. <text v-if="creatingMember">创建中...</text>
  328. <text v-else>确认创建</text>
  329. </view>
  330. </view>
  331. </view>
  332. </view>
  333. <view class="draft-bar" v-if="isDraftMode && !confirming && !parsing">
  334. <view class="btn-discard" @tap="doDiscard">放弃</view>
  335. <view class="btn-confirm" @tap="doConfirm">确认入库</view>
  336. </view>
  337. <view class="draft-bar" v-if="parsing">
  338. <view class="btn-parsing">
  339. <text class="parsing-text">AI 解析中...</text>
  340. </view>
  341. </view>
  342. <view class="draft-bar" v-if="confirming">
  343. <view class="btn-parsing">
  344. <text class="parsing-text">确认中...</text>
  345. </view>
  346. </view>
  347. </view>
  348. </template>
  349. <script>
  350. import { getReportDetail, editHealthReport, queryKnowledgeBase, queryIndicatorKnowledge, getGutFloraAnalysis, confirmReportPreview, discardReportDraft, parseReportDraft, getHealthFoods, getFoodCautionList, getFamilyMemberList, addFamilyMember, reportQuickAnalyze, getMyMembership } from '../../utils/api.js'
  351. import HealthKnowledgePopup from '../../components/health-knowledge-popup.vue'
  352. import FeedbackPopup from '../../components/feedback-popup.vue'
  353. import { parseDate } from '../../utils/format.js'
  354. var INDICATOR_TYPE_MAP = {
  355. '维生素': 'vitamin',
  356. '氨基酸': 'amino_acid',
  357. '微量元素': 'indicator',
  358. '肠道屏障与代谢物': 'indicator',
  359. '短链脂肪酸': 'indicator',
  360. '神经递质与激素': 'indicator',
  361. '抗生素风险评估': 'indicator'
  362. }
  363. export default {
  364. components: { HealthKnowledgePopup, FeedbackPopup },
  365. data() {
  366. return {
  367. reportId: null,
  368. reportData: null,
  369. report: {},
  370. reportInterpretation: null,
  371. aiAnalysisText: '',
  372. isEditing: false,
  373. accordionOpen: { gut: false, risks: false, riskGroup: false },
  374. riskGroupOpen: [],
  375. nutriGroupOpen: [],
  376. scoreItems: [],
  377. allIndicators: [],
  378. gutIndicators: [],
  379. nutritionGroups: [],
  380. diseaseRisks: [],
  381. floraCount: 0,
  382. floraAbnormalCount: 0,
  383. riskCount: 0,
  384. riskAttentionCount: 0,
  385. foodCount: 0,
  386. cautionCount: 0,
  387. indicatorCount: 0,
  388. knowledgeVisible: false,
  389. knowledgeData: {},
  390. feedbackVisible: false,
  391. indStatuses: ['正常', '偏低', '偏高', '缺乏', '不足', '过多', '异常'],
  392. analysisData: null,
  393. // draft 模式字段
  394. draftId: null,
  395. isDraftMode: false,
  396. payload: null,
  397. confirming: false,
  398. parsing: false,
  399. // 被检测人选择(draft 模式)
  400. needBind: false,
  401. selectedMemberId: null,
  402. selectedMemberObj: null,
  403. familyMembers: [],
  404. showMemberPanel: false,
  405. showAutoCreateForm: false,
  406. newMember: { name: '', gender: 'male', age: '' },
  407. creatingMember: false,
  408. createdMemberId: null,
  409. matchConfidence: null,
  410. matchLabel: '',
  411. candidates: [],
  412. extractedName: '',
  413. extractedGender: '',
  414. extractedAge: '',
  415. quickAnalyze: {
  416. show: false,
  417. sending: false,
  418. messages: [],
  419. inputText: '',
  420. conversationId: '',
  421. usedToday: false,
  422. checkingMembership: false
  423. }
  424. }
  425. },
  426. computed: {
  427. gutStats: function() {
  428. var list = this.gutIndicators || []
  429. var valid = 0, abnormal = 0
  430. for (var i = 0; i < list.length; i++) {
  431. var item = list[i]
  432. if (item.indicatorValue && String(item.indicatorValue).trim() !== '') valid++
  433. if (this.isAbnormalStatus(item.status)) abnormal++
  434. }
  435. return { total: list.length, valid: valid, abnormal: abnormal }
  436. },
  437. nutriStats: function() {
  438. var list = [], groups = this.nutritionGroups || []
  439. for (var g = 0; g < groups.length; g++) {
  440. var items = groups[g].items
  441. if (items) for (var i = 0; i < items.length; i++) list.push(items[i])
  442. }
  443. var valid = 0, abnormal = 0
  444. for (var i = 0; i < list.length; i++) {
  445. var item = list[i]
  446. if (item.indicatorValue && String(item.indicatorValue).trim() !== '') valid++
  447. if (this.isAbnormalStatus(item.status)) abnormal++
  448. }
  449. return { total: list.length, valid: valid, abnormal: abnormal }
  450. },
  451. diseaseRisksAbnormal: function() {
  452. var list = this.diseaseRisks || []
  453. var count = 0
  454. for (var i = 0; i < list.length; i++) {
  455. var level = list[i].riskLevel || ''
  456. if (level !== '低风险') count++
  457. }
  458. return count
  459. },
  460. riskGroupAbnormalCount: function() {
  461. if (!this.analysisData || !this.analysisData.riskIndicators) return 0
  462. var list = this.analysisData.riskIndicators
  463. var count = 0
  464. for (var i = 0; i < list.length; i++) {
  465. var lv = list[i].level || ''
  466. if (lv !== '良好' && lv !== '正常') count++
  467. }
  468. return count
  469. },
  470. /** 报告日期格式化 — 计算属性避免模板中方法引用失效 */
  471. reportDateText: function() {
  472. var d = this.report.reportDate
  473. if (!d) return ''
  474. var date = parseDate(d)
  475. if (!date) return String(d).substring(0, 10)
  476. var y = date.getFullYear()
  477. var m = ('' + (date.getMonth() + 1)).padStart(2, '0')
  478. var day = ('' + date.getDate()).padStart(2, '0')
  479. return y + '-' + m + '-' + day
  480. },
  481. /** 报告总结描述 */
  482. reportSummaryText: function() {
  483. var parts = []
  484. if (this.report.healthRating) parts.push('综合评级: ' + this.report.healthRating)
  485. if (this.reportInterpretation && this.reportInterpretation.gutAgeAssessment)
  486. parts.push(this.reportInterpretation.gutAgeAssessment)
  487. if (this.analysisData && this.analysisData.dietaryPattern && this.analysisData.dietaryPattern.detail)
  488. parts.push(this.analysisData.dietaryPattern.detail)
  489. var funcAnalysis = this.analysisData && this.analysisData.functionalAnalysis || []
  490. var abnDims = funcAnalysis.filter(function(d) { return d.status && d.status !== '良好' })
  491. if (abnDims.length > 0)
  492. parts.push('需关注维度:' + abnDims.map(function(d) { return d.name }).join('、'))
  493. return parts.join(';')
  494. },
  495. draftMemberText: function() {
  496. if (this.selectedMemberObj) return this.selectedMemberObj.nickname || this.selectedMemberObj.name || '已选择'
  497. if (this.extractedName) return this.extractedName + '(待绑定)'
  498. return '未选择'
  499. },
  500. quickQuestions: function() {
  501. return [
  502. '这份报告最需要关注哪些异常指标?',
  503. '报告中提示了哪些健康风险?',
  504. '有哪些改善建议?'
  505. ]
  506. }
  507. },
  508. onLoad: function(options) {
  509. this.reportId = options.reportId ? parseInt(options.reportId) : null
  510. this.draftId = options.draftId ? parseInt(options.draftId) : null
  511. if (this.draftId) {
  512. this.isDraftMode = true
  513. this.loadDraftData()
  514. } else if (this.reportId) {
  515. this.loadData()
  516. }
  517. },
  518. methods: {
  519. // ===== 报告快速分析 =====
  520. openQuickAnalyze: function() {
  521. this.quickAnalyze.show = true
  522. var self = this
  523. if (self.quickAnalyze.checkingMembership) return
  524. self.quickAnalyze.checkingMembership = true
  525. getMyMembership().then(function(res) {
  526. var level = res.data && res.data.memberLevel
  527. if (level && level !== 'FREE') {
  528. self.quickAnalyze.usedToday = false
  529. }
  530. }).catch(function() {}).finally(function() {
  531. self.quickAnalyze.checkingMembership = false
  532. })
  533. },
  534. closeQuickAnalyze: function() {
  535. this.quickAnalyze.show = false
  536. },
  537. sendQuickQuestion: function(q) {
  538. if (q && q.trim()) {
  539. this.quickAnalyze.inputText = q
  540. this.sendQuickAnalyze()
  541. }
  542. },
  543. sendQuickAnalyze: function() {
  544. var self = this
  545. var text = (this.quickAnalyze.inputText || '').trim()
  546. if (!text || this.quickAnalyze.sending) return
  547. this.quickAnalyze.messages.push({ role: 'user', content: text })
  548. this.quickAnalyze.inputText = ''
  549. this.quickAnalyze.sending = true
  550. reportQuickAnalyze(text, this.reportId, this.quickAnalyze.conversationId, this.draftId).then(function(res) {
  551. if (res.code === 200 && res.data) {
  552. self.quickAnalyze.messages.push({ role: 'ai', content: res.data.answer || '(无回答)' })
  553. if (res.data.conversationId) {
  554. self.quickAnalyze.conversationId = res.data.conversationId
  555. }
  556. } else {
  557. var msg = res.message || '分析失败'
  558. if (msg.indexOf('每日仅限') !== -1) {
  559. self.quickAnalyze.usedToday = true
  560. }
  561. self.quickAnalyze.messages.push({ role: 'ai', content: msg })
  562. }
  563. }).catch(function() {
  564. self.quickAnalyze.messages.push({ role: 'ai', content: 'AI 服务暂不可用,请稍后重试' })
  565. }).finally(function() {
  566. self.quickAnalyze.sending = false
  567. })
  568. },
  569. goBack: function() { uni.navigateBack() },
  570. /** 状态 → CSS安全英文(兼容中英文,与 report-confirm 保持一致) */
  571. _cssForStatus: function(s) {
  572. var k = String(s || '').trim()
  573. var map = { 'high': 'high', 'low': 'low', 'abnormal': 'abnormal', 'normal': 'normal', '缺乏': 'deficient', '偏低': 'low', '不足': 'insufficient', '正常': 'normal', '注意': 'warning', '偏高': 'high', '异常': 'abnormal', '过多': 'excess', '高风险': 'high-risk', '低风险': 'low-risk', '未检出': 'not-detected' }
  574. return map[k] || 'normal'
  575. },
  576. // 与 _cssForStatus 保持同一白名单:仅明确异常/需关注的状态计入异常数,未知状态视为正常,避免“样式正常却计入异常”
  577. isAbnormalStatus: function(status) {
  578. if (!status) return false
  579. var k = String(status).trim()
  580. var abnormalMap = { 'high': 1, 'low': 1, 'abnormal': 1, '偏高': 1, '偏低': 1, '异常': 1, '缺乏': 1, '不足': 1, '注意': 1, '高风险': 1, '过多': 1 }
  581. return !!abnormalMap[k]
  582. },
  583. statusKey: function(s) {
  584. var map = { '良好': 'good', '注意': 'warning', '需关注': 'danger', '关注': 'danger', '正常': 'normal', '偏高': 'high', '偏低': 'low' }
  585. return map[s] || 'normal'
  586. },
  587. genderText: function(g) {
  588. if (g === 'male') return '男'
  589. if (g === 'female') return '女'
  590. return g || ''
  591. },
  592. toggleMemberPanel: function() {
  593. this.showMemberPanel = !this.showMemberPanel
  594. if (this.showMemberPanel && this.familyMembers.length === 0) {
  595. this.loadFamilyMembers()
  596. }
  597. },
  598. loadFamilyMembers: function() {
  599. var self = this
  600. getFamilyMemberList({ visibleOnly: true }).then(function(res) {
  601. if (res.code === 200) {
  602. self.familyMembers = res.data || []
  603. }
  604. }).catch(function() {})
  605. },
  606. selectAndCloseDraft: function(member) {
  607. this.selectedMemberId = member.id || member.userId
  608. this.selectedMemberObj = member
  609. this.createdMemberId = null
  610. this.needBind = false
  611. this.showMemberPanel = false
  612. this.showAutoCreateForm = false
  613. },
  614. unbindDraftMember: function() {
  615. this.selectedMemberId = null
  616. this.selectedMemberObj = null
  617. this.createdMemberId = null
  618. this.needBind = false
  619. this.showMemberPanel = false
  620. this.showAutoCreateForm = false
  621. },
  622. toggleAutoCreateDraft: function() {
  623. this.showAutoCreateForm = !this.showAutoCreateForm
  624. },
  625. doAutoCreateDraft: function() {
  626. var self = this
  627. if (!self.newMember.name) {
  628. uni.showToast({ title: '请填写姓名', icon: 'none' })
  629. return
  630. }
  631. self.creatingMember = true
  632. addFamilyMember({
  633. name: self.newMember.name,
  634. gender: self.newMember.gender,
  635. age: self.newMember.age ? parseInt(self.newMember.age) : null
  636. }).then(function(res) {
  637. self.creatingMember = false
  638. if (res.code === 200) {
  639. var member = res.data || {}
  640. self.selectedMemberId = member.id || member.userId
  641. self.selectedMemberObj = member
  642. self.createdMemberId = self.selectedMemberId
  643. self.needBind = false
  644. self.showMemberPanel = false
  645. self.showAutoCreateForm = false
  646. self.loadFamilyMembers()
  647. uni.showToast({ title: '创建成功', icon: 'success' })
  648. } else {
  649. uni.showToast({ title: res.message || '创建失败', icon: 'none' })
  650. }
  651. }).catch(function() {
  652. self.creatingMember = false
  653. uni.showToast({ title: '创建异常', icon: 'none' })
  654. })
  655. },
  656. loadData: function() {
  657. var self = this
  658. getReportDetail(this.reportId).then(function(res) {
  659. if (res.code === 200 && res.data) {
  660. self.reportData = res.data
  661. var reportObj = res.data.report || res.data
  662. // HealthReport 实体无 gender/age 字段,从 payload.summary 补齐(采集到但未落库)
  663. var ps = (res.data.payload && res.data.payload.summary) || {}
  664. self.report = Object.assign({}, reportObj, {
  665. gender: reportObj.gender || ps.gender || '',
  666. age: reportObj.age != null ? reportObj.age : (ps.age != null ? ps.age : '')
  667. })
  668. self.reportInterpretation = res.data.reportInterpretation || null
  669. // 解析 AI 解读(aiAnalysis 为 JSON 字符串 {analysis, traceId, ...})
  670. var aiRaw = reportObj.aiAnalysis
  671. if (aiRaw) {
  672. try {
  673. var aiObj = typeof aiRaw === 'string' ? JSON.parse(aiRaw) : aiRaw
  674. self.aiAnalysisText = (aiObj && aiObj.analysis) || ''
  675. } catch (e) {
  676. self.aiAnalysisText = ''
  677. }
  678. }
  679. self.buildScores(res.data)
  680. self.buildIndicators(res.data)
  681. self.floraCount = (res.data.gutFlora || []).length
  682. // 菌群详情:统计异常/需关注的数量(白名单,与手风琴异常口径一致)
  683. var flora = res.data.gutFlora || []
  684. self.floraAbnormalCount = flora.filter(function(i) {
  685. return self.isAbnormalStatus(i.status)
  686. }).length
  687. self.riskCount = (res.data.diseaseRisks || []).length
  688. // 疾病风险:内联展示 + 颜色区分,统计需关注的数量(风险等级非"低风险"即视为需关注)
  689. var risks = res.data.diseaseRisks || []
  690. self.diseaseRisks = risks.map(function(r) {
  691. r._cssClass = self._cssForRiskLevel(r.riskLevel)
  692. return r
  693. })
  694. self.riskAttentionCount = risks.filter(function(r) {
  695. return r.riskLevel && r.riskLevel !== '低风险'
  696. }).length
  697. // 饮食推荐:共用 /api/health/foods 接口获取数量
  698. self.foodCount = 0
  699. getHealthFoods(self.reportId).then(function(foodRes) {
  700. if (foodRes && foodRes.code === 200 && foodRes.data) {
  701. self.foodCount = (Array.isArray(foodRes.data) ? foodRes.data : []).length
  702. }
  703. }).catch(function() {})
  704. // 异步加载戒备食材数量(按报告 subjectId 查,家长 userId 查不到孩子的推荐指数)
  705. var cautionUserId = (self.report && self.report.subjectId) || uni.getStorageSync('userId')
  706. if (cautionUserId) {
  707. getFoodCautionList(cautionUserId).then(function(cautionRes) {
  708. if (cautionRes && cautionRes.code === 200) {
  709. self.cautionCount = (cautionRes.data || []).length
  710. }
  711. }).catch(function() {})
  712. }
  713. self.indicatorCount = (res.data.indicators || []).length
  714. }
  715. })
  716. // 加载菌群功能分析
  717. this.loadAnalysis()
  718. },
  719. loadAnalysis: function() {
  720. var self = this
  721. getGutFloraAnalysis(this.reportId).then(function(res) {
  722. if (res.code === 200 && res.data && !res.data.error) {
  723. self.analysisData = res.data
  724. }
  725. }).catch(function() {})
  726. },
  727. loadDraftData: function() {
  728. var self = this
  729. self.parsing = true
  730. parseReportDraft(self.draftId).then(function(res) {
  731. self.parsing = false
  732. if (res.code !== 200 || !res.data) {
  733. uni.showToast({ title: res.message || '加载失败', icon: 'none' })
  734. return
  735. }
  736. self.payload = res.data.payload || {}
  737. self.isDraftMode = true
  738. // 填充成员匹配信息(后端解析时已按姓名/性别/年龄尝试匹配家庭成员)
  739. self.extractedName = res.data.extractedName || ''
  740. self.extractedGender = res.data.extractedGender || ''
  741. self.extractedAge = res.data.extractedAge || ''
  742. self.matchedMemberId = res.data.matchedMemberId || null
  743. self.needBind = !!res.data.needBind
  744. self.matchConfidence = res.data.confidence !== undefined ? parseFloat(res.data.confidence) : null
  745. self.matchLabel = res.data.matchLabel || ''
  746. self.candidates = res.data.candidates || []
  747. // 高置信度自动选中
  748. if (self.matchedMemberId && self.matchConfidence >= 0.8) {
  749. self.selectedMemberId = self.matchedMemberId
  750. // 在 candidates 中找对应对象
  751. for (var i = 0; i < self.candidates.length; i++) {
  752. if ((self.candidates[i].id || self.candidates[i].userId) === self.selectedMemberId) {
  753. self.selectedMemberObj = self.candidates[i]
  754. break
  755. }
  756. }
  757. }
  758. self.applyDraftPayload(self.payload)
  759. self.loadFamilyMembers()
  760. }).catch(function() {
  761. self.parsing = false
  762. uni.showToast({ title: '加载异常', icon: 'none' })
  763. })
  764. },
  765. applyDraftPayload: function(payload) {
  766. // 将 draft payload 映射到模板所需的数据结构(report/scoreItems/indicators)
  767. var report = {}
  768. if (payload.summary) {
  769. var s = payload.summary
  770. report.personName = s.personName || ''
  771. report.gender = s.gender || ''
  772. report.age = s.age != null ? s.age : ''
  773. report.reportNumber = s.reportNumber || ''
  774. report.gutAge = s.gutAge || ''
  775. report.gutType = s.gutType || ''
  776. report.shannonIndex = s.shannonIndex || ''
  777. report.reportDate = s.reportDate || ''
  778. report.overallScore = s.overallScore
  779. report.gutHealthScore = s.gutHealthScore
  780. report.chronicDiseaseScore = s.chronicDiseaseScore
  781. report.nutritionScore = s.nutritionScore
  782. report.balanceScore = s.balanceScore
  783. report.diversityScore = s.diversityScore
  784. report.beneficialScore = s.beneficialScore
  785. report.harmfulScore = s.harmfulScore
  786. report.coreGenusScore = s.coreGenusScore
  787. }
  788. this.report = report
  789. this.buildScores({ report: report })
  790. this.buildDraftIndicators(payload.indicators || [])
  791. },
  792. buildDraftIndicators: function(indicators) {
  793. // 复用 buildIndicators 的分组逻辑,输入为 payload.indicators
  794. var all = []
  795. for (var i = 0; i < indicators.length; i++) {
  796. var ind = indicators[i]
  797. var item = {
  798. indicatorName: ind.indicatorName || ind.name || '',
  799. indicatorValue: ind.indicatorValue || ind.value || '',
  800. status: ind.status || '正常',
  801. refRange: ind.refRange || '',
  802. category: ind.category || '其他'
  803. }
  804. item._cssClass = this._cssForStatus(item.status)
  805. all.push(item)
  806. }
  807. this.allIndicators = all
  808. this.indicatorCount = all.length
  809. this._groupIndicators(all)
  810. },
  811. _groupIndicators: function(all) {
  812. var self = this
  813. var gut = []
  814. var nutriGroups = []
  815. var nutriMap = {}
  816. for (var i = 0; i < all.length; i++) {
  817. var ind = all[i]
  818. var g = ind.category
  819. var isGutBarrier = g === '肠道屏障与代谢物' || g === '短链脂肪酸' ||
  820. g === '神经递质与激素' || g === '抗生素风险评估'
  821. if (isGutBarrier) {
  822. gut.push(ind)
  823. } else {
  824. if (!nutriMap[g]) nutriMap[g] = []
  825. nutriMap[g].push(ind)
  826. }
  827. }
  828. this.gutIndicators = gut
  829. for (var key in nutriMap) {
  830. nutriGroups.push({ category: key, items: nutriMap[key] })
  831. }
  832. this.nutritionGroups = nutriGroups
  833. },
  834. buildScores: function(data) {
  835. // 兼容两套字段名:实体存 balanceScore/diversityScore 等,旧视图读 gutBalanceScore 等
  836. var r = data.report || data
  837. var s = r.summary || r
  838. var items = []
  839. var scoreFields = [
  840. { key: 'overallScore', label: '综合', color: '#4A9BD7' },
  841. { key: 'gutHealthScore', label: '菌群健康', color: '#10B981' },
  842. { key: 'chronicDiseaseScore', label: '慢病控制', color: '#F59E0B' },
  843. { key: 'nutritionScore', label: '营养均衡', color: '#FF8C42' },
  844. { key: 'balanceScore', label: '平衡', color: '#6366F1' },
  845. { key: 'diversityScore', label: '多样性', color: '#FF6B9D' },
  846. { key: 'beneficialScore', label: '有益菌', color: '#10B981' },
  847. { key: 'harmfulScore', label: '有害菌', color: '#C62828' },
  848. { key: 'coreGenusScore', label: '核心菌属', color: '#4A9BD7' }
  849. ]
  850. for (var i = 0; i < scoreFields.length; i++) {
  851. var f = scoreFields[i]
  852. var val = s[f.key] != null ? s[f.key] : r[f.key]
  853. if (val != null) {
  854. items.push({ value: val, label: f.label, color: f.color })
  855. }
  856. }
  857. this.scoreItems = items
  858. },
  859. buildIndicators: function(data) {
  860. var indicators = data.indicators || []
  861. // 为每个指标添加_cssClass字段(wxss不支持中文类名)
  862. var self = this
  863. indicators.forEach(function(item) {
  864. item._cssClass = self._cssForStatus(item.status)
  865. })
  866. this.allIndicators = indicators
  867. var gutCategories = ['肠道屏障与代谢物', '短链脂肪酸', '神经递质与激素', '抗生素风险评估']
  868. this.gutIndicators = indicators.filter(function(item) {
  869. return gutCategories.indexOf(item.category) >= 0
  870. })
  871. var seen = {}
  872. var groups = []
  873. for (var i = 0; i < indicators.length; i++) {
  874. var item = indicators[i]
  875. if (gutCategories.indexOf(item.category) >= 0) continue
  876. if (!seen[item.category]) {
  877. seen[item.category] = true
  878. groups.push({ category: item.category, items: [] })
  879. }
  880. for (var g = 0; g < groups.length; g++) {
  881. if (groups[g].category === item.category) {
  882. groups[g].items.push(item)
  883. break
  884. }
  885. }
  886. }
  887. for (var gi = 0; gi < groups.length; gi++) {
  888. var abnormal = 0
  889. for (var ii = 0; ii < groups[gi].items.length; ii++) {
  890. if (this.isAbnormalStatus(groups[gi].items[ii].status)) abnormal++
  891. }
  892. groups[gi].abnormalCount = abnormal
  893. }
  894. this.nutritionGroups = groups
  895. },
  896. toggleAccordion: function(key) {
  897. this.accordionOpen[key] = !this.accordionOpen[key]
  898. },
  899. toggleNutriGroup: function(idx) {
  900. var arr = this.nutriGroupOpen
  901. arr[idx] = !arr[idx]
  902. this.nutriGroupOpen = [].concat(arr)
  903. },
  904. toggleRiskGroup: function(idx) {
  905. var arr = this.riskGroupOpen
  906. arr[idx] = !arr[idx]
  907. this.riskGroupOpen = [].concat(arr)
  908. },
  909. /** 中文风险等级 → CSS安全英文 */
  910. _cssForRiskLevel: function(level) {
  911. var map = { '低风险': 'low', '注意': 'warning', '需注意': 'warning', '高风险': 'high', '异常': 'abnormal' }
  912. return map[level] || 'low'
  913. },
  914. indicatorType: function(item) {
  915. return INDICATOR_TYPE_MAP[item.category] || 'nutrient'
  916. },
  917. indStatusIndex: function(status) {
  918. var idx = this.indStatuses.indexOf(status)
  919. return idx >= 0 ? idx : 0
  920. },
  921. onIndStatusChange: function(e, item) {
  922. item.status = this.indStatuses[e.detail.value]
  923. },
  924. showKnowledge: function(type, name) {
  925. var self = this
  926. // 优先使用 KB v3 查询指标调整建议
  927. queryIndicatorKnowledge(name).then(function(res) {
  928. if (res.code === 200 && res.data) {
  929. var v3 = res.data
  930. var high = v3['偏高影响'] || ''
  931. var low = v3['偏低影响'] || ''
  932. self.knowledgeData = {
  933. itemName: v3['名称'] || name,
  934. category: v3['说明'] || '',
  935. description: (high ? '偏高影响:' + high + '\n' : '') + (low ? '偏低影响:' + low : ''),
  936. suggestion: v3['调整建议'] || ''
  937. }
  938. self.knowledgeVisible = true
  939. } else {
  940. // 回退旧版 DB 查询
  941. self.queryOldKnowledgeBase(type, name)
  942. }
  943. }).catch(function() {
  944. self.queryOldKnowledgeBase(type, name)
  945. })
  946. },
  947. queryOldKnowledgeBase: function(type, name) {
  948. var self = this
  949. queryKnowledgeBase(type, name).then(function(res) {
  950. if (res.code === 200 && res.data) {
  951. self.knowledgeData = res.data
  952. self.knowledgeVisible = true
  953. } else {
  954. self.knowledgeData = { itemName: name }
  955. self.knowledgeVisible = true
  956. }
  957. })
  958. },
  959. closeKnowledge: function() {
  960. this.knowledgeVisible = false
  961. this.knowledgeData = {}
  962. },
  963. closeFeedback: function() {
  964. this.feedbackVisible = false
  965. },
  966. openFeedbackFromAnalyze: function() {
  967. this.quickAnalyze.show = false
  968. this.feedbackVisible = true
  969. },
  970. toggleEdit: function() {
  971. this.isEditing = true
  972. },
  973. cancelEdit: function() {
  974. this.isEditing = false
  975. this.loadData()
  976. },
  977. saveEdit: function() {
  978. var self = this
  979. var payload = {
  980. summary: {
  981. overallScore: this.report.overallScore,
  982. gutHealthScore: this.report.gutHealthScore,
  983. chronicDiseaseScore: this.report.chronicDiseaseScore,
  984. nutritionScore: this.report.nutritionScore,
  985. balanceScore: this.report.balanceScore,
  986. diversityScore: this.report.diversityScore,
  987. beneficialScore: this.report.beneficialScore,
  988. harmfulScore: this.report.harmfulScore,
  989. coreGenusScore: this.report.coreGenusScore
  990. },
  991. indicators: this.allIndicators
  992. }
  993. editHealthReport(this.reportId, payload, 0).then(function(res) {
  994. if (res.code === 200) {
  995. uni.showToast({ title: '已保存', icon: 'success' })
  996. self.isEditing = false
  997. self.loadData()
  998. } else {
  999. uni.showToast({ title: res.message || '保存失败', icon: 'none' })
  1000. }
  1001. })
  1002. },
  1003. doConfirm: function() {
  1004. var self = this
  1005. if (!self.draftId) return
  1006. self.confirming = true
  1007. // 有选中成员时传 subjectId,否则传 null:后端确认入库但不绑定任何用户/家庭,也不重算维度
  1008. var req = { draftId: self.draftId, payload: self.payload }
  1009. if (self.selectedMemberId) req.subjectId = self.selectedMemberId
  1010. confirmReportPreview(req).then(function(res) {
  1011. self.confirming = false
  1012. if (res.code === 200) {
  1013. var reportId = res.data && res.data.reportId
  1014. var planId = res.data && res.data.planId
  1015. var goDetail = function() {
  1016. if (reportId) {
  1017. uni.redirectTo({ url: '/pages/health/gut-flora-detail?reportId=' + reportId })
  1018. } else {
  1019. uni.navigateBack()
  1020. }
  1021. }
  1022. if (planId) {
  1023. uni.showModal({
  1024. title: '报告已入库',
  1025. content: '健康方案已自动生成,是否立即查看并确认任务?',
  1026. confirmText: '查看方案',
  1027. cancelText: '看报告',
  1028. success: function(modalRes) {
  1029. if (modalRes.confirm) {
  1030. uni.redirectTo({ url: '/pages/health/health-plan-summary?planId=' + planId })
  1031. } else {
  1032. goDetail()
  1033. }
  1034. }
  1035. })
  1036. } else {
  1037. goDetail()
  1038. }
  1039. } else {
  1040. uni.showToast({ title: res.message || '确认失败', icon: 'none' })
  1041. }
  1042. }).catch(function() {
  1043. self.confirming = false
  1044. uni.showToast({ title: '确认异常', icon: 'none' })
  1045. })
  1046. },
  1047. doDiscard: function() {
  1048. var self = this
  1049. if (!self.draftId) return
  1050. uni.showModal({
  1051. title: '提示',
  1052. content: '确定放弃该报告草稿吗?',
  1053. success: function(res) {
  1054. if (!res.confirm) return
  1055. discardReportDraft(self.draftId).then(function(res2) {
  1056. if (res2.code === 200) {
  1057. uni.navigateBack()
  1058. } else {
  1059. uni.showToast({ title: res2.message || '操作失败', icon: 'none' })
  1060. }
  1061. })
  1062. }
  1063. })
  1064. },
  1065. goToSpecies: function() {
  1066. uni.navigateTo({ url: '/pages/health/gut-flora-species-detail?reportId=' + this.reportId })
  1067. },
  1068. goToRisks: function() {
  1069. uni.navigateTo({ url: '/pages/health/gut-flora-risks-detail?reportId=' + this.reportId })
  1070. },
  1071. goToFoods: function() {
  1072. uni.navigateTo({ url: '/pages/health/gut-flora-foods-detail?reportId=' + this.reportId })
  1073. },
  1074. goToCaution: function() {
  1075. // 报告详情入口:传 reportId(按报告快照返回戒备食材)+ memberId(报告绑定主体 subjectId)
  1076. var mid = (this.report && this.report.subjectId) || uni.getStorageSync('currentMemberId') || ''
  1077. var url = '/pages/growth/caution-detail?reportId=' + this.reportId
  1078. if (mid) url += '&memberId=' + mid
  1079. uni.navigateTo({ url: url })
  1080. },
  1081. goToIndicators: function() {
  1082. // 展开所有营养分组手风琴
  1083. var arr = []
  1084. for (var i = 0; i < this.nutritionGroups.length; i++) arr.push(true)
  1085. this.nutriGroupOpen = arr
  1086. if (this.nutritionGroups.length === 0) uni.showToast({ title: '暂无营养指标', icon: 'none' })
  1087. },
  1088. // 问答功能已屏蔽
  1089. goAIChat: function() {}
  1090. }
  1091. }
  1092. </script>
  1093. <style scoped>
  1094. .page { min-height: 100vh; background: #f8f8f8; }
  1095. .content { height: calc(100vh - 100rpx); }
  1096. .report-header { background: linear-gradient(135deg, #4A9BD7, #6366F1); padding: 40rpx 30rpx; color: #fff; }
  1097. .report-title { font-size: 36rpx; font-weight: 700; display: block; margin-bottom: 16rpx; }
  1098. .report-meta { display: flex; gap: 20rpx; margin-bottom: 6rpx; }
  1099. .meta-item { font-size: 24rpx; opacity: 0.9; }
  1100. .badge-row { display: flex; gap: 16rpx; margin-top: 12rpx; }
  1101. .badge { padding: 6rpx 20rpx; background: rgba(255,255,255,0.2); border-radius: 20rpx; font-size: 22rpx; }
  1102. .scores-wrap { display: flex; flex-wrap: wrap; padding: 20rpx 10rpx; background: #fff; }
  1103. .score-item { display: flex; flex-direction: column; align-items: center; margin: 12rpx 10rpx; width: calc(25% - 20rpx); }
  1104. .score-ring { width: 100rpx; height: 100rpx; border-radius: 50%; border: 6rpx solid; display: flex; align-items: center; justify-content: center; font-size: 32rpx; font-weight: 700; margin-bottom: 8rpx; }
  1105. .score-label { font-size: 22rpx; color: #666; }
  1106. .quick-nav { display: flex; padding: 20rpx; background: #fff; margin-bottom: 16rpx; }
  1107. .nav-card { flex: 1; display: flex; flex-direction: column; align-items: center; padding: 16rpx 0; }
  1108. .caution-card {
  1109. background: #FEF2F2;
  1110. border-radius: 16rpx;
  1111. border: 2rpx solid #FECACA;
  1112. }
  1113. .caution-count { color: #DC2626; font-weight: 700; }
  1114. .nav-icon { font-size: 40rpx; margin-bottom: 6rpx; }
  1115. .nav-label { font-size: 24rpx; color: #333; }
  1116. .nav-count { font-size: 20rpx; color: #999; }
  1117. .accordion-section { background: #fff; margin-bottom: 16rpx; }
  1118. .accordion-header { display: flex; justify-content: space-between; padding: 30rpx; border-bottom: 1rpx solid #f0f0f0; }
  1119. .accordion-title { font-size: 30rpx; font-weight: 600; color: #333; }
  1120. .accordion-stats { font-size: 22rpx; color: #999; margin-left: 16rpx; }
  1121. .abnormal-num { color: #C62828; font-weight: 700; }
  1122. .accordion-arrow { font-size: 24rpx; color: #999; }
  1123. .accordion-body { padding: 10rpx 30rpx 20rpx; }
  1124. /* 疾病风险内联卡片 */
  1125. .risk-card-inline {
  1126. display: flex;
  1127. align-items: center;
  1128. padding: 20rpx 16rpx;
  1129. border-bottom: 1rpx solid #f5f5f5;
  1130. border-left: 8rpx solid #10B981;
  1131. margin-bottom: 8rpx;
  1132. }
  1133. .risk-card-inline.risk-level-warning { border-left-color: #F59E0B; }
  1134. .risk-card-inline.risk-level-high { border-left-color: #EF4444; }
  1135. .risk-card-inline.risk-level-abnormal { border-left-color: #C62828; }
  1136. .risk-name-inline { font-size: 28rpx; color: #333; flex: 1; }
  1137. .risk-badge-inline { padding: 4rpx 16rpx; border-radius: 20rpx; font-size: 22rpx; flex-shrink: 0; }
  1138. .risk-badge-inline.badge-low { background: #E8F5E9; color: #2E7D32; }
  1139. .risk-badge-inline.badge-warning { background: #FFF3E0; color: #E65100; }
  1140. .risk-badge-inline.badge-high { background: #FFEBEE; color: #C62828; }
  1141. .risk-badge-inline.badge-abnormal { background: #C62828; color: #fff; }
  1142. .risk-value-inline { font-size: 22rpx; color: #999; margin-left: 16rpx; }
  1143. /* 菌群风险分组手风琴 */
  1144. .risk-group-item { padding: 12rpx 0; border-bottom: 1rpx solid #f5f5f5; }
  1145. .risk-group-header { display: flex; align-items: center; gap: 12rpx; padding: 8rpx 0; }
  1146. .risk-group-name { font-size: 28rpx; color: #333; flex: 1; }
  1147. .risk-group-arrow { font-size: 24rpx; color: #999; }
  1148. .risk-group-body { padding: 8rpx 0 12rpx; }
  1149. .risk-level { font-size: 22rpx; padding: 4rpx 14rpx; border-radius: 8rpx; }
  1150. .risk-level.risk-good { background: #E8F5E9; color: #2E7D32; }
  1151. .risk-level.risk-warning { background: #FFF3E0; color: #E65100; }
  1152. .risk-level.risk-danger { background: #FFEBEE; color: #C62828; }
  1153. .risk-bacteria { font-size: 24rpx; color: #666; line-height: 1.6; }
  1154. .sub-group { margin-bottom: 16rpx; }
  1155. .sub-group-title { font-size: 26rpx; color: #4A9BD7; font-weight: 600; display: block; margin: 12rpx 0 8rpx; }
  1156. .indicator-card { padding: 16rpx 0; border-bottom: 1rpx solid #f5f5f5; }
  1157. .indicator-row { display: flex; align-items: center; justify-content: space-between; }
  1158. .indicator-name { font-size: 28rpx; color: #333; flex: 1; }
  1159. .arrow-up { color: #C62828; font-size: 22rpx; }
  1160. .arrow-down { color: #E65100; font-size: 22rpx; }
  1161. .arrow-warn { color: #F59E0B; font-size: 22rpx; }
  1162. .val-text { font-size: 28rpx; font-weight: 600; color: #333; }
  1163. .val-high { color: #C62828; }
  1164. .val-excess { color: #C62828; }
  1165. .val-low { color: #C62828; }
  1166. .val-deficient { color: #C62828; }
  1167. .val-insufficient { color: #C62828; }
  1168. .val-abnormal { color: #C62828; }
  1169. .val-warning { color: #F59E0B; }
  1170. .indicator-range { font-size: 22rpx; color: #bbb; display: block; margin-top: 4rpx; }
  1171. /* 指标知识库 "?" 按钮(与通用报告页一致) */
  1172. .ind-help-btn { display: inline-flex; align-items: center; justify-content: center; width: 36rpx; height: 36rpx; border-radius: 50%; background: #E3F2FD; color: #1976D2; font-size: 22rpx; font-weight: 600; margin-left: 8rpx; flex-shrink: 0; }
  1173. /* 综合评级行 */
  1174. .rating-line { margin: 12rpx 0 0; }
  1175. .rating-text { font-size: 24rpx; color: #666; display: block; }
  1176. /* 指标单位与症状 */
  1177. .indicator-unit { font-size: 22rpx; color: #999; margin-left: 6rpx; }
  1178. .indicator-symptoms { font-size: 22rpx; color: #E65100; display: block; margin-top: 4rpx; }
  1179. .indicator-edit { display: flex; gap: 8rpx; align-items: center; }
  1180. .edit-input { border: 1rpx solid #ddd; border-radius: 8rpx; padding: 6rpx 10rpx; font-size: 26rpx; width: 100rpx; }
  1181. .edit-picker { border: 1rpx solid #ddd; border-radius: 8rpx; padding: 6rpx 10rpx; font-size: 24rpx; color: #333; }
  1182. .fab { position: fixed; right: 30rpx; bottom: 40rpx; width: 100rpx; height: 100rpx; background: #4A9BD7; border-radius: 50%; display: flex; align-items: center; justify-content: center; box-shadow: 0 4rpx 16rpx rgba(74,155,215,0.4); }
  1183. .fab-icon { font-size: 40rpx; color: #fff; }
  1184. .draft-bar { position: fixed; bottom: 0; left: 0; right: 0; display: flex; padding: 20rpx 30rpx; background: #fff; border-top: 1rpx solid #eee; gap: 20rpx; z-index: 100; }
  1185. .btn-discard { flex: 1; padding: 20rpx; border: 1rpx solid #ddd; border-radius: 12rpx; text-align: center; font-size: 28rpx; color: #666; }
  1186. .btn-confirm { flex: 1; padding: 20rpx; background: #4A9BD7; border-radius: 12rpx; text-align: center; font-size: 28rpx; color: #fff; }
  1187. .btn-parsing { flex: 1; padding: 20rpx; background: #f0f0f0; border-radius: 12rpx; text-align: center; }
  1188. .parsing-text { font-size: 28rpx; color: #999; }
  1189. .bottom-bar { position: fixed; bottom: 0; left: 0; right: 0; display: flex; padding: 20rpx 30rpx; background: #fff; border-top: 1rpx solid #eee; gap: 20rpx; z-index: 100; }
  1190. .btn-cancel { flex: 1; padding: 20rpx; border: 1rpx solid #ddd; border-radius: 12rpx; text-align: center; font-size: 28rpx; color: #666; }
  1191. .btn-save { flex: 1; padding: 20rpx; background: #4A9BD7; border-radius: 12rpx; text-align: center; font-size: 28rpx; color: #fff; }
  1192. .feedback-section {
  1193. display: flex;
  1194. align-items: center;
  1195. padding: 24rpx 30rpx;
  1196. background: #fff;
  1197. margin: 16rpx;
  1198. border-radius: 12rpx;
  1199. border: 1rpx solid #f0f0f0;
  1200. }
  1201. .feedback-icon { font-size: 32rpx; margin-right: 12rpx; }
  1202. .feedback-text { flex: 1; font-size: 26rpx; color: #4A9BD7; }
  1203. .feedback-arrow { font-size: 28rpx; color: #ccc; }
  1204. .bottom-spacer { height: 40rpx; }
  1205. /* 报告解读 */
  1206. .interpretation-box {
  1207. background: #FFF8E1;
  1208. border-radius: 12rpx;
  1209. padding: 16rpx 20rpx;
  1210. margin: 16rpx 0 0;
  1211. border-left: 6rpx solid #FFB300;
  1212. }
  1213. .interp-text {
  1214. font-size: 24rpx;
  1215. color: #795548;
  1216. line-height: 1.6;
  1217. }
  1218. /* AI 解读框 */
  1219. .ai-box {
  1220. background: #E0F2F1;
  1221. border-left-color: #00897B;
  1222. }
  1223. .interp-label {
  1224. font-size: 24rpx;
  1225. font-weight: 600;
  1226. color: #00695C;
  1227. display: block;
  1228. margin-bottom: 6rpx;
  1229. }
  1230. .ai-box .interp-text {
  1231. color: #004D40;
  1232. }
  1233. /* 报告总结描述 */
  1234. .summary-box {
  1235. background: rgba(255,255,255,0.15);
  1236. border-radius: 12rpx;
  1237. padding: 16rpx 20rpx;
  1238. margin: 16rpx 0 0;
  1239. border-left: 6rpx solid #A5D6A7;
  1240. display: flex;
  1241. flex-direction: column;
  1242. gap: 8rpx;
  1243. }
  1244. .summary-title {
  1245. font-size: 24rpx;
  1246. font-weight: 600;
  1247. color: #fff;
  1248. opacity: 0.95;
  1249. }
  1250. .summary-text {
  1251. font-size: 24rpx;
  1252. color: #fff;
  1253. line-height: 1.6;
  1254. opacity: 0.95;
  1255. }
  1256. /* 解读入口 */
  1257. .ai-fab {
  1258. position: fixed;
  1259. right: 30rpx;
  1260. bottom: 100rpx;
  1261. display: flex;
  1262. flex-direction: row;
  1263. align-items: center;
  1264. background: linear-gradient(135deg, #6366F1, #8B5CF6);
  1265. border-radius: 40rpx;
  1266. padding: 16rpx 28rpx;
  1267. box-shadow: 0 4rpx 16rpx rgba(99, 102, 241, 0.4);
  1268. z-index: 100;
  1269. }
  1270. .ai-fab .fab-icon { font-size: 32rpx; margin-right: 12rpx; }
  1271. .ai-fab .fab-text { font-size: 26rpx; color: #fff; font-weight: 500; }
  1272. /* ===== 菌群功能分析 ===== */
  1273. .analysis-section {
  1274. margin: 20rpx 30rpx;
  1275. }
  1276. .section-title-bar {
  1277. margin-bottom: 16rpx;
  1278. }
  1279. .analysis-title {
  1280. font-size: 30rpx;
  1281. font-weight: bold;
  1282. color: #333;
  1283. display: block;
  1284. }
  1285. .analysis-subtitle {
  1286. font-size: 22rpx;
  1287. color: #999;
  1288. display: block;
  1289. margin-top: 4rpx;
  1290. }
  1291. .analysis-card {
  1292. background: #fff;
  1293. border-radius: 16rpx;
  1294. padding: 24rpx;
  1295. margin-bottom: 16rpx;
  1296. box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06);
  1297. }
  1298. .card-label {
  1299. font-size: 26rpx;
  1300. font-weight: 600;
  1301. color: #333;
  1302. display: block;
  1303. margin-bottom: 16rpx;
  1304. }
  1305. .func-dim {
  1306. margin-bottom: 20rpx;
  1307. }
  1308. .func-dim:last-child { margin-bottom: 0; }
  1309. .func-dim-head {
  1310. display: flex;
  1311. justify-content: space-between;
  1312. align-items: center;
  1313. margin-bottom: 8rpx;
  1314. }
  1315. .func-dim-name {
  1316. font-size: 24rpx;
  1317. color: #333;
  1318. font-weight: 500;
  1319. }
  1320. .func-badge {
  1321. font-size: 20rpx;
  1322. padding: 2rpx 14rpx;
  1323. border-radius: 20rpx;
  1324. color: #fff;
  1325. }
  1326. .func-good { background: #10B981; }
  1327. .func-warning { background: #F59E0B; }
  1328. .func-danger { background: #EF4444; }
  1329. .func-normal { background: #6B7280; }
  1330. .func-high { background: #EF4444; }
  1331. .func-low { background: #F59E0B; }
  1332. .func-bar {
  1333. height: 8rpx;
  1334. background: #f0f0f0;
  1335. border-radius: 4rpx;
  1336. overflow: hidden;
  1337. margin-bottom: 6rpx;
  1338. }
  1339. .func-bar-fill {
  1340. height: 100%;
  1341. border-radius: 4rpx;
  1342. transition: width 0.5s ease;
  1343. }
  1344. .fill-good { background: #10B981; }
  1345. .fill-warning { background: #F59E0B; }
  1346. .fill-danger { background: #EF4444; }
  1347. .fill-normal { background: #6B7280; }
  1348. .func-dim-summary {
  1349. font-size: 22rpx;
  1350. color: #888;
  1351. line-height: 1.5;
  1352. }
  1353. .pb-row {
  1354. display: flex;
  1355. gap: 20rpx;
  1356. align-items: center;
  1357. margin-bottom: 8rpx;
  1358. }
  1359. .pb-label {
  1360. font-size: 24rpx;
  1361. color: #666;
  1362. }
  1363. .pb-ratio {
  1364. font-size: 24rpx;
  1365. color: #6366F1;
  1366. font-weight: bold;
  1367. }
  1368. .pattern-name {
  1369. font-size: 28rpx;
  1370. font-weight: bold;
  1371. color: #FF8C42;
  1372. display: block;
  1373. margin-bottom: 8rpx;
  1374. }
  1375. .risk-item {
  1376. display: flex;
  1377. flex-wrap: wrap;
  1378. align-items: center;
  1379. gap: 12rpx;
  1380. padding: 12rpx 0;
  1381. border-bottom: 1rpx solid #f0f0f0;
  1382. }
  1383. .risk-item:last-child { border-bottom: none; }
  1384. .risk-name {
  1385. font-size: 24rpx;
  1386. color: #333;
  1387. font-weight: 500;
  1388. }
  1389. .risk-level {
  1390. font-size: 20rpx;
  1391. padding: 2rpx 14rpx;
  1392. border-radius: 20rpx;
  1393. color: #fff;
  1394. }
  1395. .risk-normal { background: #10B981; }
  1396. .risk-warning { background: #F59E0B; }
  1397. .risk-danger { background: #EF4444; }
  1398. .risk-bacteria {
  1399. font-size: 22rpx;
  1400. color: #999;
  1401. width: 100%;
  1402. margin-top: 4rpx;
  1403. }
  1404. .probiotic-item {
  1405. display: flex;
  1406. flex-wrap: wrap;
  1407. align-items: center;
  1408. gap: 12rpx;
  1409. padding: 12rpx 0;
  1410. border-bottom: 1rpx solid #f0f0f0;
  1411. }
  1412. .probiotic-item:last-child { border-bottom: none; }
  1413. .probiotic-name {
  1414. font-size: 24rpx;
  1415. color: #333;
  1416. font-weight: 500;
  1417. width: 120rpx;
  1418. }
  1419. .probiotic-advice {
  1420. font-size: 22rpx;
  1421. color: #888;
  1422. width: 100%;
  1423. margin-top: 4rpx;
  1424. line-height: 1.5;
  1425. }
  1426. /* ===== 报告快速分析 ===== */
  1427. .quick-analyze-overlay {
  1428. position: fixed;
  1429. top: 0; left: 0; right: 0; bottom: 0;
  1430. background: rgba(0, 0, 0, 0.45);
  1431. z-index: 98;
  1432. }
  1433. .quick-analyze-panel {
  1434. position: fixed;
  1435. left: 30rpx; right: 30rpx;
  1436. bottom: 40rpx;
  1437. background: #fff;
  1438. border-radius: 20rpx;
  1439. padding: 24rpx;
  1440. z-index: 99;
  1441. display: flex;
  1442. flex-direction: column;
  1443. max-height: 70vh;
  1444. }
  1445. .quick-analyze-header {
  1446. display: flex;
  1447. flex-direction: row;
  1448. align-items: center;
  1449. justify-content: space-between;
  1450. margin-bottom: 16rpx;
  1451. }
  1452. .quick-analyze-title { font-size: 30rpx; font-weight: bold; color: #333; }
  1453. .quick-analyze-header-right {
  1454. display: flex;
  1455. flex-direction: row;
  1456. align-items: center;
  1457. gap: 16rpx;
  1458. }
  1459. .quick-analyze-feedback {
  1460. font-size: 24rpx;
  1461. color: #5B9BD5;
  1462. padding: 8rpx;
  1463. }
  1464. .quick-analyze-feedback:active { opacity: 0.7; }
  1465. .quick-analyze-close { font-size: 32rpx; color: #999; padding: 8rpx; }
  1466. .quick-analyze-hint {
  1467. font-size: 24rpx;
  1468. color: #E65100;
  1469. background: #FFF3E0;
  1470. border-radius: 12rpx;
  1471. padding: 12rpx 16rpx;
  1472. margin-bottom: 12rpx;
  1473. }
  1474. .quick-analyze-messages {
  1475. max-height: 50vh;
  1476. margin-bottom: 16rpx;
  1477. }
  1478. .quick-analyze-message {
  1479. margin-bottom: 12rpx;
  1480. display: flex;
  1481. flex-direction: column;
  1482. }
  1483. .quick-analyze-message.msg-user { align-items: flex-end; }
  1484. .quick-analyze-message.msg-ai { align-items: flex-start; }
  1485. .quick-analyze-msg-text {
  1486. font-size: 26rpx;
  1487. color: #333;
  1488. background: #F3F4F6;
  1489. border-radius: 14rpx;
  1490. padding: 14rpx 18rpx;
  1491. max-width: 80%;
  1492. line-height: 1.5;
  1493. }
  1494. .msg-user .quick-analyze-msg-text { background: #5B9BD5; color: #fff; }
  1495. .quick-analyze-loading { display: flex; justify-content: center; padding: 12rpx 0; }
  1496. .quick-analyze-loading-text { font-size: 24rpx; color: #999; }
  1497. .quick-analyze-chips { display: flex; flex-direction: column; margin-bottom: 16rpx; }
  1498. .quick-analyze-chip {
  1499. background: #EFF6FF;
  1500. border: 1rpx solid #BFDBFE;
  1501. border-radius: 12rpx;
  1502. padding: 14rpx 18rpx;
  1503. margin-bottom: 10rpx;
  1504. }
  1505. .quick-analyze-chip:active { opacity: 0.8; }
  1506. .quick-analyze-chip-text { font-size: 26rpx; color: #1D4ED8; }
  1507. .quick-analyze-input-bar {
  1508. display: flex;
  1509. flex-direction: row;
  1510. align-items: center;
  1511. }
  1512. .quick-analyze-input {
  1513. flex: 1;
  1514. height: 72rpx;
  1515. background: #F3F4F6;
  1516. border-radius: 36rpx;
  1517. padding: 0 28rpx;
  1518. font-size: 26rpx;
  1519. color: #333;
  1520. margin-right: 16rpx;
  1521. }
  1522. .quick-analyze-send {
  1523. background: #5B9BD5;
  1524. border-radius: 36rpx;
  1525. padding: 16rpx 34rpx;
  1526. }
  1527. .quick-analyze-send.send-disabled { opacity: 0.5; }
  1528. .quick-analyze-send-text { font-size: 26rpx; color: #fff; }
  1529. </style>