VUE后台管理系统:项目架构之搭建登录架构解决方案与实现

585 阅读11分钟

学习前提

会基本的vue,会使用AI助手,学习本课本的人必须长得帅。

使用脚手架初始化搭建项目

cn.vuejs.org/guide/quick…

安装UI组件库

参考最新的UI框架

element-plus.org/zh-CN/guide…

自动引入配置

npm install -D unplugin-vue-components unplugin-auto-import

import { fileURLToPath, URL } from 'node:url'

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'

// https://vite.dev/config/
export default defineConfig({
  plugins: [
    vue(),
    vueJsx(),
    AutoImport({
      resolvers: [ElementPlusResolver()],
    }),
    Components({
      resolvers: [ElementPlusResolver()],
    }),
  ],
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url)),
    },
  },
})

安装icon

npm install @element-plus/icons-vue

注册所有图标

您需要从 @element-plus/icons-vue 中导入所有图标并进行全局注册。

// main.ts

// 如果您正在使用CDN引入,请删除下面一行。
import * as ElementPlusIconsVue from '@element-plus/icons-vue'

const app = createApp(App)
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
  app.component(key, component)
}

这样我们就可以直接在vue文件使用icon

<template>
      <el-icon>
            <avatar />
      </el-icon>
</template>

构建登录页面 UI 结构

这里不做多说,体力活。

样式处理

创建全局的 style,我们使用sass。我们安装一下

npm install -D sass-embedded

找的代码库里面的vue-admin/src/assets/main.css 改成 mian.scss然后在main里面引入。

输入下面代码

html,
body {
  height: 100%;
  margin: 0;
  padding: 0;
  background-color: red;
  -moz-osx-font-smoothing: grayscale;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB,
    Microsoft YaHei, Arial, sans-serif;
}

#app {
  height: 100%;
}

*,
*:before,
*:after {
  box-sizing: inherit;
  margin: 0;
  padding: 0;
}

a:focus,
a:active {
  outline: none;
}

a,
a:focus,
a:hover {
  cursor: pointer;
  color: inherit;
  text-decoration: none;
}

div:focus {
  outline: none;
}

.clearfix {
  &:after {
    visibility: hidden;
    display: block;
    font-size: 0;
    content: ' ';
    clear: both;
    height: 0;
  }
}

然后遇到报错了

image.png

问了AI之后,可能是下面的问题

我们先从新启动项目。但是没解决。

那解决一下vite配置呢,先官网找一下。

cn.vitejs.dev/config/shar…

一不小心就被找到了。

查询到vite本身就支持scss,我们只需要安装scss的包就行。

npm i sass 

然后对vite进行配置

页面出现了红色背景,说明样式已经OK了。

接下来就是完善login页面,详情看代码库文件 src/views/LoginView.vue

Icon 图标处理方案:SvgIcon

在我们的项目中所使用的 icon 图标,一共分为两类:

  1. element-plus 的图标
  2. 自定义的 svg 图标

这也是通常情况下企业级项目开发时,所遇到的一种常见情况。

对于 element-plus 的图标我们可以直接通过 el-icon 来进行显示,但是自定义图标的话,我们暂时还缺少显示的方式,所以说我们需要一个自定义的组件,来显示我们自定义的 svg 图标。

那么这种自定义组件处理 自定义 svg 图标的形式,就是我们在面临这种问题时的通用解决方案。

那么对于这个组件的话,它就需要拥有两种能力:

  1. 显示外部 svg 图标
  2. 显示项目内的 svg 图标

基于以上概念,我们可以创建出以下对应代码:

创建 components/SvgIcon/index.vue

<template>
  <div
    v-if="isExternal"
    :style="styleExternalIcon"
    class="svg-external-icon svg-icon"
    :class="className"
  />
  <svg v-else class="svg-icon" :class="className" aria-hidden="true">
    <use :xlink:href="iconName" />
  </svg>
</template>

<script setup>
import { isExternal as external } from '@/utils/validate'
import { defineProps, computed } from 'vue'
const props = defineProps({
  // icon 图标
  icon: {
    type: String,
    required: true
  },
  // 图标类名
  className: {
    type: String,
    default: ''
  }
})

/**
 * 判断是否为外部图标
 */
const isExternal = computed(() => external(props.icon))
/**
 * 外部图标样式
 */
const styleExternalIcon = computed(() => ({
  mask: `url(${props.icon}) no-repeat 50% 50%`,
  '-webkit-mask': `url(${props.icon}) no-repeat 50% 50%`
}))
/**
 * 项目内图标
 */
