From 18ab994b24386a461c1e6a9e7aff4c5ddc709876 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=89=88=E5=85=86=E5=A2=9E?= <你的邮箱@example.com> Date: Wed, 8 Jul 2026 08:33:08 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E5=9C=B0=E5=9B=BE=E9=80=BB?= =?UTF-8?q?=E8=BE=91=EF=BC=8C=E6=B2=BF=E7=A8=8B=E9=85=8D=E7=BD=AE=E4=BF=AE?= =?UTF-8?q?=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend-sjtb/.env.development | 2 +- frontend/.env.development | 4 +- frontend/docs/地图模块-详细说明.md | 2 +- frontend/src/api/map.ts | 16 + .../api/system/map/ConfigManagement/index.ts | 9 + frontend/src/components/mapFilter/index.vue | 413 +++++++++++++++++- .../map/application/map-orchestrator.ts | 52 ++- frontend/src/store/modules/map.ts | 245 +++++++++-- frontend/src/utils/request.ts | 2 +- .../ConfigManagement/ConfigManagementForm.vue | 16 +- .../map/components/ConfigManagement/index.vue | 71 ++- frontend/src/views/system/role/index.vue | 233 ++++++---- 12 files changed, 892 insertions(+), 173 deletions(-) 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..581fbba4 100644 --- a/frontend/.env.development +++ b/frontend/.env.development @@ -12,9 +12,9 @@ VITE_APP_BASE_API = '/dev-api' # 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/map.ts b/frontend/src/api/map.ts index 388431df..e3ac73e6 100644 --- a/frontend/src/api/map.ts +++ b/frontend/src/api/map.ts @@ -46,3 +46,19 @@ export function getKendoList(data: any) { 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/components/mapFilter/index.vue b/frontend/src/components/mapFilter/index.vue index 5a6a692c..2f3733b6 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,6 +168,36 @@ 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, @@ -156,18 +213,310 @@ watch( anchorPointSelect: null, fishSurveyZhuanZhi: false }; + resetFishSurveyState(); } ); // 备注:时间选择只通过编排器分发,由编排器决定运行态更新和数据重载。 const triggerManualValuesChange = async (val: any) => { - if (val && val.length === 2) { - await mapOrchestrator.changeTimeRange(val); + console.log('triggerManualValuesChange', 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 +574,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 +627,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/modules/map/application/map-orchestrator.ts b/frontend/src/modules/map/application/map-orchestrator.ts index bdbdd08f..e2f03725 100644 --- a/frontend/src/modules/map/application/map-orchestrator.ts +++ b/frontend/src/modules/map/application/map-orchestrator.ts @@ -115,16 +115,15 @@ export const useMapOrchestrator = () => { pageKey, isInitialLoad = false }: LoadPageOptions) => { - mapDataStore.setLoading(true); + let backgroundLoadStarted = false; try { + mapDataStore.setLoading(true); + const hasGlobalLegendConfig = Array.isArray(mapConfigStore.legendConfigOriginal) && mapConfigStore.legendConfigOriginal.length > 0; - const hasPointLayerCache = - Object.keys(mapDataStore.pointDataCache || {}).length > 0; const shouldLoadGlobalLegend = isInitialLoad && !hasGlobalLegendConfig; - const shouldPreloadLayerData = isInitialLoad && !hasPointLayerCache; const previousPageKey = mapConfigStore.lastLoadOptions?.pageKey || ''; const previousCheckedKeys = mapViewStore.getCheckedLayerKeys(); const moduleId = (route.meta?.moduleId as string) || ''; @@ -184,16 +183,31 @@ export const useMapOrchestrator = () => { mapStore.setLegendData(legendOriginal, pageLegend); } - if (shouldPreloadLayerData) { - mapStore.setSelectedLegendData(); - await mapStore.loadAllLayerData(layerConfig, checkedKeys); - } else { - await mapStore.updateLayerData(checkedKeys, true); - mapStore.setSelectedLegendData(); - } + const activePageToken = mapStore.activatePageContext( + pageKey, + layerConfig + ); + await mapStore.updateLayerData(checkedKeys, true); + mapStore.setSelectedLegendData(); + const backgroundLoadPromise = isInitialLoad + ? mapStore.loadAllLayerData(layerConfig, checkedKeys, { + pageToken: activePageToken, + skipSessionCheck: true + }) + : mapStore.loadCurrentPageLayerData(layerConfig, checkedKeys, { + pageToken: activePageToken, + skipSessionCheck: true + }); + + void backgroundLoadPromise.catch(error => { + console.error(`页面锚点后台加载失败 [${pageKey}]`, error); + }); + backgroundLoadStarted = true; } } finally { - // mapDataStore.setLoading(false); + if (!backgroundLoadStarted) { + mapDataStore.setLoading(false); + } } }; @@ -531,11 +545,19 @@ export const useMapOrchestrator = () => { }; // 备注:统一处理搜索定位,按点位编码从缓存数据中查找并飞行到目标位置。 - const focusPoint = (pointId: string, zoom = 15) => { + const focusPoint = ( + pointId: string, + zoom = 15, + fallbackPoints: any[] = [] + ) => { if (!pointId) return; - const targetPoint = mapDataStore.pointData.find((item: any) => { + const pointMatcher = (item: any) => { return item.stcd === pointId || item._id === pointId; - }); + }; + const targetPoint = + mapDataStore.pointData.find(pointMatcher) || + fallbackPoints.find(pointMatcher); + console.log('targetPoint', targetPoint); if (!targetPoint?.lgtd || !targetPoint?.lttd) return; mapClass.flyTopanto([targetPoint.lgtd, targetPoint.lttd], zoom); }; diff --git a/frontend/src/store/modules/map.ts b/frontend/src/store/modules/map.ts index b0446326..d79159a4 100644 --- a/frontend/src/store/modules/map.ts +++ b/frontend/src/store/modules/map.ts @@ -17,7 +17,9 @@ import request from '@/utils/request'; // import { urlList } from '@/utils/GisUrlList'; const mapClass = MapClass.getInstance(); const DEFAULT_LAYER_REQUEST_CONCURRENCY = 4; +const ACTIVE_PAGE_REQUEST_CONCURRENCY = 2; const ENG_POINT_LAYER_KEY = 'eng_point'; +const YLFB_POINT_LAYER_KEY = 'ylfb_point'; const ENG_POINT_MEDIUM_VISIBLE_ZOOM = 7.5; const ENG_POINT_LARGE_STATES = [ 'large_eng_built', @@ -176,6 +178,8 @@ export const useMapStore = defineStore('map', () => { >(); const activeLayerRequestKeyMap = new Map(); let loadSessionSeed = 0; + let activePageRenderToken = 0; + let activePageLayerKeys = new Set(); const normalizeLegendNameEn = mapConfigStore.normalizeLegendNameEn; @@ -333,6 +337,31 @@ export const useMapStore = defineStore('map', () => { mapDataStore.rebuildPointDataFromCache(); }; + const normalizePointLayerItems = (list: any[] = []) => { + return list + .map((item: any) => { + const iconType = item.anchoPointState; + const legendConfig = getLegendConfigByNameEn(iconType); + return { + ...item, + iconCode: item.iconCode || legendConfig?.icon || '', + code: item.code || legendConfig?.code || '', + tm: + item.sttpMap === 'WQ_ALARM' + ? item?.warnDataList?.[0]?.tm + : item?.tm, + _id: + item._id || `${item.sttpMap || iconType || 'point'}_${item.stcd}`, + layerKey: item.layerKey || YLFB_POINT_LAYER_KEY + }; + }) + .filter( + (item: any) => + (item.iconCode || item.code == 'colorLayer') && + !(item?.baseId == 'all' && item?.anchoPointState?.endsWith('_nbuilt')) + ); + }; + const ensureGISLayerConfig = (layerItem: any) => { if (!layerItem) return null; @@ -388,20 +417,11 @@ export const useMapStore = defineStore('map', () => { }; /** - * 启动新的图层加载会话,并取消上一轮仍在进行中的旧请求,避免菜单切换后旧数据回写。 + * 启动新的图层加载会话,供需要严格按最新查询条件收口的场景复用。 */ const startLoadSession = () => { loadSessionSeed += 1; - const currentSessionId = loadSessionSeed; - - inFlightRequestMap.forEach(entry => { - if (entry.sessionId !== currentSessionId) { - entry.controller.abort(); - mapDataStore.clearLayerLoadState(entry.layerKey); - } - }); - - return currentSessionId; + return loadSessionSeed; }; /** @@ -411,13 +431,37 @@ export const useMapStore = defineStore('map', () => { return loadSessionSeed === sessionId; }; + const activatePageContext = (pageKey: string, items: any[] = []) => { + void pageKey; + activePageRenderToken += 1; + activePageLayerKeys = new Set(getAllLayerKeys(items)); + return activePageRenderToken; + }; + + const shouldApplyToActivePage = (layerKey: string, pageToken?: number) => { + if (!layerKey) return false; + if (pageToken === undefined) return true; + return ( + activePageRenderToken === pageToken && activePageLayerKeys.has(layerKey) + ); + }; + + const isActivePageToken = (pageToken?: number) => { + if (pageToken === undefined) return true; + return activePageRenderToken === pageToken; + }; + /** - * 按固定并发数执行图层加载任务,避免瞬时请求过多,同时让单个失败不影响其他任务。 + * 按指定并发数执行图层加载任务,为当前页请求预留浏览器连接,避免后台预加载阻塞前台切页。 */ const runLayerLoadQueue = async ( tasks: Array<() => Promise>, concurrency: number = DEFAULT_LAYER_REQUEST_CONCURRENCY ) => { + if (tasks.length === 0) { + return []; + } + const results: PromiseSettledResult[] = []; const safeConcurrency = Math.max(1, concurrency); @@ -460,6 +504,43 @@ export const useMapStore = defineStore('map', () => { mapDataStore.setPointData(data); }; + const replacePointLayerData = ( + layerKey: string, + data: any[] = [], + visible = true + ) => { + if (!layerKey) return; + + const layerItem = findLayerByKey(layerData.value, layerKey); + const normalizedData = normalizePointLayerItems( + data.map((item: any) => ({ + ...item, + layerKey + })) + ); + + mapDataStore.setPointLayerCache(layerKey, { + checked: visible, + data: normalizedData, + cacheKey: `custom:${layerKey}:${normalizedData.length}` + }); + mapDataStore.rebuildPointDataFromCache(); + + if (layerItem) { + layerItem.data = normalizedData; + layerItem.checked = visible ? 1 : 0; + } + + if (!visible || normalizedData.length === 0) { + if (mapClass.hasLayer(layerKey)) { + mapClass.mdLayerTreeShowOrHidden(layerKey, false); + } + return; + } + + refreshPointLayerDisplayData(layerKey); + }; + /** * 图例数据转对象 */ @@ -604,9 +685,19 @@ export const useMapStore = defineStore('map', () => { * @param items 图层数据 * @param checkedKeys 选中的图层 keys(用于优先加载) */ - const loadAllLayerData = async (items: any[], checkedKeys: string[] = []) => { + const loadAllLayerData = async ( + items: any[], + checkedKeys: string[] = [], + options: { + pageToken?: number; + skipSessionCheck?: boolean; + concurrency?: number; + } = {} + ) => { mapDataStore.setLoading(true); - const currentSessionId = startLoadSession(); + const currentSessionId = options.skipSessionCheck + ? undefined + : startLoadSession(); const checkedTasks: Array<() => Promise> = []; const uncheckedTasks: Array<() => Promise> = []; const processedKeys = new Set(); @@ -620,7 +711,12 @@ export const useMapStore = defineStore('map', () => { !processedKeys.has(item.key) ) { processedKeys.add(item.key); - const task = () => loadLayerData(item, currentSessionId); + const task = () => + loadLayerData(item, { + sessionId: currentSessionId, + pageToken: options.pageToken, + skipSessionCheck: options.skipSessionCheck + }); if (checkedKeys.includes(item.key)) { checkedTasks.push(task); } else { @@ -637,9 +733,11 @@ export const useMapStore = defineStore('map', () => { processItems(items); try { - const checkedResults = await runLayerLoadQueue(checkedTasks); - const uncheckedResults = await runLayerLoadQueue(uncheckedTasks); - const failedResults = [...checkedResults, ...uncheckedResults].filter( + const loadResults = await runLayerLoadQueue( + [...checkedTasks, ...uncheckedTasks], + options.concurrency ?? DEFAULT_LAYER_REQUEST_CONCURRENCY + ); + const failedResults = loadResults.filter( result => result.status === 'rejected' ); @@ -650,7 +748,11 @@ export const useMapStore = defineStore('map', () => { ); } - if (!isCurrentLoadSession(currentSessionId)) { + if ( + (typeof currentSessionId === 'number' && + !isCurrentLoadSession(currentSessionId)) || + !isActivePageToken(options.pageToken) + ) { return; } @@ -701,7 +803,69 @@ export const useMapStore = defineStore('map', () => { // 不能再回放 load 启动瞬间的默认 checked 快照。 await updateLayerData(finalCheckedKeys, true); } finally { + const shouldFinishLoading = + (typeof currentSessionId !== 'number' || + isCurrentLoadSession(currentSessionId)) && + (options.pageToken === undefined || + activePageRenderToken === options.pageToken); + if (shouldFinishLoading) { + mapDataStore.setLoading(false); + } + } + }; + + const loadCurrentPageLayerData = async ( + items: any[], + checkedKeys: string[] = [], + options: { + pageToken?: number; + skipSessionCheck?: boolean; + } = {} + ) => { + const checkedLayerSet = new Set(checkedKeys); + const tasks: Array<() => Promise> = []; + const processedKeys = new Set(); + + const processItems = (itemList: any[]) => { + itemList.forEach(item => { + if ( + item.type === 'pointMap' && + item.key && + item.url && + checkedLayerSet.has(item.key) && + !processedKeys.has(item.key) + ) { + processedKeys.add(item.key); + tasks.push(() => + loadLayerData(item, { + sessionId: options.skipSessionCheck ? undefined : loadSessionSeed, + pageToken: options.pageToken, + skipSessionCheck: options.skipSessionCheck ?? true + }) + ); + } + + if (item.children && item.children.length > 0) { + processItems(item.children); + } + }); + }; + + processItems(items); + + if (tasks.length === 0) { mapDataStore.setLoading(false); + return; + } + + mapDataStore.setLoading(true); + + try { + await runLayerLoadQueue(tasks, ACTIVE_PAGE_REQUEST_CONCURRENCY); + } finally { + if (isActivePageToken(options.pageToken)) { + mapDataStore.setLoading(false); + } } }; @@ -852,8 +1016,17 @@ export const useMapStore = defineStore('map', () => { */ const loadLayerData = async ( layer: any, - sessionId: number = loadSessionSeed + options: { + sessionId?: number; + pageToken?: number; + skipSessionCheck?: boolean; + } = {} ) => { + const { + sessionId = loadSessionSeed, + pageToken, + skipSessionCheck = false + } = options; const { key, url, params = {}, paramJson, anchorParamJson } = layer; // 没有URL,跳过 @@ -999,7 +1172,7 @@ export const useMapStore = defineStore('map', () => { if ( controller.signal.aborted || - !isCurrentLoadSession(sessionId) || + (!skipSessionCheck && !isCurrentLoadSession(sessionId)) || activeLayerRequestKeyMap.get(key) !== requestIdentifier ) { return []; @@ -1022,22 +1195,11 @@ export const useMapStore = defineStore('map', () => { } } - list.forEach((item: any) => { - const iconType = item.anchoPointState; - const legendConfig = getLegendConfigByNameEn(iconType); - item.iconCode = legendConfig?.icon || ''; - item.code = legendConfig?.code || ''; - item.tm = - item.sttpMap === 'WQ_ALARM' - ? item?.warnDataList?.[0]?.tm - : item?.tm; - item._id = item.sttpMap + '_' + item.stcd; - }); - - list = list.filter( - (l: any) => - (l.iconCode || l.code == 'colorLayer') && - !(l?.baseId == 'all' && l?.anchoPointState?.endsWith('_nbuilt')) + list = normalizePointLayerItems( + list.map((item: any) => ({ + ...item, + layerKey: item.layerKey || key + })) ); const isLayerChecked = layer.checked === 1; @@ -1050,7 +1212,7 @@ export const useMapStore = defineStore('map', () => { layer.data = list; - if (list.length > 0) { + if (list.length > 0 && shouldApplyToActivePage(key, pageToken)) { const displayData = filterPointLayerDataForDisplay(key, list); mapClass.addInitDataLayer(displayData, key); if (isLayerChecked) { @@ -1065,7 +1227,7 @@ export const useMapStore = defineStore('map', () => { if ( controller.signal.aborted || isCanceledRequestError(error) || - !isCurrentLoadSession(sessionId) || + (!skipSessionCheck && !isCurrentLoadSession(sessionId)) || activeLayerRequestKeyMap.get(key) !== requestIdentifier ) { return []; @@ -1131,7 +1293,7 @@ export const useMapStore = defineStore('map', () => { for (const key of timeRangeLayerKeys) { const layerItem = findLayerByKey(layerData.value, key); if (layerItem && currentCheckedKeys.includes(key)) { - await loadLayerData(layerItem, currentSessionId); + await loadLayerData(layerItem, { sessionId: currentSessionId }); } } @@ -1163,6 +1325,7 @@ export const useMapStore = defineStore('map', () => { layerData, setLayerData, loadAllLayerData, + loadCurrentPageLayerData, loadLayerData, setSelectedLegendData, legendData, @@ -1174,6 +1337,7 @@ export const useMapStore = defineStore('map', () => { updateLayerData, updateLegendChecked, updateLegendCheckedBatch, + replacePointLayerData, getCheckedKeys, legendData2Obj, loading, @@ -1184,6 +1348,7 @@ export const useMapStore = defineStore('map', () => { getLayerBranchKeys, normalizeCheckedLayerKeys, refreshPointLayerDisplayData, + activatePageContext, searchTimeRange, reloadBySearchTimeRange, updateSearchTimeRange diff --git a/frontend/src/utils/request.ts b/frontend/src/utils/request.ts index 0f9323c2..5b9d040d 100644 --- a/frontend/src/utils/request.ts +++ b/frontend/src/utils/request.ts @@ -34,7 +34,7 @@ service.interceptors.request.use( config.headers._sysid = '10EC2E0B-AEA9-4757-83A2-201BA1BC54E9'; config.headers.authorization = - 'bearer 863782c5-5e51-49c6-95fd-13e6f25b9bf2'; + 'bearer 223f63a2-0b13-4757-8e15-f342098e63ca'; config.baseURL = '/'; } else { const user = useUserStoreHook(); diff --git a/frontend/src/views/system/map/components/ConfigManagement/ConfigManagementForm.vue b/frontend/src/views/system/map/components/ConfigManagement/ConfigManagementForm.vue index 90b7d48a..ba917388 100644 --- a/frontend/src/views/system/map/components/ConfigManagement/ConfigManagementForm.vue +++ b/frontend/src/views/system/map/components/ConfigManagement/ConfigManagementForm.vue @@ -78,7 +78,7 @@ :key="item.stcd" :value="item.stcd" > - {{ item.ennm }} + {{ item.stnm }} = { const handleRiverSelect = async (value: string) => { const res = await getEngInfoDropdown({ rvcd: value }); - stationList.value = res.data || []; + stationList.value = (res.data || []).map((item: any) => ({ + ...item, + stnm: item.ennm + })); }; const isEdit = computed(() => !!props.initialValues); @@ -455,7 +458,7 @@ const handleAdd = () => { const handleNodeSelect = (value: string) => { const station = stationList.value.find(item => item.stcd === value); if (station) { - newNodeData.value.ennm = station.ennm; + newNodeData.value.ennm = station.stnm; newNodeData.value.sttpName = station.sttpName; } }; @@ -583,7 +586,7 @@ const handleEditNodeSelect = (dataRef: any) => { const list = dataRef.parentId ? childStationList.value : stationList.value; const station = list.find(item => item.stcd === value); if (station) { - editNodeData.value.ennm = dataRef.parentId ? station.stnm : station.ennm; + editNodeData.value.ennm = station.stnm; editNodeData.value.sttpName = station.sttpName; editNodeData.value.stcd = station.stcd; } @@ -734,7 +737,10 @@ const initForm = async () => { // 编辑时调用接口获取一级站点列表 if (formData.rvcd) { const res = await getEngInfoDropdown({ rvcd: formData.rvcd }); - stationList.value = res.data || []; + stationList.value = (res.data || []).map((item: any) => ({ + ...item, + stnm: item.ennm + })); } } else { resetForm(); diff --git a/frontend/src/views/system/map/components/ConfigManagement/index.vue b/frontend/src/views/system/map/components/ConfigManagement/index.vue index af27d7db..a0b8d476 100644 --- a/frontend/src/views/system/map/components/ConfigManagement/index.vue +++ b/frontend/src/views/system/map/components/ConfigManagement/index.vue @@ -15,7 +15,7 @@ :list-url="getAllConfigTree" :search-params="searchParams" > -