当前位置:   article > 正文

Vue.js

Vue.js

# Vue 实战项目:使用 Vue 3 和 Element Plus 开发一个后台管理系统

## 项目简介

在这个项目中,我们将使用 Vue 3 和 Element Plus 开发一个后台管理系统,实现以下功能:

- 用户登录和注销
- 动态生成侧边栏菜单和面包屑导航
- 使用表格、表单、图表等组件展示和操作数据
- 使用 axios 和 JSON Web Token 实现数据请求和身份验证
- 使用 vue-router 实现路由跳转和权限控制
- 使用 vuex 实现状态管理和数据缓存

## 项目准备

### 环境搭建

要开始这个项目,我们需要安装以下工具:

- Node.js 14.16.0 或以上版本
- npm 6.14.11 或以上版本
- Vue CLI 4.5.12 或以上版本

我们可以使用 npm 或 yarn 来安装 Vue CLI,然后使用 Vue CLI 来创建项目:

```bash
npm install -g @vue/cli
# 或
yarn global add @vue/cli

vue create vue-admin

在创建项目的过程中,我们可以选择默认的预设选项(包含了 Vue 3、Babel 和 ESLint),或者自定义我们需要的功能。在这个项目中,我们选择了以下选项:
- Vue 3
- Babel
- TypeScript
- Router
- Vuex
- CSS Pre-processors(选择了 Sass/SCSS)
- Linter / Formatter(选择了 ESLint + Prettier)
- Unit Testing(选择了 Jest)
- E2E Testing(选择了 Cypress)
创建项目完成后,我们可以进入项目目录,然后运行以下命令来启动开发服务器:

npm run serve
# 或
yarn serve

打开浏览器,访问 http://localhost:8080 ,我们就可以看到一个简单的 Vue 应用页面。
依赖安装接下来,我们需要安装一些项目所需的依赖包,包括:
- Element Plus:一个基于 Vue 3 的 UI 组件库,提供了丰富的组件和主题定制功能。
- axios:一个基于 promise 的 HTTP 库,可以用于浏览器和 Node.js 中发送数据请求。
- js-cookie:一个简单的 JavaScript API,用于处理 cookie。
- nprogress:一个轻量级的进度条库,可以用于显示页面加载或数据请求的进度。
- dayjs:一个轻量级的 JavaScript 日期库,可以用于格式化、解析和操作日期和时间。
我们可以使用 npm 或 yarn 来安装这些依赖包:

npm install element-plus axios js-cookie nprogress dayjs
# 或
yarn add element-plus axios js-cookie nprogress dayjs

项目开发目录结构在开始编写代码之前,我们先来规划一下我们的项目目录结构。我们的目录结构大致如下:

src
├── api # 存放封装的 axios 请求方法
├── assets # 存放静态资源,如图片、字体等
├── components # 存放公共组件,如头部、侧边栏等
├── layout # 存放布局组件,如默认布局、登录布局等
├── router # 存放路由配置文件
├── store # 存放 vuex 状态管理文件
├── styles # 存放全局样式文件
├── utils # 存放工具函数,如权限判断、时间格式化等
├── views # 存放页面组件,如首页、登录页等
├── App.vue # 根组件
└── main.ts # 入口文件

用户登录和注销我们先来实现用户登录和注销的功能。为了简化演示,我们不使用真实的后端接口,而是使用一个在线的模拟接口服务 MockAPI 。我们在 MockAPI 上创建了一个项目,其中包含了一个名为 users 的资源,有以下字段:
- id:用户的唯一标识,自动生成的数字
- username:用户名,字符串
- password:密码,字符串
- token:令牌,字符串
我们可以通过以下地址访问这个资源:

https://60c9f9df772a760017204c05.mockapi.io/api/v1/users

我们可以使用 axios 来发送 GET、POST、PUT、DELETE 等请求,获取或修改这个资源的数据。为了方便管理,我们在 src/api 目录下创建一个 user.ts 文件,用于封装和用户相关的请求方法。例如,我们可以定义一个 login 方法,用于根据用户名和密码登录,并返回一个包含用户信息和令牌的对象:

// src/api/user.ts
import axios from 'axios'

// 定义用户接口
interface User {
  id: number
  username: string
  password: string
  token: string
}

// 定义登录参数接口
interface LoginParams {
  username: string
  password: string
}

// 定义登录响应接口
interface LoginResponse {
  code: number
  data: User
  message: string
}

// 创建 axios 实例
const service = axios.create({
  baseURL: 'https://60c9f9df772a760017204c05.mockapi.io/api/v1', // 基础地址
  timeout: 5000 // 请求超时时间
})

// 定义登录方法
export function login(params: LoginParams) {
  return service
    .get<User[]>('/users', {
      params
    })
    .then(res => {
      const { data } = res
      if (data.length > 0) {
        // 如果找到匹配的用户,返回成功的响应
        const user = data[0]
        return {
          code: 200,
          data: user,
          message: '登录成功'
        } as LoginResponse
      } else {
        // 如果没有找到匹配的用户,返回失败的响应
        return {
          code: 400,
          data: null,
          message: '用户名或密码错误'
        } as LoginResponse
      }
    })
}

同样,我们可以定义一个 logout 方法,用于根据令牌注销,并返回一个包含状态码和消息的对象:

// src/api/user.ts
// ...

// 定义注销参数接口
interface LogoutParams {
  token: string
}

// 定义注销响应接口
interface LogoutResponse {
  code: number
  message: string
}

// 定义注销方法
export function logout(params: LogoutParams) {
  return service
    .delete<User[]>('/users', {
      params
    })
    .then(res => {
      // 返回成功的响应
      return {
        code: 200,
        message: '注销成功'
      } as LogoutResponse
    })
}

有了这些请求方法,我们就可以在组件中调用它们,实现登录和注销的逻辑。首先,我们需要创建一个登录页面组件,用于显示登录表单和处理登录事件。我们在 src/views 目录下创建一个 login.vue 文件,内容如下:

<template>
  <el-container class="login-container">
    <el-card class="login-card">
      <div slot="header" class="login-header">
        <span>Vue Admin</span>
      </div>
      <el-form
        ref="loginForm"
        :model="loginForm"
        :rules="loginRules"
        label-width="80px"
        class="login-form"
      >
        <el-form-item label="用户名" prop="username">
          <el-input v-model="loginForm.username" prefix-icon="el-icon-user"></el-input>
        </el-form-item>
        <el-form-item label="密码" prop="password">
          <el-input
            v-model="loginForm.password"
            type="password"
            prefix

常用的构造选项

选项说明
el

唯一根标签,决定Vue实例会管理哪一个DOM节点

data

Vue实例对应的数据对象

methods

定义Vue实例的方法,可以在其他地方调用,也可以在指令中使用

computed

定义Vue实例的计算属性,可以在其他地方调用,也可以在指令中使用,本质是一个属性而不是一个函数,在调用时不用加小括号

components

定义Vue实例的子组件

filters

定义Vue实例的过滤器

watch

监听数据变化,观察和响应 Vue 实例上的数据变动

在创建Vue实例时,必不可少的一个选项就是el。el表示唯一根标签,用于指定一个页面中已存在的DOM元素来挂载Vue实例,即通过class或id选择器将页面DOM元素与Vue实例进行绑定。el的类型可以是string,也可以是HTMLElement。

使用el 绑定DOM元素

```vue
<template>
  <el-container class="login-container">
    <el-card class="login-card">
      <div slot="header" class="login-header">
        <span>Vue Admin</span>
      </div>
      <el-form
        ref="loginForm"
        :model="loginForm"
        :rules="loginRules"
        label-width="80px"
        class="login-form"
      >
        <el-form-item label="用户名" prop="username">
          <el-input v-model="loginForm.username" prefix-icon="el-icon-user"></el-input>
        </el-form-item>
        <el-form-item label="密码" prop="password">
          <el-input
            v-model="loginForm.password"
            type="password"
            prefix-icon="el-icon-lock"
          ></el-input>
        </el-form-item>
        <el-form-item>
          <el-button
            type="primary"
            @click="handleLogin"
            :loading="loading"
            class="login-button"
          >
            登录
          </el-button>
        </el-form-item>
      </el-form>
    </el-card>
  </el-container>
