3d文字碰撞修改,添加漫游功能,3d整体优化
This commit is contained in:
parent
2f0bc9f67d
commit
f1f232cb2d
@ -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'
|
||||
# 汤伟
|
||||
|
||||
BIN
frontend/public/glb/map-air.glb
Normal file
BIN
frontend/public/glb/map-air.glb
Normal file
Binary file not shown.
@ -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',
|
||||
|
||||
@ -18,6 +18,9 @@
|
||||
<BaseLayerSwitcher :map="mapClass" />
|
||||
<!-- 梯级弹框 -->
|
||||
<TjLayerModal v-model:open="tjModalVisible" />
|
||||
|
||||
<!-- 三维漫游面板 -->
|
||||
<ThreeDRoamPanel v-if="uiStore.isRoaming" />
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
@ -27,6 +30,7 @@ import MapFilter from '@/components/mapFilter/index.vue';
|
||||
import MapController from '@/components/mapController/index.vue';
|
||||
import BaseLayerSwitcher from '@/components/BaseLayerSwitcher/index.vue';
|
||||
import TjLayerModal from './TjLayerModal.vue';
|
||||
import ThreeDRoamPanel from './roam/ThreeDRoamPanel.vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useMapOrchestrator } from '@/modules/map/application/map-orchestrator';
|
||||
import { MapClass } from './map.class';
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -273,6 +273,11 @@ export class MapClass implements MapClassInterface {
|
||||
this.service.flyTopanto(position, zoom);
|
||||
}
|
||||
|
||||
/** 飞入默认视角(仅 3D 生效):初始地球视图后,图层加载完再飞入中国 */
|
||||
async flyToDefaultView(): Promise<void> {
|
||||
await this.service.flyToDefaultView();
|
||||
}
|
||||
|
||||
getCurrentZoom(): number | undefined {
|
||||
return this.service.getCurrentZoom();
|
||||
}
|
||||
|
||||
5
frontend/src/components/gis/map.d.ts
vendored
5
frontend/src/components/gis/map.d.ts
vendored
@ -78,6 +78,11 @@ export interface MapInterface {
|
||||
*/
|
||||
initPopupOverlay(popupContainer: HTMLDivElement): void;
|
||||
|
||||
/**
|
||||
* 飞入默认视角(仅 3D 有效),在图层加载完成后调用
|
||||
*/
|
||||
flyToDefaultView(): Promise<void>;
|
||||
|
||||
/**
|
||||
* 初始化加载基础图层
|
||||
* @param layer
|
||||
|
||||
@ -2499,6 +2499,11 @@ export class MapOl implements MapInterface {
|
||||
return this.view?.getZoom();
|
||||
}
|
||||
|
||||
/** 3D 专用飞入默认视角,2D 下无需操作 */
|
||||
async flyToDefaultView(): Promise<void> {
|
||||
// 2D 无动画,空实现
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成带完整圆边框的上半圆 Canvas
|
||||
* @param radius 半径(像素)
|
||||
|
||||
363
frontend/src/components/gis/roam/ThreeDRoamManager.ts
Normal file
363
frontend/src/components/gis/roam/ThreeDRoamManager.ts
Normal file
@ -0,0 +1,363 @@
|
||||
/**
|
||||
* 三维漫游管理器
|
||||
* 负责加载 CZML 飞行路径、控制动画、第一人称相机跟随
|
||||
*/
|
||||
import { czml_huanghe } from './data/riverline';
|
||||
import * as Cesium from 'cesium';
|
||||
|
||||
export class ThreeDRoamManager {
|
||||
private viewer: any;
|
||||
private dataSource: any = null;
|
||||
private roamingEntity: any = null;
|
||||
private startTime: any = null;
|
||||
private callbackEvent: (() => void) | null = null;
|
||||
private tickCallback: ((clock: any) => void) | null = null;
|
||||
private totalDuration = 0;
|
||||
private onProgress: ((seconds: number) => void) | null = null;
|
||||
private positionList: number[] = [];
|
||||
|
||||
// 开关状态
|
||||
private _flightPathVisible = true;
|
||||
private _flightModelVisible = true;
|
||||
|
||||
// 相机平滑跟随插值状态
|
||||
private _smoothCenter: Cesium.Cartesian3 | null = null;
|
||||
private _smoothOrientation: Cesium.Quaternion | null = null;
|
||||
|
||||
constructor(viewer: any) {
|
||||
this.viewer = viewer;
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化漫游(加载 CZML 并定位到第0帧)
|
||||
* @param positionList API 返回的坐标点位数组
|
||||
* @param totalTime 总时长(秒)
|
||||
* @param milestoneList 里程碑列表 [[name, time], ...]
|
||||
* @param onProgress 进度回调
|
||||
*/
|
||||
async init(
|
||||
positionList: number[],
|
||||
totalTime: number,
|
||||
milestoneList?: [string, string][],
|
||||
onProgress?: (seconds: number) => void
|
||||
): Promise<void> {
|
||||
this.onProgress = onProgress || null;
|
||||
this.totalDuration = Math.ceil(totalTime || 0);
|
||||
|
||||
// 0. 清除旧数据源(避免堆积)
|
||||
if (this.dataSource) {
|
||||
this.viewer.dataSources.remove(this.dataSource);
|
||||
this.dataSource = null;
|
||||
}
|
||||
this.roamingEntity = null;
|
||||
|
||||
// 1. 处理点位数据(每4个点 +600 形成站点停留)
|
||||
const processedList = [...positionList];
|
||||
for (let i = 0; i < processedList.length; i++) {
|
||||
if ((i + 1) % 4 === 0) {
|
||||
processedList[i] = processedList[i] + 600;
|
||||
}
|
||||
}
|
||||
this.positionList = processedList;
|
||||
|
||||
// 2. 基于模板生成 CZML,替换坐标
|
||||
const c_czml = JSON.parse(JSON.stringify(czml_huanghe));
|
||||
c_czml[1].position.cartographicDegrees = processedList;
|
||||
|
||||
// 动态更新时间区间(总时长秒 → ISO 时间)
|
||||
const totalSec = Math.ceil(totalTime || 0);
|
||||
const endDate = new Date('2021-03-16T10:00:01Z');
|
||||
endDate.setSeconds(endDate.getSeconds() + totalSec);
|
||||
const endStr = endDate.toISOString().replace(/\.\d+Z$/, 'Z');
|
||||
c_czml[0].clock.interval = `2021-03-16T10:00:01Z/${endStr}`;
|
||||
c_czml[0].clock.currentTime = '2021-03-16T10:00:01Z';
|
||||
c_czml[1].availability = `2021-03-16T10:00:01Z/${endStr}`;
|
||||
|
||||
// 3. 加载 CZML
|
||||
try {
|
||||
const dataSourcePromise = await this.viewer.dataSources.add(
|
||||
Cesium.CzmlDataSource.load(c_czml)
|
||||
);
|
||||
this.dataSource = dataSourcePromise;
|
||||
this.roamingEntity = this.dataSource.entities.getById(c_czml[1].id);
|
||||
if (this.roamingEntity) {
|
||||
this.roamingEntity.path.show = this._flightPathVisible;
|
||||
this.roamingEntity.model.show = this._flightModelVisible;
|
||||
}
|
||||
|
||||
// 4. 锁定相机到飞机模型后方(第0帧视角)
|
||||
this.viewer.trackedEntity = this.roamingEntity;
|
||||
this._smoothCenter = null;
|
||||
this._smoothOrientation = null;
|
||||
this.firstPersonModel();
|
||||
|
||||
this.startTime = this.viewer.clock.currentTime.clone();
|
||||
this.viewer.clock.clockRange = Cesium.ClockRange.CLAMPED;
|
||||
this.viewer.clock.multiplier = 1;
|
||||
} catch (error) {
|
||||
console.error('CZML 加载失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/** 开始飞行 */
|
||||
start(): void {
|
||||
if (!this.roamingEntity) {
|
||||
console.warn('请先初始化漫游数据');
|
||||
return;
|
||||
}
|
||||
this.viewer.trackedEntity = this.roamingEntity;
|
||||
this.viewer.clock.shouldAnimate = true;
|
||||
this.entitiesVisible(false);
|
||||
this.registerTickCallback();
|
||||
}
|
||||
|
||||
/** 暂停飞行(相机仍锁定在模型位置) */
|
||||
pause(): void {
|
||||
this.viewer.clock.shouldAnimate = false;
|
||||
this.entitiesVisible(true);
|
||||
this.removeTickCallback();
|
||||
}
|
||||
|
||||
/** 重置飞行(回到起点/第0帧) */
|
||||
reset(): void {
|
||||
this.unLockViewAndEntity();
|
||||
this.clearResult();
|
||||
this.viewer.clock.shouldAnimate = false;
|
||||
this._smoothCenter = null;
|
||||
this._smoothOrientation = null;
|
||||
if (this.onProgress) this.onProgress(0);
|
||||
}
|
||||
|
||||
/** 清除飞行路径和模型(保留相机视角完全不变) */
|
||||
clear(): void {
|
||||
// 隐藏模型和路径(不删数据源,不动 trackedEntity 和 lookAtTransform)
|
||||
if (this.roamingEntity) {
|
||||
this.roamingEntity.path.show = false;
|
||||
this.roamingEntity.model.show = false;
|
||||
}
|
||||
// 停止时钟(不动相机,视角完全不变)
|
||||
this.viewer.clock.shouldAnimate = false;
|
||||
// 清理引用
|
||||
this.roamingEntity = null;
|
||||
this.removeTickCallback();
|
||||
this._smoothCenter = null;
|
||||
this._smoothOrientation = null;
|
||||
if (this.onProgress) this.onProgress(0);
|
||||
}
|
||||
|
||||
/** 设置飞行倍率 */
|
||||
setSpeed(multiplier: number): void {
|
||||
this.viewer.clock.multiplier = multiplier;
|
||||
}
|
||||
|
||||
/** 切换飞行路径显隐 */
|
||||
setFlightPathVisible(visible: boolean): void {
|
||||
this._flightPathVisible = visible;
|
||||
if (this.roamingEntity) {
|
||||
this.roamingEntity.path.show = visible;
|
||||
}
|
||||
}
|
||||
|
||||
/** 切换飞行模型显隐 */
|
||||
setFlightModelVisible(visible: boolean): void {
|
||||
this._flightModelVisible = visible;
|
||||
if (this.roamingEntity) {
|
||||
this.roamingEntity.model.show = visible;
|
||||
}
|
||||
}
|
||||
|
||||
/** 跳转到指定时间点(秒) */
|
||||
jumpToTime(seconds: number): void {
|
||||
if (this.startTime) {
|
||||
this.viewer.clock.currentTime = Cesium.JulianDate.addSeconds(
|
||||
this.startTime,
|
||||
seconds,
|
||||
new Cesium.JulianDate()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/** 视角定位到指定时间点的位置 */
|
||||
flyToPositionAtTime(seconds: number): void {
|
||||
if (!this.roamingEntity || !this.startTime) return;
|
||||
const time = Cesium.JulianDate.addSeconds(
|
||||
this.startTime,
|
||||
seconds,
|
||||
new Cesium.JulianDate()
|
||||
);
|
||||
const position = this.roamingEntity.position?.getValue(time);
|
||||
if (position) {
|
||||
this.viewer.camera.flyTo({
|
||||
destination: position,
|
||||
duration: 0.5
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/** 是否已加载漫游实体 */
|
||||
get hasEntity(): boolean {
|
||||
return this.roamingEntity !== null && this.roamingEntity !== undefined;
|
||||
}
|
||||
|
||||
/** 销毁清理(完全释放) */
|
||||
destroy(skipCesiumCleanup = false): void {
|
||||
if (skipCesiumCleanup) {
|
||||
// dim 切换:只清引用,Cesium 数据由地图切换自行销毁
|
||||
this.removeTickCallback();
|
||||
this.roamingEntity = null;
|
||||
this.dataSource = null;
|
||||
this.viewer = null;
|
||||
return;
|
||||
}
|
||||
this.clear();
|
||||
if (this.dataSource) {
|
||||
this.viewer.dataSources.remove(this.dataSource);
|
||||
this.dataSource = null;
|
||||
}
|
||||
this.removeFirstPersonModel();
|
||||
this.viewer = null;
|
||||
}
|
||||
|
||||
// ==================== Private ====================
|
||||
|
||||
/** 隐藏/显示地图实体 */
|
||||
private entitiesVisible(visible: boolean): void {
|
||||
const entities = this.viewer.entities?.values;
|
||||
if (entities?.length > 0) {
|
||||
entities.forEach((it: any) => {
|
||||
const name = it?.myname;
|
||||
if (
|
||||
(name === 'tempMarketImg' ||
|
||||
name === 'tempMarkets' ||
|
||||
name === 'tempMarket') &&
|
||||
it.show !== visible
|
||||
) {
|
||||
it.show = visible;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/** 第一人称相机跟随(preRender)—— 平滑插值版 */
|
||||
private firstPersonModel(): void {
|
||||
if (this.viewer.scene) {
|
||||
this.removeFirstPersonModel();
|
||||
this.callbackEvent = this.viewer.scene.preRender.addEventListener(() => {
|
||||
this.setCameraLookAt();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/** 移除第一人称 */
|
||||
private removeFirstPersonModel(): void {
|
||||
if (this.callbackEvent && typeof this.callbackEvent === 'function') {
|
||||
this.callbackEvent();
|
||||
this.callbackEvent = null;
|
||||
}
|
||||
}
|
||||
|
||||
/** 每帧平滑插值相机到模型后方(解决转向时突然跳动) */
|
||||
private setCameraLookAt(): void {
|
||||
try {
|
||||
const entity = this.viewer.trackedEntity;
|
||||
if (!entity) return;
|
||||
|
||||
const time = this.viewer.clock.currentTime;
|
||||
const targetCenter = entity.position?.getValue(time);
|
||||
const targetOrientation = entity.orientation?.getValue(time);
|
||||
if (!targetCenter || !targetOrientation) return;
|
||||
|
||||
// 插值系数:越小越平滑(跟随延迟越大)
|
||||
const lerpFactor = 0.06;
|
||||
|
||||
// 对位置做 lerp
|
||||
let center: Cesium.Cartesian3;
|
||||
if (this._smoothCenter) {
|
||||
center = Cesium.Cartesian3.lerp(
|
||||
this._smoothCenter,
|
||||
targetCenter,
|
||||
lerpFactor,
|
||||
new Cesium.Cartesian3()
|
||||
);
|
||||
} else {
|
||||
center = targetCenter.clone();
|
||||
}
|
||||
|
||||
// 对朝向做 slerp(球面线性插值,保证旋转平滑)
|
||||
let orientation: Cesium.Quaternion;
|
||||
if (this._smoothOrientation) {
|
||||
orientation = Cesium.Quaternion.slerp(
|
||||
this._smoothOrientation,
|
||||
targetOrientation,
|
||||
lerpFactor,
|
||||
new Cesium.Quaternion()
|
||||
);
|
||||
} else {
|
||||
orientation = targetOrientation.clone();
|
||||
}
|
||||
|
||||
this._smoothCenter = center;
|
||||
this._smoothOrientation = orientation;
|
||||
|
||||
const transform = Cesium.Matrix4.fromRotationTranslation(
|
||||
Cesium.Matrix3.fromQuaternion(orientation),
|
||||
center
|
||||
);
|
||||
this.viewer.camera.lookAtTransform(
|
||||
transform,
|
||||
new Cesium.Cartesian3(-1200, 0, 500)
|
||||
);
|
||||
} catch (e) {
|
||||
// ignore render error
|
||||
}
|
||||
}
|
||||
|
||||
/** 注册 onTick 进度回调 */
|
||||
private registerTickCallback(): void {
|
||||
this.removeTickCallback();
|
||||
const render = (clock: any) => {
|
||||
if (!this.viewer.clock.shouldAnimate) return;
|
||||
if (!this.startTime) return;
|
||||
const timeOffset = Math.floor(
|
||||
Cesium.JulianDate.secondsDifference(clock.currentTime, this.startTime)
|
||||
);
|
||||
if (this.onProgress) {
|
||||
this.onProgress(timeOffset);
|
||||
}
|
||||
// 播放结束自动停止
|
||||
if (this.totalDuration > 0 && timeOffset >= this.totalDuration) {
|
||||
this.clear();
|
||||
}
|
||||
};
|
||||
this.viewer.clock.onTick.addEventListener(render);
|
||||
this.tickCallback = render;
|
||||
}
|
||||
|
||||
/** 移除 onTick 回调 */
|
||||
private removeTickCallback(): void {
|
||||
if (this.tickCallback) {
|
||||
this.viewer.clock.onTick.removeEventListener(this.tickCallback);
|
||||
this.tickCallback = null;
|
||||
}
|
||||
}
|
||||
|
||||
/** 解除实体和视角锁定 */
|
||||
private unLockViewAndEntity(): void {
|
||||
this.viewer.trackedEntity = undefined;
|
||||
if (this.viewer.camera) {
|
||||
this.viewer.camera.lookAtTransform(Cesium.Matrix4.IDENTITY);
|
||||
}
|
||||
this.removeFirstPersonModel();
|
||||
}
|
||||
|
||||
/** 清除数据源 */
|
||||
private clearResult(): void {
|
||||
this.viewer.clock.shouldAnimate = false;
|
||||
this.viewer.trackedEntity = undefined;
|
||||
if (this.dataSource) {
|
||||
this.viewer.dataSources.remove(this.dataSource);
|
||||
this.dataSource = null;
|
||||
}
|
||||
this.removeTickCallback();
|
||||
}
|
||||
}
|
||||
544
frontend/src/components/gis/roam/ThreeDRoamPanel.vue
Normal file
544
frontend/src/components/gis/roam/ThreeDRoamPanel.vue
Normal file
@ -0,0 +1,544 @@
|
||||
<template>
|
||||
<div class="three-d-roam-panel">
|
||||
<div class="container-roam">
|
||||
<!-- 漫游路径 -->
|
||||
<div v-if="!isLCJ" class="row">
|
||||
<span>漫游路径:</span>
|
||||
<a-tooltip
|
||||
placement="topLeft"
|
||||
:title="tooltipTitle"
|
||||
:open="tooltipVisible"
|
||||
color="#fff"
|
||||
overlayClassName="ThreeDRoamController_tooltip"
|
||||
>
|
||||
<a-select
|
||||
v-model:value="selectedPath"
|
||||
:options="pathOptions"
|
||||
style="width: 120px"
|
||||
@change="handlePathChange"
|
||||
/>
|
||||
</a-tooltip>
|
||||
<span class="divider" />
|
||||
</div>
|
||||
|
||||
<!-- 漫游操作 -->
|
||||
<div class="row">
|
||||
<span style="margin-left: 12px">漫游操作:</span>
|
||||
<div
|
||||
class="tool-btn"
|
||||
:style="{ pointerEvents: selectedPath ? 'auto' : 'none' }"
|
||||
@click="handlePlayToggle"
|
||||
>
|
||||
<span
|
||||
v-if="isPlaying"
|
||||
class="icon iconfont icon-pause-circle"
|
||||
style="font-size: 20px"
|
||||
title="暂停"
|
||||
/>
|
||||
<span
|
||||
v-else
|
||||
class="icon iconfont icon-play-circle"
|
||||
style="font-size: 20px"
|
||||
title="播放"
|
||||
/>
|
||||
</div>
|
||||
<div class="tool-btn" @click="handleReset">
|
||||
<span
|
||||
class="icon iconfont icon-reset"
|
||||
style="font-size: 20px"
|
||||
title="重置"
|
||||
/>
|
||||
</div>
|
||||
<div class="tool-btn" @click="handleClear">
|
||||
<span
|
||||
class="icon iconfont icon-clearMap"
|
||||
style="font-size: 20px"
|
||||
title="清除"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 倍率 -->
|
||||
<div class="row">
|
||||
<span class="divider" />
|
||||
<span>倍率:</span>
|
||||
<a-slider
|
||||
style="width: 180px"
|
||||
:min="1"
|
||||
:max="10"
|
||||
:step="0.2"
|
||||
v-model:value="speedMultiplier"
|
||||
class="slider"
|
||||
@change="handleSpeedChange"
|
||||
/>
|
||||
<div class="value-display">
|
||||
<span>{{ speedMultiplier }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 显示开关 -->
|
||||
<div class="row">
|
||||
<span class="divider" />
|
||||
<span>飞行路径:</span>
|
||||
<a-switch
|
||||
v-model:checked="showFlightPath"
|
||||
checkedChildren="开启"
|
||||
unCheckedChildren="隐藏"
|
||||
@change="handleFlightPathChange"
|
||||
/>
|
||||
<span>飞行模型:</span>
|
||||
<a-switch
|
||||
v-model:checked="showFlightModel"
|
||||
checkedChildren="开启"
|
||||
unCheckedChildren="隐藏"
|
||||
@change="handleFlightModelChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 进度条 -->
|
||||
<div class="row" style="flex: 1">
|
||||
<span class="divider" />
|
||||
<a-slider
|
||||
class="slider-roam"
|
||||
style="width: 100%"
|
||||
v-model:value="currentProgress"
|
||||
:max="totalDuration"
|
||||
:marks="milestoneMarks"
|
||||
:tipFormatter="tipFormatter"
|
||||
@afterChange="handleProgressChange"
|
||||
/>
|
||||
<div class="value-display">
|
||||
<span>{{ displayTime }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
ref,
|
||||
computed,
|
||||
onMounted,
|
||||
onUnmounted,
|
||||
watch,
|
||||
defineOptions
|
||||
} from 'vue';
|
||||
import type { SelectProps } from 'ant-design-vue';
|
||||
import {
|
||||
threedroambGetKendoListCust,
|
||||
getThreedRoamData
|
||||
} from '@/api/system/disposeManage';
|
||||
import { stcdNameList } from '@/utils/GisUrlList';
|
||||
import { ThreeDRoamManager } from './ThreeDRoamManager';
|
||||
import { MapClass } from '@/components/gis/map.class';
|
||||
import { useUiStore } from '@/store/modules/ui';
|
||||
|
||||
defineOptions({ name: 'ThreeDRoamPanel' });
|
||||
|
||||
// ==================== Props ====================
|
||||
const props = defineProps<{
|
||||
/** 是否LCJ项目(黄河基地) */
|
||||
isLCJ?: boolean;
|
||||
}>();
|
||||
|
||||
// ==================== State ====================
|
||||
const selectedPath = ref<string>('');
|
||||
const pathOptions = ref<SelectProps['options']>([]);
|
||||
const isPlaying = ref(false);
|
||||
|
||||
// 倍率
|
||||
const speedMultiplier = ref(1);
|
||||
|
||||
// 显示开关
|
||||
const showFlightPath = ref(true);
|
||||
const showFlightModel = ref(true);
|
||||
|
||||
// 进度条
|
||||
const currentProgress = ref(0);
|
||||
const totalDuration = ref(0);
|
||||
const milestoneMarks = ref<Record<number, any>>({});
|
||||
|
||||
// 提示
|
||||
const tooltipVisible = ref(false);
|
||||
const tooltipTitle = ref('');
|
||||
|
||||
const uiStore = useUiStore();
|
||||
|
||||
// 当前漫游数据
|
||||
const roamData = ref<any>(null);
|
||||
|
||||
// 退出漫游时恢复的相机状态
|
||||
let savedCamera: any = null;
|
||||
|
||||
let roamManager: ThreeDRoamManager | null = null;
|
||||
|
||||
const displayTime = computed(() => formatTime(currentProgress.value));
|
||||
|
||||
// ==================== Manager ====================
|
||||
|
||||
function getViewer(): any {
|
||||
const mapClass = MapClass.getInstance();
|
||||
return mapClass?.view || null;
|
||||
}
|
||||
|
||||
async function initRoamManager(): Promise<void> {
|
||||
const viewer = getViewer();
|
||||
if (!viewer) {
|
||||
console.warn('Cesium viewer 未就绪');
|
||||
return;
|
||||
}
|
||||
// 进入漫游:保存当前相机状态用于退出时恢复
|
||||
savedCamera = {
|
||||
position: viewer.camera.position.clone(),
|
||||
direction: viewer.camera.direction.clone(),
|
||||
up: viewer.camera.up.clone()
|
||||
};
|
||||
if (!roamManager) {
|
||||
roamManager = new ThreeDRoamManager(viewer);
|
||||
}
|
||||
// 清除旧数据后重新初始化
|
||||
roamManager.clear();
|
||||
if (!roamData.value?.list) return;
|
||||
await roamManager.init(
|
||||
roamData.value.list,
|
||||
roamData.value.totalTime || 0,
|
||||
roamData.value.milestoneList,
|
||||
(seconds: number) => {
|
||||
currentProgress.value = seconds;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// ==================== API Calls ====================
|
||||
|
||||
/** 获取飞行路径列表 */
|
||||
async function fetchFlightPathList() {
|
||||
try {
|
||||
const params = {
|
||||
logic: 'and',
|
||||
filters: [
|
||||
{
|
||||
field: 'isDeleted',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: '0'
|
||||
}
|
||||
],
|
||||
sort: [{ field: 'recordTime', dir: 'asc' }]
|
||||
};
|
||||
const res = await threedroambGetKendoListCust(params);
|
||||
const list = res?.data?.data || [];
|
||||
if (list.length > 0) {
|
||||
pathOptions.value = list.map((item: any) => ({
|
||||
label: item.name,
|
||||
value: item.code,
|
||||
bscd: item.bscd
|
||||
}));
|
||||
// 默认选中第一个
|
||||
if (!selectedPath.value) {
|
||||
selectedPath.value = list[0].code;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取飞行路径列表失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/** 根据选中的路径code获取漫游数据 */
|
||||
async function fetchRoamData(code: string) {
|
||||
if (!code) return;
|
||||
try {
|
||||
const params = {
|
||||
filter: {
|
||||
logic: 'and',
|
||||
filters: [
|
||||
{
|
||||
field: 'code',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: code
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
const res = await getThreedRoamData(params);
|
||||
const data = res || {};
|
||||
roamData.value = data;
|
||||
|
||||
// 设置总时长
|
||||
totalDuration.value = Math.ceil(data.totalTime || 0);
|
||||
|
||||
// 设置里程碑标记
|
||||
if (data.milestoneList?.length > 0) {
|
||||
const marks: Record<number, any> = {};
|
||||
data.milestoneList.forEach(
|
||||
([name, timeStr]: [string, string], i: number) => {
|
||||
const num = Number(timeStr);
|
||||
const matchedItem = stcdNameList.find(
|
||||
(item: any) => item.value === name
|
||||
);
|
||||
const labelName = matchedItem ? matchedItem.key : name;
|
||||
marks[num] = {
|
||||
style: {
|
||||
color: '#FFFFFF',
|
||||
position: 'absolute',
|
||||
top: i % 2 === 0 ? '-30px' : '-2px'
|
||||
},
|
||||
label: labelName
|
||||
};
|
||||
}
|
||||
);
|
||||
milestoneMarks.value = marks;
|
||||
} else {
|
||||
milestoneMarks.value = {};
|
||||
}
|
||||
|
||||
// 数据就绪后初始化漫游管理器
|
||||
initRoamManager();
|
||||
} catch (error) {
|
||||
console.error('获取漫游数据失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Methods ====================
|
||||
function formatTime(seconds: number): string {
|
||||
const s = Math.floor(seconds);
|
||||
const m = Math.floor(s / 60);
|
||||
const sec = s % 60;
|
||||
return `${String(m).padStart(2, '0')}:${String(sec).padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
// 进度条tooltip显示时间
|
||||
const tipFormatter = (value: number) => formatTime(value);
|
||||
|
||||
function handlePathChange(value: string) {
|
||||
selectedPath.value = value;
|
||||
// 切换路径:先清除旧数据(不用等接口返回)
|
||||
if (roamManager) {
|
||||
roamManager.clear();
|
||||
}
|
||||
isPlaying.value = false;
|
||||
currentProgress.value = 0;
|
||||
}
|
||||
|
||||
async function handlePlayToggle() {
|
||||
if (!roamManager) return;
|
||||
if (isPlaying.value) {
|
||||
roamManager.pause();
|
||||
} else {
|
||||
// 如果数据被清除(clear后),重新初始化再开始
|
||||
if (!roamManager.hasEntity) {
|
||||
await initRoamManager();
|
||||
}
|
||||
roamManager.start();
|
||||
}
|
||||
isPlaying.value = !isPlaying.value;
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
if (!roamManager) return;
|
||||
roamManager.reset();
|
||||
isPlaying.value = false;
|
||||
currentProgress.value = 0;
|
||||
// 重新初始化
|
||||
initRoamManager();
|
||||
}
|
||||
|
||||
function handleClear() {
|
||||
if (!roamManager) return;
|
||||
roamManager.clear();
|
||||
isPlaying.value = false;
|
||||
}
|
||||
|
||||
function handleSpeedChange(value: number) {
|
||||
if (roamManager) {
|
||||
roamManager.setSpeed(value);
|
||||
}
|
||||
}
|
||||
|
||||
function handleFlightPathChange(value: boolean) {
|
||||
if (roamManager) {
|
||||
roamManager.setFlightPathVisible(value);
|
||||
}
|
||||
}
|
||||
|
||||
function handleFlightModelChange(value: boolean) {
|
||||
if (roamManager) {
|
||||
roamManager.setFlightModelVisible(value);
|
||||
}
|
||||
}
|
||||
|
||||
function handleProgressChange(value: number) {
|
||||
if (roamManager) {
|
||||
roamManager.jumpToTime(value);
|
||||
roamManager.flyToPositionAtTime(value);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Lifecycle ====================
|
||||
onMounted(() => {
|
||||
fetchFlightPathList();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
// dim 切换时跳过 Cesium 清理(由地图切换自行处理),避免影响 2D 地图初始化
|
||||
const skipCesiumCleanup = !!uiStore.skipRoamCameraRestore;
|
||||
roamManager?.destroy(skipCesiumCleanup);
|
||||
roamManager = null;
|
||||
// 恢复相机到进入漫游前的位置(dim 切换时跳过,由地图模式切换自行处理)
|
||||
if (savedCamera && !uiStore.skipRoamCameraRestore) {
|
||||
const viewer = getViewer();
|
||||
if (viewer) {
|
||||
viewer.camera.flyTo({
|
||||
destination: savedCamera.position,
|
||||
orientation: {
|
||||
direction: savedCamera.direction,
|
||||
up: savedCamera.up
|
||||
},
|
||||
duration: 1.5
|
||||
});
|
||||
}
|
||||
}
|
||||
savedCamera = null;
|
||||
uiStore.skipRoamCameraRestore = false;
|
||||
});
|
||||
|
||||
// 选中路径变化时加载数据
|
||||
watch(
|
||||
selectedPath,
|
||||
newVal => {
|
||||
if (newVal) {
|
||||
fetchRoamData(newVal);
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
// ==================== Expose ====================
|
||||
defineExpose({
|
||||
selectedPath,
|
||||
pathOptions,
|
||||
isPlaying,
|
||||
speedMultiplier,
|
||||
currentProgress,
|
||||
totalDuration,
|
||||
milestoneMarks,
|
||||
displayTime,
|
||||
roamData
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.three-d-roam-panel {
|
||||
width: 100%;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.container-roam {
|
||||
width: 95%;
|
||||
height: 55px;
|
||||
position: absolute;
|
||||
z-index: 999;
|
||||
bottom: 20px;
|
||||
right: 40px;
|
||||
left: 25px;
|
||||
border: 2px solid #007fcc;
|
||||
background: rgba(12, 51, 88, 0.65);
|
||||
display: flex;
|
||||
padding: 8px 24px;
|
||||
color: white;
|
||||
:deep(.ant-select-selector) {
|
||||
color: white;
|
||||
background: transparent !important;
|
||||
border-color: rgba(0, 127, 204, 0.65);
|
||||
}
|
||||
.slider {
|
||||
:deep(.ant-slider-rail) {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
:deep(.ant-slider-handle::after) {
|
||||
background: rgb(0, 230, 252);
|
||||
}
|
||||
}
|
||||
|
||||
.slider-roam {
|
||||
margin-bottom: 10px !important;
|
||||
|
||||
&:hover {
|
||||
:deep(.ant-slider-rail) {
|
||||
background-color: rgba(255, 255, 255, 0.15) !important;
|
||||
}
|
||||
/* 刻度点 */
|
||||
:deep(.ant-slider-dot) {
|
||||
border-color: #e9e9e9;
|
||||
}
|
||||
}
|
||||
:deep(.ant-slider-mark-text) {
|
||||
margin-left: 4px;
|
||||
}
|
||||
:deep(.ant-slider-handle-click-focused) {
|
||||
border-color: #1890ff !important;
|
||||
box-shadow: none !important;
|
||||
// transform: scale(1) !important;
|
||||
outline: none !important;
|
||||
}
|
||||
:deep(.ant-slider-handle:focus),
|
||||
:deep(.ant-slider-handle:focus-visible),
|
||||
:deep(.ant-slider-handle:focus-within) {
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
:deep(.ant-slider-track) {
|
||||
background-color: rgb(6, 230, 252) !important;
|
||||
}
|
||||
:deep(.ant-slider-with-marks) {
|
||||
margin-bottom: 10px !important;
|
||||
}
|
||||
:deep(.ant-slider-rail) {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
:deep(.ant-slider-handle::after) {
|
||||
background: rgb(0, 230, 252);
|
||||
}
|
||||
:deep(.ant-slider-dot) {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
:deep(.ant-slider-dot-active) {
|
||||
border-color: #ffffff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.divider {
|
||||
height: 44px;
|
||||
width: 1px;
|
||||
background: rgba(0, 127, 204, 0.4);
|
||||
margin: 0 12px;
|
||||
}
|
||||
|
||||
.tool-btn {
|
||||
background: #2f6b98;
|
||||
border-radius: 50%;
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.value-display {
|
||||
width: 90px;
|
||||
padding: 4px 8px;
|
||||
background-color: rgba(12, 51, 88, 0.45);
|
||||
border: 1px solid rgba(0, 127, 204, 0.65);
|
||||
color: rgba(255, 255, 255, 0.35);
|
||||
}
|
||||
</style>
|
||||
50
frontend/src/components/gis/roam/data/riverline.ts
Normal file
50
frontend/src/components/gis/roam/data/riverline.ts
Normal file
@ -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: []
|
||||
}
|
||||
}
|
||||
]
|
||||
@ -43,7 +43,7 @@
|
||||
<i
|
||||
class="icon iconfont"
|
||||
:class="
|
||||
child.key === 'threedRoam' && activeKey === 'threedRoam'
|
||||
child.key === 'threedRoam' && uiStore.isRoaming
|
||||
? 'icon-closeCircle'
|
||||
: 'icon-' + child.icon
|
||||
"
|
||||
@ -396,6 +396,11 @@ const handleControllerClick = (item: any) => {
|
||||
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;
|
||||
// 卸载所有倾斜摄影
|
||||
|
||||
@ -647,7 +647,7 @@ const focusSelectedAnchorPoint = () => {
|
||||
|
||||
mapOrchestrator.focusPoint(
|
||||
selectedValue,
|
||||
uiStore.mapType === '3D' ? 9 : 14,
|
||||
uiStore.mapType === '3D' ? 8 : 14,
|
||||
isFishSurveyMode.value ? anchorPointOptions.value : undefined
|
||||
);
|
||||
};
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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<any>> = [];
|
||||
const uncheckedTasks: Array<() => Promise<any>> = [];
|
||||
const processedKeys = new Set<string>();
|
||||
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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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) {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user