简单宿舍管理系统(springboot+vue)
最近看了springboot和vue,为了练一下把前后端打通就自己手动写个简单的系统,测试一下,把代码放在仓库。
1.创建项目
1.前端
我的前端项目名叫Dormitory,然后添加插件element-plus(页面设计)和axios(后端交互)。
npm init vue@latest#这里插件下载我都选no,之后自己会手动下载使用
cd Dormitory
npm install
npm install element-plus
npm install axios
npm run dev
2.数据库
首先创建库,第一个是登陆功能,我就顺便创建一个简单的用户表t_user。
mysql -u root -p
create database dormitory;
use dormitory;
use dormitory;
CREATE TABLE t_user (
uid INT AUTO_INCREMENT COMMENT '用户id',
username VARCHAR(20) NOT NULL UNIQUE COMMENT '用户名',
password CHAR(32) NOT NULL COMMENT '密码',
role INT COMMENT '角色',
name VARCHAR(20) NOT NULL COMMENT '姓名',
gender INT COMMENT '性别:0-女,1-男',
telephone VARCHAR(50) COMMENT '手机号',
PRIMARY KEY (uid)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
3.后端
创建项目名叫dormitory_b,依赖库我用了三个(spring web,mybatis framework,mysql driver),然后配置一下jdk和maven环境和xml即可。
2.登陆
登陆这里我把uid和role保存到后端session中,username保存到前端cookie中,而且密码啥的我也没加密,就怎么简单怎么来。
1.前端
1.准备工作
这里我用到element-plus和icon,而且我是按需引入,所以首先下载插件:
npm install -D unplugin-vue-components unplugin-auto-import
npm install @element-plus/icons-vue
然后在vite.config.ts按需引入element-plus:
import { fileURLToPath, URL } from 'node:url'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [
vue(),
AutoImport({
resolvers: [ElementPlusResolver()],
}),
Components({
resolvers: [ElementPlusResolver()],
}),
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
}
});
在main.ts里将icon全局注册到App上:
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
就可以使用svg方式使用:
<el-icon><Menu /></el-icon>
然后配置路由用到了router,所以先下载:
npm install vue-router
然后在src里面建一个router文件夹,里面建一个index.js写路由配置文件,然后在main.js里面挂载一下:
import router from './router';
app.use(router)
2.登陆组件
这里登陆页面我写在了LoginView里面,后面我会配置路由和组件。
<template>
<div class="login-container">
<div class="login-form">
<el-form ref="login-form" :model="loginForm" label-width="80px" :rules="rules">
<el-form-item label="用户名" prop="username">
<el-input v-model="loginForm.username"></el-input>
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input type="password" v-model="loginForm.password"></el-input>
</el-form-item>
<el-form-item label="角色" prop="role">
<el-radio-group v-model="loginForm.role">
<el-radio label="admin">系统管理员</el-radio>
<el-radio label="dorm">宿舍管理员</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item size="large">
<el-button type="primary" @click="toLogin">登录</el-button>
</el-form-item>
</el-form>
</div>
</div>
</template>
<script>
import { ref } from 'vue';
import axios from 'axios';
export default {
setup() {
const loginForm = ref({
username: '',
password: '',
role: 'admin',
});
const rules = {
username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
password: [{ required: true, message: '请输入密码', trigger: 'blur' }],
role: [{ required: true, message: '请选择角色', trigger: 'change' }],
};
const performLogin = async () => {
try {
const formData = new FormData();//axios默认是json格式发送数据,但我后端接受的是user,所以将数据放到表单
formData.append('username', loginForm.value.username);
formData.append('password', loginForm.value.password);
formData.append('role',loginForm.value.role=='admin'?0:1);
const response = await axios.post('http://localhost:8080/users/login', formData);
const data = response.data;
if (data.state === 200) {
// 登录成功后,保存用户名到cookie
document.cookie = `username=${loginForm.value.username}; expires=; path=/`;
alert('登录成功');
} else if (data.state === 400) {
alert('用户名错误');
} else if (data.state === 401) {
alert('密码错误');
}else if (data.state === 402) {
alert('角色错误');
}
} catch (error) {
console.error('An error occurred:', error);
}
};
const toLogin = () => {
performLogin();
};
return {
loginForm,
rules,
toLogin,
};
},
};
</script>
<style scoped>
.login-container {
display: flex;
align-items: center;
justify-content: center;
height: 100vh;
}
.login-form {
padding: 80px;
border-radius: 10px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
}
</style>
3.配置
首先是在router/index.js里面写路由配置,将这个页面路由配置一下:
import { createRouter, createWebHistory } from 'vue-router'
import LoginView from '@/components/LoginView.vue'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/login',
component: LoginView
}
]
})
export default router
然后在App.vue里面写一下这个路由出口
<template>
<RouterView></RouterView>
</template>
此时就可以通过http://localhost:5173/login/
访问到这个页面。
2.后端
1.链接数据库
在application.properties中配置数据库。
spring.datasource.url=jdbc:mysql://localhost:3306/dormitory?useUnicode=true&characterEncoding=utf-8
spring.datasource.username=root
spring.datasource.password=root
2.创建用户实体类
在entity/User类里面创建用户实体类,和getset,equal,tostring方法(mac的快捷键是command+n)。
public class User {
private Integer uid;
private String username;
private String password;
private Integer role;
private String name;
private Integer gender;
private String phone;
}
3.数据操作持久层
1.配置
首先是配置一下mapper层,在启动类里面添加项目的mapper路径,让自动扫包。
@MapperScan("com.hckj.dormitory_b.mapper")
在application.properties中配置mapper地址。
mybatis.mapper-locations=classpath:mapper/*.xml
2.内容
在mapper/UserMapper接口里面写SQL语句的抽象方法(类添加@Mapper
注解),然后resources/mapper/UserMapper.xml里面写抽象方法的映射文件(这里放在resource是因为xml是静态文件)。
User findByUsername(String username);//接口
<?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.hckj.dormitory_b.mapper.UserMapper">
<select id="findByUsername" resultType="com.hckj.dormitory_b.entity.User">
select * from t_user where username=#{username}
</select>
</mapper>
3.测试
为了登陆测试,这里先给数据库里面插入一条数据:
INSERT INTO t_user (username, password, name,role, gender, telephone)
VALUES ('zoe', '111', 'dz', 0, 0,'188');
然后测试(测试类加注解:@SpringBootTest和@RunWith(SpringRunner.class)
)
@Autowired
private UserMapper userMapper;
@Test
public void findByUsername(){
System.out.println(userMapper.findByUsername("zoe"));
}
4.中间业务层
1.异常
在登录这个业务里会出现用户没有查询到和密码不匹配,所以在service的ex包里面创建UsernameNotFoundException和PasswordNotMatchException异常类,还有一个是角色不匹配RoleNotMatchException,并都继承RuntimeException,然后生成抛出异常的5种构造方法。
2.业务实现
然后写业务层的接口(加@Service
注解)并写类实现这个接口。
User login(String username, String password,Integer role);//接口
@Service
public class UserServiceImpl implements IUserService{
@Autowired
private UserMapper userMapper;
@Override
public User login(String username, String password,Integer role) {
User result = userMapper.findByUsername(username);
if (result == null) {
throw new UsernameNotFoundException("用户数据不存在");
}
String password_ = result.getPassword();
if (!password_.equals(password)) {
throw new PasswordNotMatchException("用户密码错误");
}
Integer role_=result.getRole();
if (!role_.equals(role)) {
throw new RoleNotMatchException("用户角色错误");
}
User user = new User();
user.setUid(result.getUid());
user.setUsername(result.getUsername());
user.setRole(result.getRole());
return user;
}
}
3.测试
@SpringBootTest
@RunWith(SpringRunner.class)
public class UserServiceTests {
@Autowired
private IUserService userService;
@Test
public void login(){
User user= userService.login("zoe","111",0);
System.out.println(user);
}
}
5.响应前端控制层
package com.hckj.dormitory_b.controller;
import com.hckj.dormitory_b.entity.User;
import com.hckj.dormitory_b.service.IUserService;
import com.hckj.dormitory_b.service.ex.PasswordNotMatchException;
import com.hckj.dormitory_b.service.ex.RoleNotMatchException;
import com.hckj.dormitory_b.service.ex.UsernameNotFoundException;
import jakarta.servlet.http.HttpSession;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("users")
public class UserController {
@Autowired
private IUserService userService;
@PostMapping("login")
public Map<String, Object> login(User user, HttpSession session) {
String username = user.getUsername();
String password = user.getPassword();
Integer role=user.getRole();
Map<String, Object> response = new HashMap<>();
try {
User loggedInUser = userService.login(username, password,role);
session.setAttribute("uid",loggedInUser.getUid());//将用户的uid和role保存到session
session.setAttribute("role",loggedInUser.getRole());
response.put("state", 200);
response.put("message", "登陆成功");
response.put("data", loggedInUser);
} catch (UsernameNotFoundException e) {
response.put("state", 400);
response.put("message", "用户名未找到");
response.put("data", null);
} catch (PasswordNotMatchException e) {
response.put("state", 401);
response.put("message", "密码不正确");
response.put("data", null);
} catch (RoleNotMatchException e){
response.put("state",402);
response.put("message","角色不正确");
response.put("data",null);
}
return response;
}
}
3.前后对接
对接这里其实就只是一个跨域问题,这个就是在后端工程的config/WebMvcConfig类里面加入设置,这里我前端的地址是http://localhost:5173
package com.hckj.dormitory_b.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("http://localhost:5173")
.allowedMethods("GET", "POST", "PUT", "DELETE")
.allowCredentials(true);
}
}
4.效果
3.界面
这里用element的menu做界面,我就把顶部底部和左侧都放在MenuView.vue里面,然后右侧内容给里面放个路由出口。
1.然后在router里面配路由,这里面二级路由嵌套时就用children属性,就会把children里面的component插入到父的<router-view></router-view>
,当第一个子的path为空时,就会和父用一个路由。
import { createRouter, createWebHistory } from 'vue-router'
import LoginView from '@/components/LoginView.vue'
import MenuView from '@/components/MenuView.vue'
import AddManagement from '@/components/Right/AddManagement.vue'
...
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/login',
component: LoginView,
name:'登陆'
},
{
path: '/addmanagement',
component:MenuView,
children:[{
path:'',
component:AddManagement,
name:'添加宿管'
},
...
]
}
]
})
export default router
2.登陆成功后让跳转到首页来,用到了useRouter:
import { useRouter } from 'vue-router';
const router = useRouter();
router.push('/addmanagement');
3.在menuview里面拿到cookie里面保存的username显示到主页的右上角:
<template #title>{{ username }}</template>
import { ref } from 'vue';
import { useRouter } from 'vue-router';
const router = useRouter();
const username = ref('');
const cookies = document.cookie.split('; ');
for (let i = 0; i < cookies.length; i++) {
const cookie = cookies[i].split('=');
if (cookie[0] === 'username') {
username.value = decodeURIComponent(cookie[1]);
break;
}
}
return {username};
4.右侧退出登录使用表单来二次确认:
<el-menu-item @click="showLogoutConfirmDialog">退出登录</el-menu-item>
const showLogoutConfirmDialog = () => {
ElMessageBox.confirm('确定要退出登录吗?', '退出登录', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
// 用户点击确认,执行退出操作,删除 Cookie
document.cookie = 'username=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;';
// 跳转到登录页面
router.push('/login');
})
.catch(() => {
// 用户点击取消,不执行退出操作
});
};
return {
showLogoutConfirmDialog,
};
5.左侧菜单点击后跳转到固定路由并且面包屑显示name:
<el-menu-item @click="navigateToLockregist"><el-icon><Calendar /></el-icon><span>学生缺寝记录</span></el-menu-item>
//1.面包屑显示name,使用$route.name
<el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item>
<el-breadcrumb-item>{{ $route.name }}</el-breadcrumb-item>
//2.点击跳转路由,使用router
import { useRouter } from 'vue-router';
const router = useRouter();
const navigateToLockregist = (event) => {router.push('/lockregist');};
return {navigateToLockregist}
效果如下
4.宿管模块
这几个模块儿大同小异。
1.添加
这里首先是前端,用到了表单校验(rules,prop),数据绑定(model,v-model),和后端ajax请求(json数据请求,所以后端要添加注解@RequestBody才能拿到数据;上面登陆功能用的是user对象请求),请求成功后会跳到宿管管理页面:
<template>
<el-form ref="ruleFormRef" :model="ruleForm" :rules="rules" label-width="120px" class="demo-ruleForm" :size="formSize" status-icon style="width: 300px; margin: 0 auto;">
<el-form-item label="用户名" prop="username">
<el-input v-model="ruleForm.username" />
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input v-model="ruleForm.password" />
</el-form-item>
<el-form-item label="姓名" prop="name">
<el-input v-model="ruleForm.name" />
</el-form-item>
<el-form-item label="性别" prop="gender">
<el-radio-group v-model="ruleForm.gender">
<el-radio label="男" />
<el-radio label="女" />
</el-radio-group>
</el-form-item>
<el-form-item label="联系电话" prop="telephone">
<el-input v-model="ruleForm.telephone" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submitForm(ruleFormRef)">
立即创建
</el-button>
<el-button @click="resetForm(ruleFormRef)">重置</el-button>
</el-form-item>
</el-form>
</template>
<script lang="ts" setup>
import axios from 'axios';
import { reactive, ref } from 'vue'
import type { FormInstance, FormRules } from 'element-plus'
import { useRouter } from 'vue-router';
interface RuleForm {
username:string
password:string
name: string
gender:string
telephone:string
}
const router = useRouter();
const formSize = ref('default')
const ruleFormRef = ref<FormInstance>()
const ruleForm = reactive<RuleForm>({
username:"jack",
password:"111",
name: "老王",
gender:"女",
telephone:'1008611',
})
const rules = reactive<FormRules<RuleForm>>({
username: [
{ required: true, message: '请输入用户名', trigger: 'blur' },
{ min: 2, max: 9, message: '用户名长度2-9', trigger: 'blur' },
],
password: [
{ required: true, message: '请输入用户密码', trigger: 'blur' },
],
name: [
{ required: true, message: '请输入用户名', trigger: 'blur' },
],
telephone: [
{ required: true, message: '请输入用户手机号', trigger: 'blur' },
],
})
const genderMap = {
'男': 0,
'女': 1,
};
const submitForm = (formEl: FormInstance | undefined) => {
if (!formEl) return
formEl.validate((valid, fields) => {
if (valid) {
ruleForm.gender = genderMap[ruleForm.gender];
console.log(ruleForm);
axios.post('http://localhost:8080/users/addmanagement', ruleForm)
.then(response => {
const data = response.data;
if (data.state === 200) {
alert(ruleForm.username+'添加成功');
router.push('/managemanagement');
} else if (data.state === 403) {
alert('用户名已存在');
}else if (data.state === 404) {
alert('插入数据发生未知错误');
}
})
.catch(error => {
console.error('请求错误:', error.message);
});
} else {
console.log('error submit!', fields);
}
});
}
const resetForm = (formEl: FormInstance | undefined) => {
if (!formEl) return
formEl.resetFields()
}
</script>
然后后端就按步骤,没啥知识点。
#1.mapper
Integer insertManagement(User user);
<insert id="insertManagement" useGeneratedKeys="true" keyProperty="uid">
insert into t_user(username,password,role,name,gender,telephone) values (
#{username},#{password},#{role},#{name},#{gender},#{telephone})
</insert>
#2.service
void insertManagement(String username,String password,String name,Integer gender ,String telephone);
@Override
public void insertManagement(String username,String password,String name,Integer gender ,String telephone) {
User result = userMapper.findByUsername(username);
if (result != null) {
throw new UsernameDuplicateException("用户名重复");
}
User user = new User();
user.setUsername(username);
user.setPassword(password);
user.setName(name);
user.setRole(1);
user.setGender(gender);
user.setTelephone(telephone);
Integer rows = userMapper.insertManagement(user);
if (rows != 1) {
throw new InsertException("在用户注册过程中产生了未知的异常");
}
}
#3.controller
@PostMapping("addmanagement")
public Map<String, Object> insertManagement(@RequestBody User user){
Map<String, Object> response = new HashMap<>();
try {
userService.insertManagement(user.getUsername(),user.getPassword(),user.getName(),user.getGender(), user.getTelephone());
response.put("state", 200);
response.put("message", "添加宿管成功");
response.put("data", null);
}catch (UsernameDuplicateException e){
response.put("state", 403);
response.put("message", "用户名已存在");
response.put("data", null);
}catch (InsertException e){
response.put("state", 404);
response.put("message", "插入数据时发生位置错误");
response.put("data", null);
}
return response;
}
效果如下: