前端学习(tlias为例)

这段时间以来一直在帮别人写作业,然后在github上翻到了很多别人写的源码,后端部分代码我基本上都可以看的懂。不过前端部分总是感觉很迷糊,毕竟没有系统的学过。因此:以talis为例系统学习一下Vue,ElementPlus等前端的使用

谨以此篇记录我的前端学习过程

古之立大事者,不惟有超世之才,亦必有坚忍不拔之志。

——《晁错论》

talis前端开发

一、 Vue项目入门

1. 新建vue工程化项目

npm create vite

选择vue与JavaScript

构建项目

cd到目录下

npm install

npm run dev

这样就能看到项目的初始页面了

2. 项目文件简介

  • vite.config.js:Vue项目的配置信息,如端口号
  • package.json:项目配置文件,包括项目名,版本号、依赖包、版本等
  • src 源代码文件夹
  • src/assets 静态资源目录,存放图片、字体…
  • src/components 组件目录,存放通用组件
  • src/App.vue根目录
  • src/main.js 入口文件

image-20250525101404456

3. vue项目的开发访问流程

  • index.html 默认首页
  • main.js 入口文件。创建vue实例,将其挂载到某个区域

4. Vue的API风格

1. 选项式API

选项式APT:可以用包含多个选项的对象来描述组件的逻辑,如: data,methods, mounted等。选项定义的属性都会暴露在函数内部的this上,它会指向当前的组件实例。

image-20250525104113454

2. 组合式API

组合式API:是Vue3提供的一种基于函数的组件编写方式,通过使用函数来组织和复用组件的逻辑。它提供了一种更灵活、更可组合的方式来编写组件。

image-20250525104317715

组合式API小demo

views/HelloWorld.vue

整体流程:

  • 点击按钮时会调用increment函数
  • increment函数每点击一次就会将值++
  • 最终将++后的结果展示在页面上
<script setup>
// 引入ref模块
import { ref,onMounted } from 'vue'

// 说明响应式数据
const msg = ref(0);// 里面的值为0

// 声明函数-在组合式API中没有this
function increment(){
msg.value++ //将上方的值0进行++
}

// 声明钩子函数
onMounted(()=>{
console.log('Vue mounted')
})

</script>

<template>

<!-- 使用响应式数据 @click与v-on:click操作类似,绑定个点击事件 将值进行输出 -->
<button @click="increment">{{msg}}</button>

</template>

<style scoped>

</style>

在App.vue中引用

<script setup>
import HelloWorld from './views/HelloWorld.vue'
</script>

<template>
<HelloWorld />
</template>

<style scoped>

</style>

在main.js中注销全局样式引用

import { createApp } from 'vue'
import App from './App.vue'

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

页面显示

image-20250525110623881

  • setup:是一个标识,告诉Vue需要进行一些处理,让我们可以更简洁的使用组合式AP工。
  • ref()︰接收一个内部值,返回一个响应式的ref对象,此对象只有一个指向内部值的属性value。
  • onMounted():在组合式API中的钩子方法,注册一个回调函数,在组件挂载完成后执行。

5. 组合式API小案例

🐱🐱🐱好像摸到一点点小头绪了呢好神奇

首先在views/EmpList.vue中新建以下代码

<script setup>
// 导入响应式数据
import { ref ,onMounted} from 'vue'
import axios from 'axios';

//声明响应式数据-(v-model绑定的)
const name = ref(''); //姓名
const gender = ref(''); //性别
const job = ref(''); //职位
const userList = ref([]); //用户列表-需要遍历数组

// 声明函数
async function search() {
// 基于axios请求发送异步请求,请求服务器加载数据
// await 将异步请求变为同步请求(等待程序运行完毕来获取结果)
// await必须与async一起使用
const result =await axios.get(`https://web-server.itheima.net/emps/list?name=${name.value}&gender=${gender.value}&job=${job.value}`)
// result.data.data 第一个 data 表示响应主体 (result.data),第二个 data (result.data.data) 则是实际的数据列表。
userList.value = result.data.data


}

// 定义钩子函数
onMounted(() => {
search();//自动加载收索功能
})

</script>

<template>
<div id="center">
姓名: <input type="text" name="name" v-model="name">

性别:
<select name="gender" v-model="gender">
<option value="1"></option>
<option value="2"></option>
</select>

职位:
<select name="job" v-model="job">
<option value="1">班主任</option>
<option value="2">讲师</option>
<option value="3">其他</option>
</select>

<input class="btn" type="button" value="查询" @click="search">
</div>

<table>
<thead>
<tr>
<th>序号</th>
<th>姓名</th>
<th>头像</th>
<th>性别</th>
<th>职位</th>
<th>入职时间</th>
<th>更新时间</th>
</tr>

<!-- v-for 用于列表循环渲染元素 -->
<tr v-for="(user, index) in userList" :key="user.id">
<td>{{index + 1}}</td>
<td>{{user.name}}</td>
<td> <img :src="user.image"> </td>
<td>
<span v-if="user.gender == 1"></span>
<span v-else-if="user.gender == 2"></span>
<span v-else>其他</span>
</td>
<td>
<span v-if="user.job == 1">班主任</span>
<span v-else-if="user.job == 2">讲师</span>
<span v-else-if="user.job == 3">学工主管</span>
<span v-else-if="user.job == 4">教研主管</span>
<span v-else-if="user.job == 5">咨询师</span>
<span v-else>其他</span>
</td>
<td>{{user.entrydate}}</td>
<td>{{user.updatetime}}</td>
</tr>
</thead>
</table>
</template>

<style scoped>
table,th,td {
border: 1px solid #000;
border-collapse: collapse;
line-height: 50px;
text-align: center;
}

#center,table {
width: 60%;
margin: auto;
}

#center {
margin-bottom: 20px;
}

img {
width: 50px;
}

input,select {
width: 17%;
padding: 10px;
margin-right: 30px;
border: 1px solid #ccc;
border-radius: 4px;
}

.btn {
background-color: #ccc;
}
</style>

页面就会显示

image-20250525122256824

6. ElementPlus

组件地址

1. 导入步骤

常见vue项目

npm install element-plus –save 在main.js中引用

import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'

//引入ElementPlus
createApp( App ).use(ElementPlus ).mount( ' #app ')

2. 表格组件

好像没有啥好说的基本上就是抄抄抄的代码

<script 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', },
{ date: '2016-05-04', name: 'Tom', address: 'No. 189, Grove St, Los Angeles',},
{date: '2016-05-01',name: 'Tom',address: 'No. 189, Grove St, Los Angeles',},
]

</script>

<template>
<!-- 按钮 -->
<div 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>
</div>

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

<div class="mb-4">
<el-button round>Round</el-button>
<el-button type="primary" round>Primary</el-button>
<el-button type="success" round>Success</el-button>
<el-button type="info" round>Info</el-button>
<el-button type="warning" round>Warning</el-button>
<el-button type="danger" round>Danger</el-button>
</div>

<div class="mb-4">
<el-button :icon="Search" circle />
<el-button type="primary" :icon="Edit" circle />
<el-button type="success" :icon="Check" circle />
<el-button type="info" :icon="Message" circle />
<el-button type="warning" :icon="Star" circle />
<el-button type="danger" :icon="Delete" circle />
</div>

<!-- 表格 -->
<div class="mb-4" >
<el-table :data="tableData" stripe 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="住址" width="300" align="center" />
</el-table>
</div>

</template>

<style scoped>
.mb-4 {
margin-bottom: 10px;

}

</style>

image-20250525140829333

3. 分页条组件

<script setup>
import { ref } from 'vue'
const currentPage4 = ref(1) // 当前页码
const pageSize4 = ref(10) // 每页记录数
const small = ref(false)
const background = ref(false)
const disabled = ref(false)
const total = ref(40)// 总记录数

const handleSizeChange = (val) => {
console.log(`每页展示:${val}`)
}
const handleCurrentChange = (val) => {
console.log(`当前页码: ${val}`)
}
</script>

<template>

<!-- 分页条 -->
<div class="mb-4">
<el-pagination
v-model:current-page="currentPage4"
v-model:page-size="pageSize4"
:page-sizes="[10, 20, 30, 40]"
:small="small"
:disabled="disabled"
:background="background"
layout="total, sizes, prev, pager, next, jumper"
:total="total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>

</div>

</template>

<style scoped>
.mb-4 {
margin-bottom: 10px;

}

</style>

天下苦秦已久矣!

劳资学ElementPlus就是为了解决这个分页条组件英文问题

image-20250525142909314

之前翻人家写的代码一直不知道在哪里改,现在我就知道啦!

image-20250525143212375

呜呜呜终于等到你,还好我没放弃。

4. 对话框组件

就是抄抄抄

 <script setup>
import { ref,reactive} from 'vue'
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' },
{ date: '2016-05-04', name: 'Tom', address: 'No. 189, Grove St, Los Angeles' },
{ date: '2016-05-01', name: 'Tom', address: 'No. 189, Grove St, Los Angeles' },
]

const dialogTableVisible = ref(false)//重点
<template>
<!-- 对话框 -->
<div class="mb-4">
<el-button plain @click="dialogTableVisible = true">打开对话框</el-button>
<el-dialog v-model="dialogTableVisible" title="配送地址" width="800">
<el-table :data="tableData">
<el-table-column property="date" label="日期" width="150" />
<el-table-column property="name" label="姓名" width="200" />
<el-table-column property="address" label="地址" />
</el-table>
</el-dialog>
</div>
</template>

显示效果

image-20250525145710017

5. 表单组件

抄抄抄我抄

<script setup>
import { ref, reactive } from 'vue'
// 表单
const formInline = reactive({
name: '',
gender: '',
birthday: '',
})

const onSubmit = () => {
console.log(formInline)
}

</script>

<template>

<!-- 表单 -->
<div class="mb-4">
<el-form :inline="true" :model="formInline" class="demo-form-inline">
<el-form-item label="姓名">
<el-input v-model="formInline.name" placeholder="你叫啥" clearable />
</el-form-item>
<el-form-item label="性别">
<el-select v-model="formInline.gender" placeholder="男/女/沃尔玛购物袋" clearable>
<el-option label="男" value="1" />
<el-option label="女" value="2" />
<el-option label="沃尔玛购物袋" value="0" />
</el-select>
</el-form-item>
<el-form-item label="出生日期">
<el-date-picker v-model="formInline.birthday" type="date" placeholder="选一个日期" value-format="YYYY-MM-DD" clearable />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="onSubmit">提交</el-button>
</el-form-item>
</el-form>
</div>
</template>

<style scoped>
.mb-4 {
margin-bottom: 10px;

}
.demo-form-inline .el-input {
--el-input-width: 220px;
}

.demo-form-inline .el-select {
--el-select-width: 220px;
}
</style>

最终效果

image-20250525152747155

6. ElementPlus的小案例

image-20250525152925015

我来挑战一下嘿嘿

分析主要是要有表单与表格组件,参考json定义变量名

