health-report.vue 27 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009
  1. <template>
  2. <view class="report-container">
  3. <!-- 顶部导航 -->
  4. <view class="nav-bar">
  5. <text class="nav-back" @click="goBack">‹ 返回</text>
  6. <text class="nav-title">健康报告详情</text>
  7. <view class="nav-placeholder"></view>
  8. </view>
  9. <!-- 报告头部 -->
  10. <view class="report-header" v-if="report">
  11. <view class="report-type-tag">{{ reportTypeLabel }}</view>
  12. <view class="report-scores">
  13. <view class="score-item">
  14. <view class="score-circle-lg">
  15. <text class="score-num">{{ report.overallScore || '--' }}</text>
  16. <text class="score-unit">分</text>
  17. </view>
  18. <text class="score-label">综合评分</text>
  19. </view>
  20. <view class="score-item" v-if="report.gutHealthScore">
  21. <view class="score-circle-lg score-circle-green">
  22. <text class="score-num">{{ report.gutHealthScore }}</text>
  23. <text class="score-unit">分</text>
  24. </view>
  25. <text class="score-label">肠道健康</text>
  26. </view>
  27. </view>
  28. <view class="report-info-row" v-if="report.gutAge || report.gutType">
  29. <text class="info-chip" v-if="report.gutAge">肠道年龄:{{ report.gutAge }}</text>
  30. <text class="info-chip" v-if="report.gutType">肠道分型:{{ report.gutType }}</text>
  31. </view>
  32. <text class="report-date">报告日期:{{ formatDate(report.reportDate) }}</text>
  33. </view>
  34. <!-- ===== 评分可视化面板 (仅菌群报告) ===== -->
  35. <view class="score-dashboard" v-if="report && report.reportType === 'gut_flora'">
  36. <view class="main-scores">
  37. <view class="main-score-item" v-for="item in mainScores" :key="item.key">
  38. <view class="score-ring-wrap">
  39. <svg width="140" height="140" viewBox="0 0 120 120">
  40. <circle cx="60" cy="60" r="50" fill="none" stroke="#EEEEEE" stroke-width="8"/>
  41. <circle cx="60" cy="60" r="50" fill="none" v-bind:stroke="item.color" stroke-width="8" v-bind:stroke-dasharray="item.circumference" v-bind:stroke-dashoffset="item.offset" stroke-linecap="round" transform="rotate(-90 60 60)"/>
  42. <text x="60" y="52" text-anchor="middle" font-size="32" font-weight="bold" fill="#333">{{ item.score }}</text>
  43. <text x="60" y="72" text-anchor="middle" font-size="14" fill="#999">分</text>
  44. </svg>
  45. </view>
  46. <text class="main-score-label">{{ item.label }}</text>
  47. </view>
  48. </view>
  49. <view class="flora-score-row" v-if="hasFloraScores">
  50. <view class="flora-score-item" v-for="item in floraScores" :key="item.key">
  51. <view class="flora-score-circle" v-bind:style="{ background: item.color }">
  52. <text class="flora-score-num">{{ item.score }}</text>
  53. </view>
  54. <text class="flora-score-label">{{ item.label }}</text>
  55. </view>
  56. </view>
  57. </view>
  58. <!-- 指标列表 -->
  59. <view class="section">
  60. <view class="section-title">
  61. <text>指标明细</text>
  62. <text class="section-sub">共 {{ indicators.length }} 项</text>
  63. </view>
  64. <!-- 按分类分组 -->
  65. <view class="category-group" v-for="group in groupedIndicators" :key="group.name">
  66. <view class="category-header">
  67. <text class="category-name">{{ group.name }}</text>
  68. <text class="category-count">{{ group.items.length }}项</text>
  69. </view>
  70. <view class="indicator-card" v-for="ind in group.items" :key="ind._idx">
  71. <view class="ind-header">
  72. <text class="ind-name">{{ ind.indicatorName }}</text>
  73. <view class="ind-status" :class="ind._cls">
  74. <text>{{ ind._label }}</text>
  75. </view>
  76. </view>
  77. <view class="ind-body">
  78. <view class="ind-value-wrap">
  79. <text class="ind-value">{{ ind.indicatorValue || '--' }}</text>
  80. <text class="ind-unit">{{ ind.unit || '' }}</text>
  81. </view>
  82. <text class="ind-ref">参考值:{{ ind.refRange || '暂无' }}</text>
  83. </view>
  84. </view>
  85. </view>
  86. <view class="empty-indicators" v-if="indicators.length === 0 && !loading">
  87. <text>暂无指标数据</text>
  88. </view>
  89. </view>
  90. <!-- ===== 营养缺乏分析 ===== -->
  91. <view class="section" v-if="deficiencyRecords && deficiencyRecords.length > 0">
  92. <view class="section-title">
  93. <text>🔬 营养缺乏分析</text>
  94. <text class="section-sub">{{ deficiencyRecords.length }}项</text>
  95. </view>
  96. <view class="deficiency-list">
  97. <view class="deficiency-card" v-for="dr in deficiencyRecords" :key="dr.nutrient">
  98. <view class="deficiency-header">
  99. <text class="deficiency-nutrient">{{ dr.nutrient }}</text>
  100. <view class="deficiency-type-tag" :class="'dtype-' + dr.deficiencyType">
  101. <text>{{ dr.deficiencyTypeLabel }}</text>
  102. </view>
  103. </view>
  104. <view class="deficiency-severity-row">
  105. <view class="severity-bar">
  106. <view class="severity-fill" :style="{ width: (dr.severity * 100) + '%', background: dr.severityColor }"></view>
  107. </view>
  108. <text class="severity-val">{{ dr.severity }}</text>
  109. </view>
  110. <view class="deficiency-meta" v-if="dr.indicatorName">
  111. <text class="meta-label">关联指标:</text>
  112. <text class="meta-value">{{ dr.indicatorName }} ({{ dr.indicatorValue }})</text>
  113. </view>
  114. <view class="deficiency-foods" v-if="dr.foodSuggestions">
  115. <text class="foods-label">推荐食物:</text>
  116. <text class="foods-value">{{ dr.foodSuggestions }}</text>
  117. </view>
  118. </view>
  119. </view>
  120. </view>
  121. <!-- ===== 食材推荐 ===== -->
  122. <view class="section" v-if="foods && foods.length > 0">
  123. <view class="section-title">
  124. <text>🥗 食材推荐</text>
  125. <text class="section-sub">共 {{ foods.length }} 项</text>
  126. </view>
  127. <view class="food-list">
  128. <view class="food-card" v-for="food in foods" :key="food.id">
  129. <view class="food-header">
  130. <text class="food-name">{{ food.name }}</text>
  131. <view class="food-score" :class="food.scoreClass">
  132. <text>{{ food.score || '--' }}</text>
  133. </view>
  134. </view>
  135. <view class="food-meta" v-if="food.category">
  136. <text class="food-category">{{ food.category }}</text>
  137. </view>
  138. <view class="food-nutrition" v-if="food.energyKj || food.protein || food.fat || food.carbs">
  139. <view class="nutrition-item" v-if="food.energyKj">
  140. <text class="n-label">能量</text>
  141. <text class="n-value">{{ food.energyKj }} kJ</text>
  142. </view>
  143. <view class="nutrition-item" v-if="food.protein">
  144. <text class="n-label">蛋白质</text>
  145. <text class="n-value">{{ food.protein }}g</text>
  146. </view>
  147. <view class="nutrition-item" v-if="food.fat">
  148. <text class="n-label">脂肪</text>
  149. <text class="n-value">{{ food.fat }}g</text>
  150. </view>
  151. <view class="nutrition-item" v-if="food.carbs">
  152. <text class="n-label">碳水</text>
  153. <text class="n-value">{{ food.carbs }}g</text>
  154. </view>
  155. </view>
  156. </view>
  157. </view>
  158. </view>
  159. <!-- ===== 五维雷达图 (菌群报告) ===== -->
  160. <view class="section" v-if="report && report.reportType === 'gut_flora' && radarDimensions.length > 0">
  161. <view class="section-title">
  162. <text>五维雷达图</text>
  163. </view>
  164. <view class="radar-wrap">
  165. <canvas canvas-id="radarCanvas" id="radarCanvas" class="radar-canvas" @touchstart.prevent></canvas>
  166. </view>
  167. </view>
  168. <!-- 底部占位 -->
  169. <view class="bottom-spacer"></view>
  170. <!-- 营养分析入口按钮 -->
  171. <view class="nutrition-fab" @click="goNutritionChat" v-if="report">
  172. <text class="fab-icon">🥗</text>
  173. <text class="fab-text">精准营养分析</text>
  174. </view>
  175. </view>
  176. </template>
  177. <script>
  178. import config from '@/config.js'
  179. import { getDeficiencyAnalysis } from '@/utils/api.js'
  180. const healthRequest = function(url, data) {
  181. var token = uni.getStorageSync('token')
  182. return new Promise(function(resolve, reject) {
  183. uni.request({
  184. url: config.API_BASE_URL + url,
  185. method: 'POST',
  186. data: data,
  187. header: {
  188. 'Content-Type': 'application/json',
  189. 'Authorization': token ? 'Bearer ' + token : ''
  190. },
  191. success: function(res) {
  192. if (res.data.code === 200) {
  193. resolve(res.data)
  194. } else {
  195. reject(res.data)
  196. }
  197. },
  198. fail: function(err) {
  199. reject(err)
  200. }
  201. })
  202. })
  203. }
  204. export default {
  205. data() {
  206. return {
  207. childId: null,
  208. reportId: null,
  209. report: null,
  210. reportDetail: null,
  211. indicators: [],
  212. deficiencyRecords: [],
  213. foods: [],
  214. loading: false
  215. }
  216. },
  217. computed: {
  218. reportTypeLabel: function() {
  219. if (!this.report) return ''
  220. return this.report.reportType === 'gut_flora' ? '肠道检测报告' : '体检报告'
  221. },
  222. // 预计算分组,每个指标附加 _cls (CSS类) 和 _label (显示标签)
  223. groupedIndicators: function() {
  224. var result = []
  225. var groupMap = {}
  226. var idx = 0
  227. for (var i = 0; i < this.indicators.length; i++) {
  228. var ind = this.indicators[i]
  229. var cat = ind.category || '其他'
  230. // Compute status css class
  231. var cls = 'status-normal'
  232. var label = '正常'
  233. var status = ind.status || ''
  234. var s = status.toLowerCase()
  235. if (s.indexOf('偏高') !== -1 || s.indexOf('过高') !== -1 || s.indexOf('concern') !== -1) {
  236. cls = 'status-warning'
  237. label = '偏高'
  238. } else if (s.indexOf('偏低') !== -1 || s.indexOf('过低') !== -1 || s.indexOf('注意') !== -1 || s.indexOf('warning') !== -1) {
  239. cls = 'status-warn'
  240. label = '偏低'
  241. } else if (s.indexOf('正常') !== -1 || s.indexOf('normal') !== -1 || s.indexOf('低风险') !== -1) {
  242. cls = 'status-normal'
  243. label = '正常'
  244. } else if (status) {
  245. label = status
  246. }
  247. ind._cls = cls
  248. ind._label = label
  249. ind._idx = idx
  250. idx++
  251. // Group
  252. if (!groupMap[cat]) {
  253. groupMap[cat] = { name: cat, items: [] }
  254. result.push(groupMap[cat])
  255. }
  256. groupMap[cat].items.push(ind)
  257. }
  258. return result
  259. },
  260. circumference: function() {
  261. return Math.round(2 * Math.PI * 50)
  262. },
  263. mainScores: function() {
  264. if (!this.report) return []
  265. var items = []
  266. var r = this.report
  267. var pairs = [
  268. {key:'overall', label:'综合评分', score: r.overallScore, color:'#5B9BD5'},
  269. {key:'gut', label:'肠道健康', score: r.gutHealthScore, color:'#4CAF50'},
  270. {key:'chronic', label:'慢病控制', score: r.chronicDiseaseScore, color:'#FF9800'},
  271. {key:'nutrition', label:'营养均衡', score: r.nutritionScore, color:'#2196F3'}
  272. ]
  273. var circ = Math.round(2 * Math.PI * 50)
  274. for (var i = 0; i < pairs.length; i++) {
  275. var p = pairs[i]
  276. var s = p.score || 0
  277. items.push({key: p.key, label: p.label, score: s, color: p.color, circumference: circ, offset: Math.round(circ - (s / 100) * circ)})
  278. }
  279. return items
  280. },
  281. hasFloraScores: function() {
  282. if (!this.report) return false
  283. var r = this.report
  284. return (r.balanceScore != null) || (r.diversityScore != null) || (r.beneficialScore != null) || (r.harmfulScore != null) || (r.coreGenusScore != null)
  285. },
  286. floraScores: function() {
  287. if (!this.report) return []
  288. var r = this.report
  289. var items = []
  290. var pairs = [
  291. {key:'balance', label:'菌群平衡', score: r.balanceScore},
  292. {key:'diversity', label:'菌群多样性', score: r.diversityScore},
  293. {key:'beneficial', label:'有益菌', score: r.beneficialScore},
  294. {key:'harmful', label:'有害菌', score: r.harmfulScore},
  295. {key:'coreGenus', label:'核心菌属', score: r.coreGenusScore}
  296. ]
  297. for (var i = 0; i < pairs.length; i++) {
  298. var p = pairs[i]
  299. if (p.score != null) {
  300. var s = p.score
  301. var color = '#4CAF50'
  302. if (s < 30) { color = '#F44336' }
  303. else if (s < 60) { color = '#FFC107' }
  304. items.push({key: p.key, label: p.label, score: s, color: color})
  305. }
  306. }
  307. return items
  308. },
  309. radarDimensions: function() {
  310. if (!this.report) return []
  311. var r = this.report
  312. var dims = []
  313. if (r.overallScore != null) dims.push({ label: '综合', value: r.overallScore })
  314. if (r.gutHealthScore != null) dims.push({ label: '肠道', value: r.gutHealthScore })
  315. if (r.nutritionScore != null) dims.push({ label: '营养', value: r.nutritionScore })
  316. if (r.chronicDiseaseScore != null) dims.push({ label: '慢病', value: r.chronicDiseaseScore })
  317. if (r.gutBalanceScore != null) dims.push({ label: '菌群平衡', value: r.gutBalanceScore })
  318. return dims
  319. }
  320. },
  321. onLoad: function(options) {
  322. var childId = options.childId
  323. var reportId = options.reportId
  324. this.childId = childId ? parseInt(childId) : null
  325. this.reportId = reportId ? parseInt(reportId) : null
  326. this.loadData()
  327. },
  328. onReady: function() {
  329. if (this.radarDimensions.length >= 3) {
  330. this.drawRadarChart()
  331. }
  332. },
  333. methods: {
  334. loadData: function() {
  335. var self = this
  336. uni.showLoading({ title: '加载中...' })
  337. // Load report + indicators in parallel
  338. var promises = []
  339. if (this.reportId) {
  340. promises.push(healthRequest('/api/health/report/latest', { childId: this.childId }))
  341. promises.push(healthRequest('/api/health/indicator/list', { reportId: this.reportId }))
  342. } else if (this.childId) {
  343. promises.push(healthRequest('/api/health/report/latest', { childId: this.childId }))
  344. }
  345. Promise.all(promises.map(function(p) {
  346. return p.catch(function(e) { return null })
  347. })).then(function(results) {
  348. uni.hideLoading()
  349. if (results[0] && results[0].data) {
  350. self.report = results[0].data
  351. self.reportId = self.report.id
  352. self.loadDeficiencyRecords()
  353. // Load indicators after getting reportId
  354. if (self.reportId) {
  355. healthRequest('/api/health/indicator/list', { reportId: self.reportId }).then(function(res) {
  356. if (res.data) {
  357. self.indicators = res.data
  358. }
  359. }).catch(function(e) {
  360. console.log('获取指标失败', e)
  361. })
  362. healthRequest('/api/health/report/detail', { reportId: self.reportId }).then(function(res) {
  363. if (res.data) {
  364. self.reportDetail = res.data
  365. }
  366. }).catch(function(e) {
  367. console.log('获取报告详情失败', e)
  368. })
  369. healthRequest('/api/health/foods', { reportId: self.reportId }).then(function(res) {
  370. if (res.data) {
  371. self.foods = res.data.map(function(f) {
  372. f.scoreClass = self.getFoodScoreClass(f.score)
  373. return f
  374. })
  375. }
  376. }).catch(function(e) {
  377. console.log('获取食材推荐失败', e)
  378. })
  379. }
  380. }
  381. }).catch(function(e) {
  382. uni.hideLoading()
  383. uni.showToast({ title: '加载失败', icon: 'none' })
  384. })
  385. },
  386. goBack: function() {
  387. uni.navigateBack()
  388. },
  389. goNutritionChat: function() {
  390. if (!this.reportId) return
  391. uni.navigateTo({
  392. url: '/pages/ai/chat?mode=nutrition&reportId=' + this.reportId + '&firstMessage=' + encodeURIComponent('请根据这份菌群检测报告,分析我孩子的营养状况并提出建议')
  393. })
  394. },
  395. loadDeficiencyRecords: function() {
  396. if (!this.childId) return
  397. var self = this
  398. getDeficiencyAnalysis(this.childId).then(function(res) {
  399. if (res.code === 200 && res.data) {
  400. self.deficiencyRecords = res.data.map(function(dr) {
  401. dr.deficiencyTypeLabel = self.getDeficiencyTypeLabel(dr.deficiencyType)
  402. dr.severityColor = self.getSeverityColor(dr.severity)
  403. return dr
  404. })
  405. }
  406. }).catch(function(e) {
  407. console.log('获取缺乏分析失败', e)
  408. })
  409. },
  410. getDeficiencyTypeLabel: function(type) {
  411. if (type === 'deficiency') return '缺乏'
  412. if (type === 'excess') return '偏高'
  413. if (type === 'borderline') return '边界值'
  414. return '未分类'
  415. },
  416. getSeverityColor: function(severity) {
  417. if (severity >= 0.7) return '#EF4444'
  418. if (severity >= 0.3) return '#F59E0B'
  419. return '#10B981'
  420. },
  421. drawRadarChart: function() {
  422. var dims = this.radarDimensions
  423. if (!dims || dims.length < 3) return
  424. var ctx = uni.createCanvasContext('radarCanvas', this)
  425. var cw = 300
  426. var ch = 300
  427. var cx = cw / 2
  428. var cy = ch / 2
  429. var maxR = 110
  430. var n = dims.length
  431. var angleStep = (2 * Math.PI) / n
  432. for (var level = 1; level <= 5; level++) {
  433. var r = maxR * (level / 5)
  434. ctx.setStrokeStyle('#E5E7EB')
  435. ctx.setLineWidth(1)
  436. ctx.beginPath()
  437. for (var j = 0; j < n; j++) {
  438. var a = j * angleStep - Math.PI / 2
  439. var x = cx + r * Math.cos(a)
  440. var y = cy + r * Math.sin(a)
  441. if (j === 0) ctx.moveTo(x, y)
  442. else ctx.lineTo(x, y)
  443. }
  444. ctx.closePath()
  445. ctx.stroke()
  446. }
  447. for (var k = 0; k < n; k++) {
  448. var a2 = k * angleStep - Math.PI / 2
  449. ctx.beginPath()
  450. ctx.moveTo(cx, cy)
  451. ctx.lineTo(cx + maxR * Math.cos(a2), cy + maxR * Math.sin(a2))
  452. ctx.setStrokeStyle('#E5E7EB')
  453. ctx.setLineWidth(1)
  454. ctx.stroke()
  455. }
  456. ctx.beginPath()
  457. for (var m = 0; m < n; m++) {
  458. var angle = m * angleStep - Math.PI / 2
  459. var val = dims[m].value || 0
  460. var rr = maxR * (val / 100)
  461. var px = cx + rr * Math.cos(angle)
  462. var py = cy + rr * Math.sin(angle)
  463. if (m === 0) ctx.moveTo(px, py)
  464. else ctx.lineTo(px, py)
  465. }
  466. ctx.closePath()
  467. ctx.setFillStyle('rgba(249, 115, 22, 0.2)')
  468. ctx.fill()
  469. ctx.setStrokeStyle('#F97316')
  470. ctx.setLineWidth(2)
  471. ctx.stroke()
  472. for (var q = 0; q < n; q++) {
  473. var angle3 = q * angleStep - Math.PI / 2
  474. var lx = cx + (maxR + 20) * Math.cos(angle3)
  475. var ly = cy + (maxR + 20) * Math.sin(angle3)
  476. ctx.setFontSize(11)
  477. ctx.setFillStyle('#6B7280')
  478. ctx.setTextAlign('center')
  479. ctx.setTextBaseline('middle')
  480. ctx.fillText(dims[q].label + ' ' + (dims[q].value || '--'), lx, ly)
  481. }
  482. ctx.draw()
  483. },
  484. getFoodScoreClass: function(score) {
  485. if (score == null) return 'score-unknown'
  486. if (score >= 80) return 'score-high'
  487. if (score >= 60) return 'score-mid'
  488. return 'score-low'
  489. },
  490. formatDate: function(dateStr) {
  491. if (!dateStr) return '--'
  492. try {
  493. var d = new Date(dateStr)
  494. var y = d.getFullYear()
  495. var m = ('' + (d.getMonth() + 1)).padStart(2, '0')
  496. var day = ('' + d.getDate()).padStart(2, '0')
  497. return y + '-' + m + '-' + day
  498. } catch (e) {
  499. return dateStr
  500. }
  501. }
  502. }
  503. }
  504. </script>
  505. <style scoped>
  506. .report-container {
  507. min-height: 100vh;
  508. background: #f5f7fa;
  509. }
  510. /* ===== 导航栏 ===== */
  511. .nav-bar {
  512. position: sticky;
  513. top: 0;
  514. background: #fff;
  515. display: flex;
  516. flex-direction: row;
  517. align-items: center;
  518. justify-content: space-between;
  519. padding: 20rpx 30rpx;
  520. box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.06);
  521. z-index: 100;
  522. }
  523. .nav-back {
  524. font-size: 32rpx;
  525. color: #5B9BD5;
  526. }
  527. .nav-title {
  528. font-size: 32rpx;
  529. font-weight: bold;
  530. color: #333;
  531. }
  532. .nav-placeholder {
  533. width: 80rpx;
  534. }
  535. /* ===== 报告头部 ===== */
  536. .report-header {
  537. background: linear-gradient(135deg, #E3F2FD, #BBDEFB);
  538. margin: 20rpx 30rpx;
  539. border-radius: 20rpx;
  540. padding: 30rpx 24rpx;
  541. }
  542. .report-type-tag {
  543. display: inline-block;
  544. background: #1565C0;
  545. color: #fff;
  546. font-size: 22rpx;
  547. padding: 6rpx 20rpx;
  548. border-radius: 20rpx;
  549. margin-bottom: 20rpx;
  550. }
  551. .report-scores {
  552. display: flex;
  553. flex-direction: row;
  554. margin-bottom: 20rpx;
  555. }
  556. .score-item {
  557. display: flex;
  558. flex-direction: column;
  559. align-items: center;
  560. margin-right: 60rpx;
  561. }
  562. .score-circle-lg {
  563. width: 140rpx;
  564. height: 140rpx;
  565. border-radius: 70rpx;
  566. background: #fff;
  567. display: flex;
  568. flex-direction: column;
  569. align-items: center;
  570. justify-content: center;
  571. margin-bottom: 8rpx;
  572. }
  573. .score-circle-green {
  574. background: #E8F5E9;
  575. }
  576. .score-num {
  577. font-size: 48rpx;
  578. font-weight: bold;
  579. color: #1565C0;
  580. }
  581. .score-circle-green .score-num {
  582. color: #2E7D32;
  583. }
  584. .score-unit {
  585. font-size: 22rpx;
  586. color: #64B5F6;
  587. }
  588. .score-circle-green .score-unit {
  589. color: #81C784;
  590. }
  591. .score-label {
  592. font-size: 24rpx;
  593. color: #666;
  594. }
  595. .report-info-row {
  596. display: flex;
  597. flex-direction: row;
  598. margin-bottom: 16rpx;
  599. }
  600. .info-chip {
  601. background: #fff;
  602. color: #333;
  603. font-size: 24rpx;
  604. padding: 8rpx 20rpx;
  605. border-radius: 20rpx;
  606. margin-right: 16rpx;
  607. }
  608. .report-date {
  609. font-size: 22rpx;
  610. color: #888;
  611. }
  612. /* ===== 指标明细 ===== */
  613. .section {
  614. margin: 20rpx 30rpx;
  615. }
  616. .section-title {
  617. display: flex;
  618. flex-direction: row;
  619. align-items: baseline;
  620. margin-bottom: 20rpx;
  621. font-size: 30rpx;
  622. font-weight: bold;
  623. color: #333;
  624. }
  625. .section-sub {
  626. font-size: 22rpx;
  627. color: #999;
  628. font-weight: normal;
  629. margin-left: 12rpx;
  630. }
  631. .category-group {
  632. margin-bottom: 30rpx;
  633. }
  634. .category-header {
  635. display: flex;
  636. flex-direction: row;
  637. align-items: center;
  638. justify-content: space-between;
  639. margin-bottom: 12rpx;
  640. }
  641. .category-name {
  642. font-size: 26rpx;
  643. font-weight: bold;
  644. color: #333;
  645. }
  646. .category-count {
  647. font-size: 22rpx;
  648. color: #999;
  649. }
  650. .indicator-card {
  651. background: #fff;
  652. border-radius: 16rpx;
  653. padding: 20rpx;
  654. margin-bottom: 12rpx;
  655. box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.04);
  656. }
  657. .ind-header {
  658. display: flex;
  659. flex-direction: row;
  660. align-items: center;
  661. justify-content: space-between;
  662. margin-bottom: 12rpx;
  663. }
  664. .ind-name {
  665. font-size: 28rpx;
  666. color: #333;
  667. font-weight: 500;
  668. }
  669. .ind-status {
  670. font-size: 22rpx;
  671. padding: 4rpx 16rpx;
  672. border-radius: 16rpx;
  673. }
  674. .status-normal {
  675. background: #E8F5E9;
  676. color: #2E7D32;
  677. }
  678. .status-warning {
  679. background: #FFF3E0;
  680. color: #E65100;
  681. }
  682. .status-warn {
  683. background: #F3E5F5;
  684. color: #7B1FA2;
  685. }
  686. .ind-body {
  687. display: flex;
  688. flex-direction: row;
  689. align-items: baseline;
  690. justify-content: space-between;
  691. }
  692. .ind-value-wrap {
  693. display: flex;
  694. flex-direction: row;
  695. align-items: baseline;
  696. }
  697. .ind-value {
  698. font-size: 36rpx;
  699. font-weight: bold;
  700. color: #1565C0;
  701. }
  702. .ind-unit {
  703. font-size: 24rpx;
  704. color: #999;
  705. margin-left: 8rpx;
  706. }
  707. .ind-ref {
  708. font-size: 22rpx;
  709. color: #bbb;
  710. }
  711. .empty-indicators {
  712. text-align: center;
  713. padding: 60rpx;
  714. color: #999;
  715. font-size: 28rpx;
  716. }
  717. /* ===== 底部 ===== */
  718. .bottom-spacer {
  719. height: 130rpx;
  720. }
  721. /* ===== 营养分析入口 FAB ===== */
  722. .nutrition-fab {
  723. position: fixed;
  724. bottom: 40rpx;
  725. left: 50%;
  726. transform: translateX(-50%);
  727. background: linear-gradient(135deg, #43A047, #66BB6A);
  728. color: #fff;
  729. display: flex;
  730. flex-direction: row;
  731. align-items: center;
  732. padding: 20rpx 40rpx;
  733. border-radius: 50rpx;
  734. box-shadow: 0 8rpx 24rpx rgba(67, 160, 71, 0.4);
  735. z-index: 200;
  736. }
  737. .fab-icon {
  738. font-size: 36rpx;
  739. margin-right: 12rpx;
  740. }
  741. .fab-text {
  742. font-size: 30rpx;
  743. font-weight: 600;
  744. }
  745. .score-dashboard {
  746. margin: 20rpx 30rpx;
  747. background: #fff;
  748. border-radius: 20rpx;
  749. padding: 24rpx;
  750. box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06);
  751. }
  752. .main-scores {
  753. display: flex;
  754. flex-direction: row;
  755. flex-wrap: wrap;
  756. justify-content: space-around;
  757. }
  758. .main-score-item {
  759. display: flex;
  760. flex-direction: column;
  761. align-items: center;
  762. margin-bottom: 20rpx;
  763. width: 45%;
  764. }
  765. .score-ring-wrap {
  766. margin-bottom: 8rpx;
  767. }
  768. .main-score-label {
  769. font-size: 24rpx;
  770. color: #666;
  771. font-weight: 500;
  772. }
  773. .flora-score-row {
  774. display: flex;
  775. flex-direction: row;
  776. justify-content: space-around;
  777. margin-top: 16rpx;
  778. padding-top: 20rpx;
  779. border-top: 2rpx solid #F0F0F0;
  780. }
  781. .flora-score-item {
  782. display: flex;
  783. flex-direction: column;
  784. align-items: center;
  785. }
  786. .flora-score-circle {
  787. width: 80rpx;
  788. height: 80rpx;
  789. border-radius: 40rpx;
  790. display: flex;
  791. align-items: center;
  792. justify-content: center;
  793. margin-bottom: 8rpx;
  794. }
  795. .flora-score-num {
  796. font-size: 28rpx;
  797. font-weight: bold;
  798. color: #fff;
  799. }
  800. .flora-score-label {
  801. font-size: 20rpx;
  802. color: #888;
  803. }
  804. /* ===== 营养缺乏分析 ===== */
  805. .deficiency-list {
  806. display: flex;
  807. flex-direction: column;
  808. }
  809. .deficiency-card {
  810. background: #FAFAFA;
  811. border-radius: 16rpx;
  812. padding: 20rpx;
  813. margin-bottom: 12rpx;
  814. }
  815. .deficiency-header {
  816. display: flex;
  817. flex-direction: row;
  818. align-items: center;
  819. justify-content: space-between;
  820. margin-bottom: 12rpx;
  821. }
  822. .deficiency-nutrient {
  823. font-size: 28rpx;
  824. font-weight: bold;
  825. color: #333;
  826. }
  827. .deficiency-type-tag {
  828. font-size: 22rpx;
  829. padding: 4rpx 16rpx;
  830. border-radius: 16rpx;
  831. background: #FFF3E0;
  832. color: #E65100;
  833. }
  834. .dtype-deficiency {
  835. background: #FEE2E2;
  836. color: #DC2626;
  837. }
  838. .dtype-excess {
  839. background: #FFF3E0;
  840. color: #E65100;
  841. }
  842. .dtype-borderline {
  843. background: #FEF3C7;
  844. color: #D97706;
  845. }
  846. .dtype-unclassified {
  847. background: #F3F4F6;
  848. color: #6B7280;
  849. }
  850. .deficiency-severity-row {
  851. display: flex;
  852. flex-direction: row;
  853. align-items: center;
  854. margin-bottom: 8rpx;
  855. }
  856. .severity-bar {
  857. flex: 1;
  858. height: 12rpx;
  859. background: #F3F4F6;
  860. border-radius: 6rpx;
  861. overflow: hidden;
  862. margin-right: 16rpx;
  863. }
  864. .severity-fill {
  865. height: 100%;
  866. border-radius: 6rpx;
  867. }
  868. .severity-val {
  869. font-size: 24rpx;
  870. color: #9CA3AF;
  871. width: 60rpx;
  872. text-align: right;
  873. }
  874. .deficiency-meta {
  875. font-size: 22rpx;
  876. color: #6B7280;
  877. margin-bottom: 6rpx;
  878. }
  879. .deficiency-foods {
  880. font-size: 22rpx;
  881. color: #374151;
  882. margin-top: 8rpx;
  883. }
  884. .foods-label {
  885. color: #9CA3AF;
  886. }
  887. .foods-value {
  888. color: #2E7D32;
  889. }
  890. /* ===== 雷达图 ===== */
  891. .radar-wrap {
  892. display: flex;
  893. justify-content: center;
  894. align-items: center;
  895. }
  896. .radar-canvas {
  897. width: 300px;
  898. height: 300px;
  899. margin: 0 auto;
  900. }
  901. /* ===== 食材推荐 ===== */
  902. .food-list {
  903. display: flex;
  904. flex-direction: column;
  905. }
  906. .food-card {
  907. background: #fff;
  908. border-radius: 16rpx;
  909. padding: 20rpx;
  910. margin-bottom: 12rpx;
  911. box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.04);
  912. }
  913. .food-header {
  914. display: flex;
  915. flex-direction: row;
  916. align-items: center;
  917. justify-content: space-between;
  918. margin-bottom: 8rpx;
  919. }
  920. .food-name {
  921. font-size: 28rpx;
  922. font-weight: bold;
  923. color: #333;
  924. }
  925. .food-score {
  926. font-size: 24rpx;
  927. padding: 4rpx 16rpx;
  928. border-radius: 16rpx;
  929. font-weight: 600;
  930. }
  931. .score-high {
  932. background: #DCFCE7;
  933. color: #16A34A;
  934. }
  935. .score-mid {
  936. background: #FEF3C7;
  937. color: #D97706;
  938. }
  939. .score-low {
  940. background: #FEE2E2;
  941. color: #DC2626;
  942. }
  943. .score-unknown {
  944. background: #F3F4F6;
  945. color: #6B7280;
  946. }
  947. .food-meta {
  948. margin-bottom: 8rpx;
  949. }
  950. .food-category {
  951. font-size: 22rpx;
  952. color: #6B7280;
  953. background: #F3F4F6;
  954. padding: 2rpx 12rpx;
  955. border-radius: 12rpx;
  956. }
  957. .food-nutrition {
  958. display: flex;
  959. flex-direction: row;
  960. flex-wrap: wrap;
  961. gap: 16rpx;
  962. margin-top: 8rpx;
  963. }
  964. .nutrition-item {
  965. display: flex;
  966. flex-direction: column;
  967. align-items: center;
  968. min-width: 100rpx;
  969. }
  970. .n-label {
  971. font-size: 20rpx;
  972. color: #9CA3AF;
  973. }
  974. .n-value {
  975. font-size: 24rpx;
  976. color: #374151;
  977. font-weight: 500;
  978. }
  979. </style>