water-record.vue 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161
  1. <template>
  2. <view class="water-container">
  3. <view class="nav-bar">
  4. <text class="nav-back" @click="goBack">‹ 返回</text>
  5. <text class="nav-title">饮水记录</text>
  6. <view class="nav-placeholder"></view>
  7. </view>
  8. <view class="section-card">
  9. <view class="section-title">今日饮水</view>
  10. <view class="water-progress">
  11. <view class="water-circle">
  12. <text class="water-amount">{{ todayTotal }}ml</text>
  13. <text class="water-target">/ {{ dailyTarget }}ml</text>
  14. </view>
  15. <view class="progress-bar-bg">
  16. <view class="progress-bar-fill" :style="'width:' + Math.min(100, todayTotal/dailyTarget*100) + '%'"></view>
  17. </view>
  18. </view>
  19. <view class="quick-add-btns">
  20. <text class="quick-btn" @click="addWater(100)">+100ml</text>
  21. <text class="quick-btn" @click="addWater(200)">+200ml</text>
  22. <text class="quick-btn" @click="addWater(300)">+300ml</text>
  23. <text class="quick-btn" @click="addWater(500)">+500ml</text>
  24. </view>
  25. <view class="input-row">
  26. <text class="input-label">自定义</text>
  27. <input class="input-field" v-model="customAmount" type="number" placeholder="输入ml" />
  28. <text class="input-unit" @click="addWater(parseInt(customAmount))">添加</text>
  29. </view>
  30. </view>
  31. <view class="section-card">
  32. <view class="section-title">今日饮水记录</view>
  33. <view class="record-list">
  34. <view class="record-item" v-for="(r, idx) in todayRecords" :key="idx">
  35. <text class="record-icon">💧</text>
  36. <text class="record-amount">{{ r.amountMl }}ml</text>
  37. <text class="record-time">{{ r.createdAt ? r.createdAt.substring(11,16) : '' }}</text>
  38. </view>
  39. <view class="record-empty" v-if="todayRecords.length === 0"><text>今天还没喝水,快去喝水吧!</text></view>
  40. </view>
  41. </view>
  42. <view class="section-card">
  43. <view class="section-title">本周饮水趋势</view>
  44. <view class="week-chart">
  45. <view class="day-bar" v-for="(day, idx) in weekData" :key="idx">
  46. <view class="bar-fill" :style="'height:' + Math.min(100, day.amount/dailyTarget*100) + '%'"></view>
  47. <text class="bar-label">{{ day.label }}</text>
  48. </view>
  49. </view>
  50. </view>
  51. <view class="bottom-spacer"></view>
  52. </view>
  53. </template>
  54. <script>
  55. import config from '@/config.js'
  56. export default {
  57. data() {
  58. return {
  59. childId: null,
  60. customAmount: '',
  61. todayRecords: [],
  62. weekData: [
  63. { label: '周一', amount: 0 }, { label: '周二', amount: 0 }, { label: '周三', amount: 0 },
  64. { label: '周四', amount: 0 }, { label: '周五', amount: 0 }, { label: '周六', amount: 0 }, { label: '周日', amount: 0 }
  65. ],
  66. dailyTarget: 1500
  67. }
  68. },
  69. computed: {
  70. todayTotal: function() {
  71. return this.todayRecords.reduce(function(sum, r) { return sum + (r.amountMl || 0) }, 0)
  72. }
  73. },
  74. onLoad: function(options) {
  75. this.childId = options.childId ? parseInt(options.childId) : null
  76. if (this.childId) this.loadRecords()
  77. },
  78. methods: {
  79. addWater: function(ml) {
  80. if (!ml || ml <= 0) return
  81. uni.showLoading({ title: '记录中...' })
  82. healthRequest('/api/health/water/create', {
  83. childId: this.childId,
  84. memberId: this.childId,
  85. amountMl: ml
  86. }).then(function() {
  87. uni.hideLoading()
  88. this.customAmount = ''
  89. this.loadRecords()
  90. }.bind(this)).catch(function() {
  91. uni.hideLoading()
  92. uni.showToast({ title: '记录失败', icon: 'none' })
  93. })
  94. },
  95. loadRecords: function() {
  96. var self = this
  97. healthRequest('/api/health/water/list', { childId: this.childId }).then(function(res) {
  98. if (res.data) {
  99. self.todayRecords = res.data.today || res.data.list || res.data
  100. if (res.data.week) self.weekData = res.data.week
  101. if (res.data.dailyTarget) self.dailyTarget = res.data.dailyTarget
  102. }
  103. }).catch(function() {})
  104. },
  105. goBack: function() { uni.navigateBack() }
  106. }
  107. }
  108. function healthRequest(url, data) {
  109. var token = uni.getStorageSync('token')
  110. return new Promise(function(resolve, reject) {
  111. uni.request({
  112. url: config.API_BASE_URL + url,
  113. method: 'POST',
  114. data: data,
  115. header: { 'Content-Type': 'application/json', 'Authorization': token ? 'Bearer ' + token : '' },
  116. success: function(res) { if (res.data.code === 200) resolve(res.data); else reject(res.data) },
  117. fail: function(err) { reject(err) }
  118. })
  119. })
  120. }
  121. </script>
  122. <style scoped>
  123. .water-container { min-height: 100vh; background: #f5f7fa; padding-bottom: 60rpx; }
  124. .nav-bar { position: sticky; top: 0; background: #fff; display: flex; flex-direction: row; align-items: center; justify-content: space-between; padding: 20rpx 30rpx; box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.06); z-index: 100; }
  125. .nav-back { font-size: 32rpx; color: #5B9BD5; }
  126. .nav-title { font-size: 32rpx; font-weight: bold; color: #333; }
  127. .nav-placeholder { width: 80rpx; }
  128. .section-card { margin: 20rpx 30rpx; background: #fff; border-radius: 20rpx; padding: 24rpx; box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06); }
  129. .section-title { font-size: 30rpx; font-weight: bold; color: #333; margin-bottom: 20rpx; }
  130. .water-progress { display: flex; flex-direction: column; align-items: center; margin-bottom: 20rpx; }
  131. .water-circle { display: flex; flex-direction: column; align-items: center; margin-bottom: 16rpx; }
  132. .water-amount { font-size: 56rpx; font-weight: bold; color: #2196F3; }
  133. .water-target { font-size: 24rpx; color: #999; }
  134. .progress-bar-bg { width: 100%; height: 20rpx; background: #E3F2FD; border-radius: 10rpx; overflow: hidden; }
  135. .progress-bar-fill { height: 100%; background: linear-gradient(90deg, #42A5F5, #2196F3); border-radius: 10rpx; transition: width 0.3s; }
  136. .quick-add-btns { display: flex; flex-direction: row; justify-content: space-around; margin-bottom: 16rpx; }
  137. .quick-btn { background: #E3F2FD; color: #1565C0; font-size: 26rpx; padding: 12rpx 24rpx; border-radius: 20rpx; }
  138. .input-row { display: flex; flex-direction: row; align-items: center; }
  139. .input-label { font-size: 26rpx; color: #666; width: 100rpx; }
  140. .input-field { flex: 1; background: #f5f7fa; border-radius: 12rpx; padding: 14rpx 16rpx; font-size: 28rpx; }
  141. .input-unit { font-size: 26rpx; color: #2196F3; padding: 14rpx 20rpx; }
  142. .record-list { display: flex; flex-direction: column; }
  143. .record-item { display: flex; flex-direction: row; align-items: center; padding: 10rpx 0; border-bottom: 1rpx solid #f5f5f5; }
  144. .record-icon { font-size: 28rpx; margin-right: 12rpx; }
  145. .record-amount { font-size: 28rpx; color: #333; flex: 1; }
  146. .record-time { font-size: 22rpx; color: #999; }
  147. .record-empty { text-align: center; padding: 30rpx; color: #999; font-size: 26rpx; }
  148. .week-chart { display: flex; flex-direction: row; align-items: flex-end; justify-content: space-around; height: 200rpx; }
  149. .day-bar { display: flex; flex-direction: column; align-items: center; width: 60rpx; }
  150. .bar-fill { width: 40rpx; background: linear-gradient(180deg, #42A5F5, #1565C0); border-radius: 8rpx 8rpx 0 0; min-height: 4rpx; }
  151. .bar-label { font-size: 20rpx; color: #999; margin-top: 8rpx; }
  152. .bottom-spacer { height: 60rpx; }
  153. </style>