Guigu 甑选平台第二篇:登录模块完整实现指南

36 阅读12分钟

第一章:项目结构与路由配置

1.1 创建路由配置文件

首先按照项目文档创建路由配置文件,这一步建立应用的路由骨架:

第一步:创建路由配置文件

bash

复制下载

# 在src/router目录下创建routes.ts文件
# 这是路由配置文件,集中管理所有路由规则
touch src/router/routes.ts

第二步:编写路由配置代码

创建src/router/routes.ts文件,编写以下内容:

typescript

复制下载

export default [
    {//登录路由
      path:'/login',
      component:() =>import('@/views/login/MyLogin.vue'),
      name:'login'
    },
    {
      //登录成功后的路由
      path:'/',
      component:()=>import('@/views/home/MyHome.vue'),
      name:'home'
    },
    {//404
      path:'/404',
      component:()=>import('@/views/404/MyError.vue'),
      name:'404'
    },
    {//其他任意路由
      path:'/:pathMatch(.*)*',
      redirect:'/404',
      name:'Any'
    }
  ]

路由配置解析:

  1. 登录路由path: '/login'定义了登录页面的访问路径,用户输入/login时加载MyLogin.vue组件。
  2. 动态导入:使用() => import('@/路径')语法实现代码分割,每个路由对应的组件单独打包,减少初始加载时间。
  3. 路由名称:为每个路由设置name属性,方便在代码中通过名称跳转,避免硬编码路径字符串。
  4. 通配符路由path: '/:pathMatch(.*)*'匹配所有未定义的路由,将用户重定向到404页面,提供更好的用户体验。

第三步:配置路由实例

src/router/index.ts中引入路由配置:

typescript

复制下载

import {createRouter,createWebHashHistory} from 'vue-router'
//引入路由规则
import routes from "./routes"

const router = createRouter({
  history:createWebHashHistory(),
  routes,
  //滚动行为
  scrollBehavior() {
    return {
      left: 0,
      top: 0,
    }
  }
})

export default router;

第二章:登录页面开发

2.1 创建登录页面组件

第一步:创建登录页面文件结构

bash

复制下载

# 创建登录页面目录和文件
mkdir -p src/views/login
touch src/views/login/MyLogin.vue

第二步:编写登录页面模板

按照项目文档,创建登录页面的模板部分:

vue

复制下载

<template>
  <div class="login_container">
    <el-row>
      <el-col :span="12" :xs="0"></el-col>
      <el-col :span="12" :xs="24">
        <!-- 登录表单 -->
        <el-form class="login_form" :model="loginUserInfo" :rules="rules" ref="loginForm">
          <h1>Hello</h1>
          <h2>欢迎拉到硅谷甑选</h2>
          <el-form-item prop="username">
            <el-input :prefix-icon="User" v-model="loginUserInfo.username"></el-input>
          </el-form-item>
          <el-form-item prop="password">
            <el-input
              type="password"
              :prefix-icon="Lock"
              v-model="loginUserInfo.password"
              show-password
            ></el-input>
          </el-form-item>
          <el-form-item>
            <el-button
              class="login_btn"
              type="primary"
              size="default"
              @click="login"
              :loading="loading"
              >登录</el-button
            >
          </el-form-item>
        </el-form>
      </el-col>
    </el-row>
  </div>
</template>

模板解析:

  1. 响应式布局:使用Element Plus的栅格系统,span="12"表示在中等以上屏幕占12列(50%),xs="0"表示在超小屏幕隐藏,xs="24"表示在超小屏幕占24列(100%)。
  2. 表单绑定:model="loginUserInfo"将表单绑定到数据对象,:rules="rules"绑定验证规则。
  3. 图标使用:prefix-icon="User"使用Element Plus的图标组件,需要在script中导入。
  4. 密码框特性type="password"创建密码输入框,show-password添加眼睛图标切换密码可见性。

2.2 实现登录页面逻辑

第三步:编写登录页面脚本

按照项目文档,添加script部分:

vue

复制下载

