引言:
实现了基于springboot的球场预约管理系统的后端代码后,准备实现前端简单的登录和跳转功能。
一:创建前端项目
使用create-vue官方脚手架搭建项目
在终端输入
npm create vue@latest
命名项目名Badminton-front-demo
创建好后,按照给出的命令一个个运行
cd Badminton-front-demo
npm install
npm run dev
若担心路径输错或太长麻烦,可以用键盘上的tab键补全路径
打开给出的网址 http://localhost:5173/
这里可以自己设置端口号和是否自动打开网址:在根目录下找到vite.config.ts更改,具体配置问题在这儿
成功运行后的网页如下
二:修改代码
项目根目录下找到components和views文件夹,将里面vue文件删除,然后找到App.vue,将template中的内容删除,删掉import爆红的一行代码。完成后,开始设计自己的页面
(1)登录页:
在views文件夹下创建LoginView.vue
这里原本是使用的element Plus,后来发现有个开源框架的登录也还可以,就copy了过来
<template>
<div class="login-wrap">
<div class="login-root">
<div class="login-main">
<img class="login-one-ball"
src="https://assets.codehub.cn/micro-frontend/login/fca1d5960ccf0dfc8e32719d8a1d80d2.png" />
<img class="login-two-ball"
src="https://assets.codehub.cn/micro-frontend/login/4bcf705dad662b33a4fc24aaa67f6234.png" />
<div class="login-container">
<div class="login-side">
<div class="login-bg-title">
<h1>XXX羽毛球场预约管理</h1>
<h3 style="margin: 20px auto">
Welcome to XXX
</h3>
</div>
</div>
<div class="login-ID">
<div style="font-size: 22px; margin-bottom: 15px; margin-top: 5px">
🎯 Sign in
</div>
<el-form :model="form" label-width="auto" style="max-width: 600px" class="former">
<el-form-item label="账号">
<el-input v-model="form.adminName" />
</el-form-item>
<el-form-item label="密码">
<el-input v-model="form.adminPassword" type="password" />
</el-form-item>
<el-form-item label="">
<el-button type="primary" @click="onSubmit">登录</el-button>
<el-button>取消</el-button>
</el-form-item>
</el-form>
<lay-line class="text-position" style="margin: 34px 0px;">Other login methods</lay-line>
<ul class="other-ways">
<li>
<div class="line-container">
<p class="text">微信</p>
</div>
</li>
<li>
<div class="line-container">
<p class="text">钉钉</p>
</div>
</li>
<li>
<div class="line-container">
<p class="text">Gitee</p>
</div>
</li>
<li>
<div class="line-container">
<p class="text">Github</p>
</div>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.login-captach {
display: inline-block;
vertical-align: bottom;
width: 108px;
height: 40px;
color: var(--global-primary-color);
margin-left: 8px;
border-radius: 4px;
border: 1px solid hsla(0, 0%, 60%, 0.46);
transition: border 0.2s;
box-sizing: border-box;
background: #fff;
overflow: hidden;
cursor: pointer;
}
.login-one-ball {
opacity: 0.4;
position: absolute;
max-width: 568px;
left: -400px;
bottom: 0px;
}
.login-two-ball {
opacity: 0.4;
position: absolute;
max-width: 320px;
right: -200px;
top: -60px;
}
.login-wrap {
position: fixed;
top: 0;
left: 0;
bottom: 0;
right: 0;
overflow: auto;
min-width: 600px;
z-index: 9;
background-image: url(https://sky-take-out-super-fish.oss-cn-beijing.aliyuncs.com/tuchuang/202408161602507.png);
background-repeat: no-repeat;
background-size: cover;
min-height: 100vh;
}
.login-wrap :deep(.layui-input-block) {
margin-left: 0 !important;
}
.login-root {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
display: flex;
justify-content: center;
width: 100%;
min-width: 320px;
background-color: initial;
}
.login-main {
position: relative;
display: block;
}
.logo-container {
max-width: calc(100vw - 28px);
margin-bottom: 40px;
text-align: center;
display: flex;
align-items: center;
justify-content: center;
}
.logo-container .logo {
display: inline-block;
height: 30px;
width: 143px;
background: url() no-repeat 50%;
background-size: contain;
cursor: pointer;
}
.login-container {
position: relative;
overflow: hidden;
width: 940px;
height: 540px;
max-width: calc(100vw - 28px);
border-radius: 4px;
background: hsla(0, 0%, 100%, 0.5);
backdrop-filter: blur(30px);
display: flex;
box-shadow: 6px 6px 12px 4px rgba(0, 0, 0, 0.1);
}
.login-side {
padding: 40px 20px 20px;
background-color: var(--global-primary-color);
flex: 1;
height: 100%;
}
.login-bg-title {
flex: 1;
background-image: url('https://sky-take-out-super-fish.oss-cn-beijing.aliyuncs.com/tuchuang/202408161602507.png');
height: 84%;
color: #000000;
text-align: center;
background-repeat: no-repeat;
background-position: bottom;
background-size: contain;
text-align: center;
min-width: 200px;
z-index: 1;
}
.login-ID {
padding: 20px 30px;
min-width: 420px;
}
.login-container .layui-tab-head {
background: transparent;
}
.login-container .layui-input-wrapper {
margin-top: 10px;
margin-bottom: 10px;
}
.login-container .layui-input-wrapper {
margin-top: 12px;
margin-bottom: 12px;
}
.login-container .assist {
margin-top: 5px;
margin-bottom: 5px;
letter-spacing: 2px;
}
.login-container .layui-btn {
margin: 10px 0px 10px 0px;
letter-spacing: 2px;
height: 40px;
}
.login-container .layui-line-horizontal {
letter-spacing: 2px;
margin-bottom: 34px;
margin-top: 24px;
}
.other-ways {
display: flex;
justify-content: space-between;
margin: 0;
padding: 0;
list-style: none;
font-size: 14px;
font-weight: 400;
}
.text-position {
position: relative;
top: 140px;
}
.other-ways li {
width: 100%;
}
.line-container {
position: relative;
top: 150px;
text-align: center;
cursor: pointer;
}
.line-container .icon {
height: 28px;
width: 28px;
margin-right: 0;
vertical-align: middle;
border-radius: 50%;
background: #fff;
box-shadow: 0 1px 2px 0 rgb(9 30 66 / 4%), 0 1px 4px 0 rgb(9 30 66 / 10%),
0 0 1px 0 rgb(9 30 66 / 10%);
}
.line-container .text {
display: block;
margin: 12px 0 0;
font-size: 12px;
color: #8592a6;
}
:deep(.layui-tab-title .layui-this) {
background-color: transparent;
}
.former {
position: relative;
top: 20%;
}
</style>
<script lang="ts" setup>
import { useRouter } from 'vue-router';
import { reactive, ref } from 'vue'
import axios from 'axios';
// do not use same name with ref
const form = ref({
adminName: '',
adminPassword: '',
})
const adminDto = {
adminName: form.value.adminName,
adminPassword: form.value.adminPassword,
};
const route=useRouter()
const onSubmit =async () => {
if (form.value.adminName === 'root' && form.value.adminPassword === '123456')
{
try{
const response =await axios.post("http://localhost:8080/admin/login",form.value,{
headers: {
'Content-Type': 'application/json',
},
});
console.log(adminDto);
console.log(form.value);
console.log(response.data);
handleResponse(response.data);
if (response.data.code === 0) {
route.replace('/home');
} else {
alert('登录失败,请检查用户名和密码是否正确');
}
} catch (error) {
if (axios.isAxiosError(error)) {
if (error.response) {
// The request was made and the server responded with a status code
// that falls out of the range of 2xx
console.error('Error response:', error.response.data);
alert('登录失败,请检查用户名和密码是否正确');
} else if (error.request) {
// The request was made but no response was received
console.error('No response received:', error.request);
alert('无法连接到服务器,请检查网络连接');
} else {
// Something happened in setting up the request that triggered an Error
console.error('Error setting up request:', error.message);
alert('发生未知错误,请稍后再试');
}
} else {
console.error('Unknown error:', error);
alert('发生未知错误,请稍后再试');
}
}
} else {
alert('请输入用户名和密码');
}
console.log('Submitted');
console.log(route);
};
// 设置 Axios 拦截器
axios.interceptors.request.use(function (config) {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
}, function (error) {
return Promise.reject(error);
});
const handleResponse = (data: ResponseData) =>{
console.log('Response data',data);
if (data.code === 0) {
alert('登录成功');
} else {
alert('登录失败');
}
};
interface ResponseData {
code: number;
message: string;
data: any;
}
</script>
修改添加后就成了以上代码,其中的图片url换成了我个人的阿里云图床,删除了目前不必要的功能后,运行界面如下
登录成功后要进入首页面,因此接下来创建首页,同样在views文件夹下创建Main.vue
(2)首页:
首页则使用了Element plus的布局容器
<script lang="ts" setup>
import CommonAside from "../components/CommonAside.vue"
import CommonSub from "../components/CommonSub.vue";
import CommonMain from "../components/CommonMain.vue";
import Booking from "../components/booking/Booking.vue"
import { ref } from "vue";
</script>
<template>
<div class="common-layout">
<el-container>
<el-aside width="200px"><common-aside/></el-aside>
<el-container>
<el-header><common-sub></common-sub></el-header>
<el-main>
<common-main></common-main>
</el-main>
</el-container>
</el-container>
</div>
</template>
<style lang="less" scoped>
.common-layout {
height: 100%;
&>.el-container {
height: 100%;
&>.el-aside {
height: 100%;
width: 10%;
background: #CDD0D6;
}
&>.el-container {
&>.el-header {
background-color: #D4D7DE;
padding: 0%;
}
&>.el-container{
&>.el-main {
height: 100%;
background-color: #DCDFE6;
}
}
}
}
}
</style>
其中CommonAside,CommonSub,CommonMain是顶栏,边栏和主要内容相关的页面。由import引入在标签中使用,配置css格式使得容器格式合理。接下来编辑这三个组件
(3)边栏:
在components下创建CommonAside.vue,同样使用Element Plus
<template>
<el-row class="tac">
<el-col :span="25">
<h5 class="mb-2"></h5>
<el-menu
default-active="2"
class="el-menu-vertical-demo"
@open="handleOpen"
@close="handleClose"
background-color="#CDD0D6"
>
<el-sub-menu index="1">
<template #title>
<el-icon><location /></el-icon>
<span>首页</span>
</template>
<el-menu-item-group title="预约相关">
<el-menu-item index="1-1" @click="ToBooking">预约</el-menu-item>
<el-menu-item index="1-2">查看预约</el-menu-item>
</el-menu-item-group>
<el-menu-item-group title="账号相关">
<el-menu-item index="1-3">我的</el-menu-item>
</el-menu-item-group>
<el-sub-menu index="1-4">
<template #title>关于</template>
<el-menu-item index="1-4-1">作者</el-menu-item>
<el-menu-item index="1-4-2">支持</el-menu-item>
</el-sub-menu>
</el-sub-menu>
<el-menu-item index="2" @click="handleClick">
<el-icon><icon-menu /></el-icon>
<span>返回首页</span>
</el-menu-item>
</el-menu>
</el-col>
</el-row>
</template>
<script lang="ts" setup>
import { useRouter } from 'vue-router';
const router=useRouter()
import {
Document,
Menu as IconMenu,
Location,
Setting,
} from '@element-plus/icons-vue'
const handleOpen = (key: string, keyPath: string[]) => {
console.log(key, keyPath)
}
const handleClose = (key: string, keyPath: string[]) => {
console.log(key, keyPath)
}
const handleClick = () => {
router.replace('/')
}
function ToBooking(){
router.push({
path:'/reservation'
})
}
</script>
(4)顶栏:
<template>
<el-menu
:default-active="activeIndex"
class="el-menu-demo"
mode="horizontal"
@select="handleSelect"
background-color="#D4D7DE"
>
<el-menu-item index="1">Processing Center</el-menu-item>
<el-sub-menu index="2">
<template #title>Workspace</template>
<el-menu-item index="2-1">item one</el-menu-item>
<el-menu-item index="2-2">item two</el-menu-item>
<el-menu-item index="2-3">item three</el-menu-item>
<el-sub-menu index="2-4">
<template #title>item four</template>
<el-menu-item index="2-4-1">item one</el-menu-item>
<el-menu-item index="2-4-2">item two</el-menu-item>
<el-menu-item index="2-4-3">item three</el-menu-item>
</el-sub-menu>
</el-sub-menu>
<el-menu-item index="3" disabled>Info</el-menu-item>
<el-menu-item index="4">Orders</el-menu-item>
</el-menu>
<div class="h-6" />
<!-- <el-menu
:default-active="activeIndex2"
class="el-menu-demo"
mode="horizontal"
background-color="#545c64"
text-color="#fff"
active-text-color="#ffd04b"
@select="handleSelect"
>
<el-menu-item index="1">Processing Center</el-menu-item>
<el-sub-menu index="2">
<template #title>Workspace</template>
<el-menu-item index="2-1">item one</el-menu-item>
<el-menu-item index="2-2">item two</el-menu-item>
<el-menu-item index="2-3">item three</el-menu-item>
<el-sub-menu index="2-4">
<template #title>item four</template>
<el-menu-item index="2-4-1">item one</el-menu-item>
<el-menu-item index="2-4-2">item two</el-menu-item>
<el-menu-item index="2-4-3">item three</el-menu-item>
</el-sub-menu>
</el-sub-menu>
<el-menu-item index="3" disabled>Info</el-menu-item>
<el-menu-item index="4">Orders</el-menu-item>
</el-menu> -->
</template>
<script lang="ts" setup>
import { ref } from 'vue'
const activeIndex = ref('1')
const activeIndex2 = ref('1')
const handleSelect = (key: string, keyPath: string[]) => {
console.log(key, keyPath)
}
</script>
(5)主要内容:
<template>
<el-menu
:default-active="activeIndex"
class="el-menu-demo"
mode="horizontal"
@select="handleSelect"
background-color="#D4D7DE"
>
<el-menu-item index="1">Processing Center</el-menu-item>
<el-sub-menu index="2">
<template #title>Workspace</template>
<el-menu-item index="2-1">item one</el-menu-item>
<el-menu-item index="2-2">item two</el-menu-item>
<el-menu-item index="2-3">item three</el-menu-item>
<el-sub-menu index="2-4">
<template #title>item four</template>
<el-menu-item index="2-4-1">item one</el-menu-item>
<el-menu-item index="2-4-2">item two</el-menu-item>
<el-menu-item index="2-4-3">item three</el-menu-item>
</el-sub-menu>
</el-sub-menu>
<el-menu-item index="3" disabled>Info</el-menu-item>
<el-menu-item index="4">Orders</el-menu-item>
</el-menu>
<div class="h-6" />
<!-- <el-menu
:default-active="activeIndex2"
class="el-menu-demo"
mode="horizontal"
background-color="#545c64"
text-color="#fff"
active-text-color="#ffd04b"
@select="handleSelect"
>
<el-menu-item index="1">Processing Center</el-menu-item>
<el-sub-menu index="2">
<template #title>Workspace</template>
<el-menu-item index="2-1">item one</el-menu-item>
<el-menu-item index="2-2">item two</el-menu-item>
<el-menu-item index="2-3">item three</el-menu-item>
<el-sub-menu index="2-4">
<template #title>item four</template>
<el-menu-item index="2-4-1">item one</el-menu-item>
<el-menu-item index="2-4-2">item two</el-menu-item>
<el-menu-item index="2-4-3">item three</el-menu-item>
</el-sub-menu>
</el-sub-menu>
<el-menu-item index="3" disabled>Info</el-menu-item>
<el-menu-item index="4">Orders</el-menu-item>
</el-menu> -->
</template>
<script lang="ts" setup>
import { ref } from 'vue'
const activeIndex = ref('1')
const activeIndex2 = ref('1')
const handleSelect = (key: string, keyPath: string[]) => {
console.log(key, keyPath)
}
</script>
三:路由注册
进入到router/index.ts下编辑,为几个页面注册路由
import { createRouter, createWebHistory } from 'vue-router'
import home from '../views/Main.vue'
import login from '../views/LoginView.vue'
import copy from '../views/LoginCopyView.vue'
import book from '../components/booking/Booking.vue'
import reservation from '../views/Reservation.vue'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: 'login',
component: copy
},
{
path: '/home',
name: 'home',
component: home
},
{
path: '/booking',
name: 'booking',
component: book
},
{
path: '/reservation',
name: 'reservation',
component: reservation
}
]
})
export default router
随后在App.vue下引入路由
<script setup lang="ts">
import { RouterLink, RouterView } from 'vue-router'
</script>
<template>
<RouterView></RouterView>
</template>
<style scoped>
</style>
四:前后端联调
实现管理员登录功能
后端代码接受的是JSON格式数据adminName,adminPassword,因此前端也传入JSON数据:name,password
因为遇到了后端接受不到数据的情况,所以做了些小改动
定义一个结构form,里面包含adminName,adminPassword
const form = ref({
adminName: '',
adminPassword: '',
})
手动设置数据格式,代码如下
<script lang="ts" setup>
import { useRouter } from 'vue-router';
import { reactive, ref } from 'vue'
import axios from 'axios';
// do not use same name with ref
const form = ref({
adminName: '',
adminPassword: '',
})
const adminDto = {
adminName: form.value.adminName,
adminPassword: form.value.adminPassword,
};
const route=useRouter()
const onSubmit =async () => {
if (form.value.adminName === 'root' && form.value.adminPassword === '123456')
{
try{
const response =await axios.post("http://localhost:8080/admin/login",form.value,{
headers: {
'Content-Type': 'application/json',
},
});
console.log(adminDto);
console.log(form.value);
console.log(response.data);
handleResponse(response.data);
if (response.data.code === 0) {
route.replace('/home');
} else {
alert('登录失败,请检查用户名和密码是否正确');
}
} catch (error) {
if (axios.isAxiosError(error)) {
if (error.response) {
// The request was made and the server responded with a status code
// that falls out of the range of 2xx
console.error('Error response:', error.response.data);
alert('登录失败,请检查用户名和密码是否正确');
} else if (error.request) {
// The request was made but no response was received
console.error('No response received:', error.request);
alert('无法连接到服务器,请检查网络连接');
} else {
// Something happened in setting up the request that triggered an Error
console.error('Error setting up request:', error.message);
alert('发生未知错误,请稍后再试');
}
} else {
console.error('Unknown error:', error);
alert('发生未知错误,请稍后再试');
}
}
} else {
alert('请输入用户名和密码');
}
console.log('Submitted');
console.log(route);
};
增加了多个验证和日志输出便于找到错误,随后添加axios拦截器
// 设置 Axios 拦截器
axios.interceptors.request.use(function (config) {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
}, function (error) {
return Promise.reject(error);
});
const handleResponse = (data: ResponseData) =>{
console.log('Response data',data);
if (data.code === 0) {
alert('登录成功');
} else {
alert('登录失败');
}
};
interface ResponseData {
code: number;
message: string;
data: any;
}
运行后端代码
前端页面点击登录按钮
点击确定后跳转到首页
成功登录