소스 검색

feat(dashboard): 新增智慧大屏功能模块

添加智慧大屏页面及相关组件,包括:
1. 新增大屏路由和导航入口
2. 实现各类数据展示卡片组件
3. 添加echarts-liquidfill依赖支持水球图
4. 创建图表自适应工具函数
5. 设计大屏整体布局和响应式样式
6. 添加地图展示和动态数据效果
7. 实现时间显示和基础动画效果
liujia 1 개월 전
부모
커밋
95356ce

+ 1 - 0
components.d.ts

@@ -12,6 +12,7 @@ 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 - 0
package.json

@@ -34,6 +34,7 @@
     "axios": "^1.10.0",
     "dayjs": "^1.11.13",
     "echarts": "^5.6.0",
+    "echarts-liquidfill": "^3.1.0",
     "lodash-es": "^4.17.21",
     "mqtt": "^5.13.1",
     "nanoid": "^5.1.5",

+ 12 - 0
pnpm-lock.yaml

@@ -35,6 +35,9 @@ importers:
       echarts:
         specifier: ^5.6.0
         version: 5.6.0
+      echarts-liquidfill:
+        specifier: ^3.1.0
+        version: 3.1.0(echarts@5.6.0)
       lodash-es:
         specifier: ^4.17.21
         version: 4.17.21
@@ -1424,6 +1427,11 @@ packages:
   eastasianwidth@0.2.0:
     resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
 
+  echarts-liquidfill@3.1.0:
+    resolution: {integrity: sha512-5Dlqs/jTsdTUAsd+K5LPLLTgrbbNORUSBQyk8PSy1Mg2zgHDWm83FmvA4s0ooNepCJojFYRITTQ4GU1UUSKYLw==}
+    peerDependencies:
+      echarts: ^5.0.1
+
   echarts@5.6.0:
     resolution: {integrity: sha512-oTbVTsXfKuEhxftHqL5xprgLoc0k7uScAwtryCgWF6hPYFLRwOUHiFmHGCBKP5NPFNkDVopOieyUqYGH8Fa3kA==}
 
@@ -4323,6 +4331,10 @@ snapshots:
 
   eastasianwidth@0.2.0: {}
 
+  echarts-liquidfill@3.1.0(echarts@5.6.0):
+    dependencies:
+      echarts: 5.6.0
+
   echarts@5.6.0:
     dependencies:
       tslib: 2.3.0

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

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

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

@@ -105,3 +105,31 @@ export interface StatsAlarmQueryData {
   outTotalPageNum: boolean
   totalPageNum: number
 }
+
+type AgeList = {
+  ageRange: string // 年龄段
+  count: number // 数量
+}
+type GuardList = {
+  guardType: string // 监测对象类型
+  count: number // 数量
+}
+type InstallPositionList = {
+  installPosition: string // 	安装位置
+  count: number // 	安装数量
+}
+/**
+ * 首页大屏统计出参
+ */
+export interface StatsHomeScreenQueryData {
+  deviceCount: number // 设备总数
+  onlineCount: number // 在线设备数
+  systemGuardDay: number // 系统守护天数
+  fallingCount: number // 今日跌倒统计
+  alarmCount: number // 今日告警统计
+  detectedCount: number // 当天检测到人数
+  activeRate: number // 今日活跃度
+  ageList: AgeList[] // 年龄统计信息
+  guardList: GuardList[] // 守护统计信息
+  installPositionList: InstallPositionList[] // 安装位置统计信息
+}

+ 15 - 0
src/layout/index.vue

@@ -28,6 +28,8 @@
       <a-layout-header>
         <slot name="header">
           <base-weather class="weather" mode="text"></base-weather>
+          <div class="smartScreen" @click="openSmartScreen">智慧大屏</div>
+
           <time-now class="timeNow"></time-now>
           <SyncOutlined class="refresh" @click="refresh" />
           <user-dropdown class="userDropdown"></user-dropdown>
@@ -364,6 +366,11 @@ const backHandler = async () => {
     await router.push(prevPath)
   }
 }
+
+// 打开智慧大屏
+const openSmartScreen = () => {
+  window.open('/dashboard', '_blank')
+}
 </script>
 
 <style scoped lang="less">
