SpringCloud+Vue+Python人工智能(fastAPI,机器学习,深度学习)前后端架构各功能实现思路——Vue3.0前后端分离架构——后台系统前端环境搭建

SpringCloud+Vue+Python人工智能(fastAPI,机器学习,深度学习)前后端架构各功能实现思路——主目录(持续更新):https://blog.csdn.net/grd_java/article/details/144986730

注意,因为前端业务代码重复度高,所以写多了发现,逻辑基本一样,这里直接将常用的一次引入,开发经验少的同学可能会看不懂,后面到了具体业务时,会具体讲解这些代码

1. 开发环境

1. 安装node.js的22版本。官网下载并傻瓜式安装即可,如果有更加新的版本就用最新版本之前1个版本的(例如最新是23版本,你就用22版本,最新的一般有bug)


2. 打开cmd输入node -v查看node.js版本,以及npm -v查看npm的版本,如果正确显示版本号,说明安装成功


在这里插入图片描述


  1. 安装Vue Cli:是官方的vue.js项目脚手架,可以快速创建vue项目。因为vue的ui是在Vue CLI基础上封装的,所以我们必须得用

在这里插入图片描述

  • 先安装cnpm
npm install -g cnpm --registry=https://registry.npmmirror.com/
  • 再用cnpm安装vue/cli
 cnpm install -g @vue/cli

2. Vue ui生成Vue项目

安装完成后,输入命令vue ui


在这里插入图片描述


会自动打开创建项目的页面,生成即可


在这里插入图片描述


包管理器使用npm,无需初始化仓库


在这里插入图片描述


选择手动配置


在这里插入图片描述


将主要功能选择上


在这里插入图片描述


创建项目,不保存预设


在这里插入图片描述

然后依次为其安装依赖


在这里插入图片描述

cnpm install axios
cnpm install element-plus
cnpm install sass-loader
cnpm install sass
cnpm install webpack
cnpm install svg-sprite-loader
cnpm install qs
cnpm install js-cookie
cnpm install jsencrypt
cnpm install @element-plus/icons-vue
cnpm install nprogress
cnpm install pinia
cnpm install path-browserify
cnpm install path-to-regexp
# 可选项,无需持久化,可以不引入
cnpm install pinia-plugin-persistedstate

需要的依赖(大家如果报错的话,可以和我的dependencies同步一下,避免发生冲突导致莫名其妙的报错),修改好后,删除项目中node_modules文件夹,重新执行cnpm install命令,会根据下面的依赖配置全部进行下载

  "dependencies": {
    "@element-plus/icons-vue": "^2.3.1",
    "axios": "^1.7.2",
    "core-js": "^3.8.3",
    "element-plus": "^2.2.6",
    "js-cookie": "^3.0.1",
    "jsencrypt": "^3.2.1",
    "nprogress": "^0.2.0",
    "path-browserify": "^1.0.1",
    "path-to-regexp": "2.4.0",
    "pinia": "^2.3.0",
    "qs": "^6.10.5",
    "sass": "^1.52.3",
    "sass-loader": "^13.0.0",
    "svg-sprite-loader": "^6.0.11",
    "vue": "^3.2.13",
    "vue-router": "^4.0.3",
    "webpack": "^5.73.0"
  },

3. 使用element plus

如何使用,其官网已经给出的很详细了


在这里插入图片描述

main.js中配置elementplus


在这里插入图片描述

//创建全局app,暴露出去
import { createApp } from 'vue'
export const app = createApp(App)
//App是主页面,必须引入
import App from './App.vue'

//路由
import router from './router'
app.use(router)

//pinia
import {createPinia} from 'pinia'
const pinia = createPinia()
// import piniaPluginPersistedstate from 'pinia-plugin-persistedstate' //持久化
// pinia.use(piniaPluginPersistedstate)
app.use(pinia)


/**element plus start**/
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'

