Skip to content

16-前端 Web 实战(Tlias 案例-部门管理)

在前面的课程中,我们学习了 Vue 工程化的基础内容、TS、ElementPlus,那接下来呢,我们要通过一个案例,加强大家对于 Vue 项目的理解,并掌握 Vue 项目的开发。 这个案例呢,就是我们之前所做的 Tlias 智能学习辅助系统。

在这个案例中,我们主要完成 部门管理员工管理 的功能开发。 而今天呢,我们先来完成部门管理的功能开发,而在完成部门管理的功能开发之前,先需要完成基础的准备工作。 所以今天的课程安排如下:

  • 前后端分离开发
  • 准备工作
  • 页面布局
  • Vue-Router
  • 部门管理

前后端分离开发

在之前的课程中,我们介绍过,现在的企业项目开发有 2 种开发模式:前后台混合开发 前后台分离开发

前后台混合开发,顾名思义就是前台后台代码混在一起开发。这种开发模式有如下缺点:

  • 沟通成本高:后台人员发现前端有问题,需要找前端人员修改,前端修改成功,再交给后台人员使用
  • 分工不明确:后台开发人员需要开发后台代码,也需要开发部分前端代码。很难培养专业人才
  • 不便管理:所有的代码都在一个工程中
  • 难以维护:前端代码更新,和后台无关,但是需要整个工程包括后台一起重新打包部署。

所以我们目前基本都是采用的前后台分离开发方式,如下图所示:

我们将原先的工程分为前端工程和后端工程这 2 个工程,然后前端工程交给专业的前端人员开发,后端工程交给专业的后端人员开发。

前端页面需要数据,可以通过发送异步请求,从后台工程获取。但是,我们前后台是分开来开发的,那么前端人员怎么知道后台返回数据的格式呢?后端人员开发,怎么知道前端人员需要的数据格式呢?

所以针对这个问题,我们前后台统一制定一套规范!我们前后台开发人员都需要遵循这套规范开发,这就是我们的 接口文档。接口文档有离线版和在线版本,接口文档示可以查询今天提供 资料/接口文档 里面的资料。

那么接口文档的内容怎么来的呢?是我们后台开发者根据产品经理提供的产品原型和需求文档所撰写出来的,产品原型示例可以参考今天提供资料/页面原型 里面的资料。

那么基于前后台分离开发的模式下,我们后台开发者开发一个功能的具体流程如何呢?如下图所示:

  1. 需求分析:首先我们需要阅读需求文档,分析需求,理解需求。
  2. 接口定义:查询接口文档中关于需求的接口的定义,包括地址,参数,响应数据类型等等
  3. 前后台并行开发:各自按照接口文档进行开发,实现需求
  4. 测试:前后台开发完了,各自按照接口文档进行测试
  5. 前后段联调测试:前段工程请求后端工程,测试功能

页面布局

准备工作

  1. 导入资料中准备的基础工程到 VsCode。 【复制出来放在自己的工作目录下】

  1. 启动前端项目,访问该项目

打开浏览器,访问 http://localhost:5173

我们可以试图看一下源码: 我们看一下vue.app 这里的@其实就是src.为什么能这样? 这实际上就是添加了一个别名. 然后整体结构: 目前view下的大多都是空的文件,表示不同的页面.

在提供的基础工程中,最基本的页面布局,已经准备好了。 我们通过页面原型可以看到页面的布局如下:

其实要实现这个页面布局,我们可以借助于 ElementPlus 中提供的 container 容器布局。

注意看上面的源码:这里el-container嵌套一个elcontainer. Container 布局容器,用于布局的容器组件,方便快速搭建页面的基本结构:

<el-container>:外层容器。 当子元素中包含 <el-header><el-footer> 时,全部子元素会垂直上下排列, 否则会水平左右排列。

<el-header>:顶栏容器。

<el-aside>:侧边栏容器。

<el-main>:主要区域容器。

<el-footer>:底栏容器。

[!TIP] 提示:<el-container> 子元素中包含 <el-header><el-footer> 时,全部子元素会垂直上下排列, 否则会水平左右排列。

左侧菜单布局

接下来,我们再来完成左侧菜单栏的制作。

左侧菜单栏的制作,也不需要我们自己实现,其实在 ElementPlus 中已经提供了对应的菜单组件,我们可以直接参考【PS: 其实就是复制过来,参考页面原型和需求,将其改造成我们需要的样子就可以了】。

参考代码的出处:

然后就可以参考其提供的源码,复制到我们的侧边栏部分 <el-aside> ... </el-aside>,然后根据我们案例的需要进行改造,改造成我们需要的样子即可。

最终左侧菜单栏的代码如下 ,大家可以直接导入到 views/layout/index.vue 的侧边栏区域 <el-aside> ... </el-aside>

<el-menu>//菜单
    <!-- 首页菜单 -->
    <el-menu-item index="/index">
      <el-icon><Promotion /></el-icon> 首页
    </el-menu-item>

    <!-- 班级管理菜单 -->
    <el-sub-menu index="/manage">//子菜单
      <template #title>//这样点击班级学员管理就会弹出下面的这几个菜单.这个index属性是后面动态菜单需要的.
        <el-icon><Menu /></el-icon> 班级学员管理//这个icon是element自带的.不过应该要安装一下有关的库.
      </template>
      <el-menu-item index="/clazz">
        <el-icon><HomeFilled /></el-icon>班级管理
      </el-menu-item>
      <el-menu-item index="/stu">
        <el-icon><UserFilled /></el-icon>学员管理
      </el-menu-item>
    </el-sub-menu>

    <!-- 系统信息管理 -->
    <el-sub-menu index="/system">
      <template #title>
        <el-icon><Tools /></el-icon>系统信息管理
      </template>
      <el-menu-item index="/dept">
        <el-icon><HelpFilled /></el-icon>部门管理
      </el-menu-item>
      <el-menu-item index="/emp">
        <el-icon><Avatar /></el-icon>员工管理
      </el-menu-item>
    </el-sub-menu>

    <!-- 数据统计管理 -->
    <el-sub-menu index="/report">
      <template #title>
        <el-icon><Histogram /></el-icon>数据统计管理
      </template>
      <el-menu-item index="/empReport">
        <el-icon><InfoFilled /></el-icon>员工信息统计
      </el-menu-item>
      <el-menu-item index="/stuReport">
        <el-icon><Share /></el-icon>学员信息统计
      </el-menu-item>
      <el-menu-item index="/log">
        <el-icon><Document /></el-icon>日志信息统计
      </el-menu-item>
    </el-sub-menu>
