diff --git a/frontend-sjtb/.env.development b/frontend-sjtb/.env.development index 083fa926..c3fcd89e 100644 --- a/frontend-sjtb/.env.development +++ b/frontend-sjtb/.env.development @@ -4,7 +4,7 @@ NODE_ENV='development' VITE_APP_TITLE = '水电水利建设项目全过程环境管理信息平台' -VITE_APP_PORT = 3000 +VITE_APP_PORT = 8000 VITE_APP_BASE_API = '/dev-api' # 本地环境 VITE_APP_BASE_URL = 'http://10.84.111.235:8093' diff --git a/frontend/.env.development b/frontend/.env.development index 27f26cbe..5cf083ba 100644 --- a/frontend/.env.development +++ b/frontend/.env.development @@ -7,14 +7,14 @@ VITE_APP_TITLE = '水电水利建设项目全过程环境管理信息平台' VITE_APP_PORT = 3000 VITE_APP_BASE_API = '/dev-api' # 本地环境 -# VITE_APP_BASE_URL = 'http://localhost:8093' +# VITE_APP_BASE_URL = 'http://10.84.121.127:8093' # 测试环境 # VITE_APP_BASE_URL = 'http://172.16.21.142:8093' # VITE_APP_BASE_URL = 'http://172.16.21.142:8096' # 汤伟 -# VITE_APP_BASE_URL = 'http://10.84.111.235:8093' +VITE_APP_BASE_URL = 'http://10.84.111.235:8093' # 李林 -VITE_APP_BASE_URL = 'http://10.84.111.25:8093' +# VITE_APP_BASE_URL = 'http://10.84.111.25:8093' # 测试环境线上10.84.121.122: VITE_APP_TEST_ONLINE_URL = 'https://211.99.26.225:12122' diff --git a/frontend/docs/地图模块-详细说明.md b/frontend/docs/地图模块-详细说明.md index b957f0bb..1dbede0c 100644 --- a/frontend/docs/地图模块-详细说明.md +++ b/frontend/docs/地图模块-详细说明.md @@ -299,7 +299,7 @@ 1. 先收集所有带 URL 的图层 2. 把默认勾选图层作为高优先级任务 -3. 并发加载,默认并发数为 4 +3. 收集完成后全量并发加载,不再按 4 个一组分批发起 4. 每个图层完成后立即写入缓存 5. 每个图层命中缓存或返回成功后,立即增量更新 `pointData` 6. 全部图层结束后,再统一: diff --git a/frontend/src/api/ecoFlow/index.ts b/frontend/src/api/ecoFlow/index.ts index b8673bc3..97e7771b 100644 --- a/frontend/src/api/ecoFlow/index.ts +++ b/frontend/src/api/ecoFlow/index.ts @@ -1,22 +1,12 @@ import request from '@/utils/request'; import type { EcoFlowStandard, EcoFlowQueryParams } from './types'; -/** - * 获取生态流量达标情况数据 - * @param params 查询参数 - */ -export function getEcoFlowStandardData(params?: EcoFlowQueryParams): Promise<{ data: EcoFlowStandard[] }> { - return request({ - url: '/api/eco-flow/standard', - method: 'get', - params - }); -} - -export function getQgcStaticData(params?: EcoFlowQueryParams): Promise<{ data: EcoFlowStandard[] }> { - return request({ - url: '/eng/eq/interval/qgc/getQgcStaticData', - method: 'post', - data:params - }); +export function getQgcStaticData( + params?: EcoFlowQueryParams +): Promise<{ data: EcoFlowStandard[] }> { + return request({ + url: '/eng/eq/interval/qgc/getQgcStaticData', + method: 'post', + data: params + }); } diff --git a/frontend/src/api/home/index.ts b/frontend/src/api/home/index.ts index 6809e3f2..1f2ac831 100644 --- a/frontend/src/api/home/index.ts +++ b/frontend/src/api/home/index.ts @@ -88,30 +88,3 @@ export function baseMsstbprptGetKendoList(data: any) { data: data }); } - -// 降水量实时数据 -export function getBaseStbprpDataPPTNR(data: any) { - return request({ - url: '/api/dec-lygk-base-server/base/stbprpData/GetKendoListCust?tbCode=PPTN_R', - method: 'post', - data: data - }); -} - -// 气温气压实时数据 -export function getBaseStbprpDataTMPR(data: any) { - return request({ - url: '/api/dec-lygk-base-server/base/stbprpData/GetKendoListCust?tbCode=TMP_R', - method: 'post', - data: data - }); -} - -// 风速风向实时数据 -export function getBaseStbprpDataWNDR(data: any) { - return request({ - url: '/api/dec-lygk-base-server/base/stbprpData/GetKendoListCust?tbCode=WND_R', - method: 'post', - data: data - }); -} \ No newline at end of file diff --git a/frontend/src/api/map.ts b/frontend/src/api/map.ts index 388431df..35ad43ea 100644 --- a/frontend/src/api/map.ts +++ b/frontend/src/api/map.ts @@ -3,9 +3,7 @@ import request from '@/utils/request'; // 获取图例列表 export function getModuleMapLegendList(params?: { moduleId?: string }) { const url = params?.moduleId - ? // ? `/api/wmp-sys-server/mapLegend/getModuleMapLegendList?moduleId=${params.moduleId}` - // : '/api/wmp-sys-server/mapLegend/getModuleMapLegendList'; - `/mapLegend/getModuleMapLegendList?moduleId=${params.moduleId}` + ? `/mapLegend/getModuleMapLegendList?moduleId=${params.moduleId}` : '/mapLegend/getModuleMapLegendList'; return request({ url, @@ -15,7 +13,6 @@ export function getModuleMapLegendList(params?: { moduleId?: string }) { // 获取地图配置列表 export function getMapList(data: any) { return request({ - // url: '/api/wmp-sys-server/mapmodule/getMapData', url: '/mapmodule/getMapData', method: 'post', data @@ -24,7 +21,7 @@ export function getMapList(data: any) { // 获取梯级流域地图 export function getQgcRvcd(data: any) { return request({ - url: '/api/wmp-eng-server/eng/rsvrcscdb/getQgcRvcd', + url: '/eng/base/rsvrcscdb/getQgcRvcd', method: 'post', data }); @@ -32,7 +29,7 @@ export function getQgcRvcd(data: any) { // 获取梯级流域下拉框列表 export function getRvcdList(data: any) { return request({ - url: '/api/wmp-eng-server/eng/rsvrcscdb/rvcd', + url: '/eng/base/rsvrcscdb/rvcd', method: 'post', data }); @@ -41,7 +38,23 @@ export function getRvcdList(data: any) { // 获取梯级流域下拉框图表数据 export function getKendoList(data: any) { return request({ - url: '/api/wmp-eng-server/eng/rsvrcscdb/GetKendoList', + url: '/eng/base/rsvrcscdb/GetKendoList', + method: 'post', + data + }); +} +// 鱼类分布查询 - 站点查询 +export function getFishPointList(data: any) { + return request({ + url: '/wte/we/fishPoint/qgc/GetKendoListCust', + method: 'post', + data + }); +} +// 鱼类分布查询 -鱼查询 +export function getFishList(data: any) { + return request({ + url: '/wte/we/fishList/GetKendoListCust', method: 'post', data }); diff --git a/frontend/src/api/system/map/ConfigManagement/index.ts b/frontend/src/api/system/map/ConfigManagement/index.ts index bed99f3b..b572288b 100644 --- a/frontend/src/api/system/map/ConfigManagement/index.ts +++ b/frontend/src/api/system/map/ConfigManagement/index.ts @@ -32,7 +32,16 @@ export function saveBaseWbsbChild(data: any) { data: data }); } +// 查询子节点 +export function getChildConfigTree(data: any) { + return request({ + url: '/base/msalongdetb/GetKendoList', + method: 'post', + data: data + }); +} +// 保存子节点 export function saveBaseWbsbChildDetail(data: any) { return request({ url: '/base/msalongdetb/save', diff --git a/frontend/src/api/yldc/index.ts b/frontend/src/api/yldc/index.ts index 3282a2ac..dd5a998c 100644 --- a/frontend/src/api/yldc/index.ts +++ b/frontend/src/api/yldc/index.ts @@ -38,7 +38,7 @@ export function getFprdDataInfo(data: any) { data }); } -// +// export function sdFprdRGetKendoListCust(data: any) { return request({ url: '/fpr/sdFprdR/GetKendoListCust', @@ -46,7 +46,7 @@ export function sdFprdRGetKendoListCust(data: any) { data }); } -//装置名称 /api/dec-lygk-base-server/base/vmsstbprpt/GetKendoList +//装置名称 export function vmsstbprptGetKendoList(data: any) { return request({ url: '/fpr/vmsstbprpt/GetKendoList', @@ -54,18 +54,21 @@ export function vmsstbprptGetKendoList(data: any) { data }); } -//鱼种类 /api/wmp-env-server/sdFprdR/getFishInfo +//鱼种类 export function sdFprdRgetFishInfo() { return request({ url: '/fpr/sdFprdR/getFishInfo', - method: 'post', + method: 'post' }); } //获取鱼类变化信息 -export function sdFprdRGetFtpChangeInfo(data: { baseId: string; year: string }) { +export function sdFprdRGetFtpChangeInfo(data: { + baseId: string; + year: string; +}) { return request({ url: '/fpr/sdFprdR/getFtpChangeInfo', method: 'post', data }); -} \ No newline at end of file +} diff --git a/frontend/src/api/zngj/index.ts b/frontend/src/api/zngj/index.ts index b4780180..bc8833c0 100644 --- a/frontend/src/api/zngj/index.ts +++ b/frontend/src/api/zngj/index.ts @@ -8,8 +8,8 @@ export function getAisbdbyxList(data: any) { data }); } - -// AI识别大坝环境-获取饼图+表格数据 /api/wmp-env-server/env/ai/com/GetKendoListCust + +// AI识别大坝环境-获取饼图+表格数据 export function getAiChartTable(data: any) { return request({ url: '/warn/ai/com/GetKendoListCust', @@ -18,7 +18,7 @@ export function getAiChartTable(data: any) { }); } -// AI识别大坝环境-获取电站列表 +// AI识别大坝环境-获取电站列表 export function getAiRstcd(data: any) { return request({ url: '/warn/ai/com/qgc/aiRstcd/GetKendoListCust', diff --git a/frontend/src/api/zxdzw/index.ts b/frontend/src/api/zxdzw/index.ts index 2780d6f6..310ee4a9 100644 --- a/frontend/src/api/zxdzw/index.ts +++ b/frontend/src/api/zxdzw/index.ts @@ -1,6 +1,5 @@ import request from '@/utils/request'; - export function vpcGetKendoListCust(data: any) { return request({ url: '/vap/vpc/GetKendoListCust', @@ -17,7 +16,7 @@ export function vmsstbprptGetKendoList(data: any) { data }); } -// 动物救助情况 +// 动物救助情况 export function wvaGetKendoListCust(data: any) { return request({ url: '/vap/wva/GetKendoListCust', @@ -25,11 +24,3 @@ export function wvaGetKendoListCust(data: any) { data }); } -//植物园情况介绍 -// export function vpcGetKendoListCust(data: any) { -// return request({ -// url: '/api/wmp-env-server/env/vpc/GetKendoListCust', -// method: 'post', -// data -// }); -// } \ No newline at end of file diff --git a/frontend/src/components/gis/GisView.vue b/frontend/src/components/gis/GisView.vue index 07de2437..60ea9a73 100644 --- a/frontend/src/components/gis/GisView.vue +++ b/frontend/src/components/gis/GisView.vue @@ -80,7 +80,6 @@ const init = async () => { // 备注:地图控制器只分发简单视图切换命令,避免在页面入口堆叠复杂业务逻辑。 const handleMapController = async (e: any, mapType: any) => { - console.log(e, mapType); switch (e) { case 'dim': await mapClass.switchView(mapType); @@ -106,7 +105,6 @@ const fetchTjData = async () => { for (let i = 0; i < res.data.data.length; i++) { datas.push(res.data.data[i].rvcd); } - console.log(servers.Tertiarybasin); mapClass.addTertiarybasinLayer( servers.Tertiarybasin, '#4DFFDD', @@ -119,9 +117,9 @@ const fetchTjData = async () => { // 备注:页面切换后仅触发编排器重载页面,不在组件内继续拼装业务联动。 watch( () => pageKey.value, - async newVal => { + newVal => { if (newVal && mapIsInited.value) { - await mapOrchestrator.handlePageChange(newVal, isShuiDianKaiFaMenu.value); + void mapOrchestrator.handlePageChange(newVal, isShuiDianKaiFaMenu.value); } } ); diff --git a/frontend/src/components/gis/gisUtils.ts b/frontend/src/components/gis/gisUtils.ts index f93131a6..1776cc85 100644 --- a/frontend/src/components/gis/gisUtils.ts +++ b/frontend/src/components/gis/gisUtils.ts @@ -19,7 +19,6 @@ declare global { __mapMode?: string; // 建议同时声明代码中用到的其他全局变量 } } -console.log(import.meta.env.VITE_APP_ATTACHMENT_URL); /** * 根据镜头高度获取地图级别 @@ -531,9 +530,7 @@ export const mapOutPut = (imageUrl: string) => { downloadElement.click(); }; }) - .catch((e: any) => { - console.log('e', e); - }); + .catch(() => {}); } else { downloadElement.href = canvas.toDataURL('image/png'); downloadElement.download = 'download'; diff --git a/frontend/src/components/gis/map.cesium.ts b/frontend/src/components/gis/map.cesium.ts index 073a64c5..2229d24a 100644 --- a/frontend/src/components/gis/map.cesium.ts +++ b/frontend/src/components/gis/map.cesium.ts @@ -196,7 +196,6 @@ export class MapCesium implements MapInterface { // 清理临时元素 document.body.removeChild(link); - console.log('Map image exported successfully:', fileName); } catch (error) { console.error('Failed to export map image:', error); if (error instanceof DOMException && error.name === 'SecurityError') { diff --git a/frontend/src/components/gis/map.class.ts b/frontend/src/components/gis/map.class.ts index be4caa56..c0eada73 100644 --- a/frontend/src/components/gis/map.class.ts +++ b/frontend/src/components/gis/map.class.ts @@ -33,7 +33,6 @@ export class MapClass implements MapClassInterface { this.service = new MapOl(); } static getInstance(): MapClass { - console.log('getInstance'); if (!this.instance) { this.instance = new MapClass(); } @@ -41,7 +40,6 @@ export class MapClass implements MapClassInterface { } // 地图初始化 init(container: HTMLElement, rectangle?: any): Promise { - console.log(container, rectangle); return this.service.init(container, rectangle).then(map => { this.view = map; return map; @@ -176,7 +174,6 @@ export class MapClass implements MapClassInterface { // 2. 根据类型初始化新的地图服务 if (type === '2D') { - console.log('Switching to 2D (OpenLayers)'); this.service = new MapOl(); this.service @@ -187,7 +184,6 @@ export class MapClass implements MapClassInterface { }) .catch(err => reject(err)); } else if (type === '3D') { - console.log('Switching to 3D (Cesium)'); this.service = new MapCesium(); this.service @@ -261,12 +257,7 @@ export class MapClass implements MapClassInterface { // }, 5000) // }) // } - } catch (e) { - console.log( - '%c switchView: ', - 'color: MidnightBlue; background: Aquamarine; font-size: 20px;', - e - ); + } catch { reject(); } }); diff --git a/frontend/src/components/gis/map.ol.ts b/frontend/src/components/gis/map.ol.ts index eb752d40..c5aa5d7b 100644 --- a/frontend/src/components/gis/map.ol.ts +++ b/frontend/src/components/gis/map.ol.ts @@ -60,6 +60,7 @@ export class MapOl implements MapInterface { view: View | null = null; private layerRegistry: Map = new Map(); private iconLoadState = new Map(); + private pointStyleCache = new Map(); private baseLayerConfig: any | null = null; private hydropBaseConfig: any | null = null; private REGISTRY_KEY = 'customBaseLayer'; @@ -79,7 +80,9 @@ export class MapOl implements MapInterface { private regionMaskManager: RegionMaskManager; private isBatchPopupMode = false; private batchPopupRefreshFrameId: number | null = null; + private pointLayerStyleRefreshFrameId: number | null = null; private labelVisibilityRefreshFrameId: number | null = null; + private labelVisibilityDebounceTimerId: number | null = null; constructor() { this.pointLayerManager = new PointLayerManager({ map: null, @@ -201,9 +204,9 @@ export class MapOl implements MapInterface { } }); this.map.on('moveend', () => { - this.requestRefreshPointLabelVisibility(true); + this.requestRefreshPointLabelVisibility(false, 60); if (this.isBatchPopupMode) { - this.requestUpdateBatchPopups(true); + this.requestUpdateBatchPopups(); } }); @@ -233,7 +236,7 @@ export class MapOl implements MapInterface { const currentZoom = this.view ? this.view.getZoom() : INITIAL_ZOOM; this.pointLayerManager.updateNearbyFeatureLayout(currentZoom); - this.requestRefreshPointLabelVisibility(true); + this.requestRefreshPointLabelVisibility(false, 80); } /** * 初始化 Popup Overlay @@ -334,10 +337,15 @@ export class MapOl implements MapInterface { } const currentZoom: any = this.view ? this.view.getZoom() : 4.5; + const cachedDensityVisible = feature.get('_densityVisible'); + const densityVisible = + typeof cachedDensityVisible === 'boolean' + ? cachedDensityVisible + : currentZoom >= this.getFeatureDensityMinZoom(feature); const iconTargetVisible = legendVisible !== false && regionVisible !== false && - this.shouldRenderFeatureByDensity(feature, currentZoom) && + densityVisible && this.shouldRenderNearbyFeature(feature, currentZoom); const dynamicScale = this.getDynamicPointScale(currentZoom); @@ -361,8 +369,20 @@ export class MapOl implements MapInterface { return null; } - const styles: Style[] = []; - styles.push( + const styleCacheKey = [ + iconUrl, + dynamicScale.toFixed(2), + fontSize, + labelOffsetY, + labelTargetVisible ? formattedLabelText : '', + labelTargetVisible ? 1 : 0 + ].join('|'); + const cachedStyles = this.pointStyleCache.get(styleCacheKey); + if (cachedStyles) { + return cachedStyles; + } + + const styles: Style[] = [ new Style({ image: new Icon({ src: iconUrl, @@ -372,7 +392,7 @@ export class MapOl implements MapInterface { declutterMode: 'none' }) }) - ); + ]; if (formattedLabelText && labelTargetVisible) { styles.push( @@ -394,9 +414,170 @@ export class MapOl implements MapInterface { ); } + this.pointStyleCache.set(styleCacheKey, styles); return styles; } + private markFeatureVisibilityDirty( + feature: Feature, + key: '_densityVisible' | '_iconCollisionVisible' | '_labelCollisionVisible', + visible: boolean, + dirtyLayerKeys?: Set + ) { + if (feature.get(key) === visible) { + return; + } + + feature.set(key, visible, true); + const layerKey = feature.get('_layerKey'); + if (dirtyLayerKeys && layerKey) { + dirtyLayerKeys.add(String(layerKey)); + } + } + + private flushDirtyPointLayers(dirtyLayerKeys: Set) { + dirtyLayerKeys.forEach(layerKey => { + const layer = this.pointLayerManager.getLayer(layerKey); + layer?.changed(); + }); + } + + private syncFeatureDensityVisible( + feature: Feature, + visible: boolean, + dirtyLayerKeys?: Set + ) { + this.markFeatureVisibilityDirty( + feature, + '_densityVisible', + visible, + dirtyLayerKeys + ); + } + + private getFeaturePixelGridKey( + layerKey: string, + pixelX: number, + pixelY: number, + cellSize: number + ) { + return `${layerKey}:${Math.floor(pixelX / cellSize)}:${Math.floor( + pixelY / cellSize + )}`; + } + + private hasNearbyViewportFeature( + target: { + feature: Feature; + layerKey: string; + pixelX: number; + pixelY: number; + }, + pixelGrid: Map< + string, + Array<{ + feature: Feature; + layerKey: string; + pixelX: number; + pixelY: number; + }> + >, + threshold: number + ): boolean { + const cellX = Math.floor(target.pixelX / threshold); + const cellY = Math.floor(target.pixelY / threshold); + + for (let offsetX = -1; offsetX <= 1; offsetX += 1) { + for (let offsetY = -1; offsetY <= 1; offsetY += 1) { + const bucket = pixelGrid.get( + `${target.layerKey}:${cellX + offsetX}:${cellY + offsetY}` + ); + if (!bucket?.length) { + continue; + } + + for (const candidate of bucket) { + if (candidate.feature === target.feature) { + continue; + } + if ( + Math.hypot( + candidate.pixelX - target.pixelX, + candidate.pixelY - target.pixelY + ) <= threshold + ) { + return true; + } + } + } + } + + return false; + } + + private getCollisionGridKeys( + rect: { left: number; right: number; top: number; bottom: number }, + cellSize: number + ): string[] { + const startX = Math.floor(rect.left / cellSize); + const endX = Math.floor(rect.right / cellSize); + const startY = Math.floor(rect.top / cellSize); + const endY = Math.floor(rect.bottom / cellSize); + const keys: string[] = []; + + for (let x = startX; x <= endX; x += 1) { + for (let y = startY; y <= endY; y += 1) { + keys.push(`${x}:${y}`); + } + } + + return keys; + } + + private addRectToCollisionGrid< + T extends { + left: number; + right: number; + top: number; + bottom: number; + } + >(rect: T, grid: Map, cellSize: number) { + this.getCollisionGridKeys(rect, cellSize).forEach(key => { + const bucket = grid.get(key) || []; + bucket.push(rect); + grid.set(key, bucket); + }); + } + + private findCollidingRectInGrid< + T extends { + left: number; + right: number; + top: number; + bottom: number; + } + >( + rect: { left: number; right: number; top: number; bottom: number }, + grid: Map, + cellSize: number, + isCollision: (candidate: T) => boolean + ): T | undefined { + for (const key of this.getCollisionGridKeys(rect, cellSize)) { + const bucket = grid.get(key); + if (!bucket?.length) { + continue; + } + + for (const candidate of bucket) { + if (isCollision(candidate)) { + return candidate; + } + } + } + + return undefined; + } + private refreshPointLabelVisibility() { if (!this.map || !this.view) return; @@ -442,64 +623,185 @@ export class MapOl implements MapInterface { }> = []; const viewportFeatures = this.pointLayerManager.getFeaturesInViewport(); + const dirtyLayerKeys = new Set(); + const densityIsolationThreshold = 56; + const densityPixelGrid = new Map< + string, + Array<{ + feature: Feature; + layerKey: string; + pixelX: number; + pixelY: number; + }> + >(); + const viewportEntries: Array<{ + feature: Feature; + layerKey: string; + iconUrl: string; + legendVisible: boolean; + regionVisible: boolean; + densityPriority: number; + nearbyVisible: boolean; + labelText: string; + pixelX?: number; + pixelY?: number; + }> = []; + + viewportFeatures.forEach(feature => { + const geometry = feature.getGeometry(); + const pixel = + geometry && geometry.getType() === 'Point' + ? this.map?.getPixelFromCoordinate( + (geometry as Point).getCoordinates() + ) + : null; + const layerKey = String(feature.get('_layerKey') || ''); + const entry = { + feature, + layerKey, + iconUrl: String(feature.get('_iconUrl') || ''), + legendVisible: feature.get('_legendVisible') !== false, + regionVisible: feature.get('_regionVisible') !== false, + densityPriority: this.getFeatureDensityPriority(feature), + nearbyVisible: this.shouldRenderNearbyFeature(feature, currentZoom), + labelText: this.formatPointLabelText( + feature.get('_labelText') as string + ), + pixelX: pixel?.[0], + pixelY: pixel?.[1] + }; + viewportEntries.push(entry); + + if ( + pixel && + entry.layerKey && + entry.legendVisible && + entry.regionVisible + ) { + const gridKey = this.getFeaturePixelGridKey( + entry.layerKey, + pixel[0], + pixel[1], + densityIsolationThreshold + ); + const bucket = densityPixelGrid.get(gridKey) || []; + bucket.push({ + feature, + layerKey: entry.layerKey, + pixelX: pixel[0], + pixelY: pixel[1] + }); + densityPixelGrid.set(gridKey, bucket); + } + }); if (currentZoom >= FULL_DISPLAY_NO_COLLISION_ZOOM) { - viewportFeatures.forEach(feature => { - const iconUrl = feature.get('_iconUrl') as string; - const shouldRenderIcon = - !!iconUrl && - this.isIconReady(iconUrl) && - feature.get('_legendVisible') !== false && - feature.get('_regionVisible') !== false && - this.shouldRenderFeatureByDensity(feature, currentZoom) && - this.shouldRenderNearbyFeature(feature, currentZoom); - const hasLabel = !!this.formatPointLabelText( - feature.get('_labelText') as string + viewportEntries.forEach(entry => { + const minZoom = this.getFeatureDensityMinZoom(entry.feature); + const densityVisible = + currentZoom >= minZoom || + (entry.legendVisible && + entry.regionVisible && + !!entry.layerKey && + entry.pixelX !== undefined && + entry.pixelY !== undefined && + !this.hasNearbyViewportFeature( + { + feature: entry.feature, + layerKey: entry.layerKey, + pixelX: entry.pixelX, + pixelY: entry.pixelY + }, + densityPixelGrid, + densityIsolationThreshold + )); + this.syncFeatureDensityVisible( + entry.feature, + densityVisible, + dirtyLayerKeys ); + const shouldRenderIcon = + !!entry.iconUrl && + this.isIconReady(entry.iconUrl) && + entry.legendVisible && + entry.regionVisible && + densityVisible && + entry.nearbyVisible; + const hasLabel = !!entry.labelText; - this.syncFeatureIconCollisionVisible(feature, shouldRenderIcon); + this.syncFeatureIconCollisionVisible( + entry.feature, + shouldRenderIcon, + dirtyLayerKeys + ); this.syncFeatureLabelCollisionVisible( - feature, - shouldRenderIcon && hasLabel + entry.feature, + shouldRenderIcon && hasLabel, + dirtyLayerKeys ); }); + this.flushDirtyPointLayers(dirtyLayerKeys); return; } - viewportFeatures.forEach(feature => { - const iconUrl = feature.get('_iconUrl') as string; + viewportEntries.forEach(entry => { + const minZoom = this.getFeatureDensityMinZoom(entry.feature); + const densityVisible = + currentZoom >= minZoom || + (entry.legendVisible && + entry.regionVisible && + !!entry.layerKey && + entry.pixelX !== undefined && + entry.pixelY !== undefined && + !this.hasNearbyViewportFeature( + { + feature: entry.feature, + layerKey: entry.layerKey, + pixelX: entry.pixelX, + pixelY: entry.pixelY + }, + densityPixelGrid, + densityIsolationThreshold + )); + this.syncFeatureDensityVisible( + entry.feature, + densityVisible, + dirtyLayerKeys + ); + const shouldRenderIcon = - !!iconUrl && - this.isIconReady(iconUrl) && - feature.get('_legendVisible') !== false && - feature.get('_regionVisible') !== false && - this.shouldRenderFeatureByDensity(feature, currentZoom) && - this.shouldRenderNearbyFeature(feature, currentZoom); + !!entry.iconUrl && + this.isIconReady(entry.iconUrl) && + entry.legendVisible && + entry.regionVisible && + densityVisible && + entry.nearbyVisible; if (!shouldRenderIcon) { - this.syncFeatureIconCollisionVisible(feature, false); - this.syncFeatureLabelCollisionVisible(feature, false); + this.syncFeatureIconCollisionVisible( + entry.feature, + false, + dirtyLayerKeys + ); + this.syncFeatureLabelCollisionVisible( + entry.feature, + false, + dirtyLayerKeys + ); return; } - const geometry = feature.getGeometry(); - if (!geometry || geometry.getType() !== 'Point') { - this.syncFeatureIconCollisionVisible(feature, false); - this.syncFeatureLabelCollisionVisible(feature, false); - return; - } - - const coordinates = (geometry as any).getCoordinates?.(); - if (!coordinates) { - this.syncFeatureIconCollisionVisible(feature, false); - this.syncFeatureLabelCollisionVisible(feature, false); - return; - } - - const pixel = this.map?.getPixelFromCoordinate(coordinates); - if (!pixel) { - this.syncFeatureIconCollisionVisible(feature, false); - this.syncFeatureLabelCollisionVisible(feature, false); + if (entry.pixelX === undefined || entry.pixelY === undefined) { + this.syncFeatureIconCollisionVisible( + entry.feature, + false, + dirtyLayerKeys + ); + this.syncFeatureLabelCollisionVisible( + entry.feature, + false, + dirtyLayerKeys + ); return; } @@ -508,13 +810,11 @@ export class MapOl implements MapInterface { // 避免像“小浪底/三门峡”这类近点被过早裁掉成只剩一个点。 const iconCollisionSize = Math.max(12, 16 * dynamicScale); const iconPadding = 0; - const iconLeft = pixel[0] - iconCollisionSize / 2 - iconPadding; - const iconRight = pixel[0] + iconCollisionSize / 2 + iconPadding; - const iconTop = pixel[1] - iconCollisionSize / 2 - iconPadding; - const iconBottom = pixel[1] + iconCollisionSize / 2 + iconPadding; - const labelText = this.formatPointLabelText( - feature.get('_labelText') as string - ); + const iconLeft = entry.pixelX - iconCollisionSize / 2 - iconPadding; + const iconRight = entry.pixelX + iconCollisionSize / 2 + iconPadding; + const iconTop = entry.pixelY - iconCollisionSize / 2 - iconPadding; + const iconBottom = entry.pixelY + iconCollisionSize / 2 + iconPadding; + const labelText = entry.labelText; const fontSize = this.getPointFontSize(dynamicScale); const lines = labelText ? labelText.split('\n') : ['']; const maxLineLength = Math.max(...lines.map(line => line.length), 1); @@ -525,22 +825,22 @@ export class MapOl implements MapInterface { ); const estimatedWidth = maxLineLength * fontSize * 0.6 + 16; const estimatedHeight = labelLineCount * (fontSize + 4) + 8; - const centerX = pixel[0]; - const centerY = pixel[1] + labelOffsetY; + const centerX = entry.pixelX; + const centerY = entry.pixelY + labelOffsetY; const candidateId = String( - feature.getId?.() || feature.get('stcd') || '' + entry.feature.getId?.() || entry.feature.get('stcd') || '' ); const dedupeKey = candidateId || [ - feature.get('_layerKey') || '', - pixel[0].toFixed(2), - pixel[1].toFixed(2), + entry.feature.get('_layerKey') || '', + entry.pixelX.toFixed(2), + entry.pixelY.toFixed(2), labelText ].join('|'); candidateMap.set(dedupeKey, { - feature, + feature: entry.feature, iconLeft, iconRight, iconTop, @@ -550,11 +850,11 @@ export class MapOl implements MapInterface { right: centerX + estimatedWidth / 2, top: centerY - estimatedHeight / 2, bottom: centerY + estimatedHeight / 2, - priority: Number(feature.get('_nearbyPriority') || 9999), - densityPriority: this.getFeatureDensityPriority(feature), + priority: Number(entry.feature.get('_nearbyPriority') || 9999), + densityPriority: entry.densityPriority, id: candidateId, - pixelX: pixel[0], - pixelY: pixel[1] + pixelX: entry.pixelX, + pixelY: entry.pixelY }); }); @@ -582,6 +882,16 @@ export class MapOl implements MapInterface { top: number; bottom: number; }> = []; + const placedIconGrid = new Map< + string, + Array<{ + featureId: string; + left: number; + right: number; + top: number; + bottom: number; + }> + >(); const placedLabelRects: Array<{ featureId: string; left: number; @@ -589,9 +899,17 @@ export class MapOl implements MapInterface { top: number; bottom: number; }> = []; - let hiddenByIconCollisionCount = 0; - let hiddenByLabelCollisionCount = 0; - let hiddenByIconHiddenCount = 0; + const placedLabelGrid = new Map< + string, + Array<{ + featureId: string; + left: number; + right: number; + top: number; + bottom: number; + }> + >(); + const collisionGridCellSize = 96; candidates.forEach(candidate => { const featureId = String( @@ -603,11 +921,18 @@ export class MapOl implements MapInterface { top: candidate.iconTop, bottom: candidate.iconBottom }; - const iconHasCollision = placedIconRects.some(rect => - this.checkLabelCollision(rect, iconRect) + const iconHasCollision = !!this.findCollidingRectInGrid( + iconRect, + placedIconGrid, + collisionGridCellSize, + rect => this.checkLabelCollision(rect, iconRect) ); const iconVisible = !iconHasCollision; - this.syncFeatureIconCollisionVisible(candidate.feature, iconVisible); + this.syncFeatureIconCollisionVisible( + candidate.feature, + iconVisible, + dirtyLayerKeys + ); if (iconVisible) { placedIconRects.push({ @@ -617,6 +942,11 @@ export class MapOl implements MapInterface { top: iconRect.top, bottom: iconRect.bottom }); + this.addRectToCollisionGrid( + placedIconRects[placedIconRects.length - 1], + placedIconGrid, + collisionGridCellSize + ); } }); @@ -640,37 +970,45 @@ export class MapOl implements MapInterface { const iconVisible = candidate.feature.get('_iconCollisionVisible') !== false; if (!candidate.hasLabel) { - this.syncFeatureLabelCollisionVisible(candidate.feature, false); + this.syncFeatureLabelCollisionVisible( + candidate.feature, + false, + dirtyLayerKeys + ); return; } if (!iconVisible) { - hiddenByIconHiddenCount += 1; - this.syncFeatureLabelCollisionVisible(candidate.feature, false); + this.syncFeatureLabelCollisionVisible( + candidate.feature, + false, + dirtyLayerKeys + ); return; } - const collidingIconRect = placedIconRects.find(rect => { - if (rect.featureId === featureId) { - return false; - } - return this.checkLabelCollisionWithIcon(rect, candidate); - }); + const collidingIconRect = this.findCollidingRectInGrid( + candidate, + placedIconGrid, + collisionGridCellSize, + rect => + rect.featureId !== featureId && + this.checkLabelCollisionWithIcon(rect, candidate) + ); const hasCollisionWithIcon = !!collidingIconRect; - const collidingLabelRect = placedLabelRects.find(rect => - this.checkLabelCollision(rect, candidate) + const collidingLabelRect = this.findCollidingRectInGrid( + candidate, + placedLabelGrid, + collisionGridCellSize, + rect => this.checkLabelCollision(rect, candidate) ); const hasCollisionWithLabel = !!collidingLabelRect; const visible = !hasCollisionWithIcon && !hasCollisionWithLabel; - if (!visible) { - if (hasCollisionWithIcon) { - hiddenByIconCollisionCount += 1; - } else { - hiddenByLabelCollisionCount += 1; - } - } - - this.syncFeatureLabelCollisionVisible(candidate.feature, visible); + this.syncFeatureLabelCollisionVisible( + candidate.feature, + visible, + dirtyLayerKeys + ); if (visible) { placedLabelRects.push({ @@ -680,11 +1018,25 @@ export class MapOl implements MapInterface { top: candidate.top, bottom: candidate.bottom }); + this.addRectToCollisionGrid( + placedLabelRects[placedLabelRects.length - 1], + placedLabelGrid, + collisionGridCellSize + ); } }); + this.flushDirtyPointLayers(dirtyLayerKeys); } - private requestRefreshPointLabelVisibility(immediate = false) { + private requestRefreshPointLabelVisibility( + immediate = false, + debounceMs = 0 + ) { + if (this.labelVisibilityDebounceTimerId !== null) { + window.clearTimeout(this.labelVisibilityDebounceTimerId); + this.labelVisibilityDebounceTimerId = null; + } + if (immediate) { if (this.labelVisibilityRefreshFrameId !== null) { cancelAnimationFrame(this.labelVisibilityRefreshFrameId); @@ -694,6 +1046,14 @@ export class MapOl implements MapInterface { return; } + if (debounceMs > 0) { + this.labelVisibilityDebounceTimerId = window.setTimeout(() => { + this.labelVisibilityDebounceTimerId = null; + this.requestRefreshPointLabelVisibility(); + }, debounceMs); + return; + } + if (this.labelVisibilityRefreshFrameId !== null) { return; } @@ -704,18 +1064,30 @@ export class MapOl implements MapInterface { }); } - private syncFeatureIconCollisionVisible(feature: Feature, visible: boolean) { - if (feature.get('_iconCollisionVisible') !== visible) { - feature.set('_iconCollisionVisible', visible); - feature.changed(); - } + private syncFeatureIconCollisionVisible( + feature: Feature, + visible: boolean, + dirtyLayerKeys?: Set + ) { + this.markFeatureVisibilityDirty( + feature, + '_iconCollisionVisible', + visible, + dirtyLayerKeys + ); } - private syncFeatureLabelCollisionVisible(feature: Feature, visible: boolean) { - if (feature.get('_labelCollisionVisible') !== visible) { - feature.set('_labelCollisionVisible', visible); - feature.changed(); - } + private syncFeatureLabelCollisionVisible( + feature: Feature, + visible: boolean, + dirtyLayerKeys?: Set + ) { + this.markFeatureVisibilityDirty( + feature, + '_labelCollisionVisible', + visible, + dirtyLayerKeys + ); } private isIconReady(iconUrl: string): boolean { @@ -740,11 +1112,11 @@ export class MapOl implements MapInterface { image.crossOrigin = 'anonymous'; image.onload = () => { this.iconLoadState.set(iconUrl, 'loaded'); - this.refreshPointLayerStyles(); + this.requestRefreshPointLayerStyles(); }; image.onerror = () => { this.iconLoadState.set(iconUrl, 'error'); - this.refreshPointLayerStyles(); + this.requestRefreshPointLayerStyles(); }; image.src = iconUrl; @@ -756,13 +1128,24 @@ export class MapOl implements MapInterface { return false; } + private requestRefreshPointLayerStyles() { + if (this.pointLayerStyleRefreshFrameId !== null) { + return; + } + + this.pointLayerStyleRefreshFrameId = window.requestAnimationFrame(() => { + this.pointLayerStyleRefreshFrameId = null; + this.refreshPointLayerStyles(); + }); + } + private refreshPointLayerStyles() { this.pointLayerManager.forEachLayer(layer => { layer.changed(); }); - this.requestRefreshPointLabelVisibility(true); + this.requestRefreshPointLabelVisibility(); if (this.isBatchPopupMode) { - this.requestUpdateBatchPopups(true); + this.requestUpdateBatchPopups(); } } @@ -1063,72 +1446,6 @@ export class MapOl implements MapInterface { return priority === 1; } - private createPointStyle1(feature: Feature): Style { - const color = (feature.get('iconCode') as string) || '#3399CC'; - const labelText = feature.get('titleName') as string; - const buildType = (feature.get('BuildType') as number) ?? 1; - - const currentZoom = this.view ? this.view.getZoom() : 4.5; - let dynamicScale = 0.7 + (currentZoom - 4.5) * 0.08; - dynamicScale = Math.max(0.5, Math.min(3.0, dynamicScale)); - const fontSize = Math.max(16, Math.min(24, 12 * dynamicScale)); - const radius = Math.max(6, Math.min(18, 6 * dynamicScale)); - - let imageStyle; - if (buildType === 0) { - const canvas = this.createSemiCircleCanvas(radius, color); - imageStyle = new Icon({ - img: canvas, - imgSize: [canvas.width, canvas.height], - anchor: [0.5, 0.5] - }); - } else { - imageStyle = new Circle({ - radius: radius, - fill: new Fill({ color: color }), - stroke: new Stroke({ color: 'rgba(0, 0, 0, 0.5)', width: 1 }) - }); - } - - const styleOptions: any = { - image: imageStyle - }; - - if (labelText) { - let offsetY = -20 * dynamicScale; - const specialLabels = [ - '枕头坝二级', - '老鹰岩二级', - '长洲', - '丹巴', - '玛尔挡', - '班多', - '彭水', - '李家峡', - 'ML', - 'BDa', - '乌东德', - '里底', - '大华桥', - - '巴塘' - ]; - if (specialLabels.includes(labelText)) { - offsetY = 20 * dynamicScale; - } - styleOptions.text = new Text({ - text: labelText, - offsetY: offsetY, - font: `${fontSize}px sans-serif`, - fill: new Fill({ color: '#fff' }), - stroke: new Stroke({ color: 'rgba(0, 0, 0, .9)', width: 2 }), - textAlign: 'center', - declutterMode: 'declutter' - }); - } - - return new Style(styleOptions); - } /** * 初始化加载基础图层 * @param layer 图层配置对象 @@ -2167,10 +2484,18 @@ export class MapOl implements MapInterface { * 移除地图对象,释放资源 */ destroy(): void { + if (this.pointLayerStyleRefreshFrameId !== null) { + cancelAnimationFrame(this.pointLayerStyleRefreshFrameId); + this.pointLayerStyleRefreshFrameId = null; + } if (this.labelVisibilityRefreshFrameId !== null) { cancelAnimationFrame(this.labelVisibilityRefreshFrameId); this.labelVisibilityRefreshFrameId = null; } + if (this.labelVisibilityDebounceTimerId !== null) { + window.clearTimeout(this.labelVisibilityDebounceTimerId); + this.labelVisibilityDebounceTimerId = null; + } this.removeQueryLayer(); this.regionMaskManager.destroy(); diff --git a/frontend/src/components/gis/ol/point-layer-manager.ts b/frontend/src/components/gis/ol/point-layer-manager.ts index 19e5e151..95489733 100644 --- a/frontend/src/components/gis/ol/point-layer-manager.ts +++ b/frontend/src/components/gis/ol/point-layer-manager.ts @@ -81,7 +81,11 @@ export class PointLayerManager { if (!this.map) return; const resolution = this.map.getView().getResolution(); - if (currentZoom === undefined || !Number.isFinite(currentZoom) || !resolution) { + if ( + currentZoom === undefined || + !Number.isFinite(currentZoom) || + !resolution + ) { return; } const nearbyPointConfig = getNearbyPointConfig(); @@ -109,7 +113,10 @@ export class PointLayerManager { return; } const sortedFeatures = [...features].sort((left, right) => { - return Number(left.get('_nearbyPriority') || 0) - Number(right.get('_nearbyPriority') || 0); + return ( + Number(left.get('_nearbyPriority') || 0) - + Number(right.get('_nearbyPriority') || 0) + ); }); const secondaryFeatures = sortedFeatures.filter(feature => { const priority = Number(feature.get('_nearbyPriority')); @@ -132,7 +139,8 @@ export class PointLayerManager { const groupSecondaryCount = secondaryFeatures.length; const baseOffsetPx = - Number(feature.get('_nearbyExpandOffsetPx')) || autoRule.expandOffsetPx; + Number(feature.get('_nearbyExpandOffsetPx')) || + autoRule.expandOffsetPx; const ringSize = Math.max(1, layoutRule.ringSize); const ringIndex = Math.floor(index / ringSize); const ringOffsetIndex = index % ringSize; @@ -144,10 +152,10 @@ export class PointLayerManager { currentRingCount <= 1 ? 0 : currentRingCount <= 2 - ? layoutRule.fanAngleForTwoDeg - : currentRingCount <= 4 - ? layoutRule.fanAngleForFourDeg - : layoutRule.fanAngleForManyDeg; + ? layoutRule.fanAngleForTwoDeg + : currentRingCount <= 4 + ? layoutRule.fanAngleForFourDeg + : layoutRule.fanAngleForManyDeg; const angleStepDeg = currentRingCount <= 1 ? 0 : fanAngleDeg / (currentRingCount - 1); const startAngleDeg = -90 - fanAngleDeg / 2; @@ -156,7 +164,10 @@ export class PointLayerManager { const expandOffsetPx = baseOffsetPx + ringIndex * - Math.max(layoutRule.ringGapPx, baseOffsetPx * layoutRule.ringGapFactor); + Math.max( + layoutRule.ringGapPx, + baseOffsetPx * layoutRule.ringGapFactor + ); const offsetX = Math.cos(expandAngleRad) * expandOffsetPx * resolution; const offsetY = Math.sin(expandAngleRad) * expandOffsetPx * resolution; @@ -452,7 +463,9 @@ export class PointLayerManager { // 备注:先恢复要素原始坐标,再按缩放阶段决定是否做展开偏移,避免多次缩放后偏移累计。 private resetFeatureToBaseCoordinate(feature: Feature): void { - const baseCoordinates = feature.get('_baseCoordinates') as number[] | undefined; + const baseCoordinates = feature.get('_baseCoordinates') as + | number[] + | undefined; const geometry = feature.getGeometry(); if ( !baseCoordinates || @@ -463,6 +476,9 @@ export class PointLayerManager { return; } - (geometry as Point).setCoordinates([baseCoordinates[0], baseCoordinates[1]]); + (geometry as Point).setCoordinates([ + baseCoordinates[0], + baseCoordinates[1] + ]); } } diff --git a/frontend/src/components/mapFilter/index.vue b/frontend/src/components/mapFilter/index.vue index 5a6a692c..626038af 100644 --- a/frontend/src/components/mapFilter/index.vue +++ b/frontend/src/components/mapFilter/index.vue @@ -35,7 +35,7 @@ /> - + 鱼类分布 @@ -60,21 +60,35 @@ + :loading="fishSpeciesLoading" + :max-tag-count="1" + option-filter-prop="label" + > + + {{ item.label }} + + @@ -99,16 +113,24 @@ import { ref, computed, watch } from 'vue'; import dayjs from 'dayjs'; import { useRoute } from 'vue-router'; +import { MapClass } from '@/components/gis/map.class'; 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 { useMapStore } from '@/store/modules/map'; import { DateSetting } from '@/utils/enumeration'; +import { getFishList, getFishPointList } from '@/api/map'; const route = useRoute(); const mapOrchestrator = useMapOrchestrator(); const mapDataStore = useMapDataStore(); const mapViewStore = useMapViewStore(); +const jidiSelectEventStore = useJidiSelectEventStore(); +const mapStore = useMapStore(); +const mapClass = MapClass.getInstance(); const ENG_POINT_LAYER_KEY = 'eng_point'; +const YLFB_POINT_LAYER_KEY = 'ylfb_point'; const siteRangePicker = [ { label: '全部', value: 'all' }, { label: '大型电站', value: 'large_eng_built' }, @@ -127,8 +149,13 @@ const defaultYear = const fishSurveyZhuanZhiParams = ref({ time: defaultYear.toString(), - value: [] + value: [] as string[], + anchorPointSelect: null as string | null }); +const fishPointOptions = ref([]); +const fishSpeciesOptions = ref<{ label: string; value: string }[]>([]); +const fishSpeciesLoading = ref(false); +const fishPointLoading = ref(false); const searchTimeRange = ref([ dayjs(mapViewStore.searchTimeRange[0]).format('YYYY-MM-DD HH:mm:ss'), @@ -141,12 +168,41 @@ const isMenu = (menuPath: string) => { return path.includes(menuPath); }; +const isFishSurveyMode = computed(() => { + return ( + isMenu('shengTaiDiaoCha/shuiShengShengTaiDiaoCha') && + formModel.value.fishSurveyZhuanZhi + ); +}); + +const anchorPointLoading = computed(() => { + return isFishSurveyMode.value ? fishPointLoading.value : false; +}); + +const currentFishBaseId = computed(() => { + return jidiSelectEventStore.selectedItem?.wbsCode || 'all'; +}); + +const anchorPointSelectValue = computed({ + get: () => { + return isFishSurveyMode.value + ? fishSurveyZhuanZhiParams.value.anchorPointSelect + : formModel.value.anchorPointSelect; + }, + set: (value: string | null) => { + if (isFishSurveyMode.value) { + fishSurveyZhuanZhiParams.value.anchorPointSelect = value; + return; + } + formModel.value.anchorPointSelect = value; + } +}); + // 备注:切页时只重置本地表单输入,并通过编排器复位时间范围,不再直接改运行态 store。 watch( () => route.path, () => { mapOrchestrator.resetFilterState(); - mapOrchestrator.changeEngPointCapacity('all'); searchTimeRange.value = [ dayjs(mapViewStore.searchTimeRange[0]).format('YYYY-MM-DD HH:mm:ss'), dayjs(mapViewStore.searchTimeRange[1]).format('YYYY-MM-DD HH:mm:ss') @@ -156,18 +212,309 @@ watch( anchorPointSelect: null, fishSurveyZhuanZhi: false }; + resetFishSurveyState(); } ); // 备注:时间选择只通过编排器分发,由编排器决定运行态更新和数据重载。 const triggerManualValuesChange = async (val: any) => { - if (val && val.length === 2) { - await mapOrchestrator.changeTimeRange(val); + const [startTime, endTime] = getTimeRangeByValue(val); + + if (!startTime || !endTime) { + return; } + + if (Array.isArray(val)) { + await mapOrchestrator.changeTimeRange([startTime, endTime]); + return; + } + + await fetchFishSurveyOptions(val); +}; + +const fetchFishSurveyOptions = async ( + timeValue = fishSurveyZhuanZhiParams.value.time +) => { + const [startTime, endTime] = getTimeRangeByValue(timeValue); + const currentBaseId = currentFishBaseId.value; + + if (!startTime || !endTime) { + return; + } + + const pointParams = { + filter: { + logic: 'and', + filters: [ + { + field: 'startTime', + operator: 'eq', + dataType: 'date', + value: startTime + }, + { + field: 'endTime', + operator: 'eq', + dataType: 'date', + value: endTime + }, + { field: 'lgtd', operator: 'isnotnull', dataType: 'string' } + ] + } + }; + const fishParams = { + filter: { + logic: 'and', + filters: [ + { + field: 'tm', + operator: 'gte', + dataType: 'date', + value: startTime + }, + { + field: 'tm', + operator: 'lte', + dataType: 'date', + value: endTime + }, + ...(!currentBaseId || currentBaseId === 'all' + ? [] + : [ + { + field: 'baseId', + operator: 'eq', + dataType: 'string', + value: currentBaseId + } + ]) + ] + }, + group: [ + { dir: 'des', field: 'code' }, + { dir: 'des', field: 'name' } + ] + }; + + fishPointLoading.value = true; + fishSpeciesLoading.value = true; + + try { + const [fishList, fishList1] = await Promise.all([ + getFishPointList(pointParams), + getFishList(fishParams) + ]); + const nextFishPointOptions = mapFishPointOptions(fishList); + const nextFishSpeciesOptions = mapFishSpeciesOptions(fishList1); + + fishPointOptions.value = nextFishPointOptions; + fishSpeciesOptions.value = nextFishSpeciesOptions; + fishSurveyZhuanZhiParams.value.value = nextFishSpeciesOptions.map( + item => item.value + ); + syncFishAnchorPointSelection(nextFishPointOptions); + } finally { + fishPointLoading.value = false; + fishSpeciesLoading.value = false; + } +}; + +const getTimeRangeByValue = (val: any): [string, string] => { + if (Array.isArray(val)) { + const [start, end] = val; + return [formatDateTimeValue(start), formatDateTimeValue(end)]; + } + + const year = getYearValue(val); + return [`${year}-01-01 00:00:00`, `${year}-12-31 23:59:59`]; +}; + +const formatDateTimeValue = (value: any) => { + if (!value) return ''; + return dayjs(value).format('YYYY-MM-DD HH:mm:ss'); +}; + +const getYearValue = (value: any) => { + if (typeof value === 'string' || typeof value === 'number') { + return String(value); + } + return dayjs(value).format('YYYY'); +}; + +const collectLeafItems = (items: any[] = []): any[] => { + return items.flatMap(item => { + if (Array.isArray(item?.items) && item.items.length > 0) { + return collectLeafItems(item.items); + } + return [item]; + }); +}; + +const getResponseData = (response: any) => { + return response?.data?.data; +}; + +const mapFishPointOptions = (response: any) => { + const responseData = getResponseData(response); + const sourceItems = Array.isArray(responseData) + ? responseData + : collectLeafItems(responseData?.items || []); + + return sourceItems + .map((item: any) => ({ + ...item, + stcd: item.stcd || item.STCD || item.code || item.CODE || item._id, + stnm: item.stnm || item.STNM || item.name || item.NAME || item.ennm || '', + ennm: item.ennm || item.ENNM || '', + baseName: item.baseName || item.BASENAME || '', + lgtd: item.lgtd || item.LGTD, + lttd: item.lttd || item.LTTD + })) + .filter((item: any) => item.stcd); +}; + +const mapFishSpeciesOptions = (response: any) => { + const responseData = getResponseData(response); + const sourceItems = Array.isArray(responseData) + ? collectLeafItems(responseData) + : collectLeafItems(responseData?.items || []); + const optionMap = new Map(); + + sourceItems.forEach((item: any) => { + const value = String(item.CODE || item.code || item.key || ''); + const label = item.NAME || item.name || item.key || value; + + if (!value || optionMap.has(value)) { + return; + } + + optionMap.set(value, { + label, + value + }); + }); + + return Array.from(optionMap.values()); +}; + +const selectedFishSpeciesLabels = computed(() => { + const selectedValues = new Set(fishSurveyZhuanZhiParams.value.value || []); + return fishSpeciesOptions.value + .filter(item => selectedValues.has(item.value)) + .map(item => item.label); +}); + +const fishPointLegendMeta = computed(() => { + const legendItem = + mapStore.getLegendItemsByLayerCode(YLFB_POINT_LAYER_KEY)?.[0]; + return { + iconCode: legendItem?.icon || 'default', + code: legendItem?.code || '', + nameEn: legendItem?.nameEn || 'ylfb' + }; +}); + +const filteredFishPointOptions = computed(() => { + let filteredData = [...fishPointOptions.value]; + + if (currentFishBaseId.value && currentFishBaseId.value !== 'all') { + filteredData = filteredData.filter( + (item: any) => item.baseId === currentFishBaseId.value + ); + } + + const selectedLabels = selectedFishSpeciesLabels.value; + if (!selectedLabels.length) { + return []; + } + + return filteredData.filter((item: any) => { + const ftp = String(item.ftp || item.FTP || ''); + return selectedLabels.some(label => ftp.includes(label)); + }); +}); + +const syncFishAnchorPointSelection = ( + options = filteredFishPointOptions.value +) => { + const currentValue = fishSurveyZhuanZhiParams.value.anchorPointSelect; + const matchedOption = options.find((item: any) => item.stcd === currentValue); + + fishSurveyZhuanZhiParams.value.anchorPointSelect = matchedOption + ? currentValue + : null; +}; + +const syncFishPointLayerData = (options = filteredFishPointOptions.value) => { + const visible = isFishSurveyMode.value && options.length > 0; + const layerItem = mapStore.findLayerByKey( + mapStore.layerData, + YLFB_POINT_LAYER_KEY + ); + const layerData = options.map((item: any) => { + const sttpMap = item.sttpMap || 'ylfb'; + return { + ...item, + sttp: item.sttp || 'ylfb', + sttpMap, + anchoPointState: item.anchoPointState || 'ylfb', + titleName: item.titleName || '鱼类分布', + iconCode: item.iconCode || fishPointLegendMeta.value.iconCode, + code: item.code || fishPointLegendMeta.value.code, + _id: item._id || `${sttpMap}_${item.stcd}`, + layerKey: YLFB_POINT_LAYER_KEY + }; + }); + const legendNames = mapStore + .getLegendItemsByLayerCode(YLFB_POINT_LAYER_KEY) + .map((item: any) => item?.nameEn) + .filter(Boolean); + + mapDataStore.setPointLayerCache(YLFB_POINT_LAYER_KEY, { + checked: visible, + data: layerData, + cacheKey: `custom:${YLFB_POINT_LAYER_KEY}:${layerData.length}` + }); + mapDataStore.rebuildPointDataFromCache(); + + if (layerItem) { + layerItem.data = layerData; + layerItem.checked = visible ? 1 : 0; + } + + if (legendNames.length > 0) { + mapStore.updateLegendCheckedBatch(legendNames, visible ? 1 : 0); + } + + if (!visible || layerData.length === 0) { + if (mapClass.hasLayer(YLFB_POINT_LAYER_KEY)) { + mapClass.mdLayerTreeShowOrHidden(YLFB_POINT_LAYER_KEY, false); + } + return; + } + + mapStore.refreshPointLayerDisplayData(YLFB_POINT_LAYER_KEY); +}; + +const resetFishSurveyState = () => { + fishSurveyZhuanZhiParams.value = { + time: defaultYear.toString(), + value: [], + anchorPointSelect: null + }; + fishPointOptions.value = []; + fishSpeciesOptions.value = []; + fishPointLoading.value = false; + fishSpeciesLoading.value = false; + syncFishPointLayerData([]); }; // 备注:搜索下拉只从当前缓存点位和运行态筛选条件派生,不直接驱动地图行为。 const anchorPointOptions = computed(() => { + if (isFishSurveyMode.value) { + return filteredFishPointOptions.value; + } + const pointData = mapDataStore.pointData || []; const checkedLayerKeys = new Set( mapViewStore.getCheckedLayerKeys() || [] @@ -225,13 +572,52 @@ const filterAnchorPoint = (input: string, option: any) => { // 备注:基地切换后仅重置本地选中的搜索项,不在组件中直接控制地图。 watch( - () => mapViewStore.selectedBaseId, + () => currentFishBaseId.value, () => { + if (isFishSurveyMode.value) { + syncFishAnchorPointSelection(); + return; + } formModel.value.anchorPointSelect = null; }, { immediate: true } ); +watch( + () => formModel.value.fishSurveyZhuanZhi, + async checked => { + if (!checked) { + resetFishSurveyState(); + return; + } + + await fetchFishSurveyOptions(); + } +); + +watch( + () => currentFishBaseId.value, + async () => { + if (!isFishSurveyMode.value) { + return; + } + + await fetchFishSurveyOptions(); + } +); + +watch( + filteredFishPointOptions, + options => { + if (!isFishSurveyMode.value) { + return; + } + syncFishAnchorPointSelection(options); + syncFishPointLayerData(options); + }, + { immediate: true } +); + const handleCapacityChange = (value: string) => { formModel.value.anchorPointSelect = null; mapOrchestrator.changeEngPointCapacity(value); @@ -239,7 +625,12 @@ const handleCapacityChange = (value: string) => { // 备注:搜索定位只分发定位命令,不在筛选组件里直接操作地图实例。 const handleAnchorPointChange = (value: string) => { - mapOrchestrator.focusPoint(value, 14); + if (!value) return; + mapOrchestrator.focusPoint( + value, + 14, + isFishSurveyMode.value ? anchorPointOptions.value : undefined + ); }; diff --git a/frontend/src/components/mapLegend/index.vue b/frontend/src/components/mapLegend/index.vue index 83754b94..64cc3e4d 100644 --- a/frontend/src/components/mapLegend/index.vue +++ b/frontend/src/components/mapLegend/index.vue @@ -87,7 +87,6 @@ const hasEnvFac = computed(() => ); const legendItemClick = (item: any) => { - console.log(item); if (item.parentName == '环保设施') return; if (item.canBeChecked == 0) { return; diff --git a/frontend/src/layout/components/AppMain.vue b/frontend/src/layout/components/AppMain.vue index 07fbe486..ec8921f5 100644 --- a/frontend/src/layout/components/AppMain.vue +++ b/frontend/src/layout/components/AppMain.vue @@ -8,9 +8,7 @@ import { useModelStore } from '@/store/modules/model'; import GisView from '@/components/gis/GisView.vue'; const modelStore = useModelStore(); - const tagsViewStore = useTagsViewStore(); - const router = useRoute(); const routeKey = computed(() => router.path + Math.random()); diff --git a/frontend/src/layout/components/Sidebar/index.vue b/frontend/src/layout/components/Sidebar/index.vue index 2e60c917..ac637235 100644 --- a/frontend/src/layout/components/Sidebar/index.vue +++ b/frontend/src/layout/components/Sidebar/index.vue @@ -1,17 +1,28 @@