前端Web案例-登录和退出
前端Web案例-登录和退出
1. 修改员工
修改员工

对于修改功能,分为两步实现:
- 点击 “编辑” 根据ID查询员工的信息,回显展示。
- 点击 “保存” 按钮,修改员工的信息 。
代码操作
回显展示
1). 为 "编辑" 按钮绑定事件
<el-button type="primary" size="small" @click="updateEmp(scope.row.id);>编辑</el-button>
2). 在 <script> </script>
中定义 updateEmp
函数
// 定义一个对话框标题的变量
const formTitle = ref<string>('')
//修改员工-回显
const updateEmp = async (id:number) => {
clearEmp()
dialogFormVisible.value = true
formTitle.value = '修改员工'
let result = await queryInfoApi(id)
if(result.code){
emp.value = result.data
//处理工作经历中的时间范围
let exprList = emp.value.exprList;
if(exprList && exprList.length > 0){
exprList.forEach(expr => {
expr.exprDate = [expr.begin, expr.end]
})
}
}
}
打开浏览器,点击 编辑 按钮,测试数据回显:

修改员工
完成了数据回显展示之后,接下来,我们就来完成保存修改操作。 由于修改员工与新增员工共用一个 Dialog
对话框。 点击保存按钮时,我们只需要根据 id 来判别是新增员工,还是修改员工。
那我们就需要对保存员工的函数,进行完善优化。 最终代码如下:
//-------------保存员工信息
const save = async () => {
let result = null;
//给员工经历中的起止时间 赋值
if (emp.value.exprList.length > 0) {
emp.value.exprList.forEach(expr => {
expr.begin = expr.exprDate[0]
expr.end = expr.exprDate[1]
})
}
if (emp.value.id) { // 存在id,修改
console.log('修改员工数据:', emp.value)
//发起网络请求,保存数据
result = await updateApi(emp.value)
} else {// 不存在id,修改
console.log('新增员工数据:', emp.value)
//发起网络请求,保存数据
result = await addApi(emp.value)
}
if (result.code) {//如果0 等同fasle 如果是非0等同于true
ElMessage({
type: 'success',
message: '操作成功',
})
dialogFormVisible.value = false;
queryEmp()
} else {
ElMessage.error(result.msg)
}
// 无论成功和失败,清空emp数据
clearEmp();
}
打开浏览器,测试修改员工信息操作:


完整代码
到此呢,关于员工管理的基本的增删改查功能,我们已经完成了。 目前为止,src/views/emp/index.vue
的完整代码如下:
点击查看代码
src/views/emp/index.vue
的完整代码
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import axios from 'axios';
import type { EmpModelArray, EmpModel, SearchEmpModel, PaginationParam, EmpExprModel, DeptModel, DeptModelArray } from '@/api/model/model'
import { deleteApi, queryPageApi, addApi,queryInfoApi, updateApi } from '@/api/emp'
import { queryAllApi } from '@/api/dept'
let tableData = ref<EmpModelArray>([])
import { ElMessage, ElMessageBox,type UploadProps} from 'element-plus'
// 请求后台的数据--查询部分
//当页面挂载完毕,开始执行
onMounted(() => {
// axios.get('https://mock.apifox.com/m1/3161925-0-default/depts').then(res=>{
// console.log(res)
// tableData.value=res.data.data
// })
queryEmp()
queryAllDept()
})
const handleDelete = async (id: number) => {
console.log("删除员工 id:", id)
ElMessageBox.confirm(
'您确认删除此数据吗?',
'删除员工',
{
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'warning',
}
)
.then(async () => {//点击确认按钮
//发起删除请求,根据结果,显示Message
const res = await deleteApi(id + '')
// res 结果{code:1,message:‘’,data:null }
if (res.code) {//判断code是否为0 如果并不是0,就是ture
ElMessage({
type: 'success',
message: '操作成功',
})
} else {
ElMessage.error('删除失败')
}
//无论成功和失败,都需要重新发起列表请求,筛选页面
queryEmp()
})
.catch(() => {//点击取消按钮
ElMessage({
type: 'info',
message: '取消删除',
})
})
}
// 搜索栏 start
//定义一个对象,用来接受搜索表单参数
const searchEmp = ref<SearchEmpModel>({
name: '',
gender: '',
begin: '',
end: '',
date: []
})
// 查询函数
const queryEmp = async () => {
// console.log('点击我,就可以查询数据,数据处理前:searchEmp:', searchEmp.value)
// name: '萨达萨达', gender: '1', begin: '', end: '', date: Array(2)
if (searchEmp.value.date.length > 0) {
searchEmp.value.begin = searchEmp.value.date[0]
searchEmp.value.end = searchEmp.value.date[1]
} else {
//没有选择时间范围
searchEmp.value.begin = ''
searchEmp.value.end = ''
}
// console.log('点击我,就可以查询数据,数据处理后:searchEmp:', searchEmp.value)
//上面的是查询的条件,还需要补充2个参数,当前页,每页条数
const res = await queryPageApi(
searchEmp.value.begin,
searchEmp.value.end,
searchEmp.value.gender,
searchEmp.value.name,
fenyeParam.value.currentPage,
fenyeParam.value.pageSize
)
// res是返回结果
if (res.code) {
tableData.value = res.data.rows
fenyeParam.value.total = res.data.total
}
}
const resetQueryEmp = () => {
console.log("清空搜索栏")
searchEmp.value = { name: '', gender: '', begin: '', end: '', date: [] }
// 当前页需要回到1
fenyeParam.value.currentPage = 1
//清空后,重新查询,因为没有参数,所以查询的是所有!
queryEmp();
}
// 分页栏 start
//定义一个变量 封装 每页条数和总条目数据
const fenyeParam = ref<PaginationParam>({
currentPage: 1,
pageSize: 5,
total: 0
})
const small = ref(false)
const background = ref(false)
const disabled = ref(false)
const handleSizeChange = (val: number) => {
//每页条数发生变化
console.log(`每页条数 :${val} 条`)
fenyeParam.value.pageSize = val
queryEmp();
}
const handleCurrentChange = (val: number) => {
//当前页发生变化
console.log(`当前页: ${val}`)
fenyeParam.value.currentPage = val
queryEmp();
}
// 分页栏 end
// 批量删除 start
//选中对象的容器
const multipleSelection = ref<EmpModel[]>([])
//选中对象的id容器
const selectionIds = ref<(number | undefined)[]>([])
//如果多选框选择了,就会回调次方法 其中val就是最新的值
const handleSelectionChange = (val: EmpModel[]) => {
multipleSelection.value = val
console.log("批量删除,对象容器:", multipleSelection)
//观察控制台,发i发现输出的是一个一个的员工对象
//思考:批量删除的接口,需要传递的ids=1,2,3
//解决方案:对象,对象2,对象3===》id1,id2,id3
multipleSelection.value.map((item) => {
//获取到每个对象item 将其中的id,添加到selectionIds数组中
selectionIds.value.push(item.id)
})
// 发现每次变化,都会添加,导致selectionIds会出现重复id
selectionIds.value = Array.from(new Set(selectionIds.value))
// 此时发现id已经去重了,格式:[2,3,4,5,6]可以向后台提交了,
console.log("批量删除,id容器:", selectionIds.value)
}
const deleteByIds = async () => {
//判断ids是否为空
if (selectionIds.value.length == 0) {
ElMessage.error('请勾选要删除的数据!!')
return
}
ElMessageBox.confirm(
'您确认删除此数据吗?',
'删除员工',
{
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'warning',
}
)
.then(async () => {//点击确认按钮
//发起删除请求,根据结果,显示Message
//由于批量删除的数组,格式:[1,3,4,5],传给后台的数据是这样的1,3,4
let ids = selectionIds.value.join(',')//1,3,4
const res = await deleteApi(ids)
// res 结果{code:1,message:‘’,data:null }
if (res.code) {//判断code是否为0 如果并不是0,就是ture
ElMessage({
type: 'success',
message: '操作成功',
})
} else {
ElMessage.error('删除失败')
}
selectionIds.value = [];//无论删除成功和失败,都将ids 清空
//无论成功和失败,都需要重新发起列表请求,筛选页面
queryEmp()
})
.catch(() => {//点击取消按钮
ElMessage({
type: 'info',
message: '取消删除',
})
})
}
// 批量删除 end
// 新增员工 start
// 定义一个emp员工对象,接收对话框提交的数组
let emp = ref<EmpModel>({
username: '',
password: '',
name: '',
gender: '',
phone: '',
job: '',
salary: '',
image: '',
entryDate: '',
deptId: '',
exprList: [] //工作经历
})
let labelWidth = ref(80)
//定义一个boolea类型变量,用来控制对话框的显示和隐藏
const dialogFormVisible = ref<boolean>(false)//默认false 隐藏
//准备性别和职位和部门的的下拉框数据start
//职位列表数据
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 }])
const depts = ref<DeptModelArray>([])
//发起网络请求,获取到部门的列表
const queryAllDept = async () => {
const res = await queryAllApi()
//code data message
if (res.code) {
depts.value = res.data
}
}
//准备性别和职位和部门的的下拉框数据end
//弹出新建员工对话框
const add = () => {
dialogFormVisible.value = true;
formTitle.value = '增加员工'
}
//点击新建员工对话框中的 保存
const save = async () => {
let result = null;
//给员工经历中的起止时间 赋值
if (emp.value.exprList.length > 0) {
emp.value.exprList.forEach(expr => {
expr.begin = expr.exprDate[0]
expr.end = expr.exprDate[1]
})
}
if (emp.value.id) { // 存在id,修改
console.log('修改员工数据:', emp.value)
//发起网络请求,保存数据
result = await updateApi(emp.value)
} else {// 不存在id,修改
console.log('新增员工数据:', emp.value)
//发起网络请求,保存数据
result = await addApi(emp.value)
}
if (result.code) {//如果0 等同fasle 如果是非0等同于true
ElMessage({
type: 'success',
message: '操作成功',
})
dialogFormVisible.value = false;
queryEmp()
} else {
ElMessage.error(result.msg)
}
// 无论成功和失败,清空emp数据
clearEmp();
}
// 清除提交的参数数据
const clearEmp = () => {
emp.value = {
username: '',
password: '',
name: '',
gender: '',
phone: '',
job: '',
salary: '',
image: '',
entryDate: '',
deptId: '',
exprList: []
}
}
// 员工图片上传 start
//上传给后台,成功后,回调
const handleAvatarSuccess: UploadProps['onSuccess'] = (
response,
uploadFile
) => {
emp.value.image = response.data
// console.log(response)
}
//上传前,进行校验 ,如果返回true就上传,如果返回false 就不上传,弹出警告提示!!
const beforeAvatarUpload: UploadProps['beforeUpload'] = (rawFile) => {
if (rawFile.type !== 'image/jpeg') {
ElMessage.error('必须上传JpEg格式的图片')
return false
} else if (rawFile.size / 1024 / 1024 > 2) {
ElMessage.error('上传的图片不能超过2M!!!')
return false
}
return true
}
// 员工图片上传 end
const addWorkItem=()=>{
emp.value.exprList.push({
exprDate:[],
begin:'',
end:'',
company:'',
job:'',
})
}
const delWorkItem=(expr:EmpExprModel)=>{
console.log("要删除工作经历:",expr)
if(emp.value.exprList){//不为空 ,就可以删除
const index=emp.value.exprList.indexOf(expr);
if(index!=-1){
emp.value.exprList.splice(index,1)
}
}
}
// 定义一个对话框标题的变量
const formTitle = ref<string>('')
//修改员工-回显
const updateEmp = async (id:number) => {
clearEmp()
dialogFormVisible.value = true
formTitle.value = '修改员工'
let result = await queryInfoApi(id)
if(result.code){
emp.value = result.data
//处理工作经历中的时间范围
let exprList = emp.value.exprList;
if(exprList && exprList.length > 0){
exprList.forEach(expr => {
expr.exprDate = [expr.begin, expr.end]
})
}
}
}
</script>
<template>
<h1>员工管理</h1>
<!-- 搜索栏 start -->
<!-- 搜索栏 -->
<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="queryEmp">查询</el-button>
<el-button @click="resetQueryEmp">清空</el-button>
</el-form-item>
</el-form>
<!-- 功能按钮 -->
<el-button type="success" @click="add">+ 新增员工</el-button>
<el-button type="danger" @click="deleteByIds">- 批量删除</el-button>
<br><br>
<!-- 搜索栏 end -->
<br>
<el-table :data="tableData" border style="width: 100%" @selection-change="handleSelectionChange" fit>
<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">
<template #default="scope">
<span v-if="scope.row.gender == 1">男</span>
<span v-else-if="scope.row.gender == 2">女</span>
</template>
</el-table-column>
<el-table-column prop="image" label="头像" align="center" >
<template #default="scope">
<el-image style="width: 100px; height: 100px" :src="scope.row.image" />
</template>
</el-table-column>
<el-table-column prop="deptName" label="所属部门" align="center" />
<el-table-column prop="job" label="职位" align="center" width="100px">
<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="入职时间" 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="updateEmp(scope.row.id)">编辑</el-button>
<el-button type="danger" size="small" @click="handleDelete(scope.row.id)">删除</el-button>
</template>
</el-table-column>
</el-table>
<br>
<!-- 分页条 -->
<el-pagination v-model:current-page="fenyeParam.currentPage" v-model:page-size="fenyeParam.pageSize"
:page-sizes="[5, 10, 15, 20]" :small="small" :disabled="disabled" :background="background"
layout="total, sizes, prev, pager, next, jumper" :total="fenyeParam.total" @size-change="handleSizeChange"
@current-change="handleCurrentChange" />
<!-- 新增和修改员工对话框 -->
<!-- 新增员工 / 修改员工的 DiaLog对话框 -->
<el-dialog v-model="dialogFormVisible" :title="formTitle">
<el-form :model="emp">
<!-- 第一行 -->
<el-row>
<el-col :span="12">
<el-form-item label="用户名" :label-width="labelWidth">
<el-input v-model="emp.username" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="姓名" :label-width="labelWidth">
<el-input v-model="emp.name" />
</el-form-item>
</el-col>
</el-row>
<!-- 第二行 -->
<el-row>
<el-col :span="12">
<el-form-item label="性别" :label-width="labelWidth">
<el-select v-model="emp.gender" placeholder="请选择" style="width: 100%;">
<el-option v-for=" gender in genders" :label="gender.name" :value="gender.value" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="手机号" :label-width="labelWidth">
<el-input v-model="emp.phone" />
</el-form-item>
</el-col>
</el-row>
<!-- 第三行 -->
<el-row>
<el-col :span="12">
<el-form-item label="薪资" :label-width="labelWidth">
<el-input v-model="emp.salary" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="入职日期" :label-width="labelWidth">
<el-date-picker v-model="emp.entryDate" type="date" placeholder="请选择入职日期" value-format="YYYY-MM-DD"
style="width: 100%;" />
</el-form-item>
</el-col>
</el-row>
<!-- 第四行 -->
<el-row>
<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" :label="dept.name" :value="dept.id" />
</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" :label="job.name" :value="job.value" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<!-- 第五行 -->
<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" />
<el-icon v-else class="avatar-uploader-icon"><Plus /></el-icon>
</el-upload>
</el-form-item>
</el-col>
</el-row>
<!-- 第六行 -->
<el-row>
<el-col :span="24">
<el-form-item label="工作经历" :label-width="labelWidth">
<el-button type="success" size="small" @click="addWorkItem">+ 添加工作经历</el-button>
</el-form-item>
</el-col>
</el-row>
<!-- 第七行 使用v-for -->
<el-row :gutter="5" v-for="expr in emp.exprList">
<el-col :span="10">
<el-form-item label="时间" size="small" :label-width="labelWidth">
<el-date-picker type="daterange" v-model="expr.exprDate" 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="delWorkItem(expr)">- 删除</el-button>
</el-form-item>
</el-col>
</el-row>
</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>
</template>
<style scoped>
.demo-pagination-block+.demo-pagination-block {
margin-top: 10px;
}
.demo-pagination-block .demonstration {
margin-bottom: 16px;
}
.avatar-uploader .avatar {
width: 178px;
height: 178px;
display: block;
}
</style>
<style>
.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: 178px;
height: 178px;
text-align: center;
}
</style>
总结
课堂作业
- 根据上述提示完成员工修改 🎤
2. 登录
2.1 基本功能实现
基本功能实现