const iconName = computed(() => `#icon-${props.icon}`)
</script>

<style scoped>
.svg-icon {
  width: 1em;
  height: 1em;
  vertical-align: -0.15em;
  fill: currentColor;
  overflow: hidden;
}

.svg-external-icon {
  background-color: currentColor;
  mask-size: cover !important;
  display: inline-block;
}
</style>

创建 utils/validate.js

/**
 * 判断是否为外部资源
 */
export function isExternal(path) {
  return /^(https?:|mailto:|tel:)/.test(path)
}

views/login/index.vue 中使用 外部 svghttps://res.lgdsunday.club/user.svg):

<span class="svg-container">
    <svg-icon icon="https://res.lgdsunday.club/user.svg"></svg-icon>
</span>

外部图标可正常展示。

配置@绝对路径引入

直接在vite里面配置就行,下面的AI的回答。

import path from 'path'
export default defineConfig({
  resolve: {
    alias: {
      '@': path.resolve(__dirname, '/src'), // 假设您的源代码在 src 目录
    },
  },
})

处理内部 svg 图标显示

本地图标需要在vue全局注册,因为我们使用的是vite。下面的方案,同样是AI给我的。

npm install vite-plugin-svg-icons -D

vite文件里面配置

import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'
import path from 'path'

export default () => {
  return {
    plugins: [
      createSvgIconsPlugin({
        iconDirs: [path.resolve(process.cwd(), 'src/assets/icons')],
        symbolId: 'icon-[dir]-[name]',
      }),
    ],
  }
}

配置 main.ts

import 'virtual:svg-icons-register'

  • login/index.vue 中使用 SvgIcon 引入本地 svg

    // 用户名   
    <svg-icon icon="user" />
    // 密码
    <svg-icon icon="password" />
    // 眼睛
    <svg-icon icon="eye" />
    
  • 此时 处理内容 svg 图标的代码 已经完成。

完善登录表单校验

  1. 密码框小眼睛

对于密码框存在两种状态,密文状态,明文状态。点击眼睛可以进行切换。

该功能实现为通用的处理方案,只需要动态修改input的type类型即可,其中: password为密文显示,text为明文显示 根据以上理论,即可得出以下代码: 写一个变量+点击事件控制就完事。

<el-form-item prop="password">
  <span class="svg-container">
    <svg-icon icon="password" />
  </span>
  <el-input
    v-model="form.password"
    placeholder="password"
    name="password"
    :type="passwordType"
  />
  <span class="show-pwd">
    <svg-icon
      :icon="passwordType === 'password' ? 'eye' : 'eye-open'"
      @click="onChangePwdType"
    />
  </span>
</el-form-item>
import { reactive, ref } from 'vue'

const passwordType = ref('password')

const onChange
PwdType = () => {
  passwordType.value = passwordType.value === 'password' ? 'text' : 'password'
}

配置环境变量封装 axios 模块

npm i axios 安装起来,宝贝。

首先我们先去完成第一步:封装 axios 模块。

在当前这个场景下,我们希望封装出来的 axios 模块,至少需要具备一种能力,那就是:根据当前模式的不同,设定不同的 BaseUrl ,因为通常情况下企业级项目在 开发状态生产状态 下它的 baseUrl 是不同的。

对于 @vue/cli 来说,它具备三种不同的模式:

  1. development
  2. test
  3. production

具体可以点击 这里 进行参考。

根据我们前面所提到的 开发状态和生产状态 那么此时我们的 axios 必须要满足:在 开发 || 生产 状态下,可以设定不同 BaseUrl 的能力

那么想要解决这个问题,就必须要使用到 @vue/cli 所提供的 环境变量 来去进行实现。

我们可以在项目中根目录文件夹下面,创建两个文件:

  1. .env.development
  2. .env.production

它们分别对应 开发状态生产状态

我们可以在上面两个文件中分别写入以下代码:

.env.development

# 标志
# just a flag
ENV = 'development'

# base api
VITE_APP_BASE_API = '/api'

.env.production

# 标志
ENV = 'production'

# base api
VITE_APP_BASE_API = '/prod-api'

有了这两个文件之后,我们就可以创建对应的 axios 模块

创建 utils/request.js ,写入如下代码:

import axios from 'axios'

const service = axios.create({
  baseURL: process.env.VITE_APP_BASE_API, 
  timeout: 5000
})

export default service

