vue中实现后台系统权限管理的功能

news2024/11/17 21:41:26

一、前言

后台管理系统的权限控制对于前端来说是经常用到的知识点,也比较重要,最近梳理一下写成文章,方便以后查阅。
项目中实现菜单的动态权限控制使用到了两种技术,一种是Vue Router,另一种是vue3官方推荐使用的专属状态管理库Pinia。

二、权限由 前端还是后端 来控制?

正式开始之前我们先讨论下权限由 前端还是后端 来控制?网上百度很多资料都是路由表由后端根据用户的权限动态生成的,我们项目中未采取这种方式的原因如下:

  1. 项目后期不断迭代前端会异常痛苦,前端新开发一个页面还要让后端配一下路由和权限,很不方便,不能达到真正的前后端分离。
  2. 其次,拿业务来说,虽然后端的确也是有权限验证的,但他的验证其实针对业务来划分的;比如运营主管可以编辑新增商品,而普通运营只能查看商品列表,但对于前端来说,不管是运营主管还是普通运营都是有权限进入商品列表页的。所以前端和后端权限的划分是不太一致的。
  3. 还有一点是就vue2.2.0之前异步挂载路由是很麻烦的一件事!不过好在官方也出了新的api,虽然本意是来解决ssr的痛点的。

所以我们项目解决方法就是,前端会有一份路由表,他表示了每一个路由可访问的权限。当用户登录之后,获取后端用户权限的路由表 ,再去和前端路由表比对,生成当前用户权限可访问的路由表,通过router.addRoute动态挂载到router上。但这些控制都只是页面级的,说白了前端再怎么做权限控制都不是绝对安全的,后端的权限验证是逃不掉的。

对于后端来说,会去验证前端每一个涉及请求的操作,通过用户token验证其是否有该操作权限。若没有操作权限抛出一个对应状态码,前端对该状态码做出相应提示操作。

三、权限控制的两大部分

首先权限控制分为两大部分。

1. 接口访问的权限控制
2. 页面的权限控制
1)侧边栏菜单中的页面是否能被访问
2)页面中的按钮(增删改查)的权限控制是否显示

下面就着重了解下前端如何对这两部分进行权限控制。

四、接口访问的权限控制

接口权限就是对用户的校验。正常来说,在用户登录时服务器需要给前台返回一个token,以后调用后端接口时候,后端要求哪个接口传token,前端就在哪个接口传参时加上用户token,然后服务端获取到这个token后进行比对,如果通过则可以访问接口,正常返回数据。

现有的做法是在登录成功后,后端返回一个token(该token是一个能唯一标识用户身份的一个key),之后我们将token存储到sessionStorage中用的是web-storage-cache
存储,这样下次打开页面或者刷新页面的时候能记住用户的登录状态,然后请求时带着token,代码如下:

//commonQuery.ts
//commonQuery() 是一些接口要求的公共参数
export const tokenQuery = () => {
  return Object.assign(commonQuery(), { token: wsCache.get('token') })
}
/* eslint-disable camelcase */
import request from '@/request/http.ts'
import { tokenQuery } from '@/request/commonQuery.ts'
const testApi = {
  GETADSCATEGORYLIST() {
    return request({
      method: 'post',
      data: Object.assign(
        {
          method: 'xxx'
        },
        tokenQuery()
      )
    })
  }
}
export default testApi

ps:为了保证安全性,项目所有token 有效期都是session,就是当浏览器关闭就丢失了。重新打开浏览器都需要重新登录验证,后端也会在固定时间重新刷新token,让用户重新登录一次,确保后台用户不会因为电脑遗失或者其他原因被人随意使用账号。

五、页面的权限控制

前面已经说到,页面权限控制又分为两种:

  • 侧边栏菜单中的页面是否能被访问
  • 页面中的按钮(增删改查)的权限控制是否显示
这些权限一般是在前台固定页面进行配置的,保存后记录到数据库中。

按钮权限暂且不提,页面访问权限在实现中又可以分为两种方式:

  • 显示所有菜单,当用户访问不在自己权限内的菜单时,提示权限不足。
  • 只显示用户能访问权限内的菜单,如果用户通过URL进行强制访问,则会直接进入404。

既然展现出来的不能点击,不如直接不显示,所以还是方法二用户体验更好。

具体实现流程