</template>

<script lang="ts">
import { defineComponent, ref } from 'vue'
import { useStore } from 'vuex'
import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import { login } from '@/api/user'

export default defineComponent({
  name: 'Login',
  setup() {
    // 获取 vuex store 实例
    const store = useStore()
    // 获取 vue-router 实例
    const router = useRouter()
    // 定义登录表单数据
    const loginForm = ref({
      username: '',
      password: ''
    })
    // 定义登录表单校验规则
    const loginRules = ref({
      username: [
        { required: true, message: '请输入用户名', trigger: 'blur' },
        { min: 3, max: 10, message: '用户名长度在 3 到 10 个字符', trigger: 'blur' }
      ],
      password: [
        { required: true, message: '请输入密码', trigger: 'blur' },
        { min: 6, max: 20, message: '密码长度在 6 到 20 个字符', trigger: 'blur' }
      ]
    })
    // 定义登录按钮加载状态
    const loading = ref(false)
    // 定义登录表单实例
    const loginFormRef = ref(null)
    // 定义登录事件处理函数
    const handleLogin = async () => {
      // 校验登录表单
      const valid = await loginFormRef.value.validate()
      if (!valid) {
        // 如果校验不通过,返回
        return
      }
      // 设置登录按钮加载状态为 true
      loading.value = true
      try {
        // 调用登录接口,传入登录参数
        const res = await login(loginForm.value)
        // 判断登录是否成功
        if (res.code === 200) {
          // 如果成功,保存用户信息和令牌到 vuex store
          store.commit('user/SET_USER', res.data)
          store.commit('user/SET_TOKEN', res.data.token)
          // 显示成功提示
          ElMessage.success(res.message)
          // 跳转到首页
          router.push('/')
        } else {
          // 如果失败,显示失败提示
          ElMessage.error(res.message)
        }
      } catch (error) {
        // 如果出现异常,显示异常提示
        ElMessage.error(error.message)
      } finally {
        // 设置登录按钮加载状态为 false
        loading.value = false
      }
    }
    // 返回需要在模板中使用的数据和方法
    return {
      loginForm,
      loginRules,
      loading,
      loginFormRef,
      handleLogin
    }
  }
})
</script>

