EquipmentAbnormal.vue 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773
  1. <template>
  2. <div class="equipment-abnormal">
  3. <!-- 左侧面板 -->
  4. <div class="left-panel">
  5. <!-- 搜索 -->
  6. <div class="panel-card search-card">
  7. <div class="panel-title"><el-icon><Search /></el-icon> 快速查询</div>
  8. <el-input v-model="searchKey" placeholder="设备名称/编号/地址" clearable prefix-icon="Search" size="default" />
  9. <el-select v-model="levelFilter" placeholder="异常等级" clearable style="width:100%;margin-top:10px">
  10. <el-option label="严重" value="严重" />
  11. <el-option label="一般" value="一般" />
  12. <el-option label="轻微" value="轻微" />
  13. </el-select>
  14. <el-select v-model="typeFilter" placeholder="设备类型" clearable style="width:100%;margin-top:10px">
  15. <el-option label="阀门" value="阀门" />
  16. <el-option label="调压器" value="调压器" />
  17. <el-option label="流量计" value="流量计" />
  18. <el-option label="窨井盖" value="窨井盖" />
  19. <el-option label="终端节点" value="终端节点" />
  20. <el-option label="检查井" value="检查井" />
  21. </el-select>
  22. <el-select v-model="statusFilter" placeholder="处理状态" clearable style="width:100%;margin-top:10px">
  23. <el-option label="待处理" value="待处理" />
  24. <el-option label="处理中" value="处理中" />
  25. <el-option label="已修复" value="已修复" />
  26. </el-select>
  27. <div style="display:flex;gap:8px;margin-top:10px">
  28. <el-button type="primary" size="small" @click="handleQuery" style="flex:1">查询</el-button>
  29. <el-button size="small" @click="handleReset" style="flex:1">重置</el-button>
  30. </div>
  31. </div>
  32. <!-- 异常等级分布 -->
  33. <div class="panel-card">
  34. <div class="panel-title"><el-icon><PieChart /></el-icon> 异常等级分布</div>
  35. <div class="level-list">
  36. <div class="level-item critical">
  37. <span class="level-dot"></span>
  38. <span class="level-label">严重</span>
  39. <span class="level-count">{{ stats.critical }}</span>
  40. </div>
  41. <div class="level-item warning">
  42. <span class="level-dot"></span>
  43. <span class="level-label">一般</span>
  44. <span class="level-count">{{ stats.normal }}</span>
  45. </div>
  46. <div class="level-item minor">
  47. <span class="level-dot"></span>
  48. <span class="level-label">轻微</span>
  49. <span class="level-count">{{ stats.minor }}</span>
  50. </div>
  51. </div>
  52. </div>
  53. <!-- 设备类型统计 -->
  54. <div class="panel-card">
  55. <div class="panel-title"><el-icon><Histogram /></el-icon> 设备类型统计</div>
  56. <div class="type-list">
  57. <div class="type-item" v-for="item in deviceTypeStats" :key="item.name">
  58. <span class="type-name">{{ item.name }}</span>
  59. <span class="type-bar-bg"><span class="type-bar" :style="{width: (stats.total > 0 ? item.count/stats.total*100 : 0)+'%', background: item.color}"></span></span>
  60. <span class="type-count">{{ item.count }}</span>
  61. </div>
  62. </div>
  63. </div>
  64. </div>
  65. <!-- 右侧内容区 -->
  66. <div class="right-panel">
  67. <!-- 统计卡片 -->
  68. <div class="stats-row">
  69. <div class="stat-card stat-total">
  70. <div class="stat-icon"><el-icon size="22"><WarningFilled /></el-icon></div>
  71. <div class="stat-info">
  72. <div class="stat-label">异常设备总数</div>
  73. <div class="stat-value">{{ stats.total }} <small>台</small></div>
  74. </div>
  75. </div>
  76. <div class="stat-card stat-today">
  77. <div class="stat-icon"><el-icon size="22"><Clock /></el-icon></div>
  78. <div class="stat-info">
  79. <div class="stat-label">今日新增</div>
  80. <div class="stat-value">{{ stats.today }} <small>台</small></div>
  81. </div>
  82. </div>
  83. <div class="stat-card stat-rate">
  84. <div class="stat-icon"><el-icon size="22"><TrendCharts /></el-icon></div>
  85. <div class="stat-info">
  86. <div class="stat-label">异常占比</div>
  87. <div class="stat-value">{{ stats.rate }} <small>%</small></div>
  88. </div>
  89. </div>
  90. <div class="stat-card stat-processed">
  91. <div class="stat-icon"><el-icon size="22"><CircleCheckFilled /></el-icon></div>
  92. <div class="stat-info">
  93. <div class="stat-label">已修复</div>
  94. <div class="stat-value">{{ stats.processed }} <small>台</small></div>
  95. </div>
  96. </div>
  97. <div class="stat-card stat-pending">
  98. <div class="stat-icon"><el-icon size="22"><Warning /></el-icon></div>
  99. <div class="stat-info">
  100. <div class="stat-label">待处理</div>
  101. <div class="stat-value">{{ stats.pending }} <small>台</small></div>
  102. </div>
  103. </div>
  104. <div class="stat-card stat-process-rate">
  105. <div class="stat-icon"><el-icon size="22"><DataAnalysis /></el-icon></div>
  106. <div class="stat-info">
  107. <div class="stat-label">修复率</div>
  108. <div class="stat-value">{{ stats.processRate }} <small>%</small></div>
  109. </div>
  110. </div>
  111. </div>
  112. <!-- 图表区域 - 上方: 趋势图(全宽) -->
  113. <div class="chart-card chart-trend">
  114. <div class="chart-header">
  115. <span class="chart-title">设备异常趋势图</span>
  116. <span class="chart-subtitle">近15天运行数据</span>
  117. <div class="chart-legend-custom">
  118. <span class="legend-tag tag-total">异常总数</span>
  119. <span class="legend-tag tag-new">新增异常</span>
  120. <span class="legend-tag tag-fixed">已修复</span>
  121. </div>
  122. </div>
  123. <div id="trendChart" class="chart-body chart-body-large" style="height:340px"></div>
  124. </div>
  125. <!-- 图表区域 - 下方: 两个图表并排 -->
  126. <div class="charts-row">
  127. <div class="chart-card chart-half">
  128. <div class="chart-header">
  129. <span class="chart-title">异常类型分布</span>
  130. </div>
  131. <div id="typeChart" class="chart-body" style="height:290px"></div>
  132. </div>
  133. <div class="chart-card chart-half">
  134. <div class="chart-header">
  135. <span class="chart-title">设备异常等级与处理状态</span>
  136. </div>
  137. <div id="levelChart" class="chart-body" style="height:290px"></div>
  138. </div>
  139. </div>
  140. <!-- 异常设备列表 -->
  141. <div class="table-card">
  142. <div class="table-header">
  143. <span class="table-title">异常设备列表</span>
  144. <span class="table-count">共 {{ tableTotal }} 条记录</span>
  145. </div>
  146. <el-table :data="paginatedData" border stripe v-loading="loading" max-height="460" size="small"
  147. @sort-change="handleSortChange" :default-sort="{ prop: 'alarmTime', order: 'descending' }">
  148. <el-table-column prop="id" label="序号" width="56" align="center" sortable="custom" />
  149. <el-table-column prop="deviceCode" label="设备编号" min-width="140" show-overflow-tooltip />
  150. <el-table-column prop="deviceName" label="设备名称" min-width="150" show-overflow-tooltip />
  151. <el-table-column prop="deviceType" label="设备类型" width="100" />
  152. <el-table-column prop="level" label="异常等级" width="100" align="center" sortable="custom">
  153. <template #default="{ row }">
  154. <el-tag :type="levelTagType(row.level)" size="small">{{ row.level }}</el-tag>
  155. </template>
  156. </el-table-column>
  157. <el-table-column prop="alarmDesc" label="异常描述" min-width="220" show-overflow-tooltip />
  158. <el-table-column prop="address" label="安装地址" min-width="200" show-overflow-tooltip />
  159. <el-table-column prop="alarmTime" label="异常时间" width="168" sortable="custom" />
  160. <el-table-column prop="processStatus" label="处理状态" width="100" align="center" sortable="custom">
  161. <template #default="{ row }">
  162. <el-tag :type="row.processStatus === '已修复' ? 'success' : row.processStatus === '处理中' ? 'warning' : 'danger'" size="small">
  163. {{ row.processStatus }}
  164. </el-tag>
  165. </template>
  166. </el-table-column>
  167. <el-table-column label="操作" width="150" fixed="right" align="center">
  168. <template #default="{ row }">
  169. <el-button type="primary" link size="small" @click="handleDetail(row)">详情</el-button>
  170. <el-button v-if="row.processStatus === '待处理'" type="warning" link size="small" @click="handleProcess(row)">处理</el-button>
  171. <el-button v-if="row.processStatus === '处理中'" type="success" link size="small" @click="handleComplete(row)">完成</el-button>
  172. </template>
  173. </el-table-column>
  174. </el-table>
  175. <div class="table-footer">
  176. <el-pagination
  177. v-model:current-page="currentPage"
  178. v-model:page-size="pageSize"
  179. :total="tableTotal"
  180. layout="total, prev, pager, next"
  181. :page-sizes="[10, 20]"
  182. small
  183. background
  184. />
  185. </div>
  186. </div>
  187. </div>
  188. </div>
  189. </template>
  190. <script setup>
  191. import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
  192. import * as echarts from 'echarts'
  193. import { Clock, CircleCheckFilled, DataAnalysis, Histogram, PieChart, Search, TrendCharts, Warning, WarningFilled } from '@element-plus/icons-vue'
  194. import { ElMessage, ElMessageBox } from 'element-plus'
  195. import { getEquipmentAbnormalList, getEquipmentAbnormalStats } from '@/api/pipeNetwork/gasMonitor'
  196. const loading = ref(false)
  197. const searchKey = ref('')
  198. const levelFilter = ref('')
  199. const typeFilter = ref('')
  200. const statusFilter = ref('')
  201. const currentPage = ref(1)
  202. const pageSize = ref(10)
  203. const sortProp = ref('alarmTime')
  204. const sortOrder = ref('descending')
  205. let trendChart = null
  206. let typeChart = null
  207. let levelChart = null
  208. // ==================== 统计数据(从API加载) ====================
  209. const stats = ref({
  210. total: 0, today: 0, rate: 0, processed: 0, pending: 0,
  211. processRate: 0, critical: 0, normal: 0, minor: 0
  212. })
  213. const deviceTypeStats = ref([])
  214. async function loadStats() {
  215. try {
  216. const res = await getEquipmentAbnormalStats()
  217. if (res.data) {
  218. const d = res.data
  219. stats.value = {
  220. total: d.total || 0, today: d.today || 0, rate: d.rate || 0,
  221. processed: d.processed || 0, pending: d.pending || 0,
  222. processRate: d.processRate || 0, critical: d.critical || 0,
  223. normal: d.normal || 0, minor: d.minor || 0
  224. }
  225. // 设备类型统计
  226. const colors = ['#e63946', '#f77f00', '#2d6a4f', '#7f4f24', '#6c757d', '#b08968']
  227. const typeMap = d.deviceTypeStats || {}
  228. let idx = 0
  229. deviceTypeStats.value = Object.entries(typeMap).map(([name, count]) => ({
  230. name, count, color: colors[idx++ % colors.length]
  231. }))
  232. // 图表数据
  233. chartData.value = {
  234. trend: d.trend || { dates: [], totalData: [], newData: [], fixedData: [] },
  235. typeDistribution: d.typeDistribution || { categories: [], values: [] },
  236. levelStatus: d.levelStatus || { levels: [], pending: [], processing: [], fixed: [] }
  237. }
  238. }
  239. } catch (e) { console.error(e) }
  240. }
  241. // ==================== 图表数据(从API动态加载) ====================
  242. const chartData = ref({
  243. trend: { dates: [], totalData: [], newData: [], fixedData: [] },
  244. typeDistribution: { categories: [], values: [] },
  245. levelStatus: { levels: [], pending: [], processing: [], fixed: [] }
  246. })
  247. // ==================== 表格数据(从API加载) ====================
  248. const tableData = ref([])
  249. const tableTotal = ref(0)
  250. async function loadList() {
  251. loading.value = true
  252. try {
  253. const res = await getEquipmentAbnormalList({
  254. pageNum: currentPage.value,
  255. pageSize: pageSize.value,
  256. searchKey: searchKey.value || undefined,
  257. level: levelFilter.value || undefined,
  258. type: typeFilter.value || undefined,
  259. status: statusFilter.value || undefined
  260. })
  261. if (res.data) {
  262. tableData.value = (res.data.rows || []).map((r, i) => ({
  263. ...r,
  264. id: String((currentPage.value - 1) * pageSize.value + i + 1),
  265. level: r.levelText || r.faultLevel,
  266. processStatus: r.processStatus || '待处理',
  267. alarmTime: r.alarmTime || '',
  268. alarmDesc: r.alarmDesc || '',
  269. deviceCode: r.deviceCode || '',
  270. deviceName: r.deviceName || '',
  271. deviceType: r.deviceType || '',
  272. address: r.address || ''
  273. }))
  274. tableTotal.value = res.data.total || 0
  275. }
  276. } catch (e) { console.error(e) } finally { loading.value = false }
  277. }
  278. const paginatedData = computed(() => {
  279. const list = [...tableData.value]
  280. if (sortProp.value) {
  281. list.sort((a, b) => {
  282. const va = a[sortProp.value] || ''
  283. const vb = b[sortProp.value] || ''
  284. const cmp = typeof va === 'string' ? va.localeCompare(vb) : va - vb
  285. return sortOrder.value === 'descending' ? -cmp : cmp
  286. })
  287. }
  288. return list
  289. })
  290. // 排序切换
  291. function handleSortChange({ prop, order }) {
  292. sortProp.value = prop
  293. sortOrder.value = order || 'descending'
  294. }
  295. // 分页切换时重载
  296. watch(currentPage, () => loadList())
  297. watch(pageSize, () => { currentPage.value = 1; loadList() })
  298. // ==================== 工具函数 ====================
  299. function levelTagType(level) {
  300. if (level === '严重') return 'danger'
  301. if (level === '一般') return 'warning'
  302. return 'info'
  303. }
  304. function handleQuery() {
  305. currentPage.value = 1
  306. loadList()
  307. }
  308. function handleReset() {
  309. searchKey.value = ''
  310. levelFilter.value = ''
  311. typeFilter.value = ''
  312. statusFilter.value = ''
  313. currentPage.value = 1
  314. loadList()
  315. }
  316. function handleDetail(row) {
  317. ElMessageBox.alert(
  318. `<div style="line-height:2.2;font-size:14px;">
  319. <p><b>设备编号:</b>${row.deviceCode}</p>
  320. <p><b>设备名称:</b>${row.deviceName}</p>
  321. <p><b>设备类型:</b>${row.deviceType}</p>
  322. <p><b>异常等级:</b><span style="color:${row.level==='严重'?'#e63946':row.level==='一般'?'#f77f00':'#909399'}">${row.level}</span></p>
  323. <p><b>异常描述:</b>${row.alarmDesc}</p>
  324. <p><b>安装地址:</b>${row.address}</p>
  325. <p><b>异常时间:</b>${row.alarmTime}</p>
  326. <p><b>处理状态:</b>${row.processStatus}</p>
  327. </div>`,
  328. '设备异常详情',
  329. { dangerouslyUseHTMLString: true, confirmButtonText: '关闭', customClass: 'detail-dialog' }
  330. )
  331. }
  332. function handleProcess(row) {
  333. ElMessageBox.confirm(`确认开始处理设备「${row.deviceName}」的异常?系统将自动派发工单至运维班组。`, '处理确认', {
  334. confirmButtonText: '确认处理',
  335. cancelButtonText: '取消',
  336. type: 'warning'
  337. }).then(() => {
  338. row.processStatus = '处理中'
  339. ElMessage.success(`已派发工单,设备「${row.deviceName}」处理中`)
  340. }).catch(() => {})
  341. }
  342. function handleComplete(row) {
  343. ElMessageBox.confirm(`确认设备「${row.deviceName}」的异常已修复完成?`, '修复确认', {
  344. confirmButtonText: '确认修复',
  345. cancelButtonText: '取消',
  346. type: 'success'
  347. }).then(() => {
  348. row.processStatus = '已修复'
  349. ElMessage.success(`设备「${row.deviceName}」异常已修复`)
  350. }).catch(() => {})
  351. }
  352. // ==================== ECharts ====================
  353. function initTrendChart() {
  354. const dom = document.getElementById('trendChart')
  355. if (!dom) return
  356. if (trendChart) trendChart.dispose()
  357. trendChart = echarts.init(dom)
  358. const t = chartData.value.trend
  359. trendChart.setOption({
  360. tooltip: {
  361. trigger: 'axis',
  362. backgroundColor: 'rgba(255,255,255,0.96)',
  363. borderColor: '#e0e4ea',
  364. textStyle: { color: '#2c3e50', fontSize: 13 }
  365. },
  366. legend: {
  367. data: ['异常总数', '新增异常', '已修复'],
  368. top: 0,
  369. textStyle: { color: '#606a78', fontSize: 12 }
  370. },
  371. grid: { left: 52, right: 44, top: 44, bottom: 32 },
  372. xAxis: {
  373. type: 'category',
  374. data: t.dates || [],
  375. boundaryGap: false,
  376. axisLabel: { color: '#9098a6', fontSize: 12 },
  377. axisLine: { lineStyle: { color: '#d0d5dd' } }
  378. },
  379. yAxis: {
  380. type: 'value',
  381. name: '数量(台)',
  382. nameTextStyle: { color: '#9098a6', fontSize: 12 },
  383. axisLabel: { color: '#9098a6', fontSize: 12 },
  384. splitLine: { lineStyle: { color: '#f0f2f6', type: 'dashed' } }
  385. },
  386. series: [
  387. {
  388. name: '异常总数', type: 'line',
  389. data: t.totalData || [], smooth: true, symbol: 'circle', symbolSize: 6,
  390. lineStyle: { width: 3, color: '#e63946' },
  391. itemStyle: { color: '#e63946', borderColor: '#fff', borderWidth: 2 },
  392. areaStyle: {
  393. color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
  394. { offset: 0, color: 'rgba(230,57,70,0.15)' },
  395. { offset: 1, color: 'rgba(230,57,70,0.0)' }
  396. ])
  397. }
  398. },
  399. {
  400. name: '新增异常', type: 'line',
  401. data: t.newData || [], smooth: true, symbol: 'emptyCircle', symbolSize: 6,
  402. lineStyle: { width: 2.5, color: '#f77f00' },
  403. itemStyle: { color: '#f77f00', borderColor: '#f77f00', borderWidth: 2 }
  404. },
  405. {
  406. name: '已修复', type: 'line',
  407. data: t.fixedData || [], smooth: true, symbol: 'diamond', symbolSize: 7,
  408. lineStyle: { width: 2.5, color: '#2a9d8f', type: 'dashed' },
  409. itemStyle: { color: '#2a9d8f', borderColor: '#fff', borderWidth: 2 }
  410. }
  411. ]
  412. })
  413. }
  414. function initTypeChart() {
  415. const dom = document.getElementById('typeChart')
  416. if (!dom) return
  417. if (typeChart) typeChart.dispose()
  418. typeChart = echarts.init(dom)
  419. const d = chartData.value.typeDistribution
  420. const colors = ['#457b9d', '#e76f51', '#e63946']
  421. typeChart.setOption({
  422. tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
  423. grid: { left: 46, right: 20, top: 16, bottom: 32 },
  424. xAxis: {
  425. type: 'category',
  426. data: d.categories || [],
  427. axisLabel: { color: '#9098a6', fontSize: 11, rotate: 15 },
  428. axisLine: { lineStyle: { color: '#d0d5dd' } }
  429. },
  430. yAxis: {
  431. type: 'value', name: '数量(台)',
  432. axisLabel: { color: '#9098a6', fontSize: 11 },
  433. splitLine: { lineStyle: { color: '#f0f2f6', type: 'dashed' } }
  434. },
  435. series: [{
  436. type: 'bar',
  437. data: (d.values || []).map((v, i) => ({ value: v, itemStyle: { color: colors[i] || '#6c757d', borderRadius: [4, 4, 0, 0] } })),
  438. barWidth: 28,
  439. label: { show: true, position: 'top', color: '#4e5969', fontSize: 12, fontWeight: 600 }
  440. }]
  441. })
  442. }
  443. function initLevelChart() {
  444. const dom = document.getElementById('levelChart')
  445. if (!dom) return
  446. if (levelChart) levelChart.dispose()
  447. levelChart = echarts.init(dom)
  448. const d = chartData.value.levelStatus
  449. levelChart.setOption({
  450. tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
  451. legend: {
  452. data: ['待处理', '处理中', '已修复'],
  453. top: 0,
  454. textStyle: { color: '#606a78', fontSize: 12 }
  455. },
  456. grid: { left: 46, right: 20, top: 44, bottom: 28 },
  457. xAxis: {
  458. type: 'category',
  459. data: d.levels || [],
  460. axisLabel: { color: '#9098a6', fontSize: 12 },
  461. axisLine: { lineStyle: { color: '#d0d5dd' } }
  462. },
  463. yAxis: {
  464. type: 'value', name: '数量(台)',
  465. axisLabel: { color: '#9098a6', fontSize: 11 },
  466. splitLine: { lineStyle: { color: '#f0f2f6', type: 'dashed' } },
  467. nameTextStyle: { color: '#9098a6', fontSize: 11 }
  468. },
  469. series: [
  470. {
  471. name: '待处理', type: 'bar',
  472. data: d.pending || [], barWidth: 30, barGap: '20%',
  473. itemStyle: { color: '#e63946', borderRadius: [4, 4, 0, 0] }
  474. },
  475. {
  476. name: '处理中', type: 'bar',
  477. data: d.processing || [], barWidth: 30,
  478. itemStyle: { color: '#f77f00', borderRadius: [4, 4, 0, 0] }
  479. },
  480. {
  481. name: '已修复', type: 'bar',
  482. data: d.fixed || [], barWidth: 30,
  483. itemStyle: { color: '#2a9d8f', borderRadius: [4, 4, 0, 0] }
  484. }
  485. ]
  486. })
  487. }
  488. function initCharts() {
  489. try { initTrendChart() } catch (e) { console.warn('趋势图初始化失败:', e) }
  490. try { initTypeChart() } catch (e) { console.warn('类型分布图初始化失败:', e) }
  491. try { initLevelChart() } catch (e) { console.warn('等级统计图初始化失败:', e) }
  492. }
  493. function resizeCharts() {
  494. trendChart?.resize()
  495. typeChart?.resize()
  496. levelChart?.resize()
  497. }
  498. onMounted(async () => {
  499. await nextTick()
  500. // 先加载数据,再初始化图表
  501. await Promise.all([loadStats(), loadList()])
  502. await nextTick()
  503. initCharts()
  504. window.addEventListener('resize', resizeCharts)
  505. })
  506. onBeforeUnmount(() => {
  507. window.removeEventListener('resize', resizeCharts)
  508. trendChart?.dispose()
  509. typeChart?.dispose()
  510. levelChart?.dispose()
  511. })
  512. </script>
  513. <style scoped>
  514. .equipment-abnormal {
  515. display: flex;
  516. gap: 16px;
  517. padding: 16px;
  518. background: #f0f2f6;
  519. min-height: calc(100vh - 84px);
  520. }
  521. /* ========== 左侧面板 ========== */
  522. .left-panel {
  523. width: 280px;
  524. flex-shrink: 0;
  525. display: flex;
  526. flex-direction: column;
  527. gap: 14px;
  528. }
  529. .panel-card {
  530. background: #fff;
  531. border-radius: 12px;
  532. padding: 16px;
  533. box-shadow: 0 2px 12px rgba(15, 35, 95, 0.04);
  534. }
  535. .panel-title {
  536. font-size: 14px;
  537. font-weight: 700;
  538. color: #1f2d3d;
  539. margin-bottom: 14px;
  540. display: flex;
  541. align-items: center;
  542. gap: 8px;
  543. padding-bottom: 10px;
  544. border-bottom: 1px solid #f0f2f6;
  545. }
  546. .panel-title .el-icon { color: #1890ff; }
  547. /* 等级列表 */
  548. .level-list { display: flex; flex-direction: column; gap: 8px; }
  549. .level-item {
  550. display: flex;
  551. align-items: center;
  552. gap: 10px;
  553. padding: 8px 12px;
  554. border-radius: 8px;
  555. background: #f8fafc;
  556. cursor: pointer;
  557. transition: all 0.2s;
  558. }
  559. .level-item:hover { background: #eef2f8; transform: translateX(4px); }
  560. .level-dot { width: 10px; height: 10px; border-radius: 50%; }
  561. .level-item.critical .level-dot { background: #e63946; box-shadow: 0 0 6px rgba(230,57,70,0.4); }
  562. .level-item.warning .level-dot { background: #f77f00; box-shadow: 0 0 6px rgba(247,127,0,0.4); }
  563. .level-item.minor .level-dot { background: #457b9d; box-shadow: 0 0 6px rgba(69,123,157,0.4); }
  564. .level-label { flex: 1; font-size: 13px; color: #4e5969; }
  565. .level-count { font-size: 18px; font-weight: 700; color: #1f2d3d; }
  566. /* 设备类型列表 */
  567. .type-list { display: flex; flex-direction: column; gap: 10px; }
  568. .type-item { display: flex; align-items: center; gap: 10px; }
  569. .type-name { width: 56px; font-size: 12px; color: #606a78; text-align: right; flex-shrink: 0; }
  570. .type-bar-bg { flex: 1; height: 8px; background: #f0f2f6; border-radius: 4px; overflow: hidden; }
  571. .type-bar { height: 100%; border-radius: 4px; transition: width 0.6s ease; }
  572. .type-count { width: 24px; font-size: 12px; font-weight: 600; color: #1f2d3d; text-align: right; flex-shrink: 0; }
  573. /* ========== 右侧内容区 ========== */
  574. .right-panel {
  575. flex: 1;
  576. display: flex;
  577. flex-direction: column;
  578. gap: 16px;
  579. min-width: 0;
  580. }
  581. /* 统计卡片 */
  582. .stats-row {
  583. display: grid;
  584. grid-template-columns: repeat(6, 1fr);
  585. gap: 12px;
  586. }
  587. .stat-card {
  588. background: #fff;
  589. border-radius: 12px;
  590. padding: 14px 16px;
  591. box-shadow: 0 2px 12px rgba(15, 35, 95, 0.04);
  592. display: flex;
  593. align-items: center;
  594. gap: 14px;
  595. cursor: pointer;
  596. transition: all 0.25s;
  597. }
  598. .stat-card:hover {
  599. transform: translateY(-2px);
  600. box-shadow: 0 6px 20px rgba(15, 35, 95, 0.1);
  601. }
  602. .stat-icon {
  603. width: 44px; height: 44px;
  604. border-radius: 10px;
  605. display: flex;
  606. align-items: center;
  607. justify-content: center;
  608. flex-shrink: 0;
  609. }
  610. .stat-total .stat-icon { background: #fef0f0; color: #e63946; }
  611. .stat-today .stat-icon { background: #fff3e6; color: #f77f00; }
  612. .stat-rate .stat-icon { background: #fef5e7; color: #e76f51; }
  613. .stat-processed .stat-icon { background: #e8f8f5; color: #2a9d8f; }
  614. .stat-pending .stat-icon { background: #eaf2f8; color: #457b9d; }
  615. .stat-process-rate .stat-icon { background: #e8f5e9; color: #2d6a4f; }
  616. .stat-label { font-size: 12px; color: #9098a6; margin-bottom: 4px; }
  617. .stat-value { font-size: 24px; font-weight: 700; color: #1f2d3d; line-height: 1.1; }
  618. .stat-value small { font-size: 12px; color: #9098a6; font-weight: 400; }
  619. /* 图表卡片 */
  620. .chart-card {
  621. background: #fff;
  622. border-radius: 12px;
  623. padding: 16px 20px;
  624. box-shadow: 0 2px 12px rgba(15, 35, 95, 0.04);
  625. display: flex;
  626. flex-direction: column;
  627. }
  628. .chart-trend { /* 全宽 */ }
  629. .charts-row {
  630. display: grid;
  631. grid-template-columns: 1fr 1fr;
  632. gap: 14px;
  633. }
  634. .chart-half { flex: 1; }
  635. .chart-header {
  636. display: flex;
  637. align-items: center;
  638. gap: 12px;
  639. margin-bottom: 6px;
  640. flex-wrap: wrap;
  641. }
  642. .chart-title {
  643. font-size: 15px;
  644. font-weight: 700;
  645. color: #1f2d3d;
  646. }
  647. .chart-subtitle {
  648. font-size: 12px;
  649. color: #a0a9b6;
  650. }
  651. .chart-legend-custom {
  652. margin-left: auto;
  653. display: flex;
  654. gap: 8px;
  655. }
  656. .legend-tag {
  657. font-size: 11px;
  658. padding: 2px 10px;
  659. border-radius: 10px;
  660. color: #fff;
  661. }
  662. .tag-total { background: #e63946; }
  663. .tag-new { background: #f77f00; }
  664. .tag-fixed { background: #2a9d8f; }
  665. .chart-body {
  666. flex: 1;
  667. min-height: 280px;
  668. }
  669. .chart-body-large {
  670. min-height: 330px;
  671. }
  672. /* 表格卡片 */
  673. .table-card {
  674. background: #fff;
  675. border-radius: 12px;
  676. padding: 16px 20px;
  677. box-shadow: 0 2px 12px rgba(15, 35, 95, 0.04);
  678. }
  679. .table-header {
  680. display: flex;
  681. align-items: center;
  682. justify-content: space-between;
  683. margin-bottom: 12px;
  684. }
  685. .table-title {
  686. font-size: 15px;
  687. font-weight: 700;
  688. color: #1f2d3d;
  689. }
  690. .table-count {
  691. font-size: 12px;
  692. color: #9098a6;
  693. }
  694. .table-footer {
  695. display: flex;
  696. justify-content: flex-end;
  697. margin-top: 12px;
  698. }
  699. /* ========== 响应式 ========== */
  700. @media (max-width: 1600px) {
  701. .stats-row { grid-template-columns: repeat(3, 1fr); }
  702. .left-panel { width: 260px; }
  703. }
  704. @media (max-width: 1400px) {
  705. .equipment-abnormal { flex-direction: column; }
  706. .left-panel { width: 100%; flex-direction: row; flex-wrap: wrap; }
  707. .left-panel .panel-card { flex: 1; min-width: 240px; }
  708. .charts-row { grid-template-columns: 1fr; }
  709. }
  710. @media (max-width: 768px) {
  711. .stats-row { grid-template-columns: repeat(2, 1fr); }
  712. }
  713. </style>