| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399 |
- <!-- 数据统计 - 井盖设备智能终端运维看板(报警运维看板) -->
- <template>
- <div class="alarm-dashboard">
- <header class="page-header">
- <div class="header-left">
- <el-icon><Monitor /></el-icon>
- <span class="title">井盖设备智能终端运维看板</span>
- </div>
- <el-button :icon="Refresh" @click="loadAll" :loading="loading">刷新</el-button>
- </header>
- <!-- 第一行指标 -->
- <div class="kpi-grid">
- <div class="kpi-card">
- <div class="kpi-head"><span class="kpi-icon"><el-icon><Calendar /></el-icon></span>今日新增报警</div>
- <div class="kpi-value green">{{ kpi.todayCount }}</div>
- <div class="kpi-foot">较昨日 <span :class="diffClass">{{ diffText }}</span></div>
- </div>
- <div class="kpi-card">
- <div class="kpi-head"><span class="kpi-icon"><el-icon><Document /></el-icon></span>已转工单</div>
- <div class="kpi-value">{{ kpi.transferredCount }}</div>
- <div class="kpi-foot">已派发处置的报警</div>
- </div>
- <div class="kpi-card">
- <div class="kpi-head"><span class="kpi-icon"><el-icon><Warning /></el-icon></span>未转工单</div>
- <div class="kpi-value orange">{{ kpi.untransferredCount }}</div>
- <div class="kpi-foot">待核实转派的报警</div>
- </div>
- </div>
- <!-- 第二行指标 -->
- <div class="kpi-grid">
- <div class="kpi-card">
- <div class="kpi-head"><span class="kpi-icon"><el-icon><CircleCheck /></el-icon></span>报警处置率</div>
- <div class="kpi-value green">{{ kpi.disposalRate }}<small>%</small></div>
- <div class="kpi-foot">已处置异常占比</div>
- </div>
- <div class="kpi-card">
- <div class="kpi-head"><span class="kpi-icon"><el-icon><Clock /></el-icon></span>平均响应时长</div>
- <div class="kpi-value">{{ formatDuration(kpi.avgResponseHours) }}</div>
- <div class="kpi-foot">报警处置响应</div>
- </div>
- <div class="kpi-card">
- <div class="kpi-head"><span class="kpi-icon"><el-icon><Timer /></el-icon></span>平均处置时长</div>
- <div class="kpi-value">{{ formatDuration(kpi.avgDisposalHours) }}</div>
- <div class="kpi-foot">报警处置完成</div>
- </div>
- </div>
- <!-- 已核实报警 · 分级 -->
- <div class="section-title"><span class="title-bar"></span>已核实报警 · 分级</div>
- <div class="level-grid">
- <div v-for="(item, index) in levels" :key="item.level" class="level-card" :class="'level-' + (index + 1)">
- <div class="level-value">{{ item.count ?? 0 }}</div>
- <div class="level-name">{{ item.name }}</div>
- </div>
- </div>
- <!-- 近 7 日报警数 -->
- <div class="section-title"><span class="title-bar"></span>近 7 日报警数</div>
- <div ref="trendRef" class="trend-chart" v-loading="loading"></div>
- <!-- 实时故障列表 -->
- <div class="section-title"><span class="title-bar"></span>实时故障列表<span class="title-count">共 {{ abnormalAlarms.length }} 条</span></div>
- <el-table :data="pagedAlarms" class="alarm-table" size="small" v-loading="loading">
- <el-table-column prop="deviceCode" label="设备编码" min-width="150" show-overflow-tooltip>
- <template #default="{ row }">{{ row.deviceCode || '-' }}</template>
- </el-table-column>
- <el-table-column prop="deviceName" label="设备名称" min-width="150" show-overflow-tooltip>
- <template #default="{ row }">{{ row.deviceName || '-' }}</template>
- </el-table-column>
- <el-table-column prop="installLocation" label="设备安装位置" min-width="180" show-overflow-tooltip>
- <template #default="{ row }">{{ row.installLocation || '-' }}</template>
- </el-table-column>
- <el-table-column prop="faultDesc" label="故障描述" min-width="200" show-overflow-tooltip>
- <template #default="{ row }">{{ row.faultDesc || '-' }}</template>
- </el-table-column>
- <el-table-column prop="faultTime" label="故障发生时间" min-width="160" show-overflow-tooltip>
- <template #default="{ row }">{{ row.faultTime || '-' }}</template>
- </el-table-column>
- </el-table>
- <div class="pagination-wrapper">
- <el-pagination
- background
- layout="total, sizes, prev, pager, next"
- :total="abnormalAlarms.length"
- v-model:current-page="currentPage"
- v-model:page-size="pageSize"
- :page-sizes="[10, 20, 50]"
- />
- </div>
- </div>
- </template>
- <script setup>
- import { ref, reactive, computed, onMounted, onBeforeUnmount, nextTick } from 'vue'
- import {
- Monitor, Refresh, Calendar, Document, Warning, CircleCheck, Clock, Timer
- } from '@element-plus/icons-vue'
- import { ElMessage } from 'element-plus'
- import * as echarts from 'echarts'
- import { getAlarmDashboard } from '@/api/pipeNetwork/basic'
- // ==================== 状态 ====================
- const loading = ref(false)
- const currentPage = ref(1)
- const pageSize = ref(10)
- const kpi = reactive({
- todayCount: 0,
- yesterdayCount: 0,
- todayDiff: 0,
- transferredCount: 0,
- untransferredCount: 0,
- disposalRate: 0,
- avgResponseHours: null,
- avgDisposalHours: null
- })
- // 四个等级卡片固定展示,接口无数据时全部为 0
- const FALLBACK_LEVELS = [
- { level: '1', name: '一级报警', count: 0 },
- { level: '2', name: '二级报警', count: 0 },
- { level: '3', name: '三级报警', count: 0 },
- { level: '4', name: '四级报警', count: 0 }
- ]
- const levels = ref(FALLBACK_LEVELS.map(item => ({ ...item })))
- const trend = ref([])
- const abnormalAlarms = ref([])
- const trendRef = ref(null)
- let trendChart = null
- // ==================== 展示格式化 ====================
- const diffText = computed(() => {
- const diff = Number(kpi.todayDiff) || 0
- return diff > 0 ? `+${diff}` : String(diff)
- })
- const diffClass = computed(() => {
- const diff = Number(kpi.todayDiff) || 0
- return diff > 0 ? 'up' : diff < 0 ? 'down' : 'flat'
- })
- // 后端返回小时:满 24 小时折算成天,否则展示小时;无有效样本显示占位符
- function formatDuration(hours) {
- if (hours == null || hours === '') return '—'
- const value = Number(hours)
- if (!Number.isFinite(value) || value < 0) return '—'
- if (value >= 24) {
- const days = value / 24
- return `${Number.isInteger(days) ? days : days.toFixed(1)}天`
- }
- return `${value.toFixed(1)}小时`
- }
- function fallbackTrend() {
- const result = []
- const today = new Date()
- for (let i = 6; i >= 0; i--) {
- const day = new Date(today.getFullYear(), today.getMonth(), today.getDate() - i)
- const mm = String(day.getMonth() + 1).padStart(2, '0')
- const dd = String(day.getDate()).padStart(2, '0')
- result.push({ label: `${mm}.${dd}`, count: 0 })
- }
- return result
- }
- const pagedAlarms = computed(() => {
- const start = (currentPage.value - 1) * pageSize.value
- return abnormalAlarms.value.slice(start, start + pageSize.value)
- })
- // ==================== 数据加载 ====================
- async function loadAll() {
- loading.value = true
- try {
- const { data } = await getAlarmDashboard()
- const d = data || {}
- kpi.todayCount = d.todayCount ?? 0
- kpi.yesterdayCount = d.yesterdayCount ?? 0
- kpi.todayDiff = d.todayDiff ?? 0
- kpi.transferredCount = d.transferredCount ?? 0
- kpi.untransferredCount = d.untransferredCount ?? 0
- kpi.disposalRate = d.disposalRate ?? 0
- kpi.avgResponseHours = d.avgResponseHours ?? null
- kpi.avgDisposalHours = d.avgDisposalHours ?? null
- levels.value = Array.isArray(d.levels) && d.levels.length
- ? d.levels.map(item => ({ ...item }))
- : FALLBACK_LEVELS.map(item => ({ ...item }))
- trend.value = Array.isArray(d.trend) && d.trend.length ? d.trend : fallbackTrend()
- abnormalAlarms.value = Array.isArray(d.abnormalAlarms) ? d.abnormalAlarms : []
- currentPage.value = 1
- await nextTick()
- renderTrendChart()
- } catch (error) {
- const msg = error?.message || error?.msg || '未知错误'
- ElMessage.error('统计数据加载失败: ' + msg)
- } finally {
- loading.value = false
- }
- }
- // ==================== 近 7 日趋势图 ====================
- function renderTrendChart() {
- if (!trendRef.value) return
- if (!trendChart) trendChart = echarts.init(trendRef.value)
- const rows = trend.value.length ? trend.value : fallbackTrend()
- trendChart.setOption({
- backgroundColor: 'transparent',
- grid: { left: 44, right: 24, top: 24, bottom: 32 },
- tooltip: { trigger: 'axis', backgroundColor: '#ffffff', borderColor: '#dce3ed', textStyle: { color: '#303133' } },
- xAxis: {
- type: 'category',
- boundaryGap: false,
- data: rows.map(item => item.label),
- axisLine: { lineStyle: { color: '#c9d4e2' } },
- axisTick: { show: false },
- axisLabel: { color: '#7b8a9e', fontSize: 11 }
- },
- yAxis: {
- type: 'value',
- min: 0,
- minInterval: 1,
- splitLine: { lineStyle: { color: '#e8edf4', type: 'dashed' } },
- axisLabel: { color: '#7b8a9e', fontSize: 11 }
- },
- series: [{
- name: '报警数',
- type: 'line',
- smooth: true,
- symbol: 'circle',
- symbolSize: 7,
- data: rows.map(item => Number(item.count) || 0),
- lineStyle: { width: 2, color: '#f59a23' },
- itemStyle: { color: '#f59a23', borderColor: '#ffffff', borderWidth: 2 },
- areaStyle: {
- color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
- { offset: 0, color: 'rgba(255,159,67,.32)' },
- { offset: 1, color: 'rgba(255,159,67,.02)' }
- ])
- }
- }]
- }, true)
- }
- // ==================== 生命周期 ====================
- const handleResize = () => { trendChart && trendChart.resize() }
- onMounted(async () => {
- await nextTick()
- window.addEventListener('resize', handleResize)
- loadAll()
- })
- onBeforeUnmount(() => {
- window.removeEventListener('resize', handleResize)
- trendChart && trendChart.dispose()
- trendChart = null
- })
- </script>
- <style scoped>
- .alarm-dashboard {
- min-height: 100vh;
- padding: 18px 22px 28px;
- background: #f0f2f6;
- color: #303133;
- box-sizing: border-box;
- }
- .page-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- margin-bottom: 16px;
- }
- .header-left { display: flex; align-items: center; gap: 10px; color: #409eff; }
- .header-left .title { font-size: 18px; font-weight: 600; letter-spacing: 1px; color: #1f2d3d; }
- .kpi-grid {
- display: grid;
- grid-template-columns: repeat(3, 1fr);
- gap: 14px;
- margin-bottom: 14px;
- }
- .kpi-card {
- position: relative;
- padding: 14px 18px 13px;
- border: 1px solid #e4e9f2;
- border-radius: 8px;
- background: #fff;
- box-shadow: 0 1px 4px rgba(31, 45, 61, .06);
- overflow: hidden;
- }
- .kpi-card::before {
- content: '';
- position: absolute;
- left: 0; top: 0; bottom: 0;
- width: 3px;
- background: linear-gradient(180deg, #7ec8ff, #2b7de9);
- }
- .kpi-head { display: flex; align-items: center; gap: 8px; font-size: 13px; color: #6b7a90; }
- .kpi-icon {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- width: 20px; height: 20px;
- border-radius: 4px;
- background: #eaf3ff;
- color: #409eff;
- font-size: 13px;
- }
- .kpi-value {
- margin-top: 8px;
- font-size: 30px;
- font-weight: 700;
- line-height: 1.15;
- color: #1f2d3d;
- font-variant-numeric: tabular-nums;
- }
- .kpi-value small { font-size: 16px; margin-left: 2px; font-weight: 600; }
- .kpi-value.green { color: #18a058; }
- .kpi-value.orange { color: #e8890c; }
- .kpi-foot { margin-top: 5px; font-size: 11px; color: #909399; }
- .kpi-foot .up { color: #f56c6c; font-weight: 600; }
- .kpi-foot .down { color: #18a058; font-weight: 600; }
- .kpi-foot .flat { color: #909399; }
- .section-title {
- display: flex;
- align-items: center;
- gap: 9px;
- margin: 20px 0 12px;
- font-size: 14px;
- font-weight: 600;
- color: #303133;
- }
- .title-bar {
- width: 3px; height: 14px;
- border-radius: 2px;
- background: linear-gradient(180deg, #7ec8ff, #2b7de9);
- }
- .title-count { font-size: 12px; font-weight: 400; color: #909399; }
- .level-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 14px; }
- .level-card {
- padding: 16px 10px 14px;
- text-align: center;
- border: 1px solid #e4e9f2;
- border-radius: 8px;
- background: #fff;
- box-shadow: 0 1px 4px rgba(31, 45, 61, .06);
- }
- .level-value { font-size: 28px; font-weight: 700; line-height: 1.2; color: #1f2d3d; font-variant-numeric: tabular-nums; }
- .level-name { margin-top: 6px; font-size: 12px; color: #6b7a90; }
- .level-1 { border-color: #f7c4c4; }
- .level-1 .level-value { color: #f04c4c; }
- .level-2 { border-color: #f5d9b0; }
- .level-2 .level-value { color: #e8890c; }
- .level-3 { border-color: #ede0a8; }
- .level-3 .level-value { color: #c9a227; }
- .level-4 { border-color: #b9e2ee; }
- .level-4 .level-value { color: #1a9cb5; }
- .trend-chart {
- height: 210px;
- border: 1px solid #e4e9f2;
- border-radius: 8px;
- background: #fff;
- box-shadow: 0 1px 4px rgba(31, 45, 61, .06);
- }
- .alarm-table {
- width: 100%;
- border-radius: 8px;
- overflow: hidden;
- box-shadow: 0 1px 4px rgba(31, 45, 61, .06);
- }
- .alarm-table :deep(th.el-table__cell) {
- background: #f5f7fa;
- color: #606266;
- font-weight: 600;
- }
- .pagination-wrapper {
- display: flex;
- justify-content: flex-end;
- margin-top: 12px;
- padding-top: 12px;
- border-top: 1px solid #ebeef5;
- }
- @media (max-width: 1100px) {
- .kpi-grid { grid-template-columns: repeat(2, 1fr); }
- .level-grid { grid-template-columns: repeat(2, 1fr); }
- }
- </style>
|