第三步:前端实现(审批消息展示与操作)
基于 Vue + Element UI 实现审批人页面的「消息通知」和「待审批列表」,集成到你的现有排班系统中。
1. 全局消息通知(顶部导航栏)
在系统顶部导航栏添加「消息图标」,显示未读消息数
<template> <div class="header-notify"> <!-- 消息图标 + 未读红点 --> <el-dropdown @command="handleNotifyCommand" placement="bottom-right"> <div class="notify-icon"> <el-icon size="20"><Bell /></el-icon> <span v-if="unreadCount > 0" class="unread-dot">{{ unreadCount }}</span> </div> <el-dropdown-menu slot="dropdown" class="notify-dropdown"> <el-dropdown-item disabled class="dropdown-header">待审批({{ unreadCount }})</el-dropdown-item> <el-dropdown-item v-for="todo in todoList" :key="todo.applyId" :command="{ type: 'todo', data: todo }" class="todo-item" > <div class="todo-info"> <p class="todo-title">{{ todo.businessInfo }}</p> <p class="todo-time">{{ formatTime(todo.applyTime) }}</p> </div> <el-button size="mini" type="text" @click.stop="handleGotoApproval(todo)">处理</el-button> </el-dropdown-item> <el-dropdown-item disabled v-if="todoList.length === 0">暂无待审批事项</el-dropdown-item> </el-dropdown-menu> </el-dropdown> </div> </template> <script> import { Bell } from '@element-plus/icons-vue'; import { getApproverTodoList, getUnreadNotifyCount } from '@/api/hrm/approval'; export default { components: { Bell }, data() { return { unreadCount: 0, todoList: [], timer: null // 定时器:定时刷新未读消息 }; }, mounted() { // 初始化加载待审批列表和未读消息数 this.loadTodoList(); this.loadUnreadCount(); // 定时刷新(5分钟一次,可根据需求调整) this.timer = setInterval(() => { this.loadUnreadCount(); this.loadTodoList(); }, 300000); }, beforeUnmount() { clearInterval(this.timer); // 清除定时器 }, methods: { // 加载待审批列表 async loadTodoList() { const userId = this.$store.state.user.id; // 从全局状态获取当前用户ID const res = await getApproverTodoList(userId); this.todoList = res.data; }, // 加载未读消息数 async loadUnreadCount() { const userId = this.$store.state.user.id; const res = await getUnreadNotifyCount(userId); this.unreadCount = res.data; }, // 格式化时间 formatTime(time) { return new Date(time).toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }); }, // 处理消息命令(如点击“处理”按钮) handleNotifyCommand(command) { if (command.type === 'todo') { this.handleGotoApproval(command.data); } }, // 跳转到审批详情页 handleGotoApproval(todo) { this.$router.push({ path: '/hrm/approval/detail', query: { applyId: todo.applyId, nodeId: todo.nodeId } }); } } }; </script> <style scoped> .header-notify { position: relative; margin-right: 20px; cursor: pointer; } .unread-dot { position: absolute; top: -5px; right: -5px; width: 18px; height: 18px; line-height: 18px; border-radius: 50%; background-color: #f56c6c; color: white; font-size: 12px; text-align: center; } .notify-dropdown { width: 400px; max-height: 500px; overflow-y: auto; } .todo-item { padding: 12px; border-bottom: 1px solid #f5f5f5; } .todo-title { font-size: 14px; color: #333; margin-bottom: 4px; } .todo-time { font-size: 12px; color: #999; } </style>
审批人点击「处理」后进入详情页,查看申请信息并执行同意 / 拒绝操作:
<template> <el-card class="approval-detail-card"> <div slot="header" class="card-header"> <h2>审批详情</h2> <span class="apply-no">审批单号:{{ applyDetail.applyNo }}</span> </div> <!-- 申请基本信息 --> <el-form :model="applyDetail" label-width="120px" class="apply-form"> <el-form-item label="审批类型"> <el-tag type="info">{{ applyTypeMap[applyDetail.applyType] }}</el-tag> </el-form-item> <el-form-item label="申请人"> {{ applyDetail.applyUserName }}({{ applyDetail.applyTime | formatTime }}) </el-form-item> <el-form-item label="申请内容"> <div class="business-info">{{ applyDetail.businessInfo }}</div> </el-form-item> <el-form-item label="申请备注"> <div class="apply-remark">{{ applyDetail.remark || '无' }}</div> </el-form-item> <el-form-item label="审批节点"> <div class="approval-node-list"> <div v-for="(node, index) in approvalNodeList" :key="node.id" class="approval-node" > <div class="node-header"> <span class="node-seq">第{{ node.nodeSeq }}审批人</span> <el-tag :type="getNodeStatusTagType(node.nodeStatus)"> {{ getNodeStatusText(node.nodeStatus) }} </el-tag> </div> <div class="node-content"> <p>审批人:{{ node.approverName }}</p> <p>审批时间:{{ node.approveTime ? (node.approveTime | formatTime) : '未处理' }}</p> <p>审批意见:{{ node.approveOpinion || '无' }}</p> </div> <!-- 当前用户的待处理节点:显示审批操作 --> <div v-if="node.nodeStatus === 0 && node.approverId === currentUserId" class="node-operation"> <el-form :model="handleForm" ref="handleFormRef" label-width="80px"> <el-form-item label="审批结果" prop="approveResult" required> <el-radio-group v-model="handleForm.approveResult"> <el-radio label="1">同意</el-radio> <el-radio label="2">拒绝</el-radio> </el-radio-group> </el-form-item> <el-form-item label="审批意见" prop="opinion"> <el-input v-model="handleForm.opinion" type="textarea" rows="3" placeholder="请输入审批意见(可选)" ></el-input> </el-form-item> <el-form-item> <el-button @click="handleCancel">取消</el-button> <el-button type="primary" @click="handleSubmitApproval">提交审批</el-button> </el-form-item> </el-form> </div> </div> </div> </el-form-item> </el-form> </el-card> </template> <script> import { getApprovalDetail, handleApproval } from '@/api/hrm/approval'; export default { filters: { formatTime(time) { return new Date(time).toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }); } }, data() { return { applyDetail: {}, // 审批申请详情 approvalNodeList: [], // 审批节点列表 currentUserId: this.$store.state.user.id, // 当前用户ID applyTypeMap: { // 审批类型映射 'SHIFT_SWAP': '调班申请' }, // 审批操作表单 handleForm: { nodeId: '', // 当前处理的节点ID approveResult: '1', // 默认同意 opinion: '' } }; }, mounted() { // 从路由参数获取 applyId 和 nodeId const { applyId, nodeId } = this.$route.query; this.handleForm.nodeId = nodeId; this.loadApprovalDetail(applyId); }, methods: { // 加载审批详情 async loadApprovalDetail(applyId) { const res = await getApprovalDetail(applyId); this.applyDetail = res.data.apply; this.approvalNodeList = res.data.nodes; }, // 获取节点状态文本 getNodeStatusText(status) { const statusMap = { 0: '待审批', 1: '已同意', 2: '已拒绝' }; return statusMap[status] || '未知'; }, // 获取节点状态标签类型 getNodeStatusTagType(status) { const typeMap = { 0: 'warning', 1: 'success', 2: 'danger' }; return typeMap[status] || 'info'; }, // 提交审批 async handleSubmitApproval() { this.$refs.handleFormRef.validate(async (valid) => { if (valid) { // 转换 approveResult 为数字(1=同意,2=拒绝) this.handleForm.approveResult = Number(this.handleForm.approveResult); await handleApproval(this.handleForm); this.$message.success('审批操作已提交'); this.$router.push('/hrm/approval/todo-list'); // 返回待审批列表 } }); }, // 取消 handleCancel() { this.$router.go(-1); } } }; </script> <style scoped> .approval-detail-card { margin: 20px; } .card-header { display: flex; justify-content: space-between; align-items: center; } .apply-no { font-size: 14px; color: #666; } .apply-form { margin-top: 20px; } .business-info, .apply-remark { padding: 8px; background-color: #f5f7fa; border-radius: 4px; color: #333; } .approval-node-list { margin-top: 10px; } .approval-node { padding: 15px; border: 1px solid #f5f5f5; border-radius: 4px; margin-bottom: 10px; } .node-header { display: flex; justify-content: space-between; margin-bottom: 10px; } .node-seq { font-weight: 500; color: #333; } .node-operation { margin-top: 15px; padding-top: 15px; border-top: 1px dashed #eee; } </style>
为了让审批人实时收到新的审批消息(无需刷新页面),可集成 WebSocket 实现实时推送:
- 后端:基于 Spring WebSocket 实现用户消息订阅(按用户 ID 分组)。
- 前端:页面加载时建立 WebSocket 连接,监听审批消息事件,收到消息后更新未读计数和待审批列表。
示例(前端 WebSocket 封装):
// src/utils/websocket.js let websocket = null; export function initWebSocket(userId) { if ('WebSocket' in window) { // 连接地址(如 ws://localhost:8080/ws/approval?userId=123) const wsUrl = `${process.env.VUE_APP_WS_BASE_URL}/ws/approval?userId=${userId}`; websocket = new WebSocket(wsUrl); // 连接成功 websocket.onopen = function() { console.log('WebSocket 连接成功'); }; // 接收消息 websocket.onmessage = function(event) { const message = JSON.parse(event.data); // 触发全局事件,让其他组件监听 window.dispatchEvent(new CustomEvent('approvalNotify', { detail: message })); }; // 连接关闭 websocket.onclose = function() { console.log('WebSocket 连接关闭,3秒后重连'); setTimeout(() => initWebSocket(userId), 3000); // 自动重连 }; // 连接错误 websocket.onerror = function() { console.error('WebSocket 连接错误'); }; } else { alert('您的浏览器不支持 WebSocket,无法接收实时消息'); } } // 关闭连接 export function closeWebSocket() { if (websocket) { websocket.close(); } }
在全局入口(如 main.js
或 App.vue
)初始化 WebSocket:
import { initWebSocket, closeWebSocket } from '@/utils/websocket'; // 登录后初始化(从全局状态获取用户ID) if (store.state.user.id) { initWebSocket(store.state.user.id); } // 监听全局消息事件,更新未读消息 window.addEventListener('approvalNotify', (event) => { const message = event.detail; if (message.type === 'APPROVAL_TODO') { // 刷新待审批列表和未读计数 const notifyComponent = window.notifyComponent; // 假设消息组件暴露到全局 if (notifyComponent) { notifyComponent.loadTodoList(); notifyComponent.loadUnreadCount(); } // 显示消息提示 ElMessage({ title: '新的审批通知', message: message.content, type: 'info', duration: 5000 }); } }); // 页面关闭时关闭连接 window.addEventListener('beforeunload', closeWebSocket);