Dashboard.vue 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335
  1. <template>
  2. <wrapper
  3. fill
  4. margin
  5. >
  6. <div class="l-flex__none l-flex--row c-sibling-item--v c-count">
  7. <div
  8. class="l-flex__none c-count__title u-color--black u-bold has-active u-ellipsis"
  9. @click="onChooseDepartment"
  10. >
  11. <i
  12. v-if="loading"
  13. class="el-icon-loading"
  14. />
  15. {{ group.name }}
  16. </div>
  17. <div class="l-flex__none c-count__item u-color--black u-bold u-text--center">
  18. <div>总数</div>
  19. <i
  20. v-if="monitor.loading"
  21. class="el-icon-loading"
  22. />
  23. <div v-else>{{ monitor.total }}</div>
  24. </div>
  25. <div class="l-flex__none c-count__item u-color--success dark u-bold u-text--center">
  26. <div>● 在线</div>
  27. <i
  28. v-if="monitor.loading"
  29. class="el-icon-loading"
  30. />
  31. <div v-else>{{ monitor.online }}</div>
  32. </div>
  33. <div class="l-flex__none c-count__item u-color--error dark u-bold u-text--center">
  34. <div>● 离线</div>
  35. <i
  36. v-if="monitor.loading"
  37. class="el-icon-loading"
  38. />
  39. <div v-else>{{ monitor.offline }}</div>
  40. </div>
  41. <div class="l-flex__none c-count__item u-color--info u-bold u-text--center">
  42. <div>● 未启用</div>
  43. <i
  44. v-if="monitor.loading"
  45. class="el-icon-loading"
  46. />
  47. <div v-else>{{ monitor.inactive }}</div>
  48. </div>
  49. <i
  50. class="el-icon-refresh o-icon md u-color--blue has-active"
  51. @click="onRefresh"
  52. />
  53. </div>
  54. <div
  55. v-if="__PLACEHOLDER__"
  56. class="l-flex__none c-sibling-item--v far c-cards"
  57. >
  58. <card
  59. class="c-cards__item"
  60. type="info"
  61. title="总次数"
  62. tip="100"
  63. />
  64. <card
  65. class="c-cards__item"
  66. type="safety"
  67. title="安全异常"
  68. desc="累计异常数:30"
  69. />
  70. <card
  71. class="c-cards__item"
  72. type="performance"
  73. title="性能异常"
  74. desc="累计异常数:30"
  75. count="3"
  76. />
  77. <card
  78. class="c-cards__item"
  79. type="log"
  80. title="日志异常"
  81. desc="累计异常数:30"
  82. count="10"
  83. />
  84. <card
  85. class="c-cards__item"
  86. type="date"
  87. title="即将空闲排期"
  88. count="100"
  89. />
  90. </div>
  91. <div
  92. ref="deviceContainer"
  93. class="l-flex__auto l-grid--info c-sibling-item--v far u-overflow-y--auto"
  94. >
  95. <device
  96. v-for="item in deviceOptions.list"
  97. :key="item.id"
  98. :device="item"
  99. :observer="observerDom"
  100. :flag="item.flag"
  101. />
  102. </div>
  103. <div
  104. v-if="!deviceOptions.loaded"
  105. class="c-sibling-item--v far u-text--center"
  106. >
  107. <i class="el-icon-loading" />
  108. </div>
  109. <department-drawer
  110. ref="departmentDrawer"
  111. @change="onGroupChanged"
  112. @loaded="onGroupLoaded"
  113. />
  114. </wrapper>
  115. </template>
  116. <script>
  117. import {
  118. subscribe,
  119. unsubscribe
  120. } from '@/utils/mqtt'
  121. import { ScreenshotCache } from '@/utils/cache'
  122. import {
  123. getDevicesByQuery,
  124. getDeviceStatisticsByPath
  125. } from '@/api/device'
  126. import Card from './components/Card'
  127. import Device from './components/Device'
  128. export default {
  129. name: 'Dashboard',
  130. components: {
  131. Card,
  132. Device
  133. },
  134. data () {
  135. return {
  136. monitor: { loading: true },
  137. deviceOptions: {
  138. list: [],
  139. loaded: false
  140. },
  141. loading: true,
  142. group: {}
  143. }
  144. },
  145. created () {
  146. this.$timer = -1
  147. subscribe([
  148. '+/+/online',
  149. '+/+/offline',
  150. '+/+/calendar/update'
  151. ], this.onMessage)
  152. },
  153. beforeDestroy () {
  154. ScreenshotCache.clear()
  155. clearTimeout(this.$timer)
  156. this.monitor = { loading: true }
  157. unsubscribe([
  158. '+/+/online',
  159. '+/+/offline',
  160. '+/+/calendar/update'
  161. ], this.onMessage)
  162. if (this.$observer) {
  163. this.$observer.disconnect()
  164. this.$observer = null
  165. }
  166. },
  167. methods: {
  168. observerDom (el) {
  169. if (!this.$observer) {
  170. this.$observer = new IntersectionObserver(entries => {
  171. console.log('observer')
  172. entries.forEach(entry => {
  173. const inst = entry.target.__vue__
  174. if (inst && inst.intoView && inst.outView) {
  175. if (entry.isIntersecting) {
  176. inst.intoView()
  177. } else {
  178. inst.outView()
  179. }
  180. } else {
  181. const device = this.deviceOptions.map?.[entry.target.dataset.id]
  182. if (device) {
  183. if (entry.isIntersecting) {
  184. !device.flag && (device.flag = Date.now())
  185. } else {
  186. device.flag && (device.flag = 0)
  187. }
  188. }
  189. }
  190. })
  191. }, {
  192. root: this.$refs.deviceContainer,
  193. threshold: [0.25]
  194. })
  195. }
  196. this.$observer.observe(el)
  197. },
  198. onMessage (topic) {
  199. if (!this.deviceOptions.loaded) {
  200. return
  201. }
  202. const result = /^\d+\/(\d+)\/(online|offline|calendar\/update)$/.exec(topic)
  203. if (!result) {
  204. return
  205. }
  206. const deviceId = result[1]
  207. const status = result[2]
  208. const device = this.deviceOptions.map?.[deviceId]
  209. if (device) {
  210. if (status === 'calendar/update') {
  211. device.flag = -Date.now()
  212. return
  213. }
  214. const onlineStatus = device.onlineStatus === 1 ? 'online' : 'offline'
  215. if (status === onlineStatus) {
  216. return
  217. }
  218. this.refreshDevices()
  219. }
  220. },
  221. onGroupLoaded () {
  222. this.loading = false
  223. },
  224. onGroupChanged ({ path, name }) {
  225. if (!this.group || this.group.path !== path) {
  226. this.group = { path, name }
  227. this.refreshDevices(true)
  228. }
  229. },
  230. onChooseDepartment () {
  231. this.$refs.departmentDrawer.show().then(visible => {
  232. this.loading = !visible
  233. })
  234. },
  235. onRefresh () {
  236. this.refreshDevices()
  237. },
  238. refreshDevices (force) {
  239. if (!force && this.monitor.loading) {
  240. return
  241. }
  242. const monitor = {
  243. loading: true,
  244. total: '-',
  245. online: '-',
  246. offline: '-',
  247. inactive: '-'
  248. }
  249. this.monitor = monitor
  250. clearTimeout(this.$timer)
  251. this.deviceOptions = { loaded: false }
  252. this.$observer?.disconnect()
  253. getDeviceStatisticsByPath(this.group.path).then(({ data }) => {
  254. const { deactivatedTotal, notConnectedTotal, offLineTotal, onLineTotal, total } = data
  255. monitor.total = total
  256. monitor.online = onLineTotal
  257. monitor.offline = offLineTotal + notConnectedTotal
  258. monitor.inactive = deactivatedTotal
  259. }).finally(() => {
  260. monitor.loading = false
  261. if (!this.monitor.loading) {
  262. this.getDevices(this.monitor.total - this.monitor.inactive)
  263. }
  264. })
  265. },
  266. sort (a, b) {
  267. if (a.onlineStatus === b.onlineStatus) {
  268. return a.createTime <= b.createTime ? 1 : -1
  269. }
  270. return a.onlineStatus === 1 ? -1 : 1
  271. },
  272. getDevices (total) {
  273. if (!total || total === '-') {
  274. this.deviceOptions = { list: [], loaded: true }
  275. return
  276. }
  277. const options = { list: [], loaded: false }
  278. this.deviceOptions = options
  279. getDevicesByQuery({
  280. pageNum: 1,
  281. pageSize: total,
  282. activate: 1,
  283. org: this.group.path
  284. }, { custom: true }).then(
  285. ({ data }) => {
  286. const map = {}
  287. options.list = data.sort(this.sort).map(device => {
  288. map[device.id] = device
  289. device.flag = 0
  290. return device
  291. })
  292. options.loaded = true
  293. options.map = map
  294. },
  295. ({ isCancel }) => {
  296. if (!isCancel && !this.monitor.loading) {
  297. this.$timer = setTimeout(total => {
  298. this.getDevices(total)
  299. }, 2000, total)
  300. }
  301. }
  302. )
  303. }
  304. }
  305. }
  306. </script>
  307. <style lang="scss" scoped>
  308. .c-count {
  309. justify-content: space-between;
  310. padding: $spacing--xs $spacing;
  311. border-radius: $radius;
  312. background-color: #fff;
  313. &__title {
  314. width: 200px;
  315. }
  316. &__item > div:first-child {
  317. margin-bottom: 10px;
  318. }
  319. }
  320. .c-cards {
  321. display: grid;
  322. grid-template-columns: repeat(5, 1fr);
  323. grid-column-gap: $spacing;
  324. &__item {
  325. min-width: 0;
  326. }
  327. }
  328. </style>