index.vue 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549
  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. <div class="flex justify-between items-center mb-1">
  10. <span class="text-gray-500 text-base font-medium">品类数量</span>
  11. <Icon icon="ep:menu" class="text-[32px] text-blue-400" />
  12. </div>
  13. <span class="text-3xl font-bold text-gray-700">{{ statsData.productCategoryCount }}</span>
  14. <el-divider class="my-2" />
  15. <div class="flex justify-between items-center text-gray-400 text-sm">
  16. <span>今日新增</span>
  17. <span class="text-green-500">↑ {{ statsData.productCategoryTodayCount }}</span>
  18. </div>
  19. </div>
  20. </el-card>
  21. </el-col>
  22. <el-col :span="6">
  23. <el-card class="stat-card" shadow="never">
  24. <div class="flex flex-col">
  25. <div class="flex justify-between items-center mb-1">
  26. <span class="text-gray-500 text-base font-medium">产品数量</span>
  27. <Icon icon="ep:box" class="text-[32px] text-orange-400" />
  28. </div>
  29. <span class="text-3xl font-bold text-gray-700">{{ statsData.productCount }}</span>
  30. <el-divider class="my-2" />
  31. <div class="flex justify-between items-center text-gray-400 text-sm">
  32. <span>今日新增</span>
  33. <span class="text-green-500">↑ {{ statsData.productTodayCount }}</span>
  34. </div>
  35. </div>
  36. </el-card>
  37. </el-col>
  38. <el-col :span="6">
  39. <el-card class="stat-card" shadow="never">
  40. <div class="flex flex-col">
  41. <div class="flex justify-between items-center mb-1">
  42. <span class="text-gray-500 text-base font-medium">设备数量</span>
  43. <Icon icon="ep:cpu" class="text-[32px] text-purple-400" />
  44. </div>
  45. <span class="text-3xl font-bold text-gray-700">{{ statsData.deviceCount }}</span>
  46. <el-divider class="my-2" />
  47. <div class="flex justify-between items-center text-gray-400 text-sm">
  48. <span>今日新增</span>
  49. <span class="text-green-500">↑ {{ statsData.deviceTodayCount }}</span>
  50. </div>
  51. </div>
  52. </el-card>
  53. </el-col>
  54. <el-col :span="6">
  55. <el-card class="stat-card" shadow="never">
  56. <div class="flex flex-col">
  57. <div class="flex justify-between items-center mb-1">
  58. <span class="text-gray-500 text-base font-medium">物模型消息</span>
  59. <Icon icon="ep:message" class="text-[32px] text-teal-400" />
  60. </div>
  61. <span class="text-3xl font-bold text-gray-700">{{ statsData.deviceMessageCount }}</span>
  62. <el-divider class="my-2" />
  63. <div class="flex justify-between items-center text-gray-400 text-sm">
  64. <span>今日新增</span>
  65. <span class="text-green-500">↑ {{ statsData.deviceMessageTodayCount }}</span>
  66. </div>
  67. </div>
  68. </el-card>
  69. </el-col>
  70. </el-row>
  71. <!-- 图表行 -->
  72. <el-row :gutter="16" class="mb-4">
  73. <el-col :span="12">
  74. <el-card class="chart-card" shadow="never">
  75. <template #header>
  76. <div class="flex items-center">
  77. <span class="text-base font-medium text-gray-600">设备数量统计</span>
  78. </div>
  79. </template>
  80. <div ref="chartDeviceNumStat" class="h-[240px]"></div>
  81. </el-card>
  82. </el-col>
  83. <el-col :span="12">
  84. <el-card class="chart-card" shadow="never">
  85. <template #header>
  86. <div class="flex items-center">
  87. <span class="text-base font-medium text-gray-600">设备状态统计</span>
  88. </div>
  89. </template>
  90. <el-row class="h-[240px]">
  91. <el-col :span="8" class="flex flex-col items-center">
  92. <div ref="chartDeviceOnline" 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-col :span="8" class="flex flex-col items-center">
  98. <div ref="chartDeviceOffline" class="h-[160px] w-full"></div>
  99. <div class="text-center mt-2">
  100. <span class="text-sm text-gray-600">离线设备</span>
  101. </div>
  102. </el-col>
  103. <el-col :span="8" class="flex flex-col items-center">
  104. <div ref="chartDeviceActive" class="h-[160px] w-full"></div>
  105. <div class="text-center mt-2">
  106. <span class="text-sm text-gray-600">待激活设备</span>
  107. </div>
  108. </el-col>
  109. </el-row>
  110. </el-card>
  111. </el-col>
  112. </el-row>
  113. <!-- 消息统计行 -->
  114. <el-row>
  115. <el-col :span="24">
  116. <el-card class="chart-card" shadow="never">
  117. <template #header>
  118. <div class="flex items-center justify-between">
  119. <span class="text-base font-medium text-gray-600">上下行消息量统计</span>
  120. <div class="flex items-center space-x-2">
  121. <el-radio-group v-model="timeRange" @change="handleTimeRangeChange">
  122. <el-radio-button label="1h">最近1小时</el-radio-button>
  123. <el-radio-button label="24h">最近24小时</el-radio-button>
  124. <el-radio-button label="7d">近一周</el-radio-button>
  125. </el-radio-group>
  126. <el-date-picker
  127. v-model="dateRange"
  128. type="datetimerange"
  129. range-separator="至"
  130. start-placeholder="开始时间"
  131. end-placeholder="结束时间"
  132. :default-time="[new Date(2000, 1, 1, 0, 0, 0), new Date(2000, 1, 1, 23, 59, 59)]"
  133. @change="handleDateRangeChange"
  134. />
  135. </div>
  136. </div>
  137. </template>
  138. <div ref="chartMsgStat" class="h-[300px]"></div>
  139. </el-card>
  140. </el-col>
  141. </el-row>
  142. </el-space>
  143. </div>
  144. </template>
  145. <script setup lang="ts" name="Index">
  146. import * as echarts from 'echarts/core'
  147. import { TooltipComponent, LegendComponent, TitleComponent, ToolboxComponent, GridComponent } from 'echarts/components'
  148. import { PieChart, LineChart, GaugeChart } from 'echarts/charts'
  149. import { LabelLayout, UniversalTransition } from 'echarts/features'
  150. import { CanvasRenderer } from 'echarts/renderers'
  151. import { ProductCategoryApi,IotStatisticsSummaryRespVO, IotStatisticsDeviceMessageSummaryRespVO} from '@/api/iot/statistics'
  152. import { formatDate } from '@/utils/formatTime'
  153. import { Icon } from '@/components/Icon'
  154. /** IoT 首页 */
  155. defineOptions({ name: 'IotHome' })
  156. const timeRange = ref('7d') // 修改默认选择为近一周
  157. const dateRange = ref<[Date, Date] | null>(null)
  158. const queryParams = reactive({
  159. startTime: Date.now() - 7 * 24 * 60 * 60 * 1000, // 设置默认开始时间为7天前
  160. endTime: Date.now() // 设置默认结束时间为当前时间
  161. })
  162. echarts.use([
  163. TooltipComponent,
  164. LegendComponent,
  165. PieChart,
  166. CanvasRenderer,
  167. LabelLayout,
  168. TitleComponent,
  169. ToolboxComponent,
  170. GridComponent,
  171. LineChart,
  172. UniversalTransition,
  173. GaugeChart
  174. ])
  175. const chartDeviceNumStat = ref()
  176. const chartDeviceOnline = ref()
  177. const chartDeviceOffline = ref()
  178. const chartDeviceActive = ref()
  179. const chartMsgStat = ref()
  180. // 基础统计数据
  181. const statsData = ref<IotStatisticsSummaryRespVO>({
  182. productCategoryCount: 0,
  183. productCount: 0,
  184. deviceCount: 0,
  185. deviceMessageCount: 0,
  186. productCategoryTodayCount: 0,
  187. productTodayCount: 0,
  188. deviceTodayCount: 0,
  189. deviceMessageTodayCount: 0,
  190. deviceOnlineCount: 0,
  191. deviceOfflineCount: 0,
  192. deviceInactiveCount: 0,
  193. productCategoryDeviceCounts: {}
  194. })
  195. // 消息统计数据
  196. const messageStats = ref<IotStatisticsDeviceMessageSummaryRespVO>({
  197. upstreamCounts: {},
  198. downstreamCounts: {}
  199. })
  200. /** 处理快捷时间范围选择 */
  201. const handleTimeRangeChange = (value: string) => {
  202. const now = Date.now()
  203. let startTime: number
  204. switch (value) {
  205. case '1h':
  206. startTime = now - 60 * 60 * 1000
  207. break
  208. case '24h':
  209. startTime = now - 24 * 60 * 60 * 1000
  210. break
  211. case '7d':
  212. startTime = now - 7 * 24 * 60 * 60 * 1000
  213. break
  214. default:
  215. return
  216. }
  217. // 清空日期选择器
  218. dateRange.value = null
  219. // 更新查询参数
  220. queryParams.startTime = startTime
  221. queryParams.endTime = now
  222. // 重新获取数据
  223. getStats()
  224. }
  225. /** 处理自定义日期范围选择 */
  226. const handleDateRangeChange = (value: [Date, Date] | null) => {
  227. if (value) {
  228. // 清空快捷选项
  229. timeRange.value = ''
  230. // 更新查询参数
  231. queryParams.startTime = value[0].getTime()
  232. queryParams.endTime = value[1].getTime()
  233. // 重新获取数据
  234. getStats()
  235. }
  236. }
  237. /** 获取统计数据 */
  238. const getStats = async () => {
  239. // 获取基础统计数据
  240. const summaryRes = await ProductCategoryApi.getIotStatisticsSummary()
  241. statsData.value = summaryRes
  242. // 获取消息统计数据
  243. const messageRes = await ProductCategoryApi.getIotStatisticsDeviceMessageSummary(queryParams)
  244. messageStats.value = messageRes
  245. // 初始化图表
  246. initCharts()
  247. }
  248. /** 初始化图表 */
  249. const initCharts = () => {
  250. // 设备数量统计
  251. echarts.init(chartDeviceNumStat.value).setOption({
  252. tooltip: {
  253. trigger: 'item'
  254. },
  255. legend: {
  256. top: '5%',
  257. right: '10%',
  258. align: 'left',
  259. orient: 'vertical',
  260. icon: 'circle'
  261. },
  262. series: [
  263. {
  264. name: 'Access From',
  265. type: 'pie',
  266. radius: ['50%', '80%'],
  267. avoidLabelOverlap: false,
  268. center: ['30%', '50%'],
  269. label: {
  270. show: false,
  271. position: 'outside'
  272. },
  273. emphasis: {
  274. label: {
  275. show: true,
  276. fontSize: 20,
  277. fontWeight: 'bold'
  278. }
  279. },
  280. labelLine: {
  281. show: false
  282. },
  283. data: Object.entries(statsData.value.productCategoryDeviceCounts).map(([name, value]) => ({
  284. name,
  285. value
  286. }))
  287. }
  288. ]
  289. })
  290. // 在线设备统计
  291. initGaugeChart(chartDeviceOnline.value, statsData.value.deviceOnlineCount, '#0d9')
  292. // 离线设备统计
  293. initGaugeChart(chartDeviceOffline.value, statsData.value.deviceOfflineCount, '#f50')
  294. // 待激活设备统计
  295. initGaugeChart(chartDeviceActive.value, statsData.value.deviceInactiveCount, '#05b')
  296. // 消息量统计
  297. initMessageChart()
  298. }
  299. /** 初始化仪表盘图表 */
  300. const initGaugeChart = (el: any, value: number, color: string) => {
  301. echarts.init(el).setOption({
  302. series: [
  303. {
  304. type: 'gauge',
  305. startAngle: 360,
  306. endAngle: 0,
  307. min: 0,
  308. max: statsData.value.deviceCount || 100, // 使用设备总数作为最大值
  309. progress: {
  310. show: true,
  311. width: 12,
  312. itemStyle: {
  313. color: color
  314. }
  315. },
  316. axisLine: {
  317. lineStyle: {
  318. width: 12,
  319. color: [[1, '#E5E7EB']]
  320. }
  321. },
  322. axisTick: { show: false },
  323. splitLine: { show: false },
  324. axisLabel: { show: false },
  325. pointer: { show: false },
  326. anchor: { show: false },
  327. title: { show: false },
  328. detail: {
  329. valueAnimation: true,
  330. fontSize: 24,
  331. fontWeight: 'bold',
  332. fontFamily: 'Inter, sans-serif',
  333. color: color,
  334. offsetCenter: [0, '0'],
  335. formatter: (value: number) => {
  336. return `${value}个`
  337. }
  338. },
  339. data: [{ value: value }]
  340. }
  341. ]
  342. })
  343. }
  344. /** 初始化消息统计图表 */
  345. const initMessageChart = () => {
  346. // 获取所有时间戳并排序
  347. const timestamps = Array.from(
  348. new Set([
  349. ...messageStats.value.upstreamCounts.map(item => Number(Object.keys(item)[0])),
  350. ...messageStats.value.downstreamCounts.map(item => Number(Object.keys(item)[0]))
  351. ])
  352. ).sort((a, b) => a - b) // 确保时间戳从小到大排序
  353. // 准备数据
  354. const xdata = timestamps.map(ts => formatDate(ts, 'YYYY-MM-DD HH:mm'))
  355. const upData = timestamps.map(ts => {
  356. const item = messageStats.value.upstreamCounts.find(
  357. count => Number(Object.keys(count)[0]) === ts
  358. )
  359. return item ? Object.values(item)[0] : 0
  360. })
  361. const downData = timestamps.map(ts => {
  362. const item = messageStats.value.downstreamCounts.find(
  363. count => Number(Object.keys(count)[0]) === ts
  364. )
  365. return item ? Object.values(item)[0] : 0
  366. })
  367. // 配置图表
  368. echarts.init(chartMsgStat.value).setOption({
  369. tooltip: {
  370. trigger: 'axis',
  371. backgroundColor: 'rgba(255, 255, 255, 0.9)',
  372. borderColor: '#E5E7EB',
  373. textStyle: {
  374. color: '#374151'
  375. }
  376. },
  377. legend: {
  378. data: ['上行消息量', '下行消息量'],
  379. textStyle: {
  380. color: '#374151',
  381. fontWeight: 500
  382. }
  383. },
  384. grid: {
  385. left: '3%',
  386. right: '4%',
  387. bottom: '3%',
  388. containLabel: true
  389. },
  390. xAxis: {
  391. type: 'category',
  392. boundaryGap: false,
  393. data: xdata,
  394. axisLine: {
  395. lineStyle: {
  396. color: '#E5E7EB'
  397. }
  398. },
  399. axisLabel: {
  400. color: '#6B7280'
  401. }
  402. },
  403. yAxis: {
  404. type: 'value',
  405. axisLine: {
  406. lineStyle: {
  407. color: '#E5E7EB'
  408. }
  409. },
  410. axisLabel: {
  411. color: '#6B7280'
  412. },
  413. splitLine: {
  414. lineStyle: {
  415. color: '#F3F4F6'
  416. }
  417. }
  418. },
  419. series: [
  420. {
  421. name: '上行消息量',
  422. type: 'line',
  423. smooth: true, // 添加平滑曲线
  424. data: upData,
  425. itemStyle: {
  426. color: '#3B82F6'
  427. },
  428. lineStyle: {
  429. width: 2
  430. },
  431. areaStyle: {
  432. color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
  433. { offset: 0, color: 'rgba(59, 130, 246, 0.2)' },
  434. { offset: 1, color: 'rgba(59, 130, 246, 0)' }
  435. ])
  436. }
  437. },
  438. {
  439. name: '下行消息量',
  440. type: 'line',
  441. smooth: true, // 添加平滑曲线
  442. data: downData,
  443. itemStyle: {
  444. color: '#10B981'
  445. },
  446. lineStyle: {
  447. width: 2
  448. },
  449. areaStyle: {
  450. color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
  451. { offset: 0, color: 'rgba(16, 185, 129, 0.2)' },
  452. { offset: 1, color: 'rgba(16, 185, 129, 0)' }
  453. ])
  454. }
  455. }
  456. ]
  457. })
  458. }
  459. /** 初始化 */
  460. onMounted(() => {
  461. if (document.getElementById('breadcrumb-container')) {
  462. document.getElementById('breadcrumb-container')!.style.display = 'none'
  463. }
  464. getStats()
  465. })
  466. </script>
  467. <style lang="scss" scoped>
  468. .stat-card {
  469. @apply bg-white rounded overflow-hidden;
  470. :deep(.el-card__body) {
  471. @apply p-3;
  472. }
  473. .el-divider {
  474. @apply my-2;
  475. }
  476. }
  477. .chart-card {
  478. @apply bg-white rounded overflow-hidden;
  479. :deep(.el-card__header) {
  480. @apply py-2 px-3 border-b border-gray-100 bg-white;
  481. }
  482. :deep(.el-card__body) {
  483. @apply p-3;
  484. }
  485. }
  486. // 修改图表配色方案,使其更加柔和
  487. :deep(.echarts) {
  488. .tooltip {
  489. @apply bg-white/90 border border-gray-200 shadow-sm;
  490. }
  491. .axis-line {
  492. @apply text-gray-300;
  493. }
  494. .split-line {
  495. @apply text-gray-100;
  496. }
  497. }
  498. // 添加时间选择器样式
  499. :deep(.el-radio-group) {
  500. @apply mr-4;
  501. }
  502. :deep(.el-date-editor) {
  503. @apply w-[360px];
  504. }
  505. </style>