赞
踩
该项目主要运用spring boot,vue,Hadoop,搭建一个功能系统,实现登录注册账户,对多个虚拟机的管理操作,并且可以实现虚拟机的增删改查,一键免密,一键安装Hadoop等功能。如下:
SpringBoot+Vue前后端分离虚拟机管理系统
软件和环境会用到:
jdk1.8
mysql
node 16.150.0
navicat
idea2021.1.3
这里数据库,jdk等我就不详细讲,特别说明一下,我的前端后端都在idea上编写的,所以我单独说一下vue的安装搭建,vue的搭建详见我的博客https://blog.csdn.net/m0_53720901/article/details/125075349
这里我们增加了mybatis-plus的配置,因为我们后端使用spring boot会连接数据库,使用数据库保存更新数据。
然后增加了swagger,使用Swagger,就是把相关的信息存储在它定义的描述文件里面(yml或json格式),再通过维护这个描述文件可以去更新接口文档,以及生成各端代码。简单理解:swagger就是为了方便系统生成API接口文档的。
然后就是hutool ,JWT,远程连接Linux和SSH-2协议的包等的配置
<?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.5.9</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.example</groupId> <artifactId>vuesp</artifactId> <version>0.0.1-SNAPSHOT</version> <name>vuesp</name> <description>Demo project for Spring Boot</description> <properties> <java.version>1.8</java.version> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.2.2</version> </dependency> <dependency> <groupId>com.microsoft.sqlserver</groupId> <artifactId>mssql-jdbc</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <!-- mybatis-plus --> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.1</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-actuator-autoconfigure</artifactId> </dependency> <dependency> <groupId>org.apache.velocity</groupId> <artifactId>velocity</artifactId> <version>1.7</version> </dependency> <!--swagger--> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-boot-starter</artifactId> <version>3.0.0</version> </dependency> <!-- hutool --> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.7.20</version> </dependency> <dependency> <groupId>org.apache.poi</groupId> <artifactId>poi-ooxml</artifactId> <version>4.1.2</version> </dependency> <!-- JWT --> <dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>3.10.3</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-cache</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <!-- 远程连接Linux--> <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.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> <!-- SSH-2协议的包 --> <dependency> <groupId>ch.ethz.ganymed</groupId> <artifactId>ganymed-ssh2</artifactId> <version>262</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <excludes> <exclude> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </exclude> </excludes> </configuration> </plugin> </plugins> </build> <repositories> <repository> <id>nexus-aliyun</id> <name>nexus-aliyun</name> <url>http://maven.aliyun.com/nexus/content/groups/public/</url> <releases> <enabled>true</enabled> </releases> <snapshots> <enabled>false</enabled> </snapshots> </repository> </repositories> <pluginRepositories> <pluginRepository> <id>public</id> <name>aliyun nexus</name> <url>http://maven.aliyun.com/nexus/content/groups/public/</url> <releases> <enabled>true</enabled> </releases> <snapshots> <enabled>false</enabled> </snapshots> </pluginRepository> </pluginRepositories> </project>
后端代码项目结构
conmon: 配置与前端交互返回的数据提示和内容
config: 跨域和redis
controller: 控制层
entity: 实体类
service: service层,处理接收到的数据,主要作功能代码
mapper: 从service到mapper,主要实现数据库的增删改查方法的实现
spring boot的经典代码部分;controller,entity,service,HostServiceImpl,mapper以及mapper.xml我就不在文章展示了,详情请查看文末获取全部源码。这里就展示后端与前端跨域的代码以及结果封装部分代码
跨域部分代码CorsConfig
package com.example.vuesp.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import org.springframework.web.filter.CorsFilter; @Configuration public class CorsConfig { // 当前跨域请求最大有效时长。这里默认1天 private static final long MAX_AGE = 24 * 60 * 60; @Bean public CorsFilter corsFilter() { UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); CorsConfiguration corsConfiguration = new CorsConfiguration(); corsConfiguration.addAllowedOrigin("http://localhost:8080"); // 1 设置访问源地址 corsConfiguration.addAllowedHeader("*"); // 2 设置访问源请求头 corsConfiguration.addAllowedMethod("*"); // 3 设置访问源请求方法 corsConfiguration.setMaxAge(MAX_AGE); source.registerCorsConfiguration("/**", corsConfiguration); // 4 对接口配置跨域设置 return new CorsFilter(source); } }
结果封装部分代码conmon
因为是前后端分离的项目,所以我们有必要统一一个结果返回封装类,这样前后端交互的时候有个统一的标准,约定结果返回的数据是正常的或者遇到异常了。
Constants
package com.example.vuesp.common;
public interface Constants {
String CODE_200 = "200";//成功
String CODE_500 = "500";//系统错误
String CODE_401 = "401";//权限不足
String CODE_400 = "400";//参数错误
String CODE_600 = "600";//其他业务异常
}
Result
package com.example.vuesp.common; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; /** * 接口统一返回包装类 */ @Data @NoArgsConstructor @AllArgsConstructor public class Result { private String code; private String msg; private Object data; public static Result success() { return new Result(Constants.CODE_200, "", null); } public static Result success(Object data) { return new Result(Constants.CODE_200, "", data); } public static Result error(String code, String msg) { return new Result(code, msg, null); } public static Result error() { return new Result(Constants.CODE_500, "系统错误", null); } }
在前面搭建好前端的环境项目后,将前端项目复制到idea上,方便操作执行,首先在idea新建一个Java项目然后将前端项目文件放于大文件夹下,如图我的前端项目为vue01放于后端项目下
assets放前端所需要的图片
components放导航栏与上边框
router:设置路由–名字和资源映射起来
utils:与后端的请求配置
views;前端所有展示页面的代码
main.js:主要作用是初始化vue实例并使用需要的插件
1登录页面Login.vue
这里我们用了一个数据库保存了用户的账号密码,并进行绑定,密码错误则不能进行登录。
<template> <div class="wrapper1" > <div style="margin: 0px 0px;width: 200px;height: 20px;padding: 0; border-radius: 10px"> <div class="demo-image__placeholder" style="margin: 0px 0px;width: 400px;height: 40px;padding: 0px; border-radius: 100px"> <div class="block"> <el-image :src="require('../assets/logo.png')"></el-image> </div> </div> </div> <div style="margin: 10px 300px;width: 500px;height: 40px;padding: 20px; border-radius: 10px"> <div style="margin: 200px 70px"> <el-carousel height="450px" :interval="1000"> <el-carousel-item v-for="item in imgs" :key="item"> <img :src="item" alt="" style="width: 100%"> </el-carousel-item> </el-carousel> </div> </div> <div style="margin: 100px 900px; background-color: #fff; width: 500px;height: 450px; padding: 20px; border-radius: 20px"> <div style="margin: 20px 0; text-align: center; font-size: 24px;left: 400px"><b>登 录</b></div> <el-form :model="user" :rules="rules" ref="userForm"> <el-form-item prop="username"> <el-input size="medium" style="margin: 30px 0" prefix-icon="el-icon-user" v-model="user.username"></el-input> </el-form-item> <el-form-item prop="password"> <el-input size="medium" style="margin: 50px 0" prefix-icon="el-icon-lock" show-password v-model="user.password"></el-input> </el-form-item> <el-form-item style="margin: 30px 0; text-align: center;left: 400px"> <el-button type="primary" size="small" autocomplete="off" @click="login">登录</el-button> <el-button type="warning" size="small" autocomplete="off" @click="$router.push('/register')">注册</el-button> </el-form-item> </el-form> </div> </div> </template> <script> export default { name: "Login", data() { return { user: {}, rules: { username: [ {required: true, message: '请输入用户名', trigger: 'blur'}, {min: 3, max: 10, message: '长度在 3 到 5 个字符', trigger: 'blur'} ], password: [ {required: true, message: '请输入密码', trigger: 'blur'}, {min: 1, max: 20, message: '长度在 1 到 20 个字符', trigger: 'blur'} ], }, imgs: [ require('../assets/1.jpg'), require('../assets/2.jpg'), require('../assets/3.jpg') ], files: [] } }, methods: { login() { this.$refs['userForm'].validate((valid) => { if (valid) { // 表单校验合法 this.request.post("/user/login", this.user).then(res => { if(res.code === '200') { localStorage.setItem("user", JSON.stringify(res.data)) // 存储用户信息到浏览器 this.$router.push("/") this.$message.success("登录成功!") } else { this.$message.error(res.msg) } }) } }); } } } </script> <style > .wrapper1 { height: 100vh; background-image: linear-gradient(to bottom right, #C6F0F6 , #C6F0F6); overflow: hidden; } </style>
2注册页面Register.vue
这里我们做了一个小细节,两次设置密码必须一致。
<template> <div class="wrapper"> <div style="margin: 100px auto; background-color: #fff; width: 350px; height: 550px; padding: 20px; border-radius: 10px"> <div style="margin: 20px 0; text-align: center; font-size: 24px"><b>注 册</b></div> <el-form :model="user" :rules="rules" ref="userForm"> <el-form-item prop="nickname"> <el-input placeholder="请输入昵称" size="medium" style="margin: 5px 0" prefix-icon="el-icon-user" v-model="user.nickname"></el-input> </el-form-item> <el-form-item prop="username"> <el-input placeholder="请输入账号" size="medium" style="margin: 5px 0" prefix-icon="el-icon-user" v-model="user.username"></el-input> </el-form-item> <el-form-item prop="name"> <el-input placeholder="请输入集群名称" size="medium" style="margin: 5px 0" prefix-icon="el-icon-user" v-model="user.name"></el-input> </el-form-item> <el-form-item prop="number"> <el-input placeholder="请输入主机数" size="medium" style="margin: 5px 0" prefix-icon="el-icon-user" v-model="user.number"></el-input> </el-form-item> <el-form-item prop="password"> <el-input placeholder="请输入密码" size="medium" style="margin: 5px 0" prefix-icon="el-icon-lock" show-password v-model="user.password"></el-input> </el-form-item> <el-form-item prop="confirmPassword"> <el-input placeholder="请确认密码" size="medium" style="margin: 5px 0" prefix-icon="el-icon-lock" show-password v-model="user.confirmPassword"></el-input> </el-form-item> <el-form-item style="margin: 5px 0; text-align: right"> <el-button type="primary" size="small" autocomplete="off" @click="login">注册</el-button> <el-button type="warning" size="small" autocomplete="off" @click="$router.push('/login')">返回登录</el-button> </el-form-item> </el-form> </div> </div> </template> <script> export default { name: "Login", data() { return { user: {}, rules: { nickname: [ { required: true, message: '请输入昵称', trigger: 'blur' }, { min: 1, max: 10, message: '长度在 1 到 20 个字符', trigger: 'blur' } ], username: [ { required: true, message: '请输入账号', trigger: 'blur' }, { min: 3, max: 20, message: '长度在 3 到 10 个字符', trigger: 'blur' } ], name: [ { required: true, message: '请输入集群名称', trigger: 'blur' }, { min: 1, max: 20, message: '长度在 1 到 20 个字符', trigger: 'blur' } ], number: [ { required: true, message: '请输入主机数', trigger: 'blur' }, { min: 1, max: 1, message: '长度在 1 到 1 个数字', trigger: 'blur' } ], password: [ { required: true, message: '请输入密码', trigger: 'blur' }, { min: 1, max: 20, message: '长度在 1 到 20 个字符', trigger: 'blur' } ], confirmPassword: [ { required: true, message: '请输入密码', trigger: 'blur' }, { min: 1, max: 20, message: '长度在 1 到 20 个字符', trigger: 'blur' } ], } } }, methods: { login() { this.$refs['userForm'].validate((valid) => { if (valid) { // 表单校验合法 if (this.user.password !== this.user.confirmPassword) { this.$message.error("两次输入的密码不一致") return false } this.request.post("/user/register", this.user).then(res => { if(res.code === '200') { this.$message.success("注册成功") } else { this.$message.error(res.msg) } }) } }); } } } </script> <style> .wrapper { height: 100vh; background-image: linear-gradient(to bottom right, #FC466B , #3F5EFB); overflow: hidden; } </style>
1系统左边导航栏布局Aside.vue
<!--导航栏--> <template> <el-menu :default-openeds="['1', '3']" style="min-height: 100%; overflow-x: hidden" background-color="rgb(48, 65, 86)" text-color="#fff" active-text-color="#3DA6CC" :collapse-transition="false" :collapse="isCollapse" router > <div style="height: 60px; line-height: 60px; text-align: center"> <img src="../assets/压力.png" alt="" style="width: 27px; position: relative; top: 5px; right: 5px"> <b style="color: white" v-show="logoTextShow">Hadoop|压力怪</b> </div> <el-menu-item index="/"> <template slot="title"> <i class="el-icon-s-home"></i> <span slot="title">首页</span> </template> </el-menu-item> <el-submenu index="1"> <template slot="title"> <i class="el-icon-menu"></i> <span slot="title">集群概览</span> </template> <el-menu-item index="/user"> <i class="el-icon-s-custom"></i> <span slot="title">集群展示</span> </el-menu-item> <el-menu-item index="/Download"> <i class="el-icon-download"></i> <span slot="title">产品安装</span> </el-menu-item> <el-menu-item index="/Validation"> <i class="el-icon-loading"></i> <span slot="title">产品验证</span> </el-menu-item> </el-submenu> </el-menu> </template> <script> export default { name: "Aside", props: { isCollapse: Boolean, logoTextShow: Boolean } } </script> <style scoped> </style>
2系统顶上信息显示Header.vue
<template> <div style="line-height: 60px; display: flex"> <div style="flex: 2;"> </div> <el-dropdown style="width: 100px; cursor: pointer"> <div style="display: inline-block"> <img :src="require('../assets/1.jpg')" alt="" style="width: 30px; border-radius: 50%; position: relative; top: 10px; right: 5px"> <span style="font-weight:bold">{{ user.nickname }}</span><i class="el-icon-arrow-down" style="margin-left: 5px"></i> </div> <el-dropdown-menu slot="dropdown" style="width: 100px; text-align: center"> <!-- <el-dropdown-item style="font-size: 14px; padding: 5px 0">--> <!-- <router-link to="/person">个人信息</router-link>--> <!-- </el-dropdown-item>--> <el-dropdown-item style="font-size: 14px; padding: 5px 0"> <span style="text-decoration: none" @click="logout">退出</span> </el-dropdown-item> </el-dropdown-menu> </el-dropdown> </div> </template> <script> export default { name: "Header", props: { collapseBtnClass: String, collapse: Boolean, }, data() { return { user: localStorage.getItem("user") ? JSON.parse(localStorage.getItem("user")) : {} } }, methods: { logout() { this.$router.push("/login") localStorage.removeItem("user") this.$message.success("退出成功") } } } </script> <style scoped> </style>
3首页Home.vue
<!--主页--> <template> <div class="wrapper3" > <el-row style="top: 100px;left: 450px;"> <!-- <span>压力怪</span>--> <span style="font-weight:bold;font-size: 35px;" type="success">压力怪!程序猿的选择~</span> </el-row> <el-row style="top: 470px;left: 1000px;"> <!-- <span>压力怪</span>--> <el-button type="primary" round @click="$router.push('/User')"> <i class="el-icon-d-arrow-right" style="font-weight:bold;font-size: 35px">开始使用</i> </el-button> </el-row> </div> </template> <script> export default { name: "Home", } </script> <style> .wrapper3 { height: 85vh; background:#fff url(../assets/t.webp) top left/1300px 650px no-repeat; /*repeat 10px 0px*/ overflow: hidden; } </style>
4集群展示页面
<!--集群页面--> <template> <div> <div style="margin-bottom: 20px"> <el-breadcrumb separator="/"> <el-button type="info" style="font-size: 18px" @click="$router.push('/User')"> <i class="el-icon-back"></i>返回</el-button> </el-breadcrumb> </div> <div style="margin: 10px 0"> <el-input style="width: 200px" placeholder="请输入主机名称" suffix-icon="el-icon-search" v-model="hostname"></el-input> <el-button class="ml-5" type="primary" @click="load">搜索</el-button> </div> <div style="margin: 10px 0"> <el-button type="warning" @click="handleAdd">新增主机<i class="el-icon-circle-plus-outline"></i></el-button> <el-button type="primary" @click="pass">一键免密钥 <i class="el-icon-remove-outline"></i></el-button> <el-button type="primary" @click="jdk">一键JDK <i class="el-icon-remove-outline"></i></el-button> <el-button type="primary" @click="$router.push('/Download')">产品安装<i class="el-icon-download"></i></el-button> <el-button type="primary" @click="$router.push('/Validation')">产品验证<i class="el-icon-loading"></i></el-button> </div> <el-table :data="tableData" border stripe :header-cell-class-name="headerBg"> <el-table-column prop="id" label="序号" width="200"> </el-table-column> <el-table-column prop="hostname" label="主机名" width="250"> </el-table-column> <el-table-column prop="ip" label="IP" width="200"> </el-table-column> <el-table-column prop="role" label="节点" width="245"> </el-table-column> <el-table-column prop="password" label="主机密码" width="200"> </el-table-column> <el-table-column label="操作" width="200" align="center"> <template slot-scope="scope"> <el-popconfirm class="ml-5" confirm-button-text='确定' cancel-button-text='我再想想' icon="el-icon-info" icon-color="red" title="您确定删除吗?" @confirm="del(scope.row.id)" > <el-button type="danger" slot="reference">删除主机<i class="el-icon-remove-outline"></i></el-button> </el-popconfirm> </template> </el-table-column> </el-table> <!--翻页--> <div style="padding: 10px 0"> <el-pagination @size-change="handleSizeChange" @current-change="handleCurrentChange" :current-page="pageNum" :page-sizes="[2, 5, 10, 20]" :page-size="pageSize" layout="total, sizes, prev, pager, next, jumper" :total="total"> </el-pagination> </div> <el-dialog title="集群信息" :visible.sync="dialogFormVisible" width="30%" > <el-form label-width="80px" size="small"> <el-form-item label="主机名"> <el-input v-model="form.hostname" autocomplete="off"></el-input> </el-form-item> <el-form-item label="IP"> <el-input v-model="form.ip" autocomplete="off"></el-input> </el-form-item> <el-form-item label="节点"> <el-input v-model="form.role" autocomplete="off"></el-input> </el-form-item> <el-form-item label="主机密码"> <el-input v-model="form.password" 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="save">确 定</el-button> </div> </el-dialog> </div> </template> <script> export default { name: "User", data() { return { tableData: [], total: 0, pageNum: 1, pageSize: 2, hostname: "", form: {}, dialogFormVisible: false, headerBg: 'headerBg' } }, created() { // 请求分页查询数据 this.load() }, methods: { load() { this.request.get("/host/page", { params: { pageNum: this.pageNum, pageSize: this.pageSize, hostname: this.hostname, } }).then(res => { console.log(res) this.tableData = res.records this.total = res.total }) }, handleAdd() { this.dialogFormVisible = true this.form = {} }, save() { this.request.post("/host", this.form).then(res => { if (res) { this.$message.success("保存成功") this.dialogFormVisible = false this.load() } else { this.$message.error("保存失败") } }) }, del(id) { this.request.delete("/host/" + id).then(res => { if (res) { this.$message.success("删除成功") this.load() } else { this.$message.error("删除失败") } }) }, handleSizeChange(pageSize) { console.log(pageSize) this.pageSize = pageSize this.load() }, handleCurrentChange(pageNum) { console.log(pageNum) this.pageNum = pageNum this.load() }, pass() { this.request.get("/pass").then(res => { if(res.code === '200') { this.$message.success("结果:"+res.data) } else { this.$message.error("失败") } }) }, jdk() { this.request.get("/jdk").then(res => { if(res.code === '200') { this.$message.success("结果:"+res.data) } else { this.$message.error("失败") } }) }, } } </script> <style> .headerBg { background: #eee!important; } </style>
项目做得不是很完美很好,有许多功能还未完善,也有许多bug但是自学vue做得这样子还是将就,博友们可以参考然后改进,欢迎提出意见与问题。欢迎交流,因为源码太多,需获取源码请评论。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。