maintain.vue 26 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045
  1. <template>
  2. <!-- 第一行:统计卡片行 -->
  3. <el-row :gutter="16" class="mb-6">
  4. <el-col :span="6">
  5. <el-card class="stat-card stat-card-gradient-1" shadow="hover">
  6. <div class="stat-content">
  7. <div class="stat-header">
  8. <div class="stat-icon-wrapper">
  9. <Icon icon="ep:data-line" class="stat-icon" />
  10. </div>
  11. <div class="stat-title">MTTR</div>
  12. </div>
  13. <el-divider class="stat-divider" />
  14. <div class="stat-body">
  15. <div class="stat-label">平均解决时间</div>
  16. <div class="stat-value">{{ statsData.productCategoryCount }}</div>
  17. </div>
  18. </div>
  19. </el-card>
  20. </el-col>
  21. <el-col :span="6">
  22. <el-card class="stat-card stat-card-gradient-2" shadow="hover">
  23. <div class="stat-content">
  24. <div class="stat-header">
  25. <div class="stat-icon-wrapper">
  26. <Icon icon="ep:trend-charts" class="stat-icon" />
  27. </div>
  28. <div class="stat-title">近一周工单数量</div>
  29. </div>
  30. <el-divider class="stat-divider" />
  31. <div class="stat-body">
  32. <div class="stat-item">
  33. <div class="stat-sub-label">故障上报</div>
  34. <div class="stat-sub-value text-blue-500">{{ week.failureWeek }}</div>
  35. </div>
  36. <div class="stat-item">
  37. <div class="stat-sub-label">维修工单</div>
  38. <div class="stat-sub-value text-white">{{ week.maintainWeek }}</div>
  39. </div>
  40. </div>
  41. </div>
  42. </el-card>
  43. </el-col>
  44. <el-col :span="6">
  45. <el-card class="stat-card stat-card-gradient-3" shadow="hover">
  46. <div class="stat-content">
  47. <div class="stat-header">
  48. <div class="stat-icon-wrapper">
  49. <Icon icon="ep:calendar" class="stat-icon" />
  50. </div>
  51. <div class="stat-title">近一月工单数量</div>
  52. </div>
  53. <el-divider class="stat-divider" />
  54. <div class="stat-body">
  55. <div class="stat-item">
  56. <div class="stat-sub-label">故障上报</div>
  57. <div class="stat-sub-value text-blue-500">{{ month.failureMonth }}</div>
  58. </div>
  59. <div class="stat-item">
  60. <div class="stat-sub-label">维修工单</div>
  61. <div class="stat-sub-value text-white">{{ month.maintainMonth }}</div>
  62. </div>
  63. </div>
  64. </div>
  65. </el-card>
  66. </el-col>
  67. <el-col :span="6">
  68. <el-card class="stat-card stat-card-gradient-4" shadow="hover">
  69. <div class="stat-content">
  70. <div class="stat-header">
  71. <div class="stat-icon-wrapper">
  72. <Icon icon="ep:pie-chart" class="stat-icon" />
  73. </div>
  74. <div class="stat-title">工单总数量</div>
  75. </div>
  76. <el-divider class="stat-divider" />
  77. <div class="stat-body">
  78. <div class="stat-item">
  79. <div class="stat-sub-label">故障上报</div>
  80. <div class="stat-sub-value text-blue-500">{{ total.failureTotal }}</div>
  81. </div>
  82. <div class="stat-item">
  83. <div class="stat-sub-label">维修工单</div>
  84. <div class="stat-sub-value text-white">{{ total.maintainTotal }}</div>
  85. </div>
  86. </div>
  87. </div>
  88. </el-card>
  89. </el-col>
  90. </el-row>
  91. <!-- 第二行:图表行 -->
  92. <el-row :gutter="16" class="mb-6">
  93. <el-col :span="12">
  94. <el-card class="chart-card-enhanced" shadow="hover" style="border: none">
  95. <template #header>
  96. <div class="chart-header">
  97. <div class="chart-title-wrapper">
  98. <div class="chart-title-dot"></div>
  99. <span class="chart-title">故障上报状态统计</span>
  100. </div>
  101. </div>
  102. </template>
  103. <el-row class="chart-grid">
  104. <el-col :span="6" class="chart-item">
  105. <div ref="reportingChartRef" class="gauge-chart"></div>
  106. <div class="chart-label">
  107. <span class="label-dot label-dot-blue"></span>
  108. <span class="label-text">上报中</span>
  109. </div>
  110. </el-col>
  111. <el-col :span="6" class="chart-item">
  112. <div ref="dealFinishedChartRef" class="gauge-chart"></div>
  113. <div class="chart-label">
  114. <span class="label-dot label-dot-orange"></span>
  115. <span class="label-text">处理完成</span>
  116. </div>
  117. </el-col>
  118. <el-col :span="6" class="chart-item">
  119. <div ref="transOrderChartRef" class="gauge-chart"></div>
  120. <div class="chart-label">
  121. <span class="label-dot label-dot-purple"></span>
  122. <span class="label-text">转工单</span>
  123. </div>
  124. </el-col>
  125. <el-col :span="6" class="chart-item">
  126. <div ref="orderFinishChartRef" class="gauge-chart"></div>
  127. <div class="chart-label">
  128. <span class="label-dot label-dot-cyan"></span>
  129. <span class="label-text">工单处理完成</span>
  130. </div>
  131. </el-col>
  132. </el-row>
  133. </el-card>
  134. </el-col>
  135. <el-col :span="12">
  136. <el-card class="chart-card-enhanced" shadow="hover" style="border: none">
  137. <template #header>
  138. <div class="chart-header">
  139. <div class="chart-title-wrapper">
  140. <div class="chart-title-dot"></div>
  141. <span class="chart-title">维修工单状态统计</span>
  142. </div>
  143. </div>
  144. </template>
  145. <el-row class="chart-grid">
  146. <el-col :span="12" class="chart-item">
  147. <div ref="writeChartRef" class="gauge-chart"></div>
  148. <div class="chart-label">
  149. <span class="label-dot label-dot-indigo"></span>
  150. <span class="label-text">待填写</span>
  151. </div>
  152. </el-col>
  153. <el-col :span="12" class="chart-item">
  154. <div ref="finishedChartRef" class="gauge-chart"></div>
  155. <div class="chart-label">
  156. <span class="label-dot label-dot-emerald"></span>
  157. <span class="label-text">已完成</span>
  158. </div>
  159. </el-col>
  160. </el-row>
  161. </el-card>
  162. </el-col>
  163. </el-row>
  164. <!-- 第三行:消息统计行 -->
  165. <el-row>
  166. <el-col :span="24">
  167. <el-card class="chart-card-enhanced" shadow="hover" style="border: none">
  168. <template #header>
  169. <div class="chart-header">
  170. <div class="chart-title-wrapper">
  171. <div class="chart-title-dot"></div>
  172. <span class="chart-title">近一年数量统计</span>
  173. </div>
  174. </div>
  175. </template>
  176. <div ref="chartContainer" class="bar-chart-container"></div>
  177. </el-card>
  178. </el-col>
  179. </el-row>
  180. <!-- TODO 第四行:地图 -->
  181. </template>
  182. <script setup lang="ts" name="Index">
  183. import * as echarts from 'echarts/core'
  184. import { BarChart } from 'echarts/charts' // 显式导入柱状图模块
  185. import {
  186. GridComponent,
  187. LegendComponent,
  188. TitleComponent,
  189. ToolboxComponent,
  190. TooltipComponent
  191. } from 'echarts/components'
  192. import { GaugeChart, LineChart, PieChart } from 'echarts/charts'
  193. import { LabelLayout, UniversalTransition } from 'echarts/features'
  194. import { CanvasRenderer } from 'echarts/renderers'
  195. import {
  196. IotStatisticsDeviceMessageSummaryRespVO,
  197. IotStatisticsSummaryRespVO
  198. } from '@/api/iot/statistics'
  199. import { formatDate } from '@/utils/formatTime'
  200. import { IotStatApi } from '@/api/pms/stat'
  201. // TODO @super:参考下 /Users/yunai/Java/yudao-ui-admin-vue3/src/views/mall/home/index.vue,拆一拆组件
  202. /** IoT 首页 */
  203. defineOptions({ name: 'IoTHome' })
  204. // TODO @super:使用下 Echart 组件,参考 yudao-ui-admin-vue3/src/views/mall/home/components/TradeTrendCard.vue 等
  205. echarts.use([
  206. TooltipComponent,
  207. LegendComponent,
  208. PieChart,
  209. CanvasRenderer,
  210. LabelLayout,
  211. TitleComponent,
  212. ToolboxComponent,
  213. GridComponent,
  214. LineChart,
  215. UniversalTransition,
  216. GaugeChart,
  217. BarChart
  218. ])
  219. const timeRange = ref('7d') // 修改默认选择为近一周
  220. const dateRange = ref<[Date, Date] | null>(null)
  221. const queryParams = reactive({
  222. startTime: Date.now() - 7 * 24 * 60 * 60 * 1000, // 设置默认开始时间为 7 天前
  223. endTime: Date.now() // 设置默认结束时间为当前时间
  224. })
  225. // const deviceCountChartRef = ref() // 设备数量统计的图表
  226. const reportingChartRef = ref() // 在线设备统计的图表
  227. const dealFinishedChartRef = ref() // 离线设备统计的图表
  228. const transOrderChartRef = ref() // 待激活设备统计的图表
  229. const orderFinishChartRef = ref()
  230. const deviceMessageCountChartRef = ref() // 上下行消息量统计的图表
  231. const writeChartRef = ref() // 上下行消息量统计的图表
  232. const finishedChartRef = ref() // 上下行消息量统计的图表
  233. // 基础统计数据
  234. // TODO @super:初始为 -1,然后界面展示先是加载中?试试用 cursor 改哈
  235. const statsData = ref<IotStatisticsSummaryRespVO>({
  236. productCategoryCount: 0,
  237. productCount: 0,
  238. deviceCount: 0,
  239. deviceMessageCount: 0,
  240. productCategoryTodayCount: 0,
  241. productTodayCount: 0,
  242. deviceTodayCount: 0,
  243. deviceMessageTodayCount: 0,
  244. deviceOnlineCount: 0,
  245. deviceOfflineCount: 0,
  246. deviceInactiveCount: 0,
  247. productCategoryDeviceCounts: {}
  248. })
  249. const day = ref({
  250. failureDay: undefined,
  251. maintainDay: undefined
  252. })
  253. const week = ref({
  254. failureWeek: undefined,
  255. maintainWeek: undefined
  256. })
  257. const month = ref({
  258. failureMonth: undefined,
  259. maintainMonth: undefined
  260. })
  261. const total = ref({
  262. failureTotal: undefined,
  263. maintainTotal: undefined
  264. })
  265. const status = ref({
  266. failureStatus: {
  267. reporting: 0,
  268. trans: 0,
  269. finished: 0,
  270. orderFinished: 0
  271. },
  272. maintainStatus: {
  273. finished: 0,
  274. todo: 0
  275. }
  276. })
  277. // 消息统计数据
  278. const messageStats = ref<IotStatisticsDeviceMessageSummaryRespVO>({
  279. upstreamCounts: {},
  280. downstreamCounts: {}
  281. })
  282. /** 处理快捷时间范围选择 */
  283. const handleTimeRangeChange = (timeRange: string) => {
  284. const now = Date.now()
  285. let startTime: number
  286. // TODO @super:这个的计算,看看能不能结合 dayjs 简化。因为 1h、24h、7d 感觉是比较标准的。如果没有,抽到 utils/formatTime.ts 作为一个工具方法
  287. switch (timeRange) {
  288. case '1h':
  289. startTime = now - 60 * 60 * 1000
  290. break
  291. case '24h':
  292. startTime = now - 24 * 60 * 60 * 1000
  293. break
  294. case '7d':
  295. startTime = now - 7 * 24 * 60 * 60 * 1000
  296. break
  297. default:
  298. return
  299. }
  300. // 清空日期选择器
  301. dateRange.value = null
  302. // 更新查询参数
  303. queryParams.startTime = startTime
  304. queryParams.endTime = now
  305. // 重新获取数据
  306. getStats()
  307. }
  308. /** 处理自定义日期范围选择 */
  309. const handleDateRangeChange = (value: [Date, Date] | null) => {
  310. if (value) {
  311. // 清空快捷选项
  312. timeRange.value = ''
  313. // 更新查询参数
  314. queryParams.startTime = value[0].getTime()
  315. queryParams.endTime = value[1].getTime()
  316. // 重新获取数据
  317. getStats()
  318. }
  319. }
  320. /** 获取统计数据 */
  321. const getStats = async () => {
  322. // 获取基础统计数据
  323. IotStatApi.getMainDay().then((res) => {
  324. day.value = res
  325. })
  326. IotStatApi.getMainWeek().then((res) => {
  327. week.value = res
  328. })
  329. IotStatApi.getMainMonth().then((res) => {
  330. month.value = res
  331. })
  332. IotStatApi.getMainTotal().then((res) => {
  333. total.value = res
  334. })
  335. IotStatApi.getMainStatus().then((res) => {
  336. status.value = res
  337. console.log(JSON.stringify(status.value))
  338. initCharts()
  339. })
  340. // statsData.value = await ProductCategoryApi.getIotStatisticsSummary()
  341. //
  342. // // 获取消息统计数据
  343. // messageStats.value = await ProductCategoryApi.getIotStatisticsDeviceMessageSummary(queryParams)
  344. // 初始化图表
  345. }
  346. /** 初始化图表 */
  347. const initCharts = () => {
  348. // 设备数量统计
  349. // echarts.init(deviceCountChartRef.value).setOption({
  350. // tooltip: {
  351. // trigger: 'item'
  352. // },
  353. // legend: {
  354. // top: '5%',
  355. // right: '10%',
  356. // align: 'left',
  357. // orient: 'vertical',
  358. // icon: 'circle'
  359. // },
  360. // series: [
  361. // {
  362. // name: 'Access From',
  363. // type: 'pie',
  364. // radius: ['50%', '80%'],
  365. // avoidLabelOverlap: false,
  366. // center: ['30%', '50%'],
  367. // label: {
  368. // show: false,
  369. // position: 'outside'
  370. // },
  371. // emphasis: {
  372. // label: {
  373. // show: true,
  374. // fontSize: 20,
  375. // fontWeight: 'bold'
  376. // }
  377. // },
  378. // labelLine: {
  379. // show: false
  380. // },
  381. // data: Object.entries(statsData.value.productCategoryDeviceCounts).map(([name, value]) => ({
  382. // name,
  383. // value
  384. // }))
  385. // }
  386. // ]
  387. // })
  388. // 上报中
  389. initGaugeChart(
  390. reportingChartRef.value,
  391. status.value.failureStatus.reporting === undefined ? 0 : status.value.failureStatus.reporting,
  392. '#0d9'
  393. )
  394. // 处理完成
  395. initGaugeChart(
  396. dealFinishedChartRef.value,
  397. status.value.failureStatus.finished === undefined ? 0 : status.value.failureStatus.finished,
  398. '#f50'
  399. )
  400. // 转工单
  401. initGaugeChart(
  402. transOrderChartRef.value,
  403. status.value.failureStatus.trans === undefined ? 0 : status.value.failureStatus.trans,
  404. '#05b'
  405. )
  406. // 工单完成
  407. initGaugeChart(
  408. orderFinishChartRef.value,
  409. status.value.failureStatus.orderFinished === undefined
  410. ? 0
  411. : status.value.failureStatus.orderFinished,
  412. '#05b'
  413. )
  414. // 待填写
  415. initGaugeChart(
  416. writeChartRef.value,
  417. status.value.maintainStatus.todo === undefined ? 0 : status.value.maintainStatus.todo,
  418. '#05b'
  419. )
  420. //已完成
  421. initGaugeChart(
  422. finishedChartRef.value,
  423. status.value.maintainStatus.finished === undefined ? 0 : status.value.maintainStatus.finished,
  424. '#05b'
  425. )
  426. // 消息量统计
  427. //initMessageChart()
  428. }
  429. /** 初始化仪表盘图表 */
  430. const initGaugeChart = (el: any, value: number, color: string) => {
  431. echarts.init(el).setOption({
  432. series: [
  433. {
  434. type: 'gauge',
  435. startAngle: 360,
  436. endAngle: 0,
  437. min: 0,
  438. max: statsData.value.deviceCount || 100, // 使用设备总数作为最大值
  439. progress: {
  440. show: true,
  441. width: 12,
  442. itemStyle: {
  443. color: color
  444. }
  445. },
  446. axisLine: {
  447. lineStyle: {
  448. width: 12,
  449. color: [[1, '#E5E7EB']]
  450. }
  451. },
  452. axisTick: { show: false },
  453. splitLine: { show: false },
  454. axisLabel: { show: false },
  455. pointer: { show: false },
  456. anchor: { show: false },
  457. title: { show: false },
  458. detail: {
  459. valueAnimation: true,
  460. fontSize: 24,
  461. fontWeight: 'bold',
  462. fontFamily: 'Inter, sans-serif',
  463. color: color,
  464. offsetCenter: [0, '0'],
  465. formatter: (value: number) => {
  466. return `${value} `
  467. }
  468. },
  469. data: [{ value: value }]
  470. }
  471. ]
  472. })
  473. }
  474. /** 初始化消息统计图表 */
  475. const initMessageChart = () => {
  476. // 获取所有时间戳并排序
  477. // TODO @super:一些 idea 里的红色报错,要去处理掉噢。
  478. const timestamps = Array.from(
  479. new Set([
  480. ...messageStats.value.upstreamCounts.map((item) => Number(Object.keys(item)[0])),
  481. ...messageStats.value.downstreamCounts.map((item) => Number(Object.keys(item)[0]))
  482. ])
  483. ).sort((a, b) => a - b) // 确保时间戳从小到大排序
  484. // 准备数据
  485. const xdata = timestamps.map((ts) => formatDate(ts, 'YYYY-MM-DD HH:mm'))
  486. const upData = timestamps.map((ts) => {
  487. const item = messageStats.value.upstreamCounts.find(
  488. (count) => Number(Object.keys(count)[0]) === ts
  489. )
  490. return item ? Object.values(item)[0] : 0
  491. })
  492. const downData = timestamps.map((ts) => {
  493. const item = messageStats.value.downstreamCounts.find(
  494. (count) => Number(Object.keys(count)[0]) === ts
  495. )
  496. return item ? Object.values(item)[0] : 0
  497. })
  498. // 配置图表
  499. echarts.init(deviceMessageCountChartRef.value).setOption({
  500. tooltip: {
  501. trigger: 'axis',
  502. backgroundColor: 'rgba(255, 255, 255, 0.9)',
  503. borderColor: '#E5E7EB',
  504. textStyle: {
  505. color: '#374151'
  506. }
  507. },
  508. legend: {
  509. data: ['上行消息量', '下行消息量'],
  510. textStyle: {
  511. color: '#374151',
  512. fontWeight: 500
  513. }
  514. },
  515. grid: {
  516. left: '3%',
  517. right: '4%',
  518. bottom: '3%',
  519. containLabel: true
  520. },
  521. xAxis: {
  522. type: 'category',
  523. boundaryGap: false,
  524. data: xdata,
  525. axisLine: {
  526. lineStyle: {
  527. color: '#E5E7EB'
  528. }
  529. },
  530. axisLabel: {
  531. color: '#6B7280'
  532. }
  533. },
  534. yAxis: {
  535. type: 'value',
  536. axisLine: {
  537. lineStyle: {
  538. color: '#E5E7EB'
  539. }
  540. },
  541. axisLabel: {
  542. color: '#6B7280'
  543. },
  544. splitLine: {
  545. lineStyle: {
  546. color: '#F3F4F6'
  547. }
  548. }
  549. },
  550. series: [
  551. {
  552. name: '上行消息量',
  553. type: 'line',
  554. smooth: true, // 添加平滑曲线
  555. data: upData,
  556. itemStyle: {
  557. color: '#3B82F6'
  558. },
  559. lineStyle: {
  560. width: 2
  561. },
  562. areaStyle: {
  563. color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
  564. { offset: 0, color: 'rgba(59, 130, 246, 0.2)' },
  565. { offset: 1, color: 'rgba(59, 130, 246, 0)' }
  566. ])
  567. }
  568. },
  569. {
  570. name: '下行消息量',
  571. type: 'line',
  572. smooth: true, // 添加平滑曲线
  573. data: downData,
  574. itemStyle: {
  575. color: '#10B981'
  576. },
  577. lineStyle: {
  578. width: 2
  579. },
  580. areaStyle: {
  581. color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
  582. { offset: 0, color: 'rgba(16, 185, 129, 0.2)' },
  583. { offset: 1, color: 'rgba(16, 185, 129, 0)' }
  584. ])
  585. }
  586. }
  587. ]
  588. })
  589. }
  590. const chartContainer = ref(null)
  591. let chartInstance = null
  592. // 生成过去12个月份的标签 (格式: YYYY-MM)
  593. const generateMonthLabels = () => {
  594. const months = []
  595. const date = new Date()
  596. for (let i = 11; i >= 0; i--) {
  597. const tempDate = new Date(date.getFullYear(), date.getMonth() - i, 1)
  598. const year = tempDate.getFullYear()
  599. const month = String(tempDate.getMonth() + 1).padStart(2, '0')
  600. months.push(`${year}-${month}`)
  601. }
  602. return months
  603. }
  604. // 模拟数据获取
  605. const fetchChartData = async () => {
  606. // 模拟异步请求
  607. return new Promise((resolve) => {
  608. setTimeout(() => {
  609. resolve({
  610. months: generateMonthLabels(),
  611. faults: [20, 30, 100, 40, 20, 50, 70, 80, 60, 90, 100, 100],
  612. repairs: [10, 30, 90, 30, 10, 20, 60, 50, 22, 34, 70, 85]
  613. })
  614. }, 300)
  615. })
  616. }
  617. // 初始化图表配置
  618. const initChart = async () => {
  619. if (!chartContainer.value) return
  620. // 获取数据
  621. const { months, faults, repairs } = await fetchChartData()
  622. // ECharts配置
  623. const option = {
  624. tooltip: {
  625. trigger: 'axis',
  626. axisPointer: {
  627. type: 'shadow'
  628. },
  629. formatter: (params) => {
  630. return `${params[0].axisValue}<br/>
  631. ${params[0].marker} ${params[0].seriesName}: ${params[0].value}<br/>
  632. ${params[1].marker} ${params[1].seriesName}: ${params[1].value}`
  633. }
  634. },
  635. legend: {
  636. data: ['故障上报数量', '维修工单数量'],
  637. top: 25
  638. },
  639. grid: {
  640. left: '3%',
  641. right: '4%',
  642. bottom: '3%',
  643. containLabel: true
  644. },
  645. xAxis: {
  646. type: 'category',
  647. data: months,
  648. axisLabel: {
  649. rotate: 45,
  650. margin: 15
  651. }
  652. },
  653. yAxis: {
  654. type: 'value',
  655. axisLabel: {
  656. formatter: (value) => Math.floor(value).toString()
  657. }
  658. },
  659. series: [
  660. {
  661. name: '故障上报数量',
  662. type: 'bar',
  663. barGap: 0,
  664. itemStyle: {
  665. color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
  666. { offset: 0, color: '#188df0' },
  667. { offset: 1, color: '#188df0' }
  668. ])
  669. },
  670. emphasis: {
  671. focus: 'series'
  672. },
  673. data: faults
  674. },
  675. {
  676. name: '维修工单数量',
  677. type: 'bar',
  678. itemStyle: {
  679. color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
  680. { offset: 0, color: '#d3a137' },
  681. { offset: 1, color: '#d3a137' }
  682. ])
  683. },
  684. emphasis: {
  685. focus: 'series'
  686. },
  687. data: repairs
  688. }
  689. ]
  690. }
  691. // 初始化图表
  692. chartInstance = echarts.init(chartContainer.value)
  693. chartInstance.setOption(option)
  694. // 窗口缩放监听
  695. window.addEventListener('resize', handleResize)
  696. handleResize()
  697. }
  698. // 自适应调整
  699. const handleResize = () => {
  700. chartInstance?.resize()
  701. }
  702. /** 初始化 */
  703. onMounted(() => {
  704. getStats()
  705. initChart()
  706. })
  707. onUnmounted(() => {
  708. chartInstance?.dispose()
  709. window.removeEventListener('resize', handleResize)
  710. })
  711. </script>
  712. <style lang="scss" scoped>
  713. // 统计卡片基础样式
  714. .stat-card {
  715. border-radius: 12px;
  716. border: 1px solid rgba(207, 220, 237, 0.9);
  717. transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
  718. overflow: hidden;
  719. position: relative;
  720. background: linear-gradient(180deg, #ffffff 0%, #f4f8ff 100%);
  721. box-shadow:
  722. inset 0 1px 0 rgba(255, 255, 255, 0.95),
  723. inset 0 -10px 24px rgba(210, 225, 244, 0.26),
  724. 0 10px 24px rgba(32, 66, 120, 0.08);
  725. &:hover {
  726. transform: translateY(-4px);
  727. box-shadow:
  728. inset 0 1px 0 rgba(255, 255, 255, 0.98),
  729. inset 0 -12px 26px rgba(204, 220, 243, 0.32),
  730. 0 16px 32px rgba(32, 66, 120, 0.12);
  731. }
  732. :deep(.el-card__body) {
  733. padding: 0;
  734. }
  735. }
  736. // 渐变色背景
  737. .stat-card-gradient-1 {
  738. background:
  739. radial-gradient(circle at top left, rgba(121, 164, 255, 0.16), transparent 42%),
  740. linear-gradient(180deg, #ffffff 0%, #f4f8ff 100%);
  741. }
  742. .stat-card-gradient-2 {
  743. background:
  744. radial-gradient(circle at top left, rgba(118, 186, 255, 0.14), transparent 42%),
  745. linear-gradient(180deg, #ffffff 0%, #f3f8ff 100%);
  746. }
  747. .stat-card-gradient-3 {
  748. background:
  749. radial-gradient(circle at top left, rgba(96, 154, 241, 0.14), transparent 42%),
  750. linear-gradient(180deg, #ffffff 0%, #f5f9ff 100%);
  751. }
  752. .stat-card-gradient-4 {
  753. background:
  754. radial-gradient(circle at top left, rgba(137, 176, 242, 0.14), transparent 42%),
  755. linear-gradient(180deg, #ffffff 0%, #f4f8fe 100%);
  756. }
  757. // 统计内容区域
  758. .stat-content {
  759. padding: 20px;
  760. color: #1f2a44;
  761. }
  762. .stat-header {
  763. display: flex;
  764. align-items: center;
  765. gap: 12px;
  766. margin-bottom: 16px;
  767. }
  768. .stat-icon-wrapper {
  769. width: 48px;
  770. height: 48px;
  771. background: linear-gradient(180deg, rgba(255, 255, 255, 0.96) 0%, rgba(232, 240, 252, 0.92) 100%);
  772. border: 1px solid rgba(205, 219, 239, 0.9);
  773. border-radius: 12px;
  774. display: flex;
  775. align-items: center;
  776. justify-content: center;
  777. box-shadow:
  778. inset 0 1px 0 rgba(255, 255, 255, 0.95),
  779. inset 0 -6px 12px rgba(205, 220, 242, 0.34),
  780. 0 6px 14px rgba(66, 104, 168, 0.08);
  781. }
  782. .stat-icon {
  783. font-size: 24px;
  784. color: #5d79b7;
  785. }
  786. .stat-title {
  787. font-size: 16px;
  788. font-weight: 600;
  789. color: #000;
  790. }
  791. .stat-divider {
  792. border-color: rgba(201, 214, 234, 0.85);
  793. margin: 12px 0;
  794. }
  795. .stat-body {
  796. display: flex;
  797. flex-direction: column;
  798. gap: 12px;
  799. }
  800. .stat-label {
  801. font-size: 13px;
  802. color: #7182a1;
  803. margin-bottom: 4px;
  804. }
  805. .stat-value {
  806. font-size: 36px;
  807. font-weight: 700;
  808. color: #1f2f54;
  809. line-height: 1;
  810. }
  811. .stat-item {
  812. display: flex;
  813. justify-content: space-between;
  814. align-items: center;
  815. }
  816. .stat-sub-label {
  817. font-size: 13px;
  818. color: #7182a1;
  819. }
  820. .stat-sub-value {
  821. font-size: 28px;
  822. font-weight: 700;
  823. line-height: 1;
  824. color: #1f2f54;
  825. }
  826. // 图表卡片增强样式
  827. .chart-card-enhanced {
  828. border-radius: 12px;
  829. border: 1px solid #e5e7eb;
  830. transition: all 0.3s ease;
  831. &:hover {
  832. box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08);
  833. border-color: #d1d5db;
  834. }
  835. :deep(.el-card__header) {
  836. padding: 16px 20px;
  837. border-bottom: 1px solid #f3f4f6;
  838. background: linear-gradient(to right, #fafafa, #ffffff);
  839. }
  840. :deep(.el-card__body) {
  841. padding: 20px;
  842. }
  843. }
  844. .chart-header {
  845. display: flex;
  846. align-items: center;
  847. justify-content: space-between;
  848. }
  849. .chart-title-wrapper {
  850. display: flex;
  851. align-items: center;
  852. gap: 8px;
  853. }
  854. .chart-title-dot {
  855. width: 8px;
  856. height: 8px;
  857. border-radius: 50%;
  858. background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  859. box-shadow: 0 2px 4px rgba(102, 126, 234, 0.3);
  860. }
  861. .chart-title {
  862. font-size: 16px;
  863. font-weight: 600;
  864. color: #1f2937;
  865. letter-spacing: 0.3px;
  866. }
  867. .chart-grid {
  868. min-height: 220px;
  869. }
  870. .chart-item {
  871. display: flex;
  872. flex-direction: column;
  873. align-items: center;
  874. padding: 12px;
  875. transition: all 0.2s ease;
  876. &:hover {
  877. background: rgba(102, 126, 234, 0.03);
  878. border-radius: 8px;
  879. }
  880. }
  881. .gauge-chart {
  882. width: 100%;
  883. height: 160px;
  884. }
  885. .chart-label {
  886. display: flex;
  887. align-items: center;
  888. gap: 6px;
  889. margin-top: 8px;
  890. padding: 6px 12px;
  891. background: #f9fafb;
  892. border-radius: 20px;
  893. transition: all 0.2s ease;
  894. .chart-item:hover & {
  895. background: #f3f4f6;
  896. }
  897. }
  898. .label-dot {
  899. width: 8px;
  900. height: 8px;
  901. border-radius: 50%;
  902. display: inline-block;
  903. }
  904. .label-dot-blue {
  905. background: #3b82f6;
  906. box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2);
  907. }
  908. .label-dot-orange {
  909. background: #f97316;
  910. box-shadow: 0 0 0 2px rgba(249, 115, 22, 0.2);
  911. }
  912. .label-dot-purple {
  913. background: #8b5cf6;
  914. box-shadow: 0 0 0 2px rgba(139, 92, 246, 0.2);
  915. }
  916. .label-dot-cyan {
  917. background: #06b6d4;
  918. box-shadow: 0 0 0 2px rgba(6, 182, 212, 0.2);
  919. }
  920. .label-dot-indigo {
  921. background: #6366f1;
  922. box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.2);
  923. }
  924. .label-dot-emerald {
  925. background: #10b981;
  926. box-shadow: 0 0 0 2px rgba(16, 185, 129, 0.2);
  927. }
  928. .label-text {
  929. font-size: 13px;
  930. color: #6b7280;
  931. font-weight: 500;
  932. }
  933. .bar-chart-container {
  934. height: 350px;
  935. width: 100%;
  936. }
  937. // 响应式优化
  938. @media (max-width: 1200px) {
  939. .stat-value {
  940. font-size: 28px;
  941. }
  942. .stat-sub-value {
  943. font-size: 24px;
  944. }
  945. }
  946. @media (max-width: 768px) {
  947. .stat-card {
  948. margin-bottom: 16px;
  949. }
  950. .chart-item {
  951. padding: 8px;
  952. }
  953. .gauge-chart {
  954. height: 140px;
  955. }
  956. }
  957. </style>