代码操作
导入资料中提供的登录页面的背景图片。 【
04. 登录认证-基础文件/bg1.jpg
---------> 将该文件复制到项目的src/assets
目录中】导入资料中提供的登录页面的组件。【
04. 登录认证-基础文件/login
---------> 将该目录复制到项目的src/views
目录中】导入资料中提供的登录的api文件。【
04. 登录认证-基础文件/login.ts
---------> 将该文件复制到项目的src/api
目录中】
图示:
- 在
login/index.vue
中编写登录操作交互的逻辑,最终整个文件的代码如下:
<script setup lang="ts">
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:''})
//获取到路由实例对象
const router = useRouter()
//执行登录函数
const login = async () => {
//发送请求, 执行登录
const result = await loginApi(loginForm.value)
if(result.code){//如果登录成功, 提示成功信息
ElMessage.success('登录成功')
//跳转页面 ---> Vue-Router
router.push('/index')
}else {//如果登录失败, 提示错误信息
ElMessage.error(result.msg)
}
}
//重置操作
const reset = () => {
loginForm.value = {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="login">登 录</el-button>
<el-button class="button" type="info" @click="reset">重 置</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>
- 在router/index.ts中增加登录页的路由配置
{
path: '/login',
name: 'login',
component: () => import('../views/login/index.vue') //登录页
}

6.打开浏览器,访问 http://127.0.0.1:5173/login 进行测试


通过测试,我们看到,已经登录成功了。
但是呢,登录成功后,我们并没有对服务器端返回的token进行存储。
那么也就意味着,再后续的请求中,我们是没有办法直接获取到token,并在后续的每一次请求中,把这个token传递给服务端的。
所以,接下来,我们就需要考虑的是token的存储。
我们需要考虑将token存储起来,并且还得保证,存储之后各个vue组件都可以获取到这个token。 那这里我们就可以借助于 Vue3 中提供的
Pinia状态管理工具
来解决这个问题。
总结
课堂作业
- 根据上述提示,完成登录界面的书写🎤
2.2 Pinia存储令牌
前言
问题: 目前执行登录操作,登录成功之后,并没有将令牌信息起来,在后续的每次操作中,也就拿不到登录时的令牌信息了。
方案: 需要在登录成功后,将令牌等信息存储起来。 在后续的请求中,再将令牌取出来,携带到服务端。

如果在项目的多个组件中,要共享数据,可以使用Vue3中提供的状态管理库 Pinia。
Pinia 是 Vue 的专属状态管理库,它允许你跨组件或页面共享状态。 也就意味着,我们可以使用 Pinia
来存储数据,而这些数据是可以跨组件/页面来访问的。

Store是保存状态和业务逻辑的实体、承载着全局状态。(有点像一个永远存在的组件,每个组件都可以读取数据、存入数据)。


代码操作
定义Store
参照官方文档:https://pinia.vuejs.org/zh/core-concepts/
1). 将stores/counter.ts
文件重命名为 stores/loginEmp.ts
,并定义如下内容:
import { ref } from 'vue'
import { defineStore } from 'pinia'
import type {LoginInfo} from '@/api/model/model'
export const useLoginEmpStore = defineStore('loginEmp', () => {
const loginEmp = ref<LoginInfo>({})
const setLoginEmp = (emp: LoginInfo) => { //存入
loginEmp.value = emp;
}
const getLoginEmp = () => { //获取
return loginEmp.value;
}
const clearLoginEmp = () => { //清除
loginEmp.value = {}
}
return { loginEmp, setLoginEmp, getLoginEmp, clearLoginEmp }
})

使用Pinia
在登录完成后,需要使用定义的Store,往State中存储令牌信息 。 然后在后续访问服务器端接口的时候,需要从Pinia中再获取令牌信息,然后在请求头中携带到服务端。

1). 在用户登录成功之后,往 Pinia
中存入数据。 在 login/index.vue
中,添加操作 Pinia
存储数据的代码:

登录成功之后,已经将令牌存储起来了。那么在后续的每一次请求中,都需要将令牌携带到服务端。 那我们就需要在每一次请求中,都需要将令牌在请求头 token
中携带到服务端,服务端需要对令牌进行校验,如果成功,直接访问。 如果令牌校验失败,服务器端会返回401状态码,此时前端需要跳转到登录页面。
2). 在后续的每一次Ajax请求中获取Pinia中的令牌,在请求头中将令牌携带到服务端。
如果在每一次ajax请求中,都去操作pinia,从pinia中获取令牌信息,会非常繁琐(因为一个项目中,ajax请求会非常多)。

那这里呢,我们可以通过axios的拦截器 Interceptors 来简化操作。 我们可以在axios的请求拦截器中,在这个统一的入口中,拦截请求,并从Pinia中获取令牌,然后在请求服务端的时候,在请求头中携带令牌访问。

此时,我们就需要在 utils/request.ts
中,定义请求拦截器,并在请求拦截器中获取pinia中存储的令牌数据,在请求服务端的时候,在请求头中携带 token
访问服务器端。
import axios from 'axios'
import { useLoginEmpStore } from '@/stores/loginEmp';
//使用Store
const loginEmpStore = useLoginEmpStore()
//创建axios实例对象
const request = axios.create({
baseURL: '/api',
timeout: 600000
})
//axios的响应 request 拦截器
request.interceptors.request.use((config) => {
// 在发送请求之前做些什么 --> 携带令牌到服务端 --- header : token
const loginEmp = loginEmpStore.getLoginEmp();
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
打开浏览器测试:

总结
课堂作业
- pinia有什么作用?解决了什么问题?🎤
- 参考上述步骤,完成代码?
2.3 功能完善
功能完善
需求1: 目前,即使用户未登录的情况下访问服务器,服务器会响应401状态码,但是前端并不会跳转到登录页面。
需求2: 页面刷新之后,pinia中存储的令牌数据,就消失了,再次请求就获取不到令牌数据了。
代码操作
目前,即使用户未登录的情况下访问服务器,服务器会响应401状态码,但是前端并不会跳转到登录页面。

具体代码实现如下:
//axios的响应 response 拦截器
request.interceptors.response.use(
(response) => { //成功回调
return response.data
},
(error) => { //失败回调
if(error.response.status == 401){ //跳转登录页面
ElMessage.error('登录失效, 请重新登录')
router.push('/login')
}else {
ElMessage.error('接口访问异常')
}
return Promise.reject(error)
}
)
注意: :记得将后端代码的 aop,拦截器或者过滤器打开哦!!!
问题:页面刷新之后,pinia中存储的令牌数据,就消失了,再次请求就获取不到令牌数据了。
原因:页面刷新,原来的Vue实例卸载,Pinia的Store是挂载在Vue实例上的,故刷新后原有的数据也就丢失了。
方案:Pinia持久化 (基于pinia-plugin-persistedstate)。
具体代码如下:
1). 在 main.ts
中引入持久化插件 pinia-plugin-persistedstate

main.ts
import { createApp } from 'vue'
import { createPinia } from 'pinia' //导入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'
//导入pinia持久化插件
import pinaPluginPersistedstate from 'pinia-plugin-persistedstate'
//创建vue的应用实例
const app = createApp(App)
//注册ElementPlus的Icon组件
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
//应用pinia router elementPlus
app.use(createPinia().use(pinaPluginPersistedstate))
app.use(router) //路由
app.use(ElementPlus, {locale: zhCn})
app.mount('#app')
2). 在定义 Store 时,指定Pinia持久化的参数。

