目录
- 后台管理
- 环境搭建
- 后台登录及数据请求
- 后台管理增删改查
- 导入导出和批量删除
- 问题集
后台管理
环境搭建
1 创建vue3项目 npm init vue@latest
2 运行项目 cd partner-manager
、 npm install
、 npm run dev
3 npm 配置淘宝镜像 npm config set registry https://registry.npm.taobao.org
4 配置启动
5 vite.config.js 配置端口号
export default defineConfig({
server: {
host: '0.0.0.0',
port: 7000,
https: false,
}
})
6 安装依赖
element-plus 安装:npm install element-plus --save
// main.js
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
app.use(ElementPlus)
iicon组件 安装:npm i @element-plus/icons-vue -S
axios 安装:npm i axios -S
封装 request.js 和 config.js
// config.js
export default {
serverUrl: 'localhost:9090'
}
// request.js 直接复制前端项目的
缓存持久化插件 pinia数据是缓存到内存的,开启持久化后才能缓存到浏览器
安装: npm i pinia-plugin-persistedstate -S
// main.js 引用
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
app.use(pinia)
7 布局主页面
8 分页插件中文设置:
// 在main.js里引入:
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
app.use(ElementPlus, {
locale: zhCn,
})
9 完成后台管理基本框架:头部菜单、左侧菜单、主体。主体分文搜索区域、表格区域、分页区域。
后台登录及数据请求
后台登录:实现 Login.vue,用到了 request.js 、config.js 、user.js。直接复制前端项目的,但 user.js 里面的 user 属性需
要改为 manager。否则前后端用的同一个缓存且变量名一样,退出登录时删除后端数据会导致前台数据被删。
数据请求:实现 load 方法。
const namex = ref('')
const address = ref('')
const pageNum= ref(1)
const pageSize = ref(6)
const total = ref(0)
const state = reactive({
tableData: [],
form: {} // 绑定表单类容
})
// 拿后台数据
const load = () => {
request.get('user/page',{
params: {
namex: namex.value, // 注意.value
address: address.value,
pageNum: pageNum.value,
pageSize: pageSize.value,
}
}).then(res => {
console.log(res)
state.tableData = res.data.records
total.value = res.data.total
})
}
load()
后台管理增删改查
实现退出登录
// UserServiceImpl
@Override
public void logout(String uid) {
// 退出登录 satoken提供
StpUtil.logout(uid);
log.info("用户{}退出成功", uid);
}
// WebController
@ApiOperation(value = "用户退出登录接口")
@GetMapping("/logout/{uid}")
public Result logout(@PathVariable String uid) {
userService.logout(uid);
return Result.success();
}
const logout = () => {
request.get('/logout/' + user.uid).then(res => {
if (res.code === '200') {
userStore.logout() // 把当前用户信息删除
} else {
ElMessage.error(res.msg)
}
})
}
实现分页
分页插件 Mybatis-plus官网:https://baomidou.com/
package com.partner.boot.common;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MybatisPlusConfig {
/**
* 新的分页插件,一缓和二缓遵循mybatis的规则,需要设置 MybatisConfiguration#useDeprecatedExecutor = false 避免缓存出现问题(该属性会在旧插件移除后一同移除)
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}
实现查询和重置
多条件分页模糊查询 前端还是调用load
// UserController
@GetMapping("/page")
public Result findPage(@RequestParam(defaultValue = "") String namex,
@RequestParam(defaultValue = "") String address,
@RequestParam Integer pageNum,
@RequestParam Integer pageSize) {
System.out.print(namex + " " +address + " " + pageNum + " " + pageSize);
QueryWrapper<User> queryWrapper = new QueryWrapper<User>().orderByDesc("id");
// !"" = StrUtil.isNotBlank(name) name and address
queryWrapper.like(StrUtil.isNotBlank(namex), "namex", namex);
queryWrapper.like(StrUtil.isNotBlank(address), "address", address);
return Result.success(userService.page(new Page<>(pageNum, pageSize), queryWrapper));
}
// 刷新
const reset = () => {
namex.value = ''
load()
}
设置每页信息数 当前页面数
// 页面数
const currentChange = (num) => {
console.log(num)
pageNum.value = num
load()
}
// 页面大小
const handleSizeChange = (size) => {
console.log(size)
pageSize.value = size
load()
}
<!-- 分页 @=v-on-->
<div style="margin: 10px 0">
<el-pagination
@current-change="currentChange"
@size-change="handleSizeChange"
v-model:current-page="pageNum"
v-model:page-size="pageSize"
background
:page-sizes="[2, 4, 6, 10, 20]"
layout="total, sizes, prev, pager, next, jumper"
:total=total
class="mt-4"
/>
</div>
实现新增与编辑
,element弹窗组件,dialog
const dialogFormVisible = ref(false)
const ruleFormRef = ref()
// 新增校验规则
const rules = reactive({
namex: [
{ required: true, message: '请输入名称', trigger: 'blur' },
{min: 3, max: 20, message: '长度在3-20之间', trigger: 'blur'}
]
})
// 新增
const handleAdd = () => {
dialogFormVisible.value = true
ruleFormRef.value.resetFields() // 取消校验提示
state.form = {}
}
// 保存
const save = () => {
ruleFormRef.value.validate(valid => {
// valid就是校验的结果
// if (valid) {
// request.post('/user',state.form).then(res => {
// // 保存新用户
// if (res.code === '200') {
// ElMessage.success('保存成功')
// // 关闭弹窗
// dialogFormVisible.value = false
// load() // 刷新表格数据
// } else {
// ElMessage.error(res.msg)
// }
// })
// 区分新增与修改
request.request({
url: '/user',
method: state.form.id ? 'put' : 'post',
data: state.form
}).then(res => {
if (res.code === '200') {
ElMessage.success('保存成功')
dialogFormVisible.value = false
load() // 刷新表格数据
} else {
ElMessage.error(res.msg)
}
})
})
}
// 编辑
const handleEdit = (raw) => {
state.form = JSON.parse(JSON.stringify(raw))
// 以下两段代码位置不可以互换
dialogFormVisible.value = true
ruleFormRef.value.resetFields() // 取消校验提示
}
<!-- 弹窗 -->
<el-dialog v-model="dialogFormVisible" title="信息" width="40%">
<el-form ref="ruleFormRef" :rules="rules" :model="state.form" label-width="80px"
style="padding: 0 20px" status-icon>
<!-- 保存新用户-->
<el-form-item prop="username" label="用户名" >
<el-input v-model="state.form.username" autocomplete="off" />
</el-form-item>
<el-form-item prop="namex" label="姓名" >
<el-input v-model="state.form.namex" autocomplete="off" />
</el-form-item>
<el-form-item prop="email" label="邮箱" >
<el-input v-model="state.form.email" autocomplete="off" />
</el-form-item>
<el-form-item prop="address" label="地址" >
<el-input v-model="state.form.address" autocomplete="off" />
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogFormVisible = false"> 取消 </el-button>
<el-button type="primary" @click="save"> 保存 </el-button>
</span>
</template>
</el-dialog>
增加地址字段,在数据库中加字段,在后端的实体类里也要增加。
实现删除
// 删除
const del = (id) => {
request.delete('/user/' + id).then(res => {
if (res.code === '200') {
ElMessage.success('操作成功')
load() // 刷新表格数据
} else {
ElMessage.error(res.msg)
}
})
}
删除是逻辑删除,防止数据误删除
导入导出和批量删除
导出
先实现导出,因为导入需要一个模板导,导出成一个模板,再修改模板然后实现导入。 导出实际就是打开一个新
的链接下载表格数据。导出是没有权限验证的,需要在 MyWebMvcConfig.java里面进行放 行: “/**/export”。
// 导出接口
const exportData = () => {
window.open(`http://${config.serverUrl}/user/export`)
}
<!-- 导出 -->
<el-button type="primary" @click="exportData" class="ml5">
<el-icon style="vertical-align: middle">
<Top />
</el-icon> <span style="vertical-align: middle"> 导出 </span>
</el-button>
加上这个依赖,才能实现Excel导入导出
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>5.2.3</version>
</dependency>
/**
* UserController 导出接口
*/
@GetMapping("/export")
public void export(HttpServletResponse response) throws Exception {
// 从数据库查询出所有的数据
List<User> list = userService.list();
// 在内存操作,写出到浏览器 1操作ExcelWriter的一个对象
ExcelWriter writer = ExcelUtil.getWriter(true);
// 一次性写出list内的对象到excel,使用默认样式,强制输出标题
writer.write(list, true);
// 设置浏览器响应的格式 2 response对象
response.setContentType("application/vnd.openxmlformats-"+
"officedocument.spreadsheetml.sheet;charset=utf-8");
// 编码
String fileName = URLEncoder.encode("User信息表", "UTF-8");
// attachment 附件形式
response.setHeader("Content-Disposition", "attachment;filename=" + fileName + ".xlsx");
// 3 写出到浏览器
ServletOutputStream out = response.getOutputStream();
writer.flush(out, true);
out.close();
writer.close();
}
输出为excel统一请求头
application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8
导入
导入可以理解为文件上传 ,后端没有对导入进行放行,因为是一个Post请求,所以需要校验token
const userStore = useUserStore()
const token = userStore.getBearerToken
const handleImportSuccess = () => {
// 刷新表格
load()
ElMessage.success("导入成功")
}
<!-- 导入 -->
<el-upload
class="ml5"
:show-file-list="false"
style="display: inline-block; position: relative; top: 3px"
:action='`http://${config.serverUrl}/user/import`'
:on-success="handleImportSuccess"
:headers="{ Authorization: token}"
>
<el-button type="primary">
<el-icon style="vertical-align: middle">
<Bottom />
</el-icon> <span style="vertical-align: middle"> 导入 </span>
</el-button>
</el-upload>
/**
* excel 导入
* @param file
* @throws Exception
*/
@PostMapping("/import")
public Result imp(MultipartFile file) throws Exception {
// 1 理解为文件上传 将file转换为流
InputStream inputStream = file.getInputStream();
// 2 操作ExcelReader的一个对象 从流里读数据
ExcelReader reader = ExcelUtil.getReader(inputStream);
// 3 通过 javabean 的方式读取Excel内的对象,但是要求表头必须是英文,跟javabean的属性要对应起来
List<User> list = reader.readAll(User.class);
// userService.saveBatch(list);
// 应该对用户存的数据进行校验
for (User user : list) {
userService.saveUser(user);
}
return Result.success();
}
导入用户数据的时候,注意校验数据得合法性,我是通过 saveUser 方法进行保存数据的
注意模板的选择,id列要删除,否则会报错。敏感的数据要清空,比如:uid。
批量删除
// 批量删除
const multipleSelection = ref([])
const handleSelectionChange = (val) => {
multipleSelection.value = val
}
const confirmDelBatch = () => {
if (!multipleSelection.value || !multipleSelection.value.length) {
ElMessage.warning("请选择数据")
return
}
// 把对象数组 multipleSelection.value 转化为只包含 id 的纯数字 的数组 idArr
const idArr = multipleSelection.value.map(v => v.id)
console.log(idArr)
request.post('/user/del/batch', idArr).then(res => {
if (res.code === '200') {
ElMessage.success('操作成功')
load() // 刷新表格数据
} else {
ElMessage.error(res.msg)
}
})
}
<!-- 批量删除 -->
<el-popconfirm title="您确定删除吗?" @confirm="confirmDelBatch">
<template #reference>
<el-button type="danger" style="margin-left: 5px">
<el-icon style="vertical-align: middle">
<Remove />
</el-icon> <span style="vertical-align: middle"> 批量删除 </span>
</el-button>
</template>
</el-popconfirm>
// UserController
@PostMapping("/del/batch")
public Result deleteBatch(@RequestBody List<Integer> ids) {
// 后端是list列表对象,前端是list数组,springMVC在做转换的时候,
// 他会把前端的数组 通过json的方式 映射成一个list
userService.removeByIds(ids);
return Result.success();
}
注意批量删除触发的时机,点击行的时候只是保存了行id数组,真正的删除是在点击
[批量删除] 按钮的时候触发的!
问题集
排查错误:先看请求,即前端传的值是否有问题;再看后台,后台记录可能有问题。
问题一:退出登录时删除后端数据会导致前台数据也被删除
解决:前后端用的同一个缓存且变量名一样,将前端变量名改为manager
问题二:新增保存不成功
解决:后台报错没设置默认密码,修改UserController里的save方法,将调用save改为调用saveUser,会校验用户是否存
在,也会设置初始密码。同时需要在UserServiceImpl将saveUser方法改为public,在IUserService里的定义
saveUser方法。
问题三:form = JSON.parse(JSON.stringify(raw)) 这么赋值,form会变成响应式,就没法编辑
解决: const state = reactive({ form: {} })