gxllyctx.vue 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464
  1. <!--管线流量异常提醒-->
  2. <template>
  3. <div class="pressure-alert-center">
  4. <!-- 页面头部 -->
  5. <div class="page-header">
  6. <div class="header-left">
  7. <el-icon><Warning /></el-icon>
  8. <span class="title">管线流量异常报警中心</span>
  9. <div class="stats-badge">
  10. <span class="stat-item critical"><i class="dot"></i>实时异常 {{ realtimeAlerts.length }}</span>
  11. <span class="stat-item warning"><i class="dot"></i>历史异常 {{ historyAlerts.length }}</span>
  12. <span class="stat-item"><i class="dot"></i>监测点 {{ totalPoints }}</span>
  13. </div>
  14. </div>
  15. <div class="header-right">
  16. <el-date-picker
  17. v-model="dateRange"
  18. type="daterange"
  19. range-separator="至"
  20. start-placeholder="开始日期"
  21. end-placeholder="结束日期"
  22. style="width: 260px"
  23. size="default"
  24. />
  25. <el-button type="primary" :icon="Refresh" @click="refreshData">刷新</el-button>
  26. <el-button :icon="Download" @click="exportReport">导出报表</el-button>
  27. </div>
  28. </div>
  29. <!-- KPI指标卡片 -->
  30. <div class="kpi-cards">
  31. <div class="kpi-card critical">
  32. <div class="kpi-value">{{ realtimeAlerts.length }}</div>
  33. <div class="kpi-label">实时异常</div>
  34. <div class="kpi-trend">较昨日 ↑{{ realtimeTrend }}</div>
  35. </div>
  36. <div class="kpi-card warning">
  37. <div class="kpi-value">{{ todayHistoryCount }}</div>
  38. <div class="kpi-label">今日历史异常</div>
  39. <div class="kpi-trend">待处理 {{ pendingCount }}</div>
  40. </div>
  41. <div class="kpi-card">
  42. <div class="kpi-value">{{ abnormalRate }}%</div>
  43. <div class="kpi-label">异常率</div>
  44. <div class="kpi-trend trend-down">较昨日 ↓2.1%</div>
  45. </div>
  46. <div class="kpi-card">
  47. <div class="kpi-value">{{ avgResponseTime }}</div>
  48. <div class="kpi-label">平均响应时间(分钟)</div>
  49. </div>
  50. </div>
  51. <!-- 主内容区:左侧实时异常 + 右侧历史异常 -->
  52. <div class="main-layout">
  53. <!-- 左侧实时异常提醒列表 -->
  54. <div class="alert-panel">
  55. <div class="panel-header">
  56. <span><el-icon><BellFilled /></el-icon> 实时异常提醒</span>
  57. <el-tag type="danger" effect="dark">{{ realtimeAlerts.length }} 个异常</el-tag>
  58. </div>
  59. <div class="alert-list realtime">
  60. <div
  61. v-for="alert in paginatedRealtime"
  62. :key="alert.id"
  63. class="alert-item critical"
  64. @click="viewAlertDetail(alert)"
  65. >
  66. <div class="alert-level">
  67. <el-tag type="danger" size="small">严重</el-tag>
  68. </div>
  69. <div class="alert-content">
  70. <div class="alert-title">{{ alert.pointName }}</div>
  71. <div class="alert-location">
  72. <el-icon><Location /></el-icon> {{ alert.address }}
  73. </div>
  74. <div class="alert-data">
  75. <span class="data-item">瞬时流量: {{ alert.flow }} Nm³/h</span>
  76. <span class="data-item">上限: {{ alert.threshold }} Nm³/h</span>
  77. </div>
  78. <div class="alert-time">{{ alert.alertTime }}</div>
  79. </div>
  80. <div class="alert-action">
  81. <el-button size="small" type="primary" @click.stop="handleAlert(alert)">处理</el-button>
  82. </div>
  83. </div>
  84. <div v-if="realtimeAlerts.length === 0" class="empty-state">
  85. <el-empty description="暂无实时异常" :image-size="80" />
  86. </div>
  87. </div>
  88. <div class="pagination-area" v-if="realtimeAlerts.length > pageSize">
  89. <el-pagination small layout="prev, pager, next" :total="realtimeAlerts.length" :page-size="pageSize" v-model:current-page="realtimePage" />
  90. </div>
  91. </div>
  92. <!-- 右侧历史异常提醒列表 -->
  93. <div class="history-panel">
  94. <div class="panel-header">
  95. <span><el-icon><Document /></el-icon> 历史异常提醒</span>
  96. <el-input
  97. v-model="historySearch"
  98. placeholder="搜索点位/地址"
  99. clearable
  100. :prefix-icon="Search"
  101. style="width: 180px"
  102. size="small"
  103. />
  104. </div>
  105. <div class="alert-list history">
  106. <el-table :data="paginatedHistory" stripe border style="width: 100%" @row-click="viewAlertDetail">
  107. <el-table-column prop="pointName" label="监测点位" min-width="140" show-overflow-tooltip />
  108. <el-table-column prop="address" label="位置" min-width="160" show-overflow-tooltip />
  109. <el-table-column prop="flow" label="瞬时流量(Nm³/h)" width="130" sortable>
  110. <template #default="{ row }">
  111. <span class="alert-value">{{ row.flow }}</span>
  112. </template>
  113. </el-table-column>
  114. <el-table-column prop="threshold" label="上限(Nm³/h)" width="110" />
  115. <el-table-column prop="alertTime" label="异常时间" width="150" sortable />
  116. <el-table-column prop="status" label="状态" width="80" align="center">
  117. <template #default="{ row }">
  118. <el-tag :type="row.status === 'resolved' ? 'success' : 'warning'" size="small">
  119. {{ row.status === 'resolved' ? '已处理' : '待处理' }}
  120. </el-tag>
  121. </template>
  122. </el-table-column>
  123. <el-table-column label="操作" width="80" fixed="right" align="center">
  124. <template #default="{ row }">
  125. <el-button link type="primary" size="small" @click.stop="viewAlertDetail(row)">详情</el-button>
  126. </template>
  127. </el-table-column>
  128. </el-table>
  129. </div>
  130. <div class="pagination-area">
  131. <el-pagination
  132. small
  133. layout="total, prev, pager, next"
  134. :total="filteredHistory.length"
  135. :page-size="historyPageSize"
  136. v-model:current-page="historyPage"
  137. />
  138. </div>
  139. </div>
  140. </div>
  141. <!-- 异常详情抽屉 -->
  142. <el-drawer v-model="detailDrawerVisible" :title="`异常详情 - ${currentAlert?.pointName}`" size="480px" direction="rtl" @opened="onDrawerOpened">
  143. <div v-if="currentAlert" class="alert-detail">
  144. <el-descriptions :column="1" border>
  145. <el-descriptions-item label="监测点位">{{ currentAlert.pointName }}</el-descriptions-item>
  146. <el-descriptions-item label="点位编号">{{ currentAlert.pointCode }}</el-descriptions-item>
  147. <el-descriptions-item label="详细地址">{{ currentAlert.address }}</el-descriptions-item>
  148. <el-descriptions-item label="所属管线">{{ currentAlert.pipelineName }}</el-descriptions-item>
  149. <el-descriptions-item label="异常流量">{{ currentAlert.flow }} Nm³/h</el-descriptions-item>
  150. <el-descriptions-item label="报警阈值">{{ currentAlert.threshold }} Nm³/h</el-descriptions-item>
  151. <el-descriptions-item label="异常时间">{{ currentAlert.alertTime }}</el-descriptions-item>
  152. <el-descriptions-item label="处理状态">
  153. <el-tag :type="currentAlert.status === 'resolved' ? 'success' : 'danger'">
  154. {{ currentAlert.status === 'resolved' ? '已处理' : '待处理' }}
  155. </el-tag>
  156. </el-descriptions-item>
  157. <el-descriptions-item v-if="currentAlert.resolveTime" label="处理时间">{{ currentAlert.resolveTime }}</el-descriptions-item>
  158. <el-descriptions-item label="处理人">{{ currentAlert.handler || '—' }}</el-descriptions-item>
  159. <el-descriptions-item label="处理说明">{{ currentAlert.remark || '—' }}</el-descriptions-item>
  160. </el-descriptions>
  161. <!-- 流量趋势图 -->
  162. <div class="trend-section">
  163. <div class="section-title">流量趋势(最近1小时)</div>
  164. <div ref="chartRef" class="trend-chart"></div>
  165. </div>
  166. <!-- 处理操作 -->
  167. <div class="action-section" v-if="currentAlert.status !== 'resolved'">
  168. <el-button type="primary" @click="resolveAlert">标记已处理</el-button>
  169. <el-button type="warning" @click="dispatchWorkOrder">派发工单</el-button>
  170. </div>
  171. </div>
  172. </el-drawer>
  173. </div>
  174. </template>
  175. <script setup>
  176. import { ref, computed, watch, nextTick, onBeforeUnmount } from 'vue'
  177. import { Warning, Refresh, Download, BellFilled, Document, Location, Search } from '@element-plus/icons-vue'
  178. import * as echarts from 'echarts'
  179. import { ElMessage } from 'element-plus'
  180. // ==================== 沅陵县管线流量异常报警数据 ====================
  181. // 实时异常(当前未处理)
  182. const realtimeAlerts = ref([
  183. { id: 1, pointCode: 'FL-YL-002', pointName: '辰州中路流量计2#', address: '沅陵县太常片区辰州中路调压站进口侧', pipelineName: '辰州中路高压段',
  184. flow: 2620, threshold: 2500, alertTime: '2026-06-13 15:31:45', status: 'pending',
  185. trendData: [2100, 2150, 2200, 2250, 2300, 2350, 2380, 2420, 2480, 2520, 2550, 2580, 2600, 2620, 2620, 2610, 2600, 2580, 2560, 2540, 2520, 2500, 2480, 2450] },
  186. { id: 2, pointCode: 'FL-YL-003', pointName: '古城中路流量计2#', address: '沅陵县太常片区古城中路太常安置区段', pipelineName: '古城中路中压段',
  187. flow: 820, threshold: 800, alertTime: '2026-06-13 15:29:58', status: 'pending',
  188. trendData: [680, 700, 720, 740, 750, 760, 770, 780, 790, 800, 810, 820, 820, 815, 810, 805, 800, 795, 790, 785, 780, 775, 770, 765] },
  189. { id: 3, pointCode: 'FL-YL-006', pointName: '滨江东路流量计2#', address: '沅陵县太常片区滨江东路商业街', pipelineName: '滨江东路低压段',
  190. flow: 210, threshold: 200, alertTime: '2026-06-13 15:30:40', status: 'pending',
  191. trendData: [155, 160, 165, 170, 175, 180, 185, 190, 195, 200, 205, 210, 210, 208, 206, 204, 200, 198, 195, 192, 190, 185, 180, 175] },
  192. { id: 4, pointCode: 'FL-YL-015', pointName: '建设东路流量计3#', address: '沅陵县太常片区建设东路工业园入口处', pipelineName: '建设东路高压段',
  193. flow: 2480, threshold: 2500, alertTime: '2026-06-13 14:55:20', status: 'pending',
  194. trendData: [2200, 2250, 2280, 2300, 2320, 2350, 2380, 2400, 2420, 2440, 2460, 2480, 2480, 2470, 2460, 2440, 2420, 2400, 2380, 2360, 2340, 2320, 2300, 2280] }
  195. ])
  196. // 历史异常(已处理/待处理)
  197. const historyAlerts = ref([
  198. { id: 101, pointCode: 'FL-YL-001', pointName: '辰州北路流量计1#', address: '沅陵县太常片区辰州北路门站出口计量间', pipelineName: '辰州北路高压段',
  199. flow: 2550, threshold: 2500, alertTime: '2026-06-12 14:30:00', status: 'resolved', resolveTime: '2026-06-12 15:45:00', handler: '张建国', remark: '下游用气高峰,调压后恢复正常',
  200. trendData: [2100, 2150, 2200, 2280, 2350, 2420, 2500, 2550, 2550, 2520, 2460, 2360, 2250, 2150, 2100, 2050, 2000, 1980, 1950, 1920, 1900, 1880, 1850, 1830] },
  201. { id: 102, pointCode: 'FL-YL-004', pointName: '古城北路流量计1#', address: '沅陵县太常片区古城北路与天宁路交叉口', pipelineName: '古城北路中压段',
  202. flow: 850, threshold: 800, alertTime: '2026-06-12 09:15:00', status: 'resolved', resolveTime: '2026-06-12 10:30:00', handler: '李志强', remark: '管网流量波动,阀门微调后稳定',
  203. trendData: [680, 700, 720, 740, 760, 780, 810, 840, 850, 850, 840, 810, 780, 750, 730, 710, 690, 680, 670, 660, 650, 640, 630, 620] },
  204. { id: 103, pointCode: 'FL-YL-007', pointName: '迎宾西路流量计1#', address: '沅陵县太常片区迎宾西路县政府对面', pipelineName: '迎宾西路中压段',
  205. flow: 520, threshold: 500, alertTime: '2026-06-11 16:20:00', status: 'pending', resolveTime: null, handler: null, remark: '持续超限,待现场巡检确认',
  206. trendData: [430, 440, 450, 460, 470, 480, 490, 500, 510, 520, 520, 520, 525, 530, 530, 525, 520, 520, 515, 510, 510, 505, 500, 495] },
  207. { id: 104, pointCode: 'FL-YL-012', pointName: '滨江东路流量计2#', address: '沅陵县太常片区滨江东路商业街', pipelineName: '滨江东路低压段',
  208. flow: 215, threshold: 200, alertTime: '2026-06-11 10:45:00', status: 'resolved', resolveTime: '2026-06-11 12:00:00', handler: '王大明', remark: '商业街午间用气高峰,属正常波动',
  209. trendData: [160, 165, 170, 178, 188, 195, 205, 212, 215, 215, 210, 200, 190, 180, 172, 165, 160, 156, 152, 150, 148, 145, 143, 140] },
  210. { id: 105, pointCode: 'FL-YL-009', pointName: '建设路流量计1#', address: '沅陵县太常片区建设路县政府南侧计量站', pipelineName: '建设路中压段',
  211. flow: 0, threshold: 800, alertTime: '2026-06-13 08:15:00', status: 'pending', resolveTime: null, handler: null, remark: '设备离线,数据中断,需现场检查',
  212. trendData: [820, 810, 800, 780, 750, 700, 620, 500, 350, 180, 50, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] },
  213. { id: 106, pointCode: 'FL-YL-015', pointName: '建设东路流量计3#', address: '沅陵县太常片区建设东路工业园入口处', pipelineName: '建设东路高压段',
  214. flow: 2580, threshold: 2500, alertTime: '2026-06-10 22:30:00', status: 'resolved', resolveTime: '2026-06-10 23:15:00', handler: '赵志远', remark: '工业园区夜间加班用气高峰导致',
  215. trendData: [2200, 2250, 2300, 2350, 2400, 2450, 2500, 2550, 2580, 2580, 2550, 2480, 2380, 2280, 2200, 2150, 2100, 2080, 2050, 2020, 2000, 1980, 1950, 1930] },
  216. { id: 107, pointCode: 'FL-YL-002', pointName: '辰州中路流量计2#', address: '沅陵县太常片区辰州中路调压站进口侧', pipelineName: '辰州中路高压段',
  217. flow: 2510, threshold: 2500, alertTime: '2026-06-10 18:20:00', status: 'resolved', resolveTime: '2026-06-10 19:30:00', handler: '陈志明', remark: '晚高峰居民用气激增,调压后恢复',
  218. trendData: [2180, 2220, 2280, 2350, 2420, 2480, 2510, 2510, 2480, 2400, 2300, 2220, 2150, 2100, 2060, 2020, 1980, 1950, 1920, 1900, 1880, 1850, 1830, 1800] }
  219. ])
  220. // 统计数据
  221. const totalPoints = computed(() => 15)
  222. const realtimeTrend = computed(() => 1)
  223. const todayHistoryCount = computed(() => historyAlerts.value.filter(a => a.alertTime.startsWith('2026-06-13')).length)
  224. const pendingCount = computed(() => historyAlerts.value.filter(a => a.status === 'pending').length)
  225. const abnormalRate = computed(() => (realtimeAlerts.value.length / totalPoints.value * 100).toFixed(1))
  226. const avgResponseTime = computed(() => 62)
  227. // 筛选
  228. const dateRange = ref([])
  229. const historySearch = ref('')
  230. const realtimePage = ref(1)
  231. const historyPage = ref(1)
  232. const pageSize = 5
  233. const historyPageSize = 8
  234. const filteredHistory = computed(() => {
  235. let list = historyAlerts.value
  236. if (dateRange.value && dateRange.value.length === 2) {
  237. const start = new Date(dateRange.value[0])
  238. const end = new Date(dateRange.value[1])
  239. list = list.filter(a => {
  240. const alertDate = new Date(a.alertTime)
  241. return alertDate >= start && alertDate <= end
  242. })
  243. }
  244. if (historySearch.value) {
  245. const kw = historySearch.value.toLowerCase()
  246. list = list.filter(a => a.pointName.toLowerCase().includes(kw) || a.address.toLowerCase().includes(kw))
  247. }
  248. return list
  249. })
  250. const paginatedRealtime = computed(() => {
  251. const start = (realtimePage.value - 1) * pageSize
  252. return realtimeAlerts.value.slice(start, start + pageSize)
  253. })
  254. const paginatedHistory = computed(() => {
  255. const start = (historyPage.value - 1) * historyPageSize
  256. return filteredHistory.value.slice(start, start + historyPageSize)
  257. })
  258. // 当前选中告警
  259. const currentAlert = ref(null)
  260. const detailDrawerVisible = ref(false)
  261. const chartRef = ref(null)
  262. let chartInstance = null
  263. function renderChart() {
  264. if (!chartRef.value || !currentAlert.value?.trendData) return
  265. if (!chartInstance) {
  266. chartInstance = echarts.init(chartRef.value)
  267. }
  268. const times = Array.from({ length: 24 }, (_, i) => `${i}:00`)
  269. const threshold = currentAlert.value.threshold
  270. chartInstance.setOption({
  271. tooltip: { trigger: 'axis' },
  272. xAxis: { type: 'category', data: times, axisLabel: { rotate: 45, interval: 4 } },
  273. yAxis: { type: 'value', name: 'Nm³/h', min: 0 },
  274. series: [
  275. { name: '瞬时流量', type: 'line', data: currentAlert.value.trendData, smooth: true, symbol: 'circle', symbolSize: 3, lineStyle: { color: '#f56c6c', width: 2 }, itemStyle: { color: '#f56c6c' }, areaStyle: { color: 'rgba(245,108,108,0.1)' } },
  276. { name: '报警阈值', type: 'line', data: Array(24).fill(threshold), lineStyle: { color: '#e6a23c', width: 2, type: 'dashed' }, symbol: 'none', itemStyle: { color: '#e6a23c' } }
  277. ]
  278. })
  279. }
  280. function onDrawerOpened() {
  281. nextTick(() => renderChart())
  282. }
  283. // 方法
  284. const viewAlertDetail = (alert) => {
  285. currentAlert.value = alert
  286. detailDrawerVisible.value = true
  287. }
  288. const handleAlert = (alert) => {
  289. currentAlert.value = alert
  290. detailDrawerVisible.value = true
  291. }
  292. const resolveAlert = () => {
  293. if (currentAlert.value.status === 'pending') {
  294. // 从实时异常移到历史异常
  295. const index = realtimeAlerts.value.findIndex(a => a.id === currentAlert.value.id)
  296. if (index !== -1) {
  297. const resolved = { ...currentAlert.value, status: 'resolved', resolveTime: new Date().toLocaleString(), handler: '当前用户' }
  298. realtimeAlerts.value.splice(index, 1)
  299. historyAlerts.value.unshift(resolved)
  300. } else {
  301. // 如果是历史异常中的待处理
  302. const histIndex = historyAlerts.value.findIndex(a => a.id === currentAlert.value.id)
  303. if (histIndex !== -1) {
  304. historyAlerts.value[histIndex].status = 'resolved'
  305. historyAlerts.value[histIndex].resolveTime = new Date().toLocaleString()
  306. historyAlerts.value[histIndex].handler = '当前用户'
  307. }
  308. }
  309. currentAlert.value.status = 'resolved'
  310. ElMessage.success('已标记为已处理')
  311. detailDrawerVisible.value = false
  312. }
  313. }
  314. const dispatchWorkOrder = () => {
  315. ElMessage.success('已派发维修工单')
  316. }
  317. const refreshData = () => {
  318. ElMessage.success('数据已刷新')
  319. }
  320. const exportReport = () => {
  321. ElMessage.success('导出报表(演示)')
  322. }
  323. // 监听日期范围变化重置页码
  324. watch([dateRange, historySearch], () => { historyPage.value = 1 })
  325. onBeforeUnmount(() => {
  326. try { chartInstance?.dispose() } catch {}
  327. chartInstance = null
  328. })
  329. </script>
  330. <style scoped>
  331. .pressure-alert-center {
  332. background: #f0f2f6;
  333. min-height: 100vh;
  334. padding: 20px;
  335. }
  336. .page-header {
  337. background: white;
  338. border-radius: 16px;
  339. padding: 16px 24px;
  340. margin-bottom: 20px;
  341. display: flex;
  342. justify-content: space-between;
  343. align-items: center;
  344. flex-wrap: wrap;
  345. gap: 12px;
  346. }
  347. .header-left { display: flex; align-items: center; gap: 20px; }
  348. .header-left .title { font-size: 1.3rem; font-weight: 600; }
  349. .stats-badge { display: flex; gap: 20px; }
  350. .stat-item { font-size: 13px; display: flex; align-items: center; gap: 6px; }
  351. .stat-item.critical { color: #f56c6c; }
  352. .stat-item.warning { color: #e6a23c; }
  353. .stat-item .dot { width: 8px; height: 8px; border-radius: 50%; background: #67c23a; }
  354. .stat-item.critical .dot { background: #f56c6c; }
  355. .stat-item.warning .dot { background: #e6a23c; }
  356. .header-right { display: flex; gap: 12px; }
  357. .kpi-cards {
  358. display: grid;
  359. grid-template-columns: repeat(4, 1fr);
  360. gap: 16px;
  361. margin-bottom: 20px;
  362. }
  363. .kpi-card {
  364. background: white;
  365. border-radius: 12px;
  366. padding: 16px;
  367. text-align: center;
  368. }
  369. .kpi-card.critical .kpi-value { color: #f56c6c; }
  370. .kpi-card.warning .kpi-value { color: #e6a23c; }
  371. .kpi-value { font-size: 32px; font-weight: bold; }
  372. .kpi-label { font-size: 13px; color: #909399; margin-top: 4px; }
  373. .kpi-trend { font-size: 11px; color: #67c23a; margin-top: 4px; }
  374. .kpi-trend.trend-down { color: #f56c6c; }
  375. .main-layout {
  376. display: flex;
  377. gap: 20px;
  378. }
  379. .alert-panel {
  380. width: 420px;
  381. background: white;
  382. border-radius: 16px;
  383. display: flex;
  384. flex-direction: column;
  385. overflow: hidden;
  386. }
  387. .history-panel {
  388. flex: 1;
  389. background: white;
  390. border-radius: 16px;
  391. display: flex;
  392. flex-direction: column;
  393. overflow: hidden;
  394. }
  395. .panel-header {
  396. padding: 16px;
  397. border-bottom: 1px solid #e9ecef;
  398. display: flex;
  399. justify-content: space-between;
  400. align-items: center;
  401. font-weight: 600;
  402. }
  403. .alert-list {
  404. flex: 1;
  405. overflow-y: auto;
  406. padding: 12px;
  407. }
  408. .alert-item {
  409. display: flex;
  410. gap: 12px;
  411. padding: 14px;
  412. margin-bottom: 12px;
  413. border-radius: 12px;
  414. background: #fff8f8;
  415. border: 1px solid #ffd4d4;
  416. cursor: pointer;
  417. transition: all 0.2s;
  418. }
  419. .alert-item:hover { transform: translateX(2px); box-shadow: 0 2px 8px rgba(0,0,0,0.1); }
  420. .alert-level { flex-shrink: 0; }
  421. .alert-content { flex: 1; }
  422. .alert-title { font-weight: 600; margin-bottom: 6px; }
  423. .alert-location { font-size: 12px; color: #6c757d; display: flex; align-items: center; gap: 4px; margin-bottom: 6px; }
  424. .alert-data { display: flex; gap: 16px; font-size: 12px; margin-bottom: 6px; }
  425. .alert-data .data-item { background: #f5f5f5; padding: 2px 8px; border-radius: 12px; }
  426. .alert-time { font-size: 11px; color: #aaa; }
  427. .alert-action { display: flex; align-items: center; }
  428. .empty-state { padding: 40px 0; text-align: center; }
  429. .pagination-area { padding: 12px; text-align: center; border-top: 1px solid #eee; }
  430. .alert-value { color: #f56c6c; font-weight: bold; }
  431. .alert-detail { padding: 8px; }
  432. .section-title { font-weight: 600; margin: 16px 0 12px 0; }
  433. .trend-chart { height: 200px; }
  434. .action-section { margin-top: 20px; display: flex; gap: 12px; justify-content: flex-end; }
  435. </style>