<style lang="scss" scoped>
.login-container {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 100vh;
  background: url('~@/assets/images/login-bg.jpg') no-repeat center / cover;
}

.login-card {
  width: 400px;
  padding: 20px;
}

.login-header {
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 24px;
  font-weight: bold;
}

.login-form {
  margin-top: 20px;
}

.login-button {
  width: 100%;
}
</style>

这是一个使用 Element Plus 的表单组件和按钮组件的登录页面组件,它可以实现以下功能:
- 对用户名和密码进行校验,要求不能为空,且长度在一定范围内
- 点击登录按钮时,调用登录接口,传入用户名和密码,获取用户信息和令牌
- 如果登录成功,将用户信息和令牌保存到 vuex store 中,然后跳转到首页
- 如果登录失败,显示错误提示
- 如果出现异常,显示异常提示
- 登录按钮有加载状态,表示正在登录中
这样,我们就完成了登录页面组件的编写。接下来,我们需要在路由配置文件中注册这个组件,以便在浏览器中访问它。我们在 src/router 目录下创建一个 index.ts 文件,内容如下:

// src/router/index.ts
import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router'
import Login from '@/views/login.vue'

// 定义路由配置数组
const routes: Array<RouteRecordRaw> = [
  {
    path: '/login',
    name: 'Login',
    component: Login
  }
]

// 创建路由实例
const router = createRouter({
  history: createWebHashHistory(),
  routes
})

// 导出路由实例
export default router

这里,我们使用了 vue-router 的 createRouter 和 createWebHashHistory 方法,创建了一个使用 hash 模式的路由实例,并注册了一个名为 Login 的路由,对应的组件是我们刚刚创建的 login.vue 组件。这样,我们就可以在浏览器中访问 http://localhost:8080/#/login 来看到我们的登录页面了。
为了让路由实例生效,我们还需要在入口文件 main.ts 中引入并挂载它:

// src/main.ts
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'

createApp(App).use(router).mount('#app')

这样,我们就完成了用户登录的功能。接下来,我们需要实现用户注销的功能。为了实现这个功能,我们需要在头部组件中添加一个注销按钮,点击时调用注销接口,然后清除用户信息和令牌,最后跳转到登录页面。我们在 src/components 目录下创建一个 header.vue 文件,内容如下:

<template>
  <el-header class="header">
    <el-dropdown trigger="click" @command="handleCommand">
      <span class="user-dropdown">
        <el-avatar :src="avatar" size="small"></el-avatar>
        <el-dropdown-menu slot="dropdown">
          <el-dropdown-item command="logout">注销</el-dropdown-item>
        </el-dropdown-menu>
      </span>
    </el-dropdown>
  </el-header>
</template>

<script lang="ts">
import { defineComponent, computed } from 'vue'
import { useStore } from 'vuex'
import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import { logout } from '@/api/user'

