Files
td_official/src/components/openLayersMap/index.vue

536 lines
18 KiB
Vue
Raw Normal View History

2025-05-21 11:24:53 +08:00
<template>
<div class="flex justify-between" v-loading="treeLoading">
<el-tree-v2
style="width: 340px; overflow: auto"
show-checkbox
:data="jsonData"
:height="500"
@check-change="handleCheckChange"
:props="treeProps"
@node-contextmenu="showMenu"
ref="treeRef"
@node-click="isMenuVisible = false"
>
<template #default="{ node, data }">
2025-05-27 09:16:44 +08:00
<span @dblclick="handlePosition(data, node)">{{ data.name }}</span>
2025-05-21 11:24:53 +08:00
</template>
</el-tree-v2>
<div>
<div class="ol-map" id="olMap"></div>
2025-05-27 09:16:44 +08:00
<div class="h15 mt-2" v-if="!selectLayer.length"></div>
<div class="m-0 c-white text-3 flex w-237.5 mt-2 flex-wrap" v-else>
2025-05-21 11:24:53 +08:00
<p
v-for="(item, index) in selectLayer"
2025-05-27 09:16:44 +08:00
class="pl-xl border-rd pr p-3 w-111 mr-1 bg-#909399 flex items-center cursor-pointer justify-between"
2025-05-21 11:24:53 +08:00
@click="delLayer(index)"
>
{{ item.location.name + '被选中为' + item.option }}
<el-icon>
<Close />
</el-icon>
</p>
</div>
<el-form-item label="类型" class="items-center">
<el-radio-group v-model="layerType">
<el-radio value="1" size="large">光伏板</el-radio>
<el-radio value="2" size="large">桩点/支架</el-radio>
<el-radio value="3" size="large">方阵</el-radio>
2025-05-27 09:16:44 +08:00
<el-radio value="4" size="large">逆变器</el-radio>
<el-radio value="5" size="large">箱变</el-radio>
2025-05-21 11:24:53 +08:00
</el-radio-group>
</el-form-item>
</div>
<div v-if="isMenuVisible" :style="{ left: menuX + 'px', top: menuY + 'px' }" class="fixed bg-white shadow-md rounded-md overflow-hidden">
<ul class="py-1 pl-0">
<li class="px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 cursor-pointer" @click="handleMenuItemClick('光伏板')">
<i class="fa-solid fa-check mr-2"></i>光伏板
</li>
<li class="px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 cursor-pointer" @click="handleMenuItemClick('桩点/支架')">
<i class="fa-solid fa-times mr-2"></i>桩点/支架
</li>
<li class="px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 cursor-pointer" @click="handleMenuItemClick('方阵')">
<i class="fa-solid fa-times mr-2"></i>方阵
</li>
2025-05-27 09:16:44 +08:00
<li class="px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 cursor-pointer" @click="handleMenuItemClick('逆变器')">
<i class="fa-solid fa-times mr-2"></i>逆变器
</li>
<li class="px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 cursor-pointer" @click="handleMenuItemClick('箱变')">
<i class="fa-solid fa-times mr-2"></i>箱变
</li>
2025-05-21 11:24:53 +08:00
<li class="px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 cursor-pointer" @click="handleMenuItemClick('名称')">
<i class="fa-solid fa-times mr-2"></i>名称
</li>
</ul>
</div>
</div>
<div class="float-right">
<el-button @click="emit('close')">取消</el-button>
<el-button type="primary" @click="addFacilities" :loading="loading">确定</el-button>
</div>
</template>
<script lang="ts" setup>
import Map from 'ol/Map'; // OpenLayers的主要类用于创建和管理地图
import View from 'ol/View'; // OpenLayers的视图类定义地图的视图属性
import { Tile as TileLayer } from 'ol/layer'; // OpenLayers的瓦片图层类
import { XYZ, OSM } from 'ol/source'; // OpenLayers的瓦片数据源包括XYZ格式和OpenStreetMap专用的数据源
import { fromLonLat, toLonLat } from 'ol/proj'; // OpenLayers的投影转换函数用于经纬度坐标和投影坐标之间的转换
import { defaults as defaultInteractions, DragRotateAndZoom } from 'ol/interaction'; // OpenLayers的交互类包括默认的交互集合和特定的旋转缩放交互
import { defaults as defaultControls, defaults, FullScreen, MousePosition, ScaleLine } from 'ol/control'; // OpenLayers的控件类包括默认的控件集合和特定的全屏、鼠标位置、比例尺控件
import Feature from 'ol/Feature'; // OpenLayers的要素类表示地图上的一个对象或实体
import Point from 'ol/geom/Point'; // OpenLayers的点几何类用于表示点状的地理数据
import { Vector as VectorLayer } from 'ol/layer'; // OpenLayers的矢量图层类用于显示矢量数据
import { Vector as VectorSource } from 'ol/source'; // OpenLayers的矢量数据源类用于管理和提供矢量数据
import { Circle, Style, Stroke, Fill, Icon, Text } from 'ol/style'; // OpenLayers的样式类用于定义图层的样式包括圆形样式、基本样式、边框、填充和图标
import LineString from 'ol/geom/LineString'; // OpenLayers的线几何类用于表示线状的地理数据
import Polygon from 'ol/geom/Polygon'; // OpenLayers的多边形几何类用于表示面状的地理数据
import * as turf from '@turf/turf';
2025-05-27 09:16:44 +08:00
import { FeatureCollection } from 'geojson';
2025-05-21 11:24:53 +08:00
import { TreeInstance } from 'element-plus';
2025-05-27 09:16:44 +08:00
import { addProjectFacilities, addProjectPilePoint, addProjectSquare, listDXFProject, addInverter, addBoxTransformer } from '@/api/project/project';
2025-05-27 19:53:19 +08:00
import { BatchUploader } from '@/utils/batchUpload';
2025-05-21 11:24:53 +08:00
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const props = defineProps({
projectId: String,
designId: String
});
const treeData = ref<any>([]);
const layerType = ref(null);
const contextMenu = ref(null);
const selectLayer = ref([]);
const treeRef = ref<TreeInstance>();
const treeProps = {
value: 'name'
};
const loading = ref(false);
const treeLoading = ref(false);
const emit = defineEmits(['handleCheckChange', 'close']);
let map: any = null;
const layerData = reactive<any>({});
const centerPosition = ref(fromLonLat([107.13761560163239, 23.80480003743964]));
const jsonData = computed(() => {
let id = 0;
let arr = [];
treeData.value.forEach((item: any, index: any) => {
arr.push({
name: item.name,
index
});
for (const itm of item.features) {
if (itm.geometry.id) {
break;
}
itm.geometry.id = ++id;
itm.geometry.coordinates = convertStrToNum(itm.geometry.coordinates);
}
});
return arr; // treeData.value;
});
console.log(jsonData);
2025-05-27 09:16:44 +08:00
const handlePosition = (data: any, node) => {
2025-05-21 11:24:53 +08:00
//切换中心点
2025-05-27 09:16:44 +08:00
const featureCollection: FeatureCollection = { type: 'FeatureCollection', features: treeData.value[data.index].features } as FeatureCollection;
centerPosition.value = fromLonLat(turf.center(featureCollection).geometry.coordinates);
2025-05-21 11:24:53 +08:00
map.getView().setCenter(centerPosition.value);
};
const handleCheckChange = (data: any, bool) => {
2025-05-27 09:16:44 +08:00
// 处理树形结构的选中变化
2025-05-21 11:24:53 +08:00
let features = treeData.value[data.index].features;
if (isMenuVisible.value) isMenuVisible.value = false;
if (bool) {
if (!layerData[features[0].properties.id]) {
features.forEach((item: any) => {
creatPoint(item.geometry.coordinates, item.geometry.type, item.geometry.id, item.properties.text);
});
} else {
features.forEach((item: any) => {
map.addLayer(layerData[item.geometry.id]);
});
}
} else {
features.forEach((item, index) => {
map.removeLayer(layerData[item.geometry.id]);
});
}
// creatPoint(fromLonLat(data.geometry.coordinates), data.geometry.type);
};
function initOLMap() {
// 创造地图实例
map = new Map({
// 设置地图容器的ID
target: 'olMap',
// 定义地图的图层列表,用于显示特定的地理信息。
layers: [
// 高德地图
// TileLayer表示一个瓦片图层它由一系列瓦片通常是图片组成用于在地图上显示地理数据。
new TileLayer({
// 设置图层的数据源为XYZ类型。XYZ是一个通用的瓦片图层源它允许你通过URL模板来获取瓦片
source: new XYZ({
url: 'https://webrd04.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=1&style=7&x={x}&y={y}&z={z}'
})
})
],
// 设置地图的视图参数
// View表示地图的视图它定义了地图的中心点、缩放级别、旋转角度等参数。
view: new View({
// fromLonLat是一个函数用于将经纬度坐标转换为地图的坐标系统。
center: centerPosition.value, //地图中心点
zoom: 15, // 缩放级别
minZoom: 0, // 最小缩放级别
// maxZoom: 18, // 最大缩放级别
constrainResolution: true // 因为存在非整数的缩放级别所以设置该参数为true来让每次缩放结束后自动缩放到距离最近的一个整数级别这个必须要设置当缩放在非整数级别时地图会糊
// projection: 'EPSG:4326' // 投影坐标系默认是3857
}),
// 按住shift进行旋转
// interactions: defaultInteractions().extend([new DragRotateAndZoom()]),
// 控件
// controls: defaults().extend([
// new FullScreen(), // 全屏
// new MousePosition(), // 显示鼠标当前位置的地图坐标
// new ScaleLine() // 显示比例尺
// ])
//加载控件到地图容器中
controls: defaultControls({
zoom: false,
rotate: false,
attribution: false
}).extend([
new FullScreen() // 全屏
])
});
// 事件
// map.on('moveend', (e: any) => {
// // console.log('地图移动', e);
// // 获取当前缩放级别
// var zoomLevel = map.getView().getZoom();
// // console.log('当前缩放级别:', zoomLevel);
// });
// map.on('rendercomplete', () => {
// // console.log('渲染完成');
// });
// map.on('click', (e: any) => {
// var coordinate = e.coordinate;
// // 将投影坐标转换为经纬度坐标
// var lonLatCoordinate = toLonLat(coordinate);
// // 输出转换后的经纬度坐标
// console.log('经纬度坐标:', lonLatCoordinate);
// });
}
//递归字符串数组变成数字
function convertStrToNum(arr) {
if (typeof arr === 'string') {
const num = Number(arr);
return isNaN(num) ? arr : num;
} else if (Array.isArray(arr)) {
return arr.map((item) => convertStrToNum(item));
}
return arr;
}
/**
* 创建图层
* @param {*} pointObj 坐标数组
* @param {*} type 类型
* @param {*} id 唯一id
* @param {*} name 名称
* */
const creatPoint = (pointObj: Array<any>, type: string, id: string, name?: string) => {
// 创建多边形的几何对象
let polygon;
if (type === 'Point') {
polygon = new Point(fromLonLat(pointObj));
} else if (type === 'LineString') {
const lineStringData = pointObj.map((arr: any) => fromLonLat(arr));
polygon = new Polygon([lineStringData]);
} else {
const polygonData = pointObj.map((arr: any) => arr.map((i: any) => fromLonLat(i)));
polygon = new Polygon(polygonData);
}
// 创建特征Feature
let polygonFeature = new Feature({
geometry: polygon
});
const pointStyle = new Style({
image: new Circle({
radius: 2,
fill: new Fill({
color: 'red'
})
}),
text: new Text({
font: '12px Microsoft YaHei',
text: name,
scale: 1,
fill: new Fill({
color: '#7bdd63'
})
})
});
const polygonStyle = new Style({
stroke: new Stroke({
color: type === 'LineString' ? 'skyblue' : 'purple', // 多边形边界线的颜色
width: 2 // 多边形边界线的宽度
}),
fill: new Fill({
color: 'transparent' // 多边形填充颜色,这里设置为半透明红色
})
});
// 设置多边形的样式Style
polygonFeature.setStyle(type === 'Point' ? pointStyle : polygonStyle);
// 创建和添加特征到源Source
let source = new VectorSource();
source.addFeature(polygonFeature);
// 创建图层并设置源Layer
layerData[id] = new VectorLayer();
layerData[id].setSource(source);
map.addLayer(layerData[id]);
};
// 控制菜单是否显示
const isMenuVisible = ref(false);
// 菜单的 x 坐标
const menuX = ref(0);
// 菜单的 y 坐标
const menuY = ref(0);
// 显示菜单的方法
const showMenu = (event: MouseEvent, data) => {
console.log('🚀 ~ showMenu ~ data:', data, treeData.value[data.index]);
contextMenu.value = data;
isMenuVisible.value = true;
menuX.value = event.clientX;
menuY.value = event.clientY;
};
// 处理菜单项点击事件的方法
const handleMenuItemClick = (option: string) => {
isMenuVisible.value = false;
2025-05-27 09:16:44 +08:00
if (selectLayer.value.some((item) => item.location.name === contextMenu.value.name)) {
return proxy?.$modal.msgError('已选择该图层,请勿重复选择');
}
2025-05-27 19:53:19 +08:00
if (selectLayer.value.some((item) => item.option !== '名称' && item.option !== '箱变')) {
if (option !== '名称' && option !== '箱变') return proxy?.$modal.msgError('只能选择一个类型');
2025-05-21 11:24:53 +08:00
}
selectLayer.value.push({ location: contextMenu.value, option });
2025-05-27 09:16:44 +08:00
console.log('selectLayer.value', selectLayer.value);
2025-05-21 11:24:53 +08:00
emit('handleCheckChange', selectLayer.value);
};
//删除菜单
const delLayer = (index) => {
selectLayer.value.splice(index, 1);
emit('handleCheckChange', selectLayer.value);
};
// 点击页面其他区域隐藏菜单
const closeMenuOnClickOutside = (event: MouseEvent) => {
if (isMenuVisible.value) {
const menuElement = document.querySelector('.fixed.bg-white');
if (menuElement && !menuElement.contains(event.target as Node)) {
isMenuVisible.value = false;
}
}
};
// 添加全局点击事件监听器
window.addEventListener('click', closeMenuOnClickOutside);
const getTreeData = async () => {
treeLoading.value = true;
try {
const res = await listDXFProject(props.designId);
treeData.value = res.data.layers;
treeLoading.value = false;
} catch (err) {
treeLoading.value = false;
}
};
// 组件卸载时移除事件监听器
const onUnmounted = () => {
window.removeEventListener('click', closeMenuOnClickOutside);
};
2025-05-27 09:16:44 +08:00
type LayerConfig = {
optionB: string;
apiFunc: (data: any) => Promise<any>;
};
2025-05-21 11:24:53 +08:00
2025-05-27 09:16:44 +08:00
const LAYER_CONFIG: Record<number, LayerConfig> = {
1: { optionB: '光伏板', apiFunc: addProjectFacilities },
3: { optionB: '方阵', apiFunc: addProjectSquare },
4: { optionB: '逆变器', apiFunc: addInverter },
5: { optionB: '箱变', apiFunc: addBoxTransformer }
};
const showError = (msg: string) => proxy?.$modal.msgError(msg);
const showSuccess = (msg: string) => proxy?.$modal.msgSuccess(msg);
const getGeoJsonData = (nameOption = '名称', secondOption: string): { nameGeoJson: any[]; locationGeoJson: any | null } | null => {
const nameLayers = selectLayer.value.filter((item) => item.option === nameOption);
2025-05-27 19:53:19 +08:00
const secondLayer = selectLayer.value.filter((item) => item.option === secondOption);
2025-05-27 09:16:44 +08:00
if (!nameLayers.length || !secondLayer) {
showError(`请选择${nameOption}${secondOption}`);
return null;
2025-05-21 11:24:53 +08:00
}
2025-05-27 09:16:44 +08:00
const nameGeoJson = nameLayers.map((item) => treeData.value[item.location.index]);
2025-05-27 19:53:19 +08:00
const locationGeoJson = secondLayer.map((item) => treeData.value[item.location.index]);
2025-05-27 09:16:44 +08:00
return { nameGeoJson, locationGeoJson };
};
const handleTwoLayerUpload = async (optionB: string, apiFunc: (data: any) => Promise<any>) => {
const geoJson = getGeoJsonData('名称', optionB);
if (!geoJson) return;
const data = {
projectId: props.projectId,
nameGeoJson: geoJson.nameGeoJson,
locationGeoJson: geoJson.locationGeoJson,
pointGeoJson: null
};
loading.value = true;
await apiFunc(data);
await showSuccess('添加成功');
};
const handlePointUpload = async () => {
if (selectLayer.value.length > 1) return showError('最多选择一个桩点/支架');
if (selectLayer.value[0].option !== '桩点/支架') return showError('请选择类型为桩点/支架');
2025-05-27 19:53:19 +08:00
const features = treeData.value[selectLayer.value[0].location.index]?.features || [];
if (!features.length) return showError('桩点数据为空');
2025-05-21 11:24:53 +08:00
2025-05-27 09:16:44 +08:00
loading.value = true;
2025-05-27 19:53:19 +08:00
const sessionId = new Date().getTime().toString(36) + Math.random().toString(36).substring(2, 15);
const uploader = new BatchUploader({
dataList: features,
chunkSize: 15000,
delay: 200,
uploadFunc: async (chunk, batchNum, totalBatch) => {
await addProjectPilePoint({
projectId: props.projectId,
locationGeoJson: {
type: 'FeatureCollection',
features: chunk
},
sessionId,
totalBatch,
batchNum
});
},
onComplete: () => {
showSuccess('桩点上传完成');
reset();
loading.value = false;
}
});
await uploader.start();
2025-05-27 09:16:44 +08:00
};
2025-05-21 11:24:53 +08:00
2025-05-27 09:16:44 +08:00
const addFacilities = async () => {
if (!layerType.value) return showError('请选择图层类型');
if (!selectLayer.value.length) return showError('请选择需要上传的图层');
const config = LAYER_CONFIG[layerType.value];
try {
if (layerType.value == 2) {
await handlePointUpload();
} else if (config) {
await handleTwoLayerUpload(config.optionB, config.apiFunc);
2025-05-21 11:24:53 +08:00
} else {
2025-05-27 09:16:44 +08:00
showError('不支持的图层类型');
2025-05-21 11:24:53 +08:00
}
2025-05-27 09:16:44 +08:00
} finally {
reset();
loading.value = false;
2025-05-21 11:24:53 +08:00
}
};
const reset = () => {
selectLayer.value = [];
treeRef.value?.setCheckedKeys([]);
for (const key in layerData) {
map.removeLayer(layerData[key]);
}
layerType.value = null;
};
watch(
() => props.designId,
(newId, oldId) => {
if (newId !== oldId) {
reset();
getTreeData();
}
},
{ immediate: true }
);
onMounted(() => {
// 地图初始化
initOLMap();
2025-05-27 09:16:44 +08:00
// creatPoint(
// [
// [
// [107.13205125908726, 23.806785824010216],
// [107.13218187963494, 23.806867960389773],
// [107.13215698891558, 23.806902336258318],
// [107.13202636835067, 23.8068201998575],
// [107.13205125908726, 23.806785824010216]
// ]
// ],
// 'Polygon',
// '1',
// '测试方阵'
// );
2025-05-21 11:24:53 +08:00
});
</script>
<style scoped lang="scss">
.ol-map {
height: 450px;
width: 950px;
margin: 0 auto;
}
.ol-custome-full-screen {
border-radius: 3px;
font-size: 12px;
padding: 5px 11px;
height: 24px;
background-color: #409eff;
color: #fff;
border: 1px solid #409eff;
&:active {
background-color: #337ecc;
border-color: #66b1ff;
}
&:hover {
background-color: #79bbff;
border-color: #79bbff;
}
}
li {
list-style-type: none;
}
</style>