企业管理系统如何实现自定义首页与千人千面?RuoYi Office 给出了完整方案
🌐 文档地址 :ruoyioffice.com | 📦 源码1 :gitee.com/yqzy1688/ru... |📦 源码2 :gitee.com/yqzy1688/ru... |📦 源码3 :github.com/yuqing2026/... | 💬 微信:17156169080(备注「RuoYi Office」)
当你打开钉钉、飞书或企业微信时,你看到的首页和同事看到的一样吗?------答案几乎都是「不一样」。这就是千人千面,也是现代企业管理系统的标配能力。RuoYi Office 用一套优雅的架构设计,让 OA、CRM、ERP 等系统的首页定制变得触手可及。
引言:为什么企业管理系统需要自定义首页?
还记得那个年代吗?所有员工登录 OA 系统后,看到的都是同一个首页------公司公告、考勤统计、待办列表......不管你是销售总监还是仓库管理员,首页都是一模一样的。
这种「一刀切」的首页设计带来了哪些问题?
- 信息过载:销售不需要看仓库库存,行政不关心客户线索,但大家被迫面对一堆无关信息
- 效率低下:找到自己真正需要的功能,往往需要点击 3-5 次菜单
- 体验割裂:不同角色的工作场景截然不同,却被迫使用同一套界面
- 用户流失:糟糕的首页体验直接导致系统使用率下降
据 Gartner 研究报告,个性化的工作界面能提升员工工作效率 20% 以上,同时将系统使用满意度提升 40%。
这就是为什么钉钉、飞书等头部办公软件都在大力推进千人千面的首页定制能力------不同角色看到不同内容,每个人都能打造属于自己的工作台。
但对于中小企业来说,如何在自己的管理系统中实现这一能力呢?RuoYi Office 给出了一套完整、可复用的开源方案。
什么是「千人千面」?
千人千面(Personalized Experience)是指系统根据用户的角色、权限、偏好等因素,为每个用户呈现不同的首页内容和布局。
在企业管理系统中,千人千面通常包含三个层次:
| 层次 | 说明 | 示例 |
|---|---|---|
| 角色级定制 | 不同角色看到不同的默认首页 | 管理层看数据看板,销售看客户动态,HR 看人事统计 |
| 用户级定制 | 每个用户可以自定义自己的首页布局 | 拖拽调整组件位置和大小,添加/移除组件 |
| 应用级定制 | 用户可以自定义常用应用和快捷入口 | 自主选择常用功能,个性化排序 |
RuoYi Office 在这三个层次上都做了完整实现,下面我们将从核心设计思想、数据结构设计、后端服务实现到前端交互,逐层剖析。
核心设计思想
在动手写代码之前,RuoYi Office 团队确立了几个核心设计原则:
1. 页面即模板,布局即配置
首页不是硬编码的页面,而是一个可配置的模板系统 。每个首页由一系列组件 按照特定的布局排列而成,布局信息以 JSON 形式存储在数据库中。

2. 组件化 + 注册制
所有首页上可展示的功能区块(如待办列表、通知公告、统计卡片等)都被封装为独立组件 ,通过统一的注册表进行管理。新增一个首页功能,只需要开发一个 Vue 组件并注册即可,无需修改任何框架代码。
3. 系统默认 + 用户自定义的双轨模式
系统管理员设定一个默认首页 供所有用户使用,同时每个用户可以选择或创建自己的个性化首页。如果用户没有自定义首页,则自动回退到系统默认首页。
4. RBAC 权限贯穿始终
首页上展示的应用入口、组件内容都会根据用户的角色和权限进行过滤。销售角色的用户即使添加了 ERP 组件,如果没有对应权限,也不会显示相关数据。
关键数据结构设计
好的数据结构设计是系统成功的基石。RuoYi Office 围绕首页自定义设计了 6 张核心数据表,形成了一个清晰的分层架构。
ER 关系总览

