| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344 |
- <template>
- <div class="alarm-status">
- <!-- 页面标题 -->
- <div class="page-header">
- <h2 class="page-title">告警显示</h2>
- </div>
- <!-- 概览信息栏 -->
- <a-card :bordered="false" class="overview-card">
- <div class="overview-bar">
- <div class="overview-item">
- <span class="overview-label">告警总数:</span>
- <a-tag color="red">{{ alarmCount }}</a-tag>
- </div>
- <div class="overview-item">
- <span class="overview-label">事件总数:</span>
- <a-tag color="blue">{{ events.length }}</a-tag>
- </div>
- <div class="overview-item">
- <span class="overview-label">最后更新:</span>
- <span class="overview-value">{{ lastUpdate || '--' }}</span>
- </div>
- <div class="overview-actions">
- <a-button type="primary" danger @click="clearEvents" :loading="clearing">
- <template #icon><delete-outlined /></template>
- 清除所有事件
- </a-button>
- <a-button @click="refreshData" :loading="loading">
- <template #icon><reload-outlined /></template>
- 刷新
- </a-button>
- </div>
- </div>
- </a-card>
- <!-- 告警事件列表 -->
- <a-card :bordered="false" class="events-card">
- <a-table
- :columns="columns"
- :data-source="events"
- :loading="loading"
- :pagination="{ pageSize: 20, showSizeChanger: true, showTotal: (total) => `共 ${total} 条` }"
- :scroll="{ x: 800 }"
- row-key="key"
- size="middle"
- >
- <template #bodyCell="{ column, record }">
- <template v-if="column.key === 'event_type'">
- <a-tag :color="record.event_type === 'ALARM' ? 'red' : 'blue'">
- {{ record.event_type === 'ALARM' ? '告警' : '事件' }}
- </a-tag>
- </template>
- <template v-else-if="column.key === 'alarm_type'">
- <span v-if="record.alarm_type">{{ record.alarm_type }}</span>
- <span v-else class="text-muted">-</span>
- </template>
- <template v-else-if="column.key === 'expected_jumper'">
- <span>{{ record.expected_jumper_uid || '-' }}</span>
- </template>
- <template v-else-if="column.key === 'actual_jumper'">
- <span :class="{ 'text-danger': record.actual_jumper_uid !== record.expected_jumper_uid }">
- {{ record.actual_jumper_uid || '未读到' }}
- </span>
- </template>
- <template v-else-if="column.key === 'timestamp'">
- <span class="timestamp">{{ formatTime(record.timestamp) }}</span>
- </template>
- </template>
- <template #emptyText>
- <a-empty description="暂无告警事件" />
- </template>
- </a-table>
- </a-card>
- </div>
- </template>
- <script setup>
- import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
- import { message } from 'ant-design-vue'
- import { DeleteOutlined, ReloadOutlined } from '@ant-design/icons-vue'
- import apiService from '../api/apiService'
- import { useDataStore } from '../stores/dataStore'
- const dataStore = useDataStore()
- // 状态
- const loading = ref(false)
- const clearing = ref(false)
- const events = ref([])
- const lastUpdate = ref('')
- const pollTimer = ref(null)
- // 表格列定义
- const columns = [
- {
- title: '时间',
- key: 'timestamp',
- dataIndex: 'timestamp',
- width: 180,
- sorter: (a, b) => new Date(b.timestamp) - new Date(a.timestamp)
- },
- {
- title: '面板',
- key: 'panel_id',
- dataIndex: 'panel_id',
- width: 80
- },
- {
- title: '端口',
- key: 'port_id',
- dataIndex: 'port_id',
- width: 80
- },
- {
- title: '类型',
- key: 'event_type',
- dataIndex: 'event_type',
- width: 100
- },
- {
- title: '告警类型',
- key: 'alarm_type',
- dataIndex: 'alarm_type',
- width: 150
- },
- {
- title: '期望跳线UID',
- key: 'expected_jumper',
- dataIndex: 'expected_jumper_uid',
- width: 130
- },
- {
- title: '实际跳线UID',
- key: 'actual_jumper',
- dataIndex: 'actual_jumper_uid',
- width: 130
- }
- ]
- // 计算告警数量
- const alarmCount = computed(() => {
- return events.value.filter(e => e.event_type === 'ALARM').length
- })
- // 格式化时间
- function formatTime(timestamp) {
- if (!timestamp) return '-'
- try {
- const date = new Date(timestamp)
- return date.toLocaleString('zh-CN', {
- year: 'numeric',
- month: '2-digit',
- day: '2-digit',
- hour: '2-digit',
- minute: '2-digit',
- second: '2-digit'
- })
- } catch {
- return timestamp
- }
- }
- // 加载事件数据
- async function loadEvents() {
- try {
- const res = await apiService.port.getEvents(100)
- if (res.data.success) {
- // 添加唯一key
- events.value = (res.data.events || []).map((e, i) => ({
- ...e,
- key: `${e.timestamp}-${i}`
- }))
- }
- } catch (e) {
- console.error('load events error:', e)
- }
- }
- // 刷新数据
- async function refreshData() {
- loading.value = true
- try {
- await loadEvents()
- lastUpdate.value = new Date().toLocaleTimeString()
- } finally {
- loading.value = false
- }
- }
- // 清除所有事件
- async function clearEvents() {
- clearing.value = true
- try {
- const res = await apiService.port.clearEvents()
- if (res.data.success) {
- message.success('事件已清除')
- events.value = []
- }
- } catch (e) {
- console.error('clear events error:', e)
- message.error('清除失败')
- } finally {
- clearing.value = false
- }
- }
- // 生命周期
- onMounted(async () => {
- await refreshData()
- pollTimer.value = setInterval(refreshData, 5000)
- })
- onUnmounted(() => {
- if (pollTimer.value) clearInterval(pollTimer.value)
- })
- // 监听 dataStore.mqttData 实时推送 event/alarm topic 到表格顶部
- watch(
- () => dataStore.mqttData.length,
- (newLen, oldLen) => {
- if (newLen <= (oldLen || 0)) return
- const added = dataStore.mqttData.slice(oldLen || 0, newLen)
- for (const item of added) {
- if (!item || typeof item !== 'object') continue
- const topic = item.topic || ''
- const isAlarm = topic.endsWith('/alarm')
- const isEvent = topic.endsWith('/event')
- if (!isAlarm && !isEvent) continue
- try {
- const parsed = typeof item.payload === 'string' ? JSON.parse(item.payload) : item.payload
- const p = parsed?.payload || parsed || {}
- const rec = {
- key: `rt-${parsed?.msg_id || Date.now()}-${Math.random().toString(36).slice(2, 6)}`,
- timestamp: parsed?.timestamp
- ? new Date(parsed.timestamp).toISOString()
- : item.timestamp || new Date().toISOString(),
- panel_id: p.panel_id,
- port_id: p.port_id,
- event_type: isAlarm ? 'ALARM' : 'EVENT',
- alarm_type: p.alarm_type || (isEvent ? p.event_type : null),
- expected_jumper_uid: p.expected_jumper_uid || null,
- actual_jumper_uid: p.actual_jumper_uid ?? p.jumper_uid ?? null,
- severity: p.severity || null
- }
- // 去重(同 msg_id 只插一次):按 key 前缀检测
- if (!events.value.find(e => e.key === rec.key)) {
- events.value = [rec, ...events.value].slice(0, 500)
- lastUpdate.value = new Date().toLocaleTimeString()
- }
- } catch (err) {
- console.warn('实时告警解析失败:', err, item)
- }
- }
- }
- )
- </script>
- <style scoped>
- .alarm-status {
- display: flex;
- flex-direction: column;
- gap: 16px;
- }
- .page-header {
- margin-bottom: 8px;
- }
- .page-title {
- margin: 0;
- font-size: 20px;
- font-weight: 600;
- color: #333;
- }
- .overview-card {
- flex-shrink: 0;
- }
- .overview-bar {
- display: flex;
- align-items: center;
- gap: 24px;
- flex-wrap: wrap;
- }
- .overview-item {
- display: flex;
- align-items: center;
- gap: 8px;
- }
- .overview-label {
- font-size: 14px;
- color: #666;
- }
- .overview-value {
- font-size: 14px;
- color: #333;
- }
- .overview-actions {
- margin-left: auto;
- display: flex;
- gap: 12px;
- }
- .events-card {
- flex: 1;
- min-height: 400px;
- }
- .timestamp {
- font-family: 'Monaco', 'Menlo', monospace;
- font-size: 13px;
- }
- .text-muted {
- color: #999;
- }
- .text-danger {
- color: #ff4d4f;
- font-weight: 500;
- }
- @media (max-width: 768px) {
- .overview-bar {
- flex-direction: column;
- align-items: flex-start;
- }
- .overview-actions {
- margin-left: 0;
- margin-top: 12px;
- width: 100%;
- }
- .overview-actions button {
- flex: 1;
- }
- }
- </style>
|