"code": 1,
"msg": "success",
"data": [
{
"id": 1,
"name": "谢逊",
"image": "https://web-framework.oss-cn-hangzhou.aliyuncs.com/2023/4.jpg",
"gender": 1,
"job": "1",
"entrydate": "2023-06-09",
"updatetime": "2025-05-25T08:11:11"
},
1.定义表单内容
<script setup>
import { ref,reactive } from 'vue'
// 表单
const formInline = reactive({
name: '',
gender: '',
birthday: '',
})

const onSubmit = () => {
console.log(formInline)
}
</script>

<template>
<!-- 表单 -->
<div class="mb-4">
<el-form :inline="true" :model="formInline" class="demo-form-inline">
<el-form-item label="姓名">
<el-input v-model="formInline.name" placeholder="你叫啥" clearable />
</el-form-item>
<el-form-item label="性别">
<el-select v-model="formInline.gender" placeholder="男/女/沃尔玛购物袋" clearable>
<el-option label="男" value="1" />
<el-option label="女" value="2" />
<el-option label="沃尔玛购物袋" value="0" />
</el-select>
</el-form-item>
<el-form-item label="出生日期">
<el-date-picker v-model="formInline.birthday" type="date" placeholder="选一个日期" value-format="YYYY-MM-DD" clearable />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="onSubmit">提交</el-button>
</el-form-item>
</el-form>
</div>
</template>

<style scoped>
.demo-form-inline .el-input {
--el-input-width: 220px;
}

.demo-form-inline .el-select {
--el-select-width: 220px;
}
</style>

但是我的格式好像不太对,会适应屏幕

image-20250525154025875

我去翻翻文档有没有固定的

文档没有但是找到了其他解决方案

image-20250525160859203

嗯嗯很完美

2. 完成表格内容
<script setup>
import { ref, onMounted } from 'vue'
import axios from 'axios';



//声明响应式数据-(v-model绑定的)
const name = ref('');//姓名
const gender = ref(''); //性别
const job = ref('');//职位
const userList = ref([])//用户列表-需要遍历数组

// 收索方法
async function search() {
// 基于axios请求发送异步请求,请求服务器加载数据
// await 将异步请求变为同步请求(等待程序运行完毕来获取结果)
// await必须与async一起使用
const result =await axios.get(`https://web-server.itheima.net/emps/list?name=${name.value}&gender=${gender.value}&job=${job.value}`)
// result.data.data 第一个 data 表示响应主体 (result.data),第二个 data (result.data.data) 则是实际的数据列表。
userList.value = result.data.data
}

//清空方法
function onClean(){
name.value = ''
gender.value = ''
job.value = ''
search()
}


// 定义钩子函数
onMounted(() => {
search();
})
</script>

<template>
<!-- 表单 -->
<div class="mb-3">
<div class="mb-4" style="width: 1500px;">
<el-form :inline="true" :model="formInline" class="demo-form-inline">
<el-form-item label="姓名">
<el-input v-model="name" placeholder="请输入姓名" clearable />
</el-form-item>
<el-form-item label="性别">
<el-select v-model="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="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="search">查询</el-button>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="onClean">清空</el-button>
</el-form-item>
</el-form>

<el-table :data="userList" border style="width: 1030px;">
<el-table-column prop="date" label="ID" width="100" />
<el-table-column prop="name" label="姓名" width="100" />
<el-table-column prop="address" label="头像" width="100" />
<el-table-column prop="address" label="性别" width="100" />
<el-table-column prop="address" label="职位" width="100" />
<el-table-column prop="address" label="入职日期" width="180" />
<el-table-column prop="address" label="更新日期" width="180" />
</el-table>
</div>
</div>

</template>

<style scoped>
.demo-form-inline .el-input {
--el-input-width: 220px;
}

.demo-form-inline .el-select {
--el-select-width: 220px;
}

.mb-3 {
border: 1px solid red;
width: 90%;
max-width: 1300px;
position: absolute;
left: 50%;
transform: translateX(-50%);

}
</style>

完成了表格内容,不过页面显示的很不对劲

image-20250525164043304

目前只有姓名是显示正常的,其他都不太行我在改改

<el-table-column prop="date" label="ID" width="100"  />
<el-table-column prop="name" label="姓名" width="100" />
<el-table-column prop="address" label="头像" width="100" />
<el-table-column prop="address" label="性别" width="100" />
<el-table-column prop="address" label="职位" width="100" />
<el-table-column prop="address" label="入职日期" width="180" />
<el-table-column prop="address" label="更新日期" width="180" />

好像发现问题了,我这里没有绑定嘿嘿

3. 修改表格绑定内容
<el-table :data="userList" border style="width: 1030px;">
<el-table-column prop="id" label="ID" width="100" />
<el-table-column prop="name" label="姓名" width="100" />
<el-table-column prop="image" label="头像" width="100" />
<el-table-column prop="gender" label="性别" width="100" />
<el-table-column prop="job" label="职位" width="100" />
<el-table-column prop="entrydate" label="入职日期" width="180" />
<el-table-column prop="updatetime" label="更新日期" width="180" />
</el-table>

image-20250525164411643

哇塞哇塞有效果了,不过图片不显示,感觉像是啥属性没有配置的原因,我去翻翻文档 image-20250525164808143

好像发现了,我改改

4. 更改图片属性

啊?啊?啊?

这里去官方文档里找的代码没有成功回显图片,显示失败

查看了老师的代码

<el-table-column prop="image" label="头像" width="100">
<template #default="scope">
<img :src="scope.row.image" height="40px" >
</template>
</el-table-column>

嗯…咱也不知道为啥反正就实现了

image-20250525165622958

测试功能一切正常

5. 最终效果

image-20250525172358734

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



//声明响应式数据-(v-model绑定的)
const name = ref('');//姓名
const gender = ref(''); //性别
const job = ref('');//职位
const userList = ref([])//用户列表-需要遍历数组

// 收索方法
async function search() {
// 基于axios请求发送异步请求,请求服务器加载数据
// await 将异步请求变为同步请求(等待程序运行完毕来获取结果)
// await必须与async一起使用
const result = await axios.get(`https://web-server.itheima.net/emps/list?name=${name.value}&gender=${gender.value}&job=${job.value}`)
// result.data.data 第一个 data 表示响应主体 (result.data),第二个 data (result.data.data) 则是实际的数据列表。
userList.value = result.data.data
}

//清空方法
function onClean() {
name.value = ''
gender.value = ''
job.value = ''
search()
}


// 定义钩子函数
onMounted(() => {
search();
})
</script>

<template>
<!-- 表单 -->
<div class="mb-4" style="max-width: 1200px; margin: 0 auto; min-height: 100vh; display: flex; flex-direction: column; align-items: center; margin-top: 200px;">
<el-form :inline="true" :model="formInline" class="demo-form-inline">
<el-form-item label="姓名">
<el-input v-model="name" placeholder="请输入姓名" clearable />
</el-form-item>
<el-form-item label="性别">
<el-select v-model="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="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="search">查询</el-button>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="onClean">清空</el-button>
</el-form-item>
</el-form>

<el-table :data="userList" border style="width: 1030px;">
<el-table-column prop="id" label="ID" width="110" align="center"/>
<el-table-column prop="name" label="姓名" width="110" align="center"/>
<el-table-column label="头像" width="140" align="center">
<template #default="scope">
<img :src="scope.row.image" height="80px" width="80px" >
</template>
</el-table-column>
<el-table-column label="性别" width="110" align="center">
<template #default="scope">
{{ scope.row.gender === 1 ? '男' : '女' }}
</template>
</el-table-column>
<el-table-column label="职位" width="110" align="center">
<template #default="scope">
<span v-if="scope.row.job == 1">班主任</span>
<span v-else-if="scope.row.job == 2">讲师</span>
<span v-else-if="scope.row.job == 3">学工主管</span>
<span v-else-if="scope.row.job == 4">教研主管</span>
<span v-else-if="scope.row.job == 5">咨询师</span>
<span v-else>其他</span>
</template>
</el-table-column>
<el-table-column prop="entrydate" label="入职日期" width="220" align="center"/>
<el-table-column prop="updatetime" label="更新日期" width="230" align="center"/>
</el-table>
</div>

</template>

<style scoped>
.demo-form-inline .el-input {
--el-input-width: 220px;
}

.demo-form-inline .el-select {
--el-select-width: 220px;
}


</style>

使用自定义模板prop标签可以删除

二、tlias侧边栏开发

小知识点

在老师的vue文件中有这样的一个代码

import Layout from "@/views/layout/index.vue";

这里的@就是完美Src的目录文件夹,定义在了vite.config.js文件中

// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
}
})

老师的页面布局代码使用的是Element的布局,需要在文档查看

1. 左侧侧边栏

1. 左侧菜单基本定义

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

<!-- 添加左侧侧边栏 -->
<el-menu :default-openeds="['1', '3']">
<!-- 一级菜单 -->
<el-sub-menu index="1">
<template #title>
<el-icon>
<message />
</el-icon>Navigator One
</template>
<el-menu-item index="1-1">Option 1</el-menu-item>
<el-menu-item index="1-2">Option 2</el-menu-item>
</el-sub-menu>
</el-menu>
</el-aside>

左侧侧边栏大致长这样,需要注意的是

<el-menu :default-openeds="['1', '3']">

代表的是默认展开,索引为1和3,开发过程中不需要展开,在这里就给删除了

<script setup>

</script>

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

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

<!-- 添加左侧侧边栏 -->
<el-menu>

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

<el-icon><Promotion></Promotion></el-icon>首页
</el-menu-item>

<!-- 一级菜单 学员管理-->
<el-sub-menu index="/manage">
<template #title>
<!-- 这里导入了图标组件在文档里有 -->
<el-icon><Menu /></el-icon>班级学员管理
</template>
<el-menu-item index="/clazz"><el-icon><HomeFilled /></el-icon>班级管理</el-menu-item>
<el-menu-item index="/stu"><el-icon><UserFilled /></el-icon>学员管理</el-menu-item>
</el-sub-menu>

<!-- 二级菜单 系统信息管理 -->
<el-sub-menu index="/system">
<template #title>
<!-- 这里导入了图标组件在文档里有 -->
<el-icon><Tools /></el-icon>系统信息管理
</template>
<el-menu-item index="/dept"><el-icon><HelpFilled /></el-icon>部门管理</el-menu-item>
<el-menu-item index="/emp"><el-icon><Avatar /></el-icon>员工管理</el-menu-item>
</el-sub-menu>

<!-- 三级菜单 数据统计管理 -->
<el-sub-menu index="/report">
<template #title>
<!-- 这里导入了图标组件在文档里有 -->
<el-icon><Histogram /></el-icon>数据统计管理
</template>
<el-menu-item index="/empReport"><el-icon><InfoFilled /></el-icon>员工信息统计</el-menu-item>
<el-menu-item index="/stuReport"><el-icon><Share /></el-icon>学员信息统计</el-menu-item>
<el-menu-item index="/log"><el-icon><Document /></el-icon>日志信息统计</el-menu-item>
</el-sub-menu>
</el-menu>
</el-aside>


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

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

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

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

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

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

定义完之后就长这样基本上就是抄代码

image-20250525212813258

2. 动态菜单(Router路由)

Router 路由知识点
  1. router组成

    • Router实例:路由实例,基于createRouter函数创建,维护了应用的路由信息。

      eg: /stu: /views/stu/index.vue 当配置stu时会访问的路径

    • <router-link>:路由链接组件,浏览器会解析成<a>

      eg: <router-link to='/emp'>员工管理</router-link> 会被解析为超链接标签,点击会跳转(当点击员工管理会跳转emp页面。然后这个标签会去查找我们的实例中定义的/emp的组件路径)

    • <router-view>:动态视图组件,用来渲染展示与路由路径对应的组件。

      eg: <router-view></router-view>你想在哪里展示组件就在那里加这个标

1. 开启router

<el-menu router>自需要在菜单栏上添加一个router既可以跳转了,好神奇

2.配置路由index
import { createRouter, createWebHistory } from 'vue-router'
import IndexView from '@/views/index/index.vue'
import clazzView from '@/views/clazz/index.vue'
import DeptView from '@/views/dept/index.vue'
import EmpView from '@/views/emp/index.vue'
import LogView from '@/views/log/index.vue'
import stuView from '@/views/stu/index.vue'
import EmpReportView from '@/views/report/emp/index.vue'
import stuReportView from '@/views/report/stu/index.vue'
import LayoutView from '@/views/layout/index.vue'
import LoginView from '@/views/login/index.vue'

const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{ path: '/index', name: 'index', component: IndexView },
{ path: '/clazz', name: 'clazz', component: clazzView },
{ path: '/stu', name: 'stu', component: stuView },
{ path: '/dept', name: 'dept', component: DeptView },
{ path: '/emp', name: 'emp', component: EmpView },
{ path: '/log', name: 'log', component: LogView },
{ path: '/empReport', name: 'empReport', component: EmpReportView },
{ path: '/stuReport', name: 'stuReport', component: stuReportView },
{ path: '/login', name: 'login', component: LoginView }
]
})

export default router

