前端项目创建
准备工作 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: 8085
spring:
application:
name: backend
redis:
##redis 单机环境配置
##将docker脚本部署的redis服务映射为宿主机ip
##生产环境推荐使用阿里云高可用redis服务并设置密码
host: 127.0.0.1
port: 6379
database: 0
password: 123456
ssl: false
##redis 集群环境配置
#cluster:
# nodes: 127.0.0.1:7001,127.0.0.1:7002,127.0.0.1:7003
# commandTimeout: 5000
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://xxxxx:3306/springboot?useSSL=false&useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=GMT%2B8&useCursorFetch=true
username: xxxxx
password: xxxxxx
mybatis:
type-aliases-package: com.example.backend.pojo
mapper-locations: classpath:mappers/*Mapper
configuration:
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中获取token
const 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;
// 若返回的是字符串则转换成json
if (typeof res === 'string') {
res = res ? JSON.parse(res) : res//将JSON字符串转JS对象 方便点出来
}
return res;
},
error => {
console.log('err' + error) // for debug
return 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 = 1
this.params.pageSize = 5
}
request.get("/test/search", {
params: this.params
}).then(res => {
if (res.code == 200) {
this.tableData = res.data.list
this.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=row
this.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 = val
this.findBySearch()
},
handleCurrentChange(val) {
this.params.pageNum = val
this.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 = 1
this.params.pageSize = 5
}
request.get("/test/search", {
params: this.params
}).then(res => {
if (res.code == 200) {
this.tableData = res.data.list
this.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=row
this.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 = val
this.findBySearch()
},
handleCurrentChange(val) {
this.params.pageNum = val
this.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)
@ResponseBody
public Result error(HttpServletRequest request,Exception e){
log.error("异常信息:", e);
return Result.error("系统异常", null);
}
@ExceptionHandler(CustomException.class)
@ResponseBody
public 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() {
// 在这里调用获取短信验证码的API
console.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请求到后端的注册API
request.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;
@Autowired
private UserService userService;
@PostConstruct
public 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,获取用户的id
String 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 = 1
this.params.pageSize = 5
}
request.get("/book/findBooks", {
params: this.params
}).then(res => {
if (res.code == 200) {
this.tableData = res.data.list
this.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=row
this.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 = val
this.findBySearch()
},
handleCurrentChange(val) {
this.params.pageNum = val
this.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 {
@Autowired
private 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 {
@Autowired
private BookMapper bookMapper;
@Override
public int insertBook(Book book) {
return bookMapper.insertBook(book);
}
@Override
public int updateBook(Book book) {
return bookMapper.updateBook(book);
}
@Override
public PageInfo<Book> findBooks(Book book) {
Page<Object> objects = PageHelper.startPage(book.getPageNum(), book.getPageSize());
return new PageInfo<>(bookMapper.selectBookByCondition(book));
}
@Override
public 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 {
@Autowired
private 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 = 1
this.params.pageSize = 5
}
request.get("/bookType/findBookType", {
params: this.params
}).then(res => {
if (res.code == 200) {
this.tableData = res.data.list
this.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=row
this.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 = val
this.findBySearch()
},
handleCurrentChange(val) {
this.params.pageNum = val
this.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 = 1
this.params.pageSize = 5
}
request.get("/book/findBooks", {
params: this.params
}).then(res => {
if (res.code == 200) {
this.tableData = res.data.list
this.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=row
this.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 = val
this.findBySearch()
},
handleCurrentChange(val) {
this.params.pageNum = val
this.findBySearch()
}
},
}
</script>
如果用关联表查询也可以
下面是通过id去查name
角色权限控制
例如某些页面的增删改查按钮 管理员角色可以操作 而普通用户只能浏览
某些页面管理员可以访问 但是可能普通用户不给予访问(看不到)
在用户表添加角色字段 这个字段用int 也可以 随自己
现在角色字段添加了 但是 登录admin或者张三 都是可以看到所有页面的
要做的是根据角色不同 所能看到的菜单不同
审核功能
请假申请 审核功能为例