海南系统详细设计

技术选型

vue3+ts+vite+element plus

初始化项目基本需求

  • 动态路由权限验证
  • 按钮权限
  • 接口代理转发
  • 响应请求拦截处理
  • ui框架处理
  • 侧栏菜单以及面包屑
  • 通用组件封装
技术选型

充分发挥了 Vite 的模块化和快速热更新的特性,结合 Vue 3 和 TypeScript 构建了一个基于单文件组件和组合式 API 的前端架构。这种结构允许我们充分利用 Vite 的即时开发体验,并使用 TypeScript 强大的类型检查功能,提高了代码的可维护性和可读性

动态路由设计
1
2
3
4
5
6
7
router
├── routes
│   ├── index.ts
│   ├── notFoundRoute.ts
│   └── userInfoRoute.ts
├── index.ts
└── menuTree.ts

image-20231206153615973

动态路由主要为动态添加路由的处理

动态路由在设计的时候是有两种方案:

  • 通过在前端注册路由表的方式,通过请求接口拿到权限路由,之后通过逐级查找路由表获取到真实的路由,将路由进行注册
  • 后端直接返回前端路由表,通过请求接口直接拿到真实的路由表,前端直接进行注册

第二种方式可以保证路由表更加真实安全,但是也会存在问题

  • 需要额外维护一个权限页面
  • 路由表存放在数据库中,数据需要与前端工程目录一一对应
  • 修改页面路径文件名需要同步修改数据库中的对应内容
  • 整体路由结构前端不可见,后续维护可能会更加麻烦

因为本系统安全性需求并没有那么高,所以最后还是采用可以更加灵活开发的方案一

动态路由有几个需要处理的问题:

  1. 动态路由跳转白屏
    在首次跳转的时候,还没有进行添加动态路由,此时可以通过next({ ...to, replace: true });的方式避免没有获取路由的情况

  2. 页面刷新404问题

    刷新是会重新执行路由守卫的,所以此时动态路由还没有进行注册,所以会跳转到404页面,解决方法就是将404页面的路由注册也改为动态路由,在请求的路由菜单全部注册完成之后,在注册404页面的路由,这样就可以保证路由在解析的时候可以拿到正确的路由菜单

  3. 如何设计路由表

    根据业务的模块对前端路由进行按文件进行划分,将路由地址作为键,功能模块路由文件作为值,存放在map对象的集合当中,之后拿从接口获取的路由url去匹配真实路由,再进行添加,通过路由表的形式,实现路由的懒加载

按钮权限

实现按钮级权限有三种方式:

  1. 通过注册vue的自定义指令实现对按钮的显示和隐藏
  2. 通过二次封装buttone的方式,将权限字符串作为参数传入,由组件内部进行权限判断控制显示隐藏
  3. 通过封装hooks的方式,在权限页面将hooks进行引入,同时传入权限字符串,通过vif进行显示隐藏

三种方式各有优势,最后还是采用第一种自定义指令的方式,一是为了与其他项目逻辑保持一致,另外通过自定义方式使用起来相对来说会更加简便。

image-20231207171603766

公共组件layout

image-20231207102035844

layout基本由左侧菜单栏,头部的占位区,中间为router-view,后续的页面都渲染在这里

navMenu实现:

image-20231207103947046

根据菜单表进行判断,如果没有子菜单,那么就是功能菜单,此时需要添加点击跳转的功能,如果有子菜单,那么就是父级菜单,可以点击进行折叠子菜单,没有跳转功能。

因为所有页面都依赖这个组件,所以使用嵌套路由,所有的业务功能页面都将通过路由匹配渲染在这个组件中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const route: RouteRecordRaw = {
path: '/system',
meta: {
title: '系统管理'
},
children: [
{
path: 'feature',
meta: {
title: '特征条件维护'
},
component: () => import('@/views/systemManagement/featureMaintenance/Index.vue'),
}
]
}

将功能页面作为children的一部分父级路由都为layout,因此可以在初始化路由的时候,统一使用lay组件来渲染页面内容。

通用table组件

由于后台管理项目中存在很多的table表格,还有很多带标签的表格页,所以对表格进行二次封装,便于快速创建出表格页