<script setup lang="ts">
//引入icon图标
import { User, Lock } from "@element-plus/icons-vue";
import { reactive, ref } from "vue";
import useUserStore from "@/stores/modules/user";
import { useRouter } from "vue-router";
import { ElNotification } from "element-plus";
import { getTime } from "@/utils/time";
//获取用户store实例
const userStore = useUserStore();
//获取路由器
const router = useRouter();
//响应数据--用户信息
const loginUserInfo = reactive({
  username: "admin",
  password: "123456",
});
//响应式数据--loading状态
const loading = ref<boolean>(false);
//表单对象
const loginForm = ref();

//登录按钮回调
const login = async () => {
  //表单校验成功后在发送请求
  await loginForm.value.validate();
  //loading
  loading.value = true;
  //发送请求
  userStore
    .userLogin(loginUserInfo)
    .then((req) => {
      console.log(req); //'ok'
      //loading
      loading.value = false;
      //页面跳转
      router.push("/");
      //弹框
      ElNotification({
        type: "success",
        message: "欢迎回来",
        title: `Hi,${getTime()}好`,
      });
    })
    .catch((error) => {
      console.log(error);
      //loading
      loading.value = false;
      //弹框
      ElNotification({
        type: "error",
        message: error.message,
      });
    });
};
//自定义校验rules
const validatorUserName = (rule: any, value: any, callback: any) => {
  if (value.length >= 5) {
    callback();
  } else {
    callback(new Error("账号长度至少五位数"));
  }
};
const validatorPassWrod = (rule: any, value: any, callback: any) => {
  if (value.length >= 6) {
    callback();
  } else {
    callback(new Error("密码长度至少五位数"));
  }
};
//表单校验rules
const rules = {
  username: [
    {
      trriger: "change",
      //自定义表单校验
      validator: validatorUserName,
    },
  ],
  password: [
    {
      trriger: "change",
      validator: validatorPassWrod,
    },
  ],
};
</script>

脚本解析:

  1. 响应式数据

    • reactive({ username: '', password: '' }):创建响应式表单数据对象
    • ref(false):创建响应式加载状态
  2. 自定义验证函数

    • Element Plus验证函数接收三个参数:rule(规则对象)、value(字段值)、callback(回调函数)
    • 验证通过调用callback(),失败调用callback(new Error('错误信息'))
  3. 表单验证流程

    • 先调用loginForm.value.validate()验证所有字段
    • 验证通过后才执行登录请求
  4. 登录处理流程

    • 设置loading.value = true显示加载状态
    • 调用userStore.userLogin()发起登录请求
    • 成功:跳转首页并显示欢迎通知
    • 失败:显示错误通知

2.3 添加登录页面样式

第四步:编写登录页面样式

vue

复制下载

<style scoped>
.login_container {
  width: 100%;
  height: 100vh;
  background: url("@/assets/images/background.jpg") no-repeat;
  background-size: cover;
}
.login_form {
  position: relative;
  width: 80%;
  top: 30vh;
  background: url("@/assets/images/login_form.png") no-repeat;
  background-size: cover;
  padding: 40px;
}
h1 {
  color: white;
  font-size: 40px;
}
h2 {
  color: white;
  font-size: 20px;
  margin: 20px 0px;
}
.login_btn {
  width: 100%;
}
</style>

第三章:API服务层封装

3.1 创建API类型定义

第一步:创建类型定义文件

bash

复制下载

# 创建API类型定义文件
touch src/api/user/type.ts

第二步:编写类型定义代码

typescript

复制下载

//登录接口需要携带参数type
export interface loginForm{
  username:string,
  password:string
}

//登录接口返回数据type
interface dataType{
  token?:string,
  message?:string
}
export interface loginResponseData{
  code:number,
  data:dataType
}

//查询用户信息接口返回数据type'
interface userInfo {
  username:string,
  password:string,
  desc:string,
  roles:string[],
  buttons:string[],
  routes:string[],
  token:string
}
interface user {
  checkUser:userInfo
}
export interface userResponseData {
  code:number,
  data:user
}

类型定义说明:

  1. 接口分离:将请求参数和响应数据分别定义接口,提高代码可读性。
  2. 可选属性:使用?表示可选属性,如token?表示登录成功时返回,失败时不返回。
  3. 数组类型string[]表示字符串数组,用于权限列表。

3.2 创建API请求函数

第三步:创建API接口文件

