import { watch, onUnmounted, type Ref, h } from 'vue'; import { storeToRefs } from 'pinia'; import { Modal, Input } from 'ant-design-vue'; import OlMap from 'ol/Map'; import VectorLayer from 'ol/layer/Vector'; import VectorSource from 'ol/source/Vector'; import Feature from 'ol/Feature'; import Point from 'ol/geom/Point'; import LineString from 'ol/geom/LineString'; import Polygon from 'ol/geom/Polygon'; import Circle from 'ol/geom/Circle'; import { fromLonLat, toLonLat } from 'ol/proj'; import { getLength, getArea } from 'ol/sphere'; import Style from 'ol/style/Style'; import Stroke from 'ol/style/Stroke'; import Fill from 'ol/style/Fill'; import CircleStyle from 'ol/style/Circle'; import Draw, { type DrawEvent } from 'ol/interaction/Draw'; import Select, { type SelectEvent } from 'ol/interaction/Select'; import Modify from 'ol/interaction/Modify'; import Translate from 'ol/interaction/Translate'; import Text from 'ol/style/Text'; import RegularShape from 'ol/style/RegularShape'; import { useMapStudioStore } from '../stores/mapStudioStore'; import { useUndoRedo } from './useUndoRedo'; import type { DrawStyle, SelectedDrawInfo, DrawToolType } from '../types'; /** 已绘制 feature 的自定义属性 key */ export const DRAW_FEATURE_ID_KEY = '_drawId'; export const DRAW_FEATURE_TYPE_KEY = '_drawType'; export const DRAW_FEATURE_TEXT_KEY = '_drawText'; let drawIdCounter = 0; function nextDrawId(): string { return `draw_${Date.now()}_${++drawIdCounter}`; } /** Feature 上存储单个样式对象的属性 key */ const DS_FEATURE_KEY = '_drawPerStyle'; /** * 防止选中图形时同步样式触发不必要撤回步骤的标记。 * updateSelectedDrawInfo 中设为 true,等待 Vue watch 队列消费完毕后自动恢复 false。 */ let skipStyleCp = false; /** * 样式变更的防抖定时器(150ms)。 * 同一交互内的快速连续变更(如拖拽颜色滑块)合并为 1 个撤回步骤; * 不同用户操作(如修改颜色后修改字号)之间创建独立的撤回步骤。 */ let styleCpTimer: ReturnType | null = null; /** 根据 lineType 返回 lineDash 数组 */ function getLineDash(type: string): number[] | undefined { switch (type) { case 'dashed': return [10, 6]; case 'dotted': return [3, 4]; case 'dash-dot': return [10, 4, 3, 4]; default: return undefined; } } /** * 管理自定义绘制工具(Draw/Modify/Select/Translate) * * 设计: * - Select / Modify / Translate 始终存在(永久交互),非绘制状态也能选中/编辑 * - Draw 交互跟随 drawToolActive 增删,光标也由其驱动 * - 每个绘制的 feature 独立存储自己的样式对象,右侧抽屉修改仅影响当前选中的 feature */ /** 模块级 action 引用,由 useDrawTools 填充 */ export const drawActions = { deleteSelectedDraw: null as (() => void) | null, updateSelectedTextContent: null as ((text: string) => void) | null, getSerializedFeatures: null as (() => any[]) | null, clearAllDrawings: null as (() => void) | null, restoreDrawingsFromSave: null as | ((features: any[], style: any) => void) | null }; export function useDrawTools(mapInstance: Ref) { const store = useMapStudioStore(); const { drawToolActive, drawStyle, selectedDrawInfo, activeTab, selectedDrawStyle, selectedDrawText } = storeToRefs(store); // ── 图层 ── const drawSource = new VectorSource(); const drawLayer = new VectorLayer({ source: drawSource, style: createDrawStyleFunction, zIndex: 100 }); // 绘制图形增删即时记录撤回步骤 // ⚠ OL 在 drawend 事件触发时尚未将图形添加到 source, // 因此不能依赖 drawend 内调用 checkpoint(会捕获添加前的状态,导致栈落后1个图形) // 改用 addfeature 事件(OL 将 finalized feature 添加到 source 后触发)来记录添加步骤 const { drawCheckpoint: onDrawChange, setSuppressWatcher, beginDrawBatch } = useUndoRedo(); drawSource.on('addfeature', evt => { const addedFeat = (evt as any).feature; // drawstart 阶段 OL 添加的 sketch feature 没有 DRAW_FEATURE_ID_KEY,过滤掉 // drawend 完成后 OL 添加的 finalized feature 已有 DRAW_FEATURE_ID_KEY,记录 checkpoint // 文字工具跳过:addfeature 触发时文字内容还未录入,等 onOk 确认后再手动创建快照 if ( addedFeat && addedFeat.get(DRAW_FEATURE_ID_KEY) && addedFeat.get(DRAW_FEATURE_TYPE_KEY) !== 'text' ) { onDrawChange(); } }); drawSource.on('removefeature', evt => { const removedFeat = (evt as any).feature; // 只有真实绘制的 feature 才触发 checkpoint,过滤 OL 内部 sketch feature(问题7修复) if (removedFeat && removedFeat.get(DRAW_FEATURE_ID_KEY)) { onDrawChange(); } }); // ── 永久交互(Select / Modify / Translate)─ const persistentInteractions: import('ol/interaction/Interaction')[] = []; let currentDrawInteraction: Draw | null = null; // ── Shift 键状态(用于约束直线/正方形/正圆绘制)── let shiftHeld = false; /** 当前绘制的 sketch feature(用于 shift 按住时强制更新预览) */ let sketchFeature: Feature | null = null; /** 当前 geometryFunction(用于 shift 按住时重新计算几何) */ let currentGeometryFunction: | ((coords: number[][], geom?: any) => any) | null = null; /** 最后一次传递给 geometryFunction 的原始坐标(用于 shift 按住时重新计算) */ let lastRawSketchCoords: number[][] | null = null; // ── 样式工厂(从 feature 自身读取样式,fallback 到 drawStyle)── function createDrawStyleFunction(feature: Feature): Style[] { const perStyle = feature.get(DS_FEATURE_KEY) as | Partial | undefined; const strokeColor = perStyle?.strokeColor ?? drawStyle.value.strokeColor; const strokeWidth = perStyle?.strokeWidth ?? drawStyle.value.strokeWidth; const fillColor = perStyle?.fillColor ?? drawStyle.value.fillColor; const pointColor = perStyle?.pointColor ?? drawStyle.value.pointColor; const pointRadius = perStyle?.pointRadius ?? drawStyle.value.pointRadius; const lineType = perStyle?.lineType ?? drawStyle.value.lineType; const showArrow = perStyle?.showArrow ?? drawStyle.value.showArrow; const arrowPosition = perStyle?.arrowPosition ?? drawStyle.value.arrowPosition; const arrowSize = perStyle?.arrowSize ?? drawStyle.value.arrowSize; const fontSize = perStyle?.fontSize ?? drawStyle.value.fontSize; const fontFamily = perStyle?.fontFamily ?? drawStyle.value.fontFamily; const textColor = perStyle?.textColor ?? drawStyle.value.textColor; const textStrokeColor = perStyle?.textStrokeColor ?? drawStyle.value.textStrokeColor; const textStrokeWidth = perStyle?.textStrokeWidth ?? drawStyle.value.textStrokeWidth; const fontBold = perStyle?.fontBold ?? drawStyle.value.fontBold; const stroke = strokeWidth > 0 ? new Stroke({ color: strokeColor, width: strokeWidth, lineDash: getLineDash(lineType) || [] }) : undefined; const geomType = feature.getGeometry()?.getType(); const textContent = feature.get(DRAW_FEATURE_TEXT_KEY) as | string | undefined; const drawTypeKey = feature.get(DRAW_FEATURE_TYPE_KEY) as | string | undefined; // ── 文字标注 ── if (textContent) { return [ new Style({ text: new Text({ text: textContent, font: `${fontBold ? 'bold ' : ''}${Math.max( fontSize, 12 )}px ${fontFamily}`, fill: new Fill({ color: textColor }), stroke: textStrokeWidth > 0 ? new Stroke({ color: textStrokeColor, width: textStrokeWidth }) : undefined, offsetX: 0, offsetY: 0 }) }) ]; } // ── Point(标记点)── if (geomType === 'Point') { const imgStyle = new CircleStyle({ radius: pointRadius, fill: new Fill({ color: pointColor }) }); if (stroke) imgStyle.setStroke(stroke); return [new Style({ image: imgStyle })]; } const styles: Style[] = []; // ── LineString(折线)── if (geomType === 'LineString') { if (stroke) styles.push(new Style({ stroke })); // 箭头(自由手绘不显示箭头) if (showArrow && drawTypeKey !== 'freehand') { const lineGeom = feature.getGeometry() as LineString; const coords = lineGeom.getCoordinates(); if (coords.length >= 2) { const arrowR = arrowSize === 'small' ? 6 : arrowSize === 'large' ? 12 : 9; // 结束箭头 const last = coords[coords.length - 1]; const prev = coords[coords.length - 2]; const endAngle = Math.atan2(last[1] - prev[1], last[0] - prev[0]); styles.push( new Style({ geometry: new Point(last), image: new RegularShape({ points: 3, radius: arrowR, rotation: endAngle - Math.PI / 2, fill: new Fill({ color: strokeColor }), stroke: new Stroke({ color: strokeColor, width: 1 }) }) }) ); // 起始箭头 if (arrowPosition === 'both' && coords.length >= 2) { const first = coords[0]; const second = coords[1]; const startAngle = Math.atan2( first[1] - second[1], first[0] - second[0] ); styles.push( new Style({ geometry: new Point(first), image: new RegularShape({ points: 3, radius: arrowR, rotation: startAngle - Math.PI / 2, fill: new Fill({ color: strokeColor }), stroke: new Stroke({ color: strokeColor, width: 1 }) }) }) ); } } } } // ── Polygon / Circle ── if (geomType === 'Polygon' || geomType === 'Circle') { const polyStyle = new Style({ fill: new Fill({ color: fillColor }) }); if (stroke) polyStyle.setStroke(stroke); styles.push(polyStyle); } return styles.length > 0 ? styles : []; } // ── 计算几何信息 ── function computeGeomInfo(feature: Feature): SelectedDrawInfo { const geom = feature.getGeometry()!; const geomType = geom.getType(); const id = (feature.get(DRAW_FEATURE_ID_KEY) as string) || nextDrawId(); const drawTypeKey = (feature.get(DRAW_FEATURE_TYPE_KEY) as string) || ''; const coords: number[] = []; if (geomType === 'Point') { const [lng, lat] = toLonLat((geom as Point).getCoordinates()); coords.push(lng, lat); } else if (geomType === 'LineString') { const line = geom as LineString; line.getCoordinates().forEach(p => { const [lng, lat] = toLonLat(p); coords.push(lng, lat); }); } else if (geomType === 'Polygon') { const poly = geom as Polygon; poly.getFirstCoordinate().forEach(p => { const [lng, lat] = toLonLat(p); coords.push(lng, lat); }); } else if (geomType === 'Circle') { const center = toLonLat((geom as Circle).getCenter()); coords.push(center[0], center[1]); } let length: number | undefined; let area: number | undefined; if (geomType === 'LineString') { length = getLength(geom as LineString); } else if (geomType === 'Polygon') { area = getArea(geom as Polygon); } else if (geomType === 'Circle') { area = Math.PI * (geom as Circle).getRadius() ** 2; } let displayType: SelectedDrawInfo['type'] = 'Point'; if (geomType === 'LineString') displayType = 'LineString'; else if (geomType === 'Polygon') displayType = 'Polygon'; else if (geomType === 'Circle') displayType = 'Circle'; return { id, type: displayType, drawTypeKey, coords, length, area }; } let selectedFeatureId: string | null = null; /** 将 feature 存储的样式同步到 selectedDrawStyle */ function syncPerStyleToSelected(feature: Feature | null) { if (!feature) { selectedDrawStyle.value = { ...drawStyle.value }; selectedDrawText.value = ''; return; } const stored = feature.get(DS_FEATURE_KEY) as | Partial | undefined; selectedDrawStyle.value = { strokeColor: stored?.strokeColor ?? drawStyle.value.strokeColor, strokeWidth: stored?.strokeWidth ?? drawStyle.value.strokeWidth, fillColor: stored?.fillColor ?? drawStyle.value.fillColor, pointColor: stored?.pointColor ?? drawStyle.value.pointColor, pointRadius: stored?.pointRadius ?? drawStyle.value.pointRadius, lineType: stored?.lineType ?? drawStyle.value.lineType, showArrow: stored?.showArrow ?? drawStyle.value.showArrow, arrowPosition: stored?.arrowPosition ?? drawStyle.value.arrowPosition, arrowSize: stored?.arrowSize ?? drawStyle.value.arrowSize, fontSize: stored?.fontSize ?? drawStyle.value.fontSize, fontFamily: stored?.fontFamily ?? drawStyle.value.fontFamily, textColor: stored?.textColor ?? drawStyle.value.textColor, textStrokeColor: stored?.textStrokeColor ?? drawStyle.value.textStrokeColor, textStrokeWidth: stored?.textStrokeWidth ?? drawStyle.value.textStrokeWidth, fontBold: stored?.fontBold ?? drawStyle.value.fontBold }; selectedDrawText.value = (feature.get(DRAW_FEATURE_TEXT_KEY) as string) || ''; } /** 强制刷新图层渲染 */ function refreshLayerStyle() { drawLayer.changed(); } /** 将 selectedDrawStyle 写回 feature 并刷新渲染 */ function saveSelectedDrawStyleToFeature() { if (!selectedFeatureId) return; const feature = getSelectedFeature(); if (!feature) return; const s = selectedDrawStyle.value; feature.set(DS_FEATURE_KEY, { strokeColor: s.strokeColor, strokeWidth: s.strokeWidth, fillColor: s.fillColor, pointColor: s.pointColor, pointRadius: s.pointRadius, lineType: s.lineType, showArrow: s.showArrow, arrowPosition: s.arrowPosition, arrowSize: s.arrowSize, fontSize: s.fontSize, fontFamily: s.fontFamily, textColor: s.textColor, textStrokeColor: s.textStrokeColor, textStrokeWidth: s.textStrokeWidth, fontBold: s.fontBold }); refreshLayerStyle(); // 用户主动编辑样式时创建撤回步骤(跳过选中 feature 时的首次同步) // 防抖 200ms:同一交互内的快速连续变更(如拖拽颜色滑块)合并为 1 个步骤, // 停止操作 200ms 后自动创建撤回步骤;不同用户操作之间产生独立步骤。 if (!skipStyleCp) { if (styleCpTimer) { clearTimeout(styleCpTimer); } styleCpTimer = setTimeout(() => { styleCpTimer = null; onDrawChange(); }, 200); } } // 监听 selectedDrawStyle 变化 → 自动保存到当前 feature const stopPerStyleWatch = watch( selectedDrawStyle, () => { saveSelectedDrawStyleToFeature(); }, { deep: true } ); // 监听 drawStyle(全局默认)变化 → 刷新渲染 const stopStyleWatch = watch( drawStyle, () => { refreshLayerStyle(); }, { deep: true } ); // ── 更新 selectedDrawInfo ── function updateSelectedDrawInfo(feature: Feature | null) { // 选中/取消选中图形时同步样式,不应产生撤回步骤 skipStyleCp = true; setTimeout(() => { skipStyleCp = false; }, 0); if (!feature) { selectedDrawInfo.value = null; selectedFeatureId = null; syncPerStyleToSelected(null); return; } selectedFeatureId = feature.get(DRAW_FEATURE_ID_KEY) as string; selectedDrawInfo.value = computeGeomInfo(feature); syncPerStyleToSelected(feature); } // ── 删除选中的绘制图形 ── function deleteSelectedDraw() { if (!selectedFeatureId) return; const feature = drawSource .getFeatures() .find(f => f.get(DRAW_FEATURE_ID_KEY) === selectedFeatureId); if (feature) drawSource.removeFeature(feature); selectedDrawInfo.value = null; selectedFeatureId = null; } function getSelectedFeature(): Feature | null { if (!selectedFeatureId) return null; return ( drawSource .getFeatures() .find(f => f.get(DRAW_FEATURE_ID_KEY) === selectedFeatureId) || null ); } /** 更新选中 feature 的文字内容 */ function updateSelectedTextContent(text: string) { if (!selectedFeatureId) return; const feature = getSelectedFeature(); if (!feature) return; feature.set(DRAW_FEATURE_TEXT_KEY, text); refreshLayerStyle(); onDrawChange(); } // ── 设置永久交互(Select / Modify / Translate)────────────── function setupPersistentInteractions() { if (persistentInteractions.length > 0) return; // 防重复 const map = mapInstance.value; if (!map) return; // ── Select ── const selectInteraction = new Select({ layers: [drawLayer], style: null }); selectInteraction.on('select', (evt: SelectEvent) => { const feat = evt.selected.length > 0 ? evt.selected[0] : null; // 选中图形只切换 tab 展示内容,不应产生撤回步骤(问题6修复) beginDrawBatch(); updateSelectedDrawInfo(feat); if (feat) { activeTab.value = 'draw'; } else if (!evt.deselected.length) { // 点击空白 → 取消选中 selectedDrawInfo.value = null; selectedFeatureId = null; } }); map.addInteraction(selectInteraction); persistentInteractions.push(selectInteraction); // ── Modify ── const modifyInteraction = new Modify({ source: drawSource, style: null }); map.addInteraction(modifyInteraction); persistentInteractions.push(modifyInteraction); // ── Translate ── const translateInteraction = new Translate({ layers: [drawLayer] }); translateInteraction.on('translating', () => { if (selectedFeatureId) { const feat = drawSource .getFeatures() .find(f => f.get(DRAW_FEATURE_ID_KEY) === selectedFeatureId); if (feat) { selectedDrawInfo.value = computeGeomInfo(feat); } } }); map.addInteraction(translateInteraction); persistentInteractions.push(translateInteraction); } // ── 同步 Draw 交互(跟随 drawToolActive)───────────────────── /** 根据当前工具生成 Draw 预览样式 */ function createDrawPreviewStyle(): Style[] { const ds = drawStyle.value; const isPointTool = drawToolActive.value === 'point'; const isLineTool = drawToolActive.value === 'line' || drawToolActive.value === 'freehand'; const previewStroke = ds.strokeWidth > 0 ? new Stroke({ color: ds.strokeColor, width: ds.strokeWidth, lineDash: isLineTool ? getLineDash(ds.lineType) || [] : [] }) : undefined; return [ new Style({ stroke: previewStroke, fill: new Fill({ color: ds.fillColor }), image: isPointTool ? new CircleStyle({ radius: ds.pointRadius, fill: new Fill({ color: ds.pointColor }), stroke: previewStroke || undefined }) : undefined }) ]; } // ── Shift 约束的 geometryFunction ── /** * 椭圆/正圆 geometryFunction * 使用 type:'Circle'(click-drag-release), * 第一点为外接矩形左上角,第二点为右下角 * 默认椭圆,按住 shift 为正圆 */ function createEllipseGeometryFunction(): ( coords: number[][], geom?: any ) => any { return function (coords: number[][], geom?: any): any { // 保存原始坐标,用于 Shift 按住时强制更新预览 lastRawSketchCoords = coords.map(c => [c[0], c[1]]); if (coords.length < 2) { const p = coords[0]; const pts = 64; const ring: number[][] = []; for (let i = 0; i <= pts; i++) { const a = (i / pts) * 2 * Math.PI; ring.push([p[0], p[1]]); } if (geom) { geom.setCoordinates([ring]); return geom; } return new Polygon([ring]); } // 左上角 (x1,y1) → 右下角 (x2,y2) const x1 = coords[0][0], y1 = coords[0][1]; const x2 = coords[1][0], y2 = coords[1][1]; const cx = (x1 + x2) / 2; const cy = (y1 + y2) / 2; let rx = Math.abs(x2 - x1) / 2 || 1; let ry = Math.abs(y2 - y1) / 2 || 1; if (shiftHeld) { const r = Math.max(rx, ry); rx = r; ry = r; } const pts = 64; const ring: number[][] = []; for (let i = 0; i <= pts; i++) { const a = (i / pts) * 2 * Math.PI; ring.push([cx + rx * Math.cos(a), cy + ry * Math.sin(a)]); } if (geom) { geom.setCoordinates([ring]); return geom; } return new Polygon([ring]); }; } /** * 矩形/正方形 geometryFunction * 第一点为左上角,第二点为右下角 * 默认矩形,按住 shift 为正方形 */ function createRectGeometryFunction(): ( coords: number[][], geom?: any ) => any { return function (coords: number[][], geom?: any): any { // 保存原始坐标,用于 Shift 按住时强制更新预览 lastRawSketchCoords = coords.map(c => [c[0], c[1]]); if (coords.length < 2) { const p = coords[0]; const ring: number[][] = [ [p[0], p[1]], [p[0], p[1]], [p[0], p[1]], [p[0], p[1]], [p[0], p[1]] ]; if (geom) { geom.setCoordinates([ring]); return geom; } return new Polygon([ring]); } // 左上角 (x1,y1) → 右下角 (x2,y2) const x1 = coords[0][0], y1 = coords[0][1]; let x2 = coords[1][0], y2 = coords[1][1]; if (shiftHeld) { const w = Math.abs(x2 - x1); const h = Math.abs(y2 - y1); const max = Math.max(w, h); if (w > h) { y2 = y1 + (y2 >= y1 ? 1 : -1) * max; } else { x2 = x1 + (x2 >= x1 ? 1 : -1) * max; } } const ring: number[][] = [ [Math.min(x1, x2), Math.min(y1, y2)], [Math.min(x1, x2), Math.max(y1, y2)], [Math.max(x1, x2), Math.max(y1, y2)], [Math.max(x1, x2), Math.min(y1, y2)], [Math.min(x1, x2), Math.min(y1, y2)] ]; if (geom) { geom.setCoordinates([ring]); return geom; } return new Polygon([ring]); }; } function createShiftConstrainedGeometryFunction(): ( coords: number[][], geom?: any ) => any { return function (coords: number[][], geom?: any): any { if (!geom) { geom = new LineString(coords); } else { geom.setCoordinates(coords); } if (shiftHeld && coords.length >= 2) { const c = geom.getCoordinates(); const last = c[c.length - 1]; const prev = c[c.length - 2]; const dx = Math.abs(last[0] - prev[0]); const dy = Math.abs(last[1] - prev[1]); if (dx > dy) { last[1] = prev[1]; // 水平约束 } else { last[0] = prev[0]; // 垂直约束 } geom.setCoordinates(c); } return geom; }; } function syncDrawInteraction() { const map = mapInstance.value; if (!map) return; // 暴力扫描移除所有 Draw 交互 const interactions = map.getInteractions().getArray(); for (let i = interactions.length - 1; i >= 0; i--) { if (interactions[i] instanceof Draw) { map.removeInteraction(interactions[i]); } } currentDrawInteraction = null; // 恢复默认光标 map.getViewport().style.cursor = ''; // 没有激活的工具 → 结束 if (!drawToolActive.value) return; // 十字光标 map.getViewport().style.cursor = 'crosshair'; const tool = drawToolActive.value; let drawType: string; let geometryFunction: any | undefined; currentGeometryFunction = null; // 重置,仅 rect/circle 会赋值 switch (tool) { case 'point': drawType = 'Point'; break; case 'line': drawType = 'LineString'; geometryFunction = createShiftConstrainedGeometryFunction(); break; case 'polygon': drawType = 'Polygon'; break; case 'rect': drawType = 'LineString'; geometryFunction = createRectGeometryFunction(); currentGeometryFunction = geometryFunction; break; case 'circle': drawType = 'LineString'; geometryFunction = createEllipseGeometryFunction(); currentGeometryFunction = geometryFunction; break; case 'freehand': drawType = 'LineString'; break; case 'text': drawType = 'Point'; break; default: return; } currentDrawInteraction = new Draw({ source: drawSource, type: drawType as any, geometryFunction, maxPoints: tool === 'rect' || tool === 'circle' ? 2 : undefined, freehand: tool === 'freehand', freehandCondition: () => false, condition: () => true, style: createDrawPreviewStyle() }); currentDrawInteraction.on('drawstart', (evt: DrawEvent) => { sketchFeature = evt.feature; lastRawSketchCoords = null; }); currentDrawInteraction.on('drawend', () => { sketchFeature = null; lastRawSketchCoords = null; currentGeometryFunction = null; }); currentDrawInteraction.on('drawend', (evt: DrawEvent) => { const feature = evt.feature; feature.set(DRAW_FEATURE_ID_KEY, nextDrawId()); feature.set(DRAW_FEATURE_TYPE_KEY, tool); // 如果是文字工具,弹出 ant-design-vue Modal 输入框 if (tool === 'text') { feature.set(DS_FEATURE_KEY, { strokeColor: drawStyle.value.strokeColor, strokeWidth: drawStyle.value.strokeWidth, fillColor: drawStyle.value.fillColor, pointColor: drawStyle.value.pointColor, pointRadius: drawStyle.value.pointRadius, lineType: drawStyle.value.lineType, showArrow: drawStyle.value.showArrow, arrowPosition: drawStyle.value.arrowPosition, arrowSize: drawStyle.value.arrowSize, fontSize: drawStyle.value.fontSize, fontFamily: drawStyle.value.fontFamily, textColor: drawStyle.value.textColor, textStrokeColor: drawStyle.value.textStrokeColor, textStrokeWidth: drawStyle.value.textStrokeWidth, fontBold: drawStyle.value.fontBold }); // 先抑制 watcher(suppressUntil),避免 updateSelectedDrawInfo 触发的 checkpoint(问题5修复) beginDrawBatch(); updateSelectedDrawInfo(feature); activeTab.value = 'draw'; let tempText = ''; Modal.confirm({ title: '输入文字内容', content: () => h(Input, { placeholder: '请输入文字内容', onInput: (e: any) => { tempText = e.target?.value ?? ''; } }), onOk: () => { if (tempText && tempText.trim()) { feature.set(DRAW_FEATURE_TEXT_KEY, tempText.trim()); } else { drawSource.removeFeature(feature); selectedDrawInfo.value = null; selectedFeatureId = null; } // 文字确认后手动创建撤回步骤(addfeature 阶段已跳过),捕获带文字内容的状态 onDrawChange(); // 同步样式不应产生额外撤回步骤(onDrawChange 已记录完整的文字+样式状态) skipStyleCp = true; setTimeout(() => { skipStyleCp = false; }, 0); syncPerStyleToSelected(feature); refreshLayerStyle(); }, onCancel: () => { drawSource.removeFeature(feature); selectedDrawInfo.value = null; selectedFeatureId = null; } }); return; } // 存储当前默认样式到新绘制的 feature feature.set(DS_FEATURE_KEY, { strokeColor: drawStyle.value.strokeColor, strokeWidth: drawStyle.value.strokeWidth, fillColor: drawStyle.value.fillColor, pointColor: drawStyle.value.pointColor, pointRadius: drawStyle.value.pointRadius, lineType: drawStyle.value.lineType, showArrow: drawStyle.value.showArrow, arrowPosition: drawStyle.value.arrowPosition, arrowSize: drawStyle.value.arrowSize, fontSize: drawStyle.value.fontSize, fontFamily: drawStyle.value.fontFamily, textColor: drawStyle.value.textColor, textStrokeColor: drawStyle.value.textStrokeColor, textStrokeWidth: drawStyle.value.textStrokeWidth, fontBold: drawStyle.value.fontBold }); beginDrawBatch(); updateSelectedDrawInfo(feature); activeTab.value = 'draw'; // ⚠ 不再在此处调用 onDrawChange(): // OL 在 drawend 时尚未将图形添加到 source,checkpoint 会捕获添加前的状态。 // 改为由 addfeature 监听器(已在 setup 中注册)在 OL 添加图形后自动触发。 }); map.addInteraction(currentDrawInteraction); } // ── 监听工具切换 ── const stopToolWatch = watch(drawToolActive, () => { syncDrawInteraction(); // 绘制工具激活时禁用 Select/Modify/Translate,防止与绘制交互冲突 const isDrawing = drawToolActive.value !== null; for (const interaction of persistentInteractions) { interaction.setActive(!isDrawing); } }); // ── 监听地图注入,添加图层 + 设置永久交互 ── const stopLayerWatch = watch( mapInstance, map => { if (!map) return; const layers = map.getLayers(); if (!layers.getArray().includes(drawLayer)) { layers.push(drawLayer); } setupPersistentInteractions(); syncDrawInteraction(); }, { immediate: true } ); // ── Shift 键监听 ── /** 当 Shift 状态变化时,强制更新预览几何(让矩形/圆形立即受约束) */ function updateDrawPreviewGeometry() { if ( !sketchFeature || !currentGeometryFunction || !lastRawSketchCoords || lastRawSketchCoords.length < 2 ) return; const existingGeom = sketchFeature.getGeometry(); const newGeom = currentGeometryFunction(lastRawSketchCoords, existingGeom); if (newGeom && newGeom !== existingGeom) { sketchFeature.setGeometry(newGeom); } } function onShiftDown(e: KeyboardEvent) { if (e.key === 'Shift') { shiftHeld = true; updateDrawPreviewGeometry(); } } function onShiftUp(e: KeyboardEvent) { if (e.key === 'Shift') { shiftHeld = false; updateDrawPreviewGeometry(); } } document.addEventListener('keydown', onShiftDown); document.addEventListener('keyup', onShiftUp); // ── 键盘 Delete 键删除选中图形 ── function onKeyDown(e: KeyboardEvent) { if (e.key !== 'Delete' && e.key !== 'Backspace') return; if ( e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement ) { return; } if (selectedFeatureId) deleteSelectedDraw(); } document.addEventListener('keydown', onKeyDown); // ── 清理 ── function dispose() { stopLayerWatch(); stopStyleWatch(); stopToolWatch(); stopPerStyleWatch(); if (styleCpTimer !== null) { clearTimeout(styleCpTimer); styleCpTimer = null; } document.removeEventListener('keydown', onKeyDown); document.removeEventListener('keydown', onShiftDown); document.removeEventListener('keyup', onShiftUp); const map = mapInstance.value; if (!map) return; const interactions = map.getInteractions().getArray(); for (let i = interactions.length - 1; i >= 0; i--) { if (interactions[i] instanceof Draw) { map.removeInteraction(interactions[i]); } } currentDrawInteraction = null; for (const interaction of persistentInteractions) { map.removeInteraction(interaction); } persistentInteractions.length = 0; map.removeLayer(drawLayer); } onUnmounted(() => dispose()); // ── 从序列化数据创建几何体 ── function createGeometry( type: string, coords: any, radius?: number ): import('ol/geom/Geometry') | null { try { switch (type) { case 'Point': return new Point(fromLonLat(coords)); case 'LineString': return new LineString(coords.map((p: number[]) => fromLonLat(p))); case 'Polygon': return new Polygon([coords.map((p: number[]) => fromLonLat(p))]); case 'Circle': { const center = fromLonLat(coords); const circle = new Circle(center, radius || 1000); return circle; } default: return null; } } catch { return null; } } // ── 清空所有绘制图形(恢复时使用) ── function clearAllDrawings() { drawSource.clear(); } // ── 从保存的数据恢复绘制图形(编辑时使用) ── function restoreDrawingsFromSave(features: any[], globalStyle: any) { // 确保绘制图层已添加到地图 if (mapInstance.value) { const layers = mapInstance.value.getLayers(); if (!layers.getArray().includes(drawLayer)) { layers.push(drawLayer); } } for (const item of features) { const geom = createGeometry(item.type, item.coordinates, item.radius); if (!geom) continue; const feature = new Feature({ geometry: geom }); feature.set(DRAW_FEATURE_ID_KEY, item.id || nextDrawId()); feature.set(DRAW_FEATURE_TYPE_KEY, item.drawTypeKey || ''); feature.set(DRAW_FEATURE_TEXT_KEY, item.text || ''); if (item.style && Object.keys(item.style).length > 0) { feature.set(DS_FEATURE_KEY, { ...item.style }); } drawSource.addFeature(feature); } } // ── 序列化所有绘制图形(用于保存) ── function getSerializedFeatures(): any[] { return drawSource.getFeatures().map(f => { const geom = f.getGeometry(); const geomType = geom?.getType(); let coords: any; let radius = 0; if (geomType === 'Point') { coords = toLonLat((geom as Point).getCoordinates()); } else if (geomType === 'LineString') { coords = (geom as LineString).getCoordinates().map(p => toLonLat(p)); } else if (geomType === 'Polygon') { coords = (geom as Polygon).getCoordinates()[0].map(p => toLonLat(p)); } else if (geomType === 'Circle') { const circle = geom as Circle; coords = toLonLat(circle.getCenter()); radius = circle.getRadius(); } const perStyle = f.get(DS_FEATURE_KEY) || {}; return { id: f.get(DRAW_FEATURE_ID_KEY), type: geomType, drawTypeKey: f.get(DRAW_FEATURE_TYPE_KEY) || '', text: f.get(DRAW_FEATURE_TEXT_KEY) || '', coordinates: coords, radius, style: { ...perStyle } }; }); } // 填充模块级 action 引用 drawActions.deleteSelectedDraw = deleteSelectedDraw; drawActions.updateSelectedTextContent = updateSelectedTextContent; drawActions.getSerializedFeatures = getSerializedFeatures; drawActions.clearAllDrawings = clearAllDrawings; drawActions.restoreDrawingsFromSave = restoreDrawingsFromSave; return { drawLayer, drawSource, deleteSelectedDraw, getSelectedFeature, saveSelectedDrawStyleToFeature, updateSelectedTextContent, dispose }; }