配置项:

config 类型 描述
tableConfig array 存放table的配置内容
tableData array table的展示数据
isShowRightExtend boolean 是否展示表格右侧扩展区
tagName string 展示右侧tag需要的name,需要开启
isShowRightExtend
tableConfig 类型 描述
type enum COLUMN_TYPES.COMMON,COLUMN_TYPES.EXTENDS,COLUMN_TYPES.INDEX,COLUMN_TYPES.SELECTION有普通列,序号列,多选框列,扩展列四种类型
label string 对应el-table的label,用于展示名称
prop string 对应el-table的prop,匹配列内容的字段名
width string 对应el-table的width,用于控制控制该列的宽度
列表类型 描述
COMMON 普通列表
SELECTION 列表中带有多选框如果开启了表格右侧扩展区,可以开启点击表格列表项选中当前行功能
EXTENDS 扩展列表,这种类型可以对列表数据进行改造后展示
INDEX 序号列,可以展示当前序号
关于表格的selection优化点

当使用element的selection多选框的时候,在element ui的表格中并没有提供相应的方法区获取已经选中的表格项,但是在源码中可以看到,element 是有对已选的表格项进行处理的,table的实例的selection属性就记录着用户已经勾选的表格项,所以可以通过ref去获取table的实例,之后调用selection就可以,不需要再进行手动维护一个已选项的数组和添加删除逻辑了

在element plus中,官方正式开放了获取已选表格的方法,可以通过getSelectionRows()进行获取

通用表格组件的二次封装

考虑到部分页面会存在大量布局设计相同,但不只是有表格的页面,所以会有场景需要对表格组件再次进行封装。以我封装的带标签页tabs的表格为例

该组件需要解决的问题有:如何进行深层次进行插槽内容的传递、如何在切换标签时对用户操作进行拦截,校验本页信息是否正确

  1. 深层次插槽内容传递:

通过动态插槽名+动态插槽实现

image-20231208162907273

1
2
3
4
5
6
7
8
9
10
<tab-table :tab-config="configData" ref="tabDialogRef" @tab-click="handleTabClick($event, 'dialog')"
class="table-content" :loading="dialogTableLoading">
<template #tzlbId="scope">
<span>{{ scope.scope.scope.tzId === 1 ? '加分' : '优先' }}</span>
</template>
<template #gzlbId="scope">
<span>{{ scope.scope.scope.gzlbId === 1 ? '加分' : '优先' }}</span>
</template>

</tab-table>
1
2
3
4
5
<common-table :config="pane" class="table-container" ref="tableRef" :loading="props.loading">
<template v-for="(item, index) in getTabConfig(pane)" :key="index" #[item!]="scope">
<slot :name="item" :scope="scope"></slot>
</template>
</common-table>
1
2
3
4
5
6
<el-table-column v-else-if="column.type === COLUMN_TYPES.EXTENDS" :label="column.label" :prop="column.prop"
:width="column.width">
<template #default="scope">
<slot :name="column.prop" :scope="scope.row"></slot>
</template>
</el-table-column>

因为插槽需要通过template进行包裹,所以子组件采用template根据插槽配置数组进行循环渲染,通过vue3的模板引入设置动态的插槽name,之后设置插槽存放父组件传递过来的插槽内容

在孙子组件中对对应的插槽内容进行匹配,渲染dom

  1. 拦截标签切换

    标签拦截通过使用代理模式,通过js中的proxy对象对标签绑定的activeName进行代理,代理模式允许在访问或修改目标对象的属性时进行拦截,并且可以在拦截器中执行一些额外的逻辑。

    1
    proxy = new Proxy(this.isOpenAutoActivateParams, this.getAutoActivateHandler())
    1
    2
    3
    4
    5
    6
    7
    8
    getAutoActivateHandler() {
    return {
    get: Reflect.get,
    set: (target, property, value) => {
    // ...
    },
    };
    }
    • get 拦截器使用了 Reflect.get,表示直接从目标对象中获取属性值。
    • set 拦截器在属性值发生变化时进行拦截和处理。

    使用观察者模式的思想,当属性变化时触发更新前的额外操作,进行拦截操作