index.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425
  1. <template>
  2. <div class="min-h-full bg-gray-50">
  3. <el-space direction="vertical" :fill="true" size="small" class="w-full p-2">
  4. <!-- 统计卡片行 -->
  5. <el-row :gutter="16" class="mb-4">
  6. <el-col :span="6">
  7. <el-card class="stat-card" shadow="never">
  8. <div class="flex flex-col">
  9. <span class="text-gray-500 text-base mb-1">品类数量</span>
  10. <span class="text-3xl font-bold text-gray-700">{{ statsData.categoryTotal }}</span>
  11. <el-divider class="my-2" />
  12. <div class="flex items-center text-gray-400 text-sm">
  13. <span>今日新增</span>
  14. <span class="ml-1 text-green-500">↑ 0</span>
  15. </div>
  16. </div>
  17. </el-card>
  18. </el-col>
  19. <el-col :span="6">
  20. <el-card class="stat-card" shadow="never">
  21. <div class="flex flex-col">
  22. <span class="text-gray-500 text-base mb-1">产品数量</span>
  23. <span class="text-3xl font-bold text-gray-700">{{ statsData.productTotal }}</span>
  24. <el-divider class="my-2" />
  25. <div class="flex items-center text-gray-400 text-sm">
  26. <span>今日新增</span>
  27. <span class="ml-1 text-green-500">↑ 0</span>
  28. </div>
  29. </div>
  30. </el-card>
  31. </el-col>
  32. <el-col :span="6">
  33. <el-card class="stat-card" shadow="never">
  34. <div class="flex flex-col">
  35. <span class="text-gray-500 text-base mb-1">设备数量</span>
  36. <span class="text-3xl font-bold text-gray-700">{{ statsData.deviceTotal }}</span>
  37. <el-divider class="my-2" />
  38. <div class="flex items-center text-gray-400 text-sm">
  39. <span>今日新增</span>
  40. <span class="ml-1 text-green-500">↑ 0</span>
  41. </div>
  42. </div>
  43. </el-card>
  44. </el-col>
  45. <el-col :span="6">
  46. <el-card class="stat-card" shadow="never">
  47. <div class="flex flex-col">
  48. <span class="text-gray-500 text-base mb-1">物模型消息</span>
  49. <span class="text-3xl font-bold text-gray-700">{{ statsData.reportTotal }}</span>
  50. <el-divider class="my-2" />
  51. <div class="flex items-center text-gray-400 text-sm">
  52. <span>今日新增</span>
  53. <span class="ml-1 text-green-500">↑ 0</span>
  54. </div>
  55. </div>
  56. </el-card>
  57. </el-col>
  58. </el-row>
  59. <!-- 图表行 -->
  60. <el-row :gutter="16" class="mb-4">
  61. <el-col :span="12">
  62. <el-card class="chart-card" shadow="never">
  63. <template #header>
  64. <div class="flex items-center">
  65. <span class="text-base font-medium text-gray-600">设备数量统计</span>
  66. </div>
  67. </template>
  68. <div ref="chartDeviceNumStat" class="h-[240px]"></div>
  69. </el-card>
  70. </el-col>
  71. <el-col :span="12">
  72. <el-card class="chart-card" shadow="never">
  73. <template #header>
  74. <div class="flex items-center">
  75. <span class="text-base font-medium text-gray-600">设备状态统计</span>
  76. </div>
  77. </template>
  78. <el-row class="h-[240px]">
  79. <el-col :span="8" class="flex flex-col items-center">
  80. <div ref="chartDeviceOnline" class="h-[160px] w-full"></div>
  81. <div class="text-center mt-2">
  82. <span class="text-sm text-gray-600">在线设备</span>
  83. </div>
  84. </el-col>
  85. <el-col :span="8" class="flex flex-col items-center">
  86. <div ref="chartDeviceOffline" class="h-[160px] w-full"></div>
  87. <div class="text-center mt-2">
  88. <span class="text-sm text-gray-600">离线设备</span>
  89. </div>
  90. </el-col>
  91. <el-col :span="8" class="flex flex-col items-center">
  92. <div ref="chartDeviceActive" class="h-[160px] w-full"></div>
  93. <div class="text-center mt-2">
  94. <span class="text-sm text-gray-600">待激活设备</span>
  95. </div>
  96. </el-col>
  97. </el-row>
  98. </el-card>
  99. </el-col>
  100. </el-row>
  101. <!-- 消息统计行 -->
  102. <el-row>
  103. <el-col :span="24">
  104. <el-card class="chart-card" shadow="never">
  105. <template #header>
  106. <div class="flex items-center">
  107. <span class="text-base font-medium text-gray-600">消息量统计</span>
  108. </div>
  109. </template>
  110. <div ref="chartMsgStat" class="h-[300px]"></div>
  111. </el-card>
  112. </el-col>
  113. </el-row>
  114. </el-space>
  115. </div>
  116. </template>
  117. <script setup lang="ts" name="Index">
  118. import * as echarts from 'echarts/core'
  119. import { TooltipComponent, LegendComponent, TitleComponent, ToolboxComponent, GridComponent } from 'echarts/components'
  120. import { PieChart, LineChart, GaugeChart } from 'echarts/charts'
  121. import { LabelLayout, UniversalTransition } from 'echarts/features'
  122. import { CanvasRenderer } from 'echarts/renderers'
  123. import { ProductCategoryApi } from '@/api/iot/statistics'
  124. import { formatDate } from '@/utils/formatTime'
  125. /** IoT 首页 */
  126. defineOptions({ name: 'IotHome' })
  127. echarts.use([
  128. TooltipComponent,
  129. LegendComponent,
  130. PieChart,
  131. CanvasRenderer,
  132. LabelLayout,
  133. TitleComponent,
  134. ToolboxComponent,
  135. GridComponent,
  136. LineChart,
  137. UniversalTransition,
  138. GaugeChart
  139. ])
  140. const chartDeviceNumStat = ref()
  141. const chartDeviceOnline = ref()
  142. const chartDeviceOffline = ref()
  143. const chartDeviceActive = ref()
  144. const chartMsgStat = ref()
  145. const statsData = ref({
  146. categoryTotal: 0,
  147. productTotal: 0,
  148. deviceTotal: 0,
  149. reportTotal: 0,
  150. onlineTotal: 0,
  151. offlineTotal: 0,
  152. neverOnlineTotal: 0,
  153. deviceStatsOfCategory: [],
  154. reportDataStats: []
  155. })
  156. /** 获取统计数据 */
  157. const getStats = async () => {
  158. const res = await ProductCategoryApi.getIotMainStats()
  159. statsData.value = res
  160. console.log(res)
  161. console.log(statsData.value)
  162. initCharts()
  163. }
  164. /** 初始化图表 */
  165. const initCharts = () => {
  166. // 设备数量统计
  167. echarts.init(chartDeviceNumStat.value).setOption({
  168. tooltip: {
  169. trigger: 'item'
  170. },
  171. legend: {
  172. top: '5%',
  173. right: '10%',
  174. align: 'left',
  175. orient: 'vertical',
  176. icon: 'circle'
  177. },
  178. series: [
  179. {
  180. name: 'Access From',
  181. type: 'pie',
  182. radius: ['50%', '80%'],
  183. avoidLabelOverlap: false,
  184. center: ['30%', '50%'],
  185. label: {
  186. show: false,
  187. position: 'outside'
  188. },
  189. emphasis: {
  190. label: {
  191. show: true,
  192. fontSize: 20,
  193. fontWeight: 'bold'
  194. }
  195. },
  196. labelLine: {
  197. show: false
  198. },
  199. data: statsData.value.deviceStatsOfCategory
  200. }
  201. ]
  202. })
  203. // 在线设备统计
  204. initGaugeChart(chartDeviceOnline.value, statsData.value.onlineTotal, '#0d9')
  205. // 离线设备统计
  206. initGaugeChart(chartDeviceOffline.value, statsData.value.offlineTotal, '#f50')
  207. // 待激活设备统计
  208. initGaugeChart(chartDeviceActive.value, statsData.value.neverOnlineTotal, '#05b')
  209. // 消息量统计
  210. initMessageChart()
  211. }
  212. /** 初始化仪表盘图表 */
  213. const initGaugeChart = (el: any, value: number, color: string) => {
  214. echarts.init(el).setOption({
  215. series: [
  216. {
  217. type: 'gauge',
  218. startAngle: 360,
  219. endAngle: 0,
  220. progress: {
  221. show: true,
  222. width: 12,
  223. itemStyle: {
  224. color: color
  225. }
  226. },
  227. axisLine: {
  228. lineStyle: {
  229. width: 12,
  230. color: [[1, '#E5E7EB']]
  231. }
  232. },
  233. axisTick: { show: false },
  234. splitLine: { show: false },
  235. axisLabel: { show: false },
  236. pointer: { show: false },
  237. anchor: { show: false },
  238. title: { show: false },
  239. detail: {
  240. valueAnimation: true,
  241. fontSize: 24,
  242. fontWeight: 'bold',
  243. fontFamily: 'Inter, sans-serif',
  244. color: color,
  245. offsetCenter: [0, '0'],
  246. formatter: (value: number) => {
  247. return value + '个'
  248. }
  249. },
  250. data: [{ value: value }]
  251. }
  252. ]
  253. })
  254. }
  255. /** 初始化消息统计图表 */
  256. const initMessageChart = () => {
  257. const xdata: string[] = []
  258. const upData: string[] = []
  259. const downData: string[] = []
  260. statsData.value.deviceUpMessageStats.forEach((msg) => {
  261. xdata.push(formatDate(msg.time, 'YYYY-MM-DD HH:mm'))
  262. upData.push(msg.data)
  263. })
  264. statsData.value.deviceDownMessageStats.forEach((msg) => {
  265. downData.push(msg.data)
  266. })
  267. echarts.init(chartMsgStat.value).setOption({
  268. tooltip: {
  269. trigger: 'axis',
  270. backgroundColor: 'rgba(255, 255, 255, 0.9)',
  271. borderColor: '#E5E7EB',
  272. textStyle: {
  273. color: '#374151'
  274. }
  275. },
  276. legend: {
  277. data: ['上行消息量', '下行消息量'],
  278. textStyle: {
  279. color: '#374151',
  280. fontWeight: 500
  281. }
  282. },
  283. grid: {
  284. left: '3%',
  285. right: '4%',
  286. bottom: '3%',
  287. containLabel: true
  288. },
  289. xAxis: {
  290. type: 'category',
  291. boundaryGap: false,
  292. data: xdata,
  293. axisLine: {
  294. lineStyle: {
  295. color: '#E5E7EB'
  296. }
  297. },
  298. axisLabel: {
  299. color: '#6B7280'
  300. }
  301. },
  302. yAxis: {
  303. type: 'value',
  304. axisLine: {
  305. lineStyle: {
  306. color: '#E5E7EB'
  307. }
  308. },
  309. axisLabel: {
  310. color: '#6B7280'
  311. },
  312. splitLine: {
  313. lineStyle: {
  314. color: '#F3F4F6'
  315. }
  316. }
  317. },
  318. series: [
  319. {
  320. name: '上行消息量',
  321. type: 'line',
  322. stack: 'Total',
  323. data: upData,
  324. itemStyle: {
  325. color: '#3B82F6'
  326. },
  327. lineStyle: {
  328. width: 2
  329. },
  330. areaStyle: {
  331. color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
  332. { offset: 0, color: 'rgba(59, 130, 246, 0.2)' },
  333. { offset: 1, color: 'rgba(59, 130, 246, 0)' }
  334. ])
  335. }
  336. },
  337. {
  338. name: '下行消息量',
  339. type: 'line',
  340. stack: 'Total',
  341. data: downData,
  342. itemStyle: {
  343. color: '#10B981'
  344. },
  345. lineStyle: {
  346. width: 2
  347. },
  348. areaStyle: {
  349. color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
  350. { offset: 0, color: 'rgba(16, 185, 129, 0.2)' },
  351. { offset: 1, color: 'rgba(16, 185, 129, 0)' }
  352. ])
  353. }
  354. }
  355. ]
  356. })
  357. }
  358. /** 初始化 */
  359. onMounted(() => {
  360. if (document.getElementById('breadcrumb-container')) {
  361. document.getElementById('breadcrumb-container')!.style.display = 'none'
  362. }
  363. getStats()
  364. })
  365. </script>
  366. <style lang="scss" scoped>
  367. .stat-card {
  368. @apply bg-white rounded overflow-hidden;
  369. :deep(.el-card__body) {
  370. @apply p-3;
  371. }
  372. .el-divider {
  373. @apply my-2;
  374. }
  375. }
  376. .chart-card {
  377. @apply bg-white rounded overflow-hidden;
  378. :deep(.el-card__header) {
  379. @apply py-2 px-3 border-b border-gray-100 bg-white;
  380. }
  381. :deep(.el-card__body) {
  382. @apply p-3;
  383. }
  384. }
  385. // 修改图表配色方案,使其更加柔和
  386. :deep(.echarts) {
  387. .tooltip {
  388. @apply bg-white/90 border border-gray-200 shadow-sm;
  389. }
  390. .axis-line {
  391. @apply text-gray-300;
  392. }
  393. .split-line {
  394. @apply text-gray-100;
  395. }
  396. }
  397. </style>