Procházet zdrojové kódy

feat: dashboard v0

Casper Dai před 3 roky
rodič
revize
f8f867e7b3
49 změnil soubory, kde provedl 2607 přidání a 3 odebrání
  1. binární
      src/assets/v0/bg.png
  2. 14 0
      src/assets/v0/icon_position1.svg
  3. 14 0
      src/assets/v0/icon_position2.svg
  4. binární
      src/assets/v0/screen.png
  5. 0 0
      src/components/common/AutoImage/index.vue
  6. 0 0
      src/components/common/MediaCard/index.vue
  7. 0 0
      src/components/common/Permission/index.vue
  8. 0 0
      src/components/layout/Tabbar/index.vue
  9. 0 0
      src/components/layout/Warning/index.vue
  10. 0 0
      src/components/service/EventPicker/index.vue
  11. 0 0
      src/components/service/Schedule/ScheduleCalendar/EventItem.vue
  12. 0 0
      src/components/service/Schedule/ScheduleCalendar/EventItemWeek.vue
  13. 0 0
      src/components/service/Schedule/ScheduleCalendar/PopList.vue
  14. 0 0
      src/components/service/Schedule/ScheduleCalendar/ScheduleCalendarMonth.vue
  15. 0 0
      src/components/service/Schedule/ScheduleCalendar/ScheduleCalendarWeek.vue
  16. 0 0
      src/components/service/Schedule/ScheduleCalendar/index.vue
  17. 0 0
      src/components/service/Schedule/ScheduleSwiper/index.vue
  18. 0 0
      src/components/service/Schedule/components/ScheduleWrapper.vue
  19. 0 0
      src/components/service/Schedule/index.vue
  20. 0 0
      src/components/service/Schedule/mixins/calendar.js
  21. 0 0
      src/components/service/Schedule/mixins/event.js
  22. 0 0
      src/components/service/Schedule/mixins/schedule.js
  23. 0 0
      src/components/service/external/DevicePlayer/index.vue
  24. 0 0
      src/components/service/external/camera/CameraDetail/index.vue
  25. 0 0
      src/components/service/external/camera/CameraPlayer/index.vue
  26. 0 0
      src/components/service/external/player.js
  27. 15 0
      src/icons/svg/v0/v0-alarm.svg
  28. 15 0
      src/icons/svg/v0/v0-full-screen.svg
  29. 15 0
      src/icons/svg/v0/v0-info.svg
  30. 23 0
      src/icons/svg/v0/v0-link.svg
  31. 18 0
      src/icons/svg/v0/v0-move.svg
  32. 17 3
      src/router/index.js
  33. 139 0
      src/views/dashboard/v0/AlarmInfo.vue
  34. 38 0
      src/views/dashboard/v0/AlarmLevel.vue
  35. 93 0
      src/views/dashboard/v0/AlarmRate.vue
  36. 152 0
      src/views/dashboard/v0/AlarmType.vue
  37. 255 0
      src/views/dashboard/v0/Box.vue
  38. 63 0
      src/views/dashboard/v0/Cards.vue
  39. 300 0
      src/views/dashboard/v0/DeviceCalender.vue
  40. 233 0
      src/views/dashboard/v0/DeviceInfo.vue
  41. 104 0
      src/views/dashboard/v0/DeviceSort.vue
  42. 37 0
      src/views/dashboard/v0/DeviceStatus.vue
  43. 57 0
      src/views/dashboard/v0/Header.vue
  44. 43 0
      src/views/dashboard/v0/LinkState.vue
  45. 107 0
      src/views/dashboard/v0/Map.vue
  46. 265 0
      src/views/dashboard/v0/MessageNotice.vue
  47. 247 0
      src/views/dashboard/v0/Record.vue
  48. 43 0
      src/views/dashboard/v0/api.js
  49. 300 0
      src/views/dashboard/v0/index.vue

binární
src/assets/v0/bg.png


+ 14 - 0
src/assets/v0/icon_position1.svg

@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="14px" height="19px" viewBox="0 0 14 19" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <title>icon_position备份 2</title>
+    <g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="大屏设备监控系统-单屏" transform="translate(-300.000000, -661.000000)">
+            <g id="地图位置" transform="translate(80.000000, 437.000000)">
+                <g id="icon_position备份-2" transform="translate(220.000000, 224.000000)">
+                    <ellipse id="椭圆形" fill="#333333" cx="7" cy="17" rx="4" ry="2"></ellipse>
+                    <path d="M11.9497475,2.05025253 C14.6212886,4.72179371 14.6820055,9.01549316 12.131898,11.7607688 L11.9497475,11.9497475 L7.70710678,16.1923882 C7.31658249,16.5829124 6.68341751,16.5829124 6.29289322,16.1923882 L2.05025253,11.9497475 L2.05025253,11.9497475 C-0.683417511,9.21607743 -0.683417511,4.78392257 2.05025253,2.05025253 C4.78392257,-0.683417511 9.21607743,-0.683417511 11.9497475,2.05025253 Z" id="路径" fill="#04A681"></path>
+                </g>
+            </g>
+        </g>
+    </g>
+</svg>

+ 14 - 0
src/assets/v0/icon_position2.svg

@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="14px" height="19px" viewBox="0 0 14 19" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <title>icon_position备份 4</title>
+    <g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="大屏设备监控系统-单屏" transform="translate(-314.000000, -552.000000)">
+            <g id="地图位置" transform="translate(80.000000, 437.000000)">
+                <g id="icon_position备份-4" transform="translate(234.000000, 115.000000)">
+                    <ellipse id="椭圆形" fill="#333333" cx="7" cy="17" rx="4" ry="2"></ellipse>
+                    <path d="M11.9497475,2.05025253 C14.6212886,4.72179371 14.6820055,9.01549316 12.131898,11.7607688 L11.9497475,11.9497475 L7.70710678,16.1923882 C7.31658249,16.5829124 6.68341751,16.5829124 6.29289322,16.1923882 L2.05025253,11.9497475 L2.05025253,11.9497475 C-0.683417511,9.21607743 -0.683417511,4.78392257 2.05025253,2.05025253 C4.78392257,-0.683417511 9.21607743,-0.683417511 11.9497475,2.05025253 Z" id="路径" fill="#F90C0C"></path>
+                </g>
+            </g>
+        </g>
+    </g>
+</svg>

binární
src/assets/v0/screen.png


+ 0 - 0
src/components/AutoImage/index.vue → src/components/common/AutoImage/index.vue


+ 0 - 0
src/components/card/MediaCard/index.vue → src/components/common/MediaCard/index.vue


+ 0 - 0
src/components/Permission/index.vue → src/components/common/Permission/index.vue


+ 0 - 0
src/components/Tabbar/index.vue → src/components/layout/Tabbar/index.vue


+ 0 - 0
src/components/Warning/index.vue → src/components/layout/Warning/index.vue


+ 0 - 0
src/components/EventPicker/index.vue → src/components/service/EventPicker/index.vue


+ 0 - 0
src/components/Schedule/ScheduleCalendar/EventItem.vue → src/components/service/Schedule/ScheduleCalendar/EventItem.vue


+ 0 - 0
src/components/Schedule/ScheduleCalendar/EventItemWeek.vue → src/components/service/Schedule/ScheduleCalendar/EventItemWeek.vue


+ 0 - 0
src/components/Schedule/ScheduleCalendar/PopList.vue → src/components/service/Schedule/ScheduleCalendar/PopList.vue


+ 0 - 0
src/components/Schedule/ScheduleCalendar/ScheduleCalendarMonth.vue → src/components/service/Schedule/ScheduleCalendar/ScheduleCalendarMonth.vue


+ 0 - 0
src/components/Schedule/ScheduleCalendar/ScheduleCalendarWeek.vue → src/components/service/Schedule/ScheduleCalendar/ScheduleCalendarWeek.vue


+ 0 - 0
src/components/Schedule/ScheduleCalendar/index.vue → src/components/service/Schedule/ScheduleCalendar/index.vue


+ 0 - 0
src/components/Schedule/ScheduleSwiper/index.vue → src/components/service/Schedule/ScheduleSwiper/index.vue


+ 0 - 0
src/components/Schedule/components/ScheduleWrapper.vue → src/components/service/Schedule/components/ScheduleWrapper.vue


+ 0 - 0
src/components/Schedule/index.vue → src/components/service/Schedule/index.vue


+ 0 - 0
src/components/Schedule/mixins/calendar.js → src/components/service/Schedule/mixins/calendar.js


+ 0 - 0
src/components/Schedule/mixins/event.js → src/components/service/Schedule/mixins/event.js


+ 0 - 0
src/components/Schedule/mixins/schedule.js → src/components/service/Schedule/mixins/schedule.js


+ 0 - 0
src/components/external/DevicePlayer/index.vue → src/components/service/external/DevicePlayer/index.vue


+ 0 - 0
src/components/external/camera/CameraDetail/index.vue → src/components/service/external/camera/CameraDetail/index.vue


+ 0 - 0
src/components/external/camera/CameraPlayer/index.vue → src/components/service/external/camera/CameraPlayer/index.vue


+ 0 - 0
src/components/external/player.js → src/components/service/external/player.js


+ 15 - 0
src/icons/svg/v0/v0-alarm.svg