导入路由然后配置路径

3. 在需要展示的地方导入视图
// 右侧核心区 
<el-main>
<router-view></router-view>
</el-main>
4.页面展示

image-20250525224232768

神奇!

3. 嵌套路由

案例引出:当我们在页面访问登录功能时会在右侧盒子中显示登陆页面。这肯定是不符合要求的,推测要在登录页面展示这个路由。

原因:我们在app.vue中引入了默认页面layout。当页面打开时默认显示layout,猜测修改:使用index包裹layout(卧槽卧槽卧槽)

老师说不能把这个页面写死,也就是说不要在app.vue中固定页面(猜错了嘿嘿)

1. 动态路由组件

在app.vue中填写

<router-view></router-view>

当匹配到哪个组件就需要在页面中展示哪个组件

咦~现在当我打开页面时变成空白了,也就是说没有指定默认访问页面

image-20250526153458819

推测修改:可能要在里面添加Layout组件

2. 路由嵌套

image-20250526153807133

也就是说要在大路由中嵌套layout

 {path:'/',
name:'',
component: LayoutView,
redirect:'/index',
children:[
{path:'index',name:'index',component:IndexView},
{path:'clazz',name:'clazz',component:clazzView},
{path:'dept',name:'dept',component:DeptView},
{path:'emp',name:'emp',component:EmpView},
{path:'log',name:'log',component:LogView},
{path:'stuReport',name:'stuReport',component:stuView},
{path:'empReport',name:'empReport',component:stuReportView},
]
},
{ path: '/login', name: 'login', component: LoginView }
]
})
  1. 当我们访问根路径时,默认访问的是LayoutView布局的页面,也就是我们最开始的页面。然后就能愉快的访问里面的页面啦。当我们访问login时也能正常显示
  2. 路由中配置了redirect,也就是说当我们访问layout的布局内容是,会重定向到index页面

不过感觉这里应该会加拦截器相关的内容🍔(看到大汉堡突然想吃汉堡了😋)

三、部门管理-查询

1. 页面分析

image-20250526161040143

呜呼呜呼呜呼呜呼

  • 部门管理页面首先上面有一个部门管理标题
  • 一个新增按钮(点击新增会有提示框)
  • 4列表格
  • 编辑,删除按钮

2. 页面布局开发

  1. 标题

    直接使用的h1标签

  2. 新增部门按钮,使用Ele的按钮组件

    image-20250526161909236

    这里不需要div吗,感觉也太紧凑了吧

  3. 为按钮设置一个div然后设置外边距

    <div class="container">
    <el-button type="primary">+新增部门</el-button>
    </div>

    .container{
    /* 距离上下10px 左右0px */
    margin: 20px 0px;
    }

    这样就好看哆啦

  4. 新增表格组件

    拷贝表格组件代码

    需要注意的是:部门id自增的,需要定义

    设置type属性为index即可从1开始自增

  5. 设置操作

    当我们使用表格标签时,默认显示的不能满足我们的需求,我们这就需要自定义标签(默认展示的是文字,我们现在要展示按钮)

  6. 完整布局代码

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

    //定义响应式数据
    const tableData = ref([
    {
    id: 1,
    name: "学工部",
    createTime: "2022-09-01T23:06:29",
    updateTime: "2022-09-01T23:06:29",
    },
    ]);
    </script>

    <template>
    <h1>部门管理</h1>
    <div class="container">
    <el-button type="primary">+新增部门</el-button>
    </div>

    <div class="container">
    <el-table :data="tableData" border style="width: 100%">
    <el-table-column type="index" label="序号" width="100" align="center"/>
    <el-table-column prop="name" label="部门名称" width="260" align="center" />
    <el-table-column prop="updateTime" label="最后操作时间" width="300" align="center"/>
    <el-table-column label="操作" align="center">
    <template #default="scope">
    <el-button type="primary" size="small">
    <el-icon><Edit /></el-icon>编辑
    </el-button>
    <el-button type="danger" size="small">
    <el-icon><Delete /></el-icon>删除
    </el-button>
    </template>
    </el-table-column>
    </el-table>
    </div>
    </template>

    <style scoped>
    .container {
    /* 距离上下10px 左右0px */
    margin: 20px 0px;
    }
    </style>

    image-20250526165355827

    太漂亮啦嘿嘿

3. 列表查询-加载数据

根据需求,需要在打开页面之后,需要自动加载全部部门数据,展示在表格中。

使用ApiFox创建mock模拟数据

当我们前端在开发中,假设后端还没开发完毕,这时候我们想要动态查询数据可以使用ApiFox提供的mock模拟后端生成的数据

  1. 打开ApiFox(没有就下载)

  2. 新建get请求接口,导入响应回来的接口数据(参考接口文档)

  3. 然后点击mock的云端在里面进行开启

  4. 我忘记截屏了,不过里面有一个停用按钮,我把开启就可以了(我选的是响应式优先)

  5. 具体请观看老师的视频:

在ApiFox中定义完接口还需要回到前端进行引用。这里要用到的技术是axios

完整代码

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

// 定义钩子函数,当页面加载完毕就会调用这函数,然后这个函数就会调用serch方法,
// 就会调用链接,将获取的值赋给deptList,然后就在页面显示了
onMounted(() => { //生命周期
search();
});
// 查询部门列表
const search=async() => {
const result = await axios.get("https://m1.apifoxmock.com/m1/6421010-6118327-default/depts");

// 如果code为1就显示数据,否则提示错误信息
if(result.data.code == 1) { //布尔类型
deptList.value = result.data.data;
} else {
this.$message.error(result.data.message);
}
};

//定义响应式数据
const deptList = ref([]);


</script>

<template>
<h1>部门管理</h1>
<div class="container">
<el-button type="primary">+新增部门</el-button>
</div>

<div class="container">
<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="260" align="center" />
<el-table-column prop="updateTime" label="最后操作时间" width="300" align="center"/>
<el-table-column label="操作" align="center">
<template #default="scope">
<el-button type="primary" size="small">
<el-icon><Edit /></el-icon>编辑
</el-button>
<el-button type="danger" size="small">
<el-icon><Delete /></el-icon>删除
</el-button>
</template>
</el-table-column>
</el-table>
</div>
</template>

<style scoped>
.container {
/* 距离上下10px 左右0px */
margin: 20px 0px;
}
</style>

页面显示(我的分辨率好像有问题,感觉页面显示的太小了)

6835ab59de608

这里使用axios来进行异步请求,使用钩子函数来触发。

神奇!

4. 程序优化

当前程序问题:URL是写死的不方便维护

1. 代码分析

  1. 改进方案1

    将异步请求提取到一个工具类中,这样就可以根据这个工具类来获取信息了

    这里定义了timout,是响应保留时间

    太短了的话后端打断点会报错。

  2. 改进方案2

    将与服务器进行异步请求交互的逻辑封装在一个单独的api中

    image-20250527203146290

    懂了懂了,全都懂了。我终于可以看懂别人的前端代码了

2. 具体代码

src/api/dept.js

// 导入封装好的axios实例(包含基础配置和响应拦截器)
import request from "@/utils/request";

/**
* @description 查询全部部门数据信息
* @returns {Promise} 返回Promise对象,成功时返回部门列表数据
* @example
* queryAllApi().then(data => {
* console.log('部门列表:', data);
* }).catch(error => {
* console.error('获取部门列表失败:', error);
* });
*/
export const queryAllApi = () => {
// 发起GET请求,路径为基础URL(/api)拼接/depts,即/api/depts
return request.get('/depts');
};

src/utils/request.js

import axios from 'axios';

/**
* @description 创建axios实例对象
* @param {Object} config - axios实例配置对象
* @property {string} baseURL - 基础URL,用于拼接请求地址(示例:'/api'会自动添加到所有请求URL前面)
* @property {number} timeout - 请求超时时间(单位:毫秒),超过600000毫秒未响应则请求失败
*/
const request = axios.create({
baseURL: 'https://m1.apifoxmock.com/m1/6421010-6118327-default', // 所有请求的基础路径,可根据后端接口地址统一配置
timeout: 600000 // 请求超时时间设置为10分钟(600000ms)
});

/**
* @description axios响应拦截器(处理响应数据的统一入口)
* @param {Function} response => response.data - 成功响应处理函数
* - 作用:对所有接口返回的response对象进行预处理
* - 常规操作:提取response中的data字段(后端返回的业务数据通常存储在此)
* @param {Function} error => Promise.reject(error) - 失败响应处理函数
* - 作用:统一处理请求失败的情况(如网络错误、超时、后端异常等)
* - 常规操作:将错误对象包装为Promise.reject以便上层调用捕获
*/
request.interceptors.response.use(
(response) => { // 响应成功时的回调函数
// 这里默认返回response中的data字段,通常后端返回的结构为{code: xxx, data: xxx, message: xxx}
// 可根据实际需求进一步处理(如统一错误码判断、数据格式化等)
return response.data;
},
(error) => { // 响应失败时的回调函数
// 对请求错误进行统一处理(如记录错误日志、提示用户、重试机制等)
// 此处直接返回 rejected 状态的Promise,以便调用方通过.catch()捕获错误
return Promise.reject(error);
}
);

// 导出axios实例对象,供其他模块调用
export default request;

src/views/dept/index.vue

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

// 定义钩子函数,当页面加载完毕就会调用这函数,然后这个函数就会调用serch方法,
// 就会调用链接,将获取的值赋给deptList,然后就在页面显示了
onMounted(() => { //生命周期
search();
});
// 查询部门列表
const search=async() => {
// 注释掉旧代码
// const result = await axios.get("https://m1.apifoxmock.com/m1/6421010-6118327-default/depts");
// // 如果code为1就显示数据,否则提示错误信息
// if(result.data.code == 1) { //布尔类型
// deptList.value = result.data.data;
// } else {
// this.$message.error(result.data.message);
// }

const result = await queryAllApi();
deptList.value = result.data;

};

//定义响应式数据
const deptList = ref([]);


</script>

页面正常显示数据,注意是在src目录下新建包,不是在views里面

3. 反向代理

卧槽这几集全是干货啊,好爽好爽好好爽

经过上面的优化基础功能实现了,不过还没有解决跨域问题,以及URL在src中写死的问题,修改request.js的访问路径为/api

同时修改vite.config.js代码解决跨域问题,访问后端8080路径

import { fileURLToPath, URL } from 'node:url'; // 导入Node.js的URL处理模块

import { defineConfig } from 'vite'; // 导入Vite的配置定义函数
import vue from '@vitejs/plugin-vue'; // 导入Vue插件,支持Vue单文件组件

/**
* Vite项目配置文件
* 配置参考:https://vitejs.dev/config/
*/
export default defineConfig({
// 插件配置(增强Vite功能)
plugins: [
vue(), // 启用Vue 3单文件组件支持
],

// 路径解析配置(处理模块导入路径)
resolve: {
alias: {
// 配置别名 '@' 指向项目的src目录
// 例如:import App from '@/App.vue' 等价于 import App from './src/App.vue'
'@': fileURLToPath(new URL('./src', import.meta.url))
}
},

// 开发服务器配置(仅在开发环境生效)
server: {
// 配置代理服务器(解决跨域问题)
proxy: {
// 所有以/api开头的请求路径都会被代理
'/api': {
target: 'http://localhost:8080', // 目标服务器地址(后端API地址)
secure: false, // 不验证SSL证书
changeOrigin: true, // 修改请求头中的Origin字段为目标服务器地址
rewrite: (path) => path.replace(/^\/api/, ''), // 重写请求路径,去除/api前缀
// 例如:/api/users => http://localhost:8080/users
}
}
}
});

这一段相当于固定代码

完成以上步骤请打开你的tlias后端尝试前后端联调吧🐱🐱🐱

要注释掉有关后端拦截器的代码,否则会报令牌校验错误

我这边前后端联调没有问题

image-20250527214008743

呜呜呜谢谢老师,谢谢黑马。感觉离开发又近了一步

四、部门管理-新增

1. 页面原型

image-20250528160750563

image-20250528160931086