</el-menu>

最终,浏览器打开的效果如下:

到目前为止,views/layout/index.vue 中的整体内容如下:

<script setup>

</script>

<template>
  <div class="common-layout">
    <el-container>
      <!-- Header 区域 -->
      <el-header class="header">
        <span class="title">Tlias智能学习辅助系统</span>
        <span class="right_tool">
          <a href="">
            <el-icon><EditPen /></el-icon> 修改密码 &nbsp;&nbsp;&nbsp; |  &nbsp;&nbsp;&nbsp;
          </a>
          <a href="">
            <el-icon><SwitchButton /></el-icon> 退出登录
          </a>
        </span>
      </el-header>

      <el-container>
        <!-- 左侧菜单 -->
        <el-aside width="200px" class="aside">

          <el-menu>
            <!-- 首页菜单 -->
            <el-menu-item index="/index">
              <el-icon><Promotion /></el-icon> 首页
            </el-menu-item>

            <!-- 班级管理菜单 -->
            <el-sub-menu index="/manage">
              <template #title>
                <el-icon><Menu /></el-icon> 班级学员管理
              </template>
              <el-menu-item index="/clazz">
                <el-icon><HomeFilled /></el-icon>班级管理
              </el-menu-item>
              <el-menu-item index="/stu">
                <el-icon><UserFilled /></el-icon>学员管理
              </el-menu-item>
            </el-sub-menu>

            <!-- 系统信息管理 -->
            <el-sub-menu index="/system">
              <template #title>
                <el-icon><Tools /></el-icon>系统信息管理
              </template>
              <el-menu-item index="/dept">
                <el-icon><HelpFilled /></el-icon>部门管理
              </el-menu-item>
              <el-menu-item index="/emp">
                <el-icon><Avatar /></el-icon>员工管理
              </el-menu-item>
            </el-sub-menu>

            <!-- 数据统计管理 -->
            <el-sub-menu index="/report">
              <template #title>
                <el-icon><Histogram /></el-icon>数据统计管理
              </template>
              <el-menu-item index="/empReport">
                <el-icon><InfoFilled /></el-icon>员工信息统计
              </el-menu-item>
              <el-menu-item index="/stuReport">
                <el-icon><Share /></el-icon>学员信息统计
              </el-menu-item>
              <el-menu-item index="/log">
                <el-icon><Document /></el-icon>日志信息统计
              </el-menu-item>
            </el-sub-menu>
          </el-menu>

        </el-aside>

        <el-main>
          右侧核心展示区域
        </el-main>
      </el-container>

    </el-container>
  </div>
</template>

<style scoped>
.header {
  background-image: linear-gradient(to right, #00547d, #007fa4, #00aaa0, #00d072, #a8eb12);
}

.title {
  color: white;
  font-size: 40px;
  font-family: 楷体;
  line-height: 60px;
  font-weight: bolder;
}

.right_tool{
  float: right;
  line-height: 60px;
}

a {
  color: white;
  text-decoration: none;
}

.aside {
  width: 220px;
  border-right: 1px solid #ccc;
  height: 730px;
}
</style>

目前,我们点击左侧的菜单,右侧主区域展示的内容,还不能做到动态变化。 那应该如何做到动态变化呢 ?

要解决这个问题,就需要通过 VueRouter 来解决。

VueRouter

介绍

  • Vue Router:Vue 的官方路由。 为 Vue 提供富有表现力、可配置的、方便的路由。
  • Vue 中的路由,主要定义的是路径与组件之间的对应关系。

比如,我们打开一个网站,点击左侧菜单,地址栏的地址发生变化。 地址栏地址一旦发生变化,在主区域显示对应的页面组件。

VueRouter 主要由以下三个部分组成,如下所示:

  • VueRouter:路由器类,根据路由请求在路由视图中动态渲染选中的组件
  • <router-link>:请求链接组件,浏览器会解析成<a>
  • <router-view>:动态视图组件,用来渲染展示与路由路径对应的组件 在我们使用create-vue脚手架创建的时候,会有一个选择.如果我们选择了,那么在src目录下自动会有这个/router./router只有一个文件:

这里面就是可以提供的path和组件的映射.这个页面配置的其实是:如果我们访问了xx路径,我们需要展示的是哪个vue页面. name只是"起个名字"而已 上面展示了两种配置方式: - 先import,然后在component字段直接使用. - 直接通过lamda表达式引入.

基础路由配置

1). 在 views/layout/index.vue** 中,调整代码,具体调整位置如下: **

  • 在左侧菜单栏的 <el-menu> 标签上添加 router 属性,这会让 Element Plus 的 <el-menu> 组件自动根据路由来激活对应的菜单项。
  • 使用 <router-view> 组件来渲染根据路由动态变化的内容。
  • 确保每个 <el-menu-item>index 属性值与你想要导航到的路径相匹配。
<script setup>
// 无需额外导入,因为我们只是使用了 Element Plus 和 Vue Router 的基本功能
</script>