@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="19px" height="19px" viewBox="0 0 19 19" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <title>icon_ararm</title>
+    <g id="页面-1" stroke="none">
+        <g id="大屏设备监控系统-单屏" transform="translate(-1467.000000, -618.000000)">
+            <g id="预警等级情况" transform="translate(1436.000000, 437.000000)">
+                <g id="提示等级" transform="translate(16.000000, 169.000000)">
+                    <g id="icon_alert3" transform="translate(15.000000, 12.000000)">
+                        <path d="M9.65685425,7 C11.8659932,7 13.6568542,8.790861 13.6568542,11 L13.656,15 L14.6568542,15 C15.209139,15 15.6568542,15.4477153 15.6568542,16 C15.6568542,16.5522847 15.209139,17 14.6568542,17 L4.65685425,17 C4.1045695,17 3.65685425,16.5522847 3.65685425,16 C3.65685425,15.4477153 4.1045695,15 4.65685425,15 L5.656,15 L5.65685425,11 C5.65685425,8.790861 7.44771525,7 9.65685425,7 Z M16.0208153,4.63603897 C16.4113396,5.02656326 16.4113396,5.65972824 16.0208153,6.05025253 L15.3137085,6.75735931 C14.9231842,7.1478836 14.2900192,7.1478836 13.8994949,6.75735931 C13.5089706,6.36683502 13.5089706,5.73367004 13.8994949,5.34314575 L14.6066017,4.63603897 C14.997126,4.24551468 15.630291,4.24551468 16.0208153,4.63603897 Z M4.70710678,4.63603897 L5.41421356,5.34314575 C5.80473785,5.73367004 5.80473785,6.36683502 5.41421356,6.75735931 C5.02368927,7.1478836 4.39052429,7.1478836 4,6.75735931 L3.29289322,6.05025253 C2.90236893,5.65972824 2.90236893,5.02656326 3.29289322,4.63603897 C3.68341751,4.24551468 4.31658249,4.24551468 4.70710678,4.63603897 Z M9.65685425,2 C10.209139,2 10.6568542,2.44771525 10.6568542,3 L10.6568542,4 C10.6568542,4.55228475 10.209139,5 9.65685425,5 C9.1045695,5 8.65685425,4.55228475 8.65685425,4 L8.65685425,3 C8.65685425,2.44771525 9.1045695,2 9.65685425,2 Z"></path>
+                    </g>
+                </g>
+            </g>
+        </g>
+    </g>
+</svg>

+ 15 - 0
src/icons/svg/v0/v0-full-screen.svg

@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <title>icon_full_screen</title>
+    <g id="页面-1">
+        <g id="大屏设备监控系统-单屏" transform="translate(-1365.000000, -626.000000)">
+            <g id="视频" transform="translate(508.000000, 132.000000)">
+                <g id="1" transform="translate(16.000000, 16.000000)">
+                    <g id="icon_full_screen" transform="translate(841.000000, 478.000000)">
+                        <path d="M10,3 C10.5522847,3 11,3.44771525 11,4 C11,4.55228475 10.5522847,5 10,5 L6.414,5 L10.5355339,9.12132034 C10.9260582,9.51184464 10.9260582,10.1450096 10.5355339,10.5355339 C10.1450096,10.9260582 9.51184464,10.9260582 9.12132034,10.5355339 L4.999,6.413 L5,10 C5,10.5522847 4.55228475,11 4,11 C3.44771525,11 3,10.5522847 3,10 L3,4 C3,3.44771525 3.44771525,3 4,3 L10,3 Z M10,21 C10.5522847,21 11,20.5522847 11,20 C11,19.4477153 10.5522847,19 10,19 L6.414,19 L10.5355339,14.8786797 C10.9260582,14.4881554 10.9260582,13.8549904 10.5355339,13.4644661 C10.1450096,13.0739418 9.51184464,13.0739418 9.12132034,13.4644661 L4.999,17.587 L5,14 C5,13.4477153 4.55228475,13 4,13 C3.44771525,13 3,13.4477153 3,14 L3,20 C3,20.5522847 3.44771525,21 4,21 L10,21 Z M14,3 C13.4477153,3 13,3.44771525 13,4 C13,4.55228475 13.4477153,5 14,5 L17.586,5 L13.4644661,9.12132034 C13.0739418,9.51184464 13.0739418,10.1450096 13.4644661,10.5355339 C13.8549904,10.9260582 14.4881554,10.9260582 14.8786797,10.5355339 L19.001,6.413 L19,10 C19,10.5522847 19.4477153,11 20,11 C20.5522847,11 21,10.5522847 21,10 L21,4 C21,3.44771525 20.5522847,3 20,3 L14,3 Z M14,21 C13.4477153,21 13,20.5522847 13,20 C13,19.4477153 13.4477153,19 14,19 L17.586,19 L13.4644661,14.8786797 C13.0739418,14.4881554 13.0739418,13.8549904 13.4644661,13.4644661 C13.8549904,13.0739418 14.4881554,13.0739418 14.8786797,13.4644661 L19.001,17.587 L19,14 C19,13.4477153 19.4477153,13 20,13 C20.5522847,13 21,13.4477153 21,14 L21,20 C21,20.5522847 20.5522847,21 20,21 L14,21 Z"></path>
+                    </g>
+                </g>
+            </g>
+        </g>
+    </g>
+</svg>

+ 15 - 0
src/icons/svg/v0/v0-info.svg

@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <title>icon_info</title>
+    <g id="页面-1" stroke="none">
+        <g id="大屏设备监控系统-单屏" transform="translate(-1326.000000, -626.000000)">
+            <g id="视频" transform="translate(508.000000, 132.000000)">
+                <g id="1" transform="translate(16.000000, 16.000000)">
+                    <g id="icon_info" transform="translate(802.000000, 478.000000)">
+                        <path d="M12,2 C17.5228475,2 22,6.4771525 22,12 C22,17.5228475 17.5228475,22 12,22 C6.4771525,22 2,17.5228475 2,12 C2,6.4771525 6.4771525,2 12,2 Z M12,4 C7.581722,4 4,7.581722 4,12 C4,16.418278 7.581722,20 12,20 C16.418278,20 20,16.418278 20,12 C20,7.581722 16.418278,4 12,4 Z M12,11 C12.5522847,11 13,11.4477153 13,12 L13,16 C13,16.5522847 12.5522847,17 12,17 C11.4477153,17 11,16.5522847 11,16 L11,12 C11,11.4477153 11.4477153,11 12,11 Z M12,7 C12.5522847,7 13,7.44771525 13,8 C13,8.55228475 12.5522847,9 12,9 C11.4477153,9 11,8.55228475 11,8 C11,7.44771525 11.4477153,7 12,7 Z"></path>
+                    </g>
+                </g>
+            </g>
+        </g>
+    </g>
+</svg>

+ 23 - 0
src/icons/svg/v0/v0-link.svg

@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <title>icon_link</title>
+    <g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="大屏设备监控系统-单屏" transform="translate(-1286.000000, -626.000000)" stroke="currentColor" stroke-width="2">
+            <g id="视频" transform="translate(508.000000, 132.000000)">
+                <g id="1" transform="translate(16.000000, 16.000000)">
+                    <g id="icon_link" transform="translate(762.000000, 478.000000)">
+                        <g transform="translate(1.000000, 2.000000)">
+                            <circle id="椭圆形" cx="3.5" cy="16.5" r="2.5"></circle>
+                            <circle id="椭圆形备份" cx="6.5" cy="5.5" r="2.5"></circle>
+                            <circle id="椭圆形备份-2" cx="14.5" cy="14.5" r="2.5"></circle>
+                            <circle id="椭圆形备份-3" cx="17.5" cy="3.5" r="2.5"></circle>
+                            <line x1="4" y1="14" x2="6" y2="8" id="路径-3"></line>
+                            <line x1="8" y1="7" x2="13" y2="13" id="路径-4"></line>
+                            <line x1="15" y1="12" x2="17" y2="6" id="路径-5"></line>
+                        </g>
+                    </g>
+                </g>
+            </g>
+        </g>
+    </g>
+</svg>

+ 18 - 0
src/icons/svg/v0/v0-move.svg

@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="20px" height="20px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <title>icon_move</title>
+    <g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round">
+        <g id="大屏设备监控系统-单屏" transform="translate(-1147.000000, -676.000000)" stroke="#9EA9CD" stroke-width="2">
+            <g id="视频" transform="translate(508.000000, 132.000000)">
+                <g id="设备排序" transform="translate(484.000000, 538.000000)">
+                    <g id="编组-5" transform="translate(155.000000, 6.000000)">
+                        <g id="icon_move" transform="translate(1.000000, 2.000000)">
+                            <path d="M1,5 L16.618975,5 C16.8951174,5 17.118975,4.77614237 17.118975,4.5 C17.118975,4.35161008 17.0530635,4.21088627 16.9390672,4.11588936 L12,0 L12,0" id="路径-8"></path>
+                            <path d="M0,16 L15.618975,16 C15.8951174,16 16.118975,15.7761424 16.118975,15.5 C16.118975,15.3516101 16.0530635,15.2108863 15.9390672,15.1158894 L11,11 L11,11" id="路径-8备份" transform="translate(8.500000, 13.500000) scale(-1, -1) translate(-8.500000, -13.500000) "></path>
+                        </g>
+                    </g>
+                </g>
+            </g>
+        </g>
+    </g>
+</svg>

+ 17 - 3
src/router/index.js