但是会出现ReferenceError: process is not defined的错误通常意味着你的代码在尝试访问process对象,但是该对象在你当前的执行环境中并不可用。

要解决这个问题,你可以尝试以下步骤:

确保.env文件存在并正确配置: 在项目根目录下,确保你有一个.env文件(或者.env.development.env.production等,具体取决于你的环境),并且它包含了VUE_APP_BASE_API这个环境变量。

检查Vite配置: 由于你的项目使用的是Vite,而不是Vue CLI,你需要在Vite配置中确保环境变量被正确加载。在Vite中,只有以VITE_开头的环境变量才会被暴露给你的应用程序。因此,你可能需要将VUE_APP_BASE_API更名为VITE_APP_BASE_API,并在你的代码中相应地更新它。

使用import.meta.env代替process.env: 在Vite项目中,你应该使用import.meta.env来访问环境变量,而不是process.env。因此,你的代码应该更改为:

import axios from 'axios'

const service = axios.create({
  baseURL: import.meta.env.VITE_APP_BASE_API, // 注意这里的更改
  timeout: 5000,
})

export default service

封装请求动作

有了 axios 模块之后,接下来我们就可以

  1. 封装接口请求模块
  2. 封装登录请求动作

封装接口请求模块:

创建 api 文件夹,创建 sys.js

import request from '@/utils/request'

/**
 * 登录
 */
export const login = data => {
  return request({
    url: '/sys/login',
    method: 'POST',
    data,
  })
}

/**
 * 获取用户信息
 */
export const getUserInfo = () => {
  return request({
    url: '/sys/profile',
  })
}

封装登录请求动作:

该动作我们期望把它封装到 piniaaction 中。我也是第一次使用pinia ,现学现用就行。

store 下创建 auth.js 文件,用于处理所有和 用户相关 的内容(此处需要使用第三方包 md5 ), 同时使用localStorage 进行数据存储处理:

import { defineStore } from 'pinia'
import { login, getUserInfo } from '@/api/sys'

export const useAuthStore = defineStore('auth', {
  state: () => ({
    token: null,
    user: null,
  }),
  actions: {
    async login(username, password) {
      try {
        const response = await login({ username, password })
        this.token = response.data.token
        this.user = response.data.user
        localStorage.setItem('token', this.token)
        return true
      } catch (error) {
        console.error('Login failed:', error)
        return false
      }
    },
    logout() {
      this.token = null
      this.user = null
      localStorage.removeItem('token')
    },
  },
  getters: {
    isLoggedIn: state => !!state.token,
  },
})

登录触发动作

login中,触发上面定义的action

<template>
  <el-button
    type="primary"
    style="width: 100%; margin-bottom: 30px"
    @click="onSubmit"
    >登录</el-button
  >
</template>

<script lang="ts" setup>
import { reactive, ref } from 'vue'
import { useAuthStore } from '@/stores/auth'

// do not use same name with ref
const form = reactive({
  name: '',
  password: '',
})
const authStore = useAuthStore()
const onSubmit = async () => {
  console.log('submit!', form.name, form.password)
  if (await authStore.login(username.value, password.value)) {
    alert('Login successful!')
  } else {
    alert('Login failed!')
  }
}
</script>

触发之后会得到以下错误:

该错误表示,我们当前请求的接口不存在。

出现这个问题的原因,是因为我们在前面配置环境变量时指定了 开发环境下,请求的 BaseUrl/api ,所以我们真实发出的请求为:/api/sys/login

这样的一个请求会被自动键入到当前前端所在的服务中,所以我们最终就得到了 http://192.168.18.42:8081/api/sys/login 这样的一个请求路径。

而想要处理这个问题,那么可以通过指定 vite DevServer 代理 的形式,代理当前的 url 请求。

而指定这个代理非常简单,是一种近乎固定的配置方案。

配置代理: 在 vite.config.js 文件中,添加或修改 proxy 选项,如下所示:

// vite.config.js
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [vue()],
  server: {
   proxy: {
     // 匹配所有以 /api 开头的请求
     '/api': {
       target: 'https://api.imooc-admin.lgdsunday.club/', // 你的后端服务地址
       changeOrigin: true, // 必须设置为true,否则会请求到代理服务器
       rewrite: (path) => path.replace(/^\/api/, '') // 重写路径,去掉路径中的 /api
     }
   }
  }
})

在这个配置中,所有发送到 /api 的请求都会被代理到 http://192.168.18.42:8081,并且会自动去掉请求路径中的 /api 前缀。

保存 vite.config.js 文件后,重启 Vite 开发服务器以使配置生效。

