基于 SpringBoot + Vue3 开发的完整OA系统,代码开源、运行稳定,涵盖动态工作流审批、文档管理、博客社区、在线网盘等丰富功能,适合企业自用、二次开发或全栈学习。
✅ 核心功能亮点
· 智能工作流:支持动态审批、加签、会签,可像钉钉/飞书一样自由选择审批人
· 协同办公:内置文档/图片预览、评论讨论、任务管理、日程安排等OA常用模块
· 内容社区:集成博客撰写、问答社区、文章搜索,增强内部知识共享
· 文件管理:支持在线网盘,实现文件上传、分类、共享与预览
· 流程可视化:审批过程可跟踪、可评论,提升协同透明度
技术栈简析
前端:Vue3 + Ant Design Vue + Vite + 数据可视化图表
后端:SpringBoot 2 + MyBatis Plus + Redis + JWT + Shiro
数据库:MySQL(推荐)
部署:支持Docker,提供基础部署说明
资源清单

  1. 前后端完整源码(开源协议,无加密)
  2. 数据库初始化脚本
  3. 基础部署指南(含环境配置步骤)
  4. 系统功能说明文档
  5. 在这里插入图片描述
    代码包含了你描述中最核心、最复杂的动态工作流引擎(审批流)的后端实现逻辑,以及前后端的基础框架搭建指南。你可以基于此骨架扩展文档、博客和网盘功能。

项目名称:OpenOA (基于 SpringBoot + Vue3)

一、数据库设计 (MySQL) - 核心工作流部分

这是OA系统的灵魂,支持动态审批、加签、会签。

