gHome.vue 35 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375
  1. <script setup>
  2. import { computed, nextTick, onBeforeUnmount, onMounted, ref } from 'vue'
  3. import * as echarts from 'echarts'
  4. import { Odometer, CircleCheckFilled, VideoCameraFilled, WarnTriangleFilled } from '@element-plus/icons-vue'
  5. import { wgs84ToBd09 } from '@/utils/coordTransform'
  6. import { getGasDashboardSummary, getGisPipeLines } from '@/api/pipeNetwork/basic'
  7. const nowText = ref('2025/12/31 12:59:59')
  8. const weekText = ref('星期三')
  9. const mapError = ref('')
  10. const loading = ref(false)
  11. // ========== 仪表盘数据 ==========
  12. const dashboardData = ref({})
  13. const baseCards = computed(() => {
  14. const bs = dashboardData.value.baseStats || {}
  15. return [
  16. { label: '燃气设备', value: bs.totalEquipments || 0, unit: '台', icon: Odometer, color: '#60b8ff' },
  17. { label: '在线设备', value: bs.onlineEquipments || 0, unit: '台', icon: CircleCheckFilled, color: '#52e0a8' },
  18. { label: '离线设备', value: bs.offlineEquipments || 0, unit: '台', icon: VideoCameraFilled, color: '#ff8e8e' },
  19. { label: '异常设备', value: bs.alarmCount || 0, unit: '台', icon: WarnTriangleFilled, color: '#ffb54d' }
  20. ]
  21. })
  22. const operationSummary = computed(() => {
  23. const ps = dashboardData.value.pipeStats || {}
  24. return [
  25. { label: '管网总长度', value: (ps.totalLength || 0) + 'km' },
  26. { label: '燃气管网', value: (ps.gasNetworks || 0) + '条' }
  27. ]
  28. })
  29. const repairRecords = computed(() => {
  30. const list = dashboardData.value.repairTrend || []
  31. if (list.length > 0) return list
  32. return [
  33. { month: '1月', value: 0 }, { month: '2月', value: 0 }, { month: '3月', value: 0 },
  34. { month: '4月', value: 0 }, { month: '5月', value: 0 }, { month: '6月', value: 0 }
  35. ]
  36. })
  37. const pointRows = computed(() => {
  38. const list = dashboardData.value.pointRows || []
  39. return list.length > 0 ? list : [{ id: 1, name: '暂无数据', time: '--', value: '--', status: '离线' }]
  40. })
  41. const equipmentRows = computed(() => {
  42. const list = dashboardData.value.equipmentRows || []
  43. return list.length > 0 ? list : [{ id: 1, name: '暂无数据', code: '--', location: '--', status: '--' }]
  44. })
  45. const warningRows = computed(() => {
  46. const list = dashboardData.value.warningRows || []
  47. return list.length > 0 ? list : [{ level: '中', title: '暂无告警', code: '--', time: '--:--:--', desc: '当前无告警信息', tag: '正常' }]
  48. })
  49. const hazardSummary = computed(() => {
  50. const map = dashboardData.value.hazardStats || {}
  51. return [
  52. { label: '重大隐患', value: map['重大隐患'] || 0, color: '#ff3030' },
  53. { label: '较大隐患', value: map['较大隐患'] || 0, color: '#ff9f1a' },
  54. { label: '一般隐患', value: map['一般隐患'] || 0, color: '#f3e64d' }
  55. ]
  56. })
  57. const mapKpis = computed(() => {
  58. const bs = dashboardData.value.baseStats || {}
  59. const es = dashboardData.value.equipmentStats || {}
  60. const onlineRate = bs.onlineRate
  61. const avgPressure = es.avgPressure
  62. return [
  63. { label: '在线率', value: onlineRate != null ? Number(onlineRate).toFixed(1) + '%' : '--' },
  64. { label: '压力均值', value: avgPressure != null ? Number(avgPressure).toFixed(2) + 'MPa' : '--' },
  65. { label: '今日告警', value: (es.todayAlarm || 0) + '条' }
  66. ]
  67. })
  68. /** 压力最新采集时间,用于说明均值取自哪个时刻 */
  69. const pressureTimeText = computed(() => {
  70. const text = dashboardData.value.pressureUpdatedAt
  71. if (!text) return '--'
  72. return text.length > 8 ? text.slice(-8) : text
  73. })
  74. const mapWrapRef = ref(null)
  75. const repairChartRef = ref(null)
  76. const hazardChartRef = ref(null)
  77. let mapInstance = null
  78. let repairChart = null
  79. let hazardChart = null
  80. let clockTimer = null
  81. function formatWeek(day) {
  82. return ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'][day]
  83. }
  84. function pad(value) {
  85. return String(value).padStart(2, '0')
  86. }
  87. function updateClock() {
  88. const now = new Date()
  89. nowText.value = `${now.getFullYear()}/${pad(now.getMonth() + 1)}/${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`
  90. weekText.value = formatWeek(now.getDay())
  91. }
  92. function initRepairChart() {
  93. if (!repairChartRef.value) return
  94. // 定时刷新时复用已有实例,避免整块图表重建闪烁
  95. if (!repairChart) {
  96. repairChart = echarts.init(repairChartRef.value)
  97. }
  98. const data = repairRecords.value
  99. repairChart.setOption({
  100. grid: { left: 30, right: 12, top: 20, bottom: 24, containLabel: true },
  101. xAxis: {
  102. type: 'category',
  103. data: data.map(item => item.month),
  104. axisLine: { lineStyle: { color: 'rgba(91, 167, 219, 0.8)' } },
  105. axisLabel: { color: '#d2efff' }
  106. },
  107. yAxis: {
  108. type: 'value',
  109. splitLine: { lineStyle: { color: 'rgba(91, 167, 219, 0.18)' } },
  110. axisLine: { show: false },
  111. axisLabel: { color: '#d2efff' }
  112. },
  113. tooltip: { trigger: 'axis' },
  114. series: [{
  115. type: 'bar',
  116. barWidth: 24,
  117. data: data.map(item => item.value),
  118. itemStyle: {
  119. borderRadius: [6, 6, 0, 0],
  120. color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
  121. { offset: 0, color: '#38d8ff' },
  122. { offset: 1, color: '#166dff' }
  123. ])
  124. }
  125. }]
  126. })
  127. }
  128. function initHazardChart() {
  129. if (!hazardChartRef.value) return
  130. if (!hazardChart) {
  131. hazardChart = echarts.init(hazardChartRef.value)
  132. }
  133. const data = hazardSummary.value
  134. hazardChart.setOption({
  135. grid: { left: 18, right: 18, top: 10, bottom: 10, containLabel: true },
  136. xAxis: {
  137. type: 'value',
  138. splitLine: { show: false },
  139. axisLine: { show: false },
  140. axisTick: { show: false },
  141. axisLabel: { show: false }
  142. },
  143. yAxis: {
  144. type: 'category',
  145. data: data.map(item => item.label),
  146. axisLine: { show: false },
  147. axisTick: { show: false },
  148. axisLabel: { color: '#d2efff', fontSize: 13 }
  149. },
  150. tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
  151. series: [{
  152. type: 'bar',
  153. data: data.map(item => ({ value: item.value, itemStyle: { color: item.color, borderRadius: 10 } })),
  154. barWidth: 14,
  155. label: { show: true, position: 'right', color: '#fff', fontWeight: 'bold' },
  156. showBackground: true,
  157. backgroundStyle: { color: 'rgba(255,255,255,0.08)', borderRadius: 10 }
  158. }]
  159. })
  160. }
  161. function waitForBMapGL() {
  162. return new Promise((resolve, reject) => {
  163. if (window.BMapGL) {
  164. resolve(window.BMapGL)
  165. return
  166. }
  167. let attempts = 0
  168. const timer = setInterval(() => {
  169. attempts += 1
  170. if (window.BMapGL) {
  171. clearInterval(timer)
  172. resolve(window.BMapGL)
  173. return
  174. }
  175. if (attempts >= 100) {
  176. clearInterval(timer)
  177. reject(new Error('百度地图加载超时'))
  178. }
  179. }, 200)
  180. })
  181. }
  182. /** 管网状态 → 线条颜色,取值见字典 pipe_network_status:0 正常 / 1 维护中 / 2 异常 */
  183. const PIPE_STATUS_COLOR = { 0: '#ff4d4f', 1: '#ffb020', 2: '#ff1f1f' }
  184. /** 压力点状态 → 标签配色 */
  185. const PRESSURE_STATUS_STYLE = {
  186. normal: { border: 'rgba(82, 224, 168, 0.75)', text: '#d8ffe9' },
  187. warning: { border: 'rgba(255, 181, 77, 0.85)', text: '#ffe9c7' },
  188. danger: { border: 'rgba(255, 48, 48, 0.9)', text: '#ffd6d6' },
  189. offline: { border: 'rgba(150, 170, 190, 0.7)', text: '#cfd8e3' }
  190. }
  191. /** 地图叠加物,刷新时整体清除后重画 */
  192. let pipeOverlays = []
  193. let pressureOverlays = []
  194. function clearOverlays(overlays) {
  195. if (mapInstance) {
  196. overlays.forEach(overlay => mapInstance.removeOverlay(overlay))
  197. }
  198. return []
  199. }
  200. /**
  201. * 绘制燃气管段
  202. *
  203. * 燃气管网每条记录只有起终点坐标,按状态着色,不再逐点画装饰圆(数百条管段会拖慢地图)。
  204. */
  205. function drawPipelines(BMapGL, features) {
  206. pipeOverlays = clearOverlays(pipeOverlays)
  207. features.forEach(feature => {
  208. const props = feature.properties || {}
  209. const coordinates = feature.geometry && feature.geometry.coordinates
  210. if (!Array.isArray(coordinates) || coordinates.length < 2) return
  211. const path = coordinates
  212. .map(([lng, lat]) => {
  213. const [bdLng, bdLat] = wgs84ToBd09(Number(lng), Number(lat))
  214. return new BMapGL.Point(bdLng, bdLat)
  215. })
  216. .filter(point => !Number.isNaN(point.lng) && !Number.isNaN(point.lat))
  217. if (path.length < 2) return
  218. const status = String(props.status == null ? '' : props.status)
  219. const line = new BMapGL.Polyline(path, {
  220. strokeColor: PIPE_STATUS_COLOR[status] || PIPE_STATUS_COLOR[0],
  221. strokeWeight: status === '2' ? 5 : 4,
  222. strokeOpacity: 0.92
  223. })
  224. mapInstance.addOverlay(line)
  225. pipeOverlays.push(line)
  226. })
  227. }
  228. /**
  229. * 绘制压力监测点,标签显示站点名称与最新压力值
  230. */
  231. function drawPressurePoints(BMapGL, points) {
  232. pressureOverlays = clearOverlays(pressureOverlays)
  233. points.forEach(point => {
  234. const lng = Number(point.lng)
  235. const lat = Number(point.lat)
  236. if (!lng || !lat) return
  237. const [bdLng, bdLat] = wgs84ToBd09(lng, lat)
  238. const position = new BMapGL.Point(bdLng, bdLat)
  239. const marker = new BMapGL.Marker(position)
  240. mapInstance.addOverlay(marker)
  241. pressureOverlays.push(marker)
  242. const style = PRESSURE_STATUS_STYLE[String(point.status || 'normal')] || PRESSURE_STATUS_STYLE.normal
  243. const pressureText = point.latestPressure == null
  244. ? '--'
  245. : (Number(point.latestPressure) / 1000).toFixed(2) + 'MPa'
  246. const label = new BMapGL.Label(`${point.name || '压力点'} ${pressureText}`, {
  247. offset: new BMapGL.Size(-48, -44)
  248. })
  249. label.setPosition(position)
  250. label.setStyle({
  251. color: style.text,
  252. background: 'rgba(12, 46, 82, 0.86)',
  253. border: `1px solid ${style.border}`,
  254. borderRadius: '8px',
  255. padding: '5px 9px',
  256. fontSize: '12px',
  257. lineHeight: '16px',
  258. whiteSpace: 'nowrap'
  259. })
  260. mapInstance.addOverlay(label)
  261. pressureOverlays.push(label)
  262. })
  263. }
  264. /** 按管网实际范围自动缩放,只有一条管段时按固定级别定位 */
  265. function fitPipeView(BMapGL, features) {
  266. const points = []
  267. features.forEach(feature => {
  268. const coordinates = feature.geometry && feature.geometry.coordinates
  269. if (!Array.isArray(coordinates)) return
  270. coordinates.forEach(([lng, lat]) => {
  271. const [bdLng, bdLat] = wgs84ToBd09(Number(lng), Number(lat))
  272. points.push(new BMapGL.Point(bdLng, bdLat))
  273. })
  274. })
  275. if (!points.length) return
  276. if (points.length === 1) {
  277. mapInstance.centerAndZoom(points[0], 16)
  278. return
  279. }
  280. mapInstance.setViewport(points)
  281. }
  282. /** 拉取燃气管网数据并上图 */
  283. async function loadPipeLines(BMapGL) {
  284. try {
  285. const res = await getGisPipeLines({ networkType: '0' })
  286. const features = (res.data && res.data.features) || []
  287. if (!features.length) {
  288. mapError.value = '暂无燃气管网坐标数据,请先在管网基础数据中补齐起终点经纬度。'
  289. return
  290. }
  291. mapError.value = ''
  292. drawPipelines(BMapGL, features)
  293. fitPipeView(BMapGL, features)
  294. } catch (error) {
  295. console.error('[燃气大屏] 加载燃气管网失败', error)
  296. mapError.value = '燃气管网数据加载失败,请稍后重试。'
  297. }
  298. }
  299. async function initMap() {
  300. if (!mapWrapRef.value) return
  301. try {
  302. const BMapGL = await waitForBMapGL()
  303. mapInstance = new BMapGL.Map(mapWrapRef.value, { enableMapClick: true })
  304. const center = new BMapGL.Point(110.3965, 28.4638)
  305. mapInstance.centerAndZoom(center, 15)
  306. mapInstance.enableScrollWheelZoom(true)
  307. mapInstance.setMapStyleV2({ styleId: 'a0f4e6e6f9e7a6c5d8e7f7e7f7e7f7e5' })
  308. drawPressurePoints(BMapGL, dashboardData.value.pressurePoints || [])
  309. await loadPipeLines(BMapGL)
  310. } catch (error) {
  311. mapError.value = '百度地图加载失败,请检查地图脚本或网络连接。'
  312. }
  313. }
  314. /** 定时刷新压力点位,不重建底图 */
  315. function refreshPressureLayer() {
  316. if (!mapInstance || !window.BMapGL) return
  317. drawPressurePoints(window.BMapGL, dashboardData.value.pressurePoints || [])
  318. }
  319. function handleResize() {
  320. repairChart?.resize()
  321. hazardChart?.resize()
  322. }
  323. /** 仪表盘刷新间隔(毫秒),压力点位与在线率按此周期同步最新采集值 */
  324. const REFRESH_INTERVAL = 30 * 1000
  325. let refreshTimer = null
  326. async function loadDashboardData(silent = false) {
  327. if (!silent) loading.value = true
  328. try {
  329. const res = await getGasDashboardSummary()
  330. dashboardData.value = res.data || {}
  331. refreshPressureLayer()
  332. if (silent) {
  333. initRepairChart()
  334. initHazardChart()
  335. }
  336. } catch (e) {
  337. console.error('[燃气大屏] 加载仪表盘数据失败', e)
  338. } finally {
  339. if (!silent) loading.value = false
  340. }
  341. }
  342. onMounted(async () => {
  343. updateClock()
  344. clockTimer = window.setInterval(updateClock, 1000)
  345. await loadDashboardData()
  346. await nextTick()
  347. initRepairChart()
  348. initHazardChart()
  349. await initMap()
  350. refreshTimer = window.setInterval(() => loadDashboardData(true), REFRESH_INTERVAL)
  351. window.addEventListener('resize', handleResize)
  352. })
  353. onBeforeUnmount(() => {
  354. if (clockTimer) window.clearInterval(clockTimer)
  355. if (refreshTimer) window.clearInterval(refreshTimer)
  356. window.removeEventListener('resize', handleResize)
  357. repairChart?.dispose()
  358. hazardChart?.dispose()
  359. pipeOverlays = []
  360. pressureOverlays = []
  361. mapInstance = null
  362. })
  363. </script>
  364. <template>
  365. <div class="gas-home-screen">
  366. <div class="screen-header">
  367. <div class="header-time">{{ nowText }} {{ weekText }}</div>
  368. <div class="header-title-wrap">
  369. <div class="header-title">燃气管网安全运行监测系统</div>
  370. <div class="header-subtitle">Gas Pipeline Operation Dashboard</div>
  371. </div>
  372. <div class="header-tags">
  373. <span class="header-tag">实时监测</span>
  374. <span class="header-tag">管网运行</span>
  375. <span class="header-tag">隐患预警</span>
  376. </div>
  377. </div>
  378. <div class="screen-body">
  379. <div class="side-panel left-panel">
  380. <section class="panel-card base-card">
  381. <div class="panel-title">基础概况</div>
  382. <div class="base-grid">
  383. <div v-for="item in baseCards" :key="item.label" class="base-item">
  384. <div class="base-icon" :style="{ background: item.color }">
  385. <component :is="item.icon" class="base-icon-svg" />
  386. </div>
  387. <div class="base-text">
  388. <div class="base-label">{{ item.label }}</div>
  389. <div class="base-value">{{ item.value }}<span>{{ item.unit }}</span></div>
  390. </div>
  391. </div>
  392. </div>
  393. </section>
  394. <section class="panel-card">
  395. <div class="panel-title">维修趋势</div>
  396. <div ref="repairChartRef" class="chart-box repair-chart"></div>
  397. </section>
  398. <section class="panel-card table-card">
  399. <div class="panel-title">监测点数据</div>
  400. <div class="table-wrap">
  401. <table class="data-table">
  402. <thead>
  403. <tr>
  404. <th>序号</th>
  405. <th>监测点</th>
  406. <th>时间</th>
  407. <th>数值</th>
  408. <th>状态</th>
  409. </tr>
  410. </thead>
  411. <tbody>
  412. <tr v-for="row in pointRows" :key="row.id">
  413. <td>{{ row.id }}</td>
  414. <td :title="`${row.name || '-'} / ${row.deviceName || row.deviceCode || '-'}`">{{ row.name }} / {{ row.deviceName || row.deviceCode || '-' }}</td>
  415. <td :title="row.time">{{ row.time }}</td>
  416. <td :title="row.value">{{ row.value }}</td>
  417. <td :class="['status-cell', row.status === '在线' ? 'ok' : row.status === '预警' ? 'warn' : 'off']">{{ row.status }}</td>
  418. </tr>
  419. </tbody>
  420. </table>
  421. </div>
  422. </section>
  423. </div>
  424. <div class="center-panel">
  425. <div class="map-stage">
  426. <!-- 管网、在线率、压力、告警合并为一条指标条,按份均分宽度,窄屏也不会溢出 -->
  427. <div class="map-stage-top">
  428. <div v-for="item in operationSummary" :key="item.label" class="summary-item">
  429. <span class="summary-label">{{ item.label }}</span>
  430. <strong class="summary-value">{{ item.value }}</strong>
  431. </div>
  432. <div v-for="item in mapKpis" :key="item.label" class="map-kpi">
  433. <span>{{ item.label }}</span>
  434. <strong>{{ item.value }}</strong>
  435. </div>
  436. </div>
  437. <div ref="mapWrapRef" class="map-view"></div>
  438. <div v-if="mapError" class="map-error">{{ mapError }}</div>
  439. <div class="map-panel info-panel">
  440. <div class="mini-title">当前管网信息</div>
  441. <div class="mini-row"><span>管网总长度</span><strong>{{ (dashboardData.pipeStats && dashboardData.pipeStats.totalLength) || 0 }}km</strong></div>
  442. <div class="mini-row"><span>燃气管网</span><strong>{{ (dashboardData.pipeStats && dashboardData.pipeStats.gasNetworks) || 0 }}条</strong></div>
  443. <div class="mini-row"><span>设备总数</span><strong>{{ (dashboardData.equipmentStats && dashboardData.equipmentStats.totalEquipment) || 0 }}台</strong></div>
  444. <div class="mini-row" :title="dashboardData.pressureUpdatedAt || '暂无压力采集时间'">
  445. <span>压力采集</span><strong>{{ pressureTimeText }}</strong>
  446. </div>
  447. </div>
  448. <div class="map-panel point-panel">
  449. <div class="mini-title">当前管点信息</div>
  450. <div class="mini-row"><span>监测点总数</span><strong>{{ (dashboardData.baseStats && dashboardData.baseStats.totalPoints) || 0 }}个</strong></div>
  451. <div class="mini-row"><span>在线监测点</span><strong>{{ (dashboardData.baseStats && dashboardData.baseStats.onlinePoints) || 0 }}个</strong></div>
  452. <div class="mini-row"><span>离线监测点</span><strong>{{ (dashboardData.baseStats && dashboardData.baseStats.offlinePoints) || 0 }}个</strong></div>
  453. </div>
  454. </div>
  455. </div>
  456. <div class="side-panel right-panel">
  457. <section class="panel-card">
  458. <div class="panel-title">设备运行</div>
  459. <div class="table-wrap equipment-wrap">
  460. <table class="data-table compact-table">
  461. <thead>
  462. <tr>
  463. <th>序号</th>
  464. <th>设备名称</th>
  465. <th>设备编号</th>
  466. <th>位置</th>
  467. <th>状态</th>
  468. </tr>
  469. </thead>
  470. <tbody>
  471. <tr v-for="row in equipmentRows" :key="row.id">
  472. <td>{{ row.id }}</td>
  473. <td>{{ row.name }}</td>
  474. <td>{{ row.code }}</td>
  475. <td>{{ row.location }}</td>
  476. <td>{{ row.status }}</td>
  477. </tr>
  478. </tbody>
  479. </table>
  480. </div>
  481. </section>
  482. <section class="panel-card warning-card">
  483. <div class="panel-title">告警动态</div>
  484. <div class="warning-scroll">
  485. <div class="warning-list">
  486. <div v-for="(item, index) in warningRows" :key="index" class="warning-item" :class="item.level === '高' ? 'danger' : 'notice'">
  487. <div class="warning-side">
  488. <div class="warning-badge">{{ item.level }}</div>
  489. <div class="warning-tag">{{ item.tag }}</div>
  490. </div>
  491. <div class="warning-main">
  492. <div class="warning-title-row">
  493. <span class="warning-title">{{ item.title }}</span>
  494. <span class="warning-time">{{ item.time }}</span>
  495. </div>
  496. <div class="warning-code">{{ item.code }}</div>
  497. <div class="warning-desc">{{ item.desc }}</div>
  498. </div>
  499. </div>
  500. </div>
  501. </div>
  502. </section>
  503. <section class="panel-card hazard-card">
  504. <div class="panel-title">隐患统计</div>
  505. <div class="hazard-summary three-col">
  506. <div v-for="item in hazardSummary" :key="item.label" class="hazard-summary-item">
  507. <span class="hazard-dot" :style="{ background: item.color }"></span>
  508. <span>{{ item.label }}</span>
  509. <strong :style="{ color: item.color }">{{ item.value }}项</strong>
  510. </div>
  511. </div>
  512. <div ref="hazardChartRef" class="chart-box hazard-chart"></div>
  513. </section>
  514. </div>
  515. </div>
  516. <a href="https://deerflow.tech" target="_blank" class="deerflow-mark" title="Created By Deerflow">
  517. <span class="deerflow-diamond">◆</span>
  518. <span class="deerflow-text">Deerflow</span>
  519. </a>
  520. </div>
  521. </template>
  522. <style scoped lang="scss">
  523. .gas-home-screen {
  524. min-height: 100vh;
  525. color: #fff;
  526. background:
  527. linear-gradient(180deg, rgba(14, 42, 70, 0.96), rgba(8, 20, 37, 1)),
  528. radial-gradient(circle at center, rgba(36, 122, 184, 0.18), transparent 58%),
  529. #081425;
  530. position: relative;
  531. }
  532. .gas-home-screen::before {
  533. content: '';
  534. position: absolute;
  535. inset: 0;
  536. pointer-events: none;
  537. z-index: 0;
  538. background:
  539. radial-gradient(circle at 28% 32%, rgba(56, 216, 255, 0.06) 0%, transparent 45%),
  540. radial-gradient(circle at 72% 58%, rgba(22, 109, 255, 0.05) 0%, transparent 40%),
  541. radial-gradient(circle at 50% 85%, rgba(56, 216, 255, 0.04) 0%, transparent 35%);
  542. animation: bgPulse 8s ease-in-out infinite alternate;
  543. }
  544. @keyframes bgPulse {
  545. 0% { opacity: 0.6; }
  546. 100% { opacity: 1; }
  547. }
  548. .screen-header {
  549. height: 72px;
  550. padding: 0 24px;
  551. display: grid;
  552. grid-template-columns: 320px 1fr 320px;
  553. align-items: center;
  554. position: relative;
  555. z-index: 1;
  556. background: linear-gradient(90deg, rgba(27, 98, 155, 0.95), rgba(18, 53, 98, 0.95));
  557. border-bottom: 1px solid rgba(93, 194, 255, 0.28);
  558. }
  559. .screen-header::after {
  560. content: '';
  561. position: absolute;
  562. bottom: -1px;
  563. left: 5%;
  564. width: 90%;
  565. height: 1px;
  566. background: linear-gradient(90deg, transparent, rgba(56, 216, 255, 0.7), rgba(22, 109, 255, 0.7), transparent);
  567. animation: headerGlow 4s ease-in-out infinite alternate;
  568. }
  569. @keyframes headerGlow {
  570. 0% { opacity: 0.4; }
  571. 100% { opacity: 1; }
  572. }
  573. .header-time {
  574. font-size: 18px;
  575. font-weight: 700;
  576. letter-spacing: 1.5px;
  577. color: #ddf6ff;
  578. font-variant-numeric: tabular-nums;
  579. text-shadow: 0 0 18px rgba(56, 216, 255, 0.35);
  580. }
  581. .header-title-wrap {
  582. text-align: center;
  583. }
  584. .header-title {
  585. font-size: 30px;
  586. font-weight: 800;
  587. letter-spacing: 3px;
  588. background: linear-gradient(180deg, #e8f6ff 0%, #8fd6ff 100%);
  589. -webkit-background-clip: text;
  590. -webkit-text-fill-color: transparent;
  591. background-clip: text;
  592. }
  593. .header-subtitle {
  594. margin-top: 4px;
  595. color: rgba(196, 234, 255, 0.65);
  596. font-size: 11px;
  597. letter-spacing: 3px;
  598. text-transform: uppercase;
  599. }
  600. .header-tags {
  601. display: flex;
  602. justify-content: flex-end;
  603. gap: 8px;
  604. }
  605. .header-tag {
  606. padding: 6px 14px;
  607. border: 1px solid rgba(117, 206, 255, 0.35);
  608. border-radius: 16px;
  609. color: #d8f5ff;
  610. background: rgba(13, 77, 128, 0.45);
  611. font-size: 12px;
  612. letter-spacing: 1px;
  613. transition: all 0.3s;
  614. cursor: default;
  615. }
  616. .header-tag:hover {
  617. border-color: rgba(117, 206, 255, 0.7);
  618. background: rgba(13, 77, 128, 0.65);
  619. box-shadow: 0 0 16px rgba(56, 216, 255, 0.15);
  620. }
  621. .screen-body {
  622. height: calc(100vh - 72px);
  623. padding: 12px;
  624. display: grid;
  625. grid-template-columns: 360px 1fr 360px;
  626. gap: 12px;
  627. position: relative;
  628. z-index: 1;
  629. }
  630. .side-panel,
  631. .center-panel {
  632. min-height: 0;
  633. }
  634. .left-panel,
  635. .right-panel {
  636. display: grid;
  637. gap: 12px;
  638. }
  639. .left-panel {
  640. grid-template-rows: 220px 220px 1fr;
  641. }
  642. .right-panel {
  643. grid-template-rows: 250px 260px 1fr;
  644. }
  645. .panel-card {
  646. min-height: 0;
  647. padding: 14px;
  648. position: relative;
  649. background: linear-gradient(180deg, rgba(13, 33, 56, 0.88), rgba(10, 25, 42, 0.88));
  650. border: 1px solid rgba(65, 158, 219, 0.22);
  651. border-radius: 12px;
  652. box-shadow: 0 2px 24px rgba(0, 0, 0, 0.25), inset 0 0 0 1px rgba(255, 255, 255, 0.025);
  653. transition: border-color 0.4s;
  654. }
  655. .panel-card::after {
  656. content: '';
  657. position: absolute;
  658. top: -1px;
  659. left: 20px;
  660. right: 20px;
  661. height: 1px;
  662. border-radius: 1px;
  663. background: linear-gradient(90deg, transparent, rgba(56, 216, 255, 0.25), transparent);
  664. pointer-events: none;
  665. }
  666. .panel-card:hover {
  667. border-color: rgba(65, 158, 219, 0.38);
  668. }
  669. .panel-title {
  670. position: relative;
  671. padding-left: 16px;
  672. margin-bottom: 14px;
  673. color: #8fe5ff;
  674. font-size: 18px;
  675. font-weight: 800;
  676. letter-spacing: 1px;
  677. }
  678. .panel-title::before {
  679. content: '';
  680. position: absolute;
  681. left: 0;
  682. top: 3px;
  683. width: 4px;
  684. height: 20px;
  685. border-radius: 2px;
  686. background: linear-gradient(180deg, #28d7ff, #1776ff);
  687. box-shadow: 0 0 10px rgba(40, 215, 255, 0.5);
  688. }
  689. .base-grid {
  690. display: grid;
  691. grid-template-columns: repeat(2, minmax(0, 1fr));
  692. gap: 14px;
  693. }
  694. .base-card {
  695. background: linear-gradient(180deg, rgba(10, 30, 53, 0.95), rgba(9, 24, 42, 0.95));
  696. }
  697. .base-grid {
  698. grid-template-columns: repeat(2, minmax(0, 1fr));
  699. gap: 12px;
  700. }
  701. .base-item {
  702. display: flex;
  703. align-items: center;
  704. gap: 14px;
  705. min-height: 72px;
  706. padding: 14px;
  707. border-radius: 14px;
  708. background: linear-gradient(180deg, rgba(20, 57, 89, 0.45), rgba(18, 44, 72, 0.35));
  709. border: 1px solid rgba(97, 190, 255, 0.12);
  710. transition: all 0.35s cubic-bezier(0.4, 0, 0.2, 1);
  711. cursor: default;
  712. position: relative;
  713. overflow: hidden;
  714. }
  715. .base-item::before {
  716. content: '';
  717. position: absolute;
  718. inset: 0;
  719. border-radius: 14px;
  720. background: radial-gradient(circle at 30% 30%, rgba(255, 255, 255, 0.03), transparent 70%);
  721. pointer-events: none;
  722. transition: opacity 0.35s;
  723. opacity: 0;
  724. }
  725. .base-item:hover {
  726. transform: translateY(-2px);
  727. border-color: rgba(97, 190, 255, 0.35);
  728. box-shadow: 0 8px 28px rgba(0, 0, 0, 0.35), 0 0 0 1px rgba(97, 190, 255, 0.1) inset;
  729. }
  730. .base-item:hover::before {
  731. opacity: 1;
  732. }
  733. .base-icon {
  734. width: 48px;
  735. height: 48px;
  736. border-radius: 16px;
  737. display: flex;
  738. align-items: center;
  739. justify-content: center;
  740. flex: 0 0 48px;
  741. position: relative;
  742. transition: transform 0.35s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.35s;
  743. }
  744. .base-item:hover .base-icon {
  745. transform: scale(1.08);
  746. box-shadow: 0 0 28px rgba(56, 216, 255, 0.3);
  747. }
  748. .base-icon-svg {
  749. width: 26px;
  750. height: 26px;
  751. color: #fff;
  752. transition: transform 0.35s;
  753. }
  754. .base-item:hover .base-icon-svg {
  755. transform: scale(1.1);
  756. }
  757. .base-text {
  758. min-width: 0;
  759. }
  760. .base-label {
  761. color: #c0dff5;
  762. font-size: 12px;
  763. letter-spacing: 1px;
  764. text-transform: uppercase;
  765. }
  766. .base-value {
  767. margin-top: 4px;
  768. color: #fff;
  769. font-size: 32px;
  770. font-weight: 800;
  771. line-height: 1;
  772. font-variant-numeric: tabular-nums;
  773. letter-spacing: -0.5px;
  774. }
  775. .base-value span {
  776. margin-left: 4px;
  777. color: #90d9ff;
  778. font-size: 12px;
  779. font-weight: 500;
  780. letter-spacing: 0;
  781. }
  782. .chart-box {
  783. width: 100%;
  784. height: calc(100% - 36px);
  785. }
  786. .table-card,
  787. .warning-card,
  788. .hazard-card {
  789. overflow: hidden;
  790. }
  791. .table-wrap {
  792. height: calc(100% - 38px);
  793. overflow: auto;
  794. }
  795. .data-table {
  796. width: 100%;
  797. border-collapse: collapse;
  798. table-layout: fixed;
  799. }
  800. .data-table thead {
  801. background: rgba(24, 122, 198, 0.55);
  802. }
  803. .data-table thead th {
  804. font-weight: 700;
  805. letter-spacing: 0.5px;
  806. color: #b9e4ff;
  807. }
  808. .data-table th,
  809. .data-table td {
  810. padding: 10px 6px;
  811. color: #f4fbff;
  812. text-align: center;
  813. font-size: 13px;
  814. border-bottom: 1px solid rgba(130, 194, 231, 0.1);
  815. white-space: nowrap;
  816. overflow: hidden;
  817. text-overflow: ellipsis;
  818. }
  819. .data-table tbody tr {
  820. transition: background 0.25s;
  821. }
  822. .data-table tbody tr:nth-child(odd) {
  823. background: rgba(133, 196, 242, 0.10);
  824. }
  825. .data-table tbody tr:nth-child(even) {
  826. background: rgba(133, 196, 242, 0.04);
  827. }
  828. .data-table tbody tr:hover {
  829. background: rgba(56, 216, 255, 0.12);
  830. }
  831. .compact-table th,
  832. .compact-table td {
  833. padding: 9px 4px;
  834. }
  835. .status-cell.ok {
  836. color: #6effc4;
  837. position: relative;
  838. }
  839. .status-cell.warn {
  840. color: #ffe16a;
  841. }
  842. .status-cell.off {
  843. color: #ff8e8e;
  844. }
  845. .status-cell::before {
  846. content: '';
  847. display: inline-block;
  848. width: 6px;
  849. height: 6px;
  850. border-radius: 50%;
  851. margin-right: 6px;
  852. vertical-align: middle;
  853. }
  854. .status-cell.ok::before {
  855. background: #6effc4;
  856. box-shadow: 0 0 6px rgba(110, 255, 196, 0.6);
  857. }
  858. .status-cell.warn::before {
  859. background: #ffe16a;
  860. box-shadow: 0 0 6px rgba(255, 225, 106, 0.6);
  861. }
  862. .status-cell.off::before {
  863. background: #ff8e8e;
  864. box-shadow: 0 0 6px rgba(255, 142, 142, 0.6);
  865. }
  866. .center-panel {
  867. position: relative;
  868. }
  869. .map-stage {
  870. position: relative;
  871. height: 100%;
  872. overflow: hidden;
  873. border-radius: 12px;
  874. border: 1px solid rgba(56, 159, 223, 0.2);
  875. background: rgba(8, 20, 37, 0.64);
  876. box-shadow: 0 0 40px rgba(0, 0, 0, 0.3), inset 0 0 60px rgba(8, 30, 60, 0.3);
  877. }
  878. .map-stage-top {
  879. position: absolute;
  880. top: 14px;
  881. left: 14px;
  882. right: 14px;
  883. z-index: 3;
  884. display: flex;
  885. align-items: stretch;
  886. gap: 8px;
  887. pointer-events: none;
  888. }
  889. .summary-item,
  890. .map-kpi {
  891. flex: 1 1 0;
  892. min-width: 0;
  893. padding: 8px 10px;
  894. border-radius: 10px;
  895. background: rgba(8, 31, 54, 0.82);
  896. border: 1px solid rgba(79, 177, 238, 0.18);
  897. backdrop-filter: blur(8px);
  898. transition: all 0.3s;
  899. overflow: hidden;
  900. }
  901. .summary-label,
  902. .map-kpi span {
  903. display: block;
  904. color: #91d7ff;
  905. font-size: 12px;
  906. letter-spacing: 0.5px;
  907. white-space: nowrap;
  908. overflow: hidden;
  909. text-overflow: ellipsis;
  910. }
  911. .summary-value,
  912. .map-kpi strong {
  913. display: block;
  914. margin-top: 4px;
  915. color: #fff;
  916. font-size: 20px;
  917. font-weight: 800;
  918. letter-spacing: 0.5px;
  919. white-space: nowrap;
  920. overflow: hidden;
  921. text-overflow: ellipsis;
  922. }
  923. .map-view {
  924. width: 100%;
  925. height: 100%;
  926. }
  927. .map-error {
  928. position: absolute;
  929. inset: 0;
  930. display: flex;
  931. align-items: center;
  932. justify-content: center;
  933. color: #ffd3d3;
  934. background: rgba(5, 10, 20, 0.72);
  935. z-index: 4;
  936. }
  937. .map-panel {
  938. position: absolute;
  939. z-index: 3;
  940. width: 190px;
  941. padding: 14px;
  942. border-radius: 12px;
  943. background: rgba(8, 31, 54, 0.82);
  944. border: 1px solid rgba(79, 177, 238, 0.18);
  945. backdrop-filter: blur(10px);
  946. }
  947. .info-panel {
  948. left: 18px;
  949. bottom: 20px;
  950. }
  951. .point-panel {
  952. right: 18px;
  953. bottom: 20px;
  954. }
  955. .mini-title {
  956. margin-bottom: 10px;
  957. color: #8fe5ff;
  958. font-size: 14px;
  959. font-weight: 700;
  960. letter-spacing: 0.5px;
  961. }
  962. .mini-row {
  963. display: flex;
  964. justify-content: space-between;
  965. margin-top: 6px;
  966. padding: 4px 0;
  967. color: #d6efff;
  968. font-size: 13px;
  969. border-bottom: 1px solid rgba(130, 194, 231, 0.06);
  970. }
  971. .mini-row:last-child {
  972. border-bottom: none;
  973. }
  974. .warning-scroll {
  975. height: calc(100% - 38px);
  976. overflow-y: auto;
  977. padding-right: 4px;
  978. }
  979. .warning-scroll::-webkit-scrollbar {
  980. width: 5px;
  981. }
  982. .warning-scroll::-webkit-scrollbar-thumb {
  983. border-radius: 999px;
  984. background: rgba(96, 180, 235, 0.3);
  985. }
  986. .warning-scroll::-webkit-scrollbar-track {
  987. background: transparent;
  988. }
  989. .warning-list {
  990. display: grid;
  991. gap: 10px;
  992. }
  993. .warning-item {
  994. display: flex;
  995. gap: 12px;
  996. align-items: stretch;
  997. padding: 14px;
  998. border-radius: 14px;
  999. border: 1px solid rgba(255, 108, 108, 0.1);
  1000. background: linear-gradient(180deg, rgba(72, 24, 38, 0.42), rgba(49, 19, 31, 0.30));
  1001. transition: all 0.35s cubic-bezier(0.4, 0, 0.2, 1);
  1002. position: relative;
  1003. overflow: hidden;
  1004. }
  1005. .warning-item::before {
  1006. content: '';
  1007. position: absolute;
  1008. left: 0;
  1009. top: 12px;
  1010. bottom: 12px;
  1011. width: 3px;
  1012. border-radius: 0 3px 3px 0;
  1013. transition: all 0.35s;
  1014. }
  1015. .warning-item.danger::before {
  1016. background: linear-gradient(180deg, #ff5858, #ff3030);
  1017. box-shadow: 0 0 12px rgba(255, 58, 58, 0.5);
  1018. animation: dangerPulse 2s ease-in-out infinite;
  1019. }
  1020. .warning-item.notice::before {
  1021. background: linear-gradient(180deg, #ffc635, #ff9f1a);
  1022. box-shadow: 0 0 10px rgba(255, 198, 53, 0.35);
  1023. }
  1024. @keyframes dangerPulse {
  1025. 0%, 100% { opacity: 0.6; }
  1026. 50% { opacity: 1; }
  1027. }
  1028. .warning-item:hover {
  1029. transform: translateX(3px);
  1030. }
  1031. .warning-item.danger:hover {
  1032. border-color: rgba(255, 108, 108, 0.25);
  1033. box-shadow: 0 4px 20px rgba(255, 58, 58, 0.1);
  1034. }
  1035. .warning-item.notice:hover {
  1036. border-color: rgba(255, 201, 92, 0.2);
  1037. box-shadow: 0 4px 20px rgba(255, 198, 53, 0.08);
  1038. }
  1039. .warning-item.notice {
  1040. border-color: rgba(255, 201, 92, 0.1);
  1041. background: linear-gradient(180deg, rgba(78, 63, 23, 0.28), rgba(48, 43, 20, 0.18));
  1042. }
  1043. .warning-side {
  1044. width: 52px;
  1045. flex: 0 0 52px;
  1046. display: flex;
  1047. flex-direction: column;
  1048. gap: 8px;
  1049. align-items: center;
  1050. }
  1051. .warning-badge {
  1052. width: 34px;
  1053. height: 34px;
  1054. border-radius: 10px;
  1055. display: flex;
  1056. align-items: center;
  1057. justify-content: center;
  1058. font-size: 13px;
  1059. font-weight: 800;
  1060. color: #fff;
  1061. transition: transform 0.3s;
  1062. }
  1063. .warning-item:hover .warning-badge {
  1064. transform: scale(1.1);
  1065. }
  1066. .warning-tag {
  1067. width: 100%;
  1068. padding: 3px 0;
  1069. border-radius: 999px;
  1070. text-align: center;
  1071. font-size: 10px;
  1072. letter-spacing: 0.5px;
  1073. color: #dcefff;
  1074. background: rgba(255, 255, 255, 0.06);
  1075. }
  1076. .warning-item.danger .warning-badge {
  1077. background: rgba(255, 58, 58, 0.28);
  1078. color: #ff8787;
  1079. }
  1080. .warning-item.danger .warning-title {
  1081. color: #ffd8d8;
  1082. }
  1083. .warning-item.notice .warning-badge {
  1084. background: rgba(255, 198, 53, 0.2);
  1085. color: #ffd45d;
  1086. }
  1087. .warning-item.notice .warning-title {
  1088. color: #fff2c4;
  1089. }
  1090. .warning-main {
  1091. min-width: 0;
  1092. flex: 1;
  1093. }
  1094. .warning-title-row {
  1095. display: flex;
  1096. align-items: flex-start;
  1097. justify-content: space-between;
  1098. gap: 8px;
  1099. }
  1100. .warning-title {
  1101. font-weight: 700;
  1102. font-size: 16px;
  1103. line-height: 1.2;
  1104. }
  1105. .warning-time,
  1106. .warning-code,
  1107. .warning-desc {
  1108. color: #dcefff;
  1109. font-size: 12px;
  1110. }
  1111. .warning-time {
  1112. color: #b6dfff;
  1113. white-space: nowrap;
  1114. }
  1115. .warning-code {
  1116. margin: 6px 0 4px;
  1117. color: #9ad8ff;
  1118. }
  1119. .warning-desc {
  1120. line-height: 1.5;
  1121. }
  1122. .hazard-summary {
  1123. display: flex;
  1124. gap: 12px;
  1125. margin-bottom: 10px;
  1126. }
  1127. .hazard-summary.three-col {
  1128. justify-content: space-between;
  1129. }
  1130. .hazard-summary-item {
  1131. display: flex;
  1132. align-items: center;
  1133. gap: 8px;
  1134. font-size: 13px;
  1135. padding: 8px 12px;
  1136. border-radius: 10px;
  1137. background: rgba(255, 255, 255, 0.03);
  1138. border: 1px solid rgba(255, 255, 255, 0.04);
  1139. transition: all 0.3s;
  1140. }
  1141. .hazard-summary-item:hover {
  1142. background: rgba(255, 255, 255, 0.06);
  1143. border-color: rgba(255, 255, 255, 0.08);
  1144. }
  1145. .hazard-dot {
  1146. width: 8px;
  1147. height: 24px;
  1148. display: inline-block;
  1149. border-radius: 4px;
  1150. box-shadow: 0 0 10px currentColor;
  1151. }
  1152. .hazard-chart {
  1153. height: 160px;
  1154. }
  1155. .equipment-wrap {
  1156. height: calc(100% - 38px);
  1157. }
  1158. :deep(.BMap_cpyCtrl),
  1159. :deep(.anchorBL) {
  1160. display: none !important;
  1161. }
  1162. .deerflow-mark {
  1163. position: fixed;
  1164. bottom: 14px;
  1165. right: 18px;
  1166. z-index: 10;
  1167. display: flex;
  1168. align-items: center;
  1169. gap: 6px;
  1170. text-decoration: none;
  1171. opacity: 0.28;
  1172. transition: opacity 0.4s;
  1173. }
  1174. .deerflow-mark:hover {
  1175. opacity: 0.6;
  1176. }
  1177. .deerflow-diamond {
  1178. font-size: 10px;
  1179. color: #8fe5ff;
  1180. }
  1181. .deerflow-text {
  1182. font-size: 11px;
  1183. letter-spacing: 1px;
  1184. color: #8fe5ff;
  1185. font-weight: 500;
  1186. }
  1187. @media (max-width: 1500px) {
  1188. .screen-body {
  1189. grid-template-columns: 320px 1fr 320px;
  1190. }
  1191. .header-title {
  1192. font-size: 24px;
  1193. }
  1194. .summary-item,
  1195. .map-kpi {
  1196. min-width: 0;
  1197. padding: 7px 8px;
  1198. }
  1199. .summary-label,
  1200. .map-kpi span {
  1201. font-size: 11px;
  1202. }
  1203. .summary-value,
  1204. .map-kpi strong {
  1205. font-size: 16px;
  1206. }
  1207. }
  1208. </style>