login.vue 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608
  1. <template>
  2. <view class="login-bg">
  3. <view class="login-box">
  4. <view class="login-title">欢迎登录</view>
  5. <view class="login-tabs">
  6. <view :class="['tab', loginType === 'phone' ? 'active' : '']" @click="loginType = 'phone'">手机号登录</view>
  7. <view :class="['tab', loginType === 'account' ? 'active' : '']" @click="loginType = 'account'">账号密码登录</view>
  8. </view>
  9. <view v-if="loginType === 'phone'" class="login-form">
  10. <input class="login-input" type="text" placeholder="请输入手机号" v-model="phone" />
  11. <view class="code-row">
  12. <input class="login-input code-input" type="text" placeholder="请输入验证码" v-model="code" />
  13. <button class="code-btn" @click="sendCode" :disabled="loginCodeTimer > 0 || isSendingLoginCode">
  14. {{ loginCodeTimer > 0 ? loginCodeTimer + 's' : '获取验证码' }}
  15. </button>
  16. </view>
  17. </view>
  18. <view v-else class="login-form">
  19. <input class="login-input" type="text" placeholder="请输入账号" v-model="username" />
  20. <input class="login-input" type="password" password placeholder="请输入密码" v-model="password" />
  21. <view class="form-actions">
  22. <text class="login-link" @click="toRegister">去注册</text>
  23. <text class="forgot-password-link" @click="toForgot">忘记密码?</text>
  24. </view>
  25. </view>
  26. <button class="login-btn" :disabled="!agreed" @click="doLogin">登录</button>
  27. <view class="login-agree">
  28. <checkbox :checked="agreed" @click="agreed = !agreed" />
  29. <text>我已阅读并同意</text>
  30. <text class="link" @click="openAgreement">《服务协议》</text>
  31. <text>和</text>
  32. <text class="link" @click="openPrivacy">《隐私政策》</text>
  33. </view>
  34. <view class="login-divider">第三方账号登录</view>
  35. <button class="wechat-btn" @click="oneClickLogin">
  36. <image src="/static/wechat.png" class="icon" /> 一键登录
  37. </button>
  38. </view>
  39. </view>
  40. <!-- 注册弹窗 -->
  41. <view v-if="showRegisterPopup" class="popup-overlay">
  42. <view class="register-popup-content">
  43. <view class="popup-title">注册账号</view>
  44. <view class="register-form">
  45. <input class="login-input" type="text" placeholder="请输入手机号" v-model="regPhone" />
  46. <view class="code-row">
  47. <input class="login-input code-input" type="text" placeholder="请输入验证码" v-model="regCode" />
  48. <button class="code-btn" @click="sendRegCode" :disabled="regCodeTimer > 0 || isSendingRegCode">
  49. {{ regCodeTimer > 0 ? regCodeTimer + 's' : '获取验证码' }}
  50. </button>
  51. </view>
  52. <input class="login-input" type="text" placeholder="请输入账号" v-model="regUsername" />
  53. <input class="login-input" type="password" password placeholder="请输入密码" v-model="regPassword" />
  54. </view>
  55. <button class="login-btn" @click="doRegister">注册</button>
  56. <view class="close-btn" @click="showRegisterPopup = false">
  57. <image src="/static/cw.png" class="icon"></image>
  58. </view>
  59. </view>
  60. </view>
  61. </template>
  62. <script>
  63. export default {
  64. data() {
  65. return {
  66. loginType: 'phone', // 'phone' or 'account'
  67. phone: '',
  68. code: '',
  69. username: '',
  70. password: '',
  71. agreed: false,
  72. showRegisterPopup: false,
  73. regPhone: '',
  74. regCode: '',
  75. regUsername: '',
  76. regPassword: '',
  77. regCodeTimer: 0, // Countdown timer for registration code
  78. isSendingRegCode: false, // Flag to prevent multiple requests
  79. loginCodeTimer: 0, // Countdown timer for login code
  80. isSendingLoginCode: false, // Flag to prevent multiple requests
  81. }
  82. },
  83. methods: {
  84. // 处理微信登录
  85. async oneClickLogin() {
  86. console.log('WeChat login button clicked');
  87. // 1. 先获取用户信息
  88. uni.getUserProfile({
  89. desc: '用于完善用户资料',
  90. lang: 'zh_CN',
  91. success: (userRes) => {
  92. console.log(userRes)
  93. // 2. 获取微信登录凭证
  94. uni.showLoading({ title: '登录中...', mask: true })
  95. uni.login({
  96. provider: 'weixin',
  97. success: wx_res => {
  98. // 3. 发送登录请求,参数按后端WeLogin类
  99. console.log('uni.getUserProfile success:', userRes);
  100. console.log('uni.login success, got code:', wx_res.code);
  101. uni.request({
  102. url: 'http://localhost:3333/WeChart/login',
  103. method: 'POST',
  104. data: {
  105. code: wx_res.code,
  106. weChatLoginDto: userRes.userInfo,
  107. },
  108. header: { 'content-type': 'application/json' },
  109. success: (res) => {
  110. uni.hideLoading()
  111. if (res.statusCode === 200 && res.data) {
  112. console.log('Backend login success:', res.data);
  113. uni.showToast({ title: '登录成功', icon: 'success' })
  114. setTimeout(() => {
  115. uni.switchTab({ url: '/pages/home/index' })
  116. }, 1500)
  117. } else {
  118. console.error('Backend login failed:', res);
  119. uni.showToast({ title: res.data?.message || '登录失败', icon: 'none' })
  120. }
  121. },
  122. fail: (err) => {
  123. uni.hideLoading()
  124. console.error('Backend request failed:', err);
  125. uni.showToast({ title: '微信登录失败', icon: 'none' })
  126. }
  127. })
  128. },
  129. fail: (err) => {
  130. uni.hideLoading()
  131. console.error('uni.login failed:', err);
  132. uni.showToast({ title: '微信登录失败', icon: 'none' })
  133. }
  134. })
  135. },
  136. fail: (err) => {
  137. console.error('uni.getUserProfile failed:', err);
  138. uni.showToast({ title: '获取用户信息失败', icon: 'none' })
  139. }
  140. })
  141. },
  142. async sendCode() {
  143. if (this.isSendingLoginCode || this.loginCodeTimer > 0) {
  144. return;
  145. }
  146. if (!this.phone) {
  147. uni.showToast({ title: '请输入手机号', icon: 'none' });
  148. return;
  149. }
  150. this.isSendingLoginCode = true;
  151. uni.showLoading({ title: '发送中...', mask: true });
  152. try {
  153. const res = await uni.request({
  154. url: 'http://localhost:3333/user/code',
  155. method: 'POST',
  156. data: {
  157. phone: this.phone
  158. },
  159. header: { 'content-type': 'application/json' }
  160. });
  161. uni.hideLoading();
  162. this.isSendingLoginCode = false;
  163. if (res.statusCode === 200 && res.data) {
  164. console.log('Send login code success:', res.data);
  165. uni.showToast({ title: '验证码已发送', icon: 'success' });
  166. // Start countdown
  167. this.loginCodeTimer = 60;
  168. const timerInterval = setInterval(() => {
  169. this.loginCodeTimer--;
  170. if (this.loginCodeTimer <= 0) {
  171. clearInterval(timerInterval);
  172. this.loginCodeTimer = 0;
  173. }
  174. }, 1000);
  175. } else {
  176. console.error('Send login code failed:', res);
  177. uni.showToast({ title: res.data?.message || '发送失败', icon: 'none' });
  178. }
  179. } catch (err) {
  180. uni.hideLoading();
  181. this.isSendingLoginCode = false;
  182. console.error('Send login code request failed:', err);
  183. uni.showToast({ title: '发送失败', icon: 'none' });
  184. }
  185. },
  186. async doLogin() {
  187. if (!this.agreed) {
  188. uni.showToast({ title: '请先同意协议', icon: 'none' });
  189. return;
  190. }
  191. if (!this.phone || !this.code) {
  192. uni.showToast({ title: '请填写手机号和验证码', icon: 'none' });
  193. return;
  194. }
  195. uni.showLoading({ title: '登录中...', mask: true });
  196. try {
  197. const res = await uni.request({
  198. url: 'http://localhost:3333/user/login',
  199. method: 'POST',
  200. data: {
  201. phone: this.phone,
  202. code: this.code
  203. },
  204. header: { 'content-type': 'application/json' }
  205. });
  206. uni.hideLoading();
  207. if (res.statusCode === 200 && res.data) {
  208. console.log('Login success:', res.data);
  209. uni.showToast({ title: '登录成功', icon: 'success' });
  210. // Navigate to home page on success
  211. setTimeout(() => {
  212. uni.switchTab({ url: '/pages/home/index' });
  213. }, 1500); // Delay slightly to show toast
  214. } else {
  215. console.error('Login failed:', res.data);
  216. uni.showToast({ title: res.data?.message || '登录失败', icon: 'error' });
  217. }
  218. } catch (err) {
  219. uni.hideLoading();
  220. console.error('Login request failed:', err);
  221. uni.showToast({ title: '登录失败', icon: 'none' });
  222. }
  223. },
  224. toRegister() {
  225. this.showRegisterPopup = true;
  226. },
  227. toForgot() {
  228. uni.showToast({ title: '跳转找回密码', icon: 'none' });
  229. },
  230. openAgreement() {
  231. uni.navigateTo({ url: '/pages/agreement/agreement' });
  232. },
  233. openPrivacy() {
  234. uni.navigateTo({ url: '/pages/privacy/privacy' });
  235. },
  236. qqLogin() {
  237. uni.showToast({ title: 'QQ登录', icon: 'none' });
  238. },
  239. async sendRegCode() {
  240. // 如果正在发送或倒计时中,则不执行任何操作
  241. if (this.isSendingRegCode || this.regCodeTimer > 0) {
  242. return;
  243. }
  244. // 检查手机号是否已填写
  245. if (!this.regPhone) {
  246. uni.showToast({ title: '请输入手机号', icon: 'none' });
  247. return;
  248. }
  249. // 设置状态,显示加载提示
  250. this.isSendingRegCode = true;
  251. uni.showLoading({ title: '发送中...', mask: true });
  252. try {
  253. // 发起POST请求到后端接口
  254. const res = await uni.request({
  255. url: 'http://localhost:3333/user/code',
  256. method: 'POST',
  257. data: {
  258. phone: this.regPhone // 发送手机号参数
  259. },
  260. header: { 'content-type': 'application/json' }
  261. });
  262. // 隐藏加载提示,重置发送状态
  263. uni.hideLoading();
  264. this.isSendingRegCode = false;
  265. // 根据后端响应处理结果
  266. if (res.statusCode === 200 && res.data) {
  267. console.log('Send registration code success:', res.data);
  268. uni.showToast({ title: '验证码已发送', icon: 'success' });
  269. // 开始60秒倒计时
  270. this.regCodeTimer = 60;
  271. const timerInterval = setInterval(() => {
  272. this.regCodeTimer--;
  273. if (this.regCodeTimer <= 0) {
  274. clearInterval(timerInterval); // 倒计时结束时清除定时器
  275. this.regCodeTimer = 0;
  276. }
  277. }, 1000); // 每秒更新
  278. } else {
  279. // 处理发送失败的情况
  280. console.error('Send registration code failed:', res);
  281. uni.showToast({ title: res.data?.message || '发送失败', icon: 'none' });
  282. }
  283. } catch (err) {
  284. // 处理请求异常
  285. uni.hideLoading();
  286. this.isSendingRegCode = false;
  287. console.error('Send registration code request failed:', err);
  288. uni.showToast({ title: '发送失败', icon: 'none' });
  289. }
  290. },
  291. async doRegister() {
  292. if (!this.regPhone || !this.regCode || !this.regUsername || !this.regPassword) {
  293. uni.showToast({ title: '请填写所有注册信息', icon: 'none' });
  294. return;
  295. }
  296. uni.showLoading({ title: '注册中...', mask: true });
  297. try {
  298. const res = await uni.request({
  299. url: 'http://localhost:3333/user/register',
  300. method: 'POST',
  301. data: {
  302. phone: this.regPhone,
  303. code: this.regCode,
  304. username: this.regUsername,
  305. password: this.regPassword
  306. },
  307. header: { 'content-type': 'application/json' }
  308. });
  309. uni.hideLoading();
  310. if (res.statusCode === 200 && res.data) {
  311. console.log('Registration success:', res.data);
  312. uni.showToast({ title: '注册成功', icon: 'success' });
  313. // Optionally, clear the form fields
  314. this.regPhone = '';
  315. this.regCode = '';
  316. this.regUsername = '';
  317. this.regPassword = '';
  318. // Close the popup after successful registration
  319. setTimeout(() => {
  320. this.showRegisterPopup = false;
  321. }, 1500); // Delay closing slightly to show toast
  322. } else {
  323. console.error('Registration failed:', res);
  324. uni.showToast({ title: res.data?.message || '注册失败', icon: 'none' });
  325. }
  326. } catch (err) {
  327. uni.hideLoading();
  328. console.error('Registration request failed:', err);
  329. uni.showToast({ title: '注册失败', icon: 'none' });
  330. }
  331. }
  332. }
  333. }
  334. </script>
  335. <style>
  336. .login-bg {
  337. min-height: 100vh;
  338. background: #f7f8fa;
  339. display: flex;
  340. align-items: center;
  341. justify-content: center;
  342. }
  343. .login-box {
  344. width: 90vw;
  345. max-width: 600rpx;
  346. background: #fff;
  347. border-radius: 24rpx;
  348. box-shadow: 0 8rpx 32rpx rgba(0,0,0,0.08);
  349. padding: 60rpx 40rpx 40rpx 40rpx;
  350. display: flex;
  351. flex-direction: column;
  352. align-items: center;
  353. }
  354. .login-title {
  355. font-size: 40rpx;
  356. font-weight: bold;
  357. margin-bottom: 40rpx;
  358. }
  359. .login-tabs {
  360. display: flex;
  361. width: 100%;
  362. margin-bottom: 30rpx;
  363. }
  364. .tab {
  365. flex: 1;
  366. text-align: center;
  367. font-size: 28rpx;
  368. padding: 16rpx 0;
  369. color: #888;
  370. border-bottom: 4rpx solid transparent;
  371. }
  372. .tab.active {
  373. color: #7ac81e;
  374. border-bottom: 4rpx solid #7ac81e;
  375. font-weight: bold;
  376. }
  377. .login-form {
  378. width: 100%;
  379. margin-bottom: 20rpx;
  380. }
  381. .login-input {
  382. width: 90%;
  383. height: 80rpx;
  384. border: 1rpx solid #eee;
  385. border-radius: 12rpx;
  386. margin-bottom: 20rpx;
  387. padding: 0 24rpx;
  388. font-size: 28rpx;
  389. background: #f7f8fa;
  390. }
  391. .code-row {
  392. display: flex;
  393. align-items: center;
  394. }
  395. .code-input {
  396. flex: 1;
  397. margin-right: 16rpx;
  398. }
  399. .code-btn {
  400. height: 80rpx;
  401. background: #7ac81e;
  402. color: #fff;
  403. border-radius: 12rpx;
  404. padding: 0 24rpx;
  405. font-size: 28rpx;
  406. }
  407. .login-btn {
  408. width: 100%;
  409. height: 80rpx;
  410. background: #7ac81e;
  411. color: #fff;
  412. font-size: 32rpx;
  413. border-radius: 40rpx;
  414. margin-bottom: 20rpx;
  415. }
  416. .login-btn:disabled {
  417. background: #b2e59e;
  418. }
  419. .login-agree {
  420. display: flex;
  421. align-items: center;
  422. font-size: 24rpx;
  423. color: #888;
  424. margin-bottom: 30rpx;
  425. }
  426. .login-agree .link {
  427. color: #7ac81e;
  428. margin: 0 4rpx;
  429. }
  430. .login-divider {
  431. width: 100%;
  432. text-align: center;
  433. color: #bbb;
  434. font-size: 24rpx;
  435. margin: 30rpx 0 20rpx 0;
  436. }
  437. .wechat-btn, .qq-btn {
  438. width: 100%;
  439. height: 70rpx;
  440. border-radius: 35rpx;
  441. font-size: 28rpx;
  442. display: flex;
  443. align-items: center;
  444. justify-content: center;
  445. margin-bottom: 16rpx;
  446. border: 1rpx solid #eee;
  447. background: #f7f8fa;
  448. }
  449. .wechat-btn {
  450. color: #09bb07;
  451. border: 1rpx solid #09bb07;
  452. }
  453. .qq-btn {
  454. color: #498ff6;
  455. border: 1rpx solid #498ff6;
  456. }
  457. .icon {
  458. width: 40rpx;
  459. height: 40rpx;
  460. margin-right: 16rpx;
  461. }
  462. .popup-overlay {
  463. position: fixed;
  464. top: 0;
  465. left: 0;
  466. right: 0;
  467. bottom: 0;
  468. background-color: rgba(0, 0, 0, 0.6); /* 半透明背景 */
  469. display: flex;
  470. align-items: center;
  471. justify-content: center;
  472. z-index: 999; /* 确保在最上层 */
  473. }
  474. .register-popup-content {
  475. background-color: #fff;
  476. border-radius: 24rpx;
  477. width: 85vw; /* 弹窗宽度 */
  478. max-width: 600rpx;
  479. padding: 60rpx 40rpx;
  480. position: relative; /* 用于定位关闭按钮 */
  481. display: flex;
  482. flex-direction: column;
  483. align-items: center;
  484. }
  485. .register-popup-content .popup-title {
  486. font-size: 40rpx;
  487. font-weight: bold;
  488. margin-bottom: 40rpx;
  489. color: #333;
  490. }
  491. .register-form {
  492. width: 100%;
  493. margin-bottom: 30rpx;
  494. }
  495. /* 注册表单的输入框样式,可以复用登录的 login-input */
  496. .register-form .login-input {
  497. width: 90%; /* 输入框宽度调整为100% */
  498. height: 80rpx;
  499. border: 1rpx solid #eee;
  500. border-radius: 12rpx;
  501. margin-bottom: 20rpx;
  502. padding: 0 24rpx;
  503. font-size: 28rpx;
  504. background: #f7f8fa;
  505. }
  506. /* 注册表单的获取验证码行样式,可以复用登录的 code-row */
  507. .register-form .code-row {
  508. display: flex;
  509. align-items: center;
  510. margin-bottom: 20rpx; /* 增加底部间距 */
  511. }
  512. /* 注册表单的验证码输入框样式,可以复用登录的 code-input */
  513. .register-form .code-input {
  514. flex: 1;
  515. margin-right: 16rpx;
  516. width: auto; /* 验证码输入框宽度自适应 */
  517. }
  518. /* 注册表单的获取验证码按钮样式,可以复用登录的 code-btn */
  519. .register-form .code-btn {
  520. height: 80rpx;
  521. background: #7ac81e;
  522. color: #fff;
  523. border-radius: 12rpx;
  524. padding: 0 24rpx;
  525. font-size: 28rpx;
  526. }
  527. /* 注册按钮样式,可以复用登录的 login-btn */
  528. .register-popup-content .login-btn {
  529. width: 100%;
  530. height: 80rpx;
  531. background: #7ac81e;
  532. color: #fff;
  533. font-size: 32rpx;
  534. border-radius: 40rpx;
  535. margin-bottom: 0; /* 移除底部间距 */
  536. }
  537. .close-btn {
  538. position: absolute;
  539. top: 20rpx;
  540. right: 20rpx;
  541. font-size: 36rpx; /* 调整文本大小 */
  542. color: #999;
  543. padding: 10rpx;
  544. /* 可以加个边框或者背景 */
  545. /* border: 1rpx solid #999; */
  546. /* border-radius: 50%; */
  547. /* background-color: #eee; */
  548. }
  549. /* 如果你没有引入uni-ui,可以使用文本X并给样式 */
  550. /* .close-btn text { font-size: 36rpx; } */
  551. .form-actions {
  552. width: 100%;
  553. display: flex;
  554. justify-content: space-between;
  555. align-items: center;
  556. margin-top: -10rpx;
  557. margin-bottom: 20rpx;
  558. padding: 0 10rpx;
  559. }
  560. .login-link {
  561. color: #7ac81e;
  562. font-size: 26rpx;
  563. }
  564. .forgot-password-link {
  565. color: #7ac81e;
  566. font-size: 26rpx;
  567. }
  568. </style>