child-index.vue 42 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404
  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. <!-- 通关引导进度条 -->
  29. <GateGuideBar :unlockStatus="unlockStatus" />
  30. <!-- 五维能量沙盘 -->
  31. <GateLock v-if="sandboxLocked && sandboxLockGate" :gate="sandboxLockGate" />
  32. <WuxingSandbox
  33. v-else-if="sandboxData"
  34. mode="live"
  35. :scores="familySandboxScores"
  36. :overallScore="familySandboxOverall"
  37. :members="familySandboxMembers"
  38. :personalData="sandboxPersonalData"
  39. :dimensions="energyDimensions"
  40. :totalEnergy="totalEnergy"
  41. :totalHealthIndex="totalHealthIndex"
  42. :familyUnlocked="familyUnlocked"
  43. :currentView="currentSandboxView"
  44. :memberCount="familyMembersVisible.length"
  45. starHeight="400rpx"
  46. showDimensionBar
  47. @view-change="onSandboxViewChange"
  48. @point-click="onDomainClick" />
  49. <!-- 家庭能量横条 -->
  50. <FamilyEnergyBar
  51. v-if="hasChildren"
  52. dimensionCode="child"
  53. :sandboxData="sandboxData" />
  54. <!-- 家庭成员选择条 -->
  55. <FamilyMemberStrip
  56. v-if="hasChildren && familyMembersVisible.length > 0"
  57. :members="familyMembersVisible"
  58. :selectedMemberId="selectedMemberId"
  59. @select="onFamilyMemberSelect"
  60. @removed="loadFamilyMembersVisible" />
  61. <!-- 连续打卡徽章 -->
  62. <view
  63. class="streak-badge-wrap animate-slide-up animate-stagger-3"
  64. v-if="streakDays > 0"
  65. @click="showStreakDetail"
  66. hover-class="streak-pressed"
  67. >
  68. <text class="streak-fire">🔥</text>
  69. <view class="streak-info">
  70. <text class="streak-days">{{ streakDays }}</text>
  71. <text class="streak-unit">天坚持</text>
  72. </view>
  73. </view>
  74. <!-- ============================================================
  75. B) 积分卡片 — Claymorphism 暖橙黏土风格
  76. ============================================================ -->
  77. <view
  78. class="points-card animate-bounce-in"
  79. @click="goToRewards"
  80. hover-class="points-card-pressed"
  81. >
  82. <!-- 装饰圆点 -->
  83. <view class="points-deco points-deco--1"></view>
  84. <view class="points-deco points-deco--2"></view>
  85. <view class="points-top">
  86. <view class="points-left">
  87. <text class="points-label">🌟 我的星星</text>
  88. <text class="points-number">{{ totalPoints }}</text>
  89. </view>
  90. <view class="points-level-tag">
  91. <text class="level-emoji">{{ levelEmoji }}</text>
  92. <text class="level-name">{{ levelName }}</text>
  93. </view>
  94. </view>
  95. <view class="points-progress">
  96. <text class="progress-hint" v-if="pointsToNext > 0">
  97. 还差 <text class="progress-hint-num">{{ pointsToNext }}</text> 颗星星升到 {{ nextLevelName }}
  98. </text>
  99. <text class="progress-hint" v-else>
  100. 🎉 已经是最高等级啦!
  101. </text>
  102. <view class="progress-track">
  103. <view
  104. class="progress-fill"
  105. :style="'width: ' + levelProgress + '%'"
  106. ></view>
  107. </view>
  108. </view>
  109. </view>
  110. <!-- ============================================================
  111. C) 今日任务 — 可水平滑动的黏土卡片
  112. ============================================================ -->
  113. <view class="tasks-section animate-slide-up animate-stagger-3">
  114. <view class="section-header">
  115. <text class="section-title">📋 今日任务</text>
  116. <text class="section-link" @click="goToTasks">查看全部 →</text>
  117. </view>
  118. <scroll-view
  119. class="tasks-scroll"
  120. scroll-x
  121. enhanced
  122. :show-scrollbar="false"
  123. v-if="todayTasks.length > 0"
  124. >
  125. <view
  126. class="task-clay-card"
  127. v-for="(task, tIdx) in todayTasks"
  128. :key="task.id"
  129. @click="handleTaskClick(task)"
  130. hover-class="task-card-pressed"
  131. :style="'animation-delay: ' + (tIdx * 80) + 'ms'"
  132. >
  133. <view class="task-card-icon-wrap" :class="'task-cat--' + getTaskCategoryClass(task)">
  134. <text class="task-card-icon">{{ getTaskIcon(task) }}</text>
  135. </view>
  136. <text class="task-card-name">{{ task.title }}</text>
  137. <text class="task-card-points" v-if="task.points">+{{ task.points }}⭐</text>
  138. <view class="task-card-badge">
  139. <BaseBadge :variant="getBadgeVariant(task)" size="xs">
  140. {{ getStatusText(task) }}
  141. </BaseBadge>
  142. </view>
  143. </view>
  144. </scroll-view>
  145. <!-- 今日任务空状态 -->
  146. <view class="tasks-empty" v-else @click="goToTasks">
  147. <view class="tasks-empty-icon">🎯</view>
  148. <text class="tasks-empty-title">今天没有任务哦</text>
  149. <text class="tasks-empty-desc">让爸爸妈妈给你布置任务吧~</text>
  150. </view>
  151. </view>
  152. <!-- ============================================================
  153. D) 快捷操作 — 2×2 黏土按钮网格
  154. ============================================================ -->
  155. <view class="quick-section animate-slide-up animate-stagger-4">
  156. <view class="section-header">
  157. <text class="section-title">🚀 快捷入口</text>
  158. </view>
  159. <view class="quick-grid">
  160. <view
  161. class="quick-clay-btn"
  162. @click="goToTasks"
  163. hover-class="quick-btn-pressed"
  164. >
  165. <view class="quick-btn-icon quick-icon--tasks">
  166. <text class="quick-emoji">📋</text>
  167. </view>
  168. <text class="quick-btn-label">我的任务</text>
  169. </view>
  170. <view
  171. class="quick-clay-btn"
  172. @click="goToRewards"
  173. hover-class="quick-btn-pressed"
  174. >
  175. <view class="quick-btn-icon quick-icon--rewards">
  176. <text class="quick-emoji">⭐</text>
  177. </view>
  178. <text class="quick-btn-label">心愿商城</text>
  179. </view>
  180. <view
  181. class="quick-clay-btn"
  182. @click="goToGrowth"
  183. hover-class="quick-btn-pressed"
  184. >
  185. <view class="quick-btn-icon quick-icon--growth">
  186. <text class="quick-emoji">🏆</text>
  187. </view>
  188. <text class="quick-btn-label">成长记录</text>
  189. </view>
  190. <view
  191. class="quick-clay-btn"
  192. @click="goToGames"
  193. hover-class="quick-btn-pressed"
  194. >
  195. <view class="quick-btn-icon quick-icon--games">
  196. <text class="quick-emoji">🎮</text>
  197. </view>
  198. <text class="quick-btn-label">小游戏</text>
  199. </view>
  200. <view
  201. class="quick-clay-btn"
  202. @click="goToSelfQuiz"
  203. hover-class="quick-btn-pressed"
  204. >
  205. <view class="quick-btn-icon quick-icon--quiz">
  206. <text class="quick-emoji">✏️</text>
  207. </view>
  208. <text class="quick-btn-label">自己出题</text>
  209. </view>
  210. </view>
  211. </view>
  212. <!-- ============================================================
  213. E) 最近成就 — 水平滚动徽章
  214. ============================================================ -->
  215. <view
  216. class="achievement-section animate-slide-up animate-stagger-5"
  217. v-if="earnedBadges.length > 0"
  218. >
  219. <view class="section-header">
  220. <text class="section-title">🏅 最近成就</text>
  221. </view>
  222. <scroll-view
  223. class="achievement-scroll"
  224. scroll-x
  225. enhanced
  226. :show-scrollbar="false"
  227. >
  228. <view
  229. class="achievement-item"
  230. v-for="badge in earnedBadges"
  231. :key="badge.id"
  232. hover-class="achievement-pressed"
  233. >
  234. <view class="achievement-icon-wrap">
  235. <text class="achievement-icon">{{ badge.icon }}</text>
  236. </view>
  237. <text class="achievement-name">{{ badge.name }}</text>
  238. </view>
  239. <!-- 锁定占位徽章 -->
  240. <view
  241. class="achievement-item achievement-item--locked"
  242. v-for="lockedBadge in lockedBadges"
  243. :key="lockedKey(lockedBadge)"
  244. >
  245. <view class="achievement-icon-wrap achievement-icon-wrap--locked">
  246. <text class="achievement-icon achievement-icon--locked">🔒</text>
  247. </view>
  248. <text class="achievement-name achievement-name--locked">{{ lockedBadge.name }}</text>
  249. </view>
  250. </scroll-view>
  251. </view>
  252. <!-- ============================================================
  253. F) 最近能量变动
  254. ============================================================ -->
  255. <view class="energy-log-section animate-slide-up animate-stagger-6" v-if="energyLogs.length > 0">
  256. <view class="section-header">
  257. <text class="section-title">💧 最近能量变动</text>
  258. <text class="section-link" @click="goToEnergyDetail">查看详情 →</text>
  259. </view>
  260. <view class="energy-log-list">
  261. <view class="energy-log-item" v-for="log in energyLogs" :key="log.id">
  262. <view class="log-icon-wrap" :class="'log-icon--' + ((log.amount || 0) >= 0 ? 'up' : 'down')">
  263. <text class="log-icon">{{ (log.amount || 0) >= 0 ? '⬆' : '⬇' }}</text>
  264. </view>
  265. <view class="log-info">
  266. <text class="log-title">{{ log.reason || log.description }}</text>
  267. <text class="log-time">{{ log.createdAt || log.time }}</text>
  268. </view>
  269. <text class="log-amount" :class="(log.amount || 0) >= 0 ? 'log-amount--up' : 'log-amount--down'">
  270. {{ (log.amount || 0) >= 0 ? '+' : '' }}{{ log.amount }}
  271. </text>
  272. </view>
  273. </view>
  274. </view>
  275. <!-- ===== 推荐商品+活动(混合信息流) ===== -->
  276. <RecommendedFeed
  277. v-if="hasChildren"
  278. :products="dimensionProducts"
  279. :activities="dimensionActivities"
  280. @itemClick="onFeedItemClick"
  281. @more="goMoreDiscover" />
  282. <!-- 底部安全留白 -->
  283. <view class="bottom-spacer"></view>
  284. </view>
  285. </view>
  286. </template>
  287. <script>
  288. import { getChildren, getTodayTasks, getEnergyOverview, getEnergyLogs, getActivityList, getProductsByDomain, getFamilyEnergySandbox, getEnergySandbox, getUnlockStatus, getFamilyMemberList } from '../../utils/api.js'
  289. import FamilyEnergyBar from '../../components/FamilyEnergyBar.vue'
  290. import WuxingSandbox from '../../components/wuxing-sandbox.vue'
  291. import RecommendedFeed from '../../components/RecommendedFeed.vue'
  292. import PlayfulCard from '../../components/PlayfulCard.vue'
  293. import PlayfulButton from '../../components/PlayfulButton.vue'
  294. import BaseBadge from '../../components/BaseBadge.vue'
  295. import BaseEmpty from '../../components/BaseEmpty.vue'
  296. import BaseLoading from '../../components/BaseLoading.vue'
  297. import GateLock from '../../components/gate-lock.vue'
  298. import GateGuideBar from '../../components/gate-guide-bar.vue'
  299. import OnboardingGuide from '../../components/OnboardingGuide.vue'
  300. import ConfettiCelebration from '../../components/ConfettiCelebration.vue'
  301. import FamilyMemberStrip from '../../components/FamilyMemberStrip.vue'
  302. /**
  303. * 等级配置表
  304. * 星星宝宝(0) → 月亮小朋友(200) → 太阳小天使(500) → 宇宙小英雄(1000)
  305. */
  306. var LEVELS = [
  307. { min: 0, max: 200, name: '星星宝宝', nextName: '月亮小朋友', emoji: '⭐' },
  308. { min: 200, max: 500, name: '月亮小朋友', nextName: '太阳小天使', emoji: '🌙' },
  309. { min: 500, max: 1000, name: '太阳小天使', nextName: '宇宙小英雄', emoji: '☀️' },
  310. { min: 1000, max: Infinity, name: '宇宙小英雄', nextName: '👑 MAX', emoji: '🦸' }
  311. ]
  312. /**
  313. * 任务分类 → 图标映射
  314. */
  315. var TASK_ICONS = {
  316. '学习类': '📚',
  317. '阅读类': '📖',
  318. '生活类': '🏠',
  319. '运动类': '⚽',
  320. '家务类': '🧹',
  321. '艺术类': '🎨',
  322. '其他': '⭐'
  323. }
  324. export default {
  325. components: {
  326. FamilyEnergyBar,
  327. WuxingSandbox,
  328. RecommendedFeed,
  329. PlayfulCard,
  330. PlayfulButton,
  331. BaseBadge,
  332. BaseEmpty,
  333. BaseLoading,
  334. OnboardingGuide,
  335. ConfettiCelebration,
  336. FamilyMemberStrip,
  337. GateLock,
  338. GateGuideBar
  339. },
  340. data() {
  341. return {
  342. loading: true,
  343. showOnboarding: false,
  344. hasChildren: false,
  345. nickname: '',
  346. memberId: '',
  347. totalPoints: 0,
  348. streakDays: 0,
  349. completedCount: 0,
  350. totalTaskCount: 0,
  351. todayTasks: [],
  352. badges: [
  353. { id: 1, icon: '🌟', name: '早起鸟', unlocked: false },
  354. { id: 2, icon: '📚', name: '阅读小达人', unlocked: false },
  355. { id: 3, icon: '🏃', name: '运动健将', unlocked: false },
  356. { id: 4, icon: '🎨', name: '小艺术家', unlocked: false },
  357. { id: 5, icon: '🧹', name: '家务小能手', unlocked: false }
  358. ],
  359. _mounted: false,
  360. energyDimensions: [],
  361. totalEnergy: 0,
  362. totalHealthIndex: 0,
  363. energyLogs: [],
  364. sandboxData: null,
  365. sandboxPersonalData: null,
  366. sandboxFamilyData: null,
  367. sandboxLocked: false,
  368. sandboxLockGate: null,
  369. currentSandboxView: 'personal',
  370. familyUnlocked: false,
  371. unlockStatus: null,
  372. dimensionActivities: [],
  373. dimensionProducts: [],
  374. familyMembersVisible: [],
  375. selectedMemberId: null
  376. }
  377. },
  378. computed: {
  379. // ---------- 能量沙盘 ----------
  380. sandboxScores() {
  381. if (!this.sandboxData) return { body: 0, mind: 0, wisdom: 0, action: 0, wealth: 0 }
  382. return {
  383. body: this.sandboxData.bodyScore || 0,
  384. mind: this.sandboxData.mindScore || 0,
  385. wisdom: this.sandboxData.wisdomScore || 0,
  386. action: this.sandboxData.actionScore || 0,
  387. wealth: this.sandboxData.wealthScore || 0
  388. }
  389. },
  390. familySandboxScores() {
  391. var d = this.sandboxFamilyData || {}
  392. return {
  393. body: d.bodyScore || 0,
  394. mind: d.mindScore || 0,
  395. wisdom: d.wisdomScore || 0,
  396. action: d.actionScore || 0,
  397. wealth: d.wealthScore || 0
  398. }
  399. },
  400. familySandboxOverall() {
  401. return this.sandboxFamilyData ? (this.sandboxFamilyData.overallScore || 0) : 0
  402. },
  403. familySandboxMembers() {
  404. return this.sandboxFamilyData ? (this.sandboxFamilyData.members || []) : []
  405. },
  406. // ---------- 等级相关 ----------
  407. currentLevelIndex() {
  408. const pts = this.totalPoints
  409. if (pts >= 1000) return 3
  410. if (pts >= 500) return 2
  411. if (pts >= 200) return 1
  412. return 0
  413. },
  414. levelName() {
  415. return LEVELS[this.currentLevelIndex].name
  416. },
  417. nextLevelName() {
  418. return LEVELS[this.currentLevelIndex].nextName
  419. },
  420. levelEmoji() {
  421. return LEVELS[this.currentLevelIndex].emoji
  422. },
  423. currentLevelMin() {
  424. return LEVELS[this.currentLevelIndex].min
  425. },
  426. nextLevelMax() {
  427. return LEVELS[this.currentLevelIndex].max
  428. },
  429. levelProgress() {
  430. if (this.totalPoints >= 1000) return 100
  431. const range = this.nextLevelMax - this.currentLevelMin
  432. if (range <= 0) return 100
  433. const progress = ((this.totalPoints - this.currentLevelMin) / range) * 100
  434. return Math.min(100, Math.floor(progress))
  435. },
  436. pointsToNext() {
  437. if (this.totalPoints >= 1000) return 0
  438. return this.nextLevelMax - this.totalPoints
  439. },
  440. // ---------- 任务相关 ----------
  441. taskProgress() {
  442. if (this.totalTaskCount === 0) return 0
  443. return Math.floor((this.completedCount / this.totalTaskCount) * 100)
  444. },
  445. // ---------- 庆祝动画 ----------
  446. showConfetti() {
  447. return this.taskProgress >= 100 && this.totalTaskCount > 0
  448. },
  449. // ---------- 头像表情 ----------
  450. avatarEmoji() {
  451. const level = this.currentLevelIndex
  452. if (level >= 3) return '🦸'
  453. if (level >= 2) return '😇'
  454. if (level >= 1) return '🌙'
  455. return '🐣'
  456. },
  457. // ---------- 问候语 ----------
  458. timeGreeting() {
  459. const hour = new Date().getHours()
  460. if (hour < 6) return '🌛 夜深了,还没睡呀'
  461. if (hour < 9) return '🌅 早安!新的一天开始啦'
  462. if (hour < 12) return '☀️ 上午好!今天也要加油哦'
  463. if (hour < 14) return '🌤️ 中午好!吃饱了才有力气'
  464. if (hour < 18) return '🌻 下午好!继续保持好状态'
  465. if (hour < 21) return '🌆 晚上好!今天过得开心吗'
  466. return '🌛 该休息啦,明天继续加油'
  467. },
  468. // ---------- 成就徽章 ----------
  469. earnedBadges() {
  470. return this.badges.filter(function(b) { return b.unlocked })
  471. },
  472. lockedBadges() {
  473. return this.badges.filter(function(b) { return !b.unlocked })
  474. }
  475. },
  476. mounted() {
  477. // 首次登录引导
  478. var onboardingDone = uni.getStorageSync('_onboardingDone')
  479. if (!onboardingDone) {
  480. this.showOnboarding = true
  481. }
  482. this.loadData()
  483. },
  484. onShow() {
  485. if (this._mounted) {
  486. this.loadData()
  487. }
  488. this._mounted = true
  489. },
  490. methods: {
  491. onFamilyMemberSelect(member) {
  492. this.selectedMemberId = member.id
  493. },
  494. loadFamilyMembersVisible() {
  495. var self = this
  496. getFamilyMemberList({ visibleOnly: true }).then(function(res) {
  497. self.familyMembersVisible = res.data || []
  498. }).catch(function(e) {
  499. console.log('获取可见家庭成员失败', e)
  500. self.familyMembersVisible = []
  501. })
  502. },
  503. // ======================== 数据加载 ========================
  504. async loadData() {
  505. this.loading = true
  506. try {
  507. const userInfo = uni.getStorageSync('userInfo')
  508. const role = uni.getStorageSync('currentRole') || 'child'
  509. if (role !== 'child') {
  510. uni.switchTab({ url: '/pages/index-home/index' })
  511. return
  512. }
  513. this.nickname = (userInfo && userInfo.nickname) ? userInfo.nickname : '小朋友'
  514. const childrenRes = await getFamilyMemberList({ visibleOnly: true })
  515. var allMembers = childrenRes.data || []
  516. if (!Array.isArray(allMembers)) allMembers = []
  517. // 同步设置 familyMembersVisible,避免 loadFamilyMembersVisible() 再调一次
  518. this.familyMembersVisible = allMembers
  519. const children = allMembers.filter(function(m) { return m.effectiveRole === 'child' || m.source !== 'parent' })
  520. if (children.length === 0) {
  521. this.hasChildren = false
  522. this.loading = false
  523. return
  524. }
  525. this.hasChildren = true
  526. const child = children[0] || {}
  527. this.memberId = child.id || ''
  528. this.totalPoints = child.totalPoints || 0
  529. this.streakDays = child.streakDays || 0
  530. const tasksRes = await getTodayTasks()
  531. const rawTasks = tasksRes.data || []
  532. this.todayTasks = rawTasks
  533. this.completedCount = rawTasks.filter(function(t) { return t.status === 'completed' }).length
  534. this.totalTaskCount = rawTasks.length
  535. // 解锁成就
  536. if (this.streakDays >= 7) {
  537. this.unlockBadge(1)
  538. }
  539. if (this.totalPoints >= 1000) {
  540. this.unlockBadge(2)
  541. }
  542. // 按类别统计已完成任务次数,解锁对应徽章
  543. var categoryCompletedCount = { '运动类': 0, '艺术类': 0, '家务类': 0 }
  544. for (var i = 0; i < rawTasks.length; i++) {
  545. var task = rawTasks[i]
  546. if (task.status === 'completed') {
  547. var cat = task.category || '其他'
  548. if (cat === '运动类') {
  549. categoryCompletedCount['运动类']++
  550. } else if (cat === '艺术类') {
  551. categoryCompletedCount['艺术类']++
  552. } else if (cat === '家务类') {
  553. categoryCompletedCount['家务类']++
  554. }
  555. }
  556. }
  557. if (categoryCompletedCount['运动类'] >= 5) {
  558. this.unlockBadge(3)
  559. }
  560. if (categoryCompletedCount['艺术类'] >= 5) {
  561. this.unlockBadge(4)
  562. }
  563. if (categoryCompletedCount['家务类'] >= 5) {
  564. this.unlockBadge(5)
  565. }
  566. // 加载五维能量概览(memberId 由后端从 JWT 派生)
  567. try {
  568. var overviewRes = await getEnergyOverview()
  569. if (overviewRes && overviewRes.data) {
  570. this.energyDimensions = overviewRes.data.dimensions || []
  571. this.totalEnergy = overviewRes.data.totalEnergy || 0
  572. this.totalHealthIndex = overviewRes.data.totalHealthIndex || 0
  573. }
  574. } catch (e) {
  575. console.log('获取能量概览失败', e)
  576. }
  577. // 加载最近能量变动
  578. try {
  579. var logsRes = await getEnergyLogs('', 1, 5)
  580. if (logsRes && logsRes.data) {
  581. this.energyLogs = logsRes.data.records || logsRes.data || []
  582. }
  583. } catch (e) {
  584. console.log('获取能量日志失败', e)
  585. this.energyLogs = []
  586. }
  587. // 加载通关解锁状态
  588. try {
  589. var statusRes = await getUnlockStatus()
  590. if (statusRes && statusRes.data) {
  591. this.unlockStatus = statusRes.data
  592. }
  593. } catch (e) {
  594. console.log('获取通关解锁状态失败', e)
  595. }
  596. // 加载沙盘数据(含通关门禁,一次返回个人+全家)
  597. try {
  598. var memberId = uni.getStorageSync('currentMemberId') || uni.getStorageSync('currentChildId') || null
  599. var sandboxRes = await getEnergySandbox(this.currentSandboxView, memberId)
  600. if (sandboxRes && sandboxRes.data) {
  601. var ed = sandboxRes.data
  602. if (ed.locked) {
  603. this.sandboxLocked = true
  604. this.sandboxLockGate = ed.gate || null
  605. this.sandboxData = null
  606. this.sandboxPersonalData = null
  607. this.sandboxFamilyData = null
  608. } else {
  609. this.sandboxLocked = false
  610. this.sandboxLockGate = null
  611. this.sandboxPersonalData = ed.personalData || null
  612. this.sandboxFamilyData = ed.familyData || null
  613. this.sandboxData = this.currentSandboxView === 'family' ? (ed.familyData || null) : (ed.personalData || null)
  614. this.familyUnlocked = ed.familyUnlocked || false
  615. }
  616. }
  617. } catch (e) {
  618. console.log('获取沙盘数据失败', e)
  619. }
  620. // 家庭成员已在上方 loadData 开头通过 getFamilyMemberList 一次获取,不再重复调用
  621. // this.loadFamilyMembersVisible()
  622. // 加载维度活动
  623. try {
  624. var actRes = await getActivityList({ dimensionCode: 'child', page: 1, size: 3 })
  625. if (actRes && actRes.data) {
  626. var actList = Array.isArray(actRes.data) ? actRes.data : (actRes.data.records || [])
  627. this.dimensionActivities = actList.slice(0, 3)
  628. }
  629. } catch (e) {
  630. console.log('获取活动数据失败', e)
  631. }
  632. // 加载维度商品
  633. try {
  634. var prodRes = await getProductsByDomain('child', 1, 4)
  635. if (prodRes && prodRes.data) {
  636. var prodList = Array.isArray(prodRes.data) ? prodRes.data : (prodRes.data.records || [])
  637. this.dimensionProducts = prodList.slice(0, 4)
  638. }
  639. } catch (e) {
  640. console.log('获取商品数据失败', e)
  641. }
  642. } catch (e) {
  643. console.error('加载数据失败', e)
  644. uni.showToast({ title: '加载失败', icon: 'none' })
  645. } finally {
  646. this.loading = false
  647. this.$emit('ready')
  648. }
  649. },
  650. unlockBadge(id) {
  651. var badge = this.badges.find(function(b) { return b.id === id })
  652. if (badge) badge.unlocked = true
  653. },
  654. // ======================== 导航方法 ========================
  655. onDomainClick(evt) {
  656. var code = evt && evt.code
  657. if (!code || !this.memberId) return
  658. uni.navigateTo({
  659. url: '/pages/energy/detail?memberId=' + this.memberId + '&code=' + code
  660. })
  661. },
  662. /** 切换沙盘个人/全家视角(本地切换,不重新请求) */
  663. onSandboxViewChange: function(view) {
  664. if (view === this.currentSandboxView) return
  665. this.currentSandboxView = view
  666. this.sandboxData = view === 'family' ? (this.sandboxFamilyData || null) : (this.sandboxPersonalData || null)
  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>