– 1. 流程定义表 (存储审批模板,如:请假流程、报销流程)
CREATE TABLE oa_process_def (
id bigint(20) NOT NULL AUTO_INCREMENT,
name varchar(100) NOT NULL COMMENT ‘流程名称’,
key varchar(50) NOT NULL COMMENT ‘流程标识’,
version int(11) DEFAULT 1,
content json NOT NULL COMMENT ‘流程节点JSON配置(包含节点ID、类型、审批人规则)’,
status tinyint(4) DEFAULT 1 COMMENT ‘1:启用 0:停用’,
create_time datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

– 2. 流程实例表 (每一次具体的审批请求)
CREATE TABLE oa_process_instance (
id bigint(20) NOT NULL AUTO_INCREMENT,
process_def_id bigint(20) NOT NULL,
title varchar(200) NOT NULL COMMENT ‘申请标题’,
applicant_id bigint(20) NOT NULL COMMENT ‘申请人ID’,
current_node_id varchar(50) DEFAULT NULL COMMENT ‘当前停留节点ID’,
status tinyint(4) DEFAULT 0 COMMENT ‘0:审批中 1:通过 2:驳回 3:撤销’,
business_data json DEFAULT NULL COMMENT ‘业务数据快照(如请假天数、金额)’,
create_time datetime DEFAULT CURRENT_TIMESTAMP,
finish_time datetime DEFAULT NULL,
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

– 3. 审批任务表 (具体到某个人需要做的操作)
CREATE TABLE oa_task (
id bigint(20) NOT NULL AUTO_INCREMENT,
instance_id bigint(20) NOT NULL,
node_id varchar(50) NOT NULL,
task_name varchar(100) NOT NULL,
assignee_id bigint(20) DEFAULT NULL COMMENT ‘指定办理人ID (为空表示待领取或会签组)’,
candidate_ids varchar(500) DEFAULT NULL COMMENT ‘候选人群体ID集合 (逗号分隔,用于会签)’,
task_type tinyint(4) DEFAULT 1 COMMENT ‘1:串行 2:并行(会签) 3:或签’,
status tinyint(4) DEFAULT 0 COMMENT ‘0:待处理 1:已同意 2:已驳回 3:已转办’,
comment varchar(500) DEFAULT NULL COMMENT ‘审批意见’,
handle_time datetime DEFAULT NULL,
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

– 4. 审批历史记录表
CREATE TABLE oa_task_history (
id bigint(20) NOT NULL AUTO_INCREMENT,
task_id bigint(20) NOT NULL,
operator_id bigint(20) NOT NULL,
action varchar(20) NOT NULL COMMENT ‘AGREE/REJECT/TRANSFER’,
comment varchar(500),
operate_time datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

二、后端核心代码 (SpringBoot + MyBatis Plus)

实体类 (Entity) - 流程定义
@Data
@TableName(“oa_process_def”)
public class ProcessDef {
@TableId(type = IdType.AUTO)
private Long id;
private String name;
private String key;
private Integer version;
// 使用 Jackson 处理 JSON 字段,存储节点流转逻辑
@TableField(typeHandler = JacksonTypeHandler.class)
private ProcessConfig content;
private Integer status;
private Date createTime;
}

// 嵌套类:定义流程节点结构
@Data
public class ProcessConfig {
private List nodes;
private List connections;

@Data
public static class Node {
    private String id;
    private String name;
    private String type; // START, USER_TASK, END
    private String assigneeRule; // "FIXED"(指定人), "ROLE"(角色), "MANAGER"(主管)
    private List userIds; // 指定用户ID列表
    private Long roleId; 
}

}

Service 层 - 核心工作流引擎逻辑
这是实现“动态审批、加签、会签”的关键逻辑。

@Service
public class WorkflowService {

@Autowired
private ProcessInstanceMapper instanceMapper;
@Autowired
private TaskMapper taskMapper;
@Autowired
private UserMapper userMapper;

/**
 发起流程
 */
@Transactional(rollbackFor = Exception.class)
public Long startProcess(Long userId, Long processDefId, String title, JsonNode businessData) {
    // 1. 创建实例
    ProcessInstance instance = new ProcessInstance();
    instance.setProcessDefId(processDefId);
    instance.setTitle(title);
    instance.setApplicantId(userId);
    instance.setStatus(0); // 审批中
    instance.setBusinessData(businessData);
    instanceMapper.insert(instance);

    // 2. 解析流程定义,找到第一个节点
    ProcessDef def = processDefMapper.selectById(processDefId);
    ProcessConfig config = def.getContent();
    Node startNode = config.getNodes().stream()
            .filter(n -> "START".equals(n.getType())).findFirst().orElseThrow();
    
    // 获取下一个节点
    Node firstTaskNode = getNextNode(config, startNode.getId());
    
    // 3. 创建第一个任务
    createTask(instance.getId(), firstTaskNode);
    
    return instance.getId();
}

/**
 审批任务 (同意/驳回)
 */
@Transactional(rollbackFor = Exception.class)
public void approveTask(Long taskId, Long operatorId, String action, String comment) {
    Task task = taskMapper.selectById(taskId);
    if (!task.getAssigneeId().equals(operatorId)) {
        throw new RuntimeException("无权操作此任务");
    }

    // 记录历史
    TaskHistory history = new TaskHistory();
    history.setTaskId(taskId);
    history.setOperatorId(operatorId);
    history.setAction(action);
    history.setComment(comment);
    history.setOperateTime(new Date());
    taskHistoryMapper.insert(history);

    // 更新当前任务状态
    task.setStatus("AGREE".equals(action) ? 1 : 2);
    task.setHandleTime(new Date());
    task.setComment(comment);
    taskMapper.updateById(task);

    if ("REJECT".equals(action)) {
        // 驳回:结束整个实例
        ProcessInstance instance = instanceMapper.selectById(task.getInstanceId());
        instance.setStatus(2); // 驳回
        instanceMapper.updateById(instance);
        return;
    }

    // 同意:查找下一个节点并创建新任务
    ProcessInstance instance = instanceMapper.selectById(task.getInstanceId());
    ProcessDef def = processDefMapper.selectById(instance.getProcessDefId());
    Node currentNode = def.getContent().getNodes().stream()
            .filter(n -> n.getId().equals(task.getNodeId())).findFirst().orElseThrow();
    
    Node nextNode = getNextNode(def.getContent(), currentNode.getId());
    
    if (nextNode == null || "END".equals(nextNode.getType())) {
        // 流程结束
        instance.setStatus(1); // 通过
        instance.setFinishTime(new Date());
        instanceMapper.updateById(instance);
    } else {
        // 创建下一个任务
        instance.setCurrentNodeId(nextNode.getId());
        instanceMapper.updateById(instance);
        createTask(instance.getId(), nextNode);
    }
}

/**
 根据规则创建任务 (支持会签、指定人、角色)
 */
private void createTask(Long instanceId, Node node) {
    Task task = new Task();
    task.setInstanceId(instanceId);
    task.setNodeId(node.getId());
    task.setTaskName(node.getName());
    
    if ("FIXED".equals(node.getAssigneeRule())) {
        // 指定多人 -> 会签逻辑 (这里简化为创建多个任务或存入candidate_ids)
        // 实际生产中,会签通常创建一个主任务,关联多个子任务,或者利用 candidate_ids
        task.setCandidateIds(String.join(",", node.getUserIds().stream().map(String::valueOf).toArray(String[]::new)));
        task.setTaskType(2); // 并行会签
        // 注意:这里需要根据具体业务逻辑拆分任务给每个人,或者在前端展示为“待会签”
        // 简化版:直接指派给第一个人,其他人作为抄送或后续加签
        if (!node.getUserIds().isEmpty()) {
            task.setAssigneeId(node.getUserIds().get(0));
        }
    } else if ("ROLE".equals(node.getAssigneeRule())) {
        // 根据角色查找用户,逻辑略...
    }
    
    taskMapper.insert(task);
}

// 辅助方法:根据连线找下一个节点
private Node getNextNode(ProcessConfig config, String currentId) {
    Connection conn = config.getConnections().stream()
            .filter(c -> c.getSource().equals(currentId)).findFirst().orElse(null);
    if (conn == null) return null;
    return config.getNodes().stream()
            .filter(n -> n.getId().equals(conn.getTarget())).findFirst().orElse(null);
}

}

Controller 层
@RestController
@RequestMapping(“/api/workflow”)
public class WorkflowController {

@Autowired
private WorkflowService workflowService;

@PostMapping("/start")
public Result start(@RequestBody StartProcessDTO dto, HttpServletRequest request) {
    Long userId = getCurrentUserId(request); // 从JWT/Shiro获取
    Long instanceId = workflowService.startProcess(userId, dto.getProcessDefId(), dto.getTitle(), dto.getData());
    return Result.success(instanceId);
}

@PostMapping("/task/approve")
public Result approve(@RequestBody ApproveTaskDTO dto, HttpServletRequest request) {
    Long userId = getCurrentUserId(request);
    workflowService.approveTask(dto.getTaskId(), userId, dto.getAction(), dto.getComment());
    return Result.success();
}

@GetMapping("/my-tasks")
public Result> getMyTasks(HttpServletRequest request) {
    Long userId = getCurrentUserId(request);
    // 查询分配给该用户的所有待办任务
    List tasks = taskMapper.selectList(new QueryWrapper()
            .eq("assignee_id", userId)
            .eq("status", 0));
    return Result.success(tasks);
}

}

三、前端核心代码 (Vue3 + Ant Design Vue)

项目初始化
npm create vite@latest openoa-web – --template vue
cd openoa-web
npm install ant-design-vue axios pinia vue-router echarts

审批发起页面 (StartProcess.vue)

          {{ p.name }}
        
      
    
    
    
      
    

    
    
      
    

    
      提交申请

import { ref, onMounted } from ‘vue’;
import { message } from ‘ant-design-vue’;
import axios from ‘axios’;

const loading = ref(false);
const processes = ref([]);
const form = ref({
processDefId: undefined,
title: ‘’,
content: ‘’
});

onMounted(async () => {
const res = await axios.get(‘/api/workflow/definitions’);
processes.value = res.data.data;
});

const handleSubmit = async () => {
if (!form.value.processDefId) return message.error(‘请选择流程’);
loading.value = true;
try {
await axios.post(‘/api/workflow/start’, {
processDefId: form.value.processDefId,
title: form.value.title,
data: { content: form.value.content }
});
message.success(‘发起成功!’);
// 跳转列表页
} catch (e) {
message.error(e.response?.data?.message || ‘发起失败’);
} finally {
loading.value = false;
}
};

待办任务处理页面 (TaskHandle.vue)

      办理
      详情
    
  






  同意
  驳回

import { ref, onMounted } from ‘vue’;
import { message } from ‘ant-design-vue’;
import axios from ‘axios’;

const tasks = ref([]);
const visible = ref(false);
const currentTask = ref(null);
const action = ref(‘AGREE’);
const comment = ref(‘’);

const columns = [
{ title: ‘任务名称’, dataIndex: ‘taskName’ },
{ title: ‘流程标题’, dataIndex: ‘instanceTitle’ },
{ title: ‘发起人’, dataIndex: ‘applicantName’ },
{ title: ‘时间’, dataIndex: ‘createTime’ },
{ key: ‘action’, title: ‘操作’ }
];

onMounted(async () => {
const res = await axios.get(‘/api/workflow/my-tasks’);
tasks.value = res.data.data;
});

const showModal = (record) => {
currentTask.value = record;
visible.value = true;
action.value = ‘AGREE’;
comment.value = ‘’;
};

const handleApprove = async () => {
await axios.post(‘/api/workflow/task/approve’, {
taskId: currentTask.value.id,
action: action.value,
comment: comment.value
});
message.success(‘办理成功’);
visible.value = false;
// 刷新列表
const res = await axios.get(‘/api/workflow/my-tasks’);
tasks.value = res.data.data;
};

四、资源清单与部署指南

目录结构建议
openoa-system
├── openoa-backend (SpringBoot)
│ ├── src/main/java/com/openoa
│ │ ├── controller (API接口)
│ │ ├── service (业务逻辑,含工作流引擎)
│ │ ├── mapper (MyBatis Plus)
│ │ ├── entity (实体类)
│ │ └── config (Security, CORS, MyBatisConfig)
│ └── src/main/resources
│ ├── application.yml
│ └── mapper (XML文件)
├── openoa-web (Vue3)
│ ├── src/views
│ │ ├── workflow (工作流相关页面)
│ │ ├── docs (文档管理)
│ │ └── blog (博客社区)
│ ├── src/api (Axios封装)
│ └── package.json
└── docker-compose.yml (一键部署)

Docker 部署脚本 (docker-compose.yml)
version: ‘3.8’
services:
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: root123
MYSQL_DATABASE: openoa_db
ports:
“3306:3306”
volumes:
./sql/init.sql:/docker-entrypoint-initdb.d/init.sql

redis:
image: redis:alpine
ports:
“6379:6379”

backend:
build: ./openoa-backend
ports:
“8080:8080”
depends_on:
mysql
redis
environment:
SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/openoa_db
SPRING_REDIS_HOST: redis

frontend:
build: ./openoa-web
ports:
“80:80”
depends_on:
backend

功能扩展建议
文档管理/网盘:利用 SpringBoot 的 MultipartFile 接收文件,存储到本地磁盘或 MinIO/OSS,数据库只存路径和元数据。前端使用 antd-upload 组件。
博客社区:复用通用的 CRUD 架构,增加富文本编辑器(如 WangEditor 或 TinyMCE)。
数据可视化:在后台首页引入 ECharts,统计“待办任务数”、“流程平均耗时”、“部门活跃度”等。

在这里插入图片描述
SpringBoot + Vue3 + Ant Design Vue 技术栈,我将为你实现这个动态可配置的工作台首页。

核心实现思路
后端:提供一个接口,返回当前用户可见的“应用卡片”配置(名称、图标、链接、排序)。这样不同角色的用户(如普通员工 vs 财务)看到的内容可以不同。
前端:使用 Vue3 的组件化思想,将每个板块(财务管理、差旅服务等)封装成独立组件,或者直接通过数据驱动渲染网格布局。
样式:完全复刻截图中的卡片式布局、阴影效果和配色。

一、后端代码 (SpringBoot)

实体类 (DTO) - 应用卡片配置
@Data
public class AppCardDTO {
private String category; // 分类名称,如 “财务管理”
private List items; // 该分类下的应用列表

@Data
public static class AppItem {
    private String name;      // 应用名称,如 "工资单"
    private String icon;      // 图标类名或URL
    private String color;     // 图标背景色
    private String url;       // 跳转链接
    private String desc;      // 简短描述
}

}

Controller 层 - 获取工作台数据
@RestController
@RequestMapping(“/api/dashboard”)
public class DashboardController {

@GetMapping("/config")
public Result> getDashboardConfig(HttpServletRequest request) {
    Long userId = getCurrentUserId(request);
    
    // 模拟数据:实际项目中应根据用户角色从数据库查询
    List config = generateMockData();
    
    return Result.success(config);
}

private List generateMockData() {
    List list = new ArrayList();

    // 1. 财务管理
    AppCardDTO finance = new AppCardDTO();
    finance.setCategory("财务管理");
    finance.setItems(Arrays.asList(
        createItem("工资单", "icon-pay", "#1890ff", "/finance/salary", "查看个人工资信息"),
        createItem("付款申请", "icon-payment", "#1890ff", "/finance/payment", "对外付款申请流程"),
        createItem("备用金申请", "icon-reserve", "#1890ff", "/finance/reserve", "备用金申请与核销"),
        createItem("采购申请", "icon-purchase", "#1890ff", "/finance/purchase", "采购申请流程办理"),
        createItem("用章申请", "icon-seal", "#1890ff", "/finance/seal", "印章使用申请流程"),
        createItem("费用报销", "icon-reimburse", "#52c41a", "/finance/reimburse", "费用报销流程办理")
    ));
    list.add(finance);

    // 2. 差旅服务
    AppCardDTO travel = new AppCardDTO();
    travel.setCategory("差旅服务");
    travel.setItems(Arrays.asList(
        createItem("出差申请", "icon-travel", "#52c41a", "/travel/apply", "国内出差申请"),
        createItem("外出申请", "icon-out", "#52c41a", "/travel/out", "短期外出申请")
    ));
    list.add(travel);

    // 3. 沟通协作
    AppCardDTO collab = new AppCardDTO();
    collab.setCategory("沟通协作");
    collab.setItems(Arrays.asList(
        createItem("日报", "icon-day", "#fa8c16", "/collab/daily", "填写每日工作汇报"),
        createItem("周报", "icon-week", "#52c41a", "/collab/weekly", "填写每周工作总结"),
        createItem("月报", "icon-month", "#722ed1", "/collab/monthly", "填写每月工作汇报"),
        createItem="汇报", "icon-report", "#1890ff", "/collab/report", "临时汇报提交"),
        createItem("公告", "icon-notice", "#fa8c16", "/collab/notice", "最新信息发布"),
        createItem("审批", "icon-approve", "#fa8c16", "/collab/approve", "待办、已办的审批")
    ));
    list.add(collab);

    return list;
}

private AppCardDTO.AppItem createItem(String name, String icon, String color, String url, String desc) {
    AppCardDTO.AppItem item = new AppCardDTO.AppItem();
    item.setName(name);
    item.setIcon(icon);
    item.setColor(color);
    item.setUrl(url);
    item.setDesc(desc);
    return item;
}

}

二、前端代码 (Vue3 + Ant Design Vue)

这是核心部分,我们将创建一个响应式的网格布局来展示这些卡片。

主页面组件 (Dashboard.vue)

  OpenOA
  
    开发管理
    问题反馈
    流程统计
  




  
  
    shangjia.club / dashboard/workplace
    
      
      管理员
    
  

  
  
    
    
    
      
        {{ group.category }}
        
          
            
              
                
                {{ item.name.charAt(0) }}
              
              
                {{ item.name }}
                {{ item.desc }}
              
            
          
        
      

      
      
        动态
        
          
            
              
                
                  {{ item.user }} 发起了 {{ item.process }} 的流程申请
                
                
                  
                
              
            
          
        
      
    

    
    
      
      
      
        查看全部
        
           热门博客
           知名博主
           博文排行
           博客中心
        
      

      
      
        检索资料
        
           专业资料
           实用文档
           资格考试
           精品文档
           个人中心
        
      

      
      
        学习课程
        
           全部课程
           最新课程
           热门课程
           我的课程
        
      

       
       
        全部视频
        
            推荐视频

import { ref, onMounted } from ‘vue’;
import { useRouter } from ‘vue-router’;
import axios from ‘axios’;
import {
FireOutlined, UserOutlined, LineChartOutlined, StarOutlined,
BookOutlined, FileTextOutlined, EditOutlined, FolderOutlined,
VideoCameraOutlined, ClockCircleOutlined, PlayCircleOutlined
} from ‘@ant-design/icons-vue’;

const router = useRouter();
const appGroups = ref([]);
const activities = ref([
{ user: ‘陈经理’, process: ‘用章申请(非合同类)’, time: ‘2020-09-12 10:49:47’ },
{ user: ‘李主管’, process: ‘采购申请’, time: ‘2020-09-12 09:30:00’ }
]);

onMounted(async () => {
// 调用后端接口获取配置
try {
const res = await axios.get(‘/api/dashboard/config’);
appGroups.value = res.data.data;
} catch (error) {
console.error(‘加载工作台失败’, error);
}
});

const navigateTo = (url) => {
// 简单路由跳转,实际可能需要新窗口打开
router.push(url);
};

.dashboard-container {
display: flex;
height: 100vh;
background-color: #f0f2f5;
font-family: -apple-system, BlinkMacSystemFont, ‘Segoe UI’, Roboto, ‘Helvetica Neue’, Arial, sans-serif;
}

/* 侧边栏 */
.sidebar {
width: 200px;
background: #001529;
color: white;
flex-shrink: 0;
}
.logo {
height: 64px;
line-height: 64px;
text-align: center;
font-size: 20px;
font-weight: bold;
background: #002140;
}

/* 主内容区 */
.main-content {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}

.top-bar {
height: 64px;
background: white;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 24px;
box-shadow: 0 1px 4px rgba(0,21,41,.08);
z-index: 10;
}
.user-info {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
}

/* 工作台网格布局 /
.workplace-grid {
flex: 1;
padding: 24px;
overflow-y: auto;
display: grid;
grid-template-columns: 2fr 1fr; /
左侧占2份,右侧占1份 */
gap: 24px;
align-items: start;
}

/* 左侧应用区 */
.main-apps {
display: flex;
flex-direction: column;
gap: 24px;
}

.app-section {
background: white;
padding: 24px;
border-radius: 4px;
box-shadow: 0 1px 2px -2px rgba(0,0,0,.16), 0 3px 6px 0 rgba(0,0,0,.12), 0 5px 12px 4px rgba(0,0,0,.09);
}

.section-title {
margin: 0 0 16px 0;
font-size: 16px;
color: #333;
font-weight: 500;
}

.app-cards {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 16px;
}

.app-card {
border: none;
box-shadow: 0 1px 2px -2px rgba(0,0,0,.16), 0 3px 6px 0 rgba(0,0,0,.12);
transition: all 0.3s;
cursor: pointer;
}
.app-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}

.card-inner {
display: flex;
align-items: center;
gap: 12px;
}

.icon-box {
width: 40px;
height: 40px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
font-weight: bold;
}

.card-info {
flex: 1;
}
.card-name {
font-size: 15px;
font-weight: 500;
color: #333;
margin-bottom: 4px;
}
.card-desc {
font-size: 12px;
color: #999;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}

/* 右侧小部件 */
.side-widgets {
display: flex;
flex-direction: column;
gap: 24px;
}

.widget-card {
box-shadow: 0 1px 2px -2px rgba(0,0,0,.16), 0 3px 6px 0 rgba(0,0,0,.12);
}

.widget-list {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}

.widget-item {
display: flex;
align-items: center;
gap: 6px;
font-size: 13px;
color: #666;
cursor: pointer;
padding: 4px 0;
}
.widget-item:hover {
color: #1890ff;
}

/* 响应式调整 /
@media (max-width: 1200px) {
.workplace-grid {
grid-template-columns: 1fr; /
小屏幕下单列显示 */
}
}

