套索工具(超叼版)

This commit is contained in:
Teo
2025-06-06 20:03:35 +08:00
parent f128217846
commit d6c0d6319f
9 changed files with 665 additions and 44 deletions

View File

@ -1,7 +1,15 @@
<template>
<div class="header flex justify-end">
<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">
<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>
@ -45,15 +53,20 @@ 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 } from 'ol/interaction';
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';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const selector = ref<LassoSelector | null>(null);
// 获取用户 store
const userStore = useUserStoreHook();
// 从 store 中获取项目列表和当前选中的项目
@ -122,7 +135,14 @@ const handleCheckChange = (data: any, checked: boolean, indeterminate: boolean)
}
return;
}
if (!checked) return (submitForm.value.id = ''); // 只处理第三级节点的选中事件
// 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;
@ -131,6 +151,7 @@ const handleCheckChange = (data: any, checked: boolean, indeterminate: boolean)
// 设置当前点击项为选中
treeRef.value.setChecked(data.id, true, false);
submitForm.value.id = data.id; // 设置提交表单的id
console.log('submitForm', submitForm.value);
};
//清除某一级节点所有选中状态
@ -213,6 +234,7 @@ 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(() => {
@ -267,7 +289,7 @@ const renderContent = (context, { node, data }) => {
style: 'margin-right: 8px;',
disabled: data.disabled
}),
h('span', node.label)
h('span', [node.label])
]);
}
if (node.level === 2) {
@ -278,13 +300,58 @@ const renderContent = (context, { node, data }) => {
style: 'margin-right: 8px;',
disabled: !data.threeChildren || data.threeChildren.length == 0
}),
h('span', node.label)
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;
@ -330,7 +397,13 @@ const initOLMap = () => {
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}'
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'
})
})
],
@ -360,20 +433,7 @@ const initOLMap = () => {
const zoom = map.getView().getZoom();
const scale = Math.max(zoom / 10, 1); // 缩放比例,根据需要调整公式
map.forEachFeatureAtPixel(e.pixel, (feature: Feature) => {
const geomType = feature.getGeometry().getType();
if (feature.get('status') === '2' || geomType != 'Polygon') return; // 如果是完成状态,直接返回
const isHighlighted = feature.get('highlighted') === true;
if (isHighlighted) {
feature.setStyle(defaultStyle(feature.get('name'), scale)); // 清除高亮样式
feature.set('highlighted', false);
submitForm.value.finishedDetailIdList = submitForm.value.finishedDetailIdList.filter((id) => id !== feature.get('id')); // 从已完成列表中移除
return;
}
feature.setStyle(highlightStyle(feature.get('name'), scale));
feature.set('highlighted', true);
submitForm.value.finishedDetailIdList.push(feature.get('id')); // 添加到已完成列表
toggleFeatureHighlight(feature);
});
});
map.getView().on('change:resolution', () => {
@ -389,6 +449,58 @@ const initOLMap = () => {
}
});
});
// 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;
}
});
};
const highlightStyle = (name, scale) => {
@ -401,13 +513,13 @@ const highlightStyle = (name, scale) => {
color: 'rgba(255,165,0,0.3)' // 半透明橙色
}),
text: new Text({
font: '14px Microsoft YaHei',
font: '10px Microsoft YaHei',
text: name,
placement: 'line', // 👈 关键属性
offsetX: 50, // 向右偏移 10 像素
offsetY: 20, // 向下偏移 5 像素
scale,
fill: new Fill({ color: 'orange' })
fill: new Fill({ color: '#FFFFFF' })
})
});
};
@ -419,13 +531,13 @@ const defaultStyle = (name, scale) => {
width: 2
}),
text: new Text({
font: '12px Microsoft YaHei',
font: '10px Microsoft YaHei',
text: name,
scale,
placement: 'line', // 👈 关键属性
offsetX: 50, // 向右偏移 10 像素
offsetY: 20, // 向下偏移 5 像素
fill: new Fill({ color: '#003366 ' })
fill: new Fill({ color: '#FFFFFF ' })
}),
fill: new Fill({ color: 'skyblue' })
});
@ -438,7 +550,7 @@ const successStyle = (name, scale) => {
width: 2
}),
text: new Text({
font: '14px Microsoft YaHei',
font: '10px Microsoft YaHei',
text: name,
scale,
placement: 'line', // 👈 关键属性
@ -450,6 +562,27 @@ const successStyle = (name, scale) => {
});
};
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 坐标数组
@ -477,7 +610,7 @@ const creatPoint = (pointObj: Array<any>, type: string, id: string, name?: strin
const feature = new Feature({ geometry });
const zoom = map.getView().getZoom();
const scale = Math.max(zoom / 10, 1); // 缩放比例,根据需要调整公式
const scale = Math.max(zoom / 30, 1); // 缩放比例,根据需要调整公式
const pointStyle = new Style({
image: new Circle({
radius: 2,
@ -519,15 +652,77 @@ const addPointToMap = (features: Array<any>) => {
});
};
//选中几何图形
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(() => {
// 地图初始化
initOLMap();
map.addLayer(sharedLayer);
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d', { willReadFrequently: true });
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();
});
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>
@ -566,4 +761,40 @@ onMounted(() => {
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>