一:前言
1:因为本人也是全栈初学者,现在主职是公司前端,鉴于当前行业形式,单单只掌握一门语言已经不再吃香,甚至有点危险,35岁危机极大可能提前。作为00后要始终保持危机意识,不至于在25,26岁时被裁去卖煎饼果子,所以现在决定开始自学后端,争取早日成为一名全栈开发,能独自自主完成单个项目,早日回到老家工作,开始养老人生。
2:请注意:本教程只注重功能实现(增删改查,文件上传等),前端的样式可能
会略显简陋,有强迫症的慎入!!
3:前置知识:前端(前端三件套,vue3)后端(java基础,mysql基础)
4:项目开展前提:
工具:Intellij IDEA 2023.3.4,vscode,mySql,vue3,springboot3,element-plus等
开发插件:
截图工具:pixpin
接口测试工具:postman
vscode:Volar,prettier,auto close tag
Intellij IDEA:chinese language汉化,codeGlance Pro代码地图,MybatisX,ptg
二:创建项目
1:前端项目
1:使用 npm create vue@latest 命令创建一个vue3项目,勾选pinia和router
2:npm install 下载依赖
3:按需引入element-plus,点击此链接跳转官网
4:把main.js中的框架自带样式注释或删除
5:测试一下elementplus是否被正确配置,改造app.vue,并npm run dev运行项目
![](https://img-blog.csdnimg.cn/direct/7763ca04d8fc4ab69d9fe727d06fe933.png)
可以看到,elementplus已被正确配置并成功引入
2:后端项目
1:打开idea,新建项目
![](https://img-blog.csdnimg.cn/direct/9ce2ecab53ca45f9b1a0bb8cdd95b244.png)
新建项目,勾选项目所需依赖,想知道勾选的依赖有什么作用的请自行网上搜索,加深印象。到这里项目就创建成功了。
2:思考后台样式与表设计
三:开始编写代码
1:前端页面
使用elementplus布局容器![](https://img-blog.csdnimg.cn/direct/1b598876a9324b4fb10138b26620a660.png)
app.vue
<script setup>
import { RouterLink, RouterView } from 'vue-router'
</script>
<template>
<div class="common-layout">
<el-container class="container">
<el-aside class="aside" width="200px">侧边栏</el-aside>
<el-container>
<el-header class="header">头部</el-header>
<el-main class="main">内容</el-main>
</el-container>
</el-container>
</div>
</template>
<style scoped lang="less">
.container{
height: 100vh;
.aside{
background-color: aqua;
}
.header{
background-color: chocolate;
}
.main{
background-color: beige;
}
}
</style>
在侧边栏使用menu菜单
app.vue代码
<script setup>
import { RouterLink, RouterView } from 'vue-router'
import { ref } from 'vue'
import {
Document,
Menu as IconMenu,
Location,
Setting,
} from '@element-plus/icons-vue'
let isCollapse = ref(true)
const handleOpen = (key, keyPath) => {//菜单展开
console.log(key, keyPath)
}
const handleClose = (key, keyPath) => {//菜单折叠
console.log(key, keyPath)
}
</script>
<template>
<div class="common-layout">
<el-container class="container">
<el-aside class="aside" :style="{ 'width': !isCollapse ? '200px' : '60px' }">
<el-menu default-active="/" class="el-menu-vertical-demo" :collapse="isCollapse" @open="handleOpen"
@close="handleClose" router>
<el-menu-item index="/">
<el-icon><icon-menu /></el-icon>
<template #title>首页</template>
</el-menu-item>
<el-sub-menu index="gongsi">
<template #title>
<el-icon>
<location />
</el-icon>
<span>公司信息管理</span>
</template>
<el-menu-item-group>
<el-menu-item index="/gongsi/DeptInfo">部门管理</el-menu-item>
<el-menu-item index="/gongsi/EmployeeInformation">员工管理</el-menu-item>
</el-menu-item-group>
</el-sub-menu>
<el-menu-item index="3">
<el-icon>
<document />
</el-icon>
<template #title>公司图表数据</template>
</el-menu-item>
<el-menu-item index="4">
<el-icon>
<setting />
</el-icon>
<template #title>暂未开发...</template>
</el-menu-item>
</el-menu>
</el-aside>
<el-container>
<el-header class="header">
<el-radio-group v-model="isCollapse" style="margin-bottom: 20px">
<el-radio-button :value="false">展开</el-radio-button>
<el-radio-button :value="true">折叠</el-radio-button>
</el-radio-group>
头部</el-header>
<el-main class="main">
<RouterView />
</el-main>
</el-container>
</el-container>
</div>
</template>
<style scoped lang="less">
.container {
height: 100vh;
.aside {
background-color: #fff;
}
.header {
background-color: chocolate;
}
.main {
background-color: beige;
}
}
</style>
编写homeView.vue文件
<script setup>
</script>
<template>
<div>
<h1>首页</h1>
<div style="display: flex;">
<div class="left">
<div>前端技术选型</div>
<ul>
<li>vue3</li>
<li>elementplus</li>
<li>router</li>
<li>pinia</li>
<li>axios</li>
<li>...</li>
</ul>
</div>
<div class="right">
<div>后端技术选型</div>
<ul>
<li>springboot3</li>
<li>maven</li>
<li>lombok</li>
<li>pageHelper</li>
<li>...</li>
</ul>
</div>
</div>
</div>
</template>
运行项目,效果图
2:后端接口开发
1:项目连接数据库
1-1:![](https://img-blog.csdnimg.cn/direct/c7fa1a776099466dabec1e60c559cd61.png)
1-2:
1-3:1-2用到的sql语句
-- 部门管理
create table dept(
id int unsigned primary key auto_increment comment '主键ID',
name varchar(10) not null unique comment '部门名称',
create_time datetime not null comment '创建时间',
update_time datetime not null comment '修改时间'
) comment '部门表';
-- 部门表测试数据
insert into dept (id, name, create_time, update_time)
values(
1,'学工部',now(),now()),
(2,'教研部',now(),now()),
(3,'咨询部',now(),now()),
(4,'就业部',now(),now()),
(5,'人事部',now(),now()
);
-- 员工管理(带约束)
create table emp (
id int unsigned primary key auto_increment comment 'ID',
username varchar(20) not null unique comment '用户名',
password varchar(32) default '123456' comment '密码',
name varchar(10) not null comment '姓名',
gender tinyint unsigned not null comment '性别, 说明: 1 男, 2 女',
image varchar(300) comment '图像',
job tinyint unsigned comment '职位, 说明: 1 班主任,2 讲师, 3 学工主管, 4 教研主管, 5 咨询师',
entrydate date comment '入职时间',
dept_id int unsigned comment '部门ID',
create_time datetime not null comment '创建时间',
update_time datetime not null comment '修改时间'
) comment '员工表';
-- 员工表测试数据
INSERT INTO emp
(id, username, password, name, gender, image, job,
entrydate,dept_id, create_time, update_time) VALUES
(1,'zhangsan','123456','张三',1,'1.jpg',4,'2000-01-01',2,now(),now()),
(2,'liguang','123456','李广',1,'2.jpg',2,'2015-01-01',2,now(),now()),
(3,'xinqiji','123456','辛弃疾',1,'3.jpg',2,'2008-05-01',2,now(),now()),
(4,'libai','123456','李白',1,'4.jpg',2,'2007-01-01',2,now(),now()),
(5,'sushi','123456','苏轼',1,'5.jpg',2,'2012-12-05',2,now(),now()),
(6,'dufu','123456','杜甫',2,'6.jpg',3,'2013-09-05',1,now(),now()),
(7,'dumu','123456','杜牧',2,'7.jpg',1,'2005-08-01',1,now(),now()),
(8,'zhouyu','123456','周瑜',2,'8.jpg',1,'2014-11-09',1,now(),now()),
(9,'zhugeliang','123456','诸葛亮',2,'9.jpg',1,'2011-03-11',1,now(),now()),
(10,'xiahoudun','123456','夏侯惇',2,'10.jpg',1,'2013-09-05',1,now(),now()),
(11,'liqingzhao','123456','李清照',1,'11.jpg',5,'2007-02-01',3,now(),now()),
(12,'taoyuanming','123456','陶渊明',1,'12.jpg',5,'2008-08-18',3,now(),now()),
(13,'wangwei','123456','王伟',1,'13.jpg',5,'2012-11-01',3,now(),now()),
(14,'weiyingwu','123456','韦应物',1,'14.jpg',2,'2002-08-01',2,now(),now()),
(15,'zhanghong','123456','张红',1,'15.jpg',2,'2011-05-01',2,now(),now()),
(16,'heqiao','123456','何桥',1,'16.jpg',2,'2007-01-01',2,now(),now()),
(17,'liyuan','123456','李渊',1,'17.jpg',NULL,'2015-03-21',NULL,now(),now());
1-4:
1-5:创建实体类
可以看到两个实体类的get和set方法占据了超多代码量,这时候我们下载的lombok依赖就起作用了,对上面两个实体类稍加改造
代码量得到极大的删减,看着就很舒坦!
根据java项目三层架构原则,创建控制层,业务层(逻辑层,service层),持久层(dao)文件夹
3:编写查询部门接口
3-1:按照图示编写代码
可以看到有很多注解,大大简化了我们的开发效率,因为有很多注解在这里就不一一解释了,用过几次就知道大概是什么意思了,我是个实用主义,不过多的去探究其中底层原理,会拿来用解决项目中的问题就可,想研究底层原理的可自行百度,谢谢
3-2:运行项目
出现这个页面就代表运行成功,接下来使用postman工具来测试接口
3-3:打开postman,测试接口
我们发现测试成功,有数据返回,但是createTime和updateTime字段是null,再看数据表可以看到明明是有值的,这是因为数据表的字段是create_time和update_time字段,一个是驼峰命名,一个是用下划线命名,字段名不一致,而且我们希望测试成功能在控制台打印相关信息,方便查看调试。这两个问题一块解决,如图所示:
修改完成后重启项目,再来测试一下
ok,成功!接下来就要写前端页面了,把返回的数据渲染到页面上
3-4:前端使用axios进行网络请求,并配置跨域
打开前端项目,使用 npm i axios 下载axios依赖
在vite.config.js中配置跨域,因为前后端项目端口不一样,所以即使是同一个电脑,也需要进行跨域配置
server: {
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
}
}
},
然后在src文件夹下创建utils文件夹,在utils下创建request.js文件夹,二次封装axios
在deptInfo.vue文件中测试请求
接下来在浏览器打开控制台,查看网络请求
成功拿到数据
3-5:把数据渲染到表格中
DeptInfo.vue文件
<script setup>
import request from '@/utils/request.js'
import { ref,onMounted } from 'vue'
onMounted(()=>{
getAllDept()
})
const getAllDept = () => {
request.get("/api/depts").then(res=>{
tableData.value=res.data
})
}
let tableData = ref([])
</script>
<template>
<div>
<el-table :data="tableData" style="width: 100%">
<el-table-column prop="id" label="序号" width="180" />
<el-table-column prop="name" label="部门名称" width="180" />
<el-table-column prop="updateTime" label="最后操作时间" />
<el-table-column label="操作">
<template #default="scope">
<el-button size="small" @click="handleEdit(scope.$index, scope.row)">修改</el-button>
<el-button size="small" type="danger" @click="handleDelete(scope.$index, scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
</template>
页面展示
渲染成功
4:编写增加部门接口
4-1:后端接口
重启项目,打开postman测试接口
不确定的可以打开数据表查看
新增部门成功
4-2:前端页面
DeptInfo.vue文件
<script setup>
import request from '@/utils/request.js'
import { ref,onMounted } from 'vue'
import {
Plus
} from '@element-plus/icons-vue'
onMounted(()=>{
getAllDept()
})
let tableData = ref([])
const getAllDept = () => {
request.get("/api/depts").then(res=>{
tableData.value=res.data
})
}
let addDeptButton = ref(false)
let form=ref({
name:''
})
const addDeptPrimary = () => {//点击确定,添加部门
request.post("/api/depts",form.value).then(res=>{
if(res.code==0){
ElMessage({
message: res.msg,
type: 'success',
})
getAllDept()
addDeptButton.value=false
}
})
}
const dialogcancel = () => {//点击取消,重置表单
form.value={
name:''
}
}
</script>
<template>
<div>
<el-button type="primary" size="large" @click="addDeptButton = true"><el-icon>
<Plus />
</el-icon>新增</el-button>
<el-table :data="tableData" style="width: 100%">
<el-table-column prop="id" label="序号" width="180" />
<el-table-column prop="name" label="部门名称" width="180" />
<el-table-column prop="updateTime" label="最后操作时间" />
<el-table-column label="操作">
<template #default="scope">
<el-button size="small" @click="handleEdit(scope.$index, scope.row)">修改</el-button>
<el-button size="small" type="danger" @click="handleDelete(scope.$index, scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-dialog v-model="addDeptButton" title="新增部门" width="500" draggable @close="dialogcancel">
<el-form :model="form">
<el-form-item label="部门名称" label-width="140px">
<el-input v-model="form.name" autocomplete="off" />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="addDeptButton = false">取消</el-button>
<el-button type="primary" @click="addDeptPrimary">
确定
</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
页面效果
5:编写删除部门接口
5-1:后端接口
重启项目,打开postman测试
测试成功
5-2:前端页面
页面效果
6:编写修改部门接口
6-1:后端接口
别忘了重启项目,打开postman测试
6-2:前端页面
DeptInfo.vue
<script setup>
import request from '@/utils/request.js'
import { ref,onMounted } from 'vue'
import {
Plus
} from '@element-plus/icons-vue'
import { getRowIdentity } from 'element-plus/es/components/table/src/util';
onMounted(()=>{
getAllDept()
})
let tableData = ref([])
const getAllDept = () => {
request.get("/api/depts").then(res=>{
tableData.value=res.data
})
}
let addDeptButton = ref(false)
let form=ref({
name:''
})
let addOrEdit=ref('')
const addDeptButton1 = () => {
addDeptButton.value=true
addOrEdit.value='add'
}
let deptId=ref(null)
const handleEdit = (index,row) => {
form.value.name=row.name
addDeptButton.value=true
addOrEdit.value='edit'
deptId.value=row.id
}
const addDeptPrimary = () => {//点击确定,添加部门修改部门
addOrEdit.value=='add'&&request.post("/api/depts",form.value).then(res=>{
if(res.code==0){
ElMessage({
message: res.msg,
type: 'success',
})
getAllDept()
addDeptButton.value=false
}
})
addOrEdit.value=='edit'&&request.put("/api/depts",{name:form.value.name,id:deptId.value}).then(res=>{
if(res.code==0){
ElMessage({
message: res.msg,
type: 'success',
})
getAllDept()
addDeptButton.value=false
}
})
}
const dialogcancel = () => {//点击取消,重置表单
form.value={
name:''
}
}
const handleDelete = (index,row) => {
request.delete("/api/depts/"+row.id).then(res=>{
if(res.code==0){
ElMessage({
message: res.msg,
type: 'success',
})
getAllDept()
}
})
}
</script>
<template>
<div>
<el-button type="primary" size="large" @click="addDeptButton1"><el-icon>
<Plus />
</el-icon>新增</el-button>
<el-table :data="tableData" style="width: 100%">
<el-table-column prop="id" label="序号" width="180" />
<el-table-column prop="name" label="部门名称" width="180" />
<el-table-column prop="updateTime" label="最后操作时间" />
<el-table-column label="操作">
<template #default="scope">
<el-button size="small" @click="handleEdit(scope.$index, scope.row)">修改</el-button>
<el-button size="small" type="danger" @click="handleDelete(scope.$index, scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-dialog v-model="addDeptButton" :title="addOrEdit=='add'?'新增部门':'修改部门'" width="500" draggable @close="dialogcancel">
<el-form :model="form">
<el-form-item label="部门名称" label-width="140px">
<el-input v-model="form.name" autocomplete="off" />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="addDeptButton = false">取消</el-button>
<el-button type="primary" @click="addDeptPrimary">
确定
</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
效果展示
7:分页及搜索功能实现
7-1:编写员工增删改查功能和上面是大差不差的,现在给员工查询列表加上分页功能
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>2.1.0</version>
</dependency>
在pom.xml文件中添加以上依赖
开始写接口之前还需要写一个实体类,PageBean.java
7-2:后端接口
springboot操作数据库有两种方式,一种是刚才一直在用的注解方式,另一种是xml方式,两种各有优缺点,进行复杂查询时一般使用xml方式,它更灵活,因为查询员工列表,查询参数会有分页参数,还有搜索框参数,参数较多,决定使用xml方式。
在resources文件夹中创建com/hezhanying/mapper目录,目录中创建EmpMapper.xml文件,如图所示
接下来就可以写查询语句了,需要有pageNo(第几页)pageSize(每页多少条数据)name(员工姓名)gender(性别)begin end(入职日期在begin和end之间的)
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.hezhanying.mapper.EmpMapper">
<!-- 条件分页查询 -->
<select id="getAllEmp" resultType="com.hezhanying.pojo.Emp">
select * from emp
<where>
<if test="name != null and name != ''">
name like concat('%',#{name},'%')
</if>
<if test="gender != null">
and gender = #{gender}
</if>
<if test="begin != null and end != null">
and entrydate between #{begin} and #{end}
</if>
</where>
order by update_time desc
</select>
</mapper>
开始写后端逻辑
重启项目,打开postman测试
测试成功
7-3:前端页面
EmployeeInformation.vue
<script setup>
import { ref, onMounted } from 'vue'
import request from '@/utils/request.js'
import {
Plus,
Minus
} from '@element-plus/icons-vue'
let tableData = ref([])
onMounted(() => {
getEmps()
})
const getEmps = (page = 1, pageSize = 5,name='',gender='',entryDateStart='',entryDateEnd='') => {
request.get(`/api/emps?pageNo=${page}&pageSize=${pageSize}&name=${name}&gender=${gender}&begin=${entryDateStart}&end=${entryDateEnd}`).then(res => {
tableData.value = res.data.rows
pageTotal.value = res.data.total
})
}
const multipleTableRef = ref()
const multipleSelection = ref([])
const handleSelectionChange = (val) => {
multipleSelection.value = val
}
let editOrAdd = ref('')
const handleEdit = (index, row) => {
request.get('/depts').then(res => {
deptList.value = res.data
})
dialogFormVisible.value = true
editOrAdd.value = "edit"
let { id, username, name, gender, image, deptId, job } = row
form.value.id = id
form.value.username = username
form.value.name = name
form.value.gender = gender
// ==1?'男':'女'
form.value.image = image
form.value.deptId = deptId
form.value.job = job
// ==1?'班主任':job==2?'讲师':job==3?'学工主管':job==4?'教研主管':job==5?'咨询师':'null'
console.log(index, row)
}
const handleDelete = (index, row) => {
console.log(index, row)
request.delete('/emps/' + row.id).then(res => {
if (res.code === 0) {
ElMessage({
message: res.msg,
type: 'success',
})
getEmps(currentPage4.value,pageSize4.value,searchForm.value.name,searchForm.value.gender,searchForm.value.entryDateStart,searchForm.value.entryDateEnd)
}
})
}
const dialogFormVisible = ref(false)
const form = ref({
id: '',
username: '',
name: '',
gender: '',
image: '',
deptId: '',
job: ''
})
let deptList = ref([])
let jobList = ref([
{ name: '班主任', id: 1 },
{ name: '讲师', id: 2 },
{ name: '学工主管', id: 3 },
{ name: '教研主管', id: 4 },
{ name: '咨询师', id: 5 },
])
let genderList = ref([
{ name: '男', id: 1 },
{ name: '女', id: 2 },
])
const addEmp = () => {
editOrAdd.value = "add"
dialogFormVisible.value = true
request.get('/depts').then(res => {
deptList.value = res.data
})
}
const primaryAddEmp = () => {
if (editOrAdd.value == "add") {
delete form.value.id
request.post("/emps", form.value).then(res => {
ElMessage({
message: res.msg,
type: 'success',
})
getEmps()
dialogFormVisible.value = false
})
} else if (editOrAdd.value == "edit") {
console.log('form.value :>> ', form.value);
request.put("/emps", form.value).then(res => {
ElMessage({
message: res.msg,
type: 'success',
})
getEmps()
dialogFormVisible.value = false
})
}
}
const closeDialog = (params) => {
form.value = {
id: '',
username: '',
name: '',
gender: '',
image: '',
deptId: '',
job: ''
}
}
let currentPage4 = ref(1)//第几页
let pageSize4 = ref(5)//每页几条数据
let pageTotal = ref()
const handleSizeChange = (val) => {//切换每页显示条数
getEmps(currentPage4.value, val,searchForm.value.name,searchForm.value.gender,searchForm.value.entryDateStart,searchForm.value.entryDateEnd)
pageSize4.value = val
}
const handleCurrentChange = (val) => {//切换页码
getEmps(val, pageSize4.value,searchForm.value.name,searchForm.value.gender,searchForm.value.entryDateStart,searchForm.value.entryDateEnd)
currentPage4.value = val
}
let searchForm = ref({
name: '',
gender: '',
entryDate: [],
entryDateStart: '',
entryDateEnd: '',
})
const shortcuts = [
{
text: '最近一周',
value: () => {
const end = new Date()
const start = new Date()
start.setTime(start.getTime() - 3600 * 1000 * 24 * 7)
return [start, end]
},
},
{
text: '最近一月',
value: () => {
const end = new Date()
const start = new Date()
start.setTime(start.getTime() - 3600 * 1000 * 24 * 30)
return [start, end]
},
},
{
text: '最近三月',
value: () => {
const end = new Date()
const start = new Date()
start.setTime(start.getTime() - 3600 * 1000 * 24 * 90)
return [start, end]
},
},
]
function padZeroToMonth(dateStr) {
const [year, month, day] = dateStr.split('-').map(Number)
const paddedMonth = month < 10 ? `0${month}` : month
return `${year}-${paddedMonth}-${day}`
}
const onSearch = () => {
searchForm.value.entryDateStart = searchForm.value.entryDate.length!=0 ? padZeroToMonth(searchForm.value.entryDate[0].toLocaleDateString().replace(/\//g, '-')):''
searchForm.value.entryDateEnd = searchForm.value.entryDate.length!=0 ? padZeroToMonth(searchForm.value.entryDate[1].toLocaleDateString().replace(/\//g, '-')):''
// request.get(`/emps?name=${searchForm.value.name}&gender=${searchForm.value.gender}`).then(res => {
getEmps(currentPage4.value, pageSize4.value,searchForm.value.name,searchForm.value.gender,searchForm.value.entryDateStart,searchForm.value.entryDateEnd)
// })
}
const onReset = (params) => {
searchForm.value = {
name: '',
gender: '',
entryDate: [],
entryDateStart: '',
entryDateEnd: '',
}
currentPage4.value = 1
pageSize4.value = 5
getEmps()
}
</script>
<template>
<el-form :inline="true" :model="searchForm" class="demo-form-inline">
<el-form-item label="姓名">
<el-input v-model="searchForm.name" placeholder="请输入姓名" clearable />
</el-form-item>
<el-form-item label="性别">
<el-select v-model="searchForm.gender" placeholder="请选择性别" clearable>
<el-option v-for="(item, index) in genderList" :label="item.name" :value="item.id" />
</el-select>
</el-form-item>
<el-form-item label="入职日期">
<el-date-picker v-model="searchForm.entryDate" type="daterange" unlink-panels range-separator="至"
start-placeholder="开始日期" end-placeholder="结束日期" :shortcuts="shortcuts" size="large" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="onSearch">搜索</el-button>
</el-form-item>
<el-form-item>
<el-button type="warning" @click="onReset">重置</el-button>
</el-form-item>
</el-form>
<el-button type="danger" size="large"><el-icon>
<Minus />
</el-icon>批量删除</el-button>
<el-button type="primary" size="large" @click="addEmp"><el-icon>
<Plus />
</el-icon>新增</el-button>
<el-table ref="multipleTableRef" :data="tableData" style="width: 100%" max-height="500px"
@selection-change="handleSelectionChange">
<el-table-column type="selection" />
<el-table-column property="id" label="序号" sortable />
<el-table-column property="name" label="姓名" />
<el-table-column property="image" label="头像">
<template #default="scope">
<!-- {{ scope.row.date }} -->
<img :src="scope.row.image" />
</template>
</el-table-column>
<el-table-column label="性别">
<template #default="scope">{{ scope.row.gender == 1 ? '男' : '女' }}</template>
</el-table-column>
<el-table-column label="职位">
<template #default="scope">
{{ scope.row.job == 1 ? '班主任' : scope.row.job == 2 ? '讲师' : scope.row.job == 3 ? '学工主管' :
scope.row.job == 4 ? '教研主管' : scope.row.job == 5 ? '咨询师' : '暂无职务' }}
</template>
</el-table-column>
<el-table-column property="entrydate" label="入职日期" sortable/>
<el-table-column property="updateTime" label="最后操作时间" sortable/>
<!-- <template #default="scope">{{ scope.row.updateTime.replace(/\T/g, ' ') }}</template>
</el-table-column> -->
<el-table-column label="操作">
<template #default="scope">
<el-button size="small" @click="handleEdit(scope.$index, scope.row)">修改</el-button>
<el-button size="small" type="danger" @click="handleDelete(scope.$index, scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination v-model:current-page="currentPage4" v-model:page-size="pageSize4" :page-sizes="[5, 10, 20, 30]"
background layout="total, sizes, prev, pager, next, jumper" :total="pageTotal" @size-change="handleSizeChange"
@current-change="handleCurrentChange" />
<el-dialog v-model="dialogFormVisible" :title="editOrAdd == 'edit' ? '修改学员信息' : editOrAdd == 'add' ? '新增学员' : ''"
width="500" @close="closeDialog">
<el-form :model="form">
<el-form-item label="用户名" label-width="140px">
<el-input v-model="form.username" autocomplete="off" />
</el-form-item>
<el-form-item label="姓名" label-width="140px">
<el-input v-model="form.name" autocomplete="off" />
</el-form-item>
<el-form-item label="头像" label-width="140px">
<el-input v-model="form.image" autocomplete="off" />
</el-form-item>
<el-form-item label="性别" label-width="140px">
<el-select v-model="form.gender" placeholder="请选择性别">
<el-option v-for="(item, index) in genderList" :label="item.name" :value="item.id" />
</el-select>
</el-form-item>
<el-form-item label="职位" label-width="140px">
<el-select v-model="form.job" placeholder="请选择职位">
<el-option v-for="(item, index) in jobList" :label="item.name" :value="item.id" />
</el-select>
</el-form-item>
<el-form-item label="部门" label-width="140px">
<el-select v-model="form.deptId" placeholder="请选择部门">
<el-option v-for="(item, index) in deptList" :label="item.name" :value="item.id" />
</el-select>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="dialogFormVisible = false">取消</el-button>
<el-button type="primary" @click="primaryAddEmp">
确定
</el-button>
</div>
</template>
</el-dialog>
</template>
<style scoped></style>
<style>
.demo-form-inline .el-input {
--el-input-width: 220px;
}
.demo-form-inline .el-select {
--el-select-width: 220px;
}
</style>
页面效果
非常完美!增加,修改和删除功能请参照修改删除部门接口
8:新增员工,上传头像功能实现
8-1:因为现在企业的文件都是上传到阿里云或者腾讯云上的,放到自己磁盘上的弊端很大,在这里就不演示给大家了,在实际工作中根本用不到,都是用的阿里云,腾讯云之类的,有想知道的小伙伴可以自行百度,又因为开通云服务器要钱,而且费时,等后续补充吧。暂时用string字符串类型代替吧
8-2:-----------------03-05更新分界线,开通阿里云服务器上传图片-------------------------------------
下面开始介绍使用云服务器存储图片
1:打开阿里云官网,阿里云-计算,为了无法计算的价值 (aliyun.com)
2:注册账户,有了账户就直接登录
3:登录成功后,点击控制台![](https://img-blog.csdnimg.cn/direct/39bd57ff90d8410a923ec138c543731d.png)
4:选择对象存储oss
5: 引入sdk
![](https://img-blog.csdnimg.cn/direct/dd33c3fbcd7f4dfdbcedb88446678fed.png)
然后再回到文档
再创建一个utils文件夹,文件夹内创建一个AliOSSUtils类,把复制的代码放进去
这段代码这是一个示例,并不是我们想要的,还需要改造一下,改造后的代码如下
package com.hezhanying.utils;
import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.io.InputStream;
import java.util.UUID;
@Component
public class AliOSSUtils {
private String endpoint = "";
private String accessKeyId = "";
private String accessKeySecret = "";
private String bucketName = "";
/**
* 实现上传图片到OSS
*/
public String upload(MultipartFile multipartFile) throws IOException {
// 获取上传的文件的输入流
InputStream inputStream = multipartFile.getInputStream();
// 避免文件覆盖
String originalFilename = multipartFile.getOriginalFilename();
String fileName = UUID.randomUUID().toString() + originalFilename.substring(originalFilename.lastIndexOf("."));
//上传文件到 OSS
OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
ossClient.putObject(bucketName, fileName, inputStream);
//文件访问路径
String url = endpoint.split("//")[0] + "//" + bucketName + "." + endpoint.split("//")[1] + "/" + fileName;
// 关闭ossClient
ossClient.shutdown();
return url;// 把上传到oss的路径返回
}
}
回到阿里云,搜索oss存储,点进来,这个oss对象存储没弄过的可能会提醒你需要先开通,跟着指示开通就行
请在复制进来的网址前面加上https://
点击进入bucket, 能看到我们刚才创建的东西
再controller软件包中创建UploadController类
写上传图片的接口,代码如下
package com.hezhanying.controller;
import com.hezhanying.pojo.Result;
import com.hezhanying.utils.AliOSSUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
@Slf4j
@RestController
public class UploadController {
@Autowired
private AliOSSUtils aliOSSUtils;
@PostMapping("/api/upload")
public Result upload(MultipartFile image) throws IOException {
String url= aliOSSUtils.upload(image);
return Result.success(url);
}
}
重启项目,打开postman测试啦
上传成功!
8-3:使用elementplus的el-upload组件,上传用户头像
修改这个vue文件
代码如下
<script setup>
import { ref, onMounted } from 'vue'
import request from '@/utils/request.js'
import {
Plus,
Minus, Delete, Download, ZoomIn
} from '@element-plus/icons-vue'
let tableData = ref([])
onMounted(() => {
getEmps()
})
const getEmps = (page = 1, pageSize = 5, name = '', gender = '', entryDateStart = '', entryDateEnd = '') => {
request.get(`/api/emps?pageNo=${page}&pageSize=${pageSize}&name=${name}&gender=${gender}&begin=${entryDateStart}&end=${entryDateEnd}`).then(res => {
tableData.value = res.data.rows
pageTotal.value = res.data.total
})
}
const multipleTableRef = ref()
const multipleSelection = ref([])
const handleSelectionChange = (val) => {
multipleSelection.value = val
}
let editOrAdd = ref('')
const handleEdit = (index, row) => {
request.get('/api/depts').then(res => {
deptList.value = res.data
})
dialogFormVisible.value = true
editOrAdd.value = "edit"
let { id, username, name, gender, image, deptId, job } = row
form.value.id = id
form.value.username = username
form.value.name = name
form.value.gender = gender
// ==1?'男':'女'
form.value.image = image
form.value.deptId = deptId
form.value.job = job
// ==1?'班主任':job==2?'讲师':job==3?'学工主管':job==4?'教研主管':job==5?'咨询师':'null'
console.log(index, row)
}
const handleDelete = (index, row) => {
console.log(index, row)
request.delete('/api/emps/' + row.id).then(res => {
if (res.code === 0) {
ElMessage({
message: res.msg,
type: 'success',
})
getEmps(currentPage4.value, pageSize4.value, searchForm.value.name, searchForm.value.gender, searchForm.value.entryDateStart, searchForm.value.entryDateEnd)
}
})
}
const dialogFormVisible = ref(false)
const form = ref({
id: '',
username: '',
name: '',
gender: '',
image: '',
deptId: '',
job: ''
})
let deptList = ref([])
let jobList = ref([
{ name: '班主任', id: 1 },
{ name: '讲师', id: 2 },
{ name: '学工主管', id: 3 },
{ name: '教研主管', id: 4 },
{ name: '咨询师', id: 5 },
])
let genderList = ref([
{ name: '男', id: 1 },
{ name: '女', id: 2 },
])
const addEmp = () => {
editOrAdd.value = "add"
dialogFormVisible.value = true
request.get('/api/depts').then(res => {
deptList.value = res.data
})
}
const primaryAddEmp = () => {
if (editOrAdd.value == "add") {
delete form.value.id
request.post("/api/emps", form.value).then(res => {
ElMessage({
message: res.msg,
type: 'success',
})
getEmps()
dialogFormVisible.value = false
})
} else if (editOrAdd.value == "edit") {
request.put("/api/emps", form.value).then(res => {
ElMessage({
message: res.msg,
type: 'success',
})
getEmps()
dialogFormVisible.value = false
})
}
}
const closeDialog = (params) => {
form.value = {
id: '',
username: '',
name: '',
gender: '',
image: '',
deptId: '',
job: ''
}
}
let currentPage4 = ref(1)//第几页
let pageSize4 = ref(5)//每页几条数据
let pageTotal = ref()
const handleSizeChange = (val) => {//切换每页显示条数
getEmps(currentPage4.value, val, searchForm.value.name, searchForm.value.gender, searchForm.value.entryDateStart, searchForm.value.entryDateEnd)
pageSize4.value = val
}
const handleCurrentChange = (val) => {//切换页码
getEmps(val, pageSize4.value, searchForm.value.name, searchForm.value.gender, searchForm.value.entryDateStart, searchForm.value.entryDateEnd)
currentPage4.value = val
}
let searchForm = ref({
name: '',
gender: '',
entryDate: [],
entryDateStart: '',
entryDateEnd: '',
})
const shortcuts = [
{
text: '最近一周',
value: () => {
const end = new Date()
const start = new Date()
start.setTime(start.getTime() - 3600 * 1000 * 24 * 7)
return [start, end]
},
},
{
text: '最近一月',
value: () => {
const end = new Date()
const start = new Date()
start.setTime(start.getTime() - 3600 * 1000 * 24 * 30)
return [start, end]
},
},
{
text: '最近三月',
value: () => {
const end = new Date()
const start = new Date()
start.setTime(start.getTime() - 3600 * 1000 * 24 * 90)
return [start, end]
},
},
]
function padZeroToMonth(dateStr) {
const [year, month, day] = dateStr.split('-').map(Number)
const paddedMonth = month < 10 ? `0${month}` : month
return `${year}-${paddedMonth}-${day}`
}
const onSearch = () => {
searchForm.value.entryDateStart = searchForm.value.entryDate.length != 0 ? padZeroToMonth(searchForm.value.entryDate[0].toLocaleDateString().replace(/\//g, '-')) : ''
searchForm.value.entryDateEnd = searchForm.value.entryDate.length != 0 ? padZeroToMonth(searchForm.value.entryDate[1].toLocaleDateString().replace(/\//g, '-')) : ''
getEmps(currentPage4.value, pageSize4.value, searchForm.value.name, searchForm.value.gender, searchForm.value.entryDateStart, searchForm.value.entryDateEnd)
}
const onReset = (params) => {
searchForm.value = {
name: '',
gender: '',
entryDate: [],
entryDateStart: '',
entryDateEnd: '',
}
currentPage4.value = 1
pageSize4.value = 5
getEmps()
}
const dialogImageUrl = ref('')
const dialogVisible = ref(false)
const disabled = ref(false)
const handleRemove = (file) => {
console.log("resmove",file.url)
}
const handlePictureCardPreview = (file) => {
dialogImageUrl.value = file.url
dialogVisible.value = true
}
const handleDownload = (file) => {
console.log(file.url)
}
const uploadSuccess = (file,upload) => {
console.log('filessss :>> ', file,upload);
}
const uploadChange = async(file, fileLists) => {
let formData = new FormData();
formData.append('image', file.raw);
try {
const response = await request.post('/api/upload', formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
});
form.value.image=response.data
} catch (error) {
console.error('error :>> ', error);
}
}
const bemo = (file,fileList) => {
form.value.image=''
console.log('object :>> ', file,fileList);
}
</script>
<template>
<el-form :inline="true" :model="searchForm" class="demo-form-inline">
<el-form-item label="姓名">
<el-input v-model="searchForm.name" placeholder="请输入姓名" clearable />
</el-form-item>
<el-form-item label="性别">
<el-select v-model="searchForm.gender" placeholder="请选择性别" clearable>
<el-option v-for="(item, index) in genderList" :label="item.name" :value="item.id" />
</el-select>
</el-form-item>
<el-form-item label="入职日期">
<el-date-picker v-model="searchForm.entryDate" type="daterange" unlink-panels range-separator="至"
start-placeholder="开始日期" end-placeholder="结束日期" :shortcuts="shortcuts" size="large" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="onSearch">搜索</el-button>
</el-form-item>
<el-form-item>
<el-button type="warning" @click="onReset">重置</el-button>
</el-form-item>
</el-form>
<el-button type="danger" size="large"><el-icon>
<Minus />
</el-icon>批量删除</el-button>
<el-button type="primary" size="large" @click="addEmp"><el-icon>
<Plus />
</el-icon>新增</el-button>
<el-table ref="multipleTableRef" :data="tableData" style="width: 100%" max-height="500px"
@selection-change="handleSelectionChange">
<el-table-column type="selection" />
<el-table-column property="id" label="序号" sortable />
<el-table-column property="name" label="姓名" />
<el-table-column property="image" label="头像">
<template #default="scope">
<img width="100px" height="100px" :src="scope.row.image" />
</template>
</el-table-column>
<el-table-column label="性别">
<template #default="scope">{{ scope.row.gender == 1 ? '男' : '女' }}</template>
</el-table-column>
<el-table-column label="职位">
<template #default="scope">
{{ scope.row.job == 1 ? '班主任' : scope.row.job == 2 ? '讲师' : scope.row.job == 3 ? '学工主管' :
scope.row.job == 4 ? '教研主管' : scope.row.job == 5 ? '咨询师' : '暂无职务' }}
</template>
</el-table-column>
<el-table-column property="entrydate" label="入职日期" sortable />
<el-table-column property="updateTime" label="最后操作时间" sortable />
<!-- <template #default="scope">{{ scope.row.updateTime.replace(/\T/g, ' ') }}</template>
</el-table-column> -->
<el-table-column label="操作">
<template #default="scope">
<el-button size="small" @click="handleEdit(scope.$index, scope.row)">修改</el-button>
<el-button size="small" type="danger" @click="handleDelete(scope.$index, scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination v-model:current-page="currentPage4" v-model:page-size="pageSize4" :page-sizes="[5, 10, 20, 30]"
background layout="total, sizes, prev, pager, next, jumper" :total="pageTotal" @size-change="handleSizeChange"
@current-change="handleCurrentChange" />
<el-dialog v-model="dialogFormVisible" :title="editOrAdd == 'edit' ? '修改学员信息' : editOrAdd == 'add' ? '新增学员' : ''"
width="500" @close="closeDialog">
<el-form :model="form">
<el-form-item label="用户名" label-width="140px">
<el-input v-model="form.username" autocomplete="off" />
</el-form-item>
<el-form-item label="姓名" label-width="140px">
<el-input v-model="form.name" autocomplete="off" />
</el-form-item>
<el-form-item label="头像" label-width="140px">
<el-upload action="#" accept="image/*" :limit="1" list-type="picture-card" :auto-upload="false" :on-success="uploadSuccess" :on-change="uploadChange" :on-remove="handleRemove" :before-remove="bemo">
<el-icon>
<Plus />
</el-icon>
<template #file="{ file }">
<div>
<img class="el-upload-list__item-thumbnail" :src="file.url" alt="" />
<span class="el-upload-list__item-actions">
<span class="el-upload-list__item-preview" @click="handlePictureCardPreview(file)">
<el-icon><zoom-in /></el-icon>
</span>
<span v-if="!disabled" class="el-upload-list__item-delete"
@click="handleDownload(file)">
<el-icon>
<Download />
</el-icon>
</span>
<span v-if="!disabled" class="el-upload-list__item-delete" @click="handleRemove(file)">
<el-icon>
<Delete />
</el-icon>
</span>
</span>
</div>
</template>
</el-upload>
<el-dialog v-model="dialogVisible">
<img w-full :src="dialogImageUrl" alt="Preview Image" />
</el-dialog>
</el-form-item>
<el-form-item label="性别" label-width="140px">
<el-select v-model="form.gender" placeholder="请选择性别">
<el-option v-for="(item, index) in genderList" :label="item.name" :value="item.id" />
</el-select>
</el-form-item>
<el-form-item label="职位" label-width="140px">
<el-select v-model="form.job" placeholder="请选择职位">
<el-option v-for="(item, index) in jobList" :label="item.name" :value="item.id" />
</el-select>
</el-form-item>
<el-form-item label="部门" label-width="140px">
<el-select v-model="form.deptId" placeholder="请选择部门">
<el-option v-for="(item, index) in deptList" :label="item.name" :value="item.id" />
</el-select>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="dialogFormVisible = false">取消</el-button>
<el-button type="primary" @click="primaryAddEmp">
确定
</el-button>
</div>
</template>
</el-dialog>
</template>
<style scoped>
.avatar-uploader .avatar {
width: 178px;
height: 178px;
display: block;
}
</style>
<style>
.demo-form-inline .el-input {
--el-input-width: 220px;
}
.demo-form-inline .el-select {
--el-select-width: 220px;
}
.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>
启动项目,新增用户
sql只保存图片的地址
修改的功能,只需要重新上传图片把新图片的地址替换一下就可以了
测试完成后尽快删除bucket,如果不删除产生费用
删除的过程中同样需要安全验证
友善提醒:使用云服务器时一定不能暴露id,key,地址之类的,小心被刷流量产生高额费用!!
9:--------03-07更新登录鉴权,接口过滤------------------------
9-1:登录鉴权共有三种方案,一是cookie,二是session,三是jwt(json web token),主流方案是使用jwt,这里就介绍jwt
再pom.xml文件中引入
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
在utils软件包中创建JwtUtils类,代码如下
package com.hezhanying.utils;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.util.Date;
import java.util.Map;
public class JwtUtils {
private static String signKey = "hezhanying";//签名密钥
private static Long expire = 43200000L; //有效时间
/**
* 生成JWT令牌
登录成功,生成JWT令牌并返回
* @param claims JWT第二部分负载 payload 中存储的内容
* @return
*/
public static String generateJwt(Map<String, Object> claims){
String jwt = Jwts.builder()
.addClaims(claims)//自定义信息(有效载荷)
.signWith(SignatureAlgorithm.HS256, signKey)//签名算 法(头部)
.setExpiration(new Date(System.currentTimeMillis() + expire))//过期时间
.compact();
return jwt;
}
/**
* 解析JWT令牌
* @param jwt JWT令牌
* @return JWT第二部分负载 payload 中存储的内容
*/
public static Claims parseJWT(String jwt){
Claims claims = Jwts.parser()
.setSigningKey(signKey)//指定签名密钥
.parseClaimsJws(jwt)//指定令牌Token
.getBody();
return claims;
}
}
重启打开postman测试
成功!
9-2:接口过滤器filter和Interceptor拦截器,我这里使用filter过滤器
再pom.xml文件中引入
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.76</version>
</dependency>
新建filter软件包,再软件包下新建LoginCheckFilter类,代码如下
package com.hezhanying.filter;
import com.alibaba.fastjson.JSONObject;
import com.hezhanying.pojo.Result;
import com.hezhanying.utils.JwtUtils;
import jakarta.servlet.*;
import jakarta.servlet.annotation.WebFilter;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;
import java.io.IOException;
@Slf4j
@WebFilter(urlPatterns = "/*") //拦截所有请求
public class LoginCheckFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException,ServletException {
//前置:强制转换为http协议的请求对象、响应对象 (转换原因:要使用子类中特有方法)
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
//1.获取请求url
String url = request.getRequestURL().toString();
log.info("请求路径:{}", url); //请求路径:http://localhost:8080/api/login
//2.判断请求url中是否包含login,如果包含,说明是登录操作,放行
if(url.contains("/api/login")){
chain.doFilter(request, response);//放行请求
return;//结束当前方法的执行
}
//3.获取请求头中的令牌(Authorization)
String Authorization = request.getHeader("Authorization");
log.info("从请求头中获取的令牌:{}",Authorization);
//4.判断令牌是否存在,如果不存在,返回错误结果(未登录)
if(!StringUtils.hasLength(Authorization)){
log.info("Token不存在");
Result responseResult = Result.error("NOT_LOGIN");
//把Result对象转换为JSON格式字符串 (fastjson是阿里巴巴提供的用于实现对象和json的转换工具类)
String json = JSONObject.toJSONString(responseResult);
response.setContentType("application/json;charset=utf- 8");
//响应
response.getWriter().write(json);
return;
}
//5.解析token,如果解析失败,返回错误结果(未登录)
try {
JwtUtils.parseJWT(Authorization);
}catch (Exception e){
log.info("令牌解析失败!");
Result responseResult = Result.error("NOT_LOGIN");
//把Result对象转换为JSON格式字符串 (fastjson是阿里巴巴提供的用于实现对象和json的转换工具类)
String json = JSONObject.toJSONString(responseResult);
response.setContentType("application/json;charset=utf- 8");
//响应
response.getWriter().write(json);
return;
}
//6.放行
chain.doFilter(request, response);
}
}
再加上这个注解
重启项目就可以测试了,
这里前端页面就不再展示了,接口已通,实现页面只是时间问题,登录页面及路由跳转没有技术难题
10:先就这样啦,欢迎评论区讨论,想要源码请私信,谢谢阅读!后续学到新知识还会分享
版权归原作者 鸭子嘎鹅子呱 所有, 如有侵权,请联系我们删除。