news 2026/6/11 6:40:00

企业工作台/首页设计:可拖拽磁贴 + 待办聚合 + 数据卡片实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
企业工作台/首页设计:可拖拽磁贴 + 待办聚合 + 数据卡片实现

企业工作台/首页设计:可拖拽磁贴 + 待办聚合 + 数据卡片实现

🌐演示地址:http://ruoyioffice.com | 📦源码1·GitHub:ruoyi-office | 📦源码2·GitCode:ruoyi-office | 📦源码3·Gitee:ruoyi-office | 💬微信:17156169080(备注「RuoYi Office」)

员工每天打开系统的第一个页面,决定了他对整套 ERP/OA 的第一印象。钉钉工作台、飞书多维表格首页——头部产品都在做可拖拽磁贴 + 待办聚合 + 数据卡片。本文不重复「千人千面数据模型」的理论(详见 custom-homepage-personalized-experience.md),而是聚焦RuoYi Office 工作台的前端实操grid-layout-plus怎么拖、registry.ts怎么注册 11 个磁贴、workbench-task-list怎么聚合四路 BPM 待办、workbench-app-center怎么用vuedraggable排应用。

▲ 工作台全景:设计器/渲染器分层、grid-layout-plus 24 列栅格、11 个内置磁贴组件、四 Tab 待办聚合与 vuedraggable 应用中心一图看懂

引言:工作台设计到底难在哪?

「首页」和「工作台」在企业系统里常被混用,但工程上它们要解决不同问题:

问题一:布局不可写死。不同角色需要不同磁贴组合——欢迎区、待办、公告、日程、统计卡片——硬编码<div>布局无法支撑千人千面。

问题二:拖拽与渲染要分离。设计器需要拖拽/缩放;员工日常使用时不能误触改变布局——同一套GridLayout组件,两种模式参数截然不同。

问题三:待办来源分散。待办任务、我的单据、已办、抄送——四路 BPM API,Tab 切换、Badge 统计、跳转审批详情,逻辑集中在一个磁贴里。

问题四:应用中心与权限菜单联动。「常用应用」必须从 RBAC 菜单树提取叶子节点,拖拽排序后持久化到用户维度——不能展示无权限的入口。

问题五:组件扩展不能改框架。新增「合同到期提醒」磁贴, ideally 只写 Vue 组件 + 一行registerComponent,不动index.vue

痛点不处理的后果
布局写死每改一次首页发版
拖拽不分离员工误拖布局,体验灾难
待办分散4 个菜单才能办完一件事
应用无权限过滤点击 403,信任崩塌
组件不注册设计器选不到新磁贴

RuoYi Office 的解法是:grid-layout-plus 栅格 + 组件注册制 + 设计/渲染双模式。代码路径:ruoyi-office-vben/apps/web-antd/src/views/dashboard/home/


一、目录结构与页面分层

▲ 员工登录后的工作台首页:欢迎语+天气磁贴、应用中心(发起流程/用印申请/请假销假等高频入口)、待办任务/我的单据/已办/抄送多 Tab 聚合、通知公告与日程待办——全部由磁贴拼装而成

1.1 前端目录地图

dashboard/home/ ├── index.vue # 员工工作台入口(加载我的首页) ├── renderer/ │ └── layout-renderer.vue # 只读渲染(grid-layout-plus static) ├── designer/ │ ├── index.vue # 可视化设计器 │ └── components/ │ ├── designer-canvas.vue # 可拖拽画布 │ ├── component-panel.vue # 左侧组件库 │ └── config-panel.vue # 右侧属性配置 ├── components/ │ ├── registry.ts # ★ 11 个磁贴注册表 │ ├── wrapper/component-wrapper.vue # 动态组件加载 │ ├── welcome/workbench-welcome.vue │ ├── app-center/workbench-app-center.vue # ★ vuedraggable │ ├── taskLists/workbench-task-list.vue # ★ 四 Tab 待办 │ ├── notice/workbench-notice.vue │ ├── schedule/workbench-schedule.vue │ ├── navigation/workbench-quick-nav.vue │ ├── lists/workbench-project.vue │ ├── lists/workbench-trends.vue │ ├── statistics/analytics-visits.vue │ ├── statistics/analytics-visits-data.vue │ └── charts/analytics-visits-source.vue ├── manage/ # 首页模板管理(管理员) └── types/layout.ts # GridLayoutItem 类型定义

