Files
td_official/src/views/progress/progressPaper/index.vue
2025-06-24 20:10:42 +08:00

976 lines
31 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div v-loading="geoTiffLoading" class="flex h100vh">
<div class="header flex justify-between">
<div class="tips flex items-center justify-between">
<div class="dot1">未提交</div>
<div class="dot2">已提交</div>
<div class="dot3">已选择,待提交</div>
<el-tooltip class="box-item" effect="dark" content="右键拖动生成套索区域选中/取消图形" placement="bottom">
<i class="iconfont icon-wenhao"></i>
</el-tooltip>
</div>
<el-form :model="queryParams" ref="form" label-width="80px" inline class="flex items-center">
<el-form-item label="请选择项目:" prop="pid" label-width="100" style="margin-bottom: 0; color: #fff">
<el-select v-model="selectedProjectId" placeholder="请选择" @change="handleSelect" clearable>
<el-option v-for="item in ProjectList" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
</el-form-item>
<el-form-item label="请选择方阵:" prop="pid" label-width="100" style="margin-bottom: 0">
<!-- <el-select v-model="matrixValue" placeholder="请选择" @change="handleChange" clearable>
<el-option v-for="item in matrixOptions" :key="item.id" :label="item.matrixName" :value="item.id" />
</el-select> -->
<el-cascader
:options="matrixOptions"
placeholder="请选择"
@change="handleChange"
:props="{ value: 'matrixId', label: 'name' }"
v-model="queryParams.matrixId"
clearable
/>
</el-form-item>
</el-form>
</div>
<div class="ol-map" id="olMap"></div>
<div class="aside">
<el-tree
style="max-width: 600px"
:data="progressCategoryList"
ref="treeRef"
@check-change="handleCheckChange"
:props="treeProps"
:load="loadNode"
node-key="id"
lazy
:render-content="renderContent"
check-strictly
:expand-on-click-node="false"
/>
</div>
<div class="submit">
<el-button type="primary" size="default" @click="submit" :loading="loading">提交</el-button>
</div>
</div>
</template>
<script lang="ts" setup>
import Map from 'ol/Map'; // OpenLayers的主要类用于创建和管理地图
import View from 'ol/View'; // OpenLayers的视图类定义地图的视图属性
import { Tile as TileLayer, WebGLTile } from 'ol/layer'; // OpenLayers的瓦片图层类
import { Raster, XYZ } from 'ol/source'; // OpenLayers的瓦片数据源包括XYZ格式和OpenStreetMap专用的数据源
import { defaults as defaultControls, defaults, FullScreen, MousePosition, ScaleLine } from 'ol/control';
import { fromLonLat, toLonLat, transform, transformExtent } from 'ol/proj';
import { useUserStoreHook } from '@/store/modules/user';
import { getProjectSquare, listProgressCategory, addDaily, workScheduleListPosition } from '@/api/progress/plan';
import { ProgressCategoryVO, progressPlanDetailForm } from '@/api/progress/plan/types';
import { Circle, Fill, Stroke, Style, Text } from 'ol/style';
import { defaults as defaultInteractions, DragPan } from 'ol/interaction';
import Feature from 'ol/Feature';
import { Point, Polygon } from 'ol/geom';
import VectorSource from 'ol/source/Vector';
import VectorLayer from 'ol/layer/Vector';
import Node from 'element-plus/es/components/tree/src/model/node.mjs';
import { ElCheckbox } from 'element-plus';
import { LassoSelector } from '@/utils/lassoSelect';
import { FeatureCollection, Geometry } from 'geojson';
import { MapViewFitter } from '@/utils/setMapCenter';
import PointerInteraction from 'ol/interaction/Pointer';
import { Coordinate } from 'ol/coordinate';
import GeoTIFF, { fromBlob, fromUrl, fromArrayBuffer, Pool } from 'geotiff';
import GeoTIFFSource from 'ol/source/GeoTIFF';
import ImageLayer from 'ol/layer/Image';
import Static from 'ol/source/ImageStatic';
import proj4 from 'proj4';
import { register } from 'ol/proj/proj4';
import gcoord from 'gcoord';
import { createXYZ } from 'ol/tilegrid';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const orthophoto = '@/assets/images/orthophoto.tif';
const selector = ref<LassoSelector | null>(null);
// 获取用户 store
const userStore = useUserStoreHook();
// 从 store 中获取项目列表和当前选中的项目
const currentProject = computed(() => userStore.selectedProject);
const ProjectList = computed(() => userStore.projects);
const selectedProjectId = ref(userStore.selectedProject?.id || '');
const treeRef = ref();
const queryParams = ref({
pid: undefined,
name: undefined,
unitType: undefined,
projectId: currentProject.value.id,
matrixId: undefined,
params: {}
});
const submitForm = ref<progressPlanDetailForm>({
id: '',
finishedDetailIdList: [] as string[]
});
const loading = ref(false);
const geoTiffLoading = ref(false);
const matrixOptions = ref([]);
const matrixValue = ref<number | undefined>(matrixOptions.value.length > 0 ? matrixOptions.value[0].id : undefined);
const progressCategoryList = ref<ProgressCategoryVO[]>([]);
const treeProps = {
children: 'children',
label: 'name',
isLeaf: 'leaf',
hasChildren: 'hasChildren', // 重要
disabled: 'disabled'
};
//切换项目
const handleSelect = (projectId: string) => {
const selectedProject = ProjectList.value.find((p) => p.id === projectId);
if (selectedProject) {
userStore.setSelectedProject(selectedProject);
resetMatrix();
getList();
}
};
/** 进度类别树选中事件 */
const handleCheckChange = (data: any, checked: boolean, indeterminate: boolean) => {
const node: Node | undefined = treeRef.value?.getNode(data.id);
if (node.level === 2) {
//二级节点 展开显示图层 收起删除
const children = node.childNodes.map((child) => child.data);
children.forEach((child) => {
child.disabled = !checked;
});
// 让 el-tree 刷新显示
treeRef.value.updateKeyChildren(data.id, children);
if (checked) {
addPointToMap(data.threeChildren); // 添加点到地图
clearLevel3Checked(node.level); // 清除三级节点的选中状态
treeRef.value.setChecked(data.id, true, false);
} else {
data.threeChildren.forEach((child: any) => {
const feature = featureMap[child.id];
if (feature && sharedSource.hasFeature(feature)) {
sharedSource.removeFeature(feature);
}
});
}
return;
}
// if (!checked) return (submitForm.value.id = ''); // 只处理第三级节点的选中事件
if (!checked) {
// ✅ 只在取消的是当前 id 时清空
if (submitForm.value.id === data.id) {
submitForm.value.id = '';
}
return;
}
const parent = node.parent;
if (!parent) return;
//消除所有节点的选中状态
clearLevel3Checked(node.level); // 清除三级节点的选中状态
// 设置当前点击项为选中
treeRef.value.setChecked(data.id, true, false);
submitForm.value.id = data.id; // 设置提交表单的id
console.log('submitForm', submitForm.value);
};
//清除某一级节点所有选中状态
const clearLevel3Checked = (level) => {
const rootNodes = treeRef.value.store.root.childNodes;
const clearChecked = (nodes: any[]) => {
nodes.forEach((node) => {
if (node.level === level) {
treeRef.value.setChecked(node.key, false, false);
} else if (node.childNodes?.length) {
clearChecked(node.childNodes);
}
});
};
clearChecked(rootNodes);
};
/** 关闭节点事件 */
// const closeNode = (node: any) => {
// // 清除子节点
// if (node.pid) {
// // node.threeChildren.forEach((child: any) => {
// // const feature = featureMap[child.id];
// // if (feature && sharedSource.hasFeature(feature)) {
// // sharedSource.removeFeature(feature);
// // }
// // });
// }
// };
// /** 打开节点事件 */
// const openNode = (node: any) => {
// // 清除子节点
// if (!node.pid) return;
// // addPointToMap(node.threeChildren); // 添加点到地图
// };
//懒加载子节点
const loadNode = async (node: any, resolve: (data: any[]) => void) => {
if (node.level !== 2) {
// 只对二级节点加载子节点
resolve(node.data.children || []);
return;
}
const secondLevelNodeId = node.data.id;
const res = await workScheduleListPosition(secondLevelNodeId); // 替换成你的 API
const children = res.data.detailList || [];
if (children.length === 0) {
proxy?.$modal.msgWarning(`节点 "${node.data.name}" 为空`);
resolve([]);
}
// 标记子节点为叶子节点
const threeLeafList = children.map((detail) => {
return {
...detail,
name: detail.date, // 设置为叶子节点
leaf: true, // 标记为叶子节点
disabled: true
};
});
progressCategoryList.value.forEach((item, i) => {
let indexNum = item.children.findIndex((item) => item.id === secondLevelNodeId);
if (indexNum !== -1) {
item.children[indexNum].threeChildren = res.data.facilityList; // 将子节点添加到当前节点的threeChildren属性中
// item.children[indexNum].detailChildren = children; // 将子节点添加到当前节点的threeChildren属性中
}
});
resolve(threeLeafList);
};
/** 提交按钮点击事件 */
const submit = () => {
console.log('sunbmitForm', submitForm.value);
const { finishedDetailIdList, id } = submitForm.value;
if (!id || finishedDetailIdList.length === 0) return proxy?.$modal.msgWarning('请选择图层以及日期');
loading.value = true;
addDaily(submitForm.value)
.then(() => {
proxy?.$modal.msgSuccess('提交成功');
const scale = Math.max(map.getView().getZoom() / 10, 1); // 获取当前缩放比例
sharedSource.getFeatures().forEach((feature) => {
if (feature.get('highlighted')) {
feature.setStyle(successStyle(feature.get('name'), scale)); // 转为成功样式
feature.set('highlighted', false); // 重置高亮状态
feature.set('status', '2'); // 设置为完成状态
}
});
resetTreeAndMap();
})
.catch((error) => {
proxy?.$modal.msgError(`提交失败: ${error.message}`);
});
};
//重置树形结构选中以及图层高亮
const resetTreeAndMap = () => {
// 重置树形结构选中状态
clearLevel3Checked(3);
//取消加载状态
loading.value = false;
// 清除地图上的所有高亮
const scale = Math.max(map.getView().getZoom() / 10, 1); // 获取当前缩放比例
sharedSource.getFeatures().forEach((feature) => {
if (feature.get('highlighted')) {
feature.setStyle(defaultStyle(feature.get('name'), scale)); // 恢复默认样式
feature.set('highlighted', false); // 重置高亮状态
}
});
// 清空已完成列表
submitForm.value.finishedDetailIdList = [];
};
/** 方阵选择器改变事件 */
const handleChange = (value: number) => {
queryParams.value.matrixId = value[1];
getList();
};
//限定部分节点能选择
const renderContent = (context, { node, data }) => {
if (node.level === 3) {
return h('span', { class: 'custom-tree-node' }, [
h(ElCheckbox, {
modelValue: node.checked,
'onUpdate:modelValue': (val) => node.setChecked(val),
style: 'margin-right: 8px;',
disabled: data.disabled
}),
h('span', [node.label])
]);
}
if (node.level === 2) {
return h('span', { class: 'custom-tree-node' }, [
h(ElCheckbox, {
modelValue: node.checked,
'onUpdate:modelValue': (val) => node.setChecked(val),
style: 'margin-right: 8px;',
disabled: !data.threeChildren || data.threeChildren.length == 0
}),
h('span', { onDblclick: () => handlePosition(data, node) }, node.label)
]);
} else {
return h('span', node.label);
}
};
const handlePosition = (data: any, node: any) => {
if (!data.threeChildren) return;
const fitter = new MapViewFitter(map); // 传入你的 OpenLayers 地图实例
const features: GeoJSON.Feature[] = data.threeChildren.map((item) => {
if ('type' in item && item.type === 'Feature') {
return item as GeoJSON.Feature;
}
const raw = item;
let coordinates: any;
if (raw.type === 'Polygon') {
coordinates = [(raw.positions as [string, string][]).map(([lng, lat]) => [parseFloat(lng), parseFloat(lat)])];
} else if (raw.type === 'Point') {
const [lng, lat] = raw.positions as [string, string];
coordinates = [parseFloat(lng), parseFloat(lat)];
} else {
throw new Error(`Unsupported geometry type: ${raw.type}`);
}
return {
type: 'Feature',
geometry: {
type: raw.type,
coordinates
},
properties: {
id: raw.id,
name: raw.name,
status: raw.status,
finishDate: raw.finishDate
}
};
});
if (features?.length) {
const featureCollection: FeatureCollection<Geometry> = {
type: 'FeatureCollection',
features
};
fitter.fit(featureCollection);
}
};
//切换项目重置方阵
const resetMatrix = () => {
matrixValue.value = undefined;
queryParams.value.matrixId = undefined;
matrixOptions.value = [];
};
/** 查询进度类别列表 */
const getList = async () => {
if (!queryParams.value.matrixId) {
const res = await getProjectSquare(currentProject.value.id);
if (res.data.length === 0) {
proxy?.$modal.msgWarning('当前项目下没有方阵,请先创建方阵');
} else {
let matrixList = res.data.map((item) => {
return {
...item,
matrixId: item.projectId
};
});
if (!matrixValue.value) matrixValue.value = matrixList[0].id;
matrixOptions.value = matrixList;
queryParams.value.matrixId = matrixList[0].children[0].matrixId;
}
}
loading.value = true;
queryParams.value.projectId = currentProject.value.id;
const res = await listProgressCategory(queryParams.value);
const data = proxy?.handleTree<ProgressCategoryVO>(res.data, 'id', 'pid');
if (data) {
progressCategoryList.value = data;
loading.value = false;
}
};
const imageExtent = ref(null);
const imageLayer = ref(null);
import { get as getProjection } from 'ol/proj';
const initGeoTiff = async () => {
// const tiff = await fromUrl('/image/clean_rgba_cleaned.tif');
// const image = await tiff.getImage();
// const width = image.getWidth();
// const height = image.getHeight();
// const bbox = image.getBoundingBox(); // [minX, minY, maxX, maxY]
// console.log('bbox', bbox);
// const rasters = await image.readRasters({ interleave: true });
// // 创建 Canvas
// const canvas = document.createElement('canvas');
// canvas.width = width;
// canvas.height = height;
// const ctx = canvas.getContext('2d')!;
// const imageData: any = ctx.createImageData(width, height);
// // 设置 RGBA 数据
// imageData.data.set(rasters); // ✅ 完整设置,不用手动循环
// ctx.putImageData(imageData, 0, 0);
// // 将 canvas 转成 Data URL 用作图层 source
// const imageUrl = canvas.toDataURL();
// // 转换为 WGS84 经纬度
// const minLonLat = transform([bbox[0], bbox[1]], 'EPSG:32648', 'EPSG:4326');
// const maxLonLat = transform([bbox[2], bbox[3]], 'EPSG:32648', 'EPSG:4326');
// // 转为 GCJ02高德地图坐标系
// const gcjMin = gcoord.transform(minLonLat as [number, number, number], gcoord.WGS84, gcoord.GCJ02);
// const gcjMax = gcoord.transform(maxLonLat as [number, number, number], gcoord.WGS84, gcoord.GCJ02);
// // 再转 EPSG:3857 供 OpenLayers 使用
// const minXY = fromLonLat(gcjMin);
// const maxXY = fromLonLat(gcjMax);
// imageExtent.value = [...minXY, ...maxXY];
// imageLayer.value = new ImageLayer({
// source: new Static({
// url: imageUrl,
// imageExtent: imageExtent.value,
// projection: 'EPSG:3857'
// })
// });
// console.log('imageExtent', imageExtent.value);
// 1. 你的原始瓦片的边界(来自 .tfw 或你知道的数据)
// 1. 你的 bbox 是 WGS84 经纬度
const bbox = [107.13149481208748, 23.80411597354268, 107.13487254421389, 23.80801427852998];
// 2. 转成 GCJ02高德坐标系
const gcjMin = gcoord.transform([bbox[0], bbox[1]], gcoord.WGS84, gcoord.GCJ02);
const gcjMax = gcoord.transform([bbox[2], bbox[3]], gcoord.WGS84, gcoord.GCJ02);
// 3. 再转换成 EPSG:3857用于 OpenLayers
const minXY = fromLonLat(gcjMin);
const maxXY = fromLonLat(gcjMax);
// 4. 组成瓦片范围 extent
const tileExtent = [...minXY, ...maxXY];
console.log('tileExtent', tileExtent);
// 5. 创建 tileGrid
const tileGrid = createXYZ({
extent: tileExtent,
tileSize: 256,
minZoom: 10,
maxZoom: 18
});
// 6. 使用 Web Mercator 投影 EPSG:3857
const projection = getProjection('EPSG:3857');
// 7. 创建瓦片图层
imageLayer.value = new TileLayer({
source: new XYZ({
projection,
tileGrid,
tileUrlFunction: (tileCoord) => {
if (!tileCoord) return '';
let [z, x, y] = tileCoord;
console.log(z, x, y);
y = Math.pow(2, z) - y - 1;
return `http://192.168.110.2:8000/api/projects/3/tasks/c2e3227f-343f-48b1-88c0-1432d6eab33f/orthophoto/tiles/${z}/${x}/${y}`;
}
})
});
const source = imageLayer.value.getSource();
const projections = source.getProjection();
console.log('图层使用的坐标系:', projections?.getCode());
};
let map: any = null;
const layerData = reactive<any>({});
const centerPosition = ref(fromLonLat([107.12932403398425, 23.805564054229908]));
const initOLMap = () => {
console.log(111);
// const scoure = new TileLayer({
// // 设置图层的数据源为XYZ类型。XYZ是一个通用的瓦片图层源它允许你通过URL模板来获取瓦片
// source: new XYZ({
// url: 'http://192.168.110.2:8000/api/projects/3/tasks/c2e3227f-343f-48b1-88c0-1432d6eab33f/orthophoto/tiles/{z}/{x}/{y}'
// })
// });
// console.log(scoure);
map = new Map({
// 设置地图容器的ID
target: 'olMap',
// 定义地图的图层列表,用于显示特定的地理信息。
layers: [
// 高德地图
// TileLayer表示一个瓦片图层它由一系列瓦片通常是图片组成用于在地图上显示地理数据。
new TileLayer({
// 设置图层的数据源为XYZ类型。XYZ是一个通用的瓦片图层源它允许你通过URL模板来获取瓦片
source: new XYZ({
url: 'https://webst02.is.autonavi.com/appmaptile?style=6&x={x}&y={y}&z={z}'
})
}),
new TileLayer({
// 设置图层的数据源为XYZ类型。XYZ是一个通用的瓦片图层源它允许你通过URL模板来获取瓦片
source: new XYZ({
url: 'http://webst02.is.autonavi.com/appmaptile?x={x}&y={y}&z={z}&lang=zh_cn&size=1&scale=1&style=8'
})
})
// imageLayer.value
// imageLayer.value
],
// 设置地图的视图参数
// View表示地图的视图它定义了地图的中心点、缩放级别、旋转角度等参数。
view: new View({
// fromLonLat是一个函数用于将经纬度坐标转换为地图的坐标系统。
center: centerPosition.value, //地图中心点
zoom: 15, // 缩放级别
minZoom: 0, // 最小缩放级别
// maxZoom: 18, // 最大缩放级别
constrainResolution: true // 因为存在非整数的缩放级别所以设置该参数为true来让每次缩放结束后自动缩放到距离最近的一个整数级别这个必须要设置当缩放在非整数级别时地图会糊
// projection: 'EPSG:4326' // 投影坐标系默认是3857
}),
//加载控件到地图容器中
controls: defaultControls({
zoom: false,
rotate: false,
attribution: false
}).extend([new ScaleLine()]),
interactions: defaultInteractions({
doubleClickZoom: false // 禁用双击缩放
})
});
map.on('click', (e: any) => {
var coordinate = e.coordinate;
// 将投影坐标转换为经纬度坐标
var lonLatCoordinate = toLonLat(coordinate);
// 输出转换后的经纬度坐标
console.log('经纬度坐标:', lonLatCoordinate);
const zoom = map.getView().getZoom();
const scale = Math.max(zoom / 10, 1); // 缩放比例,根据需要调整公式
map.forEachFeatureAtPixel(e.pixel, (feature: Feature) => {
toggleFeatureHighlight(feature);
});
});
map.getView().on('change:resolution', () => {
const zoom = map.getView().getZoom();
const scale = Math.max(zoom / 10, 1); // 缩放比例,根据需要调整公式
sharedSource.getFeatures().forEach((feature) => {
const style = feature.getStyle();
if (style instanceof Style && style.getText()) {
style.getText().setScale(scale);
feature.setStyle(style); // 重新应用样式
}
});
});
map.on('moveend', (e: any) => {
// console.log('地图移动', e);
// 获取当前缩放级别
var zoomLevel = map.getView().getZoom();
console.log('当前缩放级别:', zoomLevel);
});
// 4. 添加 pointermove 鼠标悬停事件
let lastFeature: Feature | null = null;
map.on('pointermove', (evt) => {
map.getTargetElement().style.cursor = '';
const feature = map.forEachFeatureAtPixel(evt.pixel, (f) => f);
const zoom = map.getView().getZoom();
const scale = Math.max(zoom / 10, 1);
// 👉 若当前划入的 feature 是不允许 hover 的,直接跳过处理
const currStatus = feature?.get('status');
const currHighlighted = feature?.get('highlighted');
if (feature && (currStatus === '2' || currHighlighted === true || feature.getGeometry()?.getType() !== 'Polygon')) {
return; // ❌ 不执行 hover 效果,也不更新 lastFeature
}
// ✅ 若进入了新的可 hover feature
if (feature && feature !== lastFeature) {
if (lastFeature) {
const lastStatus = lastFeature.get('status');
const lastHighlighted = lastFeature.get('highlighted');
if (lastStatus === '2') {
lastFeature.setStyle(successStyle(lastFeature.get('name'), scale));
} else if (lastHighlighted === true) {
lastFeature.setStyle(highlightStyle(lastFeature.get('name'), scale));
} else {
lastFeature.setStyle(defaultStyle(lastFeature.get('name'), scale));
}
}
feature.setStyle(hoverStyle(feature.get('name'), scale));
map.getTargetElement().style.cursor = 'pointer';
lastFeature = feature;
} else if (!feature && lastFeature) {
// ✅ 鼠标移出所有图形时恢复
const lastStatus = lastFeature.get('status');
const lastHighlighted = lastFeature.get('highlighted');
if (lastStatus === '2') {
lastFeature.setStyle(successStyle(lastFeature.get('name'), scale));
} else if (lastHighlighted === true) {
lastFeature.setStyle(highlightStyle(lastFeature.get('name'), scale));
} else {
lastFeature.setStyle(defaultStyle(lastFeature.get('name'), scale));
}
lastFeature = null;
}
});
};
// 你已有的 imageExtent 是 [minX, minY, maxX, maxY]
const createExtentBorderLayer = (extent: number[]) => {
// 构造矩形坐标,闭合成环,顺序可以是顺时针或逆时针
const coords = [
[
[extent[0], extent[1]],
[extent[0], extent[3]],
[extent[2], extent[3]],
[extent[2], extent[1]],
[extent[0], extent[1]]
]
];
const polygonFeature = new Feature(new Polygon(coords));
polygonFeature.setStyle(
new Style({
stroke: new Stroke({
color: 'red', // 你想要的边框颜色
width: 3 // 线宽
}),
fill: null // 不填充,纯边框
})
);
return new VectorLayer({
source: new VectorSource({
features: [polygonFeature]
})
});
};
const highlightStyle = (name, scale) => {
return new Style({
stroke: new Stroke({
color: 'orange',
width: 4
}),
fill: new Fill({
color: 'rgba(255,165,0,0.3)' // 半透明橙色
}),
text: new Text({
font: '10px Microsoft YaHei',
text: name,
placement: 'line', // 👈 关键属性
offsetX: 50, // 向右偏移 10 像素
offsetY: 20, // 向下偏移 5 像素
scale,
fill: new Fill({ color: '#FFFFFF' })
})
});
};
const defaultStyle = (name, scale) => {
return new Style({
stroke: new Stroke({
color: '#003366',
width: 2
}),
text: new Text({
font: '10px Microsoft YaHei',
text: name,
scale,
placement: 'line', // 👈 关键属性
offsetX: 50, // 向右偏移 10 像素
offsetY: 20, // 向下偏移 5 像素
fill: new Fill({ color: '#FFFFFF ' })
}),
fill: new Fill({ color: 'skyblue' })
});
};
const successStyle = (name, scale) => {
return new Style({
stroke: new Stroke({
color: '#2E7D32 ',
width: 2
}),
text: new Text({
font: '10px Microsoft YaHei',
text: name,
scale,
placement: 'line', // 👈 关键属性
offsetX: 50, // 向右偏移 10 像素
offsetY: 20, // 向下偏移 5 像素
fill: new Fill({ color: '#FFFFFF ' })
}),
fill: new Fill({ color: '#7bdd63 ' })
});
};
const hoverStyle = (name, scale) => {
return new Style({
stroke: new Stroke({
color: 'orange',
width: 2
}),
fill: new Fill({
color: 'rgba(255,165,0,0.3)' // 半透明橙色
}),
text: new Text({
font: '10px Microsoft YaHei',
text: name,
scale,
placement: 'line', // 👈 关键属性
offsetX: 50, // 向右偏移 10 像素
offsetY: 20, // 向下偏移 5 像素
fill: new Fill({ color: '#FFFFFF ' })
})
});
};
/**
* 创建图层
* @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, status?: string) => {
let geometry;
if (type === 'Point') {
geometry = new Point(fromLonLat(pointObj));
} else if (type === 'Polygon') {
const coords = pointObj.map((arr: any) => fromLonLat(arr));
geometry = new Polygon([coords]);
}
const feature = new Feature({ geometry });
const zoom = map.getView().getZoom();
const scale = Math.max(zoom / 30, 1); // 缩放比例,根据需要调整公式
const pointStyle = new Style({
image: new Circle({
radius: 2,
fill: new Fill({ color: 'red' })
}),
text: new Text({
font: '12px Microsoft YaHei',
text: name,
scale,
fill: new Fill({ color: '#7bdd63' })
})
});
const polygonStyle = status == '2' ? successStyle(name, scale) : defaultStyle(name || '', scale);
feature.setStyle(type === 'Point' ? pointStyle : polygonStyle);
feature.set('name', name || ''); // 设置名称
feature.set('status', status || ''); // 设置完成状态 2为完成 其他为未完成
feature.set('id', id || '');
// 缓存 feature用于后续判断
featureMap[id] = feature;
};
// 添加点到地图
const addPointToMap = (features: Array<any>) => {
features.forEach((item: any) => {
const fid = item.id;
// 没创建过就先创建
if (!featureMap[fid]) {
creatPoint(item.positions, item.type, fid, item.name, item.status);
}
// 添加到共享 source 中(避免重复添加)
const feature = featureMap[fid];
if (!sharedSource.hasFeature(feature)) {
sharedSource.addFeature(feature);
}
});
};
//选中几何图形
const toggleFeatureHighlight = (feature: Feature, addIfNotExist = true) => {
const zoom = map.getView().getZoom();
const scale = Math.max(zoom / 10, 1);
if (feature.get('status') === '2') return;
if (feature.getGeometry()?.getType() !== 'Polygon') return;
const isHighlighted = feature.get('highlighted') === true;
if (isHighlighted) {
feature.setStyle(defaultStyle(feature.get('name'), scale));
feature.set('highlighted', false);
const id = feature.get('id');
const idx = submitForm.value.finishedDetailIdList.indexOf(id);
if (idx > -1) submitForm.value.finishedDetailIdList.splice(idx, 1);
} else if (addIfNotExist) {
feature.setStyle(highlightStyle(feature.get('name'), scale));
feature.set('highlighted', true);
const id = feature.get('id');
if (!submitForm.value.finishedDetailIdList.includes(id)) submitForm.value.finishedDetailIdList.push(id);
}
};
onMounted(async () => {
// 地图初始化
// geoTiffLoading.value = true;
await initGeoTiff();
initOLMap();
// geoTiffLoading.value = false;
map.addLayer(sharedLayer);
selector.value = new LassoSelector(map, sharedSource, (features, isInvert = false) => {
features.forEach((feature) => {
if (isInvert) {
// Shift + 左键 -> 只执行取消选中
if (feature.get('highlighted') === true) {
toggleFeatureHighlight(feature, false); // 取消选中addIfNotExist = false
}
} else {
// 普通左键 -> 只执行选中
if (feature.get('highlighted') !== true) {
toggleFeatureHighlight(feature, true); // 选中addIfNotExist = true
}
}
});
});
enableMiddleMousePan(map);
getList();
creatPoint(fromLonLat([107.13149145799198, 23.804125705140834]), 'Point', '1', '测试点1', '1');
});
function enableMiddleMousePan(map: Map) {
// 先移除默认的 DragPan通常响应左键
const interactions = map.getInteractions();
interactions.forEach((interaction) => {
if (interaction instanceof DragPan) {
map.removeInteraction(interaction);
}
});
// 添加只响应中键的 DragPan
const middleButtonDragPan = new DragPan({
condition: (event) => {
// 只允许中键 (mouse button 1) 拖动
return event.originalEvent instanceof MouseEvent && event.originalEvent.button === 1;
}
});
map.addInteraction(middleButtonDragPan);
// 禁用中键点击默认滚动行为(浏览器可能会出现滚动箭头)
// map.getViewport().addEventListener('mousedown', (e) => {
// if (e.button === 1) e.preventDefault();
// });
}
</script>
<style lang="scss" scoped>
.ol-map {
height: 100vh;
width: 100%;
position: absolute;
z-index: 1;
}
.header {
height: 70px;
width: 100%;
position: absolute;
z-index: 2;
background: rgba(255, 255, 255, 0.2); /* 半透明白色 */
backdrop-filter: blur(10px); /* 背景模糊 */
-webkit-backdrop-filter: blur(10px); /* 兼容 Safari */
}
.aside {
position: absolute;
top: 10%;
left: 30px;
width: 300px;
height: 70vh;
background-color: rgba(255, 255, 255, 0.8);
z-index: 3;
overflow: auto;
}
.submit {
position: absolute;
bottom: 70px;
right: 70px;
z-index: 3;
}
.custom-tree-node {
display: flex;
align-items: center;
}
.tips {
margin: 0 15px;
position: relative;
font-size: 18px;
color: #fff;
> div {
margin: 0 25px;
position: relative;
font-size: 12px;
&::before {
position: absolute;
content: '';
display: inline-block;
left: -15px;
top: 30%;
width: 8px;
height: 8px;
border-radius: 50%;
}
}
.dot1 {
&::before {
background-color: #1d6fe9;
}
}
.dot2 {
&::before {
background-color: #67c23a;
}
}
.dot3 {
&::before {
background-color: #ff8d1a;
}
}
}
</style>