index.vue 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581
  1. <template>
  2. <div class="layer-list-panel" :class="{ 'fix-layer-list-panel': leftPanelHide }">
  3. <div class="layer-list-panel-title">
  4. <div class="title">
  5. <img src="@/assets/image/common/layerIcon.png" alt="" />
  6. <span style="margin-left: 0.08rem">资源列表</span>
  7. </div>
  8. <div @click="onExpandChange" style="cursor: pointer">
  9. <img v-show="isExpanded" src="@/assets/image/common/arrow-up.png" style="width: 14px; height: 12px" />
  10. <img v-show="!isExpanded" src="@/assets/image/common/arrow-up.png" style="transform: rotate(180deg); width: 14px; height: 12px" />
  11. </div>
  12. </div>
  13. <div class="layer-list-panel-content" v-show="isExpanded">
  14. <el-tree ref="treeRef" :data="treeData" show-checkbox node-key="id" :props="defaultProps" @check="onCheckChange" :default-expanded-keys="['1']">
  15. <template #default="{ node, data }">
  16. <span class="custom-tree-node">
  17. <span>{{ node.label }}</span>
  18. <i v-if="data.loading" class="el-icon-loading" style="margin-left: 4px; color: #409eff"></i>
  19. <i v-if="data.error" class="el-icon-warning" style="margin-left: 4px; color: #f56c6c" :title="data.error"></i>
  20. </span>
  21. </template>
  22. </el-tree>
  23. </div>
  24. </div>
  25. </template>
  26. <script>
  27. import * as mars3d from 'mars3d'
  28. let layerCache = {}
  29. let graphicsLayer = null
  30. let csqGraphicsLayer = null
  31. export default {
  32. name: 'LayerListView',
  33. data() {
  34. return {
  35. leftPanelHide: false,
  36. isExpanded: true,
  37. defaultProps: { children: 'children', label: 'label' },
  38. treeData: [
  39. {
  40. id: '1',
  41. label: '综合信息',
  42. children: [
  43. {
  44. id: '1-1',
  45. label: '生态区',
  46. children: [
  47. { id: '1-1-1', label: '提防背河坡脚线', meta: { type: 'polyline', url: '/sddnWeihe/geojson/提防背河坡脚线.geojson' } },
  48. { id: '1-1-2', label: '河道管理范围线', meta: { type: 'polyline', url: '/sddnWeihe/geojson/河道管理范围线.geojson' } },
  49. { id: '1-1-3', label: '一级管控区界限', meta: { type: 'polyline', url: '/sddnWeihe/geojson/一二级管控区界限.geojson' } },
  50. { id: '1-1-4', label: '生态区界限', meta: { type: 'polyline', url: '/sddnWeihe/geojson/生态区界限.geojson' } },
  51. { id: '1-1-5', label: '城市核心区', meta: { type: 'polyline', url: '/sddnWeihe/geojson/城市核心区.geojson' } },
  52. { id: '1-1-6', label: '农村区段', meta: { type: 'polyline', url: '/sddnWeihe/geojson/农村区段.geojson' } },
  53. { id: '1-1-8', label: '城市核心区(右岸)', meta: { type: 'polyline', url: '/sddnWeihe/geojson/城市核心区右岸.geojson' } },
  54. { id: '1-1-9', label: '农村区段(右岸)', meta: { type: 'polyline', url: '/sddnWeihe/geojson/农村区段右岸.geojson' } }
  55. ]
  56. },
  57. { id: '1-2', label: '监测设备', type: 'monitor' },
  58. {
  59. id: '1-3',
  60. label: '入河排水(污)口',
  61. children: [
  62. { id: '1-3-1', label: '兴平城市总排口', meta: { type: 'point', url: '/sddnWeihe/geojson/兴平城市总排口.geojson' } },
  63. { id: '1-3-2', label: '兴平城市污水处理厂排口', meta: { type: 'point', url: '/sddnWeihe/geojson/兴平城市总排口2.geojson' } },
  64. { id: '1-3-3', label: '新兴纺织园污水处理厂排口', meta: { type: 'point', url: '/sddnWeihe/geojson/新兴纺织园污水处理厂排口.geojson' } }
  65. ]
  66. },
  67. {
  68. id: '1-4',
  69. label: '河道管理站',
  70. children: [
  71. { id: '1-4-1', label: '庄头管理站', meta: { type: 'point', url: '/sddnWeihe/geojson/庄头管理站.geojson' } },
  72. { id: '1-4-2', label: '汤坊管理站', meta: { type: 'point', url: '/sddnWeihe/geojson/汤坊管理站.geojson' } },
  73. { id: '1-4-3', label: '阜寨管理站', meta: { type: 'point', url: '/sddnWeihe/geojson/阜寨管理站.geojson' } }
  74. ]
  75. }
  76. ]
  77. },
  78. {
  79. id: '2',
  80. label: '水文监测',
  81. children: [
  82. { id: '2-1', label: '水文监测点1', meta: { type: 'point', url: '/sddnWeihe/geojson/sw1.geojson' } },
  83. { id: '2-2', label: '水文监测点2', meta: { type: 'point', url: '/sddnWeihe/geojson/sw2.geojson' } }
  84. ]
  85. },
  86. {
  87. id: '3',
  88. label: '采砂区',
  89. children: [
  90. { id: '3-1', label: '兴平市宜空采砂区', type: 'csq', meta: { type: 'polygon', url: '/sddnWeihe/geojson/宜空采砂区.geojson' } },
  91. { id: '3-2', label: '兴平市团结采砂区', type: 'csq', meta: { type: 'polygon', url: '/sddnWeihe/geojson/团结采砂区.geojson' } },
  92. { id: '3-3', label: '兴平市汤坊龙兴1区采砂区', type: 'csq', meta: { type: 'polygon', url: '/sddnWeihe/geojson/坊龙兴1区采砂区.geojson' } }
  93. ]
  94. },
  95. {
  96. id: '4',
  97. label: '防汛应急预案',
  98. type: 'plan',
  99. children: [
  100. { id: '4-1', type: 'plan', label: '2025年度渭河兴平段水灾害防御工作方案' },
  101. { id: '4-2', type: 'plan', label: '2025年度渭河水灾害防御工作方案' },
  102. { id: '4-3', type: 'plan', label: '陕西省防汛应急预案' }
  103. ]
  104. }
  105. ],
  106. mainMenu: '',
  107. checkedNodes: []
  108. }
  109. },
  110. created() {
  111. this.mainMenu = this.$route.params.menu
  112. },
  113. mounted() {
  114. this.$globalEventBus.$on('toggleLeftPanel', (val) => {
  115. this.leftPanelHide = val
  116. })
  117. this.$globalEventBus.$on('closePlanDialog', (data) => {
  118. this.checkedNodes = []
  119. const ids = data.map((item) => item.id)
  120. this.cancelCheckNode(ids)
  121. })
  122. },
  123. watch: {
  124. mainMenu: {
  125. handler(val) {
  126. if (val === 'hydrologicInfo') {
  127. this.handleData(true)
  128. } else {
  129. this.handleData(false)
  130. }
  131. }
  132. }
  133. },
  134. methods: {
  135. //关闭防汛预案弹窗后取消选中
  136. cancelCheckNode(ids) {
  137. const currentCheckedKeys = this.$refs.treeRef.getCheckedKeys(true)
  138. const newArray = currentCheckedKeys.filter((item) => !ids.includes(item))
  139. this.$refs.treeRef.setCheckedKeys(newArray)
  140. },
  141. onExpandChange() {
  142. this.isExpanded = !this.isExpanded
  143. },
  144. async onCheckChange(node, checkData) {
  145. const { checkedKeys, checkedNodes } = checkData
  146. const id = node.id
  147. const isChecked = checkedKeys.includes(id)
  148. // 维护选中的预案节点
  149. this.checkedNodes = checkedNodes.filter((item) => item.type === 'plan' && !item.children)
  150. // ================== 选中 ==================
  151. if (isChecked) {
  152. if (node.type === 'plan') {
  153. this.$globalEventBus.$emit('showPlanDialog', { list: this.checkedNodes })
  154. return
  155. }
  156. if (node.type === 'monitor') {
  157. if (graphicsLayer) {
  158. graphicsLayer.show = true
  159. } else {
  160. this.getMonitorData()
  161. }
  162. return
  163. }
  164. if (node.children?.length) {
  165. // 递归加载所有叶子节点
  166. await this.loadAllLeafLayers(node.children)
  167. return
  168. }
  169. if (node.meta?.url) {
  170. if (layerCache[id]) {
  171. layerCache[id].show = true
  172. } else {
  173. await this.loadGeoJsonLayer(node)
  174. if (node.type === 'csq') {
  175. this.getCsqLayer(node.label)
  176. }
  177. }
  178. }
  179. }
  180. // ================== 取消选中 ==================
  181. else {
  182. if (node.type === 'plan') {
  183. this.$globalEventBus.$emit('showPlanDialog', { list: this.checkedNodes })
  184. return
  185. }
  186. if (node.type === 'monitor') {
  187. this.removeMonitorData()
  188. return
  189. }
  190. if (node.children?.length) {
  191. // 递归删除所有叶子节点
  192. this.removeAllLeafLayers(node.children)
  193. return
  194. }
  195. if (node.type === 'csq') {
  196. this.removeCsqLayer(node.label)
  197. }
  198. this.removeLayer(id)
  199. }
  200. },
  201. // 批量加载叶子节点
  202. async loadAllLeafLayers(children) {
  203. for (const child of children) {
  204. if (child.children?.length) {
  205. await this.loadAllLeafLayers(child.children)
  206. } else if (child.meta?.url) {
  207. if (layerCache[child.id]) {
  208. layerCache[child.id].show = true
  209. } else {
  210. await this.loadGeoJsonLayer(child)
  211. if (child.type === 'csq') {
  212. this.getCsqLayer(child.label)
  213. }
  214. }
  215. }
  216. }
  217. },
  218. // 批量删除叶子节点
  219. removeAllLeafLayers(children) {
  220. for (const child of children) {
  221. if (child.children?.length) {
  222. this.removeAllLeafLayers(child.children)
  223. } else {
  224. this.removeLayer(child.id)
  225. if (child.type === 'csq') {
  226. this.removeCsqLayer(child.label)
  227. }
  228. }
  229. }
  230. },
  231. // 删除图层的通用方法
  232. removeLayer(id) {
  233. if (layerCache[id]) {
  234. try {
  235. window.map.removeLayer(layerCache[id])
  236. } catch (e) {
  237. console.warn(`removeLayer failed: ${id}`, e)
  238. }
  239. delete layerCache[id]
  240. }
  241. },
  242. // 加载GeoJson图层的通用方法
  243. async loadGeoJsonLayer(node) {
  244. const id = node.id
  245. if (layerCache[id]) {
  246. layerCache[id].show = true
  247. return
  248. }
  249. this.$set(node, 'loading', true)
  250. this.$set(node, 'error', '')
  251. try {
  252. const layer = new mars3d.layer.GeoJsonLayer({
  253. id,
  254. name: node.label,
  255. url: node.meta.url,
  256. clampToGround: true,
  257. symbol: this.getStyleByName(node.label),
  258. flyTo: true
  259. })
  260. window.map.addLayer(layer)
  261. this.bindEvent(layer)
  262. layerCache[id] = layer
  263. // 父子关联
  264. const parent = this.$refs.treeRef.getNode(id).parent
  265. if (parent?.data?.id) {
  266. layer.options.parentId = parent.data.id
  267. }
  268. } catch (e) {
  269. this.$set(node, 'error', e.message || '加载失败')
  270. } finally {
  271. this.$set(node, 'loading', false)
  272. }
  273. },
  274. getStyleByName(name) {
  275. if (name === '生态区界限') {
  276. return { type: 'polyline', styleOptions: { color: '#0c5b0f', width: 2 } }
  277. } else if (name === '提防背河坡脚线') {
  278. return { type: 'polyline', styleOptions: { color: '#c53632', width: 2 } }
  279. } else if (name === '河道管理范围线') {
  280. return { type: 'polyline', styleOptions: { color: '#12641c', width: 2 } }
  281. } else if (name === '一级管控区界限') {
  282. return {
  283. type: 'polyline',
  284. styleOptions: { width: 2, materialType: mars3d.MaterialType.PolylineDash, materialOptions: { color: '#12641c', dashLength: 60 } }
  285. }
  286. } else if (name === '城市核心区' || name === '农村区段') {
  287. return {
  288. type: 'polygon',
  289. styleOptions: {
  290. color: '#194830',
  291. opacity: 0.6,
  292. outline: true,
  293. outlineWidth: 1,
  294. outlineColor: '#57e0a9'
  295. }
  296. }
  297. } else if (name === '城市核心区(右岸)' || name === '农村区段(右岸)') {
  298. return {
  299. type: 'polygon',
  300. styleOptions: {
  301. color: '#368d63',
  302. opacity: 0.3,
  303. outline: true,
  304. outlineWidth: 1,
  305. outlineColor: '#57e0a9'
  306. }
  307. }
  308. } else if (name.indexOf('水文') > -1) {
  309. const ptStyle = this.getPointStyle(name)
  310. return {
  311. type: 'billboard',
  312. styleOptions: {
  313. image: require('./image/水文站.png'),
  314. ...ptStyle
  315. }
  316. }
  317. } else if (name.indexOf('排口') > -1) {
  318. const ptStyle = this.getPointStyle(name)
  319. return {
  320. type: 'billboard',
  321. styleOptions: {
  322. image: require('./image/排污口.png'),
  323. ...ptStyle
  324. }
  325. }
  326. } else if (name.indexOf('管理站') > -1) {
  327. const ptStyle = this.getPointStyle(name)
  328. return {
  329. type: 'billboard',
  330. styleOptions: {
  331. image: require('./image/管理站.png'),
  332. ...ptStyle
  333. }
  334. }
  335. } else if (name.indexOf('采砂区') > -1) {
  336. return {
  337. type: 'polygon',
  338. styleOptions: {
  339. label: { text: name, font_size: 14, outline: true, outlineColor: '#000000', outlineWidth: 2 },
  340. clampToGround: true,
  341. materialType: this.mars3d.MaterialType.Grid,
  342. materialOptions: { color: '#cc9648', cellAlpha: 0.5 },
  343. outline: true,
  344. outlineWidth: 1,
  345. outlineColor: '#cc9648'
  346. }
  347. }
  348. }
  349. },
  350. getPointStyle(name) {
  351. return {
  352. scale: 0.8,
  353. label: { text: name, font_size: 14, outline: true, outlineColor: '#000000', outlineWidth: 2, pixelOffsetY: -60 },
  354. clampToGround: true,
  355. horizontalOrigin: this.Cesium.HorizontalOrigin.CENTER,
  356. verticalOrigin: this.Cesium.VerticalOrigin.BOTTOM,
  357. pixelOffset: new this.Cesium.Cartesian2(0, -6), // 偏移量
  358. distanceDisplayCondition: new this.Cesium.DistanceDisplayCondition(0.0, 500000) // 按视距显示
  359. }
  360. },
  361. // 处理水文站数据
  362. handleData(isShow) {
  363. const nodeIds = ['2-1', '2-2']
  364. this.$nextTick(() => {
  365. nodeIds.forEach((id) => {
  366. const node = this.$refs.treeRef.getNode(id)
  367. if (!node) return
  368. // 直接修改 checked 状态
  369. this.$refs.treeRef.setChecked(node, isShow)
  370. // 手动触发 check 事件
  371. this.$refs.treeRef.$emit(
  372. 'check',
  373. node.data, // 选中的节点数据
  374. {
  375. checkedKeys: this.$refs.treeRef.getCheckedKeys(),
  376. checkedNodes: this.$refs.treeRef.getCheckedNodes()
  377. }
  378. )
  379. })
  380. })
  381. },
  382. // 绑定点击事件
  383. bindEvent(layer) {
  384. const _that = this
  385. if (layer.id === '2-1' || layer.id === '2-2') {
  386. layer.on(mars3d.EventType.click, function (event) {
  387. _that.$globalEventBus.$emit('clickWaterStation', event)
  388. })
  389. }
  390. },
  391. getMonitorData() {
  392. graphicsLayer = new this.mars3d.layer.GraphicLayer()
  393. window.map.addLayer(graphicsLayer)
  394. window.requestSDK('/sddnWeiHe/device/deviceSimpleList', { pageNum: 1, pageSize: 10, platFlag: '1', operType: '0' }, {}, 'post').then((res) => {
  395. const data = res.data
  396. data.forEach((point) => {
  397. const graphic = new mars3d.graphic.BillboardEntity({
  398. position: [point.longitude, point.latitude],
  399. style: {
  400. image: require('./image/camera.png'),
  401. scale: 0.8,
  402. clampToGround: true,
  403. horizontalOrigin: this.Cesium.HorizontalOrigin.CENTER,
  404. verticalOrigin: this.Cesium.VerticalOrigin.BOTTOM,
  405. pixelOffset: new this.Cesium.Cartesian2(0, -6), // 偏移量
  406. distanceDisplayCondition: new this.Cesium.DistanceDisplayCondition(0.0, 500000) // 按视距显示
  407. },
  408. attr: { ...point }
  409. })
  410. graphicsLayer.addGraphic(graphic)
  411. let that = this
  412. graphic.on(this.mars3d.EventType.click, function () {
  413. const pointData = graphic.attr
  414. that.fetchUrl(pointData).then((res) => {
  415. if (res.code == 4001) {
  416. that.$message.warning(JSON.parse(res.msg).resultMsg)
  417. } else if (res.code == 400) {
  418. that.$message.error(res.msg)
  419. } else {
  420. const url = res.data.streamUrl
  421. that.$set(pointData, 'url', url)
  422. that.$globalEventBus.$emit('clickVideoPlay', { point: pointData, visible: true, type: 'click' })
  423. }
  424. })
  425. })
  426. })
  427. graphicsLayer.flyTo()
  428. })
  429. },
  430. fetchUrl(item) {
  431. return new Promise((resolve, reject) => {
  432. window
  433. .requestSDK(
  434. '/ttvideo/video/player/getVideoRealtimeUrl',
  435. {
  436. deviceCode: item.deviceCode,
  437. channelCode: item.channelCode,
  438. netType: '1',
  439. protocolType: 5,
  440. streamType: 1
  441. },
  442. {},
  443. 'post'
  444. )
  445. .then(async (res) => {
  446. resolve(res)
  447. })
  448. })
  449. },
  450. removeMonitorData() {
  451. if (graphicsLayer) {
  452. graphicsLayer.show = false
  453. // window.map.removeLayer(graphicsLayer)
  454. }
  455. },
  456. // 采砂区柱状图
  457. getCsqLayer(name) {
  458. const coorList = {
  459. 兴平市宜空采砂区: [108.352091, 34.202407, 100],
  460. 兴平市团结采砂区: [108.360076, 34.20512, 100],
  461. 兴平市汤坊龙兴1区采砂区: [108.474301, 34.21255, 100]
  462. }
  463. const lengthList = {
  464. 兴平市宜空采砂区: 300,
  465. 兴平市团结采砂区: 500,
  466. 兴平市汤坊龙兴1区采砂区: 600
  467. }
  468. if (csqGraphicsLayer) {
  469. csqGraphicsLayer.show = true
  470. } else {
  471. csqGraphicsLayer = new this.mars3d.layer.GraphicLayer()
  472. window.map.addLayer(csqGraphicsLayer)
  473. }
  474. const position = coorList[name]
  475. const graphic = new this.mars3d.graphic.CylinderEntity({
  476. id: name,
  477. position,
  478. style: {
  479. length: lengthList[name],
  480. topRadius: 20.0,
  481. bottomRadius: 20.0,
  482. color: '#85bc68',
  483. opacity: 0.45,
  484. clampToGround: true
  485. }
  486. })
  487. csqGraphicsLayer.addGraphic(graphic)
  488. },
  489. removeCsqLayer(name) {
  490. if (csqGraphicsLayer) {
  491. const graphic = csqGraphicsLayer.getGraphicById(name)
  492. csqGraphicsLayer.removeGraphic(graphic)
  493. }
  494. }
  495. },
  496. destroyed() {
  497. this.$globalEventBus.$off('closePlanDialog')
  498. this.$globalEventBus.$off('toggleLeftPanel')
  499. // 把图层上绑定的事件也清掉
  500. Object.values(layerCache).forEach((l) => l.off('click'))
  501. window.map.removeLayer(graphicsLayer)
  502. graphicsLayer = null
  503. window.map.removeLayer(csqGraphicsLayer)
  504. csqGraphicsLayer = null
  505. }
  506. }
  507. </script>
  508. <style lang="scss" scoped>
  509. .layer-list-panel {
  510. position: absolute;
  511. top: px-to-rem(80);
  512. left: px-to-rem(480);
  513. width: px-to-rem(240);
  514. padding: px-to-rem(10);
  515. background: #1b2535;
  516. border-radius: 6px;
  517. opacity: 0.85;
  518. z-index: 1000;
  519. transition: left 0.3s ease-in-out;
  520. .layer-list-panel-title {
  521. margin-bottom: px-to-rem(10);
  522. height: px-to-rem(30);
  523. line-height: px-to-rem(30);
  524. display: flex;
  525. justify-content: space-between;
  526. align-items: center;
  527. .title {
  528. display: flex;
  529. justify-content: center;
  530. align-items: center;
  531. font-size: px-to-rem(18);
  532. color: #eaf3fe;
  533. }
  534. }
  535. .layer-list-panel-content {
  536. margin: px-to-rem(-10);
  537. padding: px-to-rem(10);
  538. max-height: px-to-rem(340);
  539. overflow: auto;
  540. :deep(.el-tree) {
  541. background: transparent;
  542. color: #fff;
  543. .el-tree-node__content {
  544. height: unset;
  545. }
  546. .el-tree-node__content:hover,
  547. .el-upload-list__item:hover,
  548. .el-tree-node:focus > .el-tree-node__content {
  549. background-color: transparent;
  550. height: unset;
  551. }
  552. .el-tree-node__label {
  553. white-space: wrap;
  554. }
  555. }
  556. .custom-tree-node {
  557. font-size: px-to-rem(16);
  558. overflow: hidden;
  559. white-space: nowrap;
  560. text-overflow: ellipsis;
  561. }
  562. }
  563. }
  564. .fix-layer-list-panel {
  565. left: px-to-rem(20);
  566. }
  567. </style>