Browse Source

feat(dashboard): 重构仪表盘组件和图表管理;联调首页大屏接口,优化图标展示;

重构仪表盘组件结构,新增BaseChart基础组件统一管理图表
优化图表响应式布局和数据处理逻辑
添加仪表盘数据轮询功能
移除冗余代码和组件,提升性能
调整样式布局,增强视觉一致性
liujia 1 month ago
parent
commit
a40faf0

+ 1 - 0
components.d.ts

@@ -59,6 +59,7 @@ declare module 'vue' {
     AUploadDragger: typeof import('ant-design-vue/es')['UploadDragger']
     AUploadDragger: typeof import('ant-design-vue/es')['UploadDragger']
     BaseAreaViewer: typeof import('./src/components/baseAreaViewer/index.vue')['default']
     BaseAreaViewer: typeof import('./src/components/baseAreaViewer/index.vue')['default']
     BaseCard: typeof import('./src/components/baseCard/index.vue')['default']
     BaseCard: typeof import('./src/components/baseCard/index.vue')['default']
+    BaseChart: typeof import('./src/components/baseChart/index.vue')['default']
     BaseModal: typeof import('./src/components/baseModal/index.vue')['default']
     BaseModal: typeof import('./src/components/baseModal/index.vue')['default']
     BasePagination: typeof import('./src/components/basePagination/index.vue')['default']
     BasePagination: typeof import('./src/components/basePagination/index.vue')['default']
     BaseWeather: typeof import('./src/components/baseWeather/index.vue')['default']
     BaseWeather: typeof import('./src/components/baseWeather/index.vue')['default']

+ 16 - 0
src/api/stats/index.ts

@@ -21,3 +21,19 @@ export const statsHomeScreenQuery = (params: {
 }): Promise<ResponseData<TYPE.StatsHomeScreenQueryData>> => {
 }): Promise<ResponseData<TYPE.StatsHomeScreenQueryData>> => {
   return request.post('/stats/screen', params)
   return request.post('/stats/screen', params)
 }
 }
+
+// 首页大屏历史跌倒统计
+export const statsHomeScreenFallHistory = (params: {
+  tenantId: string
+  queryType: string // 查询类型 最近七天 day 最近半年 month
+}): Promise<ResponseData<TYPE.StatsHomeScreenFallHistory>> => {
+  return request.post('/stats/falling', params)
+}
+
+// 首页大屏历史告警统计
+export const statsHomeScreenAlarmHistory = (params: {
+  tenantId: string
+  queryType: string // 查询类型 最近七天 day 最近半年 month
+}): Promise<ResponseData<TYPE.StatsHomeScreenAlarmHistory>> => {
+  return request.post('/stats/alarm', params)
+}

+ 29 - 0
src/api/stats/types.ts

@@ -134,3 +134,32 @@ export interface StatsHomeScreenQueryData {
   guardList: GuardList[] // 守护统计信息
   guardList: GuardList[] // 守护统计信息
   installPositionList: InstallPositionList[] // 安装位置统计信息
   installPositionList: InstallPositionList[] // 安装位置统计信息
 }
 }
+
+/**
+ * 首页大屏历史跌倒统计出参
+ */
+
+export interface StatsHomeScreenFallHistory {
+  monthStatInfo: {
+    month: string
+    fallingCount: number
+  }[]
+  dayStatInfo: {
+    date: string
+    fallingCount: number
+  }[]
+}
+
+/**
+ * 首页大屏历史告警统计出参
+ */
+export interface StatsHomeScreenAlarmHistory {
+  monthStatInfo: {
+    month: string
+    alarmCount: number
+  }[]
+  dayStatInfo: {
+    date: string
+    alarmCount: number
+  }[]
+}

+ 30 - 0
src/components/baseChart/index.vue

@@ -0,0 +1,30 @@
+<template>
+  <div ref="chartRef" class="base-chart" :style="{ height: height ? height + 'px' : '100%' }" />
+</template>
+
+<script setup lang="ts">
+import { ref, toRef } from 'vue'
+import * as echarts from 'echarts'
+import { useSafeChart } from '@/hooks/useSafeChart'
+
+defineOptions({ name: 'BaseChart' })
+
+const props = defineProps<{
+  option: echarts.EChartsOption
+  height?: number
+}>()
+
+const chartRef = ref<HTMLDivElement | null>(null)
+const optionRef = toRef(props, 'option')
+
+useSafeChart(chartRef, optionRef)
+</script>
+
+<style scoped lang="less">
+.base-chart {
+  width: 100%;
+  height: 100%;
+  min-height: 150px;
+  min-width: 100px;
+}
+</style>

+ 106 - 0
src/hooks/useDashboardPolling.ts

@@ -0,0 +1,106 @@
+import { onMounted, onUnmounted, ref } from 'vue'
+import * as statsApi from '@/api/stats'
+import type {
+  StatsHomeScreenQueryData,
+  StatsHomeScreenFallHistory,
+  StatsHomeScreenAlarmHistory,
+} from '@/api/stats/types'
+
+export function useDashboardPolling(options: { tenantId: string; queryType: string }) {
+  const todayScreenData = ref<StatsHomeScreenQueryData | null>(null) // 今日大屏数据
+  const fallHistoryData = ref<StatsHomeScreenFallHistory | null>(null) // 历史跌倒数据
+  const alarmHistoryData = ref<StatsHomeScreenAlarmHistory | null>(null) // 历史告警数据
+
+  let realtimeTimer: ReturnType<typeof setInterval> | null = null
+  let businessTimer: ReturnType<typeof setInterval> | null = null
+  let isVisible = true
+
+  const MAX_RETRY = 3
+  let retryCountRealtime = 0
+  let retryCountBusiness = 0
+
+  // 获取今日数据
+  const getTodayData = async () => {
+    if (!isVisible) return
+    try {
+      const res = await statsApi.statsHomeScreenQuery({
+        tenantId: options.tenantId,
+      })
+      todayScreenData.value = res.data
+      retryCountRealtime = 0
+    } catch (err) {
+      retryCountRealtime++
+      console.warn(`实时数据获取失败(第 ${retryCountRealtime} 次)`, err)
+      if (retryCountRealtime < MAX_RETRY) {
+        setTimeout(getTodayData, 1000)
+      }
+    }
+  }
+
+  // 获取历史跌打倒和告警数据
+  const getBusinessData = async () => {
+    if (!isVisible) return
+    try {
+      const [fallRes, alarmRes] = await Promise.all([
+        statsApi.statsHomeScreenFallHistory({
+          tenantId: options.tenantId,
+          queryType: options.queryType,
+        }),
+        statsApi.statsHomeScreenAlarmHistory({
+          tenantId: options.tenantId,
+          queryType: options.queryType,
+        }),
+      ])
+      fallHistoryData.value = fallRes.data
+      alarmHistoryData.value = alarmRes.data
+      retryCountBusiness = 0
+    } catch (err) {
+      retryCountBusiness++
+      console.warn(`业务数据获取失败(第 ${retryCountBusiness} 次)`, err)
+      if (retryCountBusiness < MAX_RETRY) {
+        setTimeout(getBusinessData, 2000)
+      }
+    }
+  }
+
+  const startPolling = () => {
+    getTodayData()
+    getBusinessData()
+    realtimeTimer = setInterval(getTodayData, 3000) // 3秒获取一次
+    businessTimer = setInterval(getBusinessData, 30000) // 30秒获取一次
+  }
+
+  const stopPolling = () => {
+    if (realtimeTimer) clearInterval(realtimeTimer)
+    if (businessTimer) clearInterval(businessTimer)
+    realtimeTimer = null
+    businessTimer = null
+  }
+
+  const handleVisibilityChange = () => {
+    isVisible = document.visibilityState === 'visible'
+    if (isVisible) {
+      startPolling()
+    } else {
+      stopPolling()
+    }
+  }
+
+  onMounted(() => {
+    startPolling()
+    document.addEventListener('visibilitychange', handleVisibilityChange)
+  })
+
+  onUnmounted(() => {
+    stopPolling()
+    document.removeEventListener('visibilitychange', handleVisibilityChange)
+  })
+
+  console.log('🚀🚀🚀@@useDashboardPolling:', todayScreenData, fallHistoryData, alarmHistoryData)
+
+  return {
+    todayScreenData,
+    fallHistoryData,
+    alarmHistoryData,
+  }
+}

+ 113 - 0
src/hooks/useSafeChart.ts

@@ -0,0 +1,113 @@
+import { onMounted, onUnmounted, watch, ref, nextTick, type Ref } from 'vue'
+import * as echarts from 'echarts'
+
+export function useSafeChart(
+  chartRef: Ref<HTMLDivElement | null>,
+  option: Ref<echarts.EChartsOption>
+) {
+  const chartInstance = ref<echarts.ECharts | null>(null)
+  let isAlive = true
+
+  const shouldInjectGraphic = (opt: echarts.EChartsOption): boolean => {
+    const series = opt?.series
+    if (!Array.isArray(series) || series.length === 0) return true
+
+    const firstSeries = series[0]
+    const data = firstSeries?.data
+
+    return !Array.isArray(data) || data.length === 0
+  }
+
+  const getEmptyGraphic = (): echarts.EChartsOption['graphic'] => [
+    {
+      type: 'text',
+      left: 'center',
+      top: 'middle',
+      style: {
+        text: '暂无数据',
+        fontSize: 16,
+        fill: '#9cc5e0',
+      },
+    },
+  ]
+
+  const applyOption = (opt: echarts.EChartsOption) => {
+    if (!chartInstance.value) return
+
+    const enhancedOption = {
+      ...opt,
+      graphic: shouldInjectGraphic(opt)
+        ? getEmptyGraphic()
+        : Array.isArray(opt.graphic)
+          ? opt.graphic
+          : [],
+    }
+
+    chartInstance.value.setOption(enhancedOption, true)
+  }
+
+  const initChart = () => {
+    const dom = chartRef.value
+    if (!dom || dom.offsetWidth === 0) return
+
+    const existing = echarts.getInstanceByDom(dom)
+    if (existing) echarts.dispose(dom)
+
+    chartInstance.value = echarts.init(dom)
+    applyOption(option.value)
+  }
+
+  const resizeChart = () => {
+    const chart = chartInstance.value
+    const dom = chart?.getDom()
+
+    if (!isAlive || !chart || !dom || dom.offsetWidth === 0) return
+
+    try {
+      chart.resize()
+    } catch (error) {
+      console.warn('Chart resize failed, attempting recovery:', error)
+
+      try {
+        echarts.dispose(dom)
+        const newChart = echarts.init(dom)
+        chartInstance.value = newChart
+        applyOption(option.value)
+      } catch (retryError) {
+        console.error('Chart recovery failed:', retryError)
+      }
+    }
+  }
+
+  onMounted(() => {
+    nextTick(() => {
+      initChart()
+      window.addEventListener('resize', resizeChart)
+    })
+  })
+
+  onUnmounted(() => {
+    isAlive = false
+    window.removeEventListener('resize', resizeChart)
+    if (chartInstance.value) {
+      try {
+        chartInstance.value.dispose()
+      } catch (error) {
+        console.warn('Chart dispose failed:', error)
+      }
+      chartInstance.value = null
+    }
+  })
+
+  watch(
+    option,
+    (newOption) => {
+      applyOption(newOption)
+    },
+    { deep: true }
+  )
+
+  return {
+    chartInstance,
+  }
+}

+ 7 - 3
src/layout/index.vue