打开浏览器,测试:

测试完毕后,我们看到,Pinia已经将存储的数据,放在了浏览器的本地存储中,即使浏览器刷新,pinia中的数据依然存在。
总结
课堂作业
- 为什么要将登录信息持久化??🎤
2.4 退出登录
退出登录
代码操作
1). 为 layout/index.vue
组件中的 "退出登录" 按钮,绑定事件

2). 在 <script> </script>
中定义退出的逻辑
<script lang="ts" setup>
import { ref } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import router from '@/router'
import { useLoginEmpStore } from '@/stores/loginEmp'
const loginName = ref()
const loginStore = useLoginEmpStore();
loginName.value = loginStore.getLoginEmp().name;
//退出
const logout = () => {
ElMessageBox.confirm('您确认退出登录吗?' , '退出登录', {confirmButtonText:'确认', cancelButtonText:'取消', type:'warning'})
.then(async () => {
loginStore.clearLoginEmp();
router.push('/login')
ElMessage.success('退出成功')
}).catch(() => {
ElMessage.info('取消退出')
})
}
</script>

打开浏览器测试效果:

4. 打包部署
打包部署
到此呢,部门管理、员工管理、登录认证的功能,我们都已经完成了。 那接下来,我们就来说一下前端工程的打包部署。 前端项目最终开发完毕之后,是需要打包,然后部署在nginx服务器上运行的 。

代码操作
执行npm run build
即可将项目打包,打包后的文件会出现在 dist
目录中。


打包完成之后,就可以将打包后的项目,部署到 nginx
服务器上了,记得将nginx解压到一个没有中文不带空格的目录中 。
然后直接将 dist
目录中的内容,拷贝到nginx的解压目录中的 html
中即可。

然后,在nginx服务器的核心配置文件 conf/nginx
中,在 http
配置块里面 添加如下反向代理的配置:
server {
listen 90;
server_name localhost;
client_max_body_size 10m;
location / {
root html;
index index.html index.htm;
try_files $uri $uri/ /index.html;
}
location ^~ /api/ {
rewrite ^/api/(.*)$ /$1 break;
proxy_pass http://localhost:8080;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}
然后就可以双击 nginx.exe
启动项目了。 访问 http://localhost:90
如果之前nginx已经启动,需要在任务管理中结束进程!!