parent-index.vue 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110
  1. <template>
  2. <view class="container">
  3. <!-- 加载状态 -->
  4. <BaseLoading :loading="loading" text="正在加载..." />
  5. <template v-if="!loading">
  6. <!-- ===== a) 顶部欢迎区 ===== -->
  7. <view class="welcome-section animate-fade-in animate-stagger-1">
  8. <view class="welcome-header">
  9. <view class="welcome-text">
  10. <text class="greeting">{{ greetingText }}!</text>
  11. <text class="sub-greeting">{{ nickname || '家长' }}</text>
  12. </view>
  13. <view class="greeting-emoji">☀️</view>
  14. </view>
  15. </view>
  16. <!-- ===== b) 孩子卡片 ===== -->
  17. <view class="children-row animate-fade-in animate-stagger-2">
  18. <template v-if="children.length > 0">
  19. <scroll-view
  20. class="children-scroll"
  21. scroll-x
  22. :scroll-with-animation="true"
  23. :enhanced="true"
  24. :show-scrollbar="false"
  25. >
  26. <PlayfulCard
  27. v-for="child in children"
  28. :key="child.id"
  29. variant="flat"
  30. shadow="sm"
  31. :clickable="true"
  32. class="child-card"
  33. :class="{ 'child-card-single': children.length === 1 }"
  34. @click="goToChildDetail(child)"
  35. >
  36. <view class="child-card-inner">
  37. <view class="child-avatar" :class="'child-avatar-color-' + (child.id % 6)">
  38. <text class="child-avatar-text">{{ child.nickname ? child.nickname[0] : '?' }}</text>
  39. </view>
  40. <view class="child-card-body">
  41. <text class="child-card-name">{{ child.nickname }}</text>
  42. <view class="child-card-stats">
  43. <text class="child-card-points">⭐ {{ child.totalPoints || 0 }}</text>
  44. <text class="child-card-dan" v-if="child.danLevel">DAN{{ child.danLevel }}</text>
  45. </view>
  46. <view class="child-card-badges">
  47. <BaseBadge variant="primary" size="xs" v-if="child.pendingReviewCount > 0">待审 {{ child.pendingReviewCount }}</BaseBadge>
  48. <BaseBadge variant="error" size="xs" v-if="child.pendingWishCount > 0">心愿 {{ child.pendingWishCount }}</BaseBadge>
  49. </view>
  50. </view>
  51. <view class="child-card-arrow">›</view>
  52. </view>
  53. </PlayfulCard>
  54. </scroll-view>
  55. <view class="scroll-hint" v-if="children.length > 1">
  56. <text class="scroll-hint-text">← 左右滑动查看更多 →</text>
  57. </view>
  58. </template>
  59. <PlayfulCard
  60. v-else
  61. variant="flat"
  62. :clickable="true"
  63. @click="goToAddChild"
  64. class="empty-children-card"
  65. padding="28rpx"
  66. >
  67. <view class="empty-children-inner">
  68. <view class="empty-children-icon">👶</view>
  69. <view class="empty-children-text">
  70. <text class="empty-children-title">还没有添加孩子</text>
  71. <text class="empty-children-hint">点击添加孩子,开始成长之旅</text>
  72. </view>
  73. <view class="empty-children-arrow">›</view>
  74. </view>
  75. </PlayfulCard>
  76. </view>
  77. <!-- 五行能量沙盘(汐艾福) -->
  78. <view class="animate-fade-in animate-stagger-3">
  79. <wuxing-sandbox mode="parent" badgeText="综合成长力 0%" @point-click="goToDomain" />
  80. </view>
  81. <!-- 会员Banner(汐福俱乐部) -->
  82. <view class="membership-wrapper animate-fade-in animate-stagger-4">
  83. <view class="membership-banner" @click="goToMembership">
  84. <view class="banner-content">
  85. <text class="banner-icon">👑</text>
  86. <view class="banner-text">
  87. <text class="banner-title">{{ membershipText }}</text>
  88. <text class="banner-subtitle">点击查看会员权益</text>
  89. </view>
  90. <text class="banner-arrow">›</text>
  91. </view>
  92. </view>
  93. </view>
  94. <!-- ===== c) 成长数据 ===== -->
  95. <view class="stats-section animate-fade-in animate-stagger-5">
  96. <view class="section-title">成长数据</view>
  97. <view class="stats-grid">
  98. <PlayfulCard variant="flat" shadow="sm" :clickable="true" @click="goToCompletionStats" class="stat-card" padding="0">
  99. <view class="stat-card-content">
  100. <view class="stat-header">
  101. <view class="stat-icon-wrap stat-icon-orange">
  102. <text class="stat-icon-symbol">📋</text>
  103. </view>
  104. <view class="stat-body">
  105. <text class="stat-value accent-color">{{ taskCompletionRate }}%</text>
  106. <text class="stat-label">任务完成</text>
  107. </view>
  108. </view>
  109. <view class="stat-progress">
  110. <view class="progress-bar">
  111. <view class="progress-fill" :style="{ width: taskCompletionRate + '%' }"></view>
  112. </view>
  113. </view>
  114. <text class="stat-more">查看分析 ›</text>
  115. </view>
  116. </PlayfulCard>
  117. <PlayfulCard variant="flat" shadow="sm" class="stat-card" padding="0">
  118. <view class="stat-card-content">
  119. <view class="stat-header">
  120. <view class="stat-icon-wrap stat-icon-gold">
  121. <text class="stat-icon-symbol">⭐</text>
  122. </view>
  123. <view class="stat-body">
  124. <text class="stat-value gold-color">{{ (selectedChild && selectedChild.totalPoints) || 0 }}</text>
  125. <text class="stat-label">成长积分</text>
  126. </view>
  127. </view>
  128. <text class="stat-encourage">太棒了!</text>
  129. </view>
  130. </PlayfulCard>
  131. <PlayfulCard variant="flat" shadow="sm" class="stat-card" padding="0">
  132. <view class="stat-card-content">
  133. <view class="stat-header">
  134. <view class="stat-icon-wrap stat-icon-red">
  135. <text class="stat-icon-symbol">🔥</text>
  136. </view>
  137. <view class="stat-body">
  138. <text class="stat-value red-color">{{ (selectedChild && selectedChild.streakDays) || 0 }}</text>
  139. <text class="stat-label">连续天数</text>
  140. </view>
  141. </view>
  142. <text class="stat-encourage" v-if="selectedChild && selectedChild.streakDays > 0">加油保持!</text>
  143. <text class="stat-encourage muted" v-else>开始坚持吧</text>
  144. </view>
  145. </PlayfulCard>
  146. <PlayfulCard variant="flat" shadow="sm" class="stat-card" padding="0">
  147. <view class="stat-card-content">
  148. <view class="stat-header">
  149. <view class="stat-icon-wrap stat-icon-teal">
  150. <text class="stat-icon-symbol">✨</text>
  151. </view>
  152. <view class="stat-body">
  153. <text class="stat-value teal-color">{{ habitCount }}</text>
  154. <text class="stat-label">习惯打卡</text>
  155. </view>
  156. </view>
  157. <text class="stat-encourage">坚持就是胜利</text>
  158. </view>
  159. </PlayfulCard>
  160. </view>
  161. </view>
  162. <!-- ===== d) 今日重点 ===== -->
  163. <view class="today-section animate-fade-in animate-stagger-6">
  164. <view class="section-title">今日重点</view>
  165. <PlayfulCard variant="flat" class="today-card">
  166. <template slot="header">
  167. <view class="today-header">
  168. <text class="today-label">今日待完成</text>
  169. <text class="today-count">{{ pendingTasks.length }}项任务</text>
  170. </view>
  171. </template>
  172. <view class="task-list" v-if="pendingTasks.length > 0">
  173. <view class="task-item" v-for="task in pendingTasks.slice(0, 3)" :key="task.id">
  174. <view class="task-dot" :class="'dot-color-' + (task.id % 5)"></view>
  175. <text class="task-title">{{ task.title }}</text>
  176. </view>
  177. </view>
  178. <BaseEmpty v-else icon="🎉" title="今日任务已完成" description="太棒了!" :compact="true" />
  179. <view slot="actions" class="task-view-all" @click="goToTaskList">
  180. <text>查看全部</text>
  181. <text class="arrow">→</text>
  182. </view>
  183. </PlayfulCard>
  184. </view>
  185. <!-- ===== e) 成长里程碑 ===== -->
  186. <view class="milestone-section animate-fade-in animate-stagger-7">
  187. <view class="section-title">成长里程碑</view>
  188. <PlayfulCard variant="flat" class="milestone-card">
  189. <view class="milestone-item" v-if="selectedChild && selectedChild.streakDays >= 7">
  190. <view class="milestone-icon-wrap milestone-star">
  191. <text class="milestone-icon">🌟</text>
  192. </view>
  193. <text class="milestone-text">连续7天打卡</text>
  194. </view>
  195. <view class="milestone-item" v-if="selectedChild && selectedChild.streakDays >= 30">
  196. <view class="milestone-icon-wrap milestone-trophy">
  197. <text class="milestone-icon">🏆</text>
  198. </view>
  199. <text class="milestone-text">坚持一个月啦!</text>
  200. </view>
  201. <view class="milestone-item" v-if="(selectedChild && selectedChild.totalPoints) >= 1000">
  202. <view class="milestone-icon-wrap milestone-sparkle">
  203. <text class="milestone-icon">💫</text>
  204. </view>
  205. <text class="milestone-text">积分突破1000</text>
  206. </view>
  207. <BaseEmpty v-if="!hasMilestones" icon="🌱" title="开始养成好习惯" description="见证成长" :compact="true" />
  208. </PlayfulCard>
  209. </view>
  210. <!-- ===== f) 待审核任务 ===== -->
  211. <view class="review-section animate-fade-in animate-stagger-8" v-if="pendingReviewTasks.length > 0">
  212. <view class="section-title">待审核任务</view>
  213. <PlayfulCard
  214. variant="flat"
  215. shadow="sm"
  216. class="review-card"
  217. v-for="task in pendingReviewTasks"
  218. :key="task.id"
  219. padding="0"
  220. >
  221. <view class="review-card-content">
  222. <view class="review-info">
  223. <text class="task-name">{{ task.title }}</text>
  224. <text class="task-child" v-if="task.childName">提供者: {{ task.childName }}</text>
  225. </view>
  226. <view class="review-actions">
  227. <PlayfulButton variant="success" size="xs" @click="approveTask(task.id)">通过</PlayfulButton>
  228. <PlayfulButton variant="error" size="xs" @click="rejectTask(task.id)">拒绝</PlayfulButton>
  229. </view>
  230. </view>
  231. </PlayfulCard>
  232. </view>
  233. <!-- ===== g) 快捷操作 ===== -->
  234. <view class="quick-section animate-fade-in animate-stagger-9">
  235. <view class="section-title">快捷操作</view>
  236. <PlayfulCard variant="flat" class="quick-card">
  237. <view class="quick-actions">
  238. <view class="action-item" @click="addTask">
  239. <view class="action-icon act-add">
  240. <text class="action-icon-text">➕</text>
  241. </view>
  242. <text class="action-label">添加任务</text>
  243. </view>
  244. <view class="action-item" @click="goToReview">
  245. <view class="action-icon act-review">
  246. <text class="action-icon-text">📋</text>
  247. </view>
  248. <text class="action-label">审核任务</text>
  249. </view>
  250. <view class="action-item" @click="manageChildren">
  251. <view class="action-icon act-child">
  252. <text class="action-icon-text">👨‍👩‍👧</text>
  253. </view>
  254. <text class="action-label">孩子管理</text>
  255. </view>
  256. <view class="action-item" @click="goToRewards">
  257. <view class="action-icon act-reward">
  258. <text class="action-icon-text">🎁</text>
  259. </view>
  260. <text class="action-label">奖励中心</text>
  261. </view>
  262. <view class="action-item" @click="inviteFriend">
  263. <view class="action-icon act-invite">
  264. <text class="action-icon-text">📤</text>
  265. </view>
  266. <text class="action-label">邀请家人</text>
  267. </view>
  268. </view>
  269. </PlayfulCard>
  270. </view>
  271. </template>
  272. </view>
  273. </template>
  274. <script>
  275. import { getFamilyMembers, getChildren, getPendingReviewTasks, getPendingWishes, approveTask, rejectTask, getChildCompletionStats, getParentDashboard } from '../../utils/api.js'
  276. import WuxingSandbox from '../../components/wuxing-sandbox.vue'
  277. import PlayfulCard from '../../components/PlayfulCard.vue'
  278. import PlayfulButton from '../../components/PlayfulButton.vue'
  279. import BaseBadge from '../../components/BaseBadge.vue'
  280. import BaseEmpty from '../../components/BaseEmpty.vue'
  281. import BaseLoading from '../../components/BaseLoading.vue'
  282. export default {
  283. components: { WuxingSandbox, PlayfulCard, PlayfulButton, BaseBadge, BaseEmpty, BaseLoading },
  284. data() {
  285. return {
  286. nickname: '',
  287. familyMembers: [],
  288. children: [],
  289. selectedChild: null,
  290. pendingReviewTasks: [],
  291. pendingReviewWishes: [],
  292. pendingTasks: [],
  293. taskCompletionRate: 0,
  294. habitCount: 0,
  295. membershipText: '开通家庭会员 畅享全部权益',
  296. loading: true,
  297. currentChildId: '',
  298. _initialLoadDone: false,
  299. _familyId: null
  300. }
  301. },
  302. computed: {
  303. greetingText() {
  304. const hour = new Date().getHours()
  305. if (hour < 6) return '夜深了'
  306. if (hour < 9) return '早上好'
  307. if (hour < 12) return '上午好'
  308. if (hour < 14) return '中午好'
  309. if (hour < 18) return '下午好'
  310. if (hour < 22) return '晚上好'
  311. return '夜深了'
  312. },
  313. hasMilestones() {
  314. const c = this.selectedChild
  315. if (!c) return false
  316. return (c.streakDays >= 7) || (c.streakDays >= 30) || (c.totalPoints >= 1000)
  317. }
  318. },
  319. // 使用 mounted 而非 onLoad,因为本组件作为条件渲染子组件(v-if),
  320. // page 生命周期钩子(onLoad/onShow)可能在组件挂载前已触发完毕
  321. mounted() {
  322. if (!uni.getStorageSync('token')) {
  323. uni.reLaunch({ url: '/pages/login/login' })
  324. return
  325. }
  326. this.loadData()
  327. },
  328. onShow() {
  329. // 首次 onShow 紧接 mounted 之后,跳过避免双重加载
  330. if (!this._initialLoadDone) {
  331. this._initialLoadDone = true
  332. return
  333. }
  334. const currentRole = uni.getStorageSync('currentRole') || 'parent'
  335. if (currentRole === 'child') {
  336. uni.reLaunch({ url: '/pages/index/child-index' })
  337. return
  338. }
  339. if (uni.getStorageSync('token')) {
  340. this.loadData()
  341. }
  342. },
  343. methods: {
  344. async loadData() {
  345. this.loading = true
  346. try {
  347. const userInfo = uni.getStorageSync('userInfo')
  348. this.nickname = (userInfo && userInfo.nickname) ? userInfo.nickname : ''
  349. // 优先使用仪表盘接口(包含孩子列表 + 统计数据)
  350. let dashboardLoaded = false
  351. try {
  352. const dashRes = await getParentDashboard()
  353. const dashData = dashRes.data || {}
  354. const dashChildren = dashData.children || []
  355. if (dashChildren.length > 0) {
  356. this.children = dashChildren
  357. this.selectedChild = dashChildren[0]
  358. this.taskCompletionRate = dashData.avgCompletionRate || 0
  359. this.habitCount = dashData.totalHabitCount || 0
  360. // 从仪表盘获取 familyId
  361. if (dashData.familyId) {
  362. this._familyId = dashData.familyId
  363. }
  364. dashboardLoaded = true
  365. }
  366. } catch (e) {
  367. console.log('获取仪表盘数据失败,降级到单独接口', e)
  368. }
  369. // 降级:使用原有单独接口
  370. if (!dashboardLoaded) {
  371. try {
  372. const currentUserId = uni.getStorageSync('userId')
  373. const membersRes = await getFamilyMembers()
  374. const data = membersRes.data || {}
  375. const parents = (data.parents || []).filter(function(p) { return p && p.id && p.id !== currentUserId }).map(function(p) { return { ...p, role: 'parent' } })
  376. const children = (data.children || []).filter(function(c) { return c && c.id }).map(function(c) { return { ...c, role: 'child' } })
  377. this.familyMembers = [].concat(parents, children)
  378. if (children.length > 0) {
  379. this.selectedChild = children[0]
  380. }
  381. // 保存 familyId 用于后续请求
  382. if (data.familyId) {
  383. this._familyId = data.familyId
  384. }
  385. } catch (e) {
  386. console.log('获取家庭成员失败', e)
  387. }
  388. if (this.children.length === 0) {
  389. const childrenRes = await getChildren()
  390. const rawChildren = childrenRes.data || []
  391. this.children = rawChildren
  392. if (this.children.length > 0 && !this.selectedChild) {
  393. this.selectedChild = this.children[0]
  394. }
  395. }
  396. }
  397. const reviewRes = await getPendingReviewTasks()
  398. const allReviewTasks = reviewRes.data || []
  399. this.pendingReviewTasks = allReviewTasks.slice(0, 3)
  400. try {
  401. const wishRes = await getPendingWishes(this._familyId)
  402. this.pendingReviewWishes = wishRes.data || []
  403. } catch (e) {
  404. console.log('获取待处理心愿失败', e)
  405. this.pendingReviewWishes = []
  406. }
  407. const reviewMap = {}
  408. const wishMap = {}
  409. for (let i = 0; i < allReviewTasks.length; i++) {
  410. const task = allReviewTasks[i]
  411. const cid = task.childId || task.assigneeId
  412. if (cid) {
  413. reviewMap[cid] = (reviewMap[cid] || 0) + 1
  414. }
  415. }
  416. for (let j = 0; j < (this.pendingReviewWishes || []).length; j++) {
  417. const wish = this.pendingReviewWishes[j]
  418. const cid = wish.childId
  419. if (cid) {
  420. wishMap[cid] = (wishMap[cid] || 0) + 1
  421. }
  422. }
  423. this.children = this.children.map(function(c) {
  424. return {
  425. ...c,
  426. pendingReviewCount: reviewMap[c.id] || 0,
  427. pendingWishCount: wishMap[c.id] || 0
  428. }
  429. })
  430. // 如果仪表盘未加载到数据,降级到单独统计接口
  431. if (!dashboardLoaded && this.children.length > 0) {
  432. const targetChild = this.selectedChild || this.children[0]
  433. try {
  434. const statsRes = await getChildCompletionStats({ childId: targetChild.id })
  435. if (statsRes && statsRes.data) {
  436. this.taskCompletionRate = statsRes.data.completionRate || 0
  437. this.habitCount = statsRes.data.completedTasks || 0
  438. }
  439. } catch (e) {
  440. console.log('获取完成率失败', e)
  441. this.taskCompletionRate = 0
  442. }
  443. }
  444. // 待完成任务 - 使用真实统计中的pendingTasks
  445. this.pendingTasks = []
  446. if (this.children.length > 0) {
  447. const targetChild = this.selectedChild || this.children[0]
  448. try {
  449. const r2 = await getChildCompletionStats({ childId: targetChild.id })
  450. if (r2 && r2.data && r2.data.pendingTasks > 0) {
  451. for (let k = 0; k < Math.min(r2.data.pendingTasks, 3); k++) {
  452. this.pendingTasks.push({ id: 100 + k, title: '待完成任务 ' + (k + 1) })
  453. }
  454. }
  455. } catch (e) {
  456. this.pendingTasks = []
  457. }
  458. }
  459. } catch (e) {
  460. console.error('加载数据失败', e)
  461. } finally {
  462. this.loading = false
  463. }
  464. },
  465. async approveTask(taskId) {
  466. try {
  467. await approveTask(taskId)
  468. uni.showToast({ title: '已通过', icon: 'success' })
  469. this.loadData()
  470. } catch (e) {
  471. uni.showToast({ title: '操作失败', icon: 'none' })
  472. }
  473. },
  474. async rejectTask(taskId) {
  475. try {
  476. await rejectTask(taskId)
  477. uni.showToast({ title: '已拒绝', icon: 'success' })
  478. this.loadData()
  479. } catch (e) {
  480. uni.showToast({ title: '操作失败', icon: 'none' })
  481. }
  482. },
  483. goToCompletionStats() {
  484. if (this.children.length === 1) {
  485. uni.navigateTo({ url: '/pages/stats/completion-rate?childId=' + this.children[0].id })
  486. } else {
  487. uni.navigateTo({ url: '/pages/stats/completion-rate?familyView=1' })
  488. }
  489. },
  490. goToDomain(domain) {
  491. uni.showToast({ title: '即将上线', icon: 'none' })
  492. },
  493. goToMembership() {
  494. uni.showToast({ title: '即将上线', icon: 'none' })
  495. },
  496. goToChildDetail(child) {
  497. uni.navigateTo({ url: '/pages/parent/child-detail?childId=' + child.id + '&childName=' + encodeURIComponent(child.nickname || '') })
  498. },
  499. addTask() { uni.navigateTo({ url: '/pages/tasks/create-task' }) },
  500. goToReview() { uni.navigateTo({ url: '/pages/tasks/review' }) },
  501. goToRewards() { uni.switchTab({ url: '/pages/rewards/rewards' }) },
  502. goToTaskList() { uni.switchTab({ url: '/pages/tasks/tasks' }) },
  503. manageChildren() { uni.navigateTo({ url: '/pages/profile/children' }) },
  504. inviteFriend() { uni.navigateTo({ url: '/pages/parent/invite/index' }) },
  505. goToAddChild() { uni.navigateTo({ url: '/pages/profile/create-child' }) }
  506. }
  507. }
  508. </script>
  509. <style scoped>
  510. /* ========================================
  511. Flat Card Design System — 家长端暖橙扁平
  512. ======================================== */
  513. .container {
  514. padding: 32rpx;
  515. background: var(--bg, #FFF7ED);
  516. min-height: 100vh;
  517. }
  518. /* ========================================
  519. 区块标题
  520. ======================================== */
  521. .section-title {
  522. font-size: 32rpx;
  523. font-weight: 700;
  524. color: var(--text, #3D2E1E);
  525. margin-bottom: var(--space-md);
  526. }
  527. /* ========================================
  528. a) 顶部欢迎区
  529. ======================================== */
  530. .welcome-section {
  531. margin-bottom: 48rpx;
  532. }
  533. .welcome-header {
  534. display: flex;
  535. justify-content: space-between;
  536. align-items: flex-start;
  537. margin-bottom: 24rpx;
  538. }
  539. .greeting {
  540. font-size: 40rpx;
  541. font-weight: 700;
  542. color: var(--text, #3D2E1E);
  543. display: block;
  544. line-height: 1.2;
  545. }
  546. .sub-greeting {
  547. font-size: 28rpx;
  548. color: var(--text-secondary, #6B5A4A);
  549. display: block;
  550. margin-top: 8rpx;
  551. }
  552. .greeting-emoji {
  553. font-size: 64rpx;
  554. }
  555. /* ========================================
  556. b) 孩子卡片
  557. ======================================== */
  558. .children-row {
  559. margin-bottom: 48rpx;
  560. }
  561. .children-scroll {
  562. display: flex;
  563. flex-direction: row;
  564. white-space: nowrap;
  565. overflow: hidden;
  566. }
  567. .child-card {
  568. display: inline-flex !important;
  569. margin-right: 20rpx;
  570. min-width: 440rpx;
  571. width: auto;
  572. }
  573. .child-card-single {
  574. width: 100%;
  575. margin-right: 0;
  576. }
  577. .child-card-inner {
  578. display: flex;
  579. align-items: center;
  580. width: 100%;
  581. }
  582. .child-avatar {
  583. width: 80rpx;
  584. height: 80rpx;
  585. border-radius: var(--radius-full, 50%);
  586. display: flex;
  587. align-items: center;
  588. justify-content: center;
  589. flex-shrink: 0;
  590. }
  591. .child-avatar-color-0 { background: linear-gradient(135deg, var(--color-primary, #F97316), var(--color-primary-dark, #EA580C)); }
  592. .child-avatar-color-1 { background: linear-gradient(135deg, var(--color-accent, #0EA5E9), #6366F1); }
  593. .child-avatar-color-2 { background: linear-gradient(135deg, #10B981, #059669); }
  594. .child-avatar-color-3 { background: linear-gradient(135deg, #EC4899, #E11D48); }
  595. .child-avatar-color-4 { background: linear-gradient(135deg, #84CC16, #65A30D); }
  596. .child-avatar-color-5 { background: linear-gradient(135deg, #8B5CF6, #6D28D9); }
  597. .child-avatar-text {
  598. color: #FFFFFF;
  599. font-size: 36rpx;
  600. font-weight: 600;
  601. }
  602. .child-card-body {
  603. flex: 1;
  604. margin-left: 16rpx;
  605. min-width: 0;
  606. }
  607. .child-card-name {
  608. font-size: 30rpx;
  609. font-weight: 600;
  610. color: var(--text, #3D2E1E);
  611. display: block;
  612. line-height: 1.3;
  613. }
  614. .child-card-stats {
  615. display: flex;
  616. align-items: center;
  617. margin-top: 6rpx;
  618. gap: 16rpx;
  619. }
  620. .child-card-points {
  621. font-size: 24rpx;
  622. color: var(--color-primary, #F97316);
  623. font-weight: 600;
  624. }
  625. .child-card-dan {
  626. font-size: 22rpx;
  627. color: var(--color-accent, #0EA5E9);
  628. font-weight: 500;
  629. }
  630. .child-card-badges {
  631. display: flex;
  632. gap: 8rpx;
  633. margin-top: 8rpx;
  634. flex-wrap: wrap;
  635. }
  636. .child-card-arrow {
  637. font-size: 36rpx;
  638. color: var(--muted, #94A3B8);
  639. margin-left: 8rpx;
  640. flex-shrink: 0;
  641. }
  642. .scroll-hint {
  643. text-align: center;
  644. margin-top: 12rpx;
  645. }
  646. .scroll-hint-text {
  647. font-size: 22rpx;
  648. color: var(--muted, #94A3B8);
  649. }
  650. /* Empty children */
  651. .empty-children-card {
  652. margin-top: 24rpx;
  653. }
  654. .empty-children-inner {
  655. display: flex;
  656. align-items: center;
  657. }
  658. .empty-children-icon {
  659. font-size: 56rpx;
  660. margin-right: 16rpx;
  661. }
  662. .empty-children-text {
  663. flex: 1;
  664. }
  665. .empty-children-title {
  666. font-size: 28rpx;
  667. font-weight: 600;
  668. color: var(--text, #3D2E1E);
  669. display: block;
  670. }
  671. .empty-children-hint {
  672. font-size: 24rpx;
  673. color: var(--muted, #94A3B8);
  674. margin-top: 6rpx;
  675. display: block;
  676. }
  677. .empty-children-arrow {
  678. font-size: 36rpx;
  679. color: var(--muted, #94A3B8);
  680. }
  681. /* ========================================
  682. WuxingSandbox & Membership Wrappers
  683. ======================================== */
  684. .membership-wrapper {
  685. margin-bottom: 48rpx;
  686. }
  687. /* 汐福俱乐部 - 会员Banner */
  688. .membership-banner {
  689. padding: 30rpx 32rpx;
  690. background: linear-gradient(135deg, #FFD700, #FFA500);
  691. border-radius: var(--radius-md, 20rpx);
  692. box-shadow: var(--shadow-md);
  693. }
  694. .banner-content {
  695. display: flex;
  696. align-items: center;
  697. }
  698. .banner-icon {
  699. font-size: 60rpx;
  700. margin-right: 20rpx;
  701. }
  702. .banner-text {
  703. flex: 1;
  704. }
  705. .banner-title {
  706. font-size: 30rpx;
  707. font-weight: bold;
  708. color: #8B4513;
  709. display: block;
  710. }
  711. .banner-subtitle {
  712. font-size: 24rpx;
  713. color: #A0522D;
  714. margin-top: 6rpx;
  715. display: block;
  716. }
  717. .banner-arrow {
  718. font-size: 40rpx;
  719. color: #8B4513;
  720. }
  721. /* ========================================
  722. c) 成长数据
  723. ======================================== */
  724. .stats-section {
  725. margin-bottom: 48rpx;
  726. }
  727. .stats-grid {
  728. display: flex;
  729. flex-wrap: wrap;
  730. gap: 16rpx;
  731. }
  732. .stat-card {
  733. width: calc(50% - 8rpx);
  734. box-sizing: border-box;
  735. }
  736. .stat-card-content {
  737. padding: 24rpx;
  738. }
  739. .stat-header {
  740. display: flex;
  741. align-items: center;
  742. margin-bottom: 14rpx;
  743. }
  744. .stat-icon-wrap {
  745. width: 56rpx;
  746. height: 56rpx;
  747. border-radius: var(--radius-sm, 12rpx);
  748. display: flex;
  749. align-items: center;
  750. justify-content: center;
  751. margin-right: 14rpx;
  752. flex-shrink: 0;
  753. }
  754. .stat-icon-symbol {
  755. font-size: 28rpx;
  756. }
  757. .stat-icon-orange { background: rgba(249, 115, 22, 0.12); }
  758. .stat-icon-gold { background: rgba(245, 158, 11, 0.15); }
  759. .stat-icon-red { background: rgba(239, 68, 68, 0.12); }
  760. .stat-icon-teal { background: rgba(16, 185, 129, 0.12); }
  761. .stat-body {
  762. flex: 1;
  763. min-width: 0;
  764. }
  765. .stat-value {
  766. font-size: 40rpx;
  767. font-weight: 800;
  768. display: block;
  769. line-height: 1.1;
  770. }
  771. .stat-label {
  772. font-size: 22rpx;
  773. color: var(--text-secondary, #6B5A4A);
  774. display: block;
  775. margin-top: 4rpx;
  776. }
  777. .accent-color { color: var(--color-primary, #F97316); }
  778. .gold-color { color: #F59E0B; }
  779. .red-color { color: var(--color-error, #EF4444); }
  780. .teal-color { color: #10B981; }
  781. .stat-progress {
  782. margin-bottom: 10rpx;
  783. }
  784. .progress-bar {
  785. height: 8rpx;
  786. background: var(--border-light, #FFEDD5);
  787. border-radius: 999rpx;
  788. overflow: hidden;
  789. }
  790. .progress-fill {
  791. height: 100%;
  792. background: linear-gradient(90deg, var(--color-primary, #F97316), #F59E0B);
  793. border-radius: 999rpx;
  794. transition: width 0.3s ease;
  795. }
  796. .stat-more {
  797. font-size: 22rpx;
  798. color: var(--color-accent, #0EA5E9);
  799. font-weight: 500;
  800. display: block;
  801. }
  802. .stat-encourage {
  803. font-size: 22rpx;
  804. color: #10B981;
  805. font-weight: 500;
  806. margin-top: 10rpx;
  807. display: block;
  808. }
  809. .stat-encourage.muted {
  810. color: var(--muted, #94A3B8);
  811. }
  812. /* ========================================
  813. d) 今日重点
  814. ======================================== */
  815. .today-section {
  816. margin-bottom: 48rpx;
  817. }
  818. .today-card .today-header {
  819. display: flex;
  820. justify-content: space-between;
  821. align-items: center;
  822. }
  823. .today-label {
  824. font-size: 28rpx;
  825. font-weight: 600;
  826. color: var(--text, #3D2E1E);
  827. }
  828. .today-count {
  829. font-size: 24rpx;
  830. color: var(--color-primary, #F97316);
  831. font-weight: 600;
  832. }
  833. .task-list {
  834. margin-bottom: 8rpx;
  835. }
  836. .task-item {
  837. display: flex;
  838. align-items: center;
  839. padding: 18rpx 0;
  840. border-bottom: 1rpx solid var(--border-light, #FFEDD5);
  841. }
  842. .task-item:last-child {
  843. border-bottom: none;
  844. }
  845. .task-dot {
  846. width: 14rpx;
  847. height: 14rpx;
  848. border-radius: 50%;
  849. margin-right: 16rpx;
  850. flex-shrink: 0;
  851. }
  852. .dot-color-0 { background: var(--color-primary, #F97316); }
  853. .dot-color-1 { background: #10B981; }
  854. .dot-color-2 { background: var(--color-accent, #0EA5E9); }
  855. .dot-color-3 { background: #F59E0B; }
  856. .dot-color-4 { background: var(--color-error, #EF4444); }
  857. .task-title {
  858. font-size: 28rpx;
  859. color: var(--text, #3D2E1E);
  860. }
  861. .task-view-all {
  862. display: flex;
  863. justify-content: flex-end;
  864. align-items: center;
  865. color: var(--text-secondary, #6B5A4A);
  866. font-size: 24rpx;
  867. padding-top: 16rpx;
  868. border-top: 1rpx solid var(--border-light, #FFEDD5);
  869. }
  870. .task-view-all .arrow {
  871. margin-left: 8rpx;
  872. }
  873. /* ========================================
  874. e) 成长里程碑
  875. ======================================== */
  876. .milestone-section {
  877. margin-bottom: 48rpx;
  878. }
  879. .milestone-card {
  880. padding: 24rpx !important;
  881. }
  882. .milestone-item {
  883. display: flex;
  884. align-items: center;
  885. padding: 16rpx 0;
  886. border-bottom: 1rpx solid var(--border-light, #FFEDD5);
  887. }
  888. .milestone-item:last-child {
  889. border-bottom: none;
  890. }
  891. .milestone-icon-wrap {
  892. width: 56rpx;
  893. height: 56rpx;
  894. border-radius: var(--radius-sm, 12rpx);
  895. display: flex;
  896. align-items: center;
  897. justify-content: center;
  898. margin-right: 16rpx;
  899. flex-shrink: 0;
  900. }
  901. .milestone-icon {
  902. font-size: 28rpx;
  903. }
  904. .milestone-star { background: rgba(245, 158, 11, 0.15); }
  905. .milestone-trophy { background: rgba(249, 115, 22, 0.12); }
  906. .milestone-sparkle { background: rgba(16, 185, 129, 0.12); }
  907. .milestone-text {
  908. font-size: 28rpx;
  909. color: var(--text, #3D2E1E);
  910. }
  911. /* ========================================
  912. f) 待审核任务
  913. ======================================== */
  914. .review-section {
  915. margin-bottom: 48rpx;
  916. }
  917. .review-card {
  918. margin-bottom: 16rpx;
  919. }
  920. .review-card:last-child {
  921. margin-bottom: 0;
  922. }
  923. .review-card-content {
  924. display: flex;
  925. justify-content: space-between;
  926. align-items: center;
  927. padding: 24rpx;
  928. }
  929. .review-info {
  930. flex: 1;
  931. min-width: 0;
  932. }
  933. .review-info .task-name {
  934. font-size: 28rpx;
  935. color: var(--text, #3D2E1E);
  936. display: block;
  937. margin-bottom: 4rpx;
  938. }
  939. .review-info .task-child {
  940. font-size: 22rpx;
  941. color: var(--muted, #94A3B8);
  942. }
  943. .review-actions {
  944. display: flex;
  945. gap: 12rpx;
  946. flex-shrink: 0;
  947. margin-left: 16rpx;
  948. }
  949. /* ========================================
  950. g) 快捷操作
  951. ======================================== */
  952. .quick-section {
  953. margin-bottom: 48rpx;
  954. }
  955. .quick-card {
  956. padding: 16rpx 12rpx !important;
  957. }
  958. .quick-actions {
  959. display: flex;
  960. flex-wrap: wrap;
  961. }
  962. .action-item {
  963. display: flex;
  964. flex-direction: column;
  965. align-items: center;
  966. width: 20%;
  967. margin-bottom: 12rpx;
  968. transition: transform 0.15s ease;
  969. }
  970. .action-item:active {
  971. transform: scale(0.92);
  972. }
  973. .action-icon {
  974. width: 80rpx;
  975. height: 80rpx;
  976. border-radius: var(--radius-sm, 12rpx);
  977. display: flex;
  978. align-items: center;
  979. justify-content: center;
  980. margin-bottom: 10rpx;
  981. }
  982. .action-icon-text {
  983. font-size: 32rpx;
  984. }
  985. .act-add { background: rgba(249, 115, 22, 0.12); }
  986. .act-review { background: rgba(16, 185, 129, 0.12); }
  987. .act-child { background: rgba(14, 165, 233, 0.12); }
  988. .act-reward { background: rgba(245, 158, 11, 0.15); }
  989. .act-invite { background: rgba(139, 92, 246, 0.12); }
  990. .action-label {
  991. font-size: 22rpx;
  992. color: var(--text-secondary, #6B5A4A);
  993. font-weight: 500;
  994. text-align: center;
  995. }
  996. </style>