@@ -444,6 +451,14 @@ const backHandler = async () => {
     top: 0;
     z-index: 1000;
 
+    .smartScreen {
+      cursor: pointer;
+      color: #eee;
+      &:hover {
+        color: #85b8ff;
+      }
+    }
+
     .refresh {
       margin-right: 14px;
       font-size: 18px;

+ 6 - 0
src/router/index.ts

@@ -27,6 +27,12 @@ const router = createRouter({
       component: () => import('@/views/pointCloudMap/index.vue'),
       meta: { title: '设备点云图', isFullScreen: true, keepAlive: false, hidden: true },
     },
+    {
+      path: '/dashboard',
+      name: 'dashboard',
+      component: () => import('@/views/dashboard/index.vue'),
+      meta: { title: '大屏', isFullScreen: true, keepAlive: false, hidden: true },
+    },
   ],
 })
 router.beforeEach(authGuard)

+ 13 - 0
src/utils/useChartResize.ts

@@ -0,0 +1,13 @@
+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()
+  })
+}

+ 141 - 0
src/utils/useResponsiveLayout.ts

@@ -0,0 +1,141 @@
+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)
+      }
+    })
+  })
+}

BIN
src/views/dashboard/assets/img/map.jpg


+ 89 - 0
src/views/dashboard/components/AlarmHistoryCard/index.vue

@@ -0,0 +1,89 @@
+<template>
+  <TechCard>
+    <div class="card-title">历史告警统计</div>
+    <div ref="chartRef" class="chart-container" />
+  </TechCard>
+</template>
+
+<script setup lang="ts">
+import { onMounted, ref } from 'vue'
+import * as echarts from 'echarts'
+import { useChartResize } from '@/utils/useChartResize'
+import TechCard from '../TechCard/index.vue'
+// import { alarmHistoryData } from '@/store/data'
+
+defineOptions({
+  name: 'AlarmHistoryCard',
+})
+
+const alarmHistoryData = ref({
+  dates: ['2024-07-01', '2024-07-02', '2024-07-03', '2024-07-04', '2024-07-05'],
+  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)' },
+          ]),
+        },
+      },
+    ],
+  })
+
+  useChartResize(chart, chartRef.value)
+})
+</script>
+
+<style scoped>
+.card-title {
+  font-size: 16px;
+  font-weight: bold;
+  color: #f39c12;
+  margin-bottom: 12px;
+  text-align: center;
+}
+.chart-container {
+  width: 100%;
+  height: 100%;
+  min-height: 280px;
+}
+</style>

+ 85 - 0
src/views/dashboard/components/AlertFallCompareCard/index.vue

@@ -0,0 +1,85 @@
+<template>
+  <TechCard>
+    <div class="card-header">
+      <div class="title">跌倒与告警统计</div>
+    </div>
+    <div ref="chartRef" class="chart-container" />
+  </TechCard>
+</template>
+
+<script lang="ts" setup>
+import { ref, onMounted } 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
+
+onMounted(() => {
+  if (!chartRef.value) return
+  const chart = echarts.init(chartRef.value)
+
+  chart.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: [fallCount, alertCount],
+        barWidth: 40,
+        itemStyle: {
+          color: (params: { dataIndex: number }) => ['#ff4d6d', '#f39c12'][params.dataIndex],
+        },
+        label: {
+          show: true,
+          position: 'top',
+          color: '#fff',
+          fontSize: 14,
+        },
+      },
+    ],
+  })
+})
+</script>
+
+<style lang="less" scoped>
+.card-header {
+  text-align: center;
+  margin-bottom: 12px;
+
+  .title {
+    font-size: 16px;
+    font-weight: bold;
+    color: #00f0ff;
+  }
+}
+
+.chart-container {
+  width: 100%;
+  height: 200px;
+}
+</style>

+ 81 - 0
src/views/dashboard/components/DeviceAgeCard/index.vue