export default defineComponent({
  name: 'Header',
  setup() {
    // 获取 vuex store 实例
    const store = useStore()
    // 获取 vue-router 实例
    const router = useRouter()
    // 定义用户头像地址
    const avatar = computed(() => {
      return store.state.user.avatar
    })
    // 定义下拉菜单命令处理函数
    const handleCommand = async (command: string) => {
      if (command === 'logout') {
        // 如果命令是注销,调用注销接口,传入令牌
        const res = await logout({ token: store.state.user.token })
        // 判断注销是否成功
        if (res.code === 200) {
          // 如果成功,清除用户信息和令牌
          store.commit('user/SET_USER', null)
          store.commit('user/SET_TOKEN', '')
          // 显示成功提示
          ElMessage.success(res.message)
          // 跳转到登录页面
          router.push('/login')
        } else {
          // 如果失败,显示失败提示
          ElMessage.error(res.message)
        }
      }
    }
    // 返回需要在模板中使用的数据和方法
    return {
      avatar,
      handleCommand

1、v-text

v-text用来在DOM元素内部插入文本内容,该指令以文本的方式更新元素的内容,即使是HTML代码,它也只当做普通字符串处理,不会解析标签,与插值表达式作用相同。

2、v-html

v-html用来在DOM元素内部插入HTML代码内容。某些情况下,从服务器请求到的数据本身就是一个HTML代码,如果直接通过“{{}}”来输出,会将HTML代码也一起输出。v-html指令更新节点元素的 innerHTML ,内容按照HTML格式进行解析,并且显示对应的内容。

3、v-bind

v-bind指令用于实现单向动态数据绑定。

前面学习的v-text和v-html指令主要作用是将值插入到模板标签的内容当中。但是,除了标签内容需要动态来渲染外,某些标签的属性也希望动态来绑定,这时就可以使用v-bind动态绑定属性。

ps:v-bind有时候也经常用于绑定动态样式,具体的语法如下:

绑定单个动态类名时:

绑定多个动态类名时:

同时绑定静态+动态类名时:

在v-bind指令中使用逻辑判断时:

4、v-on

v-on指令用来绑定事件监听器。在普通元素上,v-on指令可以监听原生的DOM事件(如click、dblclick、keyup、mouseover等)。

小结:

Vue.js为v-bind和v-on这两个最常用的指令提供了语法糖(语法糖是指在不影响功能的情况下,添加某种方法实现同样的效果,以便于程序开发,也称为糖衣语法,通常来说使用语法糖能够增加程序的可读性,从而减少程序代码出错的机会),也可以说是缩写或简写。

5、v-for

v-for是Vue.js的循环语句,当需要遍历数组或对象时,常用的就是列表渲染指令v-for, 它需要结合着in或者of来使用。

for循环普通数组

for循环对象数组

for循环对象

for循环整数

6、v-model

v-model指令主要用于实现表单元素和数据的双向绑定,通常用在表单类元素上(如input、select、textarea等)。所谓双向绑定,指的就是Vue实例中的data与其渲染的DOM元素上的内容保持一致,两者无论谁被改变,另一方也会相应的同步更新为相同的数据。

v-model修饰符

v-model有3个修饰符,分别是lazy、number、trim

1、lazy 

v-model默认是在input事件中同步输入框中的内容,也就是一旦数据发生改变,对应的data中的数据也会发生改变。如果使用lazy修饰符,可以让数据在失去焦点或者回车时才会更新。

<input type="text" placeholder="搜索"  v-model.lazy='keyword' />

2、number

默认情况下,在输入框输入数字的时候,默认会把输入的内容当作字符串类型处理,如果加上number修饰符,就可以让你在输入数字的时候将内容转为number类型。

<input type="text" placeholder="搜索"  v-model.number='keyword' />

3、trim

trim修饰符可以去除输入内容左右两边的空格。

<input type="text" placeholder="搜索"  v-model.trim='keyword' />

7、v-if和v-else

 v-if是Vue.js的条件语句,v-if指令用于条件性地渲染一块内容,这块内容只会在指令的表达式返回 true 值的时候被渲染。特别注意的是,v-if所关联的是Vue.js的动态变量。

        v-if的使用一般有两个场景:

       1.通过条件判断展示或者隐藏某个元素或者多个元素;

        2.进行视图之间的切换。

v-else

8、v-show

v-show是另一个条件渲染语句,用于根据条件展示元素,用法与v-if大致一样。

        带有v-show的元素始终会被渲染并保留在DOM中,v-show指令使用css样式来控制元素的显示/隐藏。值得注意的是,注意,v-show不支持<template>元素,也不支持v-else。

v-if和v-show的区别

        v-if和v-show都可以用来动态地控制DOM元素的显示和隐藏,但是,二者是有区别的。v-if 是“真正”的条件渲染,因为它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建;v-if 也是“惰性”的:如果在初始渲染时条件为假,则什么也不做,直到条件第一次变为真时,才会开始渲染条件块。相比之下,v-show 就简单得多,不管初始条件是什么,元素总是会被渲染,并且只是简单地基于 CSS 进行切换。

```vue
<template>
  <el-header class="header">
    <el-dropdown trigger="click" @command="handleCommand">
      <span class="user-dropdown">
        <el-avatar :src="avatar" size="small"></el-avatar>
        <el-dropdown-menu slot="dropdown">
          <el-dropdown-item command="logout">注销</el-dropdown-item>
        </el-dropdown-menu>
      </span>
    </el-dropdown>
  </el-header>
</template>

<script lang="ts">
import { defineComponent, computed } from 'vue'
import { useStore } from 'vuex'
import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import { logout } from '@/api/user'

export default defineComponent({
  name: 'Header',
  setup() {
    // 获取 vuex store 实例
    const store = useStore()
    // 获取 vue-router 实例
    const router = useRouter()
    // 定义用户头像地址
    const avatar = computed(() => {
      return store.state.user.avatar
    })
    // 定义下拉菜单命令处理函数
    const handleCommand = async (command: string) => {
      if (command === 'logout') {
        // 如果命令是注销,调用注销接口,传入令牌
        const res = await logout({ token: store.state.user.token })
        // 判断注销是否成功
        if (res.code === 200) {
          // 如果成功,清除用户信息和令牌
          store.commit('user/SET_USER', null)
          store.commit('user/SET_TOKEN', '')
          // 显示成功提示
          ElMessage.success(res.message)
          // 跳转到登录页面
          router.push('/login')
        } else {
          // 如果失败,显示失败提示
          ElMessage.error(res.message)
        }
      }
    }
    // 返回需要在模板中使用的数据和方法
    return {
      avatar,
      handleCommand
    }
  }
})
</script>

<style lang="scss" scoped>
.header {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  height: 60px;
  background-color: #409eff;
}

.user-dropdown {
  display: flex;
  align-items: center;
  cursor: pointer;
  color: #fff;
  margin-right: 20px;
}
</style>

这是一个使用 Element Plus 的头部组件,它可以实现以下功能:
- 显示用户的头像,点击时弹出一个下拉菜单,包含一个注销选项
- 点击注销选项时,调用注销接口,传入令牌,获取状态码和消息
- 如果注销成功,清除用户信息和令牌,然后跳转到登录页面
- 如果注销失败,显示错误提示
这样,我们就完成了用户注销的功能。接下来,我们需要在布局组件中引入这个头部组件,以便在页面中显示它。我们在 src/layout 目录下创建一个 default.vue 文件,内容如下:

<template>
  <el-container class="default-container">
    <header-component></header-component>
    <el-main class="default-main">
      <router-view></router-view>
    </el-main>
  </el-container>
</template>

<script lang="ts">
import { defineComponent } from 'vue'
import HeaderComponent from '@/components/header.vue'

export default defineComponent({
  name: 'Default',
  components: {
    HeaderComponent
  }
})
</script>

<style lang="scss" scoped>
.default-container {
  height: 100vh;
}

.default-main {
  padding: 20px;
}
</style>

这是一个使用 Element Plus 的容器组件和主要组件的布局组件,它可以实现以下功能:
- 引入头部组件,并放在容器组件的头部区域
- 使用路由视图组件,展示不同的页面组件,并放在容器组件的主要区域
这样,我们就完成了布局组件的编写。接下来,我们需要在路由配置文件中注册这个组件,并将它作为其他页面组件的父组件。我们在 src/router/index.ts 文件中添加以下代码:

// src/router/index.ts
// ...
import Default from '@/layout/default.vue'

// 定义路由配置数组
const routes: Array<RouteRecordRaw> = [
  // ...
  {
    path: '/',
    name: 'Default',
    component: Default,
    children: [
      // 在这里添加其他页面组件的路由配置
    ]
  }
]

// ...

这里,我们注册了一个名为 Default 的路由,对应的组件是我们刚刚创建的 default.vue 组件,并定义了一个空的 children 数组,用于存放其他页面组件的路由配置。这样,我们就可以在浏览器中访问 http://localhost:8080/#/ 来看到我们的布局页面了。
至此,我们已经实现了用户登录和注销的功能,以及布局组件的编写。在下一篇博客中,我们将继续介绍如何使用 Vue 3 和 Element Plus 开发一个后台管理系统,实现动态生成侧边栏菜单和面包屑导航,以及使用表格、表单、图表等组件展示和操作数据的功能。敬请期待!
 

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/很楠不爱3/article/detail/98442?site
推荐阅读
相关标签
  

闽ICP备14008679号