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>