login.nvue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554
  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. // 存储 Token
  209. http.setToken(data.token)
  210. // 存储用户基本信息
  211. uni.setStorageSync('user_info', {
  212. userId: data.userId,
  213. phone: data.phone,
  214. nickname: data.nickname,
  215. avatar: data.avatar,
  216. appId: data.appId
  217. })
  218. // 单独存储各字段(便于页面直接读取)
  219. uni.setStorageSync('phone', data.phone)
  220. uni.setStorageSync('nickname', data.nickname || '')
  221. uni.setStorageSync('avatar', data.avatar || '')
  222. uni.setStorageSync('appId', data.appId || '')
  223. toast.hide()
  224. console.log("登录成功")
  225. // 跳转首页
  226. uni.reLaunch({ url:"/pages/device/list/list" })
  227. } catch (e) {
  228. toast.hide()
  229. toast.error(e.message || '登录失败,请稍后重试')
  230. }
  231. },
  232. // 微信登录(暂未实现)
  233. onWechatLogin() {
  234. if (!this.agreed) {
  235. this.isShowDrawer = true
  236. return
  237. }
  238. toast.text('微信登录暂未开放')
  239. // TODO: 微信登录实现(需绑定手机号)
  240. },
  241. // Apple登录(暂未实现)
  242. onAppleLogin() {
  243. if (!this.agreed) {
  244. this.isShowDrawer = true
  245. return
  246. }
  247. toast.text('Apple登录暂未开放')
  248. // TODO: Apple 登录实现(需绑定手机号)
  249. },
  250. // 切换协议勾选
  251. onToggleAgreement() {
  252. this.agreed = !this.agreed
  253. if (this.agreed) {
  254. uni.setStorageSync('privacy_agreed', true)
  255. } else {
  256. uni.removeStorageSync('privacy_agreed')
  257. }
  258. },
  259. // 打开隐私政策/用户协议弹窗
  260. onOpenPrivacy() {
  261. this.isShowDrawer = true
  262. },
  263. onOpenTerms() {
  264. this.isShowDrawer = true
  265. },
  266. // 弹窗中同意/不同意操作
  267. onAgree(type) {
  268. this.isShowDrawer = false
  269. if (type === 1) {
  270. this.agreed = true
  271. uni.setStorageSync('privacy_agreed', true)
  272. }
  273. },
  274. // 跳转webview查看完整协议
  275. goWebview(id) {
  276. uni.navigateTo({
  277. url: '/pages/webview/webview?id=' + id
  278. })
  279. }
  280. }
  281. }
  282. </script>
  283. <style>
  284. .login-page {
  285. flex: 1;
  286. background-color: #ffffff;
  287. padding-left: 40rpx;
  288. padding-right: 40rpx;
  289. padding-top: 160rpx;
  290. }
  291. /* 顶部欢迎语 */
  292. .header {
  293. margin-bottom: 80rpx;
  294. }
  295. .hello {
  296. font-size: 56rpx;
  297. font-weight: bold;
  298. color: #1a9e78;
  299. margin-bottom: 10rpx;
  300. }
  301. .welcome {
  302. font-size: 40rpx;
  303. font-weight: bold;
  304. color: #1a9e78;
  305. }
  306. /* 表单区域 */
  307. .form-area {
  308. margin-bottom: 60rpx;
  309. }
  310. .input-wrapper {
  311. flex-direction: row;
  312. align-items: center;
  313. background-color: #f5f5f5;
  314. border-radius: 16rpx;
  315. padding-left: 24rpx;
  316. padding-right: 24rpx;
  317. height: 100rpx;
  318. margin-bottom: 24rpx;
  319. }
  320. .input-icon {
  321. width: 40rpx;
  322. height: 40rpx;
  323. margin-right: 16rpx;
  324. }
  325. .input-field {
  326. flex: 1;
  327. font-size: 30rpx;
  328. color: #333333;
  329. height: 100rpx;
  330. }
  331. .placeholder-text {
  332. color: #999999;
  333. font-size: 30rpx;
  334. }
  335. .code-input {
  336. flex: 1;
  337. }
  338. .code-btn {
  339. font-size: 28rpx;
  340. color: #1a9e78;
  341. padding-left: 20rpx;
  342. }
  343. .code-btn-disabled {
  344. color: #cccccc;
  345. }
  346. /* 登录按钮 */
  347. .login-btn {
  348. height: 96rpx;
  349. background-color: #7ecdb8;
  350. border-radius: 48rpx;
  351. align-items: center;
  352. justify-content: center;
  353. margin-top: 20rpx;
  354. }
  355. .login-btn-disabled {
  356. background-color: #c8e6dc;
  357. }
  358. .login-btn-text {
  359. flex: 1;
  360. font-size: 32rpx;
  361. color: #ffffff;
  362. font-weight: bold;
  363. text-align: center;
  364. line-height: 96rpx;
  365. }
  366. /* 其他登录方式 */
  367. .other-login {
  368. flex: 1;
  369. align-items: center;
  370. }
  371. .divider {
  372. flex-direction: row;
  373. align-items: center;
  374. margin-top: 60rpx;
  375. margin-bottom: 50rpx;
  376. }
  377. .divider-line {
  378. width: 140rpx;
  379. height: 1px;
  380. background-color: #e0e0e0;
  381. }
  382. .divider-text {
  383. font-size: 26rpx;
  384. color: #999999;
  385. margin-left: 20rpx;
  386. margin-right: 20rpx;
  387. }
  388. /* 第三方登录按钮 */
  389. .third-party-area {
  390. flex-direction: row;
  391. justify-content: center;
  392. align-items: center;
  393. margin-bottom: 40rpx;
  394. }
  395. .third-party-btn {
  396. width: 100rpx;
  397. height: 100rpx;
  398. border-radius: 50rpx;
  399. background-color: #f5f5f5;
  400. align-items: center;
  401. justify-content: center;
  402. }
  403. .third-party-icon {
  404. width: 56rpx;
  405. height: 56rpx;
  406. }
  407. /* Apple 登录按钮 - 符合苹果HIG规范 */
  408. .apple-section {
  409. align-items: center;
  410. margin-top: 20rpx;
  411. }
  412. .apple-login-btn {
  413. flex-direction: row;
  414. align-items: center;
  415. justify-content: center;
  416. width: 580rpx;
  417. height: 88rpx;
  418. background-color: #000000;
  419. border-radius: 44rpx;
  420. }
  421. .apple-icon {
  422. width: 36rpx;
  423. height: 36rpx;
  424. margin-right: 12rpx;
  425. }
  426. .apple-btn-text {
  427. font-size: 30rpx;
  428. color: #ffffff;
  429. font-weight: 500;
  430. }
  431. /* 底部协议 */
  432. .footer {
  433. position: fixed;
  434. bottom: 60rpx;
  435. left: 40rpx;
  436. right: 40rpx;
  437. }
  438. .agreement-row {
  439. flex-direction: row;
  440. align-items: center;
  441. justify-content: center;
  442. }
  443. .checkbox {
  444. width: 32rpx;
  445. height: 32rpx;
  446. border-radius: 16rpx;
  447. border-width: 2rpx;
  448. border-style: solid;
  449. border-color: #cccccc;
  450. align-items: center;
  451. justify-content: center;
  452. margin-right: 10rpx;
  453. }
  454. .checkbox-checked {
  455. background-color: #1a9e78;
  456. border-color: #1a9e78;
  457. }
  458. .checkbox-icon {
  459. font-size: 20rpx;
  460. color: #ffffff;
  461. }
  462. .agreement-text {
  463. font-size: 24rpx;
  464. color: #999999;
  465. }
  466. .agreement-link {
  467. font-size: 24rpx;
  468. color: #1a9e78;
  469. }
  470. /* 隐私政策弹窗 */
  471. .boxcontent {
  472. padding-left: 10rpx;
  473. padding-right: 10rpx;
  474. }
  475. .rich_header {
  476. align-items: center;
  477. margin-bottom: 30rpx;
  478. }
  479. .rich_title {
  480. font-size: 34rpx;
  481. font-weight: bold;
  482. color: #333333;
  483. }
  484. .privateContent {
  485. height: 500rpx;
  486. }
  487. .contentBox {
  488. padding-left: 10rpx;
  489. padding-right: 10rpx;
  490. }
  491. .content-text {
  492. font-size: 26rpx;
  493. color: #666666;
  494. line-height: 44rpx;
  495. }
  496. .linebox {
  497. flex-direction: row;
  498. flex-wrap: wrap;
  499. align-items: center;
  500. }
  501. .link-text {
  502. font-size: 26rpx;
  503. color: #1a9e78;
  504. line-height: 44rpx;
  505. }
  506. .rich_btn {
  507. flex-direction: row;
  508. justify-content: space-between;
  509. margin-top: 30rpx;
  510. padding-left: 20rpx;
  511. padding-right: 20rpx;
  512. }
  513. .btn-disagree {
  514. flex: 1;
  515. height: 80rpx;
  516. line-height: 80rpx;
  517. text-align: center;
  518. font-size: 30rpx;
  519. color: #999999;
  520. background-color: #f5f5f5;
  521. border-radius: 40rpx;
  522. margin-right: 20rpx;
  523. }
  524. .btn-agree {
  525. flex: 1;
  526. height: 80rpx;
  527. line-height: 80rpx;
  528. text-align: center;
  529. font-size: 30rpx;
  530. color: #ffffff;
  531. background-color: #1a9e78;
  532. border-radius: 40rpx;
  533. margin-left: 20rpx;
  534. }
  535. </style>