mRealApicture.vue 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620
  1. <!--窨井盖状态监测-->
  2. <template>
  3. <div class="monitor-dashboard">
  4. <!-- 头部统计区 -->
  5. <div class="dashboard-header">
  6. <div class="stats-row">
  7. <div class="stat-card" :class="{ warning: alertStats.critical > 0 }">
  8. <div class="stat-value">{{ alertStats.critical }}</div>
  9. <div class="stat-label">严重告警</div>
  10. </div>
  11. <div class="stat-card" :class="{ warning: alertStats.warning > 0 }">
  12. <div class="stat-value">{{ alertStats.warning }}</div>
  13. <div class="stat-label">一般告警</div>
  14. </div>
  15. <div class="stat-card">
  16. <div class="stat-value">{{ onlineRate }}%</div>
  17. <div class="stat-label">设备在线率</div>
  18. </div>
  19. <div class="stat-card">
  20. <div class="stat-value">{{ avgBattery }}%</div>
  21. <div class="stat-label">平均电量</div>
  22. </div>
  23. </div>
  24. </div>
  25. <!-- 主内容区:左侧设备网格 + 右侧详情/处置面板 -->
  26. <div class="main-layout">
  27. <!-- 左侧设备卡片列表 -->
  28. <div class="device-grid">
  29. <div class="grid-header">
  30. <span>监测设备列表</span>
  31. <el-input v-model="searchText" placeholder="搜索设备编号/名称" prefix-icon="Search" clearable size="small" style="width: 200px" @keyup.enter="loadDevices" @clear="loadDevices" />
  32. </div>
  33. <div class="card-container" v-loading="loading">
  34. <div
  35. v-for="device in filteredDevices"
  36. :key="device.id"
  37. class="device-card"
  38. :class="{ 'has-alert': device.hasAlert, 'selected': selectedDevice?.id === device.id }"
  39. @click="selectDevice(device)"
  40. >
  41. <div class="card-header">
  42. <span class="device-name">{{ device.name }}</span>
  43. <el-badge :value="device.alertLevel ? device.alertLevel === 'critical' ? '严重' : '警告' : ''" :type="device.alertLevel === 'critical' ? 'danger' : 'warning'" v-if="device.hasAlert">
  44. <el-icon><WarningFilled /></el-icon>
  45. </el-badge>
  46. <el-tag v-else size="small" type="success">正常</el-tag>
  47. </div>
  48. <div class="device-location">
  49. <el-icon><Location /></el-icon> {{ device.location }}
  50. </div>
  51. <div class="status-icons">
  52. <div class="status-item" :class="{ active: device.status.tilt }" title="倾斜告警">
  53. <el-icon><TurnOff /></el-icon> 倾斜
  54. </div>
  55. <div class="status-item" :class="{ active: device.status.waterInfiltration }" title="水浸告警">
  56. <el-icon><Position /></el-icon> 水浸
  57. </div>
  58. <div class="status-item" :class="{ active: device.status.waterLevel }" title="水位告警">
  59. <el-icon><Opportunity /></el-icon> 水位
  60. </div>
  61. <div class="status-item" :class="{ active: device.status.lowBattery }" title="低电量">
  62. <el-icon><Warning /></el-icon> 低电
  63. </div>
  64. </div>
  65. <div class="device-metrics">
  66. <div class="metric">
  67. <span>电量</span>
  68. <el-progress :percentage="device.battery" :stroke-width="6" :color="batteryColor(device.battery)" :show-text="false" />
  69. <span class="metric-value">{{ device.battery }}%</span>
  70. </div>
  71. <div class="metric">
  72. <span>信号</span>
  73. <el-progress :percentage="device.signal" :stroke-width="6" :color="signalColor(device.signal)" :show-text="false" />
  74. <span class="metric-value">{{ device.signal }}%</span>
  75. </div>
  76. </div>
  77. <div class="card-footer">
  78. <span class="update-time">最后通讯: {{ device.lastTime }}</span>
  79. <el-button v-if="device.hasAlert && !device.workOrderSubmitted" type="danger" size="small" plain @click.stop="handleAlert(device)">处理告警</el-button>
  80. <el-tag v-else-if="device.workOrderSubmitted" type="warning" size="small" effect="plain">工单处理中</el-tag>
  81. </div>
  82. </div>
  83. <el-empty v-if="!loading && filteredDevices.length === 0" description="暂无设备数据" :image-size="80" />
  84. </div>
  85. </div>
  86. <!-- 右侧详情及处置面板 -->
  87. <div class="detail-panel">
  88. <div v-if="selectedDevice" class="detail-container">
  89. <div class="panel-title">
  90. <span>设备详情 & 处置机制</span>
  91. <el-button type="primary" size="small" link @click="refreshDevice">刷新状态</el-button>
  92. </div>
  93. <div class="device-basic">
  94. <h3>{{ selectedDevice.name }}</h3>
  95. <p><el-icon><Location /></el-icon> {{ selectedDevice.location }} | 编号: {{ selectedDevice.id }}</p>
  96. </div>
  97. <!-- 实时状态监测模块 -->
  98. <el-descriptions :column="2" border class="status-description">
  99. <el-descriptions-item label="倾斜状态">
  100. <el-tag :type="selectedDevice.status.tilt ? 'danger' : 'success'">{{ selectedDevice.status.tilt ? '倾斜告警' : '水平' }}</el-tag>
  101. <span v-if="selectedDevice.tiltAngle > 0" class="sub-value">(倾斜角: {{ selectedDevice.tiltAngle }}°, 阈值: {{ selectedDevice.threshold }}°)</span>
  102. </el-descriptions-item>
  103. <el-descriptions-item label="水浸状态">
  104. <el-tag :type="selectedDevice.status.waterInfiltration ? 'danger' : 'success'">{{ selectedDevice.status.waterInfiltration ? '水浸告警' : '正常' }}</el-tag>
  105. </el-descriptions-item>
  106. <el-descriptions-item label="水位状态">
  107. <el-tag :type="selectedDevice.status.waterLevel ? 'danger' : 'success'">{{ selectedDevice.status.waterLevel ? '水位告警' : '正常' }}</el-tag>
  108. </el-descriptions-item>
  109. <el-descriptions-item label="综合报警">
  110. <el-tag :type="selectedDevice.raw?.manholeData?.alarmStatus === '1' ? 'danger' : 'success'">{{ selectedDevice.raw?.manholeData?.alarmStatus === '1' ? '报警中' : '无报警' }}</el-tag>
  111. </el-descriptions-item>
  112. <el-descriptions-item label="电池电压">
  113. <el-progress :percentage="selectedDevice.battery" :format="() => `${selectedDevice.battery}%`" :color="batteryColor(selectedDevice.battery)" />
  114. <span>{{ selectedDevice.voltage }}V</span>
  115. </el-descriptions-item>
  116. <el-descriptions-item label="设备信号">
  117. <el-progress :percentage="selectedDevice.signal" :format="() => `${selectedDevice.signal}%`" :color="signalColor(selectedDevice.signal)" />
  118. <span>{{ selectedDevice.signalStrength }}</span>
  119. </el-descriptions-item>
  120. </el-descriptions>
  121. <!-- 报警与处理机制区域 -->
  122. <div class="alert-section" v-if="selectedDevice.hasAlert">
  123. <div class="alert-header">
  124. <el-icon color="red"><Warning /></el-icon>
  125. <span class="alert-title">设备告警详情</span>
  126. <el-tag :type="selectedDevice.alertLevel === 'critical' ? 'danger' : 'warning'" effect="dark">
  127. {{ selectedDevice.alertLevel === 'critical' ? '严重告警' : '一般告警' }}
  128. </el-tag>
  129. </div>
  130. <div class="alert-message">
  131. <ul>
  132. <li v-if="selectedDevice.status.tilt">⚠️ 井盖发生位移倾斜({{ selectedDevice.tiltAngle }}°),超过阈值 {{ selectedDevice.threshold }}°,可能损坏或移位。</li>
  133. <li v-if="selectedDevice.status.waterInfiltration">⚠️ 检测到水浸报警,井内可能进水,需前往现场检查。</li>
  134. <li v-if="selectedDevice.status.waterLevel">⚠️ 水位超限,溢水风险,需排水处理。</li>
  135. <li v-if="selectedDevice.status.lowBattery">🔋 电池电压过低({{ selectedDevice.battery }}%),请及时更换电池。</li>
  136. <li v-if="selectedDevice.status.lowSignal">📶 设备信号弱({{ selectedDevice.signal }}%),可能影响数据传输。</li>
  137. </ul>
  138. </div>
  139. <div class="action-buttons" v-if="!selectedDevice.workOrderSubmitted">
  140. <el-button type="primary" :loading="submitting" @click="handleDispatch(selectedDevice)">启动处理机制</el-button>
  141. <el-button @click="simulateFix(selectedDevice)">模拟修复/复位</el-button>
  142. </div>
  143. <el-alert v-else title="工单已派发,正在处理中" description="相关部门将即刻前往现场处理,请耐心等待。" type="success" :closable="false" show-icon class="submitted-alert" />
  144. <!-- 处理记录与工单 -->
  145. <div class="history-log" v-if="selectedDevice.alertHistory.length">
  146. <div class="history-title">处置记录</div>
  147. <el-timeline>
  148. <el-timeline-item v-for="(log, idx) in selectedDevice.alertHistory" :key="idx" :timestamp="log.time" :type="log.type">
  149. {{ log.content }}
  150. </el-timeline-item>
  151. </el-timeline>
  152. </div>
  153. </div>
  154. <!-- 正常时显示机制说明 -->
  155. <div v-else class="normal-message">
  156. <el-result icon="success" title="设备运行正常" sub-title="所有监测指标均在正常范围内,无报警事件。">
  157. <template #extra>
  158. <el-button type="primary" @click="simulateAlert(selectedDevice)">模拟触发告警(测试)</el-button>
  159. </template>
  160. </el-result>
  161. </div>
  162. </div>
  163. <div v-else class="empty-panel">
  164. <el-empty description="请从左侧选择一个设备查看状态" :image-size="120" />
  165. </div>
  166. </div>
  167. </div>
  168. <!-- 处理机制弹窗 -->
  169. <el-dialog v-model="dialogVisible" title="启动处理机制 - 工单派发" width="450px">
  170. <el-form :model="taskForm">
  171. <el-form-item label="处理类型">
  172. <el-select v-model="taskForm.taskType" placeholder="请选择">
  173. <el-option label="维修派遣" value="repair" />
  174. <el-option label="现场巡检" value="inspect" />
  175. <el-option label="排水作业" value="drain" />
  176. <el-option label="更换电池/模块" value="replace" />
  177. </el-select>
  178. </el-form-item>
  179. <el-form-item label="负责人">
  180. <el-input v-model="taskForm.owner" placeholder="指派负责人" />
  181. </el-form-item>
  182. <el-form-item label="备注">
  183. <el-input type="textarea" v-model="taskForm.remark" placeholder="处理说明" />
  184. </el-form-item>
  185. </el-form>
  186. <template #footer>
  187. <el-button @click="dialogVisible = false">取消</el-button>
  188. <el-button type="primary" @click="submitTask">确认派发</el-button>
  189. </template>
  190. </el-dialog>
  191. </div>
  192. </template>
  193. <script setup>
  194. import { ref, computed, onMounted } from 'vue'
  195. import { WarningFilled, Location, TurnOff, Position, Opportunity, Warning, Search } from '@element-plus/icons-vue'
  196. import { ElMessage } from 'element-plus'
  197. import { getManholeAlarmMonitorList, addManholeWorkOrder } from '@/api/pipeNetwork/basic'
  198. /* ============ 状态 ============ */
  199. const devices = ref([])
  200. const loading = ref(false)
  201. const searchText = ref('')
  202. const selectedDevice = ref(null)
  203. const dialogVisible = ref(false)
  204. const submitting = ref(false)
  205. const taskForm = ref({ taskType: 'repair', owner: '', remark: '' })
  206. /* ============ 数据加载 ============ */
  207. async function loadDevices() {
  208. loading.value = true
  209. try {
  210. const keyword = searchText.value?.trim()
  211. const res = await getManholeAlarmMonitorList({ keyword: keyword || undefined })
  212. const result = res.data || {}
  213. devices.value = (result.devices || []).map(item => normalizeDevice(item))
  214. } catch (error) {
  215. devices.value = []
  216. const msg = error?.message || error?.msg || '未知错误'
  217. console.error('[mRealApicture] 设备数据加载失败:', error)
  218. ElMessage.error('设备数据加载失败: ' + msg)
  219. } finally {
  220. loading.value = false
  221. }
  222. }
  223. /**
  224. * 将后端返回的设备数据(含 jg_device_data 最新记录)规范化为前端展示对象
  225. *
  226. * 报警等级判定逻辑:
  227. * 严重告警(critical) — 倾斜角度超阈值 / 水浸报警 / 水位报警
  228. * 一般告警(warning) — 低电量(battery<=20) / 弱信号(signal<=20)
  229. */
  230. function normalizeDevice(raw) {
  231. const md = raw?.manholeData || {}
  232. const st = raw?.equipmentStatus || {}
  233. // 数值解析
  234. const tiltAngle = parseFloat(md.tiltAngle) || 0
  235. const angleThreshold = parseFloat(md.angleAlarmThreshold) || 15
  236. const battery = parseFloat(md.batteryLevel) || 0
  237. const signal = parseFloat(md.signalStrength) || 0
  238. const isOnline = Number(st.onlineStatus) === 1
  239. // 状态标志
  240. const tilt = isOnline && tiltAngle > angleThreshold
  241. const waterInfiltration = isOnline && `${md.waterInfiltrationAlarmStatus}` === '1'
  242. const waterLevel = isOnline && `${md.waterLevelAlarmStatus}` === '1'
  243. const lowBattery = battery > 0 && battery <= 20
  244. const lowSignal = signal > 0 && signal <= 20
  245. // 告警等级:严重 > 一般
  246. const isCritical = tilt || waterInfiltration || waterLevel
  247. const isWarning = !isCritical && (lowBattery || lowSignal)
  248. const hasAlert = isCritical || isWarning
  249. const alertLevel = isCritical ? 'critical' : (isWarning ? 'warning' : null)
  250. // 电压近似值(2.4V ~ 4.2V 映射到 0~100%)
  251. const voltage = battery > 0 ? (2.4 + battery * 0.018).toFixed(1) : '—'
  252. return {
  253. id: raw?.equipmentCode || raw?.equipmentId || '',
  254. equipmentId: raw?.equipmentId || '',
  255. name: raw?.equipmentName || raw?.equipmentCode || '未命名设备',
  256. location: raw?.equipmentLocation || '暂无位置信息',
  257. battery,
  258. voltage,
  259. signal,
  260. signalStrength: signal > 60 ? '强' : signal > 30 ? '中' : signal > 0 ? '弱' : '—',
  261. status: { tilt, waterInfiltration, waterLevel, lowBattery, lowSignal },
  262. hasAlert,
  263. alertLevel,
  264. tiltAngle,
  265. threshold: angleThreshold,
  266. lastTime: md.uploadTime || md.createTime || '',
  267. alertHistory: [],
  268. workOrderSubmitted: !!raw?.workOrderSubmitted,
  269. raw
  270. }
  271. }
  272. /* ============ 计算属性 ============ */
  273. const filteredDevices = computed(() => {
  274. if (!searchText.value) return devices.value
  275. const kw = searchText.value.toLowerCase()
  276. return devices.value.filter(d =>
  277. d.id.toLowerCase().includes(kw) || d.name.toLowerCase().includes(kw)
  278. )
  279. })
  280. const alertStats = computed(() => {
  281. let critical = 0, warning = 0
  282. devices.value.forEach(d => {
  283. if (d.hasAlert) {
  284. if (d.alertLevel === 'critical') critical++
  285. else warning++
  286. }
  287. })
  288. return { critical, warning }
  289. })
  290. const onlineRate = computed(() => {
  291. if (!devices.value.length) return 0
  292. const online = devices.value.filter(d => d.signal > 0).length
  293. return Math.round((online / devices.value.length) * 100)
  294. })
  295. const avgBattery = computed(() => {
  296. if (!devices.value.length) return 0
  297. const sum = devices.value.reduce((acc, d) => acc + d.battery, 0)
  298. return Math.round(sum / devices.value.length)
  299. })
  300. /* ============ 辅助函数 ============ */
  301. const batteryColor = (percent) => percent < 20 ? '#f56c6c' : percent < 50 ? '#e6a23c' : '#67c23a'
  302. const signalColor = (percent) => percent < 30 ? '#f56c6c' : percent < 60 ? '#e6a23c' : '#67c23a'
  303. /* ============ 交互操作 ============ */
  304. const selectDevice = (device) => {
  305. selectedDevice.value = device
  306. }
  307. const handleAlert = (device) => {
  308. selectedDevice.value = device
  309. }
  310. const handleDispatch = () => {
  311. taskForm.value = { taskType: 'repair', owner: '', remark: '' }
  312. dialogVisible.value = true
  313. }
  314. // 工单类型映射: taskType → orderType(1-故障维修 2-日常巡检 3-设备保养)
  315. const ORDER_TYPE_MAP = { repair: 1, inspect: 2, drain: 1, replace: 3 }
  316. const TYPE_LABEL_MAP = { repair: '故障维修', inspect: '日常巡检', drain: '排水作业', replace: '设备保养' }
  317. const submitTask = async () => {
  318. if (!selectedDevice.value?.equipmentId) {
  319. ElMessage.warning('设备信息缺失,无法提交工单')
  320. return
  321. }
  322. if (!taskForm.value.remark?.trim()) {
  323. ElMessage.warning('请填写工单问题描述')
  324. return
  325. }
  326. submitting.value = true
  327. try {
  328. const device = selectedDevice.value
  329. await addManholeWorkOrder({
  330. deviceId: device.equipmentId,
  331. orderType: ORDER_TYPE_MAP[taskForm.value.taskType] || 1,
  332. orderLevel: device.alertLevel === 'critical' ? 1 : 2,
  333. orderDesc: `[${TYPE_LABEL_MAP[taskForm.value.taskType] || '故障维修'}] ${taskForm.value.remark}${taskForm.value.owner ? '(负责人:' + taskForm.value.owner + ')' : ''}`
  334. })
  335. // 提交成功:标记工单已派发,隐藏按钮
  336. device.workOrderSubmitted = true
  337. const typeLabel = TYPE_LABEL_MAP[taskForm.value.taskType] || '处理'
  338. device.alertHistory.unshift({
  339. time: new Date().toLocaleString(),
  340. content: `已派发${typeLabel}工单${taskForm.value.owner ? ',负责人:' + taskForm.value.owner : ''},${taskForm.value.remark}`,
  341. type: 'primary'
  342. })
  343. ElMessage.success('工单已派发,相关部门将即刻处理')
  344. dialogVisible.value = false
  345. } catch (error) {
  346. const msg = error?.message || error?.msg || '未知错误'
  347. ElMessage.error('工单派发失败: ' + msg)
  348. } finally {
  349. submitting.value = false
  350. }
  351. }
  352. const simulateFix = (device) => {
  353. device.status = { tilt: false, waterInfiltration: false, waterLevel: false, lowBattery: false, lowSignal: false }
  354. device.hasAlert = false
  355. device.alertLevel = null
  356. device.tiltAngle = 0
  357. if (device.battery < 30) device.battery += 20
  358. const fixLog = { time: new Date().toLocaleString(), content: '运维人员已现场检修,设备恢复正常', type: 'success' }
  359. device.alertHistory.unshift(fixLog)
  360. ElMessage.success('已模拟修复,设备状态恢复正常')
  361. }
  362. const simulateAlert = (device) => {
  363. device.status.tilt = true
  364. device.tiltAngle = 9.3
  365. device.status.waterLevel = true
  366. device.hasAlert = true
  367. device.alertLevel = 'critical'
  368. const alertLog = { time: new Date().toLocaleString(), content: '【模拟触发】倾斜+水位告警', type: 'danger' }
  369. device.alertHistory.unshift(alertLog)
  370. ElMessage.warning('已触发模拟告警,请及时处置')
  371. }
  372. const refreshDevice = () => {
  373. loadDevices()
  374. }
  375. /* ============ 生命周期 ============ */
  376. onMounted(() => {
  377. loadDevices()
  378. })
  379. </script>
  380. <style scoped>
  381. .monitor-dashboard {
  382. padding: 20px;
  383. background: #f0f2f6;
  384. min-height: 100vh;
  385. }
  386. .dashboard-header {
  387. background: white;
  388. border-radius: 16px;
  389. padding: 16px 24px;
  390. margin-bottom: 20px;
  391. box-shadow: 0 2px 8px rgba(0,0,0,0.04);
  392. }
  393. .stats-row {
  394. display: flex;
  395. gap: 24px;
  396. flex-wrap: wrap;
  397. }
  398. .stat-card {
  399. background: #f8fafc;
  400. border-radius: 12px;
  401. padding: 12px 20px;
  402. min-width: 100px;
  403. text-align: center;
  404. transition: all 0.2s;
  405. }
  406. .stat-card.warning {
  407. background: #fff3f0;
  408. border-left: 4px solid #f56c6c;
  409. }
  410. .stat-value {
  411. font-size: 28px;
  412. font-weight: bold;
  413. color: #2c3e50;
  414. }
  415. .stat-label {
  416. font-size: 13px;
  417. color: #6c757d;
  418. }
  419. .main-layout {
  420. display: grid;
  421. grid-template-columns: 380px 1fr;
  422. gap: 20px;
  423. }
  424. .device-grid {
  425. background: white;
  426. border-radius: 20px;
  427. overflow: hidden;
  428. box-shadow: 0 4px 12px rgba(0,0,0,0.05);
  429. display: flex;
  430. flex-direction: column;
  431. }
  432. .grid-header {
  433. display: flex;
  434. justify-content: space-between;
  435. padding: 16px;
  436. border-bottom: 1px solid #e9ecef;
  437. font-weight: 600;
  438. }
  439. .card-container {
  440. padding: 12px;
  441. max-height: 75vh;
  442. overflow-y: auto;
  443. }
  444. .device-card {
  445. border: 1px solid #e4e7ed;
  446. border-radius: 16px;
  447. padding: 14px;
  448. margin-bottom: 12px;
  449. cursor: pointer;
  450. transition: all 0.2s;
  451. background: white;
  452. }
  453. .device-card:hover {
  454. transform: translateY(-2px);
  455. box-shadow: 0 6px 14px rgba(0,0,0,0.08);
  456. }
  457. .device-card.has-alert {
  458. border-left: 4px solid #f56c6c;
  459. background: #fffaf9;
  460. }
  461. .device-card.selected {
  462. border: 2px solid #409eff;
  463. background: #ecf5ff;
  464. }
  465. .card-header {
  466. display: flex;
  467. justify-content: space-between;
  468. align-items: center;
  469. margin-bottom: 8px;
  470. }
  471. .device-name {
  472. font-weight: 700;
  473. font-size: 15px;
  474. }
  475. .device-location {
  476. font-size: 12px;
  477. color: #6c757d;
  478. margin-bottom: 10px;
  479. }
  480. .status-icons {
  481. display: flex;
  482. gap: 16px;
  483. margin: 10px 0;
  484. }
  485. .status-item {
  486. display: flex;
  487. align-items: center;
  488. gap: 4px;
  489. font-size: 12px;
  490. background: #f5f5f5;
  491. padding: 2px 8px;
  492. border-radius: 20px;
  493. color: #909399;
  494. }
  495. .status-item.active {
  496. background: #ffeded;
  497. color: #f56c6c;
  498. }
  499. .device-metrics {
  500. margin: 12px 0;
  501. }
  502. .metric {
  503. display: flex;
  504. align-items: center;
  505. gap: 8px;
  506. font-size: 12px;
  507. margin-bottom: 6px;
  508. }
  509. .metric-value {
  510. width: 36px;
  511. text-align: right;
  512. }
  513. .card-footer {
  514. display: flex;
  515. justify-content: space-between;
  516. align-items: center;
  517. font-size: 11px;
  518. color: #aaa;
  519. }
  520. .detail-panel {
  521. background: white;
  522. border-radius: 20px;
  523. box-shadow: 0 4px 12px rgba(0,0,0,0.05);
  524. padding: 20px;
  525. overflow-y: auto;
  526. }
  527. .panel-title {
  528. display: flex;
  529. justify-content: space-between;
  530. margin-bottom: 16px;
  531. font-weight: bold;
  532. font-size: 18px;
  533. border-bottom: 2px solid #eef2f6;
  534. padding-bottom: 10px;
  535. }
  536. .status-description {
  537. margin-top: 10px;
  538. }
  539. .sub-value {
  540. margin-left: 8px;
  541. font-size: 12px;
  542. color: #909399;
  543. }
  544. .alert-section {
  545. margin-top: 24px;
  546. border: 1px solid #ffd6d6;
  547. background: #fff8f6;
  548. border-radius: 16px;
  549. padding: 16px;
  550. }
  551. .alert-header {
  552. display: flex;
  553. align-items: center;
  554. gap: 12px;
  555. margin-bottom: 12px;
  556. }
  557. .alert-title {
  558. font-weight: bold;
  559. font-size: 16px;
  560. }
  561. .alert-message ul {
  562. margin: 8px 0 16px;
  563. padding-left: 20px;
  564. }
  565. .action-buttons {
  566. display: flex;
  567. gap: 12px;
  568. margin: 16px 0;
  569. }
  570. .history-log {
  571. margin-top: 16px;
  572. background: #f8f9fc;
  573. border-radius: 12px;
  574. padding: 12px;
  575. }
  576. .normal-message {
  577. margin-top: 40px;
  578. }
  579. .submitted-alert {
  580. margin: 16px 0;
  581. }
  582. .empty-panel {
  583. height: 100%;
  584. display: flex;
  585. align-items: center;
  586. justify-content: center;
  587. }
  588. </style>