Browse Source

```
feat(auth): 添加手机号和邮箱登录功能

新增手机号登录、邮箱登录接口及验证码获取功能,优化登录表单验证逻辑,
支持通过手机号或邮箱进行统一登录,并完善相关 UI 和 API 调用流程。
同时更新了全局样式和首页布局以适配新设计。
```

zhangningning 1 week ago
parent
commit
bf4436f1b2
7 changed files with 111 additions and 133 deletions
  1. 1 0
      src/App.vue
  2. 18 1
      src/api/auth.js
  3. 7 10
      src/api/request.js
  4. BIN
      src/assets/imgs/banner.png
  5. 62 113
      src/components/LoginDialog.vue
  6. 6 6
      src/pages/Home.vue
  7. 17 3
      src/styles/index.scss

+ 1 - 0
src/App.vue

@@ -164,6 +164,7 @@ const activeIndex = computed(() => {
   color: #666;
   text-align: center;
   padding: 20px 0;
+  margin-top: 240px;
   
   .footer-content {
     font-size: 14px;

+ 18 - 1
src/api/auth.js

@@ -7,4 +7,21 @@ export function getCaptcha(data = {}) {
 // 用户名登录
 export function loginUsername(data = {}) {
   return request.post('/auth/login/username',data)
-}
+}
+// 手机号登录
+export function loginPhone(data = {}) {
+  return request.post('/auth/login/phone',data)
+}
+// 邮箱登录
+export function loginEmail(data = {}) {
+  return request.post('/auth/login/email',data)
+}
+// 获取短信验证码
+export function getSmsCode(data = {}) {
+  console.log('getSmsCode',data)
+  return request.get('/auth/sms-code',data)
+}
+// 获取邮箱验证码
+export function getEmailCode(data = {}) {
+  return request.get('/auth/email-code',data)
+}

+ 7 - 10
src/api/request.js

@@ -1,4 +1,5 @@
 import axios from 'axios'
+import { ElMessage } from 'element-plus'
 
 // 创建 axios 实例
 const request = axios.create({
@@ -47,17 +48,13 @@ request.interceptors.response.use(
     // 假设 code 为 200 表示成功
     if (code === 200) {
       return {code,data,msg}
-    } else {
-      // 处理业务错误
-      console.error('业务错误:', msg)
-      
-      // 特殊状态码处理
-      if (code === 401) {
-        // token 过期或无效,跳转登录页
-        localStorage.removeItem('access_token')
-        window.location.href = '/login'
-      }
+    } else if ([410000, 410001, 410002, 401, 403].includes(code)){
+      localStorage.removeItem('access_token')
+      window.location.href = '/login'
       
+      return {code,data,msg}
+    }else {
+      ElMessage.error(msg+'response code:'+code || '请求失败')
       return {code,data,msg}
     }
   },

BIN
src/assets/imgs/banner.png


+ 62 - 113
src/components/LoginDialog.vue

@@ -17,7 +17,7 @@
     <el-tabs v-model="activeTab" @tab-change="handleTabChange" class="login-tabs">
       <el-tab-pane label="账号密码登录" name="password">
         <el-form ref="passwordFormRef" :model="passwordForm" :rules="passwordRules" label-width="0">
-          <el-form-item prop="username" class="login-form-item">
+          <el-form-item prop="account" class="login-form-item">
             <el-input
               v-model="passwordForm.account"
               placeholder="请输入账号"
@@ -37,10 +37,10 @@
             />
           </el-form-item>
           <!-- 增加动态验证码图片 -->
-          <el-form-item prop="verifyCode" class="login-form-item">
+          <el-form-item prop="captcha" class="login-form-item">
             <div class="gap10">
               <el-input
-                v-model="passwordForm.verifyCode"
+                v-model="passwordForm.captcha"
                 placeholder="请输入验证码"
                 prefix-icon="Lock"
                 show-password
@@ -72,10 +72,10 @@
       
       <el-tab-pane label="短信登录" name="sms">
         <el-form ref="smsFormRef" :model="smsForm" :rules="smsRules" label-width="0">
-          <el-form-item prop="phone" class="login-form-item">
+          <el-form-item prop="account" class="login-form-item">
             <el-input
-              v-model="smsForm.phone"
-              placeholder="请输入手机号"
+              v-model="smsForm.account"
+              placeholder="请输入手机号或邮箱"
               prefix-icon="Mobile"
               clearable
               class="login-input"
@@ -83,7 +83,7 @@
           </el-form-item>
           <el-form-item prop="smsCode" class="login-form-item">
             <el-input
-              v-model="smsForm.smsCode"
+              v-model="smsForm.verifyCode"
               placeholder="请输入验证码"
               prefix-icon="Message"
               clearable
@@ -91,7 +91,6 @@
             >
               <template #append>
                 <el-button
-                  type="primary"
                   :disabled="smsCountdown > 0"
                   @click="sendSmsCode"
                   :class="{'countdown-btn': smsCountdown > 0}"
@@ -115,52 +114,6 @@
           </el-form-item>
         </el-form>
       </el-tab-pane>
-      
-      <el-tab-pane label="邮箱登录" name="email">
-        <el-form ref="emailFormRef" :model="emailForm" :rules="emailRules" label-width="0">
-          <el-form-item prop="email" class="login-form-item">
-            <el-input
-              v-model="emailForm.email"
-              placeholder="请输入邮箱"
-              prefix-icon="Email"
-              clearable
-              class="login-input"
-            />
-          </el-form-item>
-          <el-form-item prop="emailCode" class="login-form-item">
-            <el-input
-              v-model="emailForm.emailCode"
-              placeholder="请输入验证码"
-              prefix-icon="Message"
-              clearable
-              class="login-input"
-            >
-              <template #append>
-                <el-button
-                  type="primary"
-                  :disabled="emailCountdown > 0"
-                  @click="sendEmailCode"
-                  :class="{'countdown-btn': emailCountdown > 0}"
-                  size="small"
-                >
-                  {{ emailCountdown > 0 ? `${emailCountdown}s` : '发送验证码' }}
-                </el-button>
-              </template>
-            </el-input>
-          </el-form-item>
-          <el-form-item class="login-form-item">
-            <el-button 
-              type="primary" 
-              @click="handleEmailLogin" 
-              :loading="loading" 
-              class="login-button"
-              size="large"
-            >
-              登录
-            </el-button>
-          </el-form-item>
-        </el-form>
-      </el-tab-pane>
     </el-tabs>
 
     <!-- 其他登录方式 -->
@@ -199,7 +152,7 @@ import { ref, reactive, watch, computed } from 'vue'
 import { ElMessage } from 'element-plus'
 import QQIcon from '@/assets/imgs/QQ.png'
 import WeChatIcon from '@/assets/imgs/WeChat.png'
-import { getCaptcha, loginUsername } from '@/api/auth.js'
+import { getCaptcha, loginUsername, loginPhone, loginEmail, getSmsCode, getEmailCode } from '@/api/auth.js'
 // 正则表达式
 const PHONE_REGEX = /^1[3-9]\d{9}$/;
 const EMAIL_REGEX = /^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+$/;
@@ -258,20 +211,13 @@ const passwordForm = reactive({
 
 // 短信登录表单
 const smsForm = reactive({
-  phone: '',
-  smsCode: ''
-})
-
-// 邮箱登录表单
-const emailForm = reactive({
-  email: '',
-  emailCode: ''
+  account: '',
+  verifyCode: ''
 })
 
 // 表单引用
 const passwordFormRef = ref(null)
 const smsFormRef = ref(null)
-const emailFormRef = ref(null)
 
 // 加载状态
 const loading = ref(false)
@@ -283,31 +229,25 @@ const emailCountdown = ref(0)
 // 表单验证规则
 const passwordRules = reactive({
   account: [
-    { required: true, message: '请输入账号', trigger: 'blur' }
+    { required: true, message: '请输入账号', trigger: 'blur' },
+    { validator: (val) => isPasswordPhone.value || isPasswordEmail.value, message: '请输入正确的(手机号或邮箱)', trigger: 'blur' }
   ],
   password: [
     { required: true, message: '请输入密码', trigger: 'blur' },
     { min: 6, message: '密码长度不能少于6个字符', trigger: 'blur' }
+  ],
+  captcha: [
+    { required: true, message: '请输入验证码', trigger: 'blur' }
   ]
 })
 
 const smsRules = reactive({
-  phone: [
+  account: [
     { required: true, message: '请输入手机号', trigger: 'blur' },
-    { pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号', trigger: 'blur' }
-  ],
-  smsCode: [
-    { required: true, message: '请输入验证码', trigger: 'blur' },
-    { min: 6, max: 6, message: '验证码长度为6个字符', trigger: 'blur' }
-  ]
-})
+    { validator: (rule, val) => PHONE_REGEX.test(val) || EMAIL_REGEX.test(val), message: '请输入正确的(手机号或邮箱)', trigger: 'blur' }
 
-const emailRules = reactive({
-  email: [
-    { required: true, message: '请输入邮箱', trigger: 'blur' },
-    { type: 'email', message: '请输入正确的邮箱', trigger: 'blur' }
   ],
-  emailCode: [
+  verifyCode: [
     { required: true, message: '请输入验证码', trigger: 'blur' },
     { min: 6, max: 6, message: '验证码长度为6个字符', trigger: 'blur' }
   ]
@@ -320,22 +260,34 @@ const handleTabChange = () => {
     passwordFormRef.value?.resetFields()
   } else if (activeTab.value === 'sms') {
     smsFormRef.value?.resetFields()
-  } else if (activeTab.value === 'email') {
-    emailFormRef.value?.resetFields()
   }
 }
 
 // 发送短信验证码
-const sendSmsCode = () => {
-  if (!smsForm.phone) {
-    ElMessage.warning('请先输入手机号')
+const sendSmsCode = async () => {
+  if (!smsForm.account) {
+    ElMessage.warning('请先输入手机号或邮箱')
     return
   }
 
   // 验证手机号格式
-  const phoneRegex = /^1[3-9]\d{9}$/
-  if (!phoneRegex.test(smsForm.phone)) {
-    ElMessage.warning('请输入正确的手机号')
+  if (!PHONE_REGEX.test(smsForm.account) && !EMAIL_REGEX.test(smsForm.account)) {
+    ElMessage.warning('请输入正确的手机号或邮箱')
+    return
+  }
+
+  let res = null;
+  if(PHONE_REGEX.test(smsForm.account)){
+    res = await getSmsCode({
+      phone: smsForm.account
+    })
+  } else if (EMAIL_REGEX.test(smsForm.account)){
+    res = await getEmailCode({
+      email: smsForm.account
+    })
+  }
+
+  if(res.code !== 200){
     return
   }
 
@@ -377,17 +329,17 @@ const handlePasswordLogin = () => {
   passwordFormRef.value?.validate(async (valid) => {
     if (valid) {
       loading.value = true
-       const params = new URLSearchParams();
+       const params = {};
       // 根据账号类型决定参数名
       if (isPasswordPhone.value) {
-          params.append('phone', passwordForm.account);
+        params['phone'] = passwordForm.account;
       } else if (isPasswordEmail.value) {
-          params.append('email', passwordForm.account);
+        params['email'] = passwordForm.account;
       }
       
-      params.append('password', passwordForm.password);
-      params.append('captcha', passwordForm.captcha);
-      params.append('uuid', passwordForm.uuid);
+      params['password'] = passwordForm.password;
+      params['captcha'] = passwordForm.captcha;
+      params['uuid'] = passwordForm.uuid;
       const res = await loginUsername(params);
       loading.value = false
       if (res.code === 200) {
@@ -403,35 +355,32 @@ const handlePasswordLogin = () => {
 
 // 短信登录
 const handleSmsLogin = () => {
-  smsFormRef.value?.validate((valid) => {
+  smsFormRef.value?.validate(async(valid) => {
     if (valid) {
       loading.value = true
-      // 模拟登录请求
-      setTimeout(() => {
-        loading.value = false
+      const params = {};
+      let res = {};
+      params['code'] = smsForm.verifyCode;
+      // 根据账号类型决定参数名
+      if (PHONE_REGEX.test(smsForm.account)) {
+        params['phone'] = smsForm.account;
+        res = await loginPhone(params);
+      } else if (EMAIL_REGEX.test(smsForm.account)) {
+        params['email'] = smsForm.account;
+        res = await loginEmail(params);
+      }
+      loading.value = false
+      if (res.code === 200) {
         ElMessage.success('登录成功')
-        emit('login-success', { type: 'sms', userInfo: smsForm })
+        // emit('login-success', { type: 'sms', userInfo: smsForm })
         dialogVisible.value = false
-      }, 1500)
+      } else {
+        ElMessage.error(res.msg || '登录失败')
+      }
     }
   })
 }
 
-// 邮箱登录
-const handleEmailLogin = () => {
-  emailFormRef.value?.validate((valid) => {
-    if (valid) {
-      loading.value = true
-      // 模拟登录请求
-      setTimeout(() => {
-        loading.value = false
-        ElMessage.success('登录成功')
-        emit('login-success', { type: 'email', userInfo: emailForm })
-        dialogVisible.value = false
-      }, 1500)
-    }
-  })
-}
 
 // 微信登录
 const handleWechatLogin = () => {

+ 6 - 6
src/pages/Home.vue

@@ -1,8 +1,7 @@
 <template>
   <div class="home-page container">
-    <div class="banner">
-      <!-- autoplay -->
-      <el-carousel :height="carouselHeight">
+    <div class="banner flex-center">
+      <el-carousel :height="carouselHeight" style="width: 100%;">
         <el-carousel-item>
           <img
             :src="bannerImage"
@@ -151,6 +150,8 @@ const goToCourseDetail = (id,title) => {
 <style lang="scss">
 
 .banner {
+  // margin-top: 140px;
+  height: calc(100vh - 60px);
   .carousel-image {
     width: 100%;
     height: auto;
@@ -211,8 +212,6 @@ $height-btn: 50px;
 
 // 容器样式(SCSS嵌套)
 .workflow-container {
-
- 
   padding: $padding-base;
   border-radius: $border-radius-base;
   width: 1100px;
@@ -320,9 +319,10 @@ $height-btn: 50px;
 }
 .home-page{
   .course-list{
+    margin-top: 70px;
     .course-grid {
       display: grid;
-      grid-template-columns: repeat(auto-fill, minmax(362px, 1fr));
+      grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
       gap: 20px;
     }
     .course-card {

+ 17 - 3
src/styles/index.scss

@@ -1,16 +1,26 @@
 // 全局样式文件:src/styles/index.scss
 // 1. 全局变量(可统一管理颜色、字体、间距等)
-$primary-color: #409eff; // 主色调(和 Element Plus 主色一致)
+$primary-color: #2D71FF; // 主色调(和 Element Plus 主色一致)
 $secondary-color: #667eea; // 辅助色
 $text-color: #333333; // 主要文字色
 $text-light-color: #666666; // 次要文字色
 $border-color: #eeeeee; // 边框色
 $bg-color: #f9f9f9; // 背景色
 $danger-color: #e63946; // 危险色(价格、警告等)
-body{
-	background: url('@/assets/imgs/bg.png') no-repeat center center fixed;
+
+.el-button--primary {
+	&:not(.is-plain) {
+		background-color: $primary-color;
+		border-color: $primary-color;
+	}
+	&.is-plain {
+		color: $primary-color;
+		border:none;
+		background-color: #ffffff;
+	}
 }
 
+
 // 2. 全局重置样式(统一浏览器默认样式)
 * {
   box-sizing: border-box;
@@ -33,6 +43,10 @@ body ,htm{
   line-height: 1.6;
   margin: 0;
 }
+body{
+	background: url('@/assets/imgs/bg.png') no-repeat center center fixed;
+	background-size: 100% 100%;
+}
 
 // 3. 公共样式类(全局可复用)
 // 容器宽度限制(统一页面最大宽度,居中显示)