<template>
  <div class="common-layout">
    <el-container>
      <!-- Header 区域 -->
      <el-header class="header">
        <span class="title">Tlias智能学习辅助系统</span>
        <span class="right_tool">
          <a href="">
            <el-icon><EditPen /></el-icon> 修改密码 &nbsp;&nbsp;&nbsp; |  &nbsp;&nbsp;&nbsp;
          </a>
          <a href="">
            <el-icon><SwitchButton /></el-icon> 退出登录
          </a>
        </span>
      </el-header>

      <el-container>
        <!-- 左侧菜单 -->
        <el-aside width="200px" class="aside">

          <el-menu router>//这个其实应该写成<el-menu router="true">因为这是个布尔值.但是布尔值是true时就可以简写.这样就开启了element的vue-router末世,这个模式会在激活下面的子菜单的时候会使用index的路由跳转
            <!-- 首页菜单 -->
            <el-menu-item index="/index">
              <el-icon><Promotion /></el-icon> 首页
            </el-menu-item>
            <!-- 其实一开始的写法是这样的 -->
            <!-- <router-link to="/index">
                <el-menu-item index="/index">
                  <el-icon><Promotion /></el-icon> 首页
                </el-menu-item>
            </router-link> -->
            这样点击首页就会访问对应的路径.同理,下面的子菜单都需要加一个这个.这样太麻烦了.elementplus进行了封装.也就是上面说的标签上加router属性.注意,这样只是点击到这个按钮会访问这个路径,但是路径会导致页面发生变化则是通过router-view + index的.
            <!-- 班级管理菜单 -->
            <el-sub-menu index="/manage">
              <template #title>
                <el-icon><Menu /></el-icon> 班级学员管理//注意点击这个按钮只是展示 班级管理/学员管理 这两个选项.,而不是跳转新的页面.因此不要加路由的
              </template>
              <el-menu-item index="/clazz">
                <el-icon><HomeFilled /></el-icon>班级管理
              </el-menu-item>
              <el-menu-item index="/stu">
                <el-icon><UserFilled /></el-icon>学员管理
              </el-menu-item>
            </el-sub-menu>

            <!-- 系统信息管理 -->
            <el-sub-menu index="/system">
              <template #title>
                <el-icon><Tools /></el-icon>系统信息管理
              </template>
              <el-menu-item index="/dept">
                <el-icon><HelpFilled /></el-icon>部门管理
              </el-menu-item>
              <el-menu-item index="/emp">
                <el-icon><Avatar /></el-icon>员工管理
              </el-menu-item>
            </el-sub-menu>

            <!-- 数据统计管理 -->
            <el-sub-menu index="/report">
              <template #title>
                <el-icon><Histogram /></el-icon>数据统计管理
              </template>
              <el-menu-item index="/report/emp">
                <el-icon><InfoFilled /></el-icon>员工信息统计
              </el-menu-item>
              <el-menu-item index="/report/stu">
                <el-icon><Share /></el-icon>学员信息统计
              </el-menu-item>
              <el-menu-item index="/log">
                <el-icon><Document /></el-icon>日志信息统计
              </el-menu-item>
            </el-sub-menu>
          </el-menu>
        </el-aside>

        <!-- 主展示区域 -->
        <el-main>
          <router-view></router-view>//这就是动态视图组件
        </el-main>
      </el-container>
    </el-container>
  </div>
</template>

<style scoped>
.header {
  background-image: linear-gradient(to right, #00547d, #007fa4, #00aaa0, #00d072, #a8eb12);
}

.title {
  color: white;
  font-size: 40px;
  font-family: 楷体;
  line-height: 60px;
  font-weight: bolder;
}

.right_tool{
  float: right;
  line-height: 60px;
}

a {
  color: white;
  text-decoration: none;
}

.aside {
  width: 220px;
  border-right: 1px solid #ccc;
  height: 730px;
}
</style>

2). 在 router/index.js 中配置请求路径与组件之间的关系。 其实是这样的: 也可以提出来:

import { createRouter, createWebHistory} from 'vue-router';

import IndexView from '@/views/index/index.vue';
import ClazzView from '@/views/clazz/index.vue';
import StuView from '@/views/stu/index.vue';
import DeptView from '@/views/dept/index.vue';
import EmpView from '@/views/emp/index.vue';
import EmpReportView from '@/views/report/emp/index.vue';
import StuReportView from '@/views/report/stu/index.vue';
import LogView from '@/views/log/index.vue';
import LoginView from '@/views/login/index.vue';

const routes = [
  { path: '/index', component: IndexView },
  { path: '/clazz', component: ClazzView },
  { path: '/stu', component: StuView },
  { path: '/dept', component: DeptView },
  { path: '/emp', component: EmpView },
  { path: '/report/emp', component: EmpReportView },
  { path: '/report/stu', component: StuReportView },
  { path: '/log', component: LogView },
  { path: '/login', component: LoginView },
];

const router = createRouter({
  history: createWebHistory(),
  routes,
});

export default router;
这里没加name也是可以的.这里如果我们后续用到vue的其它功能,才需要加.

其实,如果使用name,那么其实在别的代码里我们直接使用name来进行路径和组件的绑定就行.这种情况下,如果我们修改路径的名字,就不用在仓库里各种找出现过的地方了.

经过这么两步操作之后,我们就可以看到,在页面上,点击左侧菜单,右侧主展示区域,就会显示出对应的页面了。

那要完成这个功能效果,我们就需要用到 Vue 生态中的路由 Vue-Router

完善路由配置

上述我们只是完成了最基本的路由配置。 并经过测试我们发现,如果我们访问 /login 路径,会发现,登录页面是在 layout 页面中嵌套展示的,这肯定是不合适的。

这样的原因是: 这样就相当于写死了. 这样写就行: 这样,我们访问 网址/login 这个位置,就会展示登录页面了.(这也很好理解,vue router里就是这样的). 但是这样的话,如果我们访问/index或者其他路径,也是全屏展示的.

也就是,我们希望除了/login以外,都是展示layout/index.vue.这样写就行: 也就是类似最长前缀匹配,如果匹配到/login那么就访问loginView,否则,都匹配到LayoutView.

但是index.vue里面又调用了router-view,因此就有下面的嵌套路由的方法. 这种子路由不需要前面加/,因为已经有父路由加过了. 这里实际上是这样拼接的:假如访问到/dept,那么先匹配/,因此访问LayoutView;然后,dept匹配到子路由里的dept,因此就会展示DeptView,在哪展示?在LayoutView里展示.

但是如果直接访问网址(比如http:localhost:1111)实际上访问的网址其实是http:localhost:1111/,这样就会展示LayoutView.但是这样在Layoutview组件里的router-view就没法匹配到任何组件,从而展示为空白.

如果我想展示首页,就像下面重定向.这样,只要匹配到根路径并且无法继续向下匹配,那么就会重定向到/index

那接下来,我们就来优化一下路由的配置。最终配置形式如下,在 router/index.js 中做如下配置:

import { createRouter, createWebHistory } from 'vue-router'

import IndexView from '@/views/index/index.vue'
import ClazzView from '@/views/clazz/index.vue'
import DeptView from '@/views/dept/index.vue'
import EmpView from '@/views/emp/index.vue'
import LogView from '@/views/log/index.vue'
import StuView from '@/views/stu/index.vue'
import EmpReportView from '@/views/report/emp/index.vue'
import StuReportView from '@/views/report/stu/index.vue'
import LayoutView from '@/views/layout/index.vue'
import LoginView from '@/views/login/index.vue'

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    {
     path: '/', 
     name: '',
     component: LayoutView,
     redirect: '/index', //重定向
     children: [
      {path: 'index', name: 'index', component: IndexView},
      {path: 'clazz', name: 'clazz', component: ClazzView},
      {path: 'stu', name: 'stu', component: StuView},
      {path: 'dept', name: 'dept', component: DeptView},
      {path: 'emp', name: 'emp', component: EmpView},
      {path: 'log', name: 'log', component: LogView},
      {path: 'empReport', name: 'empReport', component: EmpReportView},
      {path: 'stuReport', name: 'stuReport', component: StuReportView},
     ]
    },
    {path: '/login', name: 'login', component: LoginView}
  ]
})

export default router

具体的执行访问流程如下:

  1. 当点击左侧菜单栏的员工管理菜单时,最终地址栏会访问路径 /emp
  2. 此时 VueRouter,会自动的到所配置的路由表(router/index.js)中,查找与该路径对应的组件,并展示在路由展示组件 <router-view> 对应的位置中。

部门管理

部门管理的页面内容,写在 src/views/dept/index.vue 中。

部门列表

基本布局

首先,根据页面原型、需求说明、接口文档,先完成页面的基本布局 。 可以参考 ElementPlus 中的组件,拷贝过来适当做一个改造。

我们先来完成页面的基本布局。

部门管理组件 src/views/dept/index.vue 具体的页面布局代码如下:

<script setup>
import { ref } from 'vue';

// 示例数据
const deptList= ref([
  { id: 1, name: '学工部', createTime: '2024-09-01T23:06:29', updateTime: '2024-09-01T23:06:29' },
  { id: 2, name: '教研部', createTime: '2024-09-01T23:06:29', updateTime: '2024-09-01T23:06:29' }
]);

// 编辑部门 - 根据ID查询回显数据
const handleEdit = (id) => {
  console.log(`Edit item with ID ${id}`);
  // 在这里实现编辑功能
};

// 删除部门 - 根据ID删除部门
const handleDelete = (id) => {
  console.log(`Delete item with ID ${id}`);
  // 在这里实现删除功能
};
</script>

<template>
  <h1>部门管理</h1>

  <!-- 按钮靠页面右侧显示 -->
  <el-button type="primary" @click="handleEdit" style="float: right;"> + 新增部门</el-button> <br><br>

  <el-table :data="deptList" border style="width: 100%;">
    <el-table-column type="index" label="序号" width="100" align="center"/>
    <el-table-column prop="name" label="部门名称" width="300" align="center"/>
    <el-table-column prop="updateTime" label="最后修改时间" width="300" align="center"/>
    <el-table-column fixed="right" label="操作" align="center">
      <template #default="scope">
        <el-button size="small" @click="handleEdit(scope.row.id)">修改</el-button>
        <el-button size="small" type="danger" @click="handleDelete(scope.row.id)">删除</el-button>
      </template>
    </el-table-column>
  </el-table>

</template>

<style scoped>

</style>

前端经典直觉处理:如果太拥挤,那么就使用div包裹,通过<div class="container"> + css样式 +margin边缘设置即可.

表格中每一列展示的属性 prop 都是根据接口文档来的,接口文档返回什么样的数据,我们就安装对应的数据格式进行解析。

最终页面效果,展示如下:

加载数据

根据需求,需要在新增、修改、删除部门之后,加载最新的部门数据。 在打开页面之后,也需要自动加载部门数据。 那接下来,我们就需要基于 axios 发送异步请求,动态获取数据。 假数据怎么来:

需要在 src/views/dept/index.vue 中增加如下代码,在页面加载完成发送异步请求(https://apifoxmock.com/m1/3128855-1224313-default/depts),动态加载的 Axios。

<script setup>
import {ref, onMounted} from 'vue'
import axios from 'axios'

//声明列表展示数据
let deptList= ref([])

//动态加载数据-查询部门
const queryAll = async () => {
  const result = await axios.get('https://apifoxmock.com/m1/3128855-1224313-default/depts')
  if(result.data.code){//这里用到了js的隐式类型转换 0-false 其他数字-true ''-false 其他为true null/undefined false
      deptList.value = result.data.data
  }
}

//钩子函数
onMounted(() => {
  queryAll()
})

// 编辑部门 - 根据ID查询回显数据
const handleEdit = (id) => {
  console.log(`Edit item with ID ${id}`);
  // 在这里实现编辑功能
};

// 删除部门 - 根据ID删除部门
const handleDelete = (id) => {
  console.log(`Delete item with ID ${id}`);
  // 在这里实现删除功能
};
</script>

添加代码后,最终 src/views/dept/index.vue 完整代码如下:

<script setup>
import {ref, onMounted} from 'vue'
import axios from 'axios'

//声明列表展示数据
let deptList= ref([])

//动态加载数据-查询部门
const queryAll = async () => {
  const result = await axios.get('https://apifoxmock.com/m1/3161925-0-default/depts')
  deptList.value = result.data.data//这里看上面的.
}

//钩子函数
onMounted(() => {
  queryAll()
})

// 编辑部门 - 根据ID查询回显数据
const handleEdit = (id) => {
  console.log(`Edit item with ID ${id}`);
  // 在这里实现编辑功能
};

// 删除部门 - 根据ID删除部门
const handleDelete = (id) => {
  console.log(`Delete item with ID ${id}`);
  // 在这里实现删除功能
};
</script>

<template>
  <h1>部门管理</h1>

  <!-- 按钮靠页面右侧显示 -->
  <el-button type="primary" @click="handleEdit" style="float: right;"> + 新增部门</el-button> <br><br>

  <el-table :data="deptList" border style="width: 100%;">
    <el-table-column type="index" label="序号" width="100" align="center"/>
    <el-table-column prop="name" label="部门名称" width="300" align="center"/>
    <el-table-column prop="updateTime" label="最后修改时间" width="300" align="center"/>
    <el-table-column fixed="right" label="操作" align="center">
      <template #default="scope">
        <el-button size="small" @click="handleEdit(scope.row.id)">修改</el-button>
        <el-button size="small" type="danger" @click="handleDelete(scope.row.id)">删除</el-button>
      </template>
    </el-table-column>
  </el-table>
</template>

<style scoped>

</style>

页面效果如下,页面一加载,马上就会查询出所有的部门数据:

我们可以看到数据可以正常的查询出来,并展示在页面中。

思考:直接在 Vue 组件中,基于 axios 发送异步请求,存在什么问题?

我们刚才在完成部门列表查询时,是直接基于 axios 发送异步请求,直接将接口的请求地址放在组件文件 .vue 中。 而如果开发一个大型的项目,组件文件可能会很多很多很多,如果前端开发完毕,进行前后端联调测试了,需要修改请求地址,那么此时,就需要找到每一个 .vue 文件,然后挨个修改。 所以上述的代码,虽然实现了动态加载数据的功能。 但是存在以下问题:

  • 请求路径难以维护
  • 数据解析繁琐

程序优化

1). 为了解决上述问题,我们在前端项目开发时,通常会定义一个请求处理的工具类 - src/utils/request.js 。 在这个工具类中,对 axios 进行了封装。 具体代码如下:

