<template> <div class="map-container"> <navigationBar></navigationBar> <mars-map :options="options" @onload="mapLoad"></mars-map> <!-- 搜索 --> <div class="search-container" id="box"> <div class="search-input flex-container"> <div class="input-title">点位搜索:</div> <div class="input-box"> <el-input v-model="queryParams.name" placeholder="请输入点位名称" clearable /> <div class="result-popup" v-show="popupShow"> <div class="list" v-if="list.length > 0"> <div class="list-item" v-for="item in list" :key="item.id" @click="handleResultRow(item.id, item.lonLat, item.pointName)" > <div class="enterprise-name">{{ item.pointName }}</div> <div class="loaction-icon"></div> </div> </div> <div class="no-data" v-else>暂无搜索结果</div> </div> </div> <div class="search-bnt input-title" @click="getList()">搜索</div> <div class="search-bnt input-title" style="margin-left: 10px; padding: 5px 10px" @click="handlePointSelect()" > 选择点位 </div> </div> </div> <!-- 树形结构 --> <el-drawer v-model="drawer" title="点位选择" size="20%" :append-to-body="false" > <el-tree node-key="id" :props="props" :load="loadNode" lazy accordion @node-click="handleNodeClick" /> <template #footer> <el-button type="primary" @click="finishTree">确定</el-button> </template> </el-drawer> <!-- 地图图层 --> <div class="map-layer"> <div class="model flex-container"> <div class="model-item" :class="currentModelIndex == index ? 'activeBtn' : ''" v-for="(item, index) in mapModel" :key="index" @click="changeModel(index)" > {{ item }} </div> </div> <div class="point flex-container"> <div class="point-item" :class="currentIndex == index ? 'activeBtn' : ''" v-for="(item, index) in pointLayer" :key="index" @click="handlePointLayer(index, item.value)" > {{ item.name }} </div> </div> </div> <!-- 点位详情 --> <el-dialog v-model="dialogVisible" :title="info.pointName" width="30%" class="info-dialog" > <div class="info-box"> <div class="info-title">基本信息</div> <div class="info-row"> <div class="row-lable">地址:</div> <div class="row-value">{{ info.address }}</div> </div> <div class="info-row"> <div class="row-lable">办公时间:</div> <div class="row-value">{{ info.officeHours }}</div> </div> <div class="info-row"> <div class="row-lable">联系电话:</div> <div class="row-value">{{ info.phone }}</div> </div> <div class="info-row"> <div class="row-lable">封面:</div> <div class="row-value"> <el-image class="info-image" fit="cover" v-if="info.image" :src="`${baseUrl}/api/yzt-data/file/image?name=${info.image}`" :preview-src-list="[ `${baseUrl}/api/yzt-data/file/image?name=${info.image}`, ]" > <div slot="error" class="image-slot"> <i class="el-icon-picture-outline"></i> <span>图片加载出错</span> </div> </el-image> </div> </div> <div class="info-title" v-show="info.yewuList && info.yewuList.length > 0" > 办理业务 </div> <div class="yewu-list" v-for="(item, index) in info.yewuList" :key="index" > <div class="yewu-title">{{ item.title }}</div> <div class="list-sub" v-for="(subItem, subIndex) in item.list" :key="subItem.id" @click="handleInfoByyewu(subItem.id)" > <div class="file-icon"></div> <div>{{ subIndex + 1 }}. {{ subItem.simpleName }}</div> </div> </div> </div> </el-dialog> <!-- 业务详情 --> <el-dialog width="30%" :title="businessInfo.businessName" v-model="innerVisible" class="info-dialog" > <div class="info-box"> <div class="info-row"> <div class="row-lable">业务名称:</div> <div class="row-value">{{ businessInfo.businessName }}</div> </div> <div class="info-row"> <div class="row-lable">业务简称:</div> <div class="row-value">{{ businessInfo.simpleName }}</div> </div> <div class="info-row"> <div class="row-lable">申办条件:</div> <div class="row-value">{{ businessInfo.businessInstructions }}</div> </div> <div class="info-row"> <div class="row-lable">申办要求:</div> <div class="row-value">{{ businessInfo.businessGuide }}</div> </div> <div class="info-row"> <div class="row-lable">相关材料:</div> <div class="row-value value-list"> <div v-for="(item, index) in businessInfo.formList" :key="item.id"> <div class="value-list-title">{{ item.name }}</div> <div class="value-list-subTitle" @click="lookIamge(index)"> 参考样本 </div> </div> </div> </div> <div class="info-row"> <div class="row-lable">法定时限:</div> <div class="row-value">{{ businessInfo.legaltime }}</div> </div> <div class="info-row"> <div class="row-lable">承诺时限:</div> <div class="row-value">{{ businessInfo.promisetime }}</div> </div> </div> <el-image-viewer v-if="showPreview" :url-list="businessInfo.srcList" show-progress :initial-index="initialIndex" @close="showPreview = false" /> </el-dialog> </div> </template> <script setup name="Map"> import NavigationBar from "@/components/NavigationBar"; import marsMap from "@/components/marsMap"; import markerIcon from "@/assets/images/map-marker.png"; const { proxy } = getCurrentInstance(); import { getPointListByType, getPointList, getPointinfo, getdicts, getbusinessById, getEnterpriseTree, } from "@/api/mapApi"; import { ref, reactive, onMounted, onUnmounted } from "vue"; const baseUrl = import.meta.env.VITE_APP_BASE_API; let options = reactive({ scene: { center: { lat: 32.006053, lng: 120.899041, alt: 45187, heading: 0, pitch: -45, }, showSun: false, showMoon: false, showSkyBox: false, showSkyAtmosphere: false, fog: false, backgroundColor: "#363635", // 天空背景色 globe: { baseColor: "#363635", // 地球地面背景色 showGroundAtmosphere: false, enableLighting: false, }, clock: { currentTime: "2023-11-01 12:00:00", // 固定光照时间 }, cameraController: { zoomFactor: 1.5, minimumZoomDistance: 0.1, maximumZoomDistance: 200000, enableCollisionDetection: false, // 允许进入地下 }, }, terrain: { show: false, }, basemaps: [ { id: 2017, pid: 10, name: "蓝色底图", icon: "https://data.mars3d.cn/img/thumbnail/basemap/my_blue.png", type: "gaode", layer: "vec", chinaCRS: "GCJ02", invertColor: true, filterColor: "#015CB3", brightness: 0.6, contrast: 1.8, gamma: 0.3, hue: 1, saturation: 0, show: true, }, ], }); //地图图层 const mapModel = reactive(["二维地图", "三维地图"]); //图标类型图层 const pointLayer = reactive([ { name: "服务点位", value: undefined }, { name: "轨道交通警务站", value: "13" }, { name: "巡防警务站", value: "04" }, ]); //查询条件 let queryParams = reactive({ name: "", type: 0, lonLat: undefined, }); let map = reactive({}); let mapLayer = reactive({}); let list = reactive([]); let popupShow = ref(false); let dictList = reactive([]); let info = reactive({}); let businessInfo = reactive({}); const showPreview = ref(false); let srcList = reactive([]); let initialIndex = ref(0); const dialogVisible = ref(false); const innerVisible = ref(false); let currentIndex = ref(null); let currentModelIndex = ref(0); //树形结构配置 let drawer = ref(false); const props = { label: "simpleName", children: "zones", isLeaf: (data, node) => { return data.isLeaf == 1 ? false : true; }, }; let currentNodeKey = reactive({}); onMounted(() => { //字典 getdictList(); document.addEventListener("click", handleOutsideClick); }); onUnmounted(() => { document.removeEventListener("click", handleOutsideClick); }); /** * * @param index * */ const lookIamge = (index) => { initialIndex.value = index; //阅览的索引 showPreview.value = true; // if (!exampleFile) return; // const imageItem = document.getElementById(`businessImage` + index); // console.log('sssddd',) // imageItem.click(); }; /** * 获取业务详情 * @param id */ const handleInfoByyewu = async (id) => { const res = await getbusinessById({ id, parent: info.parent, }); res.data.srcList = []; if (res.data.formList.length > 0) { res.data.srcList = res.data.formList.map((item) => { return `${baseUrl}/api/yzt-data/file/otherImage?name=${item.exampleFile}&imgname=${item.name}`; }); } businessInfo = res.data; innerVisible.value = true; }; /** * 获取详情 * @param id * @param lonLat */ const getInfo = async (id, lonLat) => { let res = await getPointinfo({ id: id, lonLat: lonLat, }); res.data.yewuList = []; for (let key in res.data.object) { res.data.yewuList.push({ title: filterTypeName(key), list: res.data.object[key], }); } return res.data; }; /** * 字典翻译 * @param key */ const filterTypeName = (key) => { if (!key) return; const index = dictList.findIndex((item) => item.value == key); if (index > -1) { return dictList[index].title; } else { return "未翻译到"; } }; /** * 单击查询结构 * @param id * @param lonLat * @param pointName */ const handleResultRow = (id, lonLat, pointName) => { searchMarker(id, lonLat); popupShow.value = false; }; /** * 检索图标 * @param id * @param lonLat */ const searchMarker = async (id, lonLat) => { const info = await getInfo(id, lonLat); queryParams.name = info.pointName; //查询图标图层上是否存在,如果不存在就新增一个新图标 const markerLength = mapLayer.markerLayer.length; const markerItem = mapLayer.markerLayer.getGraphicById(info.id); if (markerItem) { markerItem.openHighlight(); map.flyToPoint(markerItem._point, { radius: 3000, duration: 1, }); } else { initMarker(info, true, markerLength); } }; /** * 渲染图标 * @param item * @param flyTo * @param markerLength */ const initMarker = (item, flyTo = false, markerLength = 0) => { mapLayer.markerLayer.enabledEvent = false; const graphic = new mars3d.graphic.BillboardPrimitive({ id: item.id, position: item.lonLat.split(","), style: { width: 35, height: 35, scale: 1, image: markerIcon, horizontalOrigin: Cesium.HorizontalOrigin.CENTER, verticalOrigin: Cesium.VerticalOrigin.BOTTOM, label: { text: item.pointName, horizontalOrigin: Cesium.HorizontalOrigin.CENTER, verticalOrigin: Cesium.VerticalOrigin.BOTTOM, font_size: 14, color: "#fff", pixelOffsetY: -45, }, highlight: { label: { outline: true, outlineColor: "yellow", outlineOpacity: 0.7, }, }, }, flyTo: flyTo, flyToOptions: { radius: 3000, duration: 1, }, attr: item, }); mapLayer.markerLayer.addGraphic(graphic); mapLayer.markerLayer.enabledEvent = true; // 恢复事件 //图标单击事件 graphic.on(mars3d.EventType.click, async (event) => { const infoItem = event.target.attr; info = await getInfo(infoItem.id, infoItem.lonLat); queryParams.name = info.pointName; dialogVisible.value = true; }); if (markerLength > 0) { graphic.openHighlight(); } }; /** * 监听搜索结果弹出层 * @param event */ const handleOutsideClick = (event) => { const box = document.getElementById("box"); if (popupShow.value && box && !box.contains(event.target)) { popupShow.value = false; } }; /** * 点图层切换 * @param index * @param value */ const handlePointLayer = (index, value) => { if (currentIndex.value != index) { currentIndex.value = index; getMarkerType(value); } }; const getMarkerType = async (type) => { const res = await getPointListByType({ type }); mapLayer.markerLayer.clear(); res.data.map((item, index) => { if (index == 0) { map.flyToPoint(item.lonLat.split(","), { radius: 30738, duration: 1, }); } initMarker(item); }); }; /** * 点位选择 */ const handlePointSelect = () => { drawer.value = true; }; /** * 懒加载树形结构 * @param node * @param resolve */ const loadNode = async (node, resolve) => { if (node.level === 0) { // 获取第一层数据 const res = await getEnterpriseTree(); return resolve(res.data); } if (node.level > 0 && node.data.isLeaf == 0) return resolve([]); //其他层 const treeItem = await getEnterpriseTree(node.level + 1, { parent: node.data.parent, child: node.data.child, }); return resolve(treeItem.data); }; /** * 树被单击 * @param data */ const handleNodeClick = (data) => { currentNodeKey = data; }; /** * 树结构选择完毕 */ const finishTree = () => { if (!currentNodeKey.id) { proxy.$modal.msgError("该数据缺少唯一id!"); } else { searchMarker(currentNodeKey.id, currentNodeKey.lonLat); } drawer.value = false; }; /** * 二三维地图变化 * @param index */ const changeModel = (index) => { if (currentModelIndex.value != index) { currentModelIndex.value = index; if (index == 1) { if (mapLayer.tiles3dLayer == undefined) { mapLayer.tiles3dLayer = new mars3d.layer.TilesetLayer({ name: "模型名称", url: "http://localhost:9090/B3dmqlh06/tileset.json", maximumScreenSpaceError: 16, maxMemory: 1024, // 最大缓存内存大小(MB) matrixMove: { hasMiddle: false, }, flyTo: true, // cacheBytes: 1073741824, // 1024MB = 1024*1024*1024 【重要】额定显存大小(以字节为单位),允许在这个值上下波动。 // maximumCacheOverflowBytes: 2147483648, // 2048MB = 2048*1024*1024 【重要】最大显存大小(以字节为单位)。 // maximumMemoryUsage: 512, //【cesium 1.107+弃用】内存建议显存大小的50%左右,内存分配变小有利于倾斜摄影数据回收,提升性能体验 // skipLevelOfDetail: true, //是Cesium在1.5x 引入的一个优化参数,这个参数在金字塔数据加载中,可以跳过一些级别,这样整体的效率会高一些,数据占用也会小一些。但是带来的异常是:1) 加载过程中闪烁,看起来像是透过去了,数据载入完成后正常。2,有些异常的面片,这个还是因为两级LOD之间数据差异较大,导致的。当这个参数设置false,两级之间的变化更平滑,不会跳跃穿透,但是清晰的数据需要更长,而且还有个致命问题,一旦某一个tile数据无法请求到或者失败,导致一直不清晰。所以我们建议:对于网络条件好,并且数据总量较小的情况下,可以设置false,提升数据显示质量。 // loadSiblings: true, // 如果为true则不会在已加载完模型后,自动从中心开始超清化模型 // cullRequestsWhileMoving: true, // cullRequestsWhileMovingMultiplier: 10, //【重要】 值越小能够更快的剔除 // preferLeaves: true, //【重要】这个参数默认是false,同等条件下,叶子节点会优先加载。但是Cesium的tile加载优先级有很多考虑条件,这个只是其中之一,如果skipLevelOfDetail=false,这个参数几乎无意义。所以要配合skipLevelOfDetail=true来使用,此时设置preferLeaves=true。这样我们就能最快的看见符合当前视觉精度的块,对于提升大数据以及网络环境不好的前提下有一点点改善意义。 // progressiveResolutionHeightFraction: 0.5, //【重要】 数值偏于0能够让初始加载变得模糊 // dynamicScreenSpaceError: true, // true时会在真正的全屏加载完之后才清晰化模型 // preloadWhenHidden: true, //tileset.show是false时,也去预加载数据 }); map.addLayer(mapLayer.tiles3dLayer); } else { mapLayer.tiles3dLayer.show = true; map.sceneOptionsmap({ center: { lat: 31.967646, lng: 120.847855, alt: 1250, heading: 6.6, pitch: -18.8, }, }); } } else { if (mapLayer.tiles3dLayer) { mapLayer.tiles3dLayer.show = false; } } } }; const laoding3d = () => { const tiles3dLayer = new mars3d.layer.TilesetLayer({ name: "模型名称", url: "http://192.168.0.108:9090/B3dmqlh06/tileset.json", maximumScreenSpaceError: 16, maxMemory: 1024, // 最大缓存内存大小(MB) matrixMove: { hasMiddle: false, }, flyTo: true, // maximumScreenSpaceError: 16, // 【重要】数值加大,能让最终成像变模糊 // cacheBytes: 1073741824, // 1024MB = 1024*1024*1024 【重要】额定显存大小(以字节为单位),允许在这个值上下波动。 // maximumCacheOverflowBytes: 2147483648, // 2048MB = 2048*1024*1024 【重要】最大显存大小(以字节为单位)。 // maximumMemoryUsage: 512, //【cesium 1.107+弃用】内存建议显存大小的50%左右,内存分配变小有利于倾斜摄影数据回收,提升性能体验 // skipLevelOfDetail: true, //是Cesium在1.5x 引入的一个优化参数,这个参数在金字塔数据加载中,可以跳过一些级别,这样整体的效率会高一些,数据占用也会小一些。但是带来的异常是:1) 加载过程中闪烁,看起来像是透过去了,数据载入完成后正常。2,有些异常的面片,这个还是因为两级LOD之间数据差异较大,导致的。当这个参数设置false,两级之间的变化更平滑,不会跳跃穿透,但是清晰的数据需要更长,而且还有个致命问题,一旦某一个tile数据无法请求到或者失败,导致一直不清晰。所以我们建议:对于网络条件好,并且数据总量较小的情况下,可以设置false,提升数据显示质量。 // loadSiblings: true, // 如果为true则不会在已加载完模型后,自动从中心开始超清化模型 // cullRequestsWhileMoving: true, // cullRequestsWhileMovingMultiplier: 10, //【重要】 值越小能够更快的剔除 // preferLeaves: true, //【重要】这个参数默认是false,同等条件下,叶子节点会优先加载。但是Cesium的tile加载优先级有很多考虑条件,这个只是其中之一,如果skipLevelOfDetail=false,这个参数几乎无意义。所以要配合skipLevelOfDetail=true来使用,此时设置preferLeaves=true。这样我们就能最快的看见符合当前视觉精度的块,对于提升大数据以及网络环境不好的前提下有一点点改善意义。 // progressiveResolutionHeightFraction: 0.5, //【重要】 数值偏于0能够让初始加载变得模糊 // dynamicScreenSpaceError: true, // true时会在真正的全屏加载完之后才清晰化模型 // preloadWhenHidden: true, //tileset.show是false时,也去预加载数据 }); map.addLayer(tiles3dLayer); }; /** * 地图初始化完毕 * @param mapInstance */ const mapLoad = (mapInstance) => { map = mapInstance; //创建marker图层 mapLayer.markerLayer = new mars3d.layer.GraphicLayer({ allowDrillPick: true, // 如果存在坐标完全相同的图标点,可以打开该属性,click事件通过graphics判断 }); map.addLayer(mapLayer.markerLayer); }; /** * 根据关键字搜索 */ const getList = async () => { if (!queryParams.name) { proxy.$modal.msgError("请先输入点位名称"); return; } let res = await getPointList(queryParams); list = res.data; popupShow.value = true; }; /** * 字典 */ const getdictList = async () => { let res = await getdicts(); dictList = res.result; }; </script> <style scoped lang="scss"> .map-container { position: relative; height: 100%; background: #0059a2; .search-container { position: absolute; top: 150px; left: 50%; transform: translateX(-50%); width: 900px; background: rgba(15, 42, 79, 0.9); border: 1px solid #094edb; z-index: 100; padding: 13px 18px; .input-title { font-size: 16px; color: #fff; } .search-bnt { cursor: pointer; background: url("@/assets/images/btn_bg.png"); padding: 5px 16px; background-size: 100% 100%; } .input-box { position: relative; flex: 1; margin: 0 10px; .result-popup { position: absolute; top: 55px; width: 100%; height: 500px; border: 1px solid 094edb; background-color: rgba(15, 42, 79, 0.9); border: 1px solid #094edb; border-radius: 4px; box-sizing: border-box; padding: 6px 10px; display: flex; align-items: center; justify-content: center; .list { width: 100%; height: 100%; overflow-y: auto; } .no-data { font-size: 16px; color: #fff; letter-spacing: 1px; } .list-item { display: flex; align-items: center; justify-content: space-between; cursor: pointer; font-size: 16px; color: #fff; padding: 13px 6px; border-bottom: 1px solid rgba(3, 91, 178, 0.5); .loaction-icon { display: none; height: 25px; width: 25px; background: url("@/assets/images/loaction.png"); background-size: 100% 100%; } } .list-item:hover { border-radius: 4px; background-color: #409eff; .loaction-icon { display: block; } } } } } .map-layer { position: absolute; right: 30px; bottom: 30px; .model { width: 55%; margin-left: auto; margin-bottom: 15px; } & > div { background: rgba(15, 42, 79, 0.9); border: 1px solid #094edb; & > div { position: relative; cursor: pointer; padding: 6px 15px; font-size: 16px; color: #fff; } & > div:not(:last-child)::after { position: absolute; top: 50%; right: 0; transform: translateY(-50%); content: "|"; font-size: 14px; color: #409eff; } .activeBtn { background: #409eff; } } } } .flex-container { display: flex; align-items: center; } ::v-deep .el-drawer { // width: 20% !important; background: rgba(15, 42, 79, 1); border: 1px solid #094edb; .el-drawer__header { color: #fff !important; font-weight: bold; font-size: 22px; } .drawer-btn { position: absolute; right: 20px; bottom: 20px; } } ::v-deep .el-tree { background: transparent; color: #fff; font-size: 16px; } ::v-deep .el-tree-node__content { padding-top: 20px !important; padding-bottom: 20px !important; } ::v-deep .el-tree-node__content:hover { background: #409eff !important; } ::v-deep .el-tree-node:focus > .el-tree-node__content { background: #409eff !important; } ::v-deep .info-dialog { background: rgba(15, 42, 79, 1); border: 1px solid #094edb; .el-dialog__title, .el-dialog__close { color: #fff !important; font-weight: bold; font-size: 22px; } .info-title { font-size: 16px; color: #fff; font-weight: 550; margin-bottom: 15px; } .info-box { max-height: 500px; overflow-y: auto; } .info-row { display: flex; align-items: flex-start; margin-bottom: 10px; font-size: 14px; color: #fff; .row-lable { width: 75px; color: #9acfff; } .row-value { flex: 1; } .value-list { & > div { padding: 10px 0; display: flex; align-items: center; justify-content: space-between; border-bottom: 1px solid rgba(3, 91, 178, 0.5); .value-list-title { flex: 1; } .value-list-subTitle { color: #094edb; margin-left: 3px; cursor: pointer; } } } } ::v-deep .image-slot { height: 100%; width: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center; font-size: 20px; border: 1px solid #094edb; } .yewu-list { .yewu-title { margin: 15px 0; font-size: 15px; color: #fff; border-left: 5px solid #409eff; padding-left: 10px; } .list-sub { cursor: pointer; display: flex; align-items: center; font-size: 14px; color: #fff; padding: 10px 0; border-bottom: 1px solid rgba(3, 91, 178, 0.5); } .file-icon { margin-right: 10px; height: 20px; width: 20px; background: url("@/assets/images/file.png"); background-size: 100% 100%; } } } // 滚动条 /* 设置滚动条整体样式 */ ::-webkit-scrollbar { width: 8px; /* 滚动条宽度 */ } /* 设置滚动条轨道 */ ::-webkit-scrollbar-track { background: transparent; /* 轨道背景颜色 */ } /* 设置滚动条滑块 */ ::-webkit-scrollbar-thumb { background: #094edb; /* 滑块背景颜色 */ border-radius: 5px; /* 滑块圆角 */ } /* 鼠标悬停在滑块上时的样式 */ ::-webkit-scrollbar-thumb:hover { background: #02a1cb; /* 悬停时滑块背景颜色 */ } </style>