@@ -0,0 +1,81 @@
+<template>
+  <TechCard>
+    <div class="card-title">设备年龄层次分布</div>
+    <div ref="chartRef" class="chart-container" />
+  </TechCard>
+</template>
+
+<script setup lang="ts">
+import { onMounted, ref } from 'vue'
+import * as echarts from 'echarts'
+import { useChartResize } from '@/utils/useChartResize'
+import TechCard from '../TechCard/index.vue'
+
+defineOptions({
+  name: 'DeviceAgeCard',
+})
+
+const chartRef = ref<HTMLDivElement | null>(null)
+
+onMounted(() => {
+  if (!chartRef.value) return
+  const chart = echarts.init(chartRef.value)
+
+  chart.setOption({
+    tooltip: {
+      trigger: 'item',
+      appendToBody: true,
+      formatter: '{b}: {c}',
+    },
+    legend: {
+      orient: 'horizontal',
+      bottom: 10,
+      textStyle: {
+        color: '#fff',
+        fontSize: 12,
+      },
+    },
+    series: [
+      {
+        name: '设备年龄层次',
+        type: 'pie',
+        radius: ['20%', '70%'],
+        center: ['50%', '35%'],
+        roseType: 'area',
+        label: { 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岁以上' },
+        ],
+        itemStyle: {
+          color: (params: { dataIndex: number }) => {
+            const colors = ['#4dc9e6', '#2572ed', '#6de4ff', '#1a57c9', '#00bcd4']
+            return colors[params.dataIndex]
+          },
+        },
+      },
+    ],
+  })
+
+  useChartResize(chart, chartRef.value)
+})
+</script>
+
+<style scoped>
+.card-title {
+  font-size: 16px;
+  font-weight: bold;
+  color: #00f0ff;
+  margin-bottom: 12px;
+  text-align: center;
+}
+.chart-container {
+  width: 100%;
+  height: 100%;
+  min-height: 230px;
+}
+</style>

+ 86 - 0
src/views/dashboard/components/DeviceLocationCard/index.vue

@@ -0,0 +1,86 @@
+<template>
+  <TechCard>
+    <div class="card-title">设备安装位置分布</div>
+    <div ref="chartRef" class="chart-container" />
+  </TechCard>
+</template>
+
+<script setup lang="ts">
+import { onMounted, ref } from 'vue'
+import * as echarts from 'echarts'
+import { useChartResize } from '@/utils/useChartResize'
+import TechCard from '../TechCard/index.vue'
+
+defineOptions({
+  name: 'DeviceLocationCard',
+})
+
+const chartRef = ref<HTMLDivElement | null>(null)
+
+onMounted(() => {
+  if (!chartRef.value) return
+  const chart = echarts.init(chartRef.value)
+
+  chart.setOption({
+    tooltip: {
+      trigger: 'axis',
+      axisPointer: { type: 'shadow' },
+      formatter: '{b}: {c} 台',
+    },
+    grid: {
+      left: 60,
+      right: 20,
+      top: 20,
+      bottom: 20,
+    },
+    xAxis: {
+      type: 'value',
+      axisLabel: { color: '#9cc5e0' },
+      splitLine: { show: false },
+    },
+    yAxis: {
+      type: 'category',
+      data: ['卫生间', '卧室', '客厅', '餐厅'],
+      axisLabel: { color: '#9cc5e0' },
+      axisTick: { show: false },
+      axisLine: { show: false },
+    },
+    series: [
+      {
+        type: 'bar',
+        data: [12, 8, 6, 4],
+        barWidth: 14,
+        itemStyle: {
+          color: (params: { dataIndex: number }) => {
+            const colors = ['#4dc9e6', '#6de4ff', '#2572ed', '#1a57c9']
+            return colors[params.dataIndex]
+          },
+        },
+        label: {
+          show: true,
+          position: 'right',
+          color: '#00f0ff',
+          fontSize: 12,
+        },
+      },
+    ],
+  })
+
+  useChartResize(chart, chartRef.value)
+})
+</script>
+
+<style scoped>
+.card-title {
+  font-size: 16px;
+  font-weight: bold;
+  color: #00f0ff;
+  margin-bottom: 12px;
+  text-align: center;
+}
+.chart-container {
+  width: 100%;
+  height: 100%;
+  min-height: 150px;
+}
+</style>

+ 141 - 0
src/views/dashboard/components/DeviceOnlineRateCard/index.vue

