mOdataStatistics.vue 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625
  1. <!--数据统计 - 井盖设备智能终端运维看板-->
  2. <template>
  3. <div class="terminal-operation-dashboard">
  4. <!-- 页面头部 -->
  5. <div class="page-header">
  6. <div class="header-left">
  7. <el-icon><Monitor /></el-icon>
  8. <span class="title">井盖设备智能终端运维看板</span>
  9. <el-tag type="danger" effect="dark">异常终端 {{ kpi.abnormal }}</el-tag>
  10. </div>
  11. <div class="header-right">
  12. <el-date-picker
  13. v-model="dateRange"
  14. type="daterange"
  15. range-separator="至"
  16. start-placeholder="开始日期"
  17. end-placeholder="结束日期"
  18. style="width: 260px"
  19. size="default"
  20. @change="loadAll"
  21. />
  22. <el-button type="primary" :icon="Refresh" @click="loadAll" :loading="loading">刷新</el-button>
  23. </div>
  24. </div>
  25. <!-- KPI 指标卡片 -->
  26. <div class="kpi-cards">
  27. <div class="kpi-card">
  28. <div class="kpi-value">{{ kpi.total }}</div>
  29. <div class="kpi-label">终端总数</div>
  30. </div>
  31. <div class="kpi-card warning">
  32. <div class="kpi-value">{{ kpi.abnormal }}</div>
  33. <div class="kpi-label">异常终端</div>
  34. <div class="kpi-trend">占比 {{ kpi.abnormalRate }}%</div>
  35. </div>
  36. <div class="kpi-card success">
  37. <div class="kpi-value">{{ kpi.solvedRate }}%</div>
  38. <div class="kpi-label">问题解决率</div>
  39. <div class="kpi-trend">已派发 {{ kpi.submittedCount }} / 异常 {{ kpi.abnormal }}</div>
  40. </div>
  41. <div class="kpi-card">
  42. <div class="kpi-value">{{ kpi.disposalRate }}%</div>
  43. <div class="kpi-label">案件处置率</div>
  44. <div class="kpi-trend">工单总数 {{ kpi.totalCase }}</div>
  45. </div>
  46. <div class="kpi-card">
  47. <div class="kpi-value">{{ kpi.delayRate }}%</div>
  48. <div class="kpi-label">延期率</div>
  49. <div class="kpi-trend" :class="{ 'trend-down': Number(kpi.delayRate) > 0 }">统计范围内</div>
  50. </div>
  51. <div class="kpi-card">
  52. <div class="kpi-value">{{ kpi.avgResponseTime }}</div>
  53. <div class="kpi-label">平均响应时长(小时)</div>
  54. </div>
  55. </div>
  56. <!-- 图表区域 -->
  57. <div class="charts-row">
  58. <div class="chart-card">
  59. <div class="chart-header">
  60. <span>异常终端数量趋势(月度)</span>
  61. </div>
  62. <div ref="trendChartRef" class="chart-container" v-loading="loading"></div>
  63. </div>
  64. <div class="chart-card">
  65. <div class="chart-header">
  66. <span>问题类型分布</span>
  67. </div>
  68. <div ref="problemTypeChartRef" class="chart-container" v-loading="loading"></div>
  69. </div>
  70. </div>
  71. <div class="charts-row">
  72. <div class="chart-card">
  73. <div class="chart-header">
  74. <span>处置率 vs 延期率趋势</span>
  75. </div>
  76. <div ref="disposalTrendChartRef" class="chart-container" v-loading="loading"></div>
  77. </div>
  78. <div class="chart-card">
  79. <div class="chart-header">
  80. <span>区域运维情况排行</span>
  81. </div>
  82. <div ref="regionRankChartRef" class="chart-container" v-loading="loading"></div>
  83. </div>
  84. </div>
  85. <!-- 异常终端列表 -->
  86. <div class="device-list-container">
  87. <div class="list-header">
  88. <span><el-icon><List /></el-icon> 异常终端列表(共 {{ filteredAbnormalDevices.length }} 台)</span>
  89. <div class="list-filters">
  90. <el-input
  91. v-model="searchKeyword"
  92. placeholder="搜索设备编号/名称/位置"
  93. clearable
  94. :prefix-icon="Search"
  95. style="width: 200px"
  96. size="small"
  97. />
  98. <el-select v-model="faultTypeFilter" placeholder="故障类型" clearable size="small" style="width: 130px">
  99. <el-option label="倾斜超标" value="tilt" />
  100. <el-option label="水位超限" value="waterLevel" />
  101. <el-option label="水浸报警" value="waterInfiltration" />
  102. <el-option label="通讯故障" value="offline" />
  103. <el-option label="电池低电量" value="battery" />
  104. <el-option label="信号弱" value="signal" />
  105. </el-select>
  106. <el-button type="primary" size="small" @click="currentPage = 1">查询</el-button>
  107. </div>
  108. </div>
  109. <el-table :data="paginatedAbnormalDevices" stripe border style="width: 100%" v-loading="loading">
  110. <el-table-column type="index" label="序号" width="55" align="center" />
  111. <el-table-column prop="code" label="设备编号" width="140" sortable />
  112. <el-table-column prop="name" label="设备名称" width="140" show-overflow-tooltip />
  113. <el-table-column prop="deviceTypeName" label="设备类型" width="100" />
  114. <el-table-column label="故障类型" width="110">
  115. <template #default="{ row }">
  116. <el-tag v-for="ft in row.faultTypes" :key="ft.type" :type="getFaultTagType(ft.type)" size="small" style="margin: 1px">{{ ft.name }}</el-tag>
  117. </template>
  118. </el-table-column>
  119. <el-table-column prop="faultDesc" label="故障描述" min-width="180" show-overflow-tooltip />
  120. <el-table-column prop="locationShort" label="所在位置" min-width="140" show-overflow-tooltip />
  121. <el-table-column prop="abnormalTime" label="故障时间" width="150" sortable />
  122. <el-table-column label="处理状态" width="100" align="center">
  123. <template #default="{ row }">
  124. <el-tag :type="row.workOrderSubmitted ? 'success' : 'danger'" size="small">
  125. {{ row.workOrderSubmitted ? '已派发' : '待派发' }}
  126. </el-tag>
  127. </template>
  128. </el-table-column>
  129. <el-table-column prop="elapsedHours" label="已耗时(h)" width="100" sortable align="center">
  130. <template #default="{ row }">
  131. <span :class="{ 'over-time': row.elapsedHours > 48 }">{{ row.elapsedHours }}</span>
  132. </template>
  133. </el-table-column>
  134. <el-table-column label="操作" width="100" fixed="right" align="center">
  135. <template #default="{ row }">
  136. <el-button link type="primary" size="small" @click="viewDetail(row)">详情</el-button>
  137. </template>
  138. </el-table-column>
  139. </el-table>
  140. <div class="pagination-wrapper">
  141. <el-pagination
  142. background
  143. layout="total, sizes, prev, pager, next"
  144. :total="filteredAbnormalDevices.length"
  145. v-model:current-page="currentPage"
  146. v-model:page-size="pageSize"
  147. :page-sizes="[10, 20, 50]"
  148. />
  149. </div>
  150. </div>
  151. <!-- 终端详情抽屉 -->
  152. <el-drawer v-model="detailDrawerVisible" :title="`终端详情 - ${currentDevice?.code}`" size="450px" direction="rtl">
  153. <div v-if="currentDevice" class="device-detail">
  154. <el-descriptions :column="1" border>
  155. <el-descriptions-item label="设备编号">{{ currentDevice.code }}</el-descriptions-item>
  156. <el-descriptions-item label="设备名称">{{ currentDevice.name }}</el-descriptions-item>
  157. <el-descriptions-item label="设备类型">{{ currentDevice.deviceTypeName }}</el-descriptions-item>
  158. <el-descriptions-item label="故障描述">{{ currentDevice.faultDesc }}</el-descriptions-item>
  159. <el-descriptions-item label="故障时间">{{ currentDevice.abnormalTime }}</el-descriptions-item>
  160. <el-descriptions-item label="详细地址">{{ currentDevice.location }}</el-descriptions-item>
  161. <el-descriptions-item label="处理状态">
  162. <el-tag :type="currentDevice.workOrderSubmitted ? 'success' : 'danger'">
  163. {{ currentDevice.workOrderSubmitted ? '已派发' : '待派发' }}
  164. </el-tag>
  165. </el-descriptions-item>
  166. </el-descriptions>
  167. <!-- 监测数据 -->
  168. <div class="sub-title">监测数据</div>
  169. <div class="monitor-grid">
  170. <div class="monitor-item" :class="{ alert: currentDevice.status.tilt }">
  171. <div class="m-label">倾斜角度</div>
  172. <div class="m-value">{{ currentDevice.tiltAngle }}°</div>
  173. <div class="m-extra">阈值: {{ currentDevice.angleThreshold }}°</div>
  174. </div>
  175. <div class="monitor-item" :class="{ alert: currentDevice.status.waterInfiltration }">
  176. <div class="m-label">水浸</div>
  177. <div class="m-value">{{ currentDevice.status.waterInfiltration ? '报警' : '正常' }}</div>
  178. </div>
  179. <div class="monitor-item" :class="{ alert: currentDevice.status.waterLevel }">
  180. <div class="m-label">水位</div>
  181. <div class="m-value">{{ currentDevice.status.waterLevel ? '超标' : '正常' }}</div>
  182. </div>
  183. <div class="monitor-item" :class="{ alert: currentDevice.status.lowBattery }">
  184. <div class="m-label">电量</div>
  185. <div class="m-value">{{ currentDevice.battery }}%</div>
  186. </div>
  187. <div class="monitor-item" :class="{ alert: currentDevice.status.lowSignal }">
  188. <div class="m-label">信号</div>
  189. <div class="m-value">{{ currentDevice.signal }}%</div>
  190. </div>
  191. <div class="monitor-item">
  192. <div class="m-label">温度</div>
  193. <div class="m-value">{{ currentDevice.temperature || '—' }}°C</div>
  194. </div>
  195. </div>
  196. </div>
  197. </el-drawer>
  198. </div>
  199. </template>
  200. <script setup>
  201. import { ref, reactive, computed, onMounted, onBeforeUnmount, nextTick } from 'vue'
  202. import { Monitor, Refresh, List, Search } from '@element-plus/icons-vue'
  203. import { ElMessage } from 'element-plus'
  204. import * as echarts from 'echarts'
  205. import {
  206. getManholeMaintenanceDashboard
  207. } from '@/api/pipeNetwork/basic'
  208. // ==================== 常量 ====================
  209. const TYPE_MAP = { '5': '供水窨井', '9': '排水窨井', '10': '燃气窨井' }
  210. const LOW_BATTERY_THRESHOLD = 15
  211. const LOW_SIGNAL_THRESHOLD = 20
  212. // ==================== 状态 ====================
  213. const loading = ref(false)
  214. const dateRange = ref([])
  215. const searchKeyword = ref('')
  216. const faultTypeFilter = ref('')
  217. const currentPage = ref(1)
  218. const pageSize = ref(10)
  219. const detailDrawerVisible = ref(false)
  220. const currentDevice = ref(null)
  221. const kpi = reactive({
  222. total: 0, abnormal: 0, abnormalRate: '0.0',
  223. solvedRate: 0, submittedCount: 0, totalCase: 0,
  224. disposalRate: 0, delayRate: 0,
  225. avgResponseTime: '—'
  226. })
  227. const abnormalDevices = ref([])
  228. // 图表引用
  229. const trendChartRef = ref(null)
  230. const problemTypeChartRef = ref(null)
  231. const disposalTrendChartRef = ref(null)
  232. const regionRankChartRef = ref(null)
  233. let trendChart = null, problemTypeChart = null, disposalTrendChart = null, regionRankChart = null
  234. // ==================== 日期工具 ====================
  235. function formatDate(date) {
  236. const y = date.getFullYear()
  237. const m = String(date.getMonth() + 1).padStart(2, '0')
  238. const d = String(date.getDate()).padStart(2, '0')
  239. return `${y}-${m}-${d}`
  240. }
  241. // ==================== 设备数据规范化 ====================
  242. function normalizeDevice(raw) {
  243. const md = raw?.manholeData || {}
  244. const st = raw?.equipmentStatus || {}
  245. const typeId = raw?.equipmentTypeId || ''
  246. const tiltAngle = parseFloat(md.tiltAngle) || 0
  247. const angleThreshold = parseFloat(md.angleAlarmThreshold) || 15
  248. const battery = parseFloat(md.batteryLevel) || 0
  249. const signal = parseFloat(md.signalStrength) || 0
  250. const temperature = md.temperatureValue ? parseFloat(md.temperatureValue).toFixed(1) : null
  251. const isOnline = Number(st.onlineStatus) === 1
  252. const tilt = isOnline && tiltAngle > angleThreshold
  253. const waterInfiltration = isOnline && `${md.waterInfiltrationAlarmStatus}` === '1'
  254. const waterLevel = isOnline && `${md.waterLevelAlarmStatus}` === '1'
  255. const lowBattery = battery > 0 && battery <= LOW_BATTERY_THRESHOLD
  256. const lowSignal = signal > 0 && signal <= LOW_SIGNAL_THRESHOLD
  257. const offline = !isOnline
  258. const faultTypes = []
  259. if (tilt) faultTypes.push({ type: 'tilt', name: '倾斜超标' })
  260. if (waterLevel) faultTypes.push({ type: 'waterLevel', name: '水位超限' })
  261. if (waterInfiltration) faultTypes.push({ type: 'waterInfiltration', name: '水浸报警' })
  262. if (offline) faultTypes.push({ type: 'offline', name: '通讯故障' })
  263. if (lowBattery) faultTypes.push({ type: 'battery', name: '电池低电量' })
  264. if (lowSignal) faultTypes.push({ type: 'signal', name: '信号弱' })
  265. const hasAlert = faultTypes.length > 0
  266. const parts = []
  267. if (tilt) parts.push(`倾斜角度${tiltAngle}°超过阈值${angleThreshold}°`)
  268. if (waterInfiltration) parts.push('水浸报警')
  269. if (waterLevel) parts.push('水位超标报警')
  270. if (lowBattery) parts.push(`电量仅剩${battery}%`)
  271. if (lowSignal) parts.push(`信号强度${signal}%`)
  272. if (offline) parts.push('设备离线')
  273. let elapsedHours = 0
  274. const faultTime = md.uploadTime || md.createTime
  275. if (faultTime) {
  276. const diff = Date.now() - new Date(faultTime).getTime()
  277. elapsedHours = Math.max(0, Math.round(diff / (1000 * 60 * 60) * 10) / 10)
  278. }
  279. return {
  280. id: raw?.equipmentId || raw?.equipmentCode || '',
  281. code: raw?.equipmentCode || raw?.equipmentId || '',
  282. equipmentId: raw?.equipmentId || '',
  283. name: raw?.equipmentName || raw?.equipmentCode || '未命名设备',
  284. typeId,
  285. deviceTypeName: TYPE_MAP[typeId] || raw?.equipmentTypeName || '未知类型',
  286. location: raw?.equipmentLocation || '暂无位置信息',
  287. locationShort: (raw?.equipmentLocation || '暂无位置').slice(0, 20),
  288. district: raw?.district || '',
  289. battery, signal, temperature, tiltAngle, angleThreshold,
  290. status: { tilt, waterInfiltration, waterLevel, lowBattery, lowSignal, offline },
  291. faultTypes, hasAlert,
  292. faultDesc: parts.join(';') || '设备状态正常',
  293. abnormalTime: faultTime || '',
  294. elapsedHours,
  295. workOrderSubmitted: !!raw?.workOrderSubmitted
  296. }
  297. }
  298. // ==================== 数据加载 ====================
  299. async function loadAll() {
  300. loading.value = true
  301. try {
  302. const today = new Date()
  303. const sixMonthsAgo = new Date(today.getFullYear(), today.getMonth() - 5, 1)
  304. // 日期范围:默认近6个月,后端统一计算全部统计数据
  305. const startDate = dateRange.value?.[0] ? formatDate(new Date(dateRange.value[0])) : formatDate(sixMonthsAgo)
  306. const endDate = dateRange.value?.[1] ? formatDate(new Date(dateRange.value[1])) : formatDate(today)
  307. const { data } = await getManholeMaintenanceDashboard({ trendType: 2, startDate, endDate })
  308. // 1. KPI 指标
  309. const stat = data?.kpi || {}
  310. kpi.total = stat.totalTerminal || 0
  311. kpi.abnormal = stat.abnormalTerminal || 0
  312. kpi.abnormalRate = stat.abnormalRate ?? '0.0'
  313. kpi.submittedCount = stat.submittedCount || 0
  314. kpi.solvedRate = stat.solvedRate ?? 0
  315. kpi.totalCase = stat.totalCase || 0
  316. kpi.disposalRate = stat.disposalRate ?? 0
  317. kpi.delayRate = stat.delayRate ?? 0
  318. kpi.avgResponseTime = stat.avgDisposalTime ?? '—'
  319. // 2. 异常终端列表
  320. abnormalDevices.value = (data?.abnormalDevices || []).map(normalizeDevice)
  321. // 3. 渲染图表
  322. await nextTick()
  323. renderTrendChart(data?.abnormalTerminalTrend || [])
  324. renderProblemTypeChart(data?.problemTypeDistribution || [])
  325. renderDisposalTrendChart(data?.caseRateTrend || [])
  326. renderRegionRankChart(data?.regionRank || [])
  327. } catch (error) {
  328. const msg = error?.message || error?.msg || '未知错误'
  329. ElMessage.error('统计数据加载失败: ' + msg)
  330. } finally {
  331. loading.value = false
  332. }
  333. }
  334. // ==================== ECharts 渲染 ====================
  335. function renderTrendChart(trendData) {
  336. if (!trendChartRef.value) return
  337. if (!trendChart) trendChart = echarts.init(trendChartRef.value)
  338. const labels = (trendData || []).map(d => d.trendLabel || '')
  339. const abnormalValues = (trendData || []).map(d => d.abnormalTerminal || 0)
  340. const normalValues = (trendData || []).map(d => {
  341. const abnormal = d.abnormalTerminal || 0
  342. const total = d.totalTerminal || abnormal
  343. return Math.max(0, total - abnormal)
  344. })
  345. trendChart.setOption({
  346. tooltip: { trigger: 'axis' },
  347. legend: { data: ['异常终端数', '正常终端数'] },
  348. grid: { left: '8%', right: '5%', bottom: '12%', top: '15%' },
  349. xAxis: { type: 'category', data: labels.length ? labels : ['暂无数据'] },
  350. yAxis: { type: 'value', name: '数量' },
  351. series: [
  352. { name: '异常终端数', type: 'line', data: abnormalValues, smooth: true, lineStyle: { color: '#f56c6c' }, areaStyle: { opacity: 0.1 }, itemStyle: { color: '#f56c6c' } },
  353. { name: '正常终端数', type: 'line', data: normalValues, smooth: true, lineStyle: { color: '#67c23a' }, areaStyle: { opacity: 0.1 }, itemStyle: { color: '#67c23a' } }
  354. ]
  355. })
  356. }
  357. function renderProblemTypeChart(distribution) {
  358. if (!problemTypeChartRef.value) return
  359. if (!problemTypeChart) problemTypeChart = echarts.init(problemTypeChartRef.value)
  360. const colors = ['#f56c6c', '#e6a23c', '#409eff', '#909399', '#67c23a', '#9966ff']
  361. const data = (distribution || []).map((item, i) => ({
  362. name: item.name || '其他',
  363. value: item.value || 0,
  364. itemStyle: { color: colors[i % colors.length] }
  365. }))
  366. problemTypeChart.setOption({
  367. tooltip: { trigger: 'item', formatter: '{b}: {c} ({d}%)' },
  368. legend: { orient: 'vertical', left: 'left', textStyle: { fontSize: 11 } },
  369. series: [{
  370. type: 'pie', radius: '55%', center: ['55%', '50%'],
  371. data: data.length ? data : [{ name: '暂无数据', value: 0 }],
  372. label: { show: true, formatter: '{b}: {d}%' }
  373. }]
  374. })
  375. }
  376. function renderDisposalTrendChart(trendData) {
  377. if (!disposalTrendChartRef.value) return
  378. if (!disposalTrendChart) disposalTrendChart = echarts.init(disposalTrendChartRef.value)
  379. const labels = (trendData || []).map(d => d.trendLabel || '')
  380. const disposalRates = (trendData || []).map(d => Number(d.caseDisposalRate) || 0)
  381. const delayRates = (trendData || []).map(d => Number(d.caseDelayRate) || 0)
  382. disposalTrendChart.setOption({
  383. tooltip: { trigger: 'axis' },
  384. legend: { data: ['处置率(%)', '延期率(%)'] },
  385. grid: { left: '8%', right: '5%', bottom: '12%', top: '15%' },
  386. xAxis: { type: 'category', data: labels.length ? labels : ['暂无数据'] },
  387. yAxis: { type: 'value', name: '百分比(%)', max: 100 },
  388. series: [
  389. { name: '处置率(%)', type: 'line', data: disposalRates, smooth: true, itemStyle: { color: '#67c23a' } },
  390. { name: '延期率(%)', type: 'line', data: delayRates, smooth: true, itemStyle: { color: '#e6a23c' } }
  391. ]
  392. })
  393. }
  394. function renderRegionRankChart(regionRank) {
  395. if (!regionRankChartRef.value) return
  396. if (!regionRankChart) regionRankChart = echarts.init(regionRankChartRef.value)
  397. const entries = (regionRank || []).slice(0, 8)
  398. regionRankChart.setOption({
  399. tooltip: {
  400. trigger: 'axis', axisPointer: { type: 'shadow' },
  401. formatter: (params) => {
  402. const p = params[0]
  403. const item = entries[p.dataIndex]
  404. return item ? `${p.name}: 异常 ${p.value} / 总数 ${item.total}` : p.name
  405. }
  406. },
  407. grid: { left: '15%', right: '8%', bottom: '8%', top: '8%' },
  408. xAxis: { type: 'value', name: '异常数量' },
  409. yAxis: { type: 'category', data: entries.map(e => e.name), axisLabel: { fontSize: 11 } },
  410. series: [{
  411. name: '异常数量', type: 'bar',
  412. data: entries.map(e => e.abnormal),
  413. itemStyle: { color: '#409eff', borderRadius: [0, 4, 4, 0] },
  414. label: { show: true, position: 'right', fontSize: 11 }
  415. }]
  416. })
  417. }
  418. // ==================== 计算属性 ====================
  419. const filteredAbnormalDevices = computed(() => {
  420. let list = abnormalDevices.value
  421. if (searchKeyword.value) {
  422. const kw = searchKeyword.value.toLowerCase()
  423. list = list.filter(d =>
  424. d.code.toLowerCase().includes(kw) ||
  425. d.name.toLowerCase().includes(kw) ||
  426. d.location.toLowerCase().includes(kw)
  427. )
  428. }
  429. if (faultTypeFilter.value) {
  430. list = list.filter(d => d.faultTypes.some(ft => ft.type === faultTypeFilter.value))
  431. }
  432. return list
  433. })
  434. const paginatedAbnormalDevices = computed(() => {
  435. const start = (currentPage.value - 1) * pageSize.value
  436. return filteredAbnormalDevices.value.slice(start, start + pageSize.value)
  437. })
  438. // ==================== 辅助函数 ====================
  439. const getFaultTagType = (type) => {
  440. const map = { tilt: 'danger', waterLevel: 'danger', waterInfiltration: 'danger', offline: 'info', battery: 'warning', signal: 'warning' }
  441. return map[type] || 'info'
  442. }
  443. const viewDetail = (device) => {
  444. currentDevice.value = device
  445. detailDrawerVisible.value = true
  446. }
  447. // ==================== 生命周期 ====================
  448. const handleResize = () => {
  449. trendChart && trendChart.resize()
  450. problemTypeChart && problemTypeChart.resize()
  451. disposalTrendChart && disposalTrendChart.resize()
  452. regionRankChart && regionRankChart.resize()
  453. }
  454. onMounted(async () => {
  455. await nextTick()
  456. window.addEventListener('resize', handleResize)
  457. loadAll()
  458. })
  459. onBeforeUnmount(() => {
  460. window.removeEventListener('resize', handleResize)
  461. trendChart && trendChart.dispose()
  462. problemTypeChart && problemTypeChart.dispose()
  463. disposalTrendChart && disposalTrendChart.dispose()
  464. regionRankChart && regionRankChart.dispose()
  465. })
  466. </script>
  467. <style scoped>
  468. .terminal-operation-dashboard {
  469. background: #f0f2f6;
  470. min-height: 100vh;
  471. padding: 20px;
  472. }
  473. .page-header {
  474. background: white;
  475. border-radius: 16px;
  476. padding: 16px 24px;
  477. margin-bottom: 20px;
  478. display: flex;
  479. justify-content: space-between;
  480. align-items: center;
  481. flex-wrap: wrap;
  482. gap: 12px;
  483. }
  484. .header-left { display: flex; align-items: center; gap: 12px; }
  485. .header-left .title { font-size: 20px; font-weight: 600; }
  486. .header-right { display: flex; gap: 12px; flex-wrap: wrap; }
  487. .kpi-cards {
  488. display: grid;
  489. grid-template-columns: repeat(6, 1fr);
  490. gap: 16px;
  491. margin-bottom: 20px;
  492. }
  493. .kpi-card {
  494. background: white;
  495. border-radius: 16px;
  496. padding: 16px;
  497. text-align: center;
  498. box-shadow: 0 1px 4px rgba(0,0,0,0.05);
  499. }
  500. .kpi-value { font-size: 32px; font-weight: bold; color: #1f2d3d; }
  501. .kpi-label { font-size: 13px; color: #909399; margin-top: 6px; }
  502. .kpi-trend { font-size: 11px; color: #67c23a; margin-top: 4px; }
  503. .kpi-trend.trend-down { color: #f56c6c; }
  504. .kpi-card.warning .kpi-value { color: #e6a23c; }
  505. .kpi-card.success .kpi-value { color: #67c23a; }
  506. .charts-row {
  507. display: grid;
  508. grid-template-columns: 1fr 1fr;
  509. gap: 20px;
  510. margin-bottom: 20px;
  511. }
  512. .chart-card {
  513. background: white;
  514. border-radius: 16px;
  515. padding: 16px;
  516. }
  517. .chart-header {
  518. display: flex;
  519. justify-content: space-between;
  520. align-items: center;
  521. margin-bottom: 16px;
  522. font-weight: 600;
  523. }
  524. .chart-container { height: 280px; }
  525. .device-list-container {
  526. background: white;
  527. border-radius: 16px;
  528. padding: 16px;
  529. }
  530. .list-header {
  531. display: flex;
  532. justify-content: space-between;
  533. align-items: center;
  534. margin-bottom: 16px;
  535. font-weight: 600;
  536. }
  537. .list-filters { display: flex; gap: 12px; align-items: center; }
  538. .pagination-wrapper {
  539. display: flex;
  540. justify-content: flex-end;
  541. margin-top: 16px;
  542. padding-top: 12px;
  543. border-top: 1px solid #e9ecef;
  544. }
  545. .device-detail { padding: 8px; }
  546. .sub-title { font-weight: 600; margin: 16px 0 12px 0; }
  547. .over-time { color: #f56c6c; font-weight: 600; }
  548. .monitor-grid {
  549. display: grid;
  550. grid-template-columns: repeat(3, 1fr);
  551. gap: 10px;
  552. }
  553. .monitor-item {
  554. background: #f5f7fa;
  555. border-radius: 8px;
  556. padding: 10px;
  557. text-align: center;
  558. border: 1px solid #e9ecef;
  559. }
  560. .monitor-item.alert {
  561. background: #fef0f0;
  562. border-color: #f56c6c;
  563. }
  564. .m-label { font-size: 12px; color: #909399; }
  565. .m-value { font-size: 16px; font-weight: 600; color: #303133; margin: 4px 0; }
  566. .monitor-item.alert .m-value { color: #f56c6c; }
  567. .m-extra { font-size: 11px; color: #aaa; }
  568. @media (max-width: 1200px) {
  569. .kpi-cards { grid-template-columns: repeat(3, 1fr); }
  570. .charts-row { grid-template-columns: 1fr; }
  571. }
  572. </style>