07-ElementPlus元件庫

EUNEIR發表於2024-03-13

ElementPlus

簡介

ElementPlus是餓了麼團隊研發的,基於Vue3的元件庫

準備工作:

  1. 建立工程化的Vue專案 選擇 TypeScript

  2. 參照官方文件安裝ElementPlus元件庫(當前工程的目錄下)

npm install element-plus --save
  1. 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')
  1. 複製元件程式碼,調整

常用元件

Button元件

image.png

對應的程式碼:

<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 控制背景色變淡,新增邊框

表格元件

表格用於展示多條結構類似的資料,可以對資料進行排序、篩選、對比或自定義操作

image.png

<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'

image.png

<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>

顯示的效果不甚明顯:

image.png

可以根據上文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)  
};

案例

image.png

請求地址:

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>

image.png

如果要顯示圖片,就必須使用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

image.png

準備工作

image.png

  • 安裝依賴
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';

頁面佈局

image.png

公共的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>

image.png

<!--HeaderComponent.vue-->
<script setup lang="ts">  
  
</script>  
  
<template>  
    <span class="title">Tlias智慧學習輔助系統</span>  
      
    <span class="right_tool">  
          <a href="">  
            <!--圖示-->
            <el-icon><EditPen /></el-icon> 修改密碼 &nbsp;&nbsp;&nbsp; |  &nbsp;&nbsp;&nbsp;  
          </a>  
          <a href="">  
            <el-icon><SwitchButton /></el-icon> 退出登入  
          </a>  
        </span>  
</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

image.png

   <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>

當前專案的需求:

image.png

四個子選單,沒有分組

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>

但是當前直接訪問系統的介面:

image.png

因為預設的請求路徑是: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

部門管理功能實現

查詢所有

頁面佈局

image.png

需要的元件: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"; 

接下來繼續完善表格的資料顯示,介面原型顯示需要四列:

image.png

<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>

載入資料

需求:

  1. 增刪改完畢後,載入最新的部門資料
  2. 開啟頁面後,載入最新的部門資料

定義查詢部門列表的函式:

//查詢部門列表  
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功能:

image.png

複製連結作為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下會提示錯誤:

image.png

因為沒有指定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物件的型別:

image.png

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對話方塊

image.png

<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>

此時的效果:

image.png

對話方塊的標題不應該直接指定為 新增部門 ,編輯按鈕彈出的對話方塊和這個相同,編輯時標題應該為 修改部門

在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的,可以在前端給使用者提供資訊參考。

修改部門

分為兩步:

  1. 查詢回顯
  2. 儲存修改

查詢回顯

點選編輯按鈕,需要查詢回顯,為編輯按鈕繫結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();  
}

表單校驗

image.png

需要對錶單進行校驗,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 = '新增部門';  
}

員工管理

image.png

分頁查詢

頁面佈局

頁面佈局流程

  • 確定頁面佈局時所使用的Element元件
  • 確定涉及到的資料模型(介面、響應式資料)
搜尋欄

image.png

如果表單封裝的資料較多,建議封裝在一個物件中

SearchEmpModel:專門用來封裝搜尋欄的表單資料

image.png

需要使用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進行賦值

image.png

此處的賦值需要使用[[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})
表格及分頁
表格

image.png

  <!-- 列表展示 -->
  <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事件

頁面互動

分頁查詢功能

image.png

需要的資料模型:

根據介面文件可以定義請求引數的資料模型:

image.png

而我們在上文中定義了兩個資料模型:

//分頁資料模型
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}`)
  • 查詢:

image.png

let search = async () => {  
  
  let pageBean = await pageQueryApi({...searchEmp.value, ...pagination.value});  
  
  //更新列表  
  empList.value = pageBean.data.rows;  
  //更新記錄條數  
  pagination.value.total = pageBean.data.total;  
}

頁碼、條數變化的時候也需要呼叫search

清空功能

image.png

let clear = async ()=> {  
  //清空搜尋欄
  searchEmp.value = {  
    name: '',  
    gender: '',  
    begin: '',  
    end: '',  
    date: []  
  };  
  //再次查詢
  search();  
}

在清空之後,以下屬性都變為了空字串:

    name: '',  
    gender: '',  
    begin: '',  
    end: '',  

而我們在後端mybatis的動態SQL中對空字串進行了判斷。

  • 頁面載入完成自動查詢

新增員工

頁面佈局流程:

  • 確定要使用的Element元件
  • 確定涉及到的資料模型

頁面佈局

點選按鈕 彈出對話方塊,新增/編輯員工,需要的資料有兩部分:員工資訊和工作經歷資訊

image.png

涉及的資料模型:

//員工工作經歷資料介面  
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 : []  
});
使用者名稱/姓名佈局

image.png

<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>

當前顯示的效果:

image.png

頁面原型要求的顯示效果:

image.png

要在一行中顯示兩個表單元件,就需要ElementPlus提供的Layout佈局元件:透過基礎的 24 分欄,迅速簡便地建立佈局。

Layout佈局將一行(一個el-row)等分為24份,如果想設定兩個元件大小相等,只需要分別設定兩個元件(el-col)的屬性 :span = 12

image.png

<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>
性別/職位佈局:列表最佳化

image.png

之前的佈局方式:

<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>
部門佈局

image.png

與上文中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

頭像佈局

image.png

<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>
工作經歷佈局

image.png

點選新增工作經歷,工作經歷表單多一條記錄;點選刪除按鈕,刪除對應的記錄

這個功能看起來比較複雜,需要謹記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);  
  }  
}
表單校驗

在提交之前還需要進行表單校驗

對新增員工進行表單校驗需要參照介面原型的要求:

image.png

總結出如下的校驗規則:

image.png

表單校驗的流程:

  1. 定義表單例項 empFormRef,賦值給ref屬性,用來在save方法中校驗表單和在openDialog方法中重置表單狀態
  2. 定義校驗規則 FormRules,其中的泛型指定表單對應的資料模型,在需要校驗的表單項上透過prop指定規則名稱
<el-form :model="emp" ref="empFormRef" :rules="rules">
<el-form-item prop='校驗規則名稱'>

表單驗證時機:

  1. 儲存(新增/編輯)時,校驗透過提交資料,不透過提示資訊
  2. 開啟對話方塊(新增/修改)時,重置表單校驗規則

驗證時機:

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('查詢失敗')  
  }  
}

修改員工

  1. 點選編輯按鈕,資料回顯:根據ID查詢員工資訊
  2. 點選儲存,執行修改操作

image.png

資料回顯

介面層:

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('表單校驗失敗,不能提交');  
    }  
  })  
}

刪除員工

image.png

刪除員工資訊有兩個操作入口:

  • 點選每條記錄之後的 刪除 按鈕,刪除當前條記錄。
  • 點選多選框選中要刪除的員工,點選批次刪除,批次刪除員工資訊

批次刪除或刪除最終只需要呼叫服務端的同一個批次刪除介面即可。

介面文件:

/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> 修改密碼 &nbsp;&nbsp;&nbsp; |  &nbsp;&nbsp;&nbsp;  
          </a>  
          <!--讓超連結失效-->
          <a href="javascript:void(0)" @click="logout">   
            <el-icon><SwitchButton /></el-icon> 退出登入 【{{name}}】  
          </a>  
        </span>  
</template>

打包部署

相關文章