新增时点击会调用后端的新增操作

需要一个Dialog对话框组件和一个From表单组件

2. 新增部门

1. 定义dialog

  <!-- Dialog对话框组件 -->
<!-- 为了让修改部门也可以复用这个Dialog,所以要将标题动态绑定,所以要使用插槽,注意这里需要加:插槽的标签是title,所以这里要写title,而不是fromTitle -->
<el-dialog v-model="dialogFormVisible" :title="fromTitle" width="500">
<el-form :model="dept">
<el-form-item label="部门名称" :label-width="80">
<el-input v-model="dept.name" />
</el-form-item>

</el-form>
<template #footer>
<div class="dialog-footer">
<!--@click="dialogFormVisible = false"是当点击按钮被点击的时候,dialogFormVisible被设置为false,即关闭对话框 -->
<el-button @click="dialogFormVisible = false">取消</el-button>
<!-- @click="save"是当点击按钮被点击的时候,调用save方法,即保存数据 -->
<el-button type="primary" @click="save">确定</el-button>
</div>
</template>
</el-dialog>

这里需要注意的是:

  • 当我们点击新增按钮时,需要将对话框显示的调用出来。因此需要在新增按钮上添加点击事件,当点击时将按钮显示出来。然后在页面点击取消按钮会隐藏对话框
  • 然后需要在保存按钮上添加保存方法。当点击时会调用后端的插入方法
  • 为了我们的Dialog对话框能实现复用效果。将对话框的标题设置为了动态绑定效果,当点击新增时,将标题渲染为新增部门
// Dialog对话框组件
const dialogFormVisible = ref(false);
// 部门名称
const dept = ref({name:''});
// 动态表单标题,当新增时候标题为新增部门,否则为修改部门
const fromTitle = ref('');
// 保存按钮
const save = () => {
};

// 部门新增按钮
const addDept = () => {
// 先将表单显示出来
dialogFormVisible.value = true;
// 然后将表单标题设置为新增部门
fromTitle.value = '新增部门';

};

2. 导入消息组件

import { ElMessage } from "element-plus";
---
//导入
if (result.code ) {
ElMessage.success("添加成功");

// 关闭对话框
dialogFormVisible.value = false;

// 查询最新数据
search();
}else {
ElMessage.error(result.msg);
}

3. 初始代码功能完成(未完成表单校验功能)

// Dialog对话框组件
const dialogFormVisible = ref(false);
// 部门名称
const dept = ref({name:''});
// 动态表单标题,当新增时候标题为新增部门,否则为修改部门
const fromTitle = ref('');
// 保存按钮
const save = async() => {
// 发送请求,保存数据-调用接口,并且将部门数据传入给api接口,然后api在将数据使用post传入后端
// await 是 await 是一个关键字,用来等待一个 Promise 对象。当发送请求的时候,await 会等待接口返回数据,然后返回数据
// 否则会直接执行完毕用户看不到交互结果
const result = await addApi(dept.value);

// 做一个判断,如果返回的数据是成功,那么就提示用户,并且将表单清空
if (result.code ) {
ElMessage.success("添加成功");

// 关闭对话框
dialogFormVisible.value = false;

// 查询最新数据
search();
}else {
ElMessage.error(result.msg);
}
};

// 部门新增按钮
const addDept = () => {
// 先将表单显示出来
dialogFormVisible.value = true;
// 然后将表单标题设置为新增部门
fromTitle.value = '新增部门';
// 重置表单
dept.value = {name:''};

};

emm 简单来描述一下吧

  • 首先我们在Ele上面copy了代码过来,这时候我们是不是要声明这个组件,将值默认设为false(默认关闭的)
  • 那么我们怎么才能开启这个对话框呢?聪明的你肯定想到了。哎我们在点击新增部门时把对话框显示不就好了
  • 那么这样我们是不是要在新增部门按钮上添加一个点击方法呀。定义一个新增部门的方法。当用户点击新增部门时将对话框的值设为true(这样就能显示对话框了)
  • 同时呢我们还需要声明这个部门模块。<el-form :model="dept">
  • 这时候当我们想要复用dialog表单的话,我们就要将标题设为动态(不再赘述)
  • 部门名称填写完毕,这时候当我们点击保存按钮是不是要发送数据到后端?
  • 使用我们就要在api.js里面定义后端方法。定义完成后再引用。
  • 声明这个保存方法,然后使用await修饰这个方法,并且将我们部门名称这个值作为参数传入后端。当后端返回码为1时,我们就使用导入的消息通知给出提示插入成功,同时关闭对话框。否则给出失败提示(失败提示是后端返回的提示)
  • 然后需要清空表单

4. 新增优化

image-20250528183353157

1. 效果展示
// 表单校验相关
const rules = ref({
name: [
// required: true部门名称必填,trigger: "blur" 表示失去焦点时触发校验
{ required: true, message: "请输入部门名称", trigger: "blur" },
{ min: 2, max: 10, message: "长度在 2 到 10 个字符", trigger: "blur" },
],
});
<el-form :model="dept" :rules="rules" >
<el-form-item label="部门名称" :label-width="80" prop="name">

然后指定校验规则,然后将prop设置为里面具体值

2. 表单规则校验拦截

啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊我直接战吼起手…太几把折磨人了

好的现在我们一起来看一下这个校验规则上一条我们完成了表单校验,不过有个bug,就是虽然页面可以校验但是不能拦截,当我们点击保存时还是可以插入信息。下面我们来解决这个问题

  • 在表单上挂载ref="deptFormRef"
  • 然后声明这个变量,这样我们就可以对这个表单进行操作了
  • 当我们表单为空时直接返回
  • 不为空就调用这个验证规则
  • 从我的表述中我可以发现我这一块没有听懂
<script setup>
import { ref, onMounted } from "vue";
import { queryAllApi, addApi } from "@/api/dept";
import { ElMessage } from "element-plus";

// 定义钩子函数,当页面加载完毕就会调用这函数,然后这个函数就会调用serch方法,
// 就会调用链接,将获取的值赋给deptList,然后就在页面显示了
onMounted(() => { //生命周期
search();
});
// 查询部门列表
const search = async () => {
// 注释掉旧代码
// const result = await axios.get("https://m1.apifoxmock.com/m1/6421010-6118327-default/depts");
// // 如果code为1就显示数据,否则提示错误信息
// if(result.data.code == 1) { //布尔类型
// deptList.value = result.data.data;
// } else {
// this.$message.error(result.data.message);
// }

const result = await queryAllApi();
deptList.value = result.data;

};

//定义响应式数据
const deptList = ref([]);


// Dialog对话框组件
const dialogFormVisible = ref(false);
// 部门名称
const dept = ref({ name: '' });
// 动态表单标题,当新增时候标题为新增部门,否则为修改部门
const fromTitle = ref('');
// 保存按钮
const save = async () => {
// 进行表单校验
if (!deptFormRef.value) { return }
deptFormRef.value.validate(async(valid) => { // valid表示是否通过校验
if (valid) {
// 发送请求,保存数据-调用接口,并且将部门数据传入给api接口,然后api在将数据使用post传入后端
// await 是 await 是一个关键字,用来等待一个 Promise 对象。当发送请求的时候,await 会等待接口返回数据,然后返回数据
// 否则会直接执行完毕用户看不到交互结果
const result = await addApi(dept.value);

// 做一个判断,如果返回的数据是成功,那么就提示用户,并且将表单清空
if (result.code) {
ElMessage.success("添加成功");

// 关闭对话框
dialogFormVisible.value = false;

// 查询最新数据
search();
} else {
ElMessage.error(result.msg);
}
}else {
ElMessage.error("表单校验未通过");
}

});

};

// 部门新增按钮
const addDept = () => {
// 先将表单显示出来
dialogFormVisible.value = true;
// 然后将表单标题设置为新增部门
fromTitle.value = '新增部门';
// 重置表单
dept.value = { name: '' };

// 清空表单校验
if(deptFormRef.value){
deptFormRef.value.resetFields();
}

};


// 表单校验相关
const rules = ref({
name: [
// required: true部门名称必填,trigger: "blur" 表示失去焦点时触发校验
{ required: true, message: "请输入部门名称", trigger: "blur" },
{ min: 2, max: 10, message: "长度在 2 到 10 个字符", trigger: "blur" },
],
});


// 定义响应式对象-用于校验表单数据(不通过不能提交)
const deptFormRef = ref();

</script>

<template>
<h1>部门管理</h1>
<div class="container">
<!-- 绑定点击事件,点击按钮,dialogFormVisible被设置为true,即打开对话框 -->
<el-button type="primary" @click="addDept">+新增部门</el-button>
</div>

<!-- 表格区域 -->
<div class="container">
<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="260" align="center" />
<el-table-column prop="updateTime" label="最后操作时间" width="300" align="center" />
<el-table-column label="操作" align="center">
<template #default="scope">
<el-button type="primary" size="small">
<el-icon>
<Edit />
</el-icon>编辑
</el-button>
<el-button type="danger" size="small">
<el-icon>
<Delete />
</el-icon>删除
</el-button>
</template>
</el-table-column>
</el-table>
</div>


<!-- Dialog对话框组件 -->
<!-- 为了让修改部门也可以复用这个Dialog,所以要将标题动态绑定,所以要使用插槽,注意这里需要加:插槽的标签是title,所以这里要写title,而不是fromTitle -->
<el-dialog v-model="dialogFormVisible" :title="fromTitle" width="500">
<el-form :model="dept" :rules="rules" ref="deptFormRef">
<!-- ref="deptFormRef"通过这个响应式对象我们就能引用到表单数据了 -->
<el-form-item label="部门名称" :label-width="80" prop="name" >
<el-input v-model="dept.name" />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<!--@click="dialogFormVisible = false"是当点击按钮被点击的时候,dialogFormVisible被设置为false,即关闭对话框 -->
<el-button @click="dialogFormVisible = false">取消</el-button>
<!-- @click="save"是当点击按钮被点击的时候,调用save方法,即保存数据 -->
<el-button type="primary" @click="save">确定</el-button>
</div>
</template>
</el-dialog>


</template>

<style scoped>
.container {
/* 距离上下10px 左右0px */
margin: 20px 0px;
}
</style>

完整代码

五、部门管理-修改部门

1. 页面原型

image-20250529152536660

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

2. 分析-查询回显

  • 当我们点击编辑按钮时是不是就要弹Dialog出对话框,那么我们就要先绑定点击事件。调用绑定事件时我们是不是要传数据,将我们部门id传给后端(通过scpore.row就拿到了这一行的数据在.id就拿到了这个id数据)

  • 既然我们定义了点击事件我们是不是要声明

  • 调用result将参数id传入进去,并且添加判断

  • 需要展开对话框,设置标题,回显数据,重置表单校验

    // 编辑操作-接收部门id
    const edit = async (id)=>{
    // 这样就调用了api中的根据id查询部门信息的接口
    const result = await queryByIdApi(id);
    if(result.code){
    // 展示对话框
    dialogFormVisible.value = true;
    // 设置表单标题为编辑部门
    fromTitle.value = '编辑部门';
    // 将部门数据设置给dept对象(将数据回显到表单输入框中)
    dept.value = result.data;
    // 重置表单校验
    if(deptFormRef.value){
    deptFormRef.value.resetFields();
    }
    }
    }

3. 分析-修改

当我们修改点击Dialog对话框确定时是不是要保存我们修改的信息。那么我们是不是要在确定按钮上添加点击事件。这时候就发现 我们已经在确定按钮上绑定点击事件了。那么什么时候我们需要修改方法,什么时候我们需要保存方法呢?就是当我们页面中有这个数据那我们就用修改方法,当页面没有这个数据就为保存方法

基于这个思路我们就要修改保存按钮中的代码

具体代码

