You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1081 lines
33 KiB
Vue

1 year ago
<template>
<div class="navbar">
<hamburger id="hamburger-container" :is-active="sidebar.opened" class="hamburger-container"
@toggleClick="toggleSideBar"
/>
1 year ago
<breadcrumb id="breadcrumb-container" class="breadcrumb-container" v-if="!topNav"/>
<top-nav id="topmenu-container" class="topmenu-container" v-if="topNav"/>
<div class="right-menu">
<template v-if="device!=='mobile'">
<search id="header-search" class="right-menu-item"/>
1 year ago
<!-- <el-tooltip content="源码地址" effect="dark" placement="bottom">-->
<!-- <ruo-yi-git id="ruoyi-git" class="right-menu-item hover-effect" />-->
<!-- </el-tooltip>-->
1 year ago
<!-- <el-tooltip content="文档地址" effect="dark" placement="bottom">-->
<!-- <ruo-yi-doc id="ruoyi-doc" class="right-menu-item hover-effect" />-->
<!-- </el-tooltip>-->
<el-tooltip content="异常处理" class="right-menu-item hover-effect">
<span
class="exceptionHandling"
:class="{ warnTxt: alarmDataList && alarmDataList.length > 0, 'red-yellow-blink': alarmDataList && alarmDataList.length > 0 }"
@click="getAlarmData(true)"
>
<i class="el-icon-message"></i>{{
(alarmDataList && alarmDataList.length > 0 && alarmDataList.length) || ''
}}
</span>
</el-tooltip>
<screenfull id="screenfull" class="right-menu-item hover-effect"/>
1 year ago
<el-tooltip content="布局大小" effect="dark" placement="bottom">
<size-select id="size-select" class="right-menu-item hover-effect"/>
1 year ago
</el-tooltip>
</template>
<el-dropdown class="avatar-container right-menu-item hover-effect" trigger="click">
<div class="avatar-wrapper">
<img :src="avatar" class="user-avatar">
<i class="el-icon-caret-bottom"/>
1 year ago
</div>
<el-dropdown-menu slot="dropdown">
<router-link to="/user/profile">
<el-dropdown-item>个人中心</el-dropdown-item>
</router-link>
<el-dropdown-item @click.native="setting = true">
<span>布局设置</span>
</el-dropdown-item>
<el-dropdown-item divided @click.native="logout">
<span>退出登录</span>
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</div>
<!-- 告警对话框 -->
<el-dialog
:title="alarmTitle"
:visible.sync="alarmOpen"
width="1000px"
append-to-body
>
<el-tabs v-model="activeTab" type="card">
<!-- 告警列表标签页 -->
<el-tab-pane label="告警列表" name="alarmList">
<el-table
v-loading="alarmLoading"
:data="alarmDataList"
@selection-change="handleSelectionChangeAlarm"
@row-click="handleRowClick"
highlight-current-row
>
<el-table-column type="selection" width="55" align="center"/>
<el-table-column label="异常设备" align="center" prop="deviceName"/>
<el-table-column label="异常类型" align="center" prop="cause"/>
<el-table-column label="异常数据" align="center" prop="alarmData"/>
<el-table-column label="异常状态" align="center" prop="alarmStatus">
<template slot-scope="scope">
<dict-tag :options="dict.type.alarm_status" :value="scope.row.alarmStatus"/>
</template>
</el-table-column>
<el-table-column label="记录时间" align="center" prop="collectTime" width="180">
<template slot-scope="scope">
<span>{{ parseTime(scope.row.createTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="120">
<template slot-scope="scope">
<el-button
size="mini"
type="text"
icon="el-icon-view"
@click.stop="viewActionSteps(scope.row)"
>查看措施</el-button>
</template>
</el-table-column>
</el-table>
<pagination
v-show="alarmDataTotal > 0"
:total="alarmDataTotal"
:page.sync="queryParams.pageNum"
:limit.sync="queryParams.pageSize"
@pagination="getAlarmDataList"
/>
</el-tab-pane>
<!-- 处置措施标签页 -->
<el-tab-pane label="处置措施" name="actionSteps" :disabled="!currentAlarmData">
<div v-if="currentAlarmData">
<el-alert
:title="`设备:${currentAlarmData.deviceName} | 异常类型:${currentAlarmData.cause} | 异常数据:${currentAlarmData.alarmData}`"
type="warning"
:closable="false"
style="margin-bottom: 20px;"
/>
<div v-loading="actionStepsLoading">
<div v-if="actionSteps.length === 0" class="no-steps">
<el-empty description="该告警规则暂无配置处置措施"></el-empty>
</div>
<div v-else>
<el-timeline>
<el-timeline-item
v-for="(step, index) in actionSteps"
:key="step.objId"
:timestamp="`步骤 ${step.stepSequence}`"
placement="top"
type="primary"
size="large"
>
<el-card class="step-card">
<div slot="header" class="clearfix">
<span class="step-title">{{ step.stepSequence }}</span>
</div>
<!-- 步骤描述 -->
<div class="step-description">
<p>{{ step.description }}</p>
</div>
<!-- 步骤图片 -->
<div v-if="step.stepImages && step.stepImages.length > 0" class="step-images">
<div class="images-title">参考图片</div>
<div class="image-gallery">
<div
v-for="image in step.stepImages"
:key="image.objId"
class="image-item"
@click="previewImage(getFullImageUrl(image.imageUrl))"
>
<img :src="getFullImageUrl(image.imageUrl)" :alt="image.description" />
<div v-if="image.description" class="image-desc">{{ image.description }}</div>
</div>
</div>
</div>
<!-- 步骤备注 -->
<div v-if="step.remark" class="step-remark">
<el-tag type="info" size="small">备注{{ step.remark }}</el-tag>
</div>
</el-card>
</el-timeline-item>
</el-timeline>
</div>
</div>
</div>
</el-tab-pane>
</el-tabs>
<div slot="footer" class="dialog-footer">
<el-button type="primary" @click="jumpProcessing" :disabled="objIds.length === 0">
标记已处理 ({{ objIds.length }})
</el-button>
<el-button @click="closeDialog"> </el-button>
</div>
</el-dialog>
<!-- 图片预览对话框 -->
<el-dialog
title="图片预览"
:visible.sync="imagePreviewVisible"
width="80%"
append-to-body
center
>
<div class="image-preview-container">
<img :src="previewImageUrl" style="max-width: 100%; max-height: 70vh;" />
</div>
</el-dialog>
<!-- 实时告警弹窗 -->
<el-dialog
title="⚠️ 实时告警通知"
:visible.sync="realtimeAlarmDialog"
width="800px"
append-to-body
:close-on-click-modal="false"
:close-on-press-escape="false"
class="realtime-alarm-dialog"
>
<div v-if="currentRealtimeAlarm" class="alarm-content">
<!-- 设备信息 -->
<div class="alarm-section">
<h3 class="section-title">📟 设备信息</h3>
<el-row :gutter="16">
<el-col :span="12">
<div class="info-item">
<span class="label">设备ID</span>
<span class="value">{{ currentRealtimeAlarm.monitorId }}</span>
</div>
</el-col>
<el-col :span="12">
<div class="info-item">
<span class="label">告警时间</span>
<span class="value">{{ formatAlarmTime(currentRealtimeAlarm.recordTime) }}</span>
</div>
</el-col>
</el-row>
</div>
<!-- 设备当前数据 -->
<div class="alarm-section" v-if="currentRealtimeAlarm.deviceParam">
<h3 class="section-title">📊 设备当前数据</h3>
<el-row :gutter="16">
<el-col :span="8" v-if="currentRealtimeAlarm.deviceParam.temperature !== null">
<div class="data-item">
<span class="data-label">温度</span>
<span class="data-value">{{ currentRealtimeAlarm.deviceParam.temperature }}°C</span>
</div>
</el-col>
<el-col :span="8" v-if="currentRealtimeAlarm.deviceParam.humidity !== null">
<div class="data-item">
<span class="data-label">湿度</span>
<span class="data-value">{{ currentRealtimeAlarm.deviceParam.humidity }}%</span>
</div>
</el-col>
<el-col :span="8" v-if="currentRealtimeAlarm.deviceParam.illuminance !== null">
<div class="data-item">
<span class="data-label">照度</span>
<span class="data-value">{{ currentRealtimeAlarm.deviceParam.illuminance }}lx</span>
</div>
</el-col>
<el-col :span="8" v-if="currentRealtimeAlarm.deviceParam.noise !== null">
<div class="data-item">
<span class="data-label">噪声</span>
<span class="data-value">{{ currentRealtimeAlarm.deviceParam.noise }}dB</span>
</div>
</el-col>
<el-col :span="8" v-if="currentRealtimeAlarm.deviceParam.concentration !== null">
<div class="data-item">
<span class="data-label">气体浓度</span>
<span class="data-value">{{ currentRealtimeAlarm.deviceParam.concentration }}ppm</span>
</div>
</el-col>
<el-col :span="8" v-if="currentRealtimeAlarm.deviceParam.vibrationSpeed !== null">
<div class="data-item">
<span class="data-label">振动速度</span>
<span class="data-value">{{ currentRealtimeAlarm.deviceParam.vibrationSpeed || currentRealtimeAlarm.deviceParam.VibrationSpeed }}mm/s</span>
</div>
</el-col>
<el-col :span="8" v-if="currentRealtimeAlarm.deviceParam.vibrationDisplacement !== null || currentRealtimeAlarm.deviceParam.VibrationDisplacement !== null">
<div class="data-item">
<span class="data-label">振动位移</span>
<span class="data-value">{{ currentRealtimeAlarm.deviceParam.vibrationDisplacement || currentRealtimeAlarm.deviceParam.VibrationDisplacement }}um</span>
</div>
</el-col>
<el-col :span="8" v-if="currentRealtimeAlarm.deviceParam.vibrationAcceleration !== null || currentRealtimeAlarm.deviceParam.VibrationAcceleration !== null">
<div class="data-item">
<span class="data-label">振动加速度</span>
<span class="data-value">{{ currentRealtimeAlarm.deviceParam.vibrationAcceleration || currentRealtimeAlarm.deviceParam.VibrationAcceleration }}g</span>
</div>
</el-col>
<el-col :span="8" v-if="currentRealtimeAlarm.deviceParam.vibrationTemp !== null || currentRealtimeAlarm.deviceParam.VibrationTemp !== null">
<div class="data-item">
<span class="data-label">振动温度</span>
<span class="data-value">{{ currentRealtimeAlarm.deviceParam.vibrationTemp || currentRealtimeAlarm.deviceParam.VibrationTemp }}</span>
</div>
</el-col>
</el-row>
</div>
<!-- 触发的告警规则 -->
<div class="alarm-section" v-if="currentRealtimeAlarm.alarmRules && currentRealtimeAlarm.alarmRules.length > 0">
<h3 class="section-title">🚨 触发的告警规则</h3>
<div class="alarm-rules">
<div
v-for="(rule, index) in currentRealtimeAlarm.alarmRules"
:key="index"
class="rule-item"
>
<div class="rule-header">
<span class="rule-name">{{ rule.ruleName }}</span>
<el-tag :type="rule.triggerRule === 0 ? 'danger' : 'warning'" size="small">
{{ rule.triggerRule === 0 ? '大于阈值' : '小于阈值' }}
</el-tag>
</div>
<div class="rule-details">
<span class="detail-item">阈值{{ rule.triggerValue }}</span>
<span class="detail-item">监测字段{{ getFieldName(rule.monitorField) }}</span>
<span class="detail-item" v-if="rule.cause">{{ rule.cause }}</span>
</div>
</div>
</div>
</div>
<!-- 告警内容详情 -->
<div class="alarm-section" v-if="currentRealtimeAlarm.alarmContents && currentRealtimeAlarm.alarmContents.length > 0">
<h3 class="section-title">📋 告警内容详情</h3>
<div class="alarm-contents">
<div
v-for="(content, index) in currentRealtimeAlarm.alarmContents"
:key="index"
class="content-item"
>
<el-alert
:title="content"
type="error"
:closable="false"
show-icon
/>
</div>
</div>
</div>
</div>
<div slot="footer" class="dialog-footer">
<el-button
@click="closeRealtimeAlarmDialog"
:loading="alarmProcessing"
>
稍后处理
</el-button>
<el-button
type="primary"
@click="processRealtimeAlarm"
:loading="alarmProcessing"
>
确认知晓
</el-button>
</div>
</el-dialog>
1 year ago
</div>
</template>
<script>
import { mapGetters } from 'vuex'
import Breadcrumb from '@/components/Breadcrumb'
import TopNav from '@/components/TopNav'
import Hamburger from '@/components/Hamburger'
import Screenfull from '@/components/Screenfull'
import SizeSelect from '@/components/SizeSelect'
import Search from '@/components/HeaderSearch'
import RuoYiGit from '@/components/RuoYi/Git'
import RuoYiDoc from '@/components/RuoYi/Doc'
import settings from '@/settings'
import { handleExceptions, listRecordAlarmData } from '@/api/ems/record/recordAlarmData'
import { getEmsAlarmActionStepsByRuleId, getEmsAlarmActionStepsByAlarmInfo } from '@/api/ems/base/emsAlarmActionStep'
import { saveWebSocketAlarmData } from '@/api/ems/record/recordAlarmData'
1 year ago
export default {
dicts: ['alarm_type', 'alarm_status'],
data() {
return {
poolNameList: [],
poolName: '',
alarmTitle: '异常处理',
alarmOpen: false,
alarmLoading: false,
alarmDataList: [],
alarmDataTotal: 0,
// 选中数组
objIds: [],
// 非单个禁用
single: true,
// 非多个禁用
multiple: true,
// 查询参数
queryParams: {
pageNum: 1,
pageSize: 10,
alarmStatus: '1'
},
// 措施步骤相关
currentAlarmData: null,
showActionSteps: false,
actionSteps: [],
actionStepsLoading: false,
activeTab: 'alarmList',
imagePreviewVisible: false,
previewImageUrl: '',
// WebSocket告警相关
realtimeAlarmDialog: false,
currentRealtimeAlarm: null,
alarmProcessing: false
}
},
created() {
localStorage.setItem('this.alarmDataTotal', 0)
// 监听WebSocket告警事件
this.$bus.$on('websocket-alarm', this.handleRealtimeAlarm)
},
beforeDestroy() {
// 移除事件监听
this.$bus.$off('websocket-alarm', this.handleRealtimeAlarm)
},
mounted() {
// 初始获取告警数据后续完全依赖WebSocket推送
this.getAlarmData()
},
1 year ago
components: {
Breadcrumb,
TopNav,
Hamburger,
Screenfull,
SizeSelect,
Search,
RuoYiGit,
RuoYiDoc
},
computed: {
...mapGetters([
'sidebar',
'avatar',
'device'
]),
setting: {
get() {
return this.$store.state.settings.showSettings
},
set(val) {
this.$store.dispatch('settings/changeSetting', {
key: 'showSettings',
value: val
})
}
},
topNav: {
get() {
return this.$store.state.settings.topNav
}
}
},
methods: {
// 打开右下角异常
openAlarm() {
this.$notify({
title: '异常数据提示',
position: 'bottom-right',
message: this.$createElement(
'div',
{
on: {
click: () => {
this.getAlarmData(true)
}
}
},
[this.$createElement('el-button', {}, ['点击查看'])]
)
})
},
/** 报警列表 */
getAlarmData(open) {
this.alarmOpen = open
this.alarmLoading = true
this.getAlarmDataList()
},
getAlarmDataList() {
listRecordAlarmData(this.queryParams).then((response) => {
this.alarmDataList = response.rows
this.alarmDataTotal = response.total
this.alarmLoading = false
if (localStorage.getItem('this.alarmDataTotal') != this.alarmDataTotal) {
localStorage.setItem('this.alarmDataTotal', this.alarmDataTotal)
this.openAlarm()
}
})
},
/** 跳转处理 */
jumpProcessing(row) {
if (this.objIds.length === 0) {
this.$modal.msgWarning('请勾选设备进行异常处理!')
return
}
handleExceptions(this.objIds).then(response => {
this.$modal.msgSuccess('处理成功')
this.alarmOpen = false
})
},
// 多选框选中数据
handleSelectionChangeAlarm(selection) {
this.objIds = selection.map((item) => item.objId)
this.single = selection.length !== 1
this.multiple = !selection.length
},
1 year ago
toggleSideBar() {
this.$store.dispatch('app/toggleSideBar')
},
logout() {
this.$confirm('确定注销并退出系统吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.$store.dispatch('LogOut').then(() => {
if (!settings.casEnable) {
location.href = '/index'
1 year ago
}
})
}).catch(() => {
})
},
handleRowClick(row) {
this.currentAlarmData = row
this.activeTab = 'actionSteps'
this.getActionSteps(row)
},
getActionSteps(alarmData) {
this.actionStepsLoading = true
getEmsAlarmActionStepsByAlarmInfo(alarmData.monitorId, alarmData.cause).then((response) => {
this.actionSteps = response.data || []
this.actionStepsLoading = false
}).catch(() => {
this.actionSteps = []
this.actionStepsLoading = false
})
},
viewActionSteps(row) {
this.currentAlarmData = row
this.activeTab = 'actionSteps'
this.getActionSteps(row)
},
previewImage(url) {
this.previewImageUrl = url
this.imagePreviewVisible = true
},
closeDialog() {
this.alarmOpen = false
this.activeTab = 'alarmList'
},
// 获取完整的图片URL
getFullImageUrl(relativePath) {
if (!relativePath) return '';
// 如果已经是完整URL直接返回兼容历史数据
if (relativePath.startsWith('http')) {
return relativePath;
}
// 动态拼接当前环境的baseURL
const baseURL = process.env.VUE_APP_BASE_API || '';
return baseURL + relativePath;
},
// 处理WebSocket实时告警
handleRealtimeAlarm(alarmData) {
console.log('收到实时告警:', alarmData)
this.currentRealtimeAlarm = alarmData
this.realtimeAlarmDialog = true
// 同时播放提示音(可选)
this.playAlarmSound()
// 注意:这里不立即保存,等用户操作后再保存
// this.saveRealtimeAlarmData(alarmData)
},
// 播放告警提示音
playAlarmSound() {
try {
// 可以添加音频文件播放
// const audio = new Audio('/static/alarm.mp3')
// audio.play()
console.log('播放告警提示音')
} catch (error) {
console.error('播放提示音失败:', error)
}
},
// 保存WebSocket告警数据到数据库
async saveRealtimeAlarmData(alarmData, alarmStatus = 1) {
try {
// 构建符合EmsRecordAlarmData实体的数据列表
// 每个触发的告警规则对应一条EmsRecordAlarmData记录
const alarmDataList = []
if (!alarmData.alarmRules || alarmData.alarmRules.length === 0) {
console.warn('告警数据中没有告警规则')
return
}
// 获取当前时间并格式化为后端期望的格式
const getCurrentTimeForBackend = () => {
const now = new Date()
const year = now.getFullYear()
const month = String(now.getMonth() + 1).padStart(2, '0')
const day = String(now.getDate()).padStart(2, '0')
const hours = String(now.getHours()).padStart(2, '0')
const minutes = String(now.getMinutes()).padStart(2, '0')
const seconds = String(now.getSeconds()).padStart(2, '0')
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
}
// 为每个触发的告警规则创建一条记录
for (const rule of alarmData.alarmRules) {
// 获取对应字段的实际数值
const actualValue = this.getActualValueFromDeviceParam(alarmData.deviceParam, rule.monitorField)
const alarmRecord = {
// 设备相关信息
monitorId: alarmData.monitorId,
collectTime: getCurrentTimeForBackend(), // 使用当前时间并格式化为后端期望格式
// 告警类型根据规则的triggerRule设置0=大于阈值1=小于阈值)
alarmType: rule.triggerRule || 0,
// 告警状态根据用户操作设置0=已处理1=未处理)
alarmStatus: alarmStatus,
// 告警数据:实际触发告警的数值
alarmData: actualValue ? String(actualValue) : '',
// 告警原因:使用字段名称
cause: this.getFieldName(rule.monitorField),
// 其他字段可以为空,后端会设置默认值
operationName: null,
operationTime: null,
notifyUser: null
}
alarmDataList.push(alarmRecord)
console.log('构建告警记录:', {
设备ID: alarmRecord.monitorId,
告警字段: alarmRecord.cause,
实际数值: alarmRecord.alarmData,
告警类型: alarmRecord.alarmType === 0 ? '大于阈值' : '小于阈值',
规则阈值: rule.triggerValue,
记录时间: alarmRecord.collectTime,
处理状态: alarmRecord.alarmStatus === 0 ? '已处理' : '未处理'
})
}
if (alarmDataList.length === 0) {
console.warn('没有有效的告警记录可保存')
return
}
// 发送到后端保存
const response = await saveWebSocketAlarmData(alarmDataList)
if (response.code === 200) {
console.log('告警数据保存成功:', response.msg)
// 刷新告警数据列表,获取最新数据
this.getAlarmDataList()
return true
} else {
console.error('告警数据保存失败:', response.msg)
this.$message.error('告警数据保存失败: ' + response.msg)
return false
}
} catch (error) {
console.error('保存告警数据异常:', error)
this.$message.error('保存告警数据异常')
return false
}
},
// 从设备参数中获取指定字段的实际数值
getActualValueFromDeviceParam(deviceParam, monitorField) {
if (!deviceParam || monitorField === null || monitorField === undefined) {
return null
}
switch (monitorField) {
case 0: // 温度
return deviceParam.temperature
case 1: // 湿度
return deviceParam.humidity
case 2: // 振动-速度(mm/s)
return deviceParam.vibrationSpeed || deviceParam.VibrationSpeed
case 3: // 振动-位移(um)
return deviceParam.vibrationDisplacement || deviceParam.VibrationDisplacement
case 4: // 振动-加速度(g)
return deviceParam.vibrationAcceleration || deviceParam.VibrationAcceleration
case 5: // 振动-温度(℃)
return deviceParam.vibrationTemp || deviceParam.VibrationTemp
case 6: // 照度
return deviceParam.illuminance
case 7: // 噪声
return deviceParam.noise
case 8: // 气体浓度
return deviceParam.concentration
default:
console.warn('未知的监测字段:', monitorField)
return null
}
},
// 稍后处理实时告警(保存为未处理状态)
async closeRealtimeAlarmDialog() {
if (this.currentRealtimeAlarm) {
this.alarmProcessing = true
try {
// 保存告警数据状态为1未处理
const success = await this.saveRealtimeAlarmData(this.currentRealtimeAlarm, 1)
if (success) {
this.$message.info('告警已记录,状态为未处理')
}
} catch (error) {
console.error('保存告警数据失败:', error)
this.$message.error('保存告警数据失败')
} finally {
this.alarmProcessing = false
}
}
this.realtimeAlarmDialog = false
this.currentRealtimeAlarm = null
},
// 确认知晓实时告警(保存为已处理状态)
async processRealtimeAlarm() {
this.alarmProcessing = true
try {
// 保存告警数据状态为0已处理
const success = await this.saveRealtimeAlarmData(this.currentRealtimeAlarm, 0)
if (success) {
this.$message.success('告警已确认知晓并标记为已处理')
}
// 关闭弹窗
this.realtimeAlarmDialog = false
this.currentRealtimeAlarm = null
} catch (error) {
console.error('处理告警失败:', error)
this.$message.error('处理告警失败')
} finally {
this.alarmProcessing = false
}
},
// 格式化告警时间
formatAlarmTime(time) {
if (!time) return '--'
try {
const date = new Date(time)
return date.toLocaleString('zh-CN')
} catch (error) {
return time
}
},
// 获取监测字段名称
getFieldName(fieldCode) {
const fieldMap = {
0: '温度',
1: '湿度',
2: '振动-速度(mm/s)',
3: '振动-位移(um)',
4: '振动-加速度(g)',
5: '振动-温度(℃)',
6: '照度',
7: '噪声',
8: '气体浓度'
}
return fieldMap[fieldCode] || '未知字段'
1 year ago
}
}
}
</script>
<style lang="scss" scoped>
@keyframes redYellowBlink {
0% {
color: red;
}
50% {
color: rgb(186, 186, 16);
}
100% {
color: red;
}
}
.red-yellow-blink {
animation: redYellowBlink 1s infinite;
}
1 year ago
.navbar {
height: 50px;
overflow: hidden;
position: relative;
background: #fff;
box-shadow: 0 1px 4px rgba(0, 21, 41, .08);
1 year ago
.hamburger-container {
line-height: 46px;
height: 100%;
float: left;
cursor: pointer;
transition: background .3s;
-webkit-tap-highlight-color: transparent;
1 year ago
&:hover {
background: rgba(0, 0, 0, .025)
}
}
.breadcrumb-container {
float: left;
}
.topmenu-container {
position: absolute;
left: 50px;
}
.errLog-container {
display: inline-block;
vertical-align: top;
}
.right-menu {
float: right;
height: 100%;
line-height: 50px;
&:focus {
outline: none;
}
.right-menu-item {
display: inline-block;
padding: 0 8px;
height: 100%;
font-size: 18px;
color: #5a5e66;
vertical-align: text-bottom;
&.hover-effect {
cursor: pointer;
transition: background .3s;
&:hover {
background: rgba(0, 0, 0, .025)
}
}
}
.avatar-container {
margin-right: 30px;
.avatar-wrapper {
margin-top: 5px;
position: relative;
.user-avatar {
cursor: pointer;
width: 40px;
height: 40px;
border-radius: 10px;
}
.el-icon-caret-bottom {
cursor: pointer;
position: absolute;
right: -20px;
top: 25px;
font-size: 12px;
}
}
}
}
}
// 措施步骤相关样式
.step-card {
margin-bottom: 20px;
.step-title {
font-weight: bold;
font-size: 16px;
color: #409EFF;
}
.step-description {
margin-bottom: 15px;
p {
line-height: 1.6;
margin: 0;
color: #606266;
}
}
.step-images {
margin-bottom: 15px;
.images-title {
font-size: 14px;
color: #909399;
margin-bottom: 10px;
}
.image-gallery {
display: flex;
flex-wrap: wrap;
gap: 10px;
.image-item {
cursor: pointer;
border: 2px solid #f0f0f0;
border-radius: 4px;
overflow: hidden;
transition: all 0.3s;
max-width: 200px;
&:hover {
border-color: #409EFF;
transform: scale(1.02);
}
img {
width: 100%;
height: 150px;
object-fit: cover;
display: block;
}
.image-desc {
padding: 8px;
font-size: 12px;
color: #666;
background: #f9f9f9;
text-align: center;
}
}
}
}
.step-remark {
margin-top: 10px;
}
}
.no-steps {
text-align: center;
padding: 40px;
}
.image-preview-container {
text-align: center;
}
// 实时告警弹窗样式
.realtime-alarm-dialog {
.el-dialog__header {
background: linear-gradient(135deg, #ff4757, #ff6b7a);
color: white;
padding: 15px 20px;
.el-dialog__title {
color: white;
font-weight: bold;
font-size: 16px;
}
.el-dialog__close {
color: white;
&:hover {
color: #f1f1f1;
}
}
}
.alarm-content {
max-height: 60vh;
overflow-y: auto;
.alarm-section {
margin-bottom: 20px;
padding: 15px;
background: #f9f9f9;
border-radius: 8px;
border-left: 4px solid #ff4757;
.section-title {
margin: 0 0 15px 0;
font-size: 14px;
font-weight: bold;
color: #333;
}
.info-item, .data-item {
display: flex;
align-items: center;
margin-bottom: 8px;
.label, .data-label {
font-weight: 500;
color: #666;
min-width: 80px;
}
.value, .data-value {
color: #333;
font-weight: bold;
}
}
.alarm-rules {
.rule-item {
background: white;
border: 1px solid #e6e6e6;
border-radius: 6px;
padding: 12px;
margin-bottom: 10px;
.rule-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
.rule-name {
font-weight: bold;
color: #333;
flex: 1;
}
}
.rule-details {
display: flex;
flex-wrap: wrap;
gap: 15px;
.detail-item {
font-size: 13px;
color: #666;
&:before {
content: "• ";
color: #ff4757;
font-weight: bold;
}
}
}
}
}
.alarm-contents {
.content-item {
margin-bottom: 10px;
.el-alert {
.el-alert__title {
font-size: 13px;
line-height: 1.4;
}
}
}
}
}
}
.dialog-footer {
text-align: center;
padding: 20px 0 10px 0;
.el-button {
min-width: 100px;
}
}
}
1 year ago
</style>