[add] 同步前端代码
This commit is contained in:
136
plus-ui/src/components/amap/index.vue
Normal file
136
plus-ui/src/components/amap/index.vue
Normal file
@ -0,0 +1,136 @@
|
||||
<template>
|
||||
<div class="map">
|
||||
<input type="text" placeholder="请输入地址" v-model="searchValue" />
|
||||
<button @click="onSearch">搜索</button>
|
||||
<div id="container" :style="{ 'height': mapProps.height }"></div>
|
||||
|
||||
<div id="my-panel" @listElementClick="selectPostion"></div>
|
||||
<div class="flex justify-end">
|
||||
<el-button type="primary" @click="submit"> 确定 </el-button>
|
||||
<el-button @click="emit('setLocation')">取消</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { onMounted, onUnmounted } from 'vue';
|
||||
import AMapLoader from '@amap/amap-jsapi-loader';
|
||||
const { proxy } = getCurrentInstance();
|
||||
//props参数
|
||||
const mapProps = defineProps({
|
||||
height: {
|
||||
type: String,
|
||||
default: '800px'
|
||||
}
|
||||
});
|
||||
const emit = defineEmits(['setLocation']);
|
||||
const center = ref([116.397428, 39.90923]);
|
||||
const map = ref(null);
|
||||
const placeSearch = ref(null);
|
||||
const geocoder = ref(null);
|
||||
const searchValue = ref('');
|
||||
const lnglat = ref([]);
|
||||
onMounted(() => {
|
||||
window._AMapSecurityConfig = {
|
||||
securityJsCode: '3f418182f27c907265f69a708c5fa41c'
|
||||
};
|
||||
AMapLoader.load({
|
||||
key: 'ed8d05ca57affee582e2be654bac5baf', // 申请好的Web端开发者Key,首次调用 load 时必填
|
||||
version: '2.0', // 指定要加载的 JSAPI 的版本,缺省时默认为 1.4.15
|
||||
plugins: ['AMap.Scale', 'AMap.AutoComplete', 'AMap.PlaceSearch', 'AMap.Geolocation', 'AMap.Geocoder'] //需要使用的的插件列表,如比例尺'AMap.Scale',支持添加多个如:['...','...']
|
||||
})
|
||||
.then((AMap) => {
|
||||
map.value = new AMap.Map('container', {
|
||||
// 设置地图容器id
|
||||
viewMode: '3D', // 是否为3D地图模式
|
||||
zoom: 8, // 初始化地图级别
|
||||
center: center.value // 初始化地图中心点位置
|
||||
});
|
||||
//初始化搜索
|
||||
placeSearch.value = new AMap.PlaceSearch({
|
||||
pageSize: 5, //单页显示结果条数
|
||||
// pageIndex: 1, //页码
|
||||
// city: '010', //兴趣点城市
|
||||
// citylimit: true, //是否强制限制在设置的城市内搜索
|
||||
panel: 'my-panel',
|
||||
map: map.value, //展现结果的地图实例
|
||||
autoFitView: true //是否自动调整地图视野使绘制的 Marker 点都处于视口的可见范围
|
||||
});
|
||||
// 初始化Geocoder
|
||||
geocoder.value = new AMap.Geocoder({
|
||||
radius: 1000 //范围,默认:500
|
||||
});
|
||||
|
||||
// 定位
|
||||
const geolocation = new AMap.Geolocation({
|
||||
enableHighAccuracy: true, //是否使用高精度定位,默认:true
|
||||
timeout: 10000, //超过10秒后停止定位,默认:无穷大
|
||||
maximumAge: 0, //定位结果缓存0毫秒,默认:0
|
||||
convert: true, //自动偏移坐标,偏移后的坐标为高德坐标,默认:true
|
||||
showButton: true, //显示定位按钮,默认:true
|
||||
buttonPosition: 'LB', //定位按钮停靠位置,默认:'LB',左下角
|
||||
buttonOffset: new AMap.Pixel(10, 20), //定位按钮与设置的停靠位置的偏移量,默认:Pixel(10, 20)
|
||||
showMarker: true, //定位成功后在定位到的位置显示点标记,默认:true
|
||||
showCircle: true, //定位成功后用圆圈表示定位精度范围,默认:true
|
||||
panToLocation: true, //定位成功后将定位到的位置作为地图中心点,默认:true
|
||||
zoomToAccuracy: true //定位成功后调整地图视野范围使定位位置及精度范围视野内可见,默认:false
|
||||
});
|
||||
map.value.addControl(geolocation);
|
||||
|
||||
//定位到当前位置
|
||||
geolocation.getCurrentPosition((status, result) => {
|
||||
console.log(status, result);
|
||||
});
|
||||
placeSearch.value.on('selectChanged', (e) => {
|
||||
let { lng, lat } = e.selected.data.location;
|
||||
lnglat.value = [lng, lat];
|
||||
});
|
||||
})
|
||||
.catch((e) => {
|
||||
console.log(e);
|
||||
});
|
||||
});
|
||||
const onSearch = () => {
|
||||
//搜索地址
|
||||
placeSearch.value.search(searchValue.value, (status, result) => {
|
||||
if (result.info !== 'OK') return;
|
||||
let { lng, lat } = result.poiList.pois[0].location;
|
||||
lnglat.value = [lng, lat];
|
||||
});
|
||||
};
|
||||
|
||||
const submit = () => {
|
||||
if (!lnglat.value.length) {
|
||||
proxy?.$modal.msgWarning('请选择正确地址');
|
||||
return;
|
||||
}
|
||||
geocoder.value.getAddress(lnglat.value, function (status, result) {
|
||||
if (status === 'complete' && result.info === 'OK') {
|
||||
// result为对应的地理位置详细信息
|
||||
const position = {
|
||||
lng: lnglat.value[0],
|
||||
lat: lnglat.value[1],
|
||||
projectSite: result.regeocode.formattedAddress
|
||||
};
|
||||
emit('setLocation', position);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
onUnmounted(() => {
|
||||
map.value?.destroy();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
#container {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
#my-panel {
|
||||
position: absolute;
|
||||
top: 103px;
|
||||
z-index: 1;
|
||||
left: 10px;
|
||||
}
|
||||
</style>
|
597
plus-ui/src/components/openLayersMap/index.vue
Normal file
597
plus-ui/src/components/openLayersMap/index.vue
Normal file
@ -0,0 +1,597 @@
|
||||
<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 }">
|
||||
<span @dblclick="handlePosition(data, node)">{{ data.name }}</span>
|
||||
</template>
|
||||
</el-tree-v2>
|
||||
<div>
|
||||
<div class="ol-map" id="olMap"></div>
|
||||
<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>
|
||||
<p
|
||||
v-for="(item, index) in selectLayer"
|
||||
class="pl-xl border-rd pr p-3 w-111 mr-1 bg-#909399 flex items-center cursor-pointer justify-between"
|
||||
@click="delLayer(index, item.option)"
|
||||
>
|
||||
{{ 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>
|
||||
<el-radio :value="4" size="large">逆变器</el-radio>
|
||||
<el-radio :value="5" size="large">箱变</el-radio>
|
||||
</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
|
||||
v-for="(item, index) in layerTypeList"
|
||||
class="px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 cursor-pointer"
|
||||
@click="handleMenuItemClick(item, index + 1)"
|
||||
>
|
||||
<i class="fa-solid fa-check mr-2"></i>{{ item }}
|
||||
</li>
|
||||
<li class="px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 cursor-pointer" @click="handleMenuItemClick('名称', null)">
|
||||
<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 GeoJSON from 'ol/format/GeoJSON';
|
||||
import * as turf from '@turf/turf';
|
||||
import { FeatureCollection, Geometry } from 'geojson';
|
||||
import { MapViewFitter } from '@/utils/setMapCenter';
|
||||
import { TreeInstance } from 'element-plus';
|
||||
import { addProjectFacilities, addProjectPilePoint, addProjectSquare, listDXFProject, addInverter, addBoxTransformer } from '@/api/project/project';
|
||||
import { BatchUploader } from '@/utils/batchUpload';
|
||||
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
||||
|
||||
const props = defineProps({
|
||||
projectId: String,
|
||||
designId: String
|
||||
});
|
||||
const treeData = ref<any>([]);
|
||||
const layerType = ref(null);
|
||||
const layerTypeList = ref(['光伏板', '桩点/支架', '方阵', '逆变器', '箱变']);
|
||||
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;
|
||||
});
|
||||
const handlePosition = (data: any, node: any) => {
|
||||
const fitter = new MapViewFitter(map); // 传入你的 OpenLayers 地图实例
|
||||
const features = treeData.value[data.index]?.features; //features数组
|
||||
console.log('🚀 ~ handlePosition ~ features:', features);
|
||||
|
||||
if (features?.length) {
|
||||
const featureCollection: FeatureCollection<Geometry> = {
|
||||
type: 'FeatureCollection',
|
||||
features
|
||||
};
|
||||
|
||||
fitter.fit(featureCollection);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCheckChange = (data: any, bool: boolean) => {
|
||||
if (isMenuVisible.value) isMenuVisible.value = false;
|
||||
|
||||
const features = treeData.value[data.index].features;
|
||||
|
||||
if (bool) {
|
||||
features.forEach((item: any) => {
|
||||
const fid = item.geometry.id;
|
||||
|
||||
// 没创建过就先创建
|
||||
if (!featureMap[fid]) {
|
||||
creatPoint(item.geometry.coordinates, item.geometry.type, fid, item.properties.text);
|
||||
}
|
||||
|
||||
// 添加到共享 source 中(避免重复添加)
|
||||
const feature = featureMap[fid];
|
||||
if (!sharedSource.hasFeature(feature)) {
|
||||
sharedSource.addFeature(feature);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
features.forEach((item: any) => {
|
||||
const fid = item.geometry.id;
|
||||
const feature = featureMap[fid];
|
||||
if (feature && sharedSource.hasFeature(feature)) {
|
||||
sharedSource.removeFeature(feature);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
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 名称
|
||||
* */
|
||||
// 共享 source 和图层(全局一次性创建)
|
||||
const sharedSource = new VectorSource();
|
||||
const sharedLayer = new VectorLayer({
|
||||
source: sharedSource,
|
||||
renderMode: 'image' // 提高渲染性能
|
||||
} as any);
|
||||
|
||||
// id => Feature 映射表
|
||||
const featureMap: Record<string, Feature> = {};
|
||||
|
||||
const creatPoint = (pointObj: Array<any>, type: string, id: string, name?: string) => {
|
||||
let geometry;
|
||||
|
||||
if (type === 'Point') {
|
||||
geometry = new Point(fromLonLat(pointObj));
|
||||
} else if (type === 'LineString') {
|
||||
const coords = pointObj.map((arr: any) => fromLonLat(arr));
|
||||
// 注意:这里虽然是 LineString 类型,但数据实际表示的是闭合面
|
||||
geometry = new Polygon([coords]);
|
||||
} else {
|
||||
const coords = pointObj.map((arr: any) => arr.map((i: any) => fromLonLat(i)));
|
||||
geometry = new Polygon(coords);
|
||||
}
|
||||
|
||||
const feature = new Feature({ geometry });
|
||||
|
||||
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' })
|
||||
});
|
||||
|
||||
feature.setStyle(type === 'Point' ? pointStyle : polygonStyle);
|
||||
|
||||
// 缓存 feature(用于后续判断)
|
||||
featureMap[id] = feature;
|
||||
};
|
||||
|
||||
// 控制菜单是否显示
|
||||
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, index: number) => {
|
||||
isMenuVisible.value = false;
|
||||
|
||||
if (selectLayer.value.some((item) => item.location.name === contextMenu.value.name)) {
|
||||
return proxy?.$modal.msgError('已选择该图层,请勿重复选择');
|
||||
}
|
||||
if (selectLayer.value.some((item) => item.option !== '名称' && item.option !== '箱变' && item.option !== '光伏板')) {
|
||||
if (option !== '名称' && option !== '箱变') return proxy?.$modal.msgError('只能选择一个类型');
|
||||
}
|
||||
selectLayer.value.push({ location: contextMenu.value, option });
|
||||
layerType.value = index ? index : layerType.value; // 设置 layerType 为对应的索引值
|
||||
|
||||
emit('handleCheckChange', selectLayer.value);
|
||||
};
|
||||
|
||||
//删除菜单
|
||||
const delLayer = (index, option) => {
|
||||
selectLayer.value.splice(index, 1);
|
||||
if (option != '名称') {
|
||||
if (selectLayer.value.every((item) => item.option == '名称')) layerType.value = null;
|
||||
}
|
||||
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);
|
||||
};
|
||||
|
||||
type LayerConfig = {
|
||||
optionB: string;
|
||||
apiFunc: (data: any) => Promise<any>;
|
||||
};
|
||||
|
||||
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);
|
||||
const secondLayer = selectLayer.value.filter((item) => item.option === secondOption);
|
||||
|
||||
if (!nameLayers.length || !secondLayer) {
|
||||
showError(`请选择${nameOption}和${secondOption}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const nameGeoJson = nameLayers.map((item) => treeData.value[item.location.index]);
|
||||
const locationGeoJson = secondLayer.map((item) => treeData.value[item.location.index]);
|
||||
|
||||
return { nameGeoJson, locationGeoJson };
|
||||
};
|
||||
|
||||
const handleTwoLayerUpload = async (optionB: string, apiFunc: (data: any) => Promise<any>) => {
|
||||
const geoJson = getGeoJsonData('名称', optionB);
|
||||
if (!geoJson) return;
|
||||
if (optionB == '光伏板') return uploadPhotovoltaic(geoJson, apiFunc);
|
||||
const data = {
|
||||
projectId: props.projectId,
|
||||
nameGeoJson: geoJson.nameGeoJson,
|
||||
locationGeoJson: geoJson.locationGeoJson,
|
||||
pointGeoJson: null
|
||||
};
|
||||
|
||||
loading.value = true;
|
||||
await apiFunc(data);
|
||||
await showSuccess('添加成功');
|
||||
};
|
||||
//上传光伏板
|
||||
const uploadPhotovoltaic = async (geoJson: { nameGeoJson: any[]; locationGeoJson: any }, apiFunc: (data: any) => Promise<any>) => {
|
||||
// 提取原始 features
|
||||
let rawNameFeatures = geoJson.nameGeoJson || [];
|
||||
let rawLocationFeatures = geoJson.locationGeoJson || [];
|
||||
|
||||
console.log('🚀 nameGeoJson:', rawNameFeatures);
|
||||
console.log('🚀 locationGeoJson:', rawLocationFeatures);
|
||||
|
||||
// 扁平化处理 FeatureCollection
|
||||
const nameFeatures = rawNameFeatures.flatMap((fc) => fc.features || []).map((f) => ({ ...f, __source: 'name' }));
|
||||
const locationFeatures = rawLocationFeatures.flatMap((fc) => fc.features).map((f) => ({ ...f, __source: 'location' }));
|
||||
// 配对成上传单元
|
||||
type FeaturePair = { nameFeature: any; locationFeature: any };
|
||||
const pairedFeatures: FeaturePair[] = nameFeatures.map((name, i) => ({
|
||||
nameFeature: name,
|
||||
locationFeature: locationFeatures[i]
|
||||
}));
|
||||
|
||||
// 启动上传
|
||||
loading.value = true;
|
||||
|
||||
const sessionId = new Date().getTime().toString(36) + Math.random().toString(36).substring(2, 15);
|
||||
|
||||
const uploader = new BatchUploader({
|
||||
dataList: pairedFeatures,
|
||||
chunkSize: 3000, // 一次上传3000对
|
||||
delay: 200,
|
||||
uploadFunc: async (chunk, batchNum, totalBatch) => {
|
||||
const chunkNameFeatures = chunk.map((pair) => pair.nameFeature);
|
||||
const chunkLocationFeatures = chunk.map((pair) => pair.locationFeature);
|
||||
|
||||
console.log(`🚀 上传第 ${batchNum}/${totalBatch} 批,条数:`, chunk.length);
|
||||
|
||||
await apiFunc({
|
||||
projectId: props.projectId,
|
||||
nameGeoJson: [{ type: 'FeatureCollection', features: chunkNameFeatures }],
|
||||
locationGeoJson: [{ type: 'FeatureCollection', features: chunkLocationFeatures }],
|
||||
pointGeoJson: null,
|
||||
sessionId,
|
||||
totalBatch,
|
||||
batchNum
|
||||
});
|
||||
},
|
||||
onComplete: () => {
|
||||
showSuccess('图层上传完成');
|
||||
loading.value = false;
|
||||
}
|
||||
});
|
||||
await uploader.start();
|
||||
};
|
||||
|
||||
const handlePointUpload = async () => {
|
||||
if (selectLayer.value.length > 1) return showError('最多选择一个桩点/支架');
|
||||
if (selectLayer.value[0].option !== '桩点/支架') return showError('请选择类型为桩点/支架');
|
||||
|
||||
const features = treeData.value[selectLayer.value[0].location.index]?.features || [];
|
||||
if (!features.length) return showError('桩点数据为空');
|
||||
|
||||
loading.value = true;
|
||||
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();
|
||||
};
|
||||
|
||||
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);
|
||||
} else {
|
||||
showError('不支持的图层类型');
|
||||
}
|
||||
} finally {
|
||||
reset();
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const reset = () => {
|
||||
selectLayer.value = [];
|
||||
treeRef.value?.setCheckedKeys([]);
|
||||
sharedSource.clear(); // 清空共享 source 中的所有要素
|
||||
layerType.value = null;
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.designId,
|
||||
(newId, oldId) => {
|
||||
if (newId !== oldId) {
|
||||
reset();
|
||||
getTreeData();
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
// 地图初始化
|
||||
initOLMap();
|
||||
map.addLayer(sharedLayer);
|
||||
// creatPoint(
|
||||
// [
|
||||
// [
|
||||
// [107.13205125908726, 23.806785824010216],
|
||||
// [107.13218187963494, 23.806867960389773],
|
||||
// [107.13215698891558, 23.806902336258318],
|
||||
// [107.13202636835067, 23.8068201998575],
|
||||
// [107.13205125908726, 23.806785824010216]
|
||||
// ]
|
||||
// ],
|
||||
// 'Polygon',
|
||||
// '1',
|
||||
// '测试方阵'
|
||||
// );
|
||||
});
|
||||
</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>
|
57
plus-ui/src/components/webrtc/index.vue
Normal file
57
plus-ui/src/components/webrtc/index.vue
Normal file
@ -0,0 +1,57 @@
|
||||
<template>
|
||||
<video class="rtc_media_player" width="100%" height="100%" autoplay muted playsinline ref="rtcMediaPlayer"></video>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "SrsPlayer",
|
||||
data() {
|
||||
return {
|
||||
webrtc: null, // Instance of SRS SDK
|
||||
sessionId: null,
|
||||
simulatorUrl: null,
|
||||
playerVisible: false,
|
||||
url: ""
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
hideInfo() {
|
||||
document.querySelector(".alert").style.display = "none";
|
||||
},
|
||||
async startPlay(url) {
|
||||
this.url = url;
|
||||
this.playerVisible = true;
|
||||
if (this.webrtc) {
|
||||
this.webrtc.close();
|
||||
}
|
||||
this.webrtc = new SrsRtcWhipWhepAsync();
|
||||
this.$refs.rtcMediaPlayer.srcObject = this.webrtc.stream;
|
||||
console.log('stream tracks:', this.webrtc.stream.getTracks());
|
||||
try {
|
||||
const session = await this.webrtc.play(url);
|
||||
console.log('after play, stream tracks:', this.webrtc.stream.getTracks());
|
||||
this.sessionId = session.sessionid;
|
||||
this.simulatorUrl = `${session.simulator}?drop=1&username=${session.sessionid}`;
|
||||
} catch (error) {
|
||||
console.error("Error playing stream:", error);
|
||||
this.webrtc.close();
|
||||
this.playerVisible = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
beforeDestroy() {
|
||||
// Cleanup the SDK instance on component destroy
|
||||
if (window[this.url]) {
|
||||
window[this.url].close();
|
||||
window[this.url] = null
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.rtc_media_player {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
Reference in New Issue
Block a user