const save = async () => {
// 进行表单校验
if (!deptFormRef.value) { return }
deptFormRef.value.validate(async(valid) => { // valid表示是否通过校验
if (valid) {
// 发送请求,保存数据-调用接口,并且将部门数据传入给api接口,然后api在将数据使用post传入后端
// await 是 await 是一个关键字,用来等待一个 Promise 对象。当发送请求的时候,await 会等待接口返回数据,然后返回数据
// 否则会直接执行完毕用户看不到交互结果

let result;// const不允许修改
// 添加保存与修改的校验,如果id存在,那么就是修改,否则就是新增
if (dept.value.id){ // 修改
result= await updateApi(dept.value);

}else{ // 新增
result = await addApi(dept.value);

}
// 做一个判断,如果返回的数据是成功,那么就提示用户,并且将表单清空
if (result.code) {
ElMessage.success("添加成功");

// 关闭对话框
dialogFormVisible.value = false;

// 查询最新数据
search();
} else {
ElMessage.error(result.msg);
}
}else {
ElMessage.error("表单校验未通过");
}
});
};

对比 image-20250529171435405

就是根据这个id进行判断,然后调用不同的api

学到了

六、部门管理-删除部门

1. 页面原型

image-20250529172523041

2. 具体思路代码

删除时我们前端是要给出信息提示是否真的删除,当点击确定时调用api中的删除api,向后端发送删除信息即可(我的前端后端还感觉好混乱,我写前端还在想是咋在前端写的删除功能😵)

// 删除操作
const delById = async (id)=>{
// 弹框提示用户是否删除
const result = await ElMessageBox.confirm('此操作将永久删除该部门, 是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}).then(async() => { // 确认
// 删除
const result = await deleteApi(id);
// 如果删除成功,就提示用户删除成功,并且将dept
if(result.code){ //如果删除成功给出信息,并且刷新页面
ElMessage.success('删除成功');
search();
}else{//删除失败给出信息
ElMessage.error(result.msg);
}

}).catch(() => { // 取消
ElMessage.info('已取消删除');
});
}

给删除按钮绑定点击事件,要通过scope获取这一行的id。

然后就是一些信息提示代码。仔细查看就会明白了,这里不再赘述

七、员工管理-员工查询

1.页面原型

image-20250601082308695

2. 接口文档

请求参数

image-20250601082731925

响应数据

image-20250601082806444

3. 搜索栏制作

1. 基础页面设置

遇到的错误: 性别不显示

image-20250601171950560

当我第一次测试代码时我发现我的性别框无法显示

奶奶的,找了好长时间为啥,然后发现把官网下面的CSS代码拷过来就解决了

烤嫩羊!

注意时间要规定格式 value-format=“YYYY-MM-DD”

整体流程就是抄代码

<script setup>

import { ref } from 'vue';

// 搜索表单对象-根据姓名,年龄,日期查询
const searchEmp = ref({ name: '', gender: undefined, date: [] })

// 查询员工列表的函数
const search = () =>{

}
// 清空函数
const clear = () =>{
// 将表单值置为空,然后再重新查询一次
searchEmp.value={name: '', gender: undefined, date: []};
search();
}
</script>

<template>
<h1>员工管理</h1>

<!-- 制作搜索表单 -->
<div class="container">
<!-- 上栏搜索框 -->
<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="结束日期" :size="size" value-format="YYYY-MM-DD"/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="search">查询</el-button>
<el-button type="info" @click="clear">清空</el-button>
</el-form-item>
</el-form>
</div>
</template>

<style scoped>
.demo-form-inline .el-input {
--el-input-width: 180px;
}

.demo-form-inline .el-select {
--el-select-width: 180px;
}
.container {
margin: 15px 0px;
}
</style>

2. 优化data数据块(事件监听)

上一模块中,我们完成了页面的基本功能。不过还有一个功能需要解决,就是我们接口文档中的开始时间结束时间为begin和end,而我们的对象中定义的为数组。我们该怎么把data中的数组中的值给变成begin和end

方案一:当点击查询按钮时,获取数组中的值,将第一个赋值给begin,第二个赋值给end

方案二:在对象中增加两个属性(begin,end)。如果data数组中的数值发生变化(初始为null)就将第一个值给begin,第二个给end

知识点:Watch监听

作用:侦听一个或多个响应式数据源,并在数据源变化时调用传入的回调函数

用法:

  • 导入watch函数
  • 执行watch函数,传入要侦听的响应式数据源(ref对象)和回调函数;

image-20250601181541922

第一个参数为数据源,第二个参数就是回调函数,回调函数可以传递两个值

第一个参数newVal代表响应式数据变化前的数据,第二个oldVal代表变化之后的值

那么这样我们就可以通过watch监听,来监听响应式数据a的变化。一旦a发生变化就会调用后面的函数

  • 2 监听所有属性变化

当我们要监听对象的全部属性时可以使用深度监听在方法末尾添加,{deep:true}

表示对象的任意属性变化都会执行后面的函数

image-20250601201926124

  • 3 监听单个对象

image-20250601203231403

使用watch监听函数监听日期
// -------------watch监听----------

// 监听data属性
watch(()=>searchEmp.value.date,(newVal,oldVal)=>{
if(newVal.length == 2){
searchEmp.value.begin=newVal[0];
searchEmp.value.end=newVal[1];
}else{
searchEmp.value.begin='';
searchEmp.value.end='';
}
})

监听单个对象:监听serchEmp的值里面的数据,当里面数据发生变化时newVal就会被调用

那么当日期发生变化时我们的newVal就会变化。那么就将newVal里面的第一个值给begin,第二个给newVal

image-20250601204910251

这样我们的开始结束就获取到了值

4.表单制作(详细版)

为了让我下次写表单时有个参考,这里我会写的很详细

  1. 第一步:在官网找到表单项下载源码拷贝到代码里,最好包裹一个div里面
<!-- 表格区域 -->
<div class="container">
<el-table :data="tableData" border style="width: 100%">
<el-table-column prop="date" label="姓名" width="180" />
<el-table-column prop="name" label="" width="180" />
<el-table-column prop="address" label="Address" width="180"/>
<el-table-column prop="address" label="Address" />
</el-table>
</div>

初始代码长这样

  1. 第二步:table里面的data里面的tableData代表表格数据来源。那么我们这里就声明一个empList,代表表格的数据来源。那么这里就将data里面的数据改一改名字。然后去声明对象。这里要考虑:因为要传多个值,所以要声明数组,然后去查看接口文档看看具体是哪些的值

image-20250601213320967

根据这个文档我们来定义数组函数,将需要的值作为参数(里面的值在后面要删除,这里只是示例用于测试的数据,后面查询时需要将里面替换为后端传入到数据)

// 员工列表数据
const empList=ref([
{
"id": 0,
"username": "string",
"password": "string",
"name": "string",
"gender": 0,
"job": 0,
"salary": 0,
"image": "string",
"entryDate": "2019-08-24",
"deptId": 0,
"deptName": "string",
"createTime": "2019-08-24T14:15:22.123Z",
"updateTime": "2019-08-24T14:15:22.123Z"
}
])
  1. 然后根据参数我们去完善表格的列元素

    prop是元素名称,label是表格名称。必要时要使用自定义表格。使用align="center"让元素居中显示。

    删除修改按钮使用的是自定义列表

    <!-- 表格区域 -->
    <div class="container">
    <el-table :data="empList" border style="width: 100%">
    <el-table-column prop="name" label="姓名" width="120" align="center"/>
    <el-table-column prop="gender" label="性别" width="120" align="center"/>
    <el-table-column prop="image" label="头像" width="120" align="center"/>
    <el-table-column prop="deptName" label="所属部门" width="120" align="center"/>
    <el-table-column prop="job" label="职位" width="120" align="center"/>
    <el-table-column prop="entryDate" label="入职日期" width="180" align="center"/>
    <el-table-column prop="updateTime" label="最后操作时间" width="200" align="center"/>
    <el-table-column label="操作" align="center">
    <template #default="scope">
    <el-button type="primary" size="small" @click="">
    <el-icon><Edit /></el-icon>编辑</el-button>
    <el-button type="danger" size="small" @click="">
    <el-icon><Delete /></el-icon>删除
    </el-button>
    </template>
    </el-table-column>
    </el-table>
    </div>

    image-20250602111053403

  2. 在表头添加复选框

    <el-table-column type="selection" width="55" align="center" />
  3. 使用自定义列模板优化性别展示框

    <el-table-column  label="性别" width="120" align="center">
    <template #default="scope">
    {{ scope.row.gender ==1?'男':'女'}}
    </template>
    </el-table-column>

    使用scope.row获取一行中的性别,当等于1时显示男,否则显示女

    image-20250602113810005

  4. 使用自定义列模板完善图像处理

    <el-table-column  label="头像" width="120" align="center">
    <template #default="scope">
    <img :src="scope.row.image" height="40px">
    </template>
    </el-table-column>

    使用img标签,然后别忘了添加高度,不然会很大

    image-20250602114453082

  5. 使用自定义模板定义职位

    <el-table-column prop="job" label="职位" width="120" align="center">
    <template #default="scope">
    <span v-if="scope.row.job==1">班主任</span>
    <span v-else-if="scope.row.job==2">班主任</span>
    <span v-else-if="scope.row.job==3">学工主管</span>
    <span v-else-if="scope.row.job==4">教研主管</span>
    <span v-else-if="scope.row.job==5">咨询师</span>
    <span v-else>其他</span>
    </template>
    </el-table-column>

    image-20250602115931820

5. 分页条制作

<!-- 下方分页条 -->
<div class="container">
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:page-sizes="[5,10, 20, 30, 50,75,100]"
:background="background"
layout="total, sizes, prev, pager, next, jumper"
:total="total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
// 分页操作相关数据模型
const currentPage = ref(1) // 页码
const pageSize = ref(10) // 默认每页展示的记录数
const background = ref(true) //背景色(是否)
const total =ref(0) // 总记录数-后端传入的
// 每页展示记录数发生变化时会触发
const handleSizeChange = (val) => {
console.log(`每页展示${val}条记录 `)
}
// 页码发生变化时会触发
const handleCurrentChange = (val) => {
console.log(`当前页码: ${val}`)
}

这样我们的页面布局就完成了

6. 列表查询

1. 查询功能页面分析

我们要实现查询就要先分析有哪些功能需要查询

首先进入页面需要查询,根据条件进行查询,清空时需要查询,每页记录数时需要查询页码切换时需要查询

2. 功能书写

  1. 我们要写查询功能时是不是要向后端传输请求。那么我们就要定义api了是不是。那么打开api文件夹新建emp.js的api,在里面来写查询相关请求。根据请求参数模板来定义请求方式

    image-20250602133207916

    // 导入封装好的axios实例(包含基础配置和响应拦截器)
    import request from "@/utils/request";


    // 查询员工列表数据
    // 定义分页查询-那么我们要传递条件以及分页参数
    // 请求示例/emps?name=张&gender=1&begin=2007-09-e1&end=2022-09-e1&page=1&pageSize=10
    export const queryPageApi =(name,gender,begin,end,page,pageSize)=>
    request.get(`/emps?name=${name}&gender=${gender}&begin=${begin}&end=${end}&page=${page}&pageSize=${pageSize}`)

    // 新增员工

    // 根据id查询部门信息

    // 修改部门信息

    // 删除部门信息

    定义完请求之后需要在index.vue里面引用

    import{queryPageApi} from '@/api/emp'
  2. 查询员工方法

    引用之后我们就要在声明search方法。在search方法里面声明一个result方法,使用await来修饰,并且将参数声明进去,将表单里面的数据传入请求路径中

    当返回值结果为200时:我们要将返回值结果中的数据传入到表格对象的值里面去。并且我们还需要将分页相关的功能动态传入分页相关内容中去

    最后将声明的empList里面的数据删除掉即可

    // 查询员工列表的函数
    const search = async() => {
    const result =await queryPageApi(searchEmp.value.name,searchEmp.value.gender,
    searchEmp.value.begin,searchEmp.value.end,currentPage.value,pageSize.value);
    if(result.code){
    empList.value=result.data.rows;
    total.value=result.data.total
    }
    }
    // 员工列表数据
    const empList = ref([])

    image-20250602170654354

  3. 声明钩子函数,这样在页面加载完毕时就会自动触发搜索

    // 钩子函数
    onMounted(()=>{
    search();
    })
  4. 在页面与记录数发生变化时查询数据

    // 每页展示记录数发生变化时会触发
    const handleSizeChange = (val) => {
    search();
    }
    // 页码发生变化时会触发
    const handleCurrentChange = (val) => {
    search();
    }

    这样查询功能基本没有问题了