三、关键点解析

动态配置:
后端 generateMockData 方法模拟了从数据库读取配置的过程。你可以轻松地在数据库中增加一张 app_config 表,存储这些 JSON 数据,从而实现管理员在后台拖拽配置首页功能。
布局技巧:
使用了 CSS Grid (grid-template-columns: 2fr 1fr) 完美还原了截图中“左宽右窄”的布局。
repeat(auto-fill, minmax(200px, 1fr)) 让应用卡片在不同屏幕宽度下自动换行排列,无需媒体查询即可实现响应式。
视觉还原:
使用了 Ant Design 的 Card 组件和自定义阴影 (box-shadow),还原了截图中的悬浮感和层次感。
图标颜色采用了内联样式动态绑定,方便为不同模块设置不同的主题色。
扩展性:
“动态”列表可以直接对接后端的消息通知接口。
右侧的“博客”、“文库”等模块,点击后可直接跳转到对应的子系统设计页面。

在这里插入图片描述
SpringBoot + Vue3 + Ant Design Vue 来实现这个功能。核心思路是:
后端提供一个接口,返回分页的、按时间排序的动态消息列表。
前端使用 a-list 组件来渲染这个列表,并处理滚动加载(如果需要)。

一、后端代码 (SpringBoot)

实体类 (Entity & DTO)

