index.vue 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673
  1. <!--窨井盖监测首页大屏-->
  2. <template>
  3. <div class="monitor-container">
  4. <!-- 左侧栏 -->
  5. <div class="left-col">
  6. <!-- 待办工单 -->
  7. <div class="panel">
  8. <h3 class="panel-title">待办工单 <span class="title-count">{{ workOrders.length }} 条</span></h3>
  9. <el-table :data="workOrders" class="panel-table" :header-cell-style="tableHeaderStyle" size="small" v-loading="loading.work">
  10. <el-table-column prop="orderNo" label="工单编号" min-width="120" show-overflow-tooltip />
  11. <el-table-column prop="deviceCode" label="设备" min-width="100" show-overflow-tooltip />
  12. <el-table-column label="优先级" width="70">
  13. <template #default="{ row }">
  14. <span :class="['priority-tag', priorityClass(row.orderLevel)]">{{ priorityText(row.orderLevel) }}</span>
  15. </template>
  16. </el-table-column>
  17. </el-table>
  18. </div>
  19. <!-- 告警类型统计(15天内) -->
  20. <div class="panel">
  21. <h3 class="panel-title">告警类型统计(15天内)</h3>
  22. <div ref="alarmChartRef" class="chart" v-loading="loading.alarm" />
  23. </div>
  24. <!-- 设备区域信息 -->
  25. <div class="panel">
  26. <h3 class="panel-title">设备区域信息</h3>
  27. <div ref="areaChartRef" class="chart" v-loading="loading.device" />
  28. </div>
  29. </div>
  30. <!-- 中间地图区域 -->
  31. <div class="center-col">
  32. <div class="panel map-panel">
  33. <div class="map-header">
  34. <span>窨井设备分布</span>
  35. <div class="map-legend">
  36. <span><i class="legend-dot online"></i> 在线 {{ stats.online }}</span>
  37. <span><i class="legend-dot alert"></i> 告警 {{ stats.alarm }}</span>
  38. <span><i class="legend-dot offline"></i> 离线 {{ stats.offline }}</span>
  39. </div>
  40. </div>
  41. <div id="homeMap" class="bmap-container"></div>
  42. </div>
  43. <!-- 设备分类 -->
  44. <div class="panel chart-panel">
  45. <h3 class="panel-title">设备分类</h3>
  46. <div ref="deviceTypeChartRef" class="chart" v-loading="loading.device" />
  47. </div>
  48. </div>
  49. <!-- 右侧栏 -->
  50. <div class="right-col">
  51. <!-- 设备列表 -->
  52. <div class="panel">
  53. <h3 class="panel-title">设备列表 <span class="title-count">共 {{ filteredDevices.length }} 台</span></h3>
  54. <div class="filter-bar">
  55. <el-select v-model="deviceTypeFilter" size="small" placeholder="设备类型" clearable style="width: 120px">
  56. <el-option label="供水窨井" value="5" />
  57. <el-option label="排水窨井" value="9" />
  58. <el-option label="燃气窨井" value="10" />
  59. </el-select>
  60. </div>
  61. <el-table :data="filteredDevices" class="panel-table" :header-cell-style="tableHeaderStyle" size="small" v-loading="loading.device">
  62. <el-table-column prop="name" label="设备名称" min-width="100" show-overflow-tooltip />
  63. <el-table-column label="类型" width="70">
  64. <template #default="{ row }">{{ typeText(row.typeId) }}</template>
  65. </el-table-column>
  66. <el-table-column label="电量" width="60">
  67. <template #default="{ row }">
  68. <el-tag :type="batteryTag(row.battery)" size="small">{{ row.battery ?? '-' }}%</el-tag>
  69. </template>
  70. </el-table-column>
  71. <el-table-column label="状态" width="60">
  72. <template #default="{ row }">
  73. <span :class="['status-dot', row.status]"></span>
  74. <span class="status-text">{{ statusText(row.status) }}</span>
  75. </template>
  76. </el-table-column>
  77. </el-table>
  78. </div>
  79. <!-- 动态信息 -->
  80. <div class="panel">
  81. <h3 class="panel-title">动态信息</h3>
  82. <el-table :data="dynamicLogs" class="panel-table" :header-cell-style="tableHeaderStyle" size="small" v-loading="loading.alarm">
  83. <el-table-column prop="time" label="时间" width="140" />
  84. <el-table-column prop="event" label="事件" min-width="120" show-overflow-tooltip />
  85. <el-table-column label="状态" width="70">
  86. <template #default="{ row }">
  87. <span :class="['status-tag', row.alarmStatus === 1 ? 'done' : 'processing']">{{ row.alarmStatus === 1 ? '已处理' : '未处理' }}</span>
  88. </template>
  89. </el-table-column>
  90. </el-table>
  91. </div>
  92. </div>
  93. </div>
  94. </template>
  95. <script setup>
  96. import { ref, reactive, onMounted, onBeforeUnmount, nextTick, computed } from 'vue'
  97. import * as echarts from 'echarts'
  98. import { getManholeLayerData } from '@/api/pipeNetwork/basic'
  99. import { listWorkOrder } from '@/api/pipeNetwork/ops/workOrder.js'
  100. import { ORDER_STATUS_GROUP } from '@/views/subSystem/pipeNetwork/ops/workOrder/workOrderDict.js'
  101. import { getAlarmDataPage } from '@/api/drainage'
  102. import { ElMessage } from 'element-plus'
  103. import locationIcon from '@/assets/images/location.png'
  104. // ==================== 状态 ====================
  105. const loading = reactive({ work: false, alarm: false, device: false })
  106. const stats = reactive({ total: 0, online: 0, offline: 0, alarm: 0 })
  107. const workOrders = ref([])
  108. const allDevices = ref([])
  109. const dynamicLogs = ref([])
  110. const deviceTypeFilter = ref('')
  111. // 图表引用
  112. const alarmChartRef = ref(null)
  113. const areaChartRef = ref(null)
  114. const deviceTypeChartRef = ref(null)
  115. let alarmChart = null, areaChart = null, deviceTypeChart = null
  116. // ==================== 常量映射 ====================
  117. const TYPE_MAP = { '5': '供水窨井', '9': '排水窨井', '10': '燃气窨井' }
  118. // 所属片区:后端 DistrictEnum(0-城南 1-城北 2-太常)
  119. const DISTRICT_MAP = { 0: '城南', 1: '城北', 2: '太常' }
  120. function districtLabel(code) {
  121. return code == null ? '' : (DISTRICT_MAP[code] || String(code))
  122. }
  123. const tableHeaderStyle = { background: 'rgba(0,100,200,0.3)', color: '#fff', borderColor: '#2b4b8c' }
  124. const priorityText = (l) => ({ 0: '紧急', 1: '一般', 2: '低' }[l] || '-')
  125. const priorityClass = (l) => ({ 0: 'high', 1: 'medium', 2: 'low' }[l] || 'low')
  126. /** 待办工单口径:待派单/待接单/已接单/处理中/延期中(枚举派生,见 workOrderDict.js) */
  127. const TODO_ORDER_STATUS = ORDER_STATUS_GROUP.todo.join(',')
  128. /** 设备父类别ID(equipment_type.parent_id),与工单列表页保持同一口径 */
  129. const DEVICE_TYPE_PARENT_ID = '4'
  130. const typeText = (id) => TYPE_MAP[id] || '-'
  131. const statusText = (s) => ({ normal: '在线', alert: '告警', offline: '离线' }[s] || s)
  132. const batteryTag = (b) => {
  133. if (b == null) return 'info'
  134. if (Number(b) <= 20) return 'danger'
  135. if (Number(b) <= 50) return 'warning'
  136. return 'success'
  137. }
  138. // ==================== 筛选后设备列表 ====================
  139. const filteredDevices = computed(() => {
  140. if (!deviceTypeFilter.value) return allDevices.value
  141. return allDevices.value.filter(d => d.typeId === deviceTypeFilter.value)
  142. })
  143. // ==================== 数据加载 ====================
  144. async function loadAll() {
  145. await Promise.allSettled([loadDevices(), loadWorkOrders(), loadAlarmData()])
  146. await nextTick()
  147. handleResize()
  148. }
  149. async function loadDevices() {
  150. loading.device = true
  151. try {
  152. const res = await getManholeLayerData({ isQueryManholeData: true })
  153. const devices = res.data?.devices || []
  154. allDevices.value = devices.map(d => {
  155. const onlineStatus = Number(d.equipmentStatus?.onlineStatus)
  156. const alarmStatus = Number(d.equipmentStatus?.alarmStatus)
  157. const manholeAlarm = Number(d.manholeData?.alarmStatus)
  158. const isAlert = alarmStatus === 1 || manholeAlarm === 1 ||
  159. Number(d.manholeData?.waterInfiltrationAlarmStatus) === 1 ||
  160. Number(d.manholeData?.waterLevelAlarmStatus) === 1
  161. let status = 'offline'
  162. if (isAlert) status = 'alert'
  163. else if (onlineStatus === 1) status = 'normal'
  164. return {
  165. id: d.equipmentId, code: d.equipmentCode, name: d.equipmentName,
  166. typeId: d.equipmentTypeId, typeName: d.equipmentTypeName,
  167. location: d.equipmentLocation, district: districtLabel(d.district),
  168. lng: d.longitude != null ? Number(d.longitude) : null,
  169. lat: d.latitude != null ? Number(d.latitude) : null,
  170. battery: d.manholeData?.batteryLevel ?? null,
  171. status
  172. }
  173. })
  174. const statistics = res.data?.statistics || {}
  175. stats.total = statistics.total || allDevices.value.length
  176. stats.online = statistics.online || 0
  177. stats.offline = statistics.offline || 0
  178. stats.alarm = statistics.alarm || 0
  179. renderAreaChart()
  180. renderDeviceTypeChart()
  181. refreshMarkers()
  182. } catch (e) {
  183. ElMessage.error('设备数据加载失败')
  184. } finally {
  185. loading.device = false
  186. }
  187. }
  188. async function loadWorkOrders() {
  189. loading.work = true
  190. try {
  191. const res = await listWorkOrder({
  192. pageNum: 1,
  193. pageSize: 20,
  194. orderStatus: TODO_ORDER_STATUS,
  195. deviceTypeParentId: DEVICE_TYPE_PARENT_ID
  196. })
  197. workOrders.value = res.data?.records || []
  198. } catch (e) {
  199. ElMessage.error('工单数据加载失败')
  200. } finally {
  201. loading.work = false
  202. }
  203. }
  204. async function loadAlarmData() {
  205. loading.alarm = true
  206. try {
  207. const res = await getAlarmDataPage(1, 500, {})
  208. const records = res.rows || res.data?.records || res.data || []
  209. const now = new Date()
  210. const fifteenDaysAgo = new Date(now.getTime() - 15 * 24 * 60 * 60 * 1000)
  211. // 筛选15天内的告警
  212. const recentAlarms = records.filter(r => {
  213. const t = new Date(r.alarmTime || r.createTime)
  214. return t >= fifteenDaysAgo
  215. })
  216. // 告警类型统计
  217. const typeMap = {}
  218. recentAlarms.forEach(r => {
  219. const type = r.warningType || '其他'
  220. typeMap[type] = (typeMap[type] || 0) + 1
  221. })
  222. renderAlarmChart(typeMap)
  223. // 动态信息(最新10条)
  224. dynamicLogs.value = records.slice(0, 10).map(r => ({
  225. time: r.alarmTime || r.createTime || '',
  226. event: `${r.warningType || '告警'} - ${r.deviceName || r.deviceCode || ''}`,
  227. alarmStatus: r.alarmStatus ?? 0
  228. }))
  229. } catch (e) {
  230. ElMessage.error('告警数据加载失败')
  231. } finally {
  232. loading.alarm = false
  233. }
  234. }
  235. // ==================== ECharts ====================
  236. function renderAlarmChart(typeMap) {
  237. if (!alarmChartRef.value) return
  238. if (!alarmChart) alarmChart = echarts.init(alarmChartRef.value)
  239. const colors = ['#ff3333', '#ffb333', '#33cc33', '#3399ff', '#9966ff', '#ff6699', '#33cccc']
  240. const data = Object.entries(typeMap).map(([name, value], i) => ({
  241. name, value, itemStyle: { color: colors[i % colors.length] }
  242. }))
  243. alarmChart.setOption({
  244. backgroundColor: 'transparent',
  245. tooltip: { trigger: 'item', formatter: '{b}: {c} ({d}%)' },
  246. legend: { bottom: 0, textStyle: { color: '#ccc', fontSize: 10 } },
  247. series: [{
  248. type: 'pie', radius: ['35%', '60%'], center: ['50%', '40%'],
  249. label: { color: '#ccc', fontSize: 10 },
  250. data: data.length ? data : [{ name: '暂无数据', value: 0 }]
  251. }]
  252. })
  253. }
  254. function renderAreaChart() {
  255. if (!areaChartRef.value) return
  256. if (!areaChart) areaChart = echarts.init(areaChartRef.value)
  257. const areaMap = {}
  258. allDevices.value.forEach(d => {
  259. const area = d.district || d.location || '未分配区域'
  260. // 取区域名(截取前4个字)
  261. const shortArea = area.length > 6 ? area.slice(0, 6) + '...' : area
  262. areaMap[shortArea] = (areaMap[shortArea] || 0) + 1
  263. })
  264. const entries = Object.entries(areaMap).sort((a, b) => b[1] - a[1]).slice(0, 8)
  265. areaChart.setOption({
  266. backgroundColor: 'transparent',
  267. tooltip: { trigger: 'axis' },
  268. grid: { left: '12%', right: '8%', bottom: '18%', top: '8%' },
  269. xAxis: {
  270. type: 'category', data: entries.map(e => e[0]),
  271. axisLabel: { color: '#ccc', fontSize: 10, interval: 0, rotate: 30 }
  272. },
  273. yAxis: { type: 'value', axisLabel: { color: '#ccc' }, splitLine: { lineStyle: { color: 'rgba(255,255,255,0.1)' } } },
  274. series: [{ type: 'bar', data: entries.map(e => e[1]), itemStyle: { color: '#3399ff' } }]
  275. })
  276. }
  277. function renderDeviceTypeChart() {
  278. if (!deviceTypeChartRef.value) return
  279. if (!deviceTypeChart) deviceTypeChart = echarts.init(deviceTypeChartRef.value)
  280. const typeCount = { '5': 0, '9': 0, '10': 0 }
  281. allDevices.value.forEach(d => { if (typeCount[d.typeId] != null) typeCount[d.typeId]++ })
  282. const labels = ['供水窨井', '排水窨井', '燃气窨井']
  283. const values = [typeCount['5'], typeCount['9'], typeCount['10']]
  284. const colors = ['#3399ff', '#67c23a', '#e6a23c']
  285. deviceTypeChart.setOption({
  286. backgroundColor: 'transparent',
  287. tooltip: { trigger: 'axis' },
  288. grid: { left: '12%', right: '8%', bottom: '15%', top: '8%' },
  289. xAxis: {
  290. type: 'category', data: labels,
  291. axisLabel: { color: '#ccc', fontSize: 11, interval: 0 }
  292. },
  293. yAxis: { type: 'value', axisLabel: { color: '#ccc' }, splitLine: { lineStyle: { color: 'rgba(255,255,255,0.1)' } } },
  294. series: [{
  295. type: 'bar', data: values.map((v, i) => ({ value: v, itemStyle: { color: colors[i] } })),
  296. barWidth: '40%', label: { show: true, position: 'top', color: '#ccc' }
  297. }]
  298. })
  299. }
  300. // ==================== 百度地图 ====================
  301. let mapInstance = null
  302. const markerMap = new Map()
  303. const YUANLING_CENTER = { lng: 110.393, lat: 28.452 }
  304. function initMap() {
  305. const container = document.getElementById('homeMap')
  306. if (!container || typeof BMapGL === 'undefined') return
  307. try {
  308. mapInstance = new BMapGL.Map('homeMap')
  309. mapInstance.centerAndZoom(new BMapGL.Point(YUANLING_CENTER.lng, YUANLING_CENTER.lat), 13)
  310. mapInstance.enableScrollWheelZoom(true)
  311. } catch (e) { console.error('地图初始化失败', e) }
  312. }
  313. function getStatusColor(status) {
  314. if (status === 'alert') return '#F56C6C'
  315. if (status === 'offline') return '#909399'
  316. return '#67C23A'
  317. }
  318. function refreshMarkers() {
  319. if (!mapInstance) return
  320. markerMap.forEach(label => mapInstance.removeOverlay(label))
  321. markerMap.clear()
  322. allDevices.value.forEach(d => {
  323. if (d.lng == null || d.lat == null) return
  324. const color = getStatusColor(d.status)
  325. const html = `<div style="text-align:center;cursor:pointer;">
  326. <div style="color:#fff;background:${color};border-radius:3px;padding:1px 6px;font-size:10px;white-space:nowrap;display:inline-block;margin-bottom:2px;box-shadow:0 1px 3px rgba(0,0,0,0.3);">${d.name}</div>
  327. <div><img src="${locationIcon}" style="width:22px;height:22px;display:block;margin:0 auto;"/></div>
  328. </div>`
  329. const label = new BMapGL.Label(html, {
  330. position: new BMapGL.Point(d.lng, d.lat),
  331. offset: new BMapGL.Size(-25, -40)
  332. })
  333. label.setStyle({ border: 'none', background: 'transparent', padding: '0', zIndex: '10' })
  334. mapInstance.addOverlay(label)
  335. markerMap.set(d.id, label)
  336. })
  337. }
  338. // ==================== 生命周期 ====================
  339. const handleResize = () => {
  340. alarmChart && alarmChart.resize()
  341. areaChart && areaChart.resize()
  342. deviceTypeChart && deviceTypeChart.resize()
  343. mapInstance && mapInstance.resize && mapInstance.resize()
  344. }
  345. onMounted(async () => {
  346. await nextTick()
  347. initMap()
  348. window.addEventListener('resize', handleResize)
  349. loadAll()
  350. })
  351. onBeforeUnmount(() => {
  352. window.removeEventListener('resize', handleResize)
  353. alarmChart && alarmChart.dispose()
  354. areaChart && areaChart.dispose()
  355. deviceTypeChart && deviceTypeChart.dispose()
  356. if (mapInstance) {
  357. mapInstance.clearOverlays && mapInstance.clearOverlays()
  358. mapInstance = null
  359. }
  360. markerMap.clear()
  361. })
  362. </script>
  363. <!-- ==================== 全局样式(非 scoped):el-table 深色主题覆盖 ==================== -->
  364. <!-- 用 .monitor-container 限定范围,不影响其他页面 -->
  365. <style>
  366. .monitor-container .el-table {
  367. --el-table-bg-color: transparent;
  368. --el-table-tr-bg-color: transparent;
  369. --el-table-header-bg-color: transparent;
  370. --el-table-current-row-bg-color: rgba(0, 100, 200, 0.25);
  371. --el-table-row-hover-bg-color: rgba(0, 100, 200, 0.15);
  372. --el-table-text-color: #dce3ee;
  373. --el-table-header-text-color: #fff;
  374. --el-table-border-color: rgba(43, 75, 140, 0.35);
  375. --el-table-border: 1px solid rgba(43, 75, 140, 0.35);
  376. --el-fill-color-lighter: rgba(30, 40, 90, 0.25);
  377. --el-fill-color-light: rgba(30, 40, 90, 0.25);
  378. --el-fill-color: rgba(30, 40, 90, 0.25);
  379. --el-fill-color-blank: transparent;
  380. --el-text-color-regular: #dce3ee;
  381. --el-text-color-secondary: #9ba8c0;
  382. --el-border-color: rgba(43, 75, 140, 0.35);
  383. --el-border-color-lighter: rgba(43, 75, 140, 0.2);
  384. background: transparent !important;
  385. border: none !important;
  386. }
  387. /* 隐藏 EP 表格自带的底部/右侧边框伪元素 */
  388. .monitor-container .el-table::before,
  389. .monitor-container .el-table::after,
  390. .monitor-container .el-table .el-table__inner-wrapper::before,
  391. .monitor-container .el-table .el-table__inner-wrapper::after {
  392. display: none !important;
  393. }
  394. /* 内部容器全透明 */
  395. .monitor-container .el-table .el-table__inner-wrapper,
  396. .monitor-container .el-table .el-table__body-wrapper,
  397. .monitor-container .el-table .el-table__header-wrapper,
  398. .monitor-container .el-table .el-table__body,
  399. .monitor-container .el-table .el-table__header {
  400. background: transparent !important;
  401. }
  402. /* 表头单元格 — EP 2.x 使用 th.el-table__cell */
  403. .monitor-container .el-table th.el-table__cell {
  404. background: rgba(0, 100, 200, 0.35) !important;
  405. color: #fff !important;
  406. font-weight: 600;
  407. border-bottom: 1px solid #2b4b8c !important;
  408. }
  409. /* 内容单元格 */
  410. .monitor-container .el-table td.el-table__cell {
  411. background: transparent !important;
  412. border-bottom: 1px solid rgba(43, 75, 140, 0.25) !important;
  413. padding: 7px 0 !important;
  414. }
  415. /* 行 */
  416. .monitor-container .el-table .el-table__row {
  417. background: transparent !important;
  418. }
  419. .monitor-container .el-table .el-table__row:hover > td.el-table__cell {
  420. background: rgba(0, 100, 200, 0.15) !important;
  421. }
  422. /* 空数据 */
  423. .monitor-container .el-table .el-table__empty-block {
  424. background: transparent !important;
  425. }
  426. .monitor-container .el-table .el-table__empty-text {
  427. color: #8090a8;
  428. }
  429. /* 滚动条 */
  430. .monitor-container .el-table .el-scrollbar__bar {
  431. opacity: 0.4;
  432. }
  433. /* el-select 输入框深色适配 */
  434. .monitor-container .el-select .el-input__wrapper {
  435. background: rgba(30, 40, 90, 0.6);
  436. box-shadow: 0 0 0 1px rgba(43, 75, 140, 0.5) inset;
  437. }
  438. .monitor-container .el-select .el-input__inner {
  439. color: #dce3ee;
  440. }
  441. /* el-table 在 flex 容器中撑满高度(无 max-height 时) */
  442. .monitor-container .el-table {
  443. height: 100% !important;
  444. display: flex;
  445. flex-direction: column;
  446. }
  447. .monitor-container .el-table .el-table__inner-wrapper {
  448. flex: 1;
  449. min-height: 0;
  450. display: flex;
  451. flex-direction: column;
  452. }
  453. .monitor-container .el-table .el-table__body-wrapper {
  454. flex: 1;
  455. min-height: 0;
  456. }
  457. </style>
  458. <!-- ==================== 页面布局样式(scoped) ==================== -->
  459. <style scoped>
  460. .monitor-container {
  461. display: flex;
  462. width: 100%;
  463. height: 100vh;
  464. min-height: 600px;
  465. background: #1a1a4a;
  466. color: #fff;
  467. gap: 16px;
  468. padding: 16px;
  469. box-sizing: border-box;
  470. overflow: hidden;
  471. }
  472. .left-col {
  473. flex: 0 0 25%;
  474. min-width: 280px;
  475. max-width: 420px;
  476. display: flex;
  477. flex-direction: column;
  478. gap: 16px;
  479. min-height: 0;
  480. }
  481. .right-col {
  482. flex: 0 0 25%;
  483. min-width: 280px;
  484. max-width: 420px;
  485. display: flex;
  486. flex-direction: column;
  487. gap: 16px;
  488. min-height: 0;
  489. }
  490. .center-col {
  491. flex: 1;
  492. min-width: 0;
  493. display: flex;
  494. flex-direction: column;
  495. gap: 16px;
  496. min-height: 0;
  497. }
  498. .panel {
  499. flex: 1;
  500. background: rgba(30, 40, 90, 0.8);
  501. border: 1px solid #2b4b8c;
  502. border-radius: 8px;
  503. padding: 14px;
  504. box-sizing: border-box;
  505. display: flex;
  506. flex-direction: column;
  507. min-height: 0;
  508. }
  509. /* 设备分类固定高度,不参与均分 */
  510. .chart-panel {
  511. flex: 0 0 190px;
  512. }
  513. .panel-title {
  514. margin: 0 0 10px 0;
  515. font-size: 14px;
  516. color: #66ccff;
  517. border-left: 3px solid #66ccff;
  518. padding-left: 8px;
  519. display: flex;
  520. justify-content: space-between;
  521. align-items: center;
  522. flex-shrink: 0;
  523. }
  524. .title-count {
  525. font-size: 12px;
  526. color: #aaa;
  527. font-weight: normal;
  528. }
  529. .panel-table {
  530. width: 100%;
  531. flex: 1;
  532. min-height: 0;
  533. overflow: hidden;
  534. }
  535. .chart {
  536. width: 100%;
  537. flex: 1;
  538. min-height: 100px;
  539. }
  540. .map-panel {
  541. flex: 1;
  542. display: flex;
  543. flex-direction: column;
  544. min-height: 0;
  545. }
  546. .map-header {
  547. display: flex;
  548. justify-content: space-between;
  549. align-items: center;
  550. margin-bottom: 8px;
  551. font-size: 14px;
  552. color: #66ccff;
  553. flex-shrink: 0;
  554. }
  555. .map-legend {
  556. display: flex;
  557. gap: 12px;
  558. font-size: 11px;
  559. }
  560. .legend-dot {
  561. display: inline-block;
  562. width: 8px;
  563. height: 8px;
  564. border-radius: 50%;
  565. margin-right: 3px;
  566. }
  567. .legend-dot.online { background: #67c23a; }
  568. .legend-dot.alert { background: #f56c6c; }
  569. .legend-dot.offline { background: #909399; }
  570. .bmap-container {
  571. width: 100%;
  572. flex: 1;
  573. min-height: 200px;
  574. border-radius: 4px;
  575. overflow: hidden;
  576. }
  577. .filter-bar {
  578. margin-bottom: 10px;
  579. flex-shrink: 0;
  580. }
  581. .priority-tag {
  582. padding: 1px 6px;
  583. border-radius: 3px;
  584. font-size: 11px;
  585. color: #fff;
  586. }
  587. .priority-tag.high { background: #ff3333; }
  588. .priority-tag.medium { background: #ffb333; color: #333; }
  589. .priority-tag.low { background: #33cc33; }
  590. .status-tag {
  591. padding: 1px 6px;
  592. border-radius: 3px;
  593. font-size: 11px;
  594. color: #fff;
  595. }
  596. .status-tag.processing { background: #3399ff; }
  597. .status-tag.done { background: #33cc33; }
  598. .status-dot {
  599. display: inline-block;
  600. width: 8px;
  601. height: 8px;
  602. border-radius: 50%;
  603. margin-right: 3px;
  604. }
  605. .status-dot.normal { background: #67c23a; }
  606. .status-dot.alert { background: #f56c6c; }
  607. .status-dot.offline { background: #909399; }
  608. .status-text {
  609. font-size: 11px;
  610. color: #dce3ee;
  611. }
  612. /* === 响应式 === */
  613. @media (max-width: 1600px) {
  614. .left-col, .right-col { flex: 0 0 24%; min-width: 260px; max-width: 380px; }
  615. .monitor-container { gap: 12px; padding: 12px; }
  616. .panel { padding: 12px; }
  617. .chart-panel { flex-basis: 170px; }
  618. }
  619. @media (max-width: 1366px) {
  620. .left-col, .right-col { flex: 0 0 27%; min-width: 240px; max-width: 340px; }
  621. .monitor-container { gap: 10px; padding: 10px; }
  622. .panel { padding: 10px; }
  623. .chart-panel { flex-basis: 150px; }
  624. }
  625. @media (max-width: 1024px) {
  626. .monitor-container {
  627. flex-direction: column;
  628. height: auto;
  629. min-height: 100vh;
  630. overflow: auto;
  631. }
  632. .left-col, .right-col {
  633. flex: 1 1 auto;
  634. min-width: 100%;
  635. max-width: 100%;
  636. }
  637. .center-col {
  638. flex: 1 1 auto;
  639. min-width: 100%;
  640. }
  641. }
  642. </style>