child-index.vue 38 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316
  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. this.$emit('ready')
  571. }
  572. },
  573. unlockBadge(id) {
  574. var badge = this.badges.find(function(b) { return b.id === id })
  575. if (badge) badge.unlocked = true
  576. },
  577. // ======================== 导航方法 ========================
  578. onDomainClick(evt) {
  579. var code = evt && evt.code
  580. if (!code || !this.childId) return
  581. uni.navigateTo({
  582. url: '/pages/energy/detail?childId=' + this.childId + '&code=' + code
  583. })
  584. },
  585. goToRewards() {
  586. this.$emit('navigate', { page: 'rewards' })
  587. uni.switchTab({ url: '/pages/rewards/rewards' })
  588. },
  589. goToTasks() {
  590. this.$emit('navigate', { page: 'tasks' })
  591. uni.switchTab({ url: '/pages/tasks/tasks' })
  592. },
  593. goToGrowth() {
  594. this.$emit('navigate', { page: 'growth' })
  595. uni.navigateTo({ url: '/pages/growth/growth' })
  596. },
  597. goToGames() {
  598. this.$emit('navigate', { page: 'games' })
  599. uni.navigateTo({ url: '/pages/games/games' })
  600. },
  601. goToAddChild() {
  602. this.$emit('navigate', { page: 'add-child' })
  603. uni.navigateTo({ url: '/pages/profile/create-child' })
  604. },
  605. showStreakDetail() {
  606. this.$emit('navigate', { page: 'streak-detail' })
  607. uni.showToast({ title: '连续打卡 ' + this.streakDays + ' 天啦!太棒了!', icon: 'none' })
  608. },
  609. goToEnergyDetail() {
  610. if (this.childId) {
  611. uni.navigateTo({ url: '/pages/energy/detail?childId=' + this.childId + '&code=all' })
  612. }
  613. },
  614. showAchievements() {
  615. uni.showToast({ title: '成就页面开发中', icon: 'none' })
  616. },
  617. switchMode() {
  618. uni.showModal({
  619. title: '切换模式',
  620. content: '确定要切换回家长模式吗?',
  621. success: function(res) {
  622. if (res.confirm) {
  623. uni.switchTab({ url: '/pages/index/index' })
  624. }
  625. }
  626. })
  627. },
  628. goToDomain(domain) {
  629. uni.showToast({ title: '即将上线', icon: 'none' })
  630. },
  631. goToEnergyDetail(code) {
  632. // 跳转到能量详情页(T10 创建)
  633. var childId = this.childId
  634. if (!childId || !code) return
  635. uni.navigateTo({
  636. url: '/pages/energy/detail?childId=' + childId + '&code=' + code
  637. })
  638. },
  639. // ======================== 维度导航 ========================
  640. goActivityDetail: function(act) {
  641. if (act && act.id) uni.navigateTo({ url: '/pages/discover/activity-detail/activity-detail?id=' + act.id })
  642. },
  643. goMoreActivities: function() {
  644. uni.navigateTo({ url: '/pages/activity/index' })
  645. },
  646. goProductDetail: function(prod) {
  647. if (prod && prod.id) uni.navigateTo({ url: '/pages/discover/product-detail/product-detail?id=' + prod.id })
  648. },
  649. goMoreProducts: function() {
  650. uni.navigateTo({ url: '/pages/shop/index' })
  651. },
  652. // ======================== 任务交互 ========================
  653. handleTaskClick(task) {
  654. if (!task || !task.id) return
  655. this.$emit('navigate', { page: 'task-detail', taskId: task.id })
  656. // 如果任务尚未完成,弹出确认完成
  657. if (task.status !== 'completed' && task.status !== 'overdue') {
  658. uni.showModal({
  659. title: '完成任务',
  660. content: '确定完成「' + task.title + '」吗?',
  661. success: function(res) {
  662. if (res.confirm) {
  663. task.status = 'completed'
  664. // 重新计算完成数
  665. this.completedCount = this.todayTasks.filter(function(t) { return t.status === 'completed' }).length
  666. if (task.points) {
  667. uni.showToast({ title: '🎉 获得 ' + task.points + ' 颗星星!', icon: 'success' })
  668. }
  669. }
  670. }.bind(this)
  671. })
  672. }
  673. },
  674. // ======================== 任务工具方法 ========================
  675. getTaskIcon(task) {
  676. if (!task) return '📝'
  677. var cat = task.category || '其他'
  678. return TASK_ICONS[cat] || '📝'
  679. },
  680. getTaskCategoryClass(task) {
  681. if (!task) return 'default'
  682. var cat = task.category || '其他'
  683. var map = {
  684. '学习类': 'study',
  685. '阅读类': 'reading',
  686. '生活类': 'life',
  687. '运动类': 'sports',
  688. '家务类': 'chores',
  689. '艺术类': 'art'
  690. }
  691. return map[cat] || 'default'
  692. },
  693. getTaskStatus(task) {
  694. if (!task || !task.status) return 'pending'
  695. return task.status
  696. },
  697. getStatusText(task) {
  698. var status = this.getTaskStatus(task)
  699. if (status === 'completed') return '✅ 已完成'
  700. if (status === 'overdue') return '⏰ 已超时'
  701. return '⏳ 待完成'
  702. },
  703. getBadgeVariant(task) {
  704. var status = this.getTaskStatus(task)
  705. if (status === 'completed') return 'success'
  706. if (status === 'overdue') return 'error'
  707. return 'warning'
  708. },
  709. lockedKey(item) {
  710. return 'locked-' + item.id
  711. }
  712. }
  713. }
  714. </script>
  715. <style scoped>
  716. /* ============================================================
  717. 知心益家 — 孩子端首页 Flat Rounded 设计系统
  718. 色彩体系:品牌蓝(#5B9BD5)+浅蓝(#8FC5E8) | 双重阴影 | 4rpx边框
  719. 参考:uni.scss --color-primary, --bg, --shadow-clay, --radius-md
  720. ============================================================ */
  721. /* ----------------------------------------------------------
  722. 容器 & 背景
  723. ---------------------------------------------------------- */
  724. .container {
  725. min-height: 100vh;
  726. background: var(--bg, #F5F9FC);
  727. padding: 0 32rpx 48rpx;
  728. }
  729. .main-content {
  730. padding-top: 16rpx;
  731. }
  732. .bottom-spacer {
  733. height: 48rpx;
  734. }
  735. /* 连续打卡徽章 — 独立行 */
  736. .streak-badge-wrap {
  737. display: flex;
  738. align-items: center;
  739. gap: 10rpx;
  740. justify-content: flex-end;
  741. margin-bottom: 24rpx;
  742. padding-top: 4rpx;
  743. }
  744. .streak-badge {
  745. display: flex;
  746. align-items: center;
  747. gap: 10rpx;
  748. background: linear-gradient(145deg, #F5F9FC, #E0ECF8);
  749. padding: 12rpx 24rpx 12rpx 18rpx;
  750. border-radius: 40rpx;
  751. border: 4rpx solid #8FC5E8;
  752. box-shadow: inset -3rpx -3rpx 8rpx rgba(255,255,255,0.5),
  753. 4rpx 4rpx 14rpx rgba(143,197,232,0.30);
  754. flex-shrink: 0;
  755. transition: transform 0.2s cubic-bezier(0.34, 1.56, 0.64, 1);
  756. }
  757. .streak-badge:active,
  758. .streak-pressed {
  759. transform: scale(0.94);
  760. }
  761. .streak-fire {
  762. font-size: 32rpx;
  763. }
  764. .streak-info {
  765. display: flex;
  766. flex-direction: column;
  767. align-items: center;
  768. line-height: 1.2;
  769. }
  770. .streak-days {
  771. font-size: 32rpx;
  772. font-weight: 800;
  773. color: #1E4A7A;
  774. line-height: 1.1;
  775. }
  776. .streak-unit {
  777. font-size: 18rpx;
  778. font-weight: 600;
  779. color: #2563EB;
  780. }
  781. /* ----------------------------------------------------------
  782. 积分卡片 — Flat Rounded 蓝色渐变
  783. ---------------------------------------------------------- */
  784. .points-card {
  785. position: relative;
  786. background: linear-gradient(145deg, #8FC5E8, #5B9BD5);
  787. border-radius: 32rpx;
  788. padding: 32rpx;
  789. margin-bottom: 28rpx;
  790. border: 4rpx solid #3A7CC4;
  791. box-shadow: inset -4rpx -4rpx 14rpx rgba(255,255,255,0.20),
  792. 8rpx 8rpx 28rpx rgba(91,155,213,0.25);
  793. overflow: hidden;
  794. transition: transform 0.2s cubic-bezier(0.34, 1.56, 0.64, 1);
  795. }
  796. .points-card:active,
  797. .points-card-pressed {
  798. transform: scale(0.97);
  799. box-shadow: inset 2rpx 2rpx 10rpx rgba(0,0,0,0.08),
  800. 4rpx 4rpx 16rpx rgba(91,155,213,0.18);
  801. }
  802. /* 装饰圆 */
  803. .points-deco {
  804. position: absolute;
  805. border-radius: 50%;
  806. background: rgba(255,255,255,0.08);
  807. pointer-events: none;
  808. }
  809. .points-deco--1 {
  810. top: -60rpx;
  811. right: -40rpx;
  812. width: 220rpx;
  813. height: 220rpx;
  814. }
  815. .points-deco--2 {
  816. bottom: -30rpx;
  817. left: -30rpx;
  818. width: 120rpx;
  819. height: 120rpx;
  820. }
  821. .points-top {
  822. display: flex;
  823. justify-content: space-between;
  824. align-items: flex-start;
  825. margin-bottom: 20rpx;
  826. position: relative;
  827. z-index: 1;
  828. }
  829. .points-left {
  830. display: flex;
  831. flex-direction: column;
  832. }
  833. .points-label {
  834. font-size: 24rpx;
  835. color: rgba(255,255,255,0.85);
  836. font-weight: 500;
  837. margin-bottom: 4rpx;
  838. }
  839. .points-number {
  840. font-size: 64rpx;
  841. font-weight: 800;
  842. color: #FFFFFF;
  843. line-height: 1.1;
  844. text-shadow: 0 2rpx 8rpx rgba(0,0,0,0.10);
  845. }
  846. .points-level-tag {
  847. display: flex;
  848. flex-direction: column;
  849. align-items: center;
  850. background: rgba(255,255,255,0.15);
  851. padding: 12rpx 20rpx;
  852. border-radius: 24rpx;
  853. border: 2rpx solid rgba(255,255,255,0.20);
  854. min-width: 120rpx;
  855. }
  856. .level-emoji {
  857. font-size: 36rpx;
  858. line-height: 1;
  859. }
  860. .level-name {
  861. font-size: 22rpx;
  862. font-weight: 700;
  863. color: #FFD700;
  864. margin-top: 4rpx;
  865. }
  866. /* 进度条 */
  867. .points-progress {
  868. position: relative;
  869. z-index: 1;
  870. }
  871. .progress-hint {
  872. display: block;
  873. font-size: 22rpx;
  874. color: rgba(255,255,255,0.75);
  875. margin-bottom: 12rpx;
  876. }
  877. .progress-hint-num {
  878. font-weight: 700;
  879. color: #FFD700;
  880. font-size: 24rpx;
  881. }
  882. .progress-track {
  883. height: 14rpx;
  884. background: rgba(255,255,255,0.20);
  885. border-radius: 7rpx;
  886. overflow: hidden;
  887. border: 2rpx solid rgba(255,255,255,0.10);
  888. }
  889. .progress-fill {
  890. height: 100%;
  891. background: linear-gradient(90deg, #FFD700, #FFB800);
  892. border-radius: 6rpx;
  893. transition: width 0.6s cubic-bezier(0.34, 1.56, 0.64, 1);
  894. }
  895. /* ----------------------------------------------------------
  896. 任务区域 — 水平滚动黏土卡片
  897. ---------------------------------------------------------- */
  898. .tasks-section {
  899. margin-bottom: 28rpx;
  900. }
  901. .section-header {
  902. display: flex;
  903. justify-content: space-between;
  904. align-items: center;
  905. margin-bottom: 16rpx;
  906. }
  907. .section-title {
  908. font-size: 32rpx;
  909. font-weight: 700;
  910. color: var(--text, #1E293B);
  911. }
  912. .section-link {
  913. font-size: 24rpx;
  914. font-weight: 600;
  915. color: var(--color-primary, #5B9BD5);
  916. padding: 8rpx 4rpx;
  917. }
  918. .section-link:active {
  919. opacity: 0.6;
  920. }
  921. /* 任务水平滚动容器 */
  922. .tasks-scroll {
  923. display: flex;
  924. flex-direction: row;
  925. white-space: nowrap;
  926. overflow-x: auto;
  927. padding: 8rpx 0 16rpx;
  928. -webkit-overflow-scrolling: touch;
  929. }
  930. /* 单张任务黏土卡片 */
  931. .task-clay-card {
  932. display: inline-flex;
  933. flex-direction: column;
  934. align-items: center;
  935. width: 200rpx;
  936. min-height: 260rpx;
  937. background: linear-gradient(145deg, #FFFFFF, var(--bg-grey, #EDF2F7));
  938. border-radius: 28rpx;
  939. padding: 24rpx 16rpx 20rpx;
  940. margin-right: 20rpx;
  941. border: 4rpx solid var(--border, #CBD5E1);
  942. box-shadow: inset -4rpx -4rpx 10rpx rgba(255,255,255,0.6),
  943. 6rpx 6rpx 20rpx rgba(91,155,213,0.08);
  944. transition: transform 0.2s cubic-bezier(0.34, 1.56, 0.64, 1);
  945. animation: slideUp 0.4s cubic-bezier(0.16, 1, 0.3, 1) both;
  946. flex-shrink: 0;
  947. }
  948. .task-clay-card:active,
  949. .task-card-pressed {
  950. transform: scale(0.94);
  951. }
  952. /* 任务分类图标容器 */
  953. .task-card-icon-wrap {
  954. width: 88rpx;
  955. height: 88rpx;
  956. border-radius: 24rpx;
  957. display: flex;
  958. align-items: center;
  959. justify-content: center;
  960. margin-bottom: 12rpx;
  961. border: 3rpx solid var(--border-light, #E2E8F0);
  962. box-shadow: inset -2rpx -2rpx 6rpx rgba(255,255,255,0.5),
  963. 3rpx 3rpx 8rpx rgba(91,155,213,0.06);
  964. }
  965. .task-card-icon {
  966. font-size: 42rpx;
  967. line-height: 1;
  968. }
  969. /* 分类图标底色 */
  970. .task-cat--study .task-card-icon-wrap { background: rgba(14,165,233,0.12); border-color: #BAE6FD; }
  971. .task-cat--reading .task-card-icon-wrap { background: rgba(34,197,94,0.12); border-color: #BBF7D0; }
  972. .task-cat--life .task-card-icon-wrap { background: rgba(91,155,213,0.12); border-color: #CBD5E1; }
  973. .task-cat--sports .task-card-icon-wrap { background: rgba(59,130,246,0.12); border-color: #BFDBFE; }
  974. .task-cat--chores .task-card-icon-wrap { background: rgba(245,158,11,0.12); border-color: #FDE68A; }
  975. .task-cat--art .task-card-icon-wrap { background: rgba(236,72,153,0.12); border-color: #FBCFE8; }
  976. .task-cat--default .task-card-icon-wrap { background: rgba(148,163,184,0.12); border-color: #E2E8F0; }
  977. .task-card-name {
  978. font-size: 26rpx;
  979. font-weight: 600;
  980. color: var(--text, #1E293B);
  981. text-align: center;
  982. line-height: 1.3;
  983. overflow: hidden;
  984. text-overflow: ellipsis;
  985. white-space: nowrap;
  986. max-width: 100%;
  987. margin-bottom: 6rpx;
  988. }
  989. .task-card-points {
  990. font-size: 24rpx;
  991. font-weight: 700;
  992. color: var(--color-primary, #5B9BD5);
  993. margin-bottom: 10rpx;
  994. }
  995. .task-card-badge {
  996. display: flex;
  997. align-items: center;
  998. justify-content: center;
  999. min-height: 36rpx;
  1000. }
  1001. /* 任务空状态 */
  1002. .tasks-empty {
  1003. display: flex;
  1004. flex-direction: column;
  1005. align-items: center;
  1006. justify-content: center;
  1007. padding: 48rpx 32rpx;
  1008. background: var(--surface, #FFFFFF);
  1009. border-radius: 28rpx;
  1010. border: 4rpx solid var(--border, #CBD5E1);
  1011. box-shadow: var(--shadow-clay);
  1012. transition: transform 0.2s ease;
  1013. }
  1014. .tasks-empty:active {
  1015. transform: scale(0.98);
  1016. }
  1017. .tasks-empty-icon {
  1018. font-size: 64rpx;
  1019. margin-bottom: 16rpx;
  1020. }
  1021. .tasks-empty-title {
  1022. font-size: 30rpx;
  1023. font-weight: 600;
  1024. color: var(--text, #1E293B);
  1025. margin-bottom: 8rpx;
  1026. }
  1027. .tasks-empty-desc {
  1028. font-size: 24rpx;
  1029. color: var(--text-secondary, #64748B);
  1030. }
  1031. /* ----------------------------------------------------------
  1032. 快捷操作 — 2×2 黏土按钮网格
  1033. ---------------------------------------------------------- */
  1034. .quick-section {
  1035. margin-bottom: 28rpx;
  1036. }
  1037. .quick-grid {
  1038. display: flex;
  1039. flex-wrap: wrap;
  1040. gap: 20rpx;
  1041. }
  1042. .quick-grid > * {
  1043. width: calc(50% - 10rpx);
  1044. }
  1045. .quick-clay-btn {
  1046. display: flex;
  1047. flex-direction: column;
  1048. align-items: center;
  1049. justify-content: center;
  1050. padding: 28rpx 16rpx;
  1051. background: linear-gradient(145deg, #FFFFFF, var(--bg-grey, #EDF2F7));
  1052. border-radius: 28rpx;
  1053. border: 4rpx solid var(--border, #CBD5E1);
  1054. box-shadow: inset -4rpx -4rpx 10rpx rgba(255,255,255,0.6),
  1055. 6rpx 6rpx 20rpx rgba(91,155,213,0.08);
  1056. transition: transform 0.2s cubic-bezier(0.34, 1.56, 0.64, 1);
  1057. min-height: 180rpx;
  1058. }
  1059. .quick-clay-btn:active,
  1060. .quick-btn-pressed {
  1061. transform: scale(0.94);
  1062. box-shadow: inset 2rpx 2rpx 8rpx rgba(0,0,0,0.06),
  1063. 3rpx 3rpx 10rpx rgba(91,155,213,0.06);
  1064. }
  1065. .quick-btn-icon {
  1066. width: 96rpx;
  1067. height: 96rpx;
  1068. border-radius: 28rpx;
  1069. display: flex;
  1070. align-items: center;
  1071. justify-content: center;
  1072. margin-bottom: 14rpx;
  1073. border: 3rpx solid var(--border-light, #E2E8F0);
  1074. box-shadow: inset -2rpx -2rpx 6rpx rgba(255,255,255,0.5),
  1075. 3rpx 3rpx 8rpx rgba(91,155,213,0.06);
  1076. transition: transform 0.2s ease;
  1077. }
  1078. .quick-emoji {
  1079. font-size: 44rpx;
  1080. line-height: 1;
  1081. }
  1082. /* 各按钮图标色彩 */
  1083. .quick-icon--tasks { background: rgba(14,165,233,0.15); border-color: #BAE6FD; }
  1084. .quick-icon--rewards { background: rgba(245,158,11,0.15); border-color: #FDE68A; }
  1085. .quick-icon--growth { background: rgba(91,155,213,0.15); border-color: #CBD5E1; }
  1086. .quick-icon--games { background: rgba(236,72,153,0.12); border-color: #FBCFE8; }
  1087. .quick-btn-label {
  1088. font-size: 28rpx;
  1089. font-weight: 700;
  1090. color: var(--text, #1E293B);
  1091. }
  1092. /* ----------------------------------------------------------
  1093. 成就区域 — 水平滚动徽章
  1094. ---------------------------------------------------------- */
  1095. .achievement-section {
  1096. margin-bottom: 28rpx;
  1097. }
  1098. .achievement-scroll {
  1099. display: flex;
  1100. flex-direction: row;
  1101. white-space: nowrap;
  1102. overflow-x: auto;
  1103. padding: 8rpx 0 8rpx;
  1104. -webkit-overflow-scrolling: touch;
  1105. }
  1106. .achievement-item {
  1107. display: inline-flex;
  1108. flex-direction: column;
  1109. align-items: center;
  1110. width: 160rpx;
  1111. margin-right: 20rpx;
  1112. transition: transform 0.2s ease;
  1113. flex-shrink: 0;
  1114. }
  1115. .achievement-item:active,
  1116. .achievement-pressed {
  1117. transform: scale(0.92);
  1118. }
  1119. .achievement-icon-wrap {
  1120. width: 100rpx;
  1121. height: 100rpx;
  1122. border-radius: 50%;
  1123. background: linear-gradient(145deg, #DCE8F5, #C8DDF0);
  1124. border: 4rpx solid var(--border, #CBD5E1);
  1125. box-shadow: inset -3rpx -3rpx 8rpx rgba(255,255,255,0.5),
  1126. 4rpx 4rpx 14rpx rgba(91,155,213,0.08);
  1127. display: flex;
  1128. align-items: center;
  1129. justify-content: center;
  1130. margin-bottom: 10rpx;
  1131. }
  1132. .achievement-icon {
  1133. font-size: 44rpx;
  1134. line-height: 1;
  1135. }
  1136. .achievement-name {
  1137. font-size: 24rpx;
  1138. font-weight: 600;
  1139. color: var(--text, #1E293B);
  1140. text-align: center;
  1141. }
  1142. /* 锁定徽章 */
  1143. .achievement-item--locked {
  1144. opacity: 0.50;
  1145. }
  1146. .achievement-icon-wrap--locked {
  1147. background: var(--bg-grey, #EDF2F7);
  1148. border-color: var(--border-light, #E2E8F0);
  1149. box-shadow: none;
  1150. }
  1151. .achievement-icon--locked {
  1152. filter: grayscale(1);
  1153. }
  1154. .achievement-name--locked {
  1155. color: var(--muted, #94A3B8);
  1156. }
  1157. /* ----------------------------------------------------------
  1158. 加载 & 通用
  1159. ---------------------------------------------------------- */
  1160. .loading-state {
  1161. display: flex;
  1162. flex-direction: column;
  1163. align-items: center;
  1164. padding: 200rpx 0;
  1165. }
  1166. /* ----------------------------------------------------------
  1167. 能量变动日志
  1168. ---------------------------------------------------------- */
  1169. .energy-log-section { margin-bottom: 28rpx; }
  1170. .energy-log-list { background: #fff; border-radius: 20rpx; overflow: hidden; box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06); }
  1171. .energy-log-item {
  1172. display: flex; align-items: center;
  1173. padding: 20rpx 24rpx;
  1174. border-bottom: 1rpx solid #f0f0f0;
  1175. }
  1176. .energy-log-item:last-child { border-bottom: none; }
  1177. .log-icon-wrap {
  1178. width: 56rpx; height: 56rpx;
  1179. border-radius: 50%;
  1180. display: flex; align-items: center; justify-content: center;
  1181. margin-right: 16rpx; flex-shrink: 0;
  1182. }
  1183. .log-icon--up { background: rgba(16,185,129,0.12); }
  1184. .log-icon--down { background: rgba(239,68,68,0.12); }
  1185. .log-icon { font-size: 24rpx; }
  1186. .log-info { flex: 1; min-width: 0; }
  1187. .log-title { font-size: 26rpx; color: #333; display: block; }
  1188. .log-time { font-size: 20rpx; color: #999; margin-top: 4rpx; display: block; }
  1189. .log-amount { font-size: 28rpx; font-weight: 700; margin-left: 12rpx; }
  1190. .log-amount--up { color: #10B981; }
  1191. .log-amount--down { color: #EF4444; }
  1192. /* 连续打卡徽章的浮动动画 — 使用 uni.scss 全局 float keyframe */
  1193. .streak-badge {
  1194. animation: float 3s ease-in-out infinite;
  1195. }
  1196. /* 等级标签的金色光晕 — 使用 uni.scss 全局 pulseGlow keyframe */
  1197. .points-level-tag {
  1198. animation: pulseGlow 2.5s ease-in-out infinite;
  1199. }
  1200. </style>