1.2 运行时分层

层级文件职责
入口index.vue调用getMyHomePage()获取当前用户首页 ID
渲染layout-renderer.vue加载布局 JSON,只读展示磁贴
设计designer-canvas.vue拖拽/缩放/删除,保存到后端
调度component-wrapper.vuecomponentCode从 registry 取 Vue 组件
磁贴workbench-*.vue各业务 UI 实现
index.vue │ getMyHomePage() ▼ LayoutRenderer (pageId) │ getHomePageLayoutList(pageId) ▼ GridLayout + GridItem[] │ componentCode ▼ ComponentWrapper → registry.getComponent(code)

二、grid-layout-plus:24 列磁贴布局

▲ 首页设计器:左侧「组件库」按统计卡片/图表/列表/快捷导航分组,中间是 24 列栅格画布,从组件库拖拽磁贴到画布即可布局,右侧「属性配置」编辑选中磁贴参数,顶部「预览 / 保存」实时生效

2.1 为什么选 grid-layout-plus

grid-layout-plus是 Vue3 版 react-grid-layout,支持拖拽、缩放、碰撞检测、响应式栅格。RuoYi Office 采用24 列设计(与 Ant Design 栅格习惯一致),rowHeight=60px,磁贴宽高以「格」为单位存储。

参数设计模式渲染模式
colNum2424
rowHeight6060
isDraggabletruefalse
isResizabletruefalse
staticfalsetrue
margin[10, 10][10, 10]

结论前置:设计器与渲染器共用GridLayout组件,通过isDraggable/isResizable/static三个开关区分模式,避免维护两套布局引擎。

2.2 layout-renderer.vue:只读渲染核心

员工打开工作台时,layout-renderer.vue从后端拉取布局列表,转换为GridLayoutItem[]