@@ -0,0 +1,141 @@
+<template>
+  <TechCard>
+    <div class="card-header">
+      <div class="title">设备在线率</div>
+    </div>
+    <div ref="chartRef" class="chart-container"></div>
+    <div class="footer">
+      <div class="label">设备数量</div>
+      <div class="count">{{ onlineCount }} /1258 台</div>
+    </div>
+  </TechCard>
+</template>
+
+<script lang="ts" setup>
+import { ref, onMounted, onUnmounted, nextTick, watch } from 'vue'
+import * as echarts from 'echarts'
+import 'echarts-liquidfill'
+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(85.8)
+const onlineCount = 1284
+
+const timer = setInterval(() => {
+  onlineRate.value = Math.floor(Math.random() * 100)
+}, 1000)
+
+setTimeout(() => {
+  clearInterval(timer)
+}, 10000)
+
+const createChart = () => {
+  if (!chartRef.value) return
+  const chart = echarts.init(chartRef.value)
+  chartInstance.value = chart
+
+  chart.setOption({
+    series: [
+      {
+        type: 'liquidFill',
+        radius: '80%',
+        center: ['50%', '50%'],
+        data: [onlineRate.value / 100],
+        label: {
+          formatter: `${onlineRate.value}%`,
+          fontSize: 28,
+          fontWeight: 'bold',
+          color: '#00f0ff',
+        },
+        outline: {
+          show: true,
+          borderDistance: 4,
+          itemStyle: {
+            borderColor: '#00f0ff',
+            borderWidth: 2,
+          },
+        },
+        backgroundStyle: {
+          color: '#2c5364',
+        },
+        itemStyle: {
+          color: '#00f0ff',
+          opacity: 0.6,
+        },
+      },
+    ],
+  })
+}
+
+watch(onlineRate, (newVal) => {
+  if (chartInstance.value) {
+    chartInstance.value.setOption({
+      series: [
+        {
+          data: [newVal / 100],
+          label: {
+            formatter: `${newVal}%`,
+          },
+        },
+      ],
+    })
+  }
+})
+
+onMounted(() => {
+  nextTick(() => {
+    createChart()
+  })
+})
+
+onUnmounted(() => {
+  if (chartInstance.value) {
+    chartInstance.value.dispose()
+    chartInstance.value = null
+  }
+})
+</script>
+
+<style lang="less" scoped>
+.card-header {
+  text-align: center;
+  margin-bottom: 12px;
+
+  .title {
+    font-size: 16px;
+    font-weight: bold;
+    color: #00f0ff;
+  }
+}
+
+.chart-container {
+  width: 100%;
+  height: 160px;
+  min-height: 120px;
+  position: relative;
+}
+
+.footer {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  margin-top: 12px;
+  font-size: 14px;
+  color: #9cc5e0;
+
+  .label {
+    margin-right: 8px;
+  }
+
+  .count {
+    font-weight: bold;
+    color: #00f0ff;
+    font-size: 16px;
+  }
+}
+</style>

+ 100 - 0
src/views/dashboard/components/ElderActivityCard/index.vue

@@ -0,0 +1,100 @@
+<template>
+  <TechCard>
+    <div class="card-header">
+      <div class="title">长者活跃度</div>
+    </div>
+    <div ref="chartRef" class="chart-container"></div>
+    <div class="footer">
+      <div class="label">当前活跃度</div>
+      <div class="count">{{ activityRate }}%</div>
+    </div>
+  </TechCard>
+</template>
+
+<script lang="ts" setup>
+import { ref, onMounted } from 'vue'
+import * as echarts from 'echarts'
+import TechCard from '../TechCard/index.vue'
+
+defineOptions({
+  name: 'ElderActivityCard',
+})
+
+const chartRef = ref<HTMLDivElement | null>(null)
+const activityRate = 78
+
+onMounted(() => {
+  if (chartRef.value) {
+    const chart = echarts.init(chartRef.value)
+    chart.setOption({
+      tooltip: {
+        trigger: 'item',
+        appendToBody: true,
+        formatter: '{b}: {c}%',
+      },
+      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',
+        },
+      },
+    })
+  }
+})
+</script>
+
+<style lang="less" scoped>
+.card-header {
+  text-align: center;
+  margin-bottom: 12px;
+
+  .title {
+    font-size: 16px;
+    font-weight: bold;
+    color: #00f0ff;
+  }
+}
+
+.chart-container {
+  width: 100%;
+  height: 180px;
+}
+
+.footer {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  margin-top: 12px;
+  font-size: 14px;
+  color: #9cc5e0;
+
+  .label {
+    margin-right: 8px;
+  }
+
+  .count {
+    font-weight: bold;
+    color: #00f0ff;
+    font-size: 16px;
+  }
+}
+</style>