// Activity.java - 数据库实体
@Data
@TableName(“sys_activity_log”)
public class ActivityLog {
@TableId(type = IdType.AUTO)
private Long id;
private Long userId; // 操作用户ID
private String username; // 用户名 (冗余字段,方便查询)
private String avatar; // 用户头像
private String actionType; // 动作类型: START_PROCESS, APPROVE_TASK, POST_ARTICLE, etc.
private String actionDesc; // 动作描述模板: “发起了 {processName} 的流程申请”
private String targetId; // 关联的目标ID (如流程实例ID、文章ID)
private String targetType; // 目标类型: PROCESS, ARTICLE, etc.
private Date createTime;
}

// ActivityDTO.java - 返回给前端的DTO
@Data
public class ActivityDTO {
private Long id;
private String username;
private String avatar;
private String content; // 最终拼接好的完整内容,如 “陈经理 发起了 用章申请 的流程申请”
private Date time;

// 用于前端判断是否显示“查看详情”等按钮
private String linkUrl;       

}

Mapper 层

@Mapper
public interface ActivityLogMapper extends BaseMapper {
// 可以使用 MyBatis Plus 的分页插件进行分页查询
}

Service 层

@Service
public class ActivityService {

@Autowired
private ActivityLogMapper activityLogMapper;

/**
 获取动态消息列表
 */
public IPage getActivityList(Page page) {
    // 1. 分页查询数据库
    IPage logPage = activityLogMapper.selectPage(page, 
        new QueryWrapper().orderByDesc("create_time"));

    // 2. 转换为 DTO,并拼接完整的消息内容
    List dtoList = logPage.getRecords().stream().map(log -> {
        ActivityDTO dto = new ActivityDTO();
        dto.setId(log.getId());
        dto.setUsername(log.getUsername());
        dto.setAvatar(log.getAvatar());
        dto.setTime(log.getCreateTime());
        
        // 根据动作类型和目标信息,拼接最终显示的文字
        dto.setContent(formatContent(log));
        
        // 生成跳转链接
        dto.setLinkUrl(generateLink(log));
        
        return dto;
    }).collect(Collectors.toList());

    // 3. 构建返回的分页对象
    Page dtoPage = new Page(logPage.getCurrent(), logPage.getSize(), logPage.getTotal());
    dtoPage.setRecords(dtoList);
    return dtoPage;
}

// 辅助方法:格式化内容
private String formatContent(ActivityLog log) {
    // 这里可以根据 actionType 做更复杂的逻辑,比如从数据库查流程名称
    // 简化版:直接使用预设的描述
    return log.getActionDesc(); 
}

// 辅助方法:生成链接
private String generateLink(ActivityLog log) {
    if ("PROCESS".equals(log.getTargetType())) {
        return "/workflow/detail/" + log.getTargetId();
    } else if ("ARTICLE".equals(log.getTargetType())) {
        return "/blog/detail/" + log.getTargetId();
    }
    return null;
}

}

