index.vue 55 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779
  1. <template>
  2. <view>
  3. <!-- 冷启动 splash overlay — 覆盖首页直到数据加载完毕(tab 切回时不显示) -->
  4. <view class="splash-overlay" :class="{'fade-out': splashOverlayFading}" v-if="splashOverlay || splashOverlayFading">
  5. <view class="splash-overlay-bg">
  6. <view class="bg-circle bg-circle-1"></view>
  7. <view class="bg-circle bg-circle-2"></view>
  8. <view class="bg-circle bg-circle-3"></view>
  9. </view>
  10. <view class="splash-overlay-body">
  11. <view class="splash-logo-wrap">
  12. <image class="splash-logo-img" src="/static/logo.png" mode="aspectFit" />
  13. </view>
  14. <text class="splash-brand-name">浠艾福</text>
  15. <text class="splash-brand-en">Care Family</text>
  16. <view class="splash-dims-row">
  17. <view class="splash-dim-item">
  18. <view class="splash-dim-circle" style="background:rgba(255,140,66,0.3);"><text class="splash-dim-icon">🌏</text></view>
  19. <text class="splash-dim-label">身</text>
  20. </view>
  21. <view class="splash-dim-item">
  22. <view class="splash-dim-circle" style="background:rgba(255,107,157,0.3);"><text class="splash-dim-icon">🔥</text></view>
  23. <text class="splash-dim-label">心</text>
  24. </view>
  25. <view class="splash-dim-item">
  26. <view class="splash-dim-circle" style="background:rgba(99,102,241,0.3);"><text class="splash-dim-icon">⚡</text></view>
  27. <text class="splash-dim-label">智</text>
  28. </view>
  29. <view class="splash-dim-item">
  30. <view class="splash-dim-circle" style="background:rgba(16,185,129,0.3);"><text class="splash-dim-icon">🌿</text></view>
  31. <text class="splash-dim-label">行</text>
  32. </view>
  33. <view class="splash-dim-item">
  34. <view class="splash-dim-circle" style="background:rgba(245,158,11,0.3);"><text class="splash-dim-icon">💧</text></view>
  35. <text class="splash-dim-label">富</text>
  36. </view>
  37. </view>
  38. <text class="splash-tagline">给全家的一站式幸福提案</text>
  39. <view class="splash-overlay-loading">
  40. <view class="splash-overlay-dot"></view>
  41. <view class="splash-overlay-dot"></view>
  42. <view class="splash-overlay-dot"></view>
  43. </view>
  44. </view>
  45. </view>
  46. <!-- 登录态 — 直接内嵌,保持底签可见 -->
  47. <view v-if="currentRole" class="logged-in-container">
  48. <!-- ① 了解区 — 自测大卡(首屏,引导"从了解开始") -->
  49. <view class="self-test-card" @click="goSelfTest">
  50. <view class="self-test-head">
  51. <text class="self-test-icon">⚡</text>
  52. <view class="self-test-info">
  53. <text class="self-test-title">{{ selfTestTitle }}</text>
  54. <text class="self-test-sub">{{ selfTestSub }}</text>
  55. </view>
  56. <text class="self-test-arrow">›</text>
  57. </view>
  58. </view>
  59. <!-- ② 发现区 — 能量沙盘 — Canvas 五行星图(未测评时显示引导卡) -->
  60. <GateLock v-if="sandboxLocked && sandboxLockGate" :gate="sandboxLockGate" />
  61. <WuxingSandbox
  62. v-else-if="sandboxData"
  63. mode="live"
  64. :scores="sandboxScores"
  65. :overallScore="sandboxData.overallScore || 0"
  66. :members="sandboxData.members || []"
  67. showDimensionBar
  68. starHeight="480rpx" />
  69. <!-- ② 发现区 — 上传报告 -->
  70. <view class="section">
  71. <ReportUploadCard
  72. title="上传报告"
  73. desc="体检报告 · 菌群检测 · 认知测评 · 心理测评 · 情绪打卡"
  74. :tags="['体检报告', '菌群检测', '认知测评', '心理测评', '情绪打卡']"
  75. @click="goToUploadReport" />
  76. </view>
  77. <!-- ③ 行动区 — 今日任务 -->
  78. <DailyTaskCard />
  79. <!-- ③ 行动区 — 我的任务(仅当有待完成时显示) -->
  80. <view v-if="hasPendingTasks" class="section">
  81. <view class="section-header">
  82. <text class="section-title">📝 我的任务</text>
  83. <text class="section-more" @click="navTo('/pages/tasks/tasks')">全部 ›</text>
  84. </view>
  85. <view class="task-quick-list">
  86. <view
  87. v-for="task in pendingTasks"
  88. :key="task.id"
  89. class="task-quick-item"
  90. >
  91. <view class="task-quick-info">
  92. <text class="task-quick-title line-clamp-1">{{ task.title }}</text>
  93. <text class="task-quick-meta">+{{ task.points }}分</text>
  94. </view>
  95. <button class="btn-quick-complete" @click="quickCompleteTask(task)">完成</button>
  96. </view>
  97. </view>
  98. </view>
  99. <!-- ③ 行动区 — 家庭挑战任务 -->
  100. <view class="section">
  101. <view class="section-header">
  102. <text class="section-title">🔥 家庭挑战</text>
  103. <text class="section-more" @click="goChallengeManage">管理 ›</text>
  104. </view>
  105. <FamilyChallengeCard
  106. v-for="ch in activeChallenges"
  107. :key="ch.id"
  108. :challenge="ch"
  109. :showCheckin="showCheckinBtn"
  110. @checkin="onChallengeCheckin"
  111. @respond="onChallengeRespond" />
  112. <view v-if="activeChallenges.length === 0" class="challenge-empty">
  113. <text class="challenge-empty-icon">🏆</text>
  114. <text class="challenge-empty-text">您还没有挑战,去添加吧</text>
  115. <button class="challenge-add-btn" @click="goChallengeManage">去添加</button>
  116. </view>
  117. </view>
  118. <!-- ③ 行动区 — 会员入口:非会员显示开通 / 快到期显示续费 -->
  119. <view v-if="showMemberEntry" class="member-entry" @click="goMembership">
  120. <view class="member-entry-icon-wrap">
  121. <text class="member-entry-icon">👑</text>
  122. </view>
  123. <view class="member-entry-texts">
  124. <text class="member-entry-title">{{ memberEntryTitle }}</text>
  125. <text class="member-entry-desc">{{ memberEntryDesc }}</text>
  126. </view>
  127. <view class="member-entry-btn">
  128. <text class="member-entry-btn-text">{{ memberEntryBtn }}</text>
  129. </view>
  130. </view>
  131. <!-- ④ 内容区 — 快捷功能入口(单排) -->
  132. <view class="section">
  133. <view class="quick-bar">
  134. <view class="quick-item" @click="navTo('/pages/tasks/tasks')">
  135. <view class="quick-icon" style="background:rgba(99,102,241,0.15)"><text>📋</text></view>
  136. <text class="quick-label">任务</text>
  137. </view>
  138. <view class="quick-item" @click="navTo('/pages/action-detail/interaction-log')">
  139. <view class="quick-icon" style="background:rgba(16,185,129,0.15)"><text>💬</text></view>
  140. <text class="quick-label">互动</text>
  141. </view>
  142. <view class="quick-item" @click="navTo('/pages/mind-detail/family-dashboard')">
  143. <view class="quick-icon" style="background:rgba(139,92,246,0.15)"><text>🏠</text></view>
  144. <text class="quick-label">天盘</text>
  145. </view>
  146. <view class="quick-item" @click="navTo('/pages/profile-extra/family-members')">
  147. <view class="quick-icon" style="background:rgba(245,158,11,0.15)"><text>👨‍👩‍👧‍👦</text></view>
  148. <text class="quick-label">成员</text>
  149. </view>
  150. <view class="quick-item" @click="navTo('/pages/discover/circles')">
  151. <view class="quick-icon" style="background:rgba(255,107,157,0.15)"><text>🤝</text></view>
  152. <text class="quick-label">圈子</text>
  153. </view>
  154. <view class="quick-item" @click="navTo('/pages/shop/index/index')">
  155. <view class="quick-icon" style="background:rgba(249,115,22,0.15)"><text>🛒</text></view>
  156. <text class="quick-label">商城</text>
  157. </view>
  158. </view>
  159. </view>
  160. <!-- ④ 内容区 — 家庭动态 -->
  161. <FamilyFeed
  162. :items="familyFeed"
  163. :notices="notices"
  164. :showHeader="true" />
  165. <!-- ④ 内容区 — 推荐文章 -->
  166. <view class="section">
  167. <view class="section-header">
  168. <text class="section-title">📖 推荐阅读</text>
  169. <text class="section-more" @click="goMoreArticles">更多 ›</text>
  170. </view>
  171. <ArticleBookshelf
  172. :articles="actionArticles"
  173. :isLoggedIn="true"
  174. @articleClick="goArticleDetail" />
  175. </view>
  176. <!-- ④ 内容区 — 推荐商品 -->
  177. <view class="section">
  178. <view class="section-header">
  179. <text class="section-title">🛍️ 推荐商品</text>
  180. <text class="section-more" @click="goMoreProducts">更多 ›</text>
  181. </view>
  182. <DimensionProducts
  183. :isLoggedIn="true"
  184. :showMore="false"
  185. @productClick="goProductDetail" />
  186. </view>
  187. <!-- 底部占位 -->
  188. <view class="bottom-spacer" style="height: 40rpx;"></view>
  189. </view>
  190. <!-- 未登录态 — 发现页/引流内容,直接内嵌,不跳转 -->
  191. <view v-else class="discover-container">
  192. <!-- 品牌定位语 -->
  193. <view class="brand-strap">
  194. <text class="brand-strap-text">身 · 心 · 智 · 行 · 富 &nbsp;五维幸福成长</text>
  195. </view>
  196. <!-- 五行沙盘预览 -->
  197. <wuxing-sandbox mode="preview" badgeText="登录查看完整报告" @point-click="handleLogin" />
  198. <!-- 活动/商品推荐 -->
  199. <view class="recommend-section">
  200. <view class="section-header">
  201. <text class="section-title">🔥 热门推荐</text>
  202. <text class="section-more" @click="goShop">查看更多 ›</text>
  203. </view>
  204. <scroll-view scroll-x enable-flex show-scrollbar="false" class="type-filter-scroll">
  205. <view
  206. v-for="item in typeList"
  207. :key="item.value"
  208. :class="['filter-chip', currentType === item.value ? 'active' : '']"
  209. @click="onTypeChange(item.value)"
  210. >
  211. {{ item.label }}
  212. </view>
  213. </scroll-view>
  214. <view v-if="loading" class="loading-state">
  215. <text class="loading-text">加载中...</text>
  216. </view>
  217. <view v-else-if="products.length === 0" class="empty-state">
  218. <text class="empty-text">暂无商品,去商城看看吧</text>
  219. </view>
  220. <view v-else class="product-grid">
  221. <view
  222. v-for="item in products"
  223. :key="item.id"
  224. class="product-card"
  225. @click="goDetail(item.id)"
  226. >
  227. <image class="product-cover" :src="getImageUrl(item.coverImage) || '/static/default-product.png'" mode="aspectFill" />
  228. <view class="product-info">
  229. <text class="product-name">{{ item.name }}</text>
  230. <view class="product-price-row">
  231. <text class="product-price" v-if="item.price && item.price > 0">¥{{ (item.price / 100).toFixed(0) }}</text>
  232. <text class="product-price-free" v-else>免费</text>
  233. </view>
  234. </view>
  235. </view>
  236. </view>
  237. </view>
  238. <!-- 热门活动(无条件展示) -->
  239. <view class="section">
  240. <view class="section-header">
  241. <text class="section-title">🔥 热门活动</text>
  242. <text class="section-more" @click="goActivities">查看更多 ›</text>
  243. </view>
  244. <view v-if="dimensionActivities.length === 0" class="empty-state" style="padding: 40rpx 0;">
  245. <text class="empty-text">暂无活动</text>
  246. </view>
  247. <view class="activity-list" v-else>
  248. <view class="activity-card" v-for="act in dimensionActivities" :key="act.id" @click="goActivityDetail(act)">
  249. <image class="activity-cover" :src="getImageUrl(act.coverImage) || '/static/default-activity.png'" mode="aspectFill" />
  250. <view class="activity-info">
  251. <text class="activity-name line-clamp-1">{{ act.title }}</text>
  252. <view class="activity-meta">
  253. <text class="activity-time">{{ act.startTime }}</text>
  254. </view>
  255. <view class="activity-bottom">
  256. <text class="activity-status" :class="'status-' + (act.status || 'upcoming')">{{ act.status === 'ongoing' ? '进行中' : act.status === 'ended' ? '已结束' : '即将开始' }}</text>
  257. <text class="activity-fee" v-if="act.price && act.price > 0">¥{{ (act.price / 100).toFixed(0) }}</text>
  258. <text class="activity-fee fee-free" v-else>免费</text>
  259. </view>
  260. </view>
  261. </view>
  262. </view>
  263. </view>
  264. <!-- 推荐阅读(无条件展示) -->
  265. <view class="section">
  266. <view class="section-header">
  267. <text class="section-title">📖 推荐阅读</text>
  268. <text class="section-more" @click="goArticles">更多 ›</text>
  269. </view>
  270. <view class="article-list">
  271. <view v-if="articlesLoading" class="loading-state" style="padding: 40rpx 0;">
  272. <text class="loading-text">加载中...</text>
  273. </view>
  274. <!-- 无数据时显示占位卡片 -->
  275. <template v-else-if="indexArticles.length === 0">
  276. <view class="article-card" v-for="(item, i) in [1,2,3]" :key="i">
  277. <view class="article-category" :style="{ background: gradientColors[(i-1) % gradientColors.length] }">
  278. <text class="article-category-text">推荐文章</text>
  279. </view>
  280. <text class="article-title">暂无推荐文章</text>
  281. <text class="article-summary">精彩内容即将上线,敬请期待</text>
  282. <view class="article-meta">
  283. <view class="meta-item">
  284. <text class="meta-icon">&#x1F4D6;</text>
  285. <text class="meta-text">0</text>
  286. </view>
  287. <text class="article-date">--</text>
  288. </view>
  289. </view>
  290. </template>
  291. <!-- 有数据时显示文章卡片 -->
  292. <template v-else>
  293. <view class="article-card" v-for="article in indexArticles" :key="article.id" @click="goArticleDetail(article.id)">
  294. <view class="article-category" :style="{ background: article.categoryColor }">
  295. <text class="article-category-text">{{ article.category }}</text>
  296. </view>
  297. <text class="article-title">{{ article.title }}</text>
  298. <text class="article-summary">{{ article.summary }}</text>
  299. <view class="article-meta">
  300. <view class="meta-item">
  301. <text class="meta-icon">&#x1F4D6;</text>
  302. <text class="meta-text">{{ article.views }}</text>
  303. </view>
  304. <text class="article-date">{{ article.date }}</text>
  305. </view>
  306. </view>
  307. </template>
  308. </view>
  309. </view>
  310. <!-- 底部登录按钮 -->
  311. <view class="login-footer">
  312. <button class="login-btn" @click="handleLogin">登录 / 注册</button>
  313. <text class="login-footer-text">登录即表示同意《用户协议》和《隐私政策》</text>
  314. </view>
  315. <!-- 底部占位 -->
  316. <view style="height: 120rpx;"></view>
  317. </view>
  318. </view>
  319. </template>
  320. <script>
  321. import store from '../../store/index.js'
  322. import WuxingSandbox from '../../components/wuxing-sandbox.vue'
  323. import GateLock from '../../components/gate-lock.vue'
  324. import FamilyFeed from '../../components/FamilyFeed.vue'
  325. import DimensionProducts from '../../components/DimensionProducts.vue'
  326. import ArticleBookshelf from '../../components/ArticleBookshelf.vue'
  327. import FamilyChallengeCard from '../../components/FamilyChallengeCard.vue'
  328. import DailyTaskCard from '../../components/daily-task-card.vue'
  329. import ReportUploadCard from '../../components/report-upload-card.vue'
  330. import { acceptParentInvite, productList, getActivityList, getFeaturedArticles, getFamilyEnergySandbox, getFamilyMemberList, getChildren, getChallengeList, switchToFamilyMember, getMyMembership, updateChallengeProgress, respondChallenge, getTodayTasks, completeTask as completeTaskApi, getTodayParentTasks, completeParentTask as completeParentTaskApi, getTaskHistory, getNotices, getNotificationList } from '../../utils/api.js'
  331. import nav from '../../utils/nav.js'
  332. import config from '@/config.js'
  333. import { parseDate } from '../../utils/format.js'
  334. export default {
  335. components: {
  336. WuxingSandbox,
  337. GateLock,
  338. FamilyFeed,
  339. DimensionProducts,
  340. ArticleBookshelf,
  341. FamilyChallengeCard,
  342. DailyTaskCard,
  343. ReportUploadCard,
  344. },
  345. data() {
  346. // 从 storage 预取登录态/角色,确保首次渲染即正确(WeChat 渲染层/逻辑层分离架构下 onLoad 执行前已渲染)
  347. var _token = uni.getStorageSync('token')
  348. var _currentRole = ''
  349. if (_token) {
  350. _currentRole = uni.getStorageSync('currentRole') || uni.getStorageSync('role') || ''
  351. }
  352. // 冷启动标记(由 App.vue onLaunch 设置,仅在冷启动时存在)
  353. var _isFirstLaunch = (uni.getStorageSync('_showSplashOverlay') === '1')
  354. return {
  355. currentRole: _currentRole,
  356. // splash loading overlay(冷启动时展示,数据加载完关闭)
  357. splashOverlay: _isFirstLaunch,
  358. splashOverlayFading: false,
  359. _splashReady: false,
  360. _watchPageReadyDone: false,
  361. typeList: [
  362. { label: '全部', value: '' },
  363. { label: '活动', value: 'activity' },
  364. { label: '课程', value: 'course' },
  365. { label: '实物', value: 'physical' },
  366. { label: '数字', value: 'digital' },
  367. { label: '服务', value: 'service' }
  368. ],
  369. currentType: '',
  370. products: [],
  371. loading: false,
  372. dimensionActivities: [],
  373. indexArticles: [],
  374. articlesLoading: false,
  375. gradientColors: [
  376. 'linear-gradient(135deg, #10B981, #34D399)',
  377. 'linear-gradient(135deg, #5B9BD5, #8FC5E8)',
  378. 'linear-gradient(135deg, #8B5CF6, #C084FC)',
  379. 'linear-gradient(135deg, #F97316, #FB923C)',
  380. 'linear-gradient(135deg, #6366F1, #818CF8)'
  381. ],
  382. // 登录态数据
  383. familyMembersVisible: [],
  384. // 接口数据是否已检查(避免"暂无数据"闪烁)
  385. childrenChecked: false,
  386. membersChecked: false,
  387. selectedMemberId: null,
  388. sandboxData: null,
  389. sandboxLocked: false,
  390. sandboxLockGate: null,
  391. articles: [],
  392. children: [],
  393. currentChildId: null,
  394. activeChallenges: [],
  395. pendingTasks: [],
  396. familyFeed: [],
  397. notices: [],
  398. loggedInInitDone: false,
  399. // 会员状态(沙盘下方会员入口使用)
  400. isMember: false,
  401. isLifetime: false,
  402. membershipDays: 0,
  403. }
  404. },
  405. computed: {
  406. isFamilyAdmin: function() {
  407. if (!this.familyMembersVisible || this.familyMembersVisible.length === 0) return false
  408. return this.familyMembersVisible[0].isAdmin === true
  409. },
  410. sandboxScores: function() {
  411. var d = this.sandboxData || {}
  412. return {
  413. body: d.bodyScore || 0,
  414. mind: d.mindScore || 0,
  415. wisdom: d.wisdomScore || 0,
  416. action: d.actionScore || 0,
  417. wealth: d.wealthScore || 0
  418. }
  419. },
  420. hasPendingTasks: function() {
  421. return (this.pendingTasks && this.pendingTasks.length > 0)
  422. },
  423. actionArticles: function() {
  424. if (!this.articles || this.articles.length === 0) return []
  425. return this.articles.slice(0, 5)
  426. },
  427. // 会员入口显示:非会员显示开通;会员剩余≤30天显示续费
  428. showMemberEntry: function() {
  429. if (!this.isLoggedIn()) return false
  430. if (this.isLifetime) return false
  431. if (!this.isMember) return true
  432. return this.membershipDays > 0 && this.membershipDays <= 30
  433. },
  434. showCheckinBtn: function() {
  435. return (uni.getStorageSync('role') || 'parent') === 'child'
  436. },
  437. memberEntryTitle: function() {
  438. if (this.isLifetime) return '终身会员 · 永久有效'
  439. if (!this.isMember) return '开通会员,解锁全部特权'
  440. return '会员即将到期,续费恢复权益'
  441. },
  442. memberEntryDesc: function() {
  443. if (this.isLifetime) return '享全平台权益,无需续费'
  444. if (!this.isMember) return '享专属折扣 · 成长服务 · 规划指导'
  445. return '剩余 ' + this.membershipDays + ' 天,续费保持权益不中断'
  446. },
  447. memberEntryBtn: function() {
  448. if (this.isLifetime) return '已拥有'
  449. return this.isMember ? '立即续费' : '立即开通'
  450. },
  451. // 自测大卡:已有数据(沙盘解锁)显示"再次了解",否则显示引导文案
  452. selfTestTitle: function() {
  453. return this.sandboxData ? '再次了解 · 看看变化' : '先花 1 分钟,了解全家健康状况'
  454. },
  455. selfTestSub: function() {
  456. if (this.sandboxData) {
  457. return '最近综合得分 ' + (this.sandboxData.overallScore || 0) + ' 分 · 点击重新自检'
  458. }
  459. return '15 题五维自检 · 约 20 秒'
  460. }
  461. },
  462. onLoad(options) {
  463. this._doInit(options)
  464. },
  465. onShow() {
  466. // splash overlay 已在 data() 中根据 storage 标记初始化,此处只需配置关闭条件
  467. if (this.splashOverlay) {
  468. uni.removeStorageSync('_showSplashOverlay')
  469. this._splashLoadCount = 0
  470. this._splashMinPassed = false
  471. var self = this
  472. // 最低展示 800ms
  473. setTimeout(function() {
  474. self._splashMinPassed = true
  475. self._tryHideSplash()
  476. }, 800)
  477. // 最多展示 5s
  478. setTimeout(function() {
  479. if (self.splashOverlay) self._hideSplashOverlay()
  480. }, 5000)
  481. }
  482. // 始终执行初始化,确保登录态变更后及时切换视图
  483. this._doInit()
  484. },
  485. methods: {
  486. _checkSplashReady() {
  487. this._splashLoadCount++
  488. this._tryHideSplash()
  489. },
  490. _tryHideSplash() {
  491. if (this.splashOverlayFading) return
  492. if (!this._splashMinPassed) return
  493. if (this.isLoggedIn()) {
  494. // 登录用户:等待子组件的 ready 信号
  495. if (this._splashReady) this._hideSplashOverlay()
  496. } else {
  497. // 游客:等待 3 个 API 全部返回
  498. if (this._splashLoadCount >= 3) this._hideSplashOverlay()
  499. }
  500. },
  501. _hideSplashOverlay() {
  502. if (this.splashOverlayFading) return
  503. this.splashOverlayFading = true
  504. var self = this
  505. setTimeout(function() {
  506. self.splashOverlay = false
  507. self.splashOverlayFading = false
  508. }, 400)
  509. },
  510. // ===== 登录态数据加载 =====
  511. loadLoggedInData: function() {
  512. this.loadFamilyMembers()
  513. this.loadChildren()
  514. },
  515. loadChildren: function() {
  516. var self = this
  517. getChildren().then(function(res) {
  518. if (res.code === 200 && res.data) {
  519. self.children = res.data
  520. var savedChildId = uni.getStorageSync('currentChildId')
  521. if (savedChildId) {
  522. self.currentChildId = savedChildId
  523. }
  524. self.loadDimensionData()
  525. }
  526. self.childrenChecked = true
  527. self.loadTodayTasks()
  528. self.loadFamilyFeed()
  529. self.loadNotices()
  530. self.loadNotifications()
  531. }).catch(function(e) {
  532. console.log('获取孩子列表失败', e)
  533. self.childrenChecked = true
  534. })
  535. },
  536. loadDimensionData: function() {
  537. this.loadSandboxData()
  538. this.loadActionArticles()
  539. this.loadChallenges()
  540. },
  541. loadSandboxData: function() {
  542. var self = this
  543. getFamilyEnergySandbox().then(function(res) {
  544. if (res && res.data) {
  545. var d = res.data
  546. if (d.locked) {
  547. // 未解锁:显示引导卡(GateLock 渲染自检引导按钮)
  548. self.sandboxLocked = true
  549. self.sandboxLockGate = d.gate || null
  550. self.sandboxData = null
  551. } else {
  552. self.sandboxLocked = false
  553. self.sandboxLockGate = null
  554. self.sandboxData = d
  555. }
  556. self._splashReady = true
  557. self._tryHideSplash()
  558. }
  559. }).catch(function() {})
  560. },
  561. loadActionArticles: function() {
  562. var self = this
  563. getFeaturedArticles({ size: 5 }).then(function(res) {
  564. if (res.code === 200 && res.data) {
  565. var gradientColors = [
  566. 'linear-gradient(135deg, #10B981, #34D399)',
  567. 'linear-gradient(135deg, #5B9BD5, #8FC5E8)',
  568. 'linear-gradient(135deg, #8B5CF6, #C084FC)',
  569. 'linear-gradient(135deg, #F97316, #FB923C)',
  570. 'linear-gradient(135deg, #6366F1, #818CF8)'
  571. ]
  572. var list = Array.isArray(res.data) ? res.data : (res.data.records || [])
  573. self.articles = list.map(function(item, index) {
  574. return {
  575. id: item.id,
  576. category: item.categoryName || '行动社交',
  577. categoryColor: gradientColors[index % gradientColors.length],
  578. title: item.title || '',
  579. summary: item.summary || '',
  580. dimensionWeights: item.dimensionWeights || null,
  581. views: item.viewCount ? String(item.viewCount) : '0',
  582. likes: '0',
  583. date: item.publishedAt ? item.publishedAt.slice(0, 10) : '',
  584. relatedDimensions: self.parseRelatedDimensions(item.relatedDimensions),
  585. coverImage: item.coverImage
  586. }
  587. })
  588. }
  589. }).catch(function() {})
  590. },
  591. loadChallenges: function() {
  592. var self = this
  593. getChallengeList({}).then(function(res) {
  594. if (res.code === 200 && Array.isArray(res.data)) {
  595. self.activeChallenges = res.data
  596. }
  597. }).catch(function() {
  598. self.activeChallenges = []
  599. })
  600. },
  601. loadTodayTasks: function() {
  602. var self = this
  603. if (!self.currentChildId) {
  604. self.pendingTasks = []
  605. return
  606. }
  607. getTodayTasks(self.currentChildId).then(function(res) {
  608. if (res.code === 200 && Array.isArray(res.data)) {
  609. self.pendingTasks = res.data.filter(function(t) {
  610. return t.status === 'pending'
  611. })
  612. }
  613. }).catch(function() {
  614. self.pendingTasks = []
  615. })
  616. },
  617. loadFamilyFeed: function() {
  618. var self = this
  619. var children = self.children || []
  620. if (children.length === 0) {
  621. self.familyFeed = []
  622. return
  623. }
  624. var promises = children.map(function(child) {
  625. return getTaskHistory(child.id, 1, 3).then(function(res) {
  626. var tasks = []
  627. if (res.code === 200) {
  628. var data = res.data
  629. tasks = (data && data.records) ? data.records : (Array.isArray(data) ? data : [])
  630. }
  631. return tasks.filter(function(t) { return t.status === 'completed' }).map(function(t) {
  632. var points = t.pointsEarned || t.points || 0
  633. var timeStr = ''
  634. if (t.completedAt && typeof t.completedAt === 'string') {
  635. timeStr = t.completedAt.substring(11, 16)
  636. }
  637. return {
  638. text: '✅ ' + (child.nickname || '小名') + ' 完成「' + (t.title || '任务') + '」 +' + points + '分',
  639. time: timeStr,
  640. type: 'task'
  641. }
  642. })
  643. }).catch(function() { return [] })
  644. })
  645. Promise.all(promises).then(function(results) {
  646. var all = []
  647. for (var i = 0; i < results.length; i++) {
  648. for (var j = 0; j < results[i].length; j++) {
  649. all.push(results[i][j])
  650. }
  651. }
  652. self.familyFeed = all.slice(0, 10)
  653. }).catch(function() {
  654. self.familyFeed = []
  655. })
  656. },
  657. loadNotices: function() {
  658. var self = this
  659. getNotices().then(function(res) {
  660. if (res.code === 200 && res.data) {
  661. var notices = res.data.map(function(n) {
  662. var timeStr = ''
  663. if (n.createdAt) {
  664. var d = new Date(n.createdAt)
  665. timeStr = (d.getMonth() + 1) + '月' + d.getDate() + '日'
  666. }
  667. return {
  668. text: '📢 ' + n.title,
  669. time: timeStr,
  670. type: 'notice',
  671. notice: n
  672. }
  673. })
  674. self.notices = notices
  675. // 合并 notices 到 familyFeed(优先显示)
  676. self.familyFeed = notices.concat(self.familyFeed).slice(0, 10)
  677. }
  678. }).catch(function() {})
  679. },
  680. // W3: 用户通知入动态(未读优先显示在 familyFeed 顶部)
  681. loadNotifications: function() {
  682. var self = this
  683. getNotificationList().then(function(res) {
  684. if (res.code === 200 && res.data) {
  685. var notifications = res.data.map(function(n) {
  686. var timeStr = ''
  687. if (n.createdAt) {
  688. var d = parseDate(n.createdAt)
  689. if (d) {
  690. timeStr = (d.getMonth() + 1) + '月' + d.getDate() + '日'
  691. }
  692. }
  693. return {
  694. text: '📬 ' + n.title,
  695. time: timeStr,
  696. type: 'notification',
  697. unread: (n.isRead === 0),
  698. notice: n
  699. }
  700. })
  701. var unread = notifications.filter(function(n) { return n.unread })
  702. var read = notifications.filter(function(n) { return !n.unread })
  703. // 未读在前,已读在后,合并到 familyFeed 顶部
  704. self.familyFeed = unread.concat(read, self.familyFeed).slice(0, 10)
  705. }
  706. }).catch(function() {})
  707. },
  708. quickCompleteTask: function(task) {
  709. var self = this
  710. completeTaskApi(task.id, self.currentChildId, '').then(function(res) {
  711. if (res.code === 200) {
  712. uni.showToast({ title: '获得 ' + (res.data && res.data.pointsEarned || 0) + ' 积分', icon: 'success' })
  713. self.loadTodayTasks()
  714. } else {
  715. uni.showToast({ title: (res && res.message) || '完成失败', icon: 'none' })
  716. }
  717. }).catch(function() {
  718. uni.showToast({ title: '完成失败,请重试', icon: 'none' })
  719. })
  720. },
  721. onChallengeCheckin: function(payload) {
  722. var self = this
  723. updateChallengeProgress({ challengeId: payload.challengeId, delta: 1 }).then(function(res) {
  724. if (res && res.code === 200) {
  725. uni.showToast({ title: '打卡成功', icon: 'success' })
  726. self.loadChallenges()
  727. } else {
  728. uni.showToast({ title: (res && res.message) || '打卡失败', icon: 'none' })
  729. }
  730. }).catch(function() {
  731. uni.showToast({ title: '打卡失败,请重试', icon: 'none' })
  732. })
  733. },
  734. onChallengeRespond: function(payload) {
  735. var self = this
  736. respondChallenge({ challengeId: payload.challengeId, action: payload.action }).then(function(res) {
  737. if (res && res.code === 200) {
  738. uni.showToast({ title: payload.action === 'agree' ? '已同意' : '已拒绝', icon: 'success' })
  739. self.loadChallenges()
  740. } else {
  741. uni.showToast({ title: (res && res.message) || '操作失败', icon: 'none' })
  742. self.loadChallenges()
  743. }
  744. }).catch(function() {
  745. uni.showToast({ title: '操作失败,请重试', icon: 'none' })
  746. })
  747. },
  748. loadFamilyMembers: function() {
  749. var self = this
  750. getFamilyMemberList({ visibleOnly: true }).then(function(res) {
  751. if (res.code === 200 && res.data) {
  752. self.familyMembersVisible = Array.isArray(res.data) ? res.data : (res.data.list || [])
  753. }
  754. self.membersChecked = true
  755. }).catch(function(e) {
  756. console.log('获取可见家庭成员失败', e)
  757. self.familyMembersVisible = []
  758. self.membersChecked = true
  759. })
  760. },
  761. parseRelatedDimensions: function(val) {
  762. if (!val) return []
  763. if (Array.isArray(val)) return val
  764. if (typeof val === 'string') {
  765. try {
  766. var parsed = JSON.parse(val)
  767. return Array.isArray(parsed) ? parsed : []
  768. } catch (e) {
  769. return val.split(',').map(function(s) { return s.trim() }).filter(function(s) { return s })
  770. }
  771. }
  772. return []
  773. },
  774. onFamilyMemberSelect: function(member) {
  775. this.selectedMemberId = member.id
  776. },
  777. onFamilyMemberSwitch: function(member) {
  778. var self = this
  779. if (!member || !member.id) return
  780. switchToFamilyMember(member.id, member.source || 'family_member').then(function(res) {
  781. if (res.code === 200 && res.data) {
  782. store.commit('switchToMember', {
  783. memberId: res.data.memberId,
  784. effectiveRole: res.data.effectiveRole
  785. })
  786. uni.showToast({ title: '已切换到' + (member.nickname || '该成员'), icon: 'success' })
  787. setTimeout(function() {
  788. uni.reLaunch({ url: '/pages/index-home/index' })
  789. }, 1000)
  790. } else {
  791. uni.showToast({ title: res.message || '切换失败', icon: 'none' })
  792. }
  793. }).catch(function() {
  794. uni.showToast({ title: '切换失败', icon: 'none' })
  795. })
  796. },
  797. navTo: function(url) {
  798. nav.goPage(url)
  799. },
  800. goActivityDetail: function(act) {
  801. if (!act || !act.id) return
  802. uni.navigateTo({ url: '/pages/activity/activity-detail/activity-detail?id=' + act.id })
  803. },
  804. goMoreActivities: function() {
  805. uni.navigateTo({ url: '/pages/activity/index' })
  806. },
  807. goProductDetail: function(prod) {
  808. uni.navigateTo({ url: '/pages/discover-detail/product-detail/product-detail?id=' + prod.id })
  809. },
  810. goMoreProducts: function() {
  811. uni.navigateTo({ url: '/pages/shop/index/index' })
  812. },
  813. goArticleDetail: function(article) {
  814. var id = article.id || article
  815. uni.navigateTo({ url: '/pages/article-center/article-detail?id=' + id })
  816. },
  817. goMoreArticles: function() {
  818. uni.navigateTo({ url: '/pages/article-center/index?dimension=action' })
  819. },
  820. goToUploadReport: function() {
  821. var id = this.currentChildId || ''
  822. uni.navigateTo({ url: '/pages/health/report-upload?memberId=' + id + '&from=index' })
  823. },
  824. goSelfTest: function() {
  825. uni.navigateTo({ url: '/pages/health/survey-questionnaire?dim=all' })
  826. },
  827. _doInit(options) {
  828. options = options || {}
  829. if (!this.isLoggedIn()) {
  830. this.currentRole = ''
  831. this.loadProducts()
  832. this.loadDimensionActivities()
  833. this.loadFeaturedArticles()
  834. return
  835. }
  836. this.syncRoleAndTabBar()
  837. if (options.inviteCode && options.type === 'parent_invite') {
  838. this.handleParentInvite(options.inviteCode)
  839. return
  840. }
  841. var storedCode = uni.getStorageSync('inviteCode')
  842. var storedType = uni.getStorageSync('inviteType')
  843. if (storedCode && storedType === 'parent_invite') {
  844. this.handleParentInvite(storedCode)
  845. uni.removeStorageSync('inviteCode')
  846. uni.removeStorageSync('inviteType')
  847. return
  848. }
  849. // 登录用户:直接在 TabBar 页面内联渲染,保持底签可见
  850. if (!this.loggedInInitDone) {
  851. this.loggedInInitDone = true
  852. this.loadLoggedInData()
  853. }
  854. this.checkMemberStatus()
  855. this._watchPageReady()
  856. },
  857. _watchPageReady() {
  858. if (this._watchPageReadyDone) return
  859. this._watchPageReadyDone = true
  860. var that = this
  861. that.$watch('_splashReady', function(newVal) {
  862. if (newVal) that._tryHideSplash()
  863. })
  864. },
  865. isLoggedIn() {
  866. return !!uni.getStorageSync('token')
  867. },
  868. // ===== 会员状态(沙盘下方会员入口) =====
  869. checkMemberStatus: function() {
  870. var that = this
  871. getMyMembership().then(function(res) {
  872. var m = res.data || {}
  873. var level = m.memberLevel || m.levelCode || ''
  874. that.isLifetime = (level === 'LIFETIME')
  875. that.isMember = that.isLifetime || !!(level && level !== 'FREE')
  876. var mem = m.membership || m
  877. if (mem.daysRemaining != null) {
  878. that.membershipDays = mem.daysRemaining
  879. } else if (that.isLifetime) {
  880. // 终身会员:永久有效,不显示剩余天数
  881. that.membershipDays = 0
  882. }
  883. }).catch(function() {})
  884. },
  885. goMembership: function() {
  886. uni.navigateTo({ url: '/pages/membership/upgrade' })
  887. },
  888. normalizeCurrentRole() {
  889. const role = uni.getStorageSync('role') || 'parent'
  890. const currentRole = uni.getStorageSync('currentRole')
  891. if (!currentRole) {
  892. return role
  893. }
  894. // 非规划师账号不允许落到规划师首页
  895. if (currentRole === 'teacher' && role !== 'teacher') {
  896. return role
  897. }
  898. // 只有家长账号允许切换到孩子视角
  899. if (currentRole === 'child' && role !== 'parent') {
  900. return role
  901. }
  902. return currentRole
  903. },
  904. syncRoleAndTabBar() {
  905. this.currentRole = this.normalizeCurrentRole()
  906. uni.setStorageSync('currentRole', this.currentRole)
  907. },
  908. // ===== 发现页 / 未登录态方法 =====
  909. loadProducts: function() {
  910. this.loading = true
  911. var params = { page: 1, size: 10 }
  912. if (this.currentType) {
  913. params.productType = this.currentType
  914. }
  915. var self = this
  916. productList(params).then(function(res) {
  917. self.loading = false
  918. if (res && res.code === 200) {
  919. // res.data is the response wrapper, products are in res.data.data or res.data.records
  920. var rawData = res.data && (Array.isArray(res.data) ? res.data : (res.data.data || res.data.records || []))
  921. self.products = Array.isArray(rawData) ? rawData : []
  922. }
  923. self._checkSplashReady()
  924. }).catch(function() {
  925. self.loading = false
  926. self._checkSplashReady()
  927. })
  928. },
  929. loadDimensionActivities: function() {
  930. var self = this
  931. getActivityList({ dimensionCode: '', page: 1, size: 3 }).then(function(res) {
  932. if (res && res.code === 200) {
  933. // res.data is the response wrapper, activities are in res.data.data or res.data.records
  934. var rawData = res.data && (Array.isArray(res.data) ? res.data : (res.data.data || res.data.records || []))
  935. self.dimensionActivities = Array.isArray(rawData) ? rawData.slice(0, 3) : []
  936. }
  937. self._checkSplashReady()
  938. }).catch(function() {
  939. self._checkSplashReady()
  940. })
  941. },
  942. loadFeaturedArticles: function() {
  943. var self = this
  944. this.articlesLoading = true
  945. getFeaturedArticles({ size: 5 }).then(function(res) {
  946. self.articlesLoading = false
  947. if (res && res.code === 200) {
  948. var gradientColors = [
  949. 'linear-gradient(135deg, #10B981, #34D399)',
  950. 'linear-gradient(135deg, #5B9BD5, #8FC5E8)',
  951. 'linear-gradient(135deg, #8B5CF6, #C084FC)',
  952. 'linear-gradient(135deg, #F97316, #FB923C)',
  953. 'linear-gradient(135deg, #6366F1, #818CF8)'
  954. ]
  955. // res.data is the response wrapper, articles are in res.data.data
  956. var rawList = res.data && (Array.isArray(res.data) ? res.data : (res.data.data || res.data.records || []))
  957. var list = Array.isArray(rawList) ? rawList : []
  958. self.indexArticles = list.map(function(item, index) {
  959. return {
  960. id: item.id,
  961. category: item.categoryName || item.category || '热门文章',
  962. categoryColor: gradientColors[index % gradientColors.length],
  963. title: item.title || '',
  964. summary: item.summary || '',
  965. views: item.viewCount ? String(item.viewCount) : '0',
  966. date: item.publishedAt ? item.publishedAt.slice(0, 10) : '',
  967. coverImage: item.coverImage
  968. }
  969. })
  970. }
  971. self._checkSplashReady()
  972. }).catch(function() {
  973. self.articlesLoading = false
  974. self._checkSplashReady()
  975. })
  976. },
  977. goActivities: function() {
  978. uni.navigateTo({ url: '/pages/activity/index' })
  979. },
  980. goChallengeManage: function() {
  981. uni.navigateTo({ url: '/pages/health/challenge-manage' })
  982. },
  983. goArticles: function() {
  984. uni.navigateTo({ url: '/pages/mind-detail/articles' })
  985. },
  986. goArticleDetail: function(article) {
  987. var id = article && article.id ? article.id : article
  988. uni.navigateTo({ url: '/pages/article-center/article-detail?id=' + id })
  989. },
  990. onTypeChange(value) {
  991. this.currentType = value
  992. this.loadProducts()
  993. },
  994. goShop() {
  995. uni.navigateTo({ url: '/pages/shop/index/index' })
  996. },
  997. goDetail(id) {
  998. uni.navigateTo({ url: '/pages/discover-detail/product-detail/product-detail?id=' + id })
  999. },
  1000. handleLogin() {
  1001. uni.navigateTo({ url: '/pages/login/login?redirect=' + encodeURIComponent('/pages/index-home/index') })
  1002. },
  1003. async handleParentInvite(code) {
  1004. // 延迟显示,等待登录完成
  1005. setTimeout(async () => {
  1006. const hasToken = uni.getStorageSync('token')
  1007. if (!hasToken) {
  1008. // 未登录,先去登录
  1009. uni.reLaunch({
  1010. url: '/pages/login/login?inviteCode=' + code
  1011. })
  1012. return
  1013. }
  1014. // 已登录,显示确认框
  1015. uni.showModal({
  1016. title: '加入家庭',
  1017. content: '您已被邀请加入家庭,是否接受?',
  1018. success: async (res) => {
  1019. if (res.confirm) {
  1020. try {
  1021. uni.showLoading({ title: '加入中...' })
  1022. const result = await acceptParentInvite(code)
  1023. uni.hideLoading()
  1024. if (result.code === 200) {
  1025. const { role, token } = result.data
  1026. uni.setStorageSync('role', role)
  1027. uni.setStorageSync('token', token)
  1028. uni.showToast({ title: '加入成功', icon: 'success' })
  1029. // 刷新页面
  1030. setTimeout(() => {
  1031. uni.switchTab({ url: '/pages/index-home/index' })
  1032. }, 1500)
  1033. }
  1034. } catch (e) {
  1035. uni.hideLoading()
  1036. uni.showToast({ title: e.message || '加入失败', icon: 'none' })
  1037. }
  1038. }
  1039. }
  1040. })
  1041. }, 500)
  1042. },
  1043. getImageUrl: function(path) {
  1044. return config.API_BASE_URL + path
  1045. },
  1046. }
  1047. }
  1048. </script>
  1049. <style>
  1050. .discover-container {
  1051. min-height: 100vh;
  1052. background: #f5f7fa;
  1053. padding-bottom: 100rpx;
  1054. }
  1055. /* ===== 品牌定位语 ===== */
  1056. .brand-strap {
  1057. margin: 16rpx 30rpx 0;
  1058. padding: 20rpx 28rpx;
  1059. background: linear-gradient(135deg, #FFF7ED, rgba(249,115,22,0.10));
  1060. border-radius: 16rpx;
  1061. border-left: 4rpx solid #F97316;
  1062. }
  1063. .brand-strap-text {
  1064. font-size: 26rpx;
  1065. color: #9A3412;
  1066. font-weight: 500;
  1067. }
  1068. /* ===== 会员入口卡(能量沙盘下方) ===== */
  1069. .member-entry {
  1070. display: flex;
  1071. align-items: center;
  1072. margin: 20rpx 30rpx 0;
  1073. background: linear-gradient(135deg, #F97316, #FB923C);
  1074. border-radius: 20rpx;
  1075. padding: 24rpx 30rpx;
  1076. }
  1077. .member-entry-icon-wrap {
  1078. width: 64rpx;
  1079. height: 64rpx;
  1080. border-radius: 50%;
  1081. background: rgba(255,255,255,0.2);
  1082. display: flex;
  1083. align-items: center;
  1084. justify-content: center;
  1085. margin-right: 16rpx;
  1086. flex-shrink: 0;
  1087. }
  1088. .member-entry-icon {
  1089. font-size: 36rpx;
  1090. }
  1091. .member-entry-texts {
  1092. flex: 1;
  1093. display: flex;
  1094. flex-direction: column;
  1095. min-width: 0;
  1096. }
  1097. .member-entry-title {
  1098. font-size: 30rpx;
  1099. font-weight: bold;
  1100. color: #fff;
  1101. }
  1102. .member-entry-desc {
  1103. font-size: 22rpx;
  1104. color: rgba(255,255,255,0.85);
  1105. margin-top: 4rpx;
  1106. overflow: hidden;
  1107. text-overflow: ellipsis;
  1108. white-space: nowrap;
  1109. }
  1110. .member-entry-btn {
  1111. margin-left: 16rpx;
  1112. padding: 10rpx 24rpx;
  1113. background: #fff;
  1114. border-radius: 30rpx;
  1115. flex-shrink: 0;
  1116. }
  1117. .member-entry-btn-text {
  1118. font-size: 24rpx;
  1119. color: #F97316;
  1120. font-weight: bold;
  1121. }
  1122. /* ===== 推荐 ===== */
  1123. .recommend-section {
  1124. margin: 20rpx 30rpx;
  1125. }
  1126. .type-filter-scroll {
  1127. white-space: nowrap;
  1128. margin-bottom: 20rpx;
  1129. }
  1130. .filter-chip {
  1131. display: inline-block;
  1132. padding: 8rpx 24rpx;
  1133. font-size: 24rpx;
  1134. color: #666;
  1135. background: #f0f0f0;
  1136. border-radius: 30rpx;
  1137. margin-right: 16rpx;
  1138. }
  1139. .filter-chip.active {
  1140. background: #5B9BD5;
  1141. color: #fff;
  1142. }
  1143. .loading-state,
  1144. .empty-state {
  1145. display: flex;
  1146. justify-content: center;
  1147. padding: 60rpx 0;
  1148. }
  1149. .loading-text,
  1150. .empty-text {
  1151. font-size: 26rpx;
  1152. color: #999;
  1153. }
  1154. .product-grid {
  1155. display: flex;
  1156. flex-wrap: wrap;
  1157. justify-content: space-between;
  1158. }
  1159. .product-card {
  1160. width: 48%;
  1161. background: #fff;
  1162. border-radius: 16rpx;
  1163. overflow: hidden;
  1164. margin-bottom: 20rpx;
  1165. box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.06);
  1166. }
  1167. .product-cover {
  1168. width: 100%;
  1169. height: 200rpx;
  1170. background: #f0f0f0;
  1171. }
  1172. .product-info {
  1173. padding: 14rpx;
  1174. }
  1175. .product-name {
  1176. display: block;
  1177. font-size: 26rpx;
  1178. color: #333;
  1179. margin-bottom: 10rpx;
  1180. overflow: hidden;
  1181. text-overflow: ellipsis;
  1182. white-space: nowrap;
  1183. }
  1184. .product-price-row {
  1185. display: flex;
  1186. align-items: baseline;
  1187. }
  1188. .product-price {
  1189. font-size: 28rpx;
  1190. color: #5B9BD5;
  1191. font-weight: bold;
  1192. margin-right: 10rpx;
  1193. }
  1194. .product-price-login {
  1195. font-size: 24rpx;
  1196. color: #999;
  1197. }
  1198. .product-member {
  1199. font-size: 20rpx;
  1200. color: #999;
  1201. }
  1202. /* ===== 五维全景幸福矩阵 ===== */
  1203. /* ===== 底部登录 ===== */
  1204. .login-footer {
  1205. margin: 40rpx 30rpx;
  1206. display: flex;
  1207. flex-direction: column;
  1208. align-items: center;
  1209. }
  1210. .login-btn {
  1211. width: 100%;
  1212. height: 88rpx;
  1213. line-height: 88rpx;
  1214. background: linear-gradient(135deg, #5B9BD5, #3A7CC4);
  1215. color: #fff;
  1216. font-size: 32rpx;
  1217. font-weight: bold;
  1218. border-radius: 44rpx;
  1219. text-align: center;
  1220. border: none;
  1221. }
  1222. .login-btn::after {
  1223. border: none;
  1224. }
  1225. .login-footer-text {
  1226. font-size: 20rpx;
  1227. color: #bbb;
  1228. margin-top: 16rpx;
  1229. }
  1230. /* ===== 加载占位 ===== */
  1231. .loading-container {
  1232. display: flex;
  1233. justify-content: center;
  1234. align-items: center;
  1235. height: 100vh;
  1236. }
  1237. /* ===== 角色切换淡入淡出 ===== */
  1238. .role-switch-wrapper {
  1239. width: 100%;
  1240. min-height: 100vh;
  1241. }
  1242. .fade-enter-active,
  1243. .fade-leave-active {
  1244. transition: opacity 0.3s ease;
  1245. }
  1246. .fade-enter,
  1247. .fade-leave-to {
  1248. opacity: 0;
  1249. }
  1250. /* ===== 通用区块样式 ===== */
  1251. .section {
  1252. margin: 20rpx 30rpx;
  1253. }
  1254. .section-header {
  1255. display: flex;
  1256. justify-content: space-between;
  1257. align-items: center;
  1258. margin-bottom: 20rpx;
  1259. }
  1260. .section-title {
  1261. font-size: 30rpx;
  1262. font-weight: bold;
  1263. color: #333;
  1264. }
  1265. .section-more {
  1266. font-size: 24rpx;
  1267. color: #5B9BD5;
  1268. }
  1269. .empty-state {
  1270. display: flex;
  1271. justify-content: center;
  1272. padding: 60rpx 0;
  1273. }
  1274. .empty-text {
  1275. font-size: 26rpx;
  1276. color: #999;
  1277. }
  1278. /* ===== 活动列表 ===== */
  1279. .activity-list {
  1280. display: flex;
  1281. flex-direction: column;
  1282. gap: 16rpx;
  1283. }
  1284. .activity-card {
  1285. background: #fff;
  1286. border-radius: 16rpx;
  1287. overflow: hidden;
  1288. display: flex;
  1289. flex-direction: row;
  1290. box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06);
  1291. }
  1292. .activity-card:active {
  1293. opacity: 0.8;
  1294. }
  1295. .activity-cover {
  1296. width: 200rpx;
  1297. height: 160rpx;
  1298. flex-shrink: 0;
  1299. background: #f0f0f0;
  1300. }
  1301. .activity-info {
  1302. flex: 1;
  1303. padding: 16rpx;
  1304. display: flex;
  1305. flex-direction: column;
  1306. justify-content: space-between;
  1307. }
  1308. .activity-name {
  1309. font-size: 28rpx;
  1310. font-weight: bold;
  1311. color: #333;
  1312. margin-bottom: 8rpx;
  1313. }
  1314. .line-clamp-1 {
  1315. overflow: hidden;
  1316. text-overflow: ellipsis;
  1317. white-space: nowrap;
  1318. }
  1319. .activity-meta {
  1320. display: flex;
  1321. align-items: center;
  1322. margin-bottom: 8rpx;
  1323. }
  1324. .activity-time {
  1325. font-size: 22rpx;
  1326. color: #999;
  1327. }
  1328. .activity-bottom {
  1329. display: flex;
  1330. align-items: center;
  1331. gap: 12rpx;
  1332. }
  1333. .activity-status {
  1334. font-size: 20rpx;
  1335. padding: 2rpx 10rpx;
  1336. border-radius: 8rpx;
  1337. background: #E8F5E9;
  1338. color: #2E7D32;
  1339. }
  1340. .activity-status.status-upcoming {
  1341. background: #E3F2FD;
  1342. color: #1565C0;
  1343. }
  1344. .activity-status.status-ended {
  1345. background: #F5F5F5;
  1346. color: #999;
  1347. }
  1348. .activity-fee {
  1349. font-size: 24rpx;
  1350. color: #F97316;
  1351. font-weight: bold;
  1352. }
  1353. .activity-fee.fee-free {
  1354. color: #4CAF50;
  1355. }
  1356. /* ===== 文章列表 ===== */
  1357. .article-list {
  1358. display: flex;
  1359. flex-direction: column;
  1360. gap: 16rpx;
  1361. }
  1362. .article-card {
  1363. background: #fff;
  1364. border-radius: 16rpx;
  1365. padding: 24rpx;
  1366. box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06);
  1367. }
  1368. .article-card:active {
  1369. opacity: 0.8;
  1370. }
  1371. .article-category {
  1372. display: inline-block;
  1373. padding: 4rpx 14rpx;
  1374. border-radius: 8rpx;
  1375. margin-bottom: 10rpx;
  1376. }
  1377. .article-category-text {
  1378. font-size: 20rpx;
  1379. color: #fff;
  1380. }
  1381. .article-title {
  1382. display: block;
  1383. font-size: 28rpx;
  1384. font-weight: bold;
  1385. color: #333;
  1386. margin-bottom: 8rpx;
  1387. line-height: 1.4;
  1388. }
  1389. .article-summary {
  1390. display: block;
  1391. font-size: 24rpx;
  1392. color: #888;
  1393. line-height: 1.5;
  1394. margin-bottom: 12rpx;
  1395. overflow: hidden;
  1396. text-overflow: ellipsis;
  1397. display: -webkit-box;
  1398. -webkit-line-clamp: 2;
  1399. -webkit-box-orient: vertical;
  1400. }
  1401. .article-meta {
  1402. display: flex;
  1403. align-items: center;
  1404. gap: 16rpx;
  1405. }
  1406. .meta-item {
  1407. display: flex;
  1408. align-items: center;
  1409. gap: 4rpx;
  1410. }
  1411. .meta-icon {
  1412. font-size: 24rpx;
  1413. }
  1414. .meta-text {
  1415. font-size: 22rpx;
  1416. color: #999;
  1417. }
  1418. .article-date {
  1419. font-size: 22rpx;
  1420. color: #bbb;
  1421. }
  1422. /* ===== Splash Loading Overlay(冷启动蒙板,来自 merge splash.vue) ===== */
  1423. .splash-overlay {
  1424. position: fixed;
  1425. top: 0; left: 0; right: 0; bottom: 0;
  1426. z-index: 9999;
  1427. display: flex;
  1428. align-items: center;
  1429. justify-content: center;
  1430. background: linear-gradient(160deg, #FFF7ED 0%, #FFEDD5 50%, #FED7AA 100%);
  1431. overflow: hidden;
  1432. opacity: 1;
  1433. transition: opacity 0.4s ease;
  1434. }
  1435. .splash-overlay.fade-out {
  1436. opacity: 0;
  1437. pointer-events: none;
  1438. }
  1439. /* ---- bg circles from splash.vue ---- */
  1440. .splash-overlay-bg { position: absolute; top: 0; left: 0; right: 0; bottom: 0; pointer-events: none; }
  1441. .bg-circle {
  1442. position: absolute;
  1443. border-radius: 50%;
  1444. opacity: 0.12;
  1445. }
  1446. .bg-circle-1 {
  1447. width: 400rpx; height: 400rpx;
  1448. background: #FF8C42;
  1449. top: -100rpx; right: -80rpx;
  1450. animation: splash-float-1 4s ease-in-out infinite;
  1451. }
  1452. .bg-circle-2 {
  1453. width: 280rpx; height: 280rpx;
  1454. background: #FF6B9D;
  1455. bottom: 80rpx; left: -60rpx;
  1456. animation: splash-float-2 5s ease-in-out infinite;
  1457. }
  1458. .bg-circle-3 {
  1459. width: 180rpx; height: 180rpx;
  1460. background: #6366F1;
  1461. top: 50%; right: -40rpx;
  1462. animation: splash-float-3 6s ease-in-out infinite;
  1463. }
  1464. @keyframes splash-float-1 {
  1465. 0%, 100% { transform: translateY(0) scale(1); }
  1466. 50% { transform: translateY(-30rpx) scale(1.05); }
  1467. }
  1468. @keyframes splash-float-2 {
  1469. 0%, 100% { transform: translateY(0) scale(1); }
  1470. 50% { transform: translateY(20rpx) scale(1.08); }
  1471. }
  1472. @keyframes splash-float-3 {
  1473. 0%, 100% { transform: translateY(0) rotate(0deg); }
  1474. 50% { transform: translateY(-20rpx) rotate(10deg); }
  1475. }
  1476. /* ---- body fade-in ---- */
  1477. .splash-overlay-body {
  1478. display: flex;
  1479. flex-direction: column;
  1480. align-items: center;
  1481. animation: splash-body-in 0.8s cubic-bezier(0.16,1,0.3,1) forwards;
  1482. }
  1483. @keyframes splash-body-in {
  1484. from { opacity: 0; transform: translateY(40rpx) scale(0.96); }
  1485. to { opacity: 1; transform: translateY(0) scale(1); }
  1486. }
  1487. /* ---- logo ---- */
  1488. .splash-logo-wrap {
  1489. width: 160rpx; height: 160rpx;
  1490. border-radius: 40rpx;
  1491. background: rgba(255,255,255,0.95);
  1492. display: flex; align-items: center; justify-content: center;
  1493. margin-bottom: 16rpx;
  1494. box-shadow: 0 8rpx 32rpx rgba(249,115,22,0.25);
  1495. animation: splash-logo-pulse 2.5s ease-in-out infinite;
  1496. }
  1497. @keyframes splash-logo-pulse {
  1498. 0%, 100% { box-shadow: 0 8rpx 32rpx rgba(249,115,22,0.25); }
  1499. 50% { box-shadow: 0 8rpx 48rpx rgba(249,115,22,0.45); }
  1500. }
  1501. .splash-logo-img { width: 100rpx; height: 100rpx; }
  1502. /* ---- brand ---- */
  1503. .splash-brand-name {
  1504. font-size: 64rpx; font-weight: 800;
  1505. color: #7C2D12;
  1506. letter-spacing: 10rpx;
  1507. margin-bottom: 2rpx;
  1508. }
  1509. .splash-brand-en {
  1510. font-size: 20rpx;
  1511. color: rgba(124,45,18,0.5);
  1512. letter-spacing: 6rpx;
  1513. margin-bottom: 30rpx;
  1514. }
  1515. .splash-tagline {
  1516. font-size: 26rpx;
  1517. color: rgba(124,45,18,0.7);
  1518. letter-spacing: 4rpx;
  1519. margin-top: 24rpx;
  1520. }
  1521. /* ---- dims row from splash.vue (rounded circles) ---- */
  1522. .splash-dims-row {
  1523. display: flex; flex-direction: row;
  1524. align-items: center; justify-content: center;
  1525. gap: 28rpx;
  1526. }
  1527. .splash-dim-item {
  1528. display: flex; flex-direction: column;
  1529. align-items: center; gap: 8rpx;
  1530. }
  1531. .splash-dim-circle {
  1532. width: 80rpx; height: 80rpx;
  1533. border-radius: 50%;
  1534. display: flex; align-items: center; justify-content: center;
  1535. }
  1536. .splash-dim-icon { font-size: 36rpx; line-height: 1; }
  1537. .splash-dim-label {
  1538. font-size: 22rpx; font-weight: 500;
  1539. color: rgba(124,45,18,0.75);
  1540. line-height: 1;
  1541. }
  1542. /* ---- staggered dim entry from splash.vue ---- */
  1543. .splash-dim-item:nth-child(1) { animation: splash-dim-in 0.5s ease 0.1s both; }
  1544. .splash-dim-item:nth-child(2) { animation: splash-dim-in 0.5s ease 0.2s both; }
  1545. .splash-dim-item:nth-child(3) { animation: splash-dim-in 0.5s ease 0.3s both; }
  1546. .splash-dim-item:nth-child(4) { animation: splash-dim-in 0.5s ease 0.4s both; }
  1547. .splash-dim-item:nth-child(5) { animation: splash-dim-in 0.5s ease 0.5s both; }
  1548. @keyframes splash-dim-in {
  1549. from { opacity: 0; transform: translateY(16rpx) scale(0.8); }
  1550. to { opacity: 1; transform: translateY(0) scale(1); }
  1551. }
  1552. /* ---- loading dots ---- */
  1553. .splash-overlay-loading {
  1554. position: absolute;
  1555. bottom: 120rpx;
  1556. left: 0; right: 0;
  1557. display: flex;
  1558. justify-content: center;
  1559. align-items: center;
  1560. gap: 12rpx;
  1561. }
  1562. .splash-overlay-dot {
  1563. width: 14rpx; height: 14rpx;
  1564. border-radius: 50%;
  1565. background: rgba(249,115,22,0.6);
  1566. animation: splash-dot-bounce 1.4s ease-in-out infinite both;
  1567. }
  1568. .splash-overlay-dot:nth-child(1) { animation-delay: -0.32s; }
  1569. .splash-overlay-dot:nth-child(2) { animation-delay: -0.16s; }
  1570. .splash-overlay-dot:nth-child(3) { animation-delay: 0s; }
  1571. @keyframes splash-dot-bounce {
  1572. 0%, 80%, 100% { transform: scale(0.6); opacity: 0.4; }
  1573. 40% { transform: scale(1); opacity: 1; }
  1574. }
  1575. /* ---- 登录态内容布局 ---- */
  1576. .logged-in-container {
  1577. padding-bottom: 120rpx;
  1578. }
  1579. /* ===== ① 了解区 — 自测大卡(引导"从了解开始") ===== */
  1580. .self-test-card {
  1581. margin: 20rpx 30rpx 16rpx;
  1582. background: linear-gradient(135deg, #667eea, #764ba2);
  1583. border-radius: 20rpx;
  1584. padding: 24rpx 28rpx;
  1585. }
  1586. .self-test-head { display: flex; align-items: center; }
  1587. .self-test-icon { font-size: 40rpx; margin-right: 16rpx; }
  1588. .self-test-info { flex: 1; }
  1589. .self-test-title { font-size: 28rpx; font-weight: bold; color: #fff; display: block; }
  1590. .self-test-sub { font-size: 22rpx; color: rgba(255,255,255,0.8); display: block; margin-top: 4rpx; }
  1591. .self-test-arrow { font-size: 36rpx; color: rgba(255,255,255,0.7); }
  1592. /* ===== 快捷功能入口(单排,6个横向排列) ===== */
  1593. .quick-bar {
  1594. display: flex;
  1595. flex-direction: row;
  1596. justify-content: space-between;
  1597. align-items: center;
  1598. background: #fff;
  1599. border-radius: 20rpx;
  1600. padding: 20rpx 10rpx;
  1601. }
  1602. .quick-item {
  1603. flex: 1;
  1604. display: flex;
  1605. flex-direction: column;
  1606. align-items: center;
  1607. gap: 8rpx;
  1608. }
  1609. .quick-icon {
  1610. width: 80rpx;
  1611. height: 80rpx;
  1612. border-radius: 20rpx;
  1613. display: flex;
  1614. align-items: center;
  1615. justify-content: center;
  1616. }
  1617. .quick-icon text {
  1618. font-size: 36rpx;
  1619. }
  1620. .quick-item .quick-label {
  1621. font-size: 26rpx;
  1622. color: #666;
  1623. }
  1624. /* ===== 我的任务快捷列表 ===== */
  1625. .task-quick-list {
  1626. display: flex;
  1627. flex-direction: column;
  1628. gap: 12rpx;
  1629. }
  1630. .task-quick-item {
  1631. display: flex;
  1632. flex-direction: row;
  1633. align-items: center;
  1634. justify-content: space-between;
  1635. background: #fff;
  1636. border-radius: 16rpx;
  1637. padding: 18rpx 24rpx;
  1638. box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.04);
  1639. }
  1640. .task-quick-info {
  1641. display: flex;
  1642. flex-direction: column;
  1643. min-width: 0;
  1644. flex: 1;
  1645. }
  1646. .task-quick-title {
  1647. font-size: 26rpx;
  1648. color: #333;
  1649. font-weight: 500;
  1650. line-height: 1.4;
  1651. }
  1652. .task-quick-meta {
  1653. font-size: 22rpx;
  1654. color: #F97316;
  1655. margin-top: 4rpx;
  1656. font-weight: bold;
  1657. }
  1658. .btn-quick-complete {
  1659. margin-left: 16rpx;
  1660. padding: 8rpx 24rpx;
  1661. background: linear-gradient(135deg, #F97316, #FB923C);
  1662. color: #fff;
  1663. font-size: 24rpx;
  1664. font-weight: bold;
  1665. border-radius: 20rpx;
  1666. border: none;
  1667. line-height: 1;
  1668. height: 44rpx;
  1669. flex-shrink: 0;
  1670. }
  1671. .btn-quick-complete::after {
  1672. border: none;
  1673. }
  1674. /* ===== 家庭挑战空态 ===== */
  1675. .challenge-empty {
  1676. display: flex;
  1677. flex-direction: column;
  1678. align-items: center;
  1679. padding: 40rpx 0 20rpx;
  1680. }
  1681. .challenge-empty-icon {
  1682. font-size: 64rpx;
  1683. margin-bottom: 12rpx;
  1684. line-height: 1;
  1685. }
  1686. .challenge-empty-text {
  1687. font-size: 26rpx;
  1688. color: #999;
  1689. margin-bottom: 20rpx;
  1690. }
  1691. .challenge-add-btn {
  1692. padding: 10rpx 32rpx;
  1693. background: linear-gradient(135deg, #F97316, #FB923C);
  1694. color: #fff;
  1695. font-size: 24rpx;
  1696. font-weight: bold;
  1697. border-radius: 24rpx;
  1698. border: none;
  1699. line-height: 1;
  1700. height: 48rpx;
  1701. }
  1702. .challenge-add-btn::after {
  1703. border: none;
  1704. }
  1705. </style>