parent-index.vue 70 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256
  1. <template>
  2. <view class="container">
  3. <!-- 加载状态 -->
  4. <BaseLoading :loading="loading" text="正在加载..." />
  5. <template v-if="!loading">
  6. <!-- ===== a) 顶部 Banner ===== -->
  7. <!-- ===== 新手任务进度条(onboarding 置顶引导) ===== -->
  8. <OnboardingProgressBar ref="onboardingProgress" />
  9. <!-- ===== 家庭成员选择条 ===== -->
  10. <FamilyMemberStrip
  11. v-if="isLoggedIn"
  12. :members="familyMembersVisible"
  13. :selectedMemberId="selectedMemberId"
  14. :isParent="isFamilyAdmin"
  15. @select="onFamilyMemberSelect"
  16. @switch="onFamilyMemberSwitch"
  17. @removed="loadFamilyMembersVisible" />
  18. <!-- ===== 无家庭成员空状态引导卡 ===== -->
  19. <view class="empty-family-card" v-if="isLoggedIn && familyMembersVisible.length === 0">
  20. <view class="empty-family-icon">🏠</view>
  21. <text class="empty-family-title">创建我的家庭</text>
  22. <text class="empty-family-desc">添加家庭成员,开启五维能量之旅</text>
  23. <button class="empty-family-btn" @click="goAddMember">立即添加</button>
  24. </view>
  25. <!-- ===== 微行动卡片(P0:有效启动) ===== -->
  26. <MicroActionCard
  27. v-if="selectedMemberId && selectedChild && selectedChild.id"
  28. :memberId="selectedChild.id"
  29. @completed="onMicroActionCompleted" />
  30. <!-- ===== 健康预警横幅(P1-2) ===== -->
  31. <HealthAlertBanner
  32. v-if="selectedMemberId && selectedChild && selectedChild.id"
  33. :alerts="healthAlerts"
  34. @dismiss="onAlertDismiss" />
  35. <!-- ===== 家庭健康排行榜(P2-2) ===== -->
  36. <FamilyHealthRanking
  37. v-if="rankingMembers && rankingMembers.length > 0"
  38. :list="rankingMembers"
  39. :totalScore="rankingTotalScore" />
  40. <!-- ===== 家庭挑战区域 ===== -->
  41. <view class="challenge-section">
  42. <view class="challenge-section-header" @click="goToChallengeManage">
  43. <text class="challenge-section-title">家庭挑战</text>
  44. <text class="challenge-section-action">管理 →</text>
  45. </view>
  46. <FamilyChallengeCard
  47. v-for="ch in activeChallenges"
  48. :key="ch.id"
  49. :challenge="ch"
  50. @cardClick="goToChallengeManage(ch.id)" />
  51. </view>
  52. <!-- ===== 健康圈入口(P3:社区扩散) ===== -->
  53. <view class="circle-entry-section">
  54. <!-- 有健康圈数据:显示排行榜预览 -->
  55. <PlayfulCard v-if="circleName && circleRankings.length > 0" variant="flat" shadow="sm" :clickable="true" @click="goToHealthCircle" class="circle-card-wide">
  56. <view class="circle-card-content">
  57. <view class="circle-header">
  58. <view class="circle-icon-wrap">
  59. <text class="circle-icon">🏅</text>
  60. </view>
  61. <view class="circle-header-body">
  62. <text class="circle-card-title">健康圈</text>
  63. <text class="circle-card-subtitle">{{ circleName }}</text>
  64. </view>
  65. </view>
  66. <view class="circle-ranking-preview">
  67. <view class="circle-rank-row" v-for="(item, idx) in circleRankings.slice(0, 3)" :key="idx">
  68. <text class="circle-rank-badge">{{ rankIcon(idx) }}</text>
  69. <text class="circle-rank-family">家庭 {{ idx + 1 }}</text>
  70. <text class="circle-rank-score">{{ item.totalScore || 0 }} 分</text>
  71. </view>
  72. </view>
  73. <view class="circle-action">
  74. <text class="circle-action-text">查看详情 →</text>
  75. </view>
  76. </view>
  77. </PlayfulCard>
  78. <!-- 无健康圈数据:邀请加入 -->
  79. <PlayfulCard v-else variant="flat" shadow="sm" :clickable="true" @click="goToHealthCircle" class="circle-card-wide circle-card-invite">
  80. <view class="circle-invite-content">
  81. <view class="circle-invite-icon">🏅</view>
  82. <text class="circle-invite-title">健康圈</text>
  83. <text class="circle-invite-desc">加入健康圈,和更多家庭一起健康打卡</text>
  84. <text class="circle-invite-cta">去看看 →</text>
  85. </view>
  86. </PlayfulCard>
  87. </view>
  88. <!-- ===== 五维家庭自检入口(P0-1/P0-2) ===== -->
  89. <view class="selfcheck-entry-section">
  90. <PlayfulCard variant="flat" shadow="sm" :clickable="true" @click="goToSelfCheck" class="selfcheck-card-wide">
  91. <view class="selfcheck-card-content">
  92. <view class="selfcheck-icon-wrap">
  93. <view class="selfcheck-icon-circle" style="background: #FF8C42;">
  94. <text class="selfcheck-icon-char">身</text>
  95. </view>
  96. <view class="selfcheck-icon-circle" style="background: #6366F1;">
  97. <text class="selfcheck-icon-char">智</text>
  98. </view>
  99. <view class="selfcheck-icon-circle" style="background: #F59E0B;">
  100. <text class="selfcheck-icon-char">富</text>
  101. </view>
  102. <view class="selfcheck-icon-circle" style="background: #10B981;">
  103. <text class="selfcheck-icon-char">行</text>
  104. </view>
  105. <view class="selfcheck-icon-circle" style="background: #FF6B9D;">
  106. <text class="selfcheck-icon-char">心</text>
  107. </view>
  108. </view>
  109. <view class="selfcheck-body">
  110. <text class="selfcheck-title">五维家庭自检</text>
  111. <text class="selfcheck-subtitle" v-if="selfCheckScore != null">
  112. 最近得分 {{ selfCheckScore }}/45 分
  113. </text>
  114. <text class="selfcheck-subtitle" v-else>
  115. 15题 · 5分钟 · 生成五行相生寻源建议
  116. </text>
  117. </view>
  118. <text class="selfcheck-arrow">→</text>
  119. </view>
  120. </PlayfulCard>
  121. </view>
  122. <!-- ===== 家庭成员关系图谱 ===== -->
  123. <FamilyRelationGraph
  124. v-if="familyMembersVisible.length > 0"
  125. dimensionCode="home"
  126. :selfId="selfMemberId"
  127. :members="familyMembersVisible"
  128. :energyMap="energyMapForGraph"
  129. :intimacyMap="intimacyMapForGraph"
  130. @memberTap="goMemberDetail" />
  131. <!-- 通关引导进度条 -->
  132. <GateGuideBar :unlockStatus="unlockStatus" />
  133. <!-- ===== 新增:首页广告栏 ===== -->
  134. <AdBanner
  135. :activities="recommendedActivities"
  136. :products="recommendedProducts"
  137. :autoPlayInterval="3000"
  138. :swiperDuration="500" />
  139. <!-- ===== 能量沙盘 ===== -->
  140. <GateLock v-if="sandboxLocked && sandboxLockGate" :gate="sandboxLockGate" />
  141. <WuxingSandbox
  142. v-else-if="sandboxData"
  143. mode="live"
  144. :scores="familySandboxScores"
  145. :overallScore="familySandboxOverall"
  146. :members="familySandboxMembers"
  147. :personalData="sandboxPersonalData"
  148. :dimensions="energyDimensions"
  149. :totalEnergy="totalEnergy"
  150. :totalHealthIndex="totalHealthIndex"
  151. :familyUnlocked="familyUnlocked"
  152. :currentView="currentSandboxView"
  153. :memberCount="familyMembersVisible.length"
  154. starHeight="400rpx"
  155. showDimensionBar
  156. @view-change="onSandboxViewChange"
  157. @point-click="onDomainClick" />
  158. <!-- 能量数据加载失败兜底 -->
  159. <view class="error-state" v-if="errorEnergy && !sandboxData && !sandboxLocked" @click="loadData">
  160. <text class="error-state-text">家庭能量数据加载失败,点击重试</text>
  161. </view>
  162. <!-- ===== c) 成长数据 ===== -->
  163. <view class="stats-section animate-fade-in animate-stagger-5">
  164. <view class="section-title">成长数据</view>
  165. <view class="stats-row">
  166. <!-- 任务完成率:自己的 + 全家平均 -->
  167. <PlayfulCard variant="flat" shadow="sm" :clickable="true" @click="goToCompletionStats" class="stat-card-wide" padding="0">
  168. <view class="stat-card-content">
  169. <view class="stat-header">
  170. <view class="stat-icon-wrap stat-icon-orange">
  171. <text class="stat-icon-symbol">📋</text>
  172. </view>
  173. <view class="stat-body">
  174. <text class="stat-label-header">任务完成</text>
  175. </view>
  176. </view>
  177. <view class="stat-compare">
  178. <view class="stat-compare-row">
  179. <text class="compare-label">自己的</text>
  180. <text class="compare-value accent-color">{{ myCompletionRate }}%</text>
  181. </view>
  182. <view class="progress-bar-sm">
  183. <view class="progress-fill-sm" :style="{ width: myCompletionRate + '%' }"></view>
  184. </view>
  185. <view class="stat-compare-row">
  186. <text class="compare-label">全家平均</text>
  187. <text class="compare-value">{{ familyAvgCompletionRate }}%</text>
  188. </view>
  189. <view class="progress-bar-sm">
  190. <view class="progress-fill-sm muted" :style="{ width: familyAvgCompletionRate + '%' }"></view>
  191. </view>
  192. </view>
  193. <text class="stat-more">查看分析 ›</text>
  194. </view>
  195. </PlayfulCard>
  196. <!-- 成长积分:自己的 + 全家共 -->
  197. <PlayfulCard variant="flat" shadow="sm" :clickable="true" @click="goToRewards" class="stat-card-wide" padding="0">
  198. <view class="stat-card-content">
  199. <view class="stat-header">
  200. <view class="stat-icon-wrap stat-icon-gold">
  201. <text class="stat-icon-symbol">⭐</text>
  202. </view>
  203. <view class="stat-body">
  204. <text class="stat-label-header">成长积分</text>
  205. </view>
  206. </view>
  207. <view class="stat-compare">
  208. <view class="stat-compare-row">
  209. <text class="compare-label">自己的</text>
  210. <text class="compare-value gold-color">{{ myPoints }}</text>
  211. </view>
  212. <view class="stat-compare-row">
  213. <text class="compare-label">全家共</text>
  214. <text class="compare-value">{{ familyTotalPoints }}</text>
  215. </view>
  216. </view>
  217. <text class="stat-encourage" v-if="myPoints > 0">太棒了!</text>
  218. <text class="stat-encourage muted" v-else>开始赚星星吧</text>
  219. </view>
  220. </PlayfulCard>
  221. </view>
  222. </view>
  223. <!-- ===== d) 今日重点 ===== -->
  224. <view class="today-section animate-fade-in animate-stagger-7">
  225. <view class="section-title">📋 今日任务</view>
  226. <PlayfulCard variant="flat" class="today-card">
  227. <template slot="header">
  228. <view class="today-header">
  229. <text class="today-label">今日待完成</text>
  230. <text class="today-count">{{ pendingTasks.length }}项任务</text>
  231. </view>
  232. </template>
  233. <view class="task-list" v-if="pendingTasks.length > 0">
  234. <view class="task-item" v-for="task in pendingTasks.slice(0, 3)" :key="task.id">
  235. <view class="task-dot" :class="'dot-color-' + (task.id % 5)"></view>
  236. <text class="task-title">{{ task.title }}</text>
  237. </view>
  238. </view>
  239. <BaseEmpty v-else icon="🎉" title="今日任务已完成" description="太棒了!" :compact="true" />
  240. <view slot="actions" class="task-view-all" @click="goToTaskList">
  241. <text>查看全部</text>
  242. <text class="arrow">→</text>
  243. </view>
  244. </PlayfulCard>
  245. </view>
  246. <!-- ===== e) 成长里程碑 ===== -->
  247. <view class="milestone-section animate-fade-in animate-stagger-8">
  248. <view class="section-title">成长里程碑</view>
  249. <PlayfulCard variant="flat" class="milestone-card">
  250. <view class="milestone-item" v-if="selectedChild && selectedChild.streakDays >= 7">
  251. <view class="milestone-icon-wrap milestone-star">
  252. <text class="milestone-icon">🌟</text>
  253. </view>
  254. <text class="milestone-text">连续7天打卡</text>
  255. </view>
  256. <view class="milestone-item" v-if="selectedChild && selectedChild.streakDays >= 30">
  257. <view class="milestone-icon-wrap milestone-trophy">
  258. <text class="milestone-icon">🏆</text>
  259. </view>
  260. <text class="milestone-text">坚持一个月啦!</text>
  261. </view>
  262. <view class="milestone-item" v-if="(selectedChild && selectedChild.totalPoints) >= 1000">
  263. <view class="milestone-icon-wrap milestone-sparkle">
  264. <text class="milestone-icon">💫</text>
  265. </view>
  266. <text class="milestone-text">积分突破1000</text>
  267. </view>
  268. <BaseEmpty v-if="!hasMilestones" icon="🌱" title="开始养成好习惯" description="见证成长" :compact="true" />
  269. </PlayfulCard>
  270. </view>
  271. <!-- ===== f) 待处理事项 ===== -->
  272. <view class="pending-section animate-fade-in animate-stagger-9">
  273. <view class="section-title">📋 待处理事项</view>
  274. <view class="pending-grid">
  275. <view class="pending-card" @click="goToReview">
  276. <view class="pending-icon pending-icon--blue">
  277. <text class="pending-emoji">📝</text>
  278. </view>
  279. <view class="pending-info">
  280. <text class="pending-count accent-color">{{ pendingReviewTasks.length }}</text>
  281. <text class="pending-label">待审核任务</text>
  282. </view>
  283. </view>
  284. <view class="pending-card" @click="goToWishApprove">
  285. <view class="pending-icon pending-icon--pink">
  286. <text class="pending-emoji">💝</text>
  287. </view>
  288. <view class="pending-info">
  289. <text class="pending-count pink-color">{{ pendingReviewWishes.length }}</text>
  290. <text class="pending-label">待审批心愿</text>
  291. </view>
  292. </view>
  293. <view class="pending-card" @click="goToAssessmentReport">
  294. <view class="pending-icon pending-icon--green">
  295. <text class="pending-emoji">📊</text>
  296. </view>
  297. <view class="pending-info">
  298. <text class="pending-count teal-color">{{ hasNewReport ? '有新' : '暂无' }}</text>
  299. <text class="pending-label">最新测评报告</text>
  300. </view>
  301. </view>
  302. <view class="pending-card" @click="goToActivities">
  303. <view class="pending-icon pending-icon--orange">
  304. <text class="pending-emoji">🎉</text>
  305. </view>
  306. <view class="pending-info">
  307. <text class="pending-count orange-color">{{ hotActivityCount > 0 ? hotActivityCount : '暂无' }}</text>
  308. <text class="pending-label">活动推荐</text>
  309. </view>
  310. </view>
  311. </view>
  312. </view>
  313. <!-- ===== 重要关系(外部通讯录) ===== -->
  314. <view class="contact-section" v-if="!loading && familyMembersVisible.length > 0">
  315. <view class="contact-header">
  316. <text class="contact-section-title">重要关系</text>
  317. <view class="contact-header-right">
  318. <text class="contact-more" @click="loadContacts">刷新</text>
  319. <text class="contact-import-btn" @click="onShowImport">+ 导入</text>
  320. </view>
  321. </view>
  322. <scroll-view class="contact-scroll" scroll-x enable-flex>
  323. <view class="contact-scroll-inner">
  324. <view class="contact-card-wrap" v-for="item in contactList" :key="item.id" @click="onContactClick(item)">
  325. <ContactCard :contact="item" />
  326. </view>
  327. <view class="contact-card-wrap contact-add-card" @click="onShowImport">
  328. <view class="add-card-inner">
  329. <text class="add-card-icon">+</text>
  330. <text class="add-card-label">导入通讯录</text>
  331. </view>
  332. </view>
  333. </view>
  334. </scroll-view>
  335. <view class="contact-empty" v-if="contactList.length === 0">
  336. <text class="contact-empty-text">暂无重要联系人,点击导入添加</text>
  337. </view>
  338. </view>
  339. <!-- 导入联系人弹窗 -->
  340. <ContactImport
  341. :visible="showImportModal"
  342. @close="onCloseImport"
  343. @success="onImportSuccess" />
  344. <!-- 邀请码二维码弹窗 -->
  345. <view class="qr-code-modal" v-if="showQrCodeModal" @touchmove.stop.prevent @click="onCloseQrCodeModal">
  346. <view class="qr-code-modal-content" @click.stop>
  347. <view class="qr-code-modal-header">
  348. <text class="qr-code-modal-title">邀请二维码</text>
  349. <text class="qr-code-modal-close" @click="onCloseQrCodeModal">×</text>
  350. </view>
  351. <view class="qr-code-modal-body">
  352. <view class="qr-code-image-container">
  353. <image :src="(qrCodeData && qrCodeData.qrCodeBase64) || ''" class="qr-code-image" mode="aspectFit" />
  354. </view>
  355. <view class="qr-code-referral-code">
  356. <text class="qr-code-label">邀请码:</text>
  357. <text class="qr-code-code">{{ (qrCodeData && qrCodeData.referralCode) || '' }}</text>
  358. <text class="qr-code-copy" @click="onCopyReferralCode">复制</text>
  359. </view>
  360. </view>
  361. </view>
  362. </view>
  363. <!-- ===== 推荐商品+活动(混合信息流) ===== -->
  364. <RecommendedFeed
  365. :products="recommendedProducts"
  366. :activities="recommendedActivities"
  367. :loading="loadingProducts || loadingActivities"
  368. @itemClick="onFeedItemClick"
  369. @more="goMoreDiscover" />
  370. <view class="error-state" v-if="errorProducts || errorActivities">
  371. <text class="error-state-text">推荐加载失败</text>
  372. </view>
  373. <!-- ===== 推荐阅读(书册风格) ===== -->
  374. <view class="article-section animate-fade-in animate-stagger-11">
  375. <view class="article-header">
  376. <view class="section-title">📖 推荐阅读</view>
  377. <text class="article-more" @click="goArticles">更多 ›</text>
  378. </view>
  379. <ArticleBookshelf
  380. :articles="indexArticles"
  381. :loading="articlesLoading"
  382. :isLoggedIn="isLoggedIn"
  383. @articleClick="goArticleDetail"
  384. />
  385. </view>
  386. <!-- ===== h) 待审核任务 ===== -->
  387. <view class="review-section animate-fade-in animate-stagger-11" v-if="pendingReviewTasks.length > 0">
  388. <view class="section-title">待审核任务</view>
  389. <PlayfulCard
  390. variant="flat"
  391. shadow="sm"
  392. class="review-card"
  393. v-for="task in pendingReviewTasks"
  394. :key="task.id"
  395. padding="0"
  396. >
  397. <view class="review-card-content">
  398. <view class="review-info">
  399. <text class="task-name">{{ task.title }}</text>
  400. <text class="task-child" v-if="task.childName">提供者: {{ task.childName }}</text>
  401. </view>
  402. <view class="review-actions">
  403. <PlayfulButton variant="success" size="xs" @click="approveTask(task.id)">通过</PlayfulButton>
  404. <PlayfulButton variant="error" size="xs" @click="rejectTask(task.id)">拒绝</PlayfulButton>
  405. </view>
  406. </view>
  407. </PlayfulCard>
  408. </view>
  409. <!-- ===== g) 快捷操作 ===== -->
  410. <view class="quick-section animate-fade-in animate-stagger-10">
  411. <view class="section-title">快捷操作</view>
  412. <PlayfulCard variant="flat" class="quick-card">
  413. <view class="quick-actions">
  414. <view class="action-item" @click="addTask">
  415. <view class="action-icon act-add">
  416. <text class="action-icon-text">➕</text>
  417. </view>
  418. <text class="action-label">添加任务</text>
  419. </view>
  420. <view class="action-item" @click="goToReview">
  421. <view class="action-icon act-review">
  422. <text class="action-icon-text">📋</text>
  423. </view>
  424. <text class="action-label">审核任务</text>
  425. </view>
  426. <view class="action-item" @click="manageChildren">
  427. <view class="action-icon act-child">
  428. <text class="action-icon-text">👨‍👩‍👧</text>
  429. </view>
  430. <text class="action-label">成员管理</text>
  431. </view>
  432. <view class="action-item" @click="goToRewards">
  433. <view class="action-icon act-reward">
  434. <text class="action-icon-text">🎁</text>
  435. </view>
  436. <text class="action-label">奖励中心</text>
  437. </view>
  438. <!-- Added: QR Code button -->
  439. <view class="action-item" @click="onShowQrCodeModal">
  440. <view class="action-icon act-invite">
  441. <text class="action-icon-text">📱</text>
  442. </view>
  443. <text class="action-label">邀请码</text>
  444. </view>
  445. </view>
  446. </PlayfulCard>
  447. </view>
  448. </template>
  449. <!-- ===== L1 即时反馈:Toast + 庆祝 ===== -->
  450. <FeedbackToast
  451. :visible="toastVisible"
  452. :text="toastText"
  453. :icon="toastIcon"
  454. :animating="toastAnimating" />
  455. <MilestoneCelebration
  456. :visible="celebrationVisible"
  457. :title="celebrationTitle"
  458. :subtitle="celebrationSubtitle"
  459. :particles="celebrationParticles"
  460. @dismiss="celebrationVisible = false" />
  461. <!-- ===== L2 深度反馈:每日健康小语 ===== -->
  462. <DailySummary
  463. v-if="selectedMemberId && selectedChild && selectedChild.id"
  464. :memberId="selectedChild.id"
  465. @dismiss="onDailySummaryDismiss" />
  466. </view>
  467. </template>
  468. <script>
  469. import { getFamilyMemberList, getChildren, getPendingReviewTasks, getPendingWishes, approveTask, rejectTask, getChildCompletionStats, getParentDashboard, getFamilyEnergySandbox, getEnergySandbox, getUnlockStatus, getEnergyOverview, getActivityList, getProductsByDomain, getContactList, getFeaturedArticles, getActiveAlerts, dismissAlert, getFamilyRanking, getChallengeList, getCircleList, getCircleRanking, getInviteQrCode, switchToFamilyMember, getSelfCheckStatus } from '../../utils/api.js'
  470. import store from '../../store/index.js'
  471. import PlayfulCard from '../../components/PlayfulCard.vue'
  472. import PlayfulButton from '../../components/PlayfulButton.vue'
  473. import BaseBadge from '../../components/BaseBadge.vue'
  474. import BaseEmpty from '../../components/BaseEmpty.vue'
  475. import BaseLoading from '../../components/BaseLoading.vue'
  476. import FamilyRelationGraph from '../../components/FamilyRelationGraph.vue'
  477. import WuxingSandbox from '../../components/wuxing-sandbox.vue'
  478. import ContactCard from '@/pages/home-pages/components/ContactCard.vue'
  479. import FamilyHealthRanking from '@/pages/home-pages/components/FamilyHealthRanking.vue'
  480. import FamilyChallengeCard from '../../components/FamilyChallengeCard.vue'
  481. import ContactImport from '@/pages/home-pages/components/ContactImport.vue'
  482. import RecommendedFeed from '../../components/RecommendedFeed.vue'
  483. import FamilyMemberStrip from '../../components/FamilyMemberStrip.vue'
  484. import MicroActionCard from '@/pages/home-pages/components/MicroActionCard.vue'
  485. import FeedbackToast from '@/pages/home-pages/components/FeedbackToast.vue'
  486. import MilestoneCelebration from '@/pages/home-pages/components/MilestoneCelebration.vue'
  487. import DailySummary from '@/pages/home-pages/components/DailySummary.vue'
  488. import HealthAlertBanner from '@/pages/home-pages/components/HealthAlertBanner.vue'
  489. import CircleRanking from '@/pages/home-pages/components/CircleRanking.vue'
  490. import AdBanner from '@/pages/home-pages/components/AdBanner.vue'
  491. import ArticleBookshelf from '../../components/ArticleBookshelf.vue'
  492. import OnboardingProgressBar from '../../components/OnboardingProgressBar.vue'
  493. import GateLock from '../../components/gate-lock.vue'
  494. import GateGuideBar from '@/pages/home-pages/components/gate-guide-bar.vue'
  495. import feedbackEngine from '../../utils/feedback-engine.js'
  496. export default {
  497. components: { RecommendedFeed, PlayfulCard, PlayfulButton, BaseBadge, BaseEmpty, BaseLoading, FamilyRelationGraph, WuxingSandbox, ContactCard, ContactImport, FamilyMemberStrip, MicroActionCard, FeedbackToast, MilestoneCelebration, DailySummary, HealthAlertBanner, FamilyHealthRanking, FamilyChallengeCard, CircleRanking, ArticleBookshelf, OnboardingProgressBar, GateLock, GateGuideBar, AdBanner },
  498. data() {
  499. return {
  500. nickname: '',
  501. familyMembers: [],
  502. children: [],
  503. selectedChild: null,
  504. pendingReviewTasks: [],
  505. pendingReviewWishes: [],
  506. pendingWishesCount: 0,
  507. pendingTasks: [],
  508. hotActivityCount: 0,
  509. hasNewReport: false,
  510. myCompletionRate: 0,
  511. familyAvgCompletionRate: 0,
  512. myPoints: 0,
  513. familyTotalPoints: 0,
  514. // membershipText removed (no real function)
  515. loading: true,
  516. currentChildId: '',
  517. _initialLoadDone: false,
  518. _familyId: null,
  519. familyMembersVisible: [],
  520. selfMemberId: null,
  521. energyData: null,
  522. energyDimensions: [],
  523. totalEnergy: 0,
  524. totalHealthIndex: 0,
  525. recommendedActivities: [],
  526. recommendedProducts: [],
  527. sandboxData: null,
  528. sandboxPersonalData: null,
  529. sandboxFamilyData: null,
  530. errorEnergy: false,
  531. // 通关解锁状态
  532. sandboxLocked: false,
  533. sandboxLockGate: null,
  534. currentSandboxView: 'personal',
  535. familyUnlocked: false,
  536. familyLockGate: null,
  537. unlockStatus: null,
  538. errorTasks: false,
  539. errorActivities: false,
  540. errorProducts: false,
  541. loadingActivities: false,
  542. loadingProducts: false,
  543. contactList: [],
  544. showImportModal: false,
  545. // QR Code modal
  546. showQrCodeModal: false,
  547. qrCodeData: null,
  548. // 推荐阅读
  549. indexArticles: [],
  550. articlesLoading: false,
  551. isLoggedIn: false,
  552. gradientColors: [
  553. 'linear-gradient(135deg, #10B981, #34D399)',
  554. 'linear-gradient(135deg, #5B9BD5, #8FC5E8)',
  555. 'linear-gradient(135deg, #8B5CF6, #C084FC)',
  556. 'linear-gradient(135deg, #F97316, #FB923C)',
  557. 'linear-gradient(135deg, #6366F1, #818CF8)'
  558. ],
  559. selectedMemberId: null,
  560. // L1 即时反馈状态
  561. toastVisible: false,
  562. toastText: '',
  563. toastIcon: '✅',
  564. toastAnimating: false,
  565. celebrationVisible: false,
  566. celebrationTitle: '',
  567. celebrationSubtitle: '',
  568. celebrationParticles: [],
  569. // P1-2 健康预警
  570. healthAlerts: [],
  571. // P2-2 家庭互动
  572. rankingMembers: [],
  573. rankingTotalScore: 0,
  574. activeChallenges: [],
  575. // P3 健康圈(社区扩散)
  576. circleRankings: [],
  577. circleName: '',
  578. circleLoading: false,
  579. selfCheckScore: null
  580. }
  581. },
  582. computed: {
  583. isFamilyAdmin() {
  584. if (!this.familyMembersVisible || this.familyMembersVisible.length === 0) return false
  585. // isAdmin 语义为"成员本人是否为家庭创建者";自己(isSelf)是创建者即管理员
  586. var me = this.familyMembersVisible.filter(function(m) { return m.isSelf })[0]
  587. return !!(me && me.isAdmin === true)
  588. },
  589. topPendingTasks() {
  590. return this.pendingTasks.slice(0, 3)
  591. },
  592. energyScores() {
  593. var d = this.energyData
  594. if (!d) return { body: 0, mind: 0, wisdom: 0, action: 0, wealth: 0 }
  595. return { body: d.bodyScore, mind: d.mindScore, wisdom: d.wisdomScore, action: d.actionScore, wealth: d.wealthScore }
  596. },
  597. energyOverall() {
  598. return this.energyData ? this.energyData.overallScore : 0
  599. },
  600. energyMembers() {
  601. return this.energyData ? this.energyData.members : []
  602. },
  603. sandboxScores() {
  604. if (!this.sandboxData) return { body: 0, mind: 0, wisdom: 0, action: 0, wealth: 0 }
  605. return {
  606. body: this.sandboxData.bodyScore || 0,
  607. mind: this.sandboxData.mindScore || 0,
  608. wisdom: this.sandboxData.wisdomScore || 0,
  609. action: this.sandboxData.actionScore || 0,
  610. wealth: this.sandboxData.wealthScore || 0
  611. }
  612. },
  613. familySandboxScores() {
  614. var d = this.sandboxFamilyData || {}
  615. return {
  616. body: d.bodyScore || 0,
  617. mind: d.mindScore || 0,
  618. wisdom: d.wisdomScore || 0,
  619. action: d.actionScore || 0,
  620. wealth: d.wealthScore || 0
  621. }
  622. },
  623. familySandboxOverall() {
  624. return this.sandboxFamilyData ? (this.sandboxFamilyData.overallScore || 0) : 0
  625. },
  626. familySandboxMembers() {
  627. return this.sandboxFamilyData ? (this.sandboxFamilyData.members || []) : []
  628. },
  629. greetingText() {
  630. const hour = new Date().getHours()
  631. if (hour < 6) return '夜深了'
  632. if (hour < 9) return '早上好'
  633. if (hour < 12) return '上午好'
  634. if (hour < 14) return '中午好'
  635. if (hour < 18) return '下午好'
  636. if (hour < 22) return '晚上好'
  637. return '夜深了'
  638. },
  639. hasMilestones() {
  640. const c = this.selectedChild
  641. if (!c) return false
  642. return (c.streakDays >= 7) || (c.streakDays >= 30) || (c.totalPoints >= 1000)
  643. },
  644. energyMapForGraph() {
  645. var map = {}
  646. if (this.sandboxData && this.sandboxData.members) {
  647. for (var i = 0; i < this.sandboxData.members.length; i++) {
  648. var m = this.sandboxData.members[i]
  649. map[m.memberId || m.id] = {
  650. bodyScore: m.bodyScore || 0,
  651. mindScore: m.mindScore || 0,
  652. actionScore: m.actionScore || 0
  653. }
  654. }
  655. }
  656. return map
  657. },
  658. intimacyMapForGraph() {
  659. return {}
  660. }
  661. },
  662. // 使用 mounted 而非 onLoad,因为本组件作为条件渲染子组件(v-if),
  663. // page 生命周期钩子(onLoad/onShow)可能在组件挂载前已触发完毕
  664. mounted() {
  665. if (uni.getStorageSync('token')) {
  666. this.isLoggedIn = true
  667. this.loadData()
  668. }
  669. },
  670. onShow() {
  671. // 首次 onShow 紧接 mounted 之后,跳过避免双重加载
  672. if (!this._initialLoadDone) {
  673. this._initialLoadDone = true
  674. return
  675. }
  676. const currentRole = uni.getStorageSync('currentRole') || 'parent'
  677. if (currentRole === 'child') {
  678. uni.reLaunch({ url: '/pages/home-pages/child-index' })
  679. return
  680. }
  681. if (uni.getStorageSync('token')) {
  682. this.isLoggedIn = true
  683. this.loadData()
  684. }
  685. if (this.$refs.onboardingProgress) {
  686. this.$refs.onboardingProgress.reload()
  687. }
  688. },
  689. onPullDownRefresh() {
  690. this.loadData().finally(() => {
  691. uni.stopPullDownRefresh()
  692. })
  693. },
  694. methods: {
  695. rankIcon: function(idx) {
  696. var medals = ['🥇', '🥈', '🥉']
  697. return medals[idx] || '#' + (idx + 1)
  698. },
  699. async loadData() {
  700. this.loading = true
  701. try {
  702. var userInfo = uni.getStorageSync('userInfo')
  703. this.nickname = (userInfo && userInfo.nickname) ? userInfo.nickname : ''
  704. this.selfMemberId = uni.getStorageSync('userId') || null
  705. // 优先使用仪表盘接口(包含孩子列表 + 统计数据)
  706. let dashboardLoaded = false
  707. var _membersVisibleSet = false
  708. try {
  709. const dashRes = await getParentDashboard()
  710. const dashData = dashRes.data || {}
  711. const dashChildren = dashData.children || []
  712. // 过滤:仅保留孩子角色成员,排除家长/本人
  713. var currentUserId = uni.getStorageSync('userId')
  714. var filteredChildren = dashChildren.filter(function(c) {
  715. return c && c.id && c.id !== currentUserId && (c.role === 'child' || c.memberType === 'child')
  716. })
  717. if (filteredChildren.length > 0) {
  718. this.children = filteredChildren
  719. // 不自动选孩子:默认显示家长自己,等待用户从 FamilyMemberStrip 选择
  720. this.familyAvgCompletionRate = dashData.avgCompletionRate || 0
  721. // 家长自身数据(积分 + 任务完成率)
  722. this.myPoints = dashData.parentPoints || 0
  723. this.myCompletionRate = dashData.parentCompletionRate || 0
  724. // 从仪表盘获取 familyId
  725. if (dashData.familyId) {
  726. this._familyId = dashData.familyId
  727. }
  728. // 从仪表盘同步设置 familyMembersVisible(含家长本人),避免后续 loadFamilyMembersVisible() 再调 getFamilyMemberList
  729. var dashMembers = dashData.members || dashData.familyMembers || filteredChildren
  730. this.familyMembersVisible = dashMembers
  731. _membersVisibleSet = true
  732. dashboardLoaded = true
  733. }
  734. } catch (e) {
  735. console.log('获取仪表盘数据失败,降级到单独接口', e)
  736. }
  737. // 降级:使用原有单独接口(仅调用一次 getFamilyMemberList,合并 children 与 familyMembers 派生)
  738. if (!dashboardLoaded) {
  739. try {
  740. const currentUserId = uni.getStorageSync('userId')
  741. const membersRes = await getFamilyMemberList({ visibleOnly: true, includeFamily: true })
  742. const data = membersRes.data || {}
  743. var members = data.members || data || []
  744. if (!Array.isArray(members)) members = []
  745. const parents = members.filter(function(p) { return p.source === 'parent' && p.id && p.id !== currentUserId }).map(function(p) { return { ...p, role: 'parent' } })
  746. const children = members.filter(function(c) { return c.source !== 'parent' && c.id }).map(function(c) { return { ...c, role: 'child' } })
  747. this.familyMembers = [].concat(parents, children)
  748. // 从同一响应派生 children,避免再调 getChildren()(= getFamilyMemberList({}))
  749. this.children = children
  750. // 同步设置 familyMembersVisible,避免 loadFamilyMembersVisible() 重复调用 getFamilyMemberList
  751. this.familyMembersVisible = members
  752. _membersVisibleSet = true
  753. // 不自动选孩子:默认显示家长自己,等待用户从 FamilyMemberStrip 选择
  754. // 保存 familyId 用于后续请求
  755. if (data.familyId) {
  756. this._familyId = data.familyId
  757. }
  758. } catch (e) {
  759. console.log('获取家庭成员失败', e)
  760. }
  761. }
  762. const reviewRes = await getPendingReviewTasks()
  763. const allReviewTasks = reviewRes.data || []
  764. this.pendingReviewTasks = allReviewTasks.slice(0, 3)
  765. try {
  766. const wishRes = await getPendingWishes()
  767. this.pendingReviewWishes = wishRes.data || []
  768. } catch (e) {
  769. console.log('获取待处理心愿失败', e)
  770. this.pendingReviewWishes = []
  771. }
  772. const reviewMap = {}
  773. const wishMap = {}
  774. for (let i = 0; i < allReviewTasks.length; i++) {
  775. const task = allReviewTasks[i]
  776. const cid = task.memberId || task.assigneeId
  777. if (cid) {
  778. reviewMap[cid] = (reviewMap[cid] || 0) + 1
  779. }
  780. }
  781. for (let j = 0; j < (this.pendingReviewWishes || []).length; j++) {
  782. const wish = this.pendingReviewWishes[j]
  783. const cid = wish.memberId
  784. if (cid) {
  785. wishMap[cid] = (wishMap[cid] || 0) + 1
  786. }
  787. }
  788. this.children = this.children.map(function(c) {
  789. return {
  790. ...c,
  791. pendingReviewCount: reviewMap[c.id] || 0,
  792. pendingWishCount: wishMap[c.id] || 0
  793. }
  794. })
  795. // 计算全家总积分(家长 + 所有孩子)
  796. var totalPts = this.myPoints
  797. for (var pi = 0; pi < this.children.length; pi++) {
  798. totalPts += (this.children[pi].totalPoints || 0)
  799. }
  800. this.familyTotalPoints = totalPts
  801. // 如果仪表盘未加载到数据,降级:从孩子数据取近似值
  802. if (!dashboardLoaded) {
  803. // 降级:家长积分取自己的(从缓存或API),完成率取第一个孩子的
  804. const targetChild = this.selectedChild || (this.children.length > 0 ? this.children[0] : null)
  805. if (targetChild) {
  806. try {
  807. const statsRes = await getChildCompletionStats({ memberId: targetChild.id })
  808. if (statsRes && statsRes.data) {
  809. this.myCompletionRate = statsRes.data.completionRate || 0
  810. }
  811. } catch (e) {
  812. console.log('获取个人完成率失败', e)
  813. }
  814. userInfo = uni.getStorageSync('userInfo')
  815. this.myPoints = (userInfo && userInfo.totalPoints) || 0
  816. this.familyAvgCompletionRate = this.myCompletionRate
  817. }
  818. }
  819. // 待完成任务 - 优先使用后端返回的真实任务列表
  820. this.pendingTasks = []
  821. if (this.children.length > 0) {
  822. const targetChild = this.selectedChild || this.children[0]
  823. try {
  824. const r2 = await getChildCompletionStats({ memberId: targetChild.id })
  825. if (r2 && r2.data && r2.data.pendingTaskList && r2.data.pendingTaskList.length > 0) {
  826. this.pendingTasks = r2.data.pendingTaskList.slice(0, 3)
  827. } else if (r2 && r2.data && r2.data.pendingTasks > 0) {
  828. // 降级:只有数量没有列表时使用占位
  829. for (let k = 0; k < Math.min(r2.data.pendingTasks, 3); k++) {
  830. this.pendingTasks.push({ id: 'pending-' + k, title: '待完成任务' })
  831. }
  832. }
  833. } catch (e) {
  834. this.pendingTasks = []
  835. }
  836. }
  837. // 加载通关解锁状态
  838. try {
  839. var statusRes = await getUnlockStatus()
  840. if (statusRes && statusRes.data) {
  841. this.unlockStatus = statusRes.data
  842. }
  843. } catch (e) {
  844. console.log('获取通关解锁状态失败', e)
  845. }
  846. // 加载能量沙盘(含通关门禁)
  847. try {
  848. var memberId = uni.getStorageSync('currentMemberId') || uni.getStorageSync('currentChildId') || null
  849. var energyRes = await getEnergySandbox(this.currentSandboxView, memberId)
  850. if (energyRes && energyRes.data) {
  851. var ed = energyRes.data
  852. if (ed.locked) {
  853. this.sandboxLocked = true
  854. this.sandboxLockGate = ed.gate || null
  855. this.sandboxData = null
  856. this.sandboxPersonalData = null
  857. this.sandboxFamilyData = null
  858. this.energyData = null
  859. } else {
  860. this.sandboxLocked = false
  861. this.sandboxLockGate = null
  862. this.sandboxPersonalData = ed.personalData || null
  863. this.sandboxFamilyData = ed.familyData || null
  864. this.sandboxData = this.currentSandboxView === 'family' ? (ed.familyData || null) : (ed.personalData || null)
  865. this.energyData = ed.personalData || null
  866. this.familyUnlocked = ed.familyUnlocked || false
  867. this.familyLockGate = ed.familyGate || null
  868. }
  869. }
  870. } catch (e) {
  871. console.log('获取能量沙盘数据失败', e)
  872. this.energyData = null
  873. this.sandboxData = null
  874. this.errorEnergy = true
  875. }
  876. // 加载推荐活动
  877. this.loadRecommendedActivities()
  878. // 加载推荐商品
  879. this.loadRecommendedProducts()
  880. // 加载家庭成员可见列表(降级路径已在上面设置,此处跳过避免重复请求)
  881. if (!_membersVisibleSet) {
  882. this.loadFamilyMembersVisible()
  883. }
  884. this.loadSelfCheckStatus()
  885. // 加载健康预警(P1-2)
  886. this.loadAlerts()
  887. // 加载家庭健康互动(P2-2)
  888. this.loadFamilyInteraction()
  889. // 加载健康圈(P3:社区扩散)
  890. this.loadHealthCircle()
  891. // 加载外部联系人
  892. this.loadContacts()
  893. // 加载推荐阅读
  894. this.loadFeaturedArticles()
  895. // 加载五维能量概览(账本模式)
  896. var overviewChildId = this.currentChildId
  897. if (overviewChildId) {
  898. try {
  899. var overviewRes = await getEnergyOverview(overviewChildId)
  900. if (overviewRes && overviewRes.data) {
  901. this.energyDimensions = overviewRes.data.dimensions || []
  902. this.totalEnergy = overviewRes.data.totalEnergy || 0
  903. this.totalHealthIndex = overviewRes.data.totalHealthIndex || 0
  904. }
  905. } catch (e) {
  906. console.log('获取能量概览失败', e)
  907. }
  908. }
  909. } catch (e) {
  910. console.error('加载数据失败', e)
  911. } finally {
  912. this.loading = false
  913. this.$emit('ready')
  914. }
  915. },
  916. async approveTask(taskId) {
  917. try {
  918. await approveTask(taskId)
  919. uni.showToast({ title: '已通过', icon: 'success' })
  920. this.loadData()
  921. } catch (e) {
  922. uni.showToast({ title: '操作失败', icon: 'none' })
  923. }
  924. },
  925. async rejectTask(taskId) {
  926. try {
  927. await rejectTask(taskId)
  928. uni.showToast({ title: '已拒绝', icon: 'success' })
  929. this.loadData()
  930. } catch (e) {
  931. uni.showToast({ title: '操作失败', icon: 'none' })
  932. }
  933. },
  934. // 加载可见家庭成员
  935. async loadFamilyMembersVisible() {
  936. try {
  937. var res = await getFamilyMemberList({ visibleOnly: true })
  938. this.familyMembersVisible = res.data || []
  939. } catch (e) {
  940. console.log('获取可见家庭成员失败', e)
  941. this.familyMembersVisible = []
  942. }
  943. },
  944. // 成员选择
  945. goToCompletionStats() {
  946. if (this.children.length === 1) {
  947. uni.navigateTo({ url: '/pages/stats/completion-rate?memberId=' + this.children[0].id })
  948. } else {
  949. uni.navigateTo({ url: '/pages/stats/completion-rate?familyView=1' })
  950. }
  951. },
  952. onDomainClick(evt) {
  953. this.goToDomain(evt.code)
  954. },
  955. /** 切换沙盘个人/全家视角(本地切换,不重新请求) */
  956. onSandboxViewChange: function(view) {
  957. if (view === this.currentSandboxView) return
  958. this.currentSandboxView = view
  959. this.sandboxData = view === 'family' ? (this.sandboxFamilyData || null) : (this.sandboxPersonalData || null)
  960. },
  961. /** 单独加载沙盘数据(首次进入时使用,一次返回个人+全家) */
  962. loadSandboxData: async function() {
  963. try {
  964. var memberId = uni.getStorageSync('currentMemberId') || uni.getStorageSync('currentChildId') || null
  965. var energyRes = await getEnergySandbox(this.currentSandboxView, memberId)
  966. if (energyRes && energyRes.data) {
  967. var ed = energyRes.data
  968. if (ed.locked) {
  969. this.sandboxLocked = true
  970. this.sandboxLockGate = ed.gate || null
  971. this.sandboxData = null
  972. this.sandboxPersonalData = null
  973. this.sandboxFamilyData = null
  974. } else {
  975. this.sandboxLocked = false
  976. this.sandboxLockGate = null
  977. this.sandboxPersonalData = ed.personalData || null
  978. this.sandboxFamilyData = ed.familyData || null
  979. this.sandboxData = this.currentSandboxView === 'family' ? (ed.familyData || null) : (ed.personalData || null)
  980. this.familyUnlocked = ed.familyUnlocked || false
  981. this.familyLockGate = ed.familyGate || null
  982. }
  983. }
  984. } catch (e) {
  985. console.log('加载沙盘数据失败', e)
  986. }
  987. },
  988. goToDomain(code) {
  989. // 跳转到能量详情页
  990. var memberId = this.currentChildId
  991. if (!memberId || !code) return
  992. uni.navigateTo({
  993. url: '/pages/energy/detail?memberId=' + memberId + '&code=' + code
  994. })
  995. },
  996. onFamilyMemberSelect(member) {
  997. this.selectedMemberId = member.id
  998. // 选中的是孩子时,设置 selectedChild 以便子组件加载孩子数据
  999. var matched = this.children.find(function(c) { return c.id === member.id })
  1000. if (matched) {
  1001. this.selectedChild = matched
  1002. this.currentChildId = matched.id
  1003. } else {
  1004. this.selectedChild = null
  1005. this.currentChildId = ''
  1006. }
  1007. },
  1008. async onFamilyMemberSwitch(member) {
  1009. if (!member || !member.id) return
  1010. var self = this
  1011. uni.showModal({
  1012. title: '切换确认',
  1013. content: '切换到"' + (member.nickname || '该成员') + '"后,退出时需要输入您的密码。是否继续?',
  1014. success: async function(res) {
  1015. if (!res.confirm) return
  1016. try {
  1017. var result = await switchToFamilyMember(member.id, member.source || 'family_member')
  1018. if (result.code === 200 && result.data) {
  1019. store.commit('switchToMember', {
  1020. memberId: result.data.memberId,
  1021. effectiveRole: result.data.effectiveRole
  1022. })
  1023. uni.showToast({ title: '已切换到' + (member.nickname || '该成员'), icon: 'success' })
  1024. setTimeout(function() {
  1025. uni.reLaunch({ url: '/pages/index-home/index' })
  1026. }, 1000)
  1027. } else {
  1028. uni.showToast({ title: result.message || '切换失败', icon: 'none' })
  1029. }
  1030. } catch (e) {
  1031. uni.showToast({ title: '切换失败', icon: 'none' })
  1032. }
  1033. }
  1034. })
  1035. },
  1036. onMicroActionCompleted() {
  1037. this.loadData()
  1038. feedbackEngine.trigger('micro_action')
  1039. },
  1040. goAddMember() {
  1041. uni.navigateTo({ url: '/pages/family/add-member' })
  1042. },
  1043. onDailySummaryDismiss() {
  1044. // DailySummary 已关闭,无额外操作
  1045. },
  1046. loadAlerts: function() {
  1047. var memberId = this.selectedChild && this.selectedChild.id
  1048. if (!memberId) return
  1049. var self = this
  1050. getActiveAlerts({ memberId: memberId }).then(function(res) {
  1051. if (res.code === 200) {
  1052. self.healthAlerts = res.data || []
  1053. }
  1054. }).catch(function() {})
  1055. },
  1056. handleDismissAlert: function(alertId) {
  1057. var self = this
  1058. dismissAlert({ id: alertId }).then(function(res) {
  1059. if (res.code === 200) {
  1060. self.healthAlerts = self.healthAlerts.filter(function(a) { return a.id !== alertId })
  1061. }
  1062. }).catch(function() {})
  1063. },
  1064. loadFamilyInteraction: function() {
  1065. var familyId = this._familyId
  1066. if (!familyId) return
  1067. var self = this
  1068. getFamilyRanking({}).then(function(res) {
  1069. if (res.code === 200 && res.data) {
  1070. self.rankingMembers = res.data.members || []
  1071. self.rankingTotalScore = res.data.totalScore || 0
  1072. }
  1073. }).catch(function() {})
  1074. getChallengeList({}).then(function(res) {
  1075. if (res.code === 200 && Array.isArray(res.data)) {
  1076. self.activeChallenges = res.data
  1077. }
  1078. }).catch(function() {})
  1079. },
  1080. loadHealthCircle: function() {
  1081. var familyId = this._familyId
  1082. if (!familyId) return
  1083. var self = this
  1084. self.circleLoading = true
  1085. getCircleList({}).then(function(res) {
  1086. if (res.code === 200 && res.data && res.data.length > 0) {
  1087. var circleId = res.data[0].id
  1088. getCircleRanking({ circleId: circleId }).then(function(r) {
  1089. if (r.code === 200 && r.data) {
  1090. self.circleName = r.data.circleName || ''
  1091. self.circleRankings = r.data.rankings || []
  1092. }
  1093. }).catch(function() {
  1094. self.circleRankings = []
  1095. self.circleName = ''
  1096. }).finally(function() {
  1097. self.circleLoading = false
  1098. })
  1099. } else {
  1100. self.circleRankings = []
  1101. self.circleName = ''
  1102. self.circleLoading = false
  1103. }
  1104. }).catch(function() {
  1105. self.circleRankings = []
  1106. self.circleName = ''
  1107. self.circleLoading = false
  1108. })
  1109. },
  1110. goToHealthCircle: function() {
  1111. uni.navigateTo({ url: '/pages/share/index' })
  1112. },
  1113. goToSelfCheck: function() {
  1114. uni.navigateTo({ url: '/pages/family/self-check-entry' })
  1115. },
  1116. loadSelfCheckStatus: function() {
  1117. var self = this
  1118. getSelfCheckStatus().then(function(res) {
  1119. if (res.code === 200 && res.data && res.data.lastResult) {
  1120. self.selfCheckScore = res.data.lastResult.totalScore
  1121. }
  1122. }).catch(function() {})
  1123. },
  1124. goMemberDetail(member) {
  1125. if (!member || !member.memberId) return
  1126. uni.navigateTo({
  1127. url: '/pages/member-detail/member-detail?memberId=' + member.memberId + '&memberType=' + (member.memberType || 'child') + '&entrySource=relation-graph'
  1128. })
  1129. },
  1130. addTask() { uni.navigateTo({ url: '/pages/tasks/create-task' }) },
  1131. goToChallengeManage: function(id) {
  1132. uni.navigateTo({ url: '/pages/health/challenge-manage?id=' + id })
  1133. },
  1134. goToReview() { uni.navigateTo({ url: '/pages/tasks/review' }) },
  1135. goToWishApprove() { uni.navigateTo({ url: '/pages/wishes/approve' }) },
  1136. goToAssessmentReport() { uni.navigateTo({ url: '/pages/assessment/report' }) },
  1137. goToActivities() { uni.navigateTo({ url: '/pages/activity/index' }) },
  1138. goToRewards() { uni.navigateTo({ url: '/pages/rewards/rewards' }) },
  1139. onFeedItemClick(item) {
  1140. if (item.type === 'product') {
  1141. uni.navigateTo({ url: '/pages/discover-detail/product-detail/product-detail?id=' + item.id })
  1142. } else if (item.type === 'activity') {
  1143. uni.navigateTo({ url: '/pages/activity/activity-detail/activity-detail?id=' + item.id })
  1144. }
  1145. },
  1146. goMoreDiscover() {
  1147. uni.navigateTo({ url: '/pages/discover/index' })
  1148. },
  1149. loadRecommendedActivities() {
  1150. var self = this
  1151. getActivityList({ page: 1, size: 6 }).then(function(res) {
  1152. if (res && res.data) {
  1153. self.recommendedActivities = res.data.records || []
  1154. }
  1155. }).catch(function() {
  1156. self.errorActivities = true
  1157. })
  1158. },
  1159. loadRecommendedProducts() {
  1160. var self = this
  1161. getProductsByDomain('all', 1, 6).then(function(res) {
  1162. if (res && res.data) {
  1163. self.recommendedProducts = res.data.records || []
  1164. }
  1165. }).catch(function() {
  1166. self.errorProducts = true
  1167. })
  1168. },
  1169. goToTaskList() { uni.navigateTo({ url: '/pages/tasks/tasks' }) },
  1170. manageChildren() { uni.navigateTo({ url: '/pages/profile-extra/family-members' }) },
  1171. loadContacts: function() {
  1172. var self = this
  1173. getContactList({}).then(function(res) {
  1174. if (res.code === 200 && res.data) {
  1175. self.contactList = Array.isArray(res.data) ? res.data : []
  1176. }
  1177. }).catch(function(e) {
  1178. console.log('获取联系人失败', e)
  1179. })
  1180. },
  1181. onContactClick: function(item) {
  1182. uni.navigateTo({ url: '/pages/action-detail/contact-detail?id=' + item.id })
  1183. },
  1184. onShowImport: function() {
  1185. this.showImportModal = true
  1186. },
  1187. onCloseImport: function() {
  1188. this.showImportModal = false
  1189. },
  1190. onImportSuccess: function() {
  1191. this.showImportModal = false
  1192. this.loadContacts()
  1193. },
  1194. // QR Code modal methods
  1195. onShowQrCodeModal: async function() {
  1196. this.showQrCodeModal = true
  1197. this.qrCodeData = null
  1198. try {
  1199. const res = await this.getInviteQrCode()
  1200. if (res.code === 200) {
  1201. this.qrCodeData = res.data
  1202. }
  1203. } catch (e) {
  1204. console.error('获取二维码失败', e)
  1205. uni.showToast({ title: '获取二维码失败', icon: 'none' })
  1206. }
  1207. },
  1208. onCloseQrCodeModal: function() {
  1209. this.showQrCodeModal = false
  1210. this.qrCodeData = null
  1211. },
  1212. onCopyReferralCode: function() {
  1213. if (this.qrCodeData && this.qrCodeData.referralCode) {
  1214. uni.setClipboardData({
  1215. data: this.qrCodeData.referralCode,
  1216. success: () => {
  1217. uni.showToast({ title: '复制成功', icon: 'success' })
  1218. }
  1219. })
  1220. }
  1221. },
  1222. // 兼容旧引用
  1223. getInviteQrCode: function() {
  1224. return getInviteQrCode()
  1225. },
  1226. // ===== 推荐阅读 =====
  1227. loadFeaturedArticles: function() {
  1228. var self = this
  1229. this.articlesLoading = true
  1230. getFeaturedArticles({ size: 5 }).then(function(res) {
  1231. self.articlesLoading = false
  1232. if (res && res.code === 200) {
  1233. var rawList = res.data && (Array.isArray(res.data) ? res.data : (res.data.data || res.data.records || []))
  1234. var list = Array.isArray(rawList) ? rawList : []
  1235. self.indexArticles = list.map(function(item, index) {
  1236. return {
  1237. id: item.id,
  1238. category: item.categoryName || item.category || '热门文章',
  1239. categoryColor: self.gradientColors[index % self.gradientColors.length],
  1240. title: item.title || '',
  1241. summary: item.summary || '',
  1242. views: item.viewCount ? String(item.viewCount) : '0',
  1243. date: item.publishedAt ? item.publishedAt.slice(0, 10) : '',
  1244. coverImage: item.coverImage
  1245. }
  1246. })
  1247. }
  1248. }).catch(function() {
  1249. self.articlesLoading = false
  1250. })
  1251. },
  1252. goArticles: function() {
  1253. uni.navigateTo({ url: '/pages/article-center/index' })
  1254. },
  1255. goArticleDetail: function(articleOrId) {
  1256. var id = typeof articleOrId === 'object' ? articleOrId.id : articleOrId
  1257. uni.navigateTo({ url: '/pages/article-center/article-detail?id=' + id })
  1258. }
  1259. }
  1260. }
  1261. </script>
  1262. <style scoped>
  1263. /* ========================================
  1264. Flat Card Design System — 家长端暖橙扁平
  1265. ======================================== */
  1266. .container {
  1267. padding: 32rpx;
  1268. background: var(--bg, #F5F9FC);
  1269. min-height: 100vh;
  1270. }
  1271. /* ========================================
  1272. 区块标题
  1273. ======================================== */
  1274. .section-title {
  1275. font-size: 32rpx;
  1276. font-weight: 700;
  1277. color: var(--text, #1E293B);
  1278. margin-bottom: var(--space-md);
  1279. }
  1280. /* ========================================
  1281. c) 成长数据
  1282. ======================================== */
  1283. .stats-section {
  1284. margin-bottom: 48rpx;
  1285. }
  1286. .stats-row {
  1287. display: flex;
  1288. gap: 16rpx;
  1289. }
  1290. .stat-card-wide {
  1291. flex: 1;
  1292. min-width: 0;
  1293. box-sizing: border-box;
  1294. }
  1295. .stat-card-content {
  1296. padding: 24rpx;
  1297. }
  1298. .stat-header {
  1299. display: flex;
  1300. align-items: center;
  1301. margin-bottom: 16rpx;
  1302. }
  1303. .stat-icon-wrap {
  1304. width: 56rpx;
  1305. height: 56rpx;
  1306. border-radius: var(--radius-sm, 12rpx);
  1307. display: flex;
  1308. align-items: center;
  1309. justify-content: center;
  1310. margin-right: 14rpx;
  1311. flex-shrink: 0;
  1312. }
  1313. .stat-icon-symbol {
  1314. font-size: 28rpx;
  1315. }
  1316. .stat-icon-orange { background: rgba(91, 155, 213, 0.12); }
  1317. .stat-icon-gold { background: rgba(245, 158, 11, 0.15); }
  1318. .stat-body {
  1319. flex: 1;
  1320. min-width: 0;
  1321. }
  1322. .stat-label-header {
  1323. font-size: 26rpx;
  1324. font-weight: 600;
  1325. color: var(--text, #1E293B);
  1326. }
  1327. /* 对比行布局 */
  1328. .stat-compare {
  1329. margin-bottom: 12rpx;
  1330. }
  1331. .stat-compare-row {
  1332. display: flex;
  1333. justify-content: space-between;
  1334. align-items: center;
  1335. margin-bottom: 4rpx;
  1336. }
  1337. .compare-label {
  1338. font-size: 22rpx;
  1339. color: var(--text-secondary, #64748B);
  1340. }
  1341. .compare-value {
  1342. font-size: 30rpx;
  1343. font-weight: 700;
  1344. }
  1345. /* 进度条 - 紧凑版 */
  1346. .progress-bar-sm {
  1347. height: 6rpx;
  1348. background: var(--border-light, #E2E8F0);
  1349. border-radius: 999rpx;
  1350. overflow: hidden;
  1351. margin-bottom: 8rpx;
  1352. }
  1353. .progress-fill-sm {
  1354. height: 100%;
  1355. background: linear-gradient(90deg, var(--color-primary, #5B9BD5), #F59E0B);
  1356. border-radius: 999rpx;
  1357. transition: width 0.3s ease;
  1358. }
  1359. .progress-fill-sm.muted {
  1360. background: linear-gradient(90deg, #94A3B8, #CBD5E1);
  1361. }
  1362. .accent-color { color: var(--color-primary, #5B9BD5); }
  1363. .gold-color { color: #F59E0B; }
  1364. .stat-more {
  1365. font-size: 22rpx;
  1366. color: var(--color-accent, #0EA5E9);
  1367. font-weight: 500;
  1368. display: block;
  1369. }
  1370. .stat-encourage {
  1371. font-size: 22rpx;
  1372. color: #10B981;
  1373. font-weight: 500;
  1374. margin-top: 8rpx;
  1375. display: block;
  1376. }
  1377. .stat-encourage.muted {
  1378. color: var(--muted, #94A3B8);
  1379. }
  1380. /* ========================================
  1381. d) 今日重点
  1382. ======================================== */
  1383. .today-section {
  1384. margin-bottom: 48rpx;
  1385. }
  1386. .today-card .today-header {
  1387. display: flex;
  1388. justify-content: space-between;
  1389. align-items: center;
  1390. }
  1391. .today-label {
  1392. font-size: 28rpx;
  1393. font-weight: 600;
  1394. color: var(--text, #1E293B);
  1395. }
  1396. .today-count {
  1397. font-size: 24rpx;
  1398. color: var(--color-primary, #5B9BD5);
  1399. font-weight: 600;
  1400. }
  1401. .task-list {
  1402. margin-bottom: 8rpx;
  1403. }
  1404. .task-item {
  1405. display: flex;
  1406. align-items: center;
  1407. padding: 18rpx 0;
  1408. border-bottom: 1rpx solid var(--border-light, #E2E8F0);
  1409. }
  1410. .task-item:last-child {
  1411. border-bottom: none;
  1412. }
  1413. .task-dot {
  1414. width: 14rpx;
  1415. height: 14rpx;
  1416. border-radius: 50%;
  1417. margin-right: 16rpx;
  1418. flex-shrink: 0;
  1419. }
  1420. .dot-color-0 { background: var(--color-primary, #5B9BD5); }
  1421. .dot-color-1 { background: #10B981; }
  1422. .dot-color-2 { background: var(--color-accent, #0EA5E9); }
  1423. .dot-color-3 { background: #F59E0B; }
  1424. .dot-color-4 { background: var(--color-error, #EF4444); }
  1425. .task-title {
  1426. font-size: 28rpx;
  1427. color: var(--text, #1E293B);
  1428. }
  1429. .task-view-all {
  1430. display: flex;
  1431. justify-content: flex-end;
  1432. align-items: center;
  1433. color: var(--text-secondary, #64748B);
  1434. font-size: 24rpx;
  1435. padding-top: 16rpx;
  1436. border-top: 1rpx solid var(--border-light, #E2E8F0);
  1437. }
  1438. .task-view-all .arrow {
  1439. margin-left: 8rpx;
  1440. }
  1441. /* ========================================
  1442. e) 成长里程碑
  1443. ======================================== */
  1444. .milestone-section {
  1445. margin-bottom: 48rpx;
  1446. }
  1447. .milestone-card {
  1448. padding: 24rpx !important;
  1449. }
  1450. .milestone-item {
  1451. display: flex;
  1452. align-items: center;
  1453. padding: 16rpx 0;
  1454. border-bottom: 1rpx solid var(--border-light, #E2E8F0);
  1455. }
  1456. .milestone-item:last-child {
  1457. border-bottom: none;
  1458. }
  1459. .milestone-icon-wrap {
  1460. width: 56rpx;
  1461. height: 56rpx;
  1462. border-radius: var(--radius-sm, 12rpx);
  1463. display: flex;
  1464. align-items: center;
  1465. justify-content: center;
  1466. margin-right: 16rpx;
  1467. flex-shrink: 0;
  1468. }
  1469. .milestone-icon {
  1470. font-size: 28rpx;
  1471. }
  1472. .milestone-star { background: rgba(245, 158, 11, 0.15); }
  1473. .milestone-trophy { background: rgba(91, 155, 213, 0.12); }
  1474. .milestone-sparkle { background: rgba(16, 185, 129, 0.12); }
  1475. .milestone-text {
  1476. font-size: 28rpx;
  1477. color: var(--text, #1E293B);
  1478. }
  1479. /* ========================================
  1480. f) 待处理事项
  1481. ======================================== */
  1482. .pending-section { margin-bottom: 48rpx; }
  1483. .pending-grid {
  1484. display: flex;
  1485. flex-wrap: wrap;
  1486. gap: 16rpx;
  1487. }
  1488. .pending-card {
  1489. background: #fff;
  1490. border-radius: 20rpx;
  1491. padding: 24rpx;
  1492. box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06);
  1493. display: flex;
  1494. align-items: center;
  1495. width: calc(50% - 8rpx);
  1496. box-sizing: border-box;
  1497. }
  1498. .pending-card:active { opacity: 0.8; }
  1499. .pending-icon {
  1500. width: 72rpx; height: 72rpx;
  1501. border-radius: 16rpx;
  1502. display: flex; align-items: center; justify-content: center;
  1503. margin-right: 16rpx; flex-shrink: 0;
  1504. }
  1505. .pending-icon--blue { background: rgba(91,155,213,0.12); }
  1506. .pending-icon--pink { background: rgba(236,72,153,0.12); }
  1507. .pending-icon--green { background: rgba(16,185,129,0.12); }
  1508. .pending-icon--orange { background: rgba(249,115,22,0.12); }
  1509. .pending-emoji { font-size: 32rpx; }
  1510. .pending-info { flex: 1; min-width: 0; }
  1511. .pending-count { font-size: 36rpx; font-weight: 800; display: block; }
  1512. .pending-label { font-size: 22rpx; color: #666; margin-top: 4rpx; display: block; }
  1513. .pink-color { color: #EC4899; }
  1514. .orange-color { color: #F97316; }
  1515. /* ========================================
  1516. g) 成长建议
  1517. ======================================== */
  1518. .advice-section { margin-bottom: 48rpx; }
  1519. .advice-card { margin-bottom: 12rpx; }
  1520. .advice-content {
  1521. display: flex; align-items: center; padding: 24rpx;
  1522. }
  1523. .advice-icon-wrap {
  1524. width: 56rpx; height: 56rpx;
  1525. border-radius: 14rpx;
  1526. background: rgba(91,155,213,0.1);
  1527. display: flex; align-items: center; justify-content: center;
  1528. margin-right: 16rpx; flex-shrink: 0;
  1529. }
  1530. .advice-icon { font-size: 28rpx; }
  1531. .advice-text-wrap { flex: 1; min-width: 0; }
  1532. .advice-title { font-size: 28rpx; font-weight: 600; color: #333; display: block; }
  1533. .advice-desc { font-size: 24rpx; color: #999; margin-top: 4rpx; display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  1534. .advice-arrow { font-size: 36rpx; color: #ccc; margin-left: 12rpx; }
  1535. /* ========================================
  1536. h) 待审核任务
  1537. ======================================== */
  1538. .review-section {
  1539. margin-bottom: 48rpx;
  1540. }
  1541. .review-card {
  1542. margin-bottom: 16rpx;
  1543. }
  1544. .review-card:last-child {
  1545. margin-bottom: 0;
  1546. }
  1547. .review-card-content {
  1548. display: flex;
  1549. justify-content: space-between;
  1550. align-items: center;
  1551. padding: 24rpx;
  1552. }
  1553. .review-info {
  1554. flex: 1;
  1555. min-width: 0;
  1556. }
  1557. .review-info .task-name {
  1558. font-size: 28rpx;
  1559. color: var(--text, #1E293B);
  1560. display: block;
  1561. margin-bottom: 4rpx;
  1562. }
  1563. .review-info .task-child {
  1564. font-size: 22rpx;
  1565. color: var(--muted, #94A3B8);
  1566. }
  1567. .review-actions {
  1568. display: flex;
  1569. gap: 12rpx;
  1570. flex-shrink: 0;
  1571. margin-left: 16rpx;
  1572. }
  1573. /* ========================================
  1574. g) 快捷操作
  1575. ======================================== */
  1576. .quick-section {
  1577. margin-bottom: 48rpx;
  1578. }
  1579. .quick-card {
  1580. padding: 16rpx 12rpx !important;
  1581. }
  1582. .quick-actions {
  1583. display: flex;
  1584. flex-wrap: wrap;
  1585. }
  1586. .action-item {
  1587. display: flex;
  1588. flex-direction: column;
  1589. align-items: center;
  1590. width: 20%;
  1591. margin-bottom: 12rpx;
  1592. transition: transform 0.15s ease;
  1593. }
  1594. .action-item:active {
  1595. transform: scale(0.92);
  1596. }
  1597. .action-icon {
  1598. width: 80rpx;
  1599. height: 80rpx;
  1600. border-radius: var(--radius-sm, 12rpx);
  1601. display: flex;
  1602. align-items: center;
  1603. justify-content: center;
  1604. margin-bottom: 10rpx;
  1605. }
  1606. .action-icon-text {
  1607. font-size: 32rpx;
  1608. }
  1609. .act-add { background: rgba(91, 155, 213, 0.12); }
  1610. .act-review { background: rgba(16, 185, 129, 0.12); }
  1611. .act-child { background: rgba(14, 165, 233, 0.12); }
  1612. .act-reward { background: rgba(245, 158, 11, 0.15); }
  1613. .act-invite { background: rgba(139, 92, 246, 0.12); }
  1614. .action-label {
  1615. font-size: 22rpx;
  1616. color: var(--text-secondary, #64748B);
  1617. font-weight: 500;
  1618. text-align: center;
  1619. }
  1620. /* ===== Error State ===== */
  1621. .error-state {
  1622. display: flex;
  1623. align-items: center;
  1624. justify-content: center;
  1625. padding: 32rpx 24rpx;
  1626. margin-bottom: 24rpx;
  1627. background: #FFF;
  1628. border-radius: 16rpx;
  1629. border: 1rpx dashed #E2E8F0;
  1630. }
  1631. .error-state-text {
  1632. font-size: 26rpx;
  1633. color: #94A3B8;
  1634. text-align: center;
  1635. }
  1636. /* ===== 重要关系(通讯录) ===== */
  1637. .contact-section {
  1638. margin: 20rpx 20rpx 0;
  1639. }
  1640. .contact-header {
  1641. display: flex;
  1642. justify-content: space-between;
  1643. align-items: center;
  1644. margin-bottom: 16rpx;
  1645. }
  1646. .contact-section-title {
  1647. font-size: 30rpx;
  1648. font-weight: 600;
  1649. color: #333;
  1650. }
  1651. .contact-header-right {
  1652. display: flex;
  1653. gap: 16rpx;
  1654. align-items: center;
  1655. }
  1656. .contact-more {
  1657. font-size: 24rpx;
  1658. color: #999;
  1659. }
  1660. .contact-import-btn {
  1661. font-size: 24rpx;
  1662. color: #F97316;
  1663. font-weight: 500;
  1664. }
  1665. .contact-scroll {
  1666. white-space: nowrap;
  1667. overflow: hidden;
  1668. }
  1669. .contact-scroll-inner {
  1670. display: flex;
  1671. flex-direction: row;
  1672. gap: 16rpx;
  1673. padding: 8rpx 0 16rpx;
  1674. }
  1675. .contact-card-wrap {
  1676. flex-shrink: 0;
  1677. width: 220rpx;
  1678. }
  1679. .contact-card-wrap:active {
  1680. opacity: 0.8;
  1681. }
  1682. .contact-add-card {
  1683. width: 160rpx;
  1684. }
  1685. .add-card-inner {
  1686. width: 160rpx;
  1687. height: 280rpx;
  1688. background: #fff;
  1689. border-radius: 16rpx;
  1690. border: 2rpx dashed #ddd;
  1691. display: flex;
  1692. flex-direction: column;
  1693. align-items: center;
  1694. justify-content: center;
  1695. gap: 12rpx;
  1696. }
  1697. .add-card-icon {
  1698. font-size: 48rpx;
  1699. color: #ccc;
  1700. }
  1701. .add-card-label {
  1702. font-size: 22rpx;
  1703. color: #999;
  1704. }
  1705. .contact-empty {
  1706. padding: 40rpx 0;
  1707. text-align: center;
  1708. }
  1709. .contact-empty-text {
  1710. font-size: 26rpx;
  1711. color: #ccc;
  1712. }
  1713. /* ===== 推荐阅读 ===== */
  1714. .article-section {
  1715. margin-bottom: 48rpx;
  1716. }
  1717. .article-header {
  1718. display: flex;
  1719. justify-content: space-between;
  1720. align-items: center;
  1721. margin-bottom: var(--space-md);
  1722. }
  1723. .article-header .section-title {
  1724. margin-bottom: 0;
  1725. }
  1726. .article-more {
  1727. font-size: 26rpx;
  1728. color: var(--color-primary, #F97316);
  1729. }
  1730. .article-list {
  1731. display: flex;
  1732. flex-direction: column;
  1733. gap: 16rpx;
  1734. }
  1735. .article-card {
  1736. background: #fff;
  1737. border-radius: 16rpx;
  1738. padding: 24rpx;
  1739. box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06);
  1740. }
  1741. .article-card:active {
  1742. opacity: 0.8;
  1743. }
  1744. .article-category {
  1745. display: inline-block;
  1746. padding: 4rpx 14rpx;
  1747. border-radius: 8rpx;
  1748. margin-bottom: 10rpx;
  1749. }
  1750. .article-category-text {
  1751. font-size: 20rpx;
  1752. color: #fff;
  1753. }
  1754. .article-title {
  1755. display: block;
  1756. font-size: 28rpx;
  1757. font-weight: bold;
  1758. color: #333;
  1759. margin-bottom: 8rpx;
  1760. line-height: 1.4;
  1761. }
  1762. .article-summary {
  1763. display: block;
  1764. font-size: 24rpx;
  1765. color: #888;
  1766. line-height: 1.5;
  1767. margin-bottom: 12rpx;
  1768. overflow: hidden;
  1769. text-overflow: ellipsis;
  1770. display: -webkit-box;
  1771. -webkit-line-clamp: 2;
  1772. -webkit-box-orient: vertical;
  1773. }
  1774. .article-meta {
  1775. display: flex;
  1776. align-items: center;
  1777. gap: 16rpx;
  1778. }
  1779. .meta-item {
  1780. display: flex;
  1781. align-items: center;
  1782. gap: 4rpx;
  1783. }
  1784. .meta-icon {
  1785. font-size: 24rpx;
  1786. }
  1787. .meta-text {
  1788. font-size: 22rpx;
  1789. color: #999;
  1790. }
  1791. .article-date {
  1792. font-size: 22rpx;
  1793. color: #bbb;
  1794. }
  1795. /* ===== 家庭挑战区域 ===== */
  1796. .challenge-section {
  1797. margin-bottom: 28rpx;
  1798. }
  1799. .challenge-section-header {
  1800. display: flex;
  1801. justify-content: space-between;
  1802. align-items: center;
  1803. margin: 0 20rpx 12rpx;
  1804. }
  1805. .challenge-section-title {
  1806. font-size: 32rpx;
  1807. font-weight: 700;
  1808. color: #1E293B;
  1809. }
  1810. .challenge-section-action {
  1811. font-size: 24rpx;
  1812. color: #10B981;
  1813. font-weight: 600;
  1814. }
  1815. /* ===== 健康圈入口卡片(P3:社区扩散) ===== */
  1816. .circle-entry-section {
  1817. margin: 0 20rpx 28rpx 20rpx;
  1818. }
  1819. .circle-card-wide {
  1820. width: 100%;
  1821. }
  1822. .circle-card-content {
  1823. padding: 24rpx;
  1824. display: flex;
  1825. flex-direction: column;
  1826. gap: 16rpx;
  1827. }
  1828. .circle-header {
  1829. display: flex;
  1830. align-items: center;
  1831. gap: 12rpx;
  1832. }
  1833. .circle-icon-wrap {
  1834. width: 56rpx;
  1835. height: 56rpx;
  1836. border-radius: 14rpx;
  1837. background: linear-gradient(135deg, #10B981, #34D399);
  1838. display: flex;
  1839. align-items: center;
  1840. justify-content: center;
  1841. }
  1842. .circle-icon {
  1843. font-size: 28rpx;
  1844. }
  1845. .circle-header-body {
  1846. flex: 1;
  1847. }
  1848. .circle-card-title {
  1849. display: block;
  1850. font-size: 28rpx;
  1851. font-weight: 700;
  1852. color: #1E293B;
  1853. line-height: 1.4;
  1854. }
  1855. .circle-card-subtitle {
  1856. display: block;
  1857. font-size: 22rpx;
  1858. color: #6B7280;
  1859. margin-top: 2rpx;
  1860. }
  1861. .circle-ranking-preview {
  1862. background: #F0FDF4;
  1863. border-radius: 12rpx;
  1864. padding: 16rpx;
  1865. display: flex;
  1866. flex-direction: column;
  1867. gap: 10rpx;
  1868. }
  1869. .circle-rank-row {
  1870. display: flex;
  1871. align-items: center;
  1872. gap: 10rpx;
  1873. }
  1874. .circle-rank-badge {
  1875. font-size: 24rpx;
  1876. width: 40rpx;
  1877. text-align: center;
  1878. }
  1879. .circle-rank-family {
  1880. flex: 1;
  1881. font-size: 24rpx;
  1882. color: #374151;
  1883. font-weight: 500;
  1884. }
  1885. .circle-rank-score {
  1886. font-size: 24rpx;
  1887. color: #F97316;
  1888. font-weight: 600;
  1889. }
  1890. .circle-action {
  1891. display: flex;
  1892. justify-content: flex-end;
  1893. }
  1894. .circle-action-text {
  1895. font-size: 24rpx;
  1896. color: #F97316;
  1897. font-weight: 500;
  1898. }
  1899. /* 无数据:邀请加入样式 */
  1900. .circle-card-invite {
  1901. background: linear-gradient(135deg, #F0FDF4, #ECFDF5);
  1902. }
  1903. .circle-invite-content {
  1904. padding: 32rpx 24rpx;
  1905. display: flex;
  1906. flex-direction: column;
  1907. align-items: center;
  1908. gap: 12rpx;
  1909. }
  1910. .circle-invite-icon {
  1911. font-size: 48rpx;
  1912. margin-bottom: 4rpx;
  1913. }
  1914. .circle-invite-title {
  1915. font-size: 30rpx;
  1916. font-weight: 700;
  1917. color: #065F46;
  1918. }
  1919. .circle-invite-desc {
  1920. font-size: 24rpx;
  1921. color: #6B7280;
  1922. text-align: center;
  1923. line-height: 1.5;
  1924. }
  1925. .circle-invite-cta {
  1926. font-size: 26rpx;
  1927. color: #F97316;
  1928. font-weight: 600;
  1929. margin-top: 4rpx;
  1930. }
  1931. /* ===== 五维家庭自检入口卡片(P0-1/P0-2) ===== */
  1932. .selfcheck-entry-section {
  1933. margin: 0 20rpx 28rpx 20rpx;
  1934. }
  1935. .selfcheck-card-wide {
  1936. width: 100%;
  1937. }
  1938. .selfcheck-card-content {
  1939. padding: 28rpx 24rpx;
  1940. display: flex;
  1941. align-items: center;
  1942. gap: 20rpx;
  1943. }
  1944. .selfcheck-icon-wrap {
  1945. display: flex;
  1946. flex-direction: row;
  1947. align-items: center;
  1948. flex-shrink: 0;
  1949. }
  1950. .selfcheck-icon-circle {
  1951. width: 52rpx;
  1952. height: 52rpx;
  1953. border-radius: 50%;
  1954. display: flex;
  1955. align-items: center;
  1956. justify-content: center;
  1957. margin-right: -10rpx;
  1958. border: 4rpx solid #fff;
  1959. box-shadow: 0 2rpx 6rpx rgba(0, 0, 0, 0.12);
  1960. }
  1961. .selfcheck-icon-circle:last-child {
  1962. margin-right: 0;
  1963. }
  1964. .selfcheck-icon-char {
  1965. font-size: 24rpx;
  1966. font-weight: 700;
  1967. color: #fff;
  1968. }
  1969. .selfcheck-body {
  1970. flex: 1;
  1971. }
  1972. .selfcheck-title {
  1973. display: block;
  1974. font-size: 30rpx;
  1975. font-weight: 700;
  1976. color: #1E293B;
  1977. line-height: 1.4;
  1978. }
  1979. .selfcheck-subtitle {
  1980. display: block;
  1981. font-size: 22rpx;
  1982. color: #6B7280;
  1983. margin-top: 4rpx;
  1984. }
  1985. .selfcheck-arrow {
  1986. font-size: 30rpx;
  1987. color: #F97316;
  1988. font-weight: 600;
  1989. flex-shrink: 0;
  1990. }
  1991. /* ===== 邀请码二维码弹窗 ===== */
  1992. .qr-code-modal {
  1993. position: fixed;
  1994. top: 0;
  1995. left: 0;
  1996. width: 100%;
  1997. height: 100%;
  1998. background: rgba(0, 0, 0, 0.6);
  1999. display: flex;
  2000. align-items: center;
  2001. justify-content: center;
  2002. z-index: 1000;
  2003. padding: 40rpx 20rpx;
  2004. box-sizing: border-box;
  2005. }
  2006. .qr-code-modal-content {
  2007. background: #fff;
  2008. border-radius: 20rpx;
  2009. padding: 40rpx 32rpx;
  2010. max-width: 600rpx;
  2011. width: 100%;
  2012. box-shadow: 0 10rpx 40rpx rgba(0, 0, 0, 0.2);
  2013. position: relative;
  2014. }
  2015. .qr-code-modal-header {
  2016. display: flex;
  2017. justify-content: space-between;
  2018. align-items: center;
  2019. margin-bottom: 32rpx;
  2020. }
  2021. .qr-code-modal-title {
  2022. font-size: 36rpx;
  2023. font-weight: 700;
  2024. color: #1E293B;
  2025. }
  2026. .qr-code-modal-close {
  2027. font-size: 48rpx;
  2028. color: #94A3B8;
  2029. line-height: 1;
  2030. padding: 8rpx;
  2031. cursor: pointer;
  2032. }
  2033. .qr-code-modal-body {
  2034. display: flex;
  2035. flex-direction: column;
  2036. align-items: center;
  2037. gap: 32rpx;
  2038. }
  2039. .qr-code-image-container {
  2040. width: 280rpx;
  2041. height: 280rpx;
  2042. display: flex;
  2043. align-items: center;
  2044. justify-content: center;
  2045. background: #f9fafb;
  2046. border-radius: 16rpx;
  2047. padding: 20rpx;
  2048. }
  2049. .qr-code-image {
  2050. width: 100%;
  2051. height: 100%;
  2052. object-fit: contain;
  2053. }
  2054. .qr-code-referral-code {
  2055. display: flex;
  2056. align-items: center;
  2057. gap: 16rpx;
  2058. padding: 24rpx;
  2059. background: #f9fafb;
  2060. border-radius: 16rpx;
  2061. width: 100%;
  2062. box-sizing: border-box;
  2063. }
  2064. .qr-code-label {
  2065. font-size: 26rpx;
  2066. color: #64748b;
  2067. font-weight: 500;
  2068. }
  2069. .qr-code-code {
  2070. flex: 1;
  2071. font-size: 32rpx;
  2072. font-weight: 700;
  2073. color: #1E293B;
  2074. word-break: break-all;
  2075. letter-spacing: 2rpx;
  2076. }
  2077. .qr-code-copy {
  2078. font-size: 24rpx;
  2079. color: #F97316;
  2080. font-weight: 500;
  2081. padding: 8rpx 16rpx;
  2082. background: #fff;
  2083. border-radius: 8rpx;
  2084. border: 1px solid #F97316;
  2085. cursor: pointer;
  2086. }
  2087. .qr-code-copy:active {
  2088. opacity: 0.8;
  2089. }
  2090. /* 无家庭成员空状态引导卡 */
  2091. .empty-family-card {
  2092. display: flex;
  2093. flex-direction: column;
  2094. align-items: center;
  2095. margin: 20rpx 24rpx;
  2096. padding: 48rpx 30rpx;
  2097. background: #fff;
  2098. border-radius: 24rpx;
  2099. border: 2rpx dashed #FED7AA;
  2100. }
  2101. .empty-family-icon {
  2102. font-size: 88rpx;
  2103. margin-bottom: 16rpx;
  2104. }
  2105. .empty-family-title {
  2106. font-size: 34rpx;
  2107. font-weight: 600;
  2108. color: #9A3412;
  2109. margin-bottom: 10rpx;
  2110. }
  2111. .empty-family-desc {
  2112. font-size: 26rpx;
  2113. color: #999;
  2114. margin-bottom: 28rpx;
  2115. }
  2116. .empty-family-btn {
  2117. width: 320rpx;
  2118. height: 80rpx;
  2119. line-height: 80rpx;
  2120. font-size: 30rpx;
  2121. font-weight: 600;
  2122. color: #fff;
  2123. background: linear-gradient(135deg, #F97316, #FF8C42);
  2124. border-radius: 40rpx;
  2125. border: none;
  2126. }
  2127. .empty-family-btn::after {
  2128. border: none;
  2129. }
  2130. </style>