likes
comments
collection
share

最轻量的后台管理模板没有之一

作者站长头像
站长
· 阅读数 10

特色: 当前模板将自定义样式配置通过css变量的方式提取了出来,直接通过可视化去配置成你喜欢的样式效果,在下面预览地址中可以体验。

预览地址 可视化配置面版不够好看的话,可以把地址上的vue-admin改为vue-admin-el

项目地址

描述

无UI框架依赖的后台管理模板

当前项目是基于vue.js去实现的一套后台管理模板,早在2019年就已经在持续迭代,目前已经是较新的vue3.x版本;

因为在中后台项目中,大多数核心功能只有页面框架样式侧边菜单栏功能,所以除了底层 js 框架vue+vue-router以外,所有样式、功能都采用自行实现方式;之所以不使用第三方UI库的理由是:

  • 不受UI框架的约束,可以使用任何一款自己喜欢的第三方库;
  • 轻量化,因为用到的依赖极少,所以体积非常轻量,同时保证了常用到的大部分功能保留;所有的工程化配置根据自身需求去加入即可,当前模板只做代码减法;
  • 兼容性、拓展性高,模板中每个部分都是可以独立抽离和替换的,并无上手成本;当在引用某一款UI库使用时,直接引入依赖并使用即可,无需修改模板已有功能组件;
  • 别人写的模板代码太多了,都不好改!

当前模板项目的 package.json 做到了极致的精简

{
  "dependencies": {
    "nprogress": "0.2.0",
    "vue": "3.2.45",
    "vue-router": "4.1.6"
  },
  "devDependencies": {
    "@types/node": "~18.15.11",
    "@types/nprogress": "0.2.0",
    "@vitejs/plugin-vue": "4.1.0",
    "@vitejs/plugin-vue-jsx": "3.0.1",
    "sass": "~1.61.0",
    "typescript": "5.0.4",
    "vite": "4.2.1",
    "vue-tsc": "1.2.0"
  }
}

功能目录清单

  • vue-router 权限路由功能、路由记录初始进入路径功能
  • layout 部分:可视化配置样式功能、顶部伸缩布局 + 多级侧边菜单栏、路由面包屑、路由历史记录标签栏、整体自适应窗口大小布局、滚动条(类似<el-scrollbar>
  • utils 只保留使用频率极高的:日期格式化、复制、类型判断、网络请求、和一些核心功能函数
  • UI控件 + 通用组件:消息提示条、对话框、高度自适应折叠组件、dialog 组件

layout 核心布局整体

大多数情况开发者在选用开源模板时,只是为了侧边菜单栏和顶部的布局不同而选择对应的模板,所以当前项目直接将两种布局写成可以动态切换,并且加入可视化的样式配置操作,这样连css代码都不需要去看了:

最轻量的后台管理模板没有之一

侧边菜单栏为什么没有整一个折叠缩略的功能?理由是我觉得这个操作逻辑不是那么的理想,缩小后,我需要鼠标一层一层的放上去找到需要的子菜单,这一点都不方便;而且缩小菜单的目的是为了获得更大内容可视区域,所以缩小后的菜单依然还占用了一部分空间,同时使用功能变得繁琐,那干脆在收起菜单时,将她整个推出屏幕区域,这样就能使可视区域最大化。

最轻量的后台管理模板没有之一

路由权限设置

完全继承了vue-router的数据结构,只在meta对象中加入auth作为路由数组过滤操作去实现权限控制;另外根部对象的name字段则作为路由缓存的唯一值。

import { RouteRecordRaw } from "vue-router";

export interface RouteMeta {
  /** 侧边栏菜单名、document.title */
  title: string
  /** 外链地址,优先级会比`path`高 */
  link?: string
  /** `svg`名 */
  icon?: string
  /** 是否在侧边菜单栏不显示该路由 */
  hidden?: boolean
  /**
   * 路由是否需要缓存
   * - 当设置该值为`true`时,路由必须要设置`name`,页面组件中的`name`也是,不然路由缓存不生效
   */
  keepAlive?: boolean
  /**
   * 可以访问该权限的用户类型数组,与`userInfo.type`对应;
   * 传空数组或者不写该字段代表可以全部用户访问
   * 
   * | number | 用户类型 |
   * | --- | --- |
   * | 0 | 超级管理员 |
   * | 1 | 普通用户 |
   */
  auth?: Array<number>
}

/** 自定义的路由类型-继承`RouteRecordRaw` */
export type RouteItem = {
  /**
   * 路由名,类似唯一`key`
   * - 路由第一层必须要设置,因为动态路由删除时需要用到,且唯一
   * - 当设置`meta.keepAlive`为`true`时,该值必填,且唯一,另外组件中的`name`也需要对应的同步设置,不然路由缓存不生效
   */
  name?: string
  /** 子级路由 */
  children?: Array<RouteItem>
  /** 标头 */
  meta: RouteMeta
} & RouteRecordRaw

最轻量的后台管理模板没有之一

状态管理

ts的项目中,因为可以用Readonly去声明状态对象,所以这套程序设计会发挥得最好,具体示例可以在src/store/README.md中查看

网络请求

这里我使用的是根据个人习惯用原生写的ajax代码地址

理由是:

  • 代码少,功能足以覆盖常用的大部分场景
  • ts中可以更友好的声明接口返回类型

文件:api.ts request中的泛型不是必须的,不传下面 .vue 文件中res.data中的类型则是any

export interface TableItem {
  id: number
  type: "load" | "update"
  time: string
}

/**
 * @param params
 */
export function getData(params: PageInfo) {
  return request<ApiResultList<TableItem>>("GET", "/getList", params)
}

文件:demo.vue 建议直接在vscode中用鼠标去看提示,那样会更加的直观

<script lang="ts" steup>
import { ref } from "vue";
import { type TableItem, getData } from "api.ts";

const tableData = ref<TableItem>([]);

async function getTableData() {
  const res = await getData({
    pageSize: 10,
    currentPage: 1
  })
  if (res.code === 1) {
    tableData.value = res.data.list; // 这里的 .list 就是接口 传入的类型 TableItem
    // do some...
  }
}
</script>

强力建议请求函数的封装时,都始终执行 Promise.resolve 去作为正确和错误的响应。接口获取后始终以res.code === 1为判断成功,无需在内部用 try + catch 去包一层

更多使用示例请在src/api/README.md中查看

另外可根据自己喜好可以扩展 axios 这类型第三方库。

SVG 图标组件

使用方式:到阿里云图标库中下载想要的图标,然后下载svg文件,最后放到src/icons/svg目录下即可

也是自己写的一个加载器,代码十分简单:

import { readFileSync, readdirSync } from "fs";

// svg-sprite-loader 这个貌似在 vite 中用不了
// 该文件只能作为`vite.config.ts`导入使用
// 其他地方导入会报错,因为浏览器环境不支持`fs`模块

/** `id`前缀 */
let idPerfix = "";

const svgTitle = /<svg([^>+].*?)>/;

const clearHeightWidth = /(width|height)="([^>+].*?)"/g;

const hasViewBox = /(viewBox="[^>+].*?")/g;

const clearReturn = /(\r)|(\n)/g;

/**
 * 查找`svg`文件
 * @param dir 文件目录
 */
function findSvgFile(dir: string): Array<string> {
  const svgRes: Array<string> = []
  const dirents = readdirSync(dir, {
    withFileTypes: true
  });
  dirents.forEach(function(dirent) {
    if (dirent.isDirectory()) {
      svgRes.push(...findSvgFile(dir + dirent.name + "/"));
    } else {
      const svg = readFileSync(dir + dirent.name).toString().replace(clearReturn, "").replace(svgTitle, function(_, group) {
        // console.log(++i)
        // console.log(dirent.name)
        let width = 0;
        let height = 0;
        let content = group.replace(clearHeightWidth, function(val1: string, val2: string, val3: number) {
          if (val2 === "width") {
            width = val3;
          } else if (val2 === "height") {
            height = val3;
          }
          return "";
        });
        if (!hasViewBox.test(group)) {
          content += `viewBox="0 0 ${width} ${height}"`;
        }
        return `<symbol id="${idPerfix}-${dirent.name.replace(".svg", "")}" ${content}>`;
      }).replace("</svg>", "</symbol>");
      svgRes.push(svg);
    }
  });
  return svgRes;
}

/**
 * `svg`打包器
 * @param path 资源路径
 * @param perfix 后缀名(标签`id`前缀)
 */
export function svgBuilder(path: string, perfix = "icon") {
  if (path.trim() === "") return;
  idPerfix = perfix;
  const res = findSvgFile(path);
  // console.log(res.length)
  return {
    name: "svg-transform",
    transformIndexHtml(html: string) {
      return html.replace("<body>",
      `<body>
      <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="position: absolute; width: 0; height: 0">
      ${res.join("")}
      </svg>`)
    }
  }
}

转载自:https://juejin.cn/post/7350874162011750400
评论
请登录