index.vue 29 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018
  1. <template>
  2. <view class="body-container">
  3. <!-- Tabbar 切换过渡页 -->
  4. <tab-transition v-if="showTabTransition" dimCode="body" />
  5. <!-- 品牌头部 -->
  6. <PageBanner theme="body"
  7. tagline="科学管理全家健康"
  8. quote="身体健康是幸福的基础" />
  9. <!-- 游客专属区块 -->
  10. <template v-if="!isLoggedIn">
  11. <LoginGuideCard
  12. title="登录后查看全家健康数据"
  13. :items="[
  14. '查看全家身体能量数据和健康报告',
  15. '记录每日健康打卡,获取身体能量',
  16. '查看专属健康商品和活动价格'
  17. ]" />
  18. <HealthTips dimension="body" />
  19. </template>
  20. <!-- 登录后:五维能量条 -->
  21. <FamilyEnergyBar
  22. v-if="isLoggedIn"
  23. dimensionCode="body"
  24. :sandboxData="sandboxData"
  25. :dualDimension="dualDimension" />
  26. <!-- 关系图谱(登录后可见) -->
  27. <FamilyRelationGraph
  28. v-if="isLoggedIn && familyMembersVisible.length > 0"
  29. dimensionCode="body"
  30. :selfId="activeChildId"
  31. :members="familyMembersVisible"
  32. :energyMap="energyMapForGraph"
  33. :intimacyMap="intimacyMapForGraph"
  34. @memberTap="goMemberDetail" />
  35. <!-- 健康打卡卡片 -->
  36. <HealthCheckinCard
  37. v-if="isLoggedIn && activeChildId"
  38. :childId="activeChildId" />
  39. <!-- 雷达图 -->
  40. <view class="section" v-if="dimensionData">
  41. <view class="section-header" v-if="dimensionData.isAverage">
  42. <text class="section-title">七维健康图</text>
  43. <text class="avg-badge">同龄同性平均值 · 上传报告后显示个人数据</text>
  44. </view>
  45. <view class="section-header" v-else>
  46. <text class="section-title">七维健康图</text>
  47. </view>
  48. <RadarChart
  49. v-if="radarDimensions && radarDimensions.length >= 3"
  50. :dimensions="radarDimensions"
  51. :scores="radarScores"
  52. :avg-scores="radarAvgScores"
  53. fillColor="#FF8C42"
  54. gridColor="#FED7AA"
  55. labelColor="#9A3412"
  56. :width="580"
  57. :height="580"
  58. @dimensionClick="goDimensionDetail" />
  59. </view>
  60. <!-- 七维健康全景(嵌入卡片) -->
  61. <HealthDimensionsSection
  62. v-if="isLoggedIn && dimensionData"
  63. :healthData="dimensionData"
  64. :activeChildId="activeChildId" />
  65. <!-- 功能入口 -->
  66. <view class="func-section" v-if="sectionVisible('func_entries')">
  67. <view class="func-grid">
  68. <view class="func-item" v-for="item in funcList" :key="item.label" @click="onFuncClick(item)">
  69. <view class="func-icon-wrap">
  70. <text class="func-icon">{{ item.icon }}</text>
  71. </view>
  72. <text class="func-label">{{ item.label }}</text>
  73. </view>
  74. </view>
  75. </view>
  76. <!-- 健康报告摘要(登录后可见) -->
  77. <view class="section" v-if="sectionVisible('health_report') && isLoggedIn">
  78. <view class="section-header">
  79. <text class="section-title">健康报告</text>
  80. <text class="section-more" @click="goToHealthReport">查看详情 ›</text>
  81. </view>
  82. <view class="report-card" v-if="latestReport">
  83. <view class="report-type-tag">{{ reportTypeLabel }}</view>
  84. <view class="report-score-row">
  85. <view class="score-circle">
  86. <text class="score-value">{{ latestReport.overallScore || '--' }}</text>
  87. <text class="score-label">综合评分</text>
  88. </view>
  89. <view class="report-meta">
  90. <view class="meta-item" v-if="latestReport.gutHealthScore">
  91. <text class="meta-label">肠道健康</text>
  92. <text class="meta-value">{{ latestReport.gutHealthScore }}</text>
  93. </view>
  94. <view class="meta-item" v-if="latestReport.nutritionScore">
  95. <text class="meta-label">营养状况</text>
  96. <text class="meta-value">{{ latestReport.nutritionScore }}</text>
  97. </view>
  98. <view class="meta-item" v-if="latestReport.gutAge">
  99. <text class="meta-label">肠道年龄</text>
  100. <text class="meta-value">{{ latestReport.gutAge }}</text>
  101. </view>
  102. </view>
  103. </view>
  104. <text class="report-date">报告日期:{{ formatDate(latestReport.reportDate) }}</text>
  105. </view>
  106. <view class="report-empty" v-else>
  107. <text class="empty-tip">暂无健康报告</text>
  108. <text class="empty-sub">规划师录入后将自动显示</text>
  109. </view>
  110. </view>
  111. <!-- ===== 精准营养(上传报告+产品+文章) ===== -->
  112. <view class="section nutrition-section" v-if="isLoggedIn">
  113. <view class="section-header">
  114. <text class="section-title">🥗 精准营养</text>
  115. </view>
  116. <!-- 上传报告入口 -->
  117. <view class="nutrition-report-card" @click="goToUploadReport">
  118. <view class="nutrition-report-left">
  119. <text class="nutrition-icon-large">📋</text>
  120. </view>
  121. <view class="nutrition-report-right">
  122. <text class="nutrition-title">上传健康报告</text>
  123. <text class="nutrition-desc">上传体检报告或肠道检测报告,AI智能解读分析,获取个性化营养方案</text>
  124. <view class="nutrition-report-tags">
  125. <text class="tag">体检报告</text>
  126. <text class="tag">肠道检测</text>
  127. <text class="tag">AI解读</text>
  128. </view>
  129. </view>
  130. <text class="nutrition-arrow">›</text>
  131. </view>
  132. </view>
  133. <!-- 维度任务 -->
  134. <DimensionTasks
  135. v-if="isLoggedIn"
  136. dimensionCode="body"
  137. :tasks="dimensionTasks"
  138. @taskClick="onTaskClick"
  139. @moreTasks="goTasks" />
  140. <!-- 维度活动 -->
  141. <DimensionActivities
  142. dimensionCode="body"
  143. :activities="dimensionActivities"
  144. :isLoggedIn="isLoggedIn"
  145. @activityClick="goActivityDetail"
  146. @moreActivities="goMoreActivities" />
  147. <!-- 商品推荐 -->
  148. <DimensionProductList
  149. v-if="isLoggedIn"
  150. dimensionCode="body"
  151. :familyId="activeChildId"
  152. title="为你推荐" />
  153. <!-- 推荐阅读 -->
  154. <DimensionArticles
  155. dimensionCode="body"
  156. :articles="bodyArticles"
  157. :isLoggedIn="isLoggedIn"
  158. @articleClick="goArticleDetail"
  159. @moreArticles="goMoreArticles" />
  160. <!-- 底部占位 -->
  161. <view class="bottom-spacer"></view>
  162. <AIFloatingAvatar />
  163. </view>
  164. </template>
  165. <script>
  166. import TabTransition from '../../components/tab-transition.vue'
  167. import PageBanner from '../../components/PageBanner.vue'
  168. import LoginGuideCard from '../../components/LoginGuideCard.vue'
  169. import FamilyEnergyBar from '../../components/FamilyEnergyBar.vue'
  170. import DimensionTasks from '../../components/DimensionTasks.vue'
  171. import DimensionActivities from '../../components/DimensionActivities.vue'
  172. import DimensionProducts from '../../components/DimensionProducts.vue'
  173. import DimensionArticles from '../../components/DimensionArticles.vue'
  174. import FamilyRelationGraph from '../../components/FamilyRelationGraph.vue'
  175. import RadarChart from '../../components/RadarChart.vue'
  176. import HealthDimensionsSection from '../../components/HealthDimensionsSection.vue'
  177. import HealthCheckinCard from '../../components/HealthCheckinCard.vue'
  178. import AIFloatingAvatar from '../../components/AIFloatingAvatar.vue'
  179. import { getVisibleSections, getEnergyOverview, getChildren, getTodayTasksByCategory, getActivityList, getProductsByDomain, getFamilyEnergySandbox, getVisibleFamilyMembers, getDimensionOverview, getFeaturedArticles } from '../../utils/api.js'
  180. import config from '../../config.js'
  181. var BASE_URL = config.api('')
  182. const healthRequest = function(url, data) {
  183. var token = uni.getStorageSync('token')
  184. return new Promise(function(resolve, reject) {
  185. uni.request({
  186. url: BASE_URL + url,
  187. method: 'POST',
  188. data: data,
  189. header: {
  190. 'Content-Type': 'application/json',
  191. 'Authorization': token ? 'Bearer ' + token : ''
  192. },
  193. success: function(res) {
  194. if (res.data.code === 200) {
  195. resolve(res.data)
  196. } else {
  197. reject(res.data)
  198. }
  199. },
  200. fail: function(err) {
  201. reject(err)
  202. }
  203. })
  204. })
  205. }
  206. export default {
  207. components: { TabTransition, PageBanner, LoginGuideCard, FamilyEnergyBar, DimensionTasks, DimensionActivities, DimensionProducts, DimensionArticles, FamilyRelationGraph, RadarChart, HealthDimensionsSection, HealthCheckinCard, AIFloatingAvatar },
  208. data() {
  209. return {
  210. isLoggedIn: false,
  211. role: null,
  212. showTabTransition: true,
  213. activeChildId: null,
  214. activeChildName: '',
  215. userName: '',
  216. children: [],
  217. familyMembersVisible: [],
  218. sandboxData: null,
  219. dualDimension: null,
  220. visibleSections: [],
  221. latestReport: null,
  222. dimensionTasks: [],
  223. dimensionActivities: [],
  224. dimensionProducts: [],
  225. bodyArticles: [],
  226. dimensionData: null,
  227. /** Mock 平均值(7维):生长发育/睡眠质量/视力健康/免疫力/营养均衡/肠胃健康/运动活力 */
  228. radarAvgScores: [72, 68, 75, 70, 73, 69, 71],
  229. funcList: [
  230. { icon: '\u{1F3C3}', label: '运动', needLogin: true, page: '/pages/body/exercise-record' },
  231. { icon: '\u{1F957}', label: '饮食', needLogin: true, page: '/pages/body/meal-record' },
  232. { icon: '\u{1F634}', label: '作息', needLogin: true, page: '/pages/body/sleep-record' },
  233. { icon: '\u{1F9D8}', label: '冥想', needLogin: true, page: '/pages/health/meditation-index' },
  234. { icon: '\u{1F4CA}', label: '舌诊', needLogin: true, page: '/pages/health/report-upload?reportType=tongue' }
  235. ]
  236. }
  237. },
  238. computed: {
  239. reportTypeLabel: function() {
  240. if (!this.latestReport) return ''
  241. return this.latestReport.reportType === 'gut_flora' ? '肠道检测报告' : '体检报告'
  242. },
  243. energyMapForGraph: function() {
  244. var map = {}
  245. if (this.sandboxData && this.sandboxData.members) {
  246. for (var i = 0; i < this.sandboxData.members.length; i++) {
  247. var m = this.sandboxData.members[i]
  248. map[m.memberId || m.id] = {
  249. bodyScore: m.bodyScore || 0,
  250. mindScore: m.mindScore || 0,
  251. actionScore: m.actionScore || 0
  252. }
  253. }
  254. }
  255. return map
  256. },
  257. intimacyMapForGraph: function() {
  258. return {}
  259. },
  260. /** 七维雷达图:维度标签(遵守 AGENTS.md 禁止模板内方法调用,使用 computed 缓存) */
  261. radarDimensions: function() {
  262. var dims = this.dimensionData && this.dimensionData.dimensions
  263. if (!dims) return []
  264. var result = []
  265. for (var i = 0; i < dims.length; i++) {
  266. var d = dims[i]
  267. if (!d) continue
  268. result.push({ label: d.label || '', key: d.dimension || '' })
  269. }
  270. return result
  271. },
  272. /** 七维雷达图:分值(对 null/NaN 做归一化,避免 Canvas 计算 NaN 静默失败) */
  273. radarScores: function() {
  274. var dims = this.dimensionData && this.dimensionData.dimensions
  275. if (!dims) return []
  276. var result = []
  277. for (var i = 0; i < dims.length; i++) {
  278. var d = dims[i]
  279. if (!d) { result.push(0); continue }
  280. var s = d.score
  281. if (typeof s !== 'number' || isNaN(s) || s === null) s = 0
  282. result.push(s)
  283. }
  284. return result
  285. }
  286. },
  287. onShow() {
  288. this.showTabTransition = true
  289. var token = uni.getStorageSync('token')
  290. this.isLoggedIn = !!token
  291. if (this.isLoggedIn) {
  292. this.role = uni.getStorageSync('currentRole') || uni.getStorageSync('role') || null
  293. this.userName = uni.getStorageSync('nickname') || uni.getStorageSync('userName') || '家长'
  294. }
  295. this.loadSectionConfig()
  296. if (this.isLoggedIn) {
  297. this.loadChildren()
  298. this.loadFamilyMembersVisible()
  299. }
  300. // 游客也能浏览活动和商品
  301. this.loadDimensionActivities()
  302. this.loadDimensionProducts()
  303. // 游客也能浏览文章
  304. this.loadFeaturedArticles()
  305. this._watchPageReady()
  306. },
  307. methods: {
  308. loadSectionConfig: function() {
  309. var baseSections = ['func_entries']
  310. if (!uni.getStorageSync('token')) {
  311. this.visibleSections = baseSections
  312. return
  313. }
  314. var role = uni.getStorageSync('currentRole') || uni.getStorageSync('role') || null
  315. var self = this
  316. getVisibleSections({ pageKey: 'body', role: role }).then(function(res) {
  317. if (res.code === 200 && res.data) {
  318. var keys = res.data.map(function(s) { return s.sectionKey })
  319. var merged = baseSections.slice()
  320. keys.forEach(function(k) { if (merged.indexOf(k) === -1) merged.push(k) })
  321. self.visibleSections = merged
  322. }
  323. }).catch(function(e) {
  324. self.visibleSections = ['func_entries']
  325. })
  326. },
  327. loadChildren: function() {
  328. var self = this
  329. getChildren().then(function(res) {
  330. if (res.code === 200 && res.data) {
  331. self.children = res.data
  332. var savedChildId = uni.getStorageSync('currentChildId')
  333. var found = false
  334. for (var i = 0; i < self.children.length; i++) {
  335. if (self.children[i].childId === savedChildId) {
  336. self.activeChildId = savedChildId
  337. self.activeChildName = self.children[i].name || self.children[i].nickname || '孩子'
  338. found = true
  339. break
  340. }
  341. }
  342. if (!found && self.children.length > 0) {
  343. self.activeChildId = self.children[0].childId
  344. self.activeChildName = self.children[0].name || self.children[0].nickname || '孩子'
  345. uni.setStorageSync('currentChildId', self.activeChildId)
  346. }
  347. if (self.activeChildId) {
  348. self.loadDashboardData()
  349. }
  350. }
  351. }).catch(function(e) {
  352. console.log('获取孩子列表失败', e)
  353. })
  354. },
  355. loadDashboardData: function() {
  356. if (!this.activeChildId) return
  357. this.loadHealthReport()
  358. this.loadEnergyData()
  359. this.loadDimensionTasks()
  360. this.loadDimensionActivities()
  361. this.loadDimensionProducts()
  362. this.loadSandboxData()
  363. this.loadHealthDimensions()
  364. },
  365. loadHealthDimensions: function() {
  366. var memberId = this.activeChildId || (this.userInfo && this.userInfo.userId)
  367. if (!memberId) {
  368. console.log('[body] loadHealthDimensions skipped: no memberId')
  369. return
  370. }
  371. var self = this
  372. getDimensionOverview({ memberId: memberId }).then(function(res) {
  373. if (res && res.data) {
  374. self.dimensionData = res.data
  375. console.log('[body] dimensionData loaded:', res.data.dimensions ? res.data.dimensions.length : 0, 'dimensions')
  376. } else if (res) {
  377. console.warn('[body] dimension API error:', res.message || res.code)
  378. }
  379. }).catch(function(e) {
  380. console.error('[body] loadHealthDimensions failed', e)
  381. })
  382. },
  383. loadSandboxData: function() {
  384. var self = this
  385. getFamilyEnergySandbox().then(function(res) {
  386. if (res && res.data) {
  387. self.sandboxData = res.data
  388. }
  389. }).catch(function(e) {
  390. console.log('获取沙盘数据失败', e)
  391. })
  392. },
  393. loadHealthReport: function() {
  394. var self = this
  395. healthRequest('/api/health/report/latest', { childId: this.activeChildId }).then(function(res) {
  396. if (res.data) {
  397. self.latestReport = res.data
  398. }
  399. }).catch(function(e) {
  400. console.log('获取健康报告失败', e)
  401. })
  402. },
  403. loadEnergyData: function() {
  404. var self = this
  405. getEnergyOverview(this.activeChildId).then(function(res) {
  406. if (res && res.data) {
  407. var dims = res.data.dimensions
  408. if (dims && dims.length > 0) {
  409. for (var i = 0; i < dims.length; i++) {
  410. if (dims[i].code === 'body') {
  411. self.dualDimension = dims[i]
  412. break
  413. }
  414. }
  415. }
  416. }
  417. }).catch(function(e) {
  418. console.log('获取能量概览失败', e)
  419. })
  420. },
  421. loadDimensionTasks: function() {
  422. var self = this
  423. if (!this.activeChildId) return
  424. getTodayTasksByCategory(this.activeChildId, 'body').then(function(res) {
  425. if (res && res.data) {
  426. self.dimensionTasks = Array.isArray(res.data) ? res.data.slice(0, 5) : []
  427. }
  428. }).catch(function() { self.dimensionTasks = [] })
  429. },
  430. loadDimensionActivities: function() {
  431. var self = this
  432. getActivityList({ dimensionCode: 'body', page: 1, size: 3 }).then(function(res) {
  433. if (res && res.data) {
  434. var list = Array.isArray(res.data) ? res.data : (res.data.records || [])
  435. self.dimensionActivities = list.slice(0, 3)
  436. }
  437. }).catch(function() { self.dimensionActivities = [] })
  438. },
  439. loadDimensionProducts: function() {
  440. var self = this
  441. getProductsByDomain('body', 1, 4).then(function(res) {
  442. if (res && res.data) {
  443. var list = Array.isArray(res.data) ? res.data : (res.data.records || [])
  444. self.dimensionProducts = list.slice(0, 4)
  445. }
  446. }).catch(function() { self.dimensionProducts = [] })
  447. },
  448. loadFeaturedArticles: function() {
  449. var self = this
  450. getFeaturedArticles({ size: 5 }).then(function(res) {
  451. if (res.code === 200 && res.data) {
  452. var gradientColors = [
  453. 'linear-gradient(135deg, #FF8C42, #FFB074)',
  454. 'linear-gradient(135deg, #FF6B9D, #FF9DBD)',
  455. 'linear-gradient(135deg, #6366F1, #818CF8)',
  456. 'linear-gradient(135deg, #10B981, #34D399)',
  457. 'linear-gradient(135deg, #F59E0B, #FBBF24)'
  458. ]
  459. var list = Array.isArray(res.data) ? res.data : (res.data.records || [])
  460. self.bodyArticles = list.map(function(item, index) {
  461. return {
  462. id: item.id,
  463. category: item.categoryName || '身体健康',
  464. categoryColor: gradientColors[index % gradientColors.length],
  465. title: item.title || '',
  466. summary: item.summary || '',
  467. views: item.viewCount ? String(item.viewCount) : '0',
  468. date: item.publishedAt ? item.publishedAt.slice(0, 10) : ''
  469. }
  470. })
  471. }
  472. }.bind(this)).catch(function() {})
  473. },
  474. onChildChanged: function(child) {
  475. this.activeChildId = child.childId
  476. this.activeChildName = child.name || child.nickname || '孩子'
  477. this.loadDashboardData()
  478. },
  479. loadFamilyMembersVisible: function() {
  480. var self = this
  481. getVisibleFamilyMembers().then(function(res) {
  482. self.familyMembersVisible = res.data || []
  483. }).catch(function(e) {
  484. console.log('获取可见家庭成员失败', e)
  485. self.familyMembersVisible = []
  486. })
  487. },
  488. scrollToSection: function(section) {
  489. uni.pageScrollTo({ selector: '#' + section, duration: 300 })
  490. },
  491. onTaskClick: function(task) {
  492. uni.navigateTo({ url: '/pages/tasks/tasks' })
  493. },
  494. goActivityDetail: function(act) {
  495. if (!act || !act.id) return
  496. uni.navigateTo({ url: '/pages/discover-detail/activity-detail/activity-detail?id=' + act.id })
  497. },
  498. goMoreActivities: function() {
  499. uni.navigateTo({ url: '/pages/activity/index' })
  500. },
  501. goProductDetail: function(prod) {
  502. uni.navigateTo({ url: '/pages/discover-detail/product-detail/product-detail?id=' + prod.id })
  503. },
  504. goMoreProducts: function() {
  505. uni.navigateTo({ url: '/pages/shop/index' })
  506. },
  507. goArticleDetail: function(article) {
  508. var id = article.id || article
  509. uni.navigateTo({ url: '/pages/mind-detail/article-detail?id=' + id })
  510. },
  511. goMoreArticles: function() {
  512. uni.navigateTo({ url: '/pages/article-center/index?dimension=body' })
  513. },
  514. goTasks: function() { uni.switchTab({ url: '/pages/tasks/tasks' }) },
  515. goMemberDetail: function(member) {
  516. if (!member || !member.memberId) return
  517. uni.navigateTo({
  518. url: '/pages/member-detail/member-detail?memberId=' + member.memberId + '&memberType=' + (member.memberType || 'child') + '&entrySource=relation-graph&dimensionCode=body'
  519. })
  520. },
  521. goFamilyMembers: function() {
  522. uni.navigateTo({ url: '/pages/profile/family-members' })
  523. },
  524. sectionVisible: function(key) {
  525. return this.visibleSections.length === 0 || this.visibleSections.indexOf(key) !== -1
  526. },
  527. onFuncClick: function(item) {
  528. if (item.needLogin && !this.isLoggedIn) {
  529. uni.showModal({
  530. title: '提示',
  531. content: '请先登录后使用此功能',
  532. success: function(res) {
  533. if (res.confirm) uni.navigateTo({ url: '/pages/login/login' })
  534. }
  535. })
  536. return
  537. }
  538. if (item.page === 'health-dimensions') {
  539. if (!this.activeChildId) {
  540. uni.showToast({ title: '请先选择孩子', icon: 'none' })
  541. return
  542. }
  543. uni.navigateTo({ url: '/pages/body-detail/health-dimensions?childId=' + this.activeChildId })
  544. return
  545. }
  546. if (item.page) uni.navigateTo({ url: item.page })
  547. },
  548. goToHealthReport: function() {
  549. var id = this.activeChildId || uni.getStorageSync('currentChildId')
  550. if (!id) {
  551. uni.showToast({ title: '请先选择成员', icon: 'none' })
  552. return
  553. }
  554. uni.navigateTo({ url: '/pages/body-detail/health-report?childId=' + id })
  555. },
  556. goDimensionDetail: function(dimKey) {
  557. uni.navigateTo({ url: '/pages/body/dimension-detail?dimension=' + dimKey + '&childId=' + this.activeChildId })
  558. },
  559. goToCheckin: function() {
  560. var id = this.activeChildId || uni.getStorageSync('currentChildId')
  561. if (!id) {
  562. uni.showToast({ title: '请先选择成员', icon: 'none' })
  563. return
  564. }
  565. uni.navigateTo({ url: '/pages/body-detail/checkin?childId=' + id })
  566. },
  567. goToNutritionProfile: function() {
  568. var id = this.activeChildId || uni.getStorageSync('currentChildId')
  569. if (!id) {
  570. uni.showToast({ title: '请先选择成员', icon: 'none' })
  571. return
  572. }
  573. uni.navigateTo({ url: '/pages/health/nutrition-profile?childId=' + id })
  574. },
  575. goToUploadReport: function() {
  576. var id = this.activeChildId || uni.getStorageSync('currentChildId') || ''
  577. uni.navigateTo({ url: '/pages/health/report-upload?childId=' + id + '&from=body' })
  578. },
  579. formatDate: function(dateStr) {
  580. if (!dateStr) return '--'
  581. try {
  582. var d = new Date(dateStr)
  583. var y = d.getFullYear()
  584. var m = ('' + (d.getMonth() + 1)).padStart(2, '0')
  585. var day = ('' + d.getDate()).padStart(2, '0')
  586. return y + '-' + m + '-' + day
  587. } catch (e) {
  588. return dateStr
  589. }
  590. },
  591. _watchPageReady() {
  592. var self = this
  593. var unwatch = this.$watch(function() {
  594. if (self.isLoggedIn) {
  595. return !!(self.dualDimension || self.sandboxData || (self.dimensionTasks && self.dimensionTasks.length > 0))
  596. }
  597. return !!(self.dimensionActivities && self.dimensionActivities.length > 0) ||
  598. !!(self.dimensionProducts && self.dimensionProducts.length > 0)
  599. }, function(val) {
  600. if (val) {
  601. if (unwatch) unwatch()
  602. self.showTabTransition = false
  603. }
  604. })
  605. setTimeout(function() {
  606. if (unwatch) unwatch()
  607. self.showTabTransition = false
  608. }, 5000)
  609. }
  610. }
  611. }
  612. </script>
  613. <style scoped>
  614. .body-container {
  615. min-height: 100vh;
  616. background: #f5f7fa;
  617. padding-bottom: 120rpx;
  618. }
  619. /* ===== 功能入口 ===== */
  620. .func-section {
  621. margin: 20rpx 30rpx;
  622. }
  623. .func-grid {
  624. display: flex;
  625. flex-direction: row;
  626. justify-content: space-between;
  627. background: #fff;
  628. border-radius: 20rpx;
  629. padding: 24rpx 16rpx;
  630. box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06);
  631. }
  632. .func-item {
  633. display: flex;
  634. flex-direction: column;
  635. align-items: center;
  636. flex: 1;
  637. }
  638. .func-icon-wrap {
  639. width: 80rpx;
  640. height: 80rpx;
  641. border-radius: 40rpx;
  642. background: linear-gradient(135deg, #D6EAF8, #EBF2FA);
  643. display: flex;
  644. align-items: center;
  645. justify-content: center;
  646. margin-bottom: 10rpx;
  647. }
  648. .func-icon {
  649. font-size: 40rpx;
  650. }
  651. .func-label {
  652. font-size: 22rpx;
  653. color: #666;
  654. font-weight: 500;
  655. }
  656. /* ===== 通用区块 ===== */
  657. .section {
  658. margin: 20rpx 30rpx;
  659. }
  660. .section-header {
  661. display: flex;
  662. flex-direction: row;
  663. align-items: center;
  664. justify-content: space-between;
  665. margin-bottom: 16rpx;
  666. }
  667. .section-title {
  668. font-size: 30rpx;
  669. font-weight: bold;
  670. color: #333;
  671. }
  672. .avg-badge {
  673. font-size: 20rpx;
  674. color: #F97316;
  675. background: #FFF7ED;
  676. padding: 4rpx 12rpx;
  677. border-radius: 20rpx;
  678. margin-left: 12rpx;
  679. }
  680. .section-more {
  681. font-size: 24rpx;
  682. color: #5B9BD5;
  683. }
  684. /* ===== 健康报告 ===== */
  685. .report-card {
  686. background: #fff;
  687. border-radius: 20rpx;
  688. padding: 24rpx;
  689. box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06);
  690. }
  691. .report-type-tag {
  692. display: inline-block;
  693. background: #E8F5E9;
  694. color: #2E7D32;
  695. font-size: 22rpx;
  696. padding: 4rpx 16rpx;
  697. border-radius: 20rpx;
  698. margin-bottom: 16rpx;
  699. }
  700. .report-score-row {
  701. display: flex;
  702. flex-direction: row;
  703. align-items: center;
  704. }
  705. .score-circle {
  706. width: 140rpx;
  707. height: 140rpx;
  708. border-radius: 70rpx;
  709. background: linear-gradient(135deg, #E3F2FD, #BBDEFB);
  710. display: flex;
  711. flex-direction: column;
  712. align-items: center;
  713. justify-content: center;
  714. margin-right: 24rpx;
  715. }
  716. .score-value {
  717. font-size: 48rpx;
  718. font-weight: bold;
  719. color: #1565C0;
  720. }
  721. .score-label {
  722. font-size: 20rpx;
  723. color: #64B5F6;
  724. }
  725. .report-meta {
  726. flex: 1;
  727. }
  728. .meta-item {
  729. display: flex;
  730. flex-direction: row;
  731. align-items: center;
  732. margin-bottom: 12rpx;
  733. }
  734. .meta-label {
  735. font-size: 24rpx;
  736. color: #666;
  737. width: 140rpx;
  738. }
  739. .meta-value {
  740. font-size: 26rpx;
  741. color: #333;
  742. font-weight: 500;
  743. }
  744. .report-date {
  745. font-size: 22rpx;
  746. color: #bbb;
  747. margin-top: 16rpx;
  748. }
  749. .report-empty {
  750. background: #fff;
  751. border-radius: 20rpx;
  752. padding: 40rpx;
  753. text-align: center;
  754. box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06);
  755. }
  756. .empty-tip {
  757. font-size: 28rpx;
  758. color: #999;
  759. display: block;
  760. }
  761. .empty-sub {
  762. font-size: 24rpx;
  763. color: #ccc;
  764. display: block;
  765. margin-top: 8rpx;
  766. }
  767. /* ===== 快捷操作 ===== */
  768. .quick-actions {
  769. display: flex;
  770. flex-direction: row;
  771. margin-top: 30rpx;
  772. }
  773. .action-btn {
  774. flex: 1;
  775. background: #fff;
  776. border-radius: 16rpx;
  777. padding: 24rpx;
  778. display: flex;
  779. flex-direction: column;
  780. align-items: center;
  781. margin-right: 20rpx;
  782. box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06);
  783. }
  784. .action-btn:last-child {
  785. margin-right: 0;
  786. }
  787. .action-icon {
  788. font-size: 48rpx;
  789. margin-bottom: 8rpx;
  790. }
  791. .action-text {
  792. font-size: 26rpx;
  793. color: #666;
  794. }
  795. /* ===== 底部 ===== */
  796. .bottom-spacer {
  797. height: 120rpx;
  798. }
  799. .func-item:active, .action-btn:active {
  800. opacity: 0.7;
  801. }
  802. /* ===== 精准营养 ===== */
  803. .nutrition-section {
  804. margin-top: 20rpx;
  805. }
  806. .nutrition-report-card {
  807. display: flex;
  808. flex-direction: row;
  809. align-items: center;
  810. background: linear-gradient(135deg, #E8F5E9, #F1F8E9);
  811. border-radius: 20rpx;
  812. padding: 28rpx 24rpx;
  813. margin-bottom: 20rpx;
  814. box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06);
  815. }
  816. .nutrition-report-left {
  817. width: 100rpx;
  818. height: 100rpx;
  819. background: rgba(255,255,255,0.7);
  820. border-radius: 20rpx;
  821. display: flex;
  822. align-items: center;
  823. justify-content: center;
  824. margin-right: 20rpx;
  825. flex-shrink: 0;
  826. }
  827. .nutrition-icon-large {
  828. font-size: 52rpx;
  829. }
  830. .nutrition-report-right {
  831. flex: 1;
  832. }
  833. .nutrition-title {
  834. font-size: 30rpx;
  835. font-weight: bold;
  836. color: #2E7D32;
  837. display: block;
  838. margin-bottom: 6rpx;
  839. }
  840. .nutrition-desc {
  841. font-size: 22rpx;
  842. color: #558B2F;
  843. line-height: 1.5;
  844. display: block;
  845. margin-bottom: 10rpx;
  846. }
  847. .nutrition-report-tags {
  848. display: flex;
  849. flex-direction: row;
  850. gap: 10rpx;
  851. }
  852. .nutrition-report-tags .tag {
  853. font-size: 20rpx;
  854. color: #33691E;
  855. background: rgba(255,255,255,0.6);
  856. padding: 4rpx 14rpx;
  857. border-radius: 20rpx;
  858. }
  859. .nutrition-arrow {
  860. font-size: 40rpx;
  861. color: #A5D6A7;
  862. margin-left: 12rpx;
  863. }
  864. .nutrition-links {
  865. margin-bottom: 20rpx;
  866. }
  867. .links-label {
  868. font-size: 26rpx;
  869. font-weight: bold;
  870. color: #333;
  871. display: block;
  872. margin-bottom: 12rpx;
  873. }
  874. .links-row {
  875. display: flex;
  876. flex-direction: row;
  877. gap: 16rpx;
  878. }
  879. .link-item {
  880. flex: 1;
  881. display: flex;
  882. flex-direction: row;
  883. align-items: center;
  884. background: #fff;
  885. border-radius: 16rpx;
  886. padding: 20rpx 18rpx;
  887. box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.04);
  888. gap: 12rpx;
  889. }
  890. .link-item:active {
  891. opacity: 0.7;
  892. }
  893. .link-icon {
  894. font-size: 36rpx;
  895. flex-shrink: 0;
  896. }
  897. .link-texts {
  898. flex: 1;
  899. }
  900. .link-name {
  901. font-size: 24rpx;
  902. font-weight: 500;
  903. color: #333;
  904. display: block;
  905. margin-bottom: 4rpx;
  906. }
  907. .link-desc {
  908. font-size: 20rpx;
  909. color: #999;
  910. display: block;
  911. }
  912. .link-arrow {
  913. font-size: 32rpx;
  914. color: #CCC;
  915. flex-shrink: 0;
  916. }
  917. /* ===== 推荐阅读 ===== */
  918. .article-list {
  919. display: flex;
  920. flex-direction: column;
  921. gap: 16rpx;
  922. }
  923. .article-card {
  924. background: #fff;
  925. border-radius: 16rpx;
  926. padding: 24rpx;
  927. box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06);
  928. }
  929. .article-card:active {
  930. opacity: 0.8;
  931. }
  932. .article-category {
  933. display: inline-block;
  934. padding: 4rpx 14rpx;
  935. border-radius: 8rpx;
  936. margin-bottom: 10rpx;
  937. }
  938. .article-category-text {
  939. font-size: 20rpx;
  940. color: #fff;
  941. }
  942. .article-title {
  943. display: block;
  944. font-size: 28rpx;
  945. font-weight: bold;
  946. color: #333;
  947. margin-bottom: 8rpx;
  948. line-height: 1.4;
  949. }
  950. .article-summary {
  951. display: block;
  952. font-size: 24rpx;
  953. color: #888;
  954. line-height: 1.5;
  955. margin-bottom: 12rpx;
  956. overflow: hidden;
  957. text-overflow: ellipsis;
  958. display: -webkit-box;
  959. -webkit-line-clamp: 2;
  960. -webkit-box-orient: vertical;
  961. }
  962. .article-meta {
  963. display: flex;
  964. align-items: center;
  965. gap: 16rpx;
  966. }
  967. .meta-item {
  968. display: flex;
  969. align-items: center;
  970. gap: 4rpx;
  971. }
  972. .meta-icon {
  973. font-size: 24rpx;
  974. }
  975. .meta-text {
  976. font-size: 22rpx;
  977. color: #999;
  978. }
  979. .article-date {
  980. font-size: 22rpx;
  981. color: #bbb;
  982. }
  983. </style>