bash

复制下载

# 创建用户API接口文件
touch src/api/user/index.ts

第四步:编写API接口代码

typescript

复制下载

//用户相关接口

import request from "@/utils/request";
//类型
import type { loginForm, loginResponseData, userResponseData } from "./type";
//统一管理接口
enum API {
  LOGIN_URL = "user/login",
  USERINFO_URL = "/user/info",
}

//暴露请求函数
//登录接口
export const reqLogin = (data: loginForm) => request.post<any, loginResponseData>(API.LOGIN_URL, data);

//获取用户信息接口
export const reqUserInfo = () =>request.get<any, userResponseData>(API.USERINFO_URL);

API设计要点:

  1. 枚举管理URL:使用TypeScript枚举集中管理所有接口URL,避免在代码中硬编码字符串。
  2. 类型安全:为每个API函数明确定义参数类型和返回类型,提供完整的TypeScript类型支持。
  3. 统一导出:每个API函数独立导出,方便按需导入。

第四章:状态管理实现

4.1 创建用户状态管理

第一步:创建用户store文件

bash

复制下载

# 创建用户状态管理文件
mkdir -p src/stores/modules
touch src/stores/modules/user.ts

第二步:编写用户store代码

typescript

复制下载

// src/stores/modules/user.ts
//用户仓库
import {defineStore} from 'pinia'
//用户登录接口
import {reqLogin} from '@/api/user'
//参数type
import type { loginForm ,loginResponseData} from '@/api/user/type'
import type { UserState } from '../types/type'
//存储和读取token方法
import { SET_TOKEN,GET_TOKEN } from '@/utils/token'
const useUserStore = defineStore('User',{
  state:():UserState=>{
    return {
      token:GET_TOKEN()
    }
  },
  actions:{
    //用户登录方法
    async userLogin(data:loginForm){
      const result:loginResponseData = await reqLogin(data);
      // console.log(result);
      if(result.code === 200){
        //登录成功,pinia存储token数据
        console.log('登录成功');
        this.token = result.data.token as string
        //本地持久化token
        SET_TOKEN(this.token as string)
        //返回一个成功的promise
        return 'ok'
      }else{
        return Promise.reject(new Error(result.data.message))
      }
    }
  },
  getters:{
    getToken:(state)=>state.token
  }

})
//暴露
export default useUserStore;

状态管理要点:

  1. 状态初始化:在state中定义初始状态,token从localStorage读取。
  2. 异步动作actions中定义异步方法userLogin,处理登录逻辑。
  3. 状态更新:登录成功后更新store中的token状态。
  4. 数据持久化:同时将token保存到localStorage,实现页面刷新后仍保持登录状态。
  5. 错误处理:登录失败时返回拒绝的Promise,携带错误信息。

4.2 创建token工具函数

第三步:创建token工具文件

bash

复制下载

# 创建token工具文件
touch src/utils/token.ts

第四步:编写token工具代码

typescript

复制下载

// src/utils/token.ts
//本地存储和读取token数据方法
export const SET_TOKEN = (token:string)=>{
  localStorage.setItem('TOKEN',token);
}
export const GET_TOKEN = ()=>{
  return localStorage.getItem('TOKEN' as string);
}

token管理说明:

  1. 统一管理:将token的存储和读取封装成函数,便于统一管理和修改。
  2. 简单可靠:使用localStorage存储,简单可靠,支持页面刷新后保持登录状态。
  3. 可扩展性:可以轻松添加其他token管理函数,如删除、检查等。

第五章:请求封装实现

5.1 创建请求封装文件

第一步:创建请求封装文件

bash

复制下载

# 创建axios封装文件
touch src/utils/request.ts

第二步:编写请求封装代码

typescript

复制下载

// src/utils/request.ts
//axios 二次封装
import axios from "axios"
import { ElMessage } from "element-plus";

