From 55362bef8fd2b47097fe45ebb8c864b8140e68a1 Mon Sep 17 00:00:00 2001 From: Huarch Date: Thu, 19 Mar 2026 15:38:45 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8E=BB=E6=8E=89=E5=85=A8=E5=B1=80=20id=3D"de?= =?UTF-8?q?ck-canvas"=20=E8=B7=AF=E5=BE=84=EF=BC=8C=E6=94=B9=E4=B8=BA?= =?UTF-8?q?=E5=AE=9E=E4=BE=8B=E7=BA=A7=20canvasRef=EF=BC=8C=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E5=8F=AF=E8=83=BD=E5=87=BA=E7=8E=B0=E7=9A=84=20Uncaug?= =?UTF-8?q?ht=20Error:=20deck.gl:=20assertion=20failed=20=E7=9A=84?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/olmap/core/MapComponent.tsx | 74 ++++++++++++++++++---- src/utils/layers.ts | 50 ++++++++++++++- 2 files changed, 109 insertions(+), 15 deletions(-) diff --git a/src/components/olmap/core/MapComponent.tsx b/src/components/olmap/core/MapComponent.tsx index 0374982..2fa0abb 100644 --- a/src/components/olmap/core/MapComponent.tsx +++ b/src/components/olmap/core/MapComponent.tsx @@ -127,7 +127,10 @@ const MapComponent: React.FC = ({ children }) => { const MAP_VIEW_STORAGE_KEY = `${MAP_WORKSPACE}_map_view`; // 持久化 key const mapRef = useRef(null); + const canvasRef = useRef(null); const deckLayerRef = useRef(null); + const isDisposingRef = useRef(false); + const pendingTimeoutsRef = useRef([]); const [map, setMap] = useState(); const [deckLayer, setDeckLayer] = useState(); @@ -518,14 +521,37 @@ const MapComponent: React.FC = ({ children }) => { // The map and layer instances are intentionally rebuilt only when workspace or extent changes. useEffect(() => { if (!mapRef.current) return; + if (!canvasRef.current) { + return; + } + isDisposingRef.current = false; + + const addTimeout = (callback: () => void, delay: number) => { + const timerId = window.setTimeout(() => { + pendingTimeoutsRef.current = pendingTimeoutsRef.current.filter( + (id) => id !== timerId, + ); + if (isDisposingRef.current) return; + callback(); + }, delay); + pendingTimeoutsRef.current.push(timerId); + return timerId; + }; + + const clearPendingTimeouts = () => { + pendingTimeoutsRef.current.forEach((id) => clearTimeout(id)); + pendingTimeoutsRef.current = []; + }; + // 缓存 junction、pipe 数据,提供给 deck.gl 提供坐标供标签显示 - junctionSource.on("tileloadend", (event) => { + const handleJunctionTileLoadEnd = (event: any) => { + if (isDisposingRef.current) return; try { if (event.tile instanceof VectorTile) { const renderFeatures = event.tile.getFeatures(); const data = new Map(); - renderFeatures.forEach((renderFeature) => { + renderFeatures.forEach((renderFeature: any) => { const props = renderFeature.getProperties(); const featureId = props.id; if (featureId && !junctionDataIds.current.has(featureId)) { @@ -554,14 +580,15 @@ const MapComponent: React.FC = ({ children }) => { } catch (error) { console.error("Junction tile load error:", error); } - }); - pipeSource.on("tileloadend", (event) => { + }; + const handlePipeTileLoadEnd = (event: any) => { + if (isDisposingRef.current) return; try { if (event.tile instanceof VectorTile) { const renderFeatures = event.tile.getFeatures(); const data = new Map(); - renderFeatures.forEach((renderFeature) => { + renderFeatures.forEach((renderFeature: any) => { try { const props = renderFeature.getProperties(); const featureId = props.id; @@ -634,7 +661,9 @@ const MapComponent: React.FC = ({ children }) => { } catch (error) { console.error("Pipe tile load error:", error); } - }); + }; + junctionSource.on("tileloadend", handleJunctionTileLoadEnd); + pipeSource.on("tileloadend", handlePipeTileLoadEnd); // 监听 junctionsLayer 的 visible 变化 const handleJunctionVisibilityChange = () => { const isVisible = junctionsLayer.getVisible(); @@ -748,6 +777,7 @@ const MapComponent: React.FC = ({ children }) => { } // 持久化视图(中心点 + 缩放),防抖写入 localStorage const persistView = debounce(() => { + if (isDisposingRef.current) return; try { const view = map.getView(); const center = view.getCenter(); @@ -765,7 +795,7 @@ const MapComponent: React.FC = ({ children }) => { // 监听缩放变化并持久化,同时更新 currentZoom const handleViewChange = () => { - setTimeout(() => { + addTimeout(() => { const zoom = map.getView().getZoom() || 0; setCurrentZoom(zoom); persistView(); @@ -774,7 +804,7 @@ const MapComponent: React.FC = ({ children }) => { map.getView().on("change", handleViewChange); // 初始化当前缩放级别并强制触发瓦片加载 - setTimeout(() => { + addTimeout(() => { const initialZoom = map.getView().getZoom() || 11; setCurrentZoom(initialZoom); // 强制触发地图渲染,让瓦片加载事件触发 @@ -788,11 +818,11 @@ const MapComponent: React.FC = ({ children }) => { latitude: 0, zoom: 1, }, - canvas: "deck-canvas", + canvas: canvasRef.current, controller: false, // 由 OpenLayers 控制视图 layers: [], }); - const deckLayer = new DeckLayer(deck, { + const deckLayer = new DeckLayer(deck, canvasRef.current, { name: "deckLayer", value: "deckLayer", }); @@ -802,19 +832,37 @@ const MapComponent: React.FC = ({ children }) => { // 清理函数 return () => { + isDisposingRef.current = true; + clearPendingTimeouts(); + debouncedUpdateDataRef.current?.cancel(); + persistView.cancel(); + junctionSource.un("tileloadend", handleJunctionTileLoadEnd); + pipeSource.un("tileloadend", handlePipeTileLoadEnd); + map.getView().un("change", handleViewChange); junctionsLayer.un("change:visible", handleJunctionVisibilityChange); pipesLayer.un("change:visible", handlePipeVisibilityChange); + if (deckLayerRef.current && !deckLayerRef.current.isDisposedLayer()) { + try { + map.removeLayer(deckLayerRef.current); + } catch { + // Layer may have already been removed during teardown. + } + deckLayerRef.current.disposeDeck(); + } + deckLayerRef.current = null; + setDeckLayer(undefined); map.setTarget(undefined); map.dispose(); - deck.finalize(); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [MAP_WORKSPACE, MAP_EXTENT]); // 当数据变化时,更新 deck.gl 图层 useEffect(() => { + if (isDisposingRef.current) return; const deckLayer = deckLayerRef.current; if (!deckLayer) return; // 如果 deck 实例还未创建,则退出 + if (deckLayer.isDisposedLayer()) return; if (!mergedJunctionData.length) return; if (!mergedPipeData.length) return; const junctionTextLayer = new TextLayer({ @@ -964,6 +1012,7 @@ const MapComponent: React.FC = ({ children }) => { // 控制流动动画开关 useEffect(() => { + if (isDisposingRef.current) return; if (pipeText === "flow" && currentPipeCalData.length > 0) { flowAnimation.current = true; } else { @@ -976,6 +1025,7 @@ const MapComponent: React.FC = ({ children }) => { // 动画循环 const animate = () => { + if (isDisposingRef.current || deckLayer.isDisposedLayer()) return; // 动画总时长(秒) const animationDuration = 10; const bufferTime = 2; @@ -1075,7 +1125,7 @@ const MapComponent: React.FC = ({ children }) => { {children} - + diff --git a/src/utils/layers.ts b/src/utils/layers.ts index 3febb12..c9ecc30 100644 --- a/src/utils/layers.ts +++ b/src/utils/layers.ts @@ -8,6 +8,8 @@ import { toLonLat } from "ol/proj"; */ export class DeckLayer extends Layer { private deck: Deck; + private canvasEl: HTMLCanvasElement | null; + private isDisposed = false; private onVisibilityChange?: (layerId: string, visible: boolean) => void; private userVisibility: Map = new Map(); // 存储用户设置的可见性 @@ -15,10 +17,15 @@ export class DeckLayer extends Layer { * @param deckInstance deck.gl 实例 * @param layerProperties 可选:在构造时直接设置到 OpenLayers Layer 的 properties */ - constructor(deckInstance: Deck, layerProperties?: Record) { + constructor( + deckInstance: Deck, + canvasElement: HTMLCanvasElement | null, + layerProperties?: Record + ) { // 将 layerProperties 作为 Layer 的 properties 传入 super({ properties: layerProperties || {} }); this.deck = deckInstance; + this.canvasEl = canvasElement; // 再次确保属性应用到实例(兼容场景) if (layerProperties) { this.setProperties(layerProperties); @@ -38,6 +45,9 @@ export class DeckLayer extends Layer { } render(frameState: any): HTMLElement { + if (this.isDisposed) { + return this.canvasEl || document.createElement("div"); + } const { size, viewState } = frameState; const [width, height] = size; const [longitude, latitude] = toLonLat(viewState.center); @@ -46,7 +56,7 @@ export class DeckLayer extends Layer { const deckViewState = { bearing, longitude, latitude, zoom }; this.deck.setProps({ width, height, viewState: deckViewState }); this.deck.redraw(); - return document.getElementById("deck-canvas") as HTMLElement; + return this.canvasEl || document.createElement("div"); } // 获取 Deck 实例 @@ -56,16 +66,19 @@ export class DeckLayer extends Layer { // 设置图层 setDeckLayers(layers: any[]): void { + if (this.isDisposed) return; this.deck.setProps({ layers }); } // 获取当前图层 getDeckLayers(): any[] { + if (this.isDisposed) return []; return this.deck.props.layers || []; } // 添加图层 addDeckLayer(layer: any): void { + if (this.isDisposed) return; const currentLayers = this.getDeckLayers(); // 如果已有同 id 图层,则替换保持顺序;否则追加 const idx = currentLayers.findIndex((l: any) => l && l.id === layer.id); @@ -80,6 +93,7 @@ export class DeckLayer extends Layer { // 移除图层 removeDeckLayer(layerId: string): void { + if (this.isDisposed) return; const currentLayers = this.getDeckLayers(); const filteredLayers = currentLayers.filter( (layer: any) => layer && layer.id !== layerId @@ -97,6 +111,7 @@ export class DeckLayer extends Layer { // - 如果传入的是 Layer 实例,则直接替换同 id 的图层为该实例 // - 如果传入的是 props(普通对象),则基于原图层调用 clone(props) updateDeckLayer(layerId: string, layerOrProps: any): void { + if (this.isDisposed) return; const layers = this.getDeckLayers(); const updatedLayers = layers.map((layer: any) => { if (!layer || layer.id !== layerId) return layer; @@ -111,6 +126,13 @@ export class DeckLayer extends Layer { // 替换为新的 layer 实例 return layerOrProps; } + + if (layerOrProps && typeof layer.clone === "function") { + // 传入 props 时,基于原图层 clone,避免丢失类型 + return layer.clone(layerOrProps); + } + + return layer; }); this.deck.setProps({ layers: updatedLayers }); @@ -137,7 +159,7 @@ export class DeckLayer extends Layer { if (!found) return; try { // 使用 clone 来确保保持同类型实例 - this.updateDeckLayer(layerId, { ...found.props, visible }); + this.updateDeckLayer(layerId, { visible }); } catch (err) { // 降级:直接替换属性 this.updateDeckLayer(layerId, { visible }); @@ -160,4 +182,26 @@ export class DeckLayer extends Layer { setLayerProperties(props: Record): void { this.setProperties(props); } + + isDisposedLayer(): boolean { + return this.isDisposed; + } + + disposeDeck(): void { + if (this.isDisposed) return; + this.isDisposed = true; + this.onVisibilityChange = undefined; + this.userVisibility.clear(); + try { + this.deck.setProps({ layers: [] }); + } catch (error) { + console.warn("Clear deck layers failed", error); + } + try { + this.deck.finalize(); + } catch (error) { + console.warn("Finalize deck failed", error); + } + this.canvasEl = null; + } }