login.vue 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634
  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/yx-icon.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:8081/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:8081/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.loginType === 'phone') {
  192. // 手机号+验证码登录
  193. if (!this.phone || !this.code) {
  194. uni.showToast({ title: '请填写手机号和验证码', icon: 'none' });
  195. return;
  196. }
  197. uni.showLoading({ title: '登录中...', mask: true });
  198. try {
  199. const res = await uni.request({
  200. url: 'http://localhost:8081/user/login',
  201. method: 'POST',
  202. data: {
  203. phone: this.phone,
  204. code: this.code
  205. },
  206. header: { 'content-type': 'application/json' }
  207. });
  208. uni.hideLoading();
  209. if (res.statusCode === 200 && res.data) {
  210. uni.showToast({ title: '登录成功', icon: 'success' });
  211. setTimeout(() => {
  212. uni.switchTab({ url: '/pages/home/index' });
  213. }, 1500);
  214. } else {
  215. uni.showToast({ title: res.data?.message || '登录失败', icon: 'error' });
  216. }
  217. } catch (err) {
  218. uni.hideLoading();
  219. uni.showToast({ title: '登录失败', icon: 'none' });
  220. }
  221. } else if (this.loginType === 'account') {
  222. // 账号+密码登录
  223. if (!this.username || !this.password) {
  224. uni.showToast({ title: '请填写账号和密码', icon: 'none' });
  225. return;
  226. }
  227. uni.showLoading({ title: '登录中...', mask: true });
  228. try {
  229. const res = await uni.request({
  230. url: 'http://localhost:8081/user/UserPassLogin',
  231. method: 'POST',
  232. data: {
  233. username: this.username,
  234. password: this.password
  235. },
  236. header: { 'content-type': 'application/json' }
  237. });
  238. uni.hideLoading();
  239. if (res.statusCode === 200 && res.data && res.data.code === 200) {
  240. uni.showToast({ title: '登录成功', icon: 'success' });
  241. setTimeout(() => {
  242. uni.switchTab({ url: '/pages/home/index' });
  243. }, 1500);
  244. } else {
  245. uni.showToast({ title: res.data?.msg || '登录失败', icon: 'error' });
  246. }
  247. } catch (err) {
  248. uni.hideLoading();
  249. uni.showToast({ title: '登录失败', icon: 'none' });
  250. }
  251. }
  252. },
  253. toRegister() {
  254. this.showRegisterPopup = true;
  255. },
  256. toForgot() {
  257. uni.showToast({ title: '跳转找回密码', icon: 'none' });
  258. },
  259. openAgreement() {
  260. uni.navigateTo({ url: '/pages/agreement/agreement' });
  261. },
  262. openPrivacy() {
  263. uni.navigateTo({ url: '/pages/privacy/privacy' });
  264. },
  265. qqLogin() {
  266. uni.showToast({ title: 'QQ登录', icon: 'none' });
  267. },
  268. async sendRegCode() {
  269. // 如果正在发送或倒计时中,则不执行任何操作
  270. if (this.isSendingRegCode || this.regCodeTimer > 0) {
  271. return;
  272. }
  273. // 检查手机号是否已填写
  274. if (!this.regPhone) {
  275. uni.showToast({ title: '请输入手机号', icon: 'none' });
  276. return;
  277. }
  278. // 设置状态,显示加载提示
  279. this.isSendingRegCode = true;
  280. uni.showLoading({ title: '发送中...', mask: true });
  281. try {
  282. // 发起POST请求到后端接口
  283. const res = await uni.request({
  284. url: 'http://localhost:8081/user/code',
  285. method: 'POST',
  286. data: {
  287. phone: this.regPhone // 发送手机号参数
  288. },
  289. header: { 'content-type': 'application/json' }
  290. });
  291. // 隐藏加载提示,重置发送状态
  292. uni.hideLoading();
  293. this.isSendingRegCode = false;
  294. // 根据后端响应处理结果
  295. if (res.statusCode === 200 && res.data) {
  296. console.log('Send registration code success:', res.data);
  297. uni.showToast({ title: '验证码已发送', icon: 'success' });
  298. // 开始60秒倒计时
  299. this.regCodeTimer = 60;
  300. const timerInterval = setInterval(() => {
  301. this.regCodeTimer--;
  302. if (this.regCodeTimer <= 0) {
  303. clearInterval(timerInterval); // 倒计时结束时清除定时器
  304. this.regCodeTimer = 0;
  305. }
  306. }, 1000); // 每秒更新
  307. } else {
  308. // 处理发送失败的情况
  309. console.error('Send registration code failed:', res);
  310. uni.showToast({ title: res.data?.message || '发送失败', icon: 'none' });
  311. }
  312. } catch (err) {
  313. // 处理请求异常
  314. uni.hideLoading();
  315. this.isSendingRegCode = false;
  316. console.error('Send registration code request failed:', err);
  317. uni.showToast({ title: '发送失败', icon: 'none' });
  318. }
  319. },
  320. async doRegister() {
  321. if (!this.regPhone || !this.regCode || !this.regUsername || !this.regPassword) {
  322. uni.showToast({ title: '请填写所有注册信息', icon: 'none' });
  323. return;
  324. }
  325. uni.showLoading({ title: '注册中...', mask: true });
  326. try {
  327. const res = await uni.request({
  328. url: 'http://localhost:8081/user/register',
  329. method: 'POST',
  330. data: {
  331. phone: this.regPhone,
  332. code: this.regCode,
  333. username: this.regUsername,
  334. password: this.regPassword
  335. },
  336. header: { 'content-type': 'application/json' }
  337. });
  338. uni.hideLoading();
  339. if (res.statusCode === 200 && res.data) {
  340. console.log('Registration success:', res.data);
  341. uni.showToast({ title: '注册成功', icon: 'success' });
  342. // Optionally, clear the form fields
  343. this.regPhone = '';
  344. this.regCode = '';
  345. this.regUsername = '';
  346. this.regPassword = '';
  347. // Close the popup after successful registration
  348. setTimeout(() => {
  349. this.showRegisterPopup = false;
  350. }, 1500); // Delay closing slightly to show toast
  351. } else {
  352. console.error('Registration failed:', res);
  353. uni.showToast({ title: res.data?.message || '注册失败', icon: 'none' });
  354. }
  355. } catch (err) {
  356. uni.hideLoading();
  357. console.error('Registration request failed:', err);
  358. uni.showToast({ title: '注册失败', icon: 'none' });
  359. }
  360. },
  361. }
  362. }
  363. </script>
  364. <style>
  365. .login-bg {
  366. min-height: 100vh;
  367. background: #f7f8fa;
  368. display: flex;
  369. align-items: center;
  370. justify-content: center;
  371. }
  372. .login-box {
  373. width: 90vw;
  374. max-width: 600rpx;
  375. background: #fff;
  376. border-radius: 24rpx;
  377. box-shadow: 0 8rpx 32rpx rgba(0,0,0,0.08);
  378. padding: 60rpx 40rpx 40rpx 40rpx;
  379. display: flex;
  380. flex-direction: column;
  381. align-items: center;
  382. }
  383. .login-title {
  384. font-size: 40rpx;
  385. font-weight: bold;
  386. margin-bottom: 40rpx;
  387. }
  388. .login-tabs {
  389. display: flex;
  390. width: 100%;
  391. margin-bottom: 30rpx;
  392. }
  393. .tab {
  394. flex: 1;
  395. text-align: center;
  396. font-size: 28rpx;
  397. padding: 16rpx 0;
  398. color: #888;
  399. border-bottom: 4rpx solid transparent;
  400. }
  401. .tab.active {
  402. color: #7ac81e;
  403. border-bottom: 4rpx solid #7ac81e;
  404. font-weight: bold;
  405. }
  406. .login-form {
  407. width: 100%;
  408. margin-bottom: 20rpx;
  409. }
  410. .login-input {
  411. width: 90%;
  412. height: 80rpx;
  413. border: 1rpx solid #eee;
  414. border-radius: 12rpx;
  415. margin-bottom: 20rpx;
  416. padding: 0 24rpx;
  417. font-size: 28rpx;
  418. background: #f7f8fa;
  419. }
  420. .code-row {
  421. display: flex;
  422. align-items: center;
  423. }
  424. .code-input {
  425. flex: 1;
  426. margin-right: 16rpx;
  427. }
  428. .code-btn {
  429. height: 80rpx;
  430. background: #7ac81e;
  431. color: #fff;
  432. border-radius: 12rpx;
  433. padding: 0 24rpx;
  434. font-size: 28rpx;
  435. }
  436. .login-btn {
  437. width: 100%;
  438. height: 80rpx;
  439. background: #7ac81e;
  440. color: #fff;
  441. font-size: 32rpx;
  442. border-radius: 40rpx;
  443. margin-bottom: 20rpx;
  444. }
  445. .login-btn:disabled {
  446. background: #b2e59e;
  447. }
  448. .login-agree {
  449. display: flex;
  450. align-items: center;
  451. font-size: 24rpx;
  452. color: #888;
  453. margin-bottom: 30rpx;
  454. }
  455. .login-agree .link {
  456. color: #7ac81e;
  457. margin: 0 4rpx;
  458. }
  459. .login-divider {
  460. width: 100%;
  461. text-align: center;
  462. color: #bbb;
  463. font-size: 24rpx;
  464. margin: 30rpx 0 20rpx 0;
  465. }
  466. .wechat-btn, .qq-btn {
  467. width: 100%;
  468. height: 70rpx;
  469. border-radius: 35rpx;
  470. font-size: 28rpx;
  471. display: flex;
  472. align-items: center;
  473. justify-content: center;
  474. margin-bottom: 16rpx;
  475. border: 1rpx solid #eee;
  476. background: #f7f8fa;
  477. }
  478. .wechat-btn {
  479. color: #09bb07;
  480. border: 1rpx solid #09bb07;
  481. }
  482. .qq-btn {
  483. color: #498ff6;
  484. border: 1rpx solid #498ff6;
  485. }
  486. .icon {
  487. width: 40rpx;
  488. height: 40rpx;
  489. margin-right: 16rpx;
  490. }
  491. .popup-overlay {
  492. position: fixed;
  493. top: 0;
  494. left: 0;
  495. right: 0;
  496. bottom: 0;
  497. background-color: rgba(0, 0, 0, 0.6); /* 半透明背景 */
  498. display: flex;
  499. align-items: center;
  500. justify-content: center;
  501. z-index: 999; /* 确保在最上层 */
  502. }
  503. .register-popup-content {
  504. background-color: #fff;
  505. border-radius: 24rpx;
  506. width: 85vw; /* 弹窗宽度 */
  507. max-width: 600rpx;
  508. padding: 60rpx 40rpx;
  509. position: relative; /* 用于定位关闭按钮 */
  510. display: flex;
  511. flex-direction: column;
  512. align-items: center;
  513. }
  514. .register-popup-content .popup-title {
  515. font-size: 40rpx;
  516. font-weight: bold;
  517. margin-bottom: 40rpx;
  518. color: #333;
  519. }
  520. .register-form {
  521. width: 100%;
  522. margin-bottom: 30rpx;
  523. }
  524. /* 注册表单的输入框样式,可以复用登录的 login-input */
  525. .register-form .login-input {
  526. width: 90%; /* 输入框宽度调整为100% */
  527. height: 80rpx;
  528. border: 1rpx solid #eee;
  529. border-radius: 12rpx;
  530. margin-bottom: 20rpx;
  531. padding: 0 24rpx;
  532. font-size: 28rpx;
  533. background: #f7f8fa;
  534. }
  535. /* 注册表单的获取验证码行样式,可以复用登录的 code-row */
  536. .register-form .code-row {
  537. display: flex;
  538. align-items: center;
  539. margin-bottom: 20rpx; /* 增加底部间距 */
  540. }
  541. /* 注册表单的验证码输入框样式,可以复用登录的 code-input */
  542. .register-form .code-input {
  543. flex: 1;
  544. margin-right: 16rpx;
  545. width: auto; /* 验证码输入框宽度自适应 */
  546. }
  547. /* 注册表单的获取验证码按钮样式,可以复用登录的 code-btn */
  548. .register-form .code-btn {
  549. height: 80rpx;
  550. background: #7ac81e;
  551. color: #fff;
  552. border-radius: 12rpx;
  553. padding: 0 24rpx;
  554. font-size: 28rpx;
  555. }
  556. /* 注册按钮样式,可以复用登录的 login-btn */
  557. .register-popup-content .login-btn {
  558. width: 100%;
  559. height: 80rpx;
  560. background: #7ac81e;
  561. color: #fff;
  562. font-size: 32rpx;
  563. border-radius: 40rpx;
  564. margin-bottom: 0; /* 移除底部间距 */
  565. }
  566. .close-btn {
  567. position: absolute;
  568. top: 20rpx;
  569. right: 20rpx;
  570. font-size: 36rpx; /* 调整文本大小 */
  571. color: #999;
  572. padding: 10rpx;
  573. /* 可以加个边框或者背景 */
  574. /* border: 1rpx solid #999; */
  575. /* border-radius: 50%; */
  576. /* background-color: #eee; */
  577. }
  578. /* 如果你没有引入uni-ui,可以使用文本X并给样式 */
  579. /* .close-btn text { font-size: 36rpx; } */
  580. .form-actions {
  581. width: 100%;
  582. display: flex;
  583. justify-content: space-between;
  584. align-items: center;
  585. margin-top: -10rpx;
  586. margin-bottom: 20rpx;
  587. padding: 0 10rpx;
  588. }
  589. .login-link {
  590. color: #7ac81e;
  591. font-size: 26rpx;
  592. }
  593. .forgot-password-link {
  594. color: #7ac81e;
  595. font-size: 26rpx;
  596. }
  597. </style>