Prechádzať zdrojové kódy

feat(login): 完善登录功能实现用户认证流程

- 实现头部登录状态判断,未登录显示登录注册入口并支持点击跳转
- 新增ParentView组件用于路由视图渲染
- 添加InnerLink组件支持内嵌页面链接功能
- 重构登录页面,实现账户登录和验证码登录两种方式
- 集成表单验证、图标前缀、加载状态等用户体验优化
- 实现短信验证码发送倒计时功能
- 完善登录权限验证逻辑,修复路由守卫代码注释问题
- 扩展登录数据类型定义,增加手机和验证码字段支持
肖路 2 mesiacov pred
rodič
commit
373f487f47

+ 2 - 0
src/api/types.ts

@@ -26,6 +26,8 @@ export interface LoginData {
   uuid?: string;
   clientId: string;
   grantType: string;
+  mobile?: string;
+  smsCode?: string;
 }
 
 /**

+ 4 - 0
src/components/ParentView/index.vue

@@ -0,0 +1,4 @@
+<template>
+  <router-view />
+</template>
+<script setup lang="ts"></script>

+ 15 - 0
src/layout/components/InnerLink/index.vue

@@ -0,0 +1,15 @@
+<template>
+  <div :style="'height:' + height">
+    <iframe :id="iframeId" style="width: 100%; height: 100%; border: 0" :src="src"></iframe>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { propTypes } from '@/utils/propTypes';
+
+const props = defineProps({
+  src: propTypes.string.def('/'),
+  iframeId: propTypes.string.isRequired
+});
+const height = ref(document.documentElement.clientHeight - 94.5 + 'px');
+</script>

+ 17 - 3
src/layout/components/header.vue

@@ -7,8 +7,8 @@
         <div>武汉</div>
       </div>
       <div class="header-box flex-row-start">
-        <div class="header-text">您好,请登录</div>
-        <div class="header-text hig">免费注册</div>
+        <div v-if="!isLoggedIn" class="header-text" @click="goToLogin" style="cursor: pointer">请登录</div>
+        <div v-if="!isLoggedIn" class="header-text hig">免费注册</div>
         <div class="header-text">我的订单</div>
         <div class="header-text">会员中心</div>
         <div class="header-text">人才招聘</div>
@@ -19,7 +19,21 @@
   </div>
 </template>
 
-<script setup lang="ts"></script>
+<script setup lang="ts">
+import { useRouter } from 'vue-router';
+import { useUserStore } from '@/store/modules/user';
+import { computed } from 'vue';
+
+const router = useRouter();
+const userStore = useUserStore();
+
+// 判断是否已登录
+const isLoggedIn = computed(() => !!userStore.token);
+
+const goToLogin = () => {
+  router.push('/login');
+};
+</script>
 
 <style lang="scss" scoped>
 .header {

+ 44 - 45
src/permission.ts

@@ -19,51 +19,50 @@ const isWhiteList = (path: string) => {
 
 router.beforeEach(async (to, from, next) => {
   NProgress.start();
-  next();
-  // if (getToken()) {
-  //   to.meta.title && useSettingsStore().setTitle(to.meta.title as string);
-  //   /* has token*/
-  //   if (to.path === '/login') {
-  //     next({ path: '/' });
-  //     NProgress.done();
-  //   } else if (isWhiteList(to.path)) {
-  //     next();
-  //   } else {
-  //     if (useUserStore().roles.length === 0) {
-  //       isRelogin.show = true;
-  //       // 判断当前用户是否已拉取完user_info信息
-  //       const [err] = await tos(useUserStore().getInfo());
-  //       if (err) {
-  //         await useUserStore().logout();
-  //         ElMessage.error(err);
-  //         next({ path: '/' });
-  //       } else {
-  //         isRelogin.show = false;
-  //         const accessRoutes = await usePermissionStore().generateRoutes();
-  //         // 根据roles权限生成可访问的路由表
-  //         accessRoutes.forEach((route) => {
-  //           if (!isHttp(route.path)) {
-  //             router.addRoute(route); // 动态添加可访问路由表
-  //           }
-  //         });
-  //         // @ts-expect-error hack方法 确保addRoutes已完成
-  //         next({ path: to.path, replace: true, params: to.params, query: to.query, hash: to.hash, name: to.name as string }); // hack方法 确保addRoutes已完成
-  //       }
-  //     } else {
-  //       next();
-  //     }
-  //   }
-  // } else {
-  //   // 没有token
-  //   if (isWhiteList(to.path)) {
-  //     // 在免登录白名单,直接进入
-  //     next();
-  //   } else {
-  //     const redirect = encodeURIComponent(to.fullPath || '/');
-  //     next(`/login?redirect=${redirect}`); // 否则全部重定向到登录页
-  //     NProgress.done();
-  //   }
-  // }
+  if (getToken()) {
+    to.meta.title && useSettingsStore().setTitle(to.meta.title as string);
+    /* has token*/
+    if (to.path === '/login') {
+      next({ path: '/' });
+      NProgress.done();
+    } else if (isWhiteList(to.path)) {
+      next();
+    } else {
+      if (useUserStore().roles.length === 0) {
+        isRelogin.show = true;
+        // 判断当前用户是否已拉取完user_info信息
+        const [err] = await tos(useUserStore().getInfo());
+        if (err) {
+          await useUserStore().logout();
+          ElMessage.error(err);
+          next({ path: '/' });
+        } else {
+          isRelogin.show = false;
+          const accessRoutes = await usePermissionStore().generateRoutes();
+          // 根据roles权限生成可访问的路由表
+          accessRoutes.forEach((route) => {
+            if (!isHttp(route.path)) {
+              router.addRoute(route); // 动态添加可访问路由表
+            }
+          });
+          // @ts-expect-error hack方法 确保addRoutes已完成
+          next({ path: to.path, replace: true, params: to.params, query: to.query, hash: to.hash, name: to.name as string }); // hack方法 确保addRoutes已完成
+        }
+      } else {
+        next();
+      }
+    }
+  } else {
+    // 没有token
+    if (isWhiteList(to.path)) {
+      // 在免登录白名单,直接进入
+      next();
+    } else {
+      const redirect = encodeURIComponent(to.fullPath || '/');
+      next(`/login?redirect=${redirect}`); // 否则全部重定向到登录页
+      NProgress.done();
+    }
+  }
 });
 
 router.afterEach(() => {

+ 215 - 31
src/views/login.vue

@@ -4,45 +4,201 @@
     <div class="login-info flex-row-between">
       <div></div>
       <div class="login-bos">
-        <div class="login-type flex-row-between">
-          <div :class="type == 1 ? 'hig' : ''" @click="onType(1)">密码登录</div>
-          <div class="border"></div>
-          <div :class="type == 2 ? 'hig' : ''" @click="onType(2)">短信登录</div>
-        </div>
-        <template v-if="type == 1">
-          <el-input class="login-input" v-model="loginForm.username" placeholder="账号名" />
-          <el-input style="margin-top: 18px" type="password" class="login-input" v-model="loginForm.password" placeholder="请输入登录密码" />
-        </template>
-        <template v-else>
-          <el-input :maxlength="11" class="login-input" v-model="loginForm.mobile" placeholder="手机号" />
-          <el-input style="margin-top: 18px" :maxlength="6" class="login-input" v-model="loginForm.mobile" placeholder="手机号">
-            <template #suffix>
-              <span class="code">发送验证码</span>
-            </template>
-          </el-input>
-        </template>
-        <el-button class="login-btn" type="primary">登录</el-button>
-        <div class="login-text flex-row-between">
-          <div>忘记密码</div>
-          <div class="border"></div>
-          <div>免费注册</div>
-        </div>
+        <el-form ref="loginRef" :model="loginForm" :rules="loginRules">
+          <div class="login-type flex-row-between">
+            <div :class="type == 1 ? 'hig' : ''" @click="onType(1)">账户登录</div>
+            <div class="border"></div>
+            <div :class="type == 2 ? 'hig' : ''" @click="onType(2)">验证码登录</div>
+          </div>
+          <template v-if="type == 1">
+            <el-form-item prop="username">
+              <el-input class="login-input" v-model="loginForm.username" placeholder="员工编号/手机号码">
+                <template #prefix>
+                  <el-icon><User /></el-icon>
+                </template>
+              </el-input>
+            </el-form-item>
+            <el-form-item prop="password">
+              <el-input
+                class="login-input"
+                type="password"
+                v-model="loginForm.password"
+                placeholder="输入登录密码"
+                @keyup.enter="handleLogin"
+              >
+                <template #prefix>
+                  <el-icon><Lock /></el-icon>
+                </template>
+              </el-input>
+            </el-form-item>
+          </template>
+          <template v-else>
+            <el-form-item prop="mobile">
+              <el-input :maxlength="11" class="login-input" v-model="loginForm.mobile" placeholder="手机号">
+                <template #prefix>
+                  <el-icon><Iphone /></el-icon>
+                </template>
+              </el-input>
+            </el-form-item>
+            <el-form-item prop="smsCode">
+              <el-input :maxlength="6" class="login-input" v-model="loginForm.smsCode" placeholder="验证码">
+                <template #prefix>
+                  <el-icon><Message /></el-icon>
+                </template>
+                <template #suffix>
+                  <span class="code" @click="sendSmsCode">{{ smsCodeText }}</span>
+                </template>
+              </el-input>
+            </el-form-item>
+          </template>
+          <el-form-item>
+            <el-button class="login-btn" type="primary" :loading="loading" @click.prevent="handleLogin">
+              <span v-if="!loading">登录</span>
+              <span v-else>登录中...</span>
+            </el-button>
+          </el-form-item>
+          <div class="login-text flex-row-between">
+            <div @click="handleForgetPassword">忘记密码?</div>
+            <div class="border"></div>
+            <router-link to="/register" class="register-link">新用户注册</router-link>
+          </div>
+        </el-form>
       </div>
     </div>
   </div>
 </template>
 
 <script setup lang="ts">
-const type = ref<any>(1);
+import { useUserStore } from '@/store/modules/user';
+import { LoginData } from '@/api/types';
+import { to } from 'await-to-js';
+import { User, Lock, Iphone, Message } from '@element-plus/icons-vue';
 
-const loginForm = ref<any>({
+const userStore = useUserStore();
+const router = useRouter();
+
+const type = ref<number>(1);
+const loading = ref(false);
+const smsCodeText = ref('发送验证码');
+const smsCountdown = ref(0);
+const loginRef = ref<ElFormInstance>();
+const redirect = ref('/');
+
+const loginForm = ref<LoginData>({
   username: 'admin',
   password: 'admin123',
-  mobile: ''
-});
-const onType = (res: any) => {
-  type.value = res;
+  mobile: '',
+  smsCode: '',
+  tenantId: '000000',
+  rememberMe: false,
+  code: '',
+  uuid: '',
+  clientId: '',
+  grantType: 'password'
+} as LoginData);
+
+const loginRules: ElFormRules = {
+  username: [{ required: true, trigger: 'blur', message: '请输入员工编号/手机号码' }],
+  password: [{ required: true, trigger: 'blur', message: '请输入登录密码' }],
+  mobile: [{ required: true, trigger: 'blur', message: '请输入手机号' }],
+  smsCode: [{ required: true, trigger: 'blur', message: '请输入验证码' }]
+};
+
+const onType = (val: number) => {
+  type.value = val;
+  // 切换登录类型时更新 grantType
+  loginForm.value.grantType = val === 1 ? 'password' : 'sms';
+};
+
+/**
+ * 监听路由变化,获取重定向地址
+ */
+watch(
+  () => router.currentRoute.value,
+  (newRoute: any) => {
+    redirect.value = newRoute.query && newRoute.query.redirect && decodeURIComponent(newRoute.query.redirect);
+  },
+  { immediate: true }
+);
+
+/**
+ * 处理登录
+ */
+const handleLogin = () => {
+  loginRef.value?.validate(async (valid: boolean) => {
+    if (valid) {
+      loading.value = true;
+      // 勾选记住密码时保存到 localStorage
+      if (loginForm.value.rememberMe) {
+        localStorage.setItem('username', String(loginForm.value.username));
+        localStorage.setItem('password', String(loginForm.value.password));
+        localStorage.setItem('rememberMe', String(loginForm.value.rememberMe));
+      } else {
+        localStorage.removeItem('username');
+        localStorage.removeItem('password');
+        localStorage.removeItem('rememberMe');
+      }
+      // 调用登录
+      const [err] = await to(userStore.login(loginForm.value));
+      if (!err) {
+        const redirectUrl = redirect.value || '/';
+        await router.push(redirectUrl);
+        loading.value = false;
+      } else {
+        loading.value = false;
+        ElMessage.error('登录失败,请检查用户名和密码');
+      }
+    }
+  });
+};
+
+/**
+ * 发送短信验证码
+ */
+const sendSmsCode = () => {
+  if (smsCountdown.value > 0) return;
+  if (!loginForm.value.mobile) {
+    ElMessage.warning('请输入手机号');
+    return;
+  }
+  // TODO: 调用发送短信验证码接口
+  ElMessage.success('验证码已发送');
+  smsCountdown.value = 60;
+  const timer = setInterval(() => {
+    smsCountdown.value--;
+    if (smsCountdown.value > 0) {
+      smsCodeText.value = `${smsCountdown.value}s后重发`;
+    } else {
+      smsCodeText.value = '发送验证码';
+      clearInterval(timer);
+    }
+  }, 1000);
 };
+
+/**
+ * 忘记密码
+ */
+const handleForgetPassword = () => {
+  ElMessage.info('请联系管理员重置密码');
+};
+
+/**
+ * 获取保存的登录信息
+ */
+const getLoginData = () => {
+  const username = localStorage.getItem('username');
+  const password = localStorage.getItem('password');
+  const rememberMe = localStorage.getItem('rememberMe');
+  if (username && password && rememberMe) {
+    loginForm.value.username = username;
+    loginForm.value.password = password;
+    loginForm.value.rememberMe = Boolean(rememberMe);
+  }
+};
+
+onMounted(() => {
+  getLoginData();
+});
 </script>
 
 <style lang="scss" scoped>
@@ -80,6 +236,15 @@ const onType = (res: any) => {
       background: #ffffff;
       border-radius: 30px 30px 30px 30px;
       padding: 90px 85px 0 85px;
+
+      :deep(.el-form-item) {
+        margin-bottom: 18px;
+      }
+
+      :deep(.el-form-item__error) {
+        padding-top: 4px;
+      }
+
       .login-type {
         font-weight: 600;
         font-size: 22px;
@@ -110,16 +275,25 @@ const onType = (res: any) => {
       }
       :deep(.el-input__wrapper) {
         border: none;
-        /* 可选:去除聚焦时的高亮 */
         box-shadow: none;
         outline: none;
         background: #f4f6f8;
       }
+      :deep(.el-input__prefix) {
+        font-size: 18px;
+        color: #9ca3af;
+      }
       .login-btn {
         width: 350px;
         height: 50px;
-        margin-top: 60px;
+        margin-top: 20px;
         font-size: 16px;
+        background-color: #c8102e;
+        border-color: #c8102e;
+        &:hover {
+          background-color: #a50d26;
+          border-color: #a50d26;
+        }
       }
       .login-text {
         font-size: 14px;
@@ -128,12 +302,22 @@ const onType = (res: any) => {
         margin-top: 14px;
         div {
           cursor: pointer;
+          &:hover {
+            color: #c8102e;
+          }
         }
         .border {
           width: 1px;
           height: 12px;
           background: #e6e8ec;
         }
+        .register-link {
+          color: #c8102e;
+          text-decoration: none;
+          &:hover {
+            text-decoration: underline;
+          }
+        }
       }
     }
   }