WarningGisDetail.vue 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653
  1. <template>
  2. <el-drawer v-model="visible" :with-header="false" :close-on-click-modal="false" append-to-body size="780px"
  3. class="wgd-drawer" direction="rtl">
  4. <div class="wgd" v-if="record" v-loading="loading">
  5. <!-- ============ 顶部标题区 ============ -->
  6. <div class="wgd-hero" :class="`hero-${levelKey}`">
  7. <div class="hero-left">
  8. <div class="hero-badge">{{ levelText }}</div>
  9. <div class="hero-main">
  10. <div class="hero-name" :title="record.equipmentName">{{ record.equipmentName || '未命名预警' }}</div>
  11. <div class="hero-sub">
  12. <span class="hero-no">{{ record.warningNo || '—' }}</span>
  13. <span class="hero-dot">·</span>
  14. <span>{{ warningTypeText }}</span>
  15. </div>
  16. </div>
  17. </div>
  18. <div class="hero-right">
  19. <span class="hero-status" :class="`st-${statusKey}`">{{ statusTextValue }}</span>
  20. <el-button class="hero-close" text circle @click="close">
  21. <el-icon><Close /></el-icon>
  22. </el-button>
  23. </div>
  24. </div>
  25. <!-- ============ 关键指标条 ============ -->
  26. <div class="wgd-kpi">
  27. <div class="kpi-item">
  28. <div class="kpi-label">实时液位</div>
  29. <div class="kpi-value" :class="{ 'kpi-alarm': alarm }">
  30. <template v-if="metric.level !== null">{{ metric.level }}<i>m</i></template>
  31. <span v-else class="kpi-empty">—</span>
  32. </div>
  33. </div>
  34. <div class="kpi-item">
  35. <div class="kpi-label">报警阈值</div>
  36. <div class="kpi-value kpi-text">{{ metric.thresholdText || '未配置' }}</div>
  37. </div>
  38. <div class="kpi-item">
  39. <div class="kpi-label">采集时间</div>
  40. <div class="kpi-value kpi-text">{{ metric.collectTime || '—' }}</div>
  41. </div>
  42. <div class="kpi-item">
  43. <div class="kpi-label">{{ isHistory ? '归档时间' : '预警时间' }}</div>
  44. <div class="kpi-value kpi-text">
  45. {{ formatDateTime(isHistory ? archiveTime : record.createTime) }}
  46. </div>
  47. </div>
  48. </div>
  49. <!-- ============ 预警信息 ============ -->
  50. <div class="wgd-section">
  51. <div class="section-head">
  52. <span class="section-bar"></span>
  53. <span class="section-title">预警信息</span>
  54. </div>
  55. <el-descriptions :column="2" border size="small">
  56. <el-descriptions-item label="预警编号">{{ record.warningNo || '-' }}</el-descriptions-item>
  57. <el-descriptions-item label="预警名称">{{ record.equipmentName || '-' }}</el-descriptions-item>
  58. <el-descriptions-item label="预警类型">{{ warningTypeText }}</el-descriptions-item>
  59. <el-descriptions-item label="预警级别">
  60. <el-tag :type="levelTagType" size="small">{{ levelText }}</el-tag>
  61. </el-descriptions-item>
  62. <el-descriptions-item label="预警专项">{{ record.warningSpecial || '-' }}</el-descriptions-item>
  63. <el-descriptions-item label="权属单位">{{ record.ownershipUnit || '-' }}</el-descriptions-item>
  64. <el-descriptions-item label="预警时间">{{ formatDateTime(record.createTime) }}</el-descriptions-item>
  65. <el-descriptions-item :label="isHistory ? '处理状态' : '当前状态'">
  66. <el-tag :type="statusTagType" size="small">{{ statusTextValue }}</el-tag>
  67. </el-descriptions-item>
  68. <el-descriptions-item label="位置" :span="2">{{ record.location || '-' }}</el-descriptions-item>
  69. <el-descriptions-item label="预警内容" :span="2">
  70. <div class="desc-text">{{ record.content || '-' }}</div>
  71. </el-descriptions-item>
  72. <el-descriptions-item v-if="record.remark" label="备注" :span="2">{{ record.remark }}</el-descriptions-item>
  73. </el-descriptions>
  74. </div>
  75. <!-- ============ 归档信息(仅历史预警) ============ -->
  76. <div class="wgd-section" v-if="isHistory">
  77. <div class="section-head">
  78. <span class="section-bar bar-green"></span>
  79. <span class="section-title">归档信息</span>
  80. </div>
  81. <el-descriptions :column="2" border size="small">
  82. <el-descriptions-item label="归档编号">{{ archiveNo }}</el-descriptions-item>
  83. <el-descriptions-item label="归档时间">{{ formatDateTime(archiveTime) }}</el-descriptions-item>
  84. <el-descriptions-item label="发布时间">{{ formatDateTime(record.publishTime) }}</el-descriptions-item>
  85. <el-descriptions-item label="处置人">{{ record.processor || '-' }}</el-descriptions-item>
  86. </el-descriptions>
  87. </div>
  88. <!-- ============ 关联设备与预警指标 ============ -->
  89. <div class="wgd-section">
  90. <div class="section-head">
  91. <span class="section-bar bar-blue"></span>
  92. <span class="section-title">关联设备与预警指标</span>
  93. </div>
  94. <el-descriptions v-if="device" :column="2" border size="small">
  95. <el-descriptions-item label="设备名称">{{ device.equipmentName || '-' }}</el-descriptions-item>
  96. <el-descriptions-item label="设备编码">{{ device.equipmentCode || '-' }}</el-descriptions-item>
  97. <el-descriptions-item label="设备类型">
  98. {{ equipmentTypeMap.get(String(device.equipmentTypeId)) || '-' }}
  99. </el-descriptions-item>
  100. <el-descriptions-item label="在线状态">
  101. <el-tag :type="device.onlineStatus === 1 ? 'success' : 'info'" size="small">
  102. {{ device.onlineStatus === 1 ? '在线' : '离线' }}
  103. </el-tag>
  104. </el-descriptions-item>
  105. <el-descriptions-item label="安装位置" :span="2">
  106. {{ device.equipmentLocation || record.location || '-' }}
  107. </el-descriptions-item>
  108. </el-descriptions>
  109. <el-alert v-else type="info" :closable="false" show-icon class="device-missing"
  110. title="未在设备台账中找到该预警关联的设备,实时指标无法展示" />
  111. <div class="threshold-table" v-if="thresholds.length">
  112. <el-table :data="thresholds" border size="small">
  113. <el-table-column label="监测指标" prop="warningName" min-width="140" />
  114. <el-table-column label="单位" width="90" align="center">
  115. <template #default="{ row }">{{ row.warningUnit || '-' }}</template>
  116. </el-table-column>
  117. <el-table-column label="阈值范围" min-width="160" align="center">
  118. <template #default="{ row }">{{ formatThreshold(row) }}</template>
  119. </el-table-column>
  120. </el-table>
  121. </div>
  122. <div class="threshold-empty" v-else-if="device">该设备尚未配置报警阈值</div>
  123. </div>
  124. <!-- ============ 处置信息 ============ -->
  125. <div class="wgd-section">
  126. <div class="section-head">
  127. <span class="section-bar bar-orange"></span>
  128. <span class="section-title">处置信息</span>
  129. </div>
  130. <el-descriptions :column="2" border size="small">
  131. <el-descriptions-item label="发布人">{{ record.publisher || '-' }}</el-descriptions-item>
  132. <el-descriptions-item label="创建人">{{ record.createBy || '-' }}</el-descriptions-item>
  133. <el-descriptions-item label="处置人">{{ record.processor || '-' }}</el-descriptions-item>
  134. <el-descriptions-item label="最后更新">{{ formatDateTime(record.updateTime) }}</el-descriptions-item>
  135. </el-descriptions>
  136. </div>
  137. <!-- ============ 底部 ============ -->
  138. <div class="wgd-footer">
  139. <el-button @click="close">关 闭</el-button>
  140. </div>
  141. </div>
  142. </el-drawer>
  143. </template>
  144. <script setup name="WarningGisDetail">
  145. import { ref, computed, watch } from 'vue'
  146. import { Close } from '@element-plus/icons-vue'
  147. import { getEquipmentTypeList, getLatestWaterLevelByDevice } from '@/api/drainage'
  148. import { listEquipment } from '@/api/pipeNetwork/pdeviceInform'
  149. import { pageWarningThresholds } from '@/api/warningThreshold'
  150. import { listWarningTypes } from '@/api/warning/list'
  151. const props = defineProps({
  152. modelValue: { type: Boolean, default: false },
  153. /** 预警原始记录(保留后端返回的全部字段,展示靠它) */
  154. record: { type: Object, default: null },
  155. /** current=当前预警,history=历史预警(多一块归档信息) */
  156. mode: { type: String, default: 'current' }
  157. })
  158. const emit = defineEmits(['update:modelValue'])
  159. const visible = computed({
  160. get: () => props.modelValue,
  161. set: (val) => emit('update:modelValue', val)
  162. })
  163. const isHistory = computed(() => props.mode === 'history')
  164. // ==================== 基础字典 / 映射(模块级缓存,两个 GIS 页共用一次请求)====================
  165. const equipmentTypeMap = ref(new Map())
  166. const warningTypeMap = ref(new Map())
  167. let dictPromise = null
  168. function loadDicts() {
  169. if (dictPromise) return dictPromise
  170. dictPromise = Promise.allSettled([
  171. getEquipmentTypeList('2'),
  172. listWarningTypes()
  173. ]).then(([typeResult, warnResult]) => {
  174. if (typeResult.status === 'fulfilled') {
  175. const res = typeResult.value
  176. const list = res?.code !== undefined ? res.data : res
  177. const map = new Map()
  178. if (Array.isArray(list)) {
  179. list.forEach((item) => {
  180. if (item?.id !== undefined && item?.id !== null) map.set(String(item.id), item.typeName)
  181. })
  182. }
  183. equipmentTypeMap.value = map
  184. }
  185. if (warnResult.status === 'fulfilled') {
  186. const res = warnResult.value
  187. const list = res?.data || res?.rows || []
  188. const map = new Map()
  189. if (Array.isArray(list)) {
  190. list.forEach((item) => {
  191. if (item?.dictValue) map.set(String(item.dictValue), item.dictLabel || item.dictValue)
  192. })
  193. }
  194. warningTypeMap.value = map
  195. }
  196. })
  197. return dictPromise
  198. }
  199. // ==================== 设备 / 实时指标(模块级缓存)====================
  200. const deviceByName = ref(null)
  201. const waterLevelMap = ref({})
  202. const thresholdMap = ref({})
  203. let metricPromise = null
  204. const device = ref(null)
  205. const thresholds = ref([])
  206. const loading = ref(false)
  207. /** 按设备名称反查排水设备台账(一页 100 条,最多 10 页) */
  208. async function resolveDevice(equipmentName) {
  209. if (!equipmentName) return null
  210. if (!deviceByName.value) {
  211. const map = new Map()
  212. let pageNum = 1
  213. while (pageNum <= 10) {
  214. const res = await listEquipment({ rootTypeId: 2, pageNum, pageSize: 100 })
  215. const page = res?.data && Array.isArray(res.data.records) ? res.data : res
  216. const rows = page?.records || page?.rows || []
  217. if (!rows.length) break
  218. rows.forEach((item) => {
  219. if (item?.equipmentName && !map.has(item.equipmentName)) map.set(item.equipmentName, item)
  220. })
  221. if (rows.length < 100) break
  222. pageNum += 1
  223. }
  224. deviceByName.value = map
  225. }
  226. return deviceByName.value.get(equipmentName) || null
  227. }
  228. /** 实时液位 + 报警阈值,按设备编码索引 */
  229. function loadLiveMetrics() {
  230. if (metricPromise) return metricPromise
  231. metricPromise = Promise.allSettled([
  232. getLatestWaterLevelByDevice(),
  233. pageWarningThresholds({ rootTypeId: 2, pageNum: 1, pageSize: 500 })
  234. ]).then(([levelResult, thresholdResult]) => {
  235. if (levelResult.status === 'fulfilled') {
  236. const value = levelResult.value
  237. const list = Array.isArray(value) ? value : (value?.data || [])
  238. const map = {}
  239. list.forEach((item) => {
  240. if (!item?.deviceCode) return
  241. map[item.deviceCode] = { value: item.waterLevel, time: item.createTime }
  242. })
  243. waterLevelMap.value = map
  244. }
  245. if (thresholdResult.status === 'fulfilled') {
  246. const res = thresholdResult.value
  247. const page = res?.data && Array.isArray(res.data.records) ? res.data : res
  248. const rows = page?.records || page?.rows || []
  249. const map = {}
  250. rows.forEach((row) => {
  251. if (!row?.deviceCode) return
  252. if (!map[row.deviceCode]) map[row.deviceCode] = []
  253. map[row.deviceCode].push(row)
  254. })
  255. thresholdMap.value = map
  256. }
  257. })
  258. return metricPromise
  259. }
  260. // ==================== 展示计算 ====================
  261. const levelKey = computed(() => {
  262. const map = { 1: 'severe', 2: 'important', 3: 'normal' }
  263. return map[props.record?.warningLevel] || 'normal'
  264. })
  265. const levelText = computed(() => {
  266. const map = { 1: '严重', 2: '重要', 3: '一般' }
  267. return map[props.record?.warningLevel] || '一般'
  268. })
  269. const levelTagType = computed(() => {
  270. const map = { severe: 'danger', important: 'warning', normal: '' }
  271. return map[levelKey.value] || 'info'
  272. })
  273. const statusKey = computed(() => String(props.record?.status || 'unknown').toLowerCase())
  274. const statusTextValue = computed(() => {
  275. const map = {
  276. DRAFT: '草稿', PENDING: '待办', RECEIVED: '已接收',
  277. PROCESSING: '处置中', HANDLED: '已处置', CLOSED: '已解除'
  278. }
  279. return map[props.record?.status] || '-'
  280. })
  281. const statusTagType = computed(() => {
  282. const map = {
  283. DRAFT: 'info', PENDING: 'warning', RECEIVED: 'warning',
  284. PROCESSING: 'warning', HANDLED: 'success', CLOSED: ''
  285. }
  286. return map[props.record?.status] || 'info'
  287. })
  288. /** 预警类型:新数据看 warningType,历史数据回退到 deviceCode 的设备类型名 */
  289. const warningTypeText = computed(() => {
  290. const row = props.record
  291. if (!row) return '-'
  292. const byType = row.warningType ? warningTypeMap.value.get(String(row.warningType)) : null
  293. if (byType) return byType
  294. const byDevice = row.deviceCode ? equipmentTypeMap.value.get(String(row.deviceCode)) : null
  295. if (byDevice) return byDevice
  296. return row.warningType || '未分类'
  297. })
  298. /** 归档编号:预警解除即归档,由预警编号派生 */
  299. const archiveNo = computed(() => {
  300. const no = props.record?.warningNo
  301. if (!no) return '-'
  302. return `ARC${String(no).replace(/^WRN/, '').slice(-12)}`
  303. })
  304. const archiveTime = computed(() => props.record?.updateTime || props.record?.publishTime || null)
  305. /** 实时指标 + 是否越限 */
  306. const metric = computed(() => {
  307. const code = device.value?.equipmentCode
  308. const levelItem = code ? waterLevelMap.value[code] : null
  309. const list = code ? (thresholdMap.value[code] || []) : []
  310. const value = levelItem && levelItem.value !== undefined && levelItem.value !== null ? levelItem.value : null
  311. return {
  312. level: value,
  313. collectTime: levelItem?.time ? formatDateTime(levelItem.time) : '',
  314. thresholdText: list.length ? list.map(formatThreshold).join(' / ') : '',
  315. max: pickUpper(list)
  316. }
  317. })
  318. const alarm = computed(() => {
  319. const level = metric.value.level
  320. const max = metric.value.max
  321. if (level === null || max === null) return false
  322. return Number(level) > Number(max)
  323. })
  324. /** 取阈值里最严的上限(区间取 maxValue,上限型取 maxValue) */
  325. function pickUpper(list) {
  326. let upper = null
  327. list.forEach((item) => {
  328. if (item?.dataType === 1) return
  329. const val = item?.maxValue
  330. if (val === null || val === undefined || val === '') return
  331. const num = Number(val)
  332. if (Number.isNaN(num)) return
  333. upper = upper === null ? num : Math.min(upper, num)
  334. })
  335. return upper
  336. }
  337. function formatThreshold(item) {
  338. if (!item) return '-'
  339. const unit = item.warningUnit || ''
  340. const min = item.minValue
  341. const max = item.maxValue
  342. if (item.dataType === 1) return min === null || min === undefined ? '-' : `≥ ${min}${unit}`
  343. if (item.dataType === 2) return max === null || max === undefined ? '-' : `≤ ${max}${unit}`
  344. if ((min === null || min === undefined) && (max === null || max === undefined)) return '未设置'
  345. return `${min ?? '-'} ~ ${max ?? '-'}${unit}`
  346. }
  347. function formatDateTime(value) {
  348. if (!value) return '-'
  349. return String(value).replace('T', ' ').slice(0, 19)
  350. }
  351. // ==================== 加载 / 关闭 ====================
  352. async function loadDetail(row) {
  353. device.value = null
  354. thresholds.value = []
  355. if (!row) return
  356. loading.value = true
  357. try {
  358. await loadDicts()
  359. const [matched] = await Promise.all([resolveDevice(row.equipmentName), loadLiveMetrics()])
  360. device.value = matched || null
  361. const code = matched?.equipmentCode
  362. thresholds.value = code ? (thresholdMap.value[code] || []) : []
  363. } catch (error) {
  364. console.warn('预警详情加载失败', error)
  365. } finally {
  366. loading.value = false
  367. }
  368. }
  369. function close() {
  370. visible.value = false
  371. }
  372. watch(
  373. () => [props.modelValue, props.record],
  374. ([shown, row]) => {
  375. if (shown) loadDetail(row)
  376. },
  377. { immediate: true }
  378. )
  379. </script>
  380. <style scoped>
  381. .wgd {
  382. padding: 0 4px 8px;
  383. }
  384. /* ==================== 顶部标题区 ==================== */
  385. .wgd-hero {
  386. display: flex;
  387. align-items: center;
  388. justify-content: space-between;
  389. gap: 16px;
  390. margin: -20px -20px 0;
  391. padding: 22px 20px 20px;
  392. color: #fff;
  393. background: linear-gradient(135deg, #3a4a63 0%, #22304a 100%);
  394. }
  395. .wgd-hero.hero-severe {
  396. background: linear-gradient(135deg, #d9534f 0%, #8e2b28 100%);
  397. }
  398. .wgd-hero.hero-important {
  399. background: linear-gradient(135deg, #e6a23c 0%, #a86f16 100%);
  400. }
  401. .wgd-hero.hero-normal {
  402. background: linear-gradient(135deg, #409eff 0%, #1f5fa8 100%);
  403. }
  404. .hero-left {
  405. display: flex;
  406. align-items: center;
  407. gap: 14px;
  408. min-width: 0;
  409. }
  410. .hero-badge {
  411. flex-shrink: 0;
  412. width: 54px;
  413. height: 54px;
  414. border-radius: 10px;
  415. background: rgba(255, 255, 255, 0.18);
  416. border: 1px solid rgba(255, 255, 255, 0.35);
  417. display: flex;
  418. align-items: center;
  419. justify-content: center;
  420. font-size: 17px;
  421. font-weight: 700;
  422. letter-spacing: 1px;
  423. }
  424. .hero-main {
  425. min-width: 0;
  426. }
  427. .hero-name {
  428. font-size: 18px;
  429. font-weight: 600;
  430. color: #fff;
  431. line-height: 1.3;
  432. overflow: hidden;
  433. text-overflow: ellipsis;
  434. white-space: nowrap;
  435. max-width: 460px;
  436. }
  437. .hero-sub {
  438. margin-top: 6px;
  439. font-size: 12px;
  440. color: rgba(255, 255, 255, 0.78);
  441. display: flex;
  442. align-items: center;
  443. gap: 6px;
  444. }
  445. .hero-no {
  446. font-family: Consolas, Monaco, monospace;
  447. letter-spacing: 0.5px;
  448. }
  449. .hero-dot {
  450. opacity: 0.6;
  451. }
  452. .hero-right {
  453. display: flex;
  454. align-items: center;
  455. gap: 8px;
  456. flex-shrink: 0;
  457. }
  458. .hero-status {
  459. padding: 3px 10px;
  460. border-radius: 12px;
  461. font-size: 12px;
  462. background: rgba(255, 255, 255, 0.2);
  463. border: 1px solid rgba(255, 255, 255, 0.35);
  464. }
  465. .hero-status.st-pending,
  466. .hero-status.st-received,
  467. .hero-status.st-processing {
  468. background: rgba(255, 255, 255, 0.28);
  469. }
  470. .hero-close {
  471. color: rgba(255, 255, 255, 0.85);
  472. }
  473. .hero-close:hover {
  474. color: #fff;
  475. background: rgba(255, 255, 255, 0.16);
  476. }
  477. /* ==================== 关键指标条 ==================== */
  478. .wgd-kpi {
  479. display: grid;
  480. grid-template-columns: repeat(4, 1fr);
  481. gap: 1px;
  482. background: #ebeef5;
  483. border: 1px solid #ebeef5;
  484. border-radius: 6px;
  485. overflow: hidden;
  486. margin: 16px 0;
  487. }
  488. .kpi-item {
  489. background: #fafbfc;
  490. padding: 12px 14px;
  491. min-width: 0;
  492. }
  493. .kpi-label {
  494. font-size: 12px;
  495. color: #909399;
  496. margin-bottom: 6px;
  497. }
  498. .kpi-value {
  499. font-size: 22px;
  500. font-weight: 600;
  501. color: #303133;
  502. line-height: 1.2;
  503. }
  504. .kpi-value i {
  505. font-style: normal;
  506. font-size: 12px;
  507. font-weight: 400;
  508. color: #909399;
  509. margin-left: 2px;
  510. }
  511. .kpi-value.kpi-text {
  512. font-size: 13px;
  513. font-weight: 500;
  514. word-break: break-all;
  515. line-height: 1.5;
  516. }
  517. .kpi-value.kpi-alarm {
  518. color: #f56c6c;
  519. }
  520. .kpi-empty {
  521. font-size: 20px;
  522. color: #c0c4cc;
  523. }
  524. /* ==================== 分区 ==================== */
  525. .wgd-section {
  526. margin-bottom: 18px;
  527. }
  528. .section-head {
  529. display: flex;
  530. align-items: center;
  531. gap: 8px;
  532. margin-bottom: 10px;
  533. }
  534. .section-bar {
  535. width: 3px;
  536. height: 14px;
  537. border-radius: 2px;
  538. background: #f56c6c;
  539. }
  540. .section-bar.bar-green {
  541. background: #67c23a;
  542. }
  543. .section-bar.bar-blue {
  544. background: #409eff;
  545. }
  546. .section-bar.bar-orange {
  547. background: #e6a23c;
  548. }
  549. .section-title {
  550. font-size: 14px;
  551. font-weight: 600;
  552. color: #303133;
  553. }
  554. .desc-text {
  555. background: #f5f7fa;
  556. border-radius: 4px;
  557. padding: 10px;
  558. font-size: 13px;
  559. color: #606266;
  560. line-height: 1.6;
  561. max-height: 130px;
  562. overflow-y: auto;
  563. white-space: pre-wrap;
  564. word-break: break-all;
  565. }
  566. .device-missing {
  567. margin-bottom: 10px;
  568. }
  569. .threshold-table {
  570. margin-top: 12px;
  571. }
  572. .threshold-empty {
  573. margin-top: 10px;
  574. font-size: 12px;
  575. color: #909399;
  576. background: #f5f7fa;
  577. border-radius: 4px;
  578. padding: 8px 12px;
  579. }
  580. /* ==================== 底部 ==================== */
  581. .wgd-footer {
  582. display: flex;
  583. justify-content: flex-end;
  584. padding-top: 4px;
  585. }
  586. </style>