app.use(ElementPlus) //引入ElementPlus
/**element plus end**/
// 如果您正在使用CDN引入,请删除下面一行。
import * as ElementPlusIconsVue from '@element-plus/icons-vue'

//您需要从 @element-plus/icons-vue 中导入所有图标并进行全局注册。
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
    app.component(key, component)
}

//路由导航
import '@/permission'

/**引入全局样式start**/
import '@/assets/styles/reset.css'
import '@/assets/styles/border.css'
import '@/assets/styles/index.scss'
/**引入全局样式end**/

app.mount('#app')

随便去复制点代码,测试一下


在这里插入图片描述


复制到About组件中


在这里插入图片描述

4. 常用工具类

1. auth工具类,主要是操作cookie对token进行增删改查


在这里插入图片描述

//使用js-cookie存储token,这里封装了很多针对cookie等浏览器存储功能
import Cookies from 'js-cookie'
//Token的key值,我们需要根据后端需要,进行修改
// const TokenKey = 'vue_admin_template_token'
const TokenKey = 'vue_admin_template_token'
//对外暴露获取token的函数
export function getToken() {
  return Cookies.get(TokenKey)
}
//对外暴露设定token的函数
export function setToken(token) {
  return Cookies.set(TokenKey, token)
}
//对外暴露移除token的函数
export function removeToken() {
  return Cookies.remove(TokenKey)
}

2. treeutil工具类,主要用于处理树形结构

/**
 * 构造树型结构数据
 * @param {*} source 数据源
 * @param {*} id id字段 默认 'id'
 * @param {*} parentId 父节点字段 默认 'parentId'
 * @param {*} children 孩子节点字段 默认 'children'
 * @param {*} rootId 根Id 默认 0
 */
export function treeData (source, id, pid, children, rootId) {
  id = id || 'id'
  pid = pid || 'pid'
  children = children || 'children'
  rootId = rootId || 0
  const cloneData = JSON.parse(JSON.stringify(source))// 对源数据深度克隆
 return cloneData.filter(father => {
    const branchArr = cloneData.filter(child => father[id] == child[pid])// 返回每一项的子级数组
    branchArr.length > 0 ? father[children] = branchArr : delete father[children]// 如果存在子级,则给父级添加一个children属性,并赋值
    return father[pid] == rootId // 返回第一层
  })
}

/**
 * 树形结构解析,构造为普通形式
 */
export function unTreeData(source,children){
  children = children || 'children'
  let cloneData = JSON.parse(JSON.stringify(source))// 对源数据深度克隆
  let arr = []//保存最终结果
  unTreeDatadd(cloneData,children,arr)//递归解析树结构,解析结果放在arr中
  return arr;//将arr返回
}
function unTreeDatadd(source,children,arr){
  //遍历数组
  source.filter(father=>{
    if(father[children]){//如果当前元素有child,将当前元素加入arr,然后递归它的儿子加入arr,儿子都递归完,删除当前元素的children属性
      arr.push(father)//将当前元素加入arr
      unTreeDatadd(father[children],children,arr)//递归儿子
      delete father[children]//删除children
    }else{//如果当前元素没有child
      arr.push(father)//直接添加到arr
    }
  })
}

3. validate.js: 处理Path路径相关,主要使用其提供的正则匹配url地址


在这里插入图片描述

/**
 * Created by PanJiaChen on 16/11/18.
 */

/**
 * @param {string} path
 * @returns {Boolean}
 */
export function isExternal(path) {
  return /^(https?:|mailto:|tel:)/.test(path)
}

/**
 * @param {string} str
 * @returns {Boolean}
 */
export function validUsername(str) {
  const valid_map = ['admin', 'editor']
  return valid_map.indexOf(str.trim()) >= 0
}

5. router路由配置

因为不同用户要展示不同路由,所以需要定义常规路由和异步动态路由


在这里插入图片描述

import { createRouter, createWebHashHistory } from 'vue-router'

//1. 将vue-router模块,挂载到Vue.VUE3中,只需要在main.js引入即可,所以这里不需要额外操作