import axios from 'axios'

//创建axios实例对象
const request = axios.create({//创建一个新的axios实例,这里可以自定义一些东西
  baseURL: '/api',//所有通过这个axios发出的请求都会加一个baseURL
  timeout: 600000//为了后端打断点
})

//axios的响应 response 拦截器
request.interceptors.response.use(//传递了两个箭头函数
  (response) => { //成功回调
    return response.data//response就是原始的response
  },
  (error) => { //失败回调
    return Promise.reject(error)
  }
)

export default request//需要被import的必须被export

2). 而与服务端进行异步交互的逻辑,通常会按模块,封装在一个单独的 API 中,如:src/api/dept.js.注意这一般统一放到/src/api当中

import request from "@/utils/request"

//列表查询 注意要export
export const queryAllApi = () => request.get('/depts')//自动就会拼接成发送get的/api/depts
//这里其实是简化的写法,完整的写法是:
//export const queryAllApi = () {
//  return request.get('/depts')
//} 

3). 修改 src/views/dept/index.vue 中的代码

现在就不需要每次直接调用 axios 发送异步请求了,只需要将我们定义的对应模块的 API 导入进来,就可以直接使用了。

<script setup>
import {ref, onMounted} from 'vue'
import {queryAllApi} from '@/api/dept'

//声明列表展示数据
let deptList= ref([])

//动态加载数据-查询部门
const queryAll = async () => {
  const result = await queryAllApi()
  deptList.value = result.data
}

//钩子函数
onMounted(() => {
  queryAll()
})

// 编辑部门 - 根据ID查询回显数据
const handleEdit = (id) => {
  console.log(`Edit item with ID ${id}`);
  // 在这里实现编辑功能
};

// 删除部门 - 根据ID删除部门
const handleDelete = (id) => {
  console.log(`Delete item with ID ${id}`);
  // 在这里实现删除功能
};
</script>

做完上面这三步之后,我们打开浏览器发现,并不能访问到接口数据。原因是因为,目前请求路径不对。 (这是因为我们当前request的baseURL拼接好之后相当于是/api/depts,由于没有指定网址,会默认向当前的网址发送请求,也就是localhost:5173/api/depts)如何改成向localhost:8080转发呢

4). 在 vite.config.js 中配置前端请求服务器的信息 这个文件在根目录下. 在服务器中配置代理 proxy 的信息,并在配置代理时,执行目标服务器。 以及 url 路径重写的规则。这是一个反向代理.

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

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    vue(),
    vueJsx(),
  ],
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url))
    }
  },
  server: {
    proxy: {
      '/api': {
        target: 'http://localhost:8080',
        secure: false,
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, ''),
      }
    }
  }//也就是说,如果请求的路径前缀是/api,那么就会自动匹配到这个规则(去掉/api,修改目标网址)
})
ps:为什么非要加一个/api?这是为了区分究竟是向后端发起的请求还是请求一些来自前端的静态资源的. 添加内容为,绿色阴影部分的代码。

然后,我们就可以启动服务器端的程序(将之前开发的服务端程序启动起来测试一下 ),进行测试了(【注意:测试时, 记得将令牌校验的过滤器及拦截器, 以及记录日志的 AOP 程序 全部注释】)。

新增部门

功能实现

接下来,我们再来完成新增部门的功能实现。

新增部门和编辑部门,可以同用一个 Dialog 对话框。而这个对话框中,主要包括两个 ElementPlus 中的组件,分别为:Dialog 对话框组件,Form 表单组件。

1). 在 src/views/dept/index.vue 中完成页面布局,并编写交互逻辑,完成数据绑定。

完整代码如下:

<script setup>
import { ref, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import { queryAllApi, addDeptApi } from '@/api/dept'

// 声明列表展示数据
let deptList= ref([])

// 动态加载数据 - 查询部门
const queryAll = async () => {
  const result = await queryAllApi()
  deptList.value = result.data
}

// 钩子函数
onMounted(() => {
  queryAll()
})

// 编辑部门 - 根据ID查询回显数据
const handleEdit = (id) => {
  console.log(`Edit item with ID ${id}`);
  // 在这里实现编辑功能
};

// 删除部门 - 根据ID删除部门
const handleDelete = (id) => {
  console.log(`Delete item with ID ${id}`);
  // 在这里实现删除功能
};


const formTitle = ref('')
//新增部门
const add = () => {
  formTitle.value = '新增部门'
  showDialog.value = true
  deptForm.value = {name: ''}
}

// 新增部门对话框的状态
const showDialog = ref(false)
// 表单数据
const deptForm = ref({name: ''})
// 表单验证规则(可以后面再看)
const formRules = ref({
  name: [
    { required: true, message: '请输入部门名称', trigger: 'blur' },
    { min: 2, max: 10, message: '长度在 2 到 10 个字符', trigger: 'blur' }
  ]
})

// 表单标签宽度
const formLabelWidth = ref('120px')
// 表单引用
const deptFormRef = ref(null)

// 重置表单
const resetForm = () => {
  deptFormRef.value.resetFields()//这个deptFormRef也可以看后面.这里实际上就是清空表单所有的状态.比如先前遗留的值,先前某个表单不合规则的提示.
}

// 提交表单,这里的校验请看下文
const save = async () => {
  await deptFormRef.value.validate(async valid => {
    if (!valid) return
    // 提交表单
    const result = await addDeptApi(deptForm.value)
    if(result.code){
      ElMessage.success('操作成功')//其实就是alert().但是alert有阻塞效果,对话框弹出来之后如果不点确定,那么会导致阻塞在这里,不会往下执行.
      // 关闭对话框
      showDialog.value = false
      // 重置表单
      resetForm()
      // 重新加载数据
      queryAll()
    }else {
      ElMessage.error(result.msg)
    }
  })
}
</script>

<template>
  <h1>部门管理</h1>

  <!-- 按钮靠页面右侧显示 -->
  <el-button type="primary" @click="add()" style="float: right;"> + 新增部门</el-button> <br><br>

  <!-- 数据展示表格 -->
  <el-table :data="deptList" border style="width: 100%;">
    <el-table-column type="index" label="序号" width="100" align="center"/>
    <el-table-column prop="name" label="部门名称" width="300" align="center"/>
    <el-table-column prop="updateTime" label="最后修改时间" width="300" align="center"/>
    <el-table-column fixed="right" label="操作" align="center">
      <template #default="scope">
        <el-button size="small" @click="handleEdit(scope.row.id)">修改</el-button>
        <el-button size="small" type="danger" @click="handleDelete(scope.row.id)">删除</el-button>
      </template>
    </el-table-column>
  </el-table>

  <!-- 新增部门的对话框 -->
  <el-dialog v-model="showDialog" :title="formTitle" width="30%" @close="resetForm">
    <el-form :model="deptForm" :rules="formRules" ref="deptFormRef">
      <el-form-item label="部门名称" prop="name" label-width="80px">
        <el-input v-model="deptForm.name" autocomplete="off"></el-input>
      </el-form-item>
    </el-form>

    <template #footer>
      <span class="dialog-footer">
        <el-button @click="showDialog = false">取消</el-button>
        <el-button type="primary" @click="save">确定</el-button>
      </span>
    </template>
  </el-dialog>

</template>

<style scoped>

</style>

2). 在 src/api/dept.js 中增加如下代码

//添加部门
export const addDeptApi = (data) => request.post('/depts', data)

目前 src/api/dept.js 文件中完整代码如下:

import request from '@/utils/request'

//查询全部部门
export const queryAllApi = () => request.get('/depts')

//添加部门
export const addDeptApi = (data) => request.post('/depts', data)

打开浏览器进行测试,效果如下:

点击保存之后,就会将数据保存到数据库,并重新查询出最新的数据,展示在页面中。

表单校验

Form 组件允许你验证用户的输入是否符合规范,来帮助你找到和纠正错误。Form 组件提供了表单验证的功能,只需为 rules 属性传入约定的验证规则,并将 form-Itemprop 属性设置为需要验证的特殊键值即可。

操作步骤:

  • 通过 ref 属性注册元素的引用。
  • 定义表单校验规则,通过 rules 属性与表单绑定,并通过 prop 属性绑定对应的表单项。这个prop属性就是表示匹配rules当中的哪一个而已.
  • 表单提交时,校验表单,校验通过,则允许提交表单。

- trigger是触发时机,blur表示"鼠标离焦的时候就要校验了". - 注意一个prop可以绑定多个规则. - saveDept当中也要进行校验.标准的如下:(ps:这里的官方样例没有加Value是因为这是从视图里传递而来的,而我们不是) - 这个deptFormRef相当于是一个"表单对象"可以通过这个来进行一些操作.

修改部门

对于修改操作,通常会分为两步进行:

  1. 查询回显
  2. 保存修改

交互逻辑:

  1. 点击 编辑 按钮,根据 ID 进行查询,弹出对话框,完成页面回显展示。(查询回显)
  2. 点击 确定 按钮,保存修改后的数据,完成数据更新操作。(保存修改)

查询回显

1). 在 src/api/dept.js 中定义根据 id 查询的请求

//根据ID查询
export const queryInfoApi = (id) => request.get(`/depts/${id}`)

2). 在 src/views/dept/index.vue 中添加根据 ID 查询回显的逻辑

为修改按钮绑定事件 <template></template>:

<el-button size="small" @click="handleEdit(scope.row.id)">修改</el-button>

<script> </script> 添加 JS 逻辑:

// 编辑部门 - 根据ID查询回显数据
const handleEdit = async (id) => {
  console.log(`Edit item with ID ${id}`);
  formTitle.value = '修改部门'
  showDialog.value = true
  deptForm.value = {name: ''}

  const result = await queryInfoApi(id)
  if(result.code){
    deptForm.value = result.data
  }
};

到目前为止,完整的 src/views/dept/index.vue 代码如下:

<script setup>
import { ref, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import { queryAllApi, addDeptApi, queryInfoApi } from '@/api/dept'

// 声明列表展示数据
let deptList= ref([])

// 动态加载数据 - 查询部门
const queryAll = async () => {
  const result = await queryAllApi()
  deptList.value = result.data
}

// 钩子函数
onMounted(() => {
  queryAll()
})

// 编辑部门 - 根据ID查询回显数据
const handleEdit = async (id) => {
  console.log(`Edit item with ID ${id}`);
  formTitle.value = '修改部门'
  showDialog.value = true
  deptForm.value = {name: ''}

  const result = await queryInfoApi(id)
  if(result.code){
    deptForm.value = result.data
  }
};

const formTitle = ref('')
//新增部门
const add = () => {
  formTitle.value = '新增部门'
  showDialog.value = true
  deptForm.value = {name: ''}
}

// 删除部门 - 根据ID删除部门
const handleDelete = (id) => {
  console.log(`Delete item with ID ${id}`);
  // 在这里实现删除功能
};

// 新增部门对话框的状态
const showDialog = ref(false)
// 表单数据
const deptForm = ref({name: ''})
// 表单验证规则
const formRules = ref({
  name: [
    { required: true, message: '请输入部门名称', trigger: 'blur' },
    { min: 2, max: 10, message: '长度在 2 到 10 个字符', trigger: 'blur' }
  ]
})

// 表单引用
const deptFormRef = ref(null)

// 重置表单
const resetForm = () => {
  deptFormRef.value.resetFields()
}

// 提交表单
const save = async () => {
  await deptFormRef.value.validate(async valid => {
    if (!valid) return
    // 提交表单
    const result = await addDeptApi(deptForm.value)
    if(result.code){
      ElMessage.success('操作成功')
      // 关闭对话框
      showDialog.value = false
      // 重置表单
      resetForm()
      // 重新加载数据
      queryAll()
    }else {
      ElMessage.error(result.msg)
    }
  })
}
</script>

<template>
  <h1>部门管理</h1>

  <!-- 按钮靠页面右侧显示 -->
  <el-button type="primary" @click="add()" style="float: right;"> + 新增部门</el-button> <br><br>

  <!-- 数据展示表格 -->
  <el-table :data="deptList" border style="width: 100%;">
    <el-table-column type="index" label="序号" width="100" align="center"/>
    <el-table-column prop="name" label="部门名称" width="300" align="center"/>
    <el-table-column prop="updateTime" label="最后修改时间" width="300" align="center"/>
    <el-table-column fixed="right" label="操作" align="center">
      <template #default="scope">
        <el-button size="small" @click="handleEdit(scope.row.id)">修改</el-button>//拿到这一行的id
        <el-button size="small" type="danger" @click="handleDelete(scope.row.id)">删除</el-button>
      </template>
    </el-table-column>
  </el-table>

  <!-- 新增部门的对话框 -->
  <el-dialog v-model="showDialog" :title="formTitle" width="30%" @close="resetForm">
    <el-form :model="deptForm" :rules="formRules" ref="deptFormRef">
      <el-form-item label="部门名称" prop="name" label-width="80px">
        <el-input v-model="deptForm.name" autocomplete="off"></el-input>
      </el-form-item>
    </el-form>

    <template #footer>
      <span class="dialog-footer">
        <el-button @click="showDialog = false">取消</el-button>
        <el-button type="primary" @click="save">确定</el-button>
      </span>
    </template>
  </el-dialog>

</template>

<style scoped>

</style>

打开浏览器测试一下,根据 ID 查询部门页面回显:

保存修改

由于 新增部门 和 修改部门使用的是同一个 Dialog 对话框,当前点击 “确定” 按钮的时候,有可能执行的是新增操作,也有可能是修改操作。

那应该如何辨别到底是新增,还是修改操作呢 ?

其实,我们只需要根据 deptForm 对象的 id 属性值,来判断即可。 如果没有 id,则是新增操作 ;如果有 id,则是修改操作。

所以,保存修改功能实现如下:

1). 在 src/api/dept.js 中增加如下修改部门的请求

//修改部门
export const updateDeptApi = (data) => request.put('/depts', data)

2). 在 src/views/dept/index.vue 中引入上述函数,并完善(修改) save 函数的逻辑

import { queryAllApi, addDeptApi, queryInfoApi, updateDeptApi } from '@/api/dept'
// 提交表单
const save = async () => {
  await deptFormRef.value.validate(async valid => {
    if (!valid) return
    // 提交表单
    let result = null;
    if(deptForm.value.id){
      result = await updateDeptApi(deptForm.value) // 修改
    }else {
      result = await addDeptApi(deptForm.value) // 新增
    } 
    if(result.code){
      ElMessage.success('操作成功')
      // 关闭对话框
      showDialog.value = false
      // 重置表单
      resetForm()
      // 重新加载数据
      queryAll()
    }else {
      ElMessage.error(result.msg)
    }
  })
}

最终完整代码如下:

<script setup>
import { ref, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import { queryAllApi, addDeptApi, queryInfoApi, updateDeptApi } from '@/api/dept'

// 声明列表展示数据
let deptList = ref([])

// 动态加载数据 - 查询部门
const queryAll = async () => {
  const result = await queryAllApi()
  deptList .value = result.data
}

// 钩子函数
onMounted(() => {
  queryAll()
})

const formTitle = ref('')
//新增部门
const add = () => {
  formTitle.value = '新增部门'
  showDialog.value = true
  deptForm.value = {name: ''}
}

// 编辑部门 - 根据ID查询回显数据
const handleEdit = async (id) => {
  console.log(`Edit item with ID ${id}`);
  formTitle.value = '修改部门'
  showDialog.value = true
  deptForm.value = {name: ''}

  const result = await queryInfoApi(id)
  if(result.code){
    deptForm.value = result.data
  }
};

// 删除部门 - 根据ID删除部门
const handleDelete = (id) => {
  console.log(`Delete item with ID ${id}`);
  // 在这里实现删除功能
};

// 新增部门对话框的状态
const showDialog = ref(false)
// 表单数据
const deptForm = ref({name: ''})
// 表单验证规则
const formRules = ref({
  name: [
    { required: true, message: '请输入部门名称', trigger: 'blur' },
    { min: 2, max: 10, message: '长度在 2 到 10 个字符', trigger: 'blur' }
  ]
})

// 表单引用
const deptFormRef = ref(null)

// 重置表单
const resetForm = () => {
  deptFormRef.value.resetFields()
}

// 提交表单
const save = async () => {
  await deptFormRef.value.validate(async valid => {
    if (!valid) return
    // 提交表单
    let result = null;
    if(deptForm.value.id){
      result = await updateDeptApi(deptForm.value) // 修改
    }else {
      result = await addDeptApi(deptForm.value) // 新增
    } 
    if(result.code){
      ElMessage.success('操作成功')
      // 关闭对话框
      showDialog.value = false
      // 重置表单
      resetForm()
      // 重新加载数据
      queryAll()
    }else {
      ElMessage.error(result.msg)
    }
  })
}
</script>

<template>
  <h1>部门管理</h1>

  <!-- 按钮靠页面右侧显示 -->
  <el-button type="primary" @click="add()" style="float: right;"> + 新增部门</el-button> <br><br>

  <!-- 数据展示表格 -->
  <el-table :data="deptList" border style="width: 100%;">
    <el-table-column type="index" label="序号" width="100" align="center"/>
    <el-table-column prop="name" label="部门名称" width="300" align="center"/>
    <el-table-column prop="updateTime" label="最后修改时间" width="300" align="center"/>
    <el-table-column fixed="right" label="操作" align="center">
      <template #default="scope">
        <el-button size="small" @click="handleEdit(scope.row.id)">修改</el-button>
        <el-button size="small" type="danger" @click="handleDelete(scope.row.id)">删除</el-button>
      </template>
    </el-table-column>
  </el-table>

  <!-- 新增部门的对话框 -->
  <el-dialog v-model="showDialog" :title="formTitle" width="30%" @close="resetForm">
    <el-form :model="deptForm" :rules="formRules" ref="deptFormRef">
      <el-form-item label="部门名称" prop="name" label-width="80px">
        <el-input v-model="deptForm.name" autocomplete="off"></el-input>
      </el-form-item>
    </el-form>

    <template #footer>
      <span class="dialog-footer">
        <el-button @click="showDialog = false">取消</el-button>
        <el-button type="primary" @click="save">确定</el-button>
      </span>
    </template>
  </el-dialog>

</template>

<style scoped>

</style>

打开浏览器,测试修改员工功能:

删除部门

  • 点击删除按钮,需要删除当前这条数据,删除完成之后,刷新页面,展示出最新的数据。
  • 由于删除是一个比较危险的操作,为避免误操作,通常会在点击删除之后,弹出确认框进行确认。
  • Element 组件:ElMessageBox 消息弹出框组件

具体操作步骤:

1). 在 src/api/dept.js 中增加如下删除部门的请求

