login.nvue 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555
  1. <template>
  2. <view class="login-page">
  3. <!-- 顶部欢迎语 -->
  4. <view class="header">
  5. <text class="hello">HELLO,</text>
  6. <text class="welcome">欢迎使用研年健康平台</text>
  7. </view>
  8. <!-- 表单区域 -->
  9. <view class="form-area">
  10. <!-- 手机号输入 -->
  11. <view class="input-wrapper">
  12. <image class="input-icon" src="/static/login/phone.png" />
  13. <input
  14. class="input-field"
  15. type="number"
  16. maxlength="11"
  17. placeholder="请输入手机号"
  18. placeholder-class="placeholder-text"
  19. v-model="phone"
  20. />
  21. </view>
  22. <!-- 验证码输入 -->
  23. <view class="input-wrapper">
  24. <image class="input-icon" src="/static/login/pwd.png" />
  25. <input
  26. class="input-field code-input"
  27. type="number"
  28. maxlength="6"
  29. placeholder="请输入验证码"
  30. placeholder-class="placeholder-text"
  31. v-model="code"
  32. />
  33. <text
  34. class="code-btn"
  35. :class="[codeBtnDisabled ? 'code-btn-disabled' : '']"
  36. @click="onGetCode"
  37. >{{ countdown > 0 ? countdown + 's后重发' : '获取验证码' }}</text>
  38. </view>
  39. <!-- 登录按钮 -->
  40. <view
  41. class="login-btn"
  42. :class="[!canLogin ? 'login-btn-disabled' : '']"
  43. @click="onLogin"
  44. >
  45. <text class="login-btn-text" @click="onLogin">登录</text>
  46. </view>
  47. </view>
  48. <!-- 其他登录方式 -->
  49. <view class="other-login">
  50. <view class="divider">
  51. <view class="divider-line"></view>
  52. <text class="divider-text">其他登录方式</text>
  53. <view class="divider-line"></view>
  54. </view>
  55. <view class="third-party-area">
  56. <!-- 微信登录 -->
  57. <view class="third-party-btn" @click="onWechatLogin">
  58. <image class="third-party-icon" src="/static/login/wechat@2x.png" />
  59. </view>
  60. </view>
  61. <!-- Apple 登录 - 仅iOS显示,符合苹果审核规范 -->
  62. <view class="apple-section" >
  63. <view class="apple-login-btn" @click="onAppleLogin">
  64. <image class="apple-icon" src="/static/login/apple@2x.png" />
  65. <text class="apple-btn-text">通过 Apple 登录</text>
  66. </view>
  67. </view>
  68. </view>
  69. <!-- 底部协议 -->
  70. <view class="footer">
  71. <view class="agreement-row" @click="onToggleAgreement">
  72. <view class="checkbox" :class="[agreed ? 'checkbox-checked' : '']">
  73. <text class="checkbox-icon" v-if="agreed">✓</text>
  74. </view>
  75. <text class="agreement-text">登录即代表同意</text>
  76. <text class="agreement-link" @click.stop="onOpenPrivacy">隐私政策</text>
  77. <text class="agreement-text">和</text>
  78. <text class="agreement-link" @click.stop="onOpenTerms">用户协议</text>
  79. </view>
  80. </view>
  81. <!-- 隐私政策弹窗 -->
  82. <customPopup :isShow="isShowDrawer" @closePop="isShowDrawer=false">
  83. <view class="boxcontent">
  84. <view class="rich_header">
  85. <text class="rich_title">服务协议与隐私政策</text>
  86. </view>
  87. <scroll-view class="privateContent" scroll-y="true">
  88. <view class="contentBox">
  89. <text class="content-text">感谢您信任并使用研年健康平台!我们深知保护个人隐私信息的重要性,请在使用研年健康平台的服务前,认真阅读我们的</text>
  90. <view class="linebox">
  91. <text class="link-text" @click="goWebview(1)">《用户协议》</text>
  92. <text class="content-text">和</text>
  93. <text class="link-text" @click="goWebview(2)">《隐私政策》</text>
  94. </view>
  95. <text class="content-text">,以帮助您了解我们在收集、使用、存储和共享您个人信息的情况以及您享有的相关权利。</text>
  96. <text class="content-text"> 请您仔细阅读并作出适当的选择:</text>
  97. <text class="content-text"> 1.《用户协议》的主要内容包括:协议适用范围、服务内容及形式、软件使用及许可、终端责任安全、用户权利与义务、用户行为规范、知识产权声明、用户信息保护。</text>
  98. <text class="content-text"> 2.关于《隐私政策》,我们特别向您说明:</text>
  99. <text class="content-text">(1)根据合法、正当、必要的原则,我们会收集实现产品功能所必要的信息,包括:您在注册帐号时所填写的信息(昵称、手机号码);您的日志信息及设备信息、设备型号;当您选择使用部分服务时,我们将在征求您的同意后获取相关权限。</text>
  100. </view>
  101. </scroll-view>
  102. <view class="rich_btn">
  103. <text class="btn-disagree" @click="onAgree(0)">不同意</text>
  104. <text class="btn-agree" @click="onAgree(1)">同意</text>
  105. </view>
  106. </view>
  107. </customPopup>
  108. <ay-toast ref="ayToast" />
  109. </view>
  110. </template>
  111. <script>
  112. import customPopup from '@/components/customPopup/customPopup.nvue'
  113. import ayToast from '@/components/ay-toast/ay-toast.nvue'
  114. import toast from '@/utils/toast.js'
  115. import http from '@/utils/http'
  116. import { sendCode, login } from '@/utils/api/login.js'
  117. export default {
  118. components: {
  119. customPopup,
  120. ayToast
  121. },
  122. data() {
  123. return {
  124. phone: '',
  125. code: '',
  126. countdown: 0,
  127. agreed: false,
  128. isIOS: false,
  129. countdownTimer: null,
  130. isShowDrawer: false
  131. }
  132. },
  133. computed: {
  134. isPhoneValid() {
  135. return /^1[3-9]\d{9}$/.test(this.phone)
  136. },
  137. codeBtnDisabled() {
  138. return this.countdown > 0 || !this.isPhoneValid
  139. },
  140. canLogin() {
  141. return this.isPhoneValid && this.code.length >= 4 && this.agreed
  142. }
  143. },
  144. watch: {
  145. isShowDrawer(val) {
  146. if (val) {
  147. uni.hideKeyboard()
  148. }
  149. }
  150. },
  151. onLoad() {
  152. // 判断平台
  153. const sysInfo = uni.getSystemInfoSync()
  154. this.isIOS = sysInfo.platform === 'ios'
  155. // 读取持久化的隐私政策同意状态
  156. const agreedFlag = uni.getStorageSync('privacy_agreed')
  157. if (agreedFlag) {
  158. this.agreed = true
  159. }
  160. },
  161. onUnload() {
  162. if (this.countdownTimer) {
  163. clearInterval(this.countdownTimer)
  164. this.countdownTimer = null
  165. }
  166. },
  167. methods: {
  168. // 获取验证码
  169. async onGetCode() {
  170. if (this.codeBtnDisabled) return
  171. if (!this.isPhoneValid) {
  172. toast.error('请输入正确的手机号')
  173. return
  174. }
  175. try {
  176. await sendCode(this.phone)
  177. // 发送成功后开始倒计时
  178. this.countdown = 60
  179. this.countdownTimer = setInterval(() => {
  180. this.countdown--
  181. if (this.countdown <= 0) {
  182. clearInterval(this.countdownTimer)
  183. this.countdownTimer = null
  184. }
  185. }, 1000)
  186. toast.success('验证码已发送')
  187. } catch (e) {
  188. // 错误已由 http 层自动 toast 展示
  189. }
  190. },
  191. // 登录
  192. async onLogin() {
  193. if (!this.agreed) {
  194. this.isShowDrawer = true
  195. return
  196. }
  197. if (!this.isPhoneValid) {
  198. toast.error('请输入正确的手机号')
  199. return
  200. }
  201. if (this.code.length < 4) {
  202. toast.error('请输入验证码')
  203. return
  204. }
  205. toast.loading('登录中...')
  206. try {
  207. const data = await login(this.phone, this.code)
  208. console.log("获取到的token",data.token)
  209. // 存储 Token
  210. http.setToken(data.token)
  211. // 存储用户基本信息
  212. uni.setStorageSync('user_info', {
  213. userId: data.userId,
  214. phone: data.phone,
  215. nickname: data.nickname,
  216. avatar: data.avatar,
  217. appId: data.appId
  218. })
  219. // 单独存储各字段(便于页面直接读取)
  220. uni.setStorageSync('phone', data.phone)
  221. uni.setStorageSync('nickname', data.nickname || '')
  222. uni.setStorageSync('avatar', data.avatar || '')
  223. uni.setStorageSync('appId', data.appId || '')
  224. toast.hide()
  225. console.log("登录成功")
  226. // 跳转首页
  227. uni.reLaunch({ url:"/pages/device/list/list" })
  228. } catch (e) {
  229. toast.hide()
  230. toast.error(e.message || '登录失败,请稍后重试')
  231. }
  232. },
  233. // 微信登录(暂未实现)
  234. onWechatLogin() {
  235. if (!this.agreed) {
  236. this.isShowDrawer = true
  237. return
  238. }
  239. toast.text('微信登录暂未开放')
  240. // TODO: 微信登录实现(需绑定手机号)
  241. },
  242. // Apple登录(暂未实现)
  243. onAppleLogin() {
  244. if (!this.agreed) {
  245. this.isShowDrawer = true
  246. return
  247. }
  248. toast.text('Apple登录暂未开放')
  249. // TODO: Apple 登录实现(需绑定手机号)
  250. },
  251. // 切换协议勾选
  252. onToggleAgreement() {
  253. this.agreed = !this.agreed
  254. if (this.agreed) {
  255. uni.setStorageSync('privacy_agreed', true)
  256. } else {
  257. uni.removeStorageSync('privacy_agreed')
  258. }
  259. },
  260. // 打开隐私政策/用户协议弹窗
  261. onOpenPrivacy() {
  262. this.isShowDrawer = true
  263. },
  264. onOpenTerms() {
  265. this.isShowDrawer = true
  266. },
  267. // 弹窗中同意/不同意操作
  268. onAgree(type) {
  269. this.isShowDrawer = false
  270. if (type === 1) {
  271. this.agreed = true
  272. uni.setStorageSync('privacy_agreed', true)
  273. }
  274. },
  275. // 跳转webview查看完整协议
  276. goWebview(id) {
  277. uni.navigateTo({
  278. url: '/pages/webview/webview?id=' + id
  279. })
  280. }
  281. }
  282. }
  283. </script>
  284. <style>
  285. .login-page {
  286. flex: 1;
  287. background-color: #ffffff;
  288. padding-left: 40rpx;
  289. padding-right: 40rpx;
  290. padding-top: 160rpx;
  291. }
  292. /* 顶部欢迎语 */
  293. .header {
  294. margin-bottom: 80rpx;
  295. }
  296. .hello {
  297. font-size: 56rpx;
  298. font-weight: bold;
  299. color: #1a9e78;
  300. margin-bottom: 10rpx;
  301. }
  302. .welcome {
  303. font-size: 40rpx;
  304. font-weight: bold;
  305. color: #1a9e78;
  306. }
  307. /* 表单区域 */
  308. .form-area {
  309. margin-bottom: 60rpx;
  310. }
  311. .input-wrapper {
  312. flex-direction: row;
  313. align-items: center;
  314. background-color: #f5f5f5;
  315. border-radius: 16rpx;
  316. padding-left: 24rpx;
  317. padding-right: 24rpx;
  318. height: 100rpx;
  319. margin-bottom: 24rpx;
  320. }
  321. .input-icon {
  322. width: 40rpx;
  323. height: 40rpx;
  324. margin-right: 16rpx;
  325. }
  326. .input-field {
  327. flex: 1;
  328. font-size: 30rpx;
  329. color: #333333;
  330. height: 100rpx;
  331. }
  332. .placeholder-text {
  333. color: #999999;
  334. font-size: 30rpx;
  335. }
  336. .code-input {
  337. flex: 1;
  338. }
  339. .code-btn {
  340. font-size: 28rpx;
  341. color: #1a9e78;
  342. padding-left: 20rpx;
  343. }
  344. .code-btn-disabled {
  345. color: #cccccc;
  346. }
  347. /* 登录按钮 */
  348. .login-btn {
  349. height: 96rpx;
  350. background-color: #7ecdb8;
  351. border-radius: 48rpx;
  352. align-items: center;
  353. justify-content: center;
  354. margin-top: 20rpx;
  355. }
  356. .login-btn-disabled {
  357. background-color: #c8e6dc;
  358. }
  359. .login-btn-text {
  360. flex: 1;
  361. font-size: 32rpx;
  362. color: #ffffff;
  363. font-weight: bold;
  364. text-align: center;
  365. line-height: 96rpx;
  366. }
  367. /* 其他登录方式 */
  368. .other-login {
  369. flex: 1;
  370. align-items: center;
  371. }
  372. .divider {
  373. flex-direction: row;
  374. align-items: center;
  375. margin-top: 60rpx;
  376. margin-bottom: 50rpx;
  377. }
  378. .divider-line {
  379. width: 140rpx;
  380. height: 1px;
  381. background-color: #e0e0e0;
  382. }
  383. .divider-text {
  384. font-size: 26rpx;
  385. color: #999999;
  386. margin-left: 20rpx;
  387. margin-right: 20rpx;
  388. }
  389. /* 第三方登录按钮 */
  390. .third-party-area {
  391. flex-direction: row;
  392. justify-content: center;
  393. align-items: center;
  394. margin-bottom: 40rpx;
  395. }
  396. .third-party-btn {
  397. width: 100rpx;
  398. height: 100rpx;
  399. border-radius: 50rpx;
  400. background-color: #f5f5f5;
  401. align-items: center;
  402. justify-content: center;
  403. }
  404. .third-party-icon {
  405. width: 56rpx;
  406. height: 56rpx;
  407. }
  408. /* Apple 登录按钮 - 符合苹果HIG规范 */
  409. .apple-section {
  410. align-items: center;
  411. margin-top: 20rpx;
  412. }
  413. .apple-login-btn {
  414. flex-direction: row;
  415. align-items: center;
  416. justify-content: center;
  417. width: 580rpx;
  418. height: 88rpx;
  419. background-color: #000000;
  420. border-radius: 44rpx;
  421. }
  422. .apple-icon {
  423. width: 36rpx;
  424. height: 36rpx;
  425. margin-right: 12rpx;
  426. }
  427. .apple-btn-text {
  428. font-size: 30rpx;
  429. color: #ffffff;
  430. font-weight: 500;
  431. }
  432. /* 底部协议 */
  433. .footer {
  434. position: fixed;
  435. bottom: 60rpx;
  436. left: 40rpx;
  437. right: 40rpx;
  438. }
  439. .agreement-row {
  440. flex-direction: row;
  441. align-items: center;
  442. justify-content: center;
  443. }
  444. .checkbox {
  445. width: 32rpx;
  446. height: 32rpx;
  447. border-radius: 16rpx;
  448. border-width: 2rpx;
  449. border-style: solid;
  450. border-color: #cccccc;
  451. align-items: center;
  452. justify-content: center;
  453. margin-right: 10rpx;
  454. }
  455. .checkbox-checked {
  456. background-color: #1a9e78;
  457. border-color: #1a9e78;
  458. }
  459. .checkbox-icon {
  460. font-size: 20rpx;
  461. color: #ffffff;
  462. }
  463. .agreement-text {
  464. font-size: 24rpx;
  465. color: #999999;
  466. }
  467. .agreement-link {
  468. font-size: 24rpx;
  469. color: #1a9e78;
  470. }
  471. /* 隐私政策弹窗 */
  472. .boxcontent {
  473. padding-left: 10rpx;
  474. padding-right: 10rpx;
  475. }
  476. .rich_header {
  477. align-items: center;
  478. margin-bottom: 30rpx;
  479. }
  480. .rich_title {
  481. font-size: 34rpx;
  482. font-weight: bold;
  483. color: #333333;
  484. }
  485. .privateContent {
  486. height: 500rpx;
  487. }
  488. .contentBox {
  489. padding-left: 10rpx;
  490. padding-right: 10rpx;
  491. }
  492. .content-text {
  493. font-size: 26rpx;
  494. color: #666666;
  495. line-height: 44rpx;
  496. }
  497. .linebox {
  498. flex-direction: row;
  499. flex-wrap: wrap;
  500. align-items: center;
  501. }
  502. .link-text {
  503. font-size: 26rpx;
  504. color: #1a9e78;
  505. line-height: 44rpx;
  506. }
  507. .rich_btn {
  508. flex-direction: row;
  509. justify-content: space-between;
  510. margin-top: 30rpx;
  511. padding-left: 20rpx;
  512. padding-right: 20rpx;
  513. }
  514. .btn-disagree {
  515. flex: 1;
  516. height: 80rpx;
  517. line-height: 80rpx;
  518. text-align: center;
  519. font-size: 30rpx;
  520. color: #999999;
  521. background-color: #f5f5f5;
  522. border-radius: 40rpx;
  523. margin-right: 20rpx;
  524. }
  525. .btn-agree {
  526. flex: 1;
  527. height: 80rpx;
  528. line-height: 80rpx;
  529. text-align: center;
  530. font-size: 30rpx;
  531. color: #ffffff;
  532. background-color: #1a9e78;
  533. border-radius: 40rpx;
  534. margin-left: 20rpx;
  535. }
  536. </style>