Browse Source

feat: 新增智慧大屏演示版;

liujia 9 hours ago
parent
commit
d85f4cb830

+ 5 - 0
src/layout/index.vue

@@ -19,6 +19,7 @@
     <a-layout class="layout-content">
       <a-layout-header>
         <slot name="header">
+          <div class="smartScreen" @click="openSmartScreenDemo">大屏演示版</div>
           <base-weather class="weather" mode="text"></base-weather>
           <div class="smartScreen" @click="openSmartScreen">智慧大屏</div>
 
@@ -342,6 +343,10 @@ const backHandler = async () => {
 const openSmartScreen = () => {
   window.open('/dashboard', '_blank')
 }
+
+const openSmartScreenDemo = () => {
+  window.open('/dashboard-demo', '_blank')
+}
 </script>
 
 <style scoped lang="less">

+ 7 - 2
src/router/index.ts

@@ -30,10 +30,15 @@ const router = createRouter({
     {
       path: '/dashboard',
       name: 'dashboard',
-      // component: () => import('@/views/dashboard/index.vue'),
-      component: () => import('@/views/dashboard/demo.vue'),
+      component: () => import('@/views/dashboard/index.vue'),
       meta: { title: '大屏', isFullScreen: true, keepAlive: false, hidden: true },
     },
+    {
+      path: '/dashboard-demo',
+      name: 'dashboardDemo',
+      component: () => import('@/views/dashboard/demo.vue'),
+      meta: { title: '大屏演示', isFullScreen: true, keepAlive: false, hidden: true },
+    },
   ],
 })
 router.beforeEach(authGuard)

+ 5 - 2
src/views/dashboard/components/GuardObjectAgeCard/index.vue

@@ -1,6 +1,6 @@
 <template>
   <TechCard>
-    <div class="card-title">监护对象年龄分布</div>
+    <div class="card-title">{{ props.title }}</div>
     <BaseChart :option="chartOption" :height="220" />
   </TechCard>
 </template>
