|
@@ -1,9 +1,27 @@
|
|
<template>
|
|
<template>
|
|
<div class="dashboard">
|
|
<div class="dashboard">
|
|
<div class="dashboard-header">
|
|
<div class="dashboard-header">
|
|
- <div class="community-name">{{ tenantName }}</div>
|
|
|
|
- <div class="running-days">已安全守护 {{ todayData.systemGuardDay }} 天</div>
|
|
|
|
- <div class="time-info">{{ currentTime }}</div>
|
|
|
|
|
|
+ <div class="community-name">
|
|
|
|
+ <div class="fixedName"> 智慧大屏</div>
|
|
|
|
+ <div class="tenantName">{{ tenantName }}</div>
|
|
|
|
+ </div>
|
|
|
|
+ <div class="running-days"
|
|
|
|
+ >已安全守护
|
|
|
|
+ <RollingNumber :value="todayData.systemGuardDay" :itemHeight="50" :itemWidth="40" />
|
|
|
|
+ 天</div
|
|
|
|
+ >
|
|
|
|
+
|
|
|
|
+ <div v-if="isSuperAdmin" class="tenant-switcher">
|
|
|
|
+ <div class="time-info">{{ currentTime }}</div>
|
|
|
|
+ <a-select
|
|
|
|
+ v-model:value="selectedTenant"
|
|
|
|
+ placeholder="请选择"
|
|
|
|
+ style="width: 150px"
|
|
|
|
+ :options="tenantList"
|
|
|
|
+ :getPopupContainer="(trigger: HTMLElement) => trigger.parentNode"
|
|
|
|
+ @change="handleTenantChange"
|
|
|
|
+ />
|
|
|
|
+ </div>
|
|
<div class="data-flow header-flow"></div>
|
|
<div class="data-flow header-flow"></div>
|
|
</div>
|
|
</div>
|
|
<div class="dashboard-content">
|
|
<div class="dashboard-content">
|
|
@@ -108,7 +126,7 @@
|
|
</template>
|
|
</template>
|
|
|
|
|
|
<script setup lang="ts">
|
|
<script setup lang="ts">
|
|
-import { ref, onMounted, onUnmounted, watch } from 'vue'
|
|
|
|
|
|
+import { ref, onMounted, onUnmounted, watch, computed, watchEffect } from 'vue'
|
|
import DeviceOnlineRateCard from './components/DeviceOnlineRateCard/index.vue'
|
|
import DeviceOnlineRateCard from './components/DeviceOnlineRateCard/index.vue'
|
|
import AlertFallCompareCard from './components/AlertFallCompareCard/index.vue'
|
|
import AlertFallCompareCard from './components/AlertFallCompareCard/index.vue'
|
|
import ElderActivityCard from './components/ElderActivityCard/index.vue'
|
|
import ElderActivityCard from './components/ElderActivityCard/index.vue'
|
|
@@ -117,15 +135,30 @@ import DeviceLocationCard from './components/DeviceLocationCard/index.vue'
|
|
import GuardObjectAgeCard from './components/GuardObjectAgeCard/index.vue'
|
|
import GuardObjectAgeCard from './components/GuardObjectAgeCard/index.vue'
|
|
import GuardObjectTypeCard from './components/GuardObjectTypeCard/index.vue'
|
|
import GuardObjectTypeCard from './components/GuardObjectTypeCard/index.vue'
|
|
import HistoryChartCard from './components/HistoryChartCard/index.vue'
|
|
import HistoryChartCard from './components/HistoryChartCard/index.vue'
|
|
|
|
+import RollingNumber from './components/RollingNumber/index.vue'
|
|
import { useUserStore } from '@/stores/user'
|
|
import { useUserStore } from '@/stores/user'
|
|
import type { TodayData } from './types'
|
|
import type { TodayData } from './types'
|
|
import { ZoomInOutlined, ZoomOutOutlined, RedoOutlined } from '@ant-design/icons-vue'
|
|
import { ZoomInOutlined, ZoomOutOutlined, RedoOutlined } from '@ant-design/icons-vue'
|
|
import { useResponsiveLayout } from '@/utils/chartManager'
|
|
import { useResponsiveLayout } from '@/utils/chartManager'
|
|
import { useDashboardPolling } from '@/hooks/useDashboardPolling'
|
|
import { useDashboardPolling } from '@/hooks/useDashboardPolling'
|
|
import { useDict } from '@/hooks/useDict'
|
|
import { useDict } from '@/hooks/useDict'
|
|
|
|
+import * as tenantAPI from '@/api/tenant'
|
|
|
|
+import type {
|
|
|
|
+ StatsHomeScreenAlarmHistory,
|
|
|
|
+ StatsHomeScreenFallHistory,
|
|
|
|
+ StatsHomeScreenQueryData,
|
|
|
|
+} from '@/api/stats/types'
|
|
|
|
|
|
const userStore = useUserStore()
|
|
const userStore = useUserStore()
|
|
-const tenantName = (userStore.userInfo.tenantName ?? '雷能技术') + ' 智慧大屏'
|
|
|
|
|
|
+
|
|
|
|
+const tenantName = computed(() => {
|
|
|
|
+ if (isSuperAdmin.value) {
|
|
|
|
+ const selected = tenantList.value.find((item) => item.value === selectedTenant.value)
|
|
|
|
+ return selected?.label ?? '未知租户'
|
|
|
|
+ } else {
|
|
|
|
+ return userStore.userInfo.tenantName ?? '雷能技术'
|
|
|
|
+ }
|
|
|
|
+})
|
|
|
|
|
|
defineOptions({
|
|
defineOptions({
|
|
name: 'DashboardPage',
|
|
name: 'DashboardPage',
|
|
@@ -167,53 +200,125 @@ const todayData = ref<TodayData>({
|
|
|
|
|
|
useResponsiveLayout()
|
|
useResponsiveLayout()
|
|
|
|
|
|
-const {
|
|
|
|
- todayScreenData,
|
|
|
|
- fallHistoryData,
|
|
|
|
- alarmHistoryData,
|
|
|
|
- updateFallQueryType,
|
|
|
|
- updateAlarmQueryType,
|
|
|
|
-} = useDashboardPolling({ tenantId: userStore.userInfo.tenantId || '' })
|
|
|
|
|
|
+const isSuperAdmin = ref(
|
|
|
|
+ userStore.userInfo.userType === 'admin' || userStore.userInfo.userType === 'manager'
|
|
|
|
+)
|
|
|
|
+
|
|
|
|
+const selectedTenant = ref<string | number>(userStore.userInfo.tenantId || '')
|
|
|
|
+const tenantList = ref<{ label: string; value: string | number }[]>([])
|
|
|
|
+const currentTenantId = ref<string | number>(selectedTenant.value)
|
|
|
|
+
|
|
|
|
+const fetchTenantList = async () => {
|
|
|
|
+ if (!isSuperAdmin.value) {
|
|
|
|
+ currentTenantId.value = userStore.userInfo.tenantId || ''
|
|
|
|
+ return
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ const res = await tenantAPI.queryTenant({
|
|
|
|
+ pageNo: 1,
|
|
|
|
+ pageSize: 1000,
|
|
|
|
+ })
|
|
|
|
+
|
|
|
|
+ tenantList.value = res.data.rows.map((item) => ({
|
|
|
|
+ label: item.tenantName ?? '',
|
|
|
|
+ value: item.tenantId ?? '',
|
|
|
|
+ }))
|
|
|
|
+
|
|
|
|
+ selectedTenant.value = tenantList.value[0]?.value || ''
|
|
|
|
+ currentTenantId.value = selectedTenant.value
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+fetchTenantList()
|
|
|
|
+
|
|
|
|
+const handleTenantChange = () => {
|
|
|
|
+ if (selectedTenant.value) {
|
|
|
|
+ currentTenantId.value = selectedTenant.value
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+type RawDayItem = { date: string; fallingCount?: number; alarmCount?: number }
|
|
|
|
+type RawMonthItem = { month: string; fallingCount?: number; alarmCount?: number }
|
|
|
|
+
|
|
|
|
+const todayScreenData = ref<StatsHomeScreenQueryData | null>(null)
|
|
|
|
+const fallHistoryData = ref<StatsHomeScreenFallHistory | null>(null)
|
|
|
|
+const alarmHistoryData = ref<StatsHomeScreenAlarmHistory | null>(null)
|
|
|
|
+let updateFallQueryType: ((mode: 'day' | 'month') => Promise<void>) | null = null
|
|
|
|
+let updateAlarmQueryType: ((mode: 'day' | 'month') => Promise<void>) | null = null
|
|
|
|
+
|
|
|
|
+let pollingInstance: ReturnType<typeof useDashboardPolling> | null = null
|
|
|
|
+
|
|
|
|
+watch(
|
|
|
|
+ currentTenantId,
|
|
|
|
+ (newTenantId) => {
|
|
|
|
+ if (pollingInstance) {
|
|
|
|
+ pollingInstance.stop()
|
|
|
|
+ pollingInstance = null
|
|
|
|
+ alarmMode.value = 'day'
|
|
|
|
+ fallMode.value = 'day'
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ if (newTenantId) {
|
|
|
|
+ pollingInstance = useDashboardPolling({ tenantId: String(newTenantId) })
|
|
|
|
+ pollingInstance.start()
|
|
|
|
+
|
|
|
|
+ // 赋值响应式数据
|
|
|
|
+ watchEffect(() => {
|
|
|
|
+ todayScreenData.value = pollingInstance?.todayScreenData.value ?? null
|
|
|
|
+ fallHistoryData.value = pollingInstance?.fallHistoryData.value ?? null
|
|
|
|
+ alarmHistoryData.value = pollingInstance?.alarmHistoryData.value ?? null
|
|
|
|
+ })
|
|
|
|
+
|
|
|
|
+ updateFallQueryType = pollingInstance.updateFallQueryType
|
|
|
|
+ updateAlarmQueryType = pollingInstance.updateAlarmQueryType
|
|
|
|
+ }
|
|
|
|
+ },
|
|
|
|
+ { immediate: true }
|
|
|
|
+)
|
|
|
|
|
|
const { fetchDict: fetchDictGuardianship, dictNameMap: guardTypeNameMap } =
|
|
const { fetchDict: fetchDictGuardianship, dictNameMap: guardTypeNameMap } =
|
|
useDict('guardianship_type')
|
|
useDict('guardianship_type')
|
|
-fetchDictGuardianship()
|
|
|
|
|
|
|
|
const { fetchDict: fetchDictInstallPosition, dictNameMap: installPositionNameMap } =
|
|
const { fetchDict: fetchDictInstallPosition, dictNameMap: installPositionNameMap } =
|
|
useDict('install_position')
|
|
useDict('install_position')
|
|
-fetchDictInstallPosition().then(() => {
|
|
|
|
- console.log('🚀🚀🚀 installPositionNameMap.value', installPositionNameMap.value)
|
|
|
|
-})
|
|
|
|
|
|
+
|
|
|
|
+Promise.all([fetchDictGuardianship(), fetchDictInstallPosition()])
|
|
|
|
|
|
watch(
|
|
watch(
|
|
() => todayScreenData.value,
|
|
() => todayScreenData.value,
|
|
(val) => {
|
|
(val) => {
|
|
- console.log('todayScreenData更新了', val)
|
|
|
|
- todayData.value = val || todayData.value
|
|
|
|
- todayData.value.ageList = val?.ageList.filter((item) => item.count > 0) ?? []
|
|
|
|
|
|
+ console.log('🚀🚀🚀 todayScreenData更新了', val)
|
|
|
|
+ todayData.value.activeRate = val?.activeRate ?? 0
|
|
|
|
+ todayData.value.detectedCount = val?.detectedCount ?? 0
|
|
|
|
+ todayData.value.fallingCount = val?.fallingCount ?? 0
|
|
|
|
+ todayData.value.alarmCount = val?.alarmCount ?? 0
|
|
|
|
+ todayData.value.systemGuardDay = val?.systemGuardDay ?? 0
|
|
|
|
+ todayData.value.onlineCount = val?.onlineCount ?? 0
|
|
|
|
+ todayData.value.deviceCount = val?.deviceCount ?? 0
|
|
|
|
+ todayData.value.ageList = (val?.ageList && val?.ageList.filter((item) => item.count > 0)) ?? []
|
|
todayData.value.installPositionList =
|
|
todayData.value.installPositionList =
|
|
- val?.installPositionList.map((item) => ({
|
|
|
|
- ...item,
|
|
|
|
- name:
|
|
|
|
- installPositionNameMap.value[
|
|
|
|
- item.installPosition as keyof typeof installPositionNameMap.value
|
|
|
|
- ] || '未知',
|
|
|
|
- names: installPositionNameMap.value,
|
|
|
|
- })) ?? []
|
|
|
|
|
|
+ (val?.installPositionList &&
|
|
|
|
+ val?.installPositionList.map((item) => ({
|
|
|
|
+ ...item,
|
|
|
|
+ name:
|
|
|
|
+ installPositionNameMap.value[
|
|
|
|
+ item.installPosition as keyof typeof installPositionNameMap.value
|
|
|
|
+ ] || '未知',
|
|
|
|
+ names: installPositionNameMap.value,
|
|
|
|
+ }))) ??
|
|
|
|
+ []
|
|
todayData.value.guardList =
|
|
todayData.value.guardList =
|
|
- todayData.value.guardList.map((item) => ({
|
|
|
|
- ...item,
|
|
|
|
- name:
|
|
|
|
- guardTypeNameMap.value[item.guardType as keyof typeof guardTypeNameMap.value] || '未知',
|
|
|
|
- names: guardTypeNameMap.value,
|
|
|
|
- })) ?? []
|
|
|
|
|
|
+ (val?.guardList &&
|
|
|
|
+ val?.guardList.map((item) => ({
|
|
|
|
+ ...item,
|
|
|
|
+ name:
|
|
|
|
+ guardTypeNameMap.value[item.guardType as keyof typeof guardTypeNameMap.value] || '未知',
|
|
|
|
+ names: guardTypeNameMap.value,
|
|
|
|
+ }))) ??
|
|
|
|
+ []
|
|
},
|
|
},
|
|
- { immediate: true }
|
|
|
|
|
|
+ { immediate: true, deep: true }
|
|
)
|
|
)
|
|
|
|
|
|
-type RawDayItem = { date: string; fallingCount?: number; alarmCount?: number }
|
|
|
|
-type RawMonthItem = { month: string; fallingCount?: number; alarmCount?: number }
|
|
|
|
-
|
|
|
|
type StatInfo = { lable: string; count: number }
|
|
type StatInfo = { lable: string; count: number }
|
|
type HistoryData = { monthStatInfo: StatInfo[]; dayStatInfo: StatInfo[] }
|
|
type HistoryData = { monthStatInfo: StatInfo[]; dayStatInfo: StatInfo[] }
|
|
|
|
|
|
@@ -260,7 +365,7 @@ watch(
|
|
'fallingCount'
|
|
'fallingCount'
|
|
)
|
|
)
|
|
},
|
|
},
|
|
- { immediate: true }
|
|
|
|
|
|
+ { immediate: true, deep: true }
|
|
)
|
|
)
|
|
|
|
|
|
// 监听告警数据
|
|
// 监听告警数据
|
|
@@ -279,7 +384,7 @@ watch(
|
|
'alarmCount'
|
|
'alarmCount'
|
|
)
|
|
)
|
|
},
|
|
},
|
|
- { immediate: true }
|
|
|
|
|
|
+ { immediate: true, deep: true }
|
|
)
|
|
)
|
|
|
|
|
|
const alarmMode = ref<'day' | 'month'>('day')
|
|
const alarmMode = ref<'day' | 'month'>('day')
|
|
@@ -292,7 +397,7 @@ const changeAlarmMode = async (mode: 'day' | 'month') => {
|
|
if (alarmMode.value !== mode) {
|
|
if (alarmMode.value !== mode) {
|
|
alarmMode.value = mode
|
|
alarmMode.value = mode
|
|
alarmLoading.value = true
|
|
alarmLoading.value = true
|
|
- await updateAlarmQueryType(mode)
|
|
|
|
|
|
+ await updateAlarmQueryType?.(mode)
|
|
alarmLoading.value = false
|
|
alarmLoading.value = false
|
|
}
|
|
}
|
|
}
|
|
}
|
|
@@ -301,7 +406,7 @@ const changeFallMode = async (mode: 'day' | 'month') => {
|
|
if (fallMode.value !== mode) {
|
|
if (fallMode.value !== mode) {
|
|
fallMode.value = mode
|
|
fallMode.value = mode
|
|
fallLoading.value = true
|
|
fallLoading.value = true
|
|
- await updateFallQueryType(mode)
|
|
|
|
|
|
+ await updateFallQueryType?.(mode)
|
|
fallLoading.value = false
|
|
fallLoading.value = false
|
|
}
|
|
}
|
|
}
|
|
}
|
|
@@ -388,7 +493,6 @@ const handleResize = () => {
|
|
border-radius: 8px;
|
|
border-radius: 8px;
|
|
box-shadow: 0 0 15px rgba(0, 180, 255, 0.2);
|
|
box-shadow: 0 0 15px rgba(0, 180, 255, 0.2);
|
|
position: relative;
|
|
position: relative;
|
|
- overflow: hidden;
|
|
|
|
flex-wrap: wrap;
|
|
flex-wrap: wrap;
|
|
|
|
|
|
.community-name {
|
|
.community-name {
|
|
@@ -400,13 +504,55 @@ const handleResize = () => {
|
|
text-shadow: 0 0 10px rgba(109, 228, 255, 0.5);
|
|
text-shadow: 0 0 10px rgba(109, 228, 255, 0.5);
|
|
letter-spacing: 2px;
|
|
letter-spacing: 2px;
|
|
flex-shrink: 0;
|
|
flex-shrink: 0;
|
|
|
|
+ display: flex;
|
|
|
|
+ align-items: center;
|
|
|
|
+ .fixedName {
|
|
|
|
+ margin-right: 10px;
|
|
|
|
+ }
|
|
}
|
|
}
|
|
|
|
|
|
.running-days {
|
|
.running-days {
|
|
- font-size: 22px;
|
|
|
|
- color: @secondary-color;
|
|
|
|
- text-shadow: 0 0 8px rgba(109, 228, 255, 0.7);
|
|
|
|
|
|
+ position: absolute;
|
|
|
|
+ top: 70%;
|
|
|
|
+ left: 50%;
|
|
|
|
+ transform: translate(-50%, -50%);
|
|
|
|
+ display: flex;
|
|
|
|
+ align-items: center;
|
|
|
|
+ justify-content: center;
|
|
flex-shrink: 0;
|
|
flex-shrink: 0;
|
|
|
|
+
|
|
|
|
+ font-size: 24px;
|
|
|
|
+ font-family: 'Orbitron', 'Segoe UI', sans-serif;
|
|
|
|
+ color: #6de4ff;
|
|
|
|
+ text-shadow:
|
|
|
|
+ 0 0 8px rgba(109, 228, 255, 0.8),
|
|
|
|
+ 0 0 16px rgba(109, 228, 255, 0.4);
|
|
|
|
+
|
|
|
|
+ padding: 12px 24px;
|
|
|
|
+ border: 1px solid rgba(109, 228, 255, 0.4);
|
|
|
|
+ border-radius: 10px;
|
|
|
|
+ background: rgba(20, 30, 60, 0.3);
|
|
|
|
+ backdrop-filter: blur(6px);
|
|
|
|
+ box-shadow:
|
|
|
|
+ 0 0 12px rgba(109, 228, 255, 0.3),
|
|
|
|
+ inset 0 0 6px rgba(109, 228, 255, 0.2);
|
|
|
|
+
|
|
|
|
+ animation: pulse-glow 2s infinite ease-in-out;
|
|
|
|
+ z-index: 10;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ @keyframes pulse-glow {
|
|
|
|
+ 0%,
|
|
|
|
+ 100% {
|
|
|
|
+ box-shadow:
|
|
|
|
+ 0 0 12px rgba(109, 228, 255, 0.3),
|
|
|
|
+ inset 0 0 6px rgba(109, 228, 255, 0.2);
|
|
|
|
+ }
|
|
|
|
+ 50% {
|
|
|
|
+ box-shadow:
|
|
|
|
+ 0 0 20px rgba(109, 228, 255, 0.6),
|
|
|
|
+ inset 0 0 10px rgba(109, 228, 255, 0.4);
|
|
|
|
+ }
|
|
}
|
|
}
|
|
|
|
|
|
.time-info {
|
|
.time-info {
|
|
@@ -685,6 +831,64 @@ const handleResize = () => {
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
+
|
|
|
|
+:deep(.tenant-switcher) {
|
|
|
|
+ display: flex;
|
|
|
|
+ align-items: center;
|
|
|
|
+ gap: 10px;
|
|
|
|
+ font-size: 14px;
|
|
|
|
+ .ant-select {
|
|
|
|
+ background-color: @panel-bg;
|
|
|
|
+
|
|
|
|
+ .ant-select-selector {
|
|
|
|
+ background-color: @panel-bg;
|
|
|
|
+ border-color: @border-color;
|
|
|
|
+ color: @text-color;
|
|
|
|
+ box-shadow: 0 0 5px @glow-color;
|
|
|
|
+ transition: all 0.3s ease;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ &.ant-select-open .ant-select-selector {
|
|
|
|
+ border-color: @accent-color;
|
|
|
|
+ background-color: @panel-bg;
|
|
|
|
+
|
|
|
|
+ .ant-select-selection-item,
|
|
|
|
+ .ant-select-selection-placeholder {
|
|
|
|
+ color: @text-color !important;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ .ant-select-arrow {
|
|
|
|
+ color: @text-color;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ .ant-select-dropdown {
|
|
|
|
+ background-color: @bg-color;
|
|
|
|
+ color: @text-color;
|
|
|
|
+ border: 1px solid @border-color;
|
|
|
|
+ box-shadow: 0 0 10px @glow-color;
|
|
|
|
+ max-width: 180px;
|
|
|
|
+
|
|
|
|
+ .ant-select-item {
|
|
|
|
+ color: @text-color;
|
|
|
|
+ white-space: nowrap;
|
|
|
|
+ overflow: hidden;
|
|
|
|
+ text-overflow: ellipsis;
|
|
|
|
+ transition: background-color 0.2s ease;
|
|
|
|
+
|
|
|
|
+ &.ant-select-item-option-selected {
|
|
|
|
+ background-color: @accent-color;
|
|
|
|
+ color: @text-color;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ &:hover {
|
|
|
|
+ background-color: @border-color;
|
|
|
|
+ color: @text-color;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+}
|
|
</style>
|
|
</style>
|
|
|
|
|
|
<style lang="less">
|
|
<style lang="less">
|