index.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444
  1. <template>
  2. <wrapper
  3. fill
  4. margin
  5. horizontal
  6. >
  7. <div class="l-flex__fill l-flex--col c-sibling-item c-device-map">
  8. <div class="l-flex__none l-flex--row c-count">
  9. <div
  10. class="l-flex__none c-count__title u-color--black u-bold has-active u-ellipsis"
  11. @click="onChooseDepartment"
  12. >
  13. <i
  14. v-if="loading"
  15. class="el-icon-loading"
  16. />
  17. {{ group.name }}
  18. </div>
  19. <div class="l-flex__none c-count__item u-color--black u-bold u-text--center">
  20. <div>总数</div>
  21. <i
  22. v-if="monitor.loading"
  23. class="el-icon-loading"
  24. />
  25. <div v-else>{{ monitor.total }}</div>
  26. </div>
  27. <div class="l-flex__none c-count__item u-color--success dark u-bold u-text--center">
  28. <div>● 在线</div>
  29. <i
  30. v-if="monitor.loading"
  31. class="el-icon-loading"
  32. />
  33. <div v-else>{{ monitor.online }}</div>
  34. </div>
  35. <div class="l-flex__none c-count__item u-color--error dark u-bold u-text--center">
  36. <div>● 离线</div>
  37. <i
  38. v-if="monitor.loading"
  39. class="el-icon-loading"
  40. />
  41. <div v-else>{{ monitor.offline }}</div>
  42. </div>
  43. <div class="l-flex__none c-count__item u-color--info u-bold u-text--center">
  44. <div>● 未启用</div>
  45. <i
  46. v-if="monitor.loading"
  47. class="el-icon-loading"
  48. />
  49. <div v-else>{{ monitor.inactive }}</div>
  50. </div>
  51. <i
  52. class="el-icon-refresh o-icon md u-color--blue has-active"
  53. @click="onRefresh"
  54. />
  55. </div>
  56. <div class="l-flex__fill l-flex u-relative">
  57. <div
  58. ref="map"
  59. class="l-flex__fill"
  60. />
  61. <i
  62. v-if="deviceOptions.loaded"
  63. class="o-place el-icon-place has-active"
  64. @click="onPlace"
  65. />
  66. <device
  67. v-if="device"
  68. :key="device.id"
  69. class="c-device-map__device"
  70. :device="device"
  71. :status="device.status"
  72. always
  73. />
  74. </div>
  75. </div>
  76. <!-- <div
  77. ref="devicelist"
  78. v-loading="!deviceOptions.loaded"
  79. class="l-flex__none l-flex--col c-sibling-item u-width--lg u-overflow-y--auto"
  80. >
  81. <device
  82. v-for="item in deviceOptions.list"
  83. :key="item.id"
  84. class="c-sibling-item--v"
  85. :device="item"
  86. />
  87. </div> -->
  88. <department-drawer
  89. ref="departmentDrawer"
  90. @change="onGroupChanged"
  91. @loaded="onGroupLoaded"
  92. />
  93. </wrapper>
  94. </template>
  95. <script>
  96. import AMapLoader from '@amap/amap-jsapi-loader'
  97. import {
  98. subscribe,
  99. unsubscribe
  100. } from '@/utils/mqtt'
  101. import { ScreenshotCache } from '@/utils/cache'
  102. import {
  103. GET_POWER_STATUS,
  104. getPowerStatusByMessage
  105. } from '@/utils/adapter/nova'
  106. import {
  107. getDevicesByQuery,
  108. getDeviceStatisticsByPath,
  109. getStatusReport
  110. } from '@/api/device'
  111. import Device from '../components/Device.vue'
  112. const onlineIcon = require('./assets/icon_position1.svg')
  113. const offlineIcon = require('./assets/icon_position2.svg')
  114. export default {
  115. name: 'DeviceMap',
  116. components: {
  117. Device
  118. },
  119. data () {
  120. return {
  121. monitor: { loading: true },
  122. deviceOptions: {
  123. list: [],
  124. loaded: false
  125. },
  126. loading: true,
  127. group: {},
  128. device: null
  129. }
  130. },
  131. computed: {
  132. mapDevices () {
  133. return this.deviceOptions.list.filter(i => i.longitude && i.latitude)
  134. }
  135. },
  136. created () {
  137. this.$timer = -1
  138. subscribe([
  139. '+/+/online',
  140. '+/+/offline',
  141. '+/+/calendar/update',
  142. '+/+/multifunctionCard/invoke/reply'
  143. ], this.onMessage)
  144. },
  145. beforeDestroy () {
  146. ScreenshotCache.clear()
  147. clearTimeout(this.$timer)
  148. this.monitor = { loading: true }
  149. unsubscribe([
  150. '+/+/online',
  151. '+/+/offline',
  152. '+/+/calendar/update',
  153. '+/+/multifunctionCard/invoke/reply'
  154. ], this.onMessage)
  155. this.map?.destroy()
  156. },
  157. methods: {
  158. getStatusReport (options) {
  159. if (!options.power.length) {
  160. return
  161. }
  162. getStatusReport(options.power).then(
  163. ({ data }) => {
  164. const powerMap = {}
  165. data.forEach(item => {
  166. powerMap[item.deviceId] = item.switchStatus
  167. })
  168. options.power.forEach(id => {
  169. if (options.map[id].status === -2) {
  170. options.map[id].status = powerMap[id] ?? -1
  171. }
  172. })
  173. },
  174. ({ isCancel }) => {
  175. if (!isCancel && !this.monitor.loading) {
  176. setTimeout(() => {
  177. this.getStatusReport(options)
  178. }, 1000)
  179. }
  180. }
  181. )
  182. },
  183. onMessage (topic, message) {
  184. if (!this.deviceOptions.loaded) {
  185. return
  186. }
  187. const result = /^\d+\/(\d+)\/(online|offline)|calendar\/update|multifunctionCard\/invoke\/reply$/.exec(topic)
  188. if (!result) {
  189. return
  190. }
  191. const deviceId = result[1]
  192. const status = result[2]
  193. const device = this.deviceOptions.map?.[deviceId]
  194. if (device) {
  195. switch (status) {
  196. case 'calendar/update':
  197. device.flag = -Date.now()
  198. break
  199. case 'multifunctionCard/invoke/reply':
  200. message = message && JSON.parse(message)
  201. switch (message.function) {
  202. case GET_POWER_STATUS:
  203. device.status = this.getPowerStatus(message)
  204. break
  205. default:
  206. break
  207. }
  208. break
  209. default:
  210. if (status === (device.onlineStatus === 1 ? 'online' : 'offline')) {
  211. return
  212. }
  213. this.refreshDevices()
  214. break
  215. }
  216. }
  217. },
  218. getPowerStatus (message) {
  219. const { success, data } = getPowerStatusByMessage(message)
  220. if (success && data.length) {
  221. return data[0].switchStatus
  222. }
  223. return -1
  224. },
  225. onGroupLoaded () {
  226. this.loading = false
  227. },
  228. onGroupChanged ({ path, name }) {
  229. if (!this.group || this.group.path !== path) {
  230. this.group = { path, name }
  231. this.refreshDevices(true)
  232. }
  233. },
  234. onChooseDepartment () {
  235. this.$refs.departmentDrawer.show().then(visible => {
  236. this.loading = !visible
  237. })
  238. },
  239. onRefresh () {
  240. this.refreshDevices()
  241. },
  242. refreshDevices (force) {
  243. if (!force && this.monitor.loading) {
  244. return
  245. }
  246. const monitor = {
  247. loading: true,
  248. total: '-',
  249. online: '-',
  250. offline: '-',
  251. inactive: '-'
  252. }
  253. this.device = null
  254. this.map?.destroy()
  255. this.map = null
  256. this.monitor = monitor
  257. clearTimeout(this.$timer)
  258. this.deviceOptions = { loaded: false }
  259. getDeviceStatisticsByPath(this.group.path).then(({ data }) => {
  260. const { deactivatedTotal, notConnectedTotal, offLineTotal, onLineTotal, total } = data
  261. monitor.total = total
  262. monitor.online = onLineTotal
  263. monitor.offline = offLineTotal + notConnectedTotal
  264. monitor.inactive = deactivatedTotal
  265. }).finally(() => {
  266. monitor.loading = false
  267. if (!this.monitor.loading) {
  268. this.getDevices(this.monitor.total - this.monitor.inactive)
  269. }
  270. })
  271. },
  272. sort (a, b) {
  273. if (a.onlineStatus === b.onlineStatus) {
  274. return a.createTime <= b.createTime ? 1 : -1
  275. }
  276. return a.onlineStatus === 1 ? -1 : 1
  277. },
  278. getDevices (total) {
  279. if (!total || total === '-') {
  280. this.deviceOptions = { list: [], loaded: true }
  281. return
  282. }
  283. const options = { list: [], loaded: false }
  284. this.deviceOptions = options
  285. getDevicesByQuery({
  286. pageNum: 1,
  287. pageSize: total,
  288. activate: 1,
  289. org: this.group.path
  290. }, { custom: true }).then(
  291. ({ data }) => {
  292. const map = {}
  293. const ids = []
  294. options.list = data.sort(this.sort).map(device => {
  295. if (device.onlineStatus === 1) {
  296. device.status = -2
  297. ids.push(device.id)
  298. } else {
  299. device.status = -1
  300. }
  301. device.flag = 0
  302. map[device.id] = device
  303. return device
  304. })
  305. options.power = ids
  306. options.map = map
  307. options.loaded = true
  308. this.getStatusReport(options)
  309. this.initMap()
  310. },
  311. ({ isCancel }) => {
  312. if (!isCancel && !this.monitor.loading) {
  313. this.$timer = setTimeout(total => {
  314. this.getDevices(total)
  315. }, 2000, total)
  316. }
  317. }
  318. )
  319. },
  320. onDeviceChange (device) {
  321. if (!device) {
  322. this.map.setFitView()
  323. return
  324. }
  325. this.activeDevice(device.id, [device.marker])
  326. },
  327. initMap () {
  328. AMapLoader.load({
  329. key: process.env.VUE_APP_GAODE_MAP_KEY,
  330. version: '2.0',
  331. plugins: ['']
  332. }).then(AMap => {
  333. this.map = new AMap.Map(this.$refs.map)
  334. const markerList = []
  335. for (const device of this.mapDevices) {
  336. const marker = new AMap.Marker({
  337. position: [+device.longitude, +device.latitude],
  338. icon: new AMap.Icon({
  339. image: device.onlineStatus === 1 ? onlineIcon : offlineIcon,
  340. size: new AMap.Size(32, 32),
  341. imageSize: new AMap.Size(32, 32)
  342. }),
  343. label: {
  344. content: `<div class='o-online-status--${device.onlineStatus === 1 ? 'online' : 'offline'}'>${device.name}</div>`,
  345. direction: 'top',
  346. offset: new AMap.Pixel(0, -10) // 设置文本标注偏移量
  347. },
  348. extData: {
  349. id: device.id
  350. }
  351. })
  352. marker.on('click', e => {
  353. this.activeDevice(e.target.getExtData().id, e.target)
  354. })
  355. device.marker = marker
  356. markerList.unshift(marker)
  357. }
  358. this.map.add(markerList)
  359. this.map.setFitView()
  360. this.map.on('click', () => {
  361. this.device = null
  362. })
  363. })
  364. },
  365. onPlace () {
  366. this.map?.setFitView()
  367. },
  368. activeDevice (id, marker) {
  369. this.map.setFitView(marker)
  370. this.device = this.mapDevices.find(device => device.id === id)
  371. }
  372. }
  373. }
  374. </script>
  375. <style lang="scss" scoped>
  376. @mixin markLabel {
  377. color: #ffffff;
  378. border: none;
  379. padding: 5px 10px;
  380. }
  381. .c-device-map {
  382. border-radius: $radius $radius 0 0;
  383. background-color: #fff;
  384. &__device {
  385. position: absolute;
  386. top: $spacing;
  387. left: $spacing;
  388. width: $width--lg;
  389. z-index: 9;
  390. }
  391. ::v-deep {
  392. .o-online-status {
  393. &--online {
  394. @include markLabel();
  395. background-color: #333333;
  396. }
  397. &--offline {
  398. @include markLabel();
  399. background-color: #f90c0c;
  400. }
  401. }
  402. .amap-marker-label {
  403. border: none;
  404. padding: 0;
  405. }
  406. }
  407. }
  408. .c-count {
  409. justify-content: space-between;
  410. padding: $spacing--xs $spacing;
  411. &__title {
  412. width: 200px;
  413. }
  414. &__item > div:first-child {
  415. margin-bottom: 10px;
  416. }
  417. }
  418. .o-place {
  419. display: inline-flex;
  420. justify-content: center;
  421. align-items: center;
  422. position: absolute;
  423. right: $spacing;
  424. bottom: $spacing;
  425. width: 48px;
  426. height: 48px;
  427. font-size: 24px;
  428. border-radius: 50%;
  429. background-color: #fff;
  430. z-index: 9;
  431. }
  432. </style>