child-index.vue 45 KB

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