gHome.vue 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017
  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. const nowText = ref('2025/12/31 12:59:59')
  6. const weekText = ref('星期三')
  7. const mapError = ref('')
  8. const baseCards = computed(() => [
  9. { label: '监测点总数', value: 18, unit: '个', icon: Odometer, color: '#60b8ff' },
  10. { label: '在线监测点', value: 16, unit: '个', icon: CircleCheckFilled, color: '#52e0a8' },
  11. { label: '离线监测点', value: 2, unit: '个', icon: VideoCameraFilled, color: '#ff8e8e' },
  12. { label: '当日预警点', value: 3, unit: '个', icon: WarnTriangleFilled, color: '#ffb54d' }
  13. ])
  14. const operationSummary = [
  15. { label: '管网总长度', value: '128.6km' },
  16. { label: '重点管段', value: '12段' },
  17. { label: '调压站', value: '6座' }
  18. ]
  19. const repairRecords = [
  20. { month: '1月', value: 3 },
  21. { month: '2月', value: 5 },
  22. { month: '3月', value: 4 },
  23. { month: '4月', value: 6 },
  24. { month: '5月', value: 4 },
  25. { month: '6月', value: 7 }
  26. ]
  27. const pointRows = [
  28. { id: 1, name: '辰州路阀井A-01', time: '12:59', value: '0.22MPa', status: '在线' },
  29. { id: 2, name: '古城路调压箱B-02', time: '12:59', value: '0.24MPa', status: '在线' },
  30. { id: 3, name: '迎宾路阀室C-03', time: '12:59', value: '0.18MPa', status: '预警' },
  31. { id: 4, name: '建设路阀井D-04', time: '12:59', value: '0.21MPa', status: '在线' },
  32. { id: 5, name: '滨江路管点E-05', time: '12:59', value: '0.23MPa', status: '在线' },
  33. { id: 6, name: '天宁路调压站F-06', time: '12:59', value: '0.19MPa', status: '离线' },
  34. { id: 7, name: '鸳鸯路阀井G-07', time: '12:59', value: '0.25MPa', status: '在线' },
  35. { id: 8, name: '消防巷支线H-08', time: '12:59', value: '0.20MPa', status: '在线' }
  36. ]
  37. const equipmentRows = [
  38. { id: 1, name: '调压柜A', code: 'RQ-001', location: '辰州路', status: '正常' },
  39. { id: 2, name: '阀门井B', code: 'RQ-002', location: '古城路', status: '正常' },
  40. { id: 3, name: '流量计C', code: 'RQ-003', location: '迎宾路', status: '检修中' },
  41. { id: 4, name: '压力表D', code: 'RQ-004', location: '建设路', status: '正常' },
  42. { id: 5, name: '调压箱E', code: 'RQ-005', location: '滨江路', status: '正常' }
  43. ]
  44. const warningRows = [
  45. { level: '高', title: '压力波动偏高', code: '辰州路阀井A-01', time: '12:59:59', desc: '建议复核上游调压状态', tag: '压力异常' },
  46. { level: '中', title: '疑似泄漏趋势', code: '迎宾路阀室C-03', time: '12:58:40', desc: '瞬时流量较近30分钟均值偏高', tag: '流量异常' },
  47. { level: '高', title: '通信中断', code: '天宁路调压站F-06', time: '12:57:12', desc: '采集设备离线超过15分钟', tag: '设备离线' },
  48. { level: '中', title: '设备开盖异常', code: '古城路调压箱B-02', time: '12:56:22', desc: '巡检确认现场状态', tag: '巡检关注' },
  49. { level: '中', title: '阀门状态异常', code: '滨江路管点E-05', time: '12:54:16', desc: '阀门开度反馈与设定值存在偏差', tag: '阀门异常' },
  50. { level: '高', title: '瞬时流量突增', code: '鸳鸯路阀井G-07', time: '12:52:48', desc: '短时流量连续3次超出阈值上限', tag: '流量异常' },
  51. { level: '中', title: '电池电量偏低', code: '消防巷支线H-08', time: '12:49:05', desc: '建议尽快安排现场更换电池', tag: '设备维护' },
  52. { level: '高', title: '站点离线恢复失败', code: '天宁路调压站F-06', time: '12:45:31', desc: '自动重连3次未成功,请人工排查', tag: '设备离线' }
  53. ]
  54. const hazardSummary = [
  55. { label: '一般隐患', value: 14, color: '#f3e64d' },
  56. { label: '较大隐患', value: 5, color: '#ff9f1a' },
  57. { label: '重大隐患', value: 2, color: '#ff3030' }
  58. ]
  59. const mapKpis = [
  60. { label: '在线率', value: '88.9%' },
  61. { label: '压力均值', value: '0.21MPa' },
  62. { label: '今日告警', value: '4条' }
  63. ]
  64. const pipelineLines = [
  65. {
  66. name: '辰州路主干线',
  67. color: '#39f5ff',
  68. flow: '瞬时流量 0.52m³/min',
  69. points: [
  70. [110.3855, 28.4696],
  71. [110.3873, 28.4668],
  72. [110.3894, 28.4633],
  73. [110.3922, 28.4598],
  74. [110.3978, 28.4568],
  75. [110.4058, 28.4558],
  76. [110.4148, 28.4551]
  77. ]
  78. },
  79. {
  80. name: '古城路支线',
  81. color: '#14d8ff',
  82. flow: '瞬时流量 0.38m³/min',
  83. points: [
  84. [110.3892, 28.4728],
  85. [110.3928, 28.4704],
  86. [110.3964, 28.4683],
  87. [110.4011, 28.4672],
  88. [110.4072, 28.4669]
  89. ]
  90. }
  91. ]
  92. const pipePoints = [
  93. { name: 'A点', value: '0.22MPa', lng: 110.3872, lat: 28.4669 },
  94. { name: 'B点', value: '0.24MPa', lng: 110.3894, lat: 28.4633 },
  95. { name: 'C点', value: '0.18MPa', lng: 110.3922, lat: 28.4598 },
  96. { name: 'D点', value: '0.23MPa', lng: 110.3978, lat: 28.4568 },
  97. { name: 'E点', value: '0.25MPa', lng: 110.4058, lat: 28.4558 },
  98. { name: 'F点', value: '0.19MPa', lng: 110.3892, lat: 28.4728 },
  99. { name: 'G点', value: '0.21MPa', lng: 110.4011, lat: 28.4672 }
  100. ]
  101. const mapWrapRef = ref(null)
  102. const repairChartRef = ref(null)
  103. const hazardChartRef = ref(null)
  104. let mapInstance = null
  105. let repairChart = null
  106. let hazardChart = null
  107. let clockTimer = null
  108. function formatWeek(day) {
  109. return ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'][day]
  110. }
  111. function pad(value) {
  112. return String(value).padStart(2, '0')
  113. }
  114. function updateClock() {
  115. const now = new Date()
  116. nowText.value = `${now.getFullYear()}/${pad(now.getMonth() + 1)}/${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`
  117. weekText.value = formatWeek(now.getDay())
  118. }
  119. function initRepairChart() {
  120. if (!repairChartRef.value) return
  121. repairChart?.dispose()
  122. repairChart = echarts.init(repairChartRef.value)
  123. repairChart.setOption({
  124. grid: { left: 30, right: 12, top: 20, bottom: 24, containLabel: true },
  125. xAxis: {
  126. type: 'category',
  127. data: repairRecords.map(item => item.month),
  128. axisLine: { lineStyle: { color: 'rgba(91, 167, 219, 0.8)' } },
  129. axisLabel: { color: '#d2efff' }
  130. },
  131. yAxis: {
  132. type: 'value',
  133. splitLine: { lineStyle: { color: 'rgba(91, 167, 219, 0.18)' } },
  134. axisLine: { show: false },
  135. axisLabel: { color: '#d2efff' }
  136. },
  137. tooltip: { trigger: 'axis' },
  138. series: [{
  139. type: 'bar',
  140. barWidth: 24,
  141. data: repairRecords.map(item => item.value),
  142. itemStyle: {
  143. borderRadius: [6, 6, 0, 0],
  144. color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
  145. { offset: 0, color: '#38d8ff' },
  146. { offset: 1, color: '#166dff' }
  147. ])
  148. }
  149. }]
  150. })
  151. }
  152. function initHazardChart() {
  153. if (!hazardChartRef.value) return
  154. hazardChart?.dispose()
  155. hazardChart = echarts.init(hazardChartRef.value)
  156. hazardChart.setOption({
  157. grid: { left: 18, right: 18, top: 10, bottom: 10, containLabel: true },
  158. xAxis: {
  159. type: 'value',
  160. splitLine: { show: false },
  161. axisLine: { show: false },
  162. axisTick: { show: false },
  163. axisLabel: { show: false }
  164. },
  165. yAxis: {
  166. type: 'category',
  167. data: hazardSummary.map(item => item.label),
  168. axisLine: { show: false },
  169. axisTick: { show: false },
  170. axisLabel: { color: '#d2efff', fontSize: 13 }
  171. },
  172. tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
  173. series: [{
  174. type: 'bar',
  175. data: hazardSummary.map(item => ({ value: item.value, itemStyle: { color: item.color, borderRadius: 10 } })),
  176. barWidth: 14,
  177. label: { show: true, position: 'right', color: '#fff', fontWeight: 'bold' },
  178. showBackground: true,
  179. backgroundStyle: { color: 'rgba(255,255,255,0.08)', borderRadius: 10 }
  180. }]
  181. })
  182. }
  183. function waitForBMapGL() {
  184. return new Promise((resolve, reject) => {
  185. if (window.BMapGL) {
  186. resolve(window.BMapGL)
  187. return
  188. }
  189. let attempts = 0
  190. const timer = setInterval(() => {
  191. attempts += 1
  192. if (window.BMapGL) {
  193. clearInterval(timer)
  194. resolve(window.BMapGL)
  195. return
  196. }
  197. if (attempts >= 100) {
  198. clearInterval(timer)
  199. reject(new Error('百度地图加载超时'))
  200. }
  201. }, 200)
  202. })
  203. }
  204. function createFlowLabel(BMapGL, text) {
  205. const label = new BMapGL.Label(text, { offset: new BMapGL.Size(-46, -18) })
  206. label.setStyle({
  207. color: '#fff36a',
  208. background: 'rgba(255, 230, 0, 0.22)',
  209. border: '1px solid rgba(255, 230, 0, 0.55)',
  210. borderRadius: '8px',
  211. padding: '5px 8px',
  212. fontSize: '12px',
  213. boxShadow: '0 0 12px rgba(255, 230, 0, 0.22)'
  214. })
  215. return label
  216. }
  217. function addPipeLine(BMapGL, pipe) {
  218. const bmapPoints = pipe.points.map(([lng, lat]) => new BMapGL.Point(lng, lat))
  219. const polyline = new BMapGL.Polyline(bmapPoints, {
  220. strokeColor: pipe.color,
  221. strokeWeight: 4,
  222. strokeOpacity: 0.95
  223. })
  224. mapInstance.addOverlay(polyline)
  225. bmapPoints.forEach((point, index) => {
  226. const ring = new BMapGL.Circle(point, 14, {
  227. strokeColor: '#39f5ff',
  228. strokeWeight: 2,
  229. strokeOpacity: 0.7,
  230. fillColor: 'rgba(57,245,255,0.08)',
  231. fillOpacity: 0.18
  232. })
  233. mapInstance.addOverlay(ring)
  234. if (index < bmapPoints.length - 1 && index % 2 === 0) {
  235. const label = createFlowLabel(BMapGL, pipe.flow)
  236. const current = pipe.points[index]
  237. const next = pipe.points[index + 1]
  238. label.setPosition(new BMapGL.Point((current[0] + next[0]) / 2, (current[1] + next[1]) / 2))
  239. mapInstance.addOverlay(label)
  240. }
  241. })
  242. }
  243. function addPipePoint(BMapGL, point) {
  244. const markerPoint = new BMapGL.Point(point.lng, point.lat)
  245. const marker = new BMapGL.Marker(markerPoint)
  246. mapInstance.addOverlay(marker)
  247. const label = new BMapGL.Label(`${point.name} ${point.value}`, { offset: new BMapGL.Size(-42, -40) })
  248. label.setPosition(markerPoint)
  249. label.setStyle({
  250. color: '#d8faff',
  251. background: 'rgba(18, 68, 112, 0.75)',
  252. border: '1px solid rgba(57, 245, 255, 0.55)',
  253. borderRadius: '8px',
  254. padding: '5px 8px',
  255. fontSize: '12px'
  256. })
  257. mapInstance.addOverlay(label)
  258. }
  259. async function initMap() {
  260. if (!mapWrapRef.value) return
  261. try {
  262. const BMapGL = await waitForBMapGL()
  263. mapInstance = new BMapGL.Map(mapWrapRef.value, { enableMapClick: true })
  264. const center = new BMapGL.Point(110.3965, 28.4638)
  265. mapInstance.centerAndZoom(center, 15)
  266. mapInstance.enableScrollWheelZoom(true)
  267. mapInstance.setMapStyleV2({ styleId: 'a0f4e6e6f9e7a6c5d8e7f7e7f7e7f7e5' })
  268. pipelineLines.forEach(pipe => addPipeLine(BMapGL, pipe))
  269. pipePoints.forEach(point => addPipePoint(BMapGL, point))
  270. } catch (error) {
  271. mapError.value = '百度地图加载失败,请检查地图脚本或网络连接。'
  272. }
  273. }
  274. function handleResize() {
  275. repairChart?.resize()
  276. hazardChart?.resize()
  277. }
  278. onMounted(async () => {
  279. updateClock()
  280. clockTimer = window.setInterval(updateClock, 1000)
  281. await nextTick()
  282. initRepairChart()
  283. initHazardChart()
  284. initMap()
  285. window.addEventListener('resize', handleResize)
  286. })
  287. onBeforeUnmount(() => {
  288. if (clockTimer) window.clearInterval(clockTimer)
  289. window.removeEventListener('resize', handleResize)
  290. repairChart?.dispose()
  291. hazardChart?.dispose()
  292. mapInstance = null
  293. })
  294. </script>
  295. <template>
  296. <div class="gas-home-screen">
  297. <div class="screen-header">
  298. <div class="header-time">{{ nowText }} {{ weekText }}</div>
  299. <div class="header-title-wrap">
  300. <div class="header-title">燃气管网安全运行监测系统</div>
  301. <div class="header-subtitle">Gas Pipeline Operation Dashboard</div>
  302. </div>
  303. <div class="header-tags">
  304. <span class="header-tag">实时监测</span>
  305. <span class="header-tag">管网运行</span>
  306. <span class="header-tag">隐患预警</span>
  307. </div>
  308. </div>
  309. <div class="screen-body">
  310. <div class="side-panel left-panel">
  311. <section class="panel-card base-card">
  312. <div class="panel-title">基础概况</div>
  313. <div class="base-grid">
  314. <div v-for="item in baseCards" :key="item.label" class="base-item">
  315. <div class="base-icon" :style="{ background: item.color }">
  316. <component :is="item.icon" class="base-icon-svg" />
  317. </div>
  318. <div class="base-text">
  319. <div class="base-label">{{ item.label }}</div>
  320. <div class="base-value">{{ item.value }}<span>{{ item.unit }}</span></div>
  321. </div>
  322. </div>
  323. </div>
  324. </section>
  325. <section class="panel-card">
  326. <div class="panel-title">维修趋势</div>
  327. <div ref="repairChartRef" class="chart-box repair-chart"></div>
  328. </section>
  329. <section class="panel-card table-card">
  330. <div class="panel-title">监测点数据</div>
  331. <div class="table-wrap">
  332. <table class="data-table">
  333. <thead>
  334. <tr>
  335. <th>序号</th>
  336. <th>监测点</th>
  337. <th>时间</th>
  338. <th>数值</th>
  339. <th>状态</th>
  340. </tr>
  341. </thead>
  342. <tbody>
  343. <tr v-for="row in pointRows" :key="row.id">
  344. <td>{{ row.id }}</td>
  345. <td>{{ row.name }}</td>
  346. <td>{{ row.time }}</td>
  347. <td>{{ row.value }}</td>
  348. <td :class="['status-cell', row.status === '在线' ? 'ok' : row.status === '预警' ? 'warn' : 'off']">{{ row.status }}</td>
  349. </tr>
  350. </tbody>
  351. </table>
  352. </div>
  353. </section>
  354. </div>
  355. <div class="center-panel">
  356. <div class="map-stage">
  357. <div class="map-stage-top">
  358. <div class="summary-strip">
  359. <div v-for="item in operationSummary" :key="item.label" class="summary-item">
  360. <span class="summary-label">{{ item.label }}</span>
  361. <strong class="summary-value">{{ item.value }}</strong>
  362. </div>
  363. </div>
  364. <div class="map-kpis">
  365. <div v-for="item in mapKpis" :key="item.label" class="map-kpi">
  366. <span>{{ item.label }}</span>
  367. <strong>{{ item.value }}</strong>
  368. </div>
  369. </div>
  370. </div>
  371. <div ref="mapWrapRef" class="map-view"></div>
  372. <div v-if="mapError" class="map-error">{{ mapError }}</div>
  373. <div class="map-panel info-panel">
  374. <div class="mini-title">当前管网信息</div>
  375. <div class="mini-row"><span>主干线</span><strong>2条</strong></div>
  376. <div class="mini-row"><span>支线</span><strong>4条</strong></div>
  377. <div class="mini-row"><span>重点巡检</span><strong>3段</strong></div>
  378. </div>
  379. <div class="map-panel point-panel">
  380. <div class="mini-title">当前管点信息</div>
  381. <div class="mini-row"><span>在线采集</span><strong>16个</strong></div>
  382. <div class="mini-row"><span>预警点位</span><strong>3个</strong></div>
  383. <div class="mini-row"><span>离线点位</span><strong>2个</strong></div>
  384. </div>
  385. </div>
  386. </div>
  387. <div class="side-panel right-panel">
  388. <section class="panel-card">
  389. <div class="panel-title">设备运行</div>
  390. <div class="table-wrap equipment-wrap">
  391. <table class="data-table compact-table">
  392. <thead>
  393. <tr>
  394. <th>序号</th>
  395. <th>设备名称</th>
  396. <th>设备编号</th>
  397. <th>位置</th>
  398. <th>状态</th>
  399. </tr>
  400. </thead>
  401. <tbody>
  402. <tr v-for="row in equipmentRows" :key="row.id">
  403. <td>{{ row.id }}</td>
  404. <td>{{ row.name }}</td>
  405. <td>{{ row.code }}</td>
  406. <td>{{ row.location }}</td>
  407. <td>{{ row.status }}</td>
  408. </tr>
  409. </tbody>
  410. </table>
  411. </div>
  412. </section>
  413. <section class="panel-card warning-card">
  414. <div class="panel-title">告警动态</div>
  415. <div class="warning-scroll">
  416. <div class="warning-list">
  417. <div v-for="(item, index) in warningRows" :key="index" class="warning-item" :class="item.level === '高' ? 'danger' : 'notice'">
  418. <div class="warning-side">
  419. <div class="warning-badge">{{ item.level }}</div>
  420. <div class="warning-tag">{{ item.tag }}</div>
  421. </div>
  422. <div class="warning-main">
  423. <div class="warning-title-row">
  424. <span class="warning-title">{{ item.title }}</span>
  425. <span class="warning-time">{{ item.time }}</span>
  426. </div>
  427. <div class="warning-code">{{ item.code }}</div>
  428. <div class="warning-desc">{{ item.desc }}</div>
  429. </div>
  430. </div>
  431. </div>
  432. </div>
  433. </section>
  434. <section class="panel-card hazard-card">
  435. <div class="panel-title">隐患统计</div>
  436. <div class="hazard-summary three-col">
  437. <div v-for="item in hazardSummary" :key="item.label" class="hazard-summary-item">
  438. <span class="hazard-dot" :style="{ background: item.color }"></span>
  439. <span>{{ item.label }}</span>
  440. <strong :style="{ color: item.color }">{{ item.value }}项</strong>
  441. </div>
  442. </div>
  443. <div ref="hazardChartRef" class="chart-box hazard-chart"></div>
  444. </section>
  445. </div>
  446. </div>
  447. </div>
  448. </template>
  449. <style scoped lang="scss">
  450. .gas-home-screen {
  451. min-height: 100vh;
  452. color: #fff;
  453. background:
  454. linear-gradient(180deg, rgba(14, 42, 70, 0.96), rgba(8, 20, 37, 1)),
  455. radial-gradient(circle at center, rgba(36, 122, 184, 0.18), transparent 58%),
  456. #081425;
  457. }
  458. .screen-header {
  459. height: 72px;
  460. padding: 0 18px;
  461. display: grid;
  462. grid-template-columns: 320px 1fr 320px;
  463. align-items: center;
  464. border-bottom: 1px solid rgba(93, 194, 255, 0.28);
  465. background: linear-gradient(90deg, rgba(27, 98, 155, 0.95), rgba(18, 53, 98, 0.95));
  466. }
  467. .header-time {
  468. font-size: 18px;
  469. font-weight: 700;
  470. letter-spacing: 1px;
  471. color: #ddf6ff;
  472. }
  473. .header-title-wrap {
  474. text-align: center;
  475. }
  476. .header-title {
  477. font-size: 30px;
  478. font-weight: 800;
  479. letter-spacing: 2px;
  480. }
  481. .header-subtitle {
  482. margin-top: 4px;
  483. color: rgba(196, 234, 255, 0.78);
  484. font-size: 12px;
  485. letter-spacing: 1px;
  486. }
  487. .header-tags {
  488. display: flex;
  489. justify-content: flex-end;
  490. gap: 8px;
  491. }
  492. .header-tag {
  493. padding: 7px 12px;
  494. border: 1px solid rgba(117, 206, 255, 0.4);
  495. border-radius: 16px;
  496. color: #d8f5ff;
  497. background: rgba(13, 77, 128, 0.45);
  498. font-size: 12px;
  499. }
  500. .screen-body {
  501. height: calc(100vh - 72px);
  502. padding: 12px;
  503. display: grid;
  504. grid-template-columns: 360px 1fr 360px;
  505. gap: 12px;
  506. }
  507. .side-panel,
  508. .center-panel {
  509. min-height: 0;
  510. }
  511. .left-panel,
  512. .right-panel {
  513. display: grid;
  514. gap: 12px;
  515. }
  516. .left-panel {
  517. grid-template-rows: 220px 220px 1fr;
  518. }
  519. .right-panel {
  520. grid-template-rows: 250px 260px 1fr;
  521. }
  522. .panel-card {
  523. min-height: 0;
  524. padding: 12px;
  525. background: linear-gradient(180deg, rgba(13, 33, 56, 0.88), rgba(10, 25, 42, 0.88));
  526. border: 1px solid rgba(65, 158, 219, 0.28);
  527. border-radius: 10px;
  528. box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.03);
  529. }
  530. .panel-title {
  531. position: relative;
  532. padding-left: 14px;
  533. margin-bottom: 12px;
  534. color: #8fe5ff;
  535. font-size: 18px;
  536. font-weight: 800;
  537. }
  538. .panel-title::before {
  539. content: '';
  540. position: absolute;
  541. left: 0;
  542. top: 4px;
  543. width: 4px;
  544. height: 18px;
  545. border-radius: 2px;
  546. background: linear-gradient(180deg, #28d7ff, #1776ff);
  547. }
  548. .base-grid {
  549. display: grid;
  550. grid-template-columns: repeat(2, minmax(0, 1fr));
  551. gap: 14px;
  552. }
  553. .base-card {
  554. background: linear-gradient(180deg, rgba(10, 30, 53, 0.95), rgba(9, 24, 42, 0.95));
  555. }
  556. .base-grid {
  557. grid-template-columns: repeat(2, minmax(0, 1fr));
  558. gap: 12px;
  559. }
  560. .base-item {
  561. display: flex;
  562. align-items: center;
  563. gap: 12px;
  564. min-height: 72px;
  565. padding: 12px;
  566. border-radius: 14px;
  567. background: linear-gradient(180deg, rgba(20, 57, 89, 0.55), rgba(18, 44, 72, 0.45));
  568. border: 1px solid rgba(97, 190, 255, 0.16);
  569. }
  570. .base-icon {
  571. width: 48px;
  572. height: 48px;
  573. border-radius: 16px;
  574. display: flex;
  575. align-items: center;
  576. justify-content: center;
  577. flex: 0 0 48px;
  578. }
  579. .base-icon-svg {
  580. width: 26px;
  581. height: 26px;
  582. color: #fff;
  583. }
  584. .base-text {
  585. min-width: 0;
  586. }
  587. .base-label {
  588. color: #d6efff;
  589. font-size: 13px;
  590. letter-spacing: 0.5px;
  591. }
  592. .base-value {
  593. margin-top: 4px;
  594. color: #fff;
  595. font-size: 30px;
  596. font-weight: 800;
  597. line-height: 1;
  598. }
  599. .base-value span {
  600. margin-left: 4px;
  601. color: #90d9ff;
  602. font-size: 12px;
  603. font-weight: 500;
  604. }
  605. .chart-box {
  606. width: 100%;
  607. height: calc(100% - 36px);
  608. }
  609. .table-card,
  610. .warning-card,
  611. .hazard-card {
  612. overflow: hidden;
  613. }
  614. .table-wrap {
  615. height: calc(100% - 38px);
  616. overflow: auto;
  617. }
  618. .data-table {
  619. width: 100%;
  620. border-collapse: collapse;
  621. table-layout: fixed;
  622. }
  623. .data-table thead {
  624. background: rgba(24, 122, 198, 0.55);
  625. }
  626. .data-table th,
  627. .data-table td {
  628. padding: 10px 6px;
  629. color: #f4fbff;
  630. text-align: center;
  631. font-size: 13px;
  632. border-bottom: 1px solid rgba(130, 194, 231, 0.12);
  633. white-space: nowrap;
  634. overflow: hidden;
  635. text-overflow: ellipsis;
  636. }
  637. .data-table tbody tr:nth-child(odd) {
  638. background: rgba(133, 196, 242, 0.12);
  639. }
  640. .data-table tbody tr:nth-child(even) {
  641. background: rgba(133, 196, 242, 0.06);
  642. }
  643. .compact-table th,
  644. .compact-table td {
  645. padding: 9px 4px;
  646. }
  647. .status-cell.ok {
  648. color: #6effc4;
  649. }
  650. .status-cell.warn {
  651. color: #ffe16a;
  652. }
  653. .status-cell.off {
  654. color: #ff8e8e;
  655. }
  656. .center-panel {
  657. position: relative;
  658. }
  659. .map-stage {
  660. position: relative;
  661. height: 100%;
  662. overflow: hidden;
  663. border-radius: 12px;
  664. border: 1px solid rgba(56, 159, 223, 0.2);
  665. background: rgba(8, 20, 37, 0.64);
  666. }
  667. .map-stage-top {
  668. position: absolute;
  669. top: 14px;
  670. left: 14px;
  671. right: 14px;
  672. z-index: 3;
  673. display: flex;
  674. justify-content: space-between;
  675. gap: 12px;
  676. pointer-events: none;
  677. }
  678. .summary-strip,
  679. .map-kpis {
  680. display: flex;
  681. gap: 10px;
  682. }
  683. .summary-item,
  684. .map-kpi {
  685. min-width: 110px;
  686. padding: 10px 12px;
  687. border-radius: 10px;
  688. background: rgba(8, 31, 54, 0.78);
  689. border: 1px solid rgba(79, 177, 238, 0.22);
  690. }
  691. .summary-label,
  692. .map-kpi span {
  693. display: block;
  694. color: #91d7ff;
  695. font-size: 12px;
  696. }
  697. .summary-value,
  698. .map-kpi strong {
  699. display: block;
  700. margin-top: 6px;
  701. color: #fff;
  702. font-size: 20px;
  703. }
  704. .map-view {
  705. width: 100%;
  706. height: 100%;
  707. }
  708. .map-error {
  709. position: absolute;
  710. inset: 0;
  711. display: flex;
  712. align-items: center;
  713. justify-content: center;
  714. color: #ffd3d3;
  715. background: rgba(5, 10, 20, 0.72);
  716. z-index: 4;
  717. }
  718. .map-panel {
  719. position: absolute;
  720. z-index: 3;
  721. width: 190px;
  722. padding: 12px;
  723. border-radius: 10px;
  724. background: rgba(8, 31, 54, 0.78);
  725. border: 1px solid rgba(79, 177, 238, 0.22);
  726. }
  727. .info-panel {
  728. left: 18px;
  729. bottom: 20px;
  730. }
  731. .point-panel {
  732. right: 18px;
  733. bottom: 20px;
  734. }
  735. .mini-title {
  736. margin-bottom: 10px;
  737. color: #8fe5ff;
  738. font-size: 14px;
  739. font-weight: 700;
  740. }
  741. .mini-row {
  742. display: flex;
  743. justify-content: space-between;
  744. margin-top: 8px;
  745. color: #d6efff;
  746. font-size: 13px;
  747. }
  748. .warning-scroll {
  749. height: calc(100% - 38px);
  750. overflow-y: auto;
  751. padding-right: 4px;
  752. }
  753. .warning-scroll::-webkit-scrollbar {
  754. width: 6px;
  755. }
  756. .warning-scroll::-webkit-scrollbar-thumb {
  757. border-radius: 999px;
  758. background: rgba(96, 180, 235, 0.38);
  759. }
  760. .warning-scroll::-webkit-scrollbar-track {
  761. background: transparent;
  762. }
  763. .warning-list {
  764. display: grid;
  765. gap: 12px;
  766. }
  767. .warning-item {
  768. display: flex;
  769. gap: 12px;
  770. align-items: stretch;
  771. padding: 12px;
  772. border-radius: 14px;
  773. border: 1px solid rgba(255, 108, 108, 0.1);
  774. background: linear-gradient(180deg, rgba(72, 24, 38, 0.48), rgba(49, 19, 31, 0.36));
  775. }
  776. .warning-item.notice {
  777. border-color: rgba(255, 201, 92, 0.1);
  778. background: linear-gradient(180deg, rgba(78, 63, 23, 0.34), rgba(48, 43, 20, 0.24));
  779. }
  780. .warning-side {
  781. width: 56px;
  782. flex: 0 0 56px;
  783. display: flex;
  784. flex-direction: column;
  785. gap: 8px;
  786. align-items: center;
  787. }
  788. .warning-badge {
  789. width: 32px;
  790. height: 32px;
  791. border-radius: 10px;
  792. display: flex;
  793. align-items: center;
  794. justify-content: center;
  795. font-size: 13px;
  796. font-weight: 800;
  797. color: #fff;
  798. }
  799. .warning-tag {
  800. width: 100%;
  801. padding: 3px 0;
  802. border-radius: 999px;
  803. text-align: center;
  804. font-size: 11px;
  805. color: #dcefff;
  806. background: rgba(255, 255, 255, 0.08);
  807. }
  808. .warning-item.danger .warning-badge {
  809. background: rgba(255, 58, 58, 0.24);
  810. color: #ff8787;
  811. }
  812. .warning-item.danger .warning-title {
  813. color: #ffd8d8;
  814. }
  815. .warning-item.notice .warning-badge {
  816. background: rgba(255, 198, 53, 0.18);
  817. color: #ffd45d;
  818. }
  819. .warning-item.notice .warning-title {
  820. color: #fff2c4;
  821. }
  822. .warning-main {
  823. min-width: 0;
  824. flex: 1;
  825. }
  826. .warning-title-row {
  827. display: flex;
  828. align-items: flex-start;
  829. justify-content: space-between;
  830. gap: 8px;
  831. }
  832. .warning-title {
  833. font-weight: 700;
  834. font-size: 16px;
  835. line-height: 1.2;
  836. }
  837. .warning-time,
  838. .warning-code,
  839. .warning-desc {
  840. color: #dcefff;
  841. font-size: 12px;
  842. }
  843. .warning-time {
  844. color: #b6dfff;
  845. white-space: nowrap;
  846. }
  847. .warning-code {
  848. margin: 6px 0 4px;
  849. color: #9ad8ff;
  850. }
  851. .warning-desc {
  852. line-height: 1.5;
  853. }
  854. .hazard-summary {
  855. display: flex;
  856. gap: 12px;
  857. margin-bottom: 8px;
  858. }
  859. .hazard-summary.three-col {
  860. justify-content: space-between;
  861. }
  862. .hazard-summary-item {
  863. display: flex;
  864. align-items: center;
  865. gap: 8px;
  866. font-size: 13px;
  867. }
  868. .hazard-dot {
  869. width: 8px;
  870. height: 24px;
  871. display: inline-block;
  872. border-radius: 4px;
  873. }
  874. .hazard-chart {
  875. height: 160px;
  876. }
  877. .equipment-wrap {
  878. height: calc(100% - 38px);
  879. }
  880. :deep(.BMap_cpyCtrl),
  881. :deep(.anchorBL) {
  882. display: none !important;
  883. }
  884. @media (max-width: 1500px) {
  885. .screen-body {
  886. grid-template-columns: 320px 1fr 320px;
  887. }
  888. .header-title {
  889. font-size: 24px;
  890. }
  891. .summary-item,
  892. .map-kpi {
  893. min-width: 96px;
  894. }
  895. }
  896. </style>