index.vue 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259
  1. <template>
  2. <view class="container">
  3. <PageBanner theme="action" tagline="干预 · 记录 · 成长" quote="知行合一,每天进步一点点" />
  4. <FamilyMemberStrip />
  5. <view class="reminder-bar" v-if="hasIncomplete">
  6. <text class="reminder-text">⚠ 今日还有 {{ incompleteCount }} 项未记录</text>
  7. <text class="reminder-close" @click="dismissReminder">✕</text>
  8. </view>
  9. <view class="section">
  10. <view class="progress-card">
  11. <view class="progress-header">
  12. <text class="progress-title">今日干预进度</text>
  13. <text class="progress-date">{{ todayDate }}</text>
  14. </view>
  15. <view class="progress-item" v-for="(item, idx) in progressItems" :key="getProgressKey(item)">
  16. <text class="progress-icon">{{ item.icon }}</text>
  17. <text class="progress-label">{{ item.label }}</text>
  18. <text class="progress-status" :class="item.done ? 'status-done' : 'status-pending'">{{ item.done ? '✔ 已完成' : '' }}</text>
  19. <text class="progress-btn" v-if="!item.done" @click="handleProgressClick(item)">{{ item.actionText || '补录' }}</text>
  20. </view>
  21. <view class="progress-footer">
  22. <text class="footer-text">今日完成 {{ doneCount }}/{{ totalCount }}</text>
  23. <text class="footer-streak" v-if="streakDays > 0">🔥 连续打卡 {{ streakDays }} 天</text>
  24. </view>
  25. </view>
  26. </view>
  27. <view class="section">
  28. <view class="tracking-card">
  29. <view class="tracking-tabs">
  30. <view class="tracking-tab" :class="activeTab === tab.key ? 'tab-active' : ''" v-for="tab in trackingTabs" :key="tab.key" @click="switchTab(tab.key)">
  31. <text class="tab-text" :class="activeTab === tab.key ? 'tab-active-text' : ''">{{ tab.label }}</text>
  32. </view>
  33. </view>
  34. <view class="tracking-content">
  35. <view class="tracking-empty" v-if="currentRecords.length === 0">
  36. <text class="empty-icon">📊</text>
  37. <text class="empty-text">还没有记录</text>
  38. <text class="empty-hint">开始记录{{ activeTabLabel }}吧</text>
  39. </view>
  40. <view class="tracking-record" v-for="(rec, rIdx) in currentRecords" :key="getRecordKey(rec)">
  41. <text class="record-date">{{ rec._date || rec._text }}</text>
  42. <text class="record-content">{{ rec._content }}</text>
  43. </view>
  44. <view class="tracking-more" v-if="currentRecords.length > 0" @click="viewAllRecords">查看全部 ›</view>
  45. </view>
  46. </view>
  47. </view>
  48. <view class="section">
  49. <view class="milestone-card">
  50. <text class="section-title-row">🏆 里程碑</text>
  51. <view class="milestone-grid">
  52. <view class="milestone-item">
  53. <text class="milestone-icon">🔥</text>
  54. <text class="milestone-value">{{ streakDays }}天</text>
  55. <text class="milestone-label">连续打卡</text>
  56. </view>
  57. <view class="milestone-item">
  58. <text class="milestone-icon">🎯</text>
  59. <text class="milestone-value">{{ activeChallenges }}</text>
  60. <text class="milestone-label">进行中挑战</text>
  61. </view>
  62. <view class="milestone-item">
  63. <text class="milestone-icon">📈</text>
  64. <text class="milestone-value">{{ completeness }}%</text>
  65. <text class="milestone-label">档案完整度</text>
  66. </view>
  67. </view>
  68. </view>
  69. </view>
  70. <view class="section">
  71. <view class="survey-card">
  72. <text class="section-title-row">📋 定期调研</text>
  73. <view class="survey-pending" v-if="pendingSurvey">
  74. <text class="survey-alert">⚠ 本周还未完成调研</text>
  75. <text class="survey-title-text">{{ pendingSurvey.title }}</text>
  76. <text class="survey-btn" @click="startSurvey">[开始填写]</text>
  77. </view>
  78. <view class="survey-done" v-else-if="allDone"><text class="survey-done-text">✅ 所有调研已完成</text></view>
  79. <view class="survey-history-list" v-if="surveyHistory.length > 0">
  80. <text class="history-label">历史调研:</text>
  81. <view class="history-item" v-for="(h, hIdx) in surveyHistory" :key="getHistoryKey(h)">
  82. <text class="history-date">{{ h._date }}</text>
  83. <text class="history-content">{{ h._content }}</text>
  84. </view>
  85. <text class="survey-more" @click="viewAllHistory">查看全部调研 ›</text>
  86. </view>
  87. <view class="survey-empty" v-else-if="!pendingSurvey"><text class="survey-empty-text">暂无待完成调研</text></view>
  88. </view>
  89. </view>
  90. <DimensionActivities :isLoggedIn="true" @activityClick="goActivityDetail" @moreActivities="goMoreActivities" style="margin-bottom:60rpx" />
  91. </view>
  92. </template>
  93. <script>
  94. import PageBanner from '../../components/PageBanner.vue'
  95. import FamilyMemberStrip from '../../components/FamilyMemberStrip.vue'
  96. import DimensionActivities from '../../components/DimensionActivities.vue'
  97. import { healthCheckinList, getDietDailyRecords, getSurveyStatus, getSurveyHistory } from '../../utils/api.js'
  98. export default {
  99. components: { PageBanner, FamilyMemberStrip, DimensionActivities },
  100. data() { return { memberId: '', todayDate: '', hasIncomplete: false, incompleteCount: 0, progressItems: [], activeTab: 'diet', trackingTabs: [{ key: 'diet', label: '饮食' }, { key: 'exercise', label: '运动' }, { key: 'sleep', label: '睡眠' }, { key: 'emotion', label: '心情' }, { key: 'survey', label: '调研' }], trackingData: { diet: [], exercise: [], sleep: [], emotion: [], survey: [] }, streakDays: 0, activeChallenges: 0, completeness: 0, pendingSurvey: null, allDone: false, surveyHistory: [] } },
  101. computed: {
  102. doneCount: function() { var c = 0; for (var i = 0; i < this.progressItems.length; i++) if (this.progressItems[i] && this.progressItems[i].done) c++; return c },
  103. totalCount: function() { return this.progressItems.length },
  104. activeTabLabel: function() { var m = { diet: '饮食', exercise: '运动', sleep: '睡眠', emotion: '心情', survey: '调研' }; return m[this.activeTab] || '' },
  105. currentRecords: function() { return this.trackingData[this.activeTab] || [] }
  106. },
  107. onLoad(options) {
  108. var ui = uni.getStorageSync('userInfo') || {}
  109. this.memberId = options.memberId || ui.currentMemberId || ui.memberId
  110. var now = new Date()
  111. this.todayDate = now.getFullYear() + '-' + this.pad(now.getMonth() + 1) + '-' + this.pad(now.getDate())
  112. this.loadAllData()
  113. },
  114. onShow() { this.loadAllData() },
  115. methods: {
  116. pad: function(n) { return n < 10 ? '0' + n : '' + n },
  117. getProgressKey(item) { return item && item.label ? item.label : 'p-' + Math.random() },
  118. getRecordKey(rec) { return rec && rec._text ? rec._text + rec._content : 'r-' + Math.random() },
  119. getHistoryKey(h) { return h && h._date ? h._date + h._content : 'h-' + Math.random() },
  120. dismissReminder: function() { this.hasIncomplete = false },
  121. switchTab: function(key) { this.activeTab = key; this.loadTrackingData(key) },
  122. handleProgressClick: function(item) {
  123. var m = { checkin: '/pages/health/daily-checkin', diet: '/pages/health/diet-index', exercise: '/pages/health/exercise-index', sleep: '/pages/health/sleep-index', emotion: '/pages/mind/index' }
  124. var url = m[item.key]
  125. if (item.key === 'survey' && this.pendingSurvey && this.pendingSurvey.templateId) {
  126. url = '/pages/health/survey-questionnaire?templateId=' + this.pendingSurvey.templateId + '&templateTitle=' + encodeURIComponent(this.pendingSurvey.title) + '&templateDimension=' + (this.pendingSurvey.dimension || '')
  127. }
  128. if (url) uni.navigateTo({ url: url })
  129. },
  130. startSurvey: function() { if (this.pendingSurvey && this.pendingSurvey.templateId) uni.navigateTo({ url: '/pages/health/survey-questionnaire?templateId=' + this.pendingSurvey.templateId + '&templateTitle=' + encodeURIComponent(this.pendingSurvey.title) + '&templateDimension=' + (this.pendingSurvey.dimension || '') }) },
  131. viewAllRecords: function() { var m = { diet: '/pages/health/diet-index', exercise: '/pages/health/exercise-index', sleep: '/pages/health/sleep-index', survey: '/pages/health/survey-history?memberId=' + this.memberId, emotion: '/pages/mind/index' }; if (m[this.activeTab]) uni.navigateTo({ url: m[this.activeTab] }) },
  132. viewAllHistory: function() { uni.navigateTo({ url: '/pages/health/survey-history?memberId=' + this.memberId }) },
  133. goActivityDetail: function(act) { uni.navigateTo({ url: '/pages/activity/activity-detail/activity-detail?id=' + act.id }) },
  134. goMoreActivities: function() { uni.navigateTo({ url: '/pages/activity/index' }) },
  135. loadTrackingData: function(key) { return this.trackingData[key] || [] },
  136. async loadAllData() {
  137. var self = this
  138. try { await self.loadProgressItems() } catch(e) {}
  139. try { await self.loadSurveyData() } catch(e) {}
  140. try { await self.loadMilestones() } catch(e) {}
  141. },
  142. async loadProgressItems() {
  143. var self = this
  144. var items = []
  145. // 今日打卡
  146. var checkinDone = false
  147. try {
  148. var ciRes = await healthCheckinList({ memberId: self.memberId, yearMonth: self.todayDate.substring(0, 7) })
  149. if (ciRes.code === 200 && ciRes.data) {
  150. for (var ci = 0; ci < ciRes.data.length; ci++) { if (ciRes.data[ci] && ciRes.data[ci].checkinDate && ciRes.data[ci].checkinDate.substring(0, 10) === self.todayDate) { checkinDone = true; break } }
  151. }
  152. } catch(e) {}
  153. items.push({ key: 'checkin', icon: '✅', label: '今日打卡', done: checkinDone, actionText: '打卡' })
  154. // 饮食
  155. var dietDone = false
  156. try {
  157. var dRes = await getDietDailyRecords({ memberId: self.memberId, date: self.todayDate })
  158. if (dRes.code === 200 && dRes.data && dRes.data.length > 0) dietDone = true
  159. } catch(e) {}
  160. items.push({ key: 'diet', icon: '🍽️', label: '饮食记录', done: dietDone, actionText: '补录' })
  161. items.push({ key: 'exercise', icon: '🏃', label: '运动记录', done: false, actionText: '记录' })
  162. items.push({ key: 'emotion', icon: '😊', label: '心情记录', done: false, actionText: '记录' })
  163. items.push({ key: 'survey', icon: '📋', label: '本周调研', done: self.allDone, actionText: self.allDone ? '' : '开始' })
  164. self.progressItems = items
  165. self.incompleteCount = 0
  166. for (var pi = 0; pi < items.length; pi++) if (items[pi] && !items[pi].done) self.incompleteCount++
  167. self.hasIncomplete = self.incompleteCount > 0
  168. },
  169. async loadSurveyData() {
  170. var self = this
  171. try {
  172. var statusRes = await getSurveyStatus({ memberId: self.memberId })
  173. if (statusRes.code === 200 && statusRes.data) {
  174. var list = statusRes.data
  175. self.pendingSurvey = null; self.allDone = true; var hasAny = false
  176. for (var si = 0; si < list.length; si++) { var s = list[si]; if (!s) continue; hasAny = true; if (s.status === 'pending') { self.pendingSurvey = { templateId: s.templateId, title: s.title || '定期调研', dimension: s.dimension || '' }; self.allDone = false } }
  177. if (!hasAny) { self.pendingSurvey = null; self.allDone = false }
  178. }
  179. } catch(e) {}
  180. try {
  181. var histRes = await getSurveyHistory({ memberId: self.memberId })
  182. if (histRes.code === 200 && histRes.data) {
  183. var hlist = histRes.data; self.surveyHistory = []
  184. for (var hi = 0; hi < Math.min(hlist.length, 3); hi++) { var h = hlist[hi]; self.surveyHistory.push({ _date: h && h.submittedAt ? h.submittedAt.substring(0, 10) : '', _content: '已完成调研' }) }
  185. }
  186. } catch(e) {}
  187. },
  188. async loadMilestones() {
  189. var self = this
  190. try {
  191. var ciRes = await healthCheckinList({ memberId: self.memberId })
  192. if (ciRes.code === 200 && ciRes.data) self.streakDays = Math.min(ciRes.data.length, 30)
  193. } catch(e) {}
  194. self.activeChallenges = 0
  195. self.completeness = self.streakDays > 0 ? Math.min(100, Math.round(self.streakDays * 10)) : 0
  196. }
  197. }
  198. }
  199. </script>
  200. <style scoped>
  201. .container{background:#FFF7ED;min-height:100vh;padding-bottom:120rpx}
  202. .section{margin:16rpx 24rpx}
  203. .reminder-bar{margin:12rpx 24rpx;background:#FFF3E0;border:1rpx solid #FFB74D;border-radius:12rpx;padding:14rpx 20rpx;display:flex;align-items:center;justify-content:space-between}
  204. .reminder-text{font-size:24rpx;color:#E65100}
  205. .reminder-close{font-size:28rpx;color:#999}
  206. .progress-card{background:#fff;border-radius:20rpx;padding:24rpx;box-shadow:0 2rpx 12rpx rgba(0,0,0,0.06)}
  207. .progress-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:16rpx}
  208. .progress-title{font-size:28rpx;font-weight:bold;color:#333}
  209. .progress-date{font-size:22rpx;color:#999}
  210. .progress-item{display:flex;align-items:center;padding:12rpx 0;border-bottom:1rpx solid #f5f5f5}
  211. .progress-item:last-of-type{border-bottom:none}
  212. .progress-icon{font-size:28rpx;margin-right:12rpx;flex-shrink:0}
  213. .progress-label{font-size:26rpx;color:#333;flex:1}
  214. .progress-status{font-size:22rpx;margin-right:8rpx;flex-shrink:0}
  215. .status-done{color:#10B981}
  216. .status-pending{color:#ccc}
  217. .progress-btn{font-size:22rpx;color:#F97316;padding:4rpx 16rpx;border:1rpx solid #F97316;border-radius:20rpx;flex-shrink:0}
  218. .progress-footer{display:flex;justify-content:space-between;align-items:center;margin-top:16rpx;padding-top:16rpx;border-top:1rpx solid #f5f5f5}
  219. .footer-text{font-size:24rpx;color:#666;font-weight:bold}
  220. .footer-streak{font-size:24rpx;color:#F97316;font-weight:bold}
  221. .tracking-card{background:#fff;border-radius:20rpx;padding:24rpx;box-shadow:0 2rpx 12rpx rgba(0,0,0,0.06)}
  222. .tracking-tabs{display:flex;margin-bottom:16rpx}
  223. .tracking-tab{flex:1;text-align:center;padding:10rpx 0;border-radius:20rpx;margin-right:8rpx;background:#FFF7ED}
  224. .tracking-tab:last-child{margin-right:0}
  225. .tab-active{background:#F97316}
  226. .tab-text{font-size:24rpx;color:#666}
  227. .tab-active-text{color:#fff;font-weight:bold}
  228. .tracking-content{min-height:200rpx}
  229. .tracking-empty{display:flex;flex-direction:column;align-items:center;padding:60rpx 0}
  230. .empty-icon{font-size:60rpx;margin-bottom:12rpx}
  231. .empty-text{font-size:26rpx;color:#999}
  232. .empty-hint{font-size:22rpx;color:#ccc;margin-top:8rpx}
  233. .tracking-record{display:flex;align-items:center;padding:10rpx 0;border-bottom:1rpx solid #f5f5f5}
  234. .record-date{font-size:22rpx;color:#999;width:120rpx;flex-shrink:0}
  235. .record-content{font-size:24rpx;color:#333;flex:1}
  236. .tracking-more{text-align:right;font-size:24rpx;color:#F97316;padding:12rpx 0 0 0}
  237. .milestone-card{background:#fff;border-radius:20rpx;padding:24rpx;box-shadow:0 2rpx 12rpx rgba(0,0,0,0.06)}
  238. .section-title-row{font-size:26rpx;font-weight:bold;color:#333;margin-bottom:16rpx;display:block}
  239. .milestone-grid{display:flex;justify-content:space-between}
  240. .milestone-item{display:flex;flex-direction:column;align-items:center;flex:1}
  241. .milestone-icon{font-size:40rpx;margin-bottom:4rpx}
  242. .milestone-value{font-size:28rpx;font-weight:bold;color:#F97316;margin-bottom:2rpx}
  243. .milestone-label{font-size:22rpx;color:#999}
  244. .survey-card{background:#fff;border-radius:20rpx;padding:24rpx;box-shadow:0 2rpx 12rpx rgba(0,0,0,0.06)}
  245. .survey-pending{background:#FFF3E0;border-radius:12rpx;padding:16rpx;margin-bottom:12rpx}
  246. .survey-alert{font-size:24rpx;color:#E65100;font-weight:bold;display:block;margin-bottom:4rpx}
  247. .survey-title-text{font-size:24rpx;color:#333;display:block;margin-bottom:8rpx}
  248. .survey-btn{font-size:22rpx;color:#F97316;font-weight:bold}
  249. .survey-done{padding:8rpx 0}
  250. .survey-done-text{font-size:24rpx;color:#10B981}
  251. .survey-history-list{border-top:1rpx solid #f5f5f5;padding-top:12rpx}
  252. .history-label{font-size:22rpx;color:#999;display:block;margin-bottom:6rpx}
  253. .history-item{display:flex;align-items:center;padding:6rpx 0}
  254. .history-date{font-size:22rpx;color:#999;width:120rpx;flex-shrink:0}
  255. .history-content{font-size:24rpx;color:#666;flex:1}
  256. .survey-more{text-align:right;font-size:22rpx;color:#F97316;padding-top:8rpx}
  257. .survey-empty{padding:12rpx 0}
  258. .survey-empty-text{font-size:24rpx;color:#999}
  259. </style>