Kaynağa Gözat

feat: 智慧大屏历史统计数据接口对接与联调;并调整卡片布局展示;

liujia 1 ay önce
ebeveyn
işleme
d1063a2

+ 32 - 12
src/hooks/useDashboardPolling.ts

@@ -6,10 +6,14 @@ import type {
   StatsHomeScreenAlarmHistory,
 } from '@/api/stats/types'
 
-export function useDashboardPolling(options: { tenantId: string; queryType: string }) {
-  const todayScreenData = ref<StatsHomeScreenQueryData | null>(null) // 今日大屏数据
-  const fallHistoryData = ref<StatsHomeScreenFallHistory | null>(null) // 历史跌倒数据
-  const alarmHistoryData = ref<StatsHomeScreenAlarmHistory | null>(null) // 历史告警数据
+export function useDashboardPolling(options: { tenantId: string }) {
+  const todayScreenData = ref<StatsHomeScreenQueryData | null>(null)
+  const fallHistoryData = ref<StatsHomeScreenFallHistory | null>(null)
+  const alarmHistoryData = ref<StatsHomeScreenAlarmHistory | null>(null)
+
+  // queryType 分开管理
+  const fallQueryType = ref<'day' | 'month'>('day')
+  const alarmQueryType = ref<'day' | 'month'>('day')
 
   let realtimeTimer: ReturnType<typeof setInterval> | null = null
   let businessTimer: ReturnType<typeof setInterval> | null = null
@@ -19,7 +23,6 @@ export function useDashboardPolling(options: { tenantId: string; queryType: stri
   let retryCountRealtime = 0
   let retryCountBusiness = 0
 
-  // 获取今日数据 (实时数据)
   const getTodayData = async () => {
     if (!isVisible) return
     try {
@@ -37,18 +40,17 @@ export function useDashboardPolling(options: { tenantId: string; queryType: stri
     }
   }
 
-  // 获取历史数据 (跌倒和告警)
   const getHistoryData = async () => {
     if (!isVisible) return
     try {
       const [fallRes, alarmRes] = await Promise.all([
         statsApi.statsHomeScreenFallHistory({
           tenantId: options.tenantId,
-          queryType: options.queryType,
+          queryType: fallQueryType.value,
         }),
         statsApi.statsHomeScreenAlarmHistory({
           tenantId: options.tenantId,
-          queryType: options.queryType,
+          queryType: alarmQueryType.value,
         }),
       ])
       fallHistoryData.value = fallRes.data
@@ -56,7 +58,7 @@ export function useDashboardPolling(options: { tenantId: string; queryType: stri
       retryCountBusiness = 0
     } catch (err) {
       retryCountBusiness++
-      console.warn(`获取历史数据 获取失败(第 ${retryCountBusiness} 次)`, err)
+      console.warn(`历史数据获取失败(第 ${retryCountBusiness} 次)`, err)
       if (retryCountBusiness < MAX_RETRY) {
         setTimeout(getHistoryData, 2000)
       }
@@ -66,8 +68,8 @@ export function useDashboardPolling(options: { tenantId: string; queryType: stri
   const startPolling = () => {
     getTodayData()
     getHistoryData()
-    realtimeTimer = setInterval(getTodayData, 5000) // 5秒获取一次 实时数据
-    businessTimer = setInterval(getHistoryData, 30000) // 30秒获取一次 历史数据
+    realtimeTimer = setInterval(getTodayData, 5000)
+    businessTimer = setInterval(getHistoryData, 30000)
   }
 
   const stopPolling = () => {
@@ -96,11 +98,29 @@ export function useDashboardPolling(options: { tenantId: string; queryType: stri
     document.removeEventListener('visibilitychange', handleVisibilityChange)
   })
 
-  console.log('🚀🚀🚀useDashboardPolling:', todayScreenData, fallHistoryData, alarmHistoryData)
+  const updateFallQueryType = async (type: 'day' | 'month') => {
+    fallQueryType.value = type
+    const res = await statsApi.statsHomeScreenFallHistory({
+      tenantId: options.tenantId,
+      queryType: fallQueryType.value,
+    })
+    fallHistoryData.value = res.data
+  }
+
+  const updateAlarmQueryType = async (type: 'day' | 'month') => {
+    alarmQueryType.value = type
+    const res = await statsApi.statsHomeScreenAlarmHistory({
+      tenantId: options.tenantId,
+      queryType: alarmQueryType.value,
+    })
+    alarmHistoryData.value = res.data
+  }
 
   return {
     todayScreenData,
     fallHistoryData,
     alarmHistoryData,
+    updateFallQueryType,
+    updateAlarmQueryType,
   }
 }

+ 51 - 50
src/views/dashboard/components/HistoryChartCard/index.vue

@@ -1,57 +1,53 @@
 <template>
-  <TechCard>
+  <TechCard class="chart-wrapper">
     <template #extra>
-      <div class="toggle-group">
-        <button :class="{ active: mode === 'day' }" @click="mode = 'day'">按日</button>
-        <button :class="{ active: mode === 'month' }" @click="mode = 'month'">按月</button>
+      <div class="header">
+        <div class="card-title">{{ title }}</div>
+        <div classs="card-extra"><slot name="extra"></slot></div>
       </div>
     </template>
-
-    <div class="card-title">{{ title }}</div>
-    <BaseChart :option="chartOption" :height="140" />
+    <BaseChart :option="chartOption" :height="155" />
+    <div v-if="loading" class="loading-mask"> <a-spin /> </div>
   </TechCard>
 </template>
 
 <script setup lang="ts">
-import { computed, ref } from 'vue'
+import { computed } from 'vue'
 import * as echarts from 'echarts'
 import TechCard from '../TechCard/index.vue'
 
 defineOptions({ name: 'HistoryChartCard' })
 
-interface DayItem {
-  date: string
-  fallingCount: number
-}
-
-interface MonthItem {
-  month: string
-  fallingCount: number
+interface StatInfo {
+  lable: string
+  count: number
 }
 
 const props = defineProps<{
   title: string
-  dayStatInfo: DayItem[]
-  monthStatInfo: MonthItem[]
+  dayStatInfo: StatInfo[]
+  monthStatInfo: StatInfo[]
   color: string
   seriesName: string
+  mode: 'day' | 'month'
+  loading?: boolean
 }>()
 
-const mode = ref<'day' | 'month'>('day')
-
 const chartData = computed(() => {
-  const source = mode.value === 'day' ? props.dayStatInfo : props.monthStatInfo
-  const dates = source.map((item) => ('date' in item ? item.date : item.month))
-  const values = source.map((item) => item.fallingCount ?? 0)
-  return { dates, values }
+  if (props.mode === 'day') {
+    const dates = props.dayStatInfo.map((item) => item.lable)
+    const values = props.dayStatInfo.map((item) => item.count ?? 0)
+    return { dates, values }
+  } else {
+    const dates = props.monthStatInfo.map((item) => item.lable)
+    const values = props.monthStatInfo.map((item) => item.count ?? 0)
+    return { dates, values }
+  }
 })
 
 const chartOption = computed(() => ({
   grid: { top: 10, right: 20, bottom: 30, left: 30 },
-  tooltip: {
-    trigger: 'item',
-    axisPointer: { type: 'shadow' },
-  },
+  tooltip: { trigger: 'item', axisPointer: { type: 'shadow' } },
   xAxis: {
     type: 'category',
     data: chartData.value.dates,
@@ -67,8 +63,8 @@ const chartOption = computed(() => ({
   series: [
     {
       name: props.seriesName,
-      type: mode.value === 'day' ? 'line' : 'bar', // ✅ 动态切换图表类型
-      smooth: mode.value === 'day',
+      type: props.mode === 'day' ? 'line' : 'bar',
+      smooth: props.mode === 'day',
       symbol: 'circle',
       symbolSize: 8,
       data: chartData.value.values,
@@ -81,7 +77,7 @@ const chartOption = computed(() => ({
         ]),
       },
       areaStyle:
-        mode.value === 'day'
+        props.mode === 'day'
           ? {
               color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
                 { offset: 0, color: `${props.color}4D` },
@@ -95,32 +91,37 @@ const chartOption = computed(() => ({
 </script>
 
 <style scoped lang="less">
+.chart-wrapper {
+  position: relative;
+}
+
+.header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  width: 100%;
+  margin-bottom: 12px;
+}
+
 .card-title {
   font-size: 16px;
   font-weight: bold;
-  margin-bottom: 12px;
   text-align: center;
   color: v-bind(color);
 }
 
-.toggle-group {
+.loading-mask {
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background: rgba(0, 0, 0, 0.3);
+  color: white;
+  font-size: 14px;
   display: flex;
-  gap: 8px;
-  justify-content: flex-end;
-
-  button {
-    background: none;
-    border: 1px solid v-bind(color);
-    color: v-bind(color);
-    padding: 4px 10px;
-    border-radius: 4px;
-    font-size: 12px;
-    cursor: pointer;
-
-    &.active {
-      background-color: v-bind(color);
-      color: #fff;
-    }
-  }
+  align-items: center;
+  justify-content: center;
+  z-index: 10;
 }
 </style>

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

@@ -69,7 +69,6 @@ defineOptions({ name: 'TechCard' })
 
   .card-header {
     display: flex;
-    justify-content: flex-end;
     margin-bottom: 8px;
     z-index: 1;
     position: relative;

+ 167 - 40
src/views/dashboard/index.vue

@@ -44,7 +44,7 @@
           <ZoomInOutlined @click="zoomIn" />
         </a-space>
       </div>
-      <div class="block custom-scroll">
+      <div class="block custom-scroll" style="padding: 10px">
         <div class="data-row">
           <DeviceAgeCard :ageList="todayData.ageList"></DeviceAgeCard>
           <ElderActivityCard :activity-rate="todayData.activeRate"></ElderActivityCard>
@@ -53,47 +53,53 @@
         <div class="data-line">
           <HistoryChartCard
             title="历史告警统计"
-            :dayStatInfo="[
-              { date: '2024-07-01', fallingCount: 5 },
-              { date: '2024-07-02', fallingCount: 8 },
-              { date: '2024-07-03', fallingCount: 6 },
-              { date: '2024-07-04', fallingCount: 10 },
-              { date: '2024-07-05', fallingCount: 7 },
-            ]"
-            :monthStatInfo="[
-              { month: '2024-06', fallingCount: 32 },
-              { month: '2024-07', fallingCount: 41 },
-              { month: '2024-08', fallingCount: 45 },
-              { month: '2024-09', fallingCount: 30 },
-              { month: '2024-10', fallingCount: 60 },
-              { month: '2024-11', fallingCount: 35 },
-              { month: '2024-12', fallingCount: 50 },
-            ]"
+            :dayStatInfo="historyData.alarmHistoryData.dayStatInfo"
+            :monthStatInfo="historyData.alarmHistoryData.monthStatInfo"
+            :mode="alarmMode"
+            :loading="alarmLoading"
             color="#f39c12"
             seriesName="告警次数"
-          />
+          >
+            <template #extra>
+              <div class="toggle-group">
+                <button
+                  :class="['alarm-button', { active: alarmMode === 'day' }]"
+                  @click="changeAlarmMode('day')"
+                  >最近7天</button
+                >
+                <button
+                  :class="['alarm-button', { active: alarmMode === 'month' }]"
+                  @click="changeAlarmMode('month')"
+                  >最近180天</button
+                >
+              </div>
+            </template>
+          </HistoryChartCard>
 
           <HistoryChartCard
             title="历史跌倒统计"
-            :dayStatInfo="[
-              { date: '2024-07-01', fallingCount: 5 },
-              { date: '2024-07-02', fallingCount: 8 },
-              { date: '2024-07-03', fallingCount: 6 },
-              { date: '2024-07-04', fallingCount: 10 },
-              { date: '2024-07-05', fallingCount: 7 },
-            ]"
-            :monthStatInfo="[
-              { month: '2024-06', fallingCount: 32 },
-              { month: '2024-07', fallingCount: 41 },
-              { month: '2024-08', fallingCount: 60 },
-              { month: '2024-09', fallingCount: 30 },
-              { month: '2024-10', fallingCount: 35 },
-              { month: '2024-11', fallingCount: 45 },
-              { month: '2024-12', fallingCount: 70 },
-            ]"
+            :dayStatInfo="historyData.fallHistoryData.dayStatInfo"
+            :monthStatInfo="historyData.fallHistoryData.monthStatInfo"
+            :mode="fallMode"
+            :loading="fallLoading"
             color="#e74c3c"
             seriesName="跌倒次数"
-          />
+          >
+            <template #extra>
+              <div class="toggle-group">
+                <button
+                  :class="['fall-button', { active: fallMode === 'day' }]"
+                  @click="changeFallMode('day')"
+                  >最近7天</button
+                >
+                <button
+                  :class="['fall-button', { active: fallMode === 'month' }]"
+                  @click="changeFallMode('month')"
+                  >最近180天</button
+                >
+              </div>
+            </template>
+          </HistoryChartCard>
         </div>
       </div>
     </div>
@@ -161,10 +167,13 @@ const todayData = ref<TodayData>({
 
 useResponsiveLayout()
 
-const { todayScreenData, fallHistoryData, alarmHistoryData } = useDashboardPolling({
-  tenantId: userStore.userInfo.tenantId || '',
-  queryType: 'day',
-})
+const {
+  todayScreenData,
+  fallHistoryData,
+  alarmHistoryData,
+  updateFallQueryType,
+  updateAlarmQueryType,
+} = useDashboardPolling({ tenantId: userStore.userInfo.tenantId || '' })
 
 const { fetchDict: fetchDictGuardianship, dictNameMap: guardTypeNameMap } =
   useDict('guardianship_type')
@@ -202,22 +211,101 @@ watch(
   { immediate: true }
 )
 
+type RawDayItem = { date: string; fallingCount?: number; alarmCount?: number }
+type RawMonthItem = { month: string; fallingCount?: number; alarmCount?: number }
+
+type StatInfo = { lable: string; count: number }
+type HistoryData = { monthStatInfo: StatInfo[]; dayStatInfo: StatInfo[] }
+
+const historyData = ref<{
+  fallHistoryData: HistoryData
+  alarmHistoryData: HistoryData
+}>({
+  fallHistoryData: { monthStatInfo: [], dayStatInfo: [] },
+  alarmHistoryData: { monthStatInfo: [], dayStatInfo: [] },
+})
+
+// 通用转换函数
+const transformStatInfo = (
+  source: RawDayItem[] | RawMonthItem[],
+  labelKey: 'date' | 'month',
+  countKey: 'fallingCount' | 'alarmCount'
+): StatInfo[] => {
+  if (labelKey === 'date') {
+    return (source as RawDayItem[]).map((item) => ({
+      lable: item.date,
+      count: item[countKey] ?? 0,
+    }))
+  } else {
+    return (source as RawMonthItem[]).map((item) => ({
+      lable: item.month,
+      count: item[countKey] ?? 0,
+    }))
+  }
+}
+
+// 监听跌倒数据
 watch(
   () => fallHistoryData.value,
   (val) => {
     console.log('🚀🚀🚀 fallHistoryData 更新了', val)
+    historyData.value.fallHistoryData.dayStatInfo = transformStatInfo(
+      val?.dayStatInfo ?? [],
+      'date',
+      'fallingCount'
+    )
+    historyData.value.fallHistoryData.monthStatInfo = transformStatInfo(
+      val?.monthStatInfo ?? [],
+      'month',
+      'fallingCount'
+    )
   },
   { immediate: true }
 )
 
+// 监听告警数据
 watch(
   () => alarmHistoryData.value,
   (val) => {
     console.log('🚀🚀🚀 alarmHistoryData 更新了', val)
+    historyData.value.alarmHistoryData.dayStatInfo = transformStatInfo(
+      val?.dayStatInfo ?? [],
+      'date',
+      'alarmCount'
+    )
+    historyData.value.alarmHistoryData.monthStatInfo = transformStatInfo(
+      val?.monthStatInfo ?? [],
+      'month',
+      'alarmCount'
+    )
   },
   { immediate: true }
 )
 
+const alarmMode = ref<'day' | 'month'>('day')
+const fallMode = ref<'day' | 'month'>('day')
+
+const alarmLoading = ref(false)
+const fallLoading = ref(false)
+
+const changeAlarmMode = async (mode: 'day' | 'month') => {
+  if (alarmMode.value !== mode) {
+    alarmMode.value = mode
+    alarmLoading.value = true
+    await updateAlarmQueryType(mode)
+    alarmLoading.value = false
+  }
+}
+
+const changeFallMode = async (mode: 'day' | 'month') => {
+  if (fallMode.value !== mode) {
+    fallMode.value = mode
+    fallLoading.value = true
+    await updateFallQueryType(mode)
+    fallLoading.value = false
+  }
+}
+
 const toDeviceList = () => {
   window.open('/device/list', '_blank')
 }
@@ -363,7 +451,7 @@ const handleResize = () => {
       display: flex;
       flex-direction: column;
       position: relative;
-      overflow: auto;
+      overflow-y: auto;
     }
 
     .block-center {
@@ -558,6 +646,45 @@ const handleResize = () => {
     }
   }
 }
+
+.toggle-group {
+  display: flex;
+  gap: 8px;
+  justify-content: flex-end;
+
+  button {
+    background: none;
+    border: 1px solid;
+    padding: 4px 10px;
+    border-radius: 4px;
+    font-size: 12px;
+    cursor: pointer;
+
+    &.active {
+      color: #fff;
+    }
+  }
+
+  .alarm-button {
+    border-color: #f39c12;
+    color: #f39c12;
+
+    &.active {
+      background-color: #f39c12;
+      color: #fff;
+    }
+  }
+
+  .fall-button {
+    border-color: #e74c3c;
+    color: #e74c3c;
+
+    &.active {
+      background-color: #e74c3c;
+      color: #fff;
+    }
+  }
+}
 </style>
 
 <style lang="less">