diff --git a/frontend/.env.development b/frontend/.env.development index b560df26..d29767aa 100644 --- a/frontend/.env.development +++ b/frontend/.env.development @@ -7,7 +7,7 @@ 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://localhost:8093' # 测试环境 # VITE_APP_BASE_URL = 'http://172.16.21.142:8093' # 汤伟 diff --git a/frontend/public/glb/map-air.glb b/frontend/public/glb/map-air.glb new file mode 100644 index 00000000..f4b70f77 Binary files /dev/null and b/frontend/public/glb/map-air.glb differ diff --git a/frontend/src/api/system/disposeManage/index.ts b/frontend/src/api/system/disposeManage/index.ts index 3b3d9a01..a226e358 100644 --- a/frontend/src/api/system/disposeManage/index.ts +++ b/frontend/src/api/system/disposeManage/index.ts @@ -8,6 +8,14 @@ export function threedroambGetKendoListCust(data: any) { data: data }); } +export function getThreedRoamData(data: any) { + return request({ + url: '/threedroamb/getThreedRoamData', + method: 'post', + data: data + }); +} + export function threedroambsave(data: any) { return request({ url: '/threedroamb/save', @@ -47,4 +55,4 @@ export function threedroambexport(data: any) { data: data, responseType: 'blob' }); -} \ No newline at end of file +} diff --git a/frontend/src/components/gis/GisView.vue b/frontend/src/components/gis/GisView.vue index 57f2525f..c52c139b 100644 --- a/frontend/src/components/gis/GisView.vue +++ b/frontend/src/components/gis/GisView.vue @@ -18,6 +18,9 @@ + + + + + diff --git a/frontend/src/components/gis/roam/data/riverline.ts b/frontend/src/components/gis/roam/data/riverline.ts new file mode 100644 index 00000000..b9f4dbbd --- /dev/null +++ b/frontend/src/components/gis/roam/data/riverline.ts @@ -0,0 +1,50 @@ +/** + * CZML 飞行路径模板 + * 运行时由 ThreeDRoamManager 动态替换 position.cartographicDegrees 和 clock.interval + */ +export const czml_huanghe = [ + { + id: 'document', + name: '黄河八电站漫游', + version: '1.0', + clock: { + interval: '2021-03-16T10:00:01Z/2021-03-16T11:12:18Z', + currentTime: '2021-03-16T10:00:01Z', + multiplier: 1 + } + }, + { + id: 'hhriverRoamingModel', + name: '黄河八电站漫游飞机', + description: '从上游电站开始往下游电站漫游飞行模型', + availability: '2021-03-16T10:00:01Z/2021-03-16T11:12:18Z', + model: { + gltf: '/glb/map-air.glb', + scale: 10 + }, + orientation: { + velocityReference: '#position' + }, + viewFrom: { + cartesian: [-1480, -1315, 1079] + }, + path: { + resolution: 1, + material: { + polylineGlow: { + color: { + rgba: [0, 0, 200, 255] + }, + glowPower: 0.4 + } + }, + width: 3 + }, + position: { + interpolationAlgorithm: 'HERMITE', + interpolationDegree: 7, + epoch: '2021-03-16T10:00:01Z', + cartographicDegrees: [] + } + } +] diff --git a/frontend/src/components/mapController/index.vue b/frontend/src/components/mapController/index.vue index 28f2fd3d..4c0b1b1d 100644 --- a/frontend/src/components/mapController/index.vue +++ b/frontend/src/components/mapController/index.vue @@ -43,7 +43,7 @@ { case 'dim': // 切换3D/2D { const switchingTo3D = uiStore.mapType === '2D'; + // 先关闭漫游(如果有),标记跳过相机还原(由地图切换自行处理) + if (uiStore.isRoaming) { + uiStore.skipRoamCameraRestore = true; + uiStore.isRoaming = false; + } uiStore.mapType = switchingTo3D ? '3D' : '2D'; if (switchingTo3D) { // 2D→3D:关闭抽屉,请求倾斜摄影数据,开始过渡 @@ -405,7 +410,6 @@ const handleControllerClick = (item: any) => { } else { // 3D→2D:打开抽屉,清理过渡,卸载所有倾斜摄影模型 uiStore.drawerOpen = true; - uiStore.isRoaming = false; cleanup3DTransition(); activeKey.value = null; // 卸载所有倾斜摄影 diff --git a/frontend/src/components/mapFilter/index.vue b/frontend/src/components/mapFilter/index.vue index 9e495377..93af03ad 100644 --- a/frontend/src/components/mapFilter/index.vue +++ b/frontend/src/components/mapFilter/index.vue @@ -647,7 +647,7 @@ const focusSelectedAnchorPoint = () => { mapOrchestrator.focusPoint( selectedValue, - uiStore.mapType === '3D' ? 9 : 14, + uiStore.mapType === '3D' ? 8 : 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 4c0ff19e..93785339 100644 --- a/frontend/src/modules/map/application/map-orchestrator.ts +++ b/frontend/src/modules/map/application/map-orchestrator.ts @@ -227,20 +227,18 @@ export const useMapOrchestrator = () => { ); await mapStore.updateLayerData(checkedKeys, true); mapStore.setSelectedLegendData(); - const backgroundLoadPromise = isInitialLoad - ? mapStore.loadAllLayerData(layerConfig, checkedKeys, { - pageToken: activePageToken, - skipSessionCheck: true, - pageKey - }) - : mapStore.loadCurrentPageLayerData(layerConfig, checkedKeys, { - pageToken: activePageToken, - skipSessionCheck: true - }); - - void backgroundLoadPromise.catch(error => { - console.error(`页面锚点后台加载失败 [${pageKey}]`, error); - }); + if (isInitialLoad) { + mapStore.loadAllLayerData(layerConfig, checkedKeys, { + pageToken: activePageToken, + skipSessionCheck: true, + pageKey + }); + } else { + mapStore.loadCurrentPageLayerData(layerConfig, checkedKeys, { + pageToken: activePageToken, + skipSessionCheck: true + }); + } backgroundLoadStarted = true; } } finally { @@ -297,6 +295,10 @@ export const useMapOrchestrator = () => { const checkedKeys = mapViewStore.getCheckedLayerKeys(); await mapStore.updateLayerData(checkedKeys, true); mapStore.setSelectedLegendData(); + // 切换到 3D 时:先恢复图层,再停留 + 飞入中国视角(与 mountView 一致) + if (mapClass.flyToDefaultView) { + await mapClass.flyToDefaultView(); + } await syncZoomSensitiveState({ isHydroMenu: options.getIsHydroMenu(), refreshEngPoint: true @@ -419,6 +421,10 @@ export const useMapOrchestrator = () => { bindBaseSelection(); bindZoomListener(options.getIsHydroMenu); await loadPage({ pageKey: options.pageKey, isInitialLoad: true }); + // 加载完边界线等动态图层后,再从地球视图飞入中国视角 + if (mapClass.flyToDefaultView) { + await mapClass.flyToDefaultView(); + } await syncZoomSensitiveState({ isHydroMenu: options.getIsHydroMenu(), refreshEngPoint: true @@ -622,6 +628,8 @@ export const useMapOrchestrator = () => { const previousPageKey = mapConfigStore.lastLoadOptions?.pageKey || ''; if (previousPageKey && previousPageKey !== pageKey) { + // 取消旧页面正在请求的图层数据,释放 HTTP 连接 + mapStore.abortAllInFlightRequests(); hideCurrentVisibleLayers(); mapViewStore.setCheckedLayerKeys([]); mapStore.setSelectedLegendData(); diff --git a/frontend/src/store/modules/map.ts b/frontend/src/store/modules/map.ts index 99cbd6c0..7a99a910 100644 --- a/frontend/src/store/modules/map.ts +++ b/frontend/src/store/modules/map.ts @@ -808,7 +808,7 @@ export const useMapStore = defineStore('map', () => { * @param items 图层数据 * @param checkedKeys 选中的图层 keys(用于优先加载) */ - const loadAllLayerData = async ( + const loadAllLayerData = ( items: any[], checkedKeys: string[] = [], options: { @@ -824,7 +824,6 @@ export const useMapStore = defineStore('map', () => { const checkedTasks: Array<() => Promise> = []; const uncheckedTasks: Array<() => Promise> = []; const processedKeys = new Set(); - const debugStart = Date.now(); const processItems = (itemList: any[]) => { itemList.forEach(item => { @@ -857,97 +856,101 @@ export const useMapStore = defineStore('map', () => { processItems(items); - try { - // 首页初始化:所有图层接口一次性并发下发,避免分批串行等待。 - const loadResults = await Promise.allSettled( - [...checkedTasks, ...uncheckedTasks].map(task => task()) - ); - const failedResults = loadResults.filter( - result => result.status === 'rejected' - ); + const allTasks = [...checkedTasks, ...uncheckedTasks]; - if (failedResults.length > 0) { - console.warn( - '部分图层数据加载失败,但不会阻断其他图层:', - failedResults + // 方案三:逐个处理 —— 内部不 await 批量完成,每个请求独立释放 HTTP 连接 + void (async () => { + try { + const loadResults = await Promise.allSettled( + allTasks.map(task => task()) + ); + const failedResults = loadResults.filter( + result => result.status === 'rejected' ); - } - if ( - (typeof currentSessionId === 'number' && - !isCurrentLoadSession(currentSessionId)) || - !isActivePageToken(options.pageToken) - ) { - return; - } - - if ( - options.pageKey && - hasPendingPageNavigationAwayFrom(options.pageKey) - ) { - return; - } - - // 确保所有图层的数据都已经缓存到 pointDataCache 和 layer.data - const allLayerKeys = getAllLayerKeys(items); - for (const key of allLayerKeys) { - const layer = findLayerByKey(items, key); - if (layer && getEffectivePointLayerData(key).length > 0) { - syncBackgroundCacheToReactiveStore(key); - layer.data = getEffectivePointLayerData(key); - } - } - - const allPointData = mapDataStore.rebuildPointDataFromCache(allLayerKeys); - attachNearbyPointRuntimeMeta(allPointData); - const runtimeCheckedKeys = getRuntimeCheckedLayerKeys(); - const treeCheckedKeys = mapConfigStore.extractCheckedLayerKeys( - layerData.value.length > 0 ? layerData.value : items - ); - const finalCheckedKeys = - runtimeCheckedKeys.length > 0 ? runtimeCheckedKeys : treeCheckedKeys; - - // 备注:近邻点元数据需要基于全量点位统一识别,识别完成后同步刷新各图层 Feature。 - for (const key of allLayerKeys) { - const layer = findLayerByKey(items, key); - if (!layer || !hasPointLayerData(key)) { - continue; + if (failedResults.length > 0) { + console.warn( + '部分图层数据加载失败,但不会阻断其他图层:', + failedResults + ); } - const layerPoints = getPointLayerData(key); - const displayData = filterPointLayerDataForDisplay(key, layerPoints); - const cacheChecked = mapDataStore.getPointLayerCache(key)?.checked; - const shouldRestoreVisible = - typeof cacheChecked === 'boolean' - ? cacheChecked - : layer.checked === 1 || finalCheckedKeys.includes(key); - layer.data = layerPoints; - mapClass.addInitDataLayer(displayData, key); - if (shouldRestoreVisible) { - restoreLayerLegendVisibility(key); + if ( + (typeof currentSessionId === 'number' && + !isCurrentLoadSession(currentSessionId)) || + !isActivePageToken(options.pageToken) + ) { + return; } - mapClass.mdLayerTreeShowOrHidden(key, shouldRestoreVisible); - } - // 设置选中的图例数据(确保图例在所有锚点加载完成后才显示) - setSelectedLegendData(); + if ( + options.pageKey && + hasPendingPageNavigationAwayFrom(options.pageKey) + ) { + return; + } - // 备注:初始化 loading 期间用户可能已经手动改过图层勾选,这里必须以最新运行态收尾, - // 不能再回放 load 启动瞬间的默认 checked 快照。 - await updateLayerData(finalCheckedKeys, true); - } finally { - const shouldFinishLoading = - (typeof currentSessionId !== 'number' || - isCurrentLoadSession(currentSessionId)) && - (options.pageToken === undefined || - activePageRenderToken === options.pageToken); - if (shouldFinishLoading) { - finishPageLoading(options.pageToken); + // 确保所有图层的数据都已经缓存到 pointDataCache 和 layer.data + const allLayerKeys = getAllLayerKeys(items); + for (const key of allLayerKeys) { + const layer = findLayerByKey(items, key); + if (layer && getEffectivePointLayerData(key).length > 0) { + syncBackgroundCacheToReactiveStore(key); + layer.data = getEffectivePointLayerData(key); + } + } + + const allPointData = mapDataStore.rebuildPointDataFromCache(allLayerKeys); + attachNearbyPointRuntimeMeta(allPointData); + const runtimeCheckedKeys = getRuntimeCheckedLayerKeys(); + const treeCheckedKeys = mapConfigStore.extractCheckedLayerKeys( + layerData.value.length > 0 ? layerData.value : items + ); + const finalCheckedKeys = + runtimeCheckedKeys.length > 0 ? runtimeCheckedKeys : treeCheckedKeys; + + // 备注:近邻点元数据需要基于全量点位统一识别,识别完成后同步刷新各图层 Feature。 + for (const key of allLayerKeys) { + const layer = findLayerByKey(items, key); + if (!layer || !hasPointLayerData(key)) { + continue; + } + + const layerPoints = getPointLayerData(key); + const displayData = filterPointLayerDataForDisplay(key, layerPoints); + const cacheChecked = mapDataStore.getPointLayerCache(key)?.checked; + const shouldRestoreVisible = + typeof cacheChecked === 'boolean' + ? cacheChecked + : layer.checked === 1 || finalCheckedKeys.includes(key); + layer.data = layerPoints; + mapClass.addInitDataLayer(displayData, key); + if (shouldRestoreVisible) { + restoreLayerLegendVisibility(key); + } + mapClass.mdLayerTreeShowOrHidden(key, shouldRestoreVisible); + } + + // 设置选中的图例数据(确保图例在所有锚点加载完成后才显示) + setSelectedLegendData(); + + // 备注:初始化 loading 期间用户可能已经手动改过图层勾选,这里必须以最新运行态收尾, + // 不能再回放 load 启动瞬间的默认 checked 快照。 + await updateLayerData(finalCheckedKeys, true); + } finally { + const shouldFinishLoading = + (typeof currentSessionId !== 'number' || + isCurrentLoadSession(currentSessionId)) && + (options.pageToken === undefined || + activePageRenderToken === options.pageToken); + if (shouldFinishLoading) { + finishPageLoading(options.pageToken); + } } - } + })(); }; - const loadCurrentPageLayerData = async ( + const loadCurrentPageLayerData = ( items: any[], checkedKeys: string[] = [], options: { @@ -993,14 +996,12 @@ export const useMapStore = defineStore('map', () => { beginPageLoading(options.pageToken); - try { - // 菜单切换:仅当前页的勾选图层一次性并发下发,旧请求在后台继续运行。 - await Promise.allSettled(tasks.map(task => task())); - } finally { + // 方案三:逐个处理 —— 不 await 批量完成,每个请求独立释放 HTTP 连接 + void Promise.allSettled(tasks.map(task => task())).then(() => { if (isActivePageToken(options.pageToken)) { finishPageLoading(options.pageToken); } - } + }); }; /** @@ -1446,6 +1447,18 @@ export const useMapStore = defineStore('map', () => { } }; + /** + * 取消所有正在请求的图层数据,释放 HTTP 连接 + */ + const abortAllInFlightRequests = () => { + for (const [identifier, entry] of inFlightRequestMap.entries()) { + entry.controller.abort(); + inFlightRequestMap.delete(identifier); + activeLayerRequestKeyMap.delete(entry.layerKey); + } + backgroundPointLayerCache.clear(); + }; + /** * 更新搜索时间范围并重新加载描点数据 */ @@ -1461,6 +1474,7 @@ export const useMapStore = defineStore('map', () => { loadCurrentPageLayerData, loadLayerData, setSelectedLegendData, + abortAllInFlightRequests, legendData, setLegendData, legendDataSelected, diff --git a/frontend/src/store/modules/ui.ts b/frontend/src/store/modules/ui.ts index d629759e..31992ca2 100644 --- a/frontend/src/store/modules/ui.ts +++ b/frontend/src/store/modules/ui.ts @@ -8,6 +8,8 @@ export const useUiStore = defineStore('ui', () => { const mapSwitchCompletedTick = ref(0); // 3d 流域三维漫游 const isRoaming = ref(false); + // dim 切换时跳过漫游相机还原(由地图模式切换自行处理) + const skipRoamCameraRestore = ref(false); // 切换抽屉状态 const toggleDrawer = () => { @@ -26,6 +28,7 @@ export const useUiStore = defineStore('ui', () => { return { drawerOpen, isRoaming, + skipRoamCameraRestore, toggleDrawer, setDrawerOpen, mapType, diff --git a/frontend/src/utils/request.ts b/frontend/src/utils/request.ts index c8e1b177..91849a70 100644 --- a/frontend/src/utils/request.ts +++ b/frontend/src/utils/request.ts @@ -56,10 +56,9 @@ service.interceptors.response.use( if (status === 200) { if (response.data.code == 401) { message.error(response.data.msg || '请求失败'); - setTimeout(() => { - localStorage.clear(); - window.location.href = '/login'; - }, 1000); + localStorage.clear(); + useUserStoreHook().resetToken(); + router.push('/login'); return; } else if (response.data.code == 1) { message.error(response.data.msg); diff --git a/frontend/src/views/system/TiltPhotoManagement/index.vue b/frontend/src/views/system/TiltPhotoManagement/index.vue index 98aa474e..d40ecce4 100644 --- a/frontend/src/views/system/TiltPhotoManagement/index.vue +++ b/frontend/src/views/system/TiltPhotoManagement/index.vue @@ -148,7 +148,7 @@ const handleDelete = (record: any) => { zIndex: 2002, onOk: async () => { try { - let res = await deleteTiltPhoto({ id: record.id }); + let res = await deleteTiltPhoto({ stcd: record.stcd }); message.success('删除成功'); basicTable.value.refresh(); } catch (error) {