八、 员工管理-新增员工

1. 页面布局

image-20250602173022378

2. 接口文档

image-20250602173133916

请求方式为post,路径为/emps

3. Dialog 对话框

1. 页面基本布局

新增员工的对话框与修改员工的对话框可以共用

这里老师选择Copy准备好的代码,为了锻炼自己我决定自己敲一下

手敲对话框历程

折磨自己干啥,我是大傻逼,我再也不装逼手敲了

我抄我抄我抄

完整代码示例

<script setup>
//新增/修改表单
const employee = ref({
username: '',
name: '',
gender: '',
phone: '',
job: '',
salary: '',
deptId: '',
entryDate: '',
image: '',
exprList: []
})

// 控制弹窗
const dialogVisible = ref(false)
const dialogTitle = ref('新增员工')

//文件上传
// 图片上传成功后触发
const handleAvatarSuccess = (response) => {
console.log(response);
}
// 文件上传之前触发
const beforeAvatarUpload = (rawFile) => {
if (rawFile.type !== 'image/jpeg' && rawFile.type !== 'image/png') {
ElMessage.error('只支持上传图片')
return false
} else if (rawFile.size / 1024 / 1024 > 10) {
ElMessage.error('只能上传10M以内图片')
return false
}
return true
}
</script>

<template>
<!-- ...... 省略 ...... -->

<!-- 新增/修改员工的对话框 -->
<el-dialog v-model="dialogVisible" :title="dialogTitle">
<el-form :model="employee" label-width="80px">
<!-- 基本信息 -->
<!-- 第一行 -->
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="用户名">
<el-input v-model="employee.username" placeholder="请输入员工用户名,2-20个字"></el-input>
</el-form-item>
</el-col>

<el-col :span="12">
<el-form-item label="姓名">
<el-input v-model="employee.name" placeholder="请输入员工姓名,2-10个字"></el-input>
</el-form-item>
</el-col>
</el-row>

<!-- 第二行 -->
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="性别">
<el-select v-model="employee.gender" placeholder="请选择性别" style="width: 100%;">
<el-option label="男" value="1"></el-option>
<el-option label="女" value="2"></el-option>
</el-select>
</el-form-item>
</el-col>

<el-col :span="12">
<el-form-item label="手机号">
<el-input v-model="employee.phone" placeholder="请输入员工手机号"></el-input>
</el-form-item>
</el-col>
</el-row>

<!-- 第三行 -->
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="职位">
<el-select v-model="employee.job" placeholder="请选择职位" style="width: 100%;">
<el-option label="班主任" value="1"></el-option>
<el-option label="讲师" value="2"></el-option>
<el-option label="学工主管" value="3"></el-option>
<el-option label="教研主管" value="4"></el-option>
<el-option label="咨询师" value="5"></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="薪资">
<el-input v-model="employee.salary" placeholder="请输入员工薪资"></el-input>
</el-form-item>
</el-col>
</el-row>

<!-- 第四行 -->
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="所属部门">
<el-select v-model="employee.deptId" placeholder="请选择部门" style="width: 100%;">
<el-option label="研发部" value="1"></el-option>
<el-option label="市场部" value="2"></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="入职日期">
<el-date-picker v-model="employee.entryDate" type="date" style="width: 100%;" placeholder="选择日期" format="YYYY-MM-DD" value-format="YYYY-MM-DD"></el-date-picker>
</el-form-item>
</el-col>
</el-row>

<!-- 第五行 -->
<el-row :gutter="20">
<el-col :span="24">
<el-form-item label="头像">
<el-upload
class="avatar-uploader"
action="/api/upload"
:show-file-list="false"
:on-success="handleAvatarSuccess"
:before-upload="beforeAvatarUpload"
>
<img v-if="employee.image" :src="employee.image" class="avatar" />
<el-icon v-else class="avatar-uploader-icon"><Plus /></el-icon>
</el-upload>
</el-form-item>
</el-col>
</el-row>


<!-- 工作经历 -->
<!-- 第六行 -->
<el-row :gutter="10">
<el-col :span="24">
<el-form-item label="工作经历">
<el-button type="success" size="small" @click="">+ 添加工作经历</el-button>
</el-form-item>
</el-col>
</el-row>

<!-- 第七行 ... 工作经历 -->
<el-row :gutter="3">
<el-col :span="10">
<el-form-item size="small" label="时间" label-width="80px">
<el-date-picker type="daterange" range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期" format="YYYY-MM-DD" value-format="YYYY-MM-DD" ></el-date-picker>
</el-form-item>
</el-col>

<el-col :span="6">
<el-form-item size="small" label="公司" label-width="60px">
<el-input placeholder="请输入公司名称"></el-input>
</el-form-item>
</el-col>

<el-col :span="6">
<el-form-item size="small" label="职位" label-width="60px">
<el-input placeholder="请输入职位"></el-input>
</el-form-item>
</el-col>

<el-col :span="2">
<el-form-item size="small" label-width="0px">
<el-button type="danger" >- 删除</el-button>
</el-form-item>
</el-col>
</el-row>
</el-form>

<!-- 底部按钮 -->
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="">保存</el-button>
</span>
</template>
</el-dialog>

</template>

<style scoped>
.avatar {
height: 40px;
}
.avatar-uploader .avatar {
width: 78px;
height: 78px;
display: block;
}
.avatar-uploader .el-upload {
border: 1px dashed var(--el-border-color);
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
transition: var(--el-transition-duration-fast);
}

.avatar-uploader .el-upload:hover {
border-color: var(--el-color-primary);
}

.el-icon.avatar-uploader-icon {
font-size: 28px;
color: #8c939d;
width: 78px;
height: 78px;
text-align: center;
border-radius: 10px;
/* 添加灰色的虚线边框 */
border: 1px dashed var(--el-border-color);
}
</style>

这样我们的对话框组件就完成了

2. 显示对话框

完成了我们的表单项目是不是要当点击添加员工时要显示对话框

// 声明添加员工的箭头函数
const addEmp = ()=>{
dialogVisible.value=true;
dialogTitle.value='新增员工';

}
<el-button type="primary" @click="addEmp">+新增员工</el-button>

image-20250603103213889

3.表单解析

一行里面是怎么只显示两列的

<!-- 第一行 -->
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="用户名">
<el-input v-model="employee.username" placeholder="请输入员工用户名,2-20个字"></el-input>
</el-form-item>
</el-col>

<el-col :span="12">
<el-form-item label="姓名">
<el-input v-model="employee.name" placeholder="请输入员工姓名,2-10个字"></el-input>
</el-form-item>
</el-col>

使用el-row布局方式,代表一行,里面有两个el-col代表有两列

el-col :span="12"代表一行所占的宽度。总计为24块。这里的两个代表是一行2个

gutter="20"代表分栏间隔

4.页面布局优化(性别,职位,所属部门)

性别,职位,所属部门这些都不应该是页面写死的,性别与职位可以单独定义出来,所属部门由后端传入。

性别与职位是规定死的,而所属部门是动态的可以通过添加部门动态添加),因此部门相关要在后端处理

1. 性别与职位

index.vue中定义性别与职位的元数据

// --------元数据相关----------
//职位列表数据
const jobs = ref([{ name: '班主任', value: 1 },{ name: '讲师', value: 2 },{ name: '学工主管', value: 3 },{ name: '教研主管', value: 4 },{ name: '咨询师', value: 5 },{ name: '其他', value: 6 }])
//性别列表数据
const genders = ref([{ name: '男', value: 1 }, { name: '女', value: 2 }])
// --------元数据结束--------

image-20250603114516663

然后修改之前的定义,改为使用v-for进行动态查找。这样当我们需要添加性别与职位时就可以在表头进行修改了

<!-- 第二行 -->
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="性别">
<el-select v-model="employee.gender" placeholder="请选择性别" style="width: 100%;">
<el-option v-for="g in genders" :key="g.value" :label="g.name" :value="g.value"></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="职位">
<el-select v-model="employee.job" placeholder="请选择职位" style="width: 100%;">
<el-option v-for="j in jobs" :key="j.value" :label="j.name" :value="j.value"></el-option>
</el-select>
</el-form-item>
</el-col>

使用v-for来遍历我们上面的对象

v-for="j in jobs": j就是变量名称 in jobs就是对象名称

:key="j.value" 注意这里要加:号,进行动态绑定,将jobs的value作为唯一标识

:label="j.name" 动态绑定具体是那个名字 。对应<el-option label=“研发部” value=“1”>

:value="j.value" 动态绑定他们的值。 对应<el-option label=“研发部” value=“1”>

image-20250603122112852

正常回显,没有问题啦🐻‍❄️🐻‍❄️

2. 所属部门动态优化

部门与部门管理的部门内容会向关联,那么什么时候需要展示呢? 当我们点击编辑和新增时都需要展示部门,因此可以在页面加载时就加载部门。那么就在钩子函数中定义查询部门数据

  1. 查询所有部门数据首先要声明一个部门数组,便于我们后面的引用

    // 查询所有部门功能
    const depts =ref([]);
  2. 既然要查询所有部门是不是要导入查询部门的API。这里的名称会与员工查询向混淆,所以起了别名

    import { queryAllApi as queryAllDeptApi } from '@/api/dept'

  3. 导入进来了我们肯定要用啊是不是,那么就声明一个查询所有部门的函数方法。调用api查询出结果,然后将结果给我们上面声明函数

    // 查询所有部门数据
    const queryAllDepts = async() =>{
    const result=await queryAllDeptApi();
    if(result.code){
    depts.value=result.data;
    }
    }
  4. 然后就要在钩子函数中引用

    // 钩子函数
    onMounted(() => {
    // 查询员工列表数据
    search();

    // 查询所有部门数据
    queryAllDepts();
    })
  5. 最后使用for循环遍历

    <el-select v-model="employee.deptId" placeholder="请选择部门" style="width: 100%;">
    <el-option v-for="d in depts" :key="d.id" :label="d.name" :value="d.id"></el-option>
    </el-select>

    这里的id和name参照接口文档

    image-20250603130515355

  6. 最终页面显示

    image-20250603130538985

5. 图片上传相关

<el-row :gutter="20">
<el-col :span="24">
<el-form-item label="头像">
<el-upload class="avatar-uploader" action="/api/upload" :show-file-list="false"
:on-success="handleAvatarSuccess" :before-upload="beforeAvatarUpload">
<img v-if="employee.image" :src="employee.image" class="avatar" />
<el-icon v-else class="avatar-uploader-icon">
<Plus />
</el-icon>
</el-upload>
</el-form-item>
</el-col>
</el-row>

class="avatar-uploader"给一个class属性,方便我们属性css的相关

action="/api/upload" 我们在上传文件时要调用的那个接口,请求地址

:show-file-list="false"是否展示已经上传的文件列表

:on-success="handleAvatarSuccess" 文件上传成功调用的函数方法

:before-upload="beforeAvatarUpload"文件上传之前要调用的函数(检查格式与大小是否符合要求)

//文件上传
// 图片上传成功后触发
const handleAvatarSuccess = (response) => {
console.log(response);
}
// 文件上传之前触发
const beforeAvatarUpload = (rawFile) => {
if (rawFile.type !== 'image/jpeg' && rawFile.type !== 'image/png') {
ElMessage.error('只支持上传图片')
return false
} else if (rawFile.size / 1024 / 1024 > 10) {
ElMessage.error('只能上传10M以内图片')
return false
}
return true
}

校验了文件类型是不是图片,不是就返回错误信息。然后查看文件大小是否小于10M,然后阻止上传

头像回显逻辑
<img v-if="employee.image" :src="employee.image" class="avatar" />
<el-icon v-else class="avatar-uploader-icon">

