lipenghui 3 luni în urmă
părinte
comite
31e52f60ba
2 a modificat fișierele cu 808 adăugiri și 0 ștergeri
  1. 20 0
      src/api/pms/stat/index.ts
  2. 788 0
      src/views/pms/stat/maintain.vue

+ 20 - 0
src/api/pms/stat/index.ts

@@ -0,0 +1,20 @@
+import request from '@/config/axios'
+
+// 设备台账 API
+export const IotStatApi = {
+  getMainDay: async (params: any) => {
+    return await request.get({ url: `/rq/stat/main/day` })
+  },
+  getMainWeek: async (params: any) => {
+    return await request.get({ url: `/rq/stat/main/week` })
+  },
+  getMainMonth: async (params: any) => {
+    return await request.get({ url: `/rq/stat/main/month` })
+  },
+  getMainTotal: async (params: any) => {
+    return await request.get({ url: `/rq/stat/main/total` })
+  },
+  getMainStatus: async (params: any) => {
+    return await request.get({ url: `/rq/stat/main/status` })
+  },
+}

+ 788 - 0
src/views/pms/stat/maintain.vue

@@ -0,0 +1,788 @@
+<template>
+  <!-- 第一行:统计卡片行 -->
+  <el-row :gutter="16" class="mb-4">
+    <el-col :span="6">
+      <el-card class="stat-card" shadow="never">
+        <div class="flex flex-col">
+          <div class="flex justify-between items-center text-gray-400">
+            <span>昨日工单数量</span>
+          </div>
+          <el-divider />
+          <div class="flex justify-between items-center mb-1">
+            <span class="text-gray-500 text-base font-medium" style="font-size: 14px"
+              >故障上报</span
+            >
+            <span class="text-gray-500 text-base font-medium" style="font-size: 14px"
+              >维修工单</span
+            >
+          </div>
+          <div class="flex justify-between items-center mb-1">
+            <span class="text-3xl font-bold text-gray-700">
+              {{ day.failureDay }}
+            </span>
+            <span class="text-3xl font-bold text-gray-700">
+              {{ day.maintainDay }}
+            </span>
+          </div>
+          <!--          <el-divider class="my-2" />-->
+          <!--          <div class="flex justify-between items-center text-gray-400 text-sm">-->
+          <!--            <span>今日新增</span>-->
+          <!--            <span class="text-green-500">+{{ statsData.productCategoryTodayCount }}</span>-->
+          <!--          </div>-->
+        </div>
+      </el-card>
+    </el-col>
+    <el-col :span="6">
+      <el-card class="stat-card" shadow="never">
+        <div class="flex flex-col">
+          <div class="flex justify-between items-center text-gray-400">
+            <span>近一周工单数量</span>
+          </div>
+          <el-divider />
+          <div class="flex justify-between items-center mb-1">
+            <span class="text-gray-500 text-base font-medium" style="font-size: 14px"
+              >故障上报</span
+            >
+            <span class="text-gray-500 text-base font-medium" style="font-size: 14px"
+              >维修工单</span
+            >
+          </div>
+          <div class="flex justify-between items-center mb-1">
+            <span class="text-3xl font-bold text-gray-700">
+              {{ week.failureWeek }}
+            </span>
+            <span class="text-3xl font-bold text-gray-700">
+              {{ week.maintainWeek }}
+            </span>
+          </div>
+          <!--          <el-divider class="my-2" />-->
+          <!--          <div class="flex justify-between items-center text-gray-400 text-sm">-->
+          <!--            <span>今日新增</span>-->
+          <!--            <span class="text-green-500">+{{ statsData.productCategoryTodayCount }}</span>-->
+          <!--          </div>-->
+        </div>
+      </el-card>
+    </el-col>
+    <el-col :span="6">
+      <el-card class="stat-card" shadow="never">
+        <div class="flex flex-col">
+          <div class="flex justify-between items-center text-gray-400">
+            <span>近一月工单数量</span>
+          </div>
+          <el-divider />
+          <div class="flex justify-between items-center mb-1">
+            <span class="text-gray-500 text-base font-medium" style="font-size: 14px"
+              >故障上报</span
+            >
+            <span class="text-gray-500 text-base font-medium" style="font-size: 14px"
+              >维修工单</span
+            >
+          </div>
+          <div class="flex justify-between items-center mb-1">
+            <span class="text-3xl font-bold text-gray-700">
+              {{ month.failureMonth }}
+            </span>
+            <span class="text-3xl font-bold text-gray-700">
+              {{ month.maintainMonth }}
+            </span>
+          </div>
+          <!--          <el-divider class="my-2" />-->
+          <!--          <div class="flex justify-between items-center text-gray-400 text-sm">-->
+          <!--            <span>今日新增</span>-->
+          <!--            <span class="text-green-500">+{{ statsData.productCategoryTodayCount }}</span>-->
+          <!--          </div>-->
+        </div>
+      </el-card>
+    </el-col>
+    <el-col :span="6">
+      <el-card class="stat-card" shadow="never">
+        <div class="flex flex-col">
+          <div class="flex justify-between items-center text-gray-400">
+            <span>工单总数量</span>
+          </div>
+          <el-divider />
+          <div class="flex justify-between items-center mb-1">
+            <span class="text-gray-500 text-base font-medium" style="font-size: 14px"
+              >故障上报</span
+            >
+            <span class="text-gray-500 text-base font-medium" style="font-size: 14px"
+              >维修工单</span
+            >
+          </div>
+          <div class="flex justify-between items-center mb-1">
+            <span class="text-3xl font-bold text-gray-700">
+              {{ total.failureTotal }}
+            </span>
+            <span class="text-3xl font-bold text-gray-700">
+              {{ total.maintainTotal }}
+            </span>
+          </div>
+          <!--          <el-divider class="my-2" />-->
+          <!--          <div class="flex justify-between items-center text-gray-400 text-sm">-->
+          <!--            <span>今日新增</span>-->
+          <!--            <span class="text-green-500">+{{ statsData.productCategoryTodayCount }}</span>-->
+          <!--          </div>-->
+        </div>
+      </el-card>
+    </el-col>
+  </el-row>
+
+  <!-- 第二行:图表行 -->
+  <el-row :gutter="16" class="mb-4">
+    <el-col :span="12">
+      <el-card class="chart-card" shadow="never">
+        <template #header>
+          <div class="flex items-center">
+            <span class="text-base font-medium text-gray-600">故障上报状态统计</span>
+          </div>
+        </template>
+        <el-row class="h-[240px]">
+          <el-col :span="6" class="flex flex-col items-center">
+            <div ref="reportingChartRef" class="h-[160px] w-full"></div>
+            <div class="text-center mt-2">
+              <span class="text-sm text-gray-600">上报中</span>
+            </div>
+          </el-col>
+          <el-col :span="6" class="flex flex-col items-center">
+            <div ref="dealFinishedChartRef" class="h-[160px] w-full"></div>
+            <div class="text-center mt-2">
+              <span class="text-sm text-gray-600">处理完成</span>
+            </div>
+          </el-col>
+          <el-col :span="6" class="flex flex-col items-center">
+            <div ref="transOrderChartRef" class="h-[160px] w-full"></div>
+            <div class="text-center mt-2">
+              <span class="text-sm text-gray-600">转工单</span>
+            </div>
+          </el-col>
+          <el-col :span="6" class="flex flex-col items-center">
+            <div ref="orderFinishChartRef" class="h-[160px] w-full"></div>
+            <div class="text-center mt-2">
+              <span class="text-sm text-gray-600">工单处理完成</span>
+            </div>
+          </el-col>
+        </el-row>
+      </el-card>
+    </el-col>
+    <el-col :span="12">
+      <el-card class="chart-card" shadow="never">
+        <template #header>
+          <div class="flex items-center">
+            <span class="text-base font-medium text-gray-600">维修工单状态统计</span>
+          </div>
+        </template>
+        <el-row class="h-[240px]">
+          <el-col :span="12" class="flex flex-col items-center">
+            <div ref="writeChartRef" class="h-[160px] w-full"></div>
+            <div class="text-center mt-2">
+              <span class="text-sm text-gray-600">待填写</span>
+            </div>
+          </el-col>
+          <el-col :span="12" class="flex flex-col items-center">
+            <div ref="finishedChartRef" class="h-[160px] w-full"></div>
+            <div class="text-center mt-2">
+              <span class="text-sm text-gray-600">已完成</span>
+            </div>
+          </el-col>
+        </el-row>
+      </el-card>
+    </el-col>
+  </el-row>
+
+  <!-- 第三行:消息统计行 -->
+  <el-row>
+    <el-col :span="24">
+      <el-card class="chart-card" shadow="never">
+        <div ref="chartContainer" class="chart-container"></div>
+      </el-card>
+    </el-col>
+  </el-row>
+
+  <!-- TODO 第四行:地图 -->
+</template>
+
+<script setup lang="ts" name="Index">
+import * as echarts from 'echarts/core'
+import {
+  GridComponent,
+  LegendComponent,
+  TitleComponent,
+  ToolboxComponent,
+  TooltipComponent
+} from 'echarts/components'
+import { GaugeChart, LineChart, PieChart } from 'echarts/charts'
+import { LabelLayout, UniversalTransition } from 'echarts/features'
+import { CanvasRenderer } from 'echarts/renderers'
+import {
+  IotStatisticsDeviceMessageSummaryRespVO,
+  IotStatisticsSummaryRespVO
+} from '@/api/iot/statistics'
+import { formatDate } from '@/utils/formatTime'
+import { IotStatApi } from '@/api/pms/stat'
+
+// TODO @super:参考下 /Users/yunai/Java/yudao-ui-admin-vue3/src/views/mall/home/index.vue,拆一拆组件
+
+/** IoT 首页 */
+defineOptions({ name: 'IoTHome' })
+
+// TODO @super:使用下 Echart 组件,参考 yudao-ui-admin-vue3/src/views/mall/home/components/TradeTrendCard.vue 等
+echarts.use([
+  TooltipComponent,
+  LegendComponent,
+  PieChart,
+  CanvasRenderer,
+  LabelLayout,
+  TitleComponent,
+  ToolboxComponent,
+  GridComponent,
+  LineChart,
+  UniversalTransition,
+  GaugeChart
+])
+
+const timeRange = ref('7d') // 修改默认选择为近一周
+const dateRange = ref<[Date, Date] | null>(null)
+
+const queryParams = reactive({
+  startTime: Date.now() - 7 * 24 * 60 * 60 * 1000, // 设置默认开始时间为 7 天前
+  endTime: Date.now() // 设置默认结束时间为当前时间
+})
+
+// const deviceCountChartRef = ref() // 设备数量统计的图表
+const reportingChartRef = ref() // 在线设备统计的图表
+const dealFinishedChartRef = ref() // 离线设备统计的图表
+const transOrderChartRef = ref() // 待激活设备统计的图表
+const orderFinishChartRef = ref()
+const deviceMessageCountChartRef = ref() // 上下行消息量统计的图表
+const writeChartRef = ref() // 上下行消息量统计的图表
+const finishedChartRef = ref() // 上下行消息量统计的图表
+
+// 基础统计数据
+// TODO @super:初始为 -1,然后界面展示先是加载中?试试用 cursor 改哈
+const statsData = ref<IotStatisticsSummaryRespVO>({
+  productCategoryCount: 0,
+  productCount: 0,
+  deviceCount: 0,
+  deviceMessageCount: 0,
+  productCategoryTodayCount: 0,
+  productTodayCount: 0,
+  deviceTodayCount: 0,
+  deviceMessageTodayCount: 0,
+  deviceOnlineCount: 0,
+  deviceOfflineCount: 0,
+  deviceInactiveCount: 0,
+  productCategoryDeviceCounts: {}
+})
+
+const day = ref({
+  failureDay: undefined,
+  maintainDay: undefined
+})
+const week = ref({
+  failureWeek: undefined,
+  maintainWeek: undefined
+})
+const month = ref({
+  failureMonth: undefined,
+  maintainMonth: undefined
+})
+const total = ref({
+  failureTotal: undefined,
+  maintainTotal: undefined
+})
+
+const status = ref({
+  failureStatus: {
+    reporting: 0,
+    trans: 0,
+    finished: 0,
+    orderFinished: 0
+  },
+  maintainStatus: {
+    finished: 0,
+    todo: 0
+  }
+})
+// 消息统计数据
+const messageStats = ref<IotStatisticsDeviceMessageSummaryRespVO>({
+  upstreamCounts: {},
+  downstreamCounts: {}
+})
+
+/** 处理快捷时间范围选择 */
+const handleTimeRangeChange = (timeRange: string) => {
+  const now = Date.now()
+  let startTime: number
+
+  // TODO @super:这个的计算,看看能不能结合 dayjs 简化。因为 1h、24h、7d 感觉是比较标准的。如果没有,抽到 utils/formatTime.ts 作为一个工具方法
+  switch (timeRange) {
+    case '1h':
+      startTime = now - 60 * 60 * 1000
+      break
+    case '24h':
+      startTime = now - 24 * 60 * 60 * 1000
+      break
+    case '7d':
+      startTime = now - 7 * 24 * 60 * 60 * 1000
+      break
+    default:
+      return
+  }
+
+  // 清空日期选择器
+  dateRange.value = null
+
+  // 更新查询参数
+  queryParams.startTime = startTime
+  queryParams.endTime = now
+
+  // 重新获取数据
+  getStats()
+}
+
+/** 处理自定义日期范围选择 */
+const handleDateRangeChange = (value: [Date, Date] | null) => {
+  if (value) {
+    // 清空快捷选项
+    timeRange.value = ''
+
+    // 更新查询参数
+    queryParams.startTime = value[0].getTime()
+    queryParams.endTime = value[1].getTime()
+
+    // 重新获取数据
+    getStats()
+  }
+}
+
+/** 获取统计数据 */
+const getStats = async () => {
+  // 获取基础统计数据
+  IotStatApi.getMainDay().then((res) => {
+    day.value = res
+  })
+  IotStatApi.getMainWeek().then((res) => {
+    week.value = res
+  })
+  IotStatApi.getMainMonth().then((res) => {
+    month.value = res
+  })
+  IotStatApi.getMainTotal().then((res) => {
+    total.value = res
+  })
+  IotStatApi.getMainStatus().then((res) => {
+    debugger
+    status.value = res
+    console.log(JSON.stringify(status.value))
+    initCharts()
+  })
+  // statsData.value = await ProductCategoryApi.getIotStatisticsSummary()
+
+  //
+  // // 获取消息统计数据
+  // messageStats.value = await ProductCategoryApi.getIotStatisticsDeviceMessageSummary(queryParams)
+
+  // 初始化图表
+}
+
+/** 初始化图表 */
+const initCharts = () => {
+  // 设备数量统计
+  // echarts.init(deviceCountChartRef.value).setOption({
+  //   tooltip: {
+  //     trigger: 'item'
+  //   },
+  //   legend: {
+  //     top: '5%',
+  //     right: '10%',
+  //     align: 'left',
+  //     orient: 'vertical',
+  //     icon: 'circle'
+  //   },
+  //   series: [
+  //     {
+  //       name: 'Access From',
+  //       type: 'pie',
+  //       radius: ['50%', '80%'],
+  //       avoidLabelOverlap: false,
+  //       center: ['30%', '50%'],
+  //       label: {
+  //         show: false,
+  //         position: 'outside'
+  //       },
+  //       emphasis: {
+  //         label: {
+  //           show: true,
+  //           fontSize: 20,
+  //           fontWeight: 'bold'
+  //         }
+  //       },
+  //       labelLine: {
+  //         show: false
+  //       },
+  //       data: Object.entries(statsData.value.productCategoryDeviceCounts).map(([name, value]) => ({
+  //         name,
+  //         value
+  //       }))
+  //     }
+  //   ]
+  // })
+
+  // 上报中
+  initGaugeChart(
+    reportingChartRef.value,
+    status.value.failureStatus.reporting === undefined ? 0 : status.value.failureStatus.reporting,
+    '#0d9'
+  )
+  // 处理完成
+  initGaugeChart(
+    dealFinishedChartRef.value,
+    status.value.failureStatus.finished === undefined ? 0 : status.value.failureStatus.finished,
+    '#f50'
+  )
+  // 转工单
+  initGaugeChart(
+    transOrderChartRef.value,
+    status.value.failureStatus.trans === undefined ? 0 : status.value.failureStatus.trans,
+    '#05b'
+  )
+  // 工单完成
+  initGaugeChart(
+    orderFinishChartRef.value,
+    status.value.failureStatus.orderFinished === undefined
+      ? 0
+      : status.value.failureStatus.orderFinished,
+    '#05b'
+  )
+  // 待填写
+  initGaugeChart(
+    writeChartRef.value,
+    status.value.maintainStatus.todo === undefined ? 0 : status.value.maintainStatus.todo,
+    '#05b'
+  )
+  //已完成
+  initGaugeChart(
+    finishedChartRef.value,
+    status.value.maintainStatus.finished === undefined ? 0 : status.value.maintainStatus.finished,
+    '#05b'
+  )
+  // 消息量统计
+  //initMessageChart()
+}
+
+/** 初始化仪表盘图表 */
+const initGaugeChart = (el: any, value: number, color: string) => {
+  echarts.init(el).setOption({
+    series: [
+      {
+        type: 'gauge',
+        startAngle: 360,
+        endAngle: 0,
+        min: 0,
+        max: statsData.value.deviceCount || 100, // 使用设备总数作为最大值
+        progress: {
+          show: true,
+          width: 12,
+          itemStyle: {
+            color: color
+          }
+        },
+        axisLine: {
+          lineStyle: {
+            width: 12,
+            color: [[1, '#E5E7EB']]
+          }
+        },
+        axisTick: { show: false },
+        splitLine: { show: false },
+        axisLabel: { show: false },
+        pointer: { show: false },
+        anchor: { show: false },
+        title: { show: false },
+        detail: {
+          valueAnimation: true,
+          fontSize: 24,
+          fontWeight: 'bold',
+          fontFamily: 'Inter, sans-serif',
+          color: color,
+          offsetCenter: [0, '0'],
+          formatter: (value: number) => {
+            return `${value} 个`
+          }
+        },
+        data: [{ value: value }]
+      }
+    ]
+  })
+}
+
+/** 初始化消息统计图表 */
+const initMessageChart = () => {
+  // 获取所有时间戳并排序
+  // TODO @super:一些 idea 里的红色报错,要去处理掉噢。
+  const timestamps = Array.from(
+    new Set([
+      ...messageStats.value.upstreamCounts.map((item) => Number(Object.keys(item)[0])),
+      ...messageStats.value.downstreamCounts.map((item) => Number(Object.keys(item)[0]))
+    ])
+  ).sort((a, b) => a - b) // 确保时间戳从小到大排序
+
+  // 准备数据
+  const xdata = timestamps.map((ts) => formatDate(ts, 'YYYY-MM-DD HH:mm'))
+  const upData = timestamps.map((ts) => {
+    const item = messageStats.value.upstreamCounts.find(
+      (count) => Number(Object.keys(count)[0]) === ts
+    )
+    return item ? Object.values(item)[0] : 0
+  })
+  const downData = timestamps.map((ts) => {
+    const item = messageStats.value.downstreamCounts.find(
+      (count) => Number(Object.keys(count)[0]) === ts
+    )
+    return item ? Object.values(item)[0] : 0
+  })
+
+  // 配置图表
+  echarts.init(deviceMessageCountChartRef.value).setOption({
+    tooltip: {
+      trigger: 'axis',
+      backgroundColor: 'rgba(255, 255, 255, 0.9)',
+      borderColor: '#E5E7EB',
+      textStyle: {
+        color: '#374151'
+      }
+    },
+    legend: {
+      data: ['上行消息量', '下行消息量'],
+      textStyle: {
+        color: '#374151',
+        fontWeight: 500
+      }
+    },
+    grid: {
+      left: '3%',
+      right: '4%',
+      bottom: '3%',
+      containLabel: true
+    },
+    xAxis: {
+      type: 'category',
+      boundaryGap: false,
+      data: xdata,
+      axisLine: {
+        lineStyle: {
+          color: '#E5E7EB'
+        }
+      },
+      axisLabel: {
+        color: '#6B7280'
+      }
+    },
+    yAxis: {
+      type: 'value',
+      axisLine: {
+        lineStyle: {
+          color: '#E5E7EB'
+        }
+      },
+      axisLabel: {
+        color: '#6B7280'
+      },
+      splitLine: {
+        lineStyle: {
+          color: '#F3F4F6'
+        }
+      }
+    },
+    series: [
+      {
+        name: '上行消息量',
+        type: 'line',
+        smooth: true, // 添加平滑曲线
+        data: upData,
+        itemStyle: {
+          color: '#3B82F6'
+        },
+        lineStyle: {
+          width: 2
+        },
+        areaStyle: {
+          color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+            { offset: 0, color: 'rgba(59, 130, 246, 0.2)' },
+            { offset: 1, color: 'rgba(59, 130, 246, 0)' }
+          ])
+        }
+      },
+      {
+        name: '下行消息量',
+        type: 'line',
+        smooth: true, // 添加平滑曲线
+        data: downData,
+        itemStyle: {
+          color: '#10B981'
+        },
+        lineStyle: {
+          width: 2
+        },
+        areaStyle: {
+          color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+            { offset: 0, color: 'rgba(16, 185, 129, 0.2)' },
+            { offset: 1, color: 'rgba(16, 185, 129, 0)' }
+          ])
+        }
+      }
+    ]
+  })
+}
+
+const chartContainer = ref(null)
+let chartInstance = null
+
+// 生成过去12个月份的标签 (格式: YYYY-MM)
+const generateMonthLabels = () => {
+  const months = []
+  const date = new Date()
+  for (let i = 11; i >= 0; i--) {
+    const tempDate = new Date(date.getFullYear(), date.getMonth() - i, 1)
+    const year = tempDate.getFullYear()
+    const month = String(tempDate.getMonth() + 1).padStart(2, '0')
+    months.push(`${year}-${month}`)
+  }
+  return months
+}
+
+// 模拟数据获取
+const fetchChartData = async () => {
+  // 模拟异步请求
+  return new Promise((resolve) => {
+    setTimeout(() => {
+      resolve({
+        months: generateMonthLabels(),
+        faults: [100,100,1001,100,100,100,100,1001,100,100,100,100],
+        repairs: [100,100,1001,100,100,100,100,1001,100,100,100,100],
+      })
+    }, 300)
+  })
+}
+
+// 初始化图表配置
+const initChart = async () => {
+  if (!chartContainer.value) return
+
+  // 获取数据
+  const { months, faults, repairs } = await fetchChartData()
+
+  // ECharts配置
+  const option = {
+    tooltip: {
+      trigger: 'axis',
+      axisPointer: {
+        type: 'shadow'
+      },
+      formatter: (params) => {
+        return `${params[0].axisValue}<br/>
+                ${params[0].marker} ${params[0].seriesName}: ${params[0].value}<br/>
+                ${params[1].marker} ${params[1].seriesName}: ${params[1].value}`
+      }
+    },
+    legend: {
+      data: ['故障上报数量', '维修工单数量'],
+      top: 25
+    },
+    grid: {
+      left: '3%',
+      right: '4%',
+      bottom: '3%',
+      containLabel: true
+    },
+    xAxis: {
+      type: 'category',
+      data: months,
+      axisLabel: {
+        rotate: 45,
+        margin: 15
+      }
+    },
+    yAxis: {
+      type: 'value',
+      axisLabel: {
+        formatter: (value) => Math.floor(value).toString()
+      }
+    },
+    series: [
+      {
+        name: '故障上报数量',
+        type: 'bar',
+        barGap: 0,
+        itemStyle: {
+          color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+            { offset: 0, color: '#83bff6' },
+            { offset: 0.5, color: '#188df0' },
+            { offset: 1, color: '#188df0' }
+          ])
+        },
+        emphasis: {
+          focus: 'series'
+        },
+        data: faults
+      },
+      {
+        name: '维修工单数量',
+        type: 'bar',
+        itemStyle: {
+          color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+            { offset: 0, color: '#37a2da' },
+            { offset: 0.5, color: '#37a2da' },
+            { offset: 1, color: '#67e0e3' }
+          ])
+        },
+        emphasis: {
+          focus: 'series'
+        },
+        data: repairs
+      }
+    ]
+  }
+
+  // 初始化图表
+  chartInstance = echarts.init(chartContainer.value)
+  chartInstance.setOption(option)
+
+  // 窗口缩放监听
+  window.addEventListener('resize', handleResize)
+  handleResize()
+}
+
+// 自适应调整
+const handleResize = () => {
+  chartInstance?.resize()
+}
+/** 初始化 */
+onMounted(() => {
+  getStats()
+  initChart()
+})
+onUnmounted(() => {
+  chartInstance?.dispose()
+  window.removeEventListener('resize', handleResize)
+})
+</script>
+
+<style lang="scss" scoped>
+.chart-container {
+  width: 100%;
+  height: 600px;
+  margin: 20px auto;
+  background: #fff;
+  border-radius: 8px;
+  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
+  padding: 20px;
+}
+
+@media (max-width: 768px) {
+  .chart-container {
+    height: 400px;
+    padding: 10px;
+  }
+}
+</style>