1. main.ts中创建vue实例的时候将vue-router挂载,但这个时候vue-router挂载一些登录或者不用权限的公用页面。
2. 当用户登录后,后端返回用户权限下的路由表和前端写的异步路由表作比较,生成最终用户可访问的路由表。
3. 调用router.addRoute添加用户可访问的路由;先加载动态路由,再加载静态路由。
4. 使用Pinia存储路由表,根据存储可访问的路由渲染侧边栏组件。

1) router 创建路由表

创建路由表实际没什么难度,照着vue-router官方文档给的示例直接写就行。但是因为有部分页面是不需要访问权限的,所以需要将登录、404等页面写到公共路由中,而将其他需要权限的页面写到一个文件中,这样可以有效的减轻后续的维护压力。

比如下面的例子,router目录下,新建public.ts和主路由文件index.ts

// public.ts 
//不需要权限的公共路由表
import { RouteRecordRaw } from 'vue-router'
import Layout from '@/layout/index.vue'
const PublicRouter: Array<RouteRecordRaw> = [{
  name: 'login',
  path: '/login',
  component: () => import('@/views/user-manager/login/LoginPage.vue'),
  meta: {
    title: '登录',
    icon: 'dashboard-two',
    hidden: true
  }
}]

export default PublicRouter
// index.ts
import { createRouter, createWebHistory, RouteRecordRaw, createWebHashHistory } from 'vue-router'
import {AppRouteRecordRaw} from './types'
import publicRouter from './public.ts'  //不需要权限的公共路由表
const asyncFiles = require.context('./permissionModules', true, /\.ts$/)  //前端权限路由表数组;遍历文件夹中的指定文件,然后自动导入,
let permissionModules: Array<AppRouteRecordRaw> = []
asyncFiles.keys().forEach((key) => {
  permissionModules = permissionModules.concat(asyncFiles(key).default)
})
//根据sort数组排序
permissionModules.sort((a:AppRouteRecordRaw, b:AppRouteRecordRaw) => {
  return a.meta.sort - b.meta.sort
})
//创建路由对象,实例化vue的时候只挂载公共路由
const router = createRouter({
  history: createWebHashHistory(),
  routes: publicRouter
})
//导出异步路由
export const asyncRoutes: Array<AppRouteRecordRaw> = [
  ...permissionModules
]
 //导出路由对象,在main.ts中引用
export default router 
//main.ts
import App from './App.vue'
import router from './router'
import { createApp } from 'vue'

const app = createApp(App) //实例化vue的时候只挂载公共路由
app.use(router)
app.mount('#app')

permissionModules文件夹,可参考下。

在这里插入图片描述

这里有个知识点路由拆分,也是前端工程化,能帮助我们大大提升效率,更好维护代码,让代码更简洁。

使用require.context实现前端工程自动化,自动导入模块

如果把所有的路由信息都写在一个文件就会显得非常臃肿,同时也不便于观看维护;
vue中路由的信息都是依赖于一个数组,所以可以将这个数组拆开,分到其他文件夹下的ts文件中,通过export导出这些子数组变量,最后将这些子数组变量合并成一个大的数组。

2) src/store/permission.ts

之前我们就说过登录后会获取到用户权限路由表,然后用的 Pinia 状态管理,官方文档 有详细介绍,可以去看,下面请看代码:

// login.ts
function saveInfo(data:ReturnInfo) { // 处理用户登录信息
 setStorePermissionMenu(data.menus)  //存储权限菜单和按钮
 setStoreToken(data.token)  //存储token
 GenerateRoutes().then((res:RouteRecordRaw[]) => {  // 比对生成用户权限可访问的路由表
   res.forEach(
     async(route:RouteRecordRaw) => {
       await router.addRoute(route) //动态添加可访问路由表
     })
   setStoreIsAddRouters() //设置动态添加路由完毕
 })
 router.push({ path: '/' })  //登录成功之后重定向到首页
}
// store目录下的permission.ts
import { defineStore } from 'pinia'
import type { RouteRecordRaw } from 'vue-router'
import { deepClone } from '@/utils/tool'
import { asyncRoutes } from '@/router/index.ts'
import publicRouter from '@/router/public.ts'
import * as storage from '@/utils/token.ts'
export const usePermissionStore = defineStore({
  id: 'permission ', // id必填,且需要唯一
  state: () => {
    return {
      token: storage.getToken() || '',
      isAddRouters: false, // 是否已动态添加路由,刷新即重置,不存在sessionStorage中
      permissionMenu: storage.getPermissionMenu() || [] as any[], // 接口获取的路由列表
      addRouters: [] as RouteRecordRaw[], // 需要动态添加的路由
      permissionBtn: storage.getPermissionBtns() || [] as string[],
      menuData: storage.getMenuData() || [] as any[] // 左侧菜单,如果展示菜单与路由列表不一致,请自行处理
    }
  },

  actions: {
    GenerateRoutes(): Promise<unknown> {
      return new Promise((resolve) => {
        // 路由权限控制
        let routerMap: RouteRecordRaw[] = []
        console.log(asyncRoutes)
        routerMap = this.generateFn(deepClone(asyncRoutes, ['component']))
        // 先加载动态路由,再加载静态路由
        // 404页面放在最后加载;否则后面的所有页面都会被拦截到404
        this.addRouters = routerMap.concat([
          {
            path: '/:path(.*)*',
            redirect: '/error',
            name: '404',
            meta: {
              hidden: true,
              breadcrumb: false
            }
          }
        ])
        this.menuData = deepClone(publicRouter, ['component']).concat(routerMap)
        storage.setMenuData(this.menuData) //存储侧边栏菜单
        resolve(this.addRouters)
      })
    },
    //循环后端返回的用户权限路由表和前端写好的异步路由表,根据path路径是否相等进行匹配
    // 这部分是关键点
    generateFn(originRoutes: RouteRecordRaw[]): RouteRecordRaw[] {
      const res: RouteRecordRaw[] = []
      originRoutes.forEach(route => { // 循环所有前端写的需要权限的路由
        let data: any = null
        for (let i = 0; i < this.permissionMenu.length; i++) { // 循环后台返回的一维权限菜单,进行匹配
          if (this.permissionMenu[i].path === route.path) {
            data = Object.assign({}, route)
            break
          }
        }
        if (data && route.children && route.children.length > 0) {
          data.children = this.generateFn(route.children)
        }
        if (data) {
          res.push(data as RouteRecordRaw)
        }
      })
      return res
    },
    // 处理并存储用户权限路由和权限按钮
    setStorePermissionMenu(permissionMenu: any[]) {
      this.permissionMenu = []
      this.permissionBtn = []
      filterMenu(permissionMenu, 0, this.permissionMenu, this.permissionBtn)
      storage.setPermissionBtns(this.permissionBtn)
      storage.setPermissionMenu(this.permissionMenu)
    },
    setStoreIsAddRouters() {
      this.isAddRouters = true
    },
    setStoreToken(token: string) {
      this.token = token
      storage.setToken(token)
    }
  }
})
//处理后端返回的数据,变成我们想要的结构
function filterMenu(resMenu: any, count: number, permissionMenu: any[], permissionBtn: string[]) {
  resMenu.forEach((item: any) => {
    if (count === 2) {
      permissionBtn.push(item.view_url)
    } else {
      const obj = {
        path: item.view_url,
        title: item.name,
        hidden: item.isShow !== '1',
        // sort: item.sort
      }
      permissionMenu.push(obj)
    }
    if (item.list) {
      const level = count + 1
      filterMenu(item.list, level, permissionMenu, permissionBtn)
    }
  })
}

上面的代码说白了就是干了一件事,通过后端接口返回的用户权限和之前前端写的异步路由的每一个页面所需要的权限做匹配,最后返回一个该用户能够访问路由有哪些。

注意:
这里有一个需要非常注意的点就是404页面一定要最后加载,否则后面的所有页面都会被拦截到404。

3) permission.ts 使用 路由拦截器

//permission.ts

router.beforeEach((to: RouteLocationNormalized, from:RouteLocationNormalized, next: any) => {
  // Start progress bar
  NProgress.start()
  const usePermission = usePermissionStore()
  const { token, isAddRouters } = storeToRefs(usePermission)
  const { GenerateRoutes, setStoreIsAddRouters } = usePermission
  // Determine whether the user has logged in
  if (token.value !== '') { // 已经登陆
    if (to.path === '/login') {
      next({ path: '/' })
      NProgress.done()
    } else {
      // 如果动态添加路由完毕
      if (isAddRouters.value === true) {
        next()
        NProgress.done()
      } else {
        //没有动态添加路由,则动态添加路由
        GenerateRoutes().then((res:RouteRecordRaw[]) => { 
          res.forEach(
            async(route:RouteRecordRaw) => {
              await router.addRoute(route) 
            })
          const redirectPath = (from.query.redirect || to.path) as string
          const redirect = decodeURIComponent(redirectPath)
          const nextData = to.path === redirect ? { ...to, replace: true } : { path: redirect }
          setStoreIsAddRouters() //设置动态添加路由完毕
          next(nextData)
          NProgress.done()
        }).catch((err: any) => {
          console.error(err)
        })
      }
    }
  } else { // 未登陆
    if (whiteList.indexOf(to.path) !== -1) { //免登录白名单,直接进入
      next()
      NProgress.done()
    } else {
      // 否则全部重定向到登录页
      next({
        path: '/login',
        query: {
          redirect: to.path
        }
      })
      NProgress.done()
    }
  }
})

最后在main.ts引入

//main.ts
import '@/permission'

注意:
这里有一个需要注意的点就是router.addRoute之后next()可能会失效,因为可能next()的时候路由并没有完全add完成。
所以我们在登录那里,添加动态路由表完毕之后设置isAddRouters = true,然后路由跳转之前先去判断isAddRouters = true,路由放行;否则再次动态添加路由。

关于router.addRoute()
新版Vue Router中用router.addRoute来替代原有的router.addRoutes来动态添加路由、子路由。
在之前通过后端动态返回前端路由一直很难做的,因为vue-router必须是要在vue实例之前就挂载上去的,不太方便动态改变。有了router.addRoute 我们就可以相对方便的做权限控制了。

4) 侧边栏

在前面的基础上,使用element-ui的el-menumenu-item,就能很方便实现动态显示侧边栏了。
只要我们从Pinia的store中拿到菜单数据menuData,v-for循环渲染数据就行了。

侧边栏点击高亮问题:element-ui官方给了el-menu default-active属性,我们只要

:default-active="$route.path"  //将 default-active 指向当前路由就可以了。

到这里我们已经完成了对页面访问的权限控制,接下来我们来讲解一下操作按扭的权限部分。

3) 按钮级别权限控制

封装了一个全局自定义指令权限,能简单快速的实现按钮级别的权限判断。
关于全局自定义指令感兴趣的可参考我之前的文章 vue3全局自定义指令实现按钮权限控制

实现步骤:

  1. 我们登录成功之后,处理接口返回数据的时候,就存储了用户权限按钮数组permissionBtn,如下图:
    在这里插入图片描述

  2. 进行封装,项目根目录下新建一个directives文件夹 =》permission.ts和index.ts
    主要思路就是用户没有这个按钮权限的话,隐藏按钮。

// permission.ts
// 引入vue中定义的指令对应的类型定义
import { Directive } from 'vue'
export const permission: Directive = {
  // mounted是指令的一个生命周期
  mounted(el, binding) {
    // value 获取用户使用自定义指令绑定的内容
    const { value } = binding
    // 获取用户所有的权限按钮
    const permissionBtn = wsCache.get('permission')
    // 判断用户使用自定义指令,是否使用正确了
    if (value && value instanceof Array && value.length > 0) {
      const permissionFunc = value
      //判断传递进来的按钮权限,用户是否拥有
      //Array.some(), 数组中有一个结果是true返回true,剩下的元素不会再检测
      const hasPermission = permissionBtn.some((role: any) => {
        return permissionFunc.includes(role)
      })
       // 当用户没有这个按钮权限时,设置隐藏这个按钮
      if (!hasPermission) {
        el.style.display = 'none'
      }
    } else {
      throw new Error('need roles! Like v-permission="[\'admin\',\'editor\']"')
    }
  }
}

// 注意,我们这里写的自定义指令,传递内容是一个数组,也就说,按钮权限可能是由
// 多个因素决定的,如果你的业务场景只由一个因素决定,自定义指令也可以不传递一个数组,
// 只传递一个字符串就可以了
// index.ts
export * from './permission'
  1. main.ts中注册为全局指令
import App from './App.vue'
import { createApp, Directive } from 'vue'
import * as directives from '@/directives' //权限判断指令
const app = createApp(App)

console.log(directives, 'directives') //打印发现是导出的自定义指令名,permission
Object.keys(directives).forEach(key => {  //Object.keys() 返回一个数组,值是所有可遍历属性的key名
  app.directive(key, (directives as { [key: string ]: Directive })[key])  //key是自定义指令名字;后面应该是自定义指令的值,值类型是string
})
  1. 在页面中使用,控制按钮显示
<template>
  <button v-permission="['auto.add']">新增</button>
  <button v-permission="['auto.update']">编辑</button>
  <button v-permission="['auto.delete']">删除</button>
</template>

六、遇到的问题

1. 路由刷新失效问题

目前解决方案是将处理好的权限路由,通过web-storage-cache保存到sessionStorage,因为它扩展了序列化方法,可以直接存储json对象,刷新不会重置。然后放到store里面。最后从store里面取出,调用router.addRoute()方法。
关于sessionStorage和web-storage-cache的差异

到此为止,由前端配置权限控制流程就差不多了,如果有疑问,或者文章有什么错误,欢迎留言评论。

学习过程中参考了:
vue中如何实现后台管理系统的权限控制
手摸手,带你用vue撸后台 系列二(登录权限篇)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/173082.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

蓝桥杯嵌入式第十届学习记录

1&#xff1a;拷贝LCD工程代码作为模板2&#xff1a;注意放置代码得顺序3&#xff1a;注意公共头函数键盘4&#xff1a;串口配置出来方便dubug模式正常接收数据5:记得打定时器中断&#xff08;去历程定时器里面寻找&#xff01;&#xff09;6&#xff1a;细节地方7;LCD每个位置…

[hive]数仓分层|用户纬度拉链表|维度建模

https://www.modb.pro/404?redirect%2Fdb%2F241289一、数仓分层1、ODS层&#xff1a;原始数据层ODS(Ooriginal Ddata Sstore)1)设计要点存储来自多个业务系统、前端埋点、爬虫获取的一系列数据源的数据。我们要做三件事&#xff1a;【1】保持数据原貌不做任何修改&#xff0c;…

一阶高通滤波器学习

导读&#xff1a;本期文章主要介绍一阶高通滤波器。一阶高通滤波器与一阶低通滤波器很相似&#xff0c;都是利用电容阻低频信号通高频信号&#xff0c;电感阻高频信号通低频信号的特点。一、一阶高通滤波器介绍滤波器是作为一种选频装置&#xff0c;是信号处理中的一个重要的概…

Linux(二)进程概念

目录 一、冯诺依曼体系结构 二、操作系统 三、进程概念 1、程序与进程的区别&#xff1a; 2、cpu分时机制 3、pcb——进程控制块 4、进程是什么&#xff1f; 四、进程状态 1、linux状态 2、僵尸态 pid_t fork(void)&#xff1a; fork创建进程之后&#xff0c;父子进…

vector以及list

之前已经学习过了string类&#xff0c;接下来介绍c中的另外两个类—— vector和list&#xff1b; vector 之前介绍的string类是c所特定的字符数组&#xff1b; 而vector可以看做是string类的扩展&#xff0c;因为它是一个模板类&#xff1b; 它可以作为任何类型的数组&#x…

小侃设计模式(廿二)-访问者模式

1.概述 访问者模式&#xff08;Visitor Pattern&#xff09;指的是在类的内部结构不变的情况下&#xff0c;不同的访问者访问这个对象都会呈现出不同的处理方式。它的主要作用时将数据结构与数据操作分离&#xff0c;将不同的算法与其所作用的对象进行分离。本文将详述访问者模…

DW动手学数据分析Task2:数据清洗及特征处理

文章目录一 数据清洗1 缺失值观察与处理1.1 缺失值观察1.2 缺失值处理2 重复值观察与处理二 特征处理1 分箱&#xff08;离散化&#xff09;处理2 对文本变量进行转换3 从纯文本Name特征里提取出Titles的特征3 参考文章一 数据清洗 数据清洗&#xff1a;我们拿到的数据通常是不…

树的知识概括锦囊(一)

作者&#xff1a;爱塔居 专栏&#xff1a;数据结构 作者简介&#xff1a;大三学生&#xff0c;希望跟大家一起进步&#xff01; 文章目录 目录 文章目录 一、树形结构 二、树的基础知识 三、二叉树 3.1 概念 3.2 特殊的二叉树 ​编辑 3.3 二叉树的性质 四、习题挑战 一、树形结…

手把手教你学51单片机-如何学习单片机

大多数大学生之所以最后变的平庸,不是因为脑子多么笨,也不是全怪自己贪玩不上进,只是没有一个好的领路人,许多学校可能挂着导师的名头,但是多数是挂羊头卖狗肉或者是干脆不管。最后等大学生毕业之后,那些所谓的老师就会说学生很差或者学习很差,反正就是跟自己没啥关系。…

OSPF综合实验(华为)

题目&#xff1a; 思路&#xff1a; 首先配置每个区域的路由和环回地址&#xff0c;其次&#xff0c;根据题目要求打通每个网络的连接&#xff0c;区域0用MGRE打通网络&#xff0c;区域4需要重发布&#xff0c;其次再考虑优化的问题。ip地址的规划是为了更好的路由汇总&#x…

《 Unity Shader 入门精要》第5章 开始 Unity Shader 学习之旅

第5章 开始 Unity Shader 学习之旅 5.2 一个最简单的顶点/片元着色器 顶点/片元着色器的基本结构 // Upgrade NOTE: replaced mul(UNITY_MATRIX_MVP,*) with UnityObjectToClipPos(*)// 定义 shader 的名字 Shader "Chapter 5/Simple Shader" {SubShader{Pass {//…

自动驾驶控制算法之车辆横向控制(project)

本文为深蓝学院-自动驾驶控制与规划-第三章作业 目录 1 projection introduction 2 思路提示 2.1 ComputeControlCmd 2.2 ComputeLateralErrors 3 Corer Case 3.1 Low speed operation 3.2 Extra damping on heading 3.3 Steer into constant radius curve 4 ROSLGSV…

FFmpeg-4.2.4的filter: drawbox源码分析

1. vf_drawbox.c功能 有两个功能 ,添加方框,和添加网格; 1.1 添加方框效果 1.2 添加网格效果

c++数据结构-树(详细总结附代码,一看就懂)

树的定义一棵树是由n&#xff08;n>0&#xff09;个元素组成的有限集合&#xff0c;其中&#xff1a;&#xff08;1&#xff09;每个元素称为结点&#xff08;node&#xff09;&#xff08;2&#xff09;有一个特定的结点&#xff0c;称为根结点或树根&#xff08;root&…

上海亚商投顾:双创指数低开高走,数字经济继续活跃

上海亚商投顾前言&#xff1a;无惧大盘涨跌&#xff0c;解密龙虎榜资金&#xff0c;跟踪一线游资和机构资金动向&#xff0c;识别短期热点和强势个股。市场情绪三大指数今日低开高走&#xff0c;创业板指午后涨超1%&#xff0c;科创50指数涨超1.6%。数字经济概念继续爆发&#…

算法基础(一):时间复杂度和空间复杂度

算法基础&#xff08;一&#xff09;&#xff1a;时间复杂度和空间复杂度时间复杂度 O(1)O(1)O(1) O(N)O(N)O(N) O(logN)O(log N)O(logN) O(MN)O(MN)O(MN) O(NlogN)O(Nlog N)O(NlogN)、O(MlogN)O(Mlog N)O(MlogN) O(N2)O(N^2)O(N2)空间复杂度一些算法基础知识点和leetcode题解&…

【网络通信】【电信运营商实战工程师】思科设备篇-思科设备企业网实战

电信运营商实战工程师系列文章. 思科设备篇-思科设备企业网实战. 文章目录1. 思科设备基本开局配置2. ARP协议、交换机工作原理及广播风暴问题3. 思科设备 VLAN 及单臂路由实战4. 思科三层交换机实现 VLAN 间路由实战5. 思科设备静态默认及浮动路由实战6. 思科设备NAT实战全集1…

YOLO_V8训练自己的数据集

YOLO_V8在2023年开年横空出世&#xff0c;在春节前还得卷一下。由于YOLO_V8和YOLO_V5是同一个作者&#xff0c;所以很多操作都是一样的&#xff0c;下面主要描述一下如何用自己的数据集进行训练和测试&#xff08;非命令行的方式&#xff09;。1、训练数据和模型的目录结构这里…

设计模式学习(九):Abstract Factory抽象工厂模式

目录 一、什么是Abstract Factory模式 二、Abstract Factory示例代码 2.1 类之间的关系 2.2 抽象的零件:ltem类 2.3 抽象的零件:Link类 2.4 抽象的零件:Tray类 2.5 抽象的产品: Page类 2.6 抽象的工厂:Factory类 2.7 使用工厂将零件组装称为产品:Main类 2.8 具体的工厂…

linux三剑客之AWK

目录 AWK是什么 AWK基本结构 a.txt的文本实例 AWK内置变量 a.txt的文本实例 AWK自定义变量 a.txt的文本实例 AWK内置函数 a.txt的文本实例 awk高级输出 a.txt的文本实例 排序输出 a.txt的文本实例 条件选择输出 a.txt的文本实例 控制语句 a.txt的文本实例 AWK是什…