三维修改

This commit is contained in:
扈兆增 2026-07-10 17:56:16 +08:00
parent 90bd2bac1f
commit 8d17118b40
13 changed files with 1566 additions and 134 deletions

View File

@ -32,8 +32,10 @@ import { useMapOrchestrator } from '@/modules/map/application/map-orchestrator';
import { MapClass } from './map.class';
import { getQgcRvcd } from '@/api/map';
import { servers } from './mapurlManage';
import { useUiStore } from '@/store/modules/ui';
const mapOrchestrator = useMapOrchestrator();
const uiStore = useUiStore();
const route = useRoute();
const mapClass = MapClass.getInstance();
@ -82,7 +84,11 @@ const init = async () => {
const handleMapController = async (e: any, mapType: any) => {
switch (e) {
case 'dim':
await mapClass.switchView(mapType);
await mapOrchestrator.switchMapType(mapType, {
popupContainer: popupRef.value,
getIsHydroMenu: () => isShuiDianKaiFaMenu.value
});
uiStore.markMapSwitchCompleted();
break;
case 4: //
tlyLayerVisible.value = !tlyLayerVisible.value;

File diff suppressed because it is too large Load Diff

View File

@ -272,4 +272,8 @@ export class MapClass implements MapClassInterface {
flyTopanto(position: number[], zoom: number): void {
this.service.flyTopanto(position, zoom);
}
getCurrentZoom(): number | undefined {
return this.service.getCurrentZoom();
}
}

View File

@ -124,6 +124,12 @@ export interface MapInterface {
fitBounds(bbox, bearing): void;
flyTopanto(positon, zoom): void;
/**
*
* 2D zoom3D
*/
getCurrentZoom(): number | undefined;
// /**
// * 加载倾斜摄影数据
// * @param HH3DUrlArray

View File

@ -49,7 +49,7 @@ const MIN_ZOOM = 4.23;
const MAX_ZOOM = 22;
const INITIAL_ZOOM = 4.5;
const BATCH_POPUP_MODE_ZOOM = 15;
const FULL_DISPLAY_NO_COLLISION_ZOOM = 15;
const FULL_DISPLAY_NO_COLLISION_ZOOM = Number.POSITIVE_INFINITY;
// 定义边界 [minX, minY, maxX, maxY] (Web Mercator 坐标)
const BOUNDS_SW = [26.5, -9.99999999999929];
@ -80,6 +80,8 @@ export class MapOl implements MapInterface {
private regionMaskManager: RegionMaskManager;
private isBatchPopupMode = false;
private batchPopupRefreshFrameId: number | null = null;
private batchPopupPendingRebuild = false;
private batchPopupPostRenderListenerKey: any = null;
private pointLayerStyleRefreshFrameId: number | null = null;
private labelVisibilityRefreshFrameId: number | null = null;
private labelVisibilityDebounceTimerId: number | null = null;
@ -200,13 +202,14 @@ export class MapOl implements MapInterface {
this.map.on('pointerdrag', () => {
if (this.isBatchPopupMode) {
this.requestUpdateBatchPopups();
this.ensureBatchPopupPostRenderSync();
}
});
this.map.on('moveend', () => {
this.clearBatchPopupPostRenderSync();
this.requestRefreshPointLabelVisibility(false, 60);
if (this.isBatchPopupMode) {
this.requestUpdateBatchPopups();
this.requestUpdateBatchPopups(false, true);
}
});
@ -291,8 +294,9 @@ export class MapOl implements MapInterface {
*/
private enableBatchPopupMode() {
this.isBatchPopupMode = true;
this.clearBatchPopupPostRenderSync();
this.popupManager.clearBatchPopups();
this.requestUpdateBatchPopups(true);
this.requestUpdateBatchPopups(true, true);
}
/**
@ -300,6 +304,8 @@ export class MapOl implements MapInterface {
*/
private disableBatchPopupMode() {
this.isBatchPopupMode = false;
this.batchPopupPendingRebuild = false;
this.clearBatchPopupPostRenderSync();
if (this.batchPopupRefreshFrameId !== null) {
cancelAnimationFrame(this.batchPopupRefreshFrameId);
this.batchPopupRefreshFrameId = null;
@ -314,10 +320,39 @@ export class MapOl implements MapInterface {
private updateBatchPopups() {
const features = this.pointLayerManager.getFeaturesInViewport();
// 传入样式检查器,过滤掉 declutter/距离过滤等隐藏的锚点
this.popupManager.showPopupsForFeatures(features, feature => {
this.popupManager.showPopupsForFeatures(
features,
feature => {
const style = this.createPointStyle(feature);
return style !== null;
},
{
rebuild: this.batchPopupPendingRebuild
}
);
}
private ensureBatchPopupPostRenderSync() {
if (!this.map || this.batchPopupPostRenderListenerKey) {
return;
}
this.batchPopupPostRenderListenerKey = this.map.on('postrender', () => {
if (!this.isBatchPopupMode) {
return;
}
this.popupManager.updateBatchPopupPositions(feature => {
const style = this.createPointStyle(feature);
return style !== null;
});
});
}
private clearBatchPopupPostRenderSync() {
if (this.batchPopupPostRenderListenerKey) {
unByKey(this.batchPopupPostRenderListenerKey);
this.batchPopupPostRenderListenerKey = null;
}
}
/**
* ( Leaflet DivIcon )
@ -350,6 +385,7 @@ export class MapOl implements MapInterface {
const dynamicScale = this.getDynamicPointScale(currentZoom);
const fontSize = this.getPointFontSize(dynamicScale);
const engPriority = this.getFeatureEngRenderPriority(feature);
const formattedLabelText = this.formatPointLabelText(labelText);
const labelLineCount = formattedLabelText
? formattedLabelText.split('\n').length
@ -363,7 +399,10 @@ export class MapOl implements MapInterface {
feature.get('_labelCollisionVisible') === true;
const finalIconVisible = iconTargetVisible && iconCollisionVisible;
const labelTargetVisible =
finalIconVisible && !!formattedLabelText && labelCollisionVisible;
!this.isBatchPopupMode &&
finalIconVisible &&
!!formattedLabelText &&
labelCollisionVisible;
if (!finalIconVisible) {
return null;
@ -373,6 +412,7 @@ export class MapOl implements MapInterface {
iconUrl,
dynamicScale.toFixed(2),
fontSize,
engPriority,
labelOffsetY,
labelTargetVisible ? formattedLabelText : '',
labelTargetVisible ? 1 : 0
@ -384,6 +424,7 @@ export class MapOl implements MapInterface {
const styles: Style[] = [
new Style({
zIndex: engPriority,
image: new Icon({
src: iconUrl,
scale: dynamicScale,
@ -397,7 +438,7 @@ export class MapOl implements MapInterface {
if (formattedLabelText && labelTargetVisible) {
styles.push(
new Style({
zIndex: 101,
zIndex: engPriority + 1,
text: new Text({
text: formattedLabelText,
offsetY: labelOffsetY,
@ -598,6 +639,7 @@ export class MapOl implements MapInterface {
top: number;
bottom: number;
priority: number;
engPriority: number;
densityPriority: number;
id: string;
pixelX: number;
@ -616,6 +658,7 @@ export class MapOl implements MapInterface {
top: number;
bottom: number;
priority: number;
engPriority: number;
densityPriority: number;
id: string;
pixelX: number;
@ -808,7 +851,7 @@ export class MapOl implements MapInterface {
const dynamicScale = this.getDynamicPointScale(currentZoom);
// 备注:图标碰撞盒适当小于视觉图标尺寸,允许相邻站点轻微贴近显示,
// 避免像“小浪底/三门峡”这类近点被过早裁掉成只剩一个点。
const iconCollisionSize = Math.max(12, 16 * dynamicScale);
const iconCollisionSize = Math.max(14, 24 * dynamicScale);
const iconPadding = 0;
const iconLeft = entry.pixelX - iconCollisionSize / 2 - iconPadding;
const iconRight = entry.pixelX + iconCollisionSize / 2 + iconPadding;
@ -851,6 +894,7 @@ export class MapOl implements MapInterface {
top: centerY - estimatedHeight / 2,
bottom: centerY + estimatedHeight / 2,
priority: Number(entry.feature.get('_nearbyPriority') || 9999),
engPriority: this.getFeatureEngRenderPriority(entry.feature),
densityPriority: entry.densityPriority,
id: candidateId,
pixelX: entry.pixelX,
@ -863,6 +907,9 @@ export class MapOl implements MapInterface {
});
candidates.sort((left, right) => {
if (left.engPriority !== right.engPriority) {
return right.engPriority - left.engPriority;
}
if (left.priority !== right.priority) {
return left.priority - right.priority;
}
@ -951,6 +998,9 @@ export class MapOl implements MapInterface {
});
const labelCandidates = [...candidates].sort((left, right) => {
if (left.engPriority !== right.engPriority) {
return right.engPriority - left.engPriority;
}
if (left.priority !== right.priority) {
return left.priority - right.priority;
}
@ -1149,10 +1199,11 @@ export class MapOl implements MapInterface {
}
}
private requestUpdateBatchPopups(immediate = false) {
private requestUpdateBatchPopups(immediate = false, rebuild = true) {
if (!this.isBatchPopupMode) {
return;
}
this.batchPopupPendingRebuild = this.batchPopupPendingRebuild || rebuild;
if (immediate) {
if (this.batchPopupRefreshFrameId !== null) {
@ -1160,6 +1211,7 @@ export class MapOl implements MapInterface {
this.batchPopupRefreshFrameId = null;
}
this.updateBatchPopups();
this.batchPopupPendingRebuild = false;
return;
}
@ -1173,6 +1225,7 @@ export class MapOl implements MapInterface {
return;
}
this.updateBatchPopups();
this.batchPopupPendingRebuild = false;
});
}
@ -1326,6 +1379,27 @@ export class MapOl implements MapInterface {
return densityDisplayRules.at(-1)?.minZoom ?? 0;
}
private isFeatureEng(feature: Feature): boolean {
const layerKey = String(feature.get('_layerKey') || '').toLowerCase();
const sttpMap = String(
feature.get('_sttpMap') || feature.get('sttpMap') || ''
).toUpperCase();
const sttpCode = String(
feature.get('sttpCode') || feature.get('sttp') || ''
).toUpperCase();
return (
layerKey.includes('eng_point') ||
sttpMap === 'ENG' ||
sttpMap === 'ENG2' ||
sttpCode === 'ENG'
);
}
private getFeatureEngRenderPriority(feature: Feature): number {
return this.isFeatureEng(feature) ? 200 : 100;
}
/**
*
* distance
@ -1407,10 +1481,9 @@ export class MapOl implements MapInterface {
const nearbyGroupId = feature.get('_nearbyGroupId');
const groupSize = Number(feature.get('_nearbyGroupSize'));
const priority = Number(feature.get('_nearbyPriority'));
const replaceAtZoom = Number(feature.get('_nearbyReplaceAtZoom'));
const expandAtZoom = Number(feature.get('_nearbyExpandAtZoom'));
const showZoomMin = feature.get('_nearbyShowZoomMin');
const showZoomMax = feature.get('_nearbyShowZoomMax');
const expandAtZoom = feature.get('_nearbyExpandAtZoom');
if (!nearbyGroupId || !Number.isFinite(groupSize) || groupSize < 2) {
return true;
@ -1436,14 +1509,14 @@ export class MapOl implements MapInterface {
return false;
}
if (Number.isFinite(expandAtZoom) && currentZoom >= expandAtZoom) {
if (
expandAtZoom !== null &&
expandAtZoom !== undefined &&
currentZoom >= Number(expandAtZoom)
) {
return true;
}
if (Number.isFinite(replaceAtZoom) && currentZoom >= replaceAtZoom) {
return priority <= Math.min(2, groupSize);
}
return priority === 1;
}
/**
@ -2436,6 +2509,10 @@ export class MapOl implements MapInterface {
);
}
getCurrentZoom(): number | undefined {
return this.view?.getZoom();
}
/**
* Canvas
* @param radius
@ -2496,6 +2573,7 @@ export class MapOl implements MapInterface {
window.clearTimeout(this.labelVisibilityDebounceTimerId);
this.labelVisibilityDebounceTimerId = null;
}
this.clearBatchPopupPostRenderSync();
this.removeQueryLayer();
this.regionMaskManager.destroy();

View File

@ -318,7 +318,7 @@ export class PointLayerManager {
layerKey: string,
matchValue: string,
checked: boolean,
fieldKey: string = 'anchoPointState'
fieldKey = 'anchoPointState'
): void {
const vectorLayer = this.layerRegistry.get(layerKey);
if (!vectorLayer) return;

View File

@ -4,7 +4,10 @@ import OlMap from 'ol/Map';
import Overlay from 'ol/Overlay';
import VectorLayer from 'ol/layer/Vector';
import VectorSource from 'ol/source/Vector';
import { generatePopupHtml } from '@/utils/popupHtmlGenerator';
import {
generatePopupHtml,
shouldPreferEng2Popup
} from '@/utils/popupHtmlGenerator';
type PopupManagerOptions = {
map: OlMap | null;
@ -27,6 +30,11 @@ export class PopupManager {
private map: OlMap | null;
private popupOverlay: Overlay | null = null;
private popupElement: HTMLElement | null = null;
private batchPopupContainer: HTMLDivElement | null = null;
private batchPopupItems: Array<{
feature: Feature;
element: HTMLDivElement;
}> = [];
private lastHoveredId: string | number | null = null;
private animationFrameId: number | null = null;
private getPointLayers: () => VectorLayer<VectorSource>[];
@ -49,6 +57,11 @@ export class PopupManager {
// 备注:初始化 Popup Overlay 并挂载到地图实例。
initPopupOverlay(container: HTMLElement) {
this.popupElement = container;
this.popupElement.style.display = 'none';
this.popupElement.style.removeProperty('position');
this.popupElement.style.removeProperty('transform');
this.popupElement.style.removeProperty('left');
this.popupElement.style.removeProperty('top');
this.popupElement.style.setProperty('pointer-events', 'none', 'important');
this.popupMouseEnterHandler = () => {
this.forceHidePopup();
@ -190,7 +203,7 @@ export class PopupManager {
if (feature && coordinate) {
const props = feature.getProperties();
const popupHtml = props.popupHtml || generatePopupHtml(props);
const popupHtml = this.getPopupHtml(props);
if (popupHtml) {
this.popupElement.innerHTML = popupHtml;
@ -204,22 +217,37 @@ export class PopupManager {
this.popupElement.style.display = 'none';
}
private getPopupHtml(props: Record<string, any>) {
return props.popupHtml || generatePopupHtml(props);
}
// 备注:批量显示多个要素的 Popup使用绝对定位的 div 而非 Overlay。
showPopupsForFeatures(
features: Feature[],
styleChecker?: (feature: Feature) => boolean
styleChecker?: (feature: Feature) => boolean,
options?: {
rebuild?: boolean;
}
) {
if (!this.map || !this.popupElement) return;
// 清除之前批量显示的 popups
this.clearBatchPopups();
const shouldRebuild = options?.rebuild !== false;
// 过滤:只显示图例可见的要素
const visibleFeatures = features.filter(
f => f.get('_legendVisible') !== false
);
if (visibleFeatures.length === 0) return;
if (visibleFeatures.length === 0) {
this.clearBatchPopups();
return;
}
if (!shouldRebuild && this.batchPopupItems.length > 0) {
this.updateBatchPopupPositions(styleChecker);
return;
}
this.clearBatchPopups();
const mapElement = this.map.getTargetElement();
const batchContainer = document.createElement('div');
@ -234,6 +262,7 @@ export class PopupManager {
z-index: 1000;
`;
batchContainer.id = 'batch-popup-container';
this.batchPopupContainer = batchContainer;
// 先添加容器到 DOM以便后续元素能正确测量尺寸
mapElement.appendChild(batchContainer);
@ -245,12 +274,6 @@ export class PopupManager {
top: number;
bottom: number;
}> = [];
let styleHiddenCount = 0;
let blockedCount = 0;
let renderedCount = 0;
const renderedIds: Array<string | number | null> = [];
const blockedIds: Array<string | number | null> = [];
visibleFeatures.forEach(feature => {
const geom = feature.getGeometry();
if (!geom || geom.getType() !== 'Point') return;
@ -261,12 +284,15 @@ export class PopupManager {
// 检查样式是否隐藏declutter、距离过滤等
if (styleChecker && !styleChecker(feature)) {
styleHiddenCount += 1;
return;
}
const props = feature.getProperties();
const popupHtml = props.popupHtml || generatePopupHtml(props);
const popupHtml = shouldPreferEng2Popup(props)
? generatePopupHtml(props, { forceEng2: true }) ||
props.popupHtml ||
generatePopupHtml(props)
: this.getPopupHtml(props);
if (!popupHtml) return;
// 创建与原始 popupElement 完全相同的样式
@ -314,8 +340,10 @@ export class PopupManager {
popupEl.style.visibility = 'visible';
popupEl.style.left = `${popupX}px`;
popupEl.style.top = `${popupY}px`;
renderedCount += 1;
renderedIds.push((feature.getId?.() as string | number | null) ?? null);
this.batchPopupItems.push({
feature,
element: popupEl
});
placedPopups.push({
left: popupRect.left,
@ -325,15 +353,76 @@ export class PopupManager {
});
} else {
// 有碰撞,移除该 popup
blockedCount += 1;
blockedIds.push((feature.getId?.() as string | number | null) ?? null);
batchContainer.removeChild(popupEl);
}
});
}
updateBatchPopupPositions(styleChecker?: (feature: Feature) => boolean) {
if (
!this.map ||
!this.batchPopupContainer ||
this.batchPopupItems.length === 0
) {
return;
}
const mapSize = this.map.getSize();
const viewportWidth = mapSize?.[0] ?? 0;
const viewportHeight = mapSize?.[1] ?? 0;
const viewportPadding = 48;
this.batchPopupItems.forEach(({ feature, element }) => {
if (feature.get('_legendVisible') === false) {
element.style.display = 'none';
return;
}
if (styleChecker && !styleChecker(feature)) {
element.style.display = 'none';
return;
}
const geom = feature.getGeometry();
if (!geom || geom.getType() !== 'Point') {
element.style.display = 'none';
return;
}
const coords = (geom as Point).getCoordinates();
const pixel = this.map?.getPixelFromCoordinate(coords);
if (!pixel) {
element.style.display = 'none';
return;
}
const popupX = pixel[0];
const popupY = pixel[1] - 10;
if (
popupX < -viewportPadding ||
popupY < -viewportPadding ||
popupX > viewportWidth + viewportPadding ||
popupY > viewportHeight + viewportPadding
) {
element.style.display = 'none';
return;
}
element.style.left = `${popupX}px`;
element.style.top = `${popupY}px`;
element.style.display = 'block';
});
}
// 备注:清除批量显示的 popups。
clearBatchPopups() {
this.batchPopupItems = [];
if (this.batchPopupContainer) {
this.batchPopupContainer.remove();
this.batchPopupContainer = null;
return;
}
const existing = document.getElementById('batch-popup-container');
if (existing) {
existing.remove();

View File

@ -1,5 +1,8 @@
<template>
<div class="map-filter-container">
<div
class="map-filter-container"
:style="{ left: uiStore.mapType == '2D' ? '220px' : '20px' }"
>
<div class="toolbar">
<a-form :model="formModel">
<a-row :gutter="10">
@ -110,7 +113,7 @@
</div>
</template>
<script setup lang="ts">
import { ref, computed, watch } from 'vue';
import { ref, computed, watch, nextTick } from 'vue';
import dayjs from 'dayjs';
import { useRoute } from 'vue-router';
import { MapClass } from '@/components/gis/map.class';
@ -118,6 +121,7 @@ import { useMapOrchestrator } from '@/modules/map/application/map-orchestrator';
import { useMapDataStore } from '@/modules/map/stores/map-data.store';
import { useMapViewStore } from '@/modules/map/stores/map-view.store';
import { useJidiSelectEventStore } from '@/store/modules/jidiSelectEvent';
import { useUiStore } from '@/store/modules/ui';
import { useMapStore } from '@/store/modules/map';
import { DateSetting } from '@/utils/enumeration';
import { getFishList, getFishPointList } from '@/api/map';
@ -126,6 +130,7 @@ const route = useRoute();
const mapOrchestrator = useMapOrchestrator();
const mapDataStore = useMapDataStore();
const mapViewStore = useMapViewStore();
const uiStore = useUiStore();
const jidiSelectEventStore = useJidiSelectEventStore();
const mapStore = useMapStore();
const mapClass = MapClass.getInstance();
@ -156,6 +161,7 @@ const fishPointOptions = ref<any[]>([]);
const fishSpeciesOptions = ref<{ label: string; value: string }[]>([]);
const fishSpeciesLoading = ref(false);
const fishPointLoading = ref(false);
const refocusTimer = ref<ReturnType<typeof setTimeout> | null>(null);
const searchTimeRange = ref([
dayjs(mapViewStore.searchTimeRange[0]).format('YYYY-MM-DD HH:mm:ss'),
@ -623,15 +629,48 @@ const handleCapacityChange = (value: string) => {
mapOrchestrator.changeEngPointCapacity(value);
};
//
const handleAnchorPointChange = (value: string) => {
if (!value) return;
const clearRefocusTimer = () => {
if (!refocusTimer.value) {
return;
}
clearTimeout(refocusTimer.value);
refocusTimer.value = null;
};
const focusSelectedAnchorPoint = () => {
const selectedValue = anchorPointSelectValue.value;
if (!selectedValue) {
return;
}
mapOrchestrator.focusPoint(
value,
selectedValue,
14,
isFishSurveyMode.value ? anchorPointOptions.value : undefined
);
};
//
const handleAnchorPointChange = (value: string) => {
if (!value) return;
focusSelectedAnchorPoint();
};
watch(
() => uiStore.mapSwitchCompletedTick,
async (newVal, oldVal) => {
if (newVal === oldVal || !anchorPointSelectValue.value) {
return;
}
await nextTick();
clearRefocusTimer();
refocusTimer.value = setTimeout(() => {
refocusTimer.value = null;
focusSelectedAnchorPoint();
}, 120);
}
);
</script>
<style lang="scss" scoped>

View File

@ -1,9 +1,11 @@
<script setup lang="ts">
import { onMounted, ref } from 'vue';
import { useUiStore } from '@/store/modules/ui';
import { useJidiSelectEventStore } from '@/store/modules/jidiSelectEvent';
const loading = ref(false);
const isOpen = ref(true);
const JidiSelectEventStore = useJidiSelectEventStore();
const uiStore = useUiStore();
const jidiDataNum = ref(9);
const itemClick = (index: any) => {
@ -13,7 +15,7 @@ onMounted(() => {});
</script>
<template>
<div class="jidiSelectorMod">
<div class="jidiSelectorMod" v-if="uiStore.mapType == '2D'">
<a-spin :spinning="JidiSelectEventStore.loading">
<div
class="qgc-dropdown-select"

View File

@ -43,6 +43,7 @@ const HYDRO_DYNAMIC_LAYER_KEYS = [
];
const ENG_POINT_LAYER_KEY = 'eng_point';
const ENG_POINT_MEDIUM_VISIBLE_ZOOM = 7.5;
const CESIUM_ZOOM_SYNC_DEBOUNCE_MS = 80;
const LARGE_ENG_LEGEND_PREFIX = 'large_eng_';
const MID_ENG_LEGEND_PREFIX = 'mid_eng_';
@ -55,12 +56,20 @@ export const useMapOrchestrator = () => {
const mapViewStore = useMapViewStore();
const jidiSelectEventStore = useJidiSelectEventStore();
let activePageLoadRequestId = 0;
let zoomListenerKey: any = null;
let removeZoomListener: (() => void) | null = null;
let stopBaseSelectionWatch: WatchStopHandle | null = null;
const initializedBaseLayerKeys = new Set<string>();
const baseSelectionDebounceTimer = ref<ReturnType<typeof setTimeout> | null>(
null
);
const zoomSyncDebounceTimer = ref<ReturnType<typeof setTimeout> | null>(null);
let latestHydroMenuGetter: (() => boolean) | null = null;
let currentMapType: '2D' | '3D' = '2D';
let hydroMenuDefaultCheckedKeys = new Set<string>();
let hydroDynamicLayerSyncTask: {
visible: boolean;
promise: Promise<string[] | void>;
} | null = null;
// 备注:统一获取当前地图视图对象,避免在多个组件里重复兼容 2D/3D view 访问方式。
const getCurrentView = () => {
@ -69,7 +78,14 @@ export const useMapOrchestrator = () => {
// 备注:统一获取当前地图缩放级别,供页面切换和缩放监听复用。
const getCurrentZoom = (): number | undefined => {
return getCurrentView()?.getZoom?.();
return mapClass.getCurrentZoom();
};
const clearZoomSyncDebounceTimer = () => {
if (zoomSyncDebounceTimer.value) {
clearTimeout(zoomSyncDebounceTimer.value);
zoomSyncDebounceTimer.value = null;
}
};
// 备注:统一把当前页面中的 GIS 基础图层初始化到地图实例,避免在图层树组件里做配置解析和加层编排。
@ -95,7 +111,20 @@ export const useMapOrchestrator = () => {
}
if (item.key === 'customBaseLayer') {
sessionStorage.setItem('customBaseLayer', JSON.stringify(item.config));
const serializableBaseLayerConfig = {
key: item.config?.key,
id: item.config?.id,
name: item.config?.name,
type: item.config?.type,
url: item.config?.url,
url_3d: item.config?.url_3d,
layers: item.config?.layers,
rasteropacity: item.config?.rasteropacity
};
sessionStorage.setItem(
'customBaseLayer',
JSON.stringify(serializableBaseLayerConfig)
);
}
if (!initializedBaseLayerKeys.has(item.key)) {
@ -154,6 +183,9 @@ export const useMapOrchestrator = () => {
// 先设置图层数据(更新 checkedLayerKeys再设置图例数据
if (layerConfig.length > 0) {
hydroMenuDefaultCheckedKeys = new Set(
mapConfigStore.extractCheckedLayerKeys(layerConfig)
);
mapStore.setLayerData(layerConfig);
ensureBaseLayersInitialized(layerConfig);
@ -229,10 +261,58 @@ export const useMapOrchestrator = () => {
await mapClass.init(container);
if (popupContainer) {
const popupHost = container.parentElement;
if (popupHost && popupContainer.parentElement !== popupHost) {
popupHost.appendChild(popupContainer);
}
popupContainer.style.display = 'none';
mapClass.initPopupOverlay(popupContainer);
}
};
const replayCurrentMapState = async (
options: Pick<InitializeOptions, 'popupContainer'> & {
getIsHydroMenu: () => boolean;
}
) => {
initializedBaseLayerKeys.clear();
const currentZoom = getCurrentZoom();
if (currentZoom !== undefined) {
mapViewStore.setCurrentZoomLevel(currentZoom);
}
if (options.popupContainer) {
const container = document.getElementById('mapContainer');
const popupHost = container?.parentElement || null;
if (popupHost && options.popupContainer.parentElement !== popupHost) {
popupHost.appendChild(options.popupContainer);
}
options.popupContainer.style.display = 'none';
mapClass.initPopupOverlay(options.popupContainer);
}
ensureBaseLayersInitialized(mapStore.layerData || []);
const checkedKeys = mapViewStore.getCheckedLayerKeys();
await mapStore.updateLayerData(checkedKeys, true);
mapStore.setSelectedLegendData();
await syncZoomSensitiveState({
isHydroMenu: options.getIsHydroMenu(),
refreshEngPoint: true
});
if (currentMapType === '2D') {
const selectedBaseId = mapViewStore.selectedBaseId;
mapClass.jdPanelControlShowAndHidden(
selectedBaseId || 'all',
!!selectedBaseId
);
}
bindZoomListener(options.getIsHydroMenu);
};
// 备注:统一完成首屏页面配置加载,供挂载阶段和后续页面重载复用。
const initialize = async ({
container,
@ -249,20 +329,18 @@ export const useMapOrchestrator = () => {
// 备注:统一绑定地图缩放监听,让页面组件只负责触发初始化,不再直接处理缩放联动细节。
const bindZoomListener = (getIsHydroMenu: () => boolean) => {
if (zoomListenerKey) {
unByKey(zoomListenerKey);
zoomListenerKey = null;
if (removeZoomListener) {
removeZoomListener();
removeZoomListener = null;
}
const view = getCurrentView();
if (!view?.on) return;
clearZoomSyncDebounceTimer();
const currentZoom = getCurrentZoom();
if (currentZoom !== undefined) {
mapViewStore.setCurrentZoomLevel(currentZoom);
}
zoomListenerKey = view.on('change:resolution', async () => {
const emitZoomChange = async () => {
const zoom = getCurrentZoom();
if (zoom === undefined) return;
@ -271,7 +349,36 @@ export const useMapOrchestrator = () => {
zoom,
getIsHydroMenu()
);
};
if (currentMapType === '3D') {
const removeCesiumListener =
mapClass.view?.camera?.changed?.addEventListener(() => {
clearZoomSyncDebounceTimer();
zoomSyncDebounceTimer.value = setTimeout(() => {
zoomSyncDebounceTimer.value = null;
void emitZoomChange();
}, CESIUM_ZOOM_SYNC_DEBOUNCE_MS);
});
if (typeof removeCesiumListener === 'function') {
removeZoomListener = () => {
clearZoomSyncDebounceTimer();
removeCesiumListener();
};
}
return;
}
const view = getCurrentView();
if (!view?.on) return;
const zoomListenerKey = view.on('change:resolution', () => {
void emitZoomChange();
});
removeZoomListener = () => {
unByKey(zoomListenerKey);
};
};
// 备注:统一绑定基地切换事件,让页面组件不再直接 watch 外部基地选择源。
@ -308,10 +415,34 @@ export const useMapOrchestrator = () => {
getIsHydroMenu: () => boolean;
}
) => {
currentMapType = '2D';
latestHydroMenuGetter = options.getIsHydroMenu;
await initializeMapShell(options);
bindBaseSelection();
bindZoomListener(options.getIsHydroMenu);
await loadPage({ pageKey: options.pageKey, isInitialLoad: true });
await syncZoomSensitiveState({
isHydroMenu: options.getIsHydroMenu(),
refreshEngPoint: true
});
};
const switchMapType = async (
type: '2D' | '3D',
options: Pick<InitializeOptions, 'popupContainer'> & {
getIsHydroMenu?: () => boolean;
} = {}
) => {
const hydroMenuGetter =
options.getIsHydroMenu || latestHydroMenuGetter || (() => false);
latestHydroMenuGetter = hydroMenuGetter;
currentMapType = type;
await mapClass.switchView(type);
await replayCurrentMapState({
popupContainer: options.popupContainer,
getIsHydroMenu: hydroMenuGetter
});
};
// 备注:统一处理菜单切换后的页面配置重载,供页面 watch 直接复用。
@ -361,17 +492,24 @@ export const useMapOrchestrator = () => {
(layer: any) => layer?.url && !mapDataStore.hasPointLayerData(layer.key)
);
for (const layer of layersToLoad) {
await mapStore.loadLayerData(layer);
if (layersToLoad.length === 0) {
return;
}
await Promise.all(layersToLoad.map(layer => mapStore.loadLayerData(layer)));
};
// 备注:统一控制水电开发菜单下的动态图层增删,避免页面组件直接操作加载和勾选细节。
const syncHydroDynamicLayers = async (visible: boolean) => {
if (
hydroDynamicLayerSyncTask &&
hydroDynamicLayerSyncTask.visible === visible
) {
return hydroDynamicLayerSyncTask.promise;
}
const task = (async () => {
const currentCheckedKeys = mapViewStore.getCheckedLayerKeys();
const pageDefaultCheckedKeys = new Set(
mapConfigStore.extractCheckedLayerKeys(mapStore.layerData || [])
);
if (visible) {
const layersToAdd = HYDRO_DYNAMIC_LAYER_KEYS.filter(
@ -392,7 +530,7 @@ export const useMapOrchestrator = () => {
if (!HYDRO_DYNAMIC_LAYER_KEYS.includes(key)) {
return true;
}
return pageDefaultCheckedKeys.has(key);
return hydroMenuDefaultCheckedKeys.has(key);
});
if (nextCheckedKeys.length === currentCheckedKeys.length) {
@ -402,6 +540,43 @@ export const useMapOrchestrator = () => {
return applyLayerSelection({
checkedKeys: nextCheckedKeys
});
})().finally(() => {
if (hydroDynamicLayerSyncTask?.promise === task) {
hydroDynamicLayerSyncTask = null;
}
});
hydroDynamicLayerSyncTask = {
visible,
promise: task
};
return task;
};
// 备注:统一按当前缩放对齐首页菜单的中型站过滤和 12 级动态图层状态。
const syncZoomSensitiveState = async ({
isHydroMenu,
refreshEngPoint = false
}: {
isHydroMenu: boolean;
refreshEngPoint?: boolean;
}) => {
const currentZoom = getCurrentZoom();
if (currentZoom !== undefined) {
mapViewStore.setCurrentZoomLevel(currentZoom);
}
if (
isHydroMenu &&
refreshEngPoint &&
mapViewStore.getCheckedLayerKeys().includes(ENG_POINT_LAYER_KEY)
) {
mapStore.refreshPointLayerDisplayData(ENG_POINT_LAYER_KEY);
}
await syncHydroDynamicLayers(
!!isHydroMenu && currentZoom !== undefined && currentZoom >= 12
);
};
// 备注:统一处理地图缩放联动,收口水电开发菜单下的动态图层阈值切换逻辑。
@ -419,6 +594,7 @@ export const useMapOrchestrator = () => {
currentZoom <= ENG_POINT_MEDIUM_VISIBLE_ZOOM);
if (
isHydroMenu &&
crossedEngPointMediumThreshold &&
mapViewStore.getCheckedLayerKeys().includes(ENG_POINT_LAYER_KEY)
) {
@ -454,13 +630,10 @@ export const useMapOrchestrator = () => {
}
return reloadPage(pageKey).then(async () => {
const currentZoom = getCurrentZoom();
if (isHydroMenu && currentZoom !== undefined && currentZoom >= 12) {
await syncHydroDynamicLayers(true);
return;
}
await syncHydroDynamicLayers(false);
await syncZoomSensitiveState({
isHydroMenu,
refreshEngPoint: true
});
});
};
@ -507,10 +680,12 @@ export const useMapOrchestrator = () => {
const changeBaseId = (baseId: string) => {
const nextBaseId = !baseId || baseId === 'all' ? '' : baseId;
mapViewStore.setSelectedBaseId(nextBaseId);
if (currentMapType === '2D') {
mapClass.jdPanelControlShowAndHidden(
baseId || 'all',
baseId !== 'all' && !!baseId
);
}
};
// 备注:统一处理时间范围变更,并触发相关图层数据重载。
@ -570,30 +745,47 @@ export const useMapOrchestrator = () => {
mapDataStore.pointData.find(pointMatcher) ||
fallbackPoints.find(pointMatcher);
if (!targetPoint?.lgtd || !targetPoint?.lttd) return;
const legendState = String(targetPoint?.anchoPointState || '');
if (legendState && mapViewStore.getLegendChecked(legendState) !== 1) {
mapStore.updateLegendChecked(legendState, 1);
}
const layerKey = String(targetPoint?.layerKey || '');
if (layerKey && mapClass.hasLayer(layerKey)) {
mapClass.mdLayerTreeShowOrHidden(layerKey, true);
}
mapClass.flyTopanto([targetPoint.lgtd, targetPoint.lttd], zoom);
};
// 备注:统一释放地图页面生命周期里注册的监听,避免页面卸载后残留联动。
const unmountView = () => {
if (zoomListenerKey) {
unByKey(zoomListenerKey);
zoomListenerKey = null;
if (removeZoomListener) {
removeZoomListener();
removeZoomListener = null;
}
clearZoomSyncDebounceTimer();
if (stopBaseSelectionWatch) {
stopBaseSelectionWatch();
stopBaseSelectionWatch = null;
}
if (baseSelectionDebounceTimer.value) {
clearTimeout(baseSelectionDebounceTimer.value);
baseSelectionDebounceTimer.value = null;
}
latestHydroMenuGetter = null;
currentMapType = '2D';
initializedBaseLayerKeys.clear();
hydroMenuDefaultCheckedKeys.clear();
};
return {
initialize,
mountView,
switchMapType,
unmountView,
loadPage,
reloadPage,

View File

@ -5,6 +5,7 @@ export const useUiStore = defineStore('ui', () => {
// 右侧抽屉状态
const drawerOpen = ref(true);
const mapType = ref('2D');
const mapSwitchCompletedTick = ref(0);
// 切换抽屉状态
const toggleDrawer = () => {
@ -16,10 +17,16 @@ export const useUiStore = defineStore('ui', () => {
drawerOpen.value = open;
};
const markMapSwitchCompleted = () => {
mapSwitchCompletedTick.value += 1;
};
return {
drawerOpen,
toggleDrawer,
setDrawerOpen,
mapType
mapType,
mapSwitchCompletedTick,
markMapSwitchCompleted
};
});

View File

@ -2642,16 +2642,19 @@ const timegj = [
* @param props -
* @returns HTML
*/
export function generatePopupHtml(props: any): string {
export function generatePopupHtml(
props: any,
options?: {
forceEng2?: boolean;
}
): string {
const p = props;
const dzFdl = getUnitConfigByCode('Other', 'FDLYKW')?.unit;
const dzZjrl = getUnitConfigByCode('Other', 'ZJRL')?.unit;
const title = p?.ennm || p?.stnm;
const titleClass = getTitleClass(title);
let newsttpMap = p.sttpMap;
console.log('newsttpMap', newsttpMap);
if (p.qecInterval) {
if (options?.forceEng2) {
newsttpMap = 'eng2';
}
let html = '';
@ -2714,7 +2717,7 @@ export function generatePopupHtml(props: any): string {
} else {
lastTmEngEqDataVo = p.lastTmEngEqDataVo;
}
if (!lastTmEngEqDataVo?.qo && !lastTmEngEqDataVo?.qi) return '';
// if (!lastTmEngEqDataVo?.qo && !lastTmEngEqDataVo?.qi) return '';
const qecLimitVal = lastTmEngEqDataVo?.qecLimit;
let qecLimitDisplay = '-';
@ -3587,3 +3590,29 @@ export function generatePopupHtml(props: any): string {
return html ? `<div>${html}</div>` : '';
}
export function getNormalizedPopupSttpMap(props: any): string {
return String(props?._sttpMap || props?.sttpMap || props?.popupSttpMap || '')
.trim()
.toUpperCase();
}
export function isEngPopupFamily(props: any): boolean {
return getNormalizedPopupSttpMap(props).startsWith('ENG');
}
export function shouldPreferEng2Popup(
props: any,
options?: {
forceEng2?: boolean;
}
): boolean {
if (options?.forceEng2) {
return true;
}
return (
isEngPopupFamily(props) &&
(!!props?.lastTmEngEqDataVo || !!props?.qecInterval)
);
}

View File

@ -58,7 +58,6 @@ service.interceptors.response.use(
message.error(response.data.msg || '请求失败');
setTimeout(() => {
localStorage.clear();
router.push('/login');
window.location.href = '/';
}, 1000);
return;