meditation-index.vue 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340
  1. <template>
  2. <view class="page">
  3. <view class="nav-bar">
  4. <view class="nav-back" @tap="goBack">
  5. <text class="back-text">‹ 返回</text>
  6. </view>
  7. <text class="nav-title">冥想记录</text>
  8. </view>
  9. <!-- 成员选择 -->
  10. <view class="member-selector-card">
  11. <text class="member-label">记录人</text>
  12. <picker mode="selector" :range="memberOptions" :value="selectedMemberIndex" @change="onMemberChange">
  13. <view class="picker-display">{{ memberOptions[selectedMemberIndex] }}</view>
  14. </picker>
  15. </view>
  16. <scroll-view class="content" scroll-y>
  17. <!-- 记录表单 -->
  18. <view class="card">
  19. <view class="card-accent"></view>
  20. <view class="card-body">
  21. <text class="card-title">记录冥想</text>
  22. <view class="form-item">
  23. <text class="form-label">冥想类型</text>
  24. <view class="type-options">
  25. <view class="type-chip" v-for="t in meditationTypes" :key="t.value"
  26. :class="{ 'type-active': meditationType === t.value }"
  27. @click="meditationType = t.value">{{ t.label }}</view>
  28. </view>
  29. </view>
  30. <view class="form-item">
  31. <text class="form-label">时长(分钟)</text>
  32. <input class="form-input" v-model="duration" type="number" placeholder="输入冥想时长" />
  33. </view>
  34. <view class="form-item">
  35. <text class="form-label">感受备注</text>
  36. <textarea class="form-textarea" v-model="remark" placeholder="记录当下的感受(可选)" maxlength="200" />
  37. </view>
  38. <view class="submit-btn" @click="submitMeditation">保存记录</view>
  39. </view>
  40. </view>
  41. <!-- 历史记录 -->
  42. <view class="card">
  43. <view class="card-accent"></view>
  44. <view class="card-body">
  45. <text class="card-title">历史记录</text>
  46. <view class="history-list">
  47. <view class="history-item" v-for="item in history" :key="item.id">
  48. <text class="history-type">{{ getTypeLabel(item.meditationType) }}</text>
  49. <text class="history-duration">{{ item.durationMinutes }} 分钟</text>
  50. <text class="history-date">{{ item.createdAt ? item.createdAt.substring(0,10) : '' }}</text>
  51. </view>
  52. <view class="history-empty" v-if="history.length === 0"><text>暂无冥想记录</text></view>
  53. </view>
  54. </view>
  55. </view>
  56. </scroll-view>
  57. </view>
  58. </template>
  59. <script>
  60. import config from '@/config.js'
  61. import { getChildren } from '@/utils/api.js'
  62. export default {
  63. data() {
  64. return {
  65. childId: null,
  66. selectedMemberIndex: 0,
  67. memberOptions: ['我自己'],
  68. memberIds: [],
  69. meditationType: 'breathing',
  70. duration: '',
  71. remark: '',
  72. history: [],
  73. meditationTypes: [
  74. { label: '呼吸觉察', value: 'breathing' },
  75. { label: '身体扫描', value: 'body_scan' },
  76. { label: '慈心冥想', value: 'loving_kindness' },
  77. { label: '行走冥想', value: 'walking' },
  78. { label: '正念观察', value: 'mindful_observing' }
  79. ]
  80. }
  81. },
  82. onLoad: function() {
  83. this.loadChildren()
  84. this.loadHistory()
  85. },
  86. methods: {
  87. loadChildren: function() {
  88. var self = this
  89. getChildren().then(function(res) {
  90. if (res.code === 200 && res.data && res.data.length > 0) {
  91. self.memberIds = res.data.map(function(c) { return c.childId })
  92. self.memberOptions = ['我自己'].concat(res.data.map(function(c) { return c.name || c.nickname || '孩子' }))
  93. }
  94. }).catch(function() {})
  95. },
  96. onMemberChange: function(e) {
  97. this.selectedMemberIndex = parseInt(e.detail.value)
  98. if (this.selectedMemberIndex === 0) {
  99. this.childId = null
  100. } else {
  101. this.childId = this.memberIds[this.selectedMemberIndex - 1]
  102. }
  103. this.loadHistory()
  104. },
  105. submitMeditation: function() {
  106. if (!this.duration || parseInt(this.duration) <= 0) {
  107. uni.showToast({ title: '请输入有效时长', icon: 'none' })
  108. return
  109. }
  110. var data = {
  111. meditationType: this.meditationType,
  112. durationMinutes: parseInt(this.duration),
  113. remark: this.remark
  114. }
  115. if (this.childId) {
  116. data.childId = this.childId
  117. data.memberId = this.childId
  118. }
  119. uni.showLoading({ title: '保存中...' })
  120. healthRequest('/api/health/meditation/create', data).then(function() {
  121. uni.hideLoading()
  122. uni.showToast({ title: '保存成功', icon: 'success' })
  123. this.duration = ''
  124. this.remark = ''
  125. this.loadHistory()
  126. }.bind(this)).catch(function() {
  127. uni.hideLoading()
  128. uni.showToast({ title: '保存失败', icon: 'none' })
  129. })
  130. },
  131. loadHistory: function() {
  132. var self = this
  133. var params = {}
  134. if (this.childId) params.childId = this.childId
  135. healthRequest('/api/health/meditation/list', params).then(function(res) {
  136. if (res.data) self.history = res.data
  137. }).catch(function() { self.history = [] })
  138. },
  139. getTypeLabel: function(type) {
  140. var map = { breathing: '呼吸觉察', body_scan: '身体扫描', loving_kindness: '慈心冥想', walking: '行走冥想', mindful_observing: '正念观察' }
  141. return map[type] || type
  142. },
  143. goBack: function() {
  144. uni.navigateBack()
  145. }
  146. }
  147. }
  148. function healthRequest(url, data) {
  149. var token = uni.getStorageSync('token')
  150. return new Promise(function(resolve, reject) {
  151. uni.request({
  152. url: config.API_BASE_URL + url,
  153. method: 'POST',
  154. data: data,
  155. header: { 'Content-Type': 'application/json', 'Authorization': token ? 'Bearer ' + token : '' },
  156. success: function(res) { if (res.data.code === 200) resolve(res.data); else reject(res.data) },
  157. fail: function(err) { reject(err) }
  158. })
  159. })
  160. }
  161. </script>
  162. <style scoped>
  163. .page {
  164. min-height: 100vh;
  165. background-color: #F5F7FA;
  166. display: flex;
  167. flex-direction: column;
  168. }
  169. .nav-bar {
  170. display: flex;
  171. align-items: center;
  172. justify-content: center;
  173. height: 88rpx;
  174. padding-top: var(--status-bar-height, 0);
  175. background-color: #FFFFFF;
  176. position: relative;
  177. border-bottom: 1rpx solid #f0f0f0;
  178. }
  179. .nav-back {
  180. position: absolute;
  181. left: 30rpx;
  182. top: 50%;
  183. transform: translateY(-50%);
  184. padding: 10rpx;
  185. }
  186. .back-text {
  187. font-size: 28rpx;
  188. color: #333;
  189. }
  190. .nav-title {
  191. font-size: 32rpx;
  192. font-weight: 600;
  193. color: #333;
  194. }
  195. .content {
  196. flex: 1;
  197. padding: 30rpx;
  198. }
  199. .member-selector-card {
  200. margin: 0 30rpx 20rpx;
  201. background: #fff;
  202. border-radius: 20rpx;
  203. padding: 20rpx 24rpx;
  204. box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06);
  205. display: flex;
  206. flex-direction: row;
  207. align-items: center;
  208. justify-content: space-between;
  209. }
  210. .member-label {
  211. font-size: 28rpx;
  212. color: #666;
  213. }
  214. .picker-display {
  215. font-size: 28rpx;
  216. color: #333;
  217. font-weight: 500;
  218. }
  219. .card {
  220. display: flex;
  221. background-color: #FFFFFF;
  222. border-radius: 20rpx;
  223. margin-bottom: 30rpx;
  224. box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06);
  225. overflow: hidden;
  226. }
  227. .card-accent {
  228. width: 8rpx;
  229. background-color: #6366F1;
  230. flex-shrink: 0;
  231. }
  232. .card-body {
  233. flex: 1;
  234. padding: 30rpx;
  235. }
  236. .card-title {
  237. font-size: 30rpx;
  238. font-weight: 600;
  239. color: #333;
  240. margin-bottom: 24rpx;
  241. display: block;
  242. }
  243. .form-item {
  244. margin-bottom: 20rpx;
  245. }
  246. .form-label {
  247. font-size: 26rpx;
  248. color: #666;
  249. display: block;
  250. margin-bottom: 12rpx;
  251. }
  252. .type-options {
  253. display: flex;
  254. flex-direction: row;
  255. flex-wrap: wrap;
  256. }
  257. .type-chip {
  258. padding: 10rpx 24rpx;
  259. border-radius: 24rpx;
  260. font-size: 24rpx;
  261. color: #666;
  262. background: #f0f0f0;
  263. margin-right: 16rpx;
  264. margin-bottom: 12rpx;
  265. }
  266. .type-active {
  267. background: #EDE9FE;
  268. color: #6366F1;
  269. font-weight: 500;
  270. }
  271. .form-input {
  272. width: 100%;
  273. box-sizing: border-box;
  274. height: 80rpx;
  275. padding: 0 24rpx;
  276. border: 2rpx solid #e5e5e5;
  277. border-radius: 12rpx;
  278. font-size: 28rpx;
  279. background: #fafafa;
  280. }
  281. .form-textarea {
  282. width: 100%;
  283. box-sizing: border-box;
  284. height: 160rpx;
  285. padding: 16rpx 24rpx;
  286. border: 2rpx solid #e5e5e5;
  287. border-radius: 12rpx;
  288. font-size: 28rpx;
  289. background: #fafafa;
  290. resize: none;
  291. }
  292. .submit-btn {
  293. background: linear-gradient(135deg, #6366F1, #818CF8);
  294. color: #fff;
  295. font-size: 32rpx;
  296. font-weight: bold;
  297. text-align: center;
  298. padding: 22rpx;
  299. border-radius: 40rpx;
  300. margin-top: 16rpx;
  301. }
  302. .history-list {
  303. display: flex;
  304. flex-direction: column;
  305. }
  306. .history-item {
  307. display: flex;
  308. flex-direction: row;
  309. align-items: center;
  310. padding: 14rpx 0;
  311. border-bottom: 1rpx solid #f5f5f5;
  312. }
  313. .history-type {
  314. font-size: 28rpx;
  315. color: #333;
  316. flex: 1;
  317. }
  318. .history-duration {
  319. font-size: 24rpx;
  320. color: #6366F1;
  321. margin-right: 16rpx;
  322. }
  323. .history-date {
  324. font-size: 22rpx;
  325. color: #999;
  326. }
  327. .history-empty {
  328. text-align: center;
  329. padding: 30rpx;
  330. color: #999;
  331. font-size: 26rpx;
  332. }
  333. </style>