feat(base): 添加工艺预警功能和设备参数监控页面优化

- 新增工艺预警路由配置和权限控制
- 实现预警列表页面,支持检查预警、批量标记已处理和导出功能
- 添加表格多选功能和批量操作按钮
- 在设备参数监控页面集成实时预警通知功能
- 实现预警面板显示未处理预警信息
- 添加定时检查预警机制和预警状态管理
- 优化安灯事件页面的列显示配置
- 简化安灯派工记录对话框的表单字段
- 更新工位选择下拉框的显示格式
- 添加批量标记预警为已处理的API接口
master
zangch@mesnac.com 2 months ago
parent 883900528a
commit 9f56de3c84

@ -59,3 +59,12 @@ export function checkThresholdAlert() {
method: 'post'
})
}
// 批量标记预警为已处理
export function batchMarkAsProcessed(data) {
return request({
url: '/base/processAlert/batchMarkProcessed',
method: 'put',
data: data
})
}

@ -521,6 +521,20 @@ export const dynamicRoutes = [
},
],
},
{
path: "/base/processAlert",
component: Layout,
hidden: true,
permissions: ["base:processAlert:list"],
children: [
{
path: "index",
component: () => import("@/views/base/processAlert/index"),
name: "ProcessAlert",
meta: {title: "工艺预警", activeMenu: "/base/processAlert"},
},
],
},
]
// 防止连续点击多次路由报错

@ -34,13 +34,17 @@
<el-col :span="1.5">
<el-button type="primary" plain icon="el-icon-refresh" size="mini" @click="handleCheckThreshold" v-hasPermi="['base:processAlert:add']"></el-button>
</el-col>
<el-col :span="1.5">
<el-button type="success" plain icon="el-icon-check" size="mini" :disabled="multiple" @click="handleBatchMarkProcessed" v-hasPermi="['base:processAlert:edit']"></el-button>
</el-col>
<el-col :span="1.5">
<el-button type="warning" plain icon="el-icon-download" size="mini" @click="handleExport" v-hasPermi="['base:processAlert:export']"></el-button>
</el-col>
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
<el-table v-loading="loading" :data="alertList" :row-class-name="tableRowClassName">
<el-table v-loading="loading" :data="alertList" :row-class-name="tableRowClassName" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="预警编号" align="center" prop="alertCode" width="120"/>
<el-table-column label="预警类型" align="center" prop="alertType" width="100"/>
<el-table-column label="预警级别" align="center" prop="alertLevel" width="80">
@ -124,7 +128,7 @@
</template>
<script>
import { listProcessAlert, getProcessAlert, handleProcessAlert, checkThresholdAlert } from "@/api/base/processAlert";
import { listProcessAlert, getProcessAlert, handleProcessAlert, checkThresholdAlert, batchMarkAsProcessed } from "@/api/base/processAlert";
export default {
name: "ProcessAlert",
@ -134,6 +138,8 @@ export default {
showSearch: true,
total: 0,
alertList: [],
ids: [],
multiple: true,
dateRange: [],
processOpen: false,
viewOpen: false,
@ -178,6 +184,10 @@ export default {
this.resetForm("queryForm");
this.handleQuery();
},
handleSelectionChange(selection) {
this.ids = selection.map(item => item.alertId);
this.multiple = !selection.length;
},
tableRowClassName({ row }) {
if (row.alertStatus === '0' && row.alertLevel === '3') {
return 'danger-row';
@ -221,6 +231,18 @@ export default {
this.getList();
});
}).catch(() => {});
},
handleBatchMarkProcessed() {
if (this.ids.length === 0) {
this.$modal.msgWarning("请选择要处理的预警");
return;
}
this.$modal.confirm('确认将选中的 ' + this.ids.length + ' 条预警标记为已处理?').then(() => {
batchMarkAsProcessed(this.ids).then(response => {
this.$modal.msgSuccess(response.msg);
this.getList();
});
}).catch(() => {});
}
}
};

