ElementPlus
简介
ElementPlus是饿了么团队研发的,基于Vue3的组件库
准备工作:
-
创建工程化的Vue项目 选择 TypeScript
-
参照官方文档安装ElementPlus组件库(当前工程的目录下)
npm install element-plus --save
- main.ts中引入Element Plus组件库 参照官方文档
//main.ts
import { createApp } from 'vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import App from './App.vue'
const app = createApp(App)
app.use(ElementPlus)
app.mount('#app')
- 复制组件代码,调整
常用组件
Button组件
对应的代码:
<template>
<el-row class="mb-4">
<el-button>Default</el-button>
<el-button type="primary">Primary</el-button>
<el-button type="success">Success</el-button>
<el-button type="info">Info</el-button>
<el-button type="warning">Warning</el-button>
<el-button type="danger">Danger</el-button>
</el-row>
<el-row class="mb-4">
<el-button plain>Plain</el-button>
<el-button type="primary" plain>Primary</el-button>
<el-button type="success" plain>Success</el-button>
<el-button type="info" plain>Info</el-button>
<el-button type="warning" plain>Warning</el-button>
<el-button type="danger" plain>Danger</el-button>
</el-row>
</template>
plain 控制背景色变淡,添加边框
表格组件
表格用于展示多条结构类似的数据,可以对数据进行排序、筛选、对比或自定义操作
<template>
<!--data:数据源数组,border:带有纵向边框-->
<el-table :data="tableData" border style="width: 100%">
<!--prop:数组中每一个对象的属性名 label:表头的名称-->
<el-table-column prop="date" label="Date" width="180" />
<el-table-column prop="name" label="Name" width="180" />
<el-table-column prop="address" label="Address" />
<!--有几个标签就渲染几列,源数组中有几个对象就渲染几行-->
</el-table>
</template>
<script lang="ts" setup>
const tableData = [
{
date: '2016-05-03',
name: 'Tom',
address: 'No. 189, Grove St, Los Angeles',
},
{
date: '2016-05-02',
name: 'Tom',
address: 'No. 189, Grove St, Los Angeles',
}
</script>
在以上基础上再增加一列:Author,需要变动数据源数组和增加一个el-table-column标签:
<template>
<el-table :data="tableData" border style="width: 100%">
<el-table-column prop="date" label="Date" width="180" />
<el-table-column prop="name" label="Name" width="180" />
<el-table-column prop="address" label="Address" />
<el-table-column prop="author" label="Author" /> <!--增加一列-->
</el-table>
</template>
<script setup lang="ts">
const tableData = [
{
date: '2016-05-03',
name: 'Tom',
address: 'No. 189, Grove St, Los Angeles',
author : 'EUNEIR' //增加一个属性
},
{
date: '2016-05-02',
name: 'Tom',
address: 'No. 189, Grove St, Los Angeles',
author : 'EUNEIR'
}
]
</script>
ElementPlus还提供了非常多的表格和表格属性
分页组件
开启中文语言包:
import ElementPlus from 'element-plus'
import zhCn from 'element-plus/dist/locale/zh-cn.mjs' //报错
app.use(ElementPlus, {
locale: zhCn,
})
需要额外配置env.d.ts:
/// <reference types="vite/client" />
declare module 'element-plus/dist/locale/zh-cn.mjs'
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:page-sizes="[100, 200, 300, 400]"
:small="small"
:disabled="disabled"
:background="background"
layout="total, sizes, prev, pager, next, jumper"
:total="400"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
分页组件中的各部分及其顺序是由layout指定的,如果页码/分页记录数变化会触发@size-change/@current-change事件的回调函数执行
import { ref } from 'vue'
const currentPage = ref(4)
const pageSize = ref(100)
const small = ref(false)
const background = ref(false)
const disabled = ref(false)
const total = ref(400)
const handleSizeChange = (val: number) => {
console.log(`${val} items per page`)
}
const handleCurrentChange = (val: number) => {
console.log(`current page: ${val}`)
}
对话框组件
<!--对应Button组件中的设置-->
<el-button text @click="dialogTableVisible = true">
打开对话框
</el-button>
<!--dialogTableVisible控制对话框的显示与因此,title是对话框的标题-->
<el-dialog v-model="dialogTableVisible" title="Shipping address">
<!--以下内容就是表格组件中的设置-->
<el-table :data="tableData">
<el-table-column property="date" label="Date" width="150" />
<el-table-column property="name" label="Name" width="200" />
<el-table-column property="address" label="Address" />
</el-table>
</el-dialog>
显示的效果不甚明显:
可以根据上文Button组件的设置来更改这个Button组件的样式:
<el-button type="primary" @click="dialogTableVisible = true">
打开对话框
</el-button>
表单组件
<!--inline:行内表单,model:表单数据对象-->
<el-form :inline="true" :model="formInline" class="demo-form-inline">
<el-form-item label="用户名">
<!--表单项双向绑定-->
<el-input v-model="formInline.user" placeholder="用户名" clearable/>
</el-form-item>
<el-form-item label="区域">
<el-select
v-model="formInline.region"
placeholder="区域"
clearable
>
<el-option label="上海" value="shanghai"/>
<el-option label="北京" value="beijing"/>
</el-select>
</el-form-item>
<el-form-item label="时间">
<el-date-picker
v-model="formInline.date"
type="date"
placeholder="选择时间"
clearable
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="onSubmit">查询</el-button>
</el-form-item>
</el-form>
const formInline = ref({
user : '',
region : '',
date : ''
})
const onSubmit = () => {
console.log(formInline.value)
};
案例
请求地址:
http://47.98.197.202/api/emps/list?name=&gender=&job=
<template>
<el-form :inline="true" :model="emp" class="demo-form-inline">
<el-form-item label="姓名">
<el-input v-model="emp.name" placeholder="请输入姓名" clearable />
</el-form-item>
<el-form-item label="性别">
<el-select
v-model="emp.gender"
placeholder="请选择"
clearable
>
<el-option label="男" value="1" />
<el-option label="女" value="2" />
</el-select>
</el-form-item>
<el-form-item label="职位">
<el-select
v-model="emp.job"
placeholder="请选择"
clearable>
<!--下拉列表的选项-->
<el-option label="班主任" value="1" />
<el-option label="讲师" value="2" />
<el-option label="其他" value="3" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="query">查询</el-button>
<el-button type="primary" @click="clear">清空</el-button>
</el-form-item>
</el-form>
<el-table :data="empList" border style="width: 100%">
<!--五列 对应5个column-->
<el-table-column prop="id" label="ID" width="180" />
<el-table-column prop="name" label="姓名" width="180" />
<el-table-column prop="img" label="头像" />
<el-table-column prop="gender" label="性别" />
<el-table-column prop="job" label="职位" />
<el-table-column prop="entrydate" label="入职日期" />
<el-table-column prop="updatetime" label="更新时间" />
</el-table>
</template>
如果要显示图片,就必须使用img标签,ElementPlus封装的el-table-column不能显示图片
需要使用ElementPlus提供的自定义列模板,用来自定义这一列的展示内容
<template #default="scope">
</template>
#default
是 插槽 slot,通过插槽可以获取到row、column、$index、store
最终表单部分:
<el-table :data="empList" border style="width: 100%">
<!--五列 对应5个column-->
<el-table-column prop="id" label="ID" width="180" align="center"/>
<el-table-column prop="name" label="姓名" width="180" align="center"/>
<el-table-column prop="image" label="头像" align="center">
<template #default="scope">
<img :src="scope.row.image" width="50px">
</template>
</el-table-column>
<el-table-column prop="gender" label="性别" align="center">
<template #default="scope">
{{scope.row.gender == 1 ? '男' : '女'}}
</template>
</el-table-column>
<el-table-column prop="job" label="职位" align="center">
<template #default="scope">
{{scope.row.job == 1 ? '班主任' : scope.row.job == 2 ? '讲师' : '其他'}}
</template>
</el-table-column>
<el-table-column prop="entrydate" label="入职日期" align="center"/>
<el-table-column prop="updatetime" label="更新时间" align="center"/>
</el-table>
Tlias
准备工作
- 安装依赖
npm install element-plus --save
npm install axios
- 配置ElementPlus
//main.ts
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'
import './assets/main.css'
//导入elementPlus
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
const app = createApp(App)
//注册ElementPlus的Icon组件
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
app.use(createPinia())
app.use(router)
app.use(ElementPlus, {locale: zhCn})
app.mount('#app')
//env.d.ts
declare module 'element-plus/dist/locale/zh-cn.mjs';
页面布局
公共的css属性可以定义在main.css中:
main.css
*{
margin: 0;
}
Container
布局需要使用Container布局容器:
<el-container>
: 外层容器<el-header>
: 顶栏容器<el-aside>
:侧边栏容器<el-container>
:主要区域容器<el-footer>
: 底栏容器
<!--IndexView.vue-->
<template>
<div class="common-layout">
<el-container>
<el-header class="header"> <HeaderComponent/> </el-header>
<el-container>
<el-aside width="200px" class="aside">
<AsideComponent/>
</el-aside>
<el-main>Main</el-main>
</el-container>
</el-container>
</div>
</template>
Header
<!--HeaderComponent.vue-->
<script setup lang="ts">
</script>
<template>
<span class="title">Tlias智能学习辅助系统</span>
<span class="right_tool">
<a href="">
<!--图标-->
<el-icon><EditPen /></el-icon> 修改密码 |
</a>
<a href="">
<el-icon><SwitchButton /></el-icon> 退出登录
</a>
</span>
</template>
<style scoped>
.title {
color: white;
font-size: 40px;
font-family: 楷体;
line-height: 60px;
}
.right_tool{
float: right;
line-height: 60px;
}
a {
color: white;
text-decoration: none;
}
</style>
修改密码和退出登录 需要使用ElementPlus提供的图标,官网提供的使用方式:
需要从
@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)
}
在图标合集中选择图标就可以直接使用:
<a href="">
<el-icon><SwitchButton /></el-icon> 退出登录
</a>
Aside
<el-aside width="200px">
<el-scrollbar>
<el-menu :default-openeds="['1', '3']">
<!--el-sub-menu是一个子菜单-->
<el-sub-menu index="1">
<template #title>
<el-icon><message /></el-icon>Navigator One
</template>
<!--el-menu-item-group是子菜单的一组-->
<el-menu-item-group>
<template #title>Group 1</template>
<!--el-menu-item是一个菜单项-->
<el-menu-item index="1-1">Option 1</el-menu-item>
<el-menu-item index="1-2">Option 2</el-menu-item>
</el-menu-item-group>
<el-menu-item-group title="Group 2">
<el-menu-item index="1-3">Option 3</el-menu-item>
</el-menu-item-group>
<el-sub-menu index="1-4">
<template #title>Option4</template>
<el-menu-item index="1-4-1">Option 4-1</el-menu-item>
</el-sub-menu>
</el-sub-menu>
<el-sub-menu index="2">
<template #title>
<el-icon><icon-menu /></el-icon>Navigator Two
</template>
<el-menu-item-group>
<template #title>Group 1</template>
<el-menu-item index="2-1">Option 1</el-menu-item>
<el-menu-item index="2-2">Option 2</el-menu-item>
</el-menu-item-group>
<el-menu-item-group title="Group 2">
<el-menu-item index="2-3">Option 3</el-menu-item>
</el-menu-item-group>
<el-sub-menu index="2-4">
<template #title>Option 4</template>
<el-menu-item index="2-4-1">Option 4-1</el-menu-item>
</el-sub-menu>
</el-sub-menu>
<el-sub-menu index="3">
<template #title>
<el-icon><setting /></el-icon>Navigator Three
</template>
<el-menu-item-group>
<template #title>Group 1</template>
<el-menu-item index="3-1">Option 1</el-menu-item>
<el-menu-item index="3-2">Option 2</el-menu-item>
</el-menu-item-group>
<el-menu-item-group title="Group 2">
<el-menu-item index="3-3">Option 3</el-menu-item>
</el-menu-item-group>
<el-sub-menu index="3-4">
<template #title>Option 4</template>
<el-menu-item index="3-4-1">Option 4-1</el-menu-item>
</el-sub-menu>
</el-sub-menu>
</el-menu>
</el-scrollbar>
</el-aside>
当前项目的需求:
四个子菜单,没有分组
Main
配置嵌套路由:
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
path : '/',
name : 'home',
component : () => import('../views/layout/IndexView.vue'),
children : [ //嵌套路由
{
path : 'index',
name : 'index',
component : () => import('../views/index/WelcomePageIndex.vue')
},
{
path : 'emp',
name : 'emp',
component : () => import('../views/emp/EmpIndex.vue')
},
{
path : 'dept',
name : 'dept',
component : () => import('../views/dept/DeptIndex.vue')
},
{
path : 'clazz',
name : 'clazz',
component : () => import('../views/clazz/ClazzIndex.vue')
},
]
})
export default router
App.vue:
<script setup lang="ts">
</script>
<template>
<!--IndexView-->
<RouterView/>
</template>
<style scoped>
</style>
IndexView.vue:
<template>
<div class="common-layout">
<el-container>
<el-header class="header"> <HeaderComponent/> </el-header>
<el-container>
<el-aside width="200px" class="aside">
<AsideComponent/>
</el-aside>
<el-main> <RouterView/> </el-main>
</el-container>
</el-container>
</div>
</template>
<el-scrollbar>
<el-menu router>
<!-- 首页菜单 -->
<!--启用vue-router模式,将index作为path进行跳转-->
<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-scrollbar>
但是当前直接访问系统的界面:
因为默认的请求路径是:http://127.0.0.1:5173/
,路由能匹配到IndexView.vue,匹配不到IndexView内部的RouterView,所以只渲染了IndexView
解决办法:对路由 /
进行重定向:
{
path : '/',
name : 'home',
component : () => import('../views/layout/IndexView.vue'),
redirect : '/index',
children : [ //嵌套路由
{
path : 'index',
name : 'index',
component : () => import('../views/index/WelcomePageIndex.vue')
}
}
访问 / 就会访问到index
部门管理功能实现
查询所有
页面布局
需要的组件:Button、Table
<script setup lang="ts">
import {ref} from "vue";
//声明表格的数据模型
let deptList = ref([]);
</script>
<template>
<h1>部门管理</h1>
<el-button type="primary">+ 新增部门</el-button>
<el-table :data="deptList" border style="width: 100%">
<el-table-column prop="date" label="Date" width="180" />
<el-table-column prop="name" label="Name" width="180" />
<el-table-column prop="address" label="Address" />
</el-table>
</template>
<style scoped>
</style>
但是我们目前使用的是ts,对于ref可以指定泛型,用来规定其中存储的数据类型,而deptList是请求服务器返回的数据,接口文档中规定了响应数据的格式:
{
"code": 1,
"msg": "success",
"data": [
{
"id": 1,
"name": "学工部",
"createTime": "2022-09-01T23:06:29",
"updateTime": "2022-09-01T23:06:29"
},
{
"id": 2,
"name": "教研部",
"createTime": "2022-09-01T23:06:29",
"updateTime": "2022-09-01T23:06:29"
}
]
}
此处的泛型就是数组类型,数组中存储的元素类型是我们定义的:
interface deptModel{
id?: number,
name: string,
updateTime?: string
}
- 没有定义createTime:前端不需要展示createTime
- updateTime和id定义为可选参数,因为dept不仅只有查询的部门,也会有新增的部门(新增的部门没有id和更新时间),这是交给后端定义的字段
定义泛型:
//声明部门的数据类型
interface deptModel{
id?: number,
name: string,
updateTime?: string
}
//声明表格的数据模型
//泛型是deptModel类型的数组
let deptList = ref<deptModel[]>([]);
一般会将所有的泛型和类型别名定义在单独的ts文件中,一般在api/model/model.ts中:
//api/model/model.ts
// ----------------------- 部门数据相关接口及类型 ---------------------
//部门数据接口
// ? 新增
export interface DeptModel {
id?: number,
name: string,
updateTime?: string
}
//部门数据数组的类型别名
export type DeptModelArray = DeptModel[]
在需要的地方引入即可:
import {ref} from "vue";
//引入类型/接口需要使用type关键字
import type {DeptModelArray} from "../../api/model/model";
//声明表格的数据模型
//泛型是deptModel类型的数组
let deptList = ref<DeptModelArray>([]);
在此处引入DeptModelArray的时候,需要回退两级目录,可以用@代表根目录src,直接在根目录下引入:
//@代表src目录
import type {DeptModelArray} from "@/api/model/model";
接下来继续完善表格的数据显示,界面原型显示需要四列:
<el-table :data="deptList" border style="width: 100%">
<el-table-column prop="date" label="序号" width="180" align="center"/>
<el-table-column prop="name" label="部门名称" width="180" align="center"/>
<el-table-column prop="address" label="最后操作时间" align="center"/>
<el-table-column prop="address" label="操作" align="center"/>
</el-table>
prop指定的是属性名,而属性名在 数据类型接口 interface DeptModel中指定了:
//api/model/model.ts
// ----------------------- 部门数据相关接口及类型 ---------------------
//部门数据接口
// ? 新增
export interface DeptModel {
id?: number,
name: string,
updateTime?: string
}
//部门数据数组的类型别名
export type DeptModelArray = DeptModel[]
其中的序号并不是id属性,ElementPlus给出了显示序号的解决办法:设置 type
属性为 index
即可显示从 1 开始的索引号。
<el-table :data="deptList" border style="width: 100%">
<el-table-column prop="" label="序号" width="180" align="center">
<template #default="scope">
</template>
</el-table-column>
<el-table-column prop="name" label="部门名称" width="180" align="center"/>
<el-table-column prop="updateTime" label="最后操作时间" align="center"/>
<el-table-column prop="" label="操作" align="center">
<template #default="scope">
</template>
</el-table-column>
加载数据
需求:
- 增删改完毕后,加载最新的部门数据
- 打开页面后,加载最新的部门数据
定义查询部门列表的函数:
//查询部门列表
const search = async ()=> {
let promise = await axios.get('/api/depts');
//返回了一个Promise对象,data字段封装了响应的数据,在后端是Result格式的JSON字符串
console.log(promise)
//promise.data 是Result,再.data是结果
deptList.value = promise.data.data;
}
onMounted(() => {
search();
});
在后端没有开发好的情况下,可以使用Apifox的Mock功能:
复制链接作为get方法的入参就可以进行测试了
search方法最好加一个判断,根据Result的code字段进行判断:
//查询部门列表
const search = async () => {
let promise = await axios.get('https://mock.apifox.com/m1/3708703-0-default/depts');
//返回了一个Promise对象,data字段封装了响应的数据,在后端是Result格式的JSON字符串
console.log(promise)
if (promise.data.code) {
//promise.data 是Result,再.data是结果
deptList.value = promise.data.data;
}
}
onMounted(() => {
search();
});
当前访问的是服务器的接口,需要进行跨域的处理:
//vite.config.ts
export default defineConfig({
plugins: [
vue(),
vueJsx(),
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
},
//跨域
server: {
cors: true,
open: true,
port: 5173,
proxy: {
'^/api': {
target: 'http://localhost:8080/',
changeOrigin: true,
//需要对/api的/进行转义
rewrite: (path) => path.replace(/^\/api/, '')
}
}
}
以上配置的含义是,匹配到/api开始的请求都将目的地址改为:http://localhost:8080/api/path,以/api/dept为例:
axios.get('/api/dept') -拦截请求-> http://localhost:8080/api/dept -/api替换为空字符串-> http://localhost:8080/dept
所以请求的方法可以直接请求/api/dept:
//查询部门列表
const search = async () => {
let promise = await axios.get('/api/depts');
//返回了一个Promise对象,data字段封装了响应的数据,在后端是Result格式的JSON字符串
console.log(promise)
if (promise.data.code) {
//promise.data 是Result,再.data是结果
deptList.value = promise.data.data;
}
}
初步优化:泛型
但是每次请求都带有/api还是比较繁琐的,并且promise.data是后端的Result对象,每次都要从promise中把Result提取出来再 .data获取数据,提取Result的操作是相同的,可以对程序进行初步优化:
- 封装请求工具类utils/request.ts:
const request = axios.create({
//请求均以/api开始
baseURL : '/api',
timeout : 60000
});
//axios的响应response的拦截器
request.interceptors.response.use(
//成功回调
(response) => {
//提取Result,await request.get()的返回值就是Result对象
return response.data;
},
//失败回调
(error) => {
//拿到错误信息,继续失败回调
return Promise.reject(error);
}
);
export default request;
/api是为了区分ajax请求,其他请求不需要Tomcat处理
axios被封装为request对象,请求可以直接通过request发起,响应的数据经过拦截器的提取,只取出服务器端响应的纯数据,我们得到的就是Result对象,此时发起请求:
const search = async () => {
//拦截器提取出Result对象
let dept = await request.get('/depts');
console.log(dept);
if (dept.code){
deptList.value = dept.data;
}
}
但是这样做,在ts下会提示错误:
因为没有指定get方法的返回值类型为ResultModel,ts无法得知其中是否有code属性
axios的get方法是有泛型的:
get<T = any, R = AxiosResponse<T>, D = any>(url: string, config?: AxiosRequestConfig<D>): Promise<R>;
axios的get方法实际上是对axios.request的一层封装,request方法:
request<T = any, R = AxiosResponse<T>, D = any>(config: AxiosRequestConfig<D>): Promise<R>;
request方法有三个泛型,T、R、D,接受AxiosRequestConfig类型的参数作为配置对象,返回值是接受泛型R的Promise类型
R的默认类型 AxiosResponse:
export interface AxiosResponse<T = any, D = any> {
data: T;
status: number;
statusText: string;
headers: RawAxiosResponseHeaders | AxiosResponseHeaders;
config: AxiosRequestConfig<D>;
request?: any;
}
AxiosResponse就是响应拦截器用到的response对象的类型:
T就是服务器端返回的数据的类型,而服务器端返回的类型是不确定的,所以定义为any
再看request方法的定义:
request<T = any, R = AxiosResponse<T>, D = any>(config: AxiosRequestConfig<D>): Promise<R>;
- T:服务器返回数据的类型
- R:服务器返回的数据经过axios一层封装得到的response对象的类型
request方法的返回值是Promise,值就是成功态的R,也就是response对象。
{ // <- AxiosResponse
data: {
code : '',
msg : '',
data : any
},
status: number,
statusText: string,
headers: RawAxiosResponseHeaders | AxiosResponseHeaders,
config: AxiosRequestConfig<D>,
request?: any
}
所以get、post、put方法的返回值都是Promise,值均为成功态的R,也就是response对象
再看我们的封装:
const request = axios.create({
baseURL: '/api',
timeout: 600000
})
//axios的响应 response 拦截器
request.interceptors.response.use(
(response) => { //成功回调
return response.data
},
(error) => { //失败回调
return Promise.reject(error)
}
)
export default request
其实就是将response中的data提取出来了,上文中提到data的类型是T=any,这样get请求得到的结果类型一定是T,因为get请求的结果就是Promise,也就是成功态的R,而R已经被我们在拦截器中转换为T了,所以我们可以直接指定T和R的类型:
const search = async () => {
//改变了await request.get方法的返回值
let dept = await request.get<ResultModel,ResultModel>('/depts') ;
console.log(dept);
if (dept.code){
deptList.value = dept.data;
}
}
此时就不会报错了。
但是这种做法是不正确的,axios的拦截器可以配置多个,多个拦截器会形成一个拦截器链,每个拦截器链的参数都是AxiosResponse类型,如果在响应回调里直接return response.data,R就变为T了,应该保证每个拦截器的签名一直,否则对下游的拦截器可能产生影响,不建议这样操作,应该将axios的get、put、post方法统一封装,返回最终需要指定的类型。
分层优化
现代前端开发会将和服务器端交互的逻辑定义在单独的api中,例如:api/dept.ts
//其实是拦截器将R变为T了,此处才能写ResultModel
export const queryAllDepts = () => request.get<any,ResultModel>('/depts');
调用:
const search = async () => {
//直接调用该函数发送请求即可
//await 拿到的就是成功态的R,拦截器已经将R变为T了
let result = await queryAllDepts();
if (result.code) {
deptList.value = result.data;
}
}
新增部门
点击新增部门按钮,弹出Dialog对话框
<script setup lang="ts">
//新增部门
// 1. 对话框
let dialogFormVisible = ref<boolean>(false);
// 表单数据,类型限定必须指定name
let dept = ref<DeptModel>({name:''});
// 2. 弹窗
let add = () => {
dialogFormVisible.value = true;
}
</script>
<template>
<h1>部门管理</h1>
<el-button type="primary" @click="add">+ 新增部门</el-button>
<el-table :data="deptList" border style="width: 100%">
...
<el-table-column prop="" label="操作" align="center">
<template #default="scope">
<el-button type="success" size="small">编辑</el-button>
<el-button type="danger" size="small">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-dialog v-model="dialogFormVisible" title="Shipping address">
<el-form :model="dept">
<el-form-item label="Promotion name" >
<el-input v-model="dept.name" autocomplete="off" />
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogFormVisible = false">Cancel</el-button>
<el-button type="primary" @click="dialogFormVisible = false">
Confirm
</el-button>
</span>
</template>
</el-dialog>
</template>
<style scoped>
</style>
此时的效果:
对话框的标题不应该直接指定为 新增部门 ,编辑按钮弹出的对话框和这个相同,编辑时标题应该为 修改部门
在add方法中赋值为新增部门,在update方法中赋值为修改部门
title应该是v-bind绑定的。
完成新增功能:
// api/dept.ts
//接口文档指明参数为dept类型
export const addApi = (dept:DeptModel) => request.post<any,ResultModel>('/depts',dept)
<script setup lang="ts">
//新增部门
// 1. 对话框
let dialogFormVisible = ref<boolean>(false);
// 表单数据,类型限定必须指定name
let dept = ref<DeptModel>({name:''});
// 对话框标题,可能是新增部门/编辑部门
let formTitle = ref<string>('');
// 2. 弹窗
let add = () => {
//显示对话框
dialogFormVisible.value = true;
//标题赋值
formTitle.value = '新增部门';
}
// 3. 保存
let save = async () => {
//调用交互层保存数据,数据在dept对象中
//体现了TS的强大之处,此处很容易写为dept
let result = await addApi(dept.value);
//成功关闭弹窗
if (result.code){
//关闭弹窗
dialogFormVisible.value = false;
//提示操作成功
ElMessage.success('保存成功');
//列表刷新
search();
}else {
//不关闭弹窗:给用户修改的机会
//提示操作失败
ElMessage.error(result.msg);
}
}
</script>
<template>
<h1>部门管理</h1>
<el-button type="primary" @click="add">+ 新增部门</el-button>
<el-table :data="deptList" border style="width: 100%">
...
<template #default="scope">
<el-button type="success" size="small">编辑</el-button>
<el-button type="danger" size="small">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-dialog v-model="dialogFormVisible" :title="formTitle" width="30%">
<el-form :model="dept">
<el-form-item label="部门名称" >
<el-input v-model="dept.name" autocomplete="off" />
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<!--取消直接设置为false-->
<el-button @click="dialogFormVisible = false">取消</el-button>
<!--确认是有逻辑的-->
<el-button type="primary" @click="save">
确定
</el-button>
</span>
</template>
</el-dialog>
</template>
<style scoped>
</style>
但是还是存在问题的:下一次弹窗还会显示dept.value.name的值,因为这次没有清空数据。
- 应该在何处设置清空dept.value.name?
不能在保存成功后清空,如果保存失败用户直接关闭窗口,下一次打开还是原先的数据
应该在弹出对话框时清空
// 2. 弹窗
let add = () => {
//清空之前的dept.value.name
dept.value.name = '';
//显示对话框
dialogFormVisible.value = true;
//标题赋值
formTitle.value = '新增部门';
}
在后端的增/删/改也是有必要返回Result的,可以在前端给用户提供信息参考。
修改部门
分为两步:
- 查询回显
- 保存修改
查询回显
点击编辑按钮,需要查询回显,为编辑按钮绑定update回调函数,需要为其传递参数id
<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="250" align="center"/>
<el-table-column prop="updateTime" label="最后操作时间" align="center" width="350"/>
<el-table-column prop="" label="操作" align="center">
<template #default="scope"> <!--传递id-->
<el-button type="success" size="small" @click="update(scope.row.id)">编辑</el-button>
<el-button type="danger" size="small">删除</el-button>
</template>
</el-table-column>
</el-table>
也体现了后端返回给前端的数据是必须带有id的,这样针对某些数据的操作才能让后端辨别数据身份
//三、修改部门
// 1.1 数据回显
const update = async (id:number)=> {
//清空之前的dept.value.name
dept.value.name = '';
//显示对话框
dialogFormVisible.value = true;
//设置标题
formTitle.value = '修改部门';
//dept.value = (await getInfoById(id)).data
//其实byId应该不能是失败的
let result = await getInfoById(id);
if (result.code){
//直接替换dept对象,替换name TS会报错
//dept.value.name = result.data.name
dept.value = result.data;
}
}
- 注意:时刻注意接口文档中/类型注解中规定的类型
保存修改
点击对话框的保存,触发修改的逻辑,但是新增部门和修改部门的对话框是同一个,在新增部门中,我们已经为对话框的保存绑定了save方法:
<el-dialog v-model="dialogFormVisible" :title="formTitle" width="30%">
<el-form :model="dept">
<el-form-item label="部门名称" >
<el-input v-model="dept.name" autocomplete="off" />
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<!--取消直接设置为false-->
<el-button @click="dialogFormVisible = false">取消</el-button>
<!--确认是有逻辑的-->
<el-button type="primary" @click="save">
确定
</el-button>
</span>
</template>
</el-dialog>
let save = async () => {
//调用交互层保存数据,数据在dept对象中
//体现了TS的强大之处,此处很容易写为dept
let result = await addApi(dept.value);
//成功关闭弹窗
if (result.code){
//关闭弹窗
dialogFormVisible.value = false;
//提示操作成功
ElMessage.success('保存成功');
//列表刷新
search();
}else {
//不关闭弹窗:给用户修改的机会
//提示操作失败
ElMessage.error(result.msg);
}
}
也就是说,在对话框的save方法中既要完成新增,又要完成修改,先定义交互层的修改方法:
export const modifyInfoApi = (dept:DeptModel) => request.put<any,ResultModel>('/depts',dept);
// 保存
let save = async () => {
let result;
//新增和修改的区别是dept.value的id属性是否有值
if (dept.value.id){
//有id修改
result = await modifyInfoApi(dept.value);
}else {
//无id新增
result = await addApi(dept.value);
}
//调用交互层保存数据,数据在dept对象中
//体现了TS的强大之处,此处很容易写为dept
//成功关闭弹窗
if (result.code){
//关闭弹窗
dialogFormVisible.value = false;
//提示操作成功
ElMessage.success('保存成功');
//列表刷新
search();
}else {
//不关闭弹窗:给用户修改的机会
//提示操作失败
ElMessage.error(result.msg);
}
}
//三、修改部门
const update = async (id:number)=> {
// 1. 数据回显
//清空之前的dept.value.name
dept.value.name = '';
//显示对话框
dialogFormVisible.value = true;
//设置标题
formTitle.value = '修改部门';
dept.value = (await getInfoById(id)).data
//其实byId应该不能是失败的
/* let result = await getInfoById(id);
if (result.code){ //直接替换dept对象,替换name TS会报错
//dept.value.name = result.data.name dept.value = result.data; }*/
}
删除部门
-
根据id删除,删除完毕刷新页面
-
点击删除之后弹出确认框 ElMessageBox
<template>
<el-button text @click="open">Click to open the Message Box</el-button>
</template>
<script lang="ts" setup>
import { ElMessage, ElMessageBox } from 'element-plus'
const open = () => {
ElMessageBox.confirm(
'proxy will permanently delete the file. Continue?',
'Warning', //警告图标
{ //确认按钮文本
confirmButtonText: 'OK',
//取消按钮文本
cancelButtonText: 'Cancel',
type: 'warning',
}
)
.then(() => {
ElMessage({
type: 'success',
message: 'Delete completed',
})
})
.catch(() => {
ElMessage({
type: 'info',
message: 'Delete canceled',
})
})
}
</script>
// 四、删除部门
const deleteById = (id:number) => {
//确认是否删除
ElMessageBox.confirm(
'是否确认删除?',
'Warning',
{
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'warning',
}
) //注意async的位置
.then(async () => {
let result = await removeByIdApi(id);
if (result.code){
ElMessage({
type: 'success',
message: '删除成功',
})
}else{
ElMessage.error(result.msg)
}
})
.catch(() => {
ElMessage({
type: 'info',
message: '取消删除',
})
})
//刷新页面
search();
}
表单校验
需要对表单进行校验,ElementPlus给了表单校验的方案:
为rules属性传入约定的验证规则,并且将form-item的prop属性设置为需要验证的特殊键值即可。
<template>
<!--rules属性-->
<el-form
ref="ruleFormRef"
:model="ruleForm"
:rules="rules"
label-width="120px"
class="demo-ruleForm"
:size="formSize"
status-icon
> <!--设置name属性-->
<el-form-item label="Activity name" prop="name">
<el-input v-model="ruleForm.name" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submitForm(ruleFormRef)">
Create
</el-button>
<el-button @click="resetForm(ruleFormRef)">Reset</el-button>
</el-form-item>
</el-form>
</template>
<script lang="ts" setup>
import { reactive, ref } from 'vue'
import type { FormInstance, FormRules } from 'element-plus'
interface RuleForm {
name: string
}
const formSize = ref('default')
const ruleFormRef = ref<FormInstance>()
const ruleForm = reactive<RuleForm>({
name: 'Hello',
})
const rules = reactive<FormRules<RuleForm>>({
name: [
{ required: true, message: 'Please input Activity name', trigger: 'blur' },
{ min: 3, max: 5, message: 'Length should be 3 to 5', trigger: 'blur' },
]
})
const submitForm = async (formEl: FormInstance | undefined) => {
if (!formEl) return
await formEl.validate((valid, fields) => {
if (valid) {
console.log('submit!')
} else {
console.log('error submit!', fields)
}
})
}
const resetForm = (formEl: FormInstance | undefined) => {
if (!formEl) return
formEl.resetFields()
}
const options = Array.from({ length: 10000 }).map((_, idx) => ({
value: `${idx + 1}`,
label: `${idx + 1}`,
}))
</script>
<el-dialog v-model="dialogFormVisible" :title="formTitle" width="30%">
<!--rules绑定校验规则-->
<el-form
:model="dept"
:rules="rules"
>
<!--prop指定使用哪条校验规则-->
<el-form-item label="部门名称" prop="name">
<el-input v-model="dept.name" autocomplete="off"/>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogFormVisible = false">取消</el-button>
<el-button type="primary" @click="save">
确定
</el-button>
</span>
</template>
</el-dialog>
rules:FormRules的泛型需要指定针对哪个类型的校验规则,已经定义了DeptModel可以直接使用
const rules = ref<FormRules<DeptModel>>({
name: [
{ required: true, message: '请输入部门名称', trigger: 'blur' },
{ min: 2, max: 10, message: '部门名称长度在2-10位之间', trigger: 'blur' },
]})
- required:必填
- message:校验失败的提示信息
- triggr:触发校验的事件
但是此时的表单虽然校验不通过,点击保存按钮还是可以发起请求的,在save方法中我们应该判断表单校验是否通过,需要拿到表单的实例,通过实例进行校验
定义表单的实例引用对象:
const deptForm = ref<FormInstance>();
保存按钮:
<el-dialog v-model="dialogFormVisible" :title="formTitle" width="30%">
<el-form
:model="dept"
:rules="rules"
>
<el-form-item label="部门名称" prop="name">
<el-input v-model="dept.name" autocomplete="off"/>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogFormVisible = false">取消</el-button>
<!--保存按钮传递表单的校验规则-->
<el-button type="primary" @click="save(deptForm)"> <!--也可以不定义这个参数-->
确定
</el-button>
</span>
</template>
</el-dialog>
save方法进行校验:
let save = async (form:FormInstance | undefined) => {
if (!form) return
await form.validate(async (valid, fields) => {
if (valid) { //valid -> true 校验 通过
//校验通过
let result;
if (dept.value.id) {
result = await modifyInfoApi(dept.value);
} else {
result = await addApi(dept.value);
}
if (result.code) {
dialogFormVisible.value = false;
ElMessage.success('保存成功');
search();
} else {
ElMessage.error(result.msg);
}
} else {
//校验失败
ElMessage.error('校验失败,不能提交')
}
})
}
实际上save方法不传递form实例也可以,直接使用
但是当前还是存在问题的:
用户第一次验证失败后,点击关闭,再次打开弹窗表单中存在的还是上一次的校验错误提示,表单的状态没有被重置
ElementPlus给出了表单状态重置的方法:
const resetForm = (formEl: FormInstance | undefined) => {
if (!formEl) return
formEl.resetFields()
}
根据前文的经验,我们应该在打开表单的时候进行状态重置:
// 2. 弹窗
let add = () => {
//清空之前的dept.value.name
dept.value.name = '';
//显示对话框
dialogFormVisible.value = true;
//标题赋值
formTitle.value = '新增部门';
resetForm(deptForm.value);
}
const update = async (id: number) => {
// 1. 数据回显
//清空之前的dept.value.name
dept.value.name = '';
//显示对话框
dialogFormVisible.value = true;
//设置标题
formTitle.value = '修改部门';
resetForm(deptForm.value);
dept.value = (await getInfoByIdApi(id)).data
}
可以发现很多代码都是重复的,可以抽取为单独的方法:
//打开对话框的通用操作
const openForm = ()=> {
//清空之前的dept.value.name
dept.value.name = '';
//显示对话框
dialogFormVisible.value = true;
//重置表单状态
resetForm(deptForm.value);
}
const update = async (id: number) => {
//重置
openForm();
//设置标题
formTitle.value = '修改部门';
dept.value = (await getInfoByIdApi(id)).data;
}
let add = () => {
openForm();
//标题赋值
formTitle.value = '新增部门';
}
员工管理
分页查询
页面布局
页面布局流程:
- 确定页面布局时所使用的Element组件
- 确定涉及到的数据模型(接口、响应式数据)
搜索栏
如果表单封装的数据较多,建议封装在一个对象中
SearchEmpModel:专门用来封装搜索栏的表单数据
需要使用ElementPlus提供的日期组件el-date-picker,type=daterange得到的是两个时间:开始时间和结束时间,这两个时间对应了searchEmp中的一个属性date数组
<script setup lang="ts">
import {ref} from "vue";
import type {SearchEmpModel} from "@/api/model/model";
let searchEmp = ref<SearchEmpModel>({
name: '',
gender : '',
begin : '',
end : '',
date : []
});
</script>
<template>
<!-- 搜索栏 model指定封装在哪个对象中-->
<el-form :inline="true" :model="searchEmp" class="demo-form-inline">
<el-form-item label="姓名">
<el-input v-model="searchEmp.name" placeholder="请输入姓名"/>
</el-form-item>
<el-form-item label="性别">
<el-select v-model="searchEmp.gender" placeholder="请选择">
<el-option label="男" value="1" />
<el-option label="女" value="2" />
</el-select>
</el-form-item>
<el-form-item label="入职时间">
<el-date-picker
v-model="searchEmp.date"
type="daterange"
range-separator="到"
start-placeholder="开始时间"
end-placeholder="结束时间"
value-format="YYYY-MM-DD"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="">查询</el-button>
<el-button @click="">清空</el-button>
</el-form-item>
</el-form>
<!-- 功能按钮 -->
<el-button type="success" @click="">+ 新增员工</el-button>
<el-button type="danger" @click="">- 批量删除</el-button>
<br><br>
</template>
日期数据封装在date数组中,传递给服务器端的数据应该是begin和end,现在需要给begin、end进行赋值
此处的赋值需要使用[[Vue3#监听属性|监听属性]]
watch(() => searchEmp.value.date, (newValue, oldValue) => {
/*
if (newValue.length !== 2){
searchEmp.value.begin = '';
searchEmp.value.end = '';
}else {
searchEmp.value.begin = newValue[0];
searchEmp.value.end = newValue[1];
}*/
if (newValue.length != 2) {
newValue.push('', '');
}
searchEmp.value.begin = newValue[0];
searchEmp.value.end = newValue[1];
}, {deep: true})
表格及分页
表格
<!-- 列表展示 -->
<el-table :data="empList" border style="width: 100%" fit >
<el-table-column prop="name" label="姓名" align="center" width="130px" />
<el-table-column prop="gender" label="性别" align="center" width="100px"/>
<el-table-column prop="image" label="头像" align="center"/>
<el-table-column prop="deptName" label="所属部门" align="center" />
<el-table-column prop="job" label="职位" align="center" width="100px"/>
<el-table-column prop="entryDate" label="入职时间" align="center" width="130px" />
<el-table-column prop="updateTime" label="最后修改时间" align="center" />
<el-table-column label="操作" align="center">
<template #default="scope">
<el-button type="primary" size="small" @click="">编辑</el-button>
<el-button type="danger" size="small" @click="">删除</el-button>
</template>
</el-table-column>
</el-table>
<br>
<!-- 分页组件Pagination -->
<el-pagination
v-model:current-page="pagination.currentPage"
v-model:page-size="pagination.pageSize"
:page-sizes="[5, 10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
:total="pagination.total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
表格第一列需要一个多选框,实现多选非常简单: 手动添加一个 el-table-column
,设 type
属性为 selection
即可。
但是多选框的选中项是要向服务器提交的数据,在选中项变化的时候应该更新数据:
<!-- 列表展示 -->
<el-table
:data="empList"
border
style="width: 100%"
fit
@selection-change="handleSelectionChange"
>
<!--多选框-->
<el-table-column type="selection" width="55" />
<el-table-column prop="name" label="姓名" align="center" width="130px" />
<el-table-column prop="gender" label="性别" align="center" width="100px"/>
<el-table-column prop="image" label="头像" align="center"/>
<el-table-column prop="deptName" label="所属部门" align="center" />
<el-table-column prop="job" label="职位" align="center" width="100px"/>
<el-table-column prop="entryDate" label="入职时间" align="center" width="130px" />
<el-table-column prop="updateTime" label="最后修改时间" align="center" />
<el-table-column label="操作" align="center">
<template #default="scope">
<el-button type="primary" size="small" @click="">编辑</el-button>
<el-button type="danger" size="small" @click="">删除</el-button>
</template>
</el-table-column>
</el-table>
@selection-change指定多选框选中项变化时的回调函数
分页
<!-- 分页组件Pagination -->
<el-pagination
v-model:current-page="pagination.currentPage"
v-model:page-size="pagination.pageSize"
:page-sizes="[5, 10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
:total="pagination.total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
分页组件的数据模型需要三个属性:
//分页参数接口
export interface PaginationParam {
currentPage: number,
pageSize: number,
total: number
}
currentPage和pageSize需要指定默认值,而total是在后端传递过来的:
<!-- 分页组件Pagination -->
<el-pagination
v-model:current-page="pagination.currentPage"
v-model:page-size="pagination.pageSize"
:page-sizes="[5, 10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
:total="pagination.total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
//分页条组件的数据模型
let pagination = ref<PaginationParam>({
//指定默认值
currentPage : 1,
pageSize : 5,
total : 0
});
分页组件的current-page和page-size是v-model双向数据绑定,在用户点击的时候自定变为用户点击的值,并且触发@size-change和@current-change事件
页面交互
分页查询功能
需要的数据模型:
根据接口文档可以定义请求参数的数据模型:
而我们在上文中定义了两个数据模型:
//分页数据模型
export interface PaginationParam {
currentPage: number,
pageSize: number,
total: number
}
//搜索栏数据模型
export interface SearchEmpModel {
name: string, //姓名
gender: string, //性别
begin: string, //开始时间
end: string, //结束时间
date: string[] //时间范围
}
//继承这两个数据模型
export interface EmpPageQueryParam extends SearchEmpModel,PaginationParam{
}
根据接口文档可以定义响应数据的数据模型:
//响应的数据:
{
"code": 1,
"msg": "success",
"data": {
"total": 1,
"rows": [
{
"id": 1,
"username": "jinyong",
"password": "123456",
"name": "金庸",
"gender": 1,
"image": "https://web-framework.oss-cn-hangzhou.aliyuncs.com/2022-09-02-00-27-53B.jpg",
"job": 2,
"salary": 8000,
"entryDate": "2015-01-01",
"deptId": 2,
"deptName": "教研部",
"createTime": "2022-09-01T23:06:30",
"updateTime": "2022-09-02T00:29:04"
}
]
}
数据模型:
//分页结果接口
export interface PageModel {
total: number,
rows: any[]
}
//统一响应结果接口
export interface PageResultModel {
code: number,
msg: string,
data: PageModel
}
或者可以定义为:
export interface ResultModel<T> {
code: number,
msg: string,
data: T
}
export interface PageModel {
total: number,
rows: any[]
}
提高了复用性
API接口层:
export const pageQueryApi =
(param:EmpPageQueryParam) => request.get<any,PageResultModel>
(`/emps?name=${param.name}&gender=${param.gender}&begin=${param.begin}
&end=${param.end}&page=${param.currentPage}&pageSize=${param.pageSize}`)
或者是:
export const myPageQueryApi =
(param:EmpPageQueryParam) => request.get<any,ResultModel<PageModel>>
(`/emps?name=${param.name}&gender=${param.gender}&begin=${param.begin}&end=${param.end}
&page=${param.currentPage}&pageSize=${param.pageSize}`)
- 查询:
let search = async () => {
let pageBean = await pageQueryApi({...searchEmp.value, ...pagination.value});
//更新列表
empList.value = pageBean.data.rows;
//更新记录条数
pagination.value.total = pageBean.data.total;
}
页码、条数变化的时候也需要调用search
清空功能
let clear = async ()=> {
//清空搜索栏
searchEmp.value = {
name: '',
gender: '',
begin: '',
end: '',
date: []
};
//再次查询
search();
}
在清空之后,以下属性都变为了空字符串:
name: '',
gender: '',
begin: '',
end: '',
而我们在后端mybatis的动态SQL中对空字符串进行了判断。
- 页面加载完成自动查询
新增员工
页面布局流程:
- 确定要使用的Element组件
- 确定涉及到的数据模型
页面布局
点击按钮 弹出对话框,新增/编辑员工,需要的数据有两部分:员工信息和工作经历信息
涉及的数据模型:
//员工工作经历数据接口
export interface EmpExprModel {
id?: number,
empId?: number,
exprDate: string[], //时间范围
begin: string,
end: string,
company: string,
job: string
}
//员工数据接口
export interface EmpModel {
id?: number,
username: string,
password: string,
name: string,
gender: string,
phone: string,
job: string,
salary: string,
image: string,
entryDate: string,
deptId: string,
deptName?: string,
exprList: EmpExprModel[]
}
注意:数据模型中属性名的定义要参照接口文档
定义响应式对象:
let formTitle = ref<string>('');
let dialogFormVisible = ref<boolean>(true);
let labelWidth = ref<number>(80);
let emp = ref<EmpModel>({
username : '',
password : '',
name : '',
gender : '',
phone: '',
job: '',
salary: '',
image: '',
entryDate: '',
deptId: '',
deptName: '',
exprList : []
});
用户名/姓名布局
<el-dialog v-model="dialogFormVisible" :title="formTitle">
<el-form :model="emp"> <el-form-item label="用户名" :label-width="formLabelWidth">
<el-input v-model="emp.username" autocomplete="off" /> </el-form-item>
<el-form-item label="姓名" :label-width="formLabelWidth">
<el-input v-model="emp.name" autocomplete="off" /> </el-form-item>
<el-form-item label="性别" :label-width="formLabelWidth">
<el-select v-model="emp.gender" placeholder="请选择">
<el-option label="男" value="1" />
<el-option label="女" value="2" />
</el-select>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogFormVisible = false">取消</el-button>
<el-button type="primary" @click="dialogFormVisible = false">
保存
</el-button>
</span>
</template>
</el-dialog>
当前显示的效果:
页面原型要求的显示效果:
要在一行中显示两个表单组件,就需要ElementPlus提供的Layout布局组件:通过基础的 24 分栏,迅速简便地创建布局。
Layout布局将一行(一个el-row)等分为24份,如果想设置两个组件大小相等,只需要分别设置两个组件(el-col)的属性 :span = 12
<el-dialog v-model="dialogFormVisible" :title="formTitle">
<el-form :model="emp">
<el-row>
<el-col :span="12">
<el-form-item label="用户名" :label-width="formLabelWidth">
<el-input v-model="emp.username" autocomplete="off" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="姓名" :label-width="formLabelWidth">
<el-input v-model="emp.name" autocomplete="off" />
</el-form-item>
</el-col>
</el-row>
<el-form-item label="性别" :label-width="formLabelWidth">
<el-select v-model="emp.gender" placeholder="请选择">
<el-option label="男" value="1" />
<el-option label="女" value="2" />
</el-select>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogFormVisible = false">取消</el-button>
<el-button type="primary" @click="dialogFormVisible = false">
保存
</el-button>
</span>
</template>
</el-dialog>
性别/职位布局:列表优化
之前的布局方式:
<el-form-item label="性别">
<el-select v-model="searchEmp.gender" placeholder="请选择">
<el-option label="男" value="1"/>
<el-option label="女" value="2"/>
</el-select>
</el-form-item>
这样做是没有问题的,但是如果后续要求 “男” 变为 “男士”,就要在HTML结构中一个一个修改,这样做太麻烦了。
建议做法:下拉列表的多个选项在数据模型中统一维护,好处是如果要添加选项/修改选项,就不需要在HTML中进行更改了
定义gender和job的响应式数据:
const genders = ref([{name : '男', value : '1'},{name : '女', value : '2'}])
const jobs = ref([
{ name: '班主任', value: 1 },
{ name: '讲师', value: 2 },
{ name: '学工主管', value: 3 },
{ name: '教研主管', value: 4 },
{ name: '咨询师', value: 5 },
{ name: '其他', value: 6 }
])
在下拉列表中展示时:
<!--性别:第二行的第一列-->
<el-col :span="12">
<el-form-item label="性别" :label-width="labelWidth">
<el-select v-model="emp.gender" placeholder="请选择" style="width: 100%;"> <!--label属性:选项显示的内容需要动态绑定-->
<el-option v-for="gender in genders" :key="gender.value" :value="gender.value" :label="gender.name"/>
</el-select>
</el-form-item>
</el-col>
<!--职位:第四行的第二列-->
<el-col :span="12">
<el-form-item label="职位" :label-width="labelWidth">
<el-select v-model="emp.job" placeholder="请选择" style="width: 100%;">
<el-option v-for="job in jobs" :key="job.value" :label="job.name" :value="job.value" />
</el-select>
</el-form-item>
</el-col>
部门布局
与上文中jobs、genders不同的是,部门数据应该是在后端查询后返回的,在api/dept.ts定义了查询所有部门的方法:
//dept.ts
export const queryAllApi = () => request.get<any,ResultModel>('/depts');
我们需要引入这个方法,但是引入这个方法名:queryAllApi 可能与本文件中其他的方法名冲突,可以指定别名:
import {queryAllApi as queryAllDeptsApi} from '@/api/dept'
let depts = ref<DeptModelArray>([]);
const queryAllDepts = async ()=> {
let result = await queryAllDeptsApi();
depts.value = result.data;
}
- queryAllDepts方法应该何时调用?
点击编辑和新增都会使用到这个对话框,也就是都需要使用部门数据,应该放在EmpIndexView的onMounted方法中调用:
onMounted(() => {
search();
queryAllDepts();
})
此时所有的信息都被封装在depts中了,在下拉列表中渲染选项:
<el-col :span="12">
<el-form-item label="所属部门" :label-width="labelWidth">
<el-select v-model="emp.deptId" placeholder="请选择" style="width: 100%;">
<el-option v-for="dept in depts" :key="dept.id" :label="dept.name" :value="dept.id" /> <!--value指定为id-->
</el-select>
</el-form-item>
</el-col>
value属性是最终提交的值,需要指定为id
头像布局
<template>
<el-upload
class="avatar-uploader"
action="https://run.mocky.io/v3/9d059bf9-4660-45f2-925d-ce80ad6c4d15"
:show-file-list="false"
:on-success="handleAvatarSuccess"
:before-upload="beforeAvatarUpload"
>
<!--
action:上传地址
on-success:上传成功hook
before-upload:上传之前的hook
-->
<img v-if="imageUrl" :src="imageUrl" class="avatar" />
<el-icon v-else class="avatar-uploader-icon"><Plus /></el-icon>
</el-upload>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import { ElMessage } from 'element-plus'
import { Plus } from '@element-plus/icons-vue'
import type { UploadProps } from 'element-plus'
const imageUrl = ref('')
//成功上传的回调函数
const handleAvatarSuccess: UploadProps['onSuccess'] = (
response,
uploadFile
) => {
imageUrl.value = URL.createObjectURL(uploadFile.raw!)
}
//上传之前的回调函数,返回false不进行上传,返回true进行上传
const beforeAvatarUpload: UploadProps['beforeUpload'] = (rawFile) => {
if (rawFile.type !== 'image/jpeg') {
ElMessage.error('Avatar picture must be JPG format!')
return false
} else if (rawFile.size / 1024 / 1024 > 2) {
ElMessage.error('Avatar picture size can not exceed 2MB!')
return false
}
return true
}
</script>
- before-upload:上传之前的回调函数,一般在该函数中进行文件校验
- on-success:在该函数中写回URL路径
上传的效果:点击Icon上传,上传成功后显示上传的图片,核心的逻辑就是以下代码控制的:
<img v-if="imageUrl" :src="imageUrl" class="avatar" />
<el-icon v-else class="avatar-uploader-icon"><Plus /></el-icon>
未上传时URL是空值,v-if不渲染img,渲染上传的Icon Plus,上传成功后,handleAvatarSuccess回调函数会将URL写入imageUrl,v-if渲染img,不渲染Icon
上传的核心属性:action,对于本系统的后端接口/upload来说:
<el-upload
class="avatar-uploader"
action="/upload"
:show-file-list="false"
:on-success="handleAvatarSuccess"
:before-upload="beforeAvatarUpload"
>
这样是无法访问到我们的接口的,因为请求路径是:http://127.0.0.1:5173/upload
这个请求不是经过axios发送的,是el-upload组件发送的,不会加上/api路径,如果想让服务器进行跨域代理,需要设置action为:/api/upload
<!-- 第五行 -->
<el-row>
<el-col :span="12">
<el-form-item label="头像" :label-width="labelWidth">
<el-upload
class="avatar-uploader"
action="/api/upload"
:show-file-list="false"
:on-success="handleAvatarSuccess"
:before-upload="beforeAvatarUpload"
>
<img v-if="emp.image" :src="emp.image" class="avatar" /> <!--有url就显示图片-->
<el-icon v-else class="avatar-uploader-icon"><Plus /></el-icon> <!--没有url就显示图标-->
</el-upload>
</el-form-item>
</el-col>
</el-row>
工作经历布局
点击添加工作经历,工作经历表单多一条记录;点击删除按钮,删除对应的记录
这个功能看起来比较复杂,需要谨记Vue的原则:Vue是基于数据驱动视图展示的
数据改变引起了视图的改变,对于工作经历来说,这个数组是具有响应式的:
- 添加时,向数组里添加元素
- 删除时,删除数组里的元素
一旦数据发生变化,视图中展示的数据就会发生变化
布局:
<!-- 第六行 -->
<el-row>
<el-col :span="24">
<el-form-item label="工作经历" :label-width="labelWidth">
<el-button type="success" size="small" @click="addEmpExpr">+ 添加工作经历</el-button>
</el-form-item>
</el-col>
</el-row>
<!-- 遍历emp.exprList数组,渲染每一条工作经历 -->
<el-row v-for="(expr,index) in emp.exprList" :gutter="5">
<el-col :span="10">
<el-form-item label="时间" size="small" :label-width="labelWidth">
<el-date-picker
v-model="expr.exprDate"
type="daterange"
range-separator="至"
start-placeholder="开始时间"
end-placeholder="结束时间"
value-format="YYYY-MM-DD"
/>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="公司" size="small">
<el-input placeholder="公司名称" v-model="expr.company" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="职位" size="small">
<el-input placeholder="职位名称" v-model="expr.job" />
</el-form-item>
</el-col>
<el-col :span="2">
<el-form-item size="small">
<el-button type="danger" @click="del(index/expr)">- 删除</el-button>
</el-form-item>
</el-col>
</el-row>
函数:
//添加工作经历的函数
const addEmpExpr = ()=> {
emp.value.exprList.push({exprDate : [],begin : '',end : '',company : '', job : ''})
}
//删除
//根据索引删除
const del = (index:number)=> {
emp.value.exprList.splice(index,0,1);
}
/*
严格模式下不能使用
const del = (expr:EmpExprModel)=> {
with (emp.value.exprList) {
splice(indexOf(expr),1);
}
}
*/
//根据对象删除
const del = (expr:EmpExprModel)=> {
let index = emp.value.exprList.indexOf(expr);
emp.value.exprList.splice(index,0,1);
}
接口文档要求的请求参数:
{
"image": "https://web-framework.oss-cn-hangzhou.aliyuncs.com/2022-09-03-07-37-38222.jpg",
"username": "linpingzhi",
"name": "林平之",
"gender": 1,
"job": 1,
"entrydate": "2022-09-18",
"deptId": 1,
"phone": "18809091234",
"salary": 8000,
"exprList": [
{
"company": "百度科技股份有限公司",
"job": "java开发",
"begin": "2012-07-01",
"end": "2019-03-03"
},
{
"company": "阿里巴巴科技股份有限公司",
"job": "架构师",
"begin": "2019-03-15",
"end": "2023-03-01"
}
]
}
我们当前的EmpExpr数据模型:
export interface EmpExprModel {
id?: number,
empId?: number,
exprDate: string[], //时间范围
begin: string,
end: string,
company: string,
job: string
}
就需要对emp.value.exprList进行操作,将每一条数据的exprDate转变为end和begin
watch(emp,(newVal,oldVal) => {
if (emp.value.exprList.length > 0){
emp.value.exprList.forEach(expr => {
expr.end = expr.exprDate[0];
expr.begin = expr.exprDate[1];
})
}
},{deep : true})
页面交互
完成新增员工的功能
为新增员工按钮绑定事件:
const addEmp = ()=> {
//清空上一次的表单数据
emp.value = {
username : '',
password : '',
name : '',
gender : '',
phone: '',
job: '',
salary: '',
image: '',
entryDate: '',
deptId: '',
deptName: '',
exprList : []
}
dialogFormVisible.value = true;
}
打开对话框,为保存按钮添加事件
接口层:
export const createEmpApi = (emp:EmpModel) => request.post<any,ResultModel>('/emps',emp);
调用:
const save = async ()=> {
//一定注意传递的入参是emp.value
let result = await createEmpApi(emp.value);
if (result.code){
ElMessage.success('保存成功');
dialogFormVisible.value = false;
//重新查询
search();
}else {
ElMessage.error(result.msg);
}
}
表单校验
在提交之前还需要进行表单校验
对新增员工进行表单校验需要参照界面原型的要求:
总结出如下的校验规则:
表单校验的流程:
- 定义表单实例 empFormRef,赋值给ref属性,用来在save方法中校验表单和在openDialog方法中重置表单状态
- 定义校验规则 FormRules,其中的泛型指定表单对应的数据模型,在需要校验的表单项上通过prop指定规则名称
<el-form :model="emp" ref="empFormRef" :rules="rules">
<el-form-item prop='校验规则名称'>
表单验证时机:
- 保存(新增/编辑)时,校验通过提交数据,不通过提示信息
- 打开对话框(新增/修改)时,重置表单校验规则
验证时机:
const save = async ()=> {
//注意async的位置
await empFormRef.value?.validate(async (valid,fields) => {
if (valid){
//一定注意传递的入参是emp.value
let result = await createEmpApi(emp.value);
if (result.code){
ElMessage.success('保存成功');
dialogFormVisible.value = false;
search();
}else {
ElMessage.error(result.msg);
}
}else {
ElMessage.error('表单校验失败,不能提交');
}
})
}
重置表单校验规则:
const openForm = ()=> {
emp.value = {
username : '',
password : '',
name : '',
gender : '',
phone: '',
job: '',
salary: '',
image: '',
entryDate: '',
deptId: '',
deptName: '',
exprList : []
};
//重置表单
empFormRef.value?.resetFields();
dialogFormVisible.value = true;
}
//新增员工按钮
const addEmp = ()=> {
formTitle.value = '新增员工';
openForm();
}
//编辑员工按钮
const update = async (id:number) => {
formTitle.value = '编辑员工';
openForm();
let result = await queryByIdApi(id);
if (result.code){
emp.value = result.data;
//后端会返回exprList,不需要判断空
emp.value.exprList.forEach(expr => {
expr.exprDate = [expr.begin,expr.end];
})
}else {
ElMessage.error('查询失败')
}
}
修改员工
- 点击编辑按钮,数据回显:根据ID查询员工信息
- 点击保存,执行修改操作
数据回显
接口层:
export const queryByIdApi = (id:number) => request.get<any,ResultModel>(`/emps/${id}`)
更新方法:
const update = async (id:number) => {
formTitle.value = '编辑员工';
openForm();
let result = await queryByIdApi(id);
if (result.code){
emp.value = result.data;
//后端会返回exprList,不需要判断空
emp.value.exprList.forEach(expr => {
expr.exprDate = [expr.begin,expr.end];
})
}else {
ElMessage.error('查询失败')
}
}
但是这样做是有问题的,数据回显不能显示。
之前的watch监听器将工作经历的exprDate转化为begin和end的代码:
watch(emp,(newVal,oldVal) => {
if (emp.value.exprList.length > 0){
emp.value.exprList.map(expr => {
expr.end = expr.exprDate[0];
expr.begin = expr.exprDate[1];
})
}
},{deep : true})
只要emp发生变化,就对emp.value.exprList进行遍历,遍历时将exprDate数组分别赋值给begin、end
emp变化的三种清空:
- 新增员工时发生变化,exprList可能是空数组,不会进行map,但最好判断exprList的长度 > 0
- 清空emp时发生变化,exprList是空数组,不进行map
- 数据回显时发生变化,exprList不是空数组,进行map,访问exprDate数组的元素
但是在数据回显的时候,后端接口没有返回exprDate属性,此时就是访问了undefined的元素,就会报错。
所以需要对watch再加一次判断,在exprDate不为空的时候进行赋值:
watch(() => emp.value.exprList,(newVal,oldVal) => {
if (emp.value.exprList.length > 0){
emp.value.exprList.map(expr => {
if (!expr.exprDate){
return;
}
expr.end = expr.exprDate[0];
expr.begin = expr.exprDate[1];
})
}
},{deep : true})
这样就避免了在数据回显时导致emp发生变化触发此监听器,从而导致访问undefined。
保存修改
和新增员工使用同一个对话框,form表单的保存按钮绑定的是一个方法:
<!--保存/取消-->
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogFormVisible = false">取消</el-button>
<el-button type="primary" @click="save">保存</el-button>
</span>
</template>
新增员工时的保存方法:
const save = async ()=> {
//注意async的位置
await empFormRef.value?.validate(async (valid,fields) => {
if (valid){
//一定注意传递的入参是emp.value
let result = await createEmpApi(emp.value);
if (result.code){
ElMessage.success('保存成功');
dialogFormVisible.value = false;
search();
}else {
ElMessage.error(result.msg);
}
}else {
ElMessage.error('表单校验失败,不能提交');
}
})
}
新增和修改的区别就是新增是没有id的,修改有id,所以可以根据有无id的区别来调用新增和修改的不同接口层方法:
const save = async ()=> {
//注意async的位置
await empFormRef.value?.validate(async (valid,fields) => {
if (valid){
let result;
if (!emp.value.id){
//无id新增
result = await createEmpApi(emp.value);
}else {
//有id修改
result = await modifyEmpApi(emp.value);
}
if (result.code){
ElMessage.success('保存成功');
dialogFormVisible.value = false;
search();
}else {
ElMessage.error(result.msg);
}
}else {
ElMessage.error('表单校验失败,不能提交');
}
})
}
删除员工
删除员工信息有两个操作入口:
- 点击每条记录之后的 删除 按钮,删除当前条记录。
- 点击多选框选中要删除的员工,点击批量删除,批量删除员工信息
批量删除或删除最终只需要调用服务端的同一个批量删除接口即可。
接口文档:
/emps?ids=1,2,3
删除的数据以get默认形式传递,接口层:
export const deleteApi = (ids:number[]) => request.delete<any,ResultModel>(`/emp/${ids}`)
以number[] 作为路径参数会自动将数组元素转化为 /emp/1,2,3
单个删除:点击删除按钮,删除单个数据
const deleteById = (id:number) => {
ElMessageBox.confirm(
'是否确认删除?',
'Warning',
{
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'warning',
}
) //注意async的位置
.then(async () => {
//确认删除的回调函数
//接口层入参是数组形式
let result = await deleteApi([id]);
if (result.code){
//删除成功
ElMessage.success('删除成功')
search();
}else{
//删除失败:展示服务器响应的信息
ElMessage.error(result.msg)
}
})
.catch(() => {
ElMessage({
type: 'info',
message: '取消删除',
})
})
}
- 批量删除
多选框的实现参照ElementPlus官网:实现多选非常简单,手动添加一个
el-table-column
,设type
属性为selection
即可。
多选框选项发生变化时会发生change事件,在ElementPlus中通过属性@selection-change指定回调函数:
<el-table
:data="empList"
border
style="width: 100%"
fit
@selection-change="handleSelectionChange"
>
<!--多选框-->
<el-table-column type="selection" width="55" />
<el-table-column prop="name" label="姓名" align="center" width="130px" />
<el-table-column prop="gender" label="性别" align="center" width="100px"/>
<el-table-column prop="image" label="头像" align="center"/>
<el-table-column prop="deptName" label="所属部门" align="center" />
<el-table-column prop="job" label="职位" align="center" width="100px"/>
<el-table-column prop="entryDate" label="入职时间" align="center" width="130px" />
<el-table-column prop="updateTime" label="最后修改时间" align="center" />
<el-table-column label="操作" align="center">
<template #default="scope">
<el-button type="primary" size="small" @click="update(scope.row.id)">编辑</el-button>
<el-button type="danger" size="small" @click="deleteById(scope.row.id)">删除</el-button>
</template>
</el-table-column>
</el-table>
回调函数应该将所有选中项的id存储在数组中
let ids = ref<(number|undefined)[]>([]);
//多选框选择项变化
const handleSelectionChange = (selectedEmps:EmpModelArray)=> {
//每次选中元素都会触发该方法
ids.value = selectedEmps.map(e => e.id);
}
批量删除的方法和单个删除的方法只有一个地方不同:
//单个删除
let result = await deleteApi([id]);
//批量删除
let result = await deleteApi(ids.value);
可以抽取为deleteEmpBatch方法:
const deleteEmpBatch = (id?:number) => {
ElMessageBox.confirm(
'是否确认删除?',
'Warning',
{
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'warning',
}
)
.then(async () => {
let result;
if (id){ //传递了入参id就单个删除
result = await deleteApi([id]);
}else { //否则就多个删除
//接口层为字符串入参:result = await deleteApi(ids.value.join(','))
result = await deleteApi(ids.value)
}
if (result.code){
//删除成功
ElMessage.success('删除成功')
search();
}else{
//删除失败:展示服务器响应的信息
ElMessage.error(result.msg)
}
})
.catch(() => {
ElMessage({
type: 'info',
message: '取消删除',
})
})
}
但是这样做是有问题的,在此处只判断id是否存在的话,如果id不存在会将事件对象event传递进来,所以还需要判断id是否为number类型的
可以通过三目运算符简化:
const deleteEmpBatch = (id?:number) => {
ElMessageBox.confirm(
'是否确认删除?',
'Warning',
{
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'warning',
}
)
.then(async () => {
let result;
result = await deleteApi(id && type of id === 'number' ? [id] : ids.value)
if (result.code){
//删除成功
ElMessage.success('删除成功')
search();
}else{
//删除失败:展示服务器响应的信息
ElMessage.error(result.msg)
}
})
.catch(() => {
ElMessage({
type: 'info',
message: '取消删除',
})
})
}
如果接口层的入参是string类型,需要传递: ids.value.join(‘,’)
绑定事件:
<el-button type="danger" @click="deleteEmpBatch">- 批量删除</el-button>
<el-button type="danger" size="small" @click="deleteEmpBatch(scope.row.id)">删除</el-button>
登录
页面布局
<script setup lang="ts">
import { ref } from 'vue'
import type { LoginEmp } from '@/api/model/model'
let loginForm = ref<LoginEmp>({username:'', password:''})
</script>
<template>
<div id="container">
<div class="login-form">
<el-form label-width="80px">
<p class="title">Tlias智能学习辅助系统</p>
<el-form-item label="用户名" prop="username">
<el-input v-model="loginForm.username" placeholder="请输入用户名"></el-input>
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input type="password" v-model="loginForm.password" placeholder="请输入密码"></el-input>
</el-form-item>
<el-form-item>
<el-button class="button" type="primary" @click="">登 录</el-button>
<el-button class="button" type="info" @click="">重 置</el-button>
</el-form-item>
</el-form>
</div>
</div>
</template>
<style scoped>
#container {
padding: 10%;
height: 410px;
background-image: url('../../assets/bg1.jpg');
background-repeat: no-repeat;
background-size: cover;
}
.login-form {
max-width: 400px;
padding: 30px;
margin: 0 auto;
border: 1px solid #e0e0e0;
border-radius: 10px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
background-color: white;
}
.title {
font-size: 30px;
font-family: '楷体';
text-align: center;
margin-bottom: 30px;
font-weight: bold;
}
.button {
margin-top: 30px;
width: 120px;
}
</style>
页面交互
用户登录成功后跳转到主页面,并且以后每次请求都要携带token
- 完成基本的员工登录操作
import { ref } from 'vue'
import type { LoginEmp } from '@/api/model/model'
import {loginApi} from "@/api/login";
import {ElMessage} from "element-plus";
import {useRouter} from "vue-router";
let loginForm = ref<LoginEmp>({username:'', password:''})
//获取当前应用的路由实例
let router = useRouter();
const login = async () => {
let result = await loginApi(loginForm.value);
if (result.code){
ElMessage.success('登录成功');
//1. 存储token
//2. 跳转页面
router.push('/index');
}else {
ElMessage.error('登录失败')
}
}
- 将登陆成功后获取到的登录信息存储起来,方便在其他组件中使用
如果在项目的多个组件中共享数据,可以使用Vue3提供的[[Vue#状态管理:pinia|状态管理库Pinia]]
在pinia中保存用户的登录信息:
//loginEmp.ts
export const useLoginEmpStore = defineStore('loginEmp', () => {
//登录信息
const loginEmp = ref<LoginInfo>({});
//设置登录信息
const setLoginEmp = (loginEmpInfo:LoginInfo) => {
loginEmp.value = loginEmpInfo;
}
//获取登录信息
const getLoginEmp = () => {
return loginEmp.value;
}
//删除登录信息
const delLoginEmp = () => {
loginEmp.value = {}
}
return { loginEmp, setLoginEmp, getLoginEmp,delLoginEmp }
})
建议使用use + 名字 + Store的形式
const login = async () => {
let result = await loginApi(loginForm.value);
if (result.code){
ElMessage.success('登录成功');
//1. 存储token
let loginEmpStore = useLoginEmpStore();
loginEmpStore.setLoginEmp(result.data);
//2. 跳转页面
router.push('/index');
}else {
ElMessage.error('登录失败')
}
}
token已经被存储在pinia中了,只需要在后续的请求中携带pinia中的token就可以。
现在的问题是如何在请求头中携带token,我们将所有的交互逻辑抽取到api层了:
export const queryAllApi = () => request.get<any,ResultModel>('/depts');
//接口文档指明参数为dept类型
export const addApi = (dept:DeptModel) => request.post<any,ResultModel>('/depts',dept);
export const getInfoByIdApi = (id:number) => request.get<any,ResultModel>(`/depts/${id}`);
export const modifyInfoApi = (dept:DeptModel) => request.put<any,ResultModel>('/depts',dept);
export const removeByIdApi = (id:number) => request.delete<any,ResultModel>(`/depts?id=${id}`)
在请求时调用的是我们封装的request:
import axios from 'axios'
//创建axios实例对象
const request = axios.create({
baseURL: '/api',
timeout: 600000
})
//axios的响应 response 拦截器
request.interceptors.response.use(
(response) => { //成功回调
return response.data
},
(error) => { //失败回调
return Promise.reject(error)
}
)
export default request
在之前设置了响应拦截器,将AxiosResponse替换为服务器端响应的数据,也可以定义一个请求拦截器,为所有请求添加请求头token:
import axios from 'axios'
import {useLoginEmpStore} from "@/stores/loginEmp";
//创建axios实例对象
const request = axios.create({
baseURL: '/api',
timeout: 600000
})
request.interceptors.request.use((config) => {
let loginEmpStore = useLoginEmpStore();
let loginEmp = loginEmpStore.getLoginEmp();
//如果登录信息存在并且有token
if (loginEmp && loginEmp.token){
config.headers['token'] = loginEmp.token;
}
return config;
}, (error) => {
return Promise.reject(error);
}
)
//axios的响应 response 拦截器
request.interceptors.response.use(
(response) => { //成功回调
return response.data
},
(error) => { //失败回调
return Promise.reject(error)
}
)
export default request
这样所有的请求都会携带token(如果用户的登录信息存在的话)
- 如果用户没有登录,直接访问组件的路径,比如/index,服务器会响应401状态码,此时应该让页面跳转到登录界面
第一种拦截方式:响应拦截器进行拦截
在响应拦截器中进行统一的拦截,如果是401状态码就跳转到登录界面:
//axios的响应 response 拦截器
request.interceptors.response.use(
(response) => { //成功回调
return response.data
},
(error) => {
//非2xx状态码会进入次回调
//error是AxiosError对象,封装了response和request
if (error.response.status == 401){
ElMessage.error('登录失效,请重新登录');
router.push('/login');
}else {
ElMessage.error('接口访问异常'); //访问失败给用户提示
}
return Promise.reject(error)
}
)
注意:此处不能使用useRouter()函数获取router对象,需要导入router对象:
//index.ts
import { createRouter, createWebHistory } from 'vue-router'
import {useLoginEmpStore} from "@/stores/loginEmp";
import {ElMessage} from "element-plus";
const router = createRouter({
...
})
export default router
在router/index.ts中导出了router对象,其他地方使用也可以导入这个对象:
//request.ts
import axios from 'axios'
import {useLoginEmpStore} from "@/stores/loginEmp";
import {ElMessage} from "element-plus";
import router from "@/router"; //导入了 @/router/index.ts,index.ts可以省略
//创建axios实例对象
const request = axios.create({
baseURL: '/api',
timeout: 600000
})
request.interceptors.request.use((config) => {
...
);
//axios的响应 response 拦截器
request.interceptors.response.use(
...
)
export default request
第二种拦截方式:全局前置路由守卫
//router/index.ts
router.beforeEach((to, from, next) => {
//不是跳转到登录页面的路由都需要判断是否登录
if (!to.path.match('/login')){
let loginEmpStore = useLoginEmpStore();
let loginEmp = loginEmpStore.getLoginEmp();
if (loginEmp && loginEmp.token){
//登录后继续路由跳转
next();
}else{
ElMessage.error('请先登录');
//未登录跳转到登录界面
router.push('/login');
}
}else {
//去往登录页面的路由直接跳转
next();
}
})
相比之下,第二种路由跳转方式不会向服务器端发起请求,但是实际开发中两种方式往往结合使用
退出登录
点击退出登录按钮,清空员工的登录信息,跳转到登录页面
<script setup lang="ts">
import {useLoginEmpStore} from "@/stores/loginEmp";
import router from "@/router";
import {ElMessage} from "element-plus";
import {ref} from "vue";
let loginEmpStore = useLoginEmpStore();
let name = ref<string>(loginEmpStore.getLoginEmp().name);
const logout = () => {
//1. 清空登录信息
loginEmpStore.delLoginEmp();
//2. 跳转到登录界面
ElMessage.success(`退出登录成功,${name.value}`);
router.push('/login');
}
</script>
<template>
<span class="title">Tlias智能学习辅助系统</span>
<span class="right_tool">
<a href="">
<el-icon><EditPen /></el-icon> 修改密码 |
</a>
<!--让超链接失效-->
<a href="javascript:void(0)" @click="logout">
<el-icon><SwitchButton /></el-icon> 退出登录 【{{name}}】
</a>
</span>
</template>