企业管理系统如何实现自定义首页与千人千面?RuoYi Office 给出了完整方案

引言:为什么企业管理系统需要自定义首页?

还记得那个年代吗?所有员工登录 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:首页配置表

首页的「身份证」,每一行代表一个首页模板。

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:首页布局配置表

记录每个首页上组件的位置和大小——这是实现「拖拽自定义」的核心数据。

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_xposition_y 精确控制组件在网格上的位置
  • config 以 JSON 格式存储组件的个性化配置(如统计卡片显示哪些指标)
3. system_home_component:组件定义表

定义系统中所有可用的首页组件,相当于一个「组件市场」。

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_configsystem_home_app_user:双层应用配置

这是实现「千人千面」应用入口的关键设计——系统级配置 + 用户级配置的双层架构:

角色说明
system_home_app_config系统管理员定义系统默认的常用应用列表
system_home_app_user普通用户每个用户自定义的应用列表

用户首次访问时,系统自动将系统级配置复制为用户的个人配置,之后用户可以自由增删改排序。这种**「写时复制」(Copy-on-Write)**的设计,既保证了初始体验的一致性,又给了用户充分的自定义空间。

后端服务实现:三个核心 Service

1. HomePageServiceImpl:首页管理服务

这是首页管理的核心服务,负责首页的增删改查和用户首页关联。

最关键的方法——获取用户首页

@Override
public HomePageDO getUserHomePage(Long userId) {
    // 先查询用户是否配置了首页
    UserHomePageDO userHomePage = userHomePageMapper.selectByUserId(userId);
    if (userHomePage != null) {
        return homePageMapper.selectById(userHomePage.getPageId());
    }
    // 如果用户未配置首页,返回默认首页
    return getDefaultHomePage();
}

这段代码完美体现了「用户优先,默认兜底」的设计理念——先查用户自定义的首页,没有则回退到系统默认首页。

布局保存——从 JSON 到数据表

@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:用户应用服务

这是实现「千人千面」应用中心的核心——每个用户看到的常用应用都不一样。

核心流程——获取我的应用列表

@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 并补充菜单路径信息
    // ...
}

这段代码揭示了千人千面的三层过滤逻辑

  1. 首次访问自动初始化:从系统配置复制一份到用户名下
  2. 权限过滤:只展示用户有权限访问的应用
  3. 个性化排序:用户可以自由调整应用顺序

权限过滤的实现

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:组件管理服务

管理首页可用的组件库——相当于一个「组件市场」的后端。

@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 采用了一个简洁优雅的组件注册机制:

// 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;
}

注册一个新组件只需一行代码:

// 注册欢迎组件
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 实现。

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

渲染流程非常清晰:

  1. 通过 API 获取当前首页的布局列表
  2. 将数据库中的 position_x/ywidth/height 映射为栅格参数
  3. 通过 ComponentWrapper 根据 componentCode 动态加载对应的 Vue 组件
  4. 渲染模式下设置 static: true,禁止拖拽和缩放

首页入口——智能路由

用户访问首页时,系统会智能决定展示哪个首页:

<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选中组件后,展示该组件的配置项

智能组件放置算法

当用户从组件面板添加一个组件时,系统会自动寻找一个不重叠的位置:

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 发送到后端:

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/ 目录下新建你的组件文件:

<!-- 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 中添加一行注册代码:

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-plusVue3 网格拖拽布局
后端框架Spring Boot 3.5 + MyBatis Plus企业级 Java 后端
权限控制Spring Security + RBAC角色级权限过滤
数据存储MySQL + JSON 配置结构化 + 灵活配置
多租户租户隔离不同企业独立首页配置

与同类方案的对比

特性RuoYi Office钉钉/飞书传统 OA 系统
首页自定义 完整支持
本站提供的所有下载资源均来自互联网,仅提供学习交流使用,版权归原作者所有。如需商业使用,请联系原作者获得授权。 如您发现有涉嫌侵权的内容,请联系我们 邮箱:alixiixcom@163.com