@@ -454,9 +454,13 @@ const openSmartScreen = () => {
     .smartScreen {
     .smartScreen {
       cursor: pointer;
       cursor: pointer;
       color: #eee;
       color: #eee;
-      &:hover {
-        color: #85b8ff;
-      }
+      font-size: 1.2em;
+      font-weight: bold;
+      background: linear-gradient(to right, #ff6ec4, #f9d423, #00c9ff, #92fe9d);
+      -webkit-background-clip: text;
+      background-clip: text;
+      -webkit-text-fill-color: transparent;
+      text-shadow: 0 0 2px rgba(255, 255, 255, 0.3);
     }
     }
 
 
     .refresh {
     .refresh {

+ 78 - 0
src/utils/chartManager.ts

@@ -0,0 +1,78 @@
+import { onMounted, onUnmounted, ref } from 'vue'
+import * as echarts from 'echarts'
+
+const chartInstances: Map<string, echarts.ECharts> = new Map()
+let instanceCounter = 0
+
+export const windowSize = ref({
+  width: window.innerWidth,
+  height: window.innerHeight,
+})
+
+export function addChartInstance(chart: echarts.ECharts) {
+  const id = `chart_${instanceCounter++}`
+  chartInstances.set(id, chart)
+  return id
+}
+
+export function removeChartInstance(id: string) {
+  const chart = chartInstances.get(id)
+  if (chart) {
+    chartInstances.delete(id)
+    try {
+      chart.dispose()
+    } catch (error) {
+      console.warn('Chart dispose failed:', error)
+    }
+  }
+}
+
+export function safeInitChart(dom: HTMLElement): echarts.ECharts {
+  const existing = echarts.getInstanceByDom(dom)
+  if (existing) {
+    echarts.dispose(dom)
+  }
+  return echarts.init(dom)
+}
+
+export function useResponsiveLayout() {
+  const handleResize = () => {
+    windowSize.value = {
+      width: window.innerWidth,
+      height: window.innerHeight,
+    }
+
+    requestAnimationFrame(() => {
+      chartInstances.forEach((chart, id) => {
+        try {
+          if (chart.getDom()) {
+            chart.resize()
+          } else {
+            console.warn(`Chart ${id} 已被销毁或 DOM 不存在,跳过 resize`)
+          }
+        } catch (error) {
+          console.warn(`Chart resize failed for ${id}:`, error)
+        }
+      })
+    })
+  }
+
+  onMounted(() => {
+    window.addEventListener('resize', handleResize)
+    handleResize()
+  })
+
+  onUnmounted(() => {
+    window.removeEventListener('resize', handleResize)
+    chartInstances.forEach((chart) => {
+      try {
+        chart.dispose()
+      } catch (error) {
+        console.warn('Chart dispose failed during cleanup:', error)
+      }
+    })
+    chartInstances.clear()
+  })
+
+  return { windowSize }
+}

+ 0 - 13
src/utils/useChartResize.ts

@@ -1,13 +0,0 @@
-import { onUnmounted } from 'vue'
-import * as echarts from 'echarts'
-
-export function useChartResize(chart: echarts.ECharts, container: HTMLElement) {
-  const observer = new ResizeObserver(() => {
-    chart.resize()
-  })
-  observer.observe(container)
-
-  onUnmounted(() => {
-    observer.disconnect()
-  })
-}

+ 0 - 141
src/utils/useResponsiveLayout.ts

@@ -1,141 +0,0 @@
-import { onMounted, onUnmounted, ref } from 'vue'
-import * as echarts from 'echarts'
-
-// 存储所有需要响应尺寸变化的图表实例
-const chartInstances: Map<string, echarts.ECharts> = new Map()
-let instanceCounter = 0
-
-// 当前窗口尺寸
-const windowSize = ref({
-  width: window.innerWidth,
-  height: window.innerHeight,
-})
-
-// 添加图表实例到监听列表
-export function addChartInstance(chart: echarts.ECharts) {
-  const id = `chart_${instanceCounter++}`
-  chartInstances.set(id, chart)
-  return id
-}
-
-// 从监听列表中移除图表实例
-export function removeChartInstance(id: string) {
-  const chart = chartInstances.get(id)
-  if (chart) {
-    chartInstances.delete(id)
-    try {
-      chart.dispose()
-    } catch (error) {
-      console.warn('Chart dispose failed:', error)
-    }
-  }
-}
-
-// 响应式布局钩子
-export function useResponsiveLayout() {
-  // 窗口尺寸变化处理函数
-  const handleResize = () => {
-    const newWidth = window.innerWidth
-    const newHeight = window.innerHeight
-
-    // 更新窗口尺寸状态
-    windowSize.value = {
-      width: newWidth,
-      height: newHeight,
-    }
-
-    // 使用requestAnimationFrame优化性能
-    requestAnimationFrame(() => {
-      // 调整所有图表大小
-      chartInstances.forEach((chart, id) => {
-        try {
-          chart.resize()
-        } catch (error) {
-          console.warn(`Chart resize failed for ${id}:`, error)
-          // 如果图表调整失败,尝试重新初始化
-          try {
-            const dom = chart.getDom()
-            if (dom) {
-              const newChart = echarts.init(dom)
-              chartInstances.set(id, newChart)
-              // 这里可以添加重新设置图表选项的逻辑
-            }
-          } catch (retryError) {
-            console.error(`Failed to reinitialize chart ${id}:`, retryError)
-          }
-        }
-      })
-    })
-  }
-
-  onMounted(() => {
-    // 监听窗口大小变化事件
-    window.addEventListener('resize', handleResize)
-    // 初始化时触发一次调整
-    handleResize()
-  })
-
-  onUnmounted(() => {
-    // 清理事件监听
-    window.removeEventListener('resize', handleResize)
-    // 清理所有图表实例
-    chartInstances.forEach((chart) => {
-      try {
-        chart.dispose()
-      } catch (error) {
-        console.warn('Chart dispose failed during cleanup:', error)
-      }
-    })
-    chartInstances.clear()
-  })
-
-  return {
-    windowSize,
-  }
-}
-
-// 导出响应式断点常量
-export const BREAKPOINTS = {
-  SMALL: 768,
-  MEDIUM: 1200,
-  LARGE: 1600,
-  XLARGE: 1920,
-}
-
-// 判断当前窗口是否为移动设备
-export function isMobile() {
-  return windowSize.value.width < BREAKPOINTS.SMALL
-}
-
-// 判断当前窗口是否为平板
-export function isTablet() {
-  return windowSize.value.width >= BREAKPOINTS.SMALL && windowSize.value.width < BREAKPOINTS.MEDIUM
-}
-
-// 判断当前窗口是否为桌面
-export function isDesktop() {
-  return windowSize.value.width >= BREAKPOINTS.MEDIUM
-}
-
-// 判断当前窗口是否为大屏幕
-export function isLargeScreen() {
-  return windowSize.value.width >= BREAKPOINTS.LARGE
-}
-
-// 判断当前窗口是否为超大屏幕
-export function isXLargeScreen() {
-  return windowSize.value.width >= BREAKPOINTS.XLARGE
-}
-
-// 强制调整所有图表大小
-export function forceResizeAllCharts() {
-  requestAnimationFrame(() => {
-    chartInstances.forEach((chart) => {
-      try {
-        chart.resize()
-      } catch (error) {
-        console.warn('Forced chart resize failed:', error)
-      }
-    })
-  })
-}

+ 48 - 63
src/views/dashboard/components/AlarmHistoryCard/index.vue

@@ -1,79 +1,69 @@
 <template>
 <template>
   <TechCard>
   <TechCard>
     <div class="card-title">历史告警统计</div>
     <div class="card-title">历史告警统计</div>
-    <div ref="chartRef" class="chart-container" />
+    <BaseChart :option="chartOption" :height="175" />
   </TechCard>
   </TechCard>
 </template>
 </template>
 
 
 <script setup lang="ts">
 <script setup lang="ts">
-import { onMounted, ref } from 'vue'
-import * as echarts from 'echarts'
-import { useChartResize } from '@/utils/useChartResize'
+import { computed, ref } from 'vue'
 import TechCard from '../TechCard/index.vue'
 import TechCard from '../TechCard/index.vue'
-// import { alarmHistoryData } from '@/store/data'
+import * as echarts from 'echarts'
 
 
-defineOptions({
-  name: 'AlarmHistoryCard',
-})
+defineOptions({ name: 'AlarmHistoryCard' })
 
 
 const alarmHistoryData = ref({
 const alarmHistoryData = ref({
   dates: ['2024-07-01', '2024-07-02', '2024-07-03', '2024-07-04', '2024-07-05'],
   dates: ['2024-07-01', '2024-07-02', '2024-07-03', '2024-07-04', '2024-07-05'],
   values: [10, 15, 20, 12, 8],
   values: [10, 15, 20, 12, 8],
 })
 })
-const chartRef = ref<HTMLDivElement | null>(null)
 
 
-onMounted(() => {
-  if (!chartRef.value) return
-  const chart = echarts.init(chartRef.value)
-  chart.setOption({
-    grid: { top: 10, right: 20, bottom: 30, left: 30 },
-    tooltip: {
-      trigger: 'axis',
-      axisPointer: { type: 'shadow' },
-    },
-    xAxis: {
-      type: 'category',
-      data: alarmHistoryData.value.dates,
-      axisLine: { lineStyle: { color: '#2a3b5a' } },
-      axisLabel: { color: '#9cc5e0', fontSize: 12 },
-    },
-    yAxis: {
-      type: 'value',
-      axisLine: { lineStyle: { color: '#2a3b5a' } },
-      axisLabel: { color: '#9cc5e0', fontSize: 12 },
-      splitLine: { lineStyle: { color: 'rgba(42, 59, 90, 0.3)' } },
-    },
-    series: [
-      {
-        name: '告警次数',
-        type: 'line',
-        smooth: true,
-        symbol: 'circle',
-        symbolSize: 8,
-        data: alarmHistoryData.value.values,
-        itemStyle: { color: '#f39c12' },
-        lineStyle: {
-          width: 3,
-          color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [
-            { offset: 0, color: '#f39c12' },
-            { offset: 1, color: '#e74c3c' },
-          ]),
-        },
-        areaStyle: {
-          color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
-            { offset: 0, color: 'rgba(243, 156, 18, 0.3)' },
-            { offset: 1, color: 'rgba(243, 156, 18, 0.1)' },
-          ]),
-        },
+// 图表配置项
+const chartOption = computed(() => ({
+  grid: { top: 10, right: 20, bottom: 30, left: 30 },
+  tooltip: {
+    trigger: 'axis',
+    axisPointer: { type: 'shadow' },
+  },
+  xAxis: {
+    type: 'category',
+    data: alarmHistoryData.value.dates,
+    axisLine: { lineStyle: { color: '#2a3b5a' } },
+    axisLabel: { color: '#9cc5e0', fontSize: 12 },
+  },
+  yAxis: {
+    type: 'value',
+    axisLine: { lineStyle: { color: '#2a3b5a' } },
+    axisLabel: { color: '#9cc5e0', fontSize: 12 },
+    splitLine: { lineStyle: { color: 'rgba(42, 59, 90, 0.3)' } },
+  },
+  series: [
+    {
+      name: '告警次数',
+      type: 'line',
+      smooth: true,
+      symbol: 'circle',
+      symbolSize: 8,
+      data: alarmHistoryData.value.values,
+      itemStyle: { color: '#f39c12' },
+      lineStyle: {
+        width: 3,
+        color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [
+          { offset: 0, color: '#f39c12' },
+          { offset: 1, color: '#e74c3c' },
+        ]),
       },
       },
-    ],
-  })
-
-  useChartResize(chart, chartRef.value)
-})
+      areaStyle: {
+        color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+          { offset: 0, color: 'rgba(243, 156, 18, 0.3)' },
+          { offset: 1, color: 'rgba(243, 156, 18, 0.1)' },
+        ]),
+      },
+    },
+  ],
+}))
 </script>
 </script>
 
 
-<style scoped>
+<style scoped lang="less">
 .card-title {
 .card-title {
   font-size: 16px;
   font-size: 16px;
   font-weight: bold;
   font-weight: bold;
@@ -81,9 +71,4 @@ onMounted(() => {
   margin-bottom: 12px;
   margin-bottom: 12px;
   text-align: center;
   text-align: center;
 }
 }
-.chart-container {
-  width: 100%;
-  height: 100%;
-  min-height: 280px;
-}
 </style>
 </style>

+ 42 - 95
src/views/dashboard/components/AlertFallCompareCard/index.vue

@@ -3,20 +3,16 @@
     <div class="card-header">
     <div class="card-header">
       <div class="title">跌倒与告警统计</div>
       <div class="title">跌倒与告警统计</div>
     </div>
     </div>
-    <div ref="chartRef" class="chart-container" />
+    <BaseChart :option="chartOption" :height="200" />
   </TechCard>
   </TechCard>
 </template>
 </template>
 
 
 <script lang="ts" setup>
 <script lang="ts" setup>
-import { ref, onMounted, onUnmounted, watch, nextTick } from 'vue'
-import * as echarts from 'echarts'
+import { computed } from 'vue'
 import TechCard from '../TechCard/index.vue'
 import TechCard from '../TechCard/index.vue'
 
 
 defineOptions({ name: 'AlertFallCompareCard' })
 defineOptions({ name: 'AlertFallCompareCard' })
 
 
-const chartRef = ref<HTMLDivElement | null>(null)
-const chartInstance = ref<echarts.ECharts | null>(null)
-
 interface Props {
 interface Props {
   fallCount: number
   fallCount: number
   alertCount: number
   alertCount: number
@@ -27,91 +23,47 @@ const props = withDefaults(defineProps<Props>(), {
   alertCount: 0,
   alertCount: 0,
 })
 })
 
 
-const createChart = () => {
-  if (!chartRef.value) return
-
-  if (chartInstance.value) {
-    chartInstance.value.dispose()
-  }
-
-  chartInstance.value = echarts.init(chartRef.value)
-  updateChart()
-}
-
-const updateChart = () => {
-  if (!chartInstance.value) return
-
-  chartInstance.value.setOption({
-    tooltip: {
-      trigger: 'axis',
-      axisPointer: { type: 'shadow' },
-      formatter: '{b}: {c} 次',
-    },
-    grid: {
-      left: 40,
-      right: 20,
-      top: 20,
-      bottom: 40,
-    },
-    xAxis: {
-      type: 'category',
-      data: ['跌倒', '告警'],
-      axisLabel: { color: '#9cc5e0' },
-      axisTick: { show: false },
-      axisLine: { show: false },
-    },
-    yAxis: {
-      type: 'value',
-      axisLabel: { color: '#9cc5e0' },
-      splitLine: { show: false },
-    },
-    series: [
-      {
-        type: 'bar',
-        data: [props.fallCount, props.alertCount],
-        barWidth: 40,
-        itemStyle: {
-          color: (params: { dataIndex: number }) => ['#ff4d6d', '#f39c12'][params.dataIndex],
-        },
-        label: {
-          show: true,
-          position: 'top',
-          color: '#fff',
-          fontSize: 14,
-        },
+const chartOption = computed(() => ({
+  tooltip: {
+    trigger: 'item',
+    axisPointer: { type: 'shadow' },
+    formatter: '{b}: {c} 次',
+  },
+  grid: {
+    left: 40,
+    right: 20,
+    top: 20,
+    bottom: 40,
+  },
+  xAxis: {
+    type: 'category',
+    data: ['跌倒', '告警'],
+    axisLabel: { color: '#9cc5e0' },
+    axisTick: { show: false },
+    axisLine: { show: false },
+  },
+  yAxis: {
+    type: 'value',
+    axisLabel: { color: '#9cc5e0' },
+    splitLine: { show: false },
+  },
+  series: [
+    {
+      type: 'bar',
+      data: [props.fallCount, props.alertCount],
+      barWidth: 40,
+      itemStyle: {
+        color: (params: { dataIndex: number }) => ['#ff4d6d', '#f39c12'][params.dataIndex],
       },
       },
-    ],
-  })
-}
-
-const resizeChart = () => {
-  if (chartInstance.value) {
-    chartInstance.value.resize()
-  }
-}
-
-onMounted(() => {
-  nextTick(() => {
-    createChart()
-    window.addEventListener('resize', resizeChart)
-  })
-})
-
-watch(
-  () => [props.fallCount, props.alertCount],
-  () => {
-    updateChart()
-  }
-)
-
-// 组件卸载时销毁图表实例和事件监听
-onUnmounted(() => {
-  if (chartInstance.value) {
-    chartInstance.value.dispose()
-    chartInstance.value = null
-  }
-  window.removeEventListener('resize', resizeChart)
-})
+      label: {
+        show: true,
+        position: 'top',
+        color: '#fff',
+        fontSize: 14,
+      },
+    },
+  ],
+}))
 </script>
 </script>
 
 
 <style lang="less" scoped>
 <style lang="less" scoped>
@@ -125,9 +77,4 @@ onUnmounted(() => {
     color: #00f0ff;
     color: #00f0ff;
   }
   }
 }
 }
