index.vue 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692
  1. <template>
  2. <view class="login-page">
  3. <view class="brand-area">
  4. <image class="logo" src="/static/logo.png" mode="aspectFit" />
  5. <text class="brand-title">小鹅通</text>
  6. <view class="slogan-area">
  7. <text class="slogan">让知识创造价值</text>
  8. <view class="slogan-underline"></view>
  9. </view>
  10. </view>
  11. <view class="phone-area">
  12. <text class="phone">199****4261</text>
  13. <text class="carrier">号码认证由中国电信提供</text>
  14. </view>
  15. <button class="main-btn login-btn" :disabled="!agreed" @tap="handleLogin">一键登录</button>
  16. <button class="main-btn wechat-btn" :disabled="!agreed" @tap="socialLogin('wechat')">
  17. <image class="wechat-icon" src="/static/wechat.png" /> 微信登录
  18. </button>
  19. <view class="protocol-area">
  20. <checkbox :checked="agreed" @tap="agreed = !agreed" color="#2979ff" />
  21. <text class="protocol-text">
  22. 我已阅读并同意
  23. <text class="link" @tap="openProtocol('service')">《服务协议》</text>、
  24. <text class="link" @tap="openProtocol('privacy')">《隐私政策》</text>
  25. </text>
  26. </view>
  27. <view class="other-login">
  28. <text class="other-title">其他方式登录</text>
  29. <view class="other-icons">
  30. <view @tap="showLoginPopup = true">
  31. <image class="icon" src="/static/phone.png" />
  32. </view>
  33. <view >
  34. <image class="icon" src="/static/qq.png" />
  35. </view>
  36. </view>
  37. </view>
  38. <!-- 登录弹窗 -->
  39. <view v-if="showLoginPopup" class="popup-mask">
  40. <view class="popup-box">
  41. <text class="close-btn" @tap="showLoginPopup = false">×</text>
  42. <view class="popup-title">登录</view>
  43. <view class="popup-tabs">
  44. <text :class="{active: loginTab==='phone'}" @tap="loginTab='phone'">手机号登录</text>
  45. <text :class="{active: loginTab==='account'}" @tap="loginTab='account'">账号密码登录</text>
  46. </view>
  47. <view v-if="loginTab==='phone'">
  48. <view class="input-row">
  49. <input v-model="loginPhone" placeholder="请输入手机号" />
  50. </view>
  51. <view class="input-row code-row">
  52. <input v-model="loginCode" placeholder="请输入验证码" />
  53. <button class="code-btn" :disabled="loginCodeCountdown > 0" @tap="getLoginCode">
  54. {{ loginCodeCountdown > 0 ? loginCodeCountdown + 's' : '获取验证码' }}
  55. </button>
  56. </view>
  57. </view>
  58. <view v-else>
  59. <view class="input-row">
  60. <input v-model="loginAccount" placeholder="请输入账号" />
  61. </view>
  62. <view class="input-row">
  63. <input v-model="loginPassword" type="password" password placeholder="请输入密码" />
  64. </view>
  65. </view>
  66. <button class="main-btn popup-login-btn" @tap="handlePopupLogin">登录</button>
  67. <view class="popup-footer">
  68. <text @tap="openRegister">去注册</text>
  69. </view>
  70. </view>
  71. </view>
  72. <!-- 注册弹窗 -->
  73. <view v-if="showRegisterPopup" class="popup-mask">
  74. <view class="popup-box">
  75. <text class="close-btn" @tap="showRegisterPopup = false">×</text>
  76. <view class="popup-title">注册</view>
  77. <view class="input-row">
  78. <input v-model="regPhone" placeholder="请输入手机号" />
  79. </view>
  80. <view class="input-row code-row">
  81. <input v-model="regCode" placeholder="请输入验证码" />
  82. <button class="code-btn" @tap="getRegCode">
  83. {{ regCodeCountdown > 0 ? regCodeCountdown + 's' : '获取验证码' }}
  84. </button>
  85. </view>
  86. <view class="input-row">
  87. <input v-model="regAccount" placeholder="请输入账号" />
  88. </view>
  89. <view class="input-row">
  90. <input v-model="regPassword" type="password" placeholder="请输入密码" />
  91. </view>
  92. <button class="main-btn popup-login-btn" @tap="handleRegister">注册</button>
  93. </view>
  94. </view>
  95. </view>
  96. </template>
  97. <script setup>
  98. import { ref, onUnmounted } from 'vue'
  99. const agreed = ref(false)
  100. const showLoginPopup = ref(false)
  101. const showRegisterPopup = ref(false)
  102. const loginTab = ref('phone')
  103. const loginPhone = ref('')
  104. const loginCode = ref('')
  105. const loginAccount = ref('')
  106. const loginPassword = ref('')
  107. const regPhone = ref('')
  108. const regCode = ref('')
  109. const regAccount = ref('')
  110. const regPassword = ref('')
  111. const loginCodeCountdown = ref(0)
  112. const regCodeCountdown = ref(0)
  113. let loginCodeTimer = null
  114. let regCodeTimer = null
  115. function handleLogin() {
  116. if (!agreed.value) {
  117. uni.showToast({ title: '请先同意协议', icon: 'none' })
  118. return
  119. }
  120. // 登录逻辑
  121. }
  122. function openProtocol(type) {
  123. const urls = {
  124. service: '/pages/protocol/service',
  125. privacy: '/pages/protocol/privacy'
  126. }
  127. uni.navigateTo({ url: urls[type] })
  128. }
  129. function socialLogin(type) {
  130. // 微信、QQ等登录逻辑
  131. }
  132. function openRegister() {
  133. showLoginPopup.value = false
  134. showRegisterPopup.value = true
  135. }
  136. function getLoginCode() {
  137. if (loginCodeCountdown.value > 0) return;
  138. if (!/^1[3-9]\d{9}$/.test(loginPhone.value)) {
  139. uni.showToast({ title: '请输入正确手机号', icon: 'none' });
  140. return;
  141. }
  142. // 先启动倒计时
  143. loginCodeCountdown.value = 60;
  144. loginCodeTimer = setInterval(() => {
  145. if (loginCodeCountdown.value > 0) {
  146. loginCodeCountdown.value--;
  147. } else {
  148. clearInterval(loginCodeTimer);
  149. loginCodeTimer = null;
  150. }
  151. }, 1000);
  152. console.log('开始获取验证码...');
  153. uni.request({
  154. url: 'http://localhost:9500/user/code',
  155. method: 'POST',
  156. data: { phone: loginPhone.value },
  157. success(res) {
  158. console.log('验证码响应:', res.data);
  159. if (res.statusCode === 200) {
  160. uni.showToast({ title: '验证码已发送', icon: 'none' });
  161. } else {
  162. uni.showToast({ title: res.data.msg || '发送失败', icon: 'none' });
  163. }
  164. },
  165. fail(err) {
  166. console.error('获取验证码失败:', err);
  167. uni.showToast({ title: '网络错误', icon: 'none' });
  168. }
  169. });
  170. }
  171. function getRegCode() {
  172. if (regCodeCountdown.value > 0) return;
  173. if (!/^1[3-9]\d{9}$/.test(regPhone.value)) {
  174. uni.showToast({ title: '请输入正确手机号', icon: 'none' });
  175. return;
  176. }
  177. // 先启动倒计时
  178. regCodeCountdown.value = 60;
  179. regCodeTimer = setInterval(() => {
  180. if (regCodeCountdown.value > 0) {
  181. regCodeCountdown.value--;
  182. } else {
  183. clearInterval(regCodeTimer);
  184. regCodeTimer = null;
  185. }
  186. }, 1000);
  187. console.log('开始获取注册验证码...');
  188. uni.request({
  189. url: 'http://localhost:9500/user/code',
  190. method: 'POST',
  191. data: { phone: regPhone.value },
  192. success(res) {
  193. console.log('验证码响应:', res.data);
  194. if (res.statusCode === 200) {
  195. uni.showToast({ title: '验证码已发送', icon: 'none' });
  196. } else {
  197. uni.showToast({ title: res.data.msg || '发送失败', icon: 'none' });
  198. }
  199. },
  200. fail(err) {
  201. console.error('获取验证码失败:', err);
  202. uni.showToast({ title: '网络错误', icon: 'none' });
  203. }
  204. });
  205. }
  206. function handlePopupLogin() {
  207. if (loginTab.value === 'phone') {
  208. if (!/^1[3-9]\d{9}$/.test(loginPhone.value)) {
  209. uni.showToast({ title: '请输入正确手机号', icon: 'none' });
  210. return;
  211. }
  212. if (!loginCode.value) {
  213. uni.showToast({ title: '请输入验证码', icon: 'none' });
  214. return;
  215. }
  216. console.log('开始登录请求...');
  217. uni.request({
  218. url: 'http://localhost:9500/user/login',
  219. method: 'POST',
  220. data: {
  221. phone: loginPhone.value,
  222. code: loginCode.value
  223. },
  224. success(res) {
  225. console.log('登录响应数据:', res.data);
  226. console.log('响应状态码:', res.statusCode);
  227. // 检查响应状态码和数据结构
  228. if (res.statusCode === 200) {
  229. // 存储登录信息
  230. uni.setStorageSync('userPhone', loginPhone.value);
  231. uni.setStorageSync('isLoggedIn', true);
  232. // 关闭弹窗
  233. showLoginPopup.value = false;
  234. // 显示成功提示
  235. uni.showToast({
  236. title: '登录成功',
  237. icon: 'success',
  238. duration: 1500
  239. });
  240. // 直接使用 reLaunch 跳转
  241. console.log('开始跳转到首页...');
  242. uni.reLaunch({
  243. url: '/pages/index/index',
  244. success: () => {
  245. console.log('跳转成功');
  246. },
  247. fail: (err) => {
  248. console.error('reLaunch 跳转失败:', err);
  249. // 如果 reLaunch 失败,尝试使用 switchTab
  250. console.log('尝试使用 switchTab 跳转...');
  251. uni.switchTab({
  252. url: '/pages/index/index',
  253. success: () => {
  254. console.log('switchTab 跳转成功');
  255. },
  256. fail: (err) => {
  257. console.error('switchTab 跳转失败:', err);
  258. uni.showToast({
  259. title: '页面跳转失败,请重试',
  260. icon: 'none'
  261. });
  262. }
  263. });
  264. }
  265. });
  266. } else {
  267. console.log('登录失败,状态码:', res.statusCode);
  268. uni.showToast({
  269. title: res.data.msg || '登录失败,请重试',
  270. icon: 'none'
  271. });
  272. }
  273. },
  274. fail(err) {
  275. console.error('登录请求失败:', err);
  276. uni.showToast({ title: '网络错误', icon: 'none' });
  277. }
  278. });
  279. } else {
  280. if (!loginAccount.value) {
  281. uni.showToast({ title: '请输入账号', icon: 'none' });
  282. return;
  283. }
  284. if (!loginPassword.value) {
  285. uni.showToast({ title: '请输入密码', icon: 'none' });
  286. return;
  287. }
  288. console.log('开始登录请求...');
  289. uni.request({
  290. url: 'http://localhost:9500/user/loginup',
  291. method: 'POST',
  292. data: {
  293. username: loginAccount.value,
  294. password: loginPassword.value
  295. },
  296. success(res) {
  297. console.log('登录响应数据:', res.data);
  298. console.log('响应状态码:', res.statusCode);
  299. // 检查响应状态码和数据结构
  300. if (res.statusCode === 200 && res.data.code === 200) {
  301. // 存储登录信息
  302. uni.setStorageSync('userPhone', loginAccount.value);
  303. uni.setStorageSync('isLoggedIn', true);
  304. // 关闭弹窗
  305. showLoginPopup.value = false;
  306. // 显示成功提示
  307. uni.showToast({
  308. title: '登录成功',
  309. icon: 'success',
  310. duration: 1500
  311. });
  312. // 直接使用 reLaunch 跳转
  313. console.log('开始跳转到首页...');
  314. uni.reLaunch({
  315. url: '/pages/index/index',
  316. success: () => {
  317. console.log('跳转成功');
  318. },
  319. fail: (err) => {
  320. console.error('reLaunch 跳转失败:', err);
  321. // 如果 reLaunch 失败,尝试使用 switchTab
  322. console.log('尝试使用 switchTab 跳转...');
  323. uni.switchTab({
  324. url: '/pages/index/index',
  325. success: () => {
  326. console.log('switchTab 跳转成功');
  327. },
  328. fail: (err) => {
  329. console.error('switchTab 跳转失败:', err);
  330. uni.showToast({
  331. title: '页面跳转失败,请重试',
  332. icon: 'none'
  333. });
  334. }
  335. });
  336. }
  337. });
  338. } else {
  339. console.log('登录失败,状态码:', res.statusCode);
  340. console.log('登录失败,错误信息:', res.data.msg);
  341. uni.showToast({
  342. title: res.data.msg || '账号或密码错误',
  343. icon: 'none',
  344. duration: 2000
  345. });
  346. }
  347. },
  348. fail(err) {
  349. console.error('登录请求失败:', err);
  350. uni.showToast({
  351. title: '网络错误,请稍后重试',
  352. icon: 'none',
  353. duration: 2000
  354. });
  355. }
  356. });
  357. }
  358. }
  359. function handleRegister() {
  360. if (!/^1[3-9]\d{9}$/.test(regPhone.value)) {
  361. uni.showToast({ title: '请输入正确手机号', icon: 'none' });
  362. return;
  363. }
  364. if (!regCode.value) {
  365. uni.showToast({ title: '请输入验证码', icon: 'none' });
  366. return;
  367. }
  368. if (!regAccount.value) {
  369. uni.showToast({ title: '请输入账号', icon: 'none' });
  370. return;
  371. }
  372. if (!regPassword.value) {
  373. uni.showToast({ title: '请输入密码', icon: 'none' });
  374. return;
  375. }
  376. console.log('开始注册请求...');
  377. uni.request({
  378. url: 'http://localhost:9500/user/register',
  379. method: 'POST',
  380. data: {
  381. phone: regPhone.value,
  382. code: regCode.value,
  383. username: regAccount.value,
  384. password: regPassword.value
  385. },
  386. success(res) {
  387. console.log('注册响应数据:', res.data);
  388. console.log('响应状态码:', res.statusCode);
  389. if (res.statusCode === 200) {
  390. // 显示成功提示
  391. uni.showToast({
  392. title: '注册成功',
  393. icon: 'success',
  394. duration: 1500
  395. });
  396. // 关闭注册弹窗
  397. showRegisterPopup.value = false;
  398. // 延迟后打开登录弹窗
  399. setTimeout(() => {
  400. showLoginPopup.value = true;
  401. loginTab.value = 'phone';
  402. // 自动填充手机号
  403. loginPhone.value = regPhone.value;
  404. }, 1500);
  405. } else {
  406. console.log('注册失败,状态码:', res.statusCode);
  407. uni.showToast({
  408. title: res.data.msg || '注册失败,请重试',
  409. icon: 'none'
  410. });
  411. }
  412. },
  413. fail(err) {
  414. console.error('注册请求失败:', err);
  415. uni.showToast({ title: '网络错误', icon: 'none' });
  416. }
  417. });
  418. }
  419. onUnmounted(() => {
  420. if (loginCodeTimer) clearInterval(loginCodeTimer)
  421. if (regCodeTimer) clearInterval(regCodeTimer)
  422. })
  423. </script>
  424. <style lang="scss" scoped>
  425. .login-page {
  426. min-height: 100vh;
  427. background: #fff;
  428. display: flex;
  429. flex-direction: column;
  430. align-items: center;
  431. justify-content: flex-start;
  432. padding: 0 40rpx;
  433. }
  434. .brand-area {
  435. margin-top: 100rpx;
  436. display: flex;
  437. flex-direction: column;
  438. align-items: center;
  439. .logo {
  440. width: 120rpx;
  441. height: 120rpx;
  442. margin-bottom: 20rpx;
  443. }
  444. .brand-title {
  445. font-size: 44rpx;
  446. font-weight: bold;
  447. color: #222;
  448. margin-bottom: 20rpx;
  449. }
  450. .slogan-area {
  451. display: flex;
  452. flex-direction: column;
  453. align-items: center;
  454. .slogan {
  455. font-size: 28rpx;
  456. color: #2979ff;
  457. margin-bottom: 8rpx;
  458. }
  459. .slogan-underline {
  460. width: 220rpx;
  461. height: 8rpx;
  462. background: #e3f0ff;
  463. border-radius: 4rpx;
  464. }
  465. }
  466. }
  467. .phone-area {
  468. margin: 500rpx 0 40rpx 0;
  469. display: flex;
  470. flex-direction: column;
  471. align-items: center;
  472. .phone {
  473. font-size: 36rpx;
  474. color: #222;
  475. font-weight: 500;
  476. margin-bottom: 10rpx;
  477. }
  478. .carrier {
  479. font-size: 24rpx;
  480. color: #aaa;
  481. }
  482. }
  483. .main-btn {
  484. width: 100%;
  485. height: 88rpx;
  486. border-radius: 44rpx;
  487. font-size: 32rpx;
  488. font-weight: 500;
  489. margin-bottom: 32rpx;
  490. }
  491. .login-btn {
  492. background: #1976ff;
  493. color: #fff;
  494. border: none;
  495. }
  496. .wechat-btn {
  497. background: #1aad19;
  498. color: #fff;
  499. border: none;
  500. display: flex;
  501. align-items: center;
  502. justify-content: center;
  503. .wechat-icon {
  504. width: 40rpx;
  505. height: 40rpx;
  506. margin-right: 16rpx;
  507. vertical-align: middle;
  508. }
  509. }
  510. .protocol-area {
  511. display: flex;
  512. align-items: center;
  513. margin: 30rpx 0 0 0;
  514. .protocol-text {
  515. font-size: 22rpx;
  516. color: #999;
  517. margin-left: 10rpx;
  518. .link {
  519. color: #1976ff;
  520. }
  521. }
  522. }
  523. .other-login {
  524. margin-top: 80rpx;
  525. display: flex;
  526. flex-direction: column;
  527. align-items: center;
  528. .other-title {
  529. font-size: 24rpx;
  530. color: #bbb;
  531. margin-bottom: 20rpx;
  532. }
  533. .other-icons {
  534. display: flex;
  535. gap: 60rpx;
  536. .icon {
  537. width: 60rpx;
  538. height: 60rpx;
  539. }
  540. }
  541. }
  542. .icon-circle {
  543. width: 80rpx;
  544. height: 80rpx;
  545. border: 1px solid #ccc;
  546. border-radius: 50%;
  547. display: flex;
  548. align-items: center;
  549. justify-content: center;
  550. margin: 0 20rpx;
  551. background: #fff;
  552. }
  553. .popup-mask {
  554. position: fixed;
  555. left: 0; top: 0; right: 0; bottom: 0;
  556. background: rgba(0,0,0,0.25);
  557. z-index: 999;
  558. display: flex;
  559. align-items: center;
  560. justify-content: center;
  561. }
  562. .popup-box {
  563. width: 90vw;
  564. max-width: 600rpx;
  565. background: #fff;
  566. border-radius: 24rpx;
  567. box-shadow: 0 8rpx 32rpx rgba(0,0,0,0.10);
  568. padding: 60rpx 40rpx 40rpx 40rpx;
  569. position: relative;
  570. display: flex;
  571. flex-direction: column;
  572. align-items: stretch;
  573. }
  574. .close-btn {
  575. position: absolute;
  576. right: 32rpx;
  577. top: 24rpx;
  578. font-size: 48rpx;
  579. color: #bbb;
  580. z-index: 2;
  581. font-weight: bold;
  582. }
  583. .popup-title {
  584. text-align: center;
  585. font-size: 36rpx;
  586. font-weight: bold;
  587. color: #222;
  588. margin-bottom: 32rpx;
  589. letter-spacing: 2rpx;
  590. }
  591. .popup-tabs {
  592. display: flex;
  593. justify-content: center;
  594. margin-bottom: 32rpx;
  595. text {
  596. font-size: 28rpx;
  597. margin: 0 36rpx;
  598. color: #888;
  599. padding-bottom: 8rpx;
  600. position: relative;
  601. &.active {
  602. color: #1976ff;
  603. font-weight: bold;
  604. }
  605. &.active::after {
  606. content: '';
  607. display: block;
  608. position: absolute;
  609. left: 0; right: 0; bottom: 0;
  610. height: 4rpx;
  611. background: #1976ff;
  612. border-radius: 2rpx;
  613. }
  614. }
  615. }
  616. .input-row {
  617. margin-bottom: 28rpx;
  618. display: flex;
  619. align-items: center;
  620. input {
  621. flex: 1;
  622. height: 80rpx;
  623. border-radius: 40rpx;
  624. background: #f7f8fa;
  625. border: 1px solid #eee;
  626. padding: 0 32rpx;
  627. font-size: 28rpx;
  628. color: #333;
  629. }
  630. }
  631. .code-row {
  632. input {
  633. flex: 1;
  634. margin-right: 16rpx;
  635. }
  636. .code-btn {
  637. width: 160rpx;
  638. height: 64rpx;
  639. border-radius: 32rpx;
  640. background: #1976ff;
  641. color: #fff;
  642. font-size: 24rpx;
  643. border: none;
  644. padding: 0;
  645. }
  646. }
  647. .popup-login-btn {
  648. margin-top: 12rpx;
  649. margin-bottom: 16rpx;
  650. width: 100%;
  651. height: 88rpx;
  652. border-radius: 44rpx;
  653. font-size: 32rpx;
  654. font-weight: 500;
  655. background: #1976ff;
  656. color: #fff;
  657. border: none;
  658. }
  659. .popup-footer {
  660. text-align: center;
  661. margin-top: 10rpx;
  662. color: #1976ff;
  663. font-size: 26rpx;
  664. font-weight: 500;
  665. letter-spacing: 1rpx;
  666. }
  667. </style>