const request = axios.create({
  baseURL:import.meta.env.VITE_APP_BASE_API,
  timeout:5000
});
//request实例添加请求与响应拦截器
request.interceptors.request.use((config) =>{
  //confog配置对象,headers属性请求头,携带公共参数
  return config;
})
//响应拦截器
request.interceptors.response.use((response)=>{
  return response.data;
},(error)=>{
  let message = '';
  const status = error.response.status;
  switch(status){
    case 401:
      message = 'TOKEN过期'
      break;
    case 403:
      message = '无权访问'
      break;
    case 404:
      message = '请求地址错误'
      break;
    case 500:
      message = '服务器出现问题'
      break;
    default:
      message = '网络出现问题'
      break;
  }
  ElMessage({
    type:'error',
    message
  });
  return Promise.reject(error);
})

export default request;

请求封装要点:

  1. 实例创建:使用axios.create()创建独立实例,可以设置默认配置。

  2. 环境变量import.meta.env.VITE_APP_BASE_API从Vite环境变量读取API基础URL。

  3. 请求拦截器:在请求发送前统一处理,如添加token到请求头。

  4. 响应拦截器

    • 成功:直接返回response.data,简化业务代码
    • 失败:根据HTTP状态码显示友好错误提示
  5. 错误统一处理:将HTTP错误转换为用户友好的中文提示。

第六章:Mock数据配置

6.1 创建Mock数据文件

第一步:创建Mock配置文件

bash

复制下载

# 创建Mock数据目录和文件
mkdir -p mock
touch mock/user.ts

第二步:编写Mock数据代码

typescript

复制下载

// mock/user.ts
// 用户相关的Mock数据,用于开发阶段模拟后端接口

/*
 * @Description: Stay hungry,Stay foolish
 * @Author: Huccct
 * @Date: 2024-03-21
 */

// 模拟用户列表数据
const userList = [
  {
    id: 1,
    username: 'admin',
    password: '123456',
    name: '超级管理员',
    phone: '13800138000',
    roleName: '超级管理员',
    createTime: '2024-03-21',
    updateTime: '2024-03-21',
    status: 1,
  },
  {
    id: 2,
    username: 'test',
    password: '123456',
    name: '测试用户',
    phone: '13800138001',
    roleName: '普通管理员',
    createTime: '2024-03-21',
    updateTime: '2024-03-21',
    status: 1,
  },
]

export default [
  // 用户登录接口
  {
    url: '/api/user/login',
    method: 'post',
    response: ({ body }) => {
      const { username, password } = body
      const checkUser = userList.find(
        (item) => item.username === username && item.password === password,
      )
      if (!checkUser) {
        return { code: 201, data: { message: '账号或者密码不正确' } }
      }
      return { code: 200, data: {token:'Admin Token' }}
    },
  },
  // 获取用户信息
  {
    url: '/api/user/info',
    method: 'get',
    response: (request) => {
      const token = request.headers.token
      if (token === 'Admin Token') {
        return {
          code: 200,
          data: {
            name: 'admin',
            avatar:
              'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif',
            roles: ['admin'],
            buttons: ['cuser.detail'],
            routes: [
              'home',
              'Acl',
              'User',
              'Role',
              'Permission',
              'Product',
              'Trademark',
              'Attr',
              'Spu',
              'Sku',
            ],
          },
          message: '获取用户信息成功',
        }
      }
      return {
        code: 201,
        data: null,
        message: '获取用户信息失败',
      }
    },
  },
  // 获取用户列表
  {
    url: '/api/acl/user/:page/:limit',
    method: 'get',
    response: ({ query }) => {
      const { username } = query
      let filteredList = userList
      if (username) {
        filteredList = userList.filter((user) =>
          user.username.includes(username),
        )
      }
      return {
        code: 200,
        data: {
          records: filteredList,
          total: filteredList.length,
        },
      }
    },
  },
  // 添加/更新用户
  {
    url: '/api/acl/user/save',
    method: 'post',
    response: ({ body }) => {
      const newUser = {
        ...body,
        id: userList.length + 1,
        createTime: new Date().toISOString().split('T')[0],
        updateTime: new Date().toISOString().split('T')[0],
        status: 1,
      }
      userList.push(newUser)
      return { code: 200, data: null, message: '添加成功' }
    },
  },
  {
    url: '/api/acl/user/update',
    method: 'put',
    response: ({ body }) => {
      const index = userList.findIndex((item) => item.id === body.id)
      if (index !== -1) {
        userList[index] = {
          ...userList[index],
          ...body,
          updateTime: new Date().toISOString().split('T')[0],
        }
      }
      return { code: 200, data: null, message: '更新成功' }
    },
  },
  // 删除用户
  {
    url: '/api/acl/user/remove/:id',
    method: 'delete',
    response: (request) => {
      const id = request.query.id
      if (!id) {
        return { code: 201, data: null, message: '参数错误' }
      }
      const index = userList.findIndex((item) => item.id === Number(id))
      if (index !== -1) {
        userList.splice(index, 1)
        return { code: 200, data: null, message: '删除成功' }
      }
      return { code: 201, data: null, message: '用户不存在' }
    },
  },
  // 批量删除用户
  {
    url: '/api/acl/user/batchRemove',
    method: 'delete',
    response: ({ body }) => {
      const { idList } = body
      idList.forEach((id) => {
        const index = userList.findIndex((item) => item.id === id)
        if (index !== -1) {
          userList.splice(index, 1)
        }
      })
      return { code: 200, data: null, message: '批量删除成功' }
    },
  },
  // 获取用户角色
  {
    url: '/api/acl/user/toAssign/:userId',
    method: 'get',
    response: () => {
      return {
        code: 200,
        data: {
          assignRoles: [
            {
              id: 1,
              roleName: '超级管理员',
              createTime: '2024-03-21',
              updateTime: '2024-03-21',
            },
          ],
          allRolesList: [
            {
              id: 1,
              roleName: '超级管理员',
              createTime: '2024-03-21',
              updateTime: '2024-03-21',
            },
            {
              id: 2,
              roleName: '普通管理员',
              createTime: '2024-03-21',
              updateTime: '2024-03-21',
            },
          ],
        },
      }
    },
  },
  // 分配用户角色
  {
    url: '/api/acl/user/doAssignRole',
    method: 'post',
    response: () => {
      return { code: 200, data: null, message: '分配角色成功' }
    },
  },
  // 用户登出接口
  {
    url: '/api/user/logout',
    method: 'post',
    response: () => {
      return { code: 200, data: null, message: '退出成功' }
    },
  },
]