import{GridItem,GridLayout}from'grid-layout-plus';import{getHomePageLayoutList}from'#/api/system/home';constlayoutConfig=ref({colNum:24,rowHeight:60,isDraggable:false,// 渲染模式不可拖拽isResizable:false,margin:[10,10],containerPadding:[10,10],verticalCompact:false,});asyncfunctionloadLayout(){constlayoutItems=awaitgetHomePageLayoutList(props.pageId);layout.value=layoutItems.map((item)=>({i:`item-${item.id}`,x:item.positionX,y:item.positionY,w:item.width,h:item.height,componentCode:item.componentCode,config:item.config?JSON.parse(item.config):{},isDraggable:false,isResizable:false,static:true,}));}

模板中每个GridItem包裹ComponentWrapper,把componentCodeconfig传给具体磁贴:

<GridItem v-for="item in layout" :key="item.i" :x="item.x" :y="item.y" :w="item.w" :h="item.h" :i="item.i" :static="item.static"> <div class="layout-item-content h-full w-full overflow-hidden rounded bg-white shadow-sm"> <ComponentWrapper :component-code="item.componentCode" :config="item.config" /> </div> </GridItem>

2.3 designer-canvas.vue:设计模式差异

设计器打开isDraggable=trueisResizable=true,布局变更通过emit('update:layout')回传父组件保存:

functionhandleLayoutUpdated(newLayout:any[]){constupdatedLayout=newLayout.map((item)=>{constexistingItem=localLayout.value.find((l)=>l.i===item.i);return{...existingItem,x:item.x,y:item.y,w:item.w,h:item.h};});emit('update:layout',updatedLayout);}

2.4 GridLayoutItem 数据结构

exportinterfaceGridLayoutItem{i:string;// 唯一标识 item-{id}x:number;// 列位置 0-23y:number;// 行位置w:number;// 宽度(格数)h:number;// 高度(格数)componentCode:string;// 磁贴编码,对应 registryconfig?:Record<string,any>;// 磁贴私有配置 JSONstatic?:boolean;}

后端表system_home_page_layout存储position_x/ywidth/heightcomponent_codeconfig


三、11 个内置磁贴:registry.ts 注册制

3.1 注册表设计

所有可拖拽到工作台的磁贴,在components/registry.ts统一注册:

exportinterfaceComponentRegistryItem{code:string;component:Component;name:string;description?:string;}constcomponentRegistry=newMap<string,ComponentRegistryItem>();exportfunctionregisterComponent(item:ComponentRegistryItem){componentRegistry.set(item.code,item);}exportfunctiongetComponent(code:string):Component|undefined{returncomponentRegistry.get(code)?.component;}

3.2 完整 11 组件清单

code组件文件名称功能
workbench_welcomeworkbench-welcome.vue欢迎组件用户名、问候语、天气
workbench_app_centerworkbench-app-center.vue应用中心常用应用 + vuedraggable
workbench_task_listworkbench-task-list.vue任务列表四 Tab BPM 待办聚合
workbench_noticeworkbench-notice.vue通知公告系统公告列表
workbench_scheduleworkbench-schedule.vue我的日程日历 + 待办事项
workbench_quick_navworkbench-quick-nav.vue快捷导航自定义快捷入口
workbench_projectworkbench-project.vue项目列表项目卡片
workbench_trendsworkbench-trends.vue动态列表最新动态
analytics_visitsanalytics-visits.vue访问统计访问量 KPI 卡片
analytics_visits_dataanalytics-visits-data.vue数据统计访问详情
analytics_visits_sourceanalytics-visits-source.vue访问来源饼图分析

注册示例:

registerComponent({code:'workbench_task_list',component:WorkbenchTaskList,name:'任务列表',description:'展示我的单据、待办任务、已办任务、抄送我的',});

3.3 ComponentWrapper 动态加载

<script setup lang="ts"> import { computed } from 'vue'; import { getComponent } from '../registry'; const props = defineProps<{ componentCode: string; config?: Record<string, any>; }>(); const DynamicComponent = computed(() => getComponent(props.componentCode)); </script> <template> <component :is="DynamicComponent" v-if="DynamicComponent" v-bind="config" /> <div v-else>组件 {{ componentCode }} 未注册</div> </template>

3.4 新增磁贴四步

  1. components/下新建workbench-xxx.vue
  2. registry.tsregisterComponent({ code: 'workbench_xxx', ... })
  3. 后端system_home_component表录入组件元数据(设计器组件库展示)
  4. 在设计器中拖入画布,调整 w/h,保存布局

四、待办聚合:workbench-task-list.vue

4.1 四 Tab 设计

workbench-task-list是企业工作台的核心磁贴——把 BPM 四条线合并为一个 Tab 面板:

Tab Key标签API说明
todo待办任务getTaskTodoPage当前用户待审批
myBill我的单据getProcessInstanceMyPage我发起的流程
done已办任务getTaskDonePage我已处理的
copy抄送我的getProcessInstanceCopyPage抄送通知
typeTabKey='todo'|'done'|'myBill'|'copy';consttabs=computed(()=>[{key:'todo',label:'待办任务',count:statistics.value.todo},{key:'myBill',label:'我的单据',count:statistics.value.myBill},{key:'done',label:'已办任务',count:statistics.value.done},{key:'copy',label:'抄送我的',count:statistics.value.copy},]);

4.2 列定义与 Tab 差异

不同 Tab 返回的数据结构略有差异(Task vs ProcessInstance vs Copy),组件用customRender统一列展示:

todo/donemyBillcopy
单据类型processInstance.namenameprocessInstanceName
单据编号processInstance.billCodebillCodebillCode
审批状态DictTagDictTag + 流程图
摘要summary JSONsummarysummary
特殊列任务节点/审批时间发起时间抄送节点/抄送时间

4.3 统计 Badge 与跳转

组件挂载时并行请求四路 API 的total填充 Badge;点击行跳转 BPM 审批详情:

functionhandleViewDetail(record:any){consttab=activeTab.value;letpath='';if(tab==='todo'||tab==='done'){path=`/bpm/task/detail?id=${record.id}`;}elseif(tab==='myBill'){path=`/bpm/process-instance/detail?id=${record.id}`;}elseif(tab==='copy'){path=`/bpm/process-instance/detail?id=${record.processInstanceId}`;}if(path)router.push(path);}

4.4 可配置 props

interfaceProps{maxRecordNum?:number;// 默认 10,控制表格最大行数}

设计器 config JSON 可覆盖maxRecordNum,小磁贴显示 5 条,大磁贴显示 20 条。


五、应用中心:workbench-app-center.vue + vuedraggable

5.1 功能概述

应用中心磁贴展示用户「常用应用」图标墙,支持:

  • 从 RBAC 权限菜单选择叶子节点添加
  • vuedraggable拖拽排序
  • 删除应用(Popconfirm)
  • 排序持久化updateUserAppSort

5.2 从权限菜单提取可选应用

constmenuOptions=computed(()=>{constmenus=accessStore.accessMenus||[];constoptions=[];constprocessMenus=(menuList:any[],parents:any[]=[])=>{for(constmenuofmenuList){constchildren=Array.isArray(menu.children)?menu.children:[];constisLeaf=children.length===0;if(isLeaf&&menu.path){constid=menu.id??menu.menuId??menu.meta?.id;if(!id)return;options.push({id:Number(id),name:menu.meta?.title??menu.name,path:menu.path,icon:menu.icon??menu.meta?.icon??'carbon:application',});}if(children.length>0)processMenus(children,[...parents,menu]);}};processMenus(menus);returnoptions;});

设计要点:只取有path的叶子菜单,天然过滤目录节点;无 ID 的菜单跳过并 warn,避免后端保存失败。

5.3 vuedraggable 排序持久化

<Draggable v-model="appList" item-key="id" :disabled="!enableDrag" ghost-class="app-ghost" @start="isDragging = true" @end="handleDragEnd" > <template #item="{ element }"> <div class="app-item" @click="handleAppClick(element)"> <IconifyIcon :icon="element.icon" /> <span>{{ element.name }}</span> </div> </template> </Draggable>
asyncfunctionhandleDragEnd(){isDragging.value=false;constsortList=appList.value.map((app,index)=>({id:app.id,sort:index+1,}));awaitupdateUserAppSort(sortList);message.success('排序已保存');}

5.4 首次初始化

用户首次进入且无应用数据时,调用initUserApp()从系统默认应用模板初始化,避免空白磁贴。

Props默认说明
maxAppCount12最大展示应用数
gridCols4图标网格列数
enableDragtrue是否允许拖拽排序

六、index.vue 入口与预览模式

consthomePageInfo=ref<any>(null);asyncfunctionloadHomePage(){homePageInfo.value=awaitgetMyHomePage();}constpreviewPageId=computed(()=>{constpageId=route.query.preview;returnpageId?Number(pageId):null;});constcurrentPageId=computed(()=>{returnpreviewPageId.value||homePageInfo.value?.id;});
  • 预览模式:URL?preview={pageId},设计器保存前预览

其余磁贴(welcome、notice、schedule、analytics 数据卡片)按同一ComponentWrapper模式加载。

▲ 首页管理列表:支持维护多套首页模板(默认工作台、自定义首页…),每套可「设计 / 设置为首页 / 启用」——配合按角色下发不同默认首页,即可实现「千人千面」的工作台

与 custom-homepage-personalized-experience.md 的分工:该文讲数据表与后端服务;本文讲 grid-layout 拖拽、registry 注册、待办聚合、vuedraggable 应用中心前端实操


七、技术亮点总结

设计要点实现方式价值
磁贴布局grid-layout-plus拖拽/缩放/栅格化
双模式isDraggable/static 开关设计灵活、使用稳定
组件扩展registry.ts新增磁贴不改框架
动态加载ComponentWrapper按 code 懒渲染
待办聚合4 Tab + 4 API一屏办完审批
应用中心vuedraggable用户自定义排序
权限联动accessStore.accessMenus不展示无权限入口
预览?preview=pageId设计器即时验证

八、快速体验

在线演示

  • 🌐 地址:http://ruoyioffice.com/web/
  • 👤 账号:admin/admin123
  • 📍 登录后默认进入工作台首页

推荐体验流程

  1. 登录后观察默认工作台磁贴布局(欢迎 + 待办 + 应用中心)
  2. 点击待办 Tab 切换「待办任务 / 我的单据 / 已办 / 抄送」
  3. 在应用中心拖拽图标调整顺序,刷新页面验证持久化
  4. 进入工作台 → 首页管理(或/workspace/home/manage
  5. 打开设计器,从左侧组件库拖入「我的日程」磁贴
  6. 调整大小位置,保存后在员工首页查看效果
  7. 阅读registry.ts理解组件注册机制

本地启动

cd W:\ruoyi-office\ruoyi-office-vben pnpm dev:antd# 访问 http://localhost:5800

源码路径

ruoyi-office-vben/apps/web-antd/src/views/dashboard/home/

常见问题(FAQ)

RuoYi Office 工作台支持拖拽布局吗?

支持。基于grid-layout-plus24 列栅格,管理员在设计器中拖拽/缩放磁贴,员工端只读渲染。

内置有哪些磁贴组件?

11 个:welcome、app_center、task_list、notice、schedule、quick_nav、project、trends,以及 3 个 analytics 数据卡片。见components/registry.ts

如何新增自定义磁贴?

新建 Vue 组件 →registerComponent注册 → 后端组件库录入 → 设计器拖入。无需修改index.vue

待办聚合包含哪些类型?

四 Tab:待办任务、我的单据、已办任务、抄送我的,分别对接 BPM Task 和 ProcessInstance API。

结语

企业工作台不是「把几个 Chart 拼到首页」——它是员工每天工作的操作系统桌面。RuoYi Office 用 grid-layout-plus 解决布局、用 registry 解决扩展、用四 Tab 聚合解决待办分散、用 vuedraggable 解决应用排序——四个齿轮咬合,才能支撑 14 大模块、不同角色的千人千面。

如果你正在做 OA/ERP 首页改造,或基于 RuoYi Office 二开新磁贴(如「合同到期提醒」「库存预警」),这套dashboard/home/目录就是最好的起点。欢迎 Star,也欢迎微信17156169080(备注「RuoYi Office」)交流工作台定制经验。

你的团队首页有哪些「离不开的磁贴」?待办、日程还是数据看板?评论区聊聊。


💡想要体验 RuoYi Office 的工作台?

🌐在线演示:http://ruoyioffice.com/web/(账号 admin / admin123)

📦源码仓库:GitCode | GitHub

💬技术咨询:添加微信17156169080,备注「RuoYi Office」

如果觉得不错,请给个 Star 支持一下!

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/11 6:39:54

NGA论坛优化摸鱼体验插件:3分钟打造你的专属高效浏览神器

NGA论坛优化摸鱼体验插件&#xff1a;3分钟打造你的专属高效浏览神器 【免费下载链接】NGA-BBS-Script NGA论坛增强脚本&#xff0c;给你完全不一样的浏览体验 项目地址: https://gitcode.com/gh_mirrors/ng/NGA-BBS-Script 还在为NGA论坛繁杂的界面和低效的浏览体验烦恼…

作者头像 李华
网站建设 2026/6/11 6:30:06

告别AT指令!用Arduino IDE给两个ESP8266写个无线聊天室(附完整代码)

用Arduino IDE构建ESP8266无线聊天室&#xff1a;告别AT指令的现代开发实践在嵌入式开发领域&#xff0c;ESP8266凭借其Wi-Fi功能和低廉价格成为物联网项目的宠儿。然而&#xff0c;许多开发者仍被困在AT指令的繁琐配置中——每次修改参数都需要重新发送指令&#xff0c;调试过…

作者头像 李华