项目是基于SpringBoot+Vue前后端分离的仓库管理系统
后端:SpringBoot + MybatisPlus
前端:Node.js + Vue + element-ui
数据库:mysql
-
创建项目模块
-
导入项目依赖
pom.xml
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.5</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.wms</groupId> <artifactId>Warehouse-System</artifactId> <version>0.0.1-SNAPSHOT</version> <name>Warehouse-System</name> <description>Warehouse management system</description> <properties> <java.version>11</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.30</version> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <!--mybatisPlus--> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.4.1</version> </dependency> <!--代码生成器--> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-generator</artifactId> <version>3.4.1</version> </dependency> <dependency> <groupId>org.freemarker</groupId> <artifactId>freemarker</artifactId> <version>2.3.30</version> </dependency> <dependency> <groupId>com.spring4all</groupId> <artifactId>spring-boot-starter-swagger</artifactId> <version>1.5.1.RELEASE</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
-
导入依赖
<!--mybatisPlus--> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.2</version> </dependency>
-
创建并连接数据库
-
配置端口和数据源
application.yml
server: port: 8002 spring: datasource: url: jdbc:mysql://localhost:3306/wms?useUnicode=true&characterEncoding=utf-8&serveTimezone=UTC driver-class-name: com.mysql.cj.jdbc.Driver username: root password: 123456
-
编写实体类
@Data public class User { private int id; private String no; private String name; private String password; private int sex; private int roleId; private String phone; private String isvalid; }
-
编写Mapper接口
@Mapper public interface UserMapper extends BaseMapper<User> { public List<User> selectAll(); }
-
编写Service接口
public interface UserService extends IService<User> { public List<User> selectAll(); }
-
编写Service实现类
@Service public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService { @Autowired private UserMapper userMapper; public List<User> selectAll(){ return userMapper.selectAll(); } }
-
编写配置文件
UserMapper.xml
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.wms.mapper.UserMapper"> <select id="selectAll" resultType="com.wms.entity.User"> select * from user </select> </mapper>
-
编写测试代码
@RestController public class testController { @Autowired private UserService userService; @GetMapping public List<User> test(){ return userService.selectAll(); } }
-
测试结果
简化开发:删除之前编写的实体类、接口、实现类、配置文件以及测试类,利用MyBatisPlus代码生成器自动生成代码
-
导入依赖
注意:MybatisPlus版本用3.4.1,3.5版本的MybatisPlus会报错!
<!--代码生成器--> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-generator</artifactId> <version>3.4.1</version> </dependency> <dependency> <groupId>org.freemarker</groupId> <artifactId>freemarker</artifactId> <version>2.3.30</version> </dependency>
-
编写代码生成器
参考MybatisPlus官网:https://baomidou.com/pages/d357af/
package com.wms.common; import com.baomidou.mybatisplus.core.exceptions.MybatisPlusException; import com.baomidou.mybatisplus.core.toolkit.StringPool; import com.baomidou.mybatisplus.core.toolkit.StringUtils; import com.baomidou.mybatisplus.generator.AutoGenerator; import com.baomidou.mybatisplus.generator.InjectionConfig; import com.baomidou.mybatisplus.generator.config.*; import com.baomidou.mybatisplus.generator.config.po.TableInfo; import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy; import com.baomidou.mybatisplus.generator.engine.FreemarkerTemplateEngine; import java.util.ArrayList; import java.util.List; import java.util.Scanner; public class CodeGenerator { /** * <p> * 读取控制台内容 * </p> */ public static String scanner(String tip) { Scanner scanner = new Scanner(System.in); StringBuilder help = new StringBuilder(); help.append("请输入" + tip + ":"); System.out.println(help.toString()); if (scanner.hasNext()) { String ipt = scanner.next(); if (StringUtils.isNotBlank(ipt)) { return ipt; } } throw new MybatisPlusException("请输入正确的" + tip + "!"); } public static void main(String[] args) { // 代码生成器 AutoGenerator mpg = new AutoGenerator(); // 全局配置 GlobalConfig gc = new GlobalConfig(); String projectPath = System.getProperty("user.dir"); gc.setOutputDir(projectPath + "/src/main/java"); gc.setAuthor("linsuwen"); gc.setOpen(false); gc.setSwagger2(true); //实体属性 Swagger2 注解 gc.setBaseResultMap(true); // XML ResultMap gc.setBaseColumnList(true); // XML columList //去掉service接口首字母的I, 如DO为User则叫UserService gc.setServiceName("%sService"); mpg.setGlobalConfig(gc); // 数据源配置 DataSourceConfig dsc = new DataSourceConfig(); dsc.setUrl("jdbc:mysql://localhost:3306/wms?useUnicode=true&characterEncoding=utf-8&serveTimezone=UTC"); // dsc.setSchemaName("public"); dsc.setDriverName("com.mysql.cj.jdbc.Driver"); dsc.setUsername("root"); dsc.setPassword("123456"); mpg.setDataSource(dsc); // 包配置 PackageConfig pc = new PackageConfig(); //pc.setModuleName(scanner("模块名")); //模块配置 pc.setParent("com.wms") .setEntity("entity") .setMapper("mapper") .setService("service") .setServiceImpl("service.Impl") .setController("controller"); mpg.setPackageInfo(pc); // 自定义配置 InjectionConfig cfg = new InjectionConfig() { @Override public void initMap() { // to do nothing } }; // 如果模板引擎是 freemarker String templatePath = "/templates/mapper.xml.ftl"; // 如果模板引擎是 velocity // String templatePath = "/templates/mapper.xml.vm"; // 自定义输出配置 List<FileOutConfig> focList = new ArrayList<>(); // 自定义配置会被优先输出 focList.add(new FileOutConfig(templatePath) { @Override public String outputFile(TableInfo tableInfo) { // 自定义输出文件名 , 如果你 Entity 设置了前后缀、此处注意 xml 的名称会跟着发生变化!! return projectPath + "/src/main/resources/mapper/" + pc.getModuleName() + "/" + tableInfo.getEntityName() + "Mapper" + StringPool.DOT_XML; } }); cfg.setFileOutConfigList(focList); mpg.setCfg(cfg); // 配置模板 TemplateConfig templateConfig = new TemplateConfig(); templateConfig.setXml(null); mpg.setTemplate(templateConfig); // 策略配置 StrategyConfig strategy = new StrategyConfig(); strategy.setNaming(NamingStrategy.underline_to_camel); strategy.setColumnNaming(NamingStrategy.underline_to_camel); //strategy.setSuperEntityClass("你自己的父类实体,没有就不用设置!"); strategy.setEntityLombokModel(true); strategy.setRestControllerStyle(true); // 公共父类 //strategy.setSuperControllerClass("你自己的父类控制器,没有就不用设置!"); // 写于父类中的公共字段 //strategy.setSuperEntityColumns("id"); strategy.setInclude(scanner("表名,多个英文逗号分割").split(",")); strategy.setControllerMappingHyphenStyle(true); //strategy.setTablePrefix(pc.getModuleName() + "_"); mpg.setStrategy(strategy); mpg.setTemplateEngine(new FreemarkerTemplateEngine()); mpg.execute(); } }
-
运行代码生成器自动生成代码
生成结果:
/*
* 新增用户
* @author linsuwen
* @date 2023/1/2 19:11
*/
@PostMapping("/save")
public boolean save(@RequestBody User user){
return userService.save(user);
}
/*
* 删除用户
* @author linsuwen
* @date 2023/1/2 19:15
*/
@GetMapping("/delete")
public boolean delete(Integer id){
return userService.removeById(id);
}
/*
* 更新用户
* @author linsuwen
* @date 2023/1/2 19:11
*/
@PostMapping("/update")
public boolean update(@RequestBody User user){
return userService.updateById(user);
}
/*
* 新增或修改:存在用户则修改,否则新增用户
* @author linsuwen
* @date 2023/1/2 19:12
*/
@PostMapping("/saveOrUpdate")
public boolean saveOrUpdate(@RequestBody User user){
return userService.saveOrUpdate(user);
}
/*
* 查询全部用户
* @author linsuwen
* @date 2023/1/2 19:26
*/
@GetMapping("/list")
public List<User> list(){
return userService.list();
}
/*
* 模糊查询
* @author linsuwen
* @date 2023/1/2 19:36
*/
@PostMapping("/listP")
public List<User> query(@RequestBody User user){
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(User::getName,user.getName());
return userService.list(wrapper);
}
-
参数的封装
注:可以不封装,在controller层用HashMap接收参数
package com.wms.common; /* * 分页参数的封装类 * @author linsuwen * @date 2023/1/2 19:53 */ @Data public class QueryPageParam { //设置默认值 private static int PAGE_SIZE=20; private static int PAGE_NUM=1; private int pageSize=PAGE_SIZE; private int pageNum=PAGE_NUM; private HashMap param = new HashMap(); }
-
添加分页拦截器
/* * MybatisPlus分页拦截器 * @author linsuwen * @date 2023/1/2 20:06 */ @Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }
-
编写分页的Mapper方法
/* * 分页查询 * @author linsuwen * @date 2023/1/2 19:48 */ @PostMapping("/lsitPage") public Result page(@RequestBody QueryPageParam query){ HashMap param = query.getParam(); String name = (String)param.get("name"); Page<User> page = new Page(); page.setCurrent(query.getPageNum()); page.setSize(query.getPageSize()); LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>(); wrapper.like(User::getName,name); IPage result = userService.page(page,wrapper); return Result.success(result.getRecords(),result.getTotal()); }
-
自定义SQL使用Wrapper
让前端收到统一的数据,方便处理
package com.wms.common;
import lombok.Data;
/*
* 返回前端统一数据的封装类
* @author linsuwen
* @date 2023/1/2 20:36
*/
@Data
public class Result {
private int code; //编码 200/400
private String msg; //成功/失败
private Long total; //总记录数
private Object data; //数据
public static Result fail(){
return result(400,"失败",0L,null);
}
public static Result success(){
return result(200,"成功",0L,null);
}
public static Result success(Object data){
return result(200,"成功",0L,data);
}
public static Result success(Object data,Long total){
return result(200,"成功",total,data);
}
private static Result result(int code,String msg,Long total,Object data){
Result res = new Result();
res.setData(data);
res.setMsg(msg);
res.setCode(code);
res.setTotal(total);
return res;
}
}
-
创建一个名为 Warehouse-System-Web 的工程
npm init #项目初始化命令 #如果想直接生成 package.json 文件,那么可以使用命令 npm init -y
-
安装依赖
#进入工程目录 cd Warehouse-System-Web #安装vue-router npm install vue-router --save-dev #安装element-ui npm i element-ui -S #安装依赖 npm install #安装SASS加载器 cnpm install sass-loader node-sass --save-dev
-
启动项目
#启动测试 npm run serve
参考Element-ui Container 布局容器:https://element.eleme.cn/#/zh-CN/component/container
Index.vue:整体页面布局 Vue 组件
从 Index.vue 中拆分出 Aside.vue 和 Header.vue 组件后,然后在 Index.vue 再导入拆分出去的组件
import Aside from "./Aside";
import Header from "./Header";
编写步骤:
- dropdown下拉
- 菜单伸缩图标
- 欢迎字样
- 去除背景,加入下拉框
代码实现:
<!--头部组件-->
<template>
<div style="display: flex;line-height: 60px;">
<div style="margin-top: 8px;">
<!--菜单伸缩-->
<i :class="icon" style="font-size: 20px;cursor: pointer;" @click="collapse"></i>
</div>
<div style="flex: 1;text-align: center;font-size: 34px;">、
<!--欢迎字样-->
<span>欢迎来到仓库管理系统</span>
</div>
<el-dropdown>
<!--dropdown下拉-->
<span>{{user.name}}</span><i class="el-icon-arrow-down" style="margin-left: 5px;"></i>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item @click.native="toUser">个人中心</el-dropdown-item>
<el-dropdown-item @click.native="logout">退出登录</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</div>
</template>
<script>
export default {
//...
}
</script>
<style scoped>
</style>
<!--侧边菜单栏组件-->
<template>
<el-menu
background-color="#545c64"
text-color="#fff"
active-text-color="#ffd04b"
style="height: 100%;"
default-active="/Home"
:collapse="isCollapse"
:collapse-transition="false"
router
>
<el-menu-item index="/Home">
<i class="el-icon-s-home"></i>
<span slot="title">首页</span>
</el-menu-item>
<el-menu-item :index="'/'+item.menuclick" v-for="(item,i) in menu" :key="i">
<i :class="item.menuicon"></i>
<span slot="title">{{item.menuname}}</span>
</el-menu-item>
</el-menu>
</template>
<script>
export default {
//...
}
</script>
<style scoped>
</style>
菜单导航页面伸缩思路:
- header点击图标提交
- 父组件改变
- aside子组件(collapse)
**Axios:**Axios是一个基于promise 的 HTTP 库,可以用在浏览器和 node.js中。
Ajax:Ajax即Asynchronous Javascript And XML(异步JavaScript和[ XML])在 2005年被Jesse James Garrett提出的新术语,用来描述一种使用现有技术集合的'新'方法,包括:HTML 或 XHTML,CSS,JavaScript,DOM,XML,XSLT以及最重要的 XMLHttpRequest。使用Ajax技术网页应用能够快速地将增量更新呈现在用户界面上,而不需要重载(刷新)整个页面,这使得程序能够更快地回应用户的操作。
Axios与Ajax的区别:
- axios是一个基于Promise的HTTP库,而ajax是对原生XHR的封装。
- ajax技术实现了局部数据的刷新,而axios实现了对ajax的封装。
**什么是跨域访问:**说到跨域访问,必须先解释一个名词:同源策略。所谓同源策略就是在浏览器端出于安全考量,向服务端发起请求必须满足:协议相同、Host(ip)相同、端口相同的条件,否则访问将被禁止,该访问也就被称为跨域访问。
虽然跨域访问被禁止之后,可以在一定程度上提高了应用的安全性,但也为开发带来了一定的麻烦。比如:我们开发一个前后端分离的易用,页面及js部署在一个主机的nginx服务中,后端接口部署在一个tomcat应用容器中,当前端向后端发起请求的时候一定是不符合同源策略的,也就无法访问。
SpringBoot下解决跨域问题的四种方式:
- 使用CorsFilter进行全局跨域配置
- 重写WebMvcConfigurer的addCorsMappings方法(全局跨域配置)
- 使用CrossOrigin注解(局部跨域配置)
- 使用HttpServletResponse设置响应头(局部跨域配置)
安装axios与跨域处理:
-
安装axios
npm install axios --save
-
在main.js全局引⼊axios
import axios from "axios"; Vue.prototype.$axios =axios;
-
解决跨域问题
重写WebMvcConfigurer的addCorsMappings方法(全局跨域配置):
package com.wms.common; /* * 解决跨域问题:重写WebMvcConfigurer的addCorsMappings方法(全局跨域配置) * @author linsuwen * @date 2023/1/3 1:30 */ @Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") //是否发送Cookie .allowCredentials(true) //放行哪些原始域 .allowedOriginPatterns("*") .allowedMethods(new String[]{"GET", "POST", "PUT", "DELETE"}) .allowedHeaders("*") .exposedHeaders("*"); } }
-
测试请求
get请求的使⽤:
this.$axios.get('http://localhost:8002/user/list').then(res=>{ console.log(res) })
post请求的使用:
this.$axios.post('http://localhost:8002/user/query',{}).then(res=>{ console.log(res) })
-
将地址设置为全局
main.js
Vue.prototype.$httpUrl='http://localhost:8002' //将地址设置为全局
-
编写登录页面
Login.vue
-
后台查询登录代码
/* * 用户登录 * @author linsuwen * @date 2023/1/3 14:08 */ @PostMapping("/login") public Result login(@RequestBody User user){ //匹配账号和密码 List<User> list = userService.lambdaQuery() .eq(User::getNo,user.getNo()) .eq(User::getPassword,user.getPassword()) .list(); return list.size()>0?Result.success(list.get(0)):Result.fail(); }
-
登录页面的路由跳转
安装路由插件
npm i vue-router@3.5.4
创建路由文件(router目录下的index.js文件),访问路由跳转到登录页面
import VueRouter from 'vue-router'; const routes = [ { path:'/', name:'login', component:()=>import('../components/Login') } ] const router = new VueRouter({ mode:'history', routes }) export default router;
在main.js中注册路由
import VueRouter from 'vue-router'; import router from './router'; Vue.use(VueRouter); new Vue({ router, render: h => h(App), }).$mount('#app')
-
主页的路由(接收路由)
App.vue
<template> <div id="app"> <router-view/> </div> </template> <script> export default { name: 'App', components: { } } </script> <style> #app { height: 100%; } </style>
-
登录成功后页面跳转到首页
...
-
展示名字(Header.vue)
<el-dropdown> <!--dropdown下拉--> <span>{{user.name}}</span><i class="el-icon-arrow-down" style="margin-left: 5px;"></i> <el-dropdown-menu slot="dropdown"> <el-dropdown-item @click.native="toUser">个人中心</el-dropdown-item> <el-dropdown-item @click.native="logout">退出登录</el-dropdown-item> </el-dropdown-menu> </el-dropdown> data(){ return { user : JSON.parse(sessionStorage.getItem('CurUser')) } }
-
退出登录事件
<el-dropdown-item @click.native="logout">退出登录</el-dropdown-item>
-
退出跳转、清空相关数据以及退出确认
logout(){ console.log('logout') this.$confirm('您确定要退出登录吗?', '提示', { confirmButtonText: '确定', //确认按钮的文字显示 type: 'warning', center: true, //文字居中显示 }) .then(() => { this.$message({ type:'success', message:'退出登录成功!' }) this.$router.push("/") sessionStorage.clear() }) .catch(() => { this.$message({ type:'info', message:'已取消退出登录!' }) }) }
-
编写页面
-
路由跳转(Header.vue)
<el-dropdown-item @click.native="toUser">个人中心</el-dropdown-item> methods:{ toUser(){ console.log('to_user') this.$router.push("/Home") } }
-
路由错误解决(router/index.js)
const VueRouterPush = VueRouter.prototype.push VueRouter.prototype.push = function push (to) { return VueRouterPush.call(this, to).catch(err => err) }
-
菜单增加router、高亮
<el-menu background-color="#545c64" text-color="#fff" active-text-color="#ffd04b" style="height: 100%;" default-active="/Home" :collapse="isCollapse" :collapse-transition="false" router >
-
配置子菜单
data(){ return { menu:[ { menuClick:'Admin', menuName:'管路员管理', menuIcon:'el-icon-s-custom' },{ menuClick:'User', menuName:'用户管理', menuIcon:'el-icon-user-solid' } ] } } }
-
模拟动态menu
<el-menu-item :index="'/'+item.menuClick" v-for="(item,i) in menu" :key="i"> <i :class="item.menuIcon"></i> <span slot="title">{{item.menuName}}</span> </el-menu-item>
vuex状态管理:
vuex是专为vue.js应用程序开发的状态管理模式。它采用集中存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。
Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。
状态管理有5个核心,分别是state、getter、mutation、action以及module。
动态路由的实现:
-
设计menu表和数据
CREATE TABLE `menu` ( `id` int NOT NULL, `menuCode` varchar(8) DEFAULT NULL COMMENT '菜单编码', `menuName` varchar(16) DEFAULT NULL COMMENT '菜单名字', `menuLevel` varchar(2) DEFAULT NULL COMMENT '菜单级别', `menuParentCode` varchar(8) DEFAULT NULL COMMENT '菜单的父code', `menuClick` varchar(16) DEFAULT NULL COMMENT '点击触发的函数', `menuRight` varchar(8) DEFAULT NULL COMMENT '权限 0超级管理员,1表示管理员,2表示普通用户,可以用逗号组合使用', `menuComponent` varchar(200) DEFAULT NULL, `menuIcon` varchar(100) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;
INSERT INTO `menu` VALUES (1, '001', '管理员管理', '1', NULL, 'Admin',
'0', 'admin/AdminManage.vue', 'el-icon-s-custom'); INSERT INTO `menu` VALUES (2, '002', '用户管理', '1', NULL, 'User', '0,1', 'user/UserManage.vue', 'el-icon-user-solid'); INSERT INTO `menu` VALUES (3, '003', '仓库管理', '1', NULL, 'Storage', '0,1', 'storage/StorageManage', 'el-icon-office-building'); INSERT INTO `menu` VALUES (4, '004', '物品分类管理', '1', NULL, 'Goodstype', '0,1', 'goodstype/GoodstypeManage', 'el-icon-menu'); INSERT INTO `menu` VALUES (5, '005', '物品管理 ', '1', NULL, 'Goods', '0,1,2', 'goods/GoodsManage', 'el-icon-s-management'); INSERT INTO `menu` VALUES (6, '006', '记录管理', '1', NULL, 'Record', '0,1,2', 'record/RecordManage', 'el-icon-s-order'); -
生成menu对应的后端代码
/* * 根据用户身份获取菜单列表 * @author linsuwen * @date 2023/1/3 20:48 */ @GetMapping("/list") public Result list(@RequestParam String roleId){ List list = menuService.lambdaQuery() .like(Menu::getMenuright,roleId) .list(); return Result.success(list); }
-
返回前端数据
思路:登录的时候一并查询 menu,这样只需要异步查询一次即可将菜单页面展示出来
修改后端UserController中的登录代码,将用户角色数据和用户角色对应的菜单数据返回给前端
/* * 用户登录 * @author linsuwen * @date 2023/1/3 14:08 */ @PostMapping("/login") public Result login(@RequestBody User user){ //匹配账号和密码 List<User> list = userService.lambdaQuery() .eq(User::getNo,user.getNo()) .eq(User::getPassword,user.getPassword()) .list(); if(list.size()>0){ User user1 = list.get(0); List<Menu> menuList = menuService.lambdaQuery() .like(Menu::getMenuright,user1.getRoleId()) .list(); HashMap res = new HashMap(); res.put("user",user1); res.put("menu",menuList); return Result.success(res); } return Result.fail(); }
-
vuex状态管理
安装vuex状态管理
npm i vuex@3.0.0
编写store
import vue from 'vue' import Vuex from 'vuex' vue.use(Vuex) //...
编写vue状态管理(store/index.js)
export default new Vuex.Store({ state: { menu: [] }, mutations: { setMenu(state,menuList) { state.menu = menuList addNewRoute(menuList) } }, getters: { getMenu(state) { return state.menu } } })
在main.js中注册
import store from "./store"
-
存储数据(Login.vue)
将登录时从后台查询到的菜单数据存储到vuex状态管理中
//存储 sessionStorage.setItem("CurUser",JSON.stringify(res.data.user)) console.log(res.data.menu) this.$store.commit("setMenu",res.data.menu)
-
生成menu数据
Aside.vue
<!--动态获取菜单--> <el-menu-item :index="'/'+item.menuclick" v-for="(item,i) in menu" :key="i"> <i :class="item.menuicon"></i> <span slot="title">{{item.menuname}}</span> </el-menu-item>
-
生成路由数据
获取路由列表(store/index.js)
let routes = router.options.routes
组装路由
routes.forEach(routeItem=>{ if(routeItem.path=="/Index"){ menuList.forEach(menu=>{ let childRoute = { path:'/'+menu.menuclick, name:menu.menuname, meta:{ title:menu.menuname }, component:()=>import('../components/'+menu.menucomponent) } routeItem.children.push(childRoute) }) } })
合并路由
router.addRoutes(routes)
错误处理
export function resetRouter() { router.matcher = new VueRouter({ mode:'history', routes: [] }).matcher }
-
列表数据
后端给前端返回列表数据
-
⽤tag转换列
数据库字段sex(0,1)=> 前端性别显示(男,女)
<template slot-scope="scope"> <el-tag :type="scope.row.sex === 1 ? 'primary' : 'success'" disable-transitions>{{scope.row.sex === 1 ? '男' : '女'}}</el-tag> </template>
-
header-cell-style设置表头样式
<el-table :data="tableData" :header-cell-style="{ background: '#f2f5fc', color: '#555555' }" >
-
加上边框
<el-table :data="tableData" :header-cell-style="{ background: '#f2f5fc', color: '#555555' }" border >
-
按钮(编辑、删除)
<el-table-column prop="operate" label="操作"> <template slot-scope="scope"> <el-button size="small" type="success" @click="mod(scope.row)">编辑</el-button> <el-popconfirm title="确定删除吗?" @confirm="del(scope.row.id)" style="margin-left: 5px;" > <el-button slot="reference" size="small" type="danger" >删除</el-button> </el-popconfirm> </template> </el-table-column>
-
后端返回结果封装(Result)
/* * 模糊查询 * @author linsuwen * @date 2023/1/2 19:36 */ @PostMapping("/listP") public Result query(@RequestBody User user){ LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>(); if(StringUtils.isNotBlank(user.getName())){ wrapper.like(User::getName,user.getName()); } return Result.success(userService.list(wrapper)); }
-
页面加上分页代码
<el-pagination @size-change="handleSizeChange" @current-change="handleCurrentChange" :current-page="pageNum" :page-sizes="[5, 10, 20,30]" :page-size="pageSize" layout="total, sizes, prev, pager, next, jumper" :total="total"> </el-pagination>
-
修改查询方法和参数
loadPost(){ this.$axios.post(this.$httpUrl+'/user/listPageC1',{ pageSize:this.pageSize, pageNum:this.pageNum, param:{ name:this.name, sex:this.sex } }).then(res=>res.data).then(res=>{ console.log(res) if(res.code==200){ this.tableData=res.data this.total=res.total }else{ alert('获取数据失败') } }) }
-
处理翻页、设置条数逻辑
handleSizeChange(val) { console.log(`每页 ${val} 条`); this.pageNum=1 this.pageSize=val this.loadPost() }, handleCurrentChange(val) { console.log(`当前页: ${val}`); this.pageNum=val this.loadPost() },
-
后端逻辑处理
@PostMapping("/listPageC") public List<User> listPageC(@RequestBody QueryPageParam query){ HashMap param = query.getParam(); String name = (String)param.get("name"); System.out.println("name=>"+(String)param.get("name")); Page<User> page = new Page(); page.setCurrent(query.getPageNum()); page.setSize(query.getPageSize()); LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper(); lambdaQueryWrapper.like(User::getName,name); IPage result = userService.pageCC(page,lambdaQueryWrapper); System.out.println("total=>"+result.getTotal()); return result.getRecords(); }
-
查询的布局(包含查询、重置按钮)
<div style="margin-bottom: 5px;"> <el-button type="primary" style="margin-left: 5px;" @click="loadPost">查询</el-button> <el-button type="success" @click="resetParam">重置</el-button> <el-button type="primary" style="margin-left: 5px;" @click="add">新增</el-button> </div>
-
输入框
<el-input v-model="name" placeholder="请输入名字" suffix-icon="el-icon-search" style="width: 200px;" @keyup.enter.native="loadPost"></el-input>
-
下拉框
<el-select v-model="sex" filterable placeholder="请选择性别" style="margin-left: 5px;"> <el-option v-for="item in sexs" :key="item.value" :label="item.label" :value="item.value"> </el-option> </el-select>
-
回车事件(查询)
@keyup.enter.native="loadPost"
-
重置处理
<el-button type="success" @click="resetParam">重置</el-button>
-
后端逻辑处理
/* * 查询功能:根据前端表单输入的信息或者下拉框选择查询用户,并以分页的形式返回前端 * @author linsuwen * @date 2023/1/4 20:28 */ @PostMapping("/listPageC1") public Result listPageC1(@RequestBody QueryPageParam query){ HashMap param = query.getParam(); String name = (String)param.get("name"); String sex = (String)param.get("sex"); String roleId = (String)param.get("roleId"); Page<User> page = new Page(); page.setCurrent(query.getPageNum()); page.setSize(query.getPageSize()); LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper(); if(StringUtils.isNotBlank(name) && !"null".equals(name)){ lambdaQueryWrapper.like(User::getName,name); } if(StringUtils.isNotBlank(sex)){ lambdaQueryWrapper.eq(User::getSex,sex); } if(StringUtils.isNotBlank(roleId)){ lambdaQueryWrapper.eq(User::getRoleId,roleId); } IPage result = userService.pageCC(page,lambdaQueryWrapper); System.out.println("total=="+result.getTotal()); return Result.success(result.getRecords(),result.getTotal()); }
-
新增按钮(Main.vue)
<el-button type="primary" style="margin-left: 5px;" @click="add">新增</el-button>
-
弹出窗口
add(){ this.centerDialogVisible = true this.$nextTick(()=>{ this.resetForm() }) }
-
编写表单(Main.vue)
<el-dialog title="提示" :visible.sync="centerDialogVisible" width="30%" center> <el-form ref="form" :rules="rules" :model="form" label-width="80px"> <el-form-item label="账号" prop="no"> <el-col :span="20"> <el-input v-model="form.no"></el-input> </el-col> </el-form-item> <el-form-item label="名字" prop="name"> <el-col :span="20"> <el-input v-model="form.name"></el-input> </el-col> </el-form-item> <el-form-item label="密码" prop="password"> <el-col :span="20"> <el-input v-model="form.password"></el-input> </el-col> </el-form-item> <el-form-item label="年龄" prop="age"> <el-col :span="20"> <el-input v-model="form.age"></el-input> </el-col> </el-form-item> <el-form-item label="性别"> <el-radio-group v-model="form.sex"> <el-radio label="1">男</el-radio> <el-radio label="0">女</el-radio> </el-radio-group> </el-form-item> <el-form-item label="电话" prop="phone"> <el-col :span="20"> <el-input v-model="form.phone"></el-input> </el-col> </el-form-item> </el-form> <span slot="footer" class="dialog-footer"> <el-button @click="centerDialogVisible = false">取 消</el-button> <el-button type="primary" @click="save">确 定</el-button> </span> </el-dialog>
-
提交数据(提示信息、列表刷新)
前端提交数据(Main.vue)
doSave({ this.$axios.post(this.$httpUrl+'/user/save',this.form).then(res=>res.data).then(res=>{ console.log(res) if(res.code==200){ this.$message({ message: '操作成功!', type: 'success' }); this.centerDialogVisible = false this.loadPost() //成功后刷新加载数据 this. resetForm() }else{ this.$message({ message: '操作失败!', type: 'error' }); } }) }
后端接收前端提交的数据并将数据存入数据库中
/* * 新增用户 * @author linsuwen * @date 2023/1/2 19:11 */ @PostMapping("/save") public Result save(@RequestBody User user){ return userService.save(user)?Result.success():Result.fail(); }
-
数据的检查
检查所输入数据(Main.vue)
rules: { no: [ {required: true, message: '请输入账号', trigger: 'blur'}, {min: 3, max: 8, message: '长度在 3 到 8 个字符', trigger: 'blur'}, {validator:checkDuplicate,trigger: 'blur'} ], name: [ {required: true, message: '请输入名字', trigger: 'blur'} ], password: [ {required: true, message: '请输入密码', trigger: 'blur'}, {min: 3, max: 8, message: '长度在 3 到 8 个字符', trigger: 'blur'} ], age: [ {required: true, message: '请输入年龄', trigger: 'blur'}, {min: 1, max: 3, message: '长度在 1 到 3 个位', trigger: 'blur'}, {pattern: /^([1-9][0-9]*){1,3}$/,message: '年龄必须为正整数字',trigger: "blur"}, {validator:checkAge,trigger: 'blur'} ], phone: [ {required: true,message: "手机号不能为空",trigger: "blur"}, {pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/, message: "请输入正确的手机号码", trigger: "blur"} ] }
let checkAge = (rule, value, callback) => { if(value>150){ callback(new Error('年龄输入过大')); }else{ callback(); } };
-
账号的唯一验证
检查账号是否已经存在(Main.vue)
let checkDuplicate =(rule,value,callback)=>{ if(this.form.id){ return callback(); } this.$axios.get(this.$httpUrl+"/user/findByNo?no="+this.form.no).then(res=>res.data).then(res=>{ if(res.code!=200){ callback() }else{ callback(new Error('账号已经存在')); } }) };
后端查询用户
/* * 根据账号查找用户 * @author linsuwen * @date 2023/1/4 14:53 */ @GetMapping("/findByNo") public Result findByNo(@RequestParam String no){ List list = userService.lambdaQuery() .eq(User::getNo,no) .list(); return list.size()>0?Result.success(list):Result.fail(); }
-
表单重置
-
传递数据到表单(Main.vue)
<el-button slot="reference" size="small" type="danger" >删除</el-button>
mod(row){ console.log(row) this.centerDialogVisible = true this.$nextTick(()=>{ //赋值到表单 this.form.id = row.id this.form.no = row.no this.form.name = row.name this.form.password = '' this.form.age = row.age +'' this.form.sex = row.sex +'' this.form.phone = row.phone this.form.roleId = row.roleId }) },
-
提交数据到后台
doMod(){ this.$axios.post(this.$httpUrl+'/user/update',this.form).then(res=>res.data).then(res=>{ console.log(res) if(res.code==200){ this.$message({ message: '操作成功!', type: 'success' }); this.centerDialogVisible = false this.loadPost() this. resetForm() }else{ this.$message({ message: '操作失败!', type: 'error' }); } }) }
-
后端逻辑处理
/* * 更新用户 * @author linsuwen * @date 2023/1/2 19:11 */ @PostMapping("/update") public Result update(@RequestBody User user){ return userService.updateById(user)?Result.success():Result.fail(); }
-
表单重置(异步)
mod(row){ console.log(row) this.centerDialogVisible = true this.$nextTick(()=>{ //赋值到表单 }) },
-
获取数据(id)
scope.row.id
-
删除确认(Main.vue)
<el-popconfirm title="确定删除吗?" @confirm="del(scope.row.id)" style="margin-left: 5px;" > <el-button slot="reference" size="small" type="danger" >删除</el-button> </el-popconfirm>
-
提交到后台
del(id){ console.log(id) this.$axios.get(this.$httpUrl+'/user/del?id='+id).then(res=>res.data).then(res=>{ console.log(res) if(res.code==200){ this.$message({ message: '操作成功!', type: 'success' }); this.loadPost() }else{ this.$message({ message: '操作失败!', type: 'error' }); } }) },
-
后端处理
/* * 删除用户 * @author linsuwen * @date 2023/1/2 19:15 */ @GetMapping("/del") public Result delete(Integer id){ return userService.removeById(id)?Result.success():Result.fail(); }
用户管理和管理员管理的区别在于 roleId 不同,所以代码可以复用
-
仓库表设计
CREATE TABLE `storage` ( `id` int NOT NULL AUTO_INCREMENT COMMENT '主键', `name` varchar(100) NOT NULL COMMENT '仓库名', `remark` varchar(1000) DEFAULT NULL COMMENT '备注', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;
-
根据表生成后端代码
使用代码生成器自动生成代码
-
编写后端增删改查代码
@RestController @RequestMapping("/storage") public class StorageController { @Autowired private StorageService storageService; /* * 新增仓库 * @author linsuwen * @date 2023/1/5 19:36 */ @PostMapping("/save") public Result save(@RequestBody Storage storage){ return storageService.save(storage)?Result.success():Result.fail(); } /* * 更新仓库 * @author linsuwen * @date 2023/1/5 19:38 */ @PostMapping("/update") public Result update(@RequestBody Storage storage){ return storageService.updateById(storage)?Result.success():Result.fail(); } /* * 删除仓库 * @author linsuwen * @date 2023/1/5 19:40 */ @GetMapping("/del") public Result del(@RequestParam String id){ return storageService.removeById(id)?Result.success():Result.fail(); } /* * 查询仓库列表 * @author linsuwen * @date 2023/1/5 19:42 */ @GetMapping("/list") public Result list(){ List list = storageService.list(); return Result.success(list); } /* * 模糊查询:根据输入查询仓库并以分页的形式展示 * @author linsuwen * @date 2023/1/5 19:43 */ @PostMapping("/listPage") public Result listPage(@RequestBody QueryPageParam query){ HashMap param = query.getParam(); String name = (String)param.get("name"); Page<Storage> page = new Page(); page.setCurrent(query.getPageNum()); page.setSize(query.getPageSize()); LambdaQueryWrapper<Storage> queryWrapper = new LambdaQueryWrapper<>(); if(StringUtils.isNotBlank(name) && !"null".equals(name)){ queryWrapper.like(Storage::getName,name); } IPage result = storageService.pageCC(page,queryWrapper); return Result.success(result.getRecords(),result.getTotal()); } }
-
postman测试查询代码
-
编写前端相关代码(storage/StorageManage.vue)
复用 user/UserManage.vue 部分代码,稍加修改
-
物品分类表设计
CREATE TABLE `goodstype` ( `id` int NOT NULL AUTO_INCREMENT COMMENT '主键', `name` varchar(100) NOT NULL COMMENT '分类名', `remark` varchar(1000) DEFAULT NULL COMMENT '备注', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;
-
根据表生成后端代码
使用代码生成器自动生成代码
-
编写后端增删改查代码
@RestController @RequestMapping("/goodstype") public class GoodstypeController { @Autowired private GoodstypeService goodstypeService; /* * 新增物品分类 * @author linsuwen * @date 2023/1/5 20:39 */ @PostMapping("/save") public Result save(@RequestBody Goodstype goodstype){ return goodstypeService.save(goodstype)?Result.success():Result.fail(); } /* * 更新物品分类 * @author linsuwen * @date 2023/1/5 20:41 */ @PostMapping("/update") public Result update(@RequestBody Goodstype goodstype){ return goodstypeService.updateById(goodstype)?Result.success():Result.fail(); } /* * 删除物品分类 * @author linsuwen * @date 2023/1/5 20:43 */ @GetMapping("/del") public Result del(@RequestParam String id){ return goodstypeService.removeById(id)?Result.success():Result.fail(); } /* * 查询物品分类列表 * @author linsuwen * @date 2023/1/5 21:06 */ @GetMapping("/list") public Result list(){ List list = goodstypeService.list(); return Result.success(list); } /* * 模糊查询:根据输入查询物品分类并以分页的形式展示 * @author linsuwen * @date 2023/1/5 21:13 */ @PostMapping("/listPage") public Result listPage(@RequestBody QueryPageParam query){ HashMap param = query.getParam(); String name = (String)param.get("name"); Page<Goodstype> page = new Page(); page.setCurrent(query.getPageNum()); page.setSize(query.getPageSize()); LambdaQueryWrapper<Goodstype> queryWrapper = new LambdaQueryWrapper(); if(StringUtils.isNotBlank(name) && !"null".equals(name)){ queryWrapper.like(Goodstype::getName,name); } IPage result = goodstypeService.pageCC(page,queryWrapper); return Result.success(result.getRecords(),result.getTotal()); } }
-
postman测试查询代码
-
编写前端相关代码(goodstype/GoodstypeManage.vue)
复用 StorageManage.vue 部分代码,稍加修改
-
物品表设计
CREATE TABLE `goods` ( `id` int NOT NULL AUTO_INCREMENT COMMENT '主键', `name` varchar(100) NOT NULL COMMENT '货名', `storage` int NOT NULL COMMENT '仓库', `goodsType` int NOT NULL COMMENT '分类', `count` int DEFAULT NULL COMMENT '数量', `remark` varchar(1000) DEFAULT NULL COMMENT '备注', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;
-
根据表生成后端代码
-
编写后端增删改查代码
@RestController @RequestMapping("/goods") public class GoodsController { @Autowired private GoodsService goodsService; /* * 新增物品 * @author linsuwen * @date 2023/1/6 12:12 */ @PostMapping("/save") public Result save(@RequestBody Goods goods){ return goodsService.save(goods)?Result.success():Result.fail(); } /* * 更新物品 * @author linsuwen * @date 2023/1/6 13:22 */ @PostMapping("/update") public Result update(@RequestBody Goods goods){ return goodsService.updateById(goods)?Result.success():Result.fail(); } /* * 删除物品 * @author linsuwen * @date 2023/1/6 13:24 */ @GetMapping("/del") public Result del(@RequestParam String id){ return goodsService.removeById(id)?Result.success():Result.fail(); } /* * 模糊查询:根据输入查询物品并以分页的形式展示 * @author linsuwen * @date 2023/1/6 13:31 */ @PostMapping("/listPage") public Result listPage(@RequestBody QueryPageParam query){ HashMap param = query.getParam(); String name = (String)param.get("name"); String goodstype = (String)param.get("goodstype"); String storage = (String)param.get("storage"); Page<Goods> page = new Page(); page.setCurrent(query.getPageNum()); page.setSize(query.getPageSize()); LambdaQueryWrapper<Goods> queryWrapper = new LambdaQueryWrapper<>(); if(StringUtils.isNotBlank(name) && !"null".equals(name)){ queryWrapper.like(Goods::getName,name); } if(StringUtils.isNotBlank(goodstype) && !"null".equals(goodstype)){ queryWrapper.like(Goods::getGoodstype,goodstype); } if(StringUtils.isNotBlank(storage) && !"null".equals(storage)){ queryWrapper.like(Goods::getStorage,storage); } IPage result = goodsService.pageCC(page,queryWrapper); return Result.success(result.getRecords(),result.getTotal()); } }
-
postman测试查询代码
-
编写前端相关代码(goods/GoodsManage.vue)
count: [ {required: true, message: '请输⼊数量', trigger: 'blur'}, {pattern: /^([1-9][0-9]*){1,4}$/,message: '数量必须为正整数字',trigger: "blur"}, {validator:checkCount,trigger: 'blur'} ],
let checkCount = (rule, value, callback) => { if(value>9999){ callback(new Error('数量输入过大')); }else{ callback(); } };
-
仓库和分类列表展示
<el-table-column prop="storage" label="仓库" width="160" :formatter="formatStorage"> </el-table-column> <el-table-column prop="goodstype" label="分类" width="160" :formatter="formatGoodstype"> </el-table-column>
formatStorage(row){ let temp = this.storageData.find(item=>{ return item.id == row.storage }) return temp && temp.name }, formatGoodstype(row){ let temp = this.goodstypeData.find(item=>{ return item.id == row.goodstype }) return temp && temp.name },
-
查询条件中增加仓库和分类的条件
-
表单中仓库和分类下拉实现
<el-select v-model="storage" placeholder="请选择仓库" style="margin-left: 5px;"> <el-option v-for="item in storageData" :key="item.id" :label="item.name" :value="item.id"> </el-option> </el-select> <el-select v-model="goodstype" placeholder="请选择分类" style="margin-left: 5px;"> <el-option v-for="item in goodstypeData" :key="item.id" :label="item.name" :value="item.id"> </el-option> </el-select>
-
记录表设计
CREATE TABLE `record` ( `id` int NOT NULL AUTO_INCREMENT COMMENT '主键', `goods` int NOT NULL COMMENT '货品id', `userId` int DEFAULT NULL COMMENT '取货人/补货人', `admin_id` int DEFAULT NULL COMMENT '操作人id', `count` int DEFAULT NULL COMMENT '数量', `createtime` datetime DEFAULT NULL COMMENT '操作时间', `remark` varchar(1000) DEFAULT NULL COMMENT '备注', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;
-
根据表生成后端代码
-
编写后端查询代码
-
编写前端相关代码
-
优化
-
列表展示商品名、仓库、分类名
-
按物品名查询、仓库、分类查询
- 表单编写
- 入库操作(记录、更新物品数量、自动填充时间)
- 用户选择
- 出入库权限控制
- 记录查询权限控制
SpringBoot + Vue项目部署到服务器上
系统Bug:刷新系统后会导致数据丢失
Bug分析:vuex持久化后,每当浏览器刷新就会丢失state中的数据
解决方法:保存这个state的数据
具体解决方法:
-
解决菜单丢失问题
安装插件vuex-persistedstate:
npm i vuex-persistedstate
引入(store/index.js):
import createPersistedState from 'vuex-persistedstate'
使用(store/index.js):
plugins:[createPersistedState()]
-
解决路由丢失问题(App.vue)
//解决前端刷新页面路由丢失问题 data(){ return{ user : JSON.parse(sessionStorage.getItem('CurUser')) } }, watch:{ '$store.state.menu':{ handler(val,old){ console.log(val) if(!old && this.user && this.user.no){ this.$store.commit('setMenu',val) } }, immediate: true } }