|
@@ -1,168 +1,303 @@
|
|
|
<template>
|
|
<template>
|
|
|
- <div class="app-container device-alarm-content">
|
|
|
|
|
|
|
+ <div class="app-container device-alarm-dashboard">
|
|
|
<el-row :gutter="20">
|
|
<el-row :gutter="20">
|
|
|
|
|
+ <!-- 左侧区域树 -->
|
|
|
<el-col :span="4" :xs="24">
|
|
<el-col :span="4" :xs="24">
|
|
|
- <div class="head-container">
|
|
|
|
|
- <el-input
|
|
|
|
|
- v-model="areaName"
|
|
|
|
|
- placeholder="请输入服务区名称"
|
|
|
|
|
- clearable
|
|
|
|
|
- size="small"
|
|
|
|
|
- prefix-icon="el-icon-search"
|
|
|
|
|
- style="margin-bottom: 20px"
|
|
|
|
|
- @input="filterTree"
|
|
|
|
|
- />
|
|
|
|
|
- </div>
|
|
|
|
|
- <div class="head-container tree-container">
|
|
|
|
|
- <el-tree
|
|
|
|
|
- ref="tree"
|
|
|
|
|
- :data="areaOptions"
|
|
|
|
|
- :props="defaultProps"
|
|
|
|
|
- :expand-on-click-node="false"
|
|
|
|
|
- :filter-node-method="filterNode"
|
|
|
|
|
- node-key="id"
|
|
|
|
|
- :default-expanded-keys="defaultExpandedKeys"
|
|
|
|
|
- highlight-current
|
|
|
|
|
- @node-click="handleNodeClick"
|
|
|
|
|
- >
|
|
|
|
|
- <span class="custom-tree-node" slot-scope="{ node, data }">
|
|
|
|
|
- <span class="tree-label">
|
|
|
|
|
- <i :class="getTreeIcon(data)" class="tree-icon"></i>
|
|
|
|
|
- {{ node.label }}
|
|
|
|
|
|
|
+ <div class="tree-panel">
|
|
|
|
|
+ <div class="tree-search">
|
|
|
|
|
+ <el-input
|
|
|
|
|
+ v-model="areaName"
|
|
|
|
|
+ placeholder="搜索服务区..."
|
|
|
|
|
+ clearable
|
|
|
|
|
+ size="small"
|
|
|
|
|
+ prefix-icon="el-icon-search"
|
|
|
|
|
+ @input="filterTree"
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="tree-content">
|
|
|
|
|
+ <el-tree
|
|
|
|
|
+ ref="tree"
|
|
|
|
|
+ :data="areaOptions"
|
|
|
|
|
+ :props="defaultProps"
|
|
|
|
|
+ :expand-on-click-node="false"
|
|
|
|
|
+ :filter-node-method="filterNode"
|
|
|
|
|
+ node-key="id"
|
|
|
|
|
+ :default-expanded-keys="defaultExpandedKeys"
|
|
|
|
|
+ highlight-current
|
|
|
|
|
+ @node-click="handleNodeClick"
|
|
|
|
|
+ >
|
|
|
|
|
+ <span class="custom-tree-node" slot-scope="{ node, data }">
|
|
|
|
|
+ <span class="tree-label">
|
|
|
|
|
+ <i :class="getTreeIcon(data)" class="tree-icon"></i>
|
|
|
|
|
+ {{ node.label }}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ <el-badge
|
|
|
|
|
+ v-if="areaAlarmCounts[data.id] > 0"
|
|
|
|
|
+ :value="areaAlarmCounts[data.id]"
|
|
|
|
|
+ :max="99"
|
|
|
|
|
+ class="area-badge"
|
|
|
|
|
+ />
|
|
|
</span>
|
|
</span>
|
|
|
- </span>
|
|
|
|
|
- </el-tree>
|
|
|
|
|
|
|
+ </el-tree>
|
|
|
|
|
+ </div>
|
|
|
</div>
|
|
</div>
|
|
|
</el-col>
|
|
</el-col>
|
|
|
|
|
|
|
|
|
|
+ <!-- 右侧主内容 -->
|
|
|
<el-col :span="20" :xs="24">
|
|
<el-col :span="20" :xs="24">
|
|
|
- <el-row :gutter="20" class="kpi-cards">
|
|
|
|
|
- <el-col :span="6" v-for="(item, index) in kpiData" :key="index">
|
|
|
|
|
- <div class="kpi-card" :class="'kpi-' + item.type">
|
|
|
|
|
- <div class="kpi-icon">
|
|
|
|
|
- <i :class="item.icon"></i>
|
|
|
|
|
- </div>
|
|
|
|
|
- <div class="kpi-content">
|
|
|
|
|
- <div class="kpi-value">{{ item.value }}</div>
|
|
|
|
|
- <div class="kpi-label">{{ item.label }}</div>
|
|
|
|
|
- </div>
|
|
|
|
|
|
|
+ <!-- KPI 统计卡片 -->
|
|
|
|
|
+ <div class="kpi-row">
|
|
|
|
|
+ <div class="kpi-card total">
|
|
|
|
|
+ <div class="kpi-icon">
|
|
|
|
|
+ <i class="el-icon-monitor"></i>
|
|
|
</div>
|
|
</div>
|
|
|
- </el-col>
|
|
|
|
|
- </el-row>
|
|
|
|
|
|
|
+ <div class="kpi-info">
|
|
|
|
|
+ <div class="kpi-value">{{ kpiData.deviceTotal }}</div>
|
|
|
|
|
+ <div class="kpi-label">设备总数</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="kpi-decoration"></div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="kpi-card online">
|
|
|
|
|
+ <div class="kpi-icon">
|
|
|
|
|
+ <i class="el-icon-success"></i>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="kpi-info">
|
|
|
|
|
+ <div class="kpi-value">{{ kpiData.onlineCount }}</div>
|
|
|
|
|
+ <div class="kpi-label">在线设备</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="kpi-rate" v-if="kpiData.deviceTotal > 0">
|
|
|
|
|
+ {{ ((kpiData.onlineCount / kpiData.deviceTotal) * 100).toFixed(1) }}%
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="kpi-card offline">
|
|
|
|
|
+ <div class="kpi-icon">
|
|
|
|
|
+ <i class="el-icon-warning"></i>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="kpi-info">
|
|
|
|
|
+ <div class="kpi-value">{{ kpiData.offlineCount }}</div>
|
|
|
|
|
+ <div class="kpi-label">离线设备</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="kpi-pulse" v-if="kpiData.offlineCount > 0"></div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="kpi-card alarm">
|
|
|
|
|
+ <div class="kpi-icon">
|
|
|
|
|
+ <i class="el-icon-bell"></i>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="kpi-info">
|
|
|
|
|
+ <div class="kpi-value">{{ kpiData.activeAlarmCount }}</div>
|
|
|
|
|
+ <div class="kpi-label">待处理告警</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="kpi-urgent" v-if="kpiData.urgentCount > 0">
|
|
|
|
|
+ <i class="el-icon-warning-outline"></i>
|
|
|
|
|
+ {{ kpiData.urgentCount }} 紧急
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
|
|
|
- <el-row :gutter="20" style="margin-top: 20px">
|
|
|
|
|
|
|
+ <!-- 图表第一行 -->
|
|
|
|
|
+ <el-row :gutter="20" class="chart-row">
|
|
|
<el-col :span="12">
|
|
<el-col :span="12">
|
|
|
- <div class="dashboard-card">
|
|
|
|
|
- <PieChartBlock title="设备状态分布" :opt-cfg="deviceStatusChart" style="height: 100%"/>
|
|
|
|
|
|
|
+ <div class="chart-card">
|
|
|
|
|
+ <div class="card-header">
|
|
|
|
|
+ <span class="card-title">
|
|
|
|
|
+ <i class="el-icon-pie-chart"></i> 设备状态分布
|
|
|
|
|
+ </span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div ref="deviceStatusChart" class="chart-body"></div>
|
|
|
</div>
|
|
</div>
|
|
|
</el-col>
|
|
</el-col>
|
|
|
<el-col :span="12">
|
|
<el-col :span="12">
|
|
|
- <div class="dashboard-card">
|
|
|
|
|
- <LineChartBlock title="告警趋势分析" :opt-cfg="alarmTrendChart" style="height: 100%">
|
|
|
|
|
- <template v-slot:filters>
|
|
|
|
|
- <SwitchTag
|
|
|
|
|
- :ds="dateTypeOptions"
|
|
|
|
|
- :defTag="dateType"
|
|
|
|
|
- :tagClick="onDateTypeSwitch"
|
|
|
|
|
- />
|
|
|
|
|
- </template>
|
|
|
|
|
- </LineChartBlock>
|
|
|
|
|
|
|
+ <div class="chart-card">
|
|
|
|
|
+ <div class="card-header">
|
|
|
|
|
+ <span class="card-title">
|
|
|
|
|
+ <i class="el-icon-data-line"></i> 告警趋势分析
|
|
|
|
|
+ </span>
|
|
|
|
|
+ <el-radio-group v-model="trendType" size="mini" @change="loadAlarmTrend">
|
|
|
|
|
+ <el-radio-button label="day">按时</el-radio-button>
|
|
|
|
|
+ <el-radio-button label="month">按天</el-radio-button>
|
|
|
|
|
+ </el-radio-group>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div ref="alarmTrendChart" class="chart-body"></div>
|
|
|
</div>
|
|
</div>
|
|
|
</el-col>
|
|
</el-col>
|
|
|
</el-row>
|
|
</el-row>
|
|
|
|
|
|
|
|
- <el-row :gutter="20" style="margin-top: 20px">
|
|
|
|
|
|
|
+ <!-- 图表第二行:实时告警 + 告警级别分布 -->
|
|
|
|
|
+ <el-row :gutter="20" class="chart-row">
|
|
|
<el-col :span="14">
|
|
<el-col :span="14">
|
|
|
- <div class="dashboard-card table-card">
|
|
|
|
|
- <BlockTable title="实时告警列表" :table-data="realTimeAlarmData" style="height: 100%">
|
|
|
|
|
- <template v-slot:columns>
|
|
|
|
|
- <el-table-column type="index" label="序号" align="center" width="60"/>
|
|
|
|
|
- <el-table-column prop="subSystemName" label="子系统" align="center" width="100"/>
|
|
|
|
|
- <el-table-column prop="objName" label="设备名称" align="center" show-overflow-tooltip/>
|
|
|
|
|
- <el-table-column prop="alarmMsg" label="告警描述" align="center" show-overflow-tooltip/>
|
|
|
|
|
- <el-table-column prop="alarmTime" label="告警时间" align="center" width="160"/>
|
|
|
|
|
- <el-table-column label="告警类型" align="center" width="100">
|
|
|
|
|
|
|
+ <div class="chart-card table-card">
|
|
|
|
|
+ <div class="card-header">
|
|
|
|
|
+ <span class="card-title">
|
|
|
|
|
+ <i class="el-icon-bell"></i> 实时告警列表
|
|
|
|
|
+ </span>
|
|
|
|
|
+ <span class="table-count" v-if="realTimeAlarmList.length > 0">
|
|
|
|
|
+ 显示最新 {{ realTimeAlarmList.length }} 条
|
|
|
|
|
+ </span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="table-body">
|
|
|
|
|
+ <el-table
|
|
|
|
|
+ v-loading="tableLoading"
|
|
|
|
|
+ :data="realTimeAlarmList"
|
|
|
|
|
+ stripe
|
|
|
|
|
+ size="small"
|
|
|
|
|
+ :row-class-name="tableRowClassName"
|
|
|
|
|
+ >
|
|
|
|
|
+ <el-table-column type="index" label="#" width="45" align="center" />
|
|
|
|
|
+ <el-table-column label="级别" width="70" align="center">
|
|
|
<template slot-scope="scope">
|
|
<template slot-scope="scope">
|
|
|
- <el-tag :type="getAlarmTypeTag(scope.row.alarmType)" size="small">
|
|
|
|
|
- {{ getAlarmTypeName(scope.row.alarmType) }}
|
|
|
|
|
|
|
+ <el-tag
|
|
|
|
|
+ :type="getAlarmLevelTag(scope.row.alarmLevel)"
|
|
|
|
|
+ size="mini"
|
|
|
|
|
+ effect="dark"
|
|
|
|
|
+ :class="{ 'blink': scope.row.alarmLevel === 3 }"
|
|
|
|
|
+ >
|
|
|
|
|
+ {{ getAlarmLevelName(scope.row.alarmLevel) }}
|
|
|
</el-tag>
|
|
</el-tag>
|
|
|
</template>
|
|
</template>
|
|
|
</el-table-column>
|
|
</el-table-column>
|
|
|
- <el-table-column label="状态" align="center" width="100">
|
|
|
|
|
|
|
+ <el-table-column label="子系统" width="90" align="center" prop="subsystemName" show-overflow-tooltip />
|
|
|
|
|
+ <el-table-column label="设备名称" min-width="120" prop="targetName" show-overflow-tooltip>
|
|
|
<template slot-scope="scope">
|
|
<template slot-scope="scope">
|
|
|
- <el-dropdown
|
|
|
|
|
- @command="(cmd) => handleAlarmStateChange(cmd, scope.row)"
|
|
|
|
|
- v-if="scope.row.alarmState !== 2 && scope.row.alarmState !== 3"
|
|
|
|
|
- >
|
|
|
|
|
- <span class="el-dropdown-link">
|
|
|
|
|
- <el-tag :type="getAlarmStateTag(scope.row.alarmState)" size="small">
|
|
|
|
|
- {{ getAlarmStateName(scope.row.alarmState) }}
|
|
|
|
|
|
|
+ <span class="device-name">{{ scope.row.targetName || scope.row.targetCode }}</span>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ <el-table-column label="告警描述" min-width="150" prop="alarmMsg" show-overflow-tooltip>
|
|
|
|
|
+ <template slot-scope="scope">
|
|
|
|
|
+ <span class="alarm-msg">{{ scope.row.alarmMsg || formatAlarmContent(scope.row) }}</span>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ <el-table-column label="告警时间" width="140" align="center">
|
|
|
|
|
+ <template slot-scope="scope">
|
|
|
|
|
+ <span class="alarm-time">{{ formatTime(scope.row.alarmTime) }}</span>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ <el-table-column label="操作" width="120" align="center" fixed="right">
|
|
|
|
|
+ <template slot-scope="scope">
|
|
|
|
|
+ <template v-if="scope.row.alarmStatus < 3">
|
|
|
|
|
+ <el-dropdown
|
|
|
|
|
+ trigger="click"
|
|
|
|
|
+ @command="(cmd) => handleAlarmAction(cmd, scope.row)"
|
|
|
|
|
+ >
|
|
|
|
|
+ <el-tag
|
|
|
|
|
+ :type="getAlarmStatusTag(scope.row.alarmStatus)"
|
|
|
|
|
+ size="mini"
|
|
|
|
|
+ class="status-dropdown"
|
|
|
|
|
+ >
|
|
|
|
|
+ {{ getAlarmStatusName(scope.row.alarmStatus) }}
|
|
|
|
|
+ <i class="el-icon-arrow-down"></i>
|
|
|
</el-tag>
|
|
</el-tag>
|
|
|
- <i class="el-icon-arrow-down el-icon--right"></i>
|
|
|
|
|
- </span>
|
|
|
|
|
- <el-dropdown-menu slot="dropdown">
|
|
|
|
|
- <el-dropdown-item :command="1" v-if="scope.row.alarmState === 0">
|
|
|
|
|
- 开始处理
|
|
|
|
|
- </el-dropdown-item>
|
|
|
|
|
- <el-dropdown-item :command="2">已处置</el-dropdown-item>
|
|
|
|
|
- <el-dropdown-item :command="3">已消散</el-dropdown-item>
|
|
|
|
|
- </el-dropdown-menu>
|
|
|
|
|
- </el-dropdown>
|
|
|
|
|
- <el-tag v-else :type="getAlarmStateTag(scope.row.alarmState)" size="small">
|
|
|
|
|
- {{ getAlarmStateName(scope.row.alarmState) }}
|
|
|
|
|
- </el-tag>
|
|
|
|
|
|
|
+ <el-dropdown-menu slot="dropdown">
|
|
|
|
|
+ <el-dropdown-item command="confirm" v-if="scope.row.alarmStatus === 0">
|
|
|
|
|
+ <i class="el-icon-check"></i> 确认告警
|
|
|
|
|
+ </el-dropdown-item>
|
|
|
|
|
+ <el-dropdown-item command="handle">
|
|
|
|
|
+ <i class="el-icon-s-tools"></i> 开始处置
|
|
|
|
|
+ </el-dropdown-item>
|
|
|
|
|
+ <el-dropdown-item command="resolve" divided>
|
|
|
|
|
+ <i class="el-icon-circle-check"></i> 标记解决
|
|
|
|
|
+ </el-dropdown-item>
|
|
|
|
|
+ <el-dropdown-item command="close">
|
|
|
|
|
+ <i class="el-icon-circle-close"></i> 关闭告警
|
|
|
|
|
+ </el-dropdown-item>
|
|
|
|
|
+ </el-dropdown-menu>
|
|
|
|
|
+ </el-dropdown>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ <template v-else>
|
|
|
|
|
+ <el-tag :type="getAlarmStatusTag(scope.row.alarmStatus)" size="mini">
|
|
|
|
|
+ {{ getAlarmStatusName(scope.row.alarmStatus) }}
|
|
|
|
|
+ </el-tag>
|
|
|
|
|
+ </template>
|
|
|
</template>
|
|
</template>
|
|
|
</el-table-column>
|
|
</el-table-column>
|
|
|
- </template>
|
|
|
|
|
- </BlockTable>
|
|
|
|
|
|
|
+ </el-table>
|
|
|
|
|
+ </div>
|
|
|
</div>
|
|
</div>
|
|
|
</el-col>
|
|
</el-col>
|
|
|
-
|
|
|
|
|
<el-col :span="10">
|
|
<el-col :span="10">
|
|
|
- <div class="dashboard-card"
|
|
|
|
|
- v-if="alarmTypeChart.series[0].data && alarmTypeChart.series[0].data.length > 0"
|
|
|
|
|
- >
|
|
|
|
|
- <PieChartBlock title="告警类型分布" :opt-cfg="alarmTypeChart" style="height: 100%"/>
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- <div class="dashboard-card empty-state" v-else>
|
|
|
|
|
- <div class="card-title">告警类型分布</div>
|
|
|
|
|
- <el-empty description="暂无告警类型数据" :image-size="120"></el-empty>
|
|
|
|
|
|
|
+ <div class="chart-card">
|
|
|
|
|
+ <div class="card-header">
|
|
|
|
|
+ <span class="card-title">
|
|
|
|
|
+ <i class="el-icon-s-data"></i> 告警级别分布
|
|
|
|
|
+ </span>
|
|
|
|
|
+ <span class="card-subtitle">本年度</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div ref="alarmLevelChart" class="chart-body" v-if="hasAlarmLevelData"></div>
|
|
|
|
|
+ <div class="empty-chart" v-else>
|
|
|
|
|
+ <el-empty description="暂无告警数据" :image-size="100"></el-empty>
|
|
|
|
|
+ </div>
|
|
|
</div>
|
|
</div>
|
|
|
</el-col>
|
|
</el-col>
|
|
|
</el-row>
|
|
</el-row>
|
|
|
</el-col>
|
|
</el-col>
|
|
|
</el-row>
|
|
</el-row>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 告警处置对话框 -->
|
|
|
|
|
+ <el-dialog
|
|
|
|
|
+ :title="handleDialogTitle"
|
|
|
|
|
+ :visible.sync="handleDialogVisible"
|
|
|
|
|
+ width="500px"
|
|
|
|
|
+ append-to-body
|
|
|
|
|
+ >
|
|
|
|
|
+ <el-form ref="handleForm" :model="handleForm" label-width="80px">
|
|
|
|
|
+ <el-form-item label="告警目标">
|
|
|
|
|
+ <span>{{ currentAlarm.targetName }}</span>
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ <el-form-item label="告警内容">
|
|
|
|
|
+ <span>{{ currentAlarm.alarmMsg }}</span>
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ <el-form-item :label="handleForm.type === 'resolve' ? '解决说明' : '处置说明'">
|
|
|
|
|
+ <el-input
|
|
|
|
|
+ v-model="handleForm.content"
|
|
|
|
|
+ type="textarea"
|
|
|
|
|
+ :rows="3"
|
|
|
|
|
+ :placeholder="handleForm.type === 'resolve' ? '请描述解决方案' : '请描述处置措施'"
|
|
|
|
|
+ />
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ </el-form>
|
|
|
|
|
+ <div slot="footer">
|
|
|
|
|
+ <el-button @click="handleDialogVisible = false">取 消</el-button>
|
|
|
|
|
+ <el-button type="primary" @click="submitAlarmHandle" :loading="handleSubmitting">确 定</el-button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </el-dialog>
|
|
|
</div>
|
|
</div>
|
|
|
</template>
|
|
</template>
|
|
|
|
|
|
|
|
<script>
|
|
<script>
|
|
|
|
|
+import * as echarts from 'echarts'
|
|
|
import { areaTreeSelect } from '@/api/basecfg/area'
|
|
import { areaTreeSelect } from '@/api/basecfg/area'
|
|
|
-import {
|
|
|
|
|
- listAlarmInfo,
|
|
|
|
|
- updateAlarmInfo,
|
|
|
|
|
- fetchAlarmIndexDay,
|
|
|
|
|
- fetchAlarmIndexMonth,
|
|
|
|
|
- fetchAlarmIndexYear,
|
|
|
|
|
- fetchAlarmIndex
|
|
|
|
|
-} from '@/api/alarm/alarm-info'
|
|
|
|
|
import { listDeviceStatus } from '@/api/device/device'
|
|
import { listDeviceStatus } from '@/api/device/device'
|
|
|
-import BlockTable from '@/components/Block/BlockTable/index.vue'
|
|
|
|
|
-import LineChartBlock from '@/components/Block/charts/LineChartBlock.vue'
|
|
|
|
|
-import PieChartBlock from '@/components/Block/charts/PieChartBlock.vue'
|
|
|
|
|
-import SwitchTag from '@/components/SwitchTag/index.vue'
|
|
|
|
|
-import { DateTool } from '@/utils/DateTool'
|
|
|
|
|
-import dayjs from 'dayjs'
|
|
|
|
|
-import _ from 'lodash'
|
|
|
|
|
|
|
+import {
|
|
|
|
|
+ listAlarm,
|
|
|
|
|
+ listActiveAlarm,
|
|
|
|
|
+ confirmAlarm,
|
|
|
|
|
+ handleAlarm,
|
|
|
|
|
+ resolveAlarm,
|
|
|
|
|
+ closeAlarm,
|
|
|
|
|
+ getAlarmStatsOverview,
|
|
|
|
|
+ countAlarmByLevel,
|
|
|
|
|
+ countAlarmByTrend
|
|
|
|
|
+} from '@/api/alarm/alarm'
|
|
|
|
|
+import { parseTime } from '@/utils/ruoyi'
|
|
|
|
|
+
|
|
|
|
|
+// 告警级别配置 - 适配新模型
|
|
|
|
|
+const ALARM_LEVEL_MAP = {
|
|
|
|
|
+ 1: { name: '一般', tag: 'info', color: '#409EFF' },
|
|
|
|
|
+ 2: { name: '重要', tag: 'warning', color: '#E6A23C' },
|
|
|
|
|
+ 3: { name: '紧急', tag: 'danger', color: '#F56C6C' }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 告警状态配置 - 适配新模型
|
|
|
|
|
+const ALARM_STATUS_MAP = {
|
|
|
|
|
+ 0: { name: '活动', tag: 'danger' },
|
|
|
|
|
+ 1: { name: '已确认', tag: 'warning' },
|
|
|
|
|
+ 2: { name: '处置中', tag: 'primary' },
|
|
|
|
|
+ 3: { name: '已解决', tag: 'success' },
|
|
|
|
|
+ 4: { name: '已关闭', tag: 'info' },
|
|
|
|
|
+ 5: { name: '已恢复', tag: 'success' }
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
export default {
|
|
export default {
|
|
|
- name: 'DeviceAlarm',
|
|
|
|
|
- components: {
|
|
|
|
|
- BlockTable,
|
|
|
|
|
- LineChartBlock,
|
|
|
|
|
- PieChartBlock,
|
|
|
|
|
- SwitchTag
|
|
|
|
|
- },
|
|
|
|
|
|
|
+ name: 'DeviceAlarmDashboard',
|
|
|
data() {
|
|
data() {
|
|
|
return {
|
|
return {
|
|
|
// 区域筛选
|
|
// 区域筛选
|
|
@@ -174,28 +309,46 @@ export default {
|
|
|
children: 'children',
|
|
children: 'children',
|
|
|
label: 'label'
|
|
label: 'label'
|
|
|
},
|
|
},
|
|
|
|
|
+ areaAlarmCounts: {},
|
|
|
|
|
+
|
|
|
|
|
+ // KPI 数据
|
|
|
|
|
+ kpiData: {
|
|
|
|
|
+ deviceTotal: 0,
|
|
|
|
|
+ onlineCount: 0,
|
|
|
|
|
+ offlineCount: 0,
|
|
|
|
|
+ activeAlarmCount: 0,
|
|
|
|
|
+ urgentCount: 0
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 趋势图类型
|
|
|
|
|
+ trendType: 'day',
|
|
|
|
|
+
|
|
|
|
|
+ // 实时告警
|
|
|
|
|
+ tableLoading: false,
|
|
|
|
|
+ realTimeAlarmList: [],
|
|
|
|
|
+
|
|
|
|
|
+ // 告警级别分布数据
|
|
|
|
|
+ hasAlarmLevelData: false,
|
|
|
|
|
+
|
|
|
|
|
+ // 图表实例
|
|
|
|
|
+ charts: {
|
|
|
|
|
+ deviceStatus: null,
|
|
|
|
|
+ alarmTrend: null,
|
|
|
|
|
+ alarmLevel: null
|
|
|
|
|
+ },
|
|
|
|
|
|
|
|
- // 时间维度
|
|
|
|
|
- dateType: { val: 'year', text: '按年' },
|
|
|
|
|
- dateTypeOptions: [
|
|
|
|
|
- { val: 'day', text: '按日' },
|
|
|
|
|
- { val: 'month', text: '按月' },
|
|
|
|
|
- { val: 'year', text: '按年' }
|
|
|
|
|
- ],
|
|
|
|
|
-
|
|
|
|
|
- // KPI数据
|
|
|
|
|
- kpiData: [
|
|
|
|
|
- { label: '设备总数', value: 0, icon: 'el-icon-monitor', type: 'primary' },
|
|
|
|
|
- { label: '在线设备', value: 0, icon: 'el-icon-circle-check', type: 'success' },
|
|
|
|
|
- { label: '离线设备', value: 0, icon: 'el-icon-warning-outline', type: 'danger' },
|
|
|
|
|
- { label: '待处理告警', value: 0, icon: 'el-icon-bell', type: 'warning' }
|
|
|
|
|
- ],
|
|
|
|
|
-
|
|
|
|
|
- // 图表数据初始化
|
|
|
|
|
- deviceStatusChart: { series: [{ type: 'pie', radius: ['50%', '70%'], data: [] }] },
|
|
|
|
|
- alarmTrendChart: { unit: '', xAxis: { type: 'category', data: [] }, series: [] },
|
|
|
|
|
- alarmTypeChart: { series: [{ type: 'pie', radius: ['0%', '70%'], data: [] }] },
|
|
|
|
|
- realTimeAlarmData: []
|
|
|
|
|
|
|
+ // 处置对话框
|
|
|
|
|
+ handleDialogVisible: false,
|
|
|
|
|
+ handleDialogTitle: '处置告警',
|
|
|
|
|
+ handleSubmitting: false,
|
|
|
|
|
+ currentAlarm: {},
|
|
|
|
|
+ handleForm: {
|
|
|
|
|
+ type: 'handle',
|
|
|
|
|
+ content: ''
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 刷新定时器
|
|
|
|
|
+ refreshTimer: null
|
|
|
}
|
|
}
|
|
|
},
|
|
},
|
|
|
watch: {
|
|
watch: {
|
|
@@ -206,11 +359,16 @@ export default {
|
|
|
mounted() {
|
|
mounted() {
|
|
|
this.getAreaTree()
|
|
this.getAreaTree()
|
|
|
},
|
|
},
|
|
|
|
|
+ beforeDestroy() {
|
|
|
|
|
+ this.stopAutoRefresh()
|
|
|
|
|
+ this.disposeCharts()
|
|
|
|
|
+ window.removeEventListener('resize', this.resizeCharts)
|
|
|
|
|
+ },
|
|
|
methods: {
|
|
methods: {
|
|
|
|
|
+ // ==================== 基础方法 ====================
|
|
|
getTreeIcon(data) {
|
|
getTreeIcon(data) {
|
|
|
- if (data.id === '-1') {
|
|
|
|
|
- return 'el-icon-s-home'
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ if (data.id === '-1') return 'el-icon-s-home'
|
|
|
|
|
+ if (data.children && data.children.length > 0) return 'el-icon-folder-opened'
|
|
|
return 'el-icon-office-building'
|
|
return 'el-icon-office-building'
|
|
|
},
|
|
},
|
|
|
|
|
|
|
@@ -223,6 +381,40 @@ export default {
|
|
|
return data.label.indexOf(value) !== -1
|
|
return data.label.indexOf(value) !== -1
|
|
|
},
|
|
},
|
|
|
|
|
|
|
|
|
|
+ getAlarmLevelName(level) {
|
|
|
|
|
+ return ALARM_LEVEL_MAP[level]?.name || '未知'
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ getAlarmLevelTag(level) {
|
|
|
|
|
+ return ALARM_LEVEL_MAP[level]?.tag || 'info'
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ getAlarmStatusName(status) {
|
|
|
|
|
+ return ALARM_STATUS_MAP[status]?.name || '未知'
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ getAlarmStatusTag(status) {
|
|
|
|
|
+ return ALARM_STATUS_MAP[status]?.tag || 'info'
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ formatTime(time) {
|
|
|
|
|
+ return parseTime(time, '{m}-{d} {h}:{i}')
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ formatAlarmContent(row) {
|
|
|
|
|
+ const attrName = row.attrName || row.attrKey || '属性'
|
|
|
|
|
+ return `${row.targetName || '设备'} ${attrName}异常`
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ tableRowClassName({ row }) {
|
|
|
|
|
+ if (row.alarmStatus === 0) {
|
|
|
|
|
+ if (row.alarmLevel === 3) return 'urgent-row'
|
|
|
|
|
+ if (row.alarmLevel === 2) return 'important-row'
|
|
|
|
|
+ }
|
|
|
|
|
+ return ''
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // ==================== 数据加载 ====================
|
|
|
async getAreaTree() {
|
|
async getAreaTree() {
|
|
|
try {
|
|
try {
|
|
|
const response = await areaTreeSelect('0', 1)
|
|
const response = await areaTreeSelect('0', 1)
|
|
@@ -235,7 +427,10 @@ export default {
|
|
|
if (this.$refs.tree) {
|
|
if (this.$refs.tree) {
|
|
|
this.$refs.tree.setCurrentKey('-1')
|
|
this.$refs.tree.setCurrentKey('-1')
|
|
|
}
|
|
}
|
|
|
|
|
+ this.initCharts()
|
|
|
this.loadAllData()
|
|
this.loadAllData()
|
|
|
|
|
+ this.startAutoRefresh()
|
|
|
|
|
+ window.addEventListener('resize', this.resizeCharts)
|
|
|
})
|
|
})
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
|
console.error('获取区域树失败:', error)
|
|
console.error('获取区域树失败:', error)
|
|
@@ -248,15 +443,10 @@ export default {
|
|
|
this.loadAllData()
|
|
this.loadAllData()
|
|
|
},
|
|
},
|
|
|
|
|
|
|
|
- async onDateTypeSwitch(item) {
|
|
|
|
|
- this.dateType = item
|
|
|
|
|
- await this.loadAlarmTrend()
|
|
|
|
|
- },
|
|
|
|
|
-
|
|
|
|
|
- buildQueryParams(extraParams = {}) {
|
|
|
|
|
|
|
+ buildQueryParams(extra = {}) {
|
|
|
return {
|
|
return {
|
|
|
- areaCode: this.areaCode || '',
|
|
|
|
|
- ...extraParams
|
|
|
|
|
|
|
+ areaCode: this.areaCode || undefined,
|
|
|
|
|
+ ...extra
|
|
|
}
|
|
}
|
|
|
},
|
|
},
|
|
|
|
|
|
|
@@ -267,403 +457,685 @@ export default {
|
|
|
this.loadDeviceStatus(),
|
|
this.loadDeviceStatus(),
|
|
|
this.loadAlarmTrend(),
|
|
this.loadAlarmTrend(),
|
|
|
this.loadRealTimeAlarm(),
|
|
this.loadRealTimeAlarm(),
|
|
|
- this.loadAlarmTypeDistribution()
|
|
|
|
|
|
|
+ this.loadAlarmLevelDistribution()
|
|
|
])
|
|
])
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
|
console.error('加载数据失败:', error)
|
|
console.error('加载数据失败:', error)
|
|
|
}
|
|
}
|
|
|
},
|
|
},
|
|
|
|
|
|
|
|
|
|
+ // 加载 KPI 数据
|
|
|
async loadKpiData() {
|
|
async loadKpiData() {
|
|
|
try {
|
|
try {
|
|
|
|
|
+ // 设备状态
|
|
|
const deviceRes = await listDeviceStatus(this.buildQueryParams())
|
|
const deviceRes = await listDeviceStatus(this.buildQueryParams())
|
|
|
const deviceData = deviceRes.data || {}
|
|
const deviceData = deviceRes.data || {}
|
|
|
- const alarmRes = await listAlarmInfo(
|
|
|
|
|
- this.buildQueryParams({ pageNum: 1, pageSize: 1, alarmStateList: [0, 1] })
|
|
|
|
|
- )
|
|
|
|
|
-
|
|
|
|
|
- this.kpiData = [
|
|
|
|
|
- { label: '设备总数', value: deviceData.total || 0, icon: 'el-icon-monitor', type: 'primary' },
|
|
|
|
|
- { label: '在线设备', value: deviceData.onlineCount || 0, icon: 'el-icon-circle-check', type: 'success' },
|
|
|
|
|
- { label: '离线设备', value: deviceData.offlineCount || 0, icon: 'el-icon-warning-outline', type: 'danger' },
|
|
|
|
|
- { label: '待处理告警', value: alarmRes.total || 0, icon: 'el-icon-bell', type: 'warning' }
|
|
|
|
|
- ]
|
|
|
|
|
|
|
+
|
|
|
|
|
+ // 告警统计 - 使用新接口
|
|
|
|
|
+ const alarmRes = await getAlarmStatsOverview(this.buildQueryParams())
|
|
|
|
|
+ const alarmData = alarmRes.data || {}
|
|
|
|
|
+
|
|
|
|
|
+ this.kpiData = {
|
|
|
|
|
+ deviceTotal: deviceData.total || 0,
|
|
|
|
|
+ onlineCount: deviceData.onlineCount || 0,
|
|
|
|
|
+ offlineCount: deviceData.offlineCount || 0,
|
|
|
|
|
+ activeAlarmCount: alarmData.activeCount || 0,
|
|
|
|
|
+ urgentCount: alarmData.urgentCount || 0
|
|
|
|
|
+ }
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
|
console.error('加载KPI数据失败:', error)
|
|
console.error('加载KPI数据失败:', error)
|
|
|
}
|
|
}
|
|
|
},
|
|
},
|
|
|
|
|
|
|
|
|
|
+ // 加载设备状态分布
|
|
|
async loadDeviceStatus() {
|
|
async loadDeviceStatus() {
|
|
|
try {
|
|
try {
|
|
|
const { data } = await listDeviceStatus(this.buildQueryParams())
|
|
const { data } = await listDeviceStatus(this.buildQueryParams())
|
|
|
- this.deviceStatusChart = {
|
|
|
|
|
- series: [
|
|
|
|
|
- {
|
|
|
|
|
- type: 'pie',
|
|
|
|
|
- radius: ['50%', '70%'],
|
|
|
|
|
- data: [
|
|
|
|
|
- { value: data.onlineCount || 0, name: '在线' },
|
|
|
|
|
- { value: data.offlineCount || 0, name: '离线' }
|
|
|
|
|
- ]
|
|
|
|
|
- }
|
|
|
|
|
- ]
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ this.renderDeviceStatusChart(data || {})
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
|
console.error('加载设备状态失败:', error)
|
|
console.error('加载设备状态失败:', error)
|
|
|
}
|
|
}
|
|
|
},
|
|
},
|
|
|
|
|
|
|
|
|
|
+ // 加载告警趋势 - 使用新接口
|
|
|
async loadAlarmTrend() {
|
|
async loadAlarmTrend() {
|
|
|
try {
|
|
try {
|
|
|
- let xAxis = []
|
|
|
|
|
- let data = []
|
|
|
|
|
const params = this.buildQueryParams()
|
|
const params = this.buildQueryParams()
|
|
|
|
|
+ const res = await countAlarmByTrend(params, this.trendType)
|
|
|
|
|
+ const trendData = res.data || []
|
|
|
|
|
+ this.renderAlarmTrendChart(trendData)
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('加载告警趋势失败:', error)
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
|
|
|
- if (this.dateType.val === 'day') {
|
|
|
|
|
- const res = await fetchAlarmIndexDay(params)
|
|
|
|
|
- data = res.data || []
|
|
|
|
|
- xAxis = DateTool.getTime(24)
|
|
|
|
|
- } else if (this.dateType.val === 'month') {
|
|
|
|
|
- const res = await fetchAlarmIndexMonth(params)
|
|
|
|
|
- data = res.data || []
|
|
|
|
|
- xAxis = DateTool.getDayOfRange(dayjs().subtract(1, 'month'), dayjs(), DateTool.DateFormat.YYYY_MM_DD)
|
|
|
|
|
- } else {
|
|
|
|
|
- const res = await fetchAlarmIndexYear(params)
|
|
|
|
|
- data = res.data || []
|
|
|
|
|
- xAxis = DateTool.getMonthsOfYearAgo()
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ // 加载实时告警 - 使用新接口
|
|
|
|
|
+ async loadRealTimeAlarm() {
|
|
|
|
|
+ this.tableLoading = true
|
|
|
|
|
+ try {
|
|
|
|
|
+ // 使用活动告警接口
|
|
|
|
|
+ const res = await listActiveAlarm(this.areaCode)
|
|
|
|
|
+ // 只取前6条显示
|
|
|
|
|
+ this.realTimeAlarmList = (res.data || []).slice(0, 6)
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('加载实时告警失败:', error)
|
|
|
|
|
+ this.realTimeAlarmList = []
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ this.tableLoading = false
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
|
|
|
- const series = this.transformAlarmTrendData(data, xAxis)
|
|
|
|
|
- this.alarmTrendChart = {
|
|
|
|
|
- unit: '',
|
|
|
|
|
- xAxis: { type: 'category', data: xAxis },
|
|
|
|
|
- series
|
|
|
|
|
|
|
+ // 加载告警级别分布 - 使用新接口
|
|
|
|
|
+ async loadAlarmLevelDistribution() {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const params = this.buildQueryParams()
|
|
|
|
|
+ const res = await countAlarmByLevel(params)
|
|
|
|
|
+ const levelData = res.data || []
|
|
|
|
|
+
|
|
|
|
|
+ this.hasAlarmLevelData = levelData.length > 0
|
|
|
|
|
+ if (this.hasAlarmLevelData) {
|
|
|
|
|
+ this.$nextTick(() => {
|
|
|
|
|
+ this.renderAlarmLevelChart(levelData)
|
|
|
|
|
+ })
|
|
|
}
|
|
}
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
|
- console.error('加载告警趋势失败:', error)
|
|
|
|
|
|
|
+ console.error('加载告警级别分布失败:', error)
|
|
|
|
|
+ this.hasAlarmLevelData = false
|
|
|
}
|
|
}
|
|
|
},
|
|
},
|
|
|
|
|
|
|
|
- transformAlarmTrendData(data, xAxis) {
|
|
|
|
|
- const dayGroup = _.groupBy(data, 'alarmType')
|
|
|
|
|
- const series = []
|
|
|
|
|
- const alarmTypeMap = {
|
|
|
|
|
- 1: '一般告警', 2: '重要告警', 3: '紧急告警', 4: '恢复告警', 5: '诊断告警', 6: '其他告警'
|
|
|
|
|
|
|
+ // ==================== 图表渲染 ====================
|
|
|
|
|
+ initCharts() {
|
|
|
|
|
+ this.charts.deviceStatus = echarts.init(this.$refs.deviceStatusChart)
|
|
|
|
|
+ this.charts.alarmTrend = echarts.init(this.$refs.alarmTrendChart)
|
|
|
|
|
+ if (this.$refs.alarmLevelChart) {
|
|
|
|
|
+ this.charts.alarmLevel = echarts.init(this.$refs.alarmLevelChart)
|
|
|
}
|
|
}
|
|
|
|
|
+ },
|
|
|
|
|
|
|
|
- Object.keys(dayGroup).forEach((alarmType) => {
|
|
|
|
|
- let ds = {}
|
|
|
|
|
- dayGroup[alarmType].forEach((item) => {
|
|
|
|
|
- ds[item.dateIndex] = item.cnt
|
|
|
|
|
- })
|
|
|
|
|
- let seriesData = []
|
|
|
|
|
- xAxis.forEach((item) => seriesData.push(ds[item] || 0))
|
|
|
|
|
- series.push({
|
|
|
|
|
- name: alarmTypeMap[alarmType] || '未知',
|
|
|
|
|
|
|
+ disposeCharts() {
|
|
|
|
|
+ Object.values(this.charts).forEach(chart => {
|
|
|
|
|
+ chart && chart.dispose()
|
|
|
|
|
+ })
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ resizeCharts() {
|
|
|
|
|
+ Object.values(this.charts).forEach(chart => {
|
|
|
|
|
+ chart && chart.resize()
|
|
|
|
|
+ })
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ renderDeviceStatusChart(data) {
|
|
|
|
|
+ const option = {
|
|
|
|
|
+ tooltip: {
|
|
|
|
|
+ trigger: 'item',
|
|
|
|
|
+ formatter: '{b}: {c} ({d}%)'
|
|
|
|
|
+ },
|
|
|
|
|
+ legend: {
|
|
|
|
|
+ orient: 'vertical',
|
|
|
|
|
+ right: 20,
|
|
|
|
|
+ top: 'center'
|
|
|
|
|
+ },
|
|
|
|
|
+ color: ['#67C23A', '#F56C6C'],
|
|
|
|
|
+ series: [{
|
|
|
|
|
+ type: 'pie',
|
|
|
|
|
+ radius: ['45%', '70%'],
|
|
|
|
|
+ center: ['40%', '50%'],
|
|
|
|
|
+ avoidLabelOverlap: true,
|
|
|
|
|
+ itemStyle: {
|
|
|
|
|
+ borderRadius: 8,
|
|
|
|
|
+ borderColor: '#fff',
|
|
|
|
|
+ borderWidth: 2
|
|
|
|
|
+ },
|
|
|
|
|
+ label: {
|
|
|
|
|
+ show: true,
|
|
|
|
|
+ formatter: '{b}\n{c}台'
|
|
|
|
|
+ },
|
|
|
|
|
+ emphasis: {
|
|
|
|
|
+ label: {
|
|
|
|
|
+ show: true,
|
|
|
|
|
+ fontSize: 14,
|
|
|
|
|
+ fontWeight: 'bold'
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ data: [
|
|
|
|
|
+ { value: data.onlineCount || 0, name: '在线' },
|
|
|
|
|
+ { value: data.offlineCount || 0, name: '离线' }
|
|
|
|
|
+ ]
|
|
|
|
|
+ }]
|
|
|
|
|
+ }
|
|
|
|
|
+ this.charts.deviceStatus && this.charts.deviceStatus.setOption(option, true)
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ renderAlarmTrendChart(trendData) {
|
|
|
|
|
+ const xData = trendData.map(item => item.timeIndex)
|
|
|
|
|
+ const yData = trendData.map(item => item.cnt)
|
|
|
|
|
+
|
|
|
|
|
+ const option = {
|
|
|
|
|
+ tooltip: {
|
|
|
|
|
+ trigger: 'axis',
|
|
|
|
|
+ axisPointer: { type: 'cross' }
|
|
|
|
|
+ },
|
|
|
|
|
+ grid: {
|
|
|
|
|
+ left: '3%',
|
|
|
|
|
+ right: '4%',
|
|
|
|
|
+ bottom: '3%',
|
|
|
|
|
+ top: '10%',
|
|
|
|
|
+ containLabel: true
|
|
|
|
|
+ },
|
|
|
|
|
+ xAxis: {
|
|
|
|
|
+ type: 'category',
|
|
|
|
|
+ boundaryGap: false,
|
|
|
|
|
+ data: xData,
|
|
|
|
|
+ axisLine: { lineStyle: { color: '#dcdfe6' } },
|
|
|
|
|
+ axisLabel: { color: '#606266', fontSize: 10 }
|
|
|
|
|
+ },
|
|
|
|
|
+ yAxis: {
|
|
|
|
|
+ type: 'value',
|
|
|
|
|
+ axisLine: { show: false },
|
|
|
|
|
+ axisTick: { show: false },
|
|
|
|
|
+ splitLine: { lineStyle: { color: '#ebeef5', type: 'dashed' } },
|
|
|
|
|
+ axisLabel: { color: '#909399' }
|
|
|
|
|
+ },
|
|
|
|
|
+ series: [{
|
|
|
|
|
+ name: '告警数',
|
|
|
type: 'line',
|
|
type: 'line',
|
|
|
smooth: true,
|
|
smooth: true,
|
|
|
- data: seriesData
|
|
|
|
|
- })
|
|
|
|
|
|
|
+ symbol: 'circle',
|
|
|
|
|
+ symbolSize: 6,
|
|
|
|
|
+ itemStyle: { color: '#f56c6c' },
|
|
|
|
|
+ lineStyle: { width: 2 },
|
|
|
|
|
+ areaStyle: {
|
|
|
|
|
+ color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
|
|
|
|
+ { offset: 0, color: 'rgba(245, 108, 108, 0.4)' },
|
|
|
|
|
+ { offset: 1, color: 'rgba(245, 108, 108, 0.05)' }
|
|
|
|
|
+ ])
|
|
|
|
|
+ },
|
|
|
|
|
+ data: yData
|
|
|
|
|
+ }]
|
|
|
|
|
+ }
|
|
|
|
|
+ this.charts.alarmTrend && this.charts.alarmTrend.setOption(option, true)
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ renderAlarmLevelChart(levelData) {
|
|
|
|
|
+ if (!this.charts.alarmLevel && this.$refs.alarmLevelChart) {
|
|
|
|
|
+ this.charts.alarmLevel = echarts.init(this.$refs.alarmLevelChart)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const chartData = levelData.map(item => {
|
|
|
|
|
+ const levelInfo = ALARM_LEVEL_MAP[item.alarmLevel]
|
|
|
|
|
+ return {
|
|
|
|
|
+ value: item.cnt,
|
|
|
|
|
+ name: levelInfo?.name || `级别${item.alarmLevel}`,
|
|
|
|
|
+ itemStyle: { color: levelInfo?.color || '#909399' }
|
|
|
|
|
+ }
|
|
|
})
|
|
})
|
|
|
- return series
|
|
|
|
|
|
|
+
|
|
|
|
|
+ const option = {
|
|
|
|
|
+ tooltip: {
|
|
|
|
|
+ trigger: 'item',
|
|
|
|
|
+ formatter: '{b}: {c} ({d}%)'
|
|
|
|
|
+ },
|
|
|
|
|
+ legend: {
|
|
|
|
|
+ orient: 'vertical',
|
|
|
|
|
+ right: 20,
|
|
|
|
|
+ top: 'center'
|
|
|
|
|
+ },
|
|
|
|
|
+ series: [{
|
|
|
|
|
+ type: 'pie',
|
|
|
|
|
+ radius: ['0%', '65%'],
|
|
|
|
|
+ center: ['40%', '50%'],
|
|
|
|
|
+ roseType: 'radius',
|
|
|
|
|
+ itemStyle: {
|
|
|
|
|
+ borderRadius: 6,
|
|
|
|
|
+ borderColor: '#fff',
|
|
|
|
|
+ borderWidth: 2
|
|
|
|
|
+ },
|
|
|
|
|
+ label: {
|
|
|
|
|
+ show: true,
|
|
|
|
|
+ formatter: '{b}\n{c}条'
|
|
|
|
|
+ },
|
|
|
|
|
+ data: chartData
|
|
|
|
|
+ }]
|
|
|
|
|
+ }
|
|
|
|
|
+ this.charts.alarmLevel && this.charts.alarmLevel.setOption(option, true)
|
|
|
},
|
|
},
|
|
|
|
|
|
|
|
- async loadRealTimeAlarm() {
|
|
|
|
|
|
|
+ // ==================== 告警操作 ====================
|
|
|
|
|
+ handleAlarmAction(command, row) {
|
|
|
|
|
+ this.currentAlarm = row
|
|
|
|
|
+
|
|
|
|
|
+ switch (command) {
|
|
|
|
|
+ case 'confirm':
|
|
|
|
|
+ this.doConfirmAlarm(row)
|
|
|
|
|
+ break
|
|
|
|
|
+ case 'handle':
|
|
|
|
|
+ this.handleForm.type = 'handle'
|
|
|
|
|
+ this.handleDialogTitle = '处置告警'
|
|
|
|
|
+ this.handleForm.content = ''
|
|
|
|
|
+ this.handleDialogVisible = true
|
|
|
|
|
+ break
|
|
|
|
|
+ case 'resolve':
|
|
|
|
|
+ this.handleForm.type = 'resolve'
|
|
|
|
|
+ this.handleDialogTitle = '解决告警'
|
|
|
|
|
+ this.handleForm.content = ''
|
|
|
|
|
+ this.handleDialogVisible = true
|
|
|
|
|
+ break
|
|
|
|
|
+ case 'close':
|
|
|
|
|
+ this.doCloseAlarm(row)
|
|
|
|
|
+ break
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 确认告警
|
|
|
|
|
+ async doConfirmAlarm(row) {
|
|
|
try {
|
|
try {
|
|
|
- // 【关键修改】将 pageSize 改为 6,以适配 430px 的高度
|
|
|
|
|
- const { rows } = await listAlarmInfo(
|
|
|
|
|
- this.buildQueryParams({ pageNum: 1, pageSize: 6, alarmStateList: [0, 1] })
|
|
|
|
|
- )
|
|
|
|
|
- this.realTimeAlarmData = rows || []
|
|
|
|
|
|
|
+ await confirmAlarm(row.alarmId, '快速确认')
|
|
|
|
|
+ this.$message.success('告警已确认')
|
|
|
|
|
+ this.loadRealTimeAlarm()
|
|
|
|
|
+ this.loadKpiData()
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
|
- console.error('加载实时告警失败:', error)
|
|
|
|
|
|
|
+ this.$message.error('确认失败')
|
|
|
}
|
|
}
|
|
|
},
|
|
},
|
|
|
|
|
|
|
|
- async loadAlarmTypeDistribution() {
|
|
|
|
|
|
|
+ // 关闭告警
|
|
|
|
|
+ async doCloseAlarm(row) {
|
|
|
try {
|
|
try {
|
|
|
- const endTime = dayjs().format('YYYY-MM-DD HH:mm:ss')
|
|
|
|
|
- const startTime = dayjs().startOf('year').format('YYYY-MM-DD HH:mm:ss')
|
|
|
|
|
- const params = this.buildQueryParams({ startRecTime: startTime, endRecTime: endTime })
|
|
|
|
|
- const { data } = await fetchAlarmIndex(params)
|
|
|
|
|
-
|
|
|
|
|
- const alarmTypeMap = {
|
|
|
|
|
- 1: '一般告警', 2: '重要告警', 3: '紧急告警', 4: '恢复告警', 5: '诊断告警', 6: '其他告警'
|
|
|
|
|
- }
|
|
|
|
|
- const chartData = (data || []).map((item) => ({
|
|
|
|
|
- value: item.cnt,
|
|
|
|
|
- name: alarmTypeMap[item.alarmType] || '未知'
|
|
|
|
|
- }))
|
|
|
|
|
-
|
|
|
|
|
- this.alarmTypeChart = {
|
|
|
|
|
- series: [{ type: 'pie', radius: ['0%', '70%'], data: chartData }]
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ await this.$confirm('确定关闭此告警吗?', '提示', {
|
|
|
|
|
+ type: 'warning'
|
|
|
|
|
+ })
|
|
|
|
|
+ await closeAlarm(row.alarmId, '快速关闭')
|
|
|
|
|
+ this.$message.success('告警已关闭')
|
|
|
|
|
+ this.loadRealTimeAlarm()
|
|
|
|
|
+ this.loadKpiData()
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
|
- console.error('加载告警类型分布失败:', error)
|
|
|
|
|
|
|
+ if (error !== 'cancel') {
|
|
|
|
|
+ this.$message.error('关闭失败')
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
},
|
|
},
|
|
|
|
|
|
|
|
- async handleAlarmStateChange(command, row) {
|
|
|
|
|
|
|
+ // 提交处置
|
|
|
|
|
+ async submitAlarmHandle() {
|
|
|
|
|
+ if (!this.handleForm.content) {
|
|
|
|
|
+ this.$message.warning('请输入处置说明')
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ this.handleSubmitting = true
|
|
|
try {
|
|
try {
|
|
|
- await updateAlarmInfo({ id: row.id, alarmState: command })
|
|
|
|
|
- this.$message.success('告警状态更新成功')
|
|
|
|
|
- await this.loadRealTimeAlarm()
|
|
|
|
|
- await this.loadKpiData()
|
|
|
|
|
|
|
+ if (this.handleForm.type === 'handle') {
|
|
|
|
|
+ await handleAlarm(this.currentAlarm.alarmId, this.handleForm.content, '处置中')
|
|
|
|
|
+ this.$message.success('已开始处置')
|
|
|
|
|
+ } else if (this.handleForm.type === 'resolve') {
|
|
|
|
|
+ await resolveAlarm(this.currentAlarm.alarmId, this.handleForm.content)
|
|
|
|
|
+ this.$message.success('已标记解决')
|
|
|
|
|
+ }
|
|
|
|
|
+ this.handleDialogVisible = false
|
|
|
|
|
+ this.loadRealTimeAlarm()
|
|
|
|
|
+ this.loadKpiData()
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
|
- this.$message.error('告警状态更新失败')
|
|
|
|
|
|
|
+ this.$message.error('操作失败')
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ this.handleSubmitting = false
|
|
|
}
|
|
}
|
|
|
},
|
|
},
|
|
|
|
|
|
|
|
- getAlarmTypeName(type) {
|
|
|
|
|
- const map = { 1: '一般', 2: '重要', 3: '紧急', 4: '恢复', 5: '诊断', 6: '其他' }
|
|
|
|
|
- return map[type] || '未知'
|
|
|
|
|
- },
|
|
|
|
|
- getAlarmTypeTag(type) {
|
|
|
|
|
- const map = { 1: 'info', 2: 'warning', 3: 'danger', 4: 'success', 5: '', 6: 'info' }
|
|
|
|
|
- return map[type] || 'info'
|
|
|
|
|
- },
|
|
|
|
|
- getAlarmStateName(state) {
|
|
|
|
|
- const map = { 0: '新增', 1: '处理中', 2: '已处置', 3: '已消散' }
|
|
|
|
|
- return map[state] || '未知'
|
|
|
|
|
|
|
+ // ==================== 自动刷新 ====================
|
|
|
|
|
+ startAutoRefresh() {
|
|
|
|
|
+ this.refreshTimer = setInterval(() => {
|
|
|
|
|
+ this.loadRealTimeAlarm()
|
|
|
|
|
+ this.loadKpiData()
|
|
|
|
|
+ }, 30000)
|
|
|
},
|
|
},
|
|
|
- getAlarmStateTag(state) {
|
|
|
|
|
- const map = { 0: 'danger', 1: 'warning', 2: 'success', 3: 'info' }
|
|
|
|
|
- return map[state] || 'info'
|
|
|
|
|
|
|
+
|
|
|
|
|
+ stopAutoRefresh() {
|
|
|
|
|
+ if (this.refreshTimer) {
|
|
|
|
|
+ clearInterval(this.refreshTimer)
|
|
|
|
|
+ this.refreshTimer = null
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
</script>
|
|
</script>
|
|
|
|
|
|
|
|
<style scoped lang="scss">
|
|
<style scoped lang="scss">
|
|
|
-.device-alarm-content {
|
|
|
|
|
- background: #f5f7fa;
|
|
|
|
|
|
|
+.device-alarm-dashboard {
|
|
|
padding: 20px;
|
|
padding: 20px;
|
|
|
|
|
+ background: linear-gradient(135deg, #f5f7fa 0%, #e8ecf1 100%);
|
|
|
min-height: calc(100vh - 84px);
|
|
min-height: calc(100vh - 84px);
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
- // ---------------------------------------------------------
|
|
|
|
|
- // 核心样式:统一卡片容器
|
|
|
|
|
- // ---------------------------------------------------------
|
|
|
|
|
- .dashboard-card {
|
|
|
|
|
- height: 430px; // 【关键】统一四个板块的高度
|
|
|
|
|
- background: #fff;
|
|
|
|
|
- border-radius: 8px;
|
|
|
|
|
- padding: 15px;
|
|
|
|
|
- box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
|
|
|
|
- box-sizing: border-box;
|
|
|
|
|
- display: flex;
|
|
|
|
|
- flex-direction: column;
|
|
|
|
|
- overflow: hidden;
|
|
|
|
|
|
|
+// 左侧树面板
|
|
|
|
|
+.tree-panel {
|
|
|
|
|
+ background: #fff;
|
|
|
|
|
+ border-radius: 12px;
|
|
|
|
|
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
|
|
|
- // 空状态样式
|
|
|
|
|
- &.empty-state {
|
|
|
|
|
- .card-title {
|
|
|
|
|
- font-size: 16px;
|
|
|
|
|
- font-weight: bold;
|
|
|
|
|
- color: #333;
|
|
|
|
|
- border-left: 4px solid #409EFF;
|
|
|
|
|
- padding-left: 10px;
|
|
|
|
|
- line-height: 1;
|
|
|
|
|
- margin-bottom: 20px;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ .tree-search {
|
|
|
|
|
+ padding: 16px;
|
|
|
|
|
+ border-bottom: 1px solid #f0f2f5;
|
|
|
|
|
|
|
|
- ::v-deep .el-empty {
|
|
|
|
|
- flex: 1;
|
|
|
|
|
- display: flex;
|
|
|
|
|
- flex-direction: column;
|
|
|
|
|
- justify-content: center;
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ ::v-deep .el-input__inner {
|
|
|
|
|
+ border-radius: 20px;
|
|
|
|
|
+ border: 1px solid #e4e7ed;
|
|
|
|
|
|
|
|
- // 针对表格卡片的特殊处理
|
|
|
|
|
- &.table-card {
|
|
|
|
|
- // 深度选择器,强行让内部表格适应高度
|
|
|
|
|
- ::v-deep .block-table-container {
|
|
|
|
|
- height: 100%;
|
|
|
|
|
- display: flex;
|
|
|
|
|
- flex-direction: column;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- ::v-deep .el-table {
|
|
|
|
|
- flex: 1; // 撑满剩余高度
|
|
|
|
|
- height: 0; // 关键:Flex布局下允许自适应缩放
|
|
|
|
|
|
|
+ &:focus {
|
|
|
|
|
+ border-color: #409eff;
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 左侧树容器
|
|
|
|
|
- .head-container {
|
|
|
|
|
- background: #fff;
|
|
|
|
|
- padding: 15px;
|
|
|
|
|
- border-radius: 8px;
|
|
|
|
|
- margin-bottom: 15px;
|
|
|
|
|
- box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
|
|
|
|
|
|
+ .tree-content {
|
|
|
|
|
+ max-height: calc(100vh - 200px);
|
|
|
|
|
+ overflow-y: auto;
|
|
|
|
|
+ padding: 8px;
|
|
|
|
|
|
|
|
- &.tree-container {
|
|
|
|
|
- max-height: calc(100vh - 280px);
|
|
|
|
|
- overflow-y: auto;
|
|
|
|
|
|
|
+ &::-webkit-scrollbar {
|
|
|
|
|
+ width: 6px;
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- &::-webkit-scrollbar {
|
|
|
|
|
- width: 6px;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ &::-webkit-scrollbar-thumb {
|
|
|
|
|
+ background: #dcdfe6;
|
|
|
|
|
+ border-radius: 3px;
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- &::-webkit-scrollbar-track {
|
|
|
|
|
- background: #f1f1f1;
|
|
|
|
|
- border-radius: 3px;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ ::v-deep .el-tree {
|
|
|
|
|
+ background: transparent;
|
|
|
|
|
|
|
|
- &::-webkit-scrollbar-thumb {
|
|
|
|
|
- background: #c1c1c1;
|
|
|
|
|
- border-radius: 3px;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ .el-tree-node__content {
|
|
|
|
|
+ height: 44px;
|
|
|
|
|
+ border-radius: 8px;
|
|
|
|
|
+ margin: 2px 0;
|
|
|
|
|
|
|
|
- &::-webkit-scrollbar-thumb:hover {
|
|
|
|
|
- background: #a8a8a8;
|
|
|
|
|
|
|
+ &:hover {
|
|
|
|
|
+ background: #f5f7fa;
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- ::v-deep .el-tree {
|
|
|
|
|
- background: transparent;
|
|
|
|
|
-
|
|
|
|
|
- .el-tree-node__content {
|
|
|
|
|
- height: 40px;
|
|
|
|
|
- padding: 0 8px;
|
|
|
|
|
- transition: all 0.3s;
|
|
|
|
|
-
|
|
|
|
|
- &:hover {
|
|
|
|
|
- background-color: #f5f7fa;
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ .el-tree-node.is-current > .el-tree-node__content {
|
|
|
|
|
+ background: linear-gradient(135deg, #ecf5ff 0%, #e6f1fc 100%);
|
|
|
|
|
+ color: #409eff;
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- .el-tree-node.is-current > .el-tree-node__content {
|
|
|
|
|
- background-color: #ecf5ff;
|
|
|
|
|
- color: #409eff;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ .custom-tree-node {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ padding-right: 8px;
|
|
|
|
|
|
|
|
- .custom-tree-node {
|
|
|
|
|
- flex: 1;
|
|
|
|
|
|
|
+ .tree-label {
|
|
|
display: flex;
|
|
display: flex;
|
|
|
align-items: center;
|
|
align-items: center;
|
|
|
- justify-content: space-between;
|
|
|
|
|
- font-size: 14px;
|
|
|
|
|
-
|
|
|
|
|
- .tree-label {
|
|
|
|
|
- display: flex;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
-
|
|
|
|
|
- .tree-icon {
|
|
|
|
|
- margin-right: 8px;
|
|
|
|
|
- font-size: 16px;
|
|
|
|
|
- color: #909399;
|
|
|
|
|
- transition: color 0.3s;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+
|
|
|
|
|
+ .tree-icon {
|
|
|
|
|
+ margin-right: 8px;
|
|
|
|
|
+ font-size: 16px;
|
|
|
|
|
+ color: #909399;
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- .el-tree-node.is-current .tree-icon {
|
|
|
|
|
- color: #409eff;
|
|
|
|
|
|
|
+ .area-badge {
|
|
|
|
|
+ ::v-deep .el-badge__content {
|
|
|
|
|
+ background: #f56c6c;
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
- // KPI卡片样式
|
|
|
|
|
- .kpi-cards {
|
|
|
|
|
- margin-bottom: 20px;
|
|
|
|
|
-
|
|
|
|
|
- .kpi-card {
|
|
|
|
|
- background: #fff;
|
|
|
|
|
- border-radius: 8px;
|
|
|
|
|
- padding: 20px;
|
|
|
|
|
- display: flex;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
- box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
|
|
|
|
- transition: all 0.3s;
|
|
|
|
|
- position: relative;
|
|
|
|
|
- overflow: hidden;
|
|
|
|
|
-
|
|
|
|
|
- &:hover {
|
|
|
|
|
- transform: translateY(-4px);
|
|
|
|
|
- box-shadow: 0 4px 16px 0 rgba(0, 0, 0, 0.15);
|
|
|
|
|
- }
|
|
|
|
|
|
|
+// KPI 卡片
|
|
|
|
|
+.kpi-row {
|
|
|
|
|
+ display: grid;
|
|
|
|
|
+ grid-template-columns: repeat(4, 1fr);
|
|
|
|
|
+ gap: 16px;
|
|
|
|
|
+ margin-bottom: 20px;
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
- .kpi-icon {
|
|
|
|
|
- width: 60px;
|
|
|
|
|
- height: 60px;
|
|
|
|
|
- border-radius: 50%;
|
|
|
|
|
- display: flex;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
- justify-content: center;
|
|
|
|
|
- font-size: 28px;
|
|
|
|
|
- margin-right: 15px;
|
|
|
|
|
- flex-shrink: 0;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+.kpi-card {
|
|
|
|
|
+ background: #fff;
|
|
|
|
|
+ border-radius: 12px;
|
|
|
|
|
+ padding: 20px;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.06);
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ transition: all 0.3s ease;
|
|
|
|
|
+
|
|
|
|
|
+ &:hover {
|
|
|
|
|
+ transform: translateY(-4px);
|
|
|
|
|
+ box-shadow: 0 8px 30px rgba(0, 0, 0, 0.12);
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- .kpi-content {
|
|
|
|
|
- flex: 1;
|
|
|
|
|
|
|
+ &::before {
|
|
|
|
|
+ content: '';
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ top: 0;
|
|
|
|
|
+ left: 0;
|
|
|
|
|
+ width: 4px;
|
|
|
|
|
+ height: 100%;
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- .kpi-value {
|
|
|
|
|
- font-size: 28px;
|
|
|
|
|
- font-weight: bold;
|
|
|
|
|
- line-height: 1.2;
|
|
|
|
|
- margin-bottom: 5px;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ &.total::before { background: linear-gradient(180deg, #667eea 0%, #764ba2 100%); }
|
|
|
|
|
+ &.online::before { background: linear-gradient(180deg, #67c23a 0%, #95d475 100%); }
|
|
|
|
|
+ &.offline::before { background: linear-gradient(180deg, #f56c6c 0%, #ff7875 100%); }
|
|
|
|
|
+ &.alarm::before { background: linear-gradient(180deg, #e6a23c 0%, #f5af19 100%); }
|
|
|
|
|
|
|
|
- .kpi-label {
|
|
|
|
|
- font-size: 14px;
|
|
|
|
|
- color: #666;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ .kpi-icon {
|
|
|
|
|
+ width: 56px;
|
|
|
|
|
+ height: 56px;
|
|
|
|
|
+ border-radius: 12px;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ margin-right: 16px;
|
|
|
|
|
+
|
|
|
|
|
+ i { font-size: 26px; color: #fff; }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &.total .kpi-icon { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); }
|
|
|
|
|
+ &.online .kpi-icon { background: linear-gradient(135deg, #67c23a 0%, #95d475 100%); }
|
|
|
|
|
+ &.offline .kpi-icon { background: linear-gradient(135deg, #f56c6c 0%, #ff7875 100%); }
|
|
|
|
|
+ &.alarm .kpi-icon { background: linear-gradient(135deg, #e6a23c 0%, #f5af19 100%); }
|
|
|
|
|
+
|
|
|
|
|
+ .kpi-info {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+
|
|
|
|
|
+ .kpi-value {
|
|
|
|
|
+ font-size: 28px;
|
|
|
|
|
+ font-weight: 700;
|
|
|
|
|
+ color: #303133;
|
|
|
|
|
+ line-height: 1.2;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .kpi-label {
|
|
|
|
|
+ font-size: 13px;
|
|
|
|
|
+ color: #909399;
|
|
|
|
|
+ margin-top: 4px;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .kpi-rate {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ top: 12px;
|
|
|
|
|
+ right: 12px;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ color: #67c23a;
|
|
|
|
|
+ background: rgba(103, 194, 58, 0.1);
|
|
|
|
|
+ padding: 2px 8px;
|
|
|
|
|
+ border-radius: 10px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .kpi-pulse {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ top: 12px;
|
|
|
|
|
+ right: 12px;
|
|
|
|
|
+ width: 10px;
|
|
|
|
|
+ height: 10px;
|
|
|
|
|
+ background: #f56c6c;
|
|
|
|
|
+ border-radius: 50%;
|
|
|
|
|
+ animation: pulse 1.5s infinite;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .kpi-urgent {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ top: 12px;
|
|
|
|
|
+ right: 12px;
|
|
|
|
|
+ font-size: 11px;
|
|
|
|
|
+ color: #f56c6c;
|
|
|
|
|
+ background: rgba(245, 108, 108, 0.1);
|
|
|
|
|
+ padding: 2px 8px;
|
|
|
|
|
+ border-radius: 10px;
|
|
|
|
|
+
|
|
|
|
|
+ i { margin-right: 2px; }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .kpi-decoration {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ right: -20px;
|
|
|
|
|
+ bottom: -20px;
|
|
|
|
|
+ width: 80px;
|
|
|
|
|
+ height: 80px;
|
|
|
|
|
+ border-radius: 50%;
|
|
|
|
|
+ opacity: 0.1;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &.total .kpi-decoration { background: #667eea; }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+@keyframes pulse {
|
|
|
|
|
+ 0%, 100% { opacity: 1; transform: scale(1); }
|
|
|
|
|
+ 50% { opacity: 0.5; transform: scale(1.2); }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 图表行
|
|
|
|
|
+.chart-row {
|
|
|
|
|
+ margin-bottom: 20px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.chart-card {
|
|
|
|
|
+ background: #fff;
|
|
|
|
|
+ border-radius: 12px;
|
|
|
|
|
+ padding: 16px;
|
|
|
|
|
+ height: 380px;
|
|
|
|
|
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.06);
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+
|
|
|
|
|
+ &.table-card {
|
|
|
|
|
+ height: 420px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .card-header {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ margin-bottom: 12px;
|
|
|
|
|
+ padding-bottom: 12px;
|
|
|
|
|
+ border-bottom: 1px solid #f0f2f5;
|
|
|
|
|
+
|
|
|
|
|
+ .card-title {
|
|
|
|
|
+ font-size: 15px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ color: #303133;
|
|
|
|
|
+
|
|
|
|
|
+ i {
|
|
|
|
|
+ margin-right: 6px;
|
|
|
|
|
+ color: #409eff;
|
|
|
}
|
|
}
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- &.kpi-primary {
|
|
|
|
|
- .kpi-icon {
|
|
|
|
|
- background: rgba(64, 158, 255, 0.1);
|
|
|
|
|
- color: #409eff;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ .card-subtitle {
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ color: #909399;
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- .kpi-value {
|
|
|
|
|
- color: #409eff;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ .table-count {
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ color: #909399;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .chart-body {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ min-height: 0;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .table-body {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+
|
|
|
|
|
+ ::v-deep .el-table {
|
|
|
|
|
+ height: 100% !important;
|
|
|
|
|
+
|
|
|
|
|
+ .urgent-row {
|
|
|
|
|
+ background: rgba(245, 108, 108, 0.08) !important;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- &.kpi-success {
|
|
|
|
|
- .kpi-icon {
|
|
|
|
|
- background: rgba(103, 194, 58, 0.1);
|
|
|
|
|
- color: #67c23a;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ .important-row {
|
|
|
|
|
+ background: rgba(230, 162, 60, 0.08) !important;
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- .kpi-value {
|
|
|
|
|
- color: #67c23a;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ .device-name {
|
|
|
|
|
+ font-weight: 500;
|
|
|
|
|
+ color: #303133;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- &.kpi-danger {
|
|
|
|
|
- .kpi-icon {
|
|
|
|
|
- background: rgba(245, 108, 108, 0.1);
|
|
|
|
|
- color: #f56c6c;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ .alarm-msg {
|
|
|
|
|
+ color: #e6a23c;
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- .kpi-value {
|
|
|
|
|
- color: #f56c6c;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ .alarm-time {
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ color: #909399;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- &.kpi-warning {
|
|
|
|
|
- .kpi-icon {
|
|
|
|
|
- background: rgba(230, 162, 60, 0.1);
|
|
|
|
|
- color: #e6a23c;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ .status-dropdown {
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
|
|
|
- .kpi-value {
|
|
|
|
|
- color: #e6a23c;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ i { margin-left: 2px; }
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- .el-dropdown-link {
|
|
|
|
|
- cursor: pointer;
|
|
|
|
|
|
|
+ .empty-chart {
|
|
|
|
|
+ flex: 1;
|
|
|
display: flex;
|
|
display: flex;
|
|
|
align-items: center;
|
|
align-items: center;
|
|
|
justify-content: center;
|
|
justify-content: center;
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+// 闪烁效果
|
|
|
|
|
+.blink {
|
|
|
|
|
+ animation: blink 1s infinite;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+@keyframes blink {
|
|
|
|
|
+ 0%, 100% { opacity: 1; }
|
|
|
|
|
+ 50% { opacity: 0.5; }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 响应式
|
|
|
|
|
+@media (max-width: 1400px) {
|
|
|
|
|
+ .kpi-row {
|
|
|
|
|
+ grid-template-columns: repeat(2, 1fr);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+@media (max-width: 768px) {
|
|
|
|
|
+ .device-alarm-dashboard {
|
|
|
|
|
+ padding: 10px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .kpi-row {
|
|
|
|
|
+ grid-template-columns: 1fr;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
</style>
|
|
</style>
|