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

代码包含了你描述中最核心、最复杂的动态工作流引擎(审批流)的后端实现逻辑,以及前后端的基础框架搭建指南。你可以基于此骨架扩展文档、博客和网盘功能。
项目名称: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);
}

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



所有评论(0)