index.vue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553
  1. <template>
  2. <view>
  3. <!-- 品牌启动页(冷启动时覆盖全屏) -->
  4. <view class="splash-overlay" v-if="showSplash">
  5. <view class="splash-content">
  6. <view class="splash-logo-mark">
  7. <text class="splash-logo-char">浠</text>
  8. </view>
  9. <text class="splash-brand-name">浠艾福</text>
  10. <text class="splash-tagline">给全家的一站式幸福提案</text>
  11. <view class="splash-loading">
  12. <view class="splash-dot splash-dot--1"></view>
  13. <view class="splash-dot splash-dot--2"></view>
  14. <view class="splash-dot splash-dot--3"></view>
  15. </view>
  16. </view>
  17. </view>
  18. <!-- 未登录态 — 发现页/引流内容,直接内嵌,不跳转 -->
  19. <view v-if="!currentRole" class="discover-container">
  20. <!-- 品牌头部 -->
  21. <PageBanner theme="home"
  22. tagline="家庭健康成长俱乐部"
  23. quote="给全家的一站式幸福提案" />
  24. <!-- 五行沙盘预览 -->
  25. <wuxing-sandbox mode="preview" badgeText="登录查看完整报告" @point-click="handleLogin" />
  26. <!-- 活动/商品推荐 -->
  27. <view class="recommend-section">
  28. <view class="section-header">
  29. <text class="section-title">🔥 热门推荐</text>
  30. <text class="section-more" @click="goShop">查看更多 ›</text>
  31. </view>
  32. <scroll-view scroll-x enable-flex show-scrollbar="false" class="type-filter-scroll">
  33. <view
  34. v-for="item in typeList"
  35. :key="item.value"
  36. :class="['filter-chip', currentType === item.value ? 'active' : '']"
  37. @click="onTypeChange(item.value)"
  38. >
  39. {{ item.label }}
  40. </view>
  41. </scroll-view>
  42. <view v-if="loading" class="loading-state">
  43. <text class="loading-text">加载中...</text>
  44. </view>
  45. <view v-else-if="products.length === 0" class="empty-state">
  46. <text class="empty-text">暂无商品,去商城看看吧</text>
  47. </view>
  48. <view v-else class="product-grid">
  49. <view
  50. v-for="item in products"
  51. :key="item.id"
  52. class="product-card"
  53. @click="handleLogin()"
  54. >
  55. <image class="product-cover" :src="item.coverImage || '/static/default-product.png'" mode="aspectFill" />
  56. <view class="product-info">
  57. <text class="product-name">{{ item.name }}</text>
  58. <view class="product-price-row">
  59. <!-- 游客不显示价格 -->
  60. <text class="product-price-login">登录查看</text>
  61. </view>
  62. </view>
  63. </view>
  64. </view>
  65. </view>
  66. <!-- 为何选择浠艾福 -->
  67. <view class="why-section">
  68. <view class="section-header">
  69. <text class="section-title">💡 为什么选择浠艾福</text>
  70. </view>
  71. <view class="feature-grid">
  72. <view class="feature-item">
  73. <text class="feature-icon">🎯</text>
  74. <text class="feature-title">科学评估</text>
  75. <text class="feature-desc">DAN少儿核心素养评估体系</text>
  76. </view>
  77. <view class="feature-item">
  78. <text class="feature-icon">🏆</text>
  79. <text class="feature-title">积分激励</text>
  80. <text class="feature-desc">完成任务得积分,兑换心愿</text>
  81. </view>
  82. <view class="feature-item">
  83. <text class="feature-icon">👨‍👩‍👧‍👦</text>
  84. <text class="feature-title">家庭参与</text>
  85. <text class="feature-desc">全家一起见证成长每一步</text>
  86. </view>
  87. <view class="feature-item">
  88. <text class="feature-icon">🌟</text>
  89. <text class="feature-title">专业规划</text>
  90. <text class="feature-desc">成长规划师一对一指导</text>
  91. </view>
  92. </view>
  93. </view>
  94. <!-- 底部登录按钮 -->
  95. <view class="login-footer">
  96. <button class="login-btn" @click="handleLogin">登录 / 注册</button>
  97. <text class="login-footer-text">登录即表示同意《用户协议》和《隐私政策》</text>
  98. </view>
  99. <!-- 底部占位 -->
  100. <view style="height: 120rpx;"></view>
  101. </view>
  102. <!-- 登录态 — 按角色渲染 -->
  103. <template v-else>
  104. <parent-index v-if="currentRole === 'parent'" />
  105. <child-index v-else-if="currentRole === 'child'" />
  106. <!-- teacher-index 已废弃,教师功能折入 parent-index -->
  107. <view v-else class="loading-container">
  108. <text>加载中...</text>
  109. </view>
  110. </template>
  111. </view>
  112. </template>
  113. <script>
  114. import ParentIndex from './parent-index.vue'
  115. import ChildIndex from './child-index.vue'
  116. // TeacherIndex 已废弃,教师功能折入 parent-index
  117. import PageBanner from '../../components/PageBanner.vue'
  118. import WuxingSandbox from '../../components/wuxing-sandbox.vue'
  119. import { acceptParentInvite, productList } from '../../utils/api.js'
  120. export default {
  121. components: {
  122. ParentIndex,
  123. ChildIndex,
  124. PageBanner,
  125. WuxingSandbox
  126. },
  127. data() {
  128. return {
  129. currentRole: '',
  130. showSplash: false,
  131. typeList: [
  132. { label: '全部', value: '' },
  133. { label: '活动', value: 'activity' },
  134. { label: '课程', value: 'course' },
  135. { label: '实物', value: 'physical' },
  136. { label: '数字', value: 'digital' },
  137. { label: '服务', value: 'service' }
  138. ],
  139. currentType: '',
  140. products: [],
  141. loading: false
  142. }
  143. },
  144. onLoad(options) {
  145. // 冷启动检查:是否已展示品牌启动页
  146. if (!uni.getStorageSync('_splashReady') && !this.showSplash) {
  147. this.showSplash = true
  148. var self = this
  149. setTimeout(function() {
  150. self.showSplash = false
  151. uni.setStorageSync('_splashReady', true)
  152. // splash 结束后执行初始化
  153. self._doInit()
  154. }, 2500)
  155. return
  156. }
  157. this._doInit(options)
  158. },
  159. onShow() {
  160. if (this.showSplash) {
  161. return
  162. }
  163. this._doInit()
  164. },
  165. _doInit(options) {
  166. options = options || {}
  167. if (!this.isLoggedIn()) {
  168. this.currentRole = ''
  169. if (!this.showSplash) {
  170. this.loadProducts()
  171. }
  172. return
  173. }
  174. this.syncRoleAndTabBar()
  175. if (options.inviteCode && options.type === 'parent_invite') {
  176. this.handleParentInvite(options.inviteCode)
  177. } else {
  178. var storedCode = uni.getStorageSync('inviteCode')
  179. var storedType = uni.getStorageSync('inviteType')
  180. if (storedCode && storedType === 'parent_invite') {
  181. this.handleParentInvite(storedCode)
  182. uni.removeStorageSync('inviteCode')
  183. uni.removeStorageSync('inviteType')
  184. }
  185. }
  186. },
  187. methods: {
  188. isLoggedIn() {
  189. return !!uni.getStorageSync('token')
  190. },
  191. normalizeCurrentRole() {
  192. const role = uni.getStorageSync('role') || 'parent'
  193. const currentRole = uni.getStorageSync('currentRole')
  194. if (!currentRole) {
  195. return role
  196. }
  197. // 非规划师账号不允许落到规划师首页
  198. if (currentRole === 'teacher' && role !== 'teacher') {
  199. return role
  200. }
  201. // 只有家长账号允许切换到孩子视角
  202. if (currentRole === 'child' && role !== 'parent') {
  203. return role
  204. }
  205. return currentRole
  206. },
  207. syncRoleAndTabBar() {
  208. this.currentRole = this.normalizeCurrentRole()
  209. uni.setStorageSync('currentRole', this.currentRole)
  210. },
  211. // ===== 发现页 / 未登录态方法 =====
  212. loadProducts() {
  213. this.loading = true
  214. var params = { page: 1, size: 10 }
  215. if (this.currentType) {
  216. params.productType = this.currentType
  217. }
  218. productList(params).then(function(res) {
  219. this.loading = false
  220. if (res.code === 200 && res.data) {
  221. this.products = res.data.records || []
  222. }
  223. }.bind(this)).catch(function() {
  224. this.loading = false
  225. }.bind(this))
  226. },
  227. onTypeChange(value) {
  228. this.currentType = value
  229. this.loadProducts()
  230. },
  231. goShop() {
  232. uni.navigateTo({ url: '/pages/action/index' })
  233. },
  234. goDetail(id) {
  235. uni.navigateTo({ url: '/pages/discover/product-detail/product-detail?id=' + id })
  236. },
  237. handleLogin() {
  238. uni.navigateTo({ url: '/pages/login/login?redirect=' + encodeURIComponent('/pages/index/index') })
  239. },
  240. async handleParentInvite(code) {
  241. // 延迟显示,等待登录完成
  242. setTimeout(async () => {
  243. const hasToken = uni.getStorageSync('token')
  244. if (!hasToken) {
  245. // 未登录,先去登录
  246. uni.reLaunch({
  247. url: '/pages/login/login?inviteCode=' + code
  248. })
  249. return
  250. }
  251. // 已登录,显示确认框
  252. uni.showModal({
  253. title: '加入家庭',
  254. content: '您已被邀请加入家庭,是否接受?',
  255. success: async (res) => {
  256. if (res.confirm) {
  257. try {
  258. uni.showLoading({ title: '加入中...' })
  259. const result = await acceptParentInvite(code)
  260. uni.hideLoading()
  261. if (result.code === 200) {
  262. const { familyId, role, token } = result.data
  263. uni.setStorageSync('familyId', familyId)
  264. uni.setStorageSync('role', role)
  265. uni.setStorageSync('token', token)
  266. uni.showToast({ title: '加入成功', icon: 'success' })
  267. // 刷新页面
  268. setTimeout(() => {
  269. uni.reLaunch({ url: '/pages/index/index' })
  270. }, 1500)
  271. }
  272. } catch (e) {
  273. uni.hideLoading()
  274. uni.showToast({ title: e.message || '加入失败', icon: 'none' })
  275. }
  276. }
  277. }
  278. })
  279. }, 500)
  280. }
  281. }
  282. }
  283. </script>
  284. <style>
  285. .discover-container {
  286. min-height: 100vh;
  287. background: #f5f7fa;
  288. padding-bottom: 100rpx;
  289. }
  290. /* ===== 推荐 ===== */
  291. .recommend-section {
  292. margin: 20rpx 30rpx;
  293. }
  294. .section-header {
  295. display: flex;
  296. justify-content: space-between;
  297. align-items: center;
  298. margin-bottom: 20rpx;
  299. }
  300. .section-title {
  301. font-size: 30rpx;
  302. font-weight: bold;
  303. color: #333;
  304. }
  305. .section-more {
  306. font-size: 24rpx;
  307. color: #5B9BD5;
  308. }
  309. .type-filter-scroll {
  310. white-space: nowrap;
  311. margin-bottom: 20rpx;
  312. }
  313. .filter-chip {
  314. display: inline-block;
  315. padding: 8rpx 24rpx;
  316. font-size: 24rpx;
  317. color: #666;
  318. background: #f0f0f0;
  319. border-radius: 30rpx;
  320. margin-right: 16rpx;
  321. }
  322. .filter-chip.active {
  323. background: #5B9BD5;
  324. color: #fff;
  325. }
  326. .loading-state,
  327. .empty-state {
  328. display: flex;
  329. justify-content: center;
  330. padding: 60rpx 0;
  331. }
  332. .loading-text,
  333. .empty-text {
  334. font-size: 26rpx;
  335. color: #999;
  336. }
  337. .product-grid {
  338. display: flex;
  339. flex-wrap: wrap;
  340. justify-content: space-between;
  341. }
  342. .product-card {
  343. width: 48%;
  344. background: #fff;
  345. border-radius: 16rpx;
  346. overflow: hidden;
  347. margin-bottom: 20rpx;
  348. box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.06);
  349. }
  350. .product-cover {
  351. width: 100%;
  352. height: 200rpx;
  353. background: #f0f0f0;
  354. }
  355. .product-info {
  356. padding: 14rpx;
  357. }
  358. .product-name {
  359. display: block;
  360. font-size: 26rpx;
  361. color: #333;
  362. margin-bottom: 10rpx;
  363. overflow: hidden;
  364. text-overflow: ellipsis;
  365. white-space: nowrap;
  366. }
  367. .product-price-row {
  368. display: flex;
  369. align-items: baseline;
  370. }
  371. .product-price {
  372. font-size: 28rpx;
  373. color: #5B9BD5;
  374. font-weight: bold;
  375. margin-right: 10rpx;
  376. }
  377. .product-price-login {
  378. font-size: 24rpx;
  379. color: #999;
  380. }
  381. .product-member {
  382. font-size: 20rpx;
  383. color: #999;
  384. }
  385. /* ===== 为何选择 ===== */
  386. .why-section {
  387. margin: 30rpx;
  388. }
  389. .feature-grid {
  390. display: flex;
  391. flex-direction: row;
  392. flex-wrap: wrap;
  393. justify-content: space-between;
  394. }
  395. .feature-item {
  396. width: calc(50% - 10rpx);
  397. box-sizing: border-box;
  398. background: #fff;
  399. border-radius: 20rpx;
  400. padding: 24rpx;
  401. margin-bottom: 20rpx;
  402. display: flex;
  403. flex-direction: column;
  404. align-items: center;
  405. text-align: center;
  406. box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06);
  407. }
  408. .feature-icon {
  409. font-size: 48rpx;
  410. margin-bottom: 10rpx;
  411. }
  412. .feature-title {
  413. font-size: 24rpx;
  414. font-weight: bold;
  415. color: #333;
  416. margin-bottom: 6rpx;
  417. }
  418. .feature-desc {
  419. font-size: 20rpx;
  420. color: #999;
  421. }
  422. /* ===== 底部登录 ===== */
  423. .login-footer {
  424. margin: 40rpx 30rpx;
  425. display: flex;
  426. flex-direction: column;
  427. align-items: center;
  428. }
  429. .login-btn {
  430. width: 100%;
  431. height: 88rpx;
  432. line-height: 88rpx;
  433. background: linear-gradient(135deg, #5B9BD5, #3A7CC4);
  434. color: #fff;
  435. font-size: 32rpx;
  436. font-weight: bold;
  437. border-radius: 44rpx;
  438. text-align: center;
  439. border: none;
  440. }
  441. .login-btn::after {
  442. border: none;
  443. }
  444. .login-footer-text {
  445. font-size: 20rpx;
  446. color: #bbb;
  447. margin-top: 16rpx;
  448. }
  449. /* ===== 加载占位 ===== */
  450. .loading-container {
  451. display: flex;
  452. justify-content: center;
  453. align-items: center;
  454. height: 100vh;
  455. }
  456. /* ===== 品牌启动页覆盖 ===== */
  457. .splash-overlay {
  458. position: fixed;
  459. top: 0;
  460. left: 0;
  461. right: 0;
  462. bottom: 0;
  463. display: flex;
  464. align-items: center;
  465. justify-content: center;
  466. background: linear-gradient(160deg, #FFF7ED 0%, #F97316 100%);
  467. z-index: 9999;
  468. }
  469. .splash-content {
  470. display: flex;
  471. flex-direction: column;
  472. align-items: center;
  473. opacity: 0;
  474. animation: splash-fade-in 0.8s ease forwards;
  475. }
  476. @keyframes splash-fade-in {
  477. from { opacity: 0; transform: translateY(20rpx); }
  478. to { opacity: 1; transform: translateY(0); }
  479. }
  480. .splash-logo-mark {
  481. width: 144rpx;
  482. height: 144rpx;
  483. border-radius: 36rpx;
  484. background: rgba(255,255,255,0.3);
  485. display: flex;
  486. align-items: center;
  487. justify-content: center;
  488. margin-bottom: 24rpx;
  489. }
  490. .splash-logo-char {
  491. font-size: 72rpx;
  492. font-weight: 700;
  493. color: #FFFFFF;
  494. line-height: 1;
  495. }
  496. .splash-brand-name {
  497. font-size: 56rpx;
  498. font-weight: 700;
  499. color: #FFFFFF;
  500. letter-spacing: 8rpx;
  501. margin-bottom: 12rpx;
  502. text-shadow: 0 2rpx 12rpx rgba(0,0,0,0.1);
  503. }
  504. .splash-tagline {
  505. font-size: 28rpx;
  506. color: rgba(255,255,255,0.85);
  507. letter-spacing: 4rpx;
  508. font-weight: 400;
  509. }
  510. .splash-loading {
  511. position: absolute;
  512. bottom: 120rpx;
  513. left: 0;
  514. right: 0;
  515. display: flex;
  516. justify-content: center;
  517. align-items: center;
  518. gap: 12rpx;
  519. }
  520. .splash-dot {
  521. width: 14rpx;
  522. height: 14rpx;
  523. border-radius: 50%;
  524. background: rgba(255,255,255,0.6);
  525. animation: splash-dot-bounce 1.4s ease-in-out infinite both;
  526. }
  527. .splash-dot--1 { animation-delay: -0.32s; }
  528. .splash-dot--2 { animation-delay: -0.16s; }
  529. .splash-dot--3 { animation-delay: 0s; }
  530. @keyframes splash-dot-bounce {
  531. 0%, 80%, 100% { transform: scale(0.6); opacity: 0.4; }
  532. 40% { transform: scale(1); opacity: 1; }
  533. }
  534. </style>