dqbj.vue 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816
  1. <template>
  2. <div class="gis-page">
  3. <!-- 左侧浮动面板 -->
  4. <div class="side-panel">
  5. <div class="panel-header">
  6. <span class="panel-title">当前报警分布</span>
  7. </div>
  8. <!-- 等级筛选 -->
  9. <div class="panel-section">
  10. <div class="section-label">等级筛选</div>
  11. <div class="layer-list">
  12. <div class="layer-item">
  13. <el-checkbox v-model="levelChecked.severe" @change="onLevelChange">
  14. <span class="level-dot" style="background:#f56c6c"></span> 严重
  15. </el-checkbox>
  16. </div>
  17. <div class="layer-item">
  18. <el-checkbox v-model="levelChecked.important" @change="onLevelChange">
  19. <span class="level-dot" style="background:#e6a23c"></span> 重要
  20. </el-checkbox>
  21. </div>
  22. <div class="layer-item">
  23. <el-checkbox v-model="levelChecked.normal" @change="onLevelChange">
  24. <span class="level-dot" style="background:#409eff"></span> 一般
  25. </el-checkbox>
  26. </div>
  27. </div>
  28. </div>
  29. <!-- 统计数据 -->
  30. <div class="panel-section">
  31. <div class="section-label">报警统计</div>
  32. <div class="stats-row">
  33. <div class="stat-card">
  34. <div class="stat-value">{{ alarmList.length }}</div>
  35. <div class="stat-text">总数</div>
  36. </div>
  37. <div class="stat-card stat-severe">
  38. <div class="stat-value">{{ severeCount }}</div>
  39. <div class="stat-text">严重</div>
  40. </div>
  41. <div class="stat-card stat-important">
  42. <div class="stat-value">{{ importantCount }}</div>
  43. <div class="stat-text">重要</div>
  44. </div>
  45. <div class="stat-card stat-normal">
  46. <div class="stat-value">{{ normalCount }}</div>
  47. <div class="stat-text">一般</div>
  48. </div>
  49. </div>
  50. </div>
  51. <!-- 搜索框 -->
  52. <div class="panel-section">
  53. <div class="section-label">快速定位</div>
  54. <el-input
  55. v-model="searchKey"
  56. placeholder="输入设备名称搜索"
  57. clearable
  58. prefix-icon="Search"
  59. @input="onSearch"
  60. />
  61. <div class="search-results" v-if="searchResults.length > 0">
  62. <div
  63. class="search-item"
  64. v-for="item in searchResults"
  65. :key="item.id"
  66. @click="locatePoint(item)"
  67. >
  68. <span class="level-dot" :style="{ background: getLevelColor(item.level) }"></span>
  69. <span class="search-name">{{ item.name }}</span>
  70. <span class="search-level" :class="getLevelClass(item.level)">{{ item.level }}</span>
  71. </div>
  72. </div>
  73. </div>
  74. </div>
  75. <!-- 地图容器 -->
  76. <div id="dqbjGISMap" class="map-container"></div>
  77. <!-- 详情弹窗 -->
  78. <el-dialog
  79. v-model="detailVisible"
  80. :title="currentItem.name"
  81. width="620px"
  82. :close-on-click-modal="false"
  83. class="detail-dialog"
  84. @opened="onDialogOpened"
  85. @closed="onDialogClosed"
  86. >
  87. <div class="detail-content">
  88. <!-- 基本信息 -->
  89. <div class="detail-section">
  90. <div class="section-title">基本信息</div>
  91. <div class="detail-header">
  92. <el-tag :type="getTagType(currentItem.level)" effect="dark" size="small">
  93. {{ currentItem.level }}
  94. </el-tag>
  95. <span class="detail-name">{{ currentItem.name }}</span>
  96. </div>
  97. <div class="detail-info">
  98. <div class="info-row">
  99. <span class="info-label">设备编号</span>
  100. <span class="info-value">{{ currentItem.deviceNo }}</span>
  101. </div>
  102. <div class="info-row">
  103. <span class="info-label">报警时间</span>
  104. <span class="info-value">{{ currentItem.time }}</span>
  105. </div>
  106. <div class="info-row">
  107. <span class="info-label">报警类型</span>
  108. <span class="info-value">{{ currentItem.type }}</span>
  109. </div>
  110. <div class="info-row">
  111. <span class="info-label">当前值</span>
  112. <span class="info-value highlight">{{ currentItem.value }}</span>
  113. </div>
  114. <div class="info-row">
  115. <span class="info-label">阈值</span>
  116. <span class="info-value">{{ currentItem.threshold }}</span>
  117. </div>
  118. <div class="info-row">
  119. <span class="info-label">位置</span>
  120. <span class="info-value">{{ currentItem.location }}</span>
  121. </div>
  122. </div>
  123. </div>
  124. <!-- 监测曲线 -->
  125. <div class="detail-section">
  126. <div class="section-title">监测曲线(近24小时)</div>
  127. <div ref="chartRef" class="monitor-chart"></div>
  128. </div>
  129. <!-- 周边环境 -->
  130. <div class="detail-section">
  131. <div class="section-title">周边环境</div>
  132. <div class="env-desc">{{ currentItem.envDesc }}</div>
  133. </div>
  134. <!-- 历史记录 -->
  135. <div class="detail-section">
  136. <div class="section-title">历史记录</div>
  137. <el-timeline>
  138. <el-timeline-item
  139. v-for="(record, index) in currentItem.historyRecords"
  140. :key="index"
  141. :timestamp="record.time"
  142. placement="top"
  143. :type="getTagType(record.level)"
  144. >
  145. <div class="timeline-content">
  146. <span class="timeline-name">{{ record.name }}</span>
  147. <span class="timeline-type">{{ record.type }}</span>
  148. <span class="timeline-value">{{ record.value }}</span>
  149. </div>
  150. </el-timeline-item>
  151. </el-timeline>
  152. </div>
  153. </div>
  154. </el-dialog>
  155. </div>
  156. </template>
  157. <script setup name="Dqbj">
  158. import { ref, reactive, computed, onMounted, nextTick } from 'vue'
  159. import { Search } from '@element-plus/icons-vue'
  160. import { ElMessage } from 'element-plus'
  161. import * as echarts from 'echarts'
  162. import locationIcon from '@/assets/images/location.png'
  163. import { getAlarmDataPage } from '@/api/drainage'
  164. import useUserStore from '@/store/modules/user'
  165. // ==================== 用户信息 ====================
  166. const userStore = useUserStore()
  167. const isAdmin = computed(() => userStore.roles && userStore.roles.includes('admin'))
  168. // ==================== 等级筛选 ====================
  169. const levelChecked = reactive({ severe: true, important: true, normal: true })
  170. // ==================== 等级映射 ====================
  171. const levelMap = { 1: '严重', 2: '重要', 3: '一般' }
  172. // ==================== 报警数据(从API加载)====================
  173. const alarmList = ref([])
  174. async function loadData() {
  175. try {
  176. const res = await getAlarmDataPage(1, 200, { equipmentType: 'drainage' })
  177. const pageData = res.code !== undefined ? res.data : res
  178. const records = pageData.records || pageData.rows || []
  179. alarmList.value = records
  180. .map(row => ({
  181. id: row.id,
  182. name: row.deviceName || '-',
  183. level: row.alarmLevel === 1 ? '严重' : row.alarmLevel === 2 ? '重要' : '一般',
  184. time: row.alarmTime || '-',
  185. type: row.warningType || '-',
  186. value: row.actualValue || '-',
  187. threshold: row.maxValue || '-',
  188. location: '-',
  189. deviceNo: row.deviceCode,
  190. envDesc: '-',
  191. lng: parseFloat(row.longitude),
  192. lat: parseFloat(row.latitude),
  193. historyRecords: []
  194. }))
  195. .filter(w => w.lng && w.lat && !isNaN(w.lng) && !isNaN(w.lat))
  196. } catch (error) {
  197. console.error('加载当前报警数据失败', error)
  198. ElMessage.error('加载当前报警数据失败')
  199. }
  200. }
  201. // ==================== 统计计算 ====================
  202. const severeCount = computed(() => alarmList.value.filter(v => v.level === '严重').length)
  203. const importantCount = computed(() => alarmList.value.filter(v => v.level === '重要').length)
  204. const normalCount = computed(() => alarmList.value.filter(v => v.level === '一般').length)
  205. // ==================== 工具方法 ====================
  206. function getLevelColor(level) {
  207. const map = { '严重': '#f56c6c', '重要': '#e6a23c', '一般': '#409eff' }
  208. return map[level] || '#409eff'
  209. }
  210. function getLevelClass(level) {
  211. const map = { '严重': 'level-severe', '重要': 'level-important', '一般': 'level-normal' }
  212. return map[level] || ''
  213. }
  214. function getTagType(level) {
  215. const map = { '严重': 'danger', '重要': 'warning', '一般': '' }
  216. return map[level] || ''
  217. }
  218. function getLevelKey(level) {
  219. const map = { '严重': 'severe', '重要': 'important', '一般': 'normal' }
  220. return map[level] || 'normal'
  221. }
  222. // ==================== 搜索功能 ====================
  223. const searchKey = ref('')
  224. const searchResults = ref([])
  225. function onSearch() {
  226. if (!searchKey.value.trim()) {
  227. searchResults.value = []
  228. return
  229. }
  230. const key = searchKey.value.trim().toLowerCase()
  231. searchResults.value = alarmList.value.filter(v =>
  232. v.name.toLowerCase().includes(key) || v.location.toLowerCase().includes(key)
  233. )
  234. }
  235. function locatePoint(item) {
  236. if (mapInstance) {
  237. mapInstance.centerAndZoom(new BMapGL.Point(item.lng, item.lat), 17)
  238. openDetail(item)
  239. }
  240. searchResults.value = []
  241. searchKey.value = ''
  242. }
  243. // ==================== 地图相关 ====================
  244. let mapInstance = null
  245. const markerMap = new Map()
  246. function initMap() {
  247. const container = document.getElementById('dqbjGISMap')
  248. if (!container || typeof BMapGL === 'undefined') {
  249. console.warn('BMapGL 未加载或容器不存在')
  250. return
  251. }
  252. try {
  253. mapInstance = new BMapGL.Map('dqbjGISMap')
  254. const centerPoint = new BMapGL.Point(110.393, 28.452)
  255. mapInstance.centerAndZoom(centerPoint, 15)
  256. mapInstance.enableScrollWheelZoom(true)
  257. alarmList.value.forEach(point => {
  258. addMapMarker(point)
  259. })
  260. // 默认定位到第一个报警点位
  261. if (alarmList.value.length > 0) {
  262. const firstPoint = alarmList.value[0]
  263. mapInstance.centerAndZoom(new BMapGL.Point(firstPoint.lng, firstPoint.lat), 15)
  264. }
  265. } catch (e) {
  266. console.error('百度地图初始化失败:', e)
  267. }
  268. }
  269. function addMapMarker(point) {
  270. const color = getLevelColor(point.level)
  271. const bPoint = new BMapGL.Point(point.lng, point.lat)
  272. const html = `<div style="text-align:center;cursor:pointer;">
  273. <div style="color:#fff;background:${color};border-radius:4px;padding:2px 8px;font-size:11px;white-space:nowrap;display:inline-block;margin-bottom:2px;box-shadow:0 1px 4px rgba(0,0,0,0.3);">
  274. ${point.name}
  275. </div>
  276. <div><img src="${locationIcon}" style="width:28px;height:28px;display:block;margin:0 auto;"/></div>
  277. </div>`
  278. const label = new BMapGL.Label(html, {
  279. position: bPoint,
  280. offset: new BMapGL.Size(-30, -50)
  281. })
  282. label.setStyle({
  283. border: 'none',
  284. background: 'transparent',
  285. padding: '0',
  286. zIndex: '10'
  287. })
  288. mapInstance.addOverlay(label)
  289. markerMap.set(point.id, label)
  290. label.addEventListener('click', function () {
  291. openDetail(point)
  292. })
  293. }
  294. // ==================== 等级筛选 ====================
  295. function onLevelChange() {
  296. alarmList.value.forEach(point => {
  297. const label = markerMap.get(point.id)
  298. if (!label) return
  299. const key = getLevelKey(point.level)
  300. const visible = levelChecked[key]
  301. label.show()
  302. if (!visible) {
  303. label.hide()
  304. }
  305. })
  306. }
  307. // ==================== 详情弹窗 ====================
  308. const detailVisible = ref(false)
  309. const currentItem = ref({})
  310. const chartRef = ref(null)
  311. let chartInstance = null
  312. function openDetail(point) {
  313. currentItem.value = { ...point }
  314. detailVisible.value = true
  315. }
  316. function onDialogOpened() {
  317. nextTick(() => {
  318. initChart()
  319. })
  320. }
  321. function onDialogClosed() {
  322. if (chartInstance) {
  323. chartInstance.dispose()
  324. chartInstance = null
  325. }
  326. }
  327. // ==================== ECharts 监测曲线 ====================
  328. function initChart() {
  329. if (!chartRef.value) return
  330. chartInstance = echarts.init(chartRef.value)
  331. // 生成近24小时模拟数据
  332. const hours = []
  333. const values = []
  334. const now = new Date()
  335. const thresholdVal = parseFloat(currentItem.value.threshold) || 2.5
  336. const currentVal = parseFloat(currentItem.value) || 0
  337. const isLevel = currentItem.type === '液位超限' || currentItem.type === '降雨预警'
  338. for (let i = 23; i >= 0; i--) {
  339. const h = new Date(now.getTime() - i * 3600000)
  340. hours.push(h.getHours() + ':00')
  341. // 模拟波动数据
  342. const base = isLevel ? thresholdVal * 0.6 : thresholdVal * 0.5
  343. const peak = isLevel ? currentVal : currentVal * 0.9
  344. const noise = (Math.random() - 0.5) * base * 0.3
  345. const val = i < 6 ? peak - (6 - i) * (peak - base) / 8 + noise : base + noise + (i > 16 ? (i - 16) * (peak - base) / 8 : 0)
  346. values.push(Math.max(0, parseFloat(val.toFixed(2))))
  347. }
  348. const unit = isLevel ? 'm' : 'm³/h'
  349. const thresholdLine = Array(24).fill(thresholdVal)
  350. chartInstance.setOption({
  351. grid: { top: 30, right: 20, bottom: 30, left: 50 },
  352. tooltip: {
  353. trigger: 'axis',
  354. formatter: (params) => {
  355. let s = params[0].axisValue + '<br/>'
  356. params.forEach(p => {
  357. s += `<span style="display:inline-block;width:10px;height:10px;border-radius:50%;background:${p.color};margin-right:4px;"></span>${p.seriesName}: ${p.value}${unit}<br/>`
  358. })
  359. return s
  360. }
  361. },
  362. xAxis: {
  363. type: 'category',
  364. data: hours,
  365. axisLabel: { fontSize: 10, color: '#999' },
  366. axisLine: { lineStyle: { color: '#ddd' } }
  367. },
  368. yAxis: {
  369. type: 'value',
  370. axisLabel: { fontSize: 10, color: '#999', formatter: '{value}' + unit },
  371. splitLine: { lineStyle: { color: '#f0f0f0' } }
  372. },
  373. series: [
  374. {
  375. name: isLevel ? '液位' : '流量',
  376. type: 'line',
  377. data: values,
  378. smooth: true,
  379. lineStyle: { color: '#409eff', width: 2 },
  380. areaStyle: {
  381. color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
  382. { offset: 0, color: 'rgba(64,158,255,0.3)' },
  383. { offset: 1, color: 'rgba(64,158,255,0.02)' }
  384. ])
  385. },
  386. itemStyle: { color: '#409eff' },
  387. symbol: 'circle',
  388. symbolSize: 4
  389. },
  390. {
  391. name: '阈值',
  392. type: 'line',
  393. data: thresholdLine,
  394. lineStyle: { color: '#f56c6c', width: 1, type: 'dashed' },
  395. itemStyle: { color: '#f56c6c' },
  396. symbol: 'none'
  397. }
  398. ]
  399. })
  400. }
  401. // ==================== 生命周期 ====================
  402. onMounted(async () => {
  403. await loadData()
  404. nextTick(() => {
  405. initMap()
  406. })
  407. })
  408. </script>
  409. <style scoped>
  410. .gis-page {
  411. position: relative;
  412. width: 100%;
  413. height: 100vh;
  414. overflow: hidden;
  415. }
  416. .map-container {
  417. width: 100%;
  418. height: 100%;
  419. }
  420. /* ==================== 左侧浮动面板 ==================== */
  421. .side-panel {
  422. position: absolute;
  423. top: 16px;
  424. left: 16px;
  425. width: 280px;
  426. background: rgba(6, 30, 65, 0.85);
  427. border-radius: 8px;
  428. z-index: 100;
  429. box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4);
  430. padding: 0;
  431. max-height: calc(100% - 32px);
  432. overflow-y: auto;
  433. }
  434. .side-panel::-webkit-scrollbar {
  435. width: 4px;
  436. }
  437. .side-panel::-webkit-scrollbar-thumb {
  438. background: rgba(255, 255, 255, 0.2);
  439. border-radius: 2px;
  440. }
  441. .panel-header {
  442. padding: 16px 18px 12px;
  443. border-bottom: 1px solid rgba(255, 255, 255, 0.1);
  444. }
  445. .panel-title {
  446. font-size: 16px;
  447. font-weight: 600;
  448. color: #fff;
  449. letter-spacing: 1px;
  450. }
  451. .panel-section {
  452. padding: 14px 18px;
  453. border-bottom: 1px solid rgba(255, 255, 255, 0.06);
  454. }
  455. .panel-section:last-child {
  456. border-bottom: none;
  457. }
  458. .section-label {
  459. font-size: 12px;
  460. color: rgba(255, 255, 255, 0.5);
  461. margin-bottom: 10px;
  462. text-transform: uppercase;
  463. letter-spacing: 1px;
  464. }
  465. /* 等级列表 */
  466. .layer-list {
  467. display: flex;
  468. flex-direction: column;
  469. gap: 6px;
  470. }
  471. .layer-item :deep(.el-checkbox) {
  472. color: rgba(255, 255, 255, 0.85);
  473. height: auto;
  474. }
  475. .layer-item :deep(.el-checkbox__label) {
  476. color: rgba(255, 255, 255, 0.85);
  477. font-size: 13px;
  478. display: inline-flex;
  479. align-items: center;
  480. gap: 4px;
  481. }
  482. .layer-item :deep(.el-checkbox__input.is-checked .el-checkbox__inner) {
  483. background: #409eff;
  484. border-color: #409eff;
  485. }
  486. .level-dot {
  487. display: inline-block;
  488. width: 10px;
  489. height: 10px;
  490. border-radius: 50%;
  491. margin-right: 2px;
  492. vertical-align: middle;
  493. }
  494. /* 统计卡片 */
  495. .stats-row {
  496. display: flex;
  497. gap: 8px;
  498. }
  499. .stat-card {
  500. flex: 1;
  501. background: rgba(255, 255, 255, 0.08);
  502. border-radius: 6px;
  503. padding: 10px 4px;
  504. text-align: center;
  505. }
  506. .stat-card.stat-severe {
  507. background: rgba(245, 108, 108, 0.15);
  508. }
  509. .stat-card.stat-important {
  510. background: rgba(230, 162, 60, 0.15);
  511. }
  512. .stat-card.stat-normal {
  513. background: rgba(64, 158, 255, 0.15);
  514. }
  515. .stat-value {
  516. font-size: 20px;
  517. font-weight: 700;
  518. color: #fff;
  519. line-height: 1.2;
  520. }
  521. .stat-severe .stat-value {
  522. color: #f56c6c;
  523. }
  524. .stat-important .stat-value {
  525. color: #e6a23c;
  526. }
  527. .stat-normal .stat-value {
  528. color: #409eff;
  529. }
  530. .stat-text {
  531. font-size: 11px;
  532. color: rgba(255, 255, 255, 0.5);
  533. margin-top: 4px;
  534. }
  535. /* 搜索 */
  536. .side-panel :deep(.el-input__wrapper) {
  537. background: rgba(255, 255, 255, 0.08);
  538. box-shadow: none;
  539. border: 1px solid rgba(255, 255, 255, 0.12);
  540. }
  541. .side-panel :deep(.el-input__inner) {
  542. color: #fff;
  543. font-size: 13px;
  544. }
  545. .side-panel :deep(.el-input__inner::placeholder) {
  546. color: rgba(255, 255, 255, 0.35);
  547. }
  548. .side-panel :deep(.el-input__prefix .el-icon) {
  549. color: rgba(255, 255, 255, 0.4);
  550. }
  551. .search-results {
  552. margin-top: 8px;
  553. max-height: 200px;
  554. overflow-y: auto;
  555. border-radius: 4px;
  556. }
  557. .search-results::-webkit-scrollbar {
  558. width: 3px;
  559. }
  560. .search-results::-webkit-scrollbar-thumb {
  561. background: rgba(255, 255, 255, 0.15);
  562. border-radius: 2px;
  563. }
  564. .search-item {
  565. display: flex;
  566. align-items: center;
  567. padding: 7px 10px;
  568. cursor: pointer;
  569. border-radius: 4px;
  570. transition: background 0.15s;
  571. }
  572. .search-item:hover {
  573. background: rgba(255, 255, 255, 0.1);
  574. }
  575. .search-name {
  576. flex: 1;
  577. font-size: 12px;
  578. color: rgba(255, 255, 255, 0.85);
  579. margin-left: 6px;
  580. overflow: hidden;
  581. text-overflow: ellipsis;
  582. white-space: nowrap;
  583. }
  584. .search-level {
  585. font-size: 11px;
  586. padding: 1px 6px;
  587. border-radius: 3px;
  588. }
  589. .search-level.level-severe {
  590. color: #f56c6c;
  591. background: rgba(245, 108, 108, 0.15);
  592. }
  593. .search-level.level-important {
  594. color: #e6a23c;
  595. background: rgba(230, 162, 60, 0.15);
  596. }
  597. .search-level.level-normal {
  598. color: #409eff;
  599. background: rgba(64, 158, 255, 0.15);
  600. }
  601. /* ==================== 详情弹窗样式 ==================== */
  602. .detail-dialog :deep(.el-dialog) {
  603. border-radius: 10px;
  604. overflow: hidden;
  605. }
  606. .detail-dialog :deep(.el-dialog__header) {
  607. background: #1a1a2e;
  608. padding: 14px 20px;
  609. margin: 0;
  610. }
  611. .detail-dialog :deep(.el-dialog__title) {
  612. color: #fff;
  613. font-size: 15px;
  614. }
  615. .detail-dialog :deep(.el-dialog__headerbtn .el-dialog__close) {
  616. color: rgba(255, 255, 255, 0.6);
  617. }
  618. .detail-dialog :deep(.el-dialog__body) {
  619. padding: 0;
  620. max-height: 70vh;
  621. overflow-y: auto;
  622. }
  623. .detail-content {
  624. padding: 20px;
  625. }
  626. .detail-section {
  627. margin-bottom: 20px;
  628. }
  629. .detail-section:last-child {
  630. margin-bottom: 0;
  631. }
  632. .section-title {
  633. font-size: 14px;
  634. font-weight: 600;
  635. color: #303133;
  636. margin-bottom: 12px;
  637. padding-left: 10px;
  638. border-left: 3px solid #409eff;
  639. }
  640. .detail-header {
  641. display: flex;
  642. align-items: center;
  643. gap: 10px;
  644. margin-bottom: 12px;
  645. }
  646. .detail-name {
  647. font-size: 15px;
  648. font-weight: 600;
  649. color: #303133;
  650. }
  651. .detail-info {
  652. display: flex;
  653. flex-direction: column;
  654. }
  655. .info-row {
  656. display: flex;
  657. align-items: center;
  658. padding: 8px 0;
  659. border-bottom: 1px solid #f5f5f5;
  660. }
  661. .info-row:last-child {
  662. border-bottom: none;
  663. }
  664. .info-label {
  665. width: 80px;
  666. flex-shrink: 0;
  667. font-size: 13px;
  668. color: #909399;
  669. }
  670. .info-value {
  671. font-size: 13px;
  672. color: #303133;
  673. }
  674. .info-value.highlight {
  675. color: #f56c6c;
  676. font-weight: 600;
  677. }
  678. /* 监测曲线 */
  679. .monitor-chart {
  680. width: 100%;
  681. height: 240px;
  682. }
  683. /* 周边环境 */
  684. .env-desc {
  685. font-size: 13px;
  686. color: #606266;
  687. line-height: 1.8;
  688. background: #f8f9fa;
  689. padding: 12px 16px;
  690. border-radius: 6px;
  691. }
  692. /* 历史记录时间线 */
  693. .timeline-content {
  694. display: flex;
  695. align-items: center;
  696. gap: 12px;
  697. }
  698. .timeline-name {
  699. font-size: 13px;
  700. color: #303133;
  701. font-weight: 500;
  702. }
  703. .timeline-type {
  704. font-size: 12px;
  705. color: #909399;
  706. }
  707. .timeline-value {
  708. font-size: 12px;
  709. color: #f56c6c;
  710. font-weight: 500;
  711. }
  712. </style>