// 2.拆分路由
//2.1.常量路由,无论何种角色,都可以访问的路由
export const constantRoutes = [
  {
    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: 'Management' }
    }]
  },
  {
    path: '/login',
    name: 'login',
    component: () => import('@/views/login/index'),
    hidden: true
  },

  {
    path: '/404',
    component: () => import('@/views/404'),
    hidden: true
  },

  // {
  //   path: '/',
  //   component: Layout,
  //   redirect: '/dashboard',
  //   children: [{//这里的内容会在<router-view :key="key" />标签渲染
  //     path: 'dashboard',
  //     name: 'Dashboard',
  //     component: () => import('@/views/dashboard/index'),
  //     meta: { title: '首页', icon: 'dashboard' }
  //   }]
  // },
]
//2.2 异步路由,根据不同角色,需要过滤的路由
export const asyncRoutes = [
  {
    path: '/security',
    component: () => import( '@/layout/index.vue'),
    redirect: '/security/user',
    meta: { title: '权限管理', icon: 'Lock' },
    children: [
      {
        path: 'user',
        name: 'User',
        component: () => import('@/views/security/user/index'),
        meta: { title: '用户管理'}
      },
      {
        path: 'role',
        name: 'Role',
        component: () => import('@/views/security/role/index'),
        meta: { title: '角色管理'}
      },
      {
        path: 'menu',
        name: 'menu',
        component: () => import('@/views/security/menu/index'),
        meta: { title: '菜单管理'}
      },
    ]
  },
]
//2.3 任意路由,路径错误重定向到的路由
export const anyRoutes = { path: '*', redirect: '/404', hidden: true }// 404 page must be placed at the end !!!

// 3. 回调函数,封装了路由,routes封装的是上面的constantRoutes常量
const router = createRouter({
  history: createWebHashHistory(),
  scrollBehavior: () => ({ y: 0 }),
  routes: constantRoutes
})

// 4. 对外暴露,重置路由函数
export function resetRouter() {
  const newRouter = router
  router.matcher = newRouter.matcher // reset router
}
// 5. 对外暴露,router这个对象
export default router

6. pinia全局控制配置

主要帮助我们控制全局的token,登录等逻辑。后续业务功能很可能也会使用到
在这里插入图片描述

import {defineStore} from "pinia";
import { login, logout, getInfo } from '@/api/user' //引入了我们的api。因为我们要向后端发请求
import { getMenusByUserId } from '@/api/menu' //引入了我们的api。因为我们要向后端发请求
import {treeData,unTreeData} from '@/utils/treeutil'//处理树形结构的工具类
import { getToken, setToken, removeToken } from '@/utils/auth' //auth权限相关操作
import { resetRouter , asyncRoutes,anyRoutes,constantRoutes} from '@/router'//拆分好的路由
import router from '@/router'

//比较路由,返回需要展示的异步路由
const compareasyncRoutes = (asyncRoutes,routes)=>{
    return asyncRoutes.filter(item =>{
        if(routes.indexOf(item.path)!=-1){//如果当前路由,用户拥有权限,就展示
            if(item.children&&item.children.length){//如果有children就递归
                item.children = compareasyncRoutes(item.children,routes)
            }
            return true;
        }
    })
}
//比较路由,返回需要删除的路由
const compareDeleteRoutes = (currentRoute,oKShowRoutes)=>{
    return currentRoute.filter(item =>{
        if(oKShowRoutes.indexOf(item.path)===-1){//如果当前路由,是不可以展示的,就是需要删除的
            if(item.children&&item.children.length){//如果有children就递归
                item.children = compareasyncRoutes(item.children,oKShowRoutes)
            }
            return true;
        }
    })
}
// const defaultRoutes = (routesResult,resultAsyncRoutes)=>{
//     resultAsyncRoutes = []
//     routesResult = constantRoutes.concat(resultAsyncRoutes,anyRoutes);
//     console.log(asyncRoutes);
//     router.addRoute(routesResult)
// }

//getDefaultState常数封装了token,name,avatar
const getDefaultState = () => {
    return {
        token: getToken(),
        name: '',
        avatar: '',
        routes:[],//服务器返回的菜单path,菜单就是type类型为menu的
        roles:[],//保存当前用户拥有角色
        buttons:[],//按钮权限信息,按钮就是菜单类型为button的
        resultAsyncRoutes:[],//对比后的异步路由
        routesResult:[],//最终要展示的路由
    }
}

export const usePiniaUserStore = defineStore("piniaUserStore", {
    state: ()=>{
        return getDefaultState()
    },
    // persist:[
    //     {
    //       key: 'token',
    //       storage: localStorage,
    //     },
    //     {
    //         key: 'name',
    //         storage: localStorage,
    //     },
    //     {
    //         key: 'routesResult',
    //         storage: localStorage,
    //     }
    // ],
    getters: {
      getName: (state) => state.name,
      getToken: state => state.token,
      getAvatar: state => state.avatar,
      getRoutesResult: state => state.routesResult,
    },
    actions:{
        // 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)
                // console.log(menusResult)
                if(menusResult.code === 20000){
                    const { menus } = menusResult
                    // console.log(menus)
                    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{
                        // defaultRoutes(this.routesResult,this.resultAsyncRoutes)
                        this.resultAsyncRoutes = []
                        this.routesResult = constantRoutes.concat(this.resultAsyncRoutes,anyRoutes);
                        router.addRoute(this.routesResult)
                    }
                }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));
            }
        },

        // user logout
        async logout() {
            let result = await logout(this.token);
            if(result.code === 20000){
                removeToken() // must remove  token  first
                resetRouter()
                // commit('RESET_STATE')
                this.$reset()
            }else{
                //退出登录失败
                return Promise.reject(new Error(result.msg));
            }
        },

        // remove token
        resetToken() {
            return new Promise(resolve => {
                removeToken() // must remove  token  first
                // commit('RESET_STATE')
                this.$reset()
                resolve()
            })
        }
    }
})

7. request.js工具类

是我们发送请求的一个工具类,我们需要每次发送请求时,拦截请求并添加token,以及其它一些常用操作都封装在这个文件中


在这里插入图片描述

import axios from 'axios'
import { ElMessageBox, ElMessage } from 'element-plus'
import {usePiniaUserStore} from "@/store/pinia_user"


import { getToken,removeToken } from '@/utils/auth'

const baseURL = 'http://localhost:7071'

// create an axios instance
export const service = axios.create({
  baseURL: baseURL, // url = base url + request url
  // withCredentials: true, // send cookies when cross-domain requests
  timeout: 5000 // request timeout
})
//获取baseURL
export function getServerUrl(){return baseURL}

// request interceptor
service.interceptors.request.use(
  config => {
    const piniaUserStore = usePiniaUserStore();
    // do something before request is sent
    // if (store.getters.token) {
    if (piniaUserStore.token) {
      // let each request carry token
      // ['X-Token'] is a custom headers key
      // please modify it according to the actual situation
      config.headers['authorization'] = getToken()
    }
    return config
  },
  error => {
    // do something with request error
    console.log(error) // for debug
    return Promise.reject(error)
  }
)

// response interceptor
service.interceptors.response.use(
  /**
   * If you want to get http information such as headers or status
   * Please return  response => response
  */

  /**
   * Determine the request status by custom code
   * Here is just an example
   * You can also judge the status by HTTP Status Code
   */
  response => {
    const piniaUserStore = usePiniaUserStore();
    //获取响应数据
    const res = response.data
    // console.log("response interceptor:",res)
    //如果响应内容不存在,直接抛错
    if(res == undefined){
      return Promise.reject(new Error(res.msg || '没有获取到服务器响应,基础功能出错,无法正常运行!!!请联系管理员'))
    }
    //如果响应内容存在,但是没有响应码code,表示是无需关注响应码的响应体(例如验证码)
    //直接放行
    if(res.code == undefined){
      return res;
    }
    // console.log(res)
    // if the custom code is not 20000, it is judged as an error.
    if (res.code !== 20000) {
      if (res.msg){
        ElMessage({
          message: res.msg || 'Error',
          type: 'warning',
          duration: 2 * 1000
        })
      }


      // 50008: Illegal token; 50012: Other clients logged in; 50014: Token expired;
      if (res.code === 401 || res.code === 50008 || res.code === 50012 || res.code === 50014) {
        removeToken()
        // to re-login。You have been logged out, you can cancel to stay on this page, or log in again
        ElMessageBox.confirm('您已经被迫登出,你可以点击cancel按钮留在当前页面,或者点击Re-Login重新登录', 'Confirm logout', {
          confirmButtonText: 'Re-Login',
          cancelButtonText: 'Cancel',
          type: 'warning'
        }).then(() => {
          ElMessage({
            type: 'success',
            message: 'Delete completed',
          })
          piniaUserStore.resetToken().then(() => {
            location.reload()
          })
          // store.dispatch('user/resetToken').then(() => {
          //   location.reload()
          // })
        }).catch(() => {
          ElMessage({
            type: 'info',
            message: 'Delete canceled',
          })
        })
        return res
      }



      // return Promise.reject(new Error(res.msg || 'Error'))
      // ElMessage.error(res.msg||"Error");
      return res;
    } else {
      return res
    }
  },
  error => {
    console.log('err' + error) // for debug
    ElMessage({
      message: error.msg,
      type: 'error',
      duration: 5 * 1000
    })
    return Promise.reject(error)
  }
)
/**
 * 文件上传请求
 */
export function fileUpload(url, params = {}) {
  return new Promise((resolve, reject) => {
    service({
      url:url,
      method:'post',
      data:params,
      headers:{'Content-Type':'multipart/form-data'}
    }).then((response) => {
      console.log(response);
      resolve(response);
    }).catch((error) => {
      console.log('err' + error)
      reject(error);
    })
  })
}

8. 引入全局通用css样式

网站格式应该统一一点,不可以乱七八糟,每个页面都独具一格。例如边框都是1像素的宽度,布局的比例也应该统一

在这里插入图片描述

引入完成后,记住在main.js中引入


在这里插入图片描述

border.css文件,统一边框样式


@charset "utf-8";
.border,
.border-top,
.border-right,
.border-bottom,
.border-left,
.border-topbottom,
.border-rightleft,
.border-topleft,
.border-rightbottom,
.border-topright,
.border-bottomleft {
    position: relative;
}
.border::before,
.border-top::before,
.border-right::before,
.border-bottom::before,
.border-left::before,
.border-topbottom::before,
.border-topbottom::after,
.border-rightleft::before,
.border-rightleft::after,
.border-topleft::before,
.border-topleft::after,
.border-rightbottom::before,
.border-rightbottom::after,
.border-topright::before,
.border-topright::after,
.border-bottomleft::before,
.border-bottomleft::after {
    content: "\0020";
    overflow: hidden;
    position: absolute;
}
/* border
 * 因,边框是由伪元素区域遮盖在父级
 * 故,子级若有交互,需要对子级设置
 * 定位 及 z轴
 */
.border::before {
    box-sizing: border-box;
    top: 0;
    left: 0;
    height: 100%;
    width: 100%;
    border: 1px solid #eaeaea;
    transform-origin: 0 0;
}
.border-top::before,
.border-bottom::before,
.border-topbottom::before,
.border-topbottom::after,
.border-topleft::before,
.border-rightbottom::after,
.border-topright::before,
.border-bottomleft::before {
    left: 0;
    width: 100%;
    height: 1px;
}
.border-right::before,
.border-left::before,
.border-rightleft::before,
.border-rightleft::after,
.border-topleft::after,
.border-rightbottom::before,
.border-topright::after,
.border-bottomleft::after {
    top: 0;
    width: 1px;
    height: 100%;
}
.border-top::before,
.border-topbottom::before,
.border-topleft::before,
.border-topright::before {
    border-top: 1px solid #eaeaea;
    transform-origin: 0 0;
}
.border-right::before,
.border-rightbottom::before,
.border-rightleft::before,
.border-topright::after {
    border-right: 1px solid #eaeaea;
    transform-origin: 100% 0;
}
.border-bottom::before,
.border-topbottom::after,
.border-rightbottom::after,
.border-bottomleft::before {
    border-bottom: 1px solid #eaeaea;
    transform-origin: 0 100%;
}
.border-left::before,
.border-topleft::after,
.border-rightleft::after,
.border-bottomleft::after {
    border-left: 1px solid #eaeaea;
    transform-origin: 0 0;
}
.border-top::before,
.border-topbottom::before,
.border-topleft::before,
.border-topright::before {
    top: 0;
}
.border-right::before,
.border-rightleft::after,
.border-rightbottom::before,
.border-topright::after {
    right: 0;
}
.border-bottom::before,
.border-topbottom::after,
.border-rightbottom::after,
.border-bottomleft::after {
    bottom: 0;
}
.border-left::before,
.border-rightleft::before,
.border-topleft::after,
.border-bottomleft::before {
    left: 0;
}
@media (max--moz-device-pixel-ratio: 1.49), (-webkit-max-device-pixel-ratio: 1.49), (max-device-pixel-ratio: 1.49), (max-resolution: 143dpi), (max-resolution: 1.49dppx) {
    /* 默认值,无需重置 */
}
@media (min--moz-device-pixel-ratio: 1.5) and (max--moz-device-pixel-ratio: 2.49), (-webkit-min-device-pixel-ratio: 1.5) and (-webkit-max-device-pixel-ratio: 2.49), (min-device-pixel-ratio: 1.5) and (max-device-pixel-ratio: 2.49), (min-resolution: 144dpi) and (max-resolution: 239dpi), (min-resolution: 1.5dppx) and (max-resolution: 2.49dppx) {
    .border::before {
        width: 200%;
        height: 200%;
        transform: scale(.5);
    }
    .border-top::before,
    .border-bottom::before,
    .border-topbottom::before,
    .border-topbottom::after,
    .border-topleft::before,
    .border-rightbottom::after,
    .border-topright::before,
    .border-bottomleft::before {
        transform: scaleY(.5);
    }
    .border-right::before,
    .border-left::before,
    .border-rightleft::before,
    .border-rightleft::after,
    .border-topleft::after,
    .border-rightbottom::before,
    .border-topright::after,
    .border-bottomleft::after {
        transform: scaleX(.5);
    }
}
@media (min--moz-device-pixel-ratio: 2.5), (-webkit-min-device-pixel-ratio: 2.5), (min-device-pixel-ratio: 2.5), (min-resolution: 240dpi), (min-resolution: 2.5dppx) {
    .border::before {
        width: 300%;
        height: 300%;
        transform: scale(.33333);
    }
    .border-top::before,
    .border-bottom::before,
    .border-topbottom::before,
    .border-topbottom::after,
    .border-topleft::before,
    .border-rightbottom::after,
    .border-topright::before,
    .border-bottomleft::before {
        transform: scaleY(.33333);
    }
    .border-right::before,
    .border-left::before,
    .border-rightleft::before,
    .border-rightleft::after,
    .border-topleft::after,
    .border-rightbottom::before,
    .border-topright::after,
    .border-bottomleft::after {
        transform: scaleX(.33333);
    }
}

reset.css统一全局通用样式


@charset "utf-8";
html{font-size:12px}
body,ul,ol,dl,dd,h1,h2,h3,h4,h5,h6,figure,form,fieldset,legend,input,textarea,button,p,blockquote,th,td,pre,xmp{margin:0;padding:0}
body,input,textarea,button,select,pre,xmp,tt,code,kbd,samp{line-height:1.5;font-family:tahoma,arial,"Hiragino Sans GB",simsun,sans-serif}
h1,h2,h3,h4,h5,h6,small,big,input,textarea,button,select{font-size:100%}
h1,h2,h3,h4,h5,h6{font-family:tahoma,arial,"Hiragino Sans GB","微软雅黑",simsun,sans-serif}
h1,h2,h3,h4,h5,h6,b,strong{font-weight:normal}
address,cite,dfn,em,i,optgroup,var{font-style:normal}
table{border-collapse:collapse;border-spacing:0;text-align:left}
caption,th{text-align:inherit}
ul,ol,menu{list-style:none}
fieldset,img{border:0}
img,object,input,textarea,button,select{vertical-align:middle}
article,aside,footer,header,section,nav,figure,figcaption,hgroup,details,menu{display:block}
audio,canvas,video{display:inline-block;*display:inline;*zoom:1}
blockquote:before,blockquote:after,q:before,q:after{content:"\0020"}
textarea{overflow:auto;resize:vertical}
input,textarea,button,select,a{outline:0 none;border: none;}
button::-moz-focus-inner,input::-moz-focus-inner{padding:0;border:0}
mark{background-color:transparent}
a,ins,s,u,del{text-decoration:none}
sup,sub{vertical-align:baseline}
html {overflow-x: hidden;height: 100%;font-size: 50px;-webkit-tap-highlight-color: transparent;}
body {font-family: Arial, "Microsoft Yahei", "Helvetica Neue", Helvetica, sans-serif;color: #333;font-size: .28em;line-height: 1;-webkit-text-size-adjust: none;}
hr {height: .02rem;margin: .1rem 0;border: medium none;border-top: .02rem solid #cacaca;}
a {color: #25a4bb;text-decoration: none;}

3. index.scss是css3布局用的


body {
  height: 100%;
  -moz-osx-font-smoothing: grayscale;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Arial, sans-serif;
}

label {
  font-weight: 700;
}

html {
  height: 100%;
  box-sizing: border-box;
}

#app {
  height: 100%;
}

*,
*:before,
*:after {
  box-sizing: inherit;
}

a:focus,
a:active {
  outline: none;
}

a,
a:focus,
a:hover {
  cursor: pointer;
  color: inherit;
  text-decoration: none;
}

div:focus {
  outline: none;
}

.clearfix {
  &:after {
    visibility: hidden;
    display: block;
    font-size: 0;
    content: " ";
    clear: both;
    height: 0;
  }
}

// main-container global css
.app-container {
  padding: 20px;
}

9. 页面title

1. 新建settings.js,暴露全局标题


在这里插入图片描述

/**
 * module.exports‌是Node.js中用于导出模块的一个特殊对象。在Node.js中,每个模块都有一个module对象,
 * 该对象代表当前模块,而module.exports是module对象的一个属性,用于导出模块中的函数、对象或值。
 * 其他文件可以通过require()函数来使用这些导出的内容‌
 * **/
module.exports = {

  title: 'Vue Admin Template',

  /**
   * @type {boolean} true | false
   * @description Whether fix the header
   */
  fixedHeader: false,

  /**
   * @type {boolean} true | false
   * @description Whether show the logo in sidebar
   */
  sidebarLogo: true
}

2. 生成页面title的工具类,get-page-title.js


在这里插入图片描述

import defaultSettings from '@/settings'
// 如果defaultSettings.title为空,默认指定为'Vue Admin Template'
const title = defaultSettings.title || 'Vue Admin Template'
//生成title,pageTitle是当前路由到的页面的title
export default function getPageTitle(pageTitle) {
  if (pageTitle) {
    return `${pageTitle} - ${title}`
  }
  return `${title}`
}

10. 导航守卫

每次路由的改变,都会调用此守卫,如果token过期等触发,就路由到登录页面


在这里插入图片描述

main.js中注册,(但是硬性要求登录和获取当前登录用户信息两个接口,如果没写好的话,先不要在main.js中注册)


在这里插入图片描述

/**
 * 路由导航守卫
 **/
import router from "@/router";

// import store from "@/store";
import {usePiniaUserStore} from "@/store/pinia_user"

import {ElMessage} from "element-plus";
import {getToken} from "@/utils/auth";

import NProgress from "nprogress";
import getPageTitle from "@/utils/get-page-title";

NProgress.configure({ showSpinner: false })
//白名单
const whiteList = ['/login']

//当发生路由跳转进行异步调用,to表示要去的路由,from表示要离开的路由,next用来执行钩子,有多种重载形式,比如next(),next({})
router.beforeEach( async (to, from, next) => {
    // console.log(router.getRoutes())
    const piniaUserStore = usePiniaUserStore();
    const toPath = to.path
    // console.log(toPath)
    // 开始进度条
    NProgress.start()
    //设置页面标题
    document.title = getPageTitle(to.meta.title);
    // 通过token确定用户是否已登录
    const hasToken = getToken()
    //如果有token,则已登录
    if (hasToken) {
        // console.log('已登录的用户')
        /**动态路由需要解决,刷新路由丢失问题**/
        //判断是否为刷新页面,vuex中无数据,则为刷新页面
        // if (store.state.user.routesResult.length === 0) {
        if (piniaUserStore.routesResult.length === 0) {
            console.log('检测到刷新页面,重新获取用户信息')
            // 获取用户信息(关于异步路由的东西我放这里了)
            // store.dispatch('user/getInfo').then(()=>{
            //     // console.log('获取成功,开始跳转路由:',to.path)
            //     next({path:to.path})
            // })
            await piniaUserStore.getInfo().then(() => {
                // console.log('获取成功,开始跳转路由:', toPath)
                next({path: toPath})
            })
        }
        // else if(piniaUserStore.routesResult.length !== 0) {
        //     console.log("非刷新页面")
        //     next()
        // }
        //如果用户要去的是登录页,直接从定向到主页,因为已经登录了,不用重复登录
        else if (to.path === '/login') {
            // console.log('正在前往login')
            next({path: '/'})
            NProgress.done()//进度条转态为完成
        } else {//如果要前往的路由不是/login
            //console.log("非login")
            //获取用户名
            // const hasGetUserInfo = store.getters.name
            const hasGetUserInfo = piniaUserStore.name
            //如果用户名存在
            if (hasGetUserInfo) {
                //console.log('如果用户名存在')
                next()//进行管道中的下一个钩子。如果全部钩子执行完了,则导航的状态就是 confirmed (确认的)。
            } else {//不存在
                // try {
                    // 获取用户信息
                    // store.dispatch('user/getInfo')
                await piniaUserStore.getInfo().then(e=>{
                    //console.log("获取用户信息成功!")
                    next()//进行管道中的下一个钩子。如果全部钩子执行完了,则导航的状态就是 confirmed (确认的)。
                }).catch(async error=>{//如果获取用户信息出错
                    // 删除令牌,进入登录页面重新登录
                    // store.dispatch('user/resetToken')
                    await piniaUserStore.resetToken()
                    ElMessage.error(error || 'Has Error')
                    next(`/login?redirect=${toPath}`)//next('/') 或者 next({ path: '/' }): 跳转到一个不同的地址
                    NProgress.done()//进度条结束
                })

                // } catch (error) {//如果获取用户信息出错

                // }
            }
        }
    } else {
        /* has no token 没有登录的话*/
        //是否是白名单的路径
        if (whiteList.indexOf(to.path) !== -1) {
            // in the free login whitelist, go directly
            next()
        } else {//不是白名单的路径直接定位到登录页面
            // other pages that do not have permission to access are redirected to the login page.
            next(`/login?redirect=${to.path}`)
            NProgress.done()
        }
    }
})
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ydenergy_殷志鹏

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值