这里先做一个判断v-if="employee.image"里面有没有值呢?有就展示图片,没有就展示一个+号的图标

那么我们该如何回显图片呢?我们只需要将我们上传图片的URL传给image属性值就可以了

那么我们就在文件上传成功时把URL给image属性即可

// 图片上传成功后触发
const handleAvatarSuccess = (response) => {
employee.value.image=response.data;
}

这里的response.data;为啥就是图像路径呢,可能与系统设计的有关。返回的就是一个URL

image-20250603203300769

6. 工作经历的页面交互

1. 页面原型

image-20250603205520481

当点击新增按钮时会往exprList[]数组中新增一条数据。

当点击删除按钮时会在exprList[]数组中删除一条数据

vue是基于数据驱动视图展示的

2. 实现思路

  1. 既然我们要点击添加工作经历就新增一个表格那么我们是不是需要绑定一个点击事件

    <el-row :gutter="10">
    <el-col :span="24">
    <el-form-item label="工作经历">
    <el-button type="success" size="small" @click="addExprItem">+ 添加工作经历</el-button>
    </el-form-item>
    </el-col>
    </el-row>
  2. 绑定完点击事件然后就要去声明

    // -------------添加工作经历相关-----------
    const addExprItem =()=>{
    // 为数组添加元素数据
    employee.value.exprList.push({
    company:'',job:'',begin:'',end:'',exprDate:[]
    });


    }

    声明一个点击事件,点击新增按钮是就调用employee对象里面的值中的exprList然后使用push函数就可以新增数据了。

    这里面要放公式,职位,开始时间,结束时间,以及一个时间数组

    欸?这时候就要问了,这个时间数组是干嘛的??

    我们页面中的开始时间和结束时间使用的是一个范围日期选择器。所以这个返回时间会存放进那个时间数组。然后就要使用我们的watch监听事件去监听数组变化,在将数组中的值传入开始时间结束时间里面去

    image-20250603211647019

    哇可以了耶。

  3. 页面新增添加工作经历表格

    既然我们已经成功可以添加员工工作经历了,那么我们该如何让页面回显呢?

    嘿嘿我们使用一个for循环不就好啦。循环遍历这个数组元素然后回显到页面上

    <!-- 第七行 ...  工作经历 -->
    <el-row :gutter="3" v-for="expr in employee.exprList">
    <el-col :span="10">
    <el-form-item size="small" label="时间" label-width="80px">
    <el-date-picker type="daterange" v-model="expr.exprDate" range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期"
    format="YYYY-MM-DD" value-format="YYYY-MM-DD"></el-date-picker>
    </el-form-item>
    </el-col>

    <el-col :span="6">
    <el-form-item size="small" label="公司" label-width="60px">
    <el-input placeholder="请输入公司名称" v-model="expr.company"></el-input>
    </el-form-item>
    </el-col>

    <el-col :span="6">
    <el-form-item size="small" label="职位" label-width="60px">
    <el-input placeholder="请输入职位" v-model="expr.job"></el-input>
    </el-form-item>
    </el-col>

    <el-col :span="2">
    <el-form-item size="small" label-width="0px">
    <el-button type="danger">- 删除</el-button>
    </el-form-item>
    </el-col>
    </el-row>
    </el-form>

    image-20250603213140053

    在工作经历上添加for循环,循环遍历数组。使用v-model来绑定数据

  4. 删除功能

    当我们点击某一个按钮时就要删除某一条工作经历

    所以我们要先往删除按钮上绑定点击事件

    <el-form-item size="small" label-width="0px">
    <el-button type="danger" @click="delExprItem">- 删除</el-button>
    </el-form-item>

    那我们怎么样才能指定某一条进行删除呢?

    我们可以在调用这个函数时将当前的索引传递过去。那我们这么样才能获取到这个数据呢

    我们可以根据v-for来获取当前索引,当我们遍历时拿到这个索引,在删除时将索引传递给删除按钮

    <!-- 第七行 ...  工作经历 -->
    <el-row :gutter="3" v-for="(expr,index) in employee.exprList">
    <el-col :span="10">
    <el-form-item size="small" label="时间" label-width="80px">
    <el-date-picker type="daterange" v-model="expr.exprDate" range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期"
    format="YYYY-MM-DD" value-format="YYYY-MM-DD"></el-date-picker>
    </el-form-item>
    </el-col>

    <el-col :span="6">
    <el-form-item size="small" label="公司" label-width="60px">
    <el-input placeholder="请输入公司名称" v-model="expr.company"></el-input>
    </el-form-item>
    </el-col>

    <el-col :span="6">
    <el-form-item size="small" label="职位" label-width="60px">
    <el-input placeholder="请输入职位" v-model="expr.job"></el-input>
    </el-form-item>
    </el-col>

    <el-col :span="2">
    <el-form-item size="small" label-width="0px">
    <el-button type="danger" @click="delExprItem(index)">- 删除</el-button>
    </el-form-item>
    </el-col>
    </el-row>
    </el-form>

    <!-- 底部按钮 -->
    <template #footer>
    <span class="dialog-footer">
    <el-button @click="dialogVisible = false">取消</el-button>
    <el-button type="primary" @click="">保存</el-button>
    </span>
    </template>
    </el-dialog>

    重点看绑定的index

    然后我们声明函数,根据索引进行删除

    // 删除工作经历
    const delExprItem = (index) => {
    employee.value.exprList.splice(index,1);
    }

    然后就能实现功能了,前端太麻烦了😫😫😫

  5. 使用Watch监听绑定开始与结束时间

    // 监听employee员工对象中的工作经历信息将值传给开始与结束时间
    watch(() =>employee.value.exprList,(newVal,oldVal) => {
    if(employee.value.exprList && employee.value.exprList.length>0){
    employee.value.exprList.forEach((expr) =>{
    expr.begin=expr.exprDate[0];
    expr.end=expr.exprDate[1];
    })
    }
    },{deep:true}) // 深度监听

    这里使用watch监听函数,首先要指明监听的是哪个对象,我们要监听employee的员工数组对象,然后声明数组变化时的值。

    当我们的数组中有值,并且值大于1时我们需要监听数组

    使用forEach循环遍历数组,定义变量名称,然后将数组里面的第一个值给开始时间,第二个给结束时间

    这里最关键的是要使用深度监听,如果直接监听数组只有数组的引用发生变化才会生效,这里当数组里面内容发生变化都需要监听,因此要使用深度监听

7. 保存员工

1. 页面分析

  1. 当点击提交按钮时向后端发送异步请求,将参数传入后端来保存数据
  2. 保存成功要给出提示信息。然后关闭当前对话框
  3. 重新查询列表数据
  4. 保存失败给出对应的错误信息

2. 功能开发

  1. 首先在api/js文件中说明添加操作的请求,然后在index中引用

    export const addApi = (emp) => request.post('/emps', emp);
  2. 在保存按钮上添加点击事件

    <span class="dialog-footer">
    <el-button @click="dialogVisible = false">取消</el-button>
    <el-button type="primary" @click="save">保存</el-button>
    </span>
  3. 然后去声明这个按钮。调用api当返回值为200说明插入成功了,然后发送信息关闭对话框重新搜索

    //-------保存员工相关--------
    const save= async() =>{
    const result = await addApi(employee.value);
    if(result.code){
    ElMessage.success("保存成功");
    dialogVisible.value=false;
    search();
    }else{
    ElMessage.error(result.msg);
    }
    }
  4. 插入成功

    image-20250604090222231

  5. 哎 这时候又有一个bug,当我们已经新增完毕用户了,但是用户的信息还在表单里面

    image-20250604090421609

  6. 解决

    当点击新增员工时将之前的数据全部置为空

    // 声明添加员工的箭头函数
    const addEmp = () => {
    dialogVisible.value = true;
    dialogTitle.value = '新增员工';
    employee.value={
    username: '',
    name: '',
    gender: '',
    phone: '',
    job: '',
    salary: '',
    deptId: '',
    entryDate: '',
    image: '',
    exprList: []
    }

    }
  7. 添加表单验证规则

    image-20250604093605566

    • 首先在el-form表单项上添加:rules="reles"的表单绑定项,然后在具体的el-form-item使用prop绑定名称

      示例:

      <el-form :model="employee" label-width="80px" :rules="rules" >
      <!-- 基本信息 -->
      <!-- 第一行 -->
      <el-row :gutter="20">
      <el-col :span="12">
      <el-form-item label="用户名" prop="username">
      <el-input v-model="employee.username" placeholder="请输入员工用户名,2-20个字" ></el-input>
      </el-form-item>
      </el-col>
    • 然后去声明这个对象在里面添加校验规则

      //-------表单校验相关-------
      const rules = ref({
      username: [
      { required: true, message: '请输入用户名', trigger: 'blur' },
      { min: 2, max: 20, message: '用户名长度应在2到20个字符之间', trigger: 'blur' }
      ],
      name: [
      { required: true, message: '请输入姓名', trigger: 'blur' },
      { min: 2, max: 10, message: '姓名长度应在2到10个字符之间', trigger: 'blur' }
      ],
      gender: [
      { required: true, message: '请选择性别', trigger: 'change' }
      ],
      phone: [
      { required: true, message: '请输入手机号', trigger: 'blur' },
      { pattern: /^1[3-9]\d{9}$/, message: '请输入有效的手机号', trigger: 'blur' }
      ]
      });
    • 这时候会发现我们表单校验虽然校验了但是没有实现,因此还需要再提交表单时进行校验

      声明一个表单引用对象

      // 表单验证的响应式对象
      const empFromRef =ref();

      然后再el-form进行引用

      <el-form :model="employee" label-width="80px" :rules="rules" ref="empFromRef">

      这样我们就可以通过这个响应式对象来操作表单对象了

    • 重新修改保存员工的代码,添加表单校验信息,校验通过才可以执行添加代码

      //-------保存员工相关--------
      const save= async() =>{
      // 表单校验-只有通过了才可以执行下面的代码
      if(!empFromRef.value) return;
      empFromRef.value.validate(async (valid) =>{ // valid标识是否通过:true通过/false不通过
      if(valid){ // 通过
      const result = await addApi(employee.value);
      if(result.code){
      ElMessage.success("保存成功");
      dialogVisible.value=false;
      search();
      }else{
      ElMessage.error(result.msg);
      }
      }else{ //不通过
      ElMessage.error('表单校验不通过');
      }
      })
      }
    • 最后一步:当重新添加时需要清空表单校验规则

      修改添加员工的相关代码,添加清空表单校验规则代码

      // 声明添加员工的箭头函数
      const addEmp = () => {
      dialogVisible.value = true;
      dialogTitle.value = '新增员工';
      employee.value={
      username: '',
      name: '',
      gender: '',
      phone: '',
      job: '',
      salary: '',
      deptId: '',
      entryDate: '',
      image: '',
      exprList: []
      }

      // 重置表单校验规则
      if(empFromRef.value){
      empFromRef.value.resetFields();
      }
      }

九、修改员工

1. 页面原型

image-20250604102946823

2. 查询回显功能

为编辑按钮绑定点击事件