Controller 层

@RestController
@RequestMapping(“/api/activity”)
public class ActivityController {

@Autowired
private ActivityService activityService;

@GetMapping("/list")
public Result> getList(
        @RequestParam(defaultValue = "1") long current,
        @RequestParam(defaultValue = "10") long size) {
    
    Page page = new Page(current, size);
    IPage result = activityService.getActivityList(page);
    
    return Result.success(result);
}

}

二、前端代码 (Vue3 + Ant Design Vue)

动态


  
    
      
        
          
          
            {{ item.content }}
          
          {{ item.content }}
        
        
          
        
      
      
      
      
         0
         0
       
      -->
    
  
  
  
  
    
  




  加载更多

import { ref, onMounted } from ‘vue’;
import { useRouter } from ‘vue-router’;
import axios from ‘axios’;
import dayjs from ‘dayjs’;
import relativeTime from ‘dayjs/plugin/relativeTime’;
import ‘dayjs/locale/zh-cn’;

// 初始化 dayjs 插件,用于显示 “刚刚”, “5分钟前”
dayjs.extend(relativeTime);
dayjs.locale(‘zh-cn’);

const router = useRouter();
const activities = ref([]);
const loading = ref(false);
const loadingMore = ref(false);
const hasMore = ref(true);
const currentPage = ref(1);
const pageSize = 10;