-
-.chart-container {
-  width: 100%;
-  height: 200px;
-}
 </style>
 </style>

+ 31 - 32
src/views/dashboard/components/DeviceAgeCard/index.vue

@@ -1,27 +1,40 @@
 <template>
 <template>
   <TechCard>
   <TechCard>
     <div class="card-title">设备年龄层次分布</div>
     <div class="card-title">设备年龄层次分布</div>
-    <div ref="chartRef" class="chart-container" />
+    <BaseChart :option="chartOption" :height="220" />
   </TechCard>
   </TechCard>
 </template>
 </template>
 
 
 <script setup lang="ts">
 <script setup lang="ts">
-import { onMounted, ref } from 'vue'
-import * as echarts from 'echarts'
-import { useChartResize } from '@/utils/useChartResize'
+import { computed } from 'vue'
 import TechCard from '../TechCard/index.vue'
 import TechCard from '../TechCard/index.vue'
 
 
-defineOptions({
-  name: 'DeviceAgeCard',
-})
+defineOptions({ name: 'DeviceAgeCard' })
+
+interface AgeItem {
+  ageRange: string
+  count: number
+}
+
+interface Props {
+  ageList: AgeItem[]
+}
+
+const props = defineProps<Props>()
 
 
-const chartRef = ref<HTMLDivElement | null>(null)
+const chartOption = computed(() => {
+  if (!props.ageList || props.ageList.length === 0) {
+    return {
+      series: [],
+    }
+  }
 
 
-onMounted(() => {
-  if (!chartRef.value) return
-  const chart = echarts.init(chartRef.value)
+  const data = props.ageList.map((item) => ({
+    name: item.ageRange || '未知',
+    value: item.count ?? 0,
+  }))
 
 
-  chart.setOption({
+  return {
     tooltip: {
     tooltip: {
       trigger: 'item',
       trigger: 'item',
       appendToBody: true,
       appendToBody: true,
@@ -39,43 +52,29 @@ onMounted(() => {
       {
       {
         name: '设备年龄层次',
         name: '设备年龄层次',
         type: 'pie',
         type: 'pie',
-        radius: ['20%', '70%'],
-        center: ['50%', '35%'],
+        radius: ['20%', '55%'],
+        center: ['50%', '40%'],
         roseType: 'area',
         roseType: 'area',
         label: { show: false },
         label: { show: false },
         labelLine: { show: false },
         labelLine: { show: false },
-        data: [
-          { value: 15, name: '40-50岁' },
-          { value: 20, name: '50-60岁' },
-          { value: 40, name: '60-70岁' },
-          { value: 45, name: '70-80岁' },
-          { value: 50, name: '80岁以上' },
-        ],
+        data,
         itemStyle: {
         itemStyle: {
           color: (params: { dataIndex: number }) => {
           color: (params: { dataIndex: number }) => {
             const colors = ['#4dc9e6', '#2572ed', '#6de4ff', '#1a57c9', '#00bcd4']
             const colors = ['#4dc9e6', '#2572ed', '#6de4ff', '#1a57c9', '#00bcd4']
-            return colors[params.dataIndex]
+            return colors[params.dataIndex % colors.length]
           },
           },
         },
         },
       },
       },
     ],
     ],
-  })
-
-  useChartResize(chart, chartRef.value)
+  }
 })
 })
 </script>
 </script>
 
 
-<style scoped>
+<style scoped lang="less">
 .card-title {
 .card-title {
   font-size: 16px;
   font-size: 16px;
   font-weight: bold;
   font-weight: bold;
   color: #00f0ff;
   color: #00f0ff;
-  margin-bottom: 12px;
   text-align: center;
   text-align: center;
 }
 }
-.chart-container {
-  width: 100%;
-  height: 100%;
-  min-height: 230px;
-}
 </style>
 </style>

+ 8 - 62
src/views/dashboard/components/DeviceLocationCard/index.vue

@@ -1,21 +1,15 @@
 <template>
 <template>
   <TechCard>
   <TechCard>
     <div class="card-title">设备安装位置分布</div>
     <div class="card-title">设备安装位置分布</div>
-    <div ref="chartRef" class="chart-container" />
+    <BaseChart :option="chartOption" :height="200" />
   </TechCard>
   </TechCard>
 </template>
 </template>
 
 
 <script setup lang="ts">
 <script setup lang="ts">
-import { onMounted, ref, onUnmounted, watch, nextTick } from 'vue'
-import * as echarts from 'echarts'
+import { computed } from 'vue'
 import TechCard from '../TechCard/index.vue'
 import TechCard from '../TechCard/index.vue'
 
 
-defineOptions({
-  name: 'DeviceLocationCard',
-})
-
-const chartRef = ref<HTMLDivElement | null>(null)
-const chartInstance = ref<echarts.ECharts | null>(null)
+defineOptions({ name: 'DeviceLocationCard' })
 
 
 interface Props {
 interface Props {
   data: Array<{ name?: string; count: number; installPosition: string }>
   data: Array<{ name?: string; count: number; installPosition: string }>
@@ -25,27 +19,13 @@ const props = withDefaults(defineProps<Props>(), {
   data: () => [],
   data: () => [],
 })
 })
 
 
-// 初始化图表
-const initChart = () => {
-  if (!chartRef.value) return
-
-  if (chartInstance.value) {
-    chartInstance.value.dispose()
-  }
-
-  chartInstance.value = echarts.init(chartRef.value)
-  updateChart()
-}
-
-const updateChart = () => {
-  if (!chartInstance.value) return
-
-  const locationNames = props.data.map((item) => item.name)
+const chartOption = computed(() => {
+  const locationNames = props.data.map((item) => item.name || item.installPosition)
   const locationValues = props.data.map((item) => item.count)
   const locationValues = props.data.map((item) => item.count)
 
 
-  chartInstance.value.setOption({
+  return {
     tooltip: {
     tooltip: {
-      trigger: 'axis',
+      trigger: 'item',
       axisPointer: { type: 'shadow' },
       axisPointer: { type: 'shadow' },
       formatter: '{b}: {c} 台',
       formatter: '{b}: {c} 台',
     },
     },
@@ -86,40 +66,11 @@ const updateChart = () => {
         },
         },
       },
       },
     ],
     ],
-  })
-}
-
-const resizeChart = () => {
-  if (chartInstance.value) {
-    chartInstance.value.resize()
-  }
-}
-
-onMounted(() => {
-  nextTick(() => {
-    initChart()
-    window.addEventListener('resize', resizeChart)
-  })
-})
-
-watch(
-  () => props.data,
-  () => {
-    updateChart()
-  },
-  { deep: true }
-)
-
-onUnmounted(() => {
-  if (chartInstance.value) {
-    chartInstance.value.dispose()
-    chartInstance.value = null
   }
   }
-  window.removeEventListener('resize', resizeChart)
 })
 })
 </script>
 </script>
 
 