@@ -18,9 +18,12 @@ interface AgeItem {
 
 interface Props {
   ageList: AgeItem[]
+  title?: string
 }
 
-const props = defineProps<Props>()
+const props = withDefaults(defineProps<Props>(), {
+  title: '监护对象年龄分布',
+})
 
 const chartOption = computed(() => {
   if (!props.ageList || props.ageList.length === 0) {

+ 5 - 3
src/views/dashboard/components/GuardObjectTypeCard/index.vue

@@ -1,6 +1,6 @@
 <template>
   <TechCard>
-    <div class="card-title">监护对象类型</div>
+    <div class="card-title">{{ props.title }}</div>
     <BaseChart :option="chartOption" :height="180" />
   </TechCard>
 </template>
@@ -19,10 +19,12 @@ interface GuardItem {
 
 interface Props {
   guardList: GuardItem[]
+  title?: string
 }
 
-const props = defineProps<Props>()
-
+const props = withDefaults(defineProps<Props>(), {
+  title: '监护对象类型',
+})
 // 提取分类名称和数量
 const categories = computed(() => props.guardList.map((item) => item.name))
 const counts = computed(() => props.guardList.map((item) => Number(item.count)))

+ 3 - 1
src/views/dashboard/components/MonitorPeopleCountCard/index.vue

@@ -1,7 +1,7 @@
 <template>
   <TechCard class="people-detected-card">
     <div class="card-header">
-      <div class="title">检测到人数</div>
+      <div class="title">{{ props.title }}</div>
     </div>
 
     <div class="matrix-wrapper">
@@ -42,11 +42,13 @@ defineOptions({ name: 'MonitorPeopleCountCard' })
 type Props = {
   detectedCount: number
   limit?: number // 限制显示的人数
+  title?: string
 }
 
 const props = withDefaults(defineProps<Props>(), {
   detectedCount: 0,
   limit: 28,
+  title: '检测到人数',
 })
 </script>
 

+ 248 - 29
src/views/dashboard/demo.vue

@@ -2,7 +2,7 @@
   <div class="dashboard">
     <div class="dashboard-header">
       <div class="community-name">
-        <div class="fixedName"> 智慧大屏</div>
+        <div class="fixedName"> 智慧大屏演示版</div>
         <div class="tenantName">{{ tenantName }}</div>
       </div>
       <div class="running-days"
@@ -21,26 +21,55 @@
           :getPopupContainer="(trigger: HTMLElement) => trigger.parentNode"
           @change="handleTenantChange"
         />
+        <div style="cursor: pointer; font-weight: 600" @click="goToHome">返回首页</div>
       </div>
       <div class="data-flow header-flow"></div>
     </div>
     <div class="dashboard-content">
       <div class="block custom-scroll">
         <div class="data-row">
+          <GuardObjectAgeCard :ageList="todayData.ageList" title="入住老人年龄分布" />
+          <GuardObjectTypeCard :guardList="todayData.guardList" title="检测对象分级" />
+          <MonitorPeopleCountCard :detectedCount="todayData.detectedCount" title="今日检测到人数" />
+          <ElderActivityCard :activity-rate="todayData.activeRate" />
+        </div>
+
+        <div class="editor-note">
+          <TechCard>
+            <div class="editor-note-hd">长者基础事件备忘录</div>
+            <div class="editor-note-bd">
+              <div>305房间,昨日发生滞留</div>
+              <div>802老人,昨日发生摔倒</div>
+              <div>601房间,昨日如厕异常</div>
+              <div>708房间,昨日发生摔倒</div>
+              <div>503房间,昨日发生滞留</div>
+              <div>915房间,昨日如厕异常</div>
+              <!-- 重复一遍内容以实现无缝滚动 -->
+              <div>305房间,昨日发生滞留</div>
+              <div>802老人,昨日发生摔倒</div>
+              <div>601房间,昨日如厕异常</div>
+              <div>708房间,昨日发生摔倒</div>
+              <div>503房间,昨日发生滞留</div>
+              <div>915房间,昨日如厕异常</div>
+            </div>
+          </TechCard>
+        </div>
+
+        <!-- <div class="data-row">
           <DeviceOnlineRateCard
             :online-count="todayData.onlineCount"
             :device-count="todayData.deviceCount"
           ></DeviceOnlineRateCard>
           <MonitorPeopleCountCard :detectedCount="todayData.detectedCount"></MonitorPeopleCountCard>
-        </div>
-        <div class="data-row">
+        </div> -->
+        <!-- <div class="data-row">
           <GuardObjectTypeCard :guardList="todayData.guardList"></GuardObjectTypeCard>
           <AlertFallCompareCard
             :fall-count="todayData.fallingCount"
             :alert-count="todayData.alarmCount"
           ></AlertFallCompareCard>
-        </div>
-        <DeviceLocationCard :data="todayData.installPositionList"></DeviceLocationCard>
+        </div> -->
+        <!-- <DeviceLocationCard :data="todayData.installPositionList"></DeviceLocationCard> -->
       </div>
       <div class="block block-center custom-scroll">
         <div class="map-container">
@@ -64,9 +93,22 @@
       </div>
       <div class="block custom-scroll" style="padding: 10px">
         <div class="data-row">
+          <DeviceOnlineRateCard
+            :online-count="todayData.onlineCount"
+            :device-count="todayData.deviceCount"
+          />
+          <DeviceLocationCard :data="todayData.installPositionList" />
+        </div>
+        <AlertFallCompareCard
+          :fall-count="todayData.fallingCount"
+          :alert-count="todayData.alarmCount"
+          style="margin-bottom: 10px"
+        />
+
+        <!-- <div class="data-row">
           <GuardObjectAgeCard :ageList="todayData.ageList"></GuardObjectAgeCard>
           <ElderActivityCard :activity-rate="todayData.activeRate"></ElderActivityCard>
-        </div>
+        </div> -->
 
         <div class="data-line">
           <HistoryChartCard
@@ -122,13 +164,15 @@
       </div>
     </div>
     <div class="dashboard-footer">
-      <Copyright
+      <!-- <Copyright
         company="合肥雷能信息技术有限公司"
         icp="皖ICP备2024060056号-3"
         icp-link="https://beian.miit.gov.cn"
         icp-text="皖ICP备2024060056号-3"
         font-color="#4774a7"
-      />
+      /> -->
+      <div>305房间19:00 发生跌倒事件,请及时处理;</div>
+      <div>802老人17:00 发生异常消失,已处理;</div>
     </div>
   </div>
 </template>
@@ -156,6 +200,8 @@ import type {
   StatsHomeScreenFallHistory,
   StatsHomeScreenQueryData,
 } from '@/api/stats/types'
+import TechCard from './components/TechCard/index.vue'
+import { set } from 'lodash-es'
 
 const userStore = useUserStore()
 
@@ -267,7 +313,7 @@ watch(
 
     if (newTenantId) {
       pollingInstance = useDashboardPolling({ tenantId: String(newTenantId) })
-      pollingInstance.start()
+      // pollingInstance.start()
 
       // 赋值响应式数据
       watchEffect(() => {
@@ -278,11 +324,119 @@ watch(
 
       updateFallQueryType = pollingInstance.updateFallQueryType
       updateAlarmQueryType = pollingInstance.updateAlarmQueryType
+
+      todayData.value.systemGuardDay = Math.round(Math.random() * 100) // 守护天数
+
+      setInterval(() => {
+        console.log('🚀🚀🚀 mock 数据更新了')
+        localStorage.setItem('todayData', JSON.stringify(todayData.value))
+        mockData()
+      }, 2000)
+
+      setInterval(() => {
+        todayData.value.systemGuardDay = Math.round(Math.random() * 100) // 守护天数
+      }, 5000)
     }
   },
   { immediate: true }
 )
 
+// mock 数据
+const mockData = () => {
+  // todayData.value.systemGuardDay = Math.round(Math.random() * 100) // 守护天数
+  // 年龄分布
+  todayData.value.ageList = [
+    { ageRange: '60-69岁', count: Math.floor(Math.random() * 50) + 10 },
+    { ageRange: '70-79岁', count: Math.floor(Math.random() * 50) + 10 },
+    { ageRange: '80-89岁', count: Math.floor(Math.random() * 50) + 10 },
+    { ageRange: '90-99岁', count: Math.floor(Math.random() * 50) + 10 },
+    { ageRange: '100岁以上', count: Math.floor(Math.random() * 50) + 10 },
+  ]
+  // 检测对象分级
+  todayData.value.guardList = [
+    { guardType: 'parent', name: '重点对象', count: Math.floor(Math.random() * 50) + 10 },
+    { guardType: 'child', name: '一般对象', count: Math.floor(Math.random() * 50) + 10 },
+    { guardType: 'guardian', name: '普通对象', count: Math.floor(Math.random() * 50) + 10 },
+  ]
+  // 检测人数
+  todayData.value.detectedCount = Math.floor(Math.random() * 500) + 100
+  // 长者活跃度
+  todayData.value.activeRate = Math.round(Math.random() * 100)
+  // 设备在线率
+  todayData.value.onlineCount = Math.round(Math.random() * 2500)
+  todayData.value.deviceCount = 2568
+  // 设备安装位置
+  todayData.value.installPositionList = [
+    { installPosition: 'Bedroom', name: '卧室', count: Math.floor(Math.random() * 50) + 10 },
+    { installPosition: 'LivingRoom', name: '客厅', count: Math.floor(Math.random() * 50) + 10 },
+    { installPosition: 'Restaurant', name: '餐厅', count: Math.floor(Math.random() * 50) + 10 },
+    { installPosition: 'Toilet', name: '卫生间', count: Math.floor(Math.random() * 50) + 10 },
+  ]
+  // 跌倒次数
+  todayData.value.fallingCount = Math.floor(Math.random() * 500) + 100
+  // 报警次数
+  todayData.value.alarmCount = Math.floor(Math.random() * 500) + 100
+
+  const generateRecent7DaysData = () => {
+    const today = new Date()
+    const dayStatInfo = []
+
+    for (let i = 6; i >= 0; i--) {
+      const date = new Date(today)
+      date.setDate(today.getDate() - i)
+      const formattedDate = date.toISOString().split('T')[0]
+      dayStatInfo.push({
+        date: formattedDate,
+        fallingCount: Math.floor(Math.random() * 50) + 10,
+        alarmCount: Math.floor(Math.random() * 50) + 10,
+      })
+    }
+
+    return dayStatInfo
+  }
+
+  const generateRecent180DaysMonthlyData = () => {
+    const today = new Date()
+    const monthStatInfo = []
+
+    for (let i = 5; i >= 0; i--) {
+      const date = new Date(today)
+      date.setMonth(today.getMonth() - i)
+      const formattedMonth = date.toISOString().slice(0, 7)
+      monthStatInfo.push({
+        month: formattedMonth,
+        fallingCount: Math.floor(Math.random() * 200) + 50,
+        alarmCount: Math.floor(Math.random() * 200) + 50,
+      })
+    }
+
+    return monthStatInfo
+  }
+
+  fallHistoryData.value = {
+    dayStatInfo: generateRecent7DaysData().map((d) => ({
+      date: d.date,
+      fallingCount: d.fallingCount,
+    })),
+    monthStatInfo: generateRecent180DaysMonthlyData().map((m) => ({
+      month: m.month,
+      fallingCount: m.fallingCount,
+    })),
+  }
+
+  alarmHistoryData.value = {
+    dayStatInfo: generateRecent7DaysData().map((d) => ({
+      date: d.date,
+      alarmCount: d.alarmCount,
+    })),
+    monthStatInfo: generateRecent180DaysMonthlyData().map((m) => ({
+      month: m.month,
+      alarmCount: m.alarmCount,
+    })),
+  }
+}
+mockData()
+
 const { fetchDict: fetchDictGuardianship, dictNameMap: guardTypeNameMap } =
   useDict('guardianship_type')
 
@@ -299,10 +453,10 @@ watch(
     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.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.ageList = (val?.ageList && val?.ageList.filter((item) => item.count > 0)) ?? []
     todayData.value.installPositionList =
       (val?.installPositionList &&
         val?.installPositionList.map((item) => ({
@@ -314,15 +468,15 @@ watch(
           names: installPositionNameMap.value,
         }))) ??
       []
-    todayData.value.guardList =
-      (val?.guardList &&
-        val?.guardList.map((item) => ({
-          ...item,
-          name:
-            guardTypeNameMap.value[item.guardType as keyof typeof guardTypeNameMap.value] || '未知',
-          names: guardTypeNameMap.value,
-        }))) ??
-      []
+    // todayData.value.guardList =
+    //   (val?.guardList &&
+    //     val?.guardList.map((item) => ({
+    //       ...item,
+    //       name:
+    //         guardTypeNameMap.value[item.guardType as keyof typeof guardTypeNameMap.value] || '未知',
+    //       names: guardTypeNameMap.value,
+    //     }))) ??
+    //   []
   },
   { immediate: true, deep: true }
 )
@@ -456,6 +610,10 @@ const handleResize = () => {
     scale.value = 0.7
   }
 }
+
+const goToHome = () => {
+  window.open('/', '_blank')
+}
 </script>
 
 <style scoped lang="less">
@@ -517,6 +675,13 @@ const handleResize = () => {
       .fixedName {
         margin-right: 10px;
       }
+
+      .tenantName {
+        max-width: 300px;
+        white-space: nowrap;
+        overflow: hidden;
+        text-overflow: ellipsis;
+      }
     }
 
     .running-days {
@@ -590,7 +755,8 @@ const handleResize = () => {
   }
 
   &-content {
-    padding: 24px;
+    // padding: 24px;
+    padding: 10px 20px 5px;
     flex: 1;
     border-radius: 8px;
     display: flex;
@@ -723,21 +889,74 @@ const handleResize = () => {
       margin-bottom: 12px;
     }
 
+    // 长者基础事件备忘录
+    .editor-note {
+      color: @text-color;
+      margin-bottom: 10px;
+
+      &-hd {
+        font-weight: bold;
+        margin-bottom: 8px;
+        color: #6de4ff;
+      }
+
+      &-bd {
+        height: 130px;
+        overflow: hidden;
+        position: relative;
+        line-height: 1.5;
+      }
+
+      &-bd::before {
+        content: '';
+        position: absolute;
+        top: 0;
+        left: 0;
+        right: 0;
+        bottom: 0;
+        pointer-events: none;
+      }
+
+      &-bd > div {
+        animation: scrollUp 12s linear infinite;
+      }
+
+      @keyframes scrollUp {
+        0% {
+          transform: translateY(100%);
+        }
+        100% {
+          transform: translateY(-100%);
+        }
+      }
+    }
+
     .data-line {
       display: grid;
-      grid-template-columns: 1fr;
+      //  grid-template-columns: 1fr;
+      grid-template-columns: 1fr 1fr;
       gap: 12px;
     }
   }
 
   &-footer {
-    height: 20px;
-    color: @text-color;
-    color: #6de4ff;
-    text-align: center;
-    line-height: 30px;
-    font-size: 12px;
-    margin-bottom: 10px;
+    // height: 20px;
+    // color: @text-color;
+    // color: #6de4ff;
+    // text-align: center;
+    // line-height: 30px;
+    // font-size: 12px;
+    // margin-bottom: 10px;
+
+    display: flex;
+    justify-content: space-between;
+    div {
+      flex-grow: 1;
+      text-align: center;
+      padding: 10px;
+      border: 3px solid @border-color;
+      margin: 10px 24px;
+    }
   }
 
   @media (max-width: 1600px) {