@ -1,10 +1,50 @@
<template>
<div class="app-container">
<!-- 预警通知弹窗 -->
<el-notification
v-for="alert in alertNotifications"
:key="alert.alertId"
:title="getAlertTitle(alert)"
:message="alert.alertContent"
:type="getAlertType(alert)"
:duration="0"
position="top-right"
@close="removeAlertNotification(alert.alertId)"
style="margin-bottom: 10px;"
>
<div slot="default" class="alert-notification-content">
<div class="alert-item">
<span class="alert-label">设备:</span>
<span class="alert-value">{{ alert.deviceName }} ({{ alert.deviceCode }})</span>
</div>
<div class="alert-item">
<span class="alert-label">参数:</span>
<span class="alert-value">{{ alert.paramName }}</span>
</div>
<div class="alert-item">
<span class="alert-label">当前值:</span>
<span class="alert-value alert-value-danger">{{ alert.alertValue }}</span>
</div>
<div class="alert-item">
<span class="alert-label">阈值:</span>
<span class="alert-value">{{ alert.thresholdValue }}</span>
</div>
<div class="alert-actions">
<el-button size="mini" type="primary" @click="viewAlertDetail(alert)"></el-button>
<el-button size="mini" @click="removeAlertNotification(alert.alertId)"></el-button>
</div>
</div>
</el-notification>
<!-- 页面头部 -->
<div class="page-header">
<div class="page-title">设备工艺参数监控</div>
<div class="page-actions">
<span class="page-time">{{ nowTimeStr }}</span>
<!-- 预警提醒图标 -->
<el-badge :value="unreadAlertCount" :hidden="unreadAlertCount === 0" class="alert-badge">
<el-button type="danger" size="mini" icon="el-icon-warning" circle @click="showAlertPanel = !showAlertPanel" />
</el-badge>
<el-switch
v-model="autoRefresh"
active-text="自动刷新开"
@ -17,6 +57,54 @@
</div>
</div>
<!-- 预警面板可折叠 -->
<div v-if="showAlertPanel" class="alert-panel">
<div class="alert-panel-header">
<span class="alert-panel-title">
<i class="el-icon-warning-outline"></i>
实时预警 ({{ unreadAlertCount }} 条未处理)
</span>
<div class="alert-panel-actions">
<el-button v-if="alertList.length > 0" type="text" size="mini" icon="el-icon-check" @click="handleMarkAllProcessed"></el-button>
<el-button type="text" size="mini" icon="el-icon-close" @click="showAlertPanel = false" />
</div>
</div>
<div class="alert-list" v-loading="alertLoading">
<div v-if="alertList.length === 0" class="no-alert">
<i class="el-icon-success"></i>
<span>暂无预警信息</span>
</div>
<div
v-for="alert in alertList"
:key="alert.alertId"
:class="['alert-item', 'alert-level-' + alert.alertLevel]"
>
<div class="alert-item-header">
<el-tag :type="getAlertTagType(alert)" size="mini">{{ getAlertLevelText(alert) }}</el-tag>
<span class="alert-time">{{ parseTime(alert.alertTime, '{h}:{i}:{s}') }}</span>
</div>
<div class="alert-item-content">
<div class="alert-content-row">
<span class="alert-label">设备:</span>
<span class="alert-value">{{ alert.deviceName }} ({{ alert.deviceCode }})</span>
</div>
<div class="alert-content-row">
<span class="alert-label">参数:</span>
<span class="alert-value">{{ alert.paramName }}</span>
</div>
<div class="alert-content-row">
<span class="alert-label">内容:</span>
<span class="alert-value">{{ alert.alertContent }}</span>
</div>
</div>
<div class="alert-item-actions">
<el-button size="mini" type="text" @click="viewAlertDetail(alert)"></el-button>
<el-button size="mini" type="text" @click="markAsRead(alert)"></el-button>
</div>
</div>
</div>
</div>
<!-- 主体内容左侧设备列表 + 右侧参数展示 -->
<div class="main-content">
<!-- 左侧设备列表 -->
@ -112,6 +200,7 @@
<script>
import { getLatestVal } from "@/api/baseDeviceParamVal/val";
import { getDeviceLedgerList } from "@/api/base/deviceLedger";
import { listProcessAlert, handleProcessAlert, checkThresholdAlert, batchMarkAsProcessed } from "@/api/base/processAlert";
export default {
name: "Val",
@ -134,6 +223,14 @@ export default {
deviceParams: [],
paramLoading: false,
paramSearchKey: '',
//
alertList: [],
alertNotifications: [],
alertLoading: false,
showAlertPanel: false,
alertTimer: null,
alertCheckIntervalMs: 30000, // 30
lastAlertIds: [], // ID
};
},
computed: {
@ -166,16 +263,23 @@ export default {
if (this.deviceParams.length === 0) return '-';
const t = this.deviceParams[0].recordTime || this.deviceParams[0].collectTime;
return t ? this.parseTime(t, '{y}-{m}-{d} {h}:{i}:{s}') : '-';
},
//
unreadAlertCount() {
return this.alertList.filter(alert => alert.alertStatus === '0').length;
}
},
created() {
this.tickClock();
this.loadDeviceList();
this.loadAllLatestParams(); //
this.loadAlerts(); //
if (this.autoRefresh) this.startAutoRefresh();
this.startAlertCheck(); //
},
beforeDestroy() {
this.stopAutoRefresh();
this.stopAlertCheck();
if (this.clockTimer) {
clearInterval(this.clockTimer);
this.clockTimer = null;
@ -211,12 +315,28 @@ export default {
this.refreshTimer = null;
}
},
/** 开始定时检查预警 */
startAlertCheck() {
this.stopAlertCheck();
this.alertTimer = setInterval(() => {
this.checkAndLoadAlerts();
}, this.alertCheckIntervalMs);
},
/** 停止定时检查预警 */
stopAlertCheck() {
if (this.alertTimer) {
clearInterval(this.alertTimer);
this.alertTimer = null;
}
},
/** 自动刷新切换 */
onAutoRefreshChange(val) {
if (val) {
this.startAutoRefresh();
this.startAlertCheck();
} else {
this.stopAutoRefresh();
this.stopAlertCheck();
}
},
/** 手动刷新 */
@ -274,6 +394,109 @@ export default {
this.paramLoading = false;
this.deviceParams = [];
});
},
/** 加载预警列表 */
loadAlerts() {
this.alertLoading = true;
listProcessAlert({
pageNum: 1,
pageSize: 20,
alertStatus: '0' //
}).then(response => {
this.alertList = response.rows || [];
this.alertLoading = false;
}).catch(() => {
this.alertLoading = false;
this.alertList = [];
});
},
/** 检查并加载预警(定时调用) */
checkAndLoadAlerts() {
//
checkThresholdAlert().then(() => {
//
const currentAlertIds = this.alertList.map(a => a.alertId);
this.loadAlerts().then(() => {
//
const newAlerts = this.alertList.filter(alert => !currentAlertIds.includes(alert.alertId));
if (newAlerts.length > 0) {
//
newAlerts.forEach(alert => {
this.showAlertNotification(alert);
});
}
});
}).catch(() => {
// 使
this.loadAlerts();
});
},
/** 显示预警通知 */
showAlertNotification(alert) {
//
if (this.alertNotifications.find(n => n.alertId === alert.alertId)) {
return;
}
this.alertNotifications.push(alert);
},
/** 移除预警通知 */
removeAlertNotification(alertId) {
const index = this.alertNotifications.findIndex(n => n.alertId === alertId);
if (index > -1) {
this.alertNotifications.splice(index, 1);
}
},
/** 获取预警标题 */
getAlertTitle(alert) {
return alert.alertLevel === '3' ? '【紧急预警】' : alert.alertLevel === '2' ? '【重要预警】' : '【一般预警】';
},
/** 获取预警类型 */
getAlertType(alert) {
return alert.alertLevel === '3' ? 'error' : alert.alertLevel === '2' ? 'warning' : 'info';
},
/** 获取预警标签类型 */
getAlertTagType(alert) {
return alert.alertLevel === '3' ? 'danger' : alert.alertLevel === '2' ? 'warning' : 'info';
},
/** 获取预警级别文本 */
getAlertLevelText(alert) {
return alert.alertLevel === '3' ? '紧急' : alert.alertLevel === '2' ? '重要' : '一般';
},
/** 查看预警详情 */
viewAlertDetail(alert) {
this.$router.push({
path: '/base/processAlert',
query: { alertId: alert.alertId }
});
},
/** 标记为已读 */
markAsRead(alert) {
handleProcessAlert({
alertId: alert.alertId,
alertStatus: '1',
handleResult: '已查看'
}).then(() => {
this.$message.success('标记成功');
this.loadAlerts();
});
},
/** 全部标记为已处理 */
handleMarkAllProcessed() {
const alertIds = this.alertList.map(alert => alert.alertId);
if (alertIds.length === 0) {
this.$message.warning('暂无预警需要处理');
return;
}
this.$confirm('确认将全部 ' + alertIds.length + ' 条预警标记为已处理?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
batchMarkAsProcessed(alertIds).then(() => {
this.$message.success('标记成功');
this.loadAlerts();
});
}).catch(() => {});
}
}
};
@ -305,6 +528,142 @@ export default {
opacity: 0.9;
}
/* 预警徽章 */
.alert-badge {
margin-right: 4px;
}
/* 预警面板 */
.alert-panel {
background: #fff;
border-radius: 6px;
border: 1px solid #e8e8e8;
margin-bottom: 12px;
max-height: 400px;
overflow: hidden;
}
.alert-panel-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 16px;
border-bottom: 1px solid #f0f0f0;
background: #fafafa;
}
.alert-panel-title {
font-size: 14px;
font-weight: 600;
color: #333;
display: flex;
align-items: center;
gap: 6px;
}
.alert-panel-title i {
color: #f56c6c;
}
.alert-panel-actions {
display: flex;
align-items: center;
gap: 8px;
}
.alert-list {
max-height: 340px;
overflow-y: auto;
padding: 8px;
}
.no-alert {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px 20px;
color: #67c23a;
}
.no-alert i {
font-size: 36px;
margin-bottom: 8px;
}
.alert-item {
background: #fff;
border: 1px solid #e8e8e8;
border-radius: 6px;
padding: 10px;
margin-bottom: 8px;
border-left: 4px solid #909399;
}
.alert-item.alert-level-1 {
border-left-color: #909399;
background: #f5f7fa;
}
.alert-item.alert-level-2 {
border-left-color: #e6a23c;
background: #fdf6ec;
}
.alert-item.alert-level-3 {
border-left-color: #f56c6c;
background: #fef0f0;
}
.alert-item-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.alert-time {
font-size: 12px;
color: #999;
}
.alert-item-content {
margin-bottom: 8px;
}
.alert-content-row {
display: flex;
align-items: center;
margin-bottom: 4px;
font-size: 13px;
}
.alert-label {
color: #999;
min-width: 40px;
}
.alert-value {
color: #333;
flex: 1;
}
.alert-item-actions {
display: flex;
justify-content: flex-end;
gap: 8px;
}
/* 预警通知内容 */
.alert-notification-content {
font-size: 13px;
}
.alert-notification-content .alert-item {
display: flex;
align-items: center;
margin-bottom: 6px;
}
.alert-notification-content .alert-label {
color: #999;
min-width: 50px;
}
.alert-notification-content .alert-value {
color: #333;
flex: 1;
}
.alert-notification-content .alert-value-danger {
color: #f56c6c;
font-weight: 600;
}
.alert-notification-content .alert-actions {
margin-top: 10px;
display: flex;
justify-content: flex-end;
gap: 8px;
}
/* 主体布局 */
.main-content {
display: flex;

@ -652,11 +652,11 @@ export default {
{ key: 10, label: `设备ID`, visible: false },
{ key: 11, label: `设备编码`, visible: false },
{ key: 12, label: `优先级`, visible: true },
{ key: 13, label: `事件状态`, visible: true },
{ key: 13, label: `事件状态`, visible: false },
{ key: 14, label: `呼叫描述`, visible: false },
{ key: 15, label: `确认人`, visible: true },
{ key: 16, label: `确认时间`, visible: true },
{ key: 17, label: `开始处理时间`, visible: true },
{ key: 17, label: `开始处理时间`, visible: false },
{ key: 18, label: `处理完成/解决时间`, visible: true },
{ key: 19, label: `处理/解决措施`, visible: true },
{ key: 20, label: `取消原因`, visible: false },
@ -664,7 +664,7 @@ export default {
{ key: 22, label: `升级时间`, visible: false },
{ key: 23, label: `确认截止时间`, visible: false },
{ key: 24, label: `解决截止时间`, visible: false },
{ key: 25, label: `是否有效`, visible: true },
{ key: 25, label: `是否有效`, visible: false },
{ key: 26, label: `备注`, visible: true },
{ key: 27, label: `创建人`, visible: false },
{ key: 28, label: `创建时间`, visible: false },

@ -214,54 +214,26 @@
@pagination="getList"
/>
<!-- 添加或修改安灯派工记录派工即通知/待办对话框 -->
<!-- 添加或修改安灯派工记录对话框只允许修改状态和备注 -->
<el-dialog :title="title" :visible.sync="open" width="500px" append-to-body>
<el-form ref="form" :model="form" :rules="rules" label-width="80px">
<!-- 事件ID改为下拉选择不分页 -->
<el-form-item label="事件ID" prop="eventId">
<el-select v-model="form.eventId" placeholder="请选择事件">
<el-option
v-for="evt in eventOptions"
:key="evt.eventId"
:label="(evt.callCode || ('事件#' + evt.eventId)) + (evt.stationCode ? (' - ' + evt.stationCode) : '')"
:value="evt.eventId"
/>
</el-select>
<el-form-item label="呼叫单号" prop="callCode">
<span class="form-value">{{ getCallCodeByEventId(form.eventId) }}</span>
</el-form-item>
<el-form-item label="被分配用户" prop="assigneeUserId">
<el-select v-model="form.assigneeUserId" placeholder="请选择用户" @change="onAssigneeChange">
<el-option
v-for="u in userOptions"
:key="u.userId"
:label="u.nickName || u.userName || String(u.userId)"
:value="u.userId"
/>
</el-select>
<el-form-item label="工位" prop="stationCode">
<span class="form-value">{{ getStationNameByCode(form.stationCode) }}</span>
</el-form-item>
<el-form-item label="被分配用户姓名" prop="assigneeUserName">
<el-input v-model="form.assigneeUserName" placeholder="请输入被分配用户姓名" />
</el-form-item>
<el-form-item label="被分配角色编码" prop="assigneeRoleKey">
<el-input v-model="form.assigneeRoleKey" placeholder="请输入被分配角色编码" />
</el-form-item>
<el-form-item label="被分配班组编码" prop="assigneeTeamCode">
<el-input v-model="form.assigneeTeamCode" placeholder="请输入被分配班组编码" />
<el-form-item label="分配用户" prop="assigneeUserName">
<span class="form-value">{{ form.assigneeUserName || '-' }}</span>
</el-form-item>
<el-form-item label="分配时间" prop="assignedTime">
<el-date-picker clearable
v-model="form.assignedTime"
type="date"
value-format="yyyy-MM-dd"
placeholder="请选择分配时间">
</el-date-picker>
<span class="form-value">{{ form.assignedTime ? parseTime(form.assignedTime, '{y}-{m}-{d} {h}:{i}:{s}') : '-' }}</span>
</el-form-item>
<!-- 接单时间改为只读展示后端自动维护 -->
<el-form-item label="接单时间">
<span>{{ form.acceptTime ? parseTime(form.acceptTime, '{y}-{m}-{d} {h}:{i}:{s}') : '后端自动维护' }}</span>
<el-form-item label="接单时间" prop="acceptTime">
<span class="form-value">{{ form.acceptTime ? parseTime(form.acceptTime, '{y}-{m}-{d} {h}:{i}:{s}') : '-' }}</span>
</el-form-item>
<!-- 完成时间改为只读展示后端自动维护 -->
<el-form-item label="完成时间">
<span>{{ form.finishTime ? parseTime(form.finishTime, '{y}-{m}-{d} {h}:{i}:{s}') : '后端自动维护' }}</span>
<el-form-item label="完成时间" prop="finishTime">
<span class="form-value">{{ form.finishTime ? parseTime(form.finishTime, '{y}-{m}-{d} {h}:{i}:{s}') : '-' }}</span>
</el-form-item>
<el-form-item label="派工状态" prop="status">
<el-select v-model="form.status" placeholder="请选择派工状态">
@ -273,9 +245,6 @@
></el-option>
</el-select>
</el-form-item>
<el-form-item label="是否有效" prop="isFlag">
<el-input v-model="form.isFlag" placeholder="请输入是否有效" />
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" placeholder="请输入备注" />
</el-form-item>
@ -292,6 +261,7 @@
import { listAndonEventAssignment, getAndonEventAssignment, delAndonEventAssignment, addAndonEventAssignment, updateAndonEventAssignment } from "@/api/production/andonEventAssignment";
import { getAndonEventList } from "@/api/production/andonEvent";
import { selectUserList } from "@/api/system/user";
import { findProductLineList } from "@/api/base/productLine";
export default {
name: "AndonEventAssignment",
@ -314,6 +284,8 @@ export default {
andonEventAssignmentList: [],
//
eventOptions: [],
//
stationOptions: [],
//
userOptions: [],
//
@ -364,7 +336,7 @@ export default {
{ key: 7, label: `接单时间`, visible: true },
{ key: 8, label: `完成时间`, visible: true },
{ key: 9, label: `派工状态`, visible: true },
{ key: 10, label: `是否有效`, visible: true },
{ key: 10, label: `是否有效`, visible: false },
{ key: 11, label: `备注`, visible: true },
{ key: 12, label: `创建人`, visible: false },
{ key: 13, label: `创建时间`, visible: false },
@ -394,6 +366,10 @@ export default {
this.defaultUserName = u.nickName || u.userName || String(u.userId);
}
});
//
findProductLineList({ productLineType: 2 }).then(res => {
this.stationOptions = res.rows || res.data || [];
});
},
methods: {
/** 查询安灯派工记录(派工即通知/待办)列表 */
@ -526,6 +502,18 @@ export default {
this.download('production/andonEventAssignment/export', {
...this.queryParams
}, `andonEventAssignment_${new Date().getTime()}.xlsx`)
},
/** 根据事件ID获取呼叫单号 */
getCallCodeByEventId(eventId) {
if (!eventId) return '-';
const event = this.eventOptions.find(e => e.eventId === eventId);
return event ? event.callCode || '-' : '-';
},
/** 根据工位编码获取工位名称 */
getStationNameByCode(stationCode) {
if (!stationCode) return '-';
const station = this.stationOptions ? this.stationOptions.find(s => s.stationCode === stationCode) : null;
return station ? station.stationName : stationCode;
}
}
};

@ -155,9 +155,9 @@
<el-select v-model="form.stationCode" placeholder="请选择绑定的工位(必填)" filterable style="width: 100%">
<el-option
v-for="item in processStationList"
:key="item.productLineCode"
:label="item.productLineCode + ' - ' + item.productLineName"
:value="item.productLineCode"
:key="item.stationCode"
:label="item.stationName + ' (' + item.stationCode + ')'"
:value="item.stationCode"
/>
</el-select>
</el-form-item>

Loading…
Cancel
Save