index.vue 28 KB

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