Ver código fonte

feat(dashboard): 重构仪表盘组件并添加数据绑定功能,首页大屏统计接口对接;

- 重构所有图表组件,添加响应式更新和销毁逻辑
- 为图表组件添加props数据绑定
- 实现仪表盘数据从API获取并展示
- 优化地图组件交互,添加缩放功能
- 添加滚动条样式优化
- 更新类型定义以匹配API数据结构
liujia 1 mês atrás
pai
commit
9ac8d42

+ 0 - 1
components.d.ts

@@ -12,7 +12,6 @@ declare module 'vue' {
     AAvatar: typeof import('ant-design-vue/es')['Avatar']
     ABadge: typeof import('ant-design-vue/es')['Badge']
     AButton: typeof import('ant-design-vue/es')['Button']
-    ACard: typeof import('ant-design-vue/es')['Card']
     ACascader: typeof import('ant-design-vue/es')['Cascader']
     ACheckbox: typeof import('ant-design-vue/es')['Checkbox']
     ACheckboxGroup: typeof import('ant-design-vue/es')['CheckboxGroup']

+ 1 - 1
src/api/stats/index.ts

@@ -17,7 +17,7 @@ export const statsAlarmQuery = (
 
 // 首页大屏统计
 export const statsHomeScreenQuery = (params: {
-  tenantId: number
+  tenantId: string
 }): Promise<ResponseData<TYPE.StatsHomeScreenQueryData>> => {
   return request.post('/stats/screen', params)
 }

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

@@ -117,6 +117,7 @@ type GuardList = {
 type InstallPositionList = {
   installPosition: string // 	安装位置
   count: number // 	安装数量
+  name?: string // 安装位置名称
 }
 /**
  * 首页大屏统计出参

+ 2 - 0
src/api/user/types.ts

@@ -38,4 +38,6 @@ export interface LoginResponseData {
   phone: string // 手机号
   userId: number // userId
   account: string // 账户
+  tenantId: string // 租户ID
+  tenantName: string // 租户名称
 }

+ 6 - 0
src/stores/user.ts

@@ -22,6 +22,8 @@ type UserInfo = {
   phone: string // 手机号
   userId: number // userId
   account: string // 账户
+  tenantId: string // 租户ID
+  tenantName: string // 租户名称
 }
 
 export const useUserStore = defineStore(
@@ -44,6 +46,8 @@ export const useUserStore = defineStore(
       phone: '',
       userId: 0,
       account: '',
+      tenantId: '',
+      tenantName: '',
     })
 
     // 登录
@@ -94,6 +98,8 @@ export const useUserStore = defineStore(
         phone: '',
         userId: 0,
         account: '',
+        tenantId: '',
+        tenantName: '',
       }
       const redirectPath = router.currentRoute.value.fullPath || ''
       console.log('✅ userStore Logout', redirectPath)

+ 55 - 7
src/views/dashboard/components/AlertFallCompareCard/index.vue

@@ -8,21 +8,40 @@
 </template>
 
 <script lang="ts" setup>
-import { ref, onMounted } from 'vue'
+import { ref, onMounted, onUnmounted, watch, nextTick } from 'vue'
 import * as echarts from 'echarts'
 import TechCard from '../TechCard/index.vue'
 
 defineOptions({ name: 'AlertFallCompareCard' })
 
 const chartRef = ref<HTMLDivElement | null>(null)
-const fallCount = 3
-const alertCount = 1
+const chartInstance = ref<echarts.ECharts | null>(null)
 
-onMounted(() => {
+interface Props {
+  fallCount: number
+  alertCount: number
+}
+
+const props = withDefaults(defineProps<Props>(), {
+  fallCount: 0,
+  alertCount: 0,
+})
+
+const createChart = () => {
   if (!chartRef.value) return
-  const chart = echarts.init(chartRef.value)
 
-  chart.setOption({
+  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' },
@@ -49,7 +68,7 @@ onMounted(() => {
     series: [
       {
         type: 'bar',
-        data: [fallCount, alertCount],
+        data: [props.fallCount, props.alertCount],
         barWidth: 40,
         itemStyle: {
           color: (params: { dataIndex: number }) => ['#ff4d6d', '#f39c12'][params.dataIndex],
@@ -63,6 +82,35 @@ onMounted(() => {
       },
     ],
   })
+}
+
+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)
 })
 </script>
 

+ 58 - 9
src/views/dashboard/components/DeviceLocationCard/index.vue

@@ -6,9 +6,8 @@
 </template>
 
 <script setup lang="ts">
-import { onMounted, ref } from 'vue'
+import { onMounted, ref, onUnmounted, watch, nextTick } from 'vue'
 import * as echarts from 'echarts'
-import { useChartResize } from '@/utils/useChartResize'
 import TechCard from '../TechCard/index.vue'
 
 defineOptions({
@@ -16,12 +15,35 @@ defineOptions({
 })
 
 const chartRef = ref<HTMLDivElement | null>(null)
+const chartInstance = ref<echarts.ECharts | null>(null)
 
-onMounted(() => {
+interface Props {
+  data: Array<{ name?: string; count: number; installPosition: string }>
+}
+
+const props = withDefaults(defineProps<Props>(), {
+  data: () => [],
+})
+
+// 初始化图表
+const initChart = () => {
   if (!chartRef.value) return
-  const chart = echarts.init(chartRef.value)
 
-  chart.setOption({
+  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 locationValues = props.data.map((item) => item.count)
+
+  chartInstance.value.setOption({
     tooltip: {
       trigger: 'axis',
       axisPointer: { type: 'shadow' },
@@ -40,7 +62,7 @@ onMounted(() => {
     },
     yAxis: {
       type: 'category',
-      data: ['卫生间', '卧室', '客厅', '餐厅'],
+      data: locationNames,
       axisLabel: { color: '#9cc5e0' },
       axisTick: { show: false },
       axisLine: { show: false },
@@ -48,12 +70,12 @@ onMounted(() => {
     series: [
       {
         type: 'bar',
-        data: [12, 8, 6, 4],
+        data: locationValues,
         barWidth: 14,
         itemStyle: {
           color: (params: { dataIndex: number }) => {
             const colors = ['#4dc9e6', '#6de4ff', '#2572ed', '#1a57c9']
-            return colors[params.dataIndex]
+            return colors[params.dataIndex % colors.length]
           },
         },
         label: {
@@ -65,8 +87,35 @@ onMounted(() => {
       },
     ],
   })
+}
+
+const resizeChart = () => {
+  if (chartInstance.value) {
+    chartInstance.value.resize()
+  }
+}
+
+onMounted(() => {
+  nextTick(() => {
+    initChart()
+    window.addEventListener('resize', resizeChart)
+  })
+})
+
+watch(
+  () => props.data,
+  () => {
+    updateChart()
+  },
+  { deep: true }
+)
 
-  useChartResize(chart, chartRef.value)
+onUnmounted(() => {
+  if (chartInstance.value) {
+    chartInstance.value.dispose()
+    chartInstance.value = null
+  }
+  window.removeEventListener('resize', resizeChart)
 })
 </script>
 

+ 37 - 25
src/views/dashboard/components/DeviceOnlineRateCard/index.vue

@@ -6,7 +6,7 @@
     <div ref="chartRef" class="chart-container"></div>
     <div class="footer">
       <div class="label">设备数量</div>
-      <div class="count">{{ onlineCount }} /1258 台</div>
+      <div class="count">{{ onlineCount }} / {{ deviceCount }} 台</div>
     </div>
   </TechCard>
 </template>
@@ -23,32 +23,41 @@ defineOptions({
 
 const chartRef = ref<HTMLDivElement | null>(null)
 const chartInstance = ref<echarts.ECharts | null>(null)
-const onlineRate = ref(85.8)
-const onlineCount = 1284
+const onlineRate = ref<string>('')
 
-const timer = setInterval(() => {
-  onlineRate.value = Math.floor(Math.random() * 100)
-}, 1000)
-
-setTimeout(() => {
-  clearInterval(timer)
-}, 10000)
+type Props = {
+  onlineCount: number
+  deviceCount: number
+}
+// const emit = defineEmits<{
+//   (e: 'success', value: void): void
+// }>()
+const props = withDefaults(defineProps<Props>(), {
+  onlineCount: 0,
+  deviceCount: 0,
+})
 
 const createChart = () => {
   if (!chartRef.value) return
   const chart = echarts.init(chartRef.value)
   chartInstance.value = chart
 
-  chart.setOption({
+  updateChart()
+}
+
+const updateChart = () => {
+  if (!chartInstance.value) return
+
+  chartInstance.value.setOption({
     series: [
       {
         type: 'liquidFill',
         radius: '80%',
         center: ['50%', '50%'],
-        data: [onlineRate.value / 100],
+        data: [onlineRate.value],
         label: {
           formatter: `${onlineRate.value}%`,
-          fontSize: 28,
+          fontSize: 20,
           fontWeight: 'bold',
           color: '#00f0ff',
         },
@@ -72,24 +81,26 @@ const createChart = () => {
   })
 }
 
-watch(onlineRate, (newVal) => {
+const resizeChart = () => {
   if (chartInstance.value) {
-    chartInstance.value.setOption({
-      series: [
-        {
-          data: [newVal / 100],
-          label: {
-            formatter: `${newVal}%`,
-          },
-        },
-      ],
-    })
+    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)
   })
 })
 
@@ -97,6 +108,7 @@ onUnmounted(() => {
   if (chartInstance.value) {
     chartInstance.value.dispose()
     chartInstance.value = null
+    window.removeEventListener('resize', resizeChart)
   }
 })
 </script>

+ 92 - 36
src/views/dashboard/components/ElderActivityCard/index.vue

@@ -6,13 +6,13 @@
     <div ref="chartRef" class="chart-container"></div>
     <div class="footer">
       <div class="label">当前活跃度</div>
-      <div class="count">{{ activityRate }}%</div>
+      <div class="count">{{ rate }}%</div>
     </div>
   </TechCard>
 </template>
 
 <script lang="ts" setup>
-import { ref, onMounted } from 'vue'
+import { ref, onMounted, watch, onUnmounted, nextTick } from 'vue'
 import * as echarts from 'echarts'
 import TechCard from '../TechCard/index.vue'
 
@@ -21,44 +21,100 @@ defineOptions({
 })
 
 const chartRef = ref<HTMLDivElement | null>(null)
-const activityRate = 78
+const chartInstance = ref<echarts.ECharts | null>(null)
 
-onMounted(() => {
-  if (chartRef.value) {
-    const chart = echarts.init(chartRef.value)
-    chart.setOption({
-      tooltip: {
-        trigger: 'item',
-        appendToBody: true,
-        formatter: '{b}: {c}%',
+type Props = {
+  activityRate: number
+}
+
+const props = withDefaults(defineProps<Props>(), {
+  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}%',
+    },
+    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 },
       },
-      series: [
-        {
-          type: 'pie',
-          radius: ['50%', '70%'],
-          center: ['50%', '50%'],
-          data: [
-            { value: activityRate, name: '活跃', itemStyle: { color: '#00f0ff' } },
-            { value: 100 - activityRate, name: '非活跃', itemStyle: { color: '#2c5364' } },
-          ],
-          label: { show: false },
-          labelLine: { show: false },
-        },
-      ],
-      graphic: {
-        type: 'text',
-        left: 'center',
-        top: 'center',
-        style: {
-          text: `${activityRate}%`,
-          fontSize: 28,
-          fontWeight: 'bold',
-          fill: '#00f0ff',
-          textShadow: '0 0 10px #00ff9f',
-        },
+    ],
+    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>
 

+ 54 - 8
src/views/dashboard/components/ObjectDistributionCard/index.vue

@@ -6,9 +6,8 @@
 </template>
 
 <script setup lang="ts">
-import { onMounted, ref } from 'vue'
+import { onMounted, ref, onUnmounted, watch, nextTick } from 'vue'
 import * as echarts from 'echarts'
-import { useChartResize } from '@/utils/useChartResize'
 import TechCard from '../TechCard/index.vue'
 
 defineOptions({
@@ -16,12 +15,33 @@ defineOptions({
 })
 
 const chartRef = ref<HTMLDivElement | null>(null)
+const chartInstance = ref<echarts.ECharts | null>(null)
 
-onMounted(() => {
+interface Props {
+  importantCount: number
+  normalCount: number
+}
+
+const props = withDefaults(defineProps<Props>(), {
+  importantCount: 20,
+  normalCount: 45,
+})
+
+const initChart = () => {
   if (!chartRef.value) return
-  const chart = echarts.init(chartRef.value)
 
-  chart.setOption({
+  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' },
@@ -48,12 +68,12 @@ onMounted(() => {
     series: [
       {
         type: 'bar',
-        data: [20, 45],
+        data: [props.importantCount, props.normalCount],
         barWidth: 30,
         itemStyle: {
           color: (params: { dataIndex: number }) => {
             const colors = ['#f39c12', '#2ecc71']
-            return colors[params.dataIndex]
+            return colors[params.dataIndex % colors.length]
           },
         },
         label: {
@@ -65,8 +85,34 @@ onMounted(() => {
       },
     ],
   })
+}
+
+const resizeChart = () => {
+  if (chartInstance.value) {
+    chartInstance.value.resize()
+  }
+}
+
+onMounted(() => {
+  nextTick(() => {
+    initChart()
+    window.addEventListener('resize', resizeChart)
+  })
+})
+
+watch(
+  () => [props.importantCount, props.normalCount],
+  () => {
+    updateChart()
+  }
+)
 
-  useChartResize(chart, chartRef.value)
+onUnmounted(() => {
+  if (chartInstance.value) {
+    chartInstance.value.dispose()
+    chartInstance.value = null
+  }
+  window.removeEventListener('resize', resizeChart)
 })
 </script>
 

+ 96 - 21
src/views/dashboard/components/PeopleDetectedCard/index.vue

@@ -6,16 +6,23 @@
 
     <div class="matrix-wrapper">
       <div class="scan-overlay" />
-      <div class="matrix-grid">
-        <div v-for="(item, index) in 12" :key="index" class="person-icon">
-          <svg viewBox="0 0 64 64" class="person-svg">
-            <circle cx="32" cy="12" r="6" />
-            <rect x="28" y="20" width="8" height="20" rx="4" />
-            <path d="M28 22 L20 32" />
-            <path d="M36 22 L44 32" />
-            <path d="M30 40 L26 52" />
-            <path d="M34 40 L38 52" />
-          </svg>
+      <div class="matrix-scroll">
+        <div class="matrix-grid">
+          <div
+            v-for="(item, index) in 12"
+            :key="index"
+            class="person-icon"
+            :class="{ active: detectedCount > 6 || index < detectedCount }"
+          >
+            <svg viewBox="0 0 64 64" class="person-svg">
+              <circle cx="32" cy="12" r="6" />
+              <rect x="28" y="20" width="8" height="20" rx="4" />
+              <path d="M28 22 L20 32" />
+              <path d="M36 22 L44 32" />
+              <path d="M30 40 L26 52" />
+              <path d="M34 40 L38 52" />
+            </svg>
+          </div>
         </div>
       </div>
     </div>
@@ -28,12 +35,17 @@
 </template>
 
 <script setup lang="ts">
-import { ref } from 'vue'
 import TechCard from '../TechCard/index.vue'
 
 defineOptions({ name: 'PeopleScanGlowCard' })
 
-const detectedCount = ref(8)
+type Props = {
+  detectedCount: number
+}
+
+withDefaults(defineProps<Props>(), {
+  detectedCount: 0,
+})
 </script>
 
 <style scoped lang="less">
@@ -51,11 +63,13 @@ const detectedCount = ref(8)
 .matrix-wrapper {
   position: relative;
   padding: 24px 12px;
+  height: 144px;
   overflow: hidden;
 }
 
 .scan-overlay {
   position: absolute;
+  top: 0;
   left: 0;
   width: 100%;
   height: 2px;
@@ -80,18 +94,52 @@ const detectedCount = ref(8)
   }
 }
 
+.matrix-scroll {
+  height: 100%;
+  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;
+    height: 0;
+    width: 0;
+  }
+
+  scrollbar-width: thin;
+  scrollbar-color: transparent transparent;
+
+  &:hover {
+    scrollbar-color: #00f0ff transparent;
+  }
+}
+
 .matrix-grid {
   display: grid;
-  grid-template-columns: repeat(4, 1fr);
-  gap: 1px;
-  justify-items: center;
-  position: relative;
-  z-index: 2;
+  grid-template-columns: repeat(auto-fit, minmax(40px, 1fr));
+  grid-auto-rows: 48px;
+  gap: 8px;
 }
 
 .person-icon {
-  width: 36px;
+  width: 100%;
   height: 36px;
+  position: relative;
 
   .person-svg {
     width: 100%;
@@ -99,13 +147,40 @@ const detectedCount = ref(8)
     stroke-width: 3.5;
     fill: none;
     stroke: #00ff9f;
-    transition:
-      transform 0.3s ease,
-      opacity 0.3s ease;
+    opacity: 0.3;
+    transition: opacity 0.3s ease;
+    filter: drop-shadow(0 0 2px #00ff9f);
+  }
+}
+
+.person-icon.active .person-svg {
+  stroke: #00ff9f;
+  opacity: 1;
+  filter: drop-shadow(0 0 6px #00ff9f);
+}
+
+@keyframes radarPulse {
+  0% {
+    transform: scale(1);
+    opacity: 1;
+    filter: drop-shadow(0 0 4px #00ff9f);
+  }
+  50% {
+    transform: scale(1.3);
+    opacity: 0.6;
+    filter: drop-shadow(0 0 12px #00ff9f);
+  }
+  100% {
+    transform: scale(1);
+    opacity: 1;
     filter: drop-shadow(0 0 4px #00ff9f);
   }
 }
 
+.person-icon.active:hover .person-svg {
+  animation: radarPulse 1.5s ease-in-out infinite;
+}
+
 .footer {
   display: flex;
   justify-content: center;

+ 194 - 67
src/views/dashboard/index.vue

@@ -2,37 +2,48 @@
   <div class="dashboard">
     <!-- <ScreenPage /> -->
     <div class="dashboard-header">
-      <div class="community-name">雷能社区智慧大屏</div>
-      <div class="running-days">已安全守护 365 天</div>
+      <div class="community-name">{{ tenantName }}</div>
+      <div class="running-days">已安全守护 {{ todayData.systemGuardDay }} 天</div>
       <div class="time-info">{{ currentTime }}</div>
       <div class="data-flow header-flow"></div>
     </div>
-    <div class="dashboard-content">
+    <div class="dashboard-content custom-scroll">
       <div class="block">
         <div class="data-grid">
-          <DeviceOnlineRateCard></DeviceOnlineRateCard>
-          <PeopleDetectedCard></PeopleDetectedCard>
-          <!-- <ElderActivityCard></ElderActivityCard> -->
-          <ObjectDistributionCard></ObjectDistributionCard>
-          <AlertFallCompareCard></AlertFallCompareCard>
+          <DeviceOnlineRateCard
+            :online-count="todayData.onlineCount"
+            :device-count="todayData.deviceCount"
+          ></DeviceOnlineRateCard>
+          <PeopleDetectedCard :detectedCount="todayData.detectedCount"></PeopleDetectedCard>
+          <ObjectDistributionCard :important-count="12" :normal-count="23"></ObjectDistributionCard>
+          <AlertFallCompareCard :fall-count="12" :alert-count="24"></AlertFallCompareCard>
         </div>
-        <DeviceLocationCard></DeviceLocationCard>
+        <DeviceLocationCard :data="todayData.installPositionList"></DeviceLocationCard>
       </div>
-      <div class="block block-center">
+      <div class="block block-center custom-scroll">
         <div class="map-container">
-          <img class="map-img" src="./assets/img/map.jpg" alt="" />
-          <!-- <img class="map-img" src="./assets/img/map1.png" alt="" /> -->
-          <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
+            class="map-container-wrapper"
+            :style="{ transform: `scale(${scale})`, transformOrigin: 'center center' }"
+            @click="toDeviceList"
+          >
+            <img class="map-img" src="./assets/img/map.jpg" alt="" />
+            <div class="map-label building-1" @click="toDeviceList">1号楼</div>
+            <div class="map-label building-2" @click="toDeviceList">2号楼</div>
+            <div class="map-label building-3" @click="toDeviceList">3号楼</div>
+            <div class="map-label building-4" @click="toDeviceList">4号楼</div>
+          </div>
         </div>
+        <a-space class="zoom-controls">
+          <ZoomOutOutlined @click="zoomOut" />
+          <RedoOutlined @click="zoomReset" />
+          <ZoomInOutlined @click="zoomIn" />
+        </a-space>
       </div>
       <div class="block">
         <div class="data-row">
           <DeviceAgeCard></DeviceAgeCard>
-          <ElderActivityCard></ElderActivityCard>
-          <!-- <DetectionTargetCard></DetectionTargetCard> -->
+          <ElderActivityCard :activity-rate="86.758"></ElderActivityCard>
         </div>
 
         <AlarmHistoryCard style="margin-bottom: 12px"></AlarmHistoryCard>
@@ -55,14 +66,18 @@ import DeviceAgeCard from './components/DeviceAgeCard/index.vue'
 import ObjectDistributionCard from './components/ObjectDistributionCard/index.vue'
 import AlarmHistoryCard from './components/AlarmHistoryCard/index.vue'
 import FallingHistoryCard from './components/FallingHistoryCard/index.vue'
+import * as statsApi from '@/api/stats'
+import { useUserStore } from '@/stores/user'
+import type { TodayData } from './types'
+import { ZoomInOutlined, ZoomOutOutlined, RedoOutlined } from '@ant-design/icons-vue'
+
+const userStore = useUserStore()
+const tenantName = (userStore.userInfo.tenantName ?? '雷能技术') + ' 智慧大屏'
 
 defineOptions({
   name: 'DashboardPage',
 })
 
-// 使用响应式布局工具
-
-// 格式化当前时间
 const currentTime = ref('')
 const formatTime = () => {
   const now = new Date()
@@ -77,24 +92,86 @@ const formatTime = () => {
 
 onMounted(() => {
   formatTime()
-  // 每秒更新一次时间
   const timeInterval = setInterval(formatTime, 1000)
 
-  // 组件卸载时清除定时器
   onUnmounted(() => {
     clearInterval(timeInterval)
   })
 })
 
-// const todayData = ref<TodayData>({
-//   deviceCount: 30,
-//   onlineCount: 25,
-//   systemGuardDay: 328,
-//   alarmCount: 3,
-//   fallingCount: 1,
-//   detectedCount: 142,
-//   activeRate: 78,
-// })
+const todayData = ref<TodayData>({
+  deviceCount: 0, // 设备总数
+  onlineCount: 0, // 在线设备数
+  systemGuardDay: 0, // 系统守护天数
+  alarmCount: 0, // 告警次数
+  fallingCount: 0, // 跌倒次数
+  detectedCount: 0, // 检测人数
+  activeRate: 0, // 活跃度
+  guardList: [], // 年龄分布
+  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()
+
+const toDeviceList = () => {
+  window.open('/device/list', '_blank')
+}
+
+const scale = ref(0.8)
+
+const zoomIn = () => {
+  scale.value += 0.1
+  if (scale.value > 1) {
+    scale.value = 1
+  }
+}
+
+const zoomReset = () => {
+  scale.value = 0.7
+}
+
+const zoomOut = () => {
+  scale.value = Math.max(0.5, scale.value - 0.1)
+}
+
+// 监听窗口大小变化
+onMounted(() => {
+  window.addEventListener('resize', handleResize)
+})
+onUnmounted(() => {
+  window.removeEventListener('resize', handleResize)
+})
+
+const handleResize = () => {
+  // 当窗口宽度小于 1200px 时,缩小地图
+  if (window.innerWidth < 1400) {
+    scale.value = 0.5
+  } else if (window.innerWidth < 1600) {
+    scale.value = 0.6
+  } else {
+    scale.value = 0.7
+  }
+}
 </script>
 
 <style scoped lang="less">
@@ -120,6 +197,41 @@ onMounted(() => {
   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 {
   background-color: @bg-color;
   color: @text-color;
@@ -163,7 +275,7 @@ onMounted(() => {
     }
 
     .time-info {
-      font-size: 18px;
+      font-size: 20px;
       color: @primary-color;
       flex-shrink: 0;
     }
@@ -196,38 +308,52 @@ onMounted(() => {
     gap: 20px;
     overflow: hidden;
     min-height: 0;
+    overflow-y: auto;
 
     .block {
       min-height: 500px;
       flex-grow: 1;
       flex-shrink: 1;
-      flex: 1 1 300px;
+      flex-basis: 400px;
       display: flex;
       flex-direction: column;
       position: relative;
-      overflow: hidden;
+      overflow: auto;
     }
 
     .block-center {
-      flex: 1 1 600px;
+      flex-grow: 1;
+      flex-shrink: 1;
+      flex-basis: 300px;
       display: flex;
-      min-width: min-content;
       position: relative;
 
       .map-container {
         position: relative;
-        display: inline-block;
+        display: flex;
         border-radius: 6px;
         height: 100%;
-        width: 100%;
+        align-items: center;
+        justify-content: center;
         background: #0b173f;
         overflow: hidden;
         user-select: none;
+        &-wrapper {
+          position: relative;
+          cursor: pointer;
+        }
+      }
+      .zoom-controls {
+        position: absolute;
+        bottom: 10px;
+        right: 10px;
+        color: @text-color;
+        font-size: 20px;
       }
 
       .map-img {
-        width: 100%;
-        height: 100%;
+        // width: 100%;
+        // height: 100%;
         object-fit: contain;
         display: block;
         border-radius: 6px;
@@ -240,61 +366,62 @@ onMounted(() => {
         border: 1px solid @border-color;
         border-radius: 4px;
         padding: 4px 8px;
-        font-size: 14px;
+        font-size: 20px;
         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;
+        cursor: pointer;
       }
 
       .building-1 {
-        top: 30%;
-        left: 30%;
+        top: 200px;
+        left: 200px;
+        animation: pulse 2s infinite;
       }
 
       .building-2 {
-        top: 50%;
-        left: 70%;
+        top: 250px;
+        left: 330px;
+        animation: pulse 2s infinite;
       }
 
       .building-3 {
-        top: 70%;
-        left: 50%;
+        top: 150px;
+        left: 450px;
+        animation: pulse 2s infinite;
       }
 
       .building-4 {
-        top: 40%;
-        left: 20%;
+        top: 350px;
+        left: 380px;
+        animation: pulse-red 2s infinite;
       }
 
       @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);
+          box-shadow: 0 0 0 12px rgba(77, 201, 230, 0);
         }
         100% {
-          transform: translate(-50%, -50%) scale(1);
           box-shadow: 0 0 0 0 rgba(77, 201, 230, 0);
         }
       }
+
+      @keyframes pulse-red {
+        0% {
+          box-shadow: 0 0 0 0 rgba(231, 76, 60, 0.4);
+        }
+        70% {
+          box-shadow: 0 0 0 12px rgba(231, 76, 60, 0);
+        }
+        100% {
+          box-shadow: 0 0 0 0 rgba(231, 76, 60, 0);
+        }
+      }
     }
 
     .data-grid {

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

@@ -22,4 +22,6 @@ export type TodayData = Pick<
   | 'alarmCount'
   | 'detectedCount'
   | 'activeRate'
+  | 'guardList'
+  | 'installPositionList'
 >