child-index.vue 38 KB

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