|
@@ -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 {
|