Mock配置说明:

  1. 用户数据:定义模拟的用户数据,包含用户名、密码、角色和token。
  2. 登录验证:模拟后端的登录验证逻辑,检查用户名和密码是否匹配。
  3. token验证:获取用户信息时需要验证token,模拟真实的后端权限验证。
  4. 延迟设置timeout: 1000设置1秒延迟,模拟真实网络请求。

第七章:模块集成与测试

7.1 集成所有模块

第一步:在main.ts中集成路由和store

typescript

复制下载

// src/main.ts
import { createApp } from "vue";
import App from "./App.vue";
//引入element-plus插件
import ElementPlus from "element-plus";
//引入element-plus的样式
import "element-plus/dist/index.css";
//引入element的icons
import * as ElementPlusIconsVue from "@element-plus/icons-vue";
//引入路由
import router from "@/router/index";
//引入仓库
import pinia from "@/stores";
//全局样式重置
import "./styles/reset.css";

//创建应用实例
const app = createApp(App);
//使用插件
app.use(ElementPlus);
app.use(router);
app.use(pinia);
//全局注册icons
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
  app.component(key, component);
}
app.mount("#app");

第二步:创建并导出store

typescript

复制下载

//大仓库
import {createPinia} from 'pinia'

export default createPinia();

第八章:开发要点总结

8.1 核心实现要点

  1. 路由配置:使用动态导入实现路由懒加载,优化首屏性能。
  2. 表单验证:使用Element Plus表单验证,结合自定义验证函数实现复杂验证逻辑。
  3. 状态管理:使用Pinia集中管理用户状态,配合localStorage实现状态持久化。
  4. 请求封装:二次封装axios,统一处理请求和响应,简化错误处理。
  5. Mock数据:使用vite-plugin-mock提供开发阶段的API模拟,支持前后端并行开发。

8.2 代码组织规范

  1. 目录结构:按照功能模块组织代码,如views/api/stores/utils/
  2. 类型安全:使用TypeScript为所有接口和数据定义类型,提高代码可靠性。
  3. 配置集中:将路由、API URL、验证规则等配置集中管理,便于维护。