@@ -43,12 +43,12 @@ export const constantRoutes = [
 export const asyncRoutes = [
   {
     path: '/',
-    redirect: '/dashboard',
+    redirect: '/home',
     component: Layout,
     children: [
       {
-        name: 'dashboard',
-        path: 'dashboard',
+        name: 'home',
+        path: 'home',
         component: () => import('@/views/dashboard/index'),
         meta: { title: '首页', icon: 'home' }
       },
@@ -222,6 +222,20 @@ export const asyncRoutes = [
       }
     ]
   },
+  {
+    path: '/dashboard',
+    component: Solo,
+    meta: { title: '大屏展示', icon: 'dm' },
+    access: [Access.MANAGE_TENANTS, Access.MANAGE_TENANT],
+    children: [
+      {
+        name: 'dashboard-v0',
+        path: 'v0',
+        component: () => import('@/views/dashboard/v0/index'),
+        meta: { title: 'V0' }
+      }
+    ]
+  },
   {
     path: '/em',
     component: Layout,

+ 139 - 0
src/views/dashboard/v0/AlarmInfo.vue

@@ -0,0 +1,139 @@
+<template>
+  <box
+    title="消息通知详情"
+    can-full-screen
+    fullscreen
+    v-on="$listeners"
+  >
+    <div class="info l-flex--col">
+      <div class="l-flex--row center info__deviceName">
+        {{ alarm.deviceName }}
+      </div>
+      <div
+        class="l-flex--row center"
+        :style="colorStyle"
+      >
+        <svg-icon
+          class="alarm__icon"
+          icon-class="v0-alarm"
+        />
+        <div class="alarm__name">
+          {{ level }}
+        </div>
+      </div>
+      <div class="l-flex--row center alarm__type">
+        {{ alarm.type }}
+      </div>
+      <div class="l-flex--row c-sibling-item--v info__row">
+        <div class="l-flex__fill l-flex--row center row__label">发生时间</div>
+        <div class="l-flex__fill">{{ alarm.happenTime }}</div>
+        <div class="l-flex__fill l-flex--row center row__label">截图</div>
+        <div class="l-flex__fill">
+          <auto-image
+            v-if="alarm.asset"
+            :src="alarm.asset.url"
+            broken="image-broken"
+            class="o-image u-pointer"
+            style="width: 128px; height: 72px;"
+            @click.native="onView"
+          />
+          <span v-else>-</span>
+        </div>
+      </div>
+      <div class="l-flex--row c-sibling-item--v far info__row">
+        <div class="l-flex__fill l-flex--row center row__label">处理方式</div>
+        <div class="l-flex__fill">{{ alarm.handle }}</div>
+        <div class="l-flex__fill l-flex--row center row__label">处理结果</div>
+        <div class="l-flex__fill">{{ alarm.status.label }}</div>
+      </div>
+      <div class="info__tip l-flex--row center">
+        如有疑问,请及时联系管理员,谢谢。
+      </div>
+    </div>
+    <preview-dialog
+      ref="previewDialog"
+      append-to-body
+    />
+  </box>
+</template>
+
+<script>
+import Box from './Box'
+
+export default {
+  name: 'AlarmInfo',
+  components: {
+    Box
+  },
+  props: {
+    alarm: {
+      type: Object,
+      required: true
+    }
+  },
+  computed: {
+    level () {
+      return ['普通等级', '提示等级', '紧急等级'][this.alarm?.level]
+    },
+    colorStyle () {
+      return this.alarm
+        ? {
+          color: ['#04A681', '#FFA000', '#F40645'][this.alarm.level]
+        }
+        : null
+    }
+  },
+  methods: {
+    onView () {
+      this.$refs.previewDialog.show(this.alarm.asset)
+    }
+  }
+}
+</script>
+<style lang="scss" scoped>
+.info {
+  &__row {
+    height: 80px;
+    line-height: 1;
+    background-color: #313a5a;
+
+    .row__label {
+      color: #9ea9cd;
+    }
+  }
+
+  &__deviceName {
+    line-height: 72px;
+    font-size: 48px;
+    font-weight: bold;
+    margin: 50px 0 69px 0;
+  }
+
+  .alarm {
+    &__icon {
+      margin-right: 8px;
+      font-size: 32px;
+    }
+
+    &__type {
+      font-size: 40px;
+      line-height: 60px;
+      font-weight: bold;
+      margin: 40px 0 80px 0;
+    }
+
+    &__name {
+      font-size: 32px;
+      font-weight: 500;
+      line-height: 48px;
+    }
+  }
+
+  &__tip {
+    color: #9ea9cd;
+    line-height: 42px;
+    font-size: 28px;
+    margin-top: 80px;
+  }
+}
+</style>

+ 38 - 0
src/views/dashboard/v0/AlarmLevel.vue

@@ -0,0 +1,38 @@
+<template>
+  <box title="预警等级情况">
+    <cards :items="itemList" />
+  </box>
+</template>
+
+<script>
+import Box from './Box'
+import Cards from './Cards'
+
+export default {
+  name: 'AlarmLevel',
+  components: {
+    Box,
+    Cards
+  },
+  props: {
+    items: {
+      type: Array,
+      required: true
+    }
+  },
+  computed: {
+    itemList () {
+      const colors = ['#0AB4FF', '#F40645', '#FFA000', '#04A681']
+
+      return this.items.map((item, index) => {
+        return {
+          ...item,
+          icon: 'v0-alarm',
+          style: { color: colors[index] }
+        }
+      })
+    }
+  }
+}
+</script>
+

+ 93 - 0
src/views/dashboard/v0/AlarmRate.vue

@@ -0,0 +1,93 @@
+<template>
+  <box title="各大屏预警率占比">
+    <div class="l-flex__fill l-flex--col">
+      <div
+        ref="canvas"
+        class="l-flex__fill"
+      />
+    </div>
+  </box>
+</template>
+
+<script>
+import { getDeviceExceptionRanking } from './api'
+import * as echarts from 'echarts'
+import Box from './Box'
+
+export default {
+  components: {
+    Box
+  },
+  mounted () {
+    this.getDeviceExceptionRanking()
+    this.$timer = setInterval(this.getDeviceExceptionRanking, 30000)
+  },
+  beforeDestroy () {
+    clearInterval(this.$timer)
+  },
+  methods: {
+    getDeviceExceptionRanking () {
+      getDeviceExceptionRanking().then(({ data: { rankItemList } }) => {
+        rankItemList = rankItemList.map(i => {
+          return { name: i.deviceName, value: Number(i.errorCount) }
+        })
+        rankItemList.sort((a, b) => b.value - a.value)
+        if (rankItemList.length > 7) {
+          const otherArray = rankItemList.splice(7)
+          rankItemList.push({
+            name: '其它',
+            value: otherArray.reduce((total, cur) => cur.value + total, 0)
+          })
+        }
+        this.initEchart(rankItemList.length ? rankItemList : [{ name: '其它', value: 0 }])
+      })
+    },
+    initEchart (rankItemList) {
+      if (!this.$echarts) {
+        this.$echarts = echarts.init(this.$refs.canvas)
+      }
+      const option = {
+        legend: {
+          right: 0,
+          top: 'center',
+          orient: 'vertical',
+          textStyle: {
+            color: '#ffffff'
+          },
+          formatter (name) {
+            return name.length > 8 ? `${name.slice(0, 8)}...` : name
+          },
+          tooltip: {
+            show: true
+          }
+        },
+        series: [
+          {
+            name: 'Chart',
+            type: 'pie',
+            radius: [15, 70],
+            center: [110, '50%'],
+            minAngle: 30,
+            roseType: 'radius',
+            label: {
+              show: true,
+              color: '#ffffff',
+              formatter: '{d}%\n\n',
+              padding: [0, -30]
+            },
+            labelLine: {
+              length: 5,
+              length2: 25
+            },
+            data: rankItemList
+          }
+        ],
+        tooltip: {
+          formatter: '名称:{b}<br />占比:{d}%'
+        }
+      }
+      this.$echarts.setOption(option)
+    }
+  }
+}
+</script>

+ 152 - 0
src/views/dashboard/v0/AlarmType.vue

@@ -0,0 +1,152 @@
+<template>
+  <box title="设备告警类型统计">
+    <div class="l-flex__fill l-flex--col">
+      <div
+        ref="canvas"
+        class="l-flex__fill"
+      />
+    </div>
+  </box>
+</template>
+
+<script>
+import { getDeviceExceptionTypeStatistics } from './api'
+import * as echarts from 'echarts'
+import Box from './Box'
+
+export default {
+  name: 'AlarmType',
+  components: {
+    Box
+  },
+  mounted () {
+    this.getDeviceExceptionTypeStatistics()
+    this.$timer = setInterval(this.getDeviceExceptionTypeStatistics, 30000)
+  },
+  beforeDestroy () {
+    clearInterval(this.$timer)
+  },
+  methods: {
+    getDeviceExceptionTypeStatistics () {
+      getDeviceExceptionTypeStatistics().then(({ data }) => {
+        data = data.map(i => {
+          return { name: i.typeName, value: Number(i.count) }
+        })
+        data.sort((a, b) => b.value - a.value)
+        if (data.length > 5) {
+          const otherArray = data.splice(5)
+          data.push({
+            name: '其它',
+            value: otherArray.reduce((total, cur) => cur.value + total, 0)
+          })
+        }
+        this.$nextTick(() => {
+          this.initEchart(data.length ? data : [{ name: '告警', value: 0 }])
+        })
+      })
+    },
+    initEchart (typeList) {
+      const xdata = typeList.map(i => `${i.name}`)
+      const ydata = typeList.map(i => i.value)
+      if (!this.$echarts) {
+        this.$echarts = echarts.init(this.$refs.canvas)
+      }
+      this.$echarts.setOption({
+        title: {
+          text: '',
+          textStyle: {
+            color: '#fff',
+            fontWeight: 'bold'
+          }
+        },
+        xAxis: {
+          name: '(类型)',
+          type: 'category',
+          data: xdata,
+          nameLocation: 'middle',
+          nameGap: 8,
+          nameTextStyle: {
+            color: '#9EA9CD',
+            padding: [0, 0, 0, 332]
+          },
+          splitLine: {
+            show: false
+          },
+          axisLine: {
+            lineStyle: {
+              color: '#313A5A'
+            }
+          },
+          axisLabel: {
+            interval: 0,
+            width: 58,
+            color: '#9EA9CD',
+            overflow: 'truncate'
+          },
+          axisTick: {
+            show: false
+          }
+        },
+        yAxis: {
+          name: '(次数)',
+          type: 'value',
+          minInterval: 1,
+          splitLine: {
+            lineStyle: {
+              color: '#313A5A'
+            }
+          },
+          nameTextStyle: {
+            color: '#9EA9CD',
+            padding: [0, 40, 0, 0]
+          },
+          axisLine: {
+            show: false,
+            lineStyle: {
+              color: '#4779BC'
+            }
+          },
+          axisLabel: {
+            color: '#9EA9CD'
+          }
+        },
+        grid: {
+          top: '40',
+          bottom: '20'
+        },
+        series: [
+          {
+            data: ydata,
+            type: 'bar',
+            barWidth: '50%',
+            itemStyle: {
+              color: '#12A3FF'
+            },
+            select: {
+              itemStyle: {
+                color: 'rgb(0, 234, 255)'
+              }
+            },
+            label: {
+              show: true,
+              color: '#fff',
+              position: 'top'
+            }
+          }
+        ],
+        emphasis: {
+          itemStyle: {
+            color: '#FFCA1A',
+            borderWidth: 1,
+            borderColor: '#FF2222',
+            borderType: 'solid'
+          }
+        },
+        tooltip: {
+          formatter: '类型:{b}<br />次数:{c}'
+        }
+      })
+    }
+  }
+}
+</script>

+ 255 - 0
src/views/dashboard/v0/Box.vue

@@ -0,0 +1,255 @@
+<template>
+  <div
+    class="l-flex--col c-box"
+    :class="{ fullscreen }"
+  >
+    <div class="c-box__border--left" />
+    <div class="c-box__border--right" />
+    <div class="l-flex__fill l-flex--col c-box__main has-bg">
+      <div
+        v-if="title"
+        class="l-flex__none c-box__header l-flex--row"
+      >
+        <div class="u-bold l-flex__fill">{{ title }}</div>
+        <div class="header__decoration">
+          <div class="decoration__bg" />
+          <div class="decoration__bg--under" />
+        </div>
+        <i
+          v-if="canFullScreen"
+          class="c-box__button has-bg u-pointer"
+          @click.stop="onToggle"
+        />
+        <div
+          v-else
+          class="header__rects l-flex--row"
+        >
+          <div class="header__rect l-flex__fill" />
+          <div class="header__rect l-flex__fill" />
+          <div class="header__rect l-flex__fill" />
+        </div>
+      </div>
+      <div class="l-flex__fill l-flex--col has-content-padding">
+        <slot />
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'Box',
+  props: {
+    title: {
+      type: String,
+      default: ''
+    },
+    fullscreen: {
+      type: [Boolean, String],
+      default: false
+    },
+    canFullScreen: {
+      type: [Boolean, String],
+      default: false
+    }
+  },
+  methods: {
+    onToggle () {
+      this.$emit('fullscreenChange', !this.fullscreen)
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.c-box {
+  position: relative;
+  height: 100%;
+  width: 100%;
+  color: #fff;
+  border: 1px solid rgba(#fff, 0.3);
+
+  &__border {
+    &--left {
+      left: 0;
+      position: absolute;
+      height: 100%;
+      width: 2px;
+      background-image: linear-gradient(
+        to bottom,
+        #fff 0,
+        #fff 8px,
+        transparent 8px,
+        transparent 12px,
+        #2956f0 16px,
+        #2956f0 24px,
+        #2956f0 32px,
+        transparent 36px,
+        transparent 42px,
+        #fff 42px,
+        #fff 50px,
+        transparent 50px,
+        transparent calc(100% - 8px),
+        #fff calc(100% - 8px),
+        #fff 100%
+      );
+    }
+
+    &--right {
+      right: 0;
+      position: absolute;
+      height: 100%;
+      width: 2px;
+      background-image: linear-gradient(
+        to bottom,
+        #fff 0,
+        #fff 8px,
+        transparent 8px,
+        transparent calc(100% - 8px),
+        #fff calc(100% - 8px),
+        #fff 100%
+      );
+    }
+  }
+
+  &.fullscreen {
+    position: absolute;
+    width: 1592px;
+    height: 964px;
+    top: 50%;
+    left: 50%;
+    border-bottom: none;
+    transform: translate(-50%, -50%);
+    overflow: visible;
+    z-index: 999;
+
+    &::before {
+      content: "";
+      position: absolute;
+      top: -58px;
+      left: -164px;
+      width: 1920px;
+      height: 1080px;
+      background-color: rgba(#000, 0.85);
+      z-index: -1;
+    }
+    .c-box__border--left {
+      background-image: linear-gradient(
+        to bottom,
+        #fff 0,
+        #fff 8px,
+        transparent 8px,
+        transparent 12px,
+        #2956f0 16px,
+        #2956f0 30px,
+        #2956f0 44px,
+        transparent 48px,
+        transparent 54px,
+        #fff 54px,
+        #fff 62px,
+        transparent 62px,
+        transparent calc(100% - 8px),
+        #fff calc(100% - 8px),
+        #fff 100%
+      );
+    }
+
+    .header__decoration {
+      margin-right: 20px;
+    }
+
+    .c-box {
+      &__main {
+        background-color: rgba(#1d274b, 0.85);
+        overflow: hidden;
+        font-size: 28px;
+      }
+
+      &__header {
+        padding: 0 16px;
+        position: relative;
+        height: 52px;
+        line-height: 70px;
+        font-size: 28px;
+      }
+
+      &__button {
+        top: 6px;
+        right: 10px;
+        width: 20px;
+        height: 20px;
+        background-image: url("~@/assets/icon_narrow.png");
+      }
+    }
+  }
+
+  &__main {
+    background-color: rgba(#1d274b, 0.85);
+    overflow: hidden;
+  }
+
+  &__header {
+    margin: 0 16px 10px;
+    position: relative;
+    height: 40px;
+    line-height: 40px;
+    font-size: 18px;
+    border-bottom: 1px solid rgba(#fff, 0.1);
+  }
+  .header__decoration {
+    position: relative;
+    width: 160px;
+    height: 8px;
+
+    .decoration__bg {
+      width: 100%;
+      height: 100%;
+      background: linear-gradient(
+        270deg,
+        rgba(34, 51, 108, 0) 0%,
+        #22336c 100%
+      );
+    }
+
+    .decoration__bg--under {
+      position: absolute;
+      width: 100%;
+      height: 100%;
+      top: 0;
+      background: repeating-linear-gradient(
+        120deg,
+        rgba(#1d274b, 0.85),
+        rgba(#1d274b, 0.85) 4px,
+        transparent 4px,
+        transparent 8px
+      );
+    }
+  }
+
+  .header__rect {
+    width: 16px;
+    height: 8px;
+    &:nth-child(1) {
+      background-color: #8ca6ff;
+    }
+    &:nth-child(2) {
+      margin-left: 4px;
+    }
+    background-color: #5279ff;
+    &:nth-child(3) {
+      margin-left: 4px;
+      background-color: #2556ff;
+    }
+  }
+
+  &__button {
+    width: 12px;
+    height: 12px;
+    background-image: url("~@/assets/icon_enlarge.png");
+  }
+}
+
+.has-content-padding {
+  padding: 0 16px 16px;
+}
+</style>

+ 63 - 0
src/views/dashboard/v0/Cards.vue

@@ -0,0 +1,63 @@
+<template>
+  <div class="c-dashoboard-cards--v0">
+    <div
+      v-for="(item, index) in items"
+      :key="index"
+      class="c-dashoboard-cards--v0__item has-padding"
+      :style="item.style"
+    >
+      <div class="l-flex--row c-dashoboard-cards--v0__header">
+        <svg-icon
+          class="c-dashoboard-cards--v0__icon"
+          :icon-class="item.icon"
+        />
+        {{ item.label }}
+      </div>
+      <div class="c-dashoboard-cards--v0__value u-bold u-ellipsis u-text-center">{{ item.value }}</div>
+    </div>
+  </div>
+</template>
+
+<script>
+
+export default {
+  name: 'Cards',
+  props: {
+    items: {
+      type: Array,
+      required: true
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.c-dashoboard-cards--v0 {
+  display: grid;
+  grid-template-columns: 1fr 1fr;
+  grid-gap: $spacing $spacing;
+
+  &__item {
+    min-width: 0;
+    height: 96px;
+    background-color: rgba(#060920, 0.3);
+  }
+
+  &__header {
+    margin-bottom: 10px;
+    height: 14px;
+    font-size: 14px;
+    line-height: 1;
+  }
+
+  &__icon {
+    margin-right: 8px;
+    font-size: 19px;
+  }
+
+  &__value {
+    max-width: 100%;
+    font-size: 40px;
+  }
+}
+</style>

+ 300 - 0
src/views/dashboard/v0/DeviceCalender.vue

@@ -0,0 +1,300 @@
+<template>
+  <box title="各大屏当前播放节目">
+    <div class="l-flex__fill l-flex--col c-calender">
+      <template v-if="tableData.length">
+        <div class="l-flex__none l-flex--row c-calender__header">
+          <div class="col__deviceName">设备名称</div>
+          <div class="col__programName">节目名称</div>
+          <div class="col__calender">时间排期</div>
+          <div class="col__createTime">更新时间</div>
+        </div>
+        <div class="l-flex__fill u-relative">
+          <div class="c-calender__list">
+            <vue-seamless-scroll
+              :data="tableData"
+              :class-option="classOption"
+            >
+              <div
+                v-for="item in tableData"
+                :key="item.id"
+                class="l-flex--row c-calender__item"
+              >
+                <div class="col__deviceName">
+                  <auto-text
+                    class="u-text-center"
+                    :text="item.deviceName "
+                  />
+                </div>
+                <div class="col__programName">
+                  <auto-text
+                    class="u-text-center"
+                    :text="item.programName"
+                  />
+                </div>
+                <div class=" col__calender">
+                  <auto-text
+                    class="u-text-center"
+                    :text=" item.calender "
+                  />
+                </div>
+                <div class=" col__createTime">
+                  <auto-text
+                    class="u-text-center"
+                    :text=" item.updateTime "
+                  />
+                </div>
+              </div>
+            </vue-seamless-scroll>
+          </div>
+        </div>
+      </template>
+      <status-wrapper v-else />
+    </div>
+  </box>
+</template>
+
+<script>
+import { getTimelines } from './api'
+import { EventFreq } from '@/constant'
+import { parseTime } from '@/utils'
+import {
+  isHit,
+  getNearestHitDate,
+  getFinishDate,
+  getStartDate,
+  toDate
+} from '@/utils/event'
+import vueSeamlessScroll from '@/views/device/detail/dashboard/vue-seamless-scroll.min.js'
+import Box from './Box'
+
+export default {
+  name: 'DeviceCalender',
+  components: {
+    Box,
+    vueSeamlessScroll
+  },
+  props: {
+    deviceList: {
+      type: Array,
+      required: true
+    }
+  },
+  data () {
+    return {
+      classOption: {
+        step: 0.5,
+        hoverStop: false
+      },
+      tableData: []
+    }
+  },
+  watch: {
+    deviceList: {
+      handler () {
+        if (this.deviceList.length) {
+          this.getTimelines()
+        } else {
+          this.tableData = []
+        }
+      },
+      immediate: true
+    }
+  },
+  methods: {
+    getIcon (index) {
+      return {
+        backgroundImage: `url("${require(`@/assets/v0/icon_alert${index}.svg`)}")`
+      }
+    },
+    getTimelines () {
+      getTimelines(this.deviceList.map(i => i.id), { custom: true }).then(data => {
+        const timeline = []
+        for (const event of data) {
+          let deviceName = ''
+          for (const device of this.deviceList) {
+            if (device.id === event.deviceId) {
+              deviceName = device.name
+              break
+            }
+          }
+          if (!event.eventDetail.length) { continue }
+          const temp = []
+          for (const item of event.eventDetail) {
+            temp.push({ ...item, deviceName, updateTime: event.updateTime })
+          }
+          timeline.push(temp)
+        }
+        this.onUpdate(timeline)
+      })
+    },
+    onUpdate (items) {
+      if (items.length) {
+        const now = new Date()
+        const timeline = items.map(events => events
+          .filter(({ until }) => !until || now < new Date(until))
+          .sort((a, b) => {
+            if (b.priority === a.priority) {
+              return toDate(a.start) - toDate(b.start)
+            }
+            return b.priority - a.priority
+          }))
+        for (const item of timeline) {
+          this.checkTimeline(item)
+        }
+      }
+    },
+    checkTimeline (timeline) {
+      if (!timeline.length) { return }
+      const now = new Date()
+      let current = null
+      let currentEndDate = null
+      let next = null
+      if (!current) {
+        for (let i = 0; i < timeline.length; i++) {
+          const event = timeline[i]
+          if (!current && isHit(event, now)) {
+            current = event
+            break
+          }
+        }
+      }
+      currentEndDate = current && getFinishDate(current, now)
+      let nextStartDate = null
+      for (let i = 0; i < timeline.length; i++) {
+        const event = timeline[i]
+        if (!current || currentEndDate || event.priority > current.priority) {
+          const rangeStart =
+            !current || event.priority > current.priority
+              ? now
+              : currentEndDate || now
+          if (!nextStartDate || rangeStart < nextStartDate) {
+            const hit = getNearestHitDate(event, rangeStart, nextStartDate)
+            if (hit && (!next || hit < nextStartDate)) {
+              next = event
+              nextStartDate = hit
+            }
+          }
+        }
+      }
+      if (
+        nextStartDate
+        && (!currentEndDate || currentEndDate > nextStartDate)
+      ) {
+        currentEndDate = nextStartDate
+      }
+      if (current) {
+        this.tableData.push(
+          this.getEventInfo(current, getStartDate(current, now), currentEndDate)
+        )
+      }
+    },
+    getEventInfo (event, startDate, endDate) {
+      const { freq } = event
+      const rawData = {
+        startDate: parseTime(startDate, '{y}.{m}.{d}'),
+        startTime: parseTime(startDate, '{h}:{i}:{s}'),
+        endDate: endDate ? parseTime(endDate, '{y}.{m}.{d}') : '',
+        endTime: endDate ? parseTime(endDate, '{h}:{i}:{s}') : ''
+      }
+      let calender = ''
+      if (freq === EventFreq.ONCE) {
+        calender = `${rawData.startDate} ${rawData.startTime}-${
+          rawData.endDate ? `-${rawData.endDate}` : ''
+        } ${rawData.endTime}`
+      } else if (freq === EventFreq.WEEKLY) {
+        calender = `每周:${rawData.startTime}${
+          rawData.endTime ? `-${rawData.startTime}` : ''
+        }`
+      }
+      return {
+        deviceName: event.deviceName,
+        programName: event.target.name,
+        calender,
+        updateTime: event.updateTime,
+        startDate: parseTime(startDate, '{y}.{m}.{d}'),
+        startTime: parseTime(startDate, '{h}:{i}:{s}'),
+        endDate: endDate ? parseTime(endDate, '{y}.{m}.{d}') : '',
+        endTime: endDate ? parseTime(endDate, '{h}:{i}:{s}') : ''
+      }
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.c-calender {
+  &__header {
+    color: #9ea9cd;
+    background-color: #313a5a;
+  }
+
+  &__list {
+    position: absolute;
+    top: 0;
+    left: 0;
+    width: 100%;
+    height: 100%;
+    overflow: hidden;
+  }
+
+  &__item {
+    color: #ffffff;
+    border-bottom: 1px solid #313a5a;
+
+    &.alarm {
+      color: #f40645;
+    }
+  }
+
+  &__header,
+  &__item {
+    font-size: 12px;
+    line-height: 36px;
+    text-align: center;
+  }
+
+  .alarm__icon {
+    width: 19px;
+    height: 19px;
+    transform: translateY(-2px);
+  }
+
+  .col {
+    &__deviceName {
+      flex: 1;
+      min-width: 0;
+    }
+
+    &__programName {
+      flex: 1;
+      min-width: 0;
+    }
+
+    &__calender {
+      flex: 2;
+      min-width: 0;
+    }
+
+    &__createTime {
+      flex: 1;
+      min-width: 0;
+      .o-button {
+        min-width: 32px;
+        height: 20px;
+        padding: 0 4px;
+        font-size: 12px;
+        border-radius: 2px;
+        background-color: #2956f0;
+      }
+      &.alarm {
+        background-color: #f40645;
+        color: #ffffff;
+      }
+    }
+  }
+
+  ::v-deep.el-empty {
+    padding: 0;
+  }
+}
+</style>

+ 233 - 0
src/views/dashboard/v0/DeviceInfo.vue

@@ -0,0 +1,233 @@
+<template>
+  <box
+    title="信息"
+    can-full-screen
+    fullscreen
+    v-on="$listeners"
+  >
+    <div
+      v-for="(row, index) in rows"
+      :key="index"
+      class="l-flex--row c-sibling-item--v far c-info__row"
+    >
+      <div
+        v-for="item in row"
+        :key="item.key"
+        class="l-flex--row l-flex__fill"
+      >
+        <div class="l-flex__fill l-flex--row center row__label">
+          {{ item.label }}
+        </div>
+        <div class="l-flex__fill">{{ device[item.key] }}</div>
+      </div>
+    </div>
+
+    <div class="l-flex--row l-flex__fill center">
+      <button
+        class="l-flex__none  o-button"
+        @click="onSwitch(true)"
+      >
+        <i class="o-button__icon el-icon-switch-button" />
+        开机
+      </button>
+      <button
+        class="l-flex__none  o-button"
+        @click="onSwitch(false)"
+      >
+        <i class="o-button__icon el-icon-switch-button" />
+        关机
+      </button>
+      <button
+        class="l-flex__none  o-button"
+        @click="onReboot"
+      >
+        <i
+          class="o-button__icon"
+          :class="{
+            'el-icon-loading': rebooting,
+            'el-icon-refresh-right': !rebooting,
+          }"
+        />
+        重启
+      </button>
+    </div>
+  </box>
+</template>
+
+<script>
+import {
+  publish,
+  subscribe,
+  unsubscribe
+} from '@/utils/mqtt'
+import Box from './Box'
+
+export default {
+  name: 'DeviceInfo',
+  components: {
+    Box
+  },
+  props: {
+    device: {
+      type: Object,
+      required: true
+    }
+  },
+  data () {
+    return {
+      online: this.device.activate === 2 && this.device.onlineStatus === 1,
+      rebooting: false,
+      rows: [
+        [
+          {
+            label: '名称',
+            key: 'name'
+          },
+          {
+            label: 'MAC',
+            key: 'mac'
+          }
+        ],
+        [
+          {
+            label: '序列号',
+            key: 'serialNumber'
+          },
+          {
+            label: '分辨率',
+            key: 'resolutionRatio'
+          }
+        ],
+        [
+          {
+            label: '产品',
+            key: 'productName'
+          },
+          {
+            label: '位置',
+            key: 'address'
+          }
+        ]
+      ]
+    }
+  },
+  created () {
+    subscribe([
+      `${this.device.productId}/${this.device.id}/online`,
+      `${this.device.productId}/${this.device.id}/offline`
+    ], this.onMessage)
+  },
+  beforeDestroy () {
+    unsubscribe([
+      `${this.device.productId}/${this.device.id}/online`,
+      `${this.device.productId}/${this.device.id}/offline`
+    ], this.onMessage)
+  },
+  methods: {
+    onMessage (topic) {
+      const result = topic.match(/^(\d+)\/(\d+)\/(online|offline)$/)
+      if (result && this.device.productId === result[1] && this.device.id === result[2]) {
+        this.rebooting = false
+        this.online = result[3] === 'online'
+      }
+    },
+    onSwitch (open) {
+      if (!this.online) {
+        this.$message({
+          type: 'warning',
+          message: '设备未上线,请稍后再试'
+        })
+        return
+      }
+      this.$confirm(`立即${open ? '开机' : '关机'}?`, {
+        type: 'warning'
+      }).then(() => {
+        this.sendTopic(open ? 'bootDevice' : 'shutdownDevice')
+      })
+    },
+    onReboot () {
+      if (!this.online) {
+        this.$message({
+          type: 'warning',
+          message: '设备未上线,请稍后再试'
+        })
+        return
+      }
+      if (this.rebooting) {
+        return
+      }
+      this.$confirm(`立即重启?`, { type: 'warning' }).then(() => {
+        publish(
+          `${this.device.productId}/${this.deviceId}/restart/ask`,
+          JSON.stringify({ timestamp: Date.now() })
+        ).then(
+          () => {
+            this.rebooting = true
+            this.$message({
+              type: 'success',
+              message: '执行成功'
+            })
+          },
+          () => {
+            this.$message({
+              type: 'warning',
+              message: '正在连接,请稍后再试'
+            })
+          }
+        )
+      })
+    },
+    sendTopic (invoke, inputs = []) {
+      publish(
+        `${this.device.productId}/${this.deviceId}/function/invoke`,
+        JSON.stringify({
+          timestamp: Date.now(),
+          function: invoke,
+          inputs
+        }),
+        true
+      ).then(
+        () => {
+          this.$message({
+            type: 'success',
+            message: '执行成功'
+          })
+        },
+        () => {
+          this.$message({
+            type: 'warning',
+            message: '正在连接,请稍后再试'
+          })
+        }
+      )
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.c-info {
+  &__row {
+    height: 80px;
+    background: #313a5a;
+
+    .row__label {
+      color: #9ea9cd;
+    }
+  }
+}
+
+.o-button {
+  width: 200px;
+  height: 80px;
+  font-size: 36px;
+
+  & ~ & {
+    margin-left: 150px;
+  }
+
+  &__icon {
+    font-size: 36px;
+  }
+}
+</style>

+ 104 - 0
src/views/dashboard/v0/DeviceSort.vue

@@ -0,0 +1,104 @@
+<template>
+  <box
+    title="设备排序"
+    can-full-screen
+    fullscreen
+    v-on="$listeners"
+  >
+    <div class="l-flex__none l-flex--row center c-sort-header">
+      按住鼠标左键拖动视频框进行排序
+      <button
+        class="c-sort-header__button o-button"
+        @click="save"
+      >
+        保存
+      </button>
+    </div>
+    <draggable
+      v-model="deviceList"
+      class="l-flex__auto c-sort-list u-overflow-y--auto"
+      animation="300"
+    >
+      <div
+        v-for="item in deviceList"
+        :key="item.id"
+        class="c-sort-list__item has-padding has-bg"
+      >
+        <div class="u-ellipsis">{{ item.name }}</div>
+      </div>
+    </draggable>
+  </box>
+</template>
+
+<script>
+import { deviceSort } from './api'
+import Draggable from 'vuedraggable'
+import Box from './Box'
+
+export default {
+  name: 'DeviceSort',
+  components: {
+    Box,
+    Draggable
+  },
+  props: {
+    list: {
+      type: Array,
+      required: true
+    }
+  },
+  data () {
+    return {
+      deviceList: this.list.slice()
+    }
+  },
+  methods: {
+    save () {
+      const length = this.deviceList.length
+      deviceSort({
+        list: this.deviceList.map((i, index) => {
+          return {
+            deviceId: i.id,
+            sort: length - index
+          }
+        })
+      }).then(() => {
+        this.$emit('save', this.deviceList)
+      })
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.c-sort-header {
+  position: relative;
+  height: 40px;
+  margin: $spacing 24px;
+  color: #7c86a7;
+  font-size: 18px;
+
+  &__button {
+    position: absolute;
+    right: 0;
+    width: 100px;
+  }
+}
+
+.c-sort-list {
+  display: grid;
+  grid-template-columns: repeat(5, 1fr);
+  grid-gap: $spacing $spacing;
+  margin: 0 24px;
+
+  &__item {
+    display: flex;
+    align-items: flex-end;
+    position: relative;
+    min-width: 0;
+    height: 158px;
+    font-size: 20px;
+    background-image: url("~@/assets/v0/screen.png");
+  }
+}
+</style>

+ 37 - 0
src/views/dashboard/v0/DeviceStatus.vue

@@ -0,0 +1,37 @@
+<template>
+  <box title="设备状态">
+    <cards :items="itemList" />
+  </box>
+</template>
+
+<script>
+import Box from './Box'
+import Cards from './Cards'
+
+export default {
+  name: 'DeviceStatus',
+  components: {
+    Box,
+    Cards
+  },
+  props: {
+    items: {
+      type: Array,
+      required: true
+    }
+  },
+  computed: {
+    itemList () {
+      const colors = ['#2956F0', '#04A681', '#F40645', '#C4C4C4']
+
+      return this.items.map((item, index) => {
+        return {
+          ...item,
+          icon: 'menu-four',
+          style: { color: colors[index] }
+        }
+      })
+    }
+  }
+}
+</script>

+ 57 - 0
src/views/dashboard/v0/Header.vue

@@ -0,0 +1,57 @@
+<template>
+  <div class="c-dashboard-header">
+    <div class="c-dashboard-header__name u-bold">{{ title }}</div>
+    <div class="c-dashboard-header__time">{{ now }}</div>
+  </div>
+</template>
+
+<script>
+import { parseTime } from '@/utils'
+
+export default {
+  data () {
+    return {
+      title: process.env.VUE_APP_NAME,
+      now: ''
+    }
+  },
+  created () {
+    this.getCurrentTime()
+    this.$timer = setInterval(this.getCurrentTime, 500)
+  },
+  beforeDestroy () {
+    clearInterval(this.$timer)
+  },
+  methods: {
+    getCurrentTime () {
+      this.now = parseTime(new Date(), '{y}/{m}/{d}   {h}:{i}')
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.c-dashboard-header {
+  position: relative;
+  height: 132px;
+  padding: 25px 0;
+  margin: 0 80px;
+  color: #fff;
+  font-size: 40px;
+  text-align: center;
+  line-height: 70px;
+
+  &__name {
+    letter-spacing: 0.2em;
+  }
+
+  &__time {
+    position: absolute;
+    right: 0;
+    bottom: 26px;
+    font-size: 24px;
+    line-height: 36px;
+    white-space: pre;
+  }
+}
+</style>

+ 43 - 0
src/views/dashboard/v0/LinkState.vue

@@ -0,0 +1,43 @@
+<template>
+  <box
+    title="全链路监测状态"
+    can-full-screen
+    fullscreen
+    v-on="$listeners"
+  >
+    <div
+      ref="box"
+      class="l-flex__fill l-flex--col"
+    >
+      <full-link
+        ref="link"
+        class="l-flex__fill"
+        :device="device"
+        :online="online"
+        theme="light"
+      />
+    </div>
+  </box>
+</template>
+
+<script>
+import Box from './Box'
+
+export default {
+  name: 'LinkState',
+  components: {
+    Box
+  },
+  props: {
+    device: {
+      type: Object,
+      required: true
+    }
+  },
+  computed: {
+    online () {
+      return this.device.activate === 2 && this.device.onlineStatus === 1
+    }
+  }
+}
+</script>

+ 107 - 0
src/views/dashboard/v0/Map.vue

@@ -0,0 +1,107 @@
+<template>
+  <box
+    title="地图位置"
+    can-full-screen
+    :fullscreen="fullscreen"
+    @fullscreenChange="onFullscreen"
+  >
+    <div
+      class="l-flex__fill l-flex--col"
+      :class="{ fullscreen }"
+    >
+      <div
+        v-if="mapMarkers.length"
+        ref="map"
+        class="l-flex__fill"
+      />
+    </div>
+  </box>
+</template>
+
+<script>
+import Box from './Box'
+import AMapLoader from '@amap/amap-jsapi-loader'
+const onlineIcon = require('@/assets/v0/icon_position1.svg')
+const offlineIcon = require('@/assets/v0/icon_position2.svg')
+
+export default {
+  components: {
+    Box
+  },
+  props: {
+    deviceList: {
+      type: Array,
+      required: true
+    }
+  },
+  data () {
+    return {
+      fullscreen: false
+    }
+  },
+  computed: {
+    mapMarkers () {
+      return this.deviceList
+        .filter(i => i.longitude && i.latitude)
+        .map(i => {
+          return {
+            position: [Number(i.longitude), Number(i.latitude)],
+            icon: i.onlineStatus === 1 ? onlineIcon : offlineIcon,
+            title: i.name
+          }
+        })
+    }
+  },
+  watch: {
+    deviceList () {
+      this.initMap()
+    }
+  },
+  mounted () {
+    this.initMap()
+  },
+  beforeDestroy () {
+    this.map?.destroy()
+  },
+  methods: {
+    onFullscreen (fullscreen) {
+      this.fullscreen = fullscreen
+      setTimeout(() => {
+        this.initMap()
+      }, 500)
+    },
+    initMap () {
+      if (!this.deviceList.length) {
+        return
+      }
+
+      AMapLoader.load({
+        key: process.env.VUE_APP_GAODE_MAP_KEY,
+        version: '2.0',
+        plugins: ['']
+      }).then(AMap => {
+        if (!this.map) {
+          this.map = new AMap.Map(this.$refs.map)
+        }
+
+        const size = this.fullscreen ? 50 : 30
+        const markers = this.mapMarkers.map(marker => new AMap.Marker({
+          ...marker,
+          map: this.map,
+          icon: new AMap.Icon({
+            image: marker.icon,
+            imageSize: new AMap.Size(size, size)
+          })
+        }))
+
+        setTimeout(() => {
+          this.$markers && this.map.remove(this.$markers)
+          this.$markers = markers
+        }, 500)
+
+        this.map.setFitView()
+      })
+    }
+  }
+}
+</script>

+ 265 - 0
src/views/dashboard/v0/MessageNotice.vue

@@ -0,0 +1,265 @@
+<template>
+  <box title="消息通知">
+    <div class="l-flex__fill l-flex--col c-messageNotice">
+      <template v-if="listData.length">
+        <div class="l-flex__none l-flex--row c-messageNotice__header">
+          <div class="l-flex__none col__time">时间</div>
+          <div class="l-flex__none col__deviceName">设备名称</div>
+          <div class="l-flex__fill col__content">内容</div>
+          <div class="l-flex__none col__opt">操作</div>
+        </div>
+        <div class="l-flex__fill u-relative">
+          <div class="c-messageNotice__newList">
+            <div
+              v-for="(item, index) in newItems"
+              :key="item.id"
+              class="l-flex--row c-messageNotice__item new"
+            >
+              <div class="l-flex__none col__time l-flex--row">
+                <svg-icon
+                  class="alarm__icon"
+                  icon-class="v0-alarm"
+                  :style="{ color: colors[item.level] }"
+                />
+                <div>{{ item.happenTime }}</div>
+              </div>
+              <div class="l-flex__none u-ellipsis col__deviceName">
+                {{ item.deviceName }}
+              </div>
+              <div class="l-flex__fill u-ellipsis col__content">
+                {{ item.type }}
+              </div>
+              <div class="l-flex__none col__opt">
+                <button
+                  class="o-button alarm"
+                  @click="onView(item, index)"
+                >
+                  查看
+                </button>
+              </div>
+            </div>
+          </div>
+          <div class="c-messageNotice__list">
+            <vue-seamless-scroll
+              ref="seamlessScroll"
+              :data="listData"
+              :class-option="classOption"
+            >
+              <div
+                v-for="item in listData"
+                :key="item.id"
+                class="l-flex--row c-messageNotice__item"
+              >
+                <div class="l-flex__none col__time l-flex--row">
+                  <svg-icon
+                    class="alarm__icon"
+                    icon-class="v0-alarm"
+                    :style="{ color: colors[item.level] }"
+                  />
+                  <div>{{ item.happenTime }}</div>
+                </div>
+                <div class="l-flex__none u-ellipsis col__deviceName">
+                  <auto-text
+                    class="u-text-center"
+                    :text="item.deviceName"
+                  />
+                </div>
+                <div class="l-flex__fill u-ellipsis col__content">
+                  <auto-text
+                    class="u-text-center"
+                    :text="item.type"
+                  />
+                </div>
+                <div class="l-flex__none col__opt">
+                  <button
+                    class="o-button"
+                    @click="onView(item)"
+                  >
+                    查看
+                  </button>
+                </div>
+              </div>
+            </vue-seamless-scroll>
+          </div>
+        </div>
+      </template>
+      <status-wrapper v-else />
+    </div>
+  </box>
+</template>
+
+<script>
+import Box from './Box'
+import { getDeviceAlarms } from '@/api/device'
+import vueSeamlessScroll from '@/views/device/detail/dashboard/vue-seamless-scroll.min.js'
+import { addTenant } from '@/api/base'
+
+export default {
+  components: {
+    Box,
+    vueSeamlessScroll
+  },
+  data () {
+    this.$num = 0
+    return {
+      error: false,
+      loading: true,
+      classOption: {
+        step: 0.4
+      },
+      listData: [],
+      newAlarmList: [],
+      colors: ['#04A681', '#FFA000', '#F40645']
+    }
+  },
+  computed: {
+    newItems () {
+      return this.newAlarmList.slice(0, 3)
+    }
+  },
+  mounted () {
+    this.getDeviceAlarms()
+    this.$timer = setInterval(this.getDeviceAlarms, 30000)
+  },
+  beforeDestroy () {
+    clearInterval(this.$timer)
+  },
+  methods: {
+    onView (item, index = null) {
+      if (index !== null) {
+        this.newAlarmList.splice(index, 1)
+      }
+      this.$emit('openAlarmInfo', item)
+    },
+    getDeviceAlarms () {
+      this.error = false
+      getDeviceAlarms(
+        addTenant({
+          pageIndex: 1,
+          pageSize: 20
+        })
+      ).then(
+        ({ data }) => {
+          if (!data.length) {
+            return
+          }
+          const length = data.length
+          const lastId = this.listData[0]?.id
+          let hasNew = false
+          for (let i = 0; i < length; i++) {
+            const item = data[i]
+            if (item.id === lastId) {
+              break
+            }
+            hasNew = true
+            if (item.level === 2) {
+              this.newAlarmList.unshift(item)
+            }
+          }
+
+          if (hasNew) {
+            this.listData = data
+            this.$refs.seamlessScroll.reset()
+          }
+        },
+        () => {
+          this.error = true
+        }
+      ).finally(() => {
+        this.loading = false
+      })
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.c-messageNotice {
+  &__header {
+    color: #9ea9cd;
+    background-color: #313a5a;
+  }
+
+  &__list {
+    width: 100%;
+    height: 100%;
+    overflow: hidden;
+  }
+
+  &__item {
+    color: #ffffff;
+    border-bottom: 1px solid #313a5a;
+
+    &.new {
+      color: #f40645;
+      font-weight: bold;
+    }
+  }
+
+  &__header,
+  &__item {
+    font-size: 12px;
+    line-height: 36px;
+    text-align: center;
+  }
+
+  .alarm__icon {
+    font-size: 14px;
+    margin-right: 4px;
+  }
+
+  .col {
+    &__time {
+      width: 140px;
+    }
+
+    &__deviceName {
+      width: 95px;
+    }
+
+    &__opt {
+      width: 50px;
+
+      .o-button {
+        min-width: 32px;
+        height: 20px;
+        padding: 0 4px;
+        font-size: 12px;
+        border-radius: 2px;
+        background-color: #2956f0;
+        &.alarm {
+          background-color: #f40645;
+          color: #ffffff;
+        }
+      }
+    }
+  }
+
+  &__newList {
+    animation: sparkle 2s linear infinite;
+
+    &:hover {
+      animation: none;
+    }
+  }
+
+  @keyframes sparkle {
+    0% {
+      opacity: 1;
+    }
+    50% {
+      opacity: 1;
+    }
+    50.01% {
+      opacity: 0;
+    }
+    100% {
+      opacity: 0;
+    }
+  }
+
+  ::v-deep.el-empty {
+    padding: 0;
+  }
+}
+</style>

+ 247 - 0
src/views/dashboard/v0/Record.vue

@@ -0,0 +1,247 @@
+<template>
+  <box>
+    <div
+      v-loading="options.loading && !options.list.length"
+      element-loading-background="transparent"
+      class="l-flex__auto l-flex--col has-top-padding"
+    >
+      <div
+        class="l-flex__fill c-sibling-item--v c-record-grid"
+        :class="gridClass"
+      >
+        <device-player
+          v-for="item in options.list"
+          :key="item.identifier"
+          :device="item"
+          controls
+          autoplay
+          retry
+        >
+          <template #controls="{canPlay, onFullScreen}">
+            <svg-icon
+              class="c-sibling-item control__icon has-active u-pointer"
+              :class="item.statusClass"
+              icon-class="v0-link"
+              @click="$emit('openLinkState', item)"
+            />
+            <svg-icon
+              class="c-sibling-item control__icon has-active u-pointer"
+              icon-class="v0-info"
+              @click="$emit('openDeviceInfo', item)"
+            />
+            <svg-icon
+              v-if="canPlay"
+              class="c-sibling-item control__icon has-active u-pointer"
+              icon-class="v0-full-screen"
+              @click="onFullScreen"
+            />
+          </template>
+        </device-player>
+      </div>
+      <status-wrapper
+        v-if="isAbnormal"
+        class="l-flex__fill"
+      />
+      <div class="l-flex__none l-flex--row c-sibling-item--v far">
+        <div class="l-flex__none c-sibling-item l-flex--row">
+          <div
+            class="l-flex--row center c-sibling-item c-tab u-pointer"
+            :class="{ active: gridClass === 'one' }"
+            @click="onChange(1)"
+          >
+            <svg-icon
+              class="c-sibling-item o-menu-icon u-pointer"
+              icon-class="menu-one"
+            />
+            <div class="c-sibling-item">单屏</div>
+          </div>
+          <div
+            class="l-flex--row center c-sibling-item c-tab u-pointer"
+            :class="{ active: gridClass === 'four' }"
+            @click="onChange(4)"
+          >
+            <svg-icon
+              class="c-sibling-item o-menu-icon u-pointer"
+              icon-class="menu-four"
+            />
+            <div class="c-sibling-item">四分屏</div>
+          </div>
+          <div
+            class="l-flex--row center c-sibling-item c-tab u-pointer"
+            :class="{ active: gridClass === 'nine' }"
+            @click="onChange(9)"
+          >
+            <svg-icon
+              class="c-sibling-item o-menu-icon u-pointer"
+              icon-class="menu-nine"
+            />
+            <div class="c-sibling-item">九分屏</div>
+          </div>
+        </div>
+        <div
+          class="l-flex__fill l-flex--row center c-sibling-item c-tab u-pointer"
+          @click="$emit('openDeviceSort')"
+        >
+          <svg-icon
+            class="o-sort c-sibling-item"
+            icon-class="v0-move"
+          />
+          <div class="c-sibling-item">设备排序</div>
+        </div>
+      </div>
+    </div>
+  </box>
+</template>
+
+<script>
+import { getDevices } from '@/api/device'
+import { createListOptions } from '@/utils'
+import { getBoundThirdPartyDevices } from '@/api/external'
+import Box from './Box'
+
+export default {
+  name: 'DeviceRecord',
+  components: {
+    Box
+  },
+  data () {
+    return {
+      options: createListOptions({ activate: 2, pageSize: 1 })
+    }
+  },
+  computed: {
+    gridClass () {
+      switch (this.options.params.pageSize) {
+        case 4:
+          return 'four'
+        case 9:
+          return 'nine'
+        default:
+          return 'one'
+      }
+    },
+    isAbnormal () {
+      const options = this.options
+      return !options.loading && options.list.length === 0
+    }
+  },
+  created () {
+    this.$cache = {}
+    this.getDevices()
+    this.$timer = setInterval(this.getDevices, 10000)
+  },
+  beforeDestroy () {
+    clearInterval(this.$timer)
+  },
+  methods: {
+    getDevices () {
+      const options = this.options
+      options.loading = true
+      getDevices(options.params, { custom: true }).then(
+        ({ data }) => {
+          options.list = data.map(item => {
+            const status = Math.max(this.$cache[item.id] || 0, item.onlineStatus === 2 ? 2 : 0)
+            return {
+              ...item,
+              statusClass: `status${status}`,
+              status
+            }
+          })
+          this.$cache = {}
+          options.list.forEach(item => {
+            this.$cache[item.id] = item.status
+            if (item.status !== 2) {
+              getBoundThirdPartyDevices(item.id, true).then(({ data: linkList }) => {
+                const status = Math.max(this.$cache[item.id] || 0, linkList && linkList.some(({ status }) => status !== 1) ? 2 : 1)
+                item.status = status
+                item.statusClass = `status${status}`
+                this.$cache[item.id] = status
+              })
+            }
+          })
+        },
+        () => {
+          options.list = []
+        }
+      ).finally(() => {
+        options.loading = false
+      })
+    },
+    onChange (num) {
+      const params = this.options.params
+      if (num === params.pageSize) {
+        return
+      }
+      params.pageSize = num
+      this.refresh()
+    },
+    refresh () {
+      this.options.list = []
+      this.getDevices()
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.o-menu-icon {
+  color: #d5d9e4;
+  font-size: 32px;
+
+  &.active {
+    color: $blue;
+  }
+}
+.control__icon {
+  width: 24px;
+  height: 24px;
+
+  &.status0 {
+    color: $warning;
+  }
+
+  &.status1 {
+    color: #fff;
+  }
+
+  &.status2 {
+    color: #f40645;
+  }
+}
+
+.c-record-grid {
+  display: grid;
+  grid-template-rows: max-content;
+  grid-row-gap: 4px;
+  grid-column-gap: 4px;
+
+  &.one {
+    grid-template-columns: 1fr;
+  }
+
+  &.four {
+    grid-template-columns: 1fr 1fr;
+  }
+
+  &.nine {
+    grid-template-columns: 1fr 1fr 1fr;
+  }
+}
+.c-tab {
+  min-width: 140px;
+  height: 32px;
+  background: #313a5a;
+  color: #9ea9cd;
+  user-select: none;
+
+  &.active,
+  &:hover {
+    background: #2956f0;
+    color: #fff;
+  }
+}
+.o-sort {
+  width: 20px;
+  height: 20px;
+}
+</style>

+ 43 - 0
src/views/dashboard/v0/api.js

@@ -0,0 +1,43 @@
+import request, { tenantRequest } from '@/utils/request'
+import { addTenant } from '@/api/base'
+
+export function getTimelines (deviceIdList, options) {
+  return request({
+    url: `/content/deviceCalender`,
+    method: 'POST',
+    ...options,
+    data: { deviceIdList }
+  }).then(({ data }) => data.map(i => { return { ...i, eventDetail: JSON.parse(i.eventDetail) } }) || [])
+}
+
+export function getDeviceExceptionRanking () {
+  return tenantRequest({
+    url: '/deviceException/ranking',
+    method: 'GET',
+    params: addTenant({})
+  })
+}
+
+export function getDeviceExceptionLevelStatistic () {
+  return tenantRequest({
+    url: '/deviceException/levelStatistic',
+    method: 'GET',
+    params: addTenant({})
+  })
+}
+
+export function getDeviceExceptionTypeStatistics (statisticDate) {
+  return tenantRequest({
+    url: '/deviceException/typeStatistics',
+    method: 'GET',
+    params: addTenant({ statisticDate })
+  })
+}
+
+export function deviceSort (data) {
+  return request({
+    url: '/device/sort',
+    method: 'POST',
+    data
+  })
+}

+ 300 - 0
src/views/dashboard/v0/index.vue

@@ -0,0 +1,300 @@
+<template>
+  <div class="c-record-dashboard">
+    <div
+      class="c-record-dashboard__main"
+      :style="style"
+    >
+      <DeviceInfo
+        v-if="showDeviceInfo"
+        :device="currentDeviceInfo"
+        @fullscreenChange="onDeviceInfoClose"
+      />
+      <AlarmInfo
+        v-if="showAlarmInfo"
+        :alarm="currentAlarmInfo"
+        @fullscreenChange="onAlarmInfoClose"
+      />
+      <DeviceSort
+        v-if="showDeviceSort"
+        :list="deviceList"
+        @fullscreenChange="onDeviceSortClose"
+        @save="onDeviceSortSave"
+      />
+      <LinkState
+        v-if="showLinkState"
+        :device="currentLinkState"
+        @fullscreenChange="onLinkStateClose"
+      />
+      <Header />
+      <div class="l-flex--col center">
+        <div class="l-flex--row">
+          <div class="l-flex--col l-flex__none">
+            <div
+              class="l-flex__none dashboard-item"
+              style="width: 404px; height: 281px"
+            >
+              <DeviceStatus :items="statusData" />
+            </div>
+            <div
+              class="l-flex__none dashboard-item"
+              style="width: 404px; height: 281px"
+            >
+              <Map :device-list="deviceList" />
+            </div>
+          </div>
+          <div class="l-flex--col l-flex__none">
+            <div
+              class="l-flex__none dashboard-item"
+              style="width: 904px; height: 586px"
+            >
+              <Record
+                ref="record"
+                @openDeviceInfo="openDeviceInfo"
+                @openDeviceSort="openDeviceSort"
+                @openLinkState="openLinkState"
+              />
+            </div>
+          </div>
+          <div class="l-flex--col l-flex__none">
+            <div
+              class="l-flex__none dashboard-item"
+              style="width: 404px; height: 281px"
+            >
+              <MessageNotice @openAlarmInfo="openAlarmInfo" />
+            </div>
+            <div
+              class="l-flex__none dashboard-item"
+              style="width: 404px; height: 281px"
+            >
+              <AlarmLevel :items="exceptionLevelStatisticArray" />
+            </div>
+          </div>
+        </div>
+        <div class="l-flex--row">
+          <div class="l-flex--col l-flex__none">
+            <div
+              class="l-flex__none dashboard-item"
+              style="width: 404px; height: 284px"
+            >
+              <AlarmType />
+            </div>
+          </div>
+          <div class="l-flex--col l-flex__none">
+            <div
+              class="l-flex__none dashboard-item"
+              style="width: 904px; height: 284px"
+            >
+              <DeviceCalender :device-list="deviceList" />
+            </div>
+          </div>
+          <div class="l-flex--col l-flex__none">
+            <div
+              class="l-flex__none dashboard-item"
+              style="width: 404px; height: 284px"
+            >
+              <AlarmRate />
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+<script>
+import {
+  getDevices,
+  getDeviceStatistics
+} from '@/api/device'
+import { getDeviceExceptionLevelStatistic } from './api'
+import DeviceStatus from './DeviceStatus'
+import DeviceCalender from './DeviceCalender'
+import Map from './Map'
+import AlarmType from './AlarmType'
+import AlarmRate from './AlarmRate'
+import AlarmLevel from './AlarmLevel'
+import DeviceInfo from './DeviceInfo'
+import AlarmInfo from './AlarmInfo'
+import MessageNotice from './MessageNotice'
+import Record from './Record'
+import DeviceSort from './DeviceSort'
+import Header from './Header'
+import LinkState from './LinkState'
+
+export default {
+  components: {
+    DeviceSort,
+    DeviceInfo,
+    AlarmInfo,
+    DeviceStatus,
+    Map,
+    AlarmType,
+    AlarmRate,
+    MessageNotice,
+    Record,
+    DeviceCalender,
+    AlarmLevel,
+    Header,
+    LinkState
+  },
+  data () {
+    return {
+      loading: false,
+      style: null,
+      statusData: [
+        { label: '设备总数', value: '-' },
+        { label: '设备在线数', value: '-' },
+        { label: '设备离线数', value: '-' },
+        { label: '设备未启用数', value: '-' }
+      ],
+      exceptionLevelStatisticArray: [
+        { label: '预警等级总数', value: '-' },
+        { label: '紧急等级', value: '-' },
+        { label: '提示等级', value: '-' },
+        { label: '普通等级', value: '-' }
+      ],
+      deviceList: [],
+      showDeviceInfo: false,
+      currentDeviceInfo: null,
+      showAlarmInfo: false,
+      currentAlarmInfo: null,
+      showDeviceSort: false,
+      currentDeviceSort: null,
+      showLinkState: false,
+      currentLinkState: null
+    }
+  },
+  created () {
+    this.getData()
+    this.$timer = setInterval(this.getData, 30000)
+  },
+  mounted () {
+    this.checkScale()
+    window.addEventListener('resize', this.checkScale)
+  },
+  beforeDestroy () {
+    clearInterval(this.$timer)
+    window.removeEventListener('resize', this.checkScale)
+  },
+  methods: {
+    checkScale () {
+      this.style = {
+        transform: `scale(${window.innerWidth / 1920}, ${window.innerHeight / 1080})`
+      }
+    },
+    getData () {
+      this.getDeviceStatistics()
+      this.getDeviceExceptionLevelStatistic()
+    },
+    openLinkState (item) {
+      this.currentLinkState = item
+      this.showLinkState = true
+    },
+    onLinkStateClose () {
+      this.currentLinkState = null
+      this.showLinkState = false
+    },
+    openDeviceInfo (item) {
+      this.showDeviceInfo = true
+      this.currentDeviceInfo = item
+    },
+    onDeviceInfoClose () {
+      this.currentDeviceInfo = null
+      this.showDeviceInfo = false
+    },
+    openDeviceSort () {
+      this.showDeviceSort = true
+    },
+    onDeviceSortClose () {
+      this.showDeviceSort = false
+    },
+    onDeviceSortSave (deviceList) {
+      this.deviceList = deviceList
+      this.showDeviceSort = false
+      this.$refs.record.refresh()
+    },
+    openAlarmInfo (alarm) {
+      this.currentAlarmInfo = alarm
+      this.showAlarmInfo = true
+    },
+    onAlarmInfoClose () {
+      this.currentAlarmInfo = null
+      this.showAlarmInfo = false
+    },
+    getDeviceExceptionLevelStatistic () {
+      getDeviceExceptionLevelStatistic().then(({ data }) => {
+        const { total, urgency, hint, common } = data
+        this.exceptionLevelStatisticArray = [
+          {
+            label: '预警等级总数',
+            value: total
+          },
+          {
+            label: '紧急等级',
+            value: urgency
+          },
+          {
+            label: '提示等级',
+            value: hint
+          },
+          {
+            label: '普通等级',
+            value: common
+          }
+        ]
+      })
+    },
+    getDeviceStatistics () {
+      getDeviceStatistics().then(({ data }) => {
+        const { notEnabledTotal, offLineTotal, onLineTotal, total } = data
+        this.statusData = [
+          { label: '设备总数', value: total || 0 },
+          { label: '设备在线数', value: onLineTotal || 0 },
+          { label: '设备离线数', value: offLineTotal || 0 },
+          { label: '设备未启用数', value: notEnabledTotal || 0 }
+        ]
+        this.getDevices(total - (notEnabledTotal || 0))
+      })
+    },
+    getDevices (total) {
+      getDevices(
+        {
+          pageNum: 1,
+          pageSize: total,
+          activate: 2
+        },
+        { custom: true }
+      ).then(({ data }) => {
+        this.deviceList = data
+      })
+    }
+  }
+}
+</script>
+<style lang="scss" scoped>
+.c-record-dashboard {
+  height: 100%;
+  overflow: hidden;
+
+  &__main {
+    width: 1920px;
+    height: 1080px;
+    background: url("~@/assets/v0/bg.png");
+    transform-origin: left top;
+  }
+}
+.dashboard-item {
+  & ~ & {
+    margin-top: 24px;
+  }
+}
+.l-flex--row {
+  & ~ & {
+    margin-top: 24px;
+  }
+}
+.l-flex--col {
+  & ~ & {
+    margin-left: 24px;
+  }
+}
+</style>