+ 90 - 0
src/views/dashboard/components/FallingHistoryCard/index.vue

@@ -0,0 +1,90 @@
+<template>
+  <TechCard>
+    <div class="card-title">历史跌倒统计</div>
+    <div ref="chartRef" class="chart-container" />
+  </TechCard>
+</template>
+
+<script setup lang="ts">
+import { onMounted, ref } from 'vue'
+import * as echarts from 'echarts'
+import { useChartResize } from '@/utils/useChartResize'
+import TechCard from '../TechCard/index.vue'
+// import { fallingHistoryData } from '@/store/data'
+
+defineOptions({
+  name: 'FallingHistoryCard',
+})
+
+const fallingHistoryData = ref({
+  dates: ['2024-07-01', '2024-07-02', '2024-07-03', '2024-07-04', '2024-07-05'],
+  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)' },
+          ]),
+        },
+      },
+    ],
+  })
+
+  useChartResize(chart, chartRef.value)
+})
+</script>
+
+<style scoped>
+.card-title {
+  font-size: 16px;
+  font-weight: bold;
+  color: #e74c3c;
+  margin-bottom: 12px;
+  text-align: center;
+}
+.chart-container {
+  width: 100%;
+  height: 100%;
+  min-height: 280px;
+}
+</style>

+ 86 - 0
src/views/dashboard/components/ObjectDistributionCard/index.vue

@@ -0,0 +1,86 @@
+<template>
+  <TechCard>
+    <div class="card-title">检测对象分布</div>
+    <div ref="chartRef" class="chart-container" />
+  </TechCard>
+</template>
+
+<script setup lang="ts">
+import { onMounted, ref } from 'vue'
+import * as echarts from 'echarts'
+import { useChartResize } from '@/utils/useChartResize'
+import TechCard from '../TechCard/index.vue'
+
+defineOptions({
+  name: 'ObjectDistributionCard',
+})
+
+const chartRef = ref<HTMLDivElement | null>(null)
+
+onMounted(() => {
+  if (!chartRef.value) return
+  const chart = echarts.init(chartRef.value)
+
+  chart.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: [20, 45],
+        barWidth: 30,
+        itemStyle: {
+          color: (params: { dataIndex: number }) => {
+            const colors = ['#f39c12', '#2ecc71']
+            return colors[params.dataIndex]
+          },
+        },
+        label: {
+          show: true,
+          position: 'top',
+          color: '#00f0ff',
+          fontSize: 12,
+        },
+      },
+    ],
+  })
+
+  useChartResize(chart, chartRef.value)
+})
+</script>
+
+<style scoped>
+.card-title {
+  font-size: 16px;
+  font-weight: bold;
+  color: #00f0ff;
+  margin-bottom: 12px;
+  text-align: center;
+}
+.chart-container {
+  width: 100%;
+  height: 100%;
+  min-height: 200px;
+}
+</style>

+ 127 - 0
src/views/dashboard/components/PeopleDetectedCard/index.vue