onMounted(() => {
loadActivities();
});

const loadActivities = async () => {
if (loading.value || (!loadingMore.value && activities.value.length > 0)) return;

loading.value = true;
try {
const res = await axios.get(‘/api/activity/list’, {
params: {
current: currentPage.value,
size: pageSize
}
});

const data = res.data.data;

if (currentPage.value === 1) {
  activities.value = data.records;
} else {
  activities.value = [...activities.value, ...data.records];
}

// 判断是否还有更多数据
hasMore.value = activities.value.length  {

loadingMore.value = true;
loadActivities();
};

const formatTime = (time) => {
// 使用 dayjs 显示相对时间,例如 “2小时前”
return dayjs(time).fromNow();
// 或者显示绝对时间: return dayjs(time).format(‘YYYY-MM-DD HH:mm:ss’);
};

const navigateTo = (url) => {
router.push(url);
};

.activity-feed {
background: white;
padding: 24px;
border-radius: 4px;
box-shadow: 0 1px 2px -2px rgba(0,0,0,.16), 0 3px 6px 0 rgba(0,0,0,.12);
}

.section-title {
margin: 0 0 16px 0;
font-size: 16px;
color: #333;
font-weight: 500;
}

/* 自定义列表项样式,使其更接近截图 */
:deep(.ant-list-item) {
padding-top: 16px;
padding-bottom: 16px;
border-bottom-color: #f0f0f0;
}
:deep(.ant-list-item:last-child) {
border-bottom: none;
}
:deep(.ant-list-item-meta-title) {
margin-bottom: 4px;
color: #333;
font-size: 14px;
line-height: 1.5;
}
:deep(.ant-list-item-meta-description) {
color: #999;
font-size: 12px;
}
:deep(.ant-list-item-meta-avatar) {
margin-right: 12px;
}

.load-more {
text-align: center;
margin-top: 16px;
}

三、如何集成到你的项目中

创建数据库表:执行第一部分的 SQL 语句创建 sys_activity_log 表。
埋点记录日志:在你的业务代码中,每当有重要操作发生时(如提交审批、发布文章),就向 sys_activity_log 表中插入一条记录。
// 示例:在审批服务中
@Autowired
private ActivityLogMapper activityLogMapper;

public void approveTask(...) {
    // ... 审批逻辑 ...
    
    // 记录动态
    ActivityLog log = new ActivityLog();
    log.setUserId(currentUser.getId());
    log.setUsername(currentUser.getName());
    log.setAvatar(currentUser.getAvatar());
    log.setActionType("APPROVE_TASK");
    log.setActionDesc(currentUser.getName() + " 同意了 " + task.getInstanceTitle() + " 的申请");
    log.setTargetId(instanceId);
    log.setTargetType("PROCESS");
    log.setCreateTime(new Date());
    activityLogMapper.insert(log);
}
![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/ead58475830a4a81ba6db5ce459d8b25.png)
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