发送异步请求,根据id查询员工的详细信息,页面进行回显

  1. api/js里面添加异步请求的相关数据,然后在index里面引用

    // 根据id查询部门信息
    export const queryInfoApi = (id) => request.get(`/emps/${id}`);
  2. 为编辑按钮绑定点击事件

    <el-button type="primary" size="small" @click="edit(scope.row.id)">
    <el-icon>
    <Edit />
    </el-icon>编辑</el-button>

    然后使用scope.row.id获取这一行的id,并且作为参数传入点击事件

  3. 声明这个点击函数

    // ------根据id查询员工信息相关-----
    const edit =async(id) =>{
    const result = await queryInfoApi(id);
    if(result.code){
    dialogVisible.value=true;
    dialogTitle.value='修改员工';
    employee.value=result.data;
    }
    }

    当我们点击按钮是将id作为参数,而后调用查询api,将结果封装在result结果里面。当返回值为200就打开对话框,将标题改为修改员工,然后将结果集中的数据作为参数进行回显

  4. 回显失败错误

    问题出现背景

    当我在页面点击编辑按钮时不会弹出对话框,反而报了错误 index.vue:161 Uncaught (in promise) TypeError: Cannot read properties of undefined (reading ‘0’) at index.vue:161:29 at watch.deep (index.vue:160:28)

    但是前端可以正确发送请求,我的后端控制台也有正确的数据返回

    问题解析
    1. 当点击“编辑”按钮时,会调用 edit 函数。

    2. edit 函数从后端API (queryInfoApi) 获取员工数据,并将其赋值给 employee.value。

    3. 后端返回的员工数据中,工作经历 (exprList) 里的每个条目包含 begin 和 end 日期,但不包含前端日期选择器所绑定的 exprDate 数组。

    4. 将后端数据直接赋值给 employee.value 后,exprList 中的条目没有 exprDate 属性。

    5. 此时,侦听 employee.value.exprList 的 watch 回调被触发。

    6. 在这个 watch 回调中,代码尝试通过 expr.exprDate[0] 和 expr.exprDate[1] 来更新 expr.begin 和 expr.end。但由于此时 expr.exprDate 是 undefined,访问 expr.exprDate[0] 就会导致报错,对话框也因此无法弹出。

    问题修改
    1. 修改 edit 函数:在从后端获取到员工数据后,但在将其赋值给 employee.value 之前,我们需要遍历 exprList (如果存在),并根据每个工作经历条目的 begin 和 end 属性来初始化其对应的 exprDate 数组。同时,要确保 exprList 本身如果从后端返回为 null 或 undefined,会被正确处理为一个空数组。

    2. 修改 exprList 的侦听器:使其更健壮,能处理 expr.exprDate 可能不是一个包含两个元素的数组的情况(例如,当用户清空日期选择器或新添加一条工作经历时,exprDate 可能是空数组 [])。

  5. 解决完错误后的代码

    // ------根据id查询员工信息相关-----
    const edit =async(id) =>{
    const result = await queryInfoApi(id);
    if(result.code){
    dialogVisible.value=true;
    dialogTitle.value='修改员工';
    employee.value=result.data;

    // 对工作经历进行处理
    let exprList= employee.value.exprList;
    if(exprList && exprList.length>0){
    exprList.forEach((expr) =>{
    expr.exprDate=[expr.begin,expr.end];
    })
    }
    }
    }

    奶奶的,找了半个小时的错误结果老师后面又讲了。

    这里对工作经历进行处理,获取到工作经历的值,然后进行遍历,将开始时间结束时间封装在exprDate里面去,这个时间段数据就可以在页面回显出来了

  6. 页面显示

    image-20250604113134418

3. 点击保存修改员工数据

我们的新增员工与修改员工复用的是同一个对话框,那么当我们新增员工时或修改员工时怎么才能区分不同的操作呢?

当有员工id时就为修改,没有就新增

  1. api中定义更新员工信息的异步请求

    //  修改部门信息
    export const updateApi = (emp) => request.put('/emps', emp);

    当将我们整个emp对象都要传入后端进行修改,然后在index里面引用

  2. 代码逻辑

    //-------保存员工相关--------
    const save= async() =>{
    // 表单校验-只有通过了才可以执行下面的代码
    if(!empFromRef.value) return;
    empFromRef.value.validate(async (valid) =>{ // valid标识是否通过:true通过/false不通过
    if(valid){ // 通过

    let result;
    if(employee.value.id){
    result = await updateApi(employee.value);
    }else{
    result = await addApi(employee.value);
    }
    if(result.code){
    ElMessage.success("保存成功");
    dialogVisible.value=false;
    search();
    }else{
    ElMessage.error(result.msg);
    }
    }else{ //不通过
    ElMessage.error('表单校验不通过');
    }
    })
    }

    当我们点击按钮时会去判断有没有id,如果有就执行更新,没有就执行添加。同时需要将result由变量变为常量

十、删除功能

1. 删除单个员工

1. 页面原型

image-20250604122139036

2. api设计

// 删除部门信息
export const deleteApi = (ids) => request.delete(`/emps?ids=${ids}`);

3. 为删除按钮绑定点击事件

// ------删除员工相关-----
const deleteById = async(id) =>{
ElMessageBox.confirm('此操作将永久删除该员工, 是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}).then(async() => {
// 删除
const result = await deleteApi(id);
if (result.code) {
ElMessage.success('删除成功');
search();
} else{
ElMessage.error(result.msg);
}
})
}

2. 批量删除

1. 页面分析

image-20250604122139036.png

  • 为表格的复选框绑定事件,点击复选框之后,获取到目前选中的条件的id(多个id可以封装到数组中)。
  • 为“批量删除”按钮绑定事件,发送异步请求到服务端,根据id批量删除员工信息。

2. 代码设计

  1. 参照官方文档需要在form 表单上绑定
<el-table :data="empList" border style="width: 100%" @selection-change="handleSelectionChange">

这个函数代表当复选框发生变化时就触发

  1. 然后去声明这个函数

    // 批量删除员工
    const handleSelectionChange = (selection) => {// selection代表当前选中的员工数据记录
    selectedIds.value = selection.map(item => item.id)
    }
  2. 为批量删除按钮绑定事件

    // 记录勾选的员工id
    const selectedIds = ref([]);

    // 批量删除员工
    const handleSelectionChange = (selection) => {// selection代表当前选中的员工数据记录
    selectedIds.value = selection.map(item => item.id)
    }

    // 批量删除
    const deleteByIds = () =>{
    ElMessageBox.confirm('此操作将永久删除该员工, 是否继续?', '提示', {
    confirmButtonText: '确定',
    cancelButtonText: '取消',
    type: 'warning',
    }).then(async() => {

    if(selectedIds.value && selectedIds.value.length>0){
    // 删除
    const result = await deleteApi(selectedIds.value);
    if (result.code) {
    ElMessage.success('删除成功');
    search();
    } else{
    ElMessage.error(result.msg);
    }
    }else {
    ElMessage.error("请选择要删除的员工");
    }

    }).catch(() => {
    ElMessage.info('已取消删除');
    });
    }

十一、 员工登录

1. 页面原型

image-20250604133945257

2. 准备工作

api/目录下新建login.js里面存放与登录有关的请求

// 登录相关
// 导入封装好的axios实例(包含基础配置和响应拦截器)
import request from "@/utils/request";

// 登录接口
export const loginApi = (data) =>request.post("/login",data);

并且在index中引用

<script setup>
import { ref } from 'vue'
import { loginApi } from '@/api/login'

let loginForm = ref({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>

3. 初始登录功能

当我们点击登录按钮时需要进行登录,点击重置按钮可以重置,因此首先要为他们绑定点击事件

<script setup>
import { ref } from 'vue'
import { loginApi } from '@/api/login'
import { ElMessage } from 'element-plus';
import { useRouter } from 'vue-router'

let loginForm = ref({username:'', password:''})
const router = useRouter();
// 登录
const login = async() =>{
const result = await loginApi(loginForm.value);
if(result.code){
// 1. 提示登录成功
ElMessage.success("登录成功");
// 2. 跳转到首页
router.push('/index');

}else{
ElMessage.error(result.msg)
}

}
// 重置
const clear = () =>{
loginForm.value.username = ''
loginForm.value.password = ''
}
</script>

这里要使用useRouter进行跳转

4.存储登录token

1. localStorage

image-20250604140517700

2. 将用户返回到token存储在浏览器中

localStorage.setItem('loginUser',JSON.stringify(result.data));

JSON.stringify(result.data)因为我们的result.data是一个对象,要转为字符串的形式,因此要使用JSON.stringify

5. 用户携带令牌登录

1. 功能引入

现在我们打开久违的idea,开启拦截器的相关功能这时登录会发现

image-20250604141909896

咦我明明登了呀 怎么会没有数据。后端就显示没有令牌了。

也就是说我们在后序的所有请求中没有将token令牌到服务器端

2. axios的拦截器

image-20250604144935575

使用axios的拦截器拦截请求,并在请求中添加token

request.js中添加如下代码

// 请求拦截器-获取localStorage中的token数据,在请求头中增加token
request.interceptors.request.use(
(config) => { // 请求成功时的回调函数
// 获取localStorage中的token数据
const loginUser= JSON.parse(localStorage.getItem('loginUser'));
// 判断token是否存在,如果存在则将其添加到请求头中
if (loginUser&&loginUser.token) {
config.headers.token=loginUser.token;
}
return config;
},
(error) => { // 请求失败时的回调函数
}
)

注意这里的loginUser 要与index里面的存入token的键保持一致

3. 解决文件上传问题

在上一节代码中我们解决了token问题,不过我们的文件上传功能却不能实现了,原因是文件上传组件不是发送的异步请求,而是我们自定义的vue进行发送的。因此不能携带令牌也就不能进行文件上传了

怎么说不好说

参照官方文档我们需要在el-update标签里面声明一个:headers="{'token':token}"用于存放的token。这里的token不能写死因此用了动态绑定

写了token我们就要声明一个token函数

// 声明token的响应式数据
const token = ref('');

那我们是什么时候加载这个请求呢?要在页面加载时就要携带请求头,因此要在钩子函数中声明

要在钩子函数中声明就要先来获取token

// 获取token
const getToken = async() =>{
const loginUser=JSON.parse(localStorage.getItem('loginUser'))
if(loginUser && loginUser.token){
token.value = loginUser.token;
}
}

因为localStorage里面放的是字符串,我们要将其转化为对象

获取成功后将里面的token赋值给token里面去,这时候就会动态绑定到upload里面去。最后在钩子函数中应用

// 钩子函数
onMounted(() => {
// 查询员工列表数据
search();

// 查询所有部门数据
queryAllDepts();

// 获取token
getToken();
})

携带令牌登录功能完成

6.未登录显示401跳转到登录页面

使用axios的拦截器的响应拦截器进行拦截,然后进行判断,如果返回的状态码是401就跳转到登陆页面

image-20250604154440344

request.interceptors.response.use(
(response) => { // 响应成功时的回调函数
// 这里默认返回response中的data字段,通常后端返回的结构为{code: xxx, data: xxx, message: xxx}
// 可根据实际需求进一步处理(如统一错误码判断、数据格式化等)
return response.data;
},
(error) => { // 响应失败时的回调函数
if (error.response.status === 401) { // 全等-类型与值相同
// 提示信息
ElMessage.error('登录超时,请重新登录');
// 跳转到登陆页面
router.push('/login');
}
// 对请求错误进行统一处理(如记录错误日志、提示用户、重试机制等)
// 此处直接返回 rejected 状态的Promise,以便调用方通过.catch()捕获错误
return Promise.reject(error);
}
);

7. 退出登录

1. 页面显示当前用户姓名

我们该如何获取当前登录的用户名呢,当我们登录时员工信息已经封装在了localStorage里面了,只要从里面获取就可以了

<script setup>
import {ref,onMounted} from 'vue'
// 当前登录员工
const loginName = ref('');
// 定义钩子函数
onMounted(()=>{
// 获取当前登录用户名
const loginUser = JSON.parse(localStorage.getItem('loginUser'));
if(loginUser&&loginUser.name){
loginName.value = loginUser.name;
}

})

</script>

从本地中获取用户名

2. 退出登录

<a href="javascript:;" @click="logout">
<el-icon>
<SwitchButton />
</el-icon> 退出登录【{{loginName}}】
</a>

退出登录需要绑定一个退出按钮,同时需要将超链接变为死链接


// 定义路由实例
const router = useRouter();
// 退出登录
const logout = () =>{
// 弹框提示用户是否删除
ElMessageBox.confirm('您确定要退出登录吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}).then(async() => { // 确认
ElMessage.success('已退出登录');
localStorage.removeItem('loginUser');
router.push('/login');

}).catch(() => { // 取消
ElMessage.info('已取消退出');
});

}

退出时给出提示信息,然后删除本地用户信息,最后进行跳转

十一、项目打包

  1. 在vscode中运行build脚本命令,这样会生成dish文件夹代表打包成功

  2. 将项目部署在Nginx