@@ -0,0 +1,127 @@
+<template>
+  <TechCard>
+    <div class="card-header">
+      <div class="title">检测到人数</div>
+    </div>
+
+    <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>
+      </div>
+    </div>
+
+    <div class="footer">
+      <div class="label">已检测人数</div>
+      <div class="count">{{ detectedCount }}人</div>
+    </div>
+  </TechCard>
+</template>
+
+<script setup lang="ts">
+import { ref } from 'vue'
+import TechCard from '../TechCard/index.vue'
+
+defineOptions({ name: 'PeopleScanGlowCard' })
+
+const detectedCount = ref(8)
+</script>
+
+<style scoped lang="less">
+.card-header {
+  text-align: center;
+  margin-bottom: 12px;
+
+  .title {
+    font-size: 16px;
+    font-weight: bold;
+    color: #00f0ff;
+  }
+}
+
+.matrix-wrapper {
+  position: relative;
+  padding: 24px 12px;
+  overflow: hidden;
+}
+
+.scan-overlay {
+  position: absolute;
+  left: 0;
+  width: 100%;
+  height: 2px;
+  background: linear-gradient(to right, #00f0ff, #3b9ff0);
+  box-shadow: 0 0 6px #00f0ff;
+  pointer-events: none;
+  z-index: 1;
+  animation: scanDown 2.4s linear infinite;
+}
+
+@keyframes scanDown {
+  0% {
+    top: 0%;
+    opacity: 1;
+  }
+  90% {
+    opacity: 1;
+  }
+  100% {
+    top: 100%;
+    opacity: 0;
+  }
+}
+
+.matrix-grid {
+  display: grid;
+  grid-template-columns: repeat(4, 1fr);
+  gap: 1px;
+  justify-items: center;
+  position: relative;
+  z-index: 2;
+}
+
+.person-icon {
+  width: 36px;
+  height: 36px;
+
+  .person-svg {
+    width: 100%;
+    height: 100%;
+    stroke-width: 3.5;
+    fill: none;
+    stroke: #00ff9f;
+    transition:
+      transform 0.3s ease,
+      opacity 0.3s ease;
+    filter: drop-shadow(0 0 4px #00ff9f);
+  }
+}
+
+.footer {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  margin-top: 12px;
+  font-size: 14px;
+  color: #9cc5e0;
+
+  .label {
+    margin-right: 8px;
+  }
+
+  .count {
+    font-weight: bold;
+    color: #00f0ff;
+    font-size: 16px;
+  }
+}
+</style>

+ 127 - 0
src/views/dashboard/components/TechCard/index.vue

@@ -0,0 +1,127 @@
+<template>
+  <div class="tech-card">
+    <div class="corner top-left"></div>
+    <div class="corner top-right"></div>
+    <div class="corner bottom-left"></div>
+    <div class="corner bottom-right"></div>
+
+    <div class="card-content">
+      <slot />
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+defineOptions({ name: 'TechCard' })
+</script>
+
+<style lang="less" scoped>
+.tech-card {
+  position: relative;
+  background: linear-gradient(135deg, #001f3f, #005f99, #00aaff);
+  background: #0b173f;
+  border-radius: 12px;
+  padding: 24px;
+  color: #fff;
+  overflow: hidden;
+  transition: box-shadow 0.3s ease;
+  width: 100%; /* 确保卡片占满父容器 */
+  height: 100%; /* 确保卡片占满父容器 */
+  min-height: 200px; /* 确保卡片有最小高度 */
+
+  /* 响应式调整 */
+  @media (max-width: 1200px) {
+    padding: 20px;
+  }
+  
+  @media (max-width: 768px) {
+    padding: 15px;
+    min-height: 180px;
+  }
+
+  &::before {
+    content: '';
+    position: absolute;
+    inset: 0;
+    border-radius: 12px;
+    padding: 3px;
+    background: linear-gradient(270deg, #ff00cc, #3333ff, #00ffee, #ffcc00);
+    background-size: 600% 600%;
+    animation: rainbowBorder 6s linear infinite;
+    mask:
+      linear-gradient(#fff 0 0) content-box,
+      linear-gradient(#fff 0 0);
+    -webkit-mask:
+      linear-gradient(#fff 0 0) content-box,
+      linear-gradient(#fff 0 0);
+    mask-composite: exclude;
+    -webkit-mask-composite: destination-out;
+    opacity: 0.3;
+    transition: opacity 0.4s ease;
+    z-index: 10;
+    pointer-events: none;
+  }
+
+  &:hover::before {
+    opacity: 1;
+    padding: 5px;
+    animation: rainbowBorder 3s linear infinite;
+  }
+
+  .card-content {
+    position: relative;
+    z-index: 1;
+  }
+
+  .corner {
+    position: absolute;
+    width: 20px;
+    height: 20px;
+    z-index: 2;
+  }
+
+  .top-left {
+    top: 3px;
+    left: 3px;
+    border-top: 2px solid #00f0ff;
+    border-left: 2px solid #00f0ff;
+    border-radius: 6px 0 0 0;
+  }
+
+  .top-right {
+    top: 3px;
+    right: 3px;
+    border-top: 2px solid #00f0ff;
+    border-right: 2px solid #00f0ff;
+    border-radius: 0 6px 0 0;
+  }
+
+  .bottom-left {
+    bottom: 3px;
+    left: 3px;
+    border-bottom: 2px solid #00f0ff;
+    border-left: 2px solid #00f0ff;
+    border-radius: 0 0 0 6px;
+  }
+
+  .bottom-right {
+    bottom: 3px;
+    right: 3px;
+    border-bottom: 2px solid #00f0ff;
+    border-right: 2px solid #00f0ff;
+    border-radius: 0 0 6px 0;
+  }
+}
+
+@keyframes rainbowBorder {
+  0% {
+    background-position: 0% 50%;
+  }
+  50% {
+    background-position: 100% 50%;
+  }
+  100% {
+    background-position: 0% 50%;
+  }
+}
+</style>

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

@@ -0,0 +1,107 @@
+<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>

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

@@ -0,0 +1,832 @@
+<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>

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

@@ -0,0 +1,68 @@
+<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>

+ 385 - 0
src/views/dashboard/index.vue

@@ -0,0 +1,385 @@
+<template>
+  <div class="dashboard">
+    <!-- <ScreenPage /> -->
+    <div class="dashboard-header">
+      <div class="community-name">雷能社区智慧大屏</div>
+      <div class="running-days">已安全守护 365 天</div>
+      <div class="time-info">{{ currentTime }}</div>
+      <div class="data-flow header-flow"></div>
+    </div>
+    <div class="dashboard-content">
+      <div class="block">
+        <div class="data-grid">
+          <DeviceOnlineRateCard></DeviceOnlineRateCard>
+          <PeopleDetectedCard></PeopleDetectedCard>
+          <!-- <ElderActivityCard></ElderActivityCard> -->
+          <ObjectDistributionCard></ObjectDistributionCard>
+          <AlertFallCompareCard></AlertFallCompareCard>
+        </div>
+        <DeviceLocationCard></DeviceLocationCard>
+      </div>
+      <div class="block block-center">
+        <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>
+      </div>
+      <div class="block">
+        <div class="data-row">
+          <DeviceAgeCard></DeviceAgeCard>
+          <ElderActivityCard></ElderActivityCard>
+          <!-- <DetectionTargetCard></DetectionTargetCard> -->
+        </div>
+
+        <AlarmHistoryCard style="margin-bottom: 12px"></AlarmHistoryCard>
+        <FallingHistoryCard></FallingHistoryCard>
+      </div>
+    </div>
+    <div class="dashboard-footer">合肥雷能信息技术有限公司 © 2025 All Rights Reserved.</div>
+  </div>
+</template>
+
+<script setup lang="ts">
+// import ScreenPage from './components/screen/index.vue'
+import { ref, onMounted, onUnmounted } from 'vue'
+import DeviceOnlineRateCard from './components/DeviceOnlineRateCard/index.vue'
+import AlertFallCompareCard from './components/AlertFallCompareCard/index.vue'
+import ElderActivityCard from './components/ElderActivityCard/index.vue'
+import PeopleDetectedCard from './components/PeopleDetectedCard/index.vue'
+import DeviceLocationCard from './components/DeviceLocationCard/index.vue'
+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'
+
+defineOptions({
+  name: 'DashboardPage',
+})
+
+// 使用响应式布局工具
+
+// 格式化当前时间
+const currentTime = ref('')
+const formatTime = () => {
+  const now = new Date()
+  const year = now.getFullYear()
+  const month = String(now.getMonth() + 1).padStart(2, '0')
+  const day = String(now.getDate()).padStart(2, '0')
+  const hours = String(now.getHours()).padStart(2, '0')
+  const minutes = String(now.getMinutes()).padStart(2, '0')
+  const seconds = String(now.getSeconds()).padStart(2, '0')
+  currentTime.value = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
+}
+
+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,
+// })
+</script>
+
+<style scoped lang="less">
+@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;
+}
+
+.dashboard {
+  background-color: @bg-color;
+  color: @text-color;
+  height: 100vh;
+  width: 100vw;
+  overflow: hidden;
+  display: flex;
+  flex-direction: column;
+
+  &-header {
+    height: 70px;
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    padding: 0 20px;
+    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;
+    flex-wrap: wrap;
+
+    .community-name {
+      font-size: 24px;
+      background: linear-gradient(90deg, @primary-color, @secondary-color);
+      -webkit-background-clip: text;
+      background-clip: text;
+      -webkit-text-fill-color: transparent;
+      text-shadow: 0 0 10px rgba(109, 228, 255, 0.5);
+      letter-spacing: 2px;
+      flex-shrink: 0;
+    }
+
+    .running-days {
+      font-size: 22px;
+      color: @secondary-color;
+      text-shadow: 0 0 8px rgba(109, 228, 255, 0.7);
+      flex-shrink: 0;
+    }
+
+    .time-info {
+      font-size: 18px;
+      color: @primary-color;
+      flex-shrink: 0;
+    }
+
+    .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 2s linear infinite;
+    }
+
+    @keyframes dataFlow {
+      0% {
+        transform: translateX(-100%);
+      }
+      100% {
+        transform: translateX(100%);
+      }
+    }
+  }
+
+  &-content {
+    padding: 24px;
+    flex: 1;
+    border-radius: 8px;
+    display: flex;
+    gap: 20px;
+    overflow: hidden;
+    min-height: 0;
+
+    .block {
+      min-height: 500px;
+      flex-grow: 1;
+      flex-shrink: 1;
+      flex: 1 1 300px;
+      display: flex;
+      flex-direction: column;
+      position: relative;
+      overflow: hidden;
+    }
+
+    .block-center {
+      flex: 1 1 600px;
+      display: flex;
+      min-width: min-content;
+      position: relative;
+
+      .map-container {
+        position: relative;
+        display: inline-block;
+        border-radius: 6px;
+        height: 100%;
+        width: 100%;
+        background: #0b173f;
+        overflow: hidden;
+        user-select: none;
+      }
+
+      .map-img {
+        width: 100%;
+        height: 100%;
+        object-fit: contain;
+        display: block;
+        border-radius: 6px;
+        pointer-events: none;
+      }
+
+      .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);
+        }
+      }
+    }
+
+    .data-grid {
+      display: grid;
+      grid-template-columns: 1fr 1fr;
+      gap: 12px;
+      margin-bottom: 12px;
+    }
+
+    .data-row {
+      display: grid;
+      grid-template-columns: 1fr 1fr;
+      gap: 12px;
+      margin-bottom: 12px;
+    }
+  }
+
+  &-footer {
+    height: 20px;
+    color: @text-color;
+    color: #6de4ff;
+    text-align: center;
+    line-height: 30px;
+    font-size: 12px;
+    margin-bottom: 10px;
+  }
+
+  @media (max-width: 1600px) {
+    .dashboard-header .community-name {
+      font-size: 20px;
+    }
+
+    .dashboard-header .running-days {
+      font-size: 18px;
+    }
+
+    .dashboard-header .time-info {
+      font-size: 16px;
+    }
+  }
+
+  @media (max-width: 1200px) {
+    .dashboard-content {
+      flex-direction: column;
+      gap: 15px;
+    }
+
+    .dashboard-content .block {
+      min-height: 400px;
+    }
+
+    .dashboard-content .data-grid,
+    .dashboard-content .data-row {
+      grid-template-columns: 1fr;
+    }
+  }
+
+  @media (max-width: 768px) {
+    .dashboard-header {
+      height: auto;
+      padding: 15px;
+      flex-direction: column;
+      gap: 10px;
+      text-align: center;
+    }
+
+    .dashboard-header .community-name,
+    .dashboard-header .running-days,
+    .dashboard-header .time-info {
+      font-size: 16px;
+    }
+
+    .dashboard-content {
+      padding: 15px;
+      min-height: calc(100vh - 150px - 50px);
+    }
+
+    .dashboard-content .block {
+      min-height: 300px;
+    }
+
+    .map-label {
+      font-size: 12px !important;
+      padding: 2px 6px !important;
+    }
+  }
+}
+</style>

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

@@ -0,0 +1,25 @@
+import type { StatsHomeScreenQueryData } from '@/api/stats/types'
+
+export interface DataCardProps {
+  title: string
+  value: string | number
+  change?: string | number
+  isPositive?: boolean
+}
+
+export interface StatusItemProps {
+  name: string
+  value: string
+  status: '正常' | '警告' | '异常'
+}
+
+export type TodayData = Pick<
+  StatsHomeScreenQueryData,
+  | 'deviceCount'
+  | 'onlineCount'
+  | 'systemGuardDay'
+  | 'fallingCount'
+  | 'alarmCount'
+  | 'detectedCount'
+  | 'activeRate'
+>