child-index.vue 41 KB

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