login.vue 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573
  1. <template>
  2. <view class="login-container">
  3. <!-- Logo 区 -->
  4. <view class="logo-section">
  5. <image class="logo" src="/static/logo.png" mode="aspectFit" @error="handleImageError"></image>
  6. <text class="app-name">浠艾福</text>
  7. <text class="slogan">给全家的一站式幸福提案</text>
  8. <text class="powered">亿图腾科技出品</text>
  9. </view>
  10. <!-- 角色价值说明 -->
  11. <view class="value-section">
  12. <view class="value-title">你能得到什么</view>
  13. <!-- 家长端 -->
  14. <view class="role-card role-parent">
  15. <view class="role-badge">
  16. <text class="role-badge-icon">👨‍👩‍👧</text>
  17. <text class="role-badge-text">家长</text>
  18. </view>
  19. <view class="role-items">
  20. <view class="role-item"><text class="role-item-icon">📋</text><text class="role-item-text">一键创建家庭成长计划</text></view>
  21. <view class="role-item"><text class="role-item-icon">📊</text><text class="role-item-text">跟踪孩子任务完成情况</text></view>
  22. <view class="role-item"><text class="role-item-icon">🧠</text><text class="role-item-text">预约专业心智测评</text></view>
  23. <view class="role-item"><text class="role-item-icon">🩺</text><text class="role-item-text">健康报告 AI 智能解析</text></view>
  24. </view>
  25. </view>
  26. <!-- 孩子端 -->
  27. <view class="role-card role-child">
  28. <view class="role-badge role-badge-child">
  29. <text class="role-badge-icon">👶</text>
  30. <text class="role-badge-text">孩子</text>
  31. </view>
  32. <view class="role-items">
  33. <view class="role-item"><text class="role-item-icon">🎯</text><text class="role-item-text">每日趣味任务挑战</text></view>
  34. <view class="role-item"><text class="role-item-icon">🏆</text><text class="role-item-text">积分兑换心愿奖励</text></view>
  35. <view class="role-item"><text class="role-item-icon">🧩</text><text class="role-item-text">专注力小游戏训练</text></view>
  36. </view>
  37. </view>
  38. </view>
  39. <!-- 五维能量 -->
  40. <view class="wuxing-section">
  41. <view class="wuxing-title">五维成长体系</view>
  42. <view class="wuxing-tags">
  43. <view class="wuxing-tag wuxing-body"><text class="wuxing-emoji">🌏</text>身·健康</view>
  44. <view class="wuxing-tag wuxing-mind"><text class="wuxing-emoji">🔥</text>心·情绪</view>
  45. <view class="wuxing-tag wuxing-wisdom"><text class="wuxing-emoji">⚡</text>智·赋能</view>
  46. <view class="wuxing-tag wuxing-action"><text class="wuxing-emoji">🌿</text>行·习惯</view>
  47. <view class="wuxing-tag wuxing-wealth"><text class="wuxing-emoji">💧</text>富·财商</view>
  48. </view>
  49. <text class="wuxing-desc">五行平衡 · 全面评估全家健康状态</text>
  50. </view>
  51. <!-- 登录表单 -->
  52. <view class="login-form">
  53. <view class="phone-section">
  54. <button
  55. class="phone-btn"
  56. open-type="getPhoneNumber"
  57. @getphonenumber="getPhoneNumber"
  58. >
  59. <text class="phone-btn-icon">📱</text>
  60. <text>手机号快捷登录</text>
  61. </button>
  62. </view>
  63. <!-- 暂不登录 -->
  64. <view class="skip-section">
  65. <text class="skip-btn" @click="handleSkipLogin">暂不登录,先逛逛</text>
  66. </view>
  67. <view class="agreement">
  68. <checkbox-group @change="onAgreementChange">
  69. <label>
  70. <checkbox value="agree" :checked="agreed" />
  71. <text class="agreement-text">我已阅读并同意</text>
  72. </label>
  73. </checkbox-group>
  74. <text class="link" @click="showAgreement">《用户协议》</text>
  75. <text class="agreement-text">和</text>
  76. <text class="link" @click="showPrivacy">《隐私政策》</text>
  77. </view>
  78. </view>
  79. </view>
  80. </template>
  81. <script>
  82. import { wechatPhoneLogin, checkTeamInviteStatus, joinTeamByInvite, bindReferral, bindFamilyInviter } from '../../utils/api.js'
  83. export default {
  84. data() {
  85. return {
  86. loading: false,
  87. agreed: false,
  88. nickname: '',
  89. avatar: '',
  90. redirectUrl: ''
  91. }
  92. },
  93. onLoad(options) {
  94. options = options || {}
  95. // 处理家庭邀请码(新流程:仅首次注册时绑定邀请人)
  96. if (options.familyInviteCode) {
  97. uni.setStorageSync('pendingFamilyInviteCode', options.familyInviteCode)
  98. }
  99. // 兼容旧邀请码参数
  100. if (options.inviteCode) {
  101. uni.setStorageSync('inviteCode', options.inviteCode)
  102. uni.setStorageSync('inviteType', options.inviteType || 'family')
  103. }
  104. // 推广码
  105. if (options.refCode) {
  106. uni.setStorageSync('pendingRefCode', options.refCode)
  107. }
  108. // 存储登录后跳转地址
  109. if (options.redirect) {
  110. this.redirectUrl = decodeURIComponent(options.redirect)
  111. }
  112. // 注意:不在 onLoad 预取 login code —— 微信 code 一次性有效(5分钟且用过即废),
  113. // 预取后如果用户隔几分钟再点击授权,code 已失效会报 invalid code (40029)。
  114. // 正确做法:getPhoneNumber 中用户点击授权时实时获取。
  115. },
  116. methods: {
  117. handleImageError(e) {
  118. console.log('Logo 加载失败,使用默认显示')
  119. },
  120. onAgreementChange(e) {
  121. this.agreed = e.detail.value.length > 0
  122. },
  123. showAgreement() {
  124. uni.navigateTo({
  125. url: '/pages/policy/agreement'
  126. })
  127. },
  128. showPrivacy() {
  129. uni.navigateTo({
  130. url: '/pages/policy/privacy'
  131. })
  132. },
  133. handleSkipLogin() {
  134. // 不登录直接进入首页
  135. uni.switchTab({ url: '/pages/index-home/index' })
  136. },
  137. getPhoneNumber: function(e) {
  138. if (!this.agreed) {
  139. uni.showToast({ title: '请先同意用户协议', icon: 'none' })
  140. return
  141. }
  142. if (!e.detail || !e.detail.code) {
  143. uni.showToast({ title: '授权失败,请重试', icon: 'none' })
  144. return
  145. }
  146. var self = this
  147. self.loading = true
  148. // 点击授权时实时获取 login code(微信 code 一次性有效,不能复用旧 code)
  149. uni.login({
  150. provider: 'weixin',
  151. success: function(loginRes) {
  152. if (!loginRes.code) {
  153. uni.showToast({ title: '网络异常,请重试', icon: 'none' })
  154. self.loading = false
  155. return
  156. }
  157. wechatPhoneLogin({ code: loginRes.code, phoneCode: e.detail.code }).then(function(res) {
  158. self.completeLogin(res.data)
  159. }).catch(function(err) {
  160. uni.showToast({ title: err.message || '登录失败', icon: 'none' })
  161. self.loading = false
  162. })
  163. },
  164. fail: function() {
  165. uni.showToast({ title: '网络异常,请重试', icon: 'none' })
  166. self.loading = false
  167. }
  168. })
  169. },
  170. completeLogin: function(data) {
  171. var self = this
  172. uni.setStorageSync('token', data.token)
  173. uni.setStorageSync('userId', data.userId)
  174. uni.setStorageSync('role', data.role)
  175. uni.setStorageSync('currentRole', data.role)
  176. uni.setStorageSync('isSwitchedChild', false)
  177. uni.setStorageSync('isSwitchedTeacher', false)
  178. if (data.openid) uni.setStorageSync('openid', data.openid)
  179. // familyId 必须持久化:家庭在注册时已由后端创建,缺失会导致页面误判无家庭而重复创建
  180. uni.setStorageSync('userInfo', { nickname: data.nickname, userId: data.userId, familyId: data.familyId })
  181. if (data.phone) uni.setStorageSync('phone', data.phone)
  182. if (data.teacherStatus) uni.setStorageSync('teacherStatus', data.teacherStatus)
  183. if (data.teacherRejectReason) uni.setStorageSync('teacherRejectReason', data.teacherRejectReason)
  184. self.$store.commit('setFamilyId', data.familyId)
  185. uni.showToast({ title: '登录成功', icon: 'success' })
  186. self.loading = false
  187. // 判断是否首次注册(后端返回 needsRoleSelect 或无 familyId)
  188. var isNewUser = data.needsRoleSelect === true || !data.familyId
  189. // 处理家庭邀请码(仅首次注册时绑定邀请人)
  190. var familyInviteCode = uni.getStorageSync('pendingFamilyInviteCode')
  191. if (familyInviteCode && isNewUser) {
  192. // 新用户注册:绑定家庭邀请人
  193. self.bindFamilyInviter(familyInviteCode).then(function() {
  194. uni.removeStorageSync('pendingFamilyInviteCode')
  195. self.handlePostLoginRouting()
  196. }).catch(function() {
  197. uni.removeStorageSync('pendingFamilyInviteCode')
  198. self.handlePostLoginRouting()
  199. })
  200. return
  201. }
  202. // 非新用户或无家庭邀请码,清理存储并走原有流程
  203. if (familyInviteCode) {
  204. uni.removeStorageSync('pendingFamilyInviteCode')
  205. }
  206. if (data.needsRoleSelect) {
  207. uni.redirectTo({ url: '/pages/user-edit/user-edit?token=' + data.token + '&userId=' + data.userId + '&autoParent=true' })
  208. } else {
  209. self.handlePostLoginRouting()
  210. }
  211. },
  212. // 绑定家庭邀请人(首次注册调用)
  213. bindFamilyInviter: function(code) {
  214. var self = this
  215. return bindFamilyInviter(code).then(function(res) {
  216. if (res && res.code === 200) {
  217. console.log('首次注册绑定家庭邀请人成功', code)
  218. uni.showToast({ title: '已关联邀请家庭', icon: 'success' })
  219. } else if (res && res.code === 400) {
  220. console.log('已有邀请人,跳过绑定')
  221. }
  222. }).catch(function(e) {
  223. console.log('绑定家庭邀请人失败', e)
  224. })
  225. },
  226. handlePostLoginRouting() {
  227. const inviteCode = uni.getStorageSync('inviteCode')
  228. const inviteType = uni.getStorageSync('inviteType')
  229. if (inviteCode && inviteType === 'team') {
  230. this.handleTeamInviteFlow(inviteCode)
  231. } else if (inviteCode) {
  232. // 非 team 邀请码(如分享文章/商品/活动的邀请码),绑定推荐关系
  233. this.handleReferralBind(inviteCode)
  234. } else {
  235. this.navigateToHome()
  236. }
  237. },
  238. async handleReferralBind(inviteCode) {
  239. try {
  240. var res = await bindReferral(inviteCode)
  241. if (res && res.code === 200) {
  242. console.log('登录后绑定邀请码成功', inviteCode)
  243. } else if (res && res.code === 400) {
  244. console.log('用户已有邀请人,跳过绑定')
  245. }
  246. } catch (e) {
  247. console.log('邀请码绑定失败', e)
  248. }
  249. uni.removeStorageSync('inviteCode')
  250. uni.removeStorageSync('inviteType')
  251. this.navigateToHome()
  252. },
  253. async handleTeamInviteFlow(inviteCode) {
  254. try {
  255. const res = await checkTeamInviteStatus(inviteCode)
  256. const status = res.data.status
  257. if (status === 'not_planner') {
  258. uni.redirectTo({ url: `/pages/guide/register/index?inviteCode=${inviteCode}` })
  259. } else if (status === 'not_in_team') {
  260. uni.showModal({
  261. title: '加入团队',
  262. content: '是否加入该规划师团队?',
  263. success: async (confirmRes) => {
  264. if (confirmRes.confirm) {
  265. try {
  266. await joinTeamByInvite(inviteCode)
  267. uni.showToast({ title: '加入成功', icon: 'success' })
  268. uni.removeStorageSync('inviteCode')
  269. uni.removeStorageSync('inviteType')
  270. this.navigateToHome()
  271. } catch (e) {
  272. uni.showToast({ title: e.message || '加入失败', icon: 'none' })
  273. }
  274. } else {
  275. this.navigateToHome()
  276. }
  277. }
  278. })
  279. } else if (status === 'already_in_team') {
  280. uni.showModal({
  281. title: '加入新团队',
  282. content: '您已在其他团队中,离开当前团队并加入新团队?',
  283. success: async (confirmRes) => {
  284. if (confirmRes.confirm) {
  285. try {
  286. await joinTeamByInvite(inviteCode)
  287. uni.showToast({ title: '加入成功', icon: 'success' })
  288. uni.removeStorageSync('inviteCode')
  289. uni.removeStorageSync('inviteType')
  290. this.navigateToHome()
  291. } catch (e) {
  292. uni.showToast({ title: e.message || '加入失败', icon: 'none' })
  293. }
  294. } else {
  295. this.navigateToHome()
  296. }
  297. }
  298. })
  299. } else {
  300. this.navigateToHome()
  301. }
  302. } catch (e) {
  303. uni.showToast({ title: e.message || '处理邀请失败', icon: 'none' })
  304. this.navigateToHome()
  305. }
  306. },
  307. navigateToHome() {
  308. if (this.redirectUrl) {
  309. const redirect = this.redirectUrl
  310. this.redirectUrl = ''
  311. // 注意: redirectTo 不支持跳转 tabBar 页面,必须用 switchTab
  312. // 只有 pages.json tabBar.list 注册的页面才能用 switchTab
  313. var tabBarPages = ['/pages/index-home/index', '/pages/mind/index', '/pages/body/index', '/pages/wisdom/index', '/pages/wealth/index']
  314. if (tabBarPages.indexOf(redirect) !== -1) {
  315. uni.switchTab({ url: redirect })
  316. } else {
  317. uni.redirectTo({ url: redirect })
  318. }
  319. } else {
  320. uni.switchTab({ url: '/pages/index-home/index' })
  321. }
  322. }
  323. }
  324. }
  325. </script>
  326. <style scoped>
  327. .login-container {
  328. min-height: 100vh;
  329. background: linear-gradient(160deg, #FFF7ED 0%, #FED7AA 60%, #F97316 100%);
  330. padding: 48rpx 40rpx 40rpx;
  331. }
  332. /* ===== Logo 区 ===== */
  333. .logo-section {
  334. text-align: center;
  335. margin-bottom: 40rpx;
  336. }
  337. .logo {
  338. width: 120rpx;
  339. height: 120rpx;
  340. border-radius: 60rpx;
  341. background: #fff;
  342. box-shadow: 0 8rpx 32rpx rgba(249,115,22,0.2);
  343. }
  344. .app-name {
  345. display: block;
  346. font-size: 52rpx;
  347. font-weight: bold;
  348. color: #fff;
  349. margin-top: 16rpx;
  350. letter-spacing: 8rpx;
  351. }
  352. .slogan {
  353. display: block;
  354. font-size: 26rpx;
  355. color: rgba(255, 255, 255, 0.85);
  356. margin-top: 8rpx;
  357. }
  358. .powered {
  359. display: block;
  360. font-size: 20rpx;
  361. color: rgba(255, 255, 255, 0.55);
  362. margin-top: 12rpx;
  363. }
  364. /* ===== 角色价值说明 ===== */
  365. .value-section {
  366. margin-bottom: 32rpx;
  367. }
  368. .value-title {
  369. font-size: 28rpx;
  370. font-weight: 600;
  371. color: #fff;
  372. margin-bottom: 20rpx;
  373. padding-left: 4rpx;
  374. }
  375. .role-card {
  376. background: rgba(255, 255, 255, 0.92);
  377. border-radius: 20rpx;
  378. padding: 24rpx 28rpx;
  379. margin-bottom: 16rpx;
  380. box-shadow: 0 4rpx 20rpx rgba(0,0,0,0.08);
  381. }
  382. .role-badge {
  383. display: inline-flex;
  384. align-items: center;
  385. gap: 8rpx;
  386. background: linear-gradient(135deg, #FF8C42, #FF6B2D);
  387. border-radius: 30rpx;
  388. padding: 8rpx 20rpx;
  389. margin-bottom: 20rpx;
  390. }
  391. .role-badge-child {
  392. background: linear-gradient(135deg, #FF6B9D, #FF3D7F);
  393. }
  394. .role-badge-icon {
  395. font-size: 28rpx;
  396. }
  397. .role-badge-text {
  398. font-size: 26rpx;
  399. font-weight: 600;
  400. color: #fff;
  401. }
  402. .role-items {
  403. display: flex;
  404. flex-direction: column;
  405. gap: 14rpx;
  406. }
  407. .role-item {
  408. display: flex;
  409. align-items: center;
  410. gap: 12rpx;
  411. }
  412. .role-item-icon {
  413. font-size: 28rpx;
  414. flex-shrink: 0;
  415. }
  416. .role-item-text {
  417. font-size: 26rpx;
  418. color: #374151;
  419. line-height: 1.5;
  420. }
  421. /* ===== 五维能量 ===== */
  422. .wuxing-section {
  423. background: rgba(255, 255, 255, 0.92);
  424. border-radius: 20rpx;
  425. padding: 28rpx;
  426. margin-bottom: 32rpx;
  427. text-align: center;
  428. box-shadow: 0 4rpx 20rpx rgba(0,0,0,0.08);
  429. }
  430. .wuxing-title {
  431. font-size: 28rpx;
  432. font-weight: 600;
  433. color: #1F2937;
  434. margin-bottom: 20rpx;
  435. }
  436. .wuxing-tags {
  437. display: flex;
  438. flex-wrap: wrap;
  439. justify-content: center;
  440. gap: 12rpx;
  441. margin-bottom: 16rpx;
  442. }
  443. .wuxing-tag {
  444. font-size: 24rpx;
  445. padding: 10rpx 20rpx;
  446. border-radius: 30rpx;
  447. color: #fff;
  448. font-weight: 500;
  449. }
  450. .wuxing-body { background: rgba(255, 140, 66, 0.85); }
  451. .wuxing-mind { background: rgba(255, 107, 157, 0.85); }
  452. .wuxing-wisdom { background: rgba(99, 102, 241, 0.85); }
  453. .wuxing-action { background: rgba(16, 185, 129, 0.85); }
  454. .wuxing-wealth { background: rgba(245, 158, 11, 0.85); }
  455. .wuxing-emoji {
  456. margin-right: 4rpx;
  457. }
  458. .wuxing-desc {
  459. font-size: 22rpx;
  460. color: #9CA3AF;
  461. }
  462. /* ===== 登录表单 ===== */
  463. .login-form {
  464. background: #fff;
  465. border-radius: 20rpx;
  466. padding: 40rpx;
  467. box-shadow: 0 4rpx 24rpx rgba(0,0,0,0.06);
  468. }
  469. .phone-section {
  470. margin-bottom: 28rpx;
  471. }
  472. .phone-btn {
  473. display: flex;
  474. align-items: center;
  475. justify-content: center;
  476. gap: 12rpx;
  477. width: 100%;
  478. height: 88rpx;
  479. background: #07c160;
  480. color: #fff;
  481. border-radius: 44rpx;
  482. font-size: 32rpx;
  483. font-weight: 500;
  484. border: none;
  485. }
  486. .phone-btn::after {
  487. border: none;
  488. }
  489. .phone-btn-icon {
  490. font-size: 36rpx;
  491. }
  492. /* ===== 跳过登录 ===== */
  493. .skip-section {
  494. text-align: center;
  495. margin-bottom: 24rpx;
  496. }
  497. .skip-btn {
  498. font-size: 28rpx;
  499. color: #F97316;
  500. font-weight: 500;
  501. padding: 12rpx 24rpx;
  502. background: rgba(249, 115, 22, 0.08);
  503. border-radius: 30rpx;
  504. }
  505. /* ===== 协议 ===== */
  506. .agreement {
  507. margin-top: 8rpx;
  508. text-align: center;
  509. font-size: 24rpx;
  510. line-height: 1.6;
  511. }
  512. .agreement checkbox {
  513. transform: scale(0.8);
  514. }
  515. .agreement-text {
  516. color: #9CA3AF;
  517. }
  518. .link {
  519. color: #5B9BD5;
  520. text-decoration: underline;
  521. }
  522. </style>