各表详解
1. system_home_page:首页配置表
首页的「身份证」,每一行代表一个首页模板。
sql
CREATE TABLE `system_home_page` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '首页ID',
`name` varchar(100) NOT NULL COMMENT '首页名称',
`code` varchar(50) NOT NULL COMMENT '首页编码',
`description` varchar(500) DEFAULT NULL COMMENT '首页描述',
`preview_image` varchar(255) DEFAULT NULL COMMENT '预览图',
`is_default` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否默认首页',
`status` tinyint NOT NULL DEFAULT 1 COMMENT '状态(0停用 1启用)',
`tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
PRIMARY KEY (`id`),
UNIQUE INDEX `idx_code`(`code`, `deleted`)
);
设计要点:
code字段唯一标识首页模板,系统保留default_workspace作为默认首页is_default标记系统默认首页,用于用户未自定义首页时的回退tenant_id支持多租户隔离,不同租户可以有不同的默认首页
2. system_home_page_layout:首页布局配置表
记录每个首页上组件的位置和大小------这是实现「拖拽自定义」的核心数据。
sql
CREATE TABLE `system_home_page_layout` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '布局ID',
`page_id` bigint NOT NULL COMMENT '首页ID',
`component_code` varchar(100) NOT NULL COMMENT '组件编码',
`position_x` int NOT NULL DEFAULT 0 COMMENT 'X坐标',
`position_y` int NOT NULL DEFAULT 0 COMMENT 'Y坐标',
`width` int NOT NULL DEFAULT 6 COMMENT '宽度(栅格数)',
`height` int NOT NULL DEFAULT 4 COMMENT '高度(栅格数)',
`config` text DEFAULT NULL COMMENT '组件配置(JSON)',
`sort` int NOT NULL DEFAULT 0 COMMENT '排序',
PRIMARY KEY (`id`),
INDEX `idx_page_id`(`page_id`, `deleted`)
);
设计要点:
- 使用 24 列栅格系统,与前端 Grid Layout 完美对齐
position_x、position_y精确控制组件在网格上的位置config以 JSON 格式存储组件的个性化配置(如统计卡片显示哪些指标)
3. system_home_component:组件定义表
定义系统中所有可用的首页组件,相当于一个「组件市场」。
sql
CREATE TABLE `system_home_component` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '组件ID',
`category_id` bigint NOT NULL COMMENT '分类ID',
`name` varchar(100) NOT NULL COMMENT '组件名称',
`code` varchar(50) NOT NULL COMMENT '组件编码',
`component_path` varchar(255) NOT NULL COMMENT '组件路径',
`default_width` int NOT NULL DEFAULT 12 COMMENT '默认宽度(网格列数1-24)',
`default_height` int NOT NULL DEFAULT 4 COMMENT '默认高度(网格行数)',
`config_schema` text DEFAULT NULL COMMENT '配置Schema(JSON格式)',
`status` tinyint NOT NULL DEFAULT 1 COMMENT '状态(0停用 1启用)',
PRIMARY KEY (`id`),
UNIQUE INDEX `idx_code`(`code`, `deleted`)
);
设计要点:
config_schema用 JSON Schema 描述组件支持哪些配置项,前端据此动态生成配置表单default_width/default_height提供默认尺寸,拖入画布时自动应用- 通过
category_id关联分类表,方便组件面板分组展示
4. system_home_app_config 与 system_home_app_user:双层应用配置
这是实现「千人千面」应用入口的关键设计------系统级配置 + 用户级配置的双层架构:
| 表 | 角色 | 说明 |
|---|---|---|
system_home_app_config |
系统管理员 | 定义系统默认的常用应用列表 |
system_home_app_user |
普通用户 | 每个用户自定义的应用列表 |
用户首次访问时,系统自动将系统级配置复制为用户的个人配置,之后用户可以自由增删改排序。这种**「写时复制」(Copy-on-Write)**的设计,既保证了初始体验的一致性,又给了用户充分的自定义空间。
后端服务实现:三个核心 Service
1. HomePageServiceImpl:首页管理服务
这是首页管理的核心服务,负责首页的增删改查和用户首页关联。
最关键的方法------获取用户首页:
java
@Override
public HomePageDO getUserHomePage(Long userId) {
// 先查询用户是否配置了首页
UserHomePageDO userHomePage = userHomePageMapper.selectByUserId(userId);
if (userHomePage != null) {
return homePageMapper.selectById(userHomePage.getPageId());
}
// 如果用户未配置首页,返回默认首页
return getDefaultHomePage();
}
这段代码完美体现了「用户优先,默认兜底」的设计理念------先查用户自定义的首页,没有则回退到系统默认首页。
布局保存------从 JSON 到数据表:
java
@Override
@Transactional(rollbackFor = Exception.class)
public void saveHomePageLayout(HomePageLayoutSaveReqVO saveReqVO) {
// 校验首页是否存在
HomePageDO homePage = validateHomePageExists(saveReqVO.getPageId());
// 校验权限:只有创建者或管理员可以保存
if (!"default_workspace".equals(homePage.getCode())) {
validateCreatorPermission(homePage);
}
// 先删除该首页的所有布局
layoutMapper.deleteByPageId(saveReqVO.getPageId());
// 解析 JSON 并逐项保存
JSONObject layoutConfig = JSONUtil.parseObj(saveReqVO.getLayoutJson());
JSONArray items = layoutConfig.getJSONArray("items");
if (items != null && !items.isEmpty()) {
for (int i = 0; i < items.size(); i++) {
JSONObject item = items.getJSONObject(i);
HomePageLayoutDO layout = new HomePageLayoutDO();
layout.setPageId(saveReqVO.getPageId());
layout.setComponentCode(item.getStr("componentCode"));
layout.setPositionX(item.getInt("x", 0));
layout.setPositionY(item.getInt("y", 0));
layout.setWidth(item.getInt("w", 6));
layout.setHeight(item.getInt("h", 4));
layout.setConfig(item.getStr("config", "{}"));
layout.setSort(i);
layoutMapper.insert(layout);
}
}
}
采用**「先删后插」**的策略保存布局,简单可靠。前端设计器将整个布局序列化为 JSON,后端解析后逐项持久化到数据库。
2. HomeAppUserServiceImpl:用户应用服务
这是实现「千人千面」应用中心的核心------每个用户看到的常用应用都不一样。
核心流程------获取我的应用列表:
java
@Override
public List<HomeAppUserRespVO> getMyAppList() {
Long userId = SecurityFrameworkUtils.getLoginUserId();
// 1. 查询用户的应用配置
List<HomeAppUserDO> appUserList = appUserMapper
.selectListByUserIdAndStatus(userId, CommonStatusEnum.ENABLE.getStatus());
// 2. 如果用户没有配置,自动从系统默认初始化
if (appUserList.isEmpty()) {
initUserApp();
appUserList = appUserMapper
.selectListByUserIdAndStatus(userId, CommonStatusEnum.ENABLE.getStatus());
}
// 3. 根据用户权限过滤应用(RBAC 控制)
Set<Long> userMenuIds = getUserMenuIds(userId);
appUserList = appUserList.stream()
.filter(app -> userMenuIds.contains(app.getMenuId()))
.collect(Collectors.toList());
// 4. 转换为 VO 并补充菜单路径信息
// ...
}
这段代码揭示了千人千面的三层过滤逻辑:
- 首次访问自动初始化:从系统配置复制一份到用户名下
- 权限过滤:只展示用户有权限访问的应用
- 个性化排序:用户可以自由调整应用顺序
权限过滤的实现:
java
private Set<Long> getUserMenuIds(Long userId) {
// 获取用户的角色列表
Set<Long> roleIds = permissionService.getUserRoleIdListByUserId(userId);
// 获取角色拥有的菜单ID列表
Set<Long> menuIds = permissionService.getRoleMenuListByRoleId(roleIds);
// 过滤出启用的菜单类型
List<MenuDO> menuList = menuService.getMenuList();
return menuList.stream()
.filter(menu -> menuIds.contains(menu.getId()))
.filter(menu -> menu.getType().equals(2)) // 只选择菜单类型
.filter(menu -> menu.getStatus().equals(CommonStatusEnum.ENABLE.getStatus()))
.map(MenuDO::getId)
.collect(Collectors.toSet());
}
通过 用户 → 角色 → 菜单 的链路,精确控制每个用户能看到的应用入口。这就是为什么同一个系统中,总经理和普通员工的应用中心展示完全不同。
3. HomeComponentServiceImpl:组件管理服务
管理首页可用的组件库------相当于一个「组件市场」的后端。
java
@Override
public Map<Long, List<HomeComponentDO>> getComponentsByCategory() {
List<HomeComponentDO> components = componentMapper.selectList(
HomeComponentDO::getStatus, CommonStatusEnum.ENABLE.getStatus());
return components.stream()
.collect(Collectors.groupingBy(HomeComponentDO::getCategoryId));
}
组件按分类分组返回给前端,设计器中的组件面板据此渲染分类列表。
前端实现:从组件注册到拖拽设计
组件注册表------前端的「组件市场」
RuoYi Office 采用了一个简洁优雅的组件注册机制:
typescript
// registry.ts - 组件注册表
export interface ComponentRegistryItem {
code: string; // 组件编码(与后端 system_home_component.code 对应)
component: Component; // Vue 组件
name: string; // 组件名称
description?: string; // 组件描述
}
const componentRegistry = new Map<string, ComponentRegistryItem>();
export function registerComponent(item: ComponentRegistryItem) {
componentRegistry.set(item.code, item);
}
export function getComponent(code: string): Component | undefined {
return componentRegistry.get(code)?.component;
}
注册一个新组件只需一行代码:
typescript
// 注册欢迎组件
registerComponent({
code: 'workbench_welcome',
component: WorkbenchWelcome,
name: '欢迎组件',
description: '展示欢迎信息、用户信息和天气',
});
// 注册通知公告组件
registerComponent({
code: 'workbench_notice',
component: WorkbenchNotice,
name: '通知公告',
description: '展示系统通知公告列表',
});
// 注册应用中心组件
registerComponent({
code: 'workbench_app_center',
component: WorkbenchAppCenter,
name: '应用中心',
description: '展示常用应用,支持拖拽排序',
});
// 更多组件...(任务列表、日程、统计卡片等)
RuoYi Office 已经内置了 11 个首页组件,涵盖了企业办公的常见场景:
| 组件 | 编码 | 功能 |
|---|---|---|
| 欢迎组件 | workbench_welcome |
展示用户信息、天气、问候语 |
| 应用中心 | workbench_app_center |
常用应用快捷入口,支持拖拽排序 |
| 任务列表 | workbench_task_list |
待办/已办/抄送任务 |
| 通知公告 | workbench_notice |
系统通知和公告 |
| 我的日程 | workbench_schedule |
日历视图的日程管理 |
| 快捷导航 | workbench_quick_nav |
常用功能快捷入口 |
| 访问统计 | analytics_visits |
数据统计卡片 |
| 数据详情 | analytics_visits_data |
访问数据折线图 |
| 来源分析 | analytics_visits_source |
流量来源饼图 |
| 项目列表 | workbench_project |
项目卡片展示 |
| 动态列表 | workbench_trends |
最新动态时间线 |
布局渲染器------将配置变为页面
LayoutRenderer 组件负责将后端存储的布局数据渲染为实际页面,基于 grid-layout-plus 实现。
vue
<script lang="ts" setup>
import { GridItem, GridLayout } from 'grid-layout-plus';
import ComponentWrapper from '../components/wrapper/component-wrapper.vue';
// 加载布局
async function loadLayout() {
const layoutItems = await getHomePageLayoutList(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) : {},
static: true, // 渲染模式:不可拖拽
}));
}
</script>
<template>
<GridLayout v-model:layout="layout" :col-num="24" :row-height="60"
:is-draggable="false" :is-resizable="false">
<GridItem v-for="item in layout" :key="item.i"
:x="item.x" :y="item.y" :w="item.w" :h="item.h" :i="item.i">
<ComponentWrapper :component-code="item.componentCode" :config="item.config" />
</GridItem>
</GridLayout>
</template>
渲染流程非常清晰:
- 通过 API 获取当前首页的布局列表
- 将数据库中的
position_x/y、width/height映射为栅格参数 - 通过
ComponentWrapper根据componentCode动态加载对应的 Vue 组件 - 渲染模式下设置
static: true,禁止拖拽和缩放
首页入口------智能路由
用户访问首页时,系统会智能决定展示哪个首页:
vue
<script lang="ts" setup>
const previewPageId = computed(() => {
const pageId = route.query.preview;
return pageId ? Number(pageId) : null;
});
const currentPageId = computed(() => {
return previewPageId.value || homePageInfo.value?.id;
});
async function loadHomePage() {
homePageInfo.value = await getMyHomePage(); // 调用后端 getUserHomePage
}
</script>
<template>
<!-- 如果是预览模式,显示预览横幅 -->
<Alert v-if="previewPageId" message="预览模式" type="info" />
<!-- 有首页配置时渲染布局 -->
<LayoutRenderer v-if="currentPageId" :page-id="currentPageId" />
<!-- 没有首页配置时引导用户去配置 -->
<Button v-else type="primary" @click="goToManage">去配置首页</Button>
</template>
首页设计器------可视化拖拽编排
首页设计器是整个功能最复杂也最有价值的部分,采用经典的三栏布局 : 
▲ RuoYi Office 首页设计器:左侧选组件、中间拖布局、右侧调配置
设计器的三栏分工:
| 区域 | 组件 | 功能 |
|---|---|---|
| 左侧 | ComponentPanel |
展示可用组件,点击即可添加到画布 |
| 中间 | DesignerCanvas |
拖拽画布,支持组件的拖动和缩放 |
| 右侧 | ConfigPanel |
选中组件后,展示该组件的配置项 |
智能组件放置算法:
当用户从组件面板添加一个组件时,系统会自动寻找一个不重叠的位置:
typescript
function findAvailablePosition(width: number, height: number) {
const colNum = 24;
if (layout.value.length === 0) return { x: 0, y: 0 };
const maxY = Math.max(...layout.value.map((item) => item.y + item.h), 0);
// 从上到下、从左到右扫描可用位置
for (let y = 0; y <= maxY + 1; y++) {
for (let x = 0; x <= colNum - width; x++) {
const isOverlap = layout.value.some((item) => {
return !(
x + width <= item.x || // 在左边
x >= item.x + item.w || // 在右边
y + height <= item.y || // 在上边
y >= item.y + item.h // 在下边
);
});
if (!isOverlap) return { x, y };
}
}
return { x: 0, y: maxY + 1 }; // 兜底:放在最底部
}
保存布局时,设计器会将画布上所有组件的位置、大小、配置序列化为 JSON,通过 API 发送到后端:
typescript
async function handleSave() {
const layoutConfig = {
items: layout.value.map((item) => ({
x: item.x, y: item.y, w: item.w, h: item.h,
componentCode: item.componentCode,
config: item.config,
})),
colNum: 24,
rowHeight: 60,
margin: [globalConfig.value.margin, globalConfig.value.margin],
containerPadding: [globalConfig.value.containerPadding, globalConfig.value.containerPadding],
};
await saveHomePageLayout({
pageId: pageId.value,
layoutJson: JSON.stringify(layoutConfig),
});
}
功能截图展示
首页管理
管理员可以在「首页管理」中查看、创建和管理所有首页模板,支持设置默认首页。 
▲ 首页管理:查看所有首页模板,可以创建新首页、设置默认首页
首页组件管理
管理系统中可用的所有首页组件,包括组件编码、分类、默认尺寸等信息。RuoYi Office 目前已内置 11 个开箱即用的首页组件,覆盖了企业办公的核心场景:
| 分类 | 组件名称 | 组件编码 | 功能说明 |
|---|---|---|---|
| 🏠 工作台 | 欢迎组件 | workbench_welcome |
显示登录用户头像、姓名、问候语和实时天气信息,打造个性化的开屏体验 |
| 🚀 工作台 | 应用中心 | workbench_app_center |
根据用户权限展示常用应用快捷入口,支持拖拽排序和自定义增删,是千人千面的核心组件 |
| ✅ 工作台 | 任务列表 | workbench_task_list |
集成 BPM 流程引擎,一站式展示我的单据、待办任务、已办任务和抄送我的 |
| 📢 工作台 | 通知公告 | workbench_notice |
实时展示系统通知和公司公告,支持未读标记和详情预览 |
| 📅 工作台 | 我的日程 | workbench_schedule |
以日历视图展示个人日程安排,支持日/周/月切换和待办事项管理 |
| 🔗 导航 | 快捷导航 | workbench_quick_nav |
展示常用功能的图标入口,支持自定义配置导航项 |
| 📊 数据统计 | 访问统计 | analytics_visits |
以卡片形式展示关键业务指标(访问量、用户数、下载量等) |
| 📈 数据统计 | 数据详情 | analytics_visits_data |
以折线图展示数据趋势变化,支持多维度数据对比 |
| 🍩 数据统计 | 来源分析 | analytics_visits_source |
以饼图分析流量来源分布,直观展示各渠道占比 |
| 📋 列表 | 项目列表 | workbench_project |
以卡片形式展示项目信息,包含项目名称、描述、日期等 |
| 📰 列表 | 动态列表 | workbench_trends |
以时间线形式展示团队最新动态和操作记录 |
💡 这些组件的设计充分考虑了企业办公的实际需求:工作台类组件 满足日常办公,数据统计类组件 满足管理层决策,列表类组件 满足信息浏览。开发者还可以根据自身业务需求,快速扩展更多自定义组件(后文会详细介绍扩展方法)。
▲ 组件管理:定义系统中可用的首页组件,支持分类、配置 Schema
可视化首页设计器
通过拖拽式的设计器自由编排首页布局,所见即所得。 
▲ 首页设计器:左侧选组件、中间拖布局、右侧调配置,实时预览效果
用户工作台效果
最终用户看到的个性化工作台,包含欢迎信息、应用中心、待办任务、通知公告、日程等。 
▲ 用户工作台:每个用户看到的都是根据自己角色和偏好定制的首页
扩展一个新组件有多简单?
得益于组件化 + 注册制的设计,在 RuoYi Office 中新增一个首页组件只需要 3 步:
第 1 步:开发 Vue 组件
在 dashboard/home/components/ 目录下新建你的组件文件:
vue
<!-- my-custom-widget.vue -->
<template>
<div class="p-4">
<h3>我的自定义组件</h3>
<p>{{ config.title || '默认标题' }}</p>
<!-- 你的业务逻辑 -->
</div>
</template>
<script lang="ts" setup>
defineProps<{ config: Record<string, any> }>();
</script>
第 2 步:注册到组件注册表
在 registry.ts 中添加一行注册代码:
typescript
import MyCustomWidget from './custom/my-custom-widget.vue';
registerComponent({
code: 'my_custom_widget',
component: MyCustomWidget,
name: '我的自定义组件',
description: '这是一个自定义的首页组件',
});
第 3 步:在后台管理中添加组件记录
在「首页组件管理」中添加一条记录,填写组件编码 my_custom_widget、默认宽高等信息。
完成!现在你就可以在首页设计器中拖拽添加你的自定义组件了。
技术架构总结
| 层次 | 技术选型 | 说明 |
|---|---|---|
| 前端框架 | Vue 3.5 + TypeScript | 响应式、类型安全 |
| UI 组件库 | Ant Design Vue + Vben Admin | 企业级 UI 组件 |
| 拖拽引擎 | grid-layout-plus | Vue3 网格拖拽布局 |
| 后端框架 | Spring Boot 3.5 + MyBatis Plus | 企业级 Java 后端 |
| 权限控制 | Spring Security + RBAC | 角色级权限过滤 |
| 数据存储 | MySQL + JSON 配置 | 结构化 + 灵活配置 |
| 多租户 | 租户隔离 | 不同企业独立首页配置 |
与同类方案的对比
| 特性 | RuoYi Office | 钉钉/飞书 | 传统 OA 系统 |
|---|---|---|---|
| 首页自定义 | ✅ 完整支持 |
