前后端项目笔记

前端项目创建

准备工作   nodejs安装   vue cli安装

vue create frontend

最后一个y的话 它会保存  方便下次创建项目  我这是手快敲错了 随自己

前端项目组件及作用

Element-UI引入

安装

npm i element-ui -S

main.js中引入

清空路口App.vue

清空Home页面

随便写个按钮

原因

全局样式控制

因为Element-UI带有一些默认样式,有时我们需要创建全局的样式控制(清空ElementUI样式)需要自定义一些样式等

一般在assets中创建 global.css

在main.js中引入

对比

Vue管理系统页面布局

容器布局

左侧菜单

点击菜单进行路由跳转

例如 我新建一个视图

配置路由

表格(编辑删除)

路由

视图

搜索 新增 (el-input)

全局配置UI按钮大小

全局表格头居中data居中

表格数据分页

后台搭建

server:port: 8085spring:application:name: backendredis:##redis 单机环境配置##将docker脚本部署的redis服务映射为宿主机ip##生产环境推荐使用阿里云高可用redis服务并设置密码host: 127.0.0.1port: 6379database: 0password: 123456ssl: false##redis 集群环境配置#cluster:#  nodes: 127.0.0.1:7001,127.0.0.1:7002,127.0.0.1:7003#  commandTimeout: 5000datasource:driver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://xxxxx:3306/springboot?useSSL=false&useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=GMT%2B8&useCursorFetch=trueusername: xxxxxpassword: xxxxxxmybatis:type-aliases-package: com.example.backend.pojomapper-locations: classpath:mappers/*Mapperconfiguration:map-underscore-to-camel-case: true
logging:level:com.example.backend.mapper: debug

前端发送axios调用接口

安装axios

npm install axios

封装axios请求

import axios from 'axios'
import router from '../router'; //创建axios实例
const request = axios.create({baseURL: 'http://localhost:8085',timeout: 5000
})// request 拦截器// 可以自动发送请求前进行一些处理// 比如统一加token, 对请求参数统一加密request.interceptors.request.use(config => {config.headers['Content-Type'] = 'application/json;charset=utf-8';// 从sessionStorage中获取tokenconst token = sessionStorage.getItem('token');if (token) {config.headers['token'] = token; // 如果token存在,让每个请求头携带token} else {// token不存在,重定向到登录页面window.location.href = '/'; // 或使用 Vue Router的 this.$router.push('/login')//this.$router.push('/')}return config;}, error => {return Promise.reject(error);});// response 拦截器// 可以在接口响应后统一处理结果request.interceptors.response.use(response => {let res = response.data;// 若返回的是字符串则转换成jsonif (typeof res === 'string') {res = res ? JSON.parse(res) : res//将JSON字符串转JS对象 方便点出来}return res;},error => {console.log('err' + error) // for debugreturn Promise.reject(error)});export default request;//导出  其他页面引用

对表格页面进行修改

改成动态数据

引入axios请求

按条件查询

添加清空按钮

分页查询

前端代码

后端引入分页依赖pagehelp

实体类

持久层

业务层

控制器

返回结果类

新增和编辑

注意后面用用户登录  没有对name进行唯一约束  编码也没有   这个注意下

Dialog对话框

message消息提示

前端新增和编辑代码 

<template><!-- 需要有一个根div --><div><div style="margin-bottom: 20px;"><el-input style="width: 300px;" v-model="params.name" placeholder="请输入姓名"></el-input><el-input style="width: 300px; margin-left: 20px;" v-model="params.phone" placeholder="请输入电话"></el-input><el-button style="margin-left: 20px;" @click="findBySearch(1)">查询</el-button><el-button style="margin-left: 20px;" @click="reset">清空</el-button><br><el-button type="primary" style="margin-top: 20px;" @click="addForm">新增</el-button><hr /></div><div><el-table :data="tableData" style="width: 100%"><el-table-column prop="name" label="姓名" width="180"></el-table-column><el-table-column prop="password" label="密码" width="180"></el-table-column><el-table-column prop="age" label="年龄"></el-table-column><el-table-column prop="phone" label="电话"></el-table-column><el-table-column label="操作"><template slot-scope="aaa"><el-button type="primary" @click="edit(aaa.row)">编辑</el-button><el-button type="danger">删除</el-button></template></el-table-column></el-table></div><div><el-pagination style="margin-top: 20px;" @size-change="handleSizeChange"@current-change="handleCurrentChange" :current-page="params.pageNum" :page-sizes="[5, 10, 15, 20]":page-size="params.pageSize" layout="total, sizes, prev, pager, next, jumper" :total="totalNum"></el-pagination></div><div><el-dialog :title="dialogTitle" :visible.sync="dialogFormVisible" @close="resetForm"><el-form :model="form"><el-form-item label="姓名" :label-width="formLabelWidth"><el-input v-model="form.name" autocomplete="off"></el-input></el-form-item><el-form-item label="年龄" :label-width="formLabelWidth"><el-input v-model="form.age" autocomplete="off"></el-input></el-form-item><el-form-item label="密码" :label-width="formLabelWidth"><el-input v-model="form.password" autocomplete="off"></el-input></el-form-item><el-form-item label="电话" :label-width="formLabelWidth"><el-input v-model="form.phone" autocomplete="off"></el-input></el-form-item></el-form><div slot="footer" class="dialog-footer"><el-button @click="dialogFormVisible = false">取 消</el-button><el-button type="primary" @click="submit" :disabled="isSubmitting">确 定</el-button></div></el-dialog></div></div>
</template>
<script>
import request from '@/api/HttpRequest'
export default {name: "AdminView",data() {return {dialogFormVisible: false,isEdit: false, // 标志是编辑状态还是新增状态dialogTitle: '', // 对话框标题params: {name: '',phone: '',pageNum: 1,pageSize: 5,},totalNum: 0,tableData: [],form: {},formLabelWidth: '120px',isSubmitting: false  //提交标志}},created() {this.findBySearch()},methods: {resetForm() {this.form = {name: '',password: '',age: '',phone: ''};},reset() {this.params.name = ''this.params.phone = ''this.findBySearch()},findBySearch(val) {if (val == 1) {this.params.pageNum = 1this.params.pageSize = 5}request.get("/test/search", {params: this.params}).then(res => {if (res.code == 200) {this.tableData = res.data.listthis.totalNum = res.data.total} else {}})},addForm() {this.resetForm(); // 重置表单this.isEdit = false; // 设置为新增模式this.dialogTitle = '新增管理员信息'; // 设置对话框标题this.dialogFormVisible = true; // 显示对话框},edit(row) {this.isEdit = true; // 设置为编辑模式this.dialogTitle = '更新管理员信息'; // 设置对话框标题this.form = Object.assign({}, row); // 深拷贝行数据到表单//this.form=rowthis.dialogFormVisible = true; // 显示对话框},submit() {if (this.isSubmitting) return;this.isSubmitting = true;const api = this.isEdit ? "/test/updateUser" : "/test/adduser";request.post(api, this.form).then(res => {if (res.code == 200) {this.$message({message: this.isEdit ? '更新成功' : '新增成功',type: 'success'});this.dialogFormVisible = false;this.resetForm()this.findBySearch(1);} else {this.$message.error('出错了,请联系管理员');}}).catch(error => {console.error("请求失败", error);this.$message.error('网络错误,请稍后再试');}).finally(() => {this.isSubmitting = false;});},handleSizeChange(val) {this.params.pageSize = valthis.findBySearch()},handleCurrentChange(val) {this.params.pageNum = valthis.findBySearch()}},
}
</script>

后端新增和编辑代码  其实新增和编辑可以用一个接口  更具实体类id是否为null既可以判断是新增操作还是更新操作

持久层

package com.example.backend.mapper;import com.example.backend.pojo.User;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;import java.util.List;/*** @author hrui* @date 2024/3/13 5:28*/
public interface UserMapper{@Select("select * from t_user")List<User> selectAllUser();@Select({"<script>" +"select * from t_user" +"<where>"+"<if test='name!=null and name!=\"\" '>" +
//                "and name=#{name}"+"and name like concat('%',#{name},'%')"+"</if>"+"<if test='phone!=null and phone!=\"\" '>" +"and phone=#{phone}"+"</if>"+"</where>"+"</script>"})List<User> selectUserByCondition(User user);@Insert({"<script>","insert into t_user","<trim prefix='(' suffix=')' suffixOverrides=','>","id,","<if test='name!=null and name!=\"\"'>","name,","</if>","<if test='password!=null and password!=\"\"'>","password,","</if>","<if test='age!=null and age!=\"\"'>","age,","</if>","<if test='phone!=null and phone!=\"\"'>","phone,","</if>","</trim>","values","<trim prefix='(' suffix=')' suffixOverrides=','>","null,","<if test='name!=null and name!=\"\"'>","#{name},","</if>","<if test='password!=null and password!=\"\"'>","#{password},","</if>","<if test='age!=null and age!=\"\"'>","#{age},","</if>","<if test='phone!=null and phone!=\"\"'>","#{phone},","</if>","</trim>","</script>"})int insertUser(User user);@Update({"<script>","UPDATE t_user","<set>","<if test='name!=null and name!=\"\"'>","name = #{name},","</if>","<if test='password!=null and password!=\"\"'>","password = #{password},","</if>","<if test='age!=null and age!=\"\"'>","age = #{age},","</if>","<if test='phone!=null and phone!=\"\"'>","phone = #{phone},","</if>","</set>","WHERE id = #{id}","</script>"})int updateUser(User user);}

删除

删除操作一般需要提示用户确认

删除代码

<template><!-- 需要有一个根div --><div><div style="margin-bottom: 20px;"><el-input style="width: 300px;" v-model="params.name" placeholder="请输入姓名"></el-input><el-input style="width: 300px; margin-left: 20px;" v-model="params.phone" placeholder="请输入电话"></el-input><el-button style="margin-left: 20px;" @click="findBySearch(1)">查询</el-button><el-button style="margin-left: 20px;" @click="reset">清空</el-button><br><el-button type="primary" style="margin-top: 20px;" @click="addForm">新增</el-button><hr /></div><div><el-table :data="tableData" style="width: 100%"><el-table-column prop="name" label="姓名" width="180"></el-table-column><el-table-column prop="password" label="密码" width="180"></el-table-column><el-table-column prop="age" label="年龄"></el-table-column><el-table-column prop="phone" label="电话"></el-table-column><el-table-column label="操作"><template slot-scope="scope"><el-button type="primary" @click="edit(scope.row)">编辑</el-button><el-popconfirm title="确定要删除这条记录吗?" @confirm="deleteRow(scope.row.id)" confirm-button-text="确定"cancel-button-text="取消"><template #reference><el-button type="danger" style="margin-left: 10px;">删除</el-button></template></el-popconfirm></template></el-table-column></el-table></div><div><el-pagination style="margin-top: 20px;" @size-change="handleSizeChange"@current-change="handleCurrentChange" :current-page="params.pageNum" :page-sizes="[5, 10, 15, 20]":page-size="params.pageSize" layout="total, sizes, prev, pager, next, jumper" :total="totalNum"></el-pagination></div><div><el-dialog :title="dialogTitle" :visible.sync="dialogFormVisible" @close="resetForm"><el-form :model="form"><el-form-item label="姓名" :label-width="formLabelWidth"><el-input v-model="form.name" autocomplete="off"></el-input></el-form-item><el-form-item label="年龄" :label-width="formLabelWidth"><el-input v-model="form.age" autocomplete="off"></el-input></el-form-item><el-form-item label="密码" :label-width="formLabelWidth"><el-input v-model="form.password" autocomplete="off"></el-input></el-form-item><el-form-item label="电话" :label-width="formLabelWidth"><el-input v-model="form.phone" autocomplete="off"></el-input></el-form-item></el-form><div slot="footer" class="dialog-footer"><el-button @click="dialogFormVisible = false">取 消</el-button><el-button type="primary" @click="submit" :disabled="isSubmitting">确 定</el-button></div></el-dialog></div></div>
</template>
<script>
import request from '@/api/HttpRequest'
export default {name: "AdminView",data() {return {dialogFormVisible: false,isEdit: false, // 标志是编辑状态还是新增状态dialogTitle: '', // 对话框标题params: {name: '',phone: '',pageNum: 1,pageSize: 5,},totalNum: 0,tableData: [],form: {},formLabelWidth: '120px',isSubmitting: false  //提交标志}},created() {this.findBySearch()},methods: {resetForm() {this.form = {name: '',password: '',age: '',phone: ''};},reset() {this.params.name = ''this.params.phone = ''this.findBySearch()},findBySearch(val) {if (val == 1) {this.params.pageNum = 1this.params.pageSize = 5}request.get("/test/search", {params: this.params}).then(res => {if (res.code == 200) {this.tableData = res.data.listthis.totalNum = res.data.total} else {}})},addForm() {this.resetForm(); // 重置表单this.isEdit = false; // 设置为新增模式this.dialogTitle = '新增管理员信息'; // 设置对话框标题this.dialogFormVisible = true; // 显示对话框},edit(row) {this.isEdit = true; // 设置为编辑模式this.dialogTitle = '更新管理员信息'; // 设置对话框标题this.form = Object.assign({}, row); // 深拷贝行数据到表单//this.form=rowthis.dialogFormVisible = true; // 显示对话框},deleteRow(id) {// 这里应该调用API来删除row代表的数据// 例如:this.isSubmitting = true;request.delete(`/test/deleteUser/${id}`).then(res => {if (res.code == 200) {this.$message.success('删除成功');this.findBySearch(); // 重新加载当前页的数据或者根据需要更新视图} else {this.$message.error('删除失败,请稍后再试');}}).catch(error => {console.error("请求失败", error);this.$message.error('网络错误,请稍后再试');}).finally(() => {this.isSubmitting = false;});},submit() {if (this.isSubmitting) return;this.isSubmitting = true;const api = this.isEdit ? "/test/updateUser" : "/test/adduser";request.post(api, this.form).then(res => {if (res.code == 200) {this.$message({message: this.isEdit ? '更新成功' : '新增成功',type: 'success'});this.dialogFormVisible = false;this.resetForm()this.findBySearch(1);} else {this.$message.error('出错了,请联系管理员');}}).catch(error => {console.error("请求失败", error);this.$message.error('网络错误,请稍后再试');}).finally(() => {this.isSubmitting = false;});},handleSizeChange(val) {this.params.pageSize = valthis.findBySearch()},handleCurrentChange(val) {this.params.pageNum = valthis.findBySearch()}},
}
</script>

后端代码

持久层

关于#号

和历史模式有关

加上使用历史模式  mode:'history'就不会出现了

登录页面

路由级别

以上所有操作都在大布局下  路由也在同一级别

要想将登录页独立的一个页面

那么要将路由分级

首先个Layout.vue用来存放 原来app.vue路由

<template><div><el-container><el-header style="background-color:blanchedalmond; display: flex; align-items: center; padding-left: 0px;"><img src="@/assets/logo.jpg" alt="" style="width:250px; margin-left: 0px;"></el-header></el-container><el-container><el-aside style="overflow:hidden;min-height: 100vh;background-color:blanchedalmond;width: 250px;"><!-- default-active="1"默认是哪个 background-color="blanchedalmond" 背景颜色 text-color="brown" 字体颜色 active-text-color="blue" 选中字体颜色 --><el-menu :default-active="$route.path" router background-color="blanchedalmond" text-color="brown"active-text-color="blue"><el-menu-item index="/"><i class="el-icon-menu"></i><span slot="title">系统首页</span></el-menu-item><el-submenu index="2"><template slot="title"><i class="el-icon-location"></i><span>用户管理</span></template><el-menu-item-group><el-menu-item index="/admin">管理员信息</el-menu-item><el-menu-item index="2-2">用户信息</el-menu-item></el-menu-item-group></el-submenu><el-submenu index="3"><template slot="title"><i class="el-icon-location"></i><span>信息管理</span></template><el-menu-item-group><el-menu-item index="3-1">xxxx信息</el-menu-item><el-menu-item index="3-2">yyyy信息</el-menu-item></el-menu-item-group></el-submenu></el-menu></el-aside><el-main><!-- 我是入口 --><router-view /></el-main></el-container></div>
</template><script>
export default {name: 'Layout'}
</script><style>
.el-menu{border-right: none !important;
}
</style>

将app.vue即程序入口还原成原来样子

<template><div id="app"><router-view/></div>
</template><style></style>

代码结构

修改路由 router下的index.js文件

import Vue from 'vue'
import VueRouter from 'vue-router'
import HomeView from '../views/HomeView.vue'
import LoginView from '../views/LoginView.vue'
Vue.use(VueRouter)const routes = [{path: '/login',name: 'login',component: LoginView},{path: '/',name: 'Layout',component: () => import('../views/Layout.vue'),children:[{path: '/',name: 'home',component: HomeView},{path: '/about',name: 'about',//懒加载component: () => import('../views/AboutView.vue')},{path: '/message',name: 'message',//懒加载component: () => import('../views/MessageView.vue')},{path: '/admin',name: 'admin',//懒加载component: () => import('../views/AdminView.vue')}]}, 
]const router = new VueRouter({mode: 'history',routes
})export default router

编写登录页面 loginView.vue

访问http://localhost:8081/login

全局异常处理

全局异常处理@ControllerAdvice可以指定包名或者类名

package com.example.backend.exception;import com.example.backend.common.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;import javax.servlet.http.HttpServletRequest;/*** @author hrui* @date 2024/3/14 11:26*/
@ControllerAdvice
@Slf4j
public class GlobalExceptionHandler {@ExceptionHandler(Exception.class)@ResponseBodypublic Result error(HttpServletRequest request,Exception e){log.error("异常信息:", e);return Result.error("系统异常", null);}@ExceptionHandler(CustomException.class)@ResponseBodypublic Result customError(HttpServletRequest request,CustomException e){log.error("CustomException异常信息:", e.getMessage());return Result.error(e.getMessage(), null);}}

注册页面

<template><div class="container"><div class="register-box"><form @submit.prevent="onRegister" class="register-form"><h2>注册</h2><div class="form-group"><input v-model="registerForm.name" type="text" placeholder="用户名" @blur="checkUsernameAvailability"required><span v-if="usernameAvailable" style="color: red;">{{ usernameErrorMessage }}</span></div><div class="form-group"><input v-model="registerForm.password" type="password" placeholder="密码" required></div><div class="form-group"><input v-model="registerForm.phone" type="text" placeholder="手机号" required></div><div class="form-group action"><!-- <button type="button" @click="triggerSlideVerification">获取短信验证码</button> --><button @click="triggerSlideVerification" :disabled="isSMSButtonDisabled">{{ smsButtonText }}</button></div><div class="form-group action"><button type="submit">注册</button></div><div class="form-group action"><button type="button" @click="goToLogin">已有账号?登录</button></div></form></div></div>
</template><script>
import request from '@/api/HttpRequest'export default {name: 'RegisterView',data() {return {registerForm: {name: '',password: '',phone: ''},usernameAvailable: false,//用户名是否可用标识usernameErrorMessage: '',//不可用显示// 添加滑动验证的可见性状态isSlideVerificationVisible: false, // 控制滑动验证窗口的显示isSMSButtonDisabled: false, // 控制获取短信验证码按钮的可用状态countdown: 0,smsButtonText: '获取短信验证码',};},methods: {triggerSlideVerification() {// 显示滑动验证(这里仅为示例,具体实现取决于所用的滑动验证库)this.isSlideVerificationVisible = true;request.get("/api/checkimg").then(res=>{if(res.code==200){console.log("data",res.data)}})// 假设滑动验证通过后调用 getSMSCode 方法this.getSMSCode();//this.startCountdown(); // 假设滑动验证通过},getSMSCode() {// 在这里调用获取短信验证码的APIconsole.log("滑动验证通过,现在获取短信验证码");// 假设滑动验证通过,隐藏滑动验证弹窗this.slideVerifyVisible = false;},startCountdown() {if (this.countdown > 0) return; // 防止重复点击this.isSMSButtonDisabled = true; // 禁用获取短信验证码的按钮this.countdown = 60;let intervalId = setInterval(() => {this.countdown--;this.smsButtonText = `${this.countdown}秒后可重发`;if (this.countdown <= 0) {clearInterval(intervalId);this.smsButtonText = '获取短信验证码';this.isSMSButtonDisabled = false; // 启用按钮this.isSlideVerificationVisible = false; // 隐藏滑动验证窗口}}, 1000);},onRegister() {// 首先检查用户名是否可用if (!this.usernameAvailable) {this.$message.error('用户名不可用,请更换用户名');return;}// 发送POST请求到后端的注册APIrequest.post('/api/register', this.registerForm).then(response => {// 处理注册成功的情况if (response.code == 200) {this.$message.success('注册成功');// 注册成功后,可以选择跳转到登录页面或其他页面this.$router.push('/login');} else {// 后端返回了错误状态,显示错误信息this.$message.error(response.msg || '注册失败');}}).catch(error => {console.error('注册请求失败', error);this.$message.error('注册请求失败,请稍后再试');});},checkUsernameAvailability() {//alert(111)if (this.registerForm.name.trim() === '') {this.usernameAvailable = true;this.usernameErrorMessage = '用户名不能为空';return;}request.get('/api/usernamecheck', {params: {name: this.registerForm.name}}).then(response => {if (response.code === 200) {this.usernameAvailable = false;this.usernameErrorMessage = '';} else {this.usernameAvailable = true;this.usernameErrorMessage = response.msg;}}).catch(error => {console.error('请求错误', error);this.$message.error('验证用户名时发生错误');});},goToLogin() {// 跳转到登录页面this.$router.push('/login');}},
};
</script><style scoped>
.container {display: flex;justify-content: center;align-items: center;height: 100vh;
}.register-box {width: 100%;max-width: 400px;padding: 20px;border: 1px solid #ccc;border-radius: 5px;box-shadow: 0 2px 4px rgba(0, 0, 0, .1);
}.register-form {display: flex;flex-direction: column;
}.form-group {margin-bottom: 20px;
}input[type="text"],
input[type="password"] {width: 100%;padding: 10px;border: 1px solid #ccc;border-radius: 4px;
}.action {display: flex;justify-content: center;
}button[type="submit"],
button[type="button"] {cursor: pointer;padding: 10px 20px;margin-right: 10px;background-color: #007bff;border: none;border-radius: 4px;color: white;
}h2 {text-align: center;margin-bottom: 20px;
}
</style>

用户登录之后到home页面 右上角退出功能

路由守卫

以上的页面  其实任何页面都可以进去

路由守卫就是对路由跳转进行身份验证(权限控制)

就是说你的地址符合我定义的规则 就允许访问

在路由配置文件里配置路由守卫

但是这种方式很不安全  因为用户可以自己设置

JWT  Token验证

就是因为上面路由守卫不安全   还是需要后端验证

大概思路是用户在登录页面登录之后  服务端会返回一个Token

客户端每次请求需要在请求头携带该Token,后端验证

通过就是可以访问,不通过返回登录页

给所有接口加上统一的调用前缀/api然后统一拦截该前缀的的请求

这样在前端的baseUrl中也需要添加

配置WebMvcConfigurer

 

JWT工具类

package com.example.backend.common;import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil;
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.example.backend.pojo.User;
import com.example.backend.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;import javax.annotation.PostConstruct;
import javax.servlet.http.HttpServletRequest;
import java.util.Date;/*** @author hrui* @date 2024/3/15 0:54*/
@Component
@Slf4j
public class JwtTokenUtils {//主要是为了静态方法中使用Spring管理的bean  赋值后可以在静态方法这种使用  用起来方便 private static UserService staticUserService;@Autowiredprivate UserService userService;@PostConstructpublic void setUserService(){staticUserService=userService;}/*** 生成Token*/public static String genToken(String userId,String sign){return JWT.create().withAudience(userId)//将用户id保存到token做为荷载.withExpiresAt(DateUtil.offsetMinute(new Date(), 15))//15分钟过期.sign(Algorithm.HMAC256(sign));//以password做为Token密钥}/*** 获取当前登录用户信息*//*** 获取当前登录的用户信息*/public static User getCurrentUser() {String token = null;try {HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();token = request.getHeader("token");if (StrUtil.isBlank(token)) {token = request.getParameter("token");}if (StrUtil.isBlank(token)) {log.error("获取头部token为空,token: {}", token);return null;}// 解析token,获取用户的idString userId = JWT.decode(token).getAudience().get(0);return staticUserService.findById(Integer.valueOf(userId));} catch (Exception e) {log.error("获取当前登录的用户信息失败, token={}", token, e);return null;}}}

将token放到user中

前端处理

那么每次请求过来在拦截器进行校验

文件上传和下载

CREATE TABLE `t_book` (
  `id` int NOT NULL AUTO_INCREMENT COMMENT '主键',
  `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '书名',
  `price` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '价格',
  `author` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '作者',
  `press` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '出版社',
  `image` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '封面',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=13 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

前端代码
<template><!-- 需要有一个根div --><div><div style="margin-bottom: 20px;"><el-input style="width: 300px;" v-model="params.name" placeholder="请输入书名"></el-input><el-input style="width: 300px; margin-left: 20px;" v-model="params.press" placeholder="请输入出版社"></el-input><el-button style="margin-left: 20px;" @click="findBySearch(1)">查询</el-button><el-button style="margin-left: 20px;" @click="reset">清空</el-button><br><el-button type="primary" style="margin-top: 20px;" @click="addForm">新增</el-button><hr /></div><div><el-table :data="tableData" style="width: 100%"><el-table-column prop="name" label="书名" width="180"></el-table-column><el-table-column prop="price" label="价格" width="180"></el-table-column><el-table-column prop="author" label="作者"></el-table-column><el-table-column prop="press" label="出版社"></el-table-column><el-table-column label="图片封面"><template slot-scope="scope"><el-image style="width: 100px; height: 100px;border-radius: 50%;" :src="'http://localhost:8085/api/file/' + scope.row.image"   :preview-src-list="['http://localhost:8085/api/file/' + scope.row.image]"></el-image></template></el-table-column><el-table-column label="操作"><template slot-scope="scope"><el-button type="primary" @click="edit(scope.row)">编辑</el-button><el-popconfirm title="确定要删除这条记录吗?" @confirm="deleteRow(scope.row.id)" confirm-button-text="确定"cancel-button-text="取消"><template #reference><el-button type="danger" style="margin-left: 10px;">删除</el-button></template></el-popconfirm></template></el-table-column></el-table></div><div><el-pagination style="margin-top: 20px;" @size-change="handleSizeChange"@current-change="handleCurrentChange" :current-page="params.pageNum" :page-sizes="[5, 10, 15, 20]":page-size="params.pageSize" layout="total, sizes, prev, pager, next, jumper" :total="totalNum"></el-pagination></div><div><el-dialog :title="dialogTitle" :visible.sync="dialogFormVisible" @close="resetForm"><el-form :model="form"><el-form-item label="书名" :label-width="formLabelWidth"><el-input v-model="form.name" autocomplete="off"></el-input></el-form-item><el-form-item label="价格" :label-width="formLabelWidth"><el-input v-model="form.price" autocomplete="off"></el-input></el-form-item><el-form-item label="作者" :label-width="formLabelWidth"><el-input v-model="form.author" autocomplete="off"></el-input></el-form-item><el-form-item label="出版社" :label-width="formLabelWidth"><el-input v-model="form.press" autocomplete="off"></el-input></el-form-item><el-form-item label="图书封面" :label-width="formLabelWidth"><!-- :headers="uploadHeaders" 可以动态加上请求头token  或者 后端放行 --><el-upload class="upload-demo" action="http://localhost:8085/api/file/upload":headers="uploadHeaders" :on-error="errorUpload" :on-success="successUpload"><el-button size="small" type="primary">点击上传</el-button></el-upload></el-form-item></el-form><div slot="footer" class="dialog-footer"><el-button @click="dialogFormVisible = false">取 消</el-button><el-button type="primary" @click="submit" :disabled="isSubmitting">确 定</el-button></div></el-dialog></div></div>
</template>
<script>
import request from '@/api/HttpRequest'
export default {name: "BookView",data() {return {dialogFormVisible: false,isEdit: false, // 标志是编辑状态还是新增状态dialogTitle: '', // 对话框标题params: {name: '',press: '',pageNum: 1,pageSize: 5,},uploadHeaders: {'token': sessionStorage.getItem('token')},totalNum: 0,tableData: [],form: {},formLabelWidth: '120px',isSubmitting: false  //提交标志}},created() {this.findBySearch()},methods: {//文件上传成功后的钩子  successUpload(res) {//console.log(res)if (res.code == 200) {this.form.image = res.data}},errorUpload(res) {console(res)},resetForm() {this.form = {name: '',price: '',author: '',press: '',image: ''};},reset() {this.params.name = ''this.params.press = ''this.findBySearch()},findBySearch(val) {if (val == 1) {this.params.pageNum = 1this.params.pageSize = 5}request.get("/book/findBooks", {params: this.params}).then(res => {if (res.code == 200) {this.tableData = res.data.listthis.totalNum = res.data.total} else {}})},addForm() {this.resetForm(); // 重置表单this.isEdit = false; // 设置为新增模式this.dialogTitle = '新增管理员信息'; // 设置对话框标题this.dialogFormVisible = true; // 显示对话框},edit(row) {this.isEdit = true; // 设置为编辑模式this.dialogTitle = '更新管理员信息'; // 设置对话框标题this.form = Object.assign({}, row); // 深拷贝行数据到表单//this.form=rowthis.dialogFormVisible = true; // 显示对话框},deleteRow(id) {// 这里应该调用API来删除row代表的数据// 例如:this.isSubmitting = true;request.delete(`/book/deleteBook/${id}`).then(res => {if (res.code == 200) {this.$message.success('删除成功');this.findBySearch(); // 重新加载当前页的数据或者根据需要更新视图} else {this.$message.error('删除失败,请稍后再试');}}).catch(error => {this.$message.error('网络错误,请稍后再试');}).finally(() => {this.isSubmitting = false;});},submit() {if (this.isSubmitting) return;this.isSubmitting = true;const api = this.isEdit ? "/book/save" : "/book/save";request.post(api, this.form).then(res => {if (res.code == 200) {this.$message({message: this.isEdit ? '更新成功' : '新增成功',type: 'success'});this.dialogFormVisible = false;this.resetForm()this.findBySearch(1);} else {this.$message.error('出错了,请联系管理员');}}).catch(error => {this.$message.error('网络错误,请稍后再试');}).finally(() => {this.isSubmitting = false;});},handleSizeChange(val) {this.params.pageSize = valthis.findBySearch()},handleCurrentChange(val) {this.params.pageNum = valthis.findBySearch()}},
}
</script>
后端代码

控制器

package com.example.backend.controller;import com.example.backend.common.Result;
import com.example.backend.pojo.Book;
import com.example.backend.service.BookService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;/*** @author hrui* @date 2024/3/15 23:24*/
@RestController
@RequestMapping("/book")
public class BookController {@Autowiredprivate BookService bookService;@RequestMapping("/save")public Result saveBook(@RequestBody Book book){
//        if(book.getId()==null){
//            int i = bookService.insertBook(book);
//            if(i>0){
//                return Result.success("新增成功", null);
//            }else{
//                return Result.error("新增失败", null);
//            }
//        }else{
//            int i=bookService.updateBook(book);
//            if(i>0){
//                return Result.success("更新成功", null);
//            }else{
//                return Result.error("更新失败", null);
//            }
//        }boolean success = book.getId() == null ? bookService.insertBook(book) > 0 : bookService.updateBook(book) > 0;return success ? Result.success("操作成功", null) : Result.error("操作失败", null);}@RequestMapping("/findBooks")public Result findBooks(Book book){return Result.success("查询成功", bookService.findBooks(book));}@RequestMapping("/deleteBook/{id}")public Result deleteBookById(@PathVariable Integer id){return bookService.deleteBookById(id)>0?Result.success("删除成功", null):Result.error("删除失败", null);}
}

业务层

package com.example.backend.service.impl;import com.example.backend.mapper.BookMapper;
import com.example.backend.pojo.Book;
import com.example.backend.pojo.User;
import com.example.backend.service.BookService;
import com.github.pagehelper.Page;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;import java.util.List;/*** @author hrui* @date 2024/3/15 23:25*/
@Service
public class BookServiceImpl implements BookService {@Autowiredprivate BookMapper bookMapper;@Overridepublic int insertBook(Book book) {return bookMapper.insertBook(book);}@Overridepublic int updateBook(Book book) {return bookMapper.updateBook(book);}@Overridepublic PageInfo<Book> findBooks(Book book) {Page<Object> objects = PageHelper.startPage(book.getPageNum(), book.getPageSize());return new PageInfo<>(bookMapper.selectBookByCondition(book));}@Overridepublic int deleteBookById(Integer id) {return bookMapper.deleteBookById(id);}
}

持久层

package com.example.backend.mapper;import com.example.backend.pojo.Book;
import com.example.backend.pojo.User;
import org.apache.ibatis.annotations.Delete;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;import java.util.List;/*** @author hrui* @date 2024/3/15 23:24*/
public interface BookMapper {@Insert({"<script>","insert into t_book","<trim prefix='(' suffix=')' suffixOverrides=','>","id,","<if test='name!=null and name!=\"\"'>","name,","</if>","<if test='price!=null and price!=\"\"'>","price,","</if>","<if test='author!=null and author!=\"\"'>","author,","</if>","<if test='press!=null and press!=\"\"'>","press,","</if>","<if test='image!=null and image!=\"\"'>","image,","</if>","</trim>","values","<trim prefix='(' suffix=')' suffixOverrides=','>","null,","<if test='name!=null and name!=\"\"'>","#{name},","</if>","<if test='price!=null and price!=\"\"'>","#{price},","</if>","<if test='author!=null and author!=\"\"'>","#{author},","</if>","<if test='press!=null and press!=\"\"'>","#{press},","</if>","<if test='image!=null and image!=\"\"'>","#{image},","</if>","</trim>","</script>"})int insertBook(Book book);@Update({"<script>","UPDATE t_book","<set>","<if test='name!=null and name!=\"\"'>","name = #{name},","</if>","<if test='price!=null and price!=\"\"'>","price = #{price},","</if>","<if test='author!=null and author!=\"\"'>","author = #{author},","</if>","<if test='press!=null and press!=\"\"'>","press = #{press},","</if>","<if test='image!=null and image!=\"\"'>","image = #{image},","</if>","</set>","WHERE id = #{id}","</script>"})int updateBook(Book book);//    @Select("select * from t_book")
//    List<Book> selectAllBooks();@Select({"<script>" +"select * from t_book" +"<where>"+"<if test='name!=null and name!=\"\" '>" +
//                "and name=#{name}"+"and name like concat('%',#{name},'%')"+"</if>"+"<if test='price!=null and price!=\"\" '>" +"and price=#{price}"+"</if>"+"<if test='author!=null and author!=\"\" '>" +"and author=#{author}"+"</if>"+"<if test='press!=null and press!=\"\" '>" +
//            "and press=#{press}"+"and press like concat('%',#{press},'%')"+"</if>"+"</where>"+"</script>"})List<Book> selectBookByCondition(Book book);@Delete("DELETE FROM t_book WHERE id = #{id}")int deleteBookById(Integer id);
}

文件上传

控制器

package com.example.backend.controller;import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.StrUtil;
import com.example.backend.common.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;import javax.servlet.http.HttpServletResponse;
import java.io.OutputStream;
import java.net.URLEncoder;
import java.util.List;/*** @author hrui* @date 2024/3/16 1:47*/
@RestController
@RequestMapping("/file")
@Slf4j
public class FileController {//文件上传存储路径private static final String filePath=System.getProperty("user.dir")+"/upload/";/*** 文件上传*/@PostMapping("/upload")public Result upload(MultipartFile file) {synchronized (FileController.class) {//加锁 确保文件名不重复String flag = System.currentTimeMillis() + "";//原始文件名String fileName = file.getOriginalFilename();try {if (!FileUtil.isDirectory(filePath)) {FileUtil.mkdir(filePath);}// 文件存储位置;原逻辑-文件名FileUtil.writeBytes(file.getBytes(), filePath + flag + "-" + fileName);System.out.println(fileName + " --上传成功");//Thread.sleep(1L);} catch (Exception e) {System.err.println(fileName + " --文件上传失败");}return Result.success("上传成功",flag);}}/*** 文件下载*/// @GetMapping映射HTTP GET请求到特定的处理方法
// @PathVariable用来接收请求URL中的flag值@GetMapping("/{flag}")public void avatarPath(@PathVariable String flag, HttpServletResponse response) {// 检查filePath是否是目录,如果不是则创建if (!FileUtil.isDirectory(filePath)) {FileUtil.mkdir(filePath);}// 用于写入响应流的输出流OutputStream os;// 获取filePath路径下的所有文件名List<String> fileNames = FileUtil.listFileNames(filePath);// 从文件名列表中筛选包含flag的文件名,如果没有找到则返回空字符串String avatar = fileNames.stream().filter(name -> name.contains(flag)).findAny().orElse("");try {// 如果avatar不为空,则执行文件下载操作if (StrUtil.isNotEmpty(avatar)) {// 设置响应的头部信息,告诉浏览器这是一个附件下载操作,并提供文件名response.addHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(avatar, "UTF-8"));// 设置响应的内容类型为字节流response.setContentType("application/octet-stream");// 读取filePath和avatar组合后的文件路径的所有字节byte[] bytes = FileUtil.readBytes(filePath + avatar);// 获取响应的输出流os = response.getOutputStream();// 写入字节到响应输出流os.write(bytes);// 刷新输出流os.flush();// 关闭输出流os.close();}} catch (Exception e) {// 如果文件传输失败,打印失败信息System.out.println("文件传输失败");}}public static void main(String[] args) {List<String> fileNames = FileUtil.listFileNames(filePath);}}

拦截器放行

实现批量删除

这里存在一个问题

实现Excel导入导出

版本不低于4.1.2

后端

package com.example.backend.controller;import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.io.IoUtil;
import cn.hutool.poi.excel.ExcelUtil;
import cn.hutool.poi.excel.ExcelWriter;
import com.example.backend.common.Result;
import com.example.backend.exception.ExcelEmptyException;
import com.example.backend.pojo.BookType;
import com.example.backend.service.BookTypeService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;/*** @author hrui* @date 2024/3/16 11:04*/
@RestController
@RequestMapping("/excel")
public class ExcelController {@Autowiredprivate BookTypeService bookTypeService;@RequestMapping("/bookTypeExcelExport")public void booTypeExcelExport(HttpServletResponse response) throws IOException {// 1. 从数据库中查询出所有数据List<BookType> list=bookTypeService.selectAllBookType();if(CollectionUtil.isEmpty(list)){throw new ExcelEmptyException("图书分类中没有数据");}// 2. 定义一个 List 和 Map<key,value> 出来,存储处理之后的数据,用于导出 list 集// 3. 遍历每一条数据,然后封装到 Map<key,value> key是列名  value是值  里,把这个 map 集到 list 集List<Map<String,Object>> list2=new ArrayList<>();for(BookType bt:list){Map<String,Object> row=new HashMap<>();row.put("图书名称", bt.getName());row.put("图书描述",bt.getDescription());list2.add(row);}// 4. 创建一个 ExcelWriter,把 list 集合放入到writer写出(生成数据)ExcelWriter writer= ExcelUtil.getWriter(true);writer.write(list,true);// 5. 把这个 excel 下载下来response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");response.setHeader("Content-Disposition", "attachment;filename=bookType.xlsx");ServletOutputStream out = response.getOutputStream();writer.flush(out, true);writer.close();IoUtil.close(System.out);};@PostMapping("/upload")public Result excelUpload(MultipartFile file) throws IOException {List<BookType> types = ExcelUtil.getReader(file.getInputStream()).readAll(BookType.class);if(!CollectionUtil.isEmpty(types)){for(BookType t:types){try{bookTypeService.insertBookType(t);}catch (Exception e){e.printStackTrace();}}}return Result.success("导入成功", null);}
}

前端

<template><!-- 需要有一个根div --><div><div style="margin-bottom: 20px;"><el-input style="width: 300px;" v-model="params.name" placeholder="请输入名称"></el-input><el-input style="width: 300px; margin-left: 20px;" v-model="params.description"placeholder="请输入描述"></el-input><el-button style="margin-left: 20px;" @click="findBySearch(1)">查询</el-button><el-button style="margin-left: 20px;" @click="reset">清空</el-button><br><el-button type="primary" style="margin-top: 20px;" @click="addForm">新增</el-button><el-popconfirm title="确定要删除这些记录吗?" @confirm="deleteBatch" confirm-button-text="确定" cancel-button-text="取消"><template #reference><el-button type="danger" style="margin-left: 10px;">批量删除</el-button></template></el-popconfirm><el-button type="success" style="margin-left: 10px;" @click="excelExport">Excel导出</el-button><el-upload style="display: inline-block;margin-left: 10px;"  action="http://localhost:8085/api/excel/upload"  :headers="uploadHeaders"  :on-success="successUpload" :show-file-list="false"><el-button type="success">Excel导入</el-button></el-upload><hr /></div><div><el-table :data="tableData" style="width: 100%" @selection-change="handleSelectionChange" row-key="id"ref="table"><el-table-column type="selection" ref="table" :reserve-selection="true" width="55"></el-table-column><el-table-column prop="name" label="分类名称" width="180"></el-table-column><el-table-column prop="description" label="分类描述"></el-table-column><el-table-column label="操作"><template slot-scope="scope"><el-button type="primary" @click="edit(scope.row)">编辑</el-button><el-popconfirm title="确定要删除这条记录吗?" @confirm="deleteRow(scope.row.id)" confirm-button-text="确定"cancel-button-text="取消"><template #reference><el-button type="danger" style="margin-left: 10px;">删除</el-button></template></el-popconfirm></template></el-table-column></el-table></div><div><el-pagination style="margin-top: 20px;" @size-change="handleSizeChange"@current-change="handleCurrentChange" :current-page="params.pageNum" :page-sizes="[5, 10, 15, 20]":page-size="params.pageSize" layout="total, sizes, prev, pager, next, jumper" :total="totalNum"></el-pagination></div><div><el-dialog :title="dialogTitle" :visible.sync="dialogFormVisible" @close="resetForm"><el-form :model="form"><el-form-item label="分类名称" :label-width="formLabelWidth"><el-input v-model="form.name" autocomplete="off"></el-input></el-form-item><el-form-item label="分类描述" :label-width="formLabelWidth"><el-input v-model="form.description" autocomplete="off"></el-input></el-form-item></el-form><div slot="footer" class="dialog-footer"><el-button @click="dialogFormVisible = false">取 消</el-button><el-button type="primary" @click="submit" :disabled="isSubmitting">确 定</el-button></div></el-dialog></div></div>
</template>
<script>
import request from '@/api/HttpRequest'
export default {name: "BookTypeView",data() {return {uploadHeaders: {'token': sessionStorage.getItem('token')},dialogFormVisible: false,isEdit: false, // 标志是编辑状态还是新增状态dialogTitle: '', // 对话框标题params: {name: '',description: '',pageNum: 1,pageSize: 5,},totalNum: 0,tableData: [],form: {},formLabelWidth: '120px',isSubmitting: false,  //提交标志multipleSelection: []}},created() {this.findBySearch()},methods: {handleExceed(files, fileList) {this.$message.warning('只能上传一张图片');},//文件上传成功后的钩子  successUpload(res) {//console.log(res)if (res.code == 200) {this.$message.success(res.msg)this.findBySearch()}else{this.$message.error(res.msg)}},excelExport() {location.href='http://localhost:8085/api/excel/bookTypeExcelExport?token='+sessionStorage.getItem("token")  },handleSelectionChange(val) {//val本身是个数组 使用 map 函数遍历 val 数组,提取每行数据的 id,并保存到 multipleSelection 数组中this.multipleSelection = val.map(item => item.id);console.log(JSON.stringify(this.multipleSelection))},deleteBatch() {//判断multipleSelection是否为空if (this.multipleSelection.length == 0) {this.$message.warning("请勾选要删除的数据")return}request.put('/bookType/deleteBatch', this.multipleSelection).then(res => {if (res.code == 200) {this.$message.success("批量删除成功")this.findBySearch()} else {this.$message.success(res.msg)}}).catch(error => {this.$message.error('网络错误,请稍后再试');}).finally(() => {});},resetForm() {this.form = {name: '',description: '',};},reset() {this.params.name = ''this.params.description = ''this.findBySearch()},findBySearch(val) {if (val == 1) {this.params.pageNum = 1this.params.pageSize = 5}request.get("/bookType/findBookType", {params: this.params}).then(res => {if (res.code == 200) {this.tableData = res.data.listthis.totalNum = res.data.total} else {}})},addForm() {this.resetForm(); // 重置表单this.isEdit = false; // 设置为新增模式this.dialogTitle = '新增管理员信息'; // 设置对话框标题this.dialogFormVisible = true; // 显示对话框},edit(row) {this.isEdit = true; // 设置为编辑模式this.dialogTitle = '更新管理员信息'; // 设置对话框标题this.form = Object.assign({}, row); // 深拷贝行数据到表单//this.form=rowthis.dialogFormVisible = true; // 显示对话框},// deleteRow(id) {//     // 这里应该调用API来删除row代表的数据//     // 例如://     this.isSubmitting = true;//     request.delete(`/bookType/deleteBookType/${id}`).then(res => {//         if (res.code == 200) {//             this.$message.success('删除成功');//             this.findBySearch(); // 重新加载当前页的数据或者根据需要更新视图//         } else {//             this.$message.error('删除失败,请稍后再试');//         }//     }).catch(error => {//         this.$message.error('网络错误,请稍后再试');//     }).finally(() => {//         this.isSubmitting = false;//     });// },submit() {if (this.isSubmitting) return;this.isSubmitting = true;const api = this.isEdit ? "/bookType/save" : "/bookType/save";request.post(api, this.form).then(res => {if (res.code == 200) {this.$message({message: this.isEdit ? '更新成功' : '新增成功',type: 'success'});this.dialogFormVisible = false;this.resetForm()this.findBySearch(1);} else {this.$message.error('出错了,请联系管理员');}}).catch(error => {this.$message.error('网络错误,请稍后再试');}).finally(() => {this.isSubmitting = false;});},handleSizeChange(val) {this.params.pageSize = valthis.findBySearch()},handleCurrentChange(val) {this.params.pageNum = valthis.findBySearch()}},
}
</script>

模块之间关联关系

将图书分类和图书信息做关联  一般不用外键  主要是添加外键存在强耦合关系

图书分类对应多个图书信息

1对多

在多的一方建个字段关联图书分类表

展示时候数据也可以从后端关联查询出来

<template><!-- 需要有一个根div --><div><div style="margin-bottom: 20px;"><el-input style="width: 300px;" v-model="params.name" placeholder="请输入书名"></el-input><el-input style="width: 300px; margin-left: 20px;" v-model="params.press" placeholder="请输入出版社"></el-input><el-button style="margin-left: 20px;" @click="findBySearch(1)">查询</el-button><el-button style="margin-left: 20px;" @click="reset">清空</el-button><br><el-button type="primary" style="margin-top: 20px;" @click="addForm">新增</el-button><hr /></div><div><el-table :data="tableData" style="width: 100%"><el-table-column prop="name" label="书名" width="180"></el-table-column><el-table-column prop="price" label="价格" width="180"></el-table-column><el-table-column prop="author" label="作者"></el-table-column><el-table-column prop="press" label="出版社"></el-table-column><el-table-column label="图片封面"><template slot-scope="scope"><el-image style="width: 100px; height: 100px;border-radius: 50%;":src="'http://localhost:8085/api/file/' + scope.row.image":preview-src-list="['http://localhost:8085/api/file/' + scope.row.image]"></el-image></template></el-table-column><el-table-column label="图书类别" width="180"><template v-slot:default="scope"><!-- 这里假设每行的数据中有 typeId 字段,且options是所有类别的数组 -->{{ findTypeName(scope.row.typeId) }}</template></el-table-column><el-table-column label="操作"><template slot-scope="scope"><el-button type="primary" @click="edit(scope.row)">编辑</el-button><el-button type="primary" @click="down(scope.row.image)">下载</el-button><el-popconfirm title="确定要删除这条记录吗?" @confirm="deleteRow(scope.row.id)" confirm-button-text="确定"cancel-button-text="取消"><template #reference><el-button type="danger" style="margin-left: 10px;">删除</el-button></template></el-popconfirm></template></el-table-column></el-table></div><div><el-pagination style="margin-top: 20px;" @size-change="handleSizeChange"@current-change="handleCurrentChange" :current-page="params.pageNum" :page-sizes="[5, 10, 15, 20]":page-size="params.pageSize" layout="total, sizes, prev, pager, next, jumper" :total="totalNum"></el-pagination></div><div><el-dialog :title="dialogTitle" :visible.sync="dialogFormVisible" @close="resetForm"><el-form :model="form"><el-form-item label="书名" :label-width="formLabelWidth"><el-input v-model="form.name" autocomplete="off"></el-input></el-form-item><el-form-item label="价格" :label-width="formLabelWidth"><el-input v-model="form.price" autocomplete="off"></el-input></el-form-item><el-form-item label="作者" :label-width="formLabelWidth"><el-input v-model="form.author" autocomplete="off"></el-input></el-form-item><el-form-item label="出版社" :label-width="formLabelWidth"><el-input v-model="form.press" autocomplete="off"></el-input></el-form-item><el-form-item label="图书封面" :label-width="formLabelWidth"><!-- :headers="uploadHeaders" 可以动态加上请求头token  或者 后端放行 --><el-upload class="upload-demo" action="http://localhost:8085/api/file/upload":headers="uploadHeaders" :on-error="errorUpload" :on-success="successUpload" :limit="1":on-exceed="handleExceed"><el-button size="small" type="primary">点击上传</el-button></el-upload></el-form-item><el-form-item label="图书类别" :label-width="formLabelWidth"><el-select v-model="form.typeId" placeholder="请选择图书类别"><el-option v-for="item in options" :key="item.id" :label="item.name" :value="item.id"></el-option></el-select></el-form-item></el-form><div slot="footer" class="dialog-footer"><el-button @click="dialogFormVisible = false">取 消</el-button><el-button type="primary" @click="submit" :disabled="isSubmitting">确 定</el-button></div></el-dialog></div></div>
</template>
<script>
import request from '@/api/HttpRequest'
export default {name: "BookView",data() {return {dialogFormVisible: false,isEdit: false, // 标志是编辑状态还是新增状态dialogTitle: '', // 对话框标题params: {name: '',press: '',pageNum: 1,pageSize: 5,},uploadHeaders: {'token': sessionStorage.getItem('token')},totalNum: 0,tableData: [],form: {},formLabelWidth: '120px',isSubmitting: false,  //提交标志options: []}},created() {this.findBySearch()this.selectOptions()},methods: {findTypeName(typeId) {const type = this.options.find(option => option.id === typeId);return type ? type.name : '';},selectOptions() {request.get("/bookType/findBookTypes").then(res => {if (res.code == 200) {this.options = res.data//console.log(res.data)}})},down(imageName) {location.href = 'http://localhost:8085/api/file/' + imageName},handleExceed(files, fileList) {this.$message.warning('只能上传一张图片');},//文件上传成功后的钩子  successUpload(res) {//console.log(res)if (res.code == 200) {this.form.image = res.data}},errorUpload(res) {console(res)},resetForm() {this.form = {name: '',price: '',author: '',press: '',image: '',typeId: ''};},reset() {this.params.name = ''this.params.press = ''this.findBySearch()},findBySearch(val) {if (val == 1) {this.params.pageNum = 1this.params.pageSize = 5}request.get("/book/findBooks", {params: this.params}).then(res => {if (res.code == 200) {this.tableData = res.data.listthis.totalNum = res.data.total} else {}})},addForm() {this.resetForm(); // 重置表单this.isEdit = false; // 设置为新增模式this.dialogTitle = '新增管理员信息'; // 设置对话框标题this.dialogFormVisible = true; // 显示对话框},edit(row) {this.isEdit = true; // 设置为编辑模式this.dialogTitle = '更新管理员信息'; // 设置对话框标题this.form = Object.assign({}, row); // 深拷贝行数据到表单//this.form=rowthis.dialogFormVisible = true; // 显示对话框},deleteRow(id) {// 这里应该调用API来删除row代表的数据// 例如:this.isSubmitting = true;request.delete(`/book/deleteBook/${id}`).then(res => {if (res.code == 200) {this.$message.success('删除成功');this.findBySearch(); // 重新加载当前页的数据或者根据需要更新视图} else {this.$message.error('删除失败,请稍后再试');}}).catch(error => {this.$message.error('网络错误,请稍后再试');}).finally(() => {this.isSubmitting = false;});},submit() {if (this.isSubmitting) return;this.isSubmitting = true;const api = this.isEdit ? "/book/save" : "/book/save";request.post(api, this.form).then(res => {if (res.code == 200) {this.$message({message: this.isEdit ? '更新成功' : '新增成功',type: 'success'});this.dialogFormVisible = false;this.resetForm()this.findBySearch(1);} else {this.$message.error('出错了,请联系管理员');}}).catch(error => {this.$message.error('网络错误,请稍后再试');}).finally(() => {this.isSubmitting = false;});},handleSizeChange(val) {this.params.pageSize = valthis.findBySearch()},handleCurrentChange(val) {this.params.pageNum = valthis.findBySearch()}},
}
</script>

如果用关联表查询也可以

下面是通过id去查name

角色权限控制

例如某些页面的增删改查按钮  管理员角色可以操作   而普通用户只能浏览

某些页面管理员可以访问   但是可能普通用户不给予访问(看不到)

在用户表添加角色字段   这个字段用int  也可以 随自己

现在角色字段添加了  但是 登录admin或者张三 都是可以看到所有页面的

要做的是根据角色不同  所能看到的菜单不同

审核功能

请假申请  审核功能为例

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/755856.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

HCIE考证心得 | 在云校的学习收获颇多

我是来自深圳信息职业技术学院22级现代移动通信3-3班的冯同学&#xff0c;我在2023年12月12日通过了华为认证Cloud Service HCIE。在此&#xff0c;我将分享考证中的心得体会给大家。 备考的六点建议 一是要细心严谨&#xff0c;做实验时要全神贯注&#xff0c;明确实验要求…

Python-GEE绘制DEM精美图片

目录 上传矢量和DEM获取添加颜色条参考文章 先连接上GEE的自己的项目 import ee import geemap geemap.set_proxy(port33210) ee.Authenticate() ee.Initialize(projecta-flyllf0313)上传矢量和DEM获取 使用Google Earth Engine&#xff08;GEE&#xff09;和Google Earth Eng…

基于单片机的模糊PID炉温控制系统设计

摘 要 电热炉是在工业热处理的生产中广泛使用的一种设备&#xff0c;电热炉的温度控制系统存在时变性&#xff0c;非线性&#xff0c;滞后性等特征&#xff0c;难以用常规PID的控制器对系统达到很好的控制效果。当控温精度的要求高时&#xff0c;使用传统的控制理论方法难以达…

亮相AWE 2024,日立中央空调打造定制空气新体验

日立中央空调于3月14日携旗下空气定制全新成果&#xff0c;亮相2024中国家电及消费电子博览会&#xff08;简称AWE 2024&#xff09;现场&#xff0c;围绕“科创先行 智引未来”这一主题&#xff0c;通过技术与产品向行业与消费者&#xff0c;展现自身对于家居空气的理解。 展会…

kanzi颜色工作流程

线性和非线性伽玛色彩空间 RGB 颜色空间的目的是表示在计算机显示器上显示的颜色。目前&#xff0c;sRGB是非线性伽玛色彩空间的标准。之所以需要它&#xff0c;是因为人类对光的感知是非线性的&#xff0c;而且计算机显示器对光强度具有非线性响应。 人眼比浅色更能区分深色…

Android 13 源码编译及报错修复

下载AOSP指定分支 repo init -u git://aosp../platform/manifest -b android-13.0.0_r83 同步代码到本地 repo sync -c 初始化编译环境, 选择构建目标 source build/envsetup.sh lunch 选择需要构建的目标&#xff0c;此处以aosp_arm64-eng为例 进行固件编译 make -j12 期间编译…

力扣热门算法题 49. 字母异位词分组,50. Pow(x, n),51. N 皇后

49. 字母异位词分组&#xff0c;50. Pow(x, n)&#xff0c;51. N 皇后&#xff0c;每题做详细思路梳理&#xff0c;配套Python&Java双语代码&#xff0c; 2024.03.19 可通过leetcode所有测试用例。 目录 49. 字母异位词分组 解题思路 完整代码 python Java 50. Pow(x…

STM32F411 Micropython使用日记

1、开发板购买&#xff1a;推荐淘宝“无名科技Nologo” 19.8包邮到手&#xff1b;买开发板还需要买SPI NorFlash&#xff0c;推荐8MB的flash&#xff0c;不懂的可以问卖家&#xff0c;买回来需要焊接好&#xff0c;也可以找店家试试看能不能帮忙焊接&#xff0c;不然micropytho…

八节【DBA从入门到实践】课程,带你快速掌握OceanBase运维管理核心技能

为帮助用户及开发者更好、更快地掌握OceanBase DBA核心技能&#xff0c;OceanBase社区设计了配套教程——“DBA从入门到实践”。8期教程带大家循序渐进掌握OceanBase运维管理核心技能。搭配随堂习题和OceanBase技术专家在线答疑&#xff0c;快速掌握重要知识点&#xff0c;并轻…

【DL经典回顾】激活函数大汇总(二十七)(Bent Identity附代码和详细公式)

激活函数大汇总&#xff08;二十七&#xff09;&#xff08;Bent Identity附代码和详细公式&#xff09; 更多激活函数见激活函数大汇总列表 一、引言 欢迎来到我们深入探索神经网络核心组成部分——激活函数的系列博客。在人工智能的世界里&#xff0c;激活函数扮演着不可或…

直观与交互:山海鲸可视化软件与Excel传统表格的对比

作为一名长期使用Excel进行数据处理和分析的用户&#xff0c;最近我尝试了一款名为山海鲸的可视化软件&#xff0c;发现它与Excel传统表格之间存在诸多明显的差异。接下来&#xff0c;我将从个人体验视角出发&#xff0c;谈谈这两种工具的不同之处。 首先&#xff0c;从数据呈…

组件化开发

一、引言 Vue.js 的组件化开发是其核心特性之一&#xff0c;它允许开发者将复杂的界面拆分为可复用的、独立的、小的组件&#xff0c;从而提高开发效率和代码的可维护性。 二、关键点 1.组件的定义 在components下创建.vue文件timecard.vue用来编辑内容。 文件创建完毕后&am…

000_【基础篇】SpringBoot概述

介绍 springboot 是 spring 提供的一个子项目&#xff0c;用于快速构建 spring 应用程序 传统的 SSM 框架要导入很多依赖的 jar 包以及配置很多的配置文件&#xff0c;麻烦、繁琐 springboot 特性 springboot 主要&#xff08;还有其他的一些特性&#xff09;有起步依赖和…

精酿啤酒:开启时尚派对的钥匙

Fendi club啤酒&#xff0c;一个代表着时尚与品味的品牌&#xff0c;如今进入了啤酒市场&#xff0c;推出了名为“Fendi club”的啤酒。这一创新的举措不仅展现了品牌的多元化发展&#xff0c;更为消费者提供了一种全新的时尚生活方式。 Fendi club啤酒不仅仅是一种产品&#x…

【C++庖丁解牛】继承的概念及定义 | 继承中的作用域 | 继承与友元继承与静态成员 | 复杂的菱形继承及菱形虚拟继承

&#x1f341;你好&#xff0c;我是 RO-BERRY &#x1f4d7; 致力于C、C、数据结构、TCP/IP、数据库等等一系列知识 &#x1f384;感谢你的陪伴与支持 &#xff0c;故事既有了开头&#xff0c;就要画上一个完美的句号&#xff0c;让我们一起加油 目录 1.继承的概念及定义1.1继…

【漏洞复现】用友U8Cloud base64 SQL注入漏洞

0x01 产品简介 用友U8 Cloud是用友推出的新一代云ERP&#xff0c;主要聚焦成长型、创新型企业&#xff0c;提供企业级云ERP整体解决方案。 0x02 漏洞概述 用友U8 Cloud 存在SQL注入漏洞&#xff0c;未授权的攻击者可通过此漏洞获取数据库权限&#xff0c;从而盗取用户数据&a…

KVM安装-kvm彻底卸载-docker安装Webvirtmgr

KVM安装和使用 一、安装 检测硬件是否支持KVM需要硬件的支持,使用命令查看硬件是否支持KVM。如果结果中有vmx(Intel)或svm(AMD)字样,就说明CPU的支持的 egrep ‘(vmx|svm)’ /proc/cpuinfo关闭selinux将 /etc/sysconfig/selinux 中的 SELinux=enforcing 修改为 SELinux=d…

AI预测福彩3D第13弹【2024年3月19日预测--第3套算法重新开始计算第2次测试】

今天咱们继续对第3套算法进行第2次测试&#xff0c;第3套算法加入了012路的权重。废话不多说了&#xff0c;直接上结果吧~ 最终&#xff0c;经过研判分析&#xff0c;2024年3月19日福彩3D的七码预测结果如下&#xff1a; 百位&#xff1a;0 1 3 4 2 7 9&#xff08;6换9&#x…

LCR144翻转二叉树(力扣简单题,Java,递归+非递归)

目录 题目描述&#xff1a; 递归代码1&#xff1a; 递归代码2&#xff1a; 非递归代码&#xff08;层次遍历&#xff09;&#xff1a; 题目描述&#xff1a; 给定一棵二叉树的根节点 root&#xff0c;请左右翻转这棵二叉树&#xff0c;并返回其根节点。 示例 1&#xff1a;…

如何在iOS系统抓取log

前言&#xff1a;因为作者目前工作领域和苹果智能家居有关&#xff0c;然后发现一些bug其实是apple sdk原生code的问题&#xff0c;所以需要给apple提radar单&#xff0c;就需要抓ios端Log充当证据给apple看&#xff0c;其实ios抓log非常简单&#xff0c;大家感兴趣可以学习下哦…