onboarding.vue 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287
  1. <template>
  2. <view class="container">
  3. <!-- 进度统计 -->
  4. <view class="progress-card">
  5. <text class="progress-title">新手任务</text>
  6. <view class="progress-bar-wrap">
  7. <view class="progress-bar">
  8. <view class="progress-fill" :style="{ width: progressPercent + '%' }"></view>
  9. </view>
  10. <text class="progress-text">{{ completedCount }}/{{ taskList.length }} 已完成</text>
  11. </view>
  12. <text class="progress-hint">完成所有任务,领取丰厚奖励!</text>
  13. </view>
  14. <!-- 任务列表 -->
  15. <view class="task-list">
  16. <view class="task-item" v-for="item in taskList" :key="item.taskType">
  17. <view class="task-left">
  18. <view class="task-icon">{{ item.icon }}</view>
  19. <view class="task-info">
  20. <text class="task-name">{{ item.name }}</text>
  21. <text class="task-reward">获得 {{ item.rewardPoints }} 积分 + {{ item.rewardEnergy }} 能量</text>
  22. </view>
  23. </view>
  24. <view class="task-right">
  25. <text class="task-status" v-if="item.status === 'CLAIMED'" :class="'status-claimed'">已领取</text>
  26. <text class="task-status" v-else-if="item.status === 'COMPLETED'" :class="'status-completed'">已完成</text>
  27. <text class="task-status" v-else :class="'status-pending'">待完成</text>
  28. <button
  29. v-if="item.status === 'COMPLETED'"
  30. class="claim-btn"
  31. @click="claimReward(item)"
  32. :disabled="item.claiming"
  33. >领取</button>
  34. </view>
  35. </view>
  36. </view>
  37. <!-- 加载状态 -->
  38. <view class="loading-state" v-if="loading">
  39. <text>加载中...</text>
  40. </view>
  41. </view>
  42. </template>
  43. <script>
  44. import { getOnboardingProgress, claimOnboardingReward } from '../../utils/api.js'
  45. var TASK_META = {
  46. REGISTER: { icon: '📝', name: '完成注册' },
  47. COMPLETE_PROFILE: { icon: '👤', name: '完善个人资料' },
  48. ADD_CHILD: { icon: '👨\u200D👩\u200D👧\u200D👦', name: '添加家庭成员' },
  49. FIRST_CHECKIN: { icon: '✅', name: '完成首次健康打卡' },
  50. FIRST_TASK: { icon: '🎯', name: '完成首次任务' },
  51. BIND_REFERRAL: { icon: '🤝', name: '绑定推荐人' },
  52. COMPLETE_SELF_CHECK: { icon: '🔍', name: '完成一次五维自检' }
  53. }
  54. export default {
  55. data() {
  56. return {
  57. taskList: [],
  58. loading: false
  59. }
  60. },
  61. computed: {
  62. completedCount() {
  63. var count = 0
  64. for (var i = 0; i < this.taskList.length; i++) {
  65. if (this.taskList[i].status === 'COMPLETED' || this.taskList[i].status === 'CLAIMED') {
  66. count++
  67. }
  68. }
  69. return count
  70. },
  71. progressPercent() {
  72. var total = this.taskList.length || 6
  73. return Math.round((this.completedCount / total) * 100)
  74. }
  75. },
  76. onLoad() {
  77. this.loadProgress()
  78. this._onLoadFired = true
  79. },
  80. onShow() {
  81. if (this._onLoadFired) {
  82. this._onLoadFired = false
  83. } else {
  84. this.loadProgress()
  85. }
  86. },
  87. methods: {
  88. async loadProgress() {
  89. this.loading = true
  90. try {
  91. var res = await getOnboardingProgress()
  92. var records = (res.data && res.data.records) || res.data || []
  93. if (!Array.isArray(records)) {
  94. records = []
  95. }
  96. this.taskList = records.map(function(item) {
  97. var meta = TASK_META[item.taskType] || { icon: '📋', name: item.taskType }
  98. return {
  99. taskType: item.taskType,
  100. icon: meta.icon,
  101. name: meta.name,
  102. rewardPoints: item.rewardPoints || 0,
  103. rewardEnergy: item.rewardEnergy || 0,
  104. status: item.status || 'PENDING',
  105. claiming: false
  106. }
  107. })
  108. } catch (e) {
  109. console.error('获取新手任务进度失败', e)
  110. }
  111. this.loading = false
  112. },
  113. async claimReward(item) {
  114. if (item.claiming) return
  115. item.claiming = true
  116. try {
  117. await claimOnboardingReward(item.taskType)
  118. item.status = 'CLAIMED'
  119. uni.showToast({
  120. title: '获得' + item.rewardPoints + '积分+' + item.rewardEnergy + '能量',
  121. icon: 'none',
  122. duration: 2000
  123. })
  124. } catch (e) {
  125. uni.showToast({ title: '领取失败', icon: 'none' })
  126. }
  127. item.claiming = false
  128. }
  129. }
  130. }
  131. </script>
  132. <style scoped>
  133. .container {
  134. padding: 30rpx;
  135. min-height: 100vh;
  136. background: #FFF7ED;
  137. }
  138. /* ===== 进度卡片 ===== */
  139. .progress-card {
  140. background: linear-gradient(135deg, #F97316, #EA580C);
  141. border-radius: 20rpx;
  142. padding: 40rpx;
  143. color: #fff;
  144. margin-bottom: 24rpx;
  145. }
  146. .progress-title {
  147. font-size: 36rpx;
  148. font-weight: bold;
  149. display: block;
  150. margin-bottom: 24rpx;
  151. }
  152. .progress-bar-wrap {
  153. display: flex;
  154. align-items: center;
  155. margin-bottom: 16rpx;
  156. }
  157. .progress-bar {
  158. flex: 1;
  159. height: 16rpx;
  160. background: rgba(255,255,255,0.3);
  161. border-radius: 8rpx;
  162. overflow: hidden;
  163. }
  164. .progress-fill {
  165. height: 100%;
  166. background: #fff;
  167. border-radius: 8rpx;
  168. transition: width 0.3s ease;
  169. }
  170. .progress-text {
  171. font-size: 24rpx;
  172. margin-left: 16rpx;
  173. white-space: nowrap;
  174. }
  175. .progress-hint {
  176. font-size: 22rpx;
  177. opacity: 0.85;
  178. display: block;
  179. }
  180. /* ===== 任务列表 ===== */
  181. .task-list {
  182. background: #fff;
  183. border-radius: 20rpx;
  184. overflow: hidden;
  185. box-shadow: 0 2rpx 12rpx rgba(249,115,22,0.08);
  186. }
  187. .task-item {
  188. display: flex;
  189. align-items: center;
  190. justify-content: space-between;
  191. padding: 28rpx 30rpx;
  192. border-bottom: 1rpx solid #FED7AA;
  193. }
  194. .task-item:last-child {
  195. border-bottom: none;
  196. }
  197. .task-left {
  198. display: flex;
  199. align-items: center;
  200. flex: 1;
  201. margin-right: 20rpx;
  202. }
  203. .task-icon {
  204. width: 72rpx;
  205. height: 72rpx;
  206. border-radius: 16rpx;
  207. background: #FEF3C7;
  208. display: flex;
  209. align-items: center;
  210. justify-content: center;
  211. font-size: 36rpx;
  212. margin-right: 20rpx;
  213. flex-shrink: 0;
  214. }
  215. .task-info {
  216. flex: 1;
  217. }
  218. .task-name {
  219. font-size: 28rpx;
  220. font-weight: 600;
  221. color: #1E293B;
  222. display: block;
  223. margin-bottom: 6rpx;
  224. }
  225. .task-reward {
  226. font-size: 22rpx;
  227. color: #F97316;
  228. display: block;
  229. }
  230. .task-right {
  231. display: flex;
  232. align-items: center;
  233. flex-shrink: 0;
  234. }
  235. .task-status {
  236. font-size: 22rpx;
  237. padding: 6rpx 16rpx;
  238. border-radius: 20rpx;
  239. margin-right: 12rpx;
  240. }
  241. .status-pending {
  242. background: #F1F5F9;
  243. color: #94A3B8;
  244. }
  245. .status-completed {
  246. background: #DCFCE7;
  247. color: #16A34A;
  248. }
  249. .status-claimed {
  250. background: #FEF3C7;
  251. color: #D97706;
  252. }
  253. .claim-btn {
  254. background: #F97316;
  255. color: #fff;
  256. font-size: 24rpx;
  257. border-radius: 30rpx;
  258. height: 56rpx;
  259. line-height: 56rpx;
  260. padding: 0 28rpx;
  261. font-weight: 500;
  262. }
  263. .claim-btn::after {
  264. border: none;
  265. }
  266. .claim-btn:active {
  267. opacity: 0.8;
  268. }
  269. .claim-btn[disabled] {
  270. opacity: 0.5;
  271. }
  272. .loading-state {
  273. text-align: center;
  274. padding: 60rpx;
  275. color: #94A3B8;
  276. font-size: 28rpx;
  277. }
  278. </style>