现在,当你在前端代码中发送请求到 /api/sys/login 时,Vite 会自动将其代理到 http://192.168.18.42:8081/sys/login

这样配置后,你就可以在开发环境下避免跨域问题,并且能够正确地发送请求到你的后端服务。如果你遇到任何问题,确保检查代理配置是否正确,以及后端服务是否运行并能够响应请求。

响应数据的统一处理

我们保存了服务端返回的 token 。但是有一个地方比较难受,那就是在 vuex 的 user 模块 中,我们获取数据端的 token 数据,通过 data.data.data.token 的形式进行获取。

一路的 data. 确实让人比较难受,如果有过 axios 拦截器处理经验的同学应该知道,对于这种问题,我们可以通过 axios 响应拦截器 进行处理。

utils/request.js 中实现以下代码:

import axios from 'axios'
import { ElMessage } from 'element-plus'

...
// 响应拦截器
service.interceptors.response.use(
  response => {
    const { success, message, data } = response.data
    //   要根据success的成功与否决定下面的操作
    if (success) {
      return data
    } else {
      // 业务错误
      ElMessage.error(message) // 提示错误消息
      return Promise.reject(new Error(message))
    }
  },
  error => {
    // TODO: 将来处理 token 超时问题
    ElMessage.error(error.message) // 提示错误信息
    return Promise.reject(error)
  }
)

export default service

登录后操作

那么截止到此时,我们距离登录操作还差最后一个功能就是 登录鉴权

只不过在进行 登录鉴权 之前我们得先去创建一个登录后的页面,也就是我们所说的登录后操作。

  1. HomeView.vue,写入以下代码:
<template>
  <div class="">Layout 页面</div>
</template>

<script setup>
import {} from 'vue'
</script>

<style lang="scss" scoped></style>
  1. router/index 中,指定对应路由表:
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
  {
    path: '/',
    name: 'home',
    component: HomeView,
  },
})
  1. 在登录成功后,完成跳转
// src/views/LoginView.vue
import { useRouter } from 'vue-router'
const router = useRouter()
const onSubmit = async () => {
  console.log('submit!', form.name, form.password)
  if (await authStore.login(form.name, form.password)) {
    alert('Login successful!')
    // 登录后操作
    router.push('/')
  } else {
    alert('Login failed!')
  }
}  

登录鉴权解决方案

在处理了登陆后操作之后,接下来我们就来看一下最后的一个功能,也就是 登录鉴权

首先我们先去对 登录鉴权 进行一个定义,什么是 登录鉴权 呢?

当用户未登陆时,不允许进入除 login 之外的其他页面。

用户登录后,token 未过期之前,不允许进入 login 页面

而想要实现这个功能,那么最好的方式就是通过 路由守卫 来进行实现。

那么明确好了 登录鉴权 的概念之后,接下来就可以去实现一下。

在此处我们使用到了 pinia 中的 getters ,此时的 getters 被当作 快捷访问 的形式进行访问。

getters: {
  isLoggedIn: state => !!state.token,
}

router/index.js文件

import { createRouter, createWebHistory } from 'vue-router'
import { useAuthStore } from '../stores/auth'
import HomeView from '../views/HomeView.vue'

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    {
      path: '/',
      name: 'home',
      component: HomeView,
    },
    {
      path: '/login',
      name: 'login',
      // route level code-splitting
      // this generates a separate chunk (login.[hash].js) for this route
      // which is lazy-loaded when the route is visited.
      component: () => import('../views/loginView.vue'),
    },
  ],
})

router.beforeEach((to, from, next) => {
  const authStore = useAuthStore()
  if (to.name === 'login' && authStore.isLoggedIn) {
    // 如果用户已经登录,尝试访问登录页面,则重定向到首页
    next({ name: 'home' })
  } else if (to.name !== 'login' && !authStore.isLoggedIn) {
    // 如果用户未登录,尝试访问非登录页面,则重定向到登录页面
    next({ name: 'login' })
  } else {
    // 其他情况,正常放行
    next()
  }
})

export default router

源码已同步到 vue-admin 包里,感兴趣的可以自行下载了解。

总结

本次学到了以下知识

  1. 登录方案相关的业务代码

    1. element-plus 相关

      1. el-form 表单
      2. 密码框状态处理
    2. 后台登录解决方案

      1. 封装 axios 模块
      2. 封装 接口请求 模块
      3. 封装登录请求动作
      4. 保存服务端返回的 token
      5. 登录鉴权