child-index.vue 38 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304
  1. <template>
  2. <view class="container">
  3. <!-- 加载状态 -->
  4. <BaseLoading :loading="loading" text="正在加载..." />
  5. <!-- 撒花庆祝 -->
  6. <ConfettiCelebration :show="showConfetti" />
  7. <!-- 首次登录引导浮层 -->
  8. <OnboardingGuide
  9. role="child"
  10. :show="showOnboarding"
  11. @close="showOnboarding = false" />
  12. <!-- 空状态 - 无孩子 -->
  13. <BaseEmpty
  14. v-if="!loading && !hasChildren"
  15. icon="🌈"
  16. title="欢迎来到知心益家!"
  17. description="让爸爸妈妈帮你添加,开始快乐成长之旅"
  18. >
  19. <template #action>
  20. <PlayfulButton variant="clay" size="lg" @click="goToAddChild">
  21. ➕ 添加我
  22. </PlayfulButton>
  23. </template>
  24. </BaseEmpty>
  25. <!-- ====== 主内容(有孩子时) ====== -->
  26. <view class="main-content" v-if="!loading && hasChildren">
  27. <!-- ===== A) 顶部 Banner ===== -->
  28. <PageBanner theme="home"
  29. tagline="家庭健康成长俱乐部"
  30. quote="给全家的一站式幸福提案" />
  31. <!-- 五维能量沙盘 -->
  32. <WuxingSandbox
  33. v-if="sandboxData"
  34. mode="live"
  35. :scores="sandboxScores"
  36. :overallScore="sandboxData.overallScore || 0"
  37. :members="sandboxData.members || []"
  38. :dimensions="energyDimensions"
  39. :totalEnergy="totalEnergy"
  40. :totalHealthIndex="totalHealthIndex"
  41. starHeight="400rpx"
  42. showDimensionBar
  43. @point-click="onDomainClick" />
  44. <!-- 家庭能量横条 -->
  45. <FamilyEnergyBar
  46. v-if="hasChildren"
  47. dimensionCode="child"
  48. :sandboxData="sandboxData" />
  49. <!-- 连续打卡徽章 -->
  50. <view
  51. class="streak-badge-wrap animate-slide-up animate-stagger-3"
  52. v-if="streakDays > 0"
  53. @click="showStreakDetail"
  54. hover-class="streak-pressed"
  55. >
  56. <text class="streak-fire">🔥</text>
  57. <view class="streak-info">
  58. <text class="streak-days">{{ streakDays }}</text>
  59. <text class="streak-unit">天坚持</text>
  60. </view>
  61. </view>
  62. <!-- ============================================================
  63. B) 积分卡片 — Claymorphism 暖橙黏土风格
  64. ============================================================ -->
  65. <view
  66. class="points-card animate-bounce-in"
  67. @click="goToRewards"
  68. hover-class="points-card-pressed"
  69. >
  70. <!-- 装饰圆点 -->
  71. <view class="points-deco points-deco--1"></view>
  72. <view class="points-deco points-deco--2"></view>
  73. <view class="points-top">
  74. <view class="points-left">
  75. <text class="points-label">🌟 我的星星</text>
  76. <text class="points-number">{{ totalPoints }}</text>
  77. </view>
  78. <view class="points-level-tag">
  79. <text class="level-emoji">{{ levelEmoji }}</text>
  80. <text class="level-name">{{ levelName }}</text>
  81. </view>
  82. </view>
  83. <view class="points-progress">
  84. <text class="progress-hint" v-if="pointsToNext > 0">
  85. 还差 <text class="progress-hint-num">{{ pointsToNext }}</text> 颗星星升到 {{ nextLevelName }}
  86. </text>
  87. <text class="progress-hint" v-else>
  88. 🎉 已经是最高等级啦!
  89. </text>
  90. <view class="progress-track">
  91. <view
  92. class="progress-fill"
  93. :style="'width: ' + levelProgress + '%'"
  94. ></view>
  95. </view>
  96. </view>
  97. </view>
  98. <!-- ============================================================
  99. C) 今日任务 — 可水平滑动的黏土卡片
  100. ============================================================ -->
  101. <view class="tasks-section animate-slide-up animate-stagger-3">
  102. <view class="section-header">
  103. <text class="section-title">📋 今日任务</text>
  104. <text class="section-link" @click="goToTasks">查看全部 →</text>
  105. </view>
  106. <scroll-view
  107. class="tasks-scroll"
  108. scroll-x
  109. enhanced
  110. :show-scrollbar="false"
  111. v-if="todayTasks.length > 0"
  112. >
  113. <view
  114. class="task-clay-card"
  115. v-for="(task, tIdx) in todayTasks"
  116. :key="task.id"
  117. @click="handleTaskClick(task)"
  118. hover-class="task-card-pressed"
  119. :style="'animation-delay: ' + (tIdx * 80) + 'ms'"
  120. >
  121. <view class="task-card-icon-wrap" :class="'task-cat--' + getTaskCategoryClass(task)">
  122. <text class="task-card-icon">{{ getTaskIcon(task) }}</text>
  123. </view>
  124. <text class="task-card-name">{{ task.title }}</text>
  125. <text class="task-card-points" v-if="task.points">+{{ task.points }}⭐</text>
  126. <view class="task-card-badge">
  127. <BaseBadge :variant="getBadgeVariant(task)" size="xs">
  128. {{ getStatusText(task) }}
  129. </BaseBadge>
  130. </view>
  131. </view>
  132. </scroll-view>
  133. <!-- 今日任务空状态 -->
  134. <view class="tasks-empty" v-else @click="goToTasks">
  135. <view class="tasks-empty-icon">🎯</view>
  136. <text class="tasks-empty-title">今天没有任务哦</text>
  137. <text class="tasks-empty-desc">让爸爸妈妈给你布置任务吧~</text>
  138. </view>
  139. </view>
  140. <!-- ============================================================
  141. D) 快捷操作 — 2×2 黏土按钮网格
  142. ============================================================ -->
  143. <view class="quick-section animate-slide-up animate-stagger-4">
  144. <view class="section-header">
  145. <text class="section-title">🚀 快捷入口</text>
  146. </view>
  147. <view class="quick-grid">
  148. <view
  149. class="quick-clay-btn"
  150. @click="goToTasks"
  151. hover-class="quick-btn-pressed"
  152. >
  153. <view class="quick-btn-icon quick-icon--tasks">
  154. <text class="quick-emoji">📋</text>
  155. </view>
  156. <text class="quick-btn-label">我的任务</text>
  157. </view>
  158. <view
  159. class="quick-clay-btn"
  160. @click="goToRewards"
  161. hover-class="quick-btn-pressed"
  162. >
  163. <view class="quick-btn-icon quick-icon--rewards">
  164. <text class="quick-emoji">⭐</text>
  165. </view>
  166. <text class="quick-btn-label">心愿商城</text>
  167. </view>
  168. <view
  169. class="quick-clay-btn"
  170. @click="goToGrowth"
  171. hover-class="quick-btn-pressed"
  172. >
  173. <view class="quick-btn-icon quick-icon--growth">
  174. <text class="quick-emoji">🏆</text>
  175. </view>
  176. <text class="quick-btn-label">成长档案</text>
  177. </view>
  178. <view
  179. class="quick-clay-btn"
  180. @click="goToGames"
  181. hover-class="quick-btn-pressed"
  182. >
  183. <view class="quick-btn-icon quick-icon--games">
  184. <text class="quick-emoji">🎮</text>
  185. </view>
  186. <text class="quick-btn-label">小游戏</text>
  187. </view>
  188. </view>
  189. </view>
  190. <!-- ============================================================
  191. E) 最近成就 — 水平滚动徽章
  192. ============================================================ -->
  193. <view
  194. class="achievement-section animate-slide-up animate-stagger-5"
  195. v-if="earnedBadges.length > 0"
  196. >
  197. <view class="section-header">
  198. <text class="section-title">🏅 最近成就</text>
  199. </view>
  200. <scroll-view
  201. class="achievement-scroll"
  202. scroll-x
  203. enhanced
  204. :show-scrollbar="false"
  205. >
  206. <view
  207. class="achievement-item"
  208. v-for="badge in earnedBadges"
  209. :key="badge.id"
  210. hover-class="achievement-pressed"
  211. >
  212. <view class="achievement-icon-wrap">
  213. <text class="achievement-icon">{{ badge.icon }}</text>
  214. </view>
  215. <text class="achievement-name">{{ badge.name }}</text>
  216. </view>
  217. <!-- 锁定占位徽章 -->
  218. <view
  219. class="achievement-item achievement-item--locked"
  220. v-for="lockedBadge in lockedBadges"
  221. :key="lockedKey(lockedBadge)"
  222. >
  223. <view class="achievement-icon-wrap achievement-icon-wrap--locked">
  224. <text class="achievement-icon achievement-icon--locked">🔒</text>
  225. </view>
  226. <text class="achievement-name achievement-name--locked">{{ lockedBadge.name }}</text>
  227. </view>
  228. </scroll-view>
  229. </view>
  230. <!-- ============================================================
  231. F) 最近能量变动
  232. ============================================================ -->
  233. <view class="energy-log-section animate-slide-up animate-stagger-6" v-if="energyLogs.length > 0">
  234. <view class="section-header">
  235. <text class="section-title">💧 最近能量变动</text>
  236. <text class="section-link" @click="goToEnergyDetail">查看详情 →</text>
  237. </view>
  238. <view class="energy-log-list">
  239. <view class="energy-log-item" v-for="log in energyLogs" :key="log.id">
  240. <view class="log-icon-wrap" :class="'log-icon--' + ((log.amount || 0) >= 0 ? 'up' : 'down')">
  241. <text class="log-icon">{{ (log.amount || 0) >= 0 ? '⬆' : '⬇' }}</text>
  242. </view>
  243. <view class="log-info">
  244. <text class="log-title">{{ log.reason || log.description }}</text>
  245. <text class="log-time">{{ log.createdAt || log.time }}</text>
  246. </view>
  247. <text class="log-amount" :class="(log.amount || 0) >= 0 ? 'log-amount--up' : 'log-amount--down'">
  248. {{ (log.amount || 0) >= 0 ? '+' : '' }}{{ log.amount }}
  249. </text>
  250. </view>
  251. </view>
  252. </view>
  253. <!-- ===== 推荐商品+活动(混合信息流) ===== -->
  254. <RecommendedFeed
  255. v-if="hasChildren"
  256. :products="dimensionProducts"
  257. :activities="dimensionActivities"
  258. @itemClick="onFeedItemClick"
  259. @more="goMoreDiscover" />
  260. <!-- 底部安全留白 -->
  261. <view class="bottom-spacer"></view>
  262. </view>
  263. </view>
  264. </template>
  265. <script>
  266. import { getChildren, getTodayTasks, getEnergyOverview, getEnergyLogs, getActivityList, getProductsByDomain, getFamilyEnergySandbox } from '../../utils/api.js'
  267. import PageBanner from '../../components/PageBanner.vue'
  268. import FamilyEnergyBar from '../../components/FamilyEnergyBar.vue'
  269. import WuxingSandbox from '../../components/wuxing-sandbox.vue'
  270. import RecommendedFeed from '../../components/RecommendedFeed.vue'
  271. import PlayfulCard from '../../components/PlayfulCard.vue'
  272. import PlayfulButton from '../../components/PlayfulButton.vue'
  273. import BaseBadge from '../../components/BaseBadge.vue'
  274. import BaseEmpty from '../../components/BaseEmpty.vue'
  275. import BaseLoading from '../../components/BaseLoading.vue'
  276. import OnboardingGuide from '../../components/OnboardingGuide.vue'
  277. import ConfettiCelebration from '../../components/ConfettiCelebration.vue'
  278. /**
  279. * 等级配置表
  280. * 星星宝宝(0) → 月亮小朋友(200) → 太阳小天使(500) → 宇宙小英雄(1000)
  281. */
  282. var LEVELS = [
  283. { min: 0, max: 200, name: '星星宝宝', nextName: '月亮小朋友', emoji: '⭐' },
  284. { min: 200, max: 500, name: '月亮小朋友', nextName: '太阳小天使', emoji: '🌙' },
  285. { min: 500, max: 1000, name: '太阳小天使', nextName: '宇宙小英雄', emoji: '☀️' },
  286. { min: 1000, max: Infinity, name: '宇宙小英雄', nextName: '👑 MAX', emoji: '🦸' }
  287. ]
  288. /**
  289. * 任务分类 → 图标映射
  290. */
  291. var TASK_ICONS = {
  292. '学习类': '📚',
  293. '阅读类': '📖',
  294. '生活类': '🏠',
  295. '运动类': '⚽',
  296. '家务类': '🧹',
  297. '艺术类': '🎨',
  298. '其他': '⭐'
  299. }
  300. export default {
  301. components: {
  302. PageBanner,
  303. FamilyEnergyBar,
  304. WuxingSandbox,
  305. RecommendedFeed,
  306. PlayfulCard,
  307. PlayfulButton,
  308. BaseBadge,
  309. BaseEmpty,
  310. BaseLoading,
  311. OnboardingGuide,
  312. ConfettiCelebration
  313. },
  314. data() {
  315. return {
  316. loading: true,
  317. showOnboarding: false,
  318. hasChildren: false,
  319. nickname: '',
  320. childId: '',
  321. totalPoints: 0,
  322. streakDays: 0,
  323. completedCount: 0,
  324. totalTaskCount: 0,
  325. todayTasks: [],
  326. badges: [
  327. { id: 1, icon: '🌟', name: '早起鸟', unlocked: false },
  328. { id: 2, icon: '📚', name: '阅读小达人', unlocked: false },
  329. { id: 3, icon: '🏃', name: '运动健将', unlocked: false },
  330. { id: 4, icon: '🎨', name: '小艺术家', unlocked: false },
  331. { id: 5, icon: '🧹', name: '家务小能手', unlocked: false }
  332. ],
  333. _mounted: false,
  334. energyDimensions: [],
  335. totalEnergy: 0,
  336. totalHealthIndex: 0,
  337. energyLogs: [],
  338. sandboxData: null,
  339. dimensionActivities: [],
  340. dimensionProducts: []
  341. }
  342. },
  343. computed: {
  344. // ---------- 能量沙盘 ----------
  345. sandboxScores() {
  346. if (!this.sandboxData) return { body: 0, mind: 0, wisdom: 0, action: 0, wealth: 0 }
  347. return {
  348. body: this.sandboxData.bodyScore || 0,
  349. mind: this.sandboxData.mindScore || 0,
  350. wisdom: this.sandboxData.wisdomScore || 0,
  351. action: this.sandboxData.actionScore || 0,
  352. wealth: this.sandboxData.wealthScore || 0
  353. }
  354. },
  355. // ---------- 等级相关 ----------
  356. currentLevelIndex() {
  357. const pts = this.totalPoints
  358. if (pts >= 1000) return 3
  359. if (pts >= 500) return 2
  360. if (pts >= 200) return 1
  361. return 0
  362. },
  363. levelName() {
  364. return LEVELS[this.currentLevelIndex].name
  365. },
  366. nextLevelName() {
  367. return LEVELS[this.currentLevelIndex].nextName
  368. },
  369. levelEmoji() {
  370. return LEVELS[this.currentLevelIndex].emoji
  371. },
  372. currentLevelMin() {
  373. return LEVELS[this.currentLevelIndex].min
  374. },
  375. nextLevelMax() {
  376. return LEVELS[this.currentLevelIndex].max
  377. },
  378. levelProgress() {
  379. if (this.totalPoints >= 1000) return 100
  380. const range = this.nextLevelMax - this.currentLevelMin
  381. if (range <= 0) return 100
  382. const progress = ((this.totalPoints - this.currentLevelMin) / range) * 100
  383. return Math.min(100, Math.floor(progress))
  384. },
  385. pointsToNext() {
  386. if (this.totalPoints >= 1000) return 0
  387. return this.nextLevelMax - this.totalPoints
  388. },
  389. // ---------- 任务相关 ----------
  390. taskProgress() {
  391. if (this.totalTaskCount === 0) return 0
  392. return Math.floor((this.completedCount / this.totalTaskCount) * 100)
  393. },
  394. // ---------- 庆祝动画 ----------
  395. showConfetti() {
  396. return this.taskProgress >= 100 && this.totalTaskCount > 0
  397. },
  398. // ---------- 头像表情 ----------
  399. avatarEmoji() {
  400. const level = this.currentLevelIndex
  401. if (level >= 3) return '🦸'
  402. if (level >= 2) return '😇'
  403. if (level >= 1) return '🌙'
  404. return '🐣'
  405. },
  406. // ---------- 问候语 ----------
  407. timeGreeting() {
  408. const hour = new Date().getHours()
  409. if (hour < 6) return '🌛 夜深了,还没睡呀'
  410. if (hour < 9) return '🌅 早安!新的一天开始啦'
  411. if (hour < 12) return '☀️ 上午好!今天也要加油哦'
  412. if (hour < 14) return '🌤️ 中午好!吃饱了才有力气'
  413. if (hour < 18) return '🌻 下午好!继续保持好状态'
  414. if (hour < 21) return '🌆 晚上好!今天过得开心吗'
  415. return '🌛 该休息啦,明天继续加油'
  416. },
  417. // ---------- 成就徽章 ----------
  418. earnedBadges() {
  419. return this.badges.filter(function(b) { return b.unlocked })
  420. },
  421. lockedBadges() {
  422. return this.badges.filter(function(b) { return !b.unlocked })
  423. }
  424. },
  425. mounted() {
  426. // 首次登录引导
  427. var onboardingDone = uni.getStorageSync('_onboardingDone')
  428. if (!onboardingDone) {
  429. this.showOnboarding = true
  430. }
  431. this.loadData()
  432. },
  433. onShow() {
  434. if (this._mounted) {
  435. this.loadData()
  436. }
  437. this._mounted = true
  438. },
  439. methods: {
  440. // ======================== 数据加载 ========================
  441. async loadData() {
  442. this.loading = true
  443. try {
  444. const userInfo = uni.getStorageSync('userInfo')
  445. const role = uni.getStorageSync('currentRole') || 'child'
  446. if (role !== 'child') {
  447. uni.switchTab({ url: '/pages/index/index' })
  448. return
  449. }
  450. this.nickname = (userInfo && userInfo.nickname) ? userInfo.nickname : '小朋友'
  451. const childrenRes = await getChildren()
  452. const children = childrenRes.data || []
  453. if (children.length === 0) {
  454. this.hasChildren = false
  455. this.loading = false
  456. return
  457. }
  458. this.hasChildren = true
  459. const child = children[0] || {}
  460. this.childId = child.id || ''
  461. this.totalPoints = child.totalPoints || 0
  462. this.streakDays = child.streakDays || 0
  463. const tasksRes = await getTodayTasks(this.childId)
  464. const rawTasks = tasksRes.data || []
  465. this.todayTasks = rawTasks
  466. this.completedCount = rawTasks.filter(function(t) { return t.status === 'completed' }).length
  467. this.totalTaskCount = rawTasks.length
  468. // 解锁成就
  469. if (this.streakDays >= 7) {
  470. this.unlockBadge(1)
  471. }
  472. if (this.totalPoints >= 1000) {
  473. this.unlockBadge(2)
  474. }
  475. // 按类别统计已完成任务次数,解锁对应徽章
  476. var categoryCompletedCount = { '运动类': 0, '艺术类': 0, '家务类': 0 }
  477. for (var i = 0; i < rawTasks.length; i++) {
  478. var task = rawTasks[i]
  479. if (task.status === 'completed') {
  480. var cat = task.category || '其他'
  481. if (cat === '运动类') {
  482. categoryCompletedCount['运动类']++
  483. } else if (cat === '艺术类') {
  484. categoryCompletedCount['艺术类']++
  485. } else if (cat === '家务类') {
  486. categoryCompletedCount['家务类']++
  487. }
  488. }
  489. }
  490. if (categoryCompletedCount['运动类'] >= 5) {
  491. this.unlockBadge(3)
  492. }
  493. if (categoryCompletedCount['艺术类'] >= 5) {
  494. this.unlockBadge(4)
  495. }
  496. if (categoryCompletedCount['家务类'] >= 5) {
  497. this.unlockBadge(5)
  498. }
  499. // 加载五维能量概览
  500. var energyChildId = this.childId
  501. if (energyChildId) {
  502. try {
  503. var overviewRes = await getEnergyOverview(energyChildId)
  504. if (overviewRes && overviewRes.data) {
  505. this.energyDimensions = overviewRes.data.dimensions || []
  506. this.totalEnergy = overviewRes.data.totalEnergy || 0
  507. this.totalHealthIndex = overviewRes.data.totalHealthIndex || 0
  508. }
  509. } catch (e) {
  510. console.log('获取能量概览失败', e)
  511. }
  512. }
  513. // 加载最近能量变动
  514. if (energyChildId) {
  515. try {
  516. var logsRes = await getEnergyLogs(energyChildId, '', 1, 5)
  517. if (logsRes && logsRes.data) {
  518. this.energyLogs = logsRes.data.records || logsRes.data || []
  519. }
  520. } catch (e) {
  521. console.log('获取能量日志失败', e)
  522. this.energyLogs = []
  523. }
  524. }
  525. // 加载沙盘数据
  526. if (energyChildId) {
  527. try {
  528. var sandboxRes = await getFamilyEnergySandbox()
  529. if (sandboxRes && sandboxRes.data) {
  530. this.sandboxData = sandboxRes.data
  531. }
  532. } catch (e) {
  533. console.log('获取沙盘数据失败', e)
  534. }
  535. }
  536. // 加载维度活动
  537. try {
  538. var actRes = await getActivityList({ dimensionCode: 'child', page: 1, size: 3 })
  539. if (actRes && actRes.data) {
  540. var actList = Array.isArray(actRes.data) ? actRes.data : (actRes.data.records || [])
  541. this.dimensionActivities = actList.slice(0, 3)
  542. }
  543. } catch (e) {
  544. console.log('获取活动数据失败', e)
  545. }
  546. // 加载维度商品
  547. try {
  548. var prodRes = await getProductsByDomain('child', 1, 4)
  549. if (prodRes && prodRes.data) {
  550. var prodList = Array.isArray(prodRes.data) ? prodRes.data : (prodRes.data.records || [])
  551. this.dimensionProducts = prodList.slice(0, 4)
  552. }
  553. } catch (e) {
  554. console.log('获取商品数据失败', e)
  555. }
  556. } catch (e) {
  557. console.error('加载数据失败', e)
  558. uni.showToast({ title: '加载失败', icon: 'none' })
  559. } finally {
  560. this.loading = false
  561. this.$emit('ready')
  562. }
  563. },
  564. unlockBadge(id) {
  565. var badge = this.badges.find(function(b) { return b.id === id })
  566. if (badge) badge.unlocked = true
  567. },
  568. // ======================== 导航方法 ========================
  569. onDomainClick(evt) {
  570. var code = evt && evt.code
  571. if (!code || !this.childId) return
  572. uni.navigateTo({
  573. url: '/pages/energy/detail?childId=' + this.childId + '&code=' + code
  574. })
  575. },
  576. goToRewards() {
  577. this.$emit('navigate', { page: 'rewards' })
  578. uni.switchTab({ url: '/pages/rewards/rewards' })
  579. },
  580. goToTasks() {
  581. this.$emit('navigate', { page: 'tasks' })
  582. uni.switchTab({ url: '/pages/tasks/tasks' })
  583. },
  584. goToGrowth() {
  585. this.$emit('navigate', { page: 'growth' })
  586. uni.navigateTo({ url: '/pages/growth/growth' })
  587. },
  588. goToGames() {
  589. this.$emit('navigate', { page: 'games' })
  590. uni.navigateTo({ url: '/pages/games/games' })
  591. },
  592. goToAddChild() {
  593. this.$emit('navigate', { page: 'add-child' })
  594. uni.navigateTo({ url: '/pages/profile/create-child' })
  595. },
  596. showStreakDetail() {
  597. this.$emit('navigate', { page: 'streak-detail' })
  598. uni.showToast({ title: '连续打卡 ' + this.streakDays + ' 天啦!太棒了!', icon: 'none' })
  599. },
  600. goToEnergyDetail() {
  601. if (this.childId) {
  602. uni.navigateTo({ url: '/pages/energy/detail?childId=' + this.childId + '&code=all' })
  603. }
  604. },
  605. showAchievements() {
  606. uni.showToast({ title: '成就页面开发中', icon: 'none' })
  607. },
  608. switchMode() {
  609. uni.showModal({
  610. title: '切换模式',
  611. content: '确定要切换回家长模式吗?',
  612. success: function(res) {
  613. if (res.confirm) {
  614. uni.switchTab({ url: '/pages/index/index' })
  615. }
  616. }
  617. })
  618. },
  619. goToDomain(domain) {
  620. uni.showToast({ title: '即将上线', icon: 'none' })
  621. },
  622. goToEnergyDetail(code) {
  623. // 跳转到能量详情页(T10 创建)
  624. var childId = this.childId
  625. if (!childId || !code) return
  626. uni.navigateTo({
  627. url: '/pages/energy/detail?childId=' + childId + '&code=' + code
  628. })
  629. },
  630. // ======================== 推荐信息流 ========================
  631. onFeedItemClick: function(item) {
  632. if (item.type === 'product') {
  633. uni.navigateTo({ url: '/pages/discover/product-detail/product-detail?id=' + item.id })
  634. } else if (item.type === 'activity') {
  635. uni.navigateTo({ url: '/pages/discover/activity-detail/activity-detail?id=' + item.id })
  636. }
  637. },
  638. goMoreDiscover: function() {
  639. uni.switchTab({ url: '/pages/discover/index' })
  640. },
  641. // ======================== 任务交互 ========================
  642. handleTaskClick(task) {
  643. if (!task || !task.id) return
  644. this.$emit('navigate', { page: 'task-detail', taskId: task.id })
  645. // 如果任务尚未完成,弹出确认完成
  646. if (task.status !== 'completed' && task.status !== 'overdue') {
  647. uni.showModal({
  648. title: '完成任务',
  649. content: '确定完成「' + task.title + '」吗?',
  650. success: function(res) {
  651. if (res.confirm) {
  652. task.status = 'completed'
  653. // 重新计算完成数
  654. this.completedCount = this.todayTasks.filter(function(t) { return t.status === 'completed' }).length
  655. if (task.points) {
  656. uni.showToast({ title: '🎉 获得 ' + task.points + ' 颗星星!', icon: 'success' })
  657. }
  658. }
  659. }.bind(this)
  660. })
  661. }
  662. },
  663. // ======================== 任务工具方法 ========================
  664. getTaskIcon(task) {
  665. if (!task) return '📝'
  666. var cat = task.category || '其他'
  667. return TASK_ICONS[cat] || '📝'
  668. },
  669. getTaskCategoryClass(task) {
  670. if (!task) return 'default'
  671. var cat = task.category || '其他'
  672. var map = {
  673. '学习类': 'study',
  674. '阅读类': 'reading',
  675. '生活类': 'life',
  676. '运动类': 'sports',
  677. '家务类': 'chores',
  678. '艺术类': 'art'
  679. }
  680. return map[cat] || 'default'
  681. },
  682. getTaskStatus(task) {
  683. if (!task || !task.status) return 'pending'
  684. return task.status
  685. },
  686. getStatusText(task) {
  687. var status = this.getTaskStatus(task)
  688. if (status === 'completed') return '✅ 已完成'
  689. if (status === 'overdue') return '⏰ 已超时'
  690. return '⏳ 待完成'
  691. },
  692. getBadgeVariant(task) {
  693. var status = this.getTaskStatus(task)
  694. if (status === 'completed') return 'success'
  695. if (status === 'overdue') return 'error'
  696. return 'warning'
  697. },
  698. lockedKey(item) {
  699. return 'locked-' + item.id
  700. }
  701. }
  702. }
  703. </script>
  704. <style scoped>
  705. /* ============================================================
  706. 知心益家 — 孩子端首页 Flat Rounded 设计系统
  707. 色彩体系:品牌蓝(#5B9BD5)+浅蓝(#8FC5E8) | 双重阴影 | 4rpx边框
  708. 参考:uni.scss --color-primary, --bg, --shadow-clay, --radius-md
  709. ============================================================ */
  710. /* ----------------------------------------------------------
  711. 容器 & 背景
  712. ---------------------------------------------------------- */
  713. .container {
  714. min-height: 100vh;
  715. background: var(--bg, #F5F9FC);
  716. padding: 0 32rpx 48rpx;
  717. }
  718. .main-content {
  719. padding-top: 16rpx;
  720. }
  721. .bottom-spacer {
  722. height: 48rpx;
  723. }
  724. /* 连续打卡徽章 — 独立行 */
  725. .streak-badge-wrap {
  726. display: flex;
  727. align-items: center;
  728. gap: 10rpx;
  729. justify-content: flex-end;
  730. margin-bottom: 24rpx;
  731. padding-top: 4rpx;
  732. }
  733. .streak-badge {
  734. display: flex;
  735. align-items: center;
  736. gap: 10rpx;
  737. background: linear-gradient(145deg, #F5F9FC, #E0ECF8);
  738. padding: 12rpx 24rpx 12rpx 18rpx;
  739. border-radius: 40rpx;
  740. border: 4rpx solid #8FC5E8;
  741. box-shadow: inset -3rpx -3rpx 8rpx rgba(255,255,255,0.5),
  742. 4rpx 4rpx 14rpx rgba(143,197,232,0.30);
  743. flex-shrink: 0;
  744. transition: transform 0.2s cubic-bezier(0.34, 1.56, 0.64, 1);
  745. }
  746. .streak-badge:active,
  747. .streak-pressed {
  748. transform: scale(0.94);
  749. }
  750. .streak-fire {
  751. font-size: 32rpx;
  752. }
  753. .streak-info {
  754. display: flex;
  755. flex-direction: column;
  756. align-items: center;
  757. line-height: 1.2;
  758. }
  759. .streak-days {
  760. font-size: 32rpx;
  761. font-weight: 800;
  762. color: #1E4A7A;
  763. line-height: 1.1;
  764. }
  765. .streak-unit {
  766. font-size: 18rpx;
  767. font-weight: 600;
  768. color: #2563EB;
  769. }
  770. /* ----------------------------------------------------------
  771. 积分卡片 — Flat Rounded 蓝色渐变
  772. ---------------------------------------------------------- */
  773. .points-card {
  774. position: relative;
  775. background: linear-gradient(145deg, #8FC5E8, #5B9BD5);
  776. border-radius: 32rpx;
  777. padding: 32rpx;
  778. margin-bottom: 28rpx;
  779. border: 4rpx solid #3A7CC4;
  780. box-shadow: inset -4rpx -4rpx 14rpx rgba(255,255,255,0.20),
  781. 8rpx 8rpx 28rpx rgba(91,155,213,0.25);
  782. overflow: hidden;
  783. transition: transform 0.2s cubic-bezier(0.34, 1.56, 0.64, 1);
  784. }
  785. .points-card:active,
  786. .points-card-pressed {
  787. transform: scale(0.97);
  788. box-shadow: inset 2rpx 2rpx 10rpx rgba(0,0,0,0.08),
  789. 4rpx 4rpx 16rpx rgba(91,155,213,0.18);
  790. }
  791. /* 装饰圆 */
  792. .points-deco {
  793. position: absolute;
  794. border-radius: 50%;
  795. background: rgba(255,255,255,0.08);
  796. pointer-events: none;
  797. }
  798. .points-deco--1 {
  799. top: -60rpx;
  800. right: -40rpx;
  801. width: 220rpx;
  802. height: 220rpx;
  803. }
  804. .points-deco--2 {
  805. bottom: -30rpx;
  806. left: -30rpx;
  807. width: 120rpx;
  808. height: 120rpx;
  809. }
  810. .points-top {
  811. display: flex;
  812. justify-content: space-between;
  813. align-items: flex-start;
  814. margin-bottom: 20rpx;
  815. position: relative;
  816. z-index: 1;
  817. }
  818. .points-left {
  819. display: flex;
  820. flex-direction: column;
  821. }
  822. .points-label {
  823. font-size: 24rpx;
  824. color: rgba(255,255,255,0.85);
  825. font-weight: 500;
  826. margin-bottom: 4rpx;
  827. }
  828. .points-number {
  829. font-size: 64rpx;
  830. font-weight: 800;
  831. color: #FFFFFF;
  832. line-height: 1.1;
  833. text-shadow: 0 2rpx 8rpx rgba(0,0,0,0.10);
  834. }
  835. .points-level-tag {
  836. display: flex;
  837. flex-direction: column;
  838. align-items: center;
  839. background: rgba(255,255,255,0.15);
  840. padding: 12rpx 20rpx;
  841. border-radius: 24rpx;
  842. border: 2rpx solid rgba(255,255,255,0.20);
  843. min-width: 120rpx;
  844. }
  845. .level-emoji {
  846. font-size: 36rpx;
  847. line-height: 1;
  848. }
  849. .level-name {
  850. font-size: 22rpx;
  851. font-weight: 700;
  852. color: #FFD700;
  853. margin-top: 4rpx;
  854. }
  855. /* 进度条 */
  856. .points-progress {
  857. position: relative;
  858. z-index: 1;
  859. }
  860. .progress-hint {
  861. display: block;
  862. font-size: 22rpx;
  863. color: rgba(255,255,255,0.75);
  864. margin-bottom: 12rpx;
  865. }
  866. .progress-hint-num {
  867. font-weight: 700;
  868. color: #FFD700;
  869. font-size: 24rpx;
  870. }
  871. .progress-track {
  872. height: 14rpx;
  873. background: rgba(255,255,255,0.20);
  874. border-radius: 7rpx;
  875. overflow: hidden;
  876. border: 2rpx solid rgba(255,255,255,0.10);
  877. }
  878. .progress-fill {
  879. height: 100%;
  880. background: linear-gradient(90deg, #FFD700, #FFB800);
  881. border-radius: 6rpx;
  882. transition: width 0.6s cubic-bezier(0.34, 1.56, 0.64, 1);
  883. }
  884. /* ----------------------------------------------------------
  885. 任务区域 — 水平滚动黏土卡片
  886. ---------------------------------------------------------- */
  887. .tasks-section {
  888. margin-bottom: 28rpx;
  889. }
  890. .section-header {
  891. display: flex;
  892. justify-content: space-between;
  893. align-items: center;
  894. margin-bottom: 16rpx;
  895. }
  896. .section-title {
  897. font-size: 32rpx;
  898. font-weight: 700;
  899. color: var(--text, #1E293B);
  900. }
  901. .section-link {
  902. font-size: 24rpx;
  903. font-weight: 600;
  904. color: var(--color-primary, #5B9BD5);
  905. padding: 8rpx 4rpx;
  906. }
  907. .section-link:active {
  908. opacity: 0.6;
  909. }
  910. /* 任务水平滚动容器 */
  911. .tasks-scroll {
  912. display: flex;
  913. flex-direction: row;
  914. white-space: nowrap;
  915. overflow-x: auto;
  916. padding: 8rpx 0 16rpx;
  917. -webkit-overflow-scrolling: touch;
  918. }
  919. /* 单张任务黏土卡片 */
  920. .task-clay-card {
  921. display: inline-flex;
  922. flex-direction: column;
  923. align-items: center;
  924. width: 200rpx;
  925. min-height: 260rpx;
  926. background: linear-gradient(145deg, #FFFFFF, var(--bg-grey, #EDF2F7));
  927. border-radius: 28rpx;
  928. padding: 24rpx 16rpx 20rpx;
  929. margin-right: 20rpx;
  930. border: 4rpx solid var(--border, #CBD5E1);
  931. box-shadow: inset -4rpx -4rpx 10rpx rgba(255,255,255,0.6),
  932. 6rpx 6rpx 20rpx rgba(91,155,213,0.08);
  933. transition: transform 0.2s cubic-bezier(0.34, 1.56, 0.64, 1);
  934. animation: slideUp 0.4s cubic-bezier(0.16, 1, 0.3, 1) both;
  935. flex-shrink: 0;
  936. }
  937. .task-clay-card:active,
  938. .task-card-pressed {
  939. transform: scale(0.94);
  940. }
  941. /* 任务分类图标容器 */
  942. .task-card-icon-wrap {
  943. width: 88rpx;
  944. height: 88rpx;
  945. border-radius: 24rpx;
  946. display: flex;
  947. align-items: center;
  948. justify-content: center;
  949. margin-bottom: 12rpx;
  950. border: 3rpx solid var(--border-light, #E2E8F0);
  951. box-shadow: inset -2rpx -2rpx 6rpx rgba(255,255,255,0.5),
  952. 3rpx 3rpx 8rpx rgba(91,155,213,0.06);
  953. }
  954. .task-card-icon {
  955. font-size: 42rpx;
  956. line-height: 1;
  957. }
  958. /* 分类图标底色 */
  959. .task-cat--study .task-card-icon-wrap { background: rgba(14,165,233,0.12); border-color: #BAE6FD; }
  960. .task-cat--reading .task-card-icon-wrap { background: rgba(34,197,94,0.12); border-color: #BBF7D0; }
  961. .task-cat--life .task-card-icon-wrap { background: rgba(91,155,213,0.12); border-color: #CBD5E1; }
  962. .task-cat--sports .task-card-icon-wrap { background: rgba(59,130,246,0.12); border-color: #BFDBFE; }
  963. .task-cat--chores .task-card-icon-wrap { background: rgba(245,158,11,0.12); border-color: #FDE68A; }
  964. .task-cat--art .task-card-icon-wrap { background: rgba(236,72,153,0.12); border-color: #FBCFE8; }
  965. .task-cat--default .task-card-icon-wrap { background: rgba(148,163,184,0.12); border-color: #E2E8F0; }
  966. .task-card-name {
  967. font-size: 26rpx;
  968. font-weight: 600;
  969. color: var(--text, #1E293B);
  970. text-align: center;
  971. line-height: 1.3;
  972. overflow: hidden;
  973. text-overflow: ellipsis;
  974. white-space: nowrap;
  975. max-width: 100%;
  976. margin-bottom: 6rpx;
  977. }
  978. .task-card-points {
  979. font-size: 24rpx;
  980. font-weight: 700;
  981. color: var(--color-primary, #5B9BD5);
  982. margin-bottom: 10rpx;
  983. }
  984. .task-card-badge {
  985. display: flex;
  986. align-items: center;
  987. justify-content: center;
  988. min-height: 36rpx;
  989. }
  990. /* 任务空状态 */
  991. .tasks-empty {
  992. display: flex;
  993. flex-direction: column;
  994. align-items: center;
  995. justify-content: center;
  996. padding: 48rpx 32rpx;
  997. background: var(--surface, #FFFFFF);
  998. border-radius: 28rpx;
  999. border: 4rpx solid var(--border, #CBD5E1);
  1000. box-shadow: var(--shadow-clay);
  1001. transition: transform 0.2s ease;
  1002. }
  1003. .tasks-empty:active {
  1004. transform: scale(0.98);
  1005. }
  1006. .tasks-empty-icon {
  1007. font-size: 64rpx;
  1008. margin-bottom: 16rpx;
  1009. }
  1010. .tasks-empty-title {
  1011. font-size: 30rpx;
  1012. font-weight: 600;
  1013. color: var(--text, #1E293B);
  1014. margin-bottom: 8rpx;
  1015. }
  1016. .tasks-empty-desc {
  1017. font-size: 24rpx;
  1018. color: var(--text-secondary, #64748B);
  1019. }
  1020. /* ----------------------------------------------------------
  1021. 快捷操作 — 2×2 黏土按钮网格
  1022. ---------------------------------------------------------- */
  1023. .quick-section {
  1024. margin-bottom: 28rpx;
  1025. }
  1026. .quick-grid {
  1027. display: flex;
  1028. flex-wrap: wrap;
  1029. gap: 20rpx;
  1030. }
  1031. .quick-grid > * {
  1032. width: calc(50% - 10rpx);
  1033. }
  1034. .quick-clay-btn {
  1035. display: flex;
  1036. flex-direction: column;
  1037. align-items: center;
  1038. justify-content: center;
  1039. padding: 28rpx 16rpx;
  1040. background: linear-gradient(145deg, #FFFFFF, var(--bg-grey, #EDF2F7));
  1041. border-radius: 28rpx;
  1042. border: 4rpx solid var(--border, #CBD5E1);
  1043. box-shadow: inset -4rpx -4rpx 10rpx rgba(255,255,255,0.6),
  1044. 6rpx 6rpx 20rpx rgba(91,155,213,0.08);
  1045. transition: transform 0.2s cubic-bezier(0.34, 1.56, 0.64, 1);
  1046. min-height: 180rpx;
  1047. }
  1048. .quick-clay-btn:active,
  1049. .quick-btn-pressed {
  1050. transform: scale(0.94);
  1051. box-shadow: inset 2rpx 2rpx 8rpx rgba(0,0,0,0.06),
  1052. 3rpx 3rpx 10rpx rgba(91,155,213,0.06);
  1053. }
  1054. .quick-btn-icon {
  1055. width: 96rpx;
  1056. height: 96rpx;
  1057. border-radius: 28rpx;
  1058. display: flex;
  1059. align-items: center;
  1060. justify-content: center;
  1061. margin-bottom: 14rpx;
  1062. border: 3rpx solid var(--border-light, #E2E8F0);
  1063. box-shadow: inset -2rpx -2rpx 6rpx rgba(255,255,255,0.5),
  1064. 3rpx 3rpx 8rpx rgba(91,155,213,0.06);
  1065. transition: transform 0.2s ease;
  1066. }
  1067. .quick-emoji {
  1068. font-size: 44rpx;
  1069. line-height: 1;
  1070. }
  1071. /* 各按钮图标色彩 */
  1072. .quick-icon--tasks { background: rgba(14,165,233,0.15); border-color: #BAE6FD; }
  1073. .quick-icon--rewards { background: rgba(245,158,11,0.15); border-color: #FDE68A; }
  1074. .quick-icon--growth { background: rgba(91,155,213,0.15); border-color: #CBD5E1; }
  1075. .quick-icon--games { background: rgba(236,72,153,0.12); border-color: #FBCFE8; }
  1076. .quick-btn-label {
  1077. font-size: 28rpx;
  1078. font-weight: 700;
  1079. color: var(--text, #1E293B);
  1080. }
  1081. /* ----------------------------------------------------------
  1082. 成就区域 — 水平滚动徽章
  1083. ---------------------------------------------------------- */
  1084. .achievement-section {
  1085. margin-bottom: 28rpx;
  1086. }
  1087. .achievement-scroll {
  1088. display: flex;
  1089. flex-direction: row;
  1090. white-space: nowrap;
  1091. overflow-x: auto;
  1092. padding: 8rpx 0 8rpx;
  1093. -webkit-overflow-scrolling: touch;
  1094. }
  1095. .achievement-item {
  1096. display: inline-flex;
  1097. flex-direction: column;
  1098. align-items: center;
  1099. width: 160rpx;
  1100. margin-right: 20rpx;
  1101. transition: transform 0.2s ease;
  1102. flex-shrink: 0;
  1103. }
  1104. .achievement-item:active,
  1105. .achievement-pressed {
  1106. transform: scale(0.92);
  1107. }
  1108. .achievement-icon-wrap {
  1109. width: 100rpx;
  1110. height: 100rpx;
  1111. border-radius: 50%;
  1112. background: linear-gradient(145deg, #DCE8F5, #C8DDF0);
  1113. border: 4rpx solid var(--border, #CBD5E1);
  1114. box-shadow: inset -3rpx -3rpx 8rpx rgba(255,255,255,0.5),
  1115. 4rpx 4rpx 14rpx rgba(91,155,213,0.08);
  1116. display: flex;
  1117. align-items: center;
  1118. justify-content: center;
  1119. margin-bottom: 10rpx;
  1120. }
  1121. .achievement-icon {
  1122. font-size: 44rpx;
  1123. line-height: 1;
  1124. }
  1125. .achievement-name {
  1126. font-size: 24rpx;
  1127. font-weight: 600;
  1128. color: var(--text, #1E293B);
  1129. text-align: center;
  1130. }
  1131. /* 锁定徽章 */
  1132. .achievement-item--locked {
  1133. opacity: 0.50;
  1134. }
  1135. .achievement-icon-wrap--locked {
  1136. background: var(--bg-grey, #EDF2F7);
  1137. border-color: var(--border-light, #E2E8F0);
  1138. box-shadow: none;
  1139. }
  1140. .achievement-icon--locked {
  1141. filter: grayscale(1);
  1142. }
  1143. .achievement-name--locked {
  1144. color: var(--muted, #94A3B8);
  1145. }
  1146. /* ----------------------------------------------------------
  1147. 加载 & 通用
  1148. ---------------------------------------------------------- */
  1149. .loading-state {
  1150. display: flex;
  1151. flex-direction: column;
  1152. align-items: center;
  1153. padding: 200rpx 0;
  1154. }
  1155. /* ----------------------------------------------------------
  1156. 能量变动日志
  1157. ---------------------------------------------------------- */
  1158. .energy-log-section { margin-bottom: 28rpx; }
  1159. .energy-log-list { background: #fff; border-radius: 20rpx; overflow: hidden; box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06); }
  1160. .energy-log-item {
  1161. display: flex; align-items: center;
  1162. padding: 20rpx 24rpx;
  1163. border-bottom: 1rpx solid #f0f0f0;
  1164. }
  1165. .energy-log-item:last-child { border-bottom: none; }
  1166. .log-icon-wrap {
  1167. width: 56rpx; height: 56rpx;
  1168. border-radius: 50%;
  1169. display: flex; align-items: center; justify-content: center;
  1170. margin-right: 16rpx; flex-shrink: 0;
  1171. }
  1172. .log-icon--up { background: rgba(16,185,129,0.12); }
  1173. .log-icon--down { background: rgba(239,68,68,0.12); }
  1174. .log-icon { font-size: 24rpx; }
  1175. .log-info { flex: 1; min-width: 0; }
  1176. .log-title { font-size: 26rpx; color: #333; display: block; }
  1177. .log-time { font-size: 20rpx; color: #999; margin-top: 4rpx; display: block; }
  1178. .log-amount { font-size: 28rpx; font-weight: 700; margin-left: 12rpx; }
  1179. .log-amount--up { color: #10B981; }
  1180. .log-amount--down { color: #EF4444; }
  1181. /* 连续打卡徽章的浮动动画 — 使用 uni.scss 全局 float keyframe */
  1182. .streak-badge {
  1183. animation: float 3s ease-in-out infinite;
  1184. }
  1185. /* 等级标签的金色光晕 — 使用 uni.scss 全局 pulseGlow keyframe */
  1186. .points-level-tag {
  1187. animation: pulseGlow 2.5s ease-in-out infinite;
  1188. }
  1189. </style>