【Dv3Admin】Vue3动态配置首页仪表盘 在做首页仪表板时最麻烦的不是图表怎么画而是组件来源动态、布局可拖拽、权限要隔离、还要能保存恢复。Home 模块就是围绕这几件事做的一套标准化实现后端下发组件定义与权限前端负责动态渲染、拖拽布局、过滤无权限组件并紧凑排列最后把用户调整后的布局持久化到后端。文章目录Home 模块开发权限控制系统布局管理系统自定义模式动态图表组件悬浮组件库API 接口数据结构使用方法扩展说明总结Home 模块开发目录结构为了后面能快速定位“配置加载在哪、布局渲染在哪、保存逻辑在哪”先把目录结构固定下来。核心逻辑都在PageVisualizationComponents/里入口只是挂载。home/ ├── index.vue # 主入口文件 └── PageVisualizationComponents/ # 可视化组件核心目录 ├── index.vue # 主可视化页面组件 ├── api.ts # API 接口定义 ├── suspendedLibrary.vue # 悬浮组件库 └── components/ └── DynamicChart.vue # 动态图表组件组件配置管理这一块解决的是“组件不是写死的”的问题前端不预置有哪些组件而是从后端拉取组件定义然后把它们转换成前端可渲染的组件字典。后续渲染、权限过滤、组件库展示都基于这份字典。下面这段代码做的事很明确请求接口拿组件配置然后遍历后端列表把每个组件整理成{ component, title, icon, width, height, config, dept_ids, role_ids }这种统一结构最后写入allCompsRef供全局使用。// 从后端获取组件定义constloadComponentsConfigasync(){constresawaitapi.getComponentsConfig({company:company.value})constremoteComponentsres?.data||{}// 将后端返回的数据转换为组件对象constcomponents{}remoteList.forEach((compDefRaw){constcompKeycompDefRaw.key||compDefRaw.component_keyconstcomponentTypecompDefRaw.component_type||echartsconstComponentcomponentTypeMap[componentType]||DynamicChart components[compKey]{component:markRaw(Component),title:compDefRaw.title,icon:compDefRaw.icon,height:compDefRaw.height??24,width:compDefRaw.width??12,config:compDefRaw.config||{},dept_ids:compDefRaw.dept_ids||[],role_ids:compDefRaw.role_ids||[],}})allCompsRef.valuecomponents}需要注意的是组件类型通过componentTypeMap做映射如果后端返回的类型前端没注册会走兜底DynamicChart这一步不能省略否则新增组件类型时很容易直接渲染失败。组件配置完全由后端控制前端通过接口动态获取。权限控制系统这一块解决的是“不同用户能看到的组件不一样”的问题。权限判断不是单纯隐藏 UI而是用于布局过滤否则会出现无权限组件虽然不渲染但依旧在布局里占位页面会留下大块空白。下面这段代码的核心逻辑是超管直接放行组件没配权限则默认所有人可见如果配置了部门或角色就分别判断用户部门和角色是否命中最终返回 true/false。// 检查用户是否有权限查看某个组件functionhasPermissionToView(componentKey){// 超管有全部权限if(info.value?.is_superuser){returntrue}constcomponentallCompsRef.value[componentKey]if(!component)returnfalse// 如果组件没有配置权限限制则所有人都可以查看constdeptIdscomponent.dept_ids||[]constroleIdscomponent.role_ids||[]if(deptIds.length0roleIds.length0){returntrue}// 检查部门权限constuserDeptIdinfo.value?.dept_info?.dept_idif(deptIds.length0userDeptId){if(deptIds.includes(userDeptId)){returntrue}}// 检查角色权限constuserRolesinfo.value?.role_info||[]if(roleIds.length0userRoles.length0){constuserRoleIdsuserRoles.map(rolerole.id)if(roleIds.some(roleIduserRoleIds.includes(roleId))){returntrue}}returnfalse}需要注意的是这里如果组件 key 在allCompsRef找不到会直接返回 false相当于“默认拒绝”避免未知组件被误展示。布局管理系统这一块解决的是“组件怎么摆、怎么拖、怎么缩放”的问题。系统使用 GridLayout 维护网格布局通过customizing控制是否允许拖拽与缩放避免用户在普通浏览状态误操作。下面这段代码的作用是用filteredLayout驱动布局渲染GridItem里根据item.element动态选择渲染的组件并把尺寸与配置传入组件。GridLayout v-model:layoutfilteredLayout :col-numcolNum :row-height1 :is-draggablecustomizing :isResizablecustomizing GridItem v-foritem in filteredLayout :xitem.x :yitem.y :witem.w :hitem.h :iitem.i component :isresolveComp(item.element) :configitem.config || {} :widthitem.w :heightitem.h / /GridItem /GridLayout这一步不能省略customizing的控制否则会导致页面在非编辑状态也能拖动布局用户一刷新就发现布局变了。布局过滤与紧凑排列这一块解决的是“权限过滤后出现空洞”的问题。过滤掉无权限组件后如果还保持原坐标页面会留下大量空白所以这里会重新计算布局坐标实现从左到右、从上到下的紧凑排列。下面这段代码的核心动作是先用hasPermissionToView过滤可见组件再根据占位情况重新安排每个组件的x/y最终返回新的紧凑布局。// 过滤布局只保留用户有权限查看的组件并重新计算位置实现紧凑布局functionfilterAndCompactLayout(rawLayout){// 过滤掉用户没有权限的组件constvisibleItemsrawLayout.filter(item{returnhasPermissionToView(item.element)})// 重新计算布局位置实现紧凑排列从左到右从上到下constnewLayout[]constoccupiednewMap()sortedItems.forEach((item){// 从左上角开始找到第一个可以放置的位置letx0,y0while(!found){// 检查当前位置及所需空间是否被占用// 如果可用标记位置并添加到布局}newLayout.push({...item,x,y})})returnnewLayout}需要注意的是这里的目标是“紧凑展示”不是保持原位置所以不同权限用户看到的布局位置可能不同这是预期行为。自定义模式这一块解决的是“用户怎么进入编辑模式并保存布局”的问题。进入自定义模式后允许拖拽和缩放并打开悬浮组件库保存时会把布局写回后端同时清理掉不应该持久化的pxData字段。自定义模式支持的操作范围自定义模式允许做的事情可以理解为打开编辑 - 改布局 - 保存或取消。恢复默认和清空画布也是同一类“操作布局数据”的动作。保存取消进入自定义模式拖拽 / 调整 / 增删组件是否保存清理 pxData保存布局到后端权限过滤 紧凑重排退出自定义模式并展示恢复原始布局进入自定义模式与保存布局下面这段custom()的作用是进入编辑状态开启拖拽缩放同时把layout深拷贝给filteredLayout用于编辑展示并且不做权限过滤保证编辑视图完整。// 进入自定义模式functioncustom(){customizing.valuetruesuspendedLibrary.value.menutrue// 自定义模式下显示所有布局项不进行权限过滤filteredLayout.valueJSON.parse(JSON.stringify(layout.value))}下面这段save()的作用是退出编辑状态复制布局并删除pxData然后根据是否存在记录选择 update 或 add保存完成后再进行一次权限过滤与紧凑排列保证最终展示口径统一。// 保存布局asyncfunctionsave(){customizing.valuefalseconstlayoutCopyJSON.parse(JSON.stringify(layout.value))layoutCopy.map(val{deleteval.pxData})const{len,id}awaitgetHomeMenuLen()if(len0){awaitapi.updateHomeMenu({id,company:company.value,setting_data:layoutCopy})}else{awaitapi.addHomeMenu({company:company.value,setting_data:layoutCopy})}// 保存后重新过滤布局filteredLayout.valuefilterAndCompactLayout(layout.value)}这一步不能省略delete pxData否则会导致后端存入渲染期像素数据后续布局会越来越难维护。动态图表组件DynamicChart 是通用图表容器用来承接后端下发的 ECharts option。它解决的是“图表类型多但不想写一堆组件”的问题后端下发什么 option前端就按 option 渲染什么图表。下面模板部分的作用是loading 时显示 skeleton数据准备好后渲染v-chart并且开启autoresize让图表跟随容器变化。template div v-ifloading classchart-loading el-skeleton :rows5 animated / /div v-chart v-else :optionchartOption autoresize :stylechartStyle / /template下面脚本部分做了三件事注册图表依赖、计算图表高度优先用pxData.hpx、监听props.config变化并更新 option。script setup langts // 注册 echarts 组件 use([ CanvasRenderer, BarChart, LineChart, PieChart, GridComponent, TooltipComponent, TitleComponent, LegendComponent, ]) // 计算图表样式优先使用 pxData实际像素值 const chartStyle computed(() { const height props.pxData?.hpx || (props.height ? ${props.height * 8}px : 240px) return { width: 100%, height: typeof height number ? ${height}px : height } }) // 监听 config 变化直接使用配置中的 echarts option watch( () props.config, (val) { chartOption.value resolveChartOption(val) loading.value false }, { deep: true, immediate: true } ) /script需要注意的是高度优先使用pxData是为了拖拽缩放后更贴近真实像素高度否则只按网格高度换算会更容易出现图表留白不一致。悬浮组件库suspendedLibrary提供自定义模式下的组件选择入口解决“用户怎么添加组件到画布”的问题。它本身不负责布局算法只负责把用户选择转换为对布局数据的增删改同时提供一些画布级操作入口清空、恢复默认等。功能项说明组件库显示控制通过浮动按钮触发组件库的显示与隐藏不占用主画布空间组件列表展示展示当前系统内所有可用的组件定义供用户选择添加到画布画布级操作入口提供清空画布、最小化组件库、恢复默认布局、关闭组件库等操作组件搜索与筛选支持对组件进行搜索和筛选作为可扩展能力预留API 接口这一部分用于把“读取布局、保存布局、拉取组件配置”落到真实接口上。Home 的关键点是布局接口属于 HomeMenu组件配置接口属于 data_visualization_component。获取首页菜单配置这段接口用于读取用户已保存的首页布局配置通常在页面初始化时调用。exportfunctiongetHomeMenu(query:any){returnrequest({url:/api/HengShenGroup/Home/HomeMenu/,method:get,params:query});}添加首页菜单配置这段接口用于首次保存首页布局。代码里会强制写入obj.key home这一步不能省略否则后端可能无法按 home 菜单识别配置。exportfunctionaddHomeMenu(obj:any){obj.keyhomereturnrequest({url:/api/HengShenGroup/Home/HomeMenu/,method:post,data:obj,});}更新首页菜单配置这段接口用于更新已存在的布局配置同样会写入obj.key home并且 URL 带上obj.id。exportfunctionupdateHomeMenu(obj:any){obj.keyhomereturnrequest({url:/api/HengShenGroup/Home/HomeMenu/obj.id/,method:put,data:obj,});}获取组件配置列表这段接口用于拉取可视化组件定义列表返回结果会被loadComponentsConfig()转换为前端可渲染组件字典。exportfunctiongetComponentsConfig(query?:any){returnrequest({url:/api/system/data_visualization_component/,method:get,params:query});}数据结构这一部分用于固定前后端约定避免后端字段变更导致前端渲染或权限判断异常。组件配置数据结构组件配置用于定义组件 key、标题图标、默认尺寸、组件类型、ECharts option、权限字段等。interfaceComponentConfig{key:string// 组件唯一标识title:string// 组件标题icon:string// 组件图标description?:string// 组件描述height:number// 默认高度网格单位width:number// 默认宽度网格单位componentType:string// 组件类型如 echartsconfig:any// 组件配置如 ECharts optiondept_ids?:number[]// 部门权限ID列表role_ids?:number[]// 角色权限ID列表isResizable?:boolean// 是否可调整大小sort?:number// 排序值}布局数据结构布局数据用于描述组件在网格中的位置与尺寸以及对应的组件 key。pxData属于渲染期数据保存时会删除。interfaceLayoutItem{i:string// 唯一标识x:number// X坐标网格单位y:number// Y坐标网格单位w:number// 宽度网格单位h:number// 高度网格单位element:string// 组件keyconfig?:any// 组件配置static?:boolean// 是否固定pxData?:{// 实际像素数据wpx?:numberhpx?:number}}使用方法这一部分从实际代码执行顺序出发把 Home 模块在运行时涉及的三条关键链路整理出来方便在调试或排查问题时按顺序对照。保存取消进入自定义模式拖拽 / 调整 / 增删组件是否保存清理 pxData保存布局到后端权限过滤 紧凑重排退出自定义模式并展示恢复原始布局初始化流程初始化阶段解决的是“页面第一次打开时组件和布局如何正确展示”的问题。整体逻辑是先拿组件定义再拿布局数据最后在渲染前完成权限过滤和布局重排确保最终进入 GridLayout 的数据是可直接展示的。初始化过程可以抽象为下面几个连续步骤顺序操作目的1获取当前公司信息作为后续接口请求的基础参数2拉取组件配置构建组件字典用于后续渲染与权限判断3拉取用户布局配置获取用户上一次保存的布局数据4权限过滤布局移除无权限组件避免越权展示5紧凑排列布局重新计算坐标避免布局空洞6渲染到 GridLayout以最终布局数据完成页面展示需要注意的是权限过滤和紧凑排列必须发生在渲染之前否则页面会先出现空白或错位再被动修正。自定义布局流程自定义布局流程解决的是“用户如何安全地修改布局并保存”的问题。进入自定义模式后页面会切换到编辑态开放拖拽、缩放和组件增删保存时只持久化必要的布局数据并在退出编辑态后重新回到标准展示流程。该流程的关键动作和数据变化如下阶段行为说明进入自定义切换为编辑状态开放拖拽、缩放显示完整布局编辑中拖拽 / 调整 / 增删组件所有操作仅作用于前端布局数据点击完成清理pxData移除渲染期像素数据避免污染后端保存布局写入后端根据是否已有记录执行新增或更新退出编辑权限过滤 紧凑重排确保最终展示与权限规则一致需要注意的是自定义模式下允许看到所有布局项只是为了编辑方便最终展示一定会在保存后重新执行权限过滤。权限控制流程权限控制贯穿整个模块但真正生效的节点只有两个布局进入渲染前以及自定义保存完成后。这样可以保证既不影响编辑体验也不会在展示阶段越权。权限执行逻辑可以整理为下面这条固定链路阶段是否进行权限判断目的页面初始化渲染是确保首次展示不越权、不占位自定义编辑中否保证编辑视图完整便于调整布局保存并退出编辑是最终展示结果与权限规则一致权限来源完全依赖组件配置中的dept_ids和role_ids字段所有判断都集中在同一个权限函数中避免逻辑分散。扩展说明这一部分用于说明后续功能扩展的标准入口位置避免在新增能力时到处插逻辑导致维护成本上升。添加新的组件类型当需要新增一种组件类型时不需要修改布局或权限逻辑只需要在组件类型映射中注册新组件即可。这样可以保证后端返回新类型时前端能够正确解析并渲染。核心修改点集中在componentTypeMapconstcomponentTypeMap{echarts:DynamicChart,chart:DynamicChart,custom:CustomComponent,// 新增自定义组件}新增组件类型时需要注意两点前端映射的 key 必须与后端返回的componentType保持一致如果未配置映射会走兜底组件或直接渲染失败通过这种方式组件扩展不会影响现有布局、权限和保存逻辑整体结构保持稳定。总结该项目围绕可配置仪表板的实际落地展开将组件动态加载、权限控制与可编辑布局整合为一套稳定实现。通过清晰区分编辑态与展示态统一权限过滤与布局重排入口保证功能灵活性的同时避免数据污染与展示失控适合作为自学阶段理解前后端协作与状态管理的综合案例。在现有结构基础上可继续扩展更多组件类型与权限策略引入更精细的数据刷新与缓存机制进一步提升性能与可维护性。随着组件规模增长该模式也可迁移到更多业务场景作为通用可视化配置方案使用。