//删除部门
export const deleteDeptApi = (id) => request.delete(`/depts?id=${id}`)

2). 在 src/views/dept/index.vue 中为 删除 按钮绑定事件

<el-button size="small" type="danger" @click="handleDelete(scope.row.id)">删除</el-button>

3). 在 src/views/dept/index.vue 编写根据 ID 删除数据的函数 。

// 删除部门 - 根据ID删除部门
const handleDelete = (id) => {
  console.log(`Delete item with ID ${id}`);
  //删除部门时, 需要弹出一个确认框, 如果是确认, 则删除部门
  ElMessageBox.confirm('此操作将永久删除该部门, 是否继续?', '提示', {
    confirmButtonText: '确定',
    cancelButtonText: '取消',
    type: 'warning'
  }).then(async () => {
    // 删除部门
    const result = await deleteDeptApi(id)
    if(result.code){
      ElMessage.success('删除成功')
      queryAll()
    }
  })
};

注意上述,用到了 ElementPlus 中的 ElMessageBox 组件,需要 import 导入进来。代码如下:

import { ElMessage, ElMessageBox } from 'element-plus'

到目前为止,完整的 src/views/dept/index.vue 代码如下:

<script setup>
import { ref, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { queryAllApi, addDeptApi, queryInfoApi, updateDeptApi, deleteDeptApi } from '@/api/dept'

// 声明列表展示数据
let deptList= ref([])

// 动态加载数据 - 查询部门
const queryAll = async () => {
  const result = await queryAllApi()
  deptList.value = result.data
}

// 钩子函数
onMounted(() => {
  queryAll()
})

const formTitle = ref('')
//新增部门
const add = () => {
  formTitle.value = '新增部门'
  showDialog.value = true
  deptForm.value = {name: ''}
}

// 编辑部门 - 根据ID查询回显数据
const handleEdit = async (id) => {
  console.log(`Edit item with ID ${id}`);
  formTitle.value = '修改部门'
  showDialog.value = true
  deptForm.value = {name: ''}

  const result = await queryInfoApi(id)
  if(result.code){
    deptForm.value = result.data
  }
};

// 删除部门 - 根据ID删除部门
const handleDelete = (id) => {
  console.log(`Delete item with ID ${id}`);
  //删除部门时, 需要弹出一个确认框, 如果是确认, 则删除部门
  ElMessageBox.confirm('此操作将永久删除该部门, 是否继续?', '提示', {
    confirmButtonText: '确定',
    cancelButtonText: '取消',
    type: 'warning'
  }).then(async () => {
    // 删除部门
    const result = await deleteDeptApi(id)
    if(result.code){
      ElMessage.success('删除成功')
      queryAll()
    }
  })
};

// 新增部门对话框的状态
const showDialog = ref(false)
// 表单数据
const deptForm = ref({name: ''})
// 表单验证规则
const formRules = ref({
  name: [
    { required: true, message: '请输入部门名称', trigger: 'blur' },
    { min: 2, max: 10, message: '长度在 2 到 10 个字符', trigger: 'blur' }
  ]
})

// 表单引用
const deptFormRef = ref(null)

// 重置表单
const resetForm = () => {
  deptFormRef.value.resetFields()
}

// 提交表单
const save = async () => {
  await deptFormRef.value.validate(async valid => {
    if (!valid) return
    // 提交表单
    let result = null;
    if(deptForm.value.id){
      result = await updateDeptApi(deptForm.value) // 修改
    }else {
      result = await addDeptApi(deptForm.value) // 新增
    } 
    if(result.code){
      ElMessage.success('操作成功')
      // 关闭对话框
      showDialog.value = false
      // 重置表单
      resetForm()
      // 重新加载数据
      queryAll()
    }else {
      ElMessage.error(result.msg)
    }
  })
}
</script>

<template>
  <h1>部门管理</h1>

  <!-- 按钮靠页面右侧显示 -->
  <el-button type="primary" @click="add()" style="float: right;"> + 新增部门</el-button> <br><br>

  <!-- 数据展示表格 -->
  <el-table :data="deptList" border style="width: 100%;">
    <el-table-column type="index" label="序号" width="100" align="center"/>
    <el-table-column prop="name" label="部门名称" width="300" align="center"/>
    <el-table-column prop="updateTime" label="最后修改时间" width="300" align="center"/>
    <el-table-column fixed="right" label="操作" align="center">
      <template #default="scope">
        <el-button size="small" @click="handleEdit(scope.row.id)">修改</el-button>
        <el-button size="small" type="danger" @click="handleDelete(scope.row.id)">删除</el-button>
      </template>
    </el-table-column>
  </el-table>

  <!-- 新增部门的对话框 -->
  <el-dialog v-model="showDialog" :title="formTitle" width="30%" @close="resetForm">
    <el-form :model="deptForm" :rules="formRules" ref="deptFormRef">
      <el-form-item label="部门名称" prop="name" label-width="80px">
        <el-input v-model="deptForm.name" autocomplete="off"></el-input>
      </el-form-item>
    </el-form>

    <template #footer>
      <span class="dialog-footer">
        <el-button @click="showDialog = false">取消</el-button>
        <el-button type="primary" @click="save">确定</el-button>
      </span>
    </template>
  </el-dialog>

</template>

<style scoped>

</style>

打开浏览器,测试删除员工功能: