后台管理模板的最佳实现方式

描述

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

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

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

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

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

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"
  }
}

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

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

项目地址

功能目录清单

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

layout 核心布局整体

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

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

路由权限设置

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

ts 复制代码
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

状态管理

Vue3 之后不需要Vuex了(虽然我在Vue2 中也没用),而是采用另外一种更简单的方式:参考 你不需要vuex

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

网络请求

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

理由是:

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

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

ts 复制代码
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中用鼠标去看提示,那样会更加的直观

html 复制代码
<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>

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

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

SVG 图标组件

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

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

ts 复制代码
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>`)
    }
  }
}
相关推荐
恋猫de小郭24 分钟前
Flutter Zero 是什么?它的出现有什么意义?为什么你需要了解下?
android·前端·flutter
崔庆才丨静觅7 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60618 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了8 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅8 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅8 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅9 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment9 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅9 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊9 小时前
jwt介绍
前端