vue3+vite+ts实现springboot项目前后端联调

引言:

实现了基于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;
  }

运行后端代码

前端页面点击登录按钮

点击确定后跳转到首页

成功登录

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值