SpringCloud+Vue+Python人工智能(fastAPI,机器学习,深度学习)前后端架构各功能实现思路——主目录(持续更新):https://blog.csdn.net/grd_java/article/details/144986730 |
---|
文章目录
1. 跨越解决
前后端分离项目必须解决跨越问题,我们在后端已经解决过了,不多赘述
2. 前端请求后端,保存token
1. auth.js工具类的作用,就是方便我们对token进行增删改查,存储时,是存储到cookie中的
文件中,统一使用功能js-cookie第三方工具包来实现对cookie的操作
2. request.js工具类,就是封装了3大件
1. service对象,发送请求时我们要求其拼接baseUrl和组合url,baseurl就是后端服务器的ip地址,组合url就是要请求的具体url。这样我们写请求时,就不用每次都手写baseurl了
2. 请求拦截器,每次发送请求时,先拦截请求,带上token(放在headers中的authorization中),如果没有就不带
3. 响应拦截器,后端响应后,进行拦截,根据后端响应的内容做一些通用处理,例如响应码不是20000的,就提示异常信息等等常用操作
3. api,就是封装了请求用户操作相关后端接口的一个工具类,这里初步定义了3个,登录,获取登录用户信息,退出登录3个接口
import {service} from '@/utils/request'
// import qs from 'qs'
// const str = "user/loging?"+qs.stringify({data})
export function login(data) {
return service({
url: 'security/login',
method: 'get',
params: data
})
}
export function getInfo() {
return service({
url: 'security/LoginUserInfo',
method: 'get'
})
}
export function logout() {
return service({
url: 'security/logout',
method: 'get'
})
}
import {service} from '@/utils/request'
export function menuList(){
return service({
url:'/security/dd-menu/menus',
method:'get'
})
}
export function addMenu(data){
return service({
url:'/security/dd-menu/',
method:'post',
data
})
}
export function updateMenu(data){
return service({
url:'/security/dd-menu/',
method:'put',
data
})
}
export function deleteMenuById(id){
return service({
url:`/security/dd-menu/${id}`,
method:"delete",
})
}
export function deleteMenuList(parameter){
return service({
url:'/security/dd-menu/ids',
method:"delete",
params: parameter
})
}
export function updateMenuAndRole(parameter){
return service({
url:'/security/dd-menu/roleAndMenu',
method:"put",
params: parameter
})
}
export function menuIdByRoleId(rid){
return service({
url:`/security/dd-menu/menuIdByRoleId/${rid}`,
method:"get"
})
}
export function getMenusByUserId(id){
return service({
url:`/security/sysMenu/getMenusByUserId/${id}`,
method:"get"
})
}
4. pinia
首先,既然是处理user相关的全局变量,肯定要引入处理相关逻辑
1. 和登录有关的接口,就是上面的api文件中的3个接口
2. 有了用户信息,就要拿到这个用户的权限,以及他能看到的路由和按钮,这个是后面的内容,这里先放着
3. 树形结构工具,就是处理菜单用的工具
4. 获取路由,路由是直接定义好的(全有的状态),但是我们要根据用户权限,去掉这些路由中当前用户不能访问的路由
其它的代码就是业务代码,具体讲到的时候再说
关于登录的就是login
5. 修改store逻辑
登录依旧为请求后,设置token
获取用户信息
// user login
//处理登录业务
async login(userInfo) {
//解构出用户名和密码
const { username,password} = userInfo;
//异步调用/api/user文件下的login
let result = await login({username: username.trim(),password:password});
// console.log(result)
if (result.code===20000){
//我们规定token为tokenHead + " " + token字符串
// console.log(result)
const token = result.tokenInfo.tokenValue;
//设置到state
// commit("SET_TOKEN",token);
this.token = token
//通过auth.js文件的函数,设置到cookie
setToken(token)
}else{
//登录失败
return Promise.reject(new Error(result.msg));
}
},
// get user info
async getInfo() {
//异步调用/api/user文件下的login
let result = await getInfo();
if(result.code === 20000){
const loginInfo = result.userInfo
if (!loginInfo) {
return reject('验证失败,请重新登录.');
}
//处理路由
let menusResult = await getMenusByUserId(loginInfo.id)
if(menusResult.code === 20000){
const { menus } = menusResult
if(menus.length > 0){
const _menus = menus
let routes = []
let buttons = []
_menus.forEach(e=>{
if(e.type === 'menu'){
routes.push(e.path)
}else if(e.type === 'button') {
buttons.push(e.permission)
}
})
// commit('SET_BUTTONS',buttons)
this.buttons = buttons;
// commit('SET_ROUTES',routes)
this.routes = routes;
const { roles } = loginInfo
// commit('SET_ROLES',roles)
this.roles = roles;
//对比后设置需要展示的异步路由
const resultAsyncRoutes = compareasyncRoutes(asyncRoutes,routes)
// commit('SET_RESULTASYNCROUTES',compareasyncRoutes(asyncRoutes,routes))
this.resultAsyncRoutes = resultAsyncRoutes
//合并常量、异步、路由最终展示的路由
this.routesResult = constantRoutes.concat(this.resultAsyncRoutes,anyRoutes);
// console.log("asd;kfjlk;asdjdkl;fds",JSON.parse(JSON.stringify(this.routesResult)))
// router.addRoute(JSON.parse(JSON.stringify(this.routesResult)))
// router.matcher.addRoutes(JSON.parse(JSON.stringify(this.routesResult)))
//将最终合并路由添加到路由中
for (let i = 0; i < resultAsyncRoutes.length; i++) {
router.addRoute(resultAsyncRoutes[i])
}
}
}else{
// commit('SET_RESULTASYNCROUTES',compareasyncRoutes(asyncRoutes,[]))
this.resultAsyncRoutes = []
this.routesResult = constantRoutes.concat(this.resultAsyncRoutes,anyRoutes);
console.log(asyncRoutes);
router.addRoute(this.routesResult)
}
const {username} = loginInfo;
// commit('SET_NAME',username);
// commit('SET_AVATAR','https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif');
this.name = username;
this.avatar = 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif';
}else{
//登录失败
return Promise.reject(new Error(result.msg));
}
},
3. 为什么没有手动传参token呢?因为我们request.js中的请求拦截器,会在每次请求时,都将token放在headers中的authorization中(如果有的话)
3. 前端登录页面逻辑实现
1. 先在常量路由中添加login路由,同时创建login对应的vue文件
{
path: '/login',
name: 'login',
component: () => import('@/views/login/index'),
hidden: true
},
2. 在App.vue中将针对about和home两个路由的link去掉,只留下router-view即可
3. 登录页面
<template>
<div class="login-container">
<!-- 绑定loginForm表单 -->
<el-form ref="loginRef"
:model="loginForm"
:rules="loginRules"
class="login-form"
auto-complete="on"
label-position="left">
<div class="title-container">
<h3 class="title">Login Form</h3>
</div>
<!--prop="username",是表单校验需要的内容-->
<el-form-item prop="username">
<span class="svg-container"><el-icon ><UserFilled /></el-icon></span>
<!-- 绑定loginFrom中的username-->
<el-input
v-model="loginForm.username"
placeholder="Username"
name="username"
type="text"
auto-complete="on"
/>
</el-form-item>
<el-form-item prop="password">
<el-icon class="svg-container"><Lock /></el-icon>
<el-input
v-model="loginForm.password"
type="password"
size="large"
placeholder="Password"
name="password"
auto-complete="on"
/>
<span class="show-pwd" @click="showPwd">
<svg-icon :icon-class="passwordType === 'password' ? 'eye' : 'eye-open'" />
</span>
</el-form-item>
<!-- <el-checkbox style="margin: 0px 0px 25px 0px">记住密码</el-checkbox>-->
<!-- <el-form-item prop="code" class="code">-->
<!-- <span class="svg-container">-->
<!--<!– <svg-icon icon-class="el-icon-s-flag" />–>-->
<!-- <i class="el-icon-edit"></i>-->
<!-- </span>-->
<!-- <el-input-->
<!-- class="code-input"-->
<!-- ref="code"-->
<!-- type="text"-->
<!-- v-model="loginForm.code"-->
<!-- placeholder="验证码"-->
<!-- name="code"-->
<!-- tabindex="2"-->
<!-- auto-complete="on"-->
<!-- @keyup.enter.native="handleLogin"-->
<!-- />-->
<!-- </el-form-item>-->
<!-- <img :src="codesrc" class="code-img" @click="getImgCode">-->
<el-button ref="loginButton" :disabled="disabledFlag" type="primary" style="width:100%;margin-bottom:30px;" @click.native.prevent="handleLogin">Login</el-button>
<div class="tips">
<span style="margin-right:20px;">username: admin</span>
<span> password: any</span>
</div>
</el-form>
</div>
</template>
<script setup>
import {watch, ref, onMounted} from 'vue'
import {ElMessage} from 'element-plus'
import {usePiniaUserStore} from '@/store/pinia_user'
import {useRouter} from 'vue-router'
const router = useRouter()//router对象,VUE3写法
const piniaUserStore = usePiniaUserStore()
//引用
const loginForm = ref({
username: '',
password: ''
})
//获取ref="loginRef"的DOM元素
const loginRef = ref(null)
//登录按钮是否禁用
const disabledFlag = ref(false)//先不禁用
//表单校验规则
const loginRules = {
username: [
//提交表单时触发,blur模糊匹配
{required: true, trigger:'blur',message: '请输入用户名'},
{min:3,max:25,message: '用户名长度应该为3-25个字符',trigger:'blur'},
],
password: [{required: true, trigger:'blur',message: '请输入密码'},]
}
/**
* 监听路由的变化,如果是从某一个页面A跳转过来进行登录,就记录A的路由,完成登录后跳转回A,而不是无论如何都跳转回主页
* watch: {
* $route: {
* handler: function(route) {
* this.redirect = route.query && route.query.redirect
* },
* immediate: true
* }
* },
*/
const redirect = ref(null)//重定向地址
//路由变动时需要的监听触发函数
const handleRouteChange = (newRoute)=>{
console.log(router.currentRoute.value.query && router.currentRoute.value.query.redirect)
redirect.value = router.currentRoute.value.query && router.currentRoute.value.query.redirect
}
//模拟immediate=true的情况
onMounted(()=>{
handleRouteChange()
})
//监听路由的变化
watch(router,handleRouteChange)
//登录逻辑
const handleLogin = () => {
//禁用登录按钮
disabledFlag.value=true
//先表单验证
loginRef.value.validate(async (valid) => {
//如果验证通过
if (valid) {
//禁用登录按钮
// disabledFlag.value=true
//action监听,user.js下的login
// await store.dispatch('user/login', loginForm.value).then(() => {
await piniaUserStore.login(loginForm.value).then(() => {
console.log('登录成功')
console.log('跳转:',redirect.value || '/')
//进行路由跳转,如果redirect存在,就跳转回redirect的地址,而不是'/'
router.push({path:redirect.value || '/'})
disabledFlag.value=false//异步过程中,阻止登录按钮的使用
}).catch(()=>{
disabledFlag.value=false//异步过程中,阻止登录按钮的使用
})
}else{//如果验证不通过
console.log('登录失败')
ElMessage.error('输入信息有问题个damn的!!!!')
return false
}
})
}
</script>
<style lang="scss">
/* 修复input 背景不协调 和光标变色 */
/* Detail see https://github.com/PanJiaChen/vue-element-admin/pull/927 */
$bg:#283443;
$light_gray:#fff;
$cursor: #fff;
@supports (-webkit-mask: none) and (not (cater-color: $cursor)) {
.login-container .el-input input {
color: $cursor;
}
}
/* reset element-ui css */
.login-container {
.el-input {
display: inline-block;
height: 47px;
width: 85%;
.el-input__wrapper{
display: inline-block;
height: 100%;
width: 100%;
background: transparent !important;//背景透明
box-shadow: 0 0 0px 0px $bg inset !important;
-webkit-text-fill-color: $cursor !important;
.el-input__inner {
//background-color: transparent;
background: transparent !important;//背景透明
transition: background-color 5000s ease-in-out 0s !important;
border: 0px;
-webkit-appearance: none;
border-radius: 0px;
padding: 12px 5px 12px 15px;
color: $light_gray;
height: 47px;
caret-color: $cursor;
}
}
}
.el-form-item {
border: 1px solid rgba(255, 255, 255, 0.1);
background: rgba(0, 0, 0, 0.1);
border-radius: 5px;
color: #454545;
}
}
</style>
<style lang="scss" scoped>
$bg:#2d3a4b;
$dark_gray:#889aa4;
$light_gray:#eee;
.login-container {
min-height: 100%;
width: 100%;
background-color: $bg;
overflow: hidden;
background-image: url("~@/assets/back.jpg");//想要使用@,必须加~前缀
background-size:100% 100%;
.code{
width: 70%;
float: left;
}
.code-img{
float: right;
width: 29.6%;
//height: 100%;
}
.login-form {
position: relative;
width: 520px;
max-width: 100%;
padding: 160px 35px 0;
margin-top: 20%;
margin: 7% auto auto auto;
overflow: hidden;
}
.tips {
font-size: 14px;
color: #fff;
margin-bottom: 10px;
span {
&:first-of-type {
margin-right: 16px;
}
}
}
.svg-container {
padding: 6px 5px 6px 15px;
color: $dark_gray;
vertical-align: middle;
width: 30px;
display: inline-block;
}
.title-container {
position: relative;
.title {
font-size: 26px;
color: $light_gray;
margin: 0px auto 40px auto;
text-align: center;
font-weight: bold;
}
}
.show-pwd {
position: absolute;
right: 10px;
top: 7px;
font-size: 16px;
color: $dark_gray;
cursor: pointer;
user-select: none;
}
}
</style>
4. 测试
输入登录页面的路由,输入用户名和密码点击登录后,成功拿到token并保存在cookies中
5. 主页
5.1 架子
后台系统页面,先将基本的架子搭起来
使用element plus的下图所示布局,复制代码
将其放入layout文件夹下的index.vue中,对应的也要定义4个子组件,将Aside、Header、Main、Footer分成4个组件
<template>
<div class="common-layout">
<el-container>
<el-aside width="200px"> <AsideMenu/> </el-aside>
<el-container>
<el-header> <Header/> </el-header>
<el-main> <Main/> </el-main>
<el-footer> <Footer/> </el-footer>
</el-container>
</el-container>
</div>
</template>
<script setup>
import AsideMenu from '@/layout/AsideMenu'
import Footer from "@/layout/Footer"
import Header from "@/layout/Header"
import Main from "@/layout/Main"
</script>
<style scoped>
</style>
5.2 左侧导航栏
然后编写左侧导航栏样式
<template>
<el-row class="tac">
<el-col :span="12" class="menuBackGroud">
<h5 class="mb-2 margin_mine">
<el-avatar
src="https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png"
class="menu-avatar"
/>
<span class="menuTitle">您好!{{piniaUserStore.name }}</span>
</h5>
<el-menu
:router="true"
active-text-color="#ffd04b"
background-color="#545c64"
class="el-menu-vertical-demo margin_mine"
default-active="3"
text-color="#fff"
@open="handleOpen"
@close="handleClose"
>
<el-sub-menu index="1">
<template #title class="">
<el-icon><location /></el-icon>
<span>Navigator One</span>
</template>
<el-menu-item index="1-1">item one</el-menu-item>
<el-menu-item index="1-2">item two</el-menu-item>
<el-menu-item index="1-3">item three</el-menu-item>
<el-sub-menu index="1-4">
<template #title>item four</template>
<el-menu-item index="1-4-1">item one</el-menu-item>
</el-sub-menu>
</el-sub-menu>
<el-menu-item index="2">
<el-icon><icon-menu /></el-icon>
<span>Navigator Two</span>
</el-menu-item>
<el-menu-item index="3" disabled>
<el-icon><document /></el-icon>
<span>Navigator Three</span>
</el-menu-item>
<el-menu-item index="4">
<el-icon><setting /></el-icon>
<span>Navigator Four</span>
</el-menu-item>
</el-menu>
</el-col>
</el-row>
</template>
<script setup>
import {
Document,
Menu as IconMenu,
Location,
Setting,
} from '@element-plus/icons-vue'
//store
// import {useStore} from "vuex"
// const store = useStore()
import {usePiniaUserStore} from '@/store/pinia_user'
const piniaUserStore = usePiniaUserStore()
import {useRouter} from "vue-router"
const router = useRouter();
import {ref, onMounted, computed} from 'vue'
const routes = computed(()=> {
return piniaUserStore.routesResult
})
onMounted(() => {
console.log(routes.value)
})
const handleOpen = (key, keyPath) => {
console.log(key, keyPath)
}
const handleClose = (key, keyPath) => {
console.log(key, keyPath)
}
</script>
<style lang="scss" scoped>
@import "@/assets/styles/ani.scss";
//$bg:#2c3e50;
$bg:rgba(#545c64,0.4);
.tac{
height: 100%;
width: 100%;
padding: 0;
margin: 0;
border: 0;
display: block;
position: absolute;
.el-col {
display: block;
position: absolute;
height: 100%;
width: 100% !important;
max-width: 100%;
.mb-2{
height: 60px;
background-color: $bg;
position: relative;
/* flex 布局 */
display: flex;
/* 实现垂直居中 */
align-items: center;
margin-bottom: 6px !important;
transition-duration: 1s;
&:hover{
background-color: rgba(#545c64,1) !important;
}
.menu-avatar{
position: relative;
margin-left: 8%;
margin-right: 8%;
left: 0;
}
.menuTitle{
position: relative;
font-weight: bold;
font-size: 15px;
color: white !important;
}
}
.el-menu{
background-color: $bg !important;
border: 0;
transition-duration: 1s;
&:hover{
background-color: rgba(#545c64,1) !important;
}
//.el-sub-menu{
// transition-duration: 1s;
// //background-color: rgba(#545c64,1) !important;
// &:hover{
// background-color: rgba(#545c64,1) !important;
// }
// .el-sub-menu__title{
// background-color: rgba(#545c64,1) !important;
// }
// .el-sub-menu{
// background-color: rgba(#545c64,0) !important;
// transition-duration: 1s;
// &:hover{
// background-color: rgba(#545c64,1) !important;
// }
// }
//}
}
//.el-menu-item-group__title{
// --el-menu-bg-color:$bg !important;
// background-color: $bg !important;
//}
//.el-menu-item-group{
// --el-menu-bg-color:$bg !important;
// background-color: $bg !important;
//
//}
.el-menu-vertical-demo{
height: 100%;
//width: 100%;
}
}
}
</style>
5.3 默认首页
进入主页后,默认重定向到的菜单地址,就是后台首页,我们将其定义为dashboard
<template>
<div class="dashboard-container">
<div class="dashboard-text">name: {{ name }}</div>
</div>
</template>
<script setup>
// import { mapGetters } from 'vuex'
// export default {
// name: 'Dashboard',
// // computed: {
// // ...mapGetters([
// // 'name'
// // ])
// // }
// }
const name = "Dashboard";
</script>
<style lang="scss" scoped>
.dashboard {
&-container {
margin: 30px;
}
&-text {
font-size: 30px;
line-height: 46px;
}
}
</style>
5.4 配置路由
我们要实现根据路由的不同显示不同页面到Main组件的位置上,初始自动显示首页,那么可以先将首页设置为默认地址的重定向
此时我们的默认首页就和’/dashboard’这个路径对应起来了,而且访问’/‘时会自动重定向到’/dashboard’
{
path: '/',
name: 'home',
redirect: '/dashboard',
component: () => import( '@/layout/index.vue'),
children: [{//这里的内容会在<router-view :key="key" />标签渲染
path: 'dashboard',
name: 'Dashboard',
component: () => import('@/views/dashboard/index'),
meta: { title: '首页', icon: 'dashboard' }
}]
},
5.5 Main组件根据路由path渲染对应页面
<router-view :key="key" />
可以渲染指定路由的组件
<template>
<section class="app-main">
<router-view v-slot="{ Component }">
<transition name="fade-transform" mode="out-in">
<div>
<component :is="Component" :key="key" />
</div>
</transition>
</router-view>
</section>
</template>
<script setup>
//获取路由
import {useRouter} from "vue-router";
import {computed} from "vue";
const route = useRouter();
const key = computed(() => {
return route.path
})
//获取当前路由path
// function key(){
// return route.path
// }
</script>
<style lang="scss">
@import "@/assets/styles/ani";
.app-main {
/*50 = navbar */
//min-height: calc(100vh - 50px);
//margin-bottom: 200px;
height: 100%;
width: 100%;
position: absolute;
left: 0;
top: 0;
//overflow: hidden !important;
}
.fixed-header+.app-main {
padding-top: 50px;
}
</style>
<style lang="scss">
// fix css style bug in open el-dialog
.el-popup-parent--hidden {
.fixed-header {
padding-right: 15px;
}
}
</style>
6. 登录页面跳转逻辑解析
要加上登录成功跳转到目标页面,有两种情况
1. 第一次登录,直接跳转’/'路径到首页即可
2. 已经在系统中操作,假设当前处于’\text\urlContr’路由,但是因为某些原因(token过期等)导致需要重新登录,则登录完成后,要跳转回’\text\urlContr’路由,而不是也跳转到首页
<script setup>
import {watch, ref, onMounted} from 'vue'
import {ElMessage} from 'element-plus'
import {usePiniaUserStore} from '@/store/pinia_user'
import {useRouter} from 'vue-router'
const router = useRouter()//router对象,VUE3写法
const piniaUserStore = usePiniaUserStore()
//引用
const loginForm = ref({
username: '',
password: ''
})
//获取ref="loginRef"的DOM元素
const loginRef = ref(null)
//登录按钮是否禁用
const disabledFlag = ref(false)//先不禁用
//表单校验规则
const loginRules = {
username: [
//提交表单时触发,blur模糊匹配
{required: true, trigger:'blur',message: '请输入用户名'},
{min:3,max:25,message: '用户名长度应该为3-25个字符',trigger:'blur'},
],
password: [{required: true, trigger:'blur',message: '请输入密码'},]
}
/**
* 监听路由的变化,如果是从某一个页面A跳转过来进行登录,就记录A的路由,完成登录后跳转回A,而不是无论如何都跳转回主页
* watch: {
* $route: {
* handler: function(route) {
* this.redirect = route.query && route.query.redirect
* },
* immediate: true
* }
* },
*/
const redirect = ref(null)//重定向地址
//路由变动时需要的监听触发函数
const handleRouteChange = (newRoute)=>{
console.log(router.currentRoute.value.query && router.currentRoute.value.query.redirect)
redirect.value = router.currentRoute.value.query && router.currentRoute.value.query.redirect
}
//模拟immediate=true的情况
onMounted(()=>{
handleRouteChange()
})
//监听路由的变化
watch(router,handleRouteChange)
//登录逻辑
const handleLogin = () => {
//禁用登录按钮
disabledFlag.value=true
//先表单验证
loginRef.value.validate(async (valid) => {
//如果验证通过
if (valid) {
//禁用登录按钮
// disabledFlag.value=true
//action监听,user.js下的login
// await store.dispatch('user/login', loginForm.value).then(() => {
await piniaUserStore.login(loginForm.value).then(() => {
console.log('登录成功')
console.log('跳转:',redirect.value || '/')
//进行路由跳转,如果redirect存在,就跳转回redirect的地址,而不是'/'
router.push({path:redirect.value || '/'})
disabledFlag.value=false//异步过程中,阻止登录按钮的使用
}).catch(()=>{
disabledFlag.value=false//异步过程中,阻止登录按钮的使用
})
}else{//如果验证不通过
console.log('登录失败')
ElMessage.error('输入信息有问题个damn的!!!!')
return false
}
})
}
</script>
7. header 和 Footer
先简单实现一下
<template>
<div ref="footer_div" class="footer bg_mine">
Copyright © 2025-2030 Taylor Sinclair 版权所有 <a href="" target="_blank">殷志鹏</a>
<div class="line-run"></div>
</div>
</template>
<script setup>
// import {ref,onMounted} from "vue";
// const footer_div = ref(null)
// onMounted(() => {
// let footer_div_dom = footer_div.value
// // console.log(footer_div_dom)
// footer_div.value.addEventListener('mouseenter', (e) => {
// // footer_div_dom.classList.add("mouseenter");
// })
// })
</script>
<style lang="scss" scoped>
@import "@/assets/styles/ani";
.footer{
//background-color: $bg_2;
//color: $text-color;
width: 100%;
height: 100%;
padding: 20px;
display: flex;
align-items: center;
position: relative;
}
.mouseenter{
positon: relative;
}
</style>