index.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294
  1. <template>
  2. <view class="container">
  3. <view v-if="!isLoggedIn">
  4. <PageBanner theme="body" tagline="科学管理全家健康" quote="身体健康是幸福的基础" />
  5. <LoginGuideCard title="登录后查看全家健康数据" :features="['申请专业测评','查看健康报告和诊断结果','了解家人健康状态']" />
  6. </view>
  7. <view v-else>
  8. <PageBanner theme="body" tagline="看见·主动健康闭环" quote="采集→看见→行动" />
  9. <view class="tab-bar">
  10. <view v-for="tab in tabs" :key="tab.code" class="tab-item" :class="{ active: currentTab === tab.code }" @click="switchTab(tab.code)">
  11. <text class="tab-icon">{{ tab.icon }}</text>
  12. <text class="tab-label">{{ tab.label }}</text>
  13. <view v-if="currentTab === tab.code" class="tab-underline" :style="{ background: tab.color }"></view>
  14. </view>
  15. </view>
  16. <view class="section section-chart">
  17. <RadarChart :dimensions="currentDimensions" :scores="currentScores" :fillColor="currentColor" :childName="selectedMemberName" width="640" height="520" />
  18. <view class="chart-empty" v-if="chartIsEmpty">
  19. <text class="empty-text">{{ currentTabLabel }}数据待采集</text>
  20. <text class="empty-sub">上传报告或完成测评后显示</text>
  21. </view>
  22. </view>
  23. <FamilyMemberStrip />
  24. <view class="section section-dc-header">
  25. <view class="dc-title-row">
  26. <text class="dc-icon">🔍</text>
  27. <text class="dc-title">看见 · 健康数据中心</text>
  28. </view>
  29. </view>
  30. <view class="section section-data-collect">
  31. <view class="data-card" @click="goPage('/pages/health/report-upload')">
  32. <view class="data-card-icon" style="background:rgba(249,115,22,0.12)"><text>📋</text></view>
  33. <view class="data-card-info">
  34. <text class="data-card-title">上传健康报告</text>
  35. <text class="data-card-desc">体检/肠道/舌诊报告 · AI智能解读</text>
  36. </view>
  37. <text class="data-card-arrow">›</text>
  38. </view>
  39. <view class="data-card data-card-assessment">
  40. <view class="data-card-head">
  41. <text class="data-card-title">在线测评</text>
  42. <text class="data-card-subtitle">多维度自测评估</text>
  43. </view>
  44. <view v-for="quiz in quizzes" :key="quiz.code" class="quiz-item">
  45. <view class="quiz-info">
  46. <text class="quiz-icon">{{ quiz.icon }}</text>
  47. <text class="quiz-label">{{ quiz.label }}</text>
  48. </view>
  49. <view v-if="quiz.score" class="quiz-score">
  50. <text class="score-value">{{ quiz.score }}</text>
  51. <text class="score-unit">分</text>
  52. </view>
  53. <button v-else class="quiz-btn" @click.stop="goQuiz(quiz.code)">开始</button>
  54. </view>
  55. </view>
  56. </view>
  57. <view class="section section-data-view">
  58. <view class="section-header">
  59. <text class="section-title">最近记录</text>
  60. <text class="section-more" @click="goPage('/pages/health/report-list')">查看全部 ›</text>
  61. </view>
  62. <view v-if="reports.length > 0">
  63. <view class="report-card" v-for="(report, idx) in reports.slice(0, 3)" :key="idx" @click="goReportDetail(report)">
  64. <text class="report-icon">📄</text>
  65. <view class="report-info">
  66. <text class="report-name">{{ report.title || '健康报告' }}</text>
  67. <text class="report-status">{{ report.status === 'resolved' ? '已解析' : '待确认' }}</text>
  68. </view>
  69. <text class="report-arrow">›</text>
  70. </view>
  71. </view>
  72. <view v-else class="empty-hint">
  73. <text class="empty-text">暂无报告和测评结果</text>
  74. <text class="empty-sub">上传报告或完成测评开启健康之旅</text>
  75. </view>
  76. </view>
  77. <view class="section section-plan">
  78. <view class="plan-card">
  79. <view class="plan-head">
  80. <text class="plan-title">📋 健康方案</text>
  81. <button class="plan-btn" @click="goPlanSummary">{{ lastPlan ? '查看方案' : '生成新方案' }}</button>
  82. </view>
  83. <text v-if="lastPlan" class="plan-goal">{{ lastPlan }}</text>
  84. <text v-else class="plan-hint">选维度 + 选成员 + 设定目标 → 智能生成</text>
  85. </view>
  86. </view>
  87. <ArticleBookshelf :articles="healthArticles" :isLoggedIn="true" @articleClick="goArticleDetail" @moreArticles="goMoreArticles" />
  88. </view>
  89. </view>
  90. </template>
  91. <script>
  92. import PageBanner from '../../components/PageBanner.vue'
  93. import LoginGuideCard from '../../components/LoginGuideCard.vue'
  94. import FamilyMemberStrip from '../../components/FamilyMemberStrip.vue'
  95. import RadarChart from '../../components/RadarChart.vue'
  96. import ArticleBookshelf from '../../components/ArticleBookshelf.vue'
  97. export default {
  98. components: { PageBanner, LoginGuideCard, FamilyMemberStrip, RadarChart, ArticleBookshelf },
  99. data() {
  100. return {
  101. isLoggedIn: false,
  102. currentTab: 'body',
  103. selectedMemberName: '',
  104. selectedMemberId: '',
  105. reports: [],
  106. lastPlan: '',
  107. healthArticles: [],
  108. tabs: [
  109. { code: 'body', label: '健康', icon: '🟠', color: '#FF8C42' },
  110. { code: 'cognitive', label: '认知', icon: '🧠', color: '#6366F1' },
  111. { code: 'mental', label: '心理', icon: '💖', color: '#FF6B9D' },
  112. { code: 'relationship', label: '社会性', icon: '🤝', color: '#10B981' }
  113. ],
  114. quizzes: [
  115. { code: 'cognitive', label: '认知能力测评', icon: '🧠', score: null },
  116. { code: 'mental', label: '心理健康测评', icon: '💖', score: null },
  117. { code: 'relationship', label: '关系质量测评', icon: '🤝', score: null }
  118. ]
  119. }
  120. },
  121. computed: {
  122. currentTabLabel: function() {
  123. var tab = this.tabs.filter(function(t) { return t.code === this.currentTab }.bind(this))[0]
  124. return tab ? tab.label : ''
  125. },
  126. currentColor: function() {
  127. var tab = this.tabs.filter(function(t) { return t.code === this.currentTab }.bind(this))[0]
  128. return tab ? tab.color : '#FF8C42'
  129. },
  130. currentDimensions: function() {
  131. if (this.currentTab === 'cognitive') {
  132. return [{ label:'记忆力',key:'memory'},{ label:'注意力',key:'attention'},{ label:'逻辑推理',key:'logic'},{ label:'空间想象',key:'spatial'},{ label:'语言表达',key:'language' }]
  133. }
  134. if (this.currentTab === 'mental') {
  135. return [{ label:'情绪稳定',key:'emotion'},{ label:'抗压能力',key:'stress'},{ label:'自我认知',key:'self'},{ label:'社交意愿',key:'social'},{ label:'幸福感',key:'happiness' }]
  136. }
  137. if (this.currentTab === 'relationship') {
  138. return [{ label:'亲子关系',key:'parent'},{ label:'夫妻关系',key:'spouse'},{ label:'亲友关系',key:'friend'},{ label:'同事关系',key:'colleague'},{ label:'社区参与',key:'community' }]
  139. }
  140. return [{ label:'生长发育',key:'growth'},{ label:'睡眠质量',key:'sleep'},{ label:'视力健康',key:'vision'},{ label:'免疫力',key:'immunity'},{ label:'营养均衡',key:'nutrition'},{ label:'肠胃健康',key:'gut'},{ label:'运动活力',key:'exercise' }]
  141. },
  142. currentScores: function() {
  143. if (this.chartIsEmpty) {
  144. var dims = this.currentDimensions, scores = []
  145. for (var i = 0; i < dims.length; i++) scores.push(0)
  146. return scores
  147. }
  148. if (this.currentTab === 'body') return [72, 68, 75, 80, 70, 78, 82]
  149. var dims = this.currentDimensions, scores = []
  150. for (var i = 0; i < dims.length; i++) scores.push(0)
  151. return scores
  152. },
  153. chartIsEmpty: function() {
  154. if (this.currentTab === 'body') return this.reports.length === 0
  155. var quiz = this.quizzes.filter(function(q) { return q.code === this.currentTab }.bind(this))[0]
  156. return !(quiz && quiz.score)
  157. }
  158. },
  159. onShow() {
  160. this.isLoggedIn = !!uni.getStorageSync('token')
  161. if (this.isLoggedIn) {
  162. this.loadHealthData()
  163. this.loadPlanList()
  164. var stored = uni.getStorageSync('currentChildId') || ''
  165. if (stored) {
  166. this.selectedMemberId = stored
  167. this.selectedMemberName = uni.getStorageSync('currentChildName') || '成员'
  168. }
  169. }
  170. },
  171. methods: {
  172. switchTab: function(code) { this.currentTab = code },
  173. goPage: function(url) { uni.navigateTo({ url: url }) },
  174. goQuiz: function(code) {
  175. var url = '/pages/body-detail/dimension-questionnaire?dim=' + code
  176. if (this.selectedMemberId) url += '&memberId=' + this.selectedMemberId
  177. uni.navigateTo({ url: url })
  178. },
  179. goReportDetail: function(report) {
  180. uni.navigateTo({ url: '/pages/health/report-list' + (report && report.id ? '?reportId=' + report.id : '') })
  181. },
  182. goPlanSummary: function() {
  183. var url = '/pages/health/health-plan-summary'
  184. if (this.selectedMemberId) url += '?memberId=' + this.selectedMemberId
  185. uni.navigateTo({ url: url })
  186. },
  187. loadHealthData: function() {
  188. var self = this
  189. try {
  190. var api = require('../../utils/api.js')
  191. api.getReportList().then(function(res) {
  192. if (res.code === 200 && res.data) {
  193. var list = Array.isArray(res.data) ? res.data : (res.data.list || [])
  194. self.reports = list
  195. }
  196. }).catch(function() {})
  197. api.getFeaturedArticles({ size: 3, dimensionCode: 'body' }).then(function(res) {
  198. if (res.code === 200 && res.data) {
  199. var list = Array.isArray(res.data) ? res.data : (res.data.records || [])
  200. self.healthArticles = list.slice(0, 3) || []
  201. }
  202. }).catch(function() {})
  203. } catch (e) { console.error('加载健康数据失败', e) }
  204. },
  205. loadPlanList: function() {
  206. var self = this
  207. var api = require('../../utils/api.js')
  208. api.getHealthPlanList({}).then(function(res) {
  209. if (res && res.code === 200 && res.data) {
  210. var list = res.data.list || res.data
  211. if (Array.isArray(list) && list.length > 0) self.lastPlan = list[0].goal || ''
  212. }
  213. }).catch(function() {})
  214. },
  215. goArticleDetail: function(article) {
  216. var id = article && article.id || article
  217. uni.navigateTo({ url: '/pages/article-center/article-detail?id=' + id })
  218. },
  219. goMoreArticles: function() {
  220. uni.navigateTo({ url: '/pages/article-center/index?dimension=body' })
  221. }
  222. }
  223. }
  224. </script>
  225. <style scoped>
  226. .container { background: #F8F8F8; min-height: 100vh; padding-bottom: 120rpx; }
  227. .section { margin: 16rpx 24rpx; }
  228. .tab-bar { display: flex; background: #fff; margin: 0 24rpx 16rpx; border-radius: 16rpx; padding: 8rpx; box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06); }
  229. .tab-item { flex: 1; display: flex; flex-direction: column; align-items: center; padding: 12rpx 0; position: relative; border-radius: 12rpx; }
  230. .tab-item.active { background: #FFF7ED; }
  231. .tab-icon { font-size: 32rpx; margin-bottom: 4rpx; }
  232. .tab-label { font-size: 26rpx; color: #666; font-weight: 500; }
  233. .tab-item.active .tab-label { color: #333; font-weight: bold; }
  234. .tab-underline { width: 40rpx; height: 4rpx; border-radius: 2rpx; margin-top: 6rpx; }
  235. .section-chart { position: relative; margin: 0 24rpx 16rpx; }
  236. .chart-empty { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); text-align: center; z-index: 3; background: rgba(255,255,255,0.85); border-radius: 12rpx; padding: 20rpx 30rpx; }
  237. .chart-empty .empty-text { font-size: 28rpx; color: #333; font-weight: 500; display: block; }
  238. .chart-empty .empty-sub { font-size: 22rpx; color: #999; display: block; margin-top: 4rpx; }
  239. .section-dc-header { margin: 20rpx 24rpx 8rpx; }
  240. .dc-title-row { display: flex; align-items: center; }
  241. .dc-icon { font-size: 32rpx; margin-right: 8rpx; }
  242. .dc-title { font-size: 32rpx; font-weight: bold; color: #333; }
  243. .section-data-collect { margin: 8rpx 24rpx; }
  244. .data-card { display: flex; align-items: center; background: #fff; border-radius: 20rpx; padding: 24rpx; margin-bottom: 12rpx; box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06); }
  245. .data-card-icon { width: 88rpx; height: 88rpx; border-radius: 20rpx; display: flex; align-items: center; justify-content: center; margin-right: 20rpx; font-size: 40rpx; }
  246. .data-card-info { flex: 1; }
  247. .data-card-title { font-size: 28rpx; font-weight: bold; color: #333; display: block; margin-bottom: 6rpx; }
  248. .data-card-desc { font-size: 22rpx; color: #999; }
  249. .data-card-arrow { font-size: 36rpx; color: #ccc; }
  250. .data-card-assessment { flex-direction: column; align-items: stretch; padding: 24rpx; }
  251. .data-card-head { display: flex; align-items: center; margin-bottom: 16rpx; }
  252. .data-card-subtitle { font-size: 22rpx; color: #999; margin-left: 12rpx; }
  253. .quiz-item { display: flex; align-items: center; padding: 16rpx 0; border-top: 1rpx solid #f0f0f0; }
  254. .quiz-item:first-of-type { border-top: none; }
  255. .quiz-info { display: flex; align-items: center; flex: 1; }
  256. .quiz-icon { font-size: 36rpx; margin-right: 16rpx; }
  257. .quiz-label { font-size: 28rpx; color: #333; }
  258. .quiz-score { display: flex; align-items: baseline; margin-right: 16rpx; }
  259. .score-value { font-size: 36rpx; font-weight: bold; color: #F97316; }
  260. .score-unit { font-size: 22rpx; color: #999; margin-left: 4rpx; }
  261. .quiz-btn { background: #F97316; color: #fff; font-size: 24rpx; border: none; border-radius: 20rpx; padding: 10rpx 30rpx; height: 56rpx; line-height: 36rpx; margin: 0; }
  262. .section-data-view { margin: 16rpx 24rpx; }
  263. .section-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12rpx; }
  264. .section-title { font-size: 28rpx; font-weight: bold; color: #333; }
  265. .section-more { font-size: 24rpx; color: #F97316; }
  266. .report-card { display: flex; align-items: center; background: #fff; border-radius: 16rpx; padding: 16rpx 20rpx; margin-bottom: 10rpx; }
  267. .report-icon { font-size: 32rpx; margin-right: 16rpx; }
  268. .report-info { flex: 1; }
  269. .report-name { font-size: 24rpx; color: #333; display: block; margin-bottom: 4rpx; }
  270. .report-status { font-size: 20rpx; color: #999; }
  271. .report-arrow { font-size: 28rpx; color: #ccc; }
  272. .empty-hint { text-align: center; padding: 30rpx 0; background: #fff; border-radius: 16rpx; }
  273. .empty-hint .empty-text { font-size: 26rpx; color: #999; display: block; }
  274. .empty-hint .empty-sub { font-size: 22rpx; color: #ccc; display: block; margin-top: 6rpx; }
  275. .section-plan { margin: 8rpx 24rpx 16rpx; }
  276. .plan-card { background: #fff; border-radius: 20rpx; padding: 24rpx; box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06); }
  277. .plan-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12rpx; }
  278. .plan-title { font-size: 28rpx; font-weight: bold; color: #333; }
  279. .plan-btn { background: linear-gradient(135deg, #F97316, #FB923C); color: #fff; font-size: 24rpx; border: none; border-radius: 20rpx; padding: 10rpx 28rpx; height: 56rpx; line-height: 36rpx; margin: 0; }
  280. .plan-goal { font-size: 24rpx; color: #666; line-height: 1.5; display: block; }
  281. .plan-hint { font-size: 22rpx; color: #999; display: block; }
  282. </style>