-<style scoped>
+<style scoped lang="less">
 .card-title {
 .card-title {
   font-size: 16px;
   font-size: 16px;
   font-weight: bold;
   font-weight: bold;
@@ -127,9 +78,4 @@ onUnmounted(() => {
   margin-bottom: 12px;
   margin-bottom: 12px;
   text-align: center;
   text-align: center;
 }
 }
-.chart-container {
-  width: 100%;
-  height: 100%;
-  min-height: 150px;
-}
 </style>
 </style>

+ 38 - 95
src/views/dashboard/components/DeviceOnlineRateCard/index.vue

@@ -3,7 +3,7 @@
     <div class="card-header">
     <div class="card-header">
       <div class="title">设备在线率</div>
       <div class="title">设备在线率</div>
     </div>
     </div>
-    <div ref="chartRef" class="chart-container"></div>
+    <BaseChart :option="chartOption" />
     <div class="footer">
     <div class="footer">
       <div class="label">设备数量</div>
       <div class="label">设备数量</div>
       <div class="count">{{ onlineCount }} / {{ deviceCount }} 台</div>
       <div class="count">{{ onlineCount }} / {{ deviceCount }} 台</div>
@@ -12,111 +12,63 @@
 </template>
 </template>
 
 
 <script lang="ts" setup>
 <script lang="ts" setup>
-import { ref, onMounted, onUnmounted, nextTick, watch } from 'vue'
-import * as echarts from 'echarts'
+import { computed } from 'vue'
 import 'echarts-liquidfill'
 import 'echarts-liquidfill'
 import TechCard from '../TechCard/index.vue'
 import TechCard from '../TechCard/index.vue'
 
 
-defineOptions({
-  name: 'DeviceOnlineRateCard',
-})
-
-const chartRef = ref<HTMLDivElement | null>(null)
-const chartInstance = ref<echarts.ECharts | null>(null)
-const onlineRate = ref<string>('')
+defineOptions({ name: 'DeviceOnlineRateCard' })
 
 
 type Props = {
 type Props = {
   onlineCount: number
   onlineCount: number
   deviceCount: number
   deviceCount: number
 }
 }
-// const emit = defineEmits<{
-//   (e: 'success', value: void): void
-// }>()
 const props = withDefaults(defineProps<Props>(), {
 const props = withDefaults(defineProps<Props>(), {
   onlineCount: 0,
   onlineCount: 0,
   deviceCount: 0,
   deviceCount: 0,
 })
 })
 
 
-const createChart = () => {
-  if (!chartRef.value) return
-  const chart = echarts.init(chartRef.value)
-  chartInstance.value = chart
-
-  updateChart()
-}
-
-const updateChart = () => {
-  if (!chartInstance.value) return
+const onlineRate = computed(() =>
+  props.deviceCount === 0
+    ? '0'
+    : Math.round((props.onlineCount / props.deviceCount) * 100).toFixed(2)
+)
 
 
-  chartInstance.value.setOption({
-    series: [
-      {
-        type: 'liquidFill',
-        radius: '80%',
-        center: ['50%', '50%'],
-        data: [onlineRate.value],
-        label: {
-          formatter: `${onlineRate.value}%`,
-          fontSize: 20,
-          fontWeight: 'bold',
-          color: '#00f0ff',
-        },
-        outline: {
-          show: true,
-          borderDistance: 4,
-          itemStyle: {
-            borderColor: '#00f0ff',
-            borderWidth: 2,
-          },
-        },
-        backgroundStyle: {
-          color: '#2c5364',
-        },
+const chartOption = computed(() => ({
+  series: [
+    {
+      type: 'liquidFill',
+      radius: '70%',
+      center: ['50%', '50%'],
+      data: [parseFloat(onlineRate.value) / 100],
+      label: {
+        formatter: `${onlineRate.value}%`,
+        fontSize: '1em',
+        fontWeight: 'bold',
+        color: '#00f0ff',
+      },
+      outline: {
+        show: true,
+        borderDistance: 4,
         itemStyle: {
         itemStyle: {
-          color: '#00f0ff',
-          opacity: 0.6,
+          borderColor: '#00f0ff',
+          borderWidth: 4,
         },
         },
       },
       },
-    ],
-  })
-}
-
-const resizeChart = () => {
-  if (chartInstance.value) {
-    chartInstance.value.resize()
-  }
-}
-
-watch(
-  () => [props.onlineCount, props.deviceCount],
-  (newProps) => {
-    onlineRate.value =
-      newProps[1] === 0 ? '0' : Math.round((newProps[0] / newProps[1]) * 100).toFixed(2)
-    updateChart()
-  },
-  { immediate: true }
-)
-
-onMounted(() => {
-  nextTick(() => {
-    createChart()
-    window.addEventListener('resize', resizeChart)
-  })
-})
-
-onUnmounted(() => {
-  if (chartInstance.value) {
-    chartInstance.value.dispose()
-    chartInstance.value = null
-    window.removeEventListener('resize', resizeChart)
-  }
-})
+      backgroundStyle: {
+        color: '#2c5364',
+      },
+      itemStyle: {
+        color: '#00f0ff',
+        opacity: 0.6,
+      },
+    },
+  ],
+}))
 </script>
 </script>
 
 
 <style lang="less" scoped>
 <style lang="less" scoped>
 .card-header {
 .card-header {
   text-align: center;
   text-align: center;
-  margin-bottom: 12px;
 
 
   .title {
   .title {
     font-size: 16px;
     font-size: 16px;
@@ -125,25 +77,16 @@ onUnmounted(() => {
   }
   }
 }
 }
 
 
-.chart-container {
-  width: 100%;
-  height: 160px;
-  min-height: 120px;
-  position: relative;
-}
-
 .footer {
 .footer {
   display: flex;
   display: flex;
   justify-content: center;
   justify-content: center;
   align-items: center;
   align-items: center;
+  flex-wrap: wrap;
   margin-top: 12px;
   margin-top: 12px;
+  gap: 8px;
   font-size: 14px;
   font-size: 14px;
   color: #9cc5e0;
   color: #9cc5e0;
 
 
-  .label {
-    margin-right: 8px;
-  }
-
   .count {
   .count {
     font-weight: bold;
     font-weight: bold;
     color: #00f0ff;
     color: #00f0ff;

+ 35 - 104
src/views/dashboard/components/ElderActivityCard/index.vue

@@ -3,7 +3,7 @@
     <div class="card-header">
     <div class="card-header">
       <div class="title">长者活跃度</div>
       <div class="title">长者活跃度</div>
     </div>
     </div>
-    <div ref="chartRef" class="chart-container"></div>
+    <BaseChart :option="chartOption" />
     <div class="footer">
     <div class="footer">
       <div class="label">当前活跃度</div>
       <div class="label">当前活跃度</div>
       <div class="count">{{ rate }}%</div>
       <div class="count">{{ rate }}%</div>
@@ -12,16 +12,10 @@
 </template>
 </template>
 
 
 <script lang="ts" setup>
 <script lang="ts" setup>
-import { ref, onMounted, watch, onUnmounted, nextTick } from 'vue'
-import * as echarts from 'echarts'
+import { computed } from 'vue'
 import TechCard from '../TechCard/index.vue'
 import TechCard from '../TechCard/index.vue'
 
 
-defineOptions({
-  name: 'ElderActivityCard',
-})
-
-const chartRef = ref<HTMLDivElement | null>(null)
-const chartInstance = ref<echarts.ECharts | null>(null)
+defineOptions({ name: 'ElderActivityCard' })
 
 
 type Props = {
 type Props = {
   activityRate: number
   activityRate: number
@@ -31,97 +25,41 @@ const props = withDefaults(defineProps<Props>(), {
   activityRate: 0,
   activityRate: 0,
 })
 })
 
 
-const rate = ref<number>(Number(props.activityRate.toFixed(2)))
-
-const createChart = () => {
-  if (!chartRef.value) return
-
-  if (chartInstance.value) {
-    chartInstance.value.dispose()
-  }
-
-  chartInstance.value = echarts.init(chartRef.value)
-  updateChart()
-}
-
-const updateChart = () => {
-  if (!chartInstance.value) return
-
-  chartInstance.value.setOption({
-    tooltip: {
-      trigger: 'item',
-      appendToBody: true,
-      formatter: '{b}: {c}%',
+const rate = computed(() => Number(props.activityRate.toFixed(2)))
+
+const chartOption = computed(() => ({
+  tooltip: {
+    trigger: 'item',
+    appendToBody: true,
+    formatter: '{b}: {c}%',
+  },
+  series: [
+    {
+      type: 'pie',
+      radius: ['0%', '75%'],
+      center: ['50%', '50%'],
+      data: [
+        {
+          value: rate.value,
+          name: '活跃',
+          itemStyle: { color: '#00f0ff' },
+        },
+        {
+          value: Number((100 - rate.value).toFixed(2)),
+          name: '非活跃',
+          itemStyle: { color: '#2c5364' },
+        },
+      ],
+      label: { show: false },
+      labelLine: { show: false },
     },
     },
-    series: [
-      {
-        type: 'pie',
-        radius: ['50%', '70%'],
-        center: ['50%', '50%'],
-        data: [
-          {
-            value: rate.value,
-            name: '活跃',
-            itemStyle: { color: '#00f0ff' },
-          },
-          {
-            value: (100 - rate.value).toFixed(2),
-            name: '非活跃',
-            itemStyle: { color: '#2c5364' },
-          },
-        ],
-        label: { show: false },
-        labelLine: { show: false },
-      },
-    ],
-    graphic: {
-      type: 'text',
-      left: 'center',
-      top: 'center',
-      style: {
-        text: `${rate.value}%`,
-        fontSize: 20,
-        fontWeight: 'bold',
-        fill: '#00f0ff',
-        textShadow: '0 0 10px #00ff9f',
-      },
-    },
-  })
-}
-
-const resizeChart = () => {
-  if (chartInstance.value) {
-    chartInstance.value.resize()
-  }
-}
-
-onMounted(() => {
-  nextTick(() => {
-    createChart()
-    window.addEventListener('resize', resizeChart)
-  })
-})
-
-watch(
-  () => props.activityRate,
-  () => {
-    updateChart()
-  }
-)
-
-onUnmounted(() => {
-  if (chartInstance.value) {
-    chartInstance.value.dispose()
-    chartInstance.value = null
-  }
-  window.removeEventListener('resize', resizeChart)
-})
+  ],
+}))
 </script>
 </script>
 
 
 <style lang="less" scoped>
 <style lang="less" scoped>
 .card-header {
 .card-header {
   text-align: center;
   text-align: center;
-  margin-bottom: 12px;
 
 
   .title {
   .title {
     font-size: 16px;
     font-size: 16px;
@@ -130,22 +68,15 @@ onUnmounted(() => {
   }
   }
 }
 }
 
 
-.chart-container {
-  width: 100%;
-  height: 180px;
-}
-
 .footer {
 .footer {
   display: flex;
   display: flex;
   justify-content: center;
   justify-content: center;
   align-items: center;
   align-items: center;
-  margin-top: 12px;
+  flex-wrap: wrap;
+  gap: 8px;
   font-size: 14px;
   font-size: 14px;
   color: #9cc5e0;
   color: #9cc5e0;
-
-  .label {
-    margin-right: 8px;
-  }
+  margin-top: 12px;
 
 
   .count {
   .count {
     font-weight: bold;
     font-weight: bold;

+ 46 - 63
src/views/dashboard/components/FallingHistoryCard/index.vue

@@ -1,80 +1,68 @@
 <template>
 <template>
   <TechCard>
   <TechCard>
     <div class="card-title">历史跌倒统计</div>
     <div class="card-title">历史跌倒统计</div>
-    <div ref="chartRef" class="chart-container" />
+    <BaseChart :option="chartOption" :height="175" />
   </TechCard>
   </TechCard>
 </template>
 </template>
 
 
 <script setup lang="ts">
 <script setup lang="ts">
-import { onMounted, ref } from 'vue'
+import { computed, ref } from 'vue'
 import * as echarts from 'echarts'
 import * as echarts from 'echarts'
-import { useChartResize } from '@/utils/useChartResize'
 import TechCard from '../TechCard/index.vue'
 import TechCard from '../TechCard/index.vue'
-// import { fallingHistoryData } from '@/store/data'
 
 
-defineOptions({
-  name: 'FallingHistoryCard',
-})
+defineOptions({ name: 'FallingHistoryCard' })
 
 
 const fallingHistoryData = ref({
 const fallingHistoryData = ref({
   dates: ['2024-07-01', '2024-07-02', '2024-07-03', '2024-07-04', '2024-07-05'],
   dates: ['2024-07-01', '2024-07-02', '2024-07-03', '2024-07-04', '2024-07-05'],
   values: [5, 8, 6, 10, 7],
   values: [5, 8, 6, 10, 7],
 })
 })
 
 
-const chartRef = ref<HTMLDivElement | null>(null)
-
-onMounted(() => {
-  if (!chartRef.value) return
-  const chart = echarts.init(chartRef.value)
-  chart.setOption({
-    grid: { top: 10, right: 20, bottom: 30, left: 30 },
-    tooltip: {
-      trigger: 'axis',
-      axisPointer: { type: 'shadow' },
-    },
-    xAxis: {
-      type: 'category',
-      data: fallingHistoryData.value.dates,
-      axisLine: { lineStyle: { color: '#2a3b5a' } },
-      axisLabel: { color: '#9cc5e0', fontSize: 12 },
-    },
-    yAxis: {
-      type: 'value',
-      axisLine: { lineStyle: { color: '#2a3b5a' } },
-      axisLabel: { color: '#9cc5e0', fontSize: 12 },
-      splitLine: { lineStyle: { color: 'rgba(42, 59, 90, 0.3)' } },
-    },
-    series: [
-      {
-        name: '跌倒次数',
-        type: 'line',
-        smooth: true,
-        symbol: 'circle',
-        symbolSize: 8,
-        data: fallingHistoryData.value.values,
-        itemStyle: { color: '#e74c3c' },
-        lineStyle: {
-          width: 3,
-          color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [
-            { offset: 0, color: '#e74c3c' },
-            { offset: 1, color: '#c0392b' },
-          ]),
-        },
-        areaStyle: {
-          color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
-            { offset: 0, color: 'rgba(231, 76, 60, 0.3)' },
-            { offset: 1, color: 'rgba(231, 76, 60, 0.1)' },
-          ]),
-        },
+const chartOption = computed(() => ({
+  grid: { top: 10, right: 20, bottom: 30, left: 30 },
+  tooltip: {
+    trigger: 'axis',
+    axisPointer: { type: 'shadow' },
+  },
+  xAxis: {
+    type: 'category',
+    data: fallingHistoryData.value.dates,
+    axisLine: { lineStyle: { color: '#2a3b5a' } },
+    axisLabel: { color: '#9cc5e0', fontSize: 12 },
+  },
+  yAxis: {
+    type: 'value',
+    axisLine: { lineStyle: { color: '#2a3b5a' } },
+    axisLabel: { color: '#9cc5e0', fontSize: 12 },
+    splitLine: { lineStyle: { color: 'rgba(42, 59, 90, 0.3)' } },
+  },
+  series: [
+    {
+      name: '跌倒次数',
+      type: 'line',
+      smooth: true,
+      symbol: 'circle',
+      symbolSize: 8,
+      data: fallingHistoryData.value.values,
+      itemStyle: { color: '#e74c3c' },
+      lineStyle: {
+        width: 3,
+        color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [
+          { offset: 0, color: '#e74c3c' },
+          { offset: 1, color: '#c0392b' },
+        ]),
       },
       },
-    ],
-  })
-
-  useChartResize(chart, chartRef.value)
-})
+      areaStyle: {
+        color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+          { offset: 0, color: 'rgba(231, 76, 60, 0.3)' },
+          { offset: 1, color: 'rgba(231, 76, 60, 0.1)' },
+        ]),
+      },
+    },
+  ],
+}))
 </script>
 </script>
 
 
-<style scoped>
+<style scoped lang="less">
 .card-title {
 .card-title {
   font-size: 16px;
   font-size: 16px;
   font-weight: bold;
   font-weight: bold;
@@ -82,9 +70,4 @@ onMounted(() => {
   margin-bottom: 12px;
   margin-bottom: 12px;
   text-align: center;
   text-align: center;
 }
 }
-.chart-container {
-  width: 100%;
-  height: 100%;
-  min-height: 280px;
-}
 </style>
 </style>

+ 56 - 103
src/views/dashboard/components/ObjectDistributionCard/index.vue

@@ -1,122 +1,80 @@
 <template>
 <template>
   <TechCard>
   <TechCard>
     <div class="card-title">检测对象分布</div>
     <div class="card-title">检测对象分布</div>
-    <div ref="chartRef" class="chart-container" />
+    <BaseChart :option="chartOption" :height="200" />
   </TechCard>
   </TechCard>
 </template>
 </template>
 
 
 <script setup lang="ts">
 <script setup lang="ts">
-import { onMounted, ref, onUnmounted, watch, nextTick } from 'vue'
-import * as echarts from 'echarts'
+import { computed } from 'vue'
 import TechCard from '../TechCard/index.vue'
 import TechCard from '../TechCard/index.vue'
 
 
-defineOptions({
-  name: 'ObjectDistributionCard',
-})
+defineOptions({ name: 'ObjectDistributionCard' })
 
 
-const chartRef = ref<HTMLDivElement | null>(null)
-const chartInstance = ref<echarts.ECharts | null>(null)
+interface GuardItem {
+  guardType: string
+  count: string | number
+}
 
 
 interface Props {
 interface Props {
-  importantCount: number
-  normalCount: number
+  guardList: GuardItem[]
 }
 }
 
 
-const props = withDefaults(defineProps<Props>(), {
-  importantCount: 20,
-  normalCount: 45,
-})
-
-const initChart = () => {
-  if (!chartRef.value) return
+const props = defineProps<Props>()
 
 
-  if (chartInstance.value) {
-    chartInstance.value.dispose()
-  }
+// 提取分类名称和数量
+const categories = computed(() => props.guardList.map((item) => item.guardType))
+const counts = computed(() => props.guardList.map((item) => Number(item.count)))
 
 
-  chartInstance.value = echarts.init(chartRef.value)
-  updateChart()
-}
+// 配色方案
+const colorPalette = ['#f39c12', '#2ecc71', '#3498db', '#e74c3c', '#9b59b6', '#1abc9c']
 
 
-const updateChart = () => {
-  if (!chartInstance.value) return
-
-  chartInstance.value.setOption({
-    tooltip: {
-      trigger: 'axis',
-      axisPointer: { type: 'shadow' },
-      formatter: '{b}: {c} 个',
-    },
-    grid: {
-      left: 40,
-      right: 20,
-      top: 20,
-      bottom: 40,
-    },
-    xAxis: {
-      type: 'category',
-      data: ['重点', '一般'],
-      axisLabel: { color: '#9cc5e0' },
-      axisTick: { show: false },
-      axisLine: { show: false },
-    },
-    yAxis: {
-      type: 'value',
-      axisLabel: { color: '#9cc5e0' },
-      splitLine: { show: false },
-    },
-    series: [
-      {
-        type: 'bar',
-        data: [props.importantCount, props.normalCount],
-        barWidth: 30,
-        itemStyle: {
-          color: (params: { dataIndex: number }) => {
-            const colors = ['#f39c12', '#2ecc71']
-            return colors[params.dataIndex % colors.length]
-          },
-        },
-        label: {
-          show: true,
-          position: 'top',
-          color: '#00f0ff',
-          fontSize: 12,
+const chartOption = computed(() => ({
+  tooltip: {
+    trigger: 'item',
+    axisPointer: { type: 'shadow' },
+    formatter: '{b}: {c} 个',
+  },
+  grid: {
+    left: 40,
+    right: 20,
+    top: 20,
+    bottom: 40,
+  },
+  xAxis: {
+    type: 'category',
+    data: categories.value,
+    axisLabel: { color: '#9cc5e0' },
+    axisTick: { show: false },
+    axisLine: { show: false },
+  },
+  yAxis: {
+    type: 'value',
+    axisLabel: { color: '#9cc5e0' },
+    splitLine: { show: false },
+  },
+  series: [
+    {
+      type: 'bar',
+      data: counts.value,
+      barWidth: 30,
+      itemStyle: {
+        color: (params: { dataIndex: number }) => {
+          return colorPalette[params.dataIndex % colorPalette.length]
         },
         },
       },
       },
-    ],
-  })
-}
-
-const resizeChart = () => {
-  if (chartInstance.value) {
-    chartInstance.value.resize()
-  }
-}
-
-onMounted(() => {
-  nextTick(() => {
-    initChart()
-    window.addEventListener('resize', resizeChart)
-  })
-})
-
-watch(
-  () => [props.importantCount, props.normalCount],
-  () => {
-    updateChart()
-  }
-)
-
-onUnmounted(() => {
-  if (chartInstance.value) {
-    chartInstance.value.dispose()
-    chartInstance.value = null
-  }
-  window.removeEventListener('resize', resizeChart)
-})
+      label: {
+        show: true,
+        position: 'top',
+        color: '#00f0ff',
+        fontSize: 12,
+      },
+    },
+  ],
+}))
 </script>
 </script>
 
 
-<style scoped>
+<style scoped lang="less">
 .card-title {
 .card-title {
   font-size: 16px;
   font-size: 16px;
   font-weight: bold;
   font-weight: bold;
@@ -124,9 +82,4 @@ onUnmounted(() => {
   margin-bottom: 12px;
   margin-bottom: 12px;
   text-align: center;
   text-align: center;
 }
 }
-.chart-container {
-  width: 100%;
-  height: 100%;
-  min-height: 200px;
-}
 </style>
 </style>

+ 20 - 14
src/views/dashboard/components/PeopleDetectedCard/index.vue

@@ -1,5 +1,5 @@
 <template>
 <template>
-  <TechCard>
+  <TechCard class="people-detected-card">
     <div class="card-header">
     <div class="card-header">
       <div class="title">检测到人数</div>
       <div class="title">检测到人数</div>
     </div>
     </div>
@@ -9,10 +9,10 @@
       <div class="matrix-scroll">
       <div class="matrix-scroll">
         <div class="matrix-grid">
         <div class="matrix-grid">
           <div
           <div
-            v-for="(item, index) in 12"
+            v-for="(item, index) in 15"
             :key="index"
             :key="index"
             class="person-icon"
             class="person-icon"
-            :class="{ active: detectedCount > 6 || index < detectedCount }"
+            :class="{ active: detectedCount > 15 || index < detectedCount }"
           >
           >
             <svg viewBox="0 0 64 64" class="person-svg">
             <svg viewBox="0 0 64 64" class="person-svg">
               <circle cx="32" cy="12" r="6" />
               <circle cx="32" cy="12" r="6" />
@@ -49,9 +49,17 @@ withDefaults(defineProps<Props>(), {
 </script>
 </script>
 
 
 <style scoped lang="less">
 <style scoped lang="less">
+.people-detected-card > {
+  :deep(.card-content) {
+    display: flex;
+    flex-direction: column;
+    height: 100%;
+    justify-content: space-between;
+  }
+}
 .card-header {
 .card-header {
   text-align: center;
   text-align: center;
-  margin-bottom: 12px;
+  margin-bottom: 14px;
 
 
   .title {
   .title {
     font-size: 16px;
     font-size: 16px;
@@ -62,8 +70,9 @@ withDefaults(defineProps<Props>(), {
 
 
 .matrix-wrapper {
 .matrix-wrapper {
   position: relative;
   position: relative;
-  padding: 24px 12px;
-  height: 144px;
+  padding: 12px 0;
+  min-height: 100px;
+  max-height: 144px;
   overflow: hidden;
   overflow: hidden;
 }
 }
 
 
@@ -131,9 +140,9 @@ withDefaults(defineProps<Props>(), {
 
 
 .matrix-grid {
 .matrix-grid {
   display: grid;
   display: grid;
-  grid-template-columns: repeat(auto-fit, minmax(40px, 1fr));
-  grid-auto-rows: 48px;
-  gap: 8px;
+  grid-template-columns: repeat(auto-fit, minmax(20px, 1fr));
+  grid-auto-rows: 30px;
+  gap: 10px;
 }
 }
 
 
 .person-icon {
 .person-icon {
@@ -185,14 +194,11 @@ withDefaults(defineProps<Props>(), {
   display: flex;
   display: flex;
   justify-content: center;
   justify-content: center;
   align-items: center;
   align-items: center;
-  margin-top: 12px;
+  flex-wrap: wrap;
+  gap: 8px;
   font-size: 14px;
   font-size: 14px;
   color: #9cc5e0;
   color: #9cc5e0;
 
 
-  .label {
-    margin-right: 8px;
-  }
-
   .count {
   .count {
     font-weight: bold;
     font-weight: bold;
     color: #00f0ff;
     color: #00f0ff;

+ 1 - 6
src/views/dashboard/components/TechCard/index.vue

@@ -23,17 +23,12 @@ defineOptions({ name: 'TechCard' })
   border-radius: 12px;
   border-radius: 12px;
   padding: 24px;
   padding: 24px;
   color: #fff;
   color: #fff;
-  overflow: hidden;
   transition: box-shadow 0.3s ease;
   transition: box-shadow 0.3s ease;
-  width: 100%; /* 确保卡片占满父容器 */
-  height: 100%; /* 确保卡片占满父容器 */
-  min-height: 200px; /* 确保卡片有最小高度 */
 
 
-  /* 响应式调整 */
   @media (max-width: 1200px) {
   @media (max-width: 1200px) {
     padding: 20px;
     padding: 20px;
   }
   }
-  
+
   @media (max-width: 768px) {
   @media (max-width: 768px) {
     padding: 15px;
     padding: 15px;
     min-height: 180px;
     min-height: 180px;

+ 0 - 107
src/views/dashboard/components/dataCard/index.vue

@@ -1,107 +0,0 @@
-<template>
-  <div class="data-card">
-    <div class="tech-border-bottom-left"></div>
-    <div class="tech-border-bottom-right"></div>
-    <div class="data-card-title">{{ title }}</div>
-    <div class="data-card-value">{{ value }}</div>
-    <div class="data-card-change" :class="isPositive ? 'positive' : 'negative'">
-      {{ change }}
-    </div>
-  </div>
-</template>
-
-<script setup lang="ts">
-import { type DataCardProps } from '../../types/index'
-
-defineOptions({
-  name: 'DateCard',
-})
-
-withDefaults(defineProps<DataCardProps>(), {
-  isPositive: true,
-})
-</script>
-
-<style lang="less" scoped>
-.data-card {
-  background: linear-gradient(135deg, #0f2027, #203a43, #2c5364);
-  border-radius: 6px;
-  padding: 12px;
-  display: flex;
-  flex-direction: column;
-  position: relative;
-  overflow: hidden;
-
-  &::before {
-    content: '';
-    position: absolute;
-    top: 0;
-    left: 0;
-    width: 16px;
-    height: 16px;
-    border-top: 2px solid #00f0ff;
-    border-left: 2px solid #00f0ff;
-    border-radius: 3px 0 0 0;
-  }
-  &::after {
-    content: '';
-    position: absolute;
-    top: 0;
-    right: 0;
-    width: 16px;
-    height: 16px;
-    border-top: 2px solid #00f0ff;
-    border-right: 2px solid #00f0ff;
-    border-radius: 0 3px 0 0;
-  }
-  & .tech-border-bottom-left {
-    content: '';
-    position: absolute;
-    bottom: 0;
-    left: 0;
-    width: 16px;
-    height: 16px;
-    border-bottom: 2px solid #00f0ff;
-    border-left: 2px solid #00f0ff;
-    border-radius: 0 0 0 3px;
-  }
-  & .tech-border-bottom-right {
-    content: '';
-    position: absolute;
-    bottom: 0;
-    right: 0;
-    width: 16px;
-    height: 16px;
-    border-bottom: 2px solid #00f0ff;
-    border-right: 2px solid #00f0ff;
-    border-radius: 0 0 3px 0;
-  }
-}
-
-.data-card-title {
-  font-size: 14px;
-  color: #66e0ff;
-  margin-bottom: 8px;
-}
-
-.data-card-value {
-  font-size: 24px;
-  font-weight: bold;
-  color: #ffffff;
-  text-shadow: 0 0 12px #00f0ff;
-}
-
-.data-card-change {
-  font-size: 12px;
-  margin-top: 4px;
-  color: #a0faff;
-}
-
-.positive {
-  color: #00ff9f;
-}
-
-.negative {
-  color: #ff4d6d;
-}
-</style>

+ 0 - 832
src/views/dashboard/components/screen/index.vue

@@ -1,832 +0,0 @@
-<template>
-  <div class="radar-monitoring-screen">
-    <!-- 顶部区域 -->
-    <div class="header">
-      <div class="system-name">雷能社区智慧大屏</div>
-      <div class="running-days">已安全守护 {{ runningDays }} 天</div>
-      <div class="time-info">{{ currentTime }}</div>
-      <div class="data-flow header-flow"></div>
-    </div>
-
-    <!-- 内容区域 -->
-    <div class="content-area">
-      <!-- 左侧面板 -->
-      <div class="panel">
-        <div class="panel-title">今日监测概览</div>
-        <div class="panel-content">
-          <div class="data-grid">
-            <DataCard title="检测到人数" :value="todayData.detectedCount" />
-            <DataCard title="长者活跃度" :value="`${todayData.activeRate}%`" />
-            <DataCard title="跌倒统计次数" :value="todayData.alarmCount" />
-            <DataCard title="告警统计次数" :value="todayData.fallingCount" />
-            <DataCard
-              title="设备在线率"
-              :value="`${((todayData.onlineCount / todayData.deviceCount) * 100).toFixed(2)}%`"
-              :change="`${todayData.onlineCount} / ${todayData.deviceCount} 台`"
-            />
-          </div>
-
-          <div class="panel-title" style="margin-top: 15px">设备分布情况</div>
-          <div class="chart-container" ref="deviceChartRef"></div>
-
-          <div class="panel-title">设备年龄层次</div>
-          <div class="chart-container" ref="ageChartRef"></div>
-        </div>
-        <div class="data-flow"></div>
-        <div class="glow-effect"></div>
-      </div>
-
-      <!-- 中间面板 -->
-      <div class="panel center-panel">
-        <div class="map-container">
-          <img class="map-img" src="../../assets/img/map.jpg" alt="小区地图" />
-          <!-- <div class="map-label">雷能小区</div> -->
-          <div class="map-label building-1">1号楼</div>
-          <div class="map-label building-2">2号楼</div>
-          <div class="map-label building-3">物业中心</div>
-          <div class="map-label building-4">3号楼</div>
-        </div>
-      </div>
-
-      <!-- 右侧面板 -->
-      <div class="panel">
-        <div class="panel-title" style="margin-top: 15px">检测对象分布</div>
-        <div class="chart-container" ref="objectChartRef"></div>
-
-        <div class="panel-title" style="margin-top: 15px">历史告警统计</div>
-        <div class="chart-container" ref="alarmHistoryRef"></div>
-
-        <div class="panel-title" style="margin-top: 15px">历史跌倒统计</div>
-        <div class="chart-container" ref="fallingHistoryRef"></div>
-        <div class="data-flow"></div>
-        <div class="glow-effect"></div>
-      </div>
-    </div>
-
-    <!-- 底部区域 -->
-    <!-- <div class="footer">
-      <div>智慧养老雷达监控系统 &copy; 2023 版本 v2.5.1 | 技术支持:400-123-4567</div>
-    </div> -->
-  </div>
-</template>
-
-<script setup lang="ts">
-import { ref, onMounted, onUnmounted } from 'vue'
-import * as echarts from 'echarts'
-import DataCard from '../dataCard/index.vue'
-import { type TodayData } from '../../types'
-
-defineOptions({
-  name: 'ScreenPage',
-})
-
-// 响应式数据
-const currentTime = ref('')
-const runningDays = ref(328)
-const todayData = ref<TodayData>({
-  deviceCount: 30,
-  onlineCount: 25,
-  systemGuardDay: 328,
-  alarmCount: 3,
-  fallingCount: 1,
-  detectedCount: 142,
-  activeRate: 78,
-})
-
-// 历史告警统计数据(最近7天)
-const alarmHistoryData = ref({
-  dates: ['1月1日', '1月2日', '1月3日', '1月4日', '1月5日', '1月6日', '1月7日'],
-  values: [3, 5, 2, 7, 4, 6, 3],
-})
-
-// 历史跌倒统计数据(最近7天)
-const fallingHistoryData = ref({
-  dates: ['1月1日', '1月2日', '1月3日', '1月4日', '1月5日', '1月6日', '1月7日'],
-  values: [1, 2, 0, 1, 3, 1, 0],
-})
-
-// 图表引用
-const deviceChartRef = ref<HTMLElement | null>(null)
-const ageChartRef = ref<HTMLElement | null>(null)
-const objectChartRef = ref<HTMLElement | null>(null)
-const alarmHistoryRef = ref<HTMLElement | null>(null)
-const fallingHistoryRef = ref<HTMLElement | null>(null)
-
-let deviceChart: echarts.ECharts | null = null
-let ageChart: echarts.ECharts | null = null
-let objectChart: echarts.ECharts | null = null
-let alarmHistoryChart: echarts.ECharts | null = null
-let fallingHistoryChart: echarts.ECharts | null = null
-const updateInterval: ReturnType<typeof setInterval> | null = null
-
-// 更新时间
-const updateTime = () => {
-  const now = new Date()
-  currentTime.value = now.toLocaleString('zh-CN', {
-    year: 'numeric',
-    month: '2-digit',
-    day: '2-digit',
-    hour: '2-digit',
-    minute: '2-digit',
-    second: '2-digit',
-    hour12: false,
-  })
-}
-
-// 初始化图表
-const initCharts = () => {
-  if (!deviceChartRef.value) return
-
-  // 设备分布情况图表(安装位置)
-  deviceChart = echarts.init(deviceChartRef.value)
-
-  deviceChart.setOption({
-    grid: { top: 10, right: 10, bottom: 10, left: 10 },
-    tooltip: {
-      trigger: 'item',
-      formatter: '{a} <br/>{b}: {c} ({d}%)',
-    },
-    series: [
-      {
-        name: '安装位置',
-        type: 'pie',
-        radius: ['20%', '70%'],
-        roseType: 'area',
-        itemStyle: {
-          borderColor: '#0a0e17',
-          borderWidth: 2,
-        },
-        label: {
-          show: true,
-          formatter: '{b}: {c}',
-          color: '#fff', // 设置文字颜色为白色
-          fontSize: 14,
-          fontWeight: 'bold',
-          textBorderColor: 'rgba(0, 0, 0, 0.8)', // 添加黑色描边
-          textBorderWidth: 2, // 描边宽度
-          textShadowColor: 'rgba(0, 0, 0, 0.5)', // 添加文字阴影
-          textShadowBlur: 4, // 阴影模糊程度
-          textShadowOffsetX: 1, // 阴影X偏移
-          textShadowOffsetY: 1, // 阴影Y偏移
-        },
-        emphasis: {
-          label: {
-            show: true,
-            fontWeight: 'bold',
-            fontSize: 16, // 放大强调时的文字
-          },
-        },
-        data: [
-          { value: 12, name: '卫生间', itemStyle: { color: '#4dc9e6' } },
-          { value: 8, name: '卧室', itemStyle: { color: '#6de4ff' } },
-          { value: 6, name: '客厅', itemStyle: { color: '#2572ed' } },
-          { value: 4, name: '餐厅', itemStyle: { color: '#1a57c9' } },
-        ],
-      },
-    ],
-  })
-
-  // 设备年龄层次图表
-  if (ageChartRef.value) {
-    ageChart = echarts.init(ageChartRef.value)
-    ageChart.setOption({
-      grid: { top: 10, right: 10, bottom: 10, left: 10 },
-      tooltip: {
-        trigger: 'item',
-        formatter: '{a} <br/>{b}: {c} ({d}%)',
-      },
-      series: [
-        {
-          name: '设备年龄',
-          type: 'pie',
-          radius: ['30%', '70%'],
-          itemStyle: {
-            borderColor: '#0a0e17',
-            borderWidth: 2,
-          },
-          label: {
-            show: true,
-            formatter: '{b}: {c}',
-            color: '#fff',
-            fontSize: 14,
-            fontWeight: 'bold',
-          },
-          emphasis: {
-            label: {
-              show: true,
-              fontWeight: 'bold',
-              fontSize: 16,
-            },
-          },
-          data: [
-            { value: 15, name: '1年内', itemStyle: { color: '#4dc9e6' } },
-            { value: 10, name: '1-2年', itemStyle: { color: '#2572ed' } },
-            { value: 5, name: '2-3年', itemStyle: { color: '#6de4ff' } },
-            { value: 2, name: '3年以上', itemStyle: { color: '#1a57c9' } },
-          ],
-        },
-      ],
-    })
-  }
-
-  // 检测对象图表
-  if (objectChartRef.value) {
-    objectChart = echarts.init(objectChartRef.value)
-    objectChart.setOption({
-      grid: { top: 10, right: 10, bottom: 10, left: 10 },
-      tooltip: {
-        trigger: 'item',
-        formatter: '{a} <br/>{b}: {c} ({d}%)',
-      },
-      series: [
-        {
-          name: '检测对象',
-          type: 'pie',
-          radius: ['30%', '70%'],
-          itemStyle: {
-            borderColor: '#0a0e17',
-            borderWidth: 2,
-          },
-          label: {
-            show: true,
-            formatter: '{b}: {c}',
-            color: '#fff',
-            fontSize: 14,
-            fontWeight: 'bold',
-          },
-          emphasis: {
-            label: {
-              show: true,
-              fontWeight: 'bold',
-              fontSize: 16,
-            },
-          },
-          data: [
-            { value: 45, name: '长者', itemStyle: { color: '#2ecc71' } },
-            { value: 20, name: '访客', itemStyle: { color: '#f39c12' } },
-            { value: 35, name: '工作人员', itemStyle: { color: '#e74c3c' } },
-          ],
-        },
-      ],
-    })
-  }
-
-  // 历史告警统计图表
-  if (alarmHistoryRef.value) {
-    alarmHistoryChart = echarts.init(alarmHistoryRef.value)
-    alarmHistoryChart.setOption({
-      grid: { top: 10, right: 20, bottom: 30, left: 30 },
-      tooltip: {
-        trigger: 'axis',
-        axisPointer: {
-          type: 'shadow',
-        },
-      },
-      xAxis: {
-        type: 'category',
-        data: alarmHistoryData.value.dates,
-        axisLine: {
-          lineStyle: {
-            color: '#2a3b5a',
-          },
-        },
-        axisLabel: {
-          color: '#9cc5e0',
-          fontSize: 12,
-        },
-      },
-      yAxis: {
-        type: 'value',
-        axisLine: {
-          lineStyle: {
-            color: '#2a3b5a',
-          },
-        },
-        axisLabel: {
-          color: '#9cc5e0',
-          fontSize: 12,
-        },
-        splitLine: {
-          lineStyle: {
-            color: 'rgba(42, 59, 90, 0.3)',
-          },
-        },
-      },
-      series: [
-        {
-          name: '告警次数',
-          type: 'line',
-          smooth: true,
-          symbol: 'circle',
-          symbolSize: 8,
-          data: alarmHistoryData.value.values,
-          itemStyle: {
-            color: '#f39c12',
-          },
-          lineStyle: {
-            width: 3,
-            color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [
-              { offset: 0, color: '#f39c12' },
-              { offset: 1, color: '#e74c3c' },
-            ]),
-          },
-          areaStyle: {
-            color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
-              { offset: 0, color: 'rgba(243, 156, 18, 0.3)' },
-              { offset: 1, color: 'rgba(243, 156, 18, 0.1)' },
-            ]),
-          },
-        },
-      ],
-    })
-  }
-
-  // 历史跌倒统计图表
-  if (fallingHistoryRef.value) {
-    fallingHistoryChart = echarts.init(fallingHistoryRef.value)
-    fallingHistoryChart.setOption({
-      grid: { top: 10, right: 20, bottom: 30, left: 30 },
-      tooltip: {
-        trigger: 'axis',
-        axisPointer: {
-          type: 'shadow',
-        },
-      },
-      xAxis: {
-        type: 'category',
-        data: fallingHistoryData.value.dates,
-        axisLine: {
-          lineStyle: {
-            color: '#2a3b5a',
-          },
-        },
-        axisLabel: {
-          color: '#9cc5e0',
-          fontSize: 12,
-        },
-      },
-      yAxis: {
-        type: 'value',
-        axisLine: {
-          lineStyle: {
-            color: '#2a3b5a',
-          },
-        },
-        axisLabel: {
-          color: '#9cc5e0',
-          fontSize: 12,
-        },
-        splitLine: {
-          lineStyle: {
-            color: 'rgba(42, 59, 90, 0.3)',
-          },
-        },
-      },
-      series: [
-        {
-          name: '跌倒次数',
-          type: 'line',
-          smooth: true,
-          symbol: 'circle',
-          symbolSize: 8,
-          data: fallingHistoryData.value.values,
-          itemStyle: {
-            color: '#e74c3c',
-          },
-          lineStyle: {
-            width: 3,
-            color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [
-              { offset: 0, color: '#e74c3c' },
-              { offset: 1, color: '#c0392b' },
-            ]),
-          },
-          areaStyle: {
-            color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
-              { offset: 0, color: 'rgba(231, 76, 60, 0.3)' },
-              { offset: 1, color: 'rgba(231, 76, 60, 0.1)' },
-            ]),
-          },
-        },
-      ],
-    })
-  }
-}
-
-// 更新图表数据
-// const updateChartData = () => {
-//   // 模拟数据更新
-//   todayData.value = {
-//     detected: Math.max(
-//       100,
-//       Math.min(200, todayData.value.detected + Math.floor(Math.random() * 10 - 5))
-//     ),
-//     activity: Math.max(
-//       60,
-//       Math.min(95, todayData.value.activity + Math.floor(Math.random() * 5 - 2))
-//     ),
-//     alerts: Math.max(0, Math.min(10, todayData.value.alerts + Math.floor(Math.random() * 3 - 1))),
-//     onlineRate: Math.max(
-//       90,
-//       Math.min(100, todayData.value.onlineRate + Math.floor(Math.random() * 3 - 1))
-//     ),
-//   }
-// }
-
-// 组件挂载时初始化
-onMounted(() => {
-  updateTime()
-  initCharts()
-
-  // 设置定时器
-  setInterval(updateTime, 1000)
-
-  // 移除对已注释函数的调用
-  // updateInterval = setInterval(updateChartData, 5000)
-
-  // 窗口大小变化时调整图表大小
-  const handleResize = () => {
-    // 调整安装位置图表大小
-    if (deviceChart && deviceChartRef.value) {
-      // 强制重新计算容器大小
-      const containerWidth = deviceChartRef.value.offsetWidth
-      const containerHeight = deviceChartRef.value.offsetHeight
-
-      // 确保有有效的尺寸
-      if (containerWidth > 0 && containerHeight > 0) {
-        deviceChart.resize()
-      }
-    }
-
-    // 调整设备年龄层次图表大小
-    if (ageChart && ageChartRef.value) {
-      const containerWidth = ageChartRef.value.offsetWidth
-      const containerHeight = ageChartRef.value.offsetHeight
-
-      if (containerWidth > 0 && containerHeight > 0) {
-        ageChart.resize()
-      }
-    }
-
-    // 调整检测对象图表大小
-    if (objectChart && objectChartRef.value) {
-      const containerWidth = objectChartRef.value.offsetWidth
-      const containerHeight = objectChartRef.value.offsetHeight
-
-      if (containerWidth > 0 && containerHeight > 0) {
-        objectChart.resize()
-      }
-    }
-
-    // 调整历史告警统计图表大小
-    if (alarmHistoryChart && alarmHistoryRef.value) {
-      const containerWidth = alarmHistoryRef.value.offsetWidth
-      const containerHeight = alarmHistoryRef.value.offsetHeight
-
-      if (containerWidth > 0 && containerHeight > 0) {
-        alarmHistoryChart.resize()
-      }
-    }
-
-    // 调整历史跌倒统计图表大小
-    if (fallingHistoryChart && fallingHistoryRef.value) {
-      const containerWidth = fallingHistoryRef.value.offsetWidth
-      const containerHeight = fallingHistoryRef.value.offsetHeight
-
-      if (containerWidth > 0 && containerHeight > 0) {
-        fallingHistoryChart.resize()
-      }
-    }
-  }
-
-  window.addEventListener('resize', handleResize)
-
-  // 组件挂载后立即触发一次调整,确保图表正确显示
-  setTimeout(() => {
-    handleResize()
-  }, 100)
-})
-
-// 组件卸载时清理
-onUnmounted(() => {
-  if (updateInterval) {
-    clearInterval(updateInterval)
-  }
-
-  if (deviceChart) {
-    deviceChart.dispose()
-  }
-
-  if (ageChart) {
-    ageChart.dispose()
-  }
-
-  if (objectChart) {
-    objectChart.dispose()
-  }
-
-  if (alarmHistoryChart) {
-    alarmHistoryChart.dispose()
-  }
-
-  if (fallingHistoryChart) {
-    fallingHistoryChart.dispose()
-  }
-})
-</script>
-
-<style lang="less" scoped>
-@bg-color: #22284a;
-@text-color: #e0e0e0;
-@panel-bg: #0b173f;
-@border-color: #2a3b5a;
-@primary-color: #4dc9e6;
-@secondary-color: #6de4ff;
-@accent-color: #2572ed;
-@success-color: #2ecc71;
-@warning-color: #f39c12;
-@danger-color: #e74c3c;
-@gradient-start: #181c41;
-@gradient-end: #22284a;
-@glow-color: rgba(77, 201, 230, 0.2);
-@data-flow-color: rgba(77, 201, 230, 0.7);
-
-* {
-  margin: 0;
-  padding: 0;
-  box-sizing: border-box;
-  font-family: 'Microsoft YaHei', Arial, sans-serif;
-}
-
-.radar-monitoring-screen {
-  background-color: @bg-color;
-  color: @text-color;
-  overflow: hidden;
-  height: 100vh;
-  padding: 12px;
-}
-
-/* 顶部区域 */
-.header {
-  height: 70px;
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  padding: 0 20px;
-  // background: linear-gradient(90deg, rgba(19, 28, 51, 0.8) 0%, rgba(32, 40, 65, 0.8) 100%);
-  background-color: @panel-bg;
-  border: 1px solid @border-color;
-  border-radius: 8px;
-  box-shadow: 0 0 15px rgba(0, 180, 255, 0.2);
-  margin-bottom: 12px;
-  position: relative;
-  overflow: hidden;
-}
-
-.system-name {
-  font-size: 24px;
-  background: linear-gradient(90deg, @primary-color, @secondary-color);
-  -webkit-background-clip: text;
-  -webkit-text-fill-color: transparent;
-  text-shadow: 0 0 10px rgba(109, 228, 255, 0.5);
-  letter-spacing: 2px;
-}
-
-.running-days {
-  font-size: 22px;
-  color: @secondary-color;
-  text-shadow: 0 0 8px rgba(109, 228, 255, 0.7);
-}
-
-.time-info {
-  font-size: 16px;
-  color: @primary-color;
-}
-
-/* 内容区域 */
-.content-area {
-  display: flex;
-  height: calc(100vh - 110px);
-  gap: 12px;
-  margin-bottom: 12px;
-}
-
-.panel {
-  flex: 1;
-  background: @panel-bg;
-  border: 1px solid @border-color;
-  border-radius: 8px;
-  padding: 15px;
-  box-shadow: 0 0 15px rgba(0, 180, 255, 0.1);
-  display: flex;
-  flex-direction: column;
-  position: relative;
-  overflow: hidden;
-
-  &::after {
-    content: '';
-    position: absolute;
-    top: 0;
-    left: 0;
-    right: 0;
-    height: 3px;
-    background: linear-gradient(90deg, @primary-color, @accent-color);
-  }
-}
-
-.panel-title {
-  font-size: 16px;
-  color: @secondary-color;
-  margin-bottom: 15px;
-  display: flex;
-  align-items: center;
-  padding-bottom: 8px;
-  border-bottom: 1px solid rgba(42, 59, 90, 0.5);
-
-  i {
-    margin-right: 8px;
-    font-size: 18px;
-  }
-}
-
-.panel-content {
-  flex: 1;
-  overflow: hidden;
-}
-
-.center-panel {
-  flex: 1.5;
-  display: flex;
-  justify-content: center;
-  align-items: center;
-  background: #0b173f;
-  min-width: min-content;
-  height: 100%;
-  position: relative;
-  padding: 10px;
-}
-
-/* 地图容器 - 作为标记点的相对定位参考系 */
-.map-container {
-  position: relative;
-  display: inline-block;
-  // width: 100%;
-  // height: 100%;
-  // max-width: 600px;
-  // max-height: 400px;
-  border-radius: 6px;
-  overflow: hidden;
-  background-color: #2ecc71;
-  /* 确保容器有明确的宽高,以便内部元素可以基于百分比定位 */
-}
-
-/* 地图图片 - 确保图片始终完全显示在容器内 */
-.map-img {
-  width: 100%;
-  height: 100%;
-  object-fit: contain;
-  display: block;
-  border-radius: 6px;
-  /* 确保图片不会超出容器,同时保持原始比例 */
-}
-
-/* 地图标签基础样式 - 确保基于地图容器的相对定位 */
-.map-label {
-  position: absolute;
-  background: rgba(32, 40, 65, 0.9);
-  border: 1px solid @border-color;
-  border-radius: 4px;
-  padding: 4px 8px;
-  font-size: 14px;
-  color: @secondary-color;
-  font-weight: bold;
-  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
-  z-index: 10;
-  transform: translate(-50%, -50%);
-  animation: pulse 2s infinite;
-  pointer-events: none;
-}
-
-/* 小区名称特殊样式 */
-.map-label:not([class*='building-']) {
-  top: 10%;
-  left: 50%;
-  background: rgba(77, 201, 230, 0.9);
-  color: #000;
-  font-size: 18px;
-  padding: 6px 12px;
-  border: 2px solid #fff;
-  animation: glow 2s infinite alternate;
-}
-
-/* 建筑位置 - 使用百分比定位确保基于图片相对位置 */
-.building-1 {
-  top: 30%;
-  left: 30%;
-}
-
-.building-2 {
-  top: 50%;
-  left: 70%;
-}
-
-.building-3 {
-  top: 70%;
-  left: 50%;
-}
-
-.building-4 {
-  top: 40%;
-  left: 20%;
-}
-
-/* 位置标记动画效果 */
-@keyframes pulse {
-  0% {
-    transform: translate(-50%, -50%) scale(1);
-    box-shadow: 0 0 0 0 rgba(77, 201, 230, 0.4);
-  }
-  70% {
-    transform: translate(-50%, -50%) scale(1.05);
-    box-shadow: 0 0 0 5px rgba(77, 201, 230, 0);
-  }
-  100% {
-    transform: translate(-50%, -50%) scale(1);
-    box-shadow: 0 0 0 0 rgba(77, 201, 230, 0);
-  }
-}
-
-/* 小区名称发光效果 */
-@keyframes glow {
-  from {
-    box-shadow: 0 0 5px rgba(77, 201, 230, 0.7);
-  }
-  to {
-    box-shadow:
-      0 0 15px rgba(77, 201, 230, 1),
-      0 0 20px rgba(77, 201, 230, 0.7);
-  }
-}
-
-.data-grid {
-  display: grid;
-  grid-template-columns: 1fr 1fr;
-  gap: 12px;
-}
-
-.chart-container {
-  width: 100%;
-  height: 200px; /* 设置固定高度 */
-  min-height: 200px; /* 确保最小高度 */
-}
-
-.status-list {
-  list-style: none;
-  max-height: 250px;
-  overflow-y: auto;
-}
-
-/* 底部区域 */
-.footer {
-  height: 50px;
-  display: flex;
-  justify-content: center;
-  align-items: center;
-  background: @panel-bg;
-  border: 1px solid @border-color;
-  border-radius: 8px;
-  color: @primary-color;
-  font-size: 14px;
-}
-
-.glow-effect {
-  position: absolute;
-  width: 100%;
-  height: 100%;
-  top: 0;
-  left: 0;
-  pointer-events: none;
-  background:
-    radial-gradient(circle at 20% 30%, rgba(37, 114, 237, 0.1) 0%, transparent 50%),
-    radial-gradient(circle at 80% 70%, rgba(77, 201, 230, 0.1) 0%, transparent 50%);
-  z-index: -1;
-}
-
-.data-flow {
-  position: absolute;
-  bottom: 0;
-  left: 0;
-  width: 100%;
-  height: 3px;
-  background: linear-gradient(90deg, transparent, rgba(77, 201, 230, 0.7), transparent);
-  animation: dataFlow 3s linear infinite;
-}
-
-@keyframes dataFlow {
-  0% {
-    transform: translateX(-100%);
-  }
-  100% {
-    transform: translateX(100%);
-  }
-}
-</style>

+ 0 - 68
src/views/dashboard/components/statusItem/index.vue

@@ -1,68 +0,0 @@
-<template>
-  <li class="status-item">
-    <div class="status-name">
-      <span class="status-indicator" :class="statusClass"></span>
-      <span>{{ name }}</span>
-    </div>
-    <div class="status-value">{{ value }}</div>
-  </li>
-</template>
-
-<script setup lang="ts">
-import { computed } from 'vue'
-import { type StatusItemProps } from '../../types'
-
-defineOptions({
-  name: 'StatusItem',
-})
-
-const props = defineProps<StatusItemProps>()
-
-const statusClass = computed(() => {
-  return {
-    正常: 'normal',
-    警告: 'warning',
-    异常: 'danger',
-  }[props.status]
-})
-</script>
-
-<style lang="less" scoped>
-.status-item {
-  display: flex;
-  justify-content: space-between;
-  padding: 10px 0;
-  border-bottom: 1px solid rgba(42, 59, 90, 0.5);
-
-  &:last-child {
-    border-bottom: none;
-  }
-}
-
-.status-name {
-  display: flex;
-  align-items: center;
-}
-
-.status-indicator {
-  width: 10px;
-  height: 10px;
-  border-radius: 50%;
-  margin-right: 10px;
-
-  &.normal {
-    background-color: #2ecc71;
-    box-shadow: 0 0 8px #2ecc71;
-  }
-
-  &.warning {
-    background-color: #f39c12;
-    box-shadow: 0 0 8px #f39c12;
-  }
-
-  &.danger {
-    background-color: #e74c3c;
-    box-shadow: 0 0 8px #e74c3c;
-  }
-}
-</style>

+ 108 - 77
src/views/dashboard/index.vue

@@ -1,21 +1,23 @@
 <template>
 <template>
   <div class="dashboard">
   <div class="dashboard">
-    <!-- <ScreenPage /> -->
     <div class="dashboard-header">
     <div class="dashboard-header">
       <div class="community-name">{{ tenantName }}</div>
       <div class="community-name">{{ tenantName }}</div>
       <div class="running-days">已安全守护 {{ todayData.systemGuardDay }} 天</div>
       <div class="running-days">已安全守护 {{ todayData.systemGuardDay }} 天</div>
       <div class="time-info">{{ currentTime }}</div>
       <div class="time-info">{{ currentTime }}</div>
       <div class="data-flow header-flow"></div>
       <div class="data-flow header-flow"></div>
     </div>
     </div>
-    <div class="dashboard-content custom-scroll">
-      <div class="block">
+    <div class="dashboard-content">
+      <div class="block custom-scroll">
         <div class="data-grid">
         <div class="data-grid">
           <DeviceOnlineRateCard
           <DeviceOnlineRateCard
             :online-count="todayData.onlineCount"
             :online-count="todayData.onlineCount"
             :device-count="todayData.deviceCount"
             :device-count="todayData.deviceCount"
           ></DeviceOnlineRateCard>
           ></DeviceOnlineRateCard>
           <PeopleDetectedCard :detectedCount="todayData.detectedCount"></PeopleDetectedCard>
           <PeopleDetectedCard :detectedCount="todayData.detectedCount"></PeopleDetectedCard>
-          <ObjectDistributionCard :important-count="12" :normal-count="23"></ObjectDistributionCard>
+          <ElderActivityCard :activity-rate="todayData.activeRate"></ElderActivityCard>
+        </div>
+        <div class="data-row">
+          <ObjectDistributionCard :guardList="todayData.guardList"></ObjectDistributionCard>
           <AlertFallCompareCard :fall-count="12" :alert-count="24"></AlertFallCompareCard>
           <AlertFallCompareCard :fall-count="12" :alert-count="24"></AlertFallCompareCard>
         </div>
         </div>
         <DeviceLocationCard :data="todayData.installPositionList"></DeviceLocationCard>
         <DeviceLocationCard :data="todayData.installPositionList"></DeviceLocationCard>
@@ -40,14 +42,15 @@
           <ZoomInOutlined @click="zoomIn" />
           <ZoomInOutlined @click="zoomIn" />
         </a-space>
         </a-space>
       </div>
       </div>
-      <div class="block">
+      <div class="block custom-scroll">
         <div class="data-row">
         <div class="data-row">
-          <DeviceAgeCard></DeviceAgeCard>
-          <ElderActivityCard :activity-rate="86.758"></ElderActivityCard>
+          <DeviceAgeCard :ageList="todayData.ageList"></DeviceAgeCard>
         </div>
         </div>
 
 
-        <AlarmHistoryCard style="margin-bottom: 12px"></AlarmHistoryCard>
-        <FallingHistoryCard></FallingHistoryCard>
+        <div class="data-line">
+          <AlarmHistoryCard></AlarmHistoryCard>
+          <FallingHistoryCard></FallingHistoryCard>
+        </div>
       </div>
       </div>
     </div>
     </div>
     <div class="dashboard-footer">合肥雷能信息技术有限公司 © 2025 All Rights Reserved.</div>
     <div class="dashboard-footer">合肥雷能信息技术有限公司 © 2025 All Rights Reserved.</div>
@@ -55,8 +58,7 @@
 </template>
 </template>
 
 
 <script setup lang="ts">
 <script setup lang="ts">
-// import ScreenPage from './components/screen/index.vue'
-import { ref, onMounted, onUnmounted } from 'vue'
+import { ref, onMounted, onUnmounted, watch } from 'vue'
 import DeviceOnlineRateCard from './components/DeviceOnlineRateCard/index.vue'
 import DeviceOnlineRateCard from './components/DeviceOnlineRateCard/index.vue'
 import AlertFallCompareCard from './components/AlertFallCompareCard/index.vue'
 import AlertFallCompareCard from './components/AlertFallCompareCard/index.vue'
 import ElderActivityCard from './components/ElderActivityCard/index.vue'
 import ElderActivityCard from './components/ElderActivityCard/index.vue'
@@ -66,10 +68,12 @@ import DeviceAgeCard from './components/DeviceAgeCard/index.vue'
 import ObjectDistributionCard from './components/ObjectDistributionCard/index.vue'
 import ObjectDistributionCard from './components/ObjectDistributionCard/index.vue'
 import AlarmHistoryCard from './components/AlarmHistoryCard/index.vue'
 import AlarmHistoryCard from './components/AlarmHistoryCard/index.vue'
 import FallingHistoryCard from './components/FallingHistoryCard/index.vue'
 import FallingHistoryCard from './components/FallingHistoryCard/index.vue'
-import * as statsApi from '@/api/stats'
 import { useUserStore } from '@/stores/user'
 import { useUserStore } from '@/stores/user'
 import type { TodayData } from './types'
 import type { TodayData } from './types'
 import { ZoomInOutlined, ZoomOutOutlined, RedoOutlined } from '@ant-design/icons-vue'
 import { ZoomInOutlined, ZoomOutOutlined, RedoOutlined } from '@ant-design/icons-vue'
+import { useResponsiveLayout } from '@/utils/chartManager'
+import { useDashboardPolling } from '@/hooks/useDashboardPolling'
+import { deviceInstallPositionNameMap } from '@/const/device'
 
 
 const userStore = useUserStore()
 const userStore = useUserStore()
 const tenantName = (userStore.userInfo.tenantName ?? '雷能技术') + ' 智慧大屏'
 const tenantName = (userStore.userInfo.tenantName ?? '雷能技术') + ' 智慧大屏'
@@ -107,31 +111,52 @@ const todayData = ref<TodayData>({
   fallingCount: 0, // 跌倒次数
   fallingCount: 0, // 跌倒次数
   detectedCount: 0, // 检测人数
   detectedCount: 0, // 检测人数
   activeRate: 0, // 活跃度
   activeRate: 0, // 活跃度
-  guardList: [], // 年龄分布
+  guardList: [], // 检测对象
+  ageList: [], // 年龄层次
   installPositionList: [], // 安装位置
   installPositionList: [], // 安装位置
 })
 })
 
 
-const fetchDashboardData = async () => {
-  try {
-    const res = await statsApi.statsHomeScreenQuery({
-      tenantId: userStore.userInfo.tenantId || '',
-    })
-    console.log('🚀🚀🚀仪表盘数据:', res)
-    const { installPositionList, guardList } = res.data
-    todayData.value = res.data
-    todayData.value.installPositionList = installPositionList
-      ? installPositionList.map((item) => ({
-          ...item,
-          name: item.installPosition || '未知',
-        }))
-      : []
-
-    todayData.value.guardList = guardList ?? []
-  } catch (error) {
-    console.error('获取仪表盘数据失败:', error)
-  }
-}
-fetchDashboardData()
+useResponsiveLayout()
+
+const { todayScreenData, fallHistoryData, alarmHistoryData } = useDashboardPolling({
+  tenantId: userStore.userInfo.tenantId || '',
+  queryType: 'day',
+})
+
+watch(
+  () => todayScreenData.value,
+  (val) => {
+    console.log('todayScreenData更新了', val)
+    todayData.value = val || todayData.value
+    todayData.value.guardList = val?.guardList ?? []
+    todayData.value.ageList = val?.ageList ?? []
+    todayData.value.installPositionList =
+      val?.installPositionList.map((item) => ({
+        ...item,
+        name:
+          deviceInstallPositionNameMap[
+            item.installPosition as keyof typeof deviceInstallPositionNameMap
+          ] || '未知',
+      })) || []
+  },
+  { immediate: true }
+)
+
+watch(
+  () => fallHistoryData.value,
+  (val) => {
+    console.log('🚀🚀🚀 fallHistoryData 更新了', val)
+  },
+  { immediate: true }
+)
+
+watch(
+  () => alarmHistoryData.value,
+  (val) => {
+    console.log('🚀🚀🚀 alarmHistoryData 更新了', val)
+  },
+  { immediate: true }
+)
 
 
 const toDeviceList = () => {
 const toDeviceList = () => {
   window.open('/device/list', '_blank')
   window.open('/device/list', '_blank')
@@ -154,7 +179,6 @@ const zoomOut = () => {
   scale.value = Math.max(0.5, scale.value - 0.1)
   scale.value = Math.max(0.5, scale.value - 0.1)
 }
 }
 
 
-// 监听窗口大小变化
 onMounted(() => {
 onMounted(() => {
   window.addEventListener('resize', handleResize)
   window.addEventListener('resize', handleResize)
 })
 })
@@ -163,7 +187,6 @@ onUnmounted(() => {
 })
 })
 
 
 const handleResize = () => {
 const handleResize = () => {
-  // 当窗口宽度小于 1200px 时,缩小地图
   if (window.innerWidth < 1400) {
   if (window.innerWidth < 1400) {
     scale.value = 0.5
     scale.value = 0.5
   } else if (window.innerWidth < 1600) {
   } else if (window.innerWidth < 1600) {
@@ -197,41 +220,6 @@ const handleResize = () => {
   font-family: 'Microsoft YaHei', Arial, sans-serif;
   font-family: 'Microsoft YaHei', Arial, sans-serif;
 }
 }
 
 
-.custom-scroll {
-  overflow-y: auto;
-
-  &::-webkit-scrollbar {
-    width: 4px;
-    height: 4px;
-  }
-
-  &::-webkit-scrollbar-track {
-    background: transparent;
-  }
-
-  &::-webkit-scrollbar-thumb {
-    background-color: transparent;
-    border-radius: 2px;
-  }
-
-  &:hover::-webkit-scrollbar-thumb {
-    background-color: #00f0ff;
-  }
-
-  &::-webkit-scrollbar-button {
-    display: none;
-    width: 0;
-    height: 0;
-  }
-
-  scrollbar-width: thin;
-  scrollbar-color: transparent transparent;
-
-  &:hover {
-    scrollbar-color: #00f0ff transparent;
-  }
-}
-
 .dashboard {
 .dashboard {
   background-color: @bg-color;
   background-color: @bg-color;
   color: @text-color;
   color: @text-color;
@@ -251,7 +239,6 @@ const handleResize = () => {
     border: 1px solid @border-color;
     border: 1px solid @border-color;
     border-radius: 8px;
     border-radius: 8px;
     box-shadow: 0 0 15px rgba(0, 180, 255, 0.2);
     box-shadow: 0 0 15px rgba(0, 180, 255, 0.2);
-    margin-bottom: 12px;
     position: relative;
     position: relative;
     overflow: hidden;
     overflow: hidden;
     flex-wrap: wrap;
     flex-wrap: wrap;
@@ -306,12 +293,10 @@ const handleResize = () => {
     border-radius: 8px;
     border-radius: 8px;
     display: flex;
     display: flex;
     gap: 20px;
     gap: 20px;
-    overflow: hidden;
     min-height: 0;
     min-height: 0;
     overflow-y: auto;
     overflow-y: auto;
 
 
     .block {
     .block {
-      min-height: 500px;
       flex-grow: 1;
       flex-grow: 1;
       flex-shrink: 1;
       flex-shrink: 1;
       flex-basis: 400px;
       flex-basis: 400px;
@@ -352,8 +337,6 @@ const handleResize = () => {
       }
       }
 
 
       .map-img {
       .map-img {
-        // width: 100%;
-        // height: 100%;
         object-fit: contain;
         object-fit: contain;
         display: block;
         display: block;
         border-radius: 6px;
         border-radius: 6px;
@@ -426,7 +409,7 @@ const handleResize = () => {
 
 
     .data-grid {
     .data-grid {
       display: grid;
       display: grid;
-      grid-template-columns: 1fr 1fr;
+      grid-template-columns: 1fr 1fr 1fr;
       gap: 12px;
       gap: 12px;
       margin-bottom: 12px;
       margin-bottom: 12px;
     }
     }
@@ -437,6 +420,12 @@ const handleResize = () => {
       gap: 12px;
       gap: 12px;
       margin-bottom: 12px;
       margin-bottom: 12px;
     }
     }
+
+    .data-line {
+      display: grid;
+      grid-template-columns: 1fr;
+      gap: 12px;
+    }
   }
   }
 
 
   &-footer {
   &-footer {
@@ -510,3 +499,45 @@ const handleResize = () => {
   }
   }
 }
 }
 </style>
 </style>
+
+<style lang="less">
+.custom-scroll {
+  overflow-y: auto;
+
+  &::-webkit-scrollbar {
+    width: 4px;
+  }
+
+  &::-webkit-scrollbar-track {
+    background: transparent;
+  }
+
+  &::-webkit-scrollbar-thumb {
+    background-color: transparent;
+    border-radius: 2px;
+  }
+
+  &:hover::-webkit-scrollbar-thumb {
+    background-color: #00f0ff;
+  }
+
+  &::-webkit-scrollbar-button {
+    display: none;
+    width: 0;
+    height: 0;
+  }
+
+  &::-webkit-scrollbar-button:single-button {
+    display: none;
+    width: 0;
+    height: 0;
+  }
+
+  scrollbar-width: thin;
+  scrollbar-color: transparent transparent;
+
+  &:hover {
+    scrollbar-color: #00f0ff transparent;
+  }
+}
+</style>

+ 1 - 0
src/views/dashboard/types/index.ts

@@ -23,5 +23,6 @@ export type TodayData = Pick<
   | 'detectedCount'
   | 'detectedCount'
   | 'activeRate'
   | 'activeRate'
   | 'guardList'
   | 'guardList'
+  | 'ageList'
   | 'installPositionList'
   | 'installPositionList'
 >
 >