3d文字碰撞修改,添加漫游功能,3d整体优化
This commit is contained in:
parent
2f0bc9f67d
commit
f1f232cb2d
@ -7,7 +7,7 @@ VITE_APP_TITLE = '水电水利建设项目全过程环境管理信息平台'
|
|||||||
VITE_APP_PORT = 3000
|
VITE_APP_PORT = 3000
|
||||||
VITE_APP_BASE_API = '/dev-api'
|
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'
|
# 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
|
data: data
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
export function getThreedRoamData(data: any) {
|
||||||
|
return request({
|
||||||
|
url: '/threedroamb/getThreedRoamData',
|
||||||
|
method: 'post',
|
||||||
|
data: data
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function threedroambsave(data: any) {
|
export function threedroambsave(data: any) {
|
||||||
return request({
|
return request({
|
||||||
url: '/threedroamb/save',
|
url: '/threedroamb/save',
|
||||||
|
|||||||
@ -18,6 +18,9 @@
|
|||||||
<BaseLayerSwitcher :map="mapClass" />
|
<BaseLayerSwitcher :map="mapClass" />
|
||||||
<!-- 梯级弹框 -->
|
<!-- 梯级弹框 -->
|
||||||
<TjLayerModal v-model:open="tjModalVisible" />
|
<TjLayerModal v-model:open="tjModalVisible" />
|
||||||
|
|
||||||
|
<!-- 三维漫游面板 -->
|
||||||
|
<ThreeDRoamPanel v-if="uiStore.isRoaming" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
@ -27,6 +30,7 @@ import MapFilter from '@/components/mapFilter/index.vue';
|
|||||||
import MapController from '@/components/mapController/index.vue';
|
import MapController from '@/components/mapController/index.vue';
|
||||||
import BaseLayerSwitcher from '@/components/BaseLayerSwitcher/index.vue';
|
import BaseLayerSwitcher from '@/components/BaseLayerSwitcher/index.vue';
|
||||||
import TjLayerModal from './TjLayerModal.vue';
|
import TjLayerModal from './TjLayerModal.vue';
|
||||||
|
import ThreeDRoamPanel from './roam/ThreeDRoamPanel.vue';
|
||||||
import { useRoute } from 'vue-router';
|
import { useRoute } from 'vue-router';
|
||||||
import { useMapOrchestrator } from '@/modules/map/application/map-orchestrator';
|
import { useMapOrchestrator } from '@/modules/map/application/map-orchestrator';
|
||||||
import { MapClass } from './map.class';
|
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);
|
this.service.flyTopanto(position, zoom);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 飞入默认视角(仅 3D 生效):初始地球视图后,图层加载完再飞入中国 */
|
||||||
|
async flyToDefaultView(): Promise<void> {
|
||||||
|
await this.service.flyToDefaultView();
|
||||||
|
}
|
||||||
|
|
||||||
getCurrentZoom(): number | undefined {
|
getCurrentZoom(): number | undefined {
|
||||||
return this.service.getCurrentZoom();
|
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;
|
initPopupOverlay(popupContainer: HTMLDivElement): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 飞入默认视角(仅 3D 有效),在图层加载完成后调用
|
||||||
|
*/
|
||||||
|
flyToDefaultView(): Promise<void>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 初始化加载基础图层
|
* 初始化加载基础图层
|
||||||
* @param layer
|
* @param layer
|
||||||
|
|||||||
@ -2499,6 +2499,11 @@ export class MapOl implements MapInterface {
|
|||||||
return this.view?.getZoom();
|
return this.view?.getZoom();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 3D 专用飞入默认视角,2D 下无需操作 */
|
||||||
|
async flyToDefaultView(): Promise<void> {
|
||||||
|
// 2D 无动画,空实现
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 生成带完整圆边框的上半圆 Canvas
|
* 生成带完整圆边框的上半圆 Canvas
|
||||||
* @param radius 半径(像素)
|
* @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
|
<i
|
||||||
class="icon iconfont"
|
class="icon iconfont"
|
||||||
:class="
|
:class="
|
||||||
child.key === 'threedRoam' && activeKey === 'threedRoam'
|
child.key === 'threedRoam' && uiStore.isRoaming
|
||||||
? 'icon-closeCircle'
|
? 'icon-closeCircle'
|
||||||
: 'icon-' + child.icon
|
: 'icon-' + child.icon
|
||||||
"
|
"
|
||||||
@ -396,6 +396,11 @@ const handleControllerClick = (item: any) => {
|
|||||||
case 'dim': // 切换3D/2D
|
case 'dim': // 切换3D/2D
|
||||||
{
|
{
|
||||||
const switchingTo3D = uiStore.mapType === '2D';
|
const switchingTo3D = uiStore.mapType === '2D';
|
||||||
|
// 先关闭漫游(如果有),标记跳过相机还原(由地图切换自行处理)
|
||||||
|
if (uiStore.isRoaming) {
|
||||||
|
uiStore.skipRoamCameraRestore = true;
|
||||||
|
uiStore.isRoaming = false;
|
||||||
|
}
|
||||||
uiStore.mapType = switchingTo3D ? '3D' : '2D';
|
uiStore.mapType = switchingTo3D ? '3D' : '2D';
|
||||||
if (switchingTo3D) {
|
if (switchingTo3D) {
|
||||||
// 2D→3D:关闭抽屉,请求倾斜摄影数据,开始过渡
|
// 2D→3D:关闭抽屉,请求倾斜摄影数据,开始过渡
|
||||||
@ -405,7 +410,6 @@ const handleControllerClick = (item: any) => {
|
|||||||
} else {
|
} else {
|
||||||
// 3D→2D:打开抽屉,清理过渡,卸载所有倾斜摄影模型
|
// 3D→2D:打开抽屉,清理过渡,卸载所有倾斜摄影模型
|
||||||
uiStore.drawerOpen = true;
|
uiStore.drawerOpen = true;
|
||||||
uiStore.isRoaming = false;
|
|
||||||
cleanup3DTransition();
|
cleanup3DTransition();
|
||||||
activeKey.value = null;
|
activeKey.value = null;
|
||||||
// 卸载所有倾斜摄影
|
// 卸载所有倾斜摄影
|
||||||
|
|||||||
@ -647,7 +647,7 @@ const focusSelectedAnchorPoint = () => {
|
|||||||
|
|
||||||
mapOrchestrator.focusPoint(
|
mapOrchestrator.focusPoint(
|
||||||
selectedValue,
|
selectedValue,
|
||||||
uiStore.mapType === '3D' ? 9 : 14,
|
uiStore.mapType === '3D' ? 8 : 14,
|
||||||
isFishSurveyMode.value ? anchorPointOptions.value : undefined
|
isFishSurveyMode.value ? anchorPointOptions.value : undefined
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -227,20 +227,18 @@ export const useMapOrchestrator = () => {
|
|||||||
);
|
);
|
||||||
await mapStore.updateLayerData(checkedKeys, true);
|
await mapStore.updateLayerData(checkedKeys, true);
|
||||||
mapStore.setSelectedLegendData();
|
mapStore.setSelectedLegendData();
|
||||||
const backgroundLoadPromise = isInitialLoad
|
if (isInitialLoad) {
|
||||||
? mapStore.loadAllLayerData(layerConfig, checkedKeys, {
|
mapStore.loadAllLayerData(layerConfig, checkedKeys, {
|
||||||
pageToken: activePageToken,
|
pageToken: activePageToken,
|
||||||
skipSessionCheck: true,
|
skipSessionCheck: true,
|
||||||
pageKey
|
pageKey
|
||||||
})
|
});
|
||||||
: mapStore.loadCurrentPageLayerData(layerConfig, checkedKeys, {
|
} else {
|
||||||
pageToken: activePageToken,
|
mapStore.loadCurrentPageLayerData(layerConfig, checkedKeys, {
|
||||||
skipSessionCheck: true
|
pageToken: activePageToken,
|
||||||
});
|
skipSessionCheck: true
|
||||||
|
});
|
||||||
void backgroundLoadPromise.catch(error => {
|
}
|
||||||
console.error(`页面锚点后台加载失败 [${pageKey}]`, error);
|
|
||||||
});
|
|
||||||
backgroundLoadStarted = true;
|
backgroundLoadStarted = true;
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
@ -297,6 +295,10 @@ export const useMapOrchestrator = () => {
|
|||||||
const checkedKeys = mapViewStore.getCheckedLayerKeys();
|
const checkedKeys = mapViewStore.getCheckedLayerKeys();
|
||||||
await mapStore.updateLayerData(checkedKeys, true);
|
await mapStore.updateLayerData(checkedKeys, true);
|
||||||
mapStore.setSelectedLegendData();
|
mapStore.setSelectedLegendData();
|
||||||
|
// 切换到 3D 时:先恢复图层,再停留 + 飞入中国视角(与 mountView 一致)
|
||||||
|
if (mapClass.flyToDefaultView) {
|
||||||
|
await mapClass.flyToDefaultView();
|
||||||
|
}
|
||||||
await syncZoomSensitiveState({
|
await syncZoomSensitiveState({
|
||||||
isHydroMenu: options.getIsHydroMenu(),
|
isHydroMenu: options.getIsHydroMenu(),
|
||||||
refreshEngPoint: true
|
refreshEngPoint: true
|
||||||
@ -419,6 +421,10 @@ export const useMapOrchestrator = () => {
|
|||||||
bindBaseSelection();
|
bindBaseSelection();
|
||||||
bindZoomListener(options.getIsHydroMenu);
|
bindZoomListener(options.getIsHydroMenu);
|
||||||
await loadPage({ pageKey: options.pageKey, isInitialLoad: true });
|
await loadPage({ pageKey: options.pageKey, isInitialLoad: true });
|
||||||
|
// 加载完边界线等动态图层后,再从地球视图飞入中国视角
|
||||||
|
if (mapClass.flyToDefaultView) {
|
||||||
|
await mapClass.flyToDefaultView();
|
||||||
|
}
|
||||||
await syncZoomSensitiveState({
|
await syncZoomSensitiveState({
|
||||||
isHydroMenu: options.getIsHydroMenu(),
|
isHydroMenu: options.getIsHydroMenu(),
|
||||||
refreshEngPoint: true
|
refreshEngPoint: true
|
||||||
@ -622,6 +628,8 @@ export const useMapOrchestrator = () => {
|
|||||||
|
|
||||||
const previousPageKey = mapConfigStore.lastLoadOptions?.pageKey || '';
|
const previousPageKey = mapConfigStore.lastLoadOptions?.pageKey || '';
|
||||||
if (previousPageKey && previousPageKey !== pageKey) {
|
if (previousPageKey && previousPageKey !== pageKey) {
|
||||||
|
// 取消旧页面正在请求的图层数据,释放 HTTP 连接
|
||||||
|
mapStore.abortAllInFlightRequests();
|
||||||
hideCurrentVisibleLayers();
|
hideCurrentVisibleLayers();
|
||||||
mapViewStore.setCheckedLayerKeys([]);
|
mapViewStore.setCheckedLayerKeys([]);
|
||||||
mapStore.setSelectedLegendData();
|
mapStore.setSelectedLegendData();
|
||||||
|
|||||||
@ -808,7 +808,7 @@ export const useMapStore = defineStore('map', () => {
|
|||||||
* @param items 图层数据
|
* @param items 图层数据
|
||||||
* @param checkedKeys 选中的图层 keys(用于优先加载)
|
* @param checkedKeys 选中的图层 keys(用于优先加载)
|
||||||
*/
|
*/
|
||||||
const loadAllLayerData = async (
|
const loadAllLayerData = (
|
||||||
items: any[],
|
items: any[],
|
||||||
checkedKeys: string[] = [],
|
checkedKeys: string[] = [],
|
||||||
options: {
|
options: {
|
||||||
@ -824,7 +824,6 @@ export const useMapStore = defineStore('map', () => {
|
|||||||
const checkedTasks: Array<() => Promise<any>> = [];
|
const checkedTasks: Array<() => Promise<any>> = [];
|
||||||
const uncheckedTasks: Array<() => Promise<any>> = [];
|
const uncheckedTasks: Array<() => Promise<any>> = [];
|
||||||
const processedKeys = new Set<string>();
|
const processedKeys = new Set<string>();
|
||||||
const debugStart = Date.now();
|
|
||||||
|
|
||||||
const processItems = (itemList: any[]) => {
|
const processItems = (itemList: any[]) => {
|
||||||
itemList.forEach(item => {
|
itemList.forEach(item => {
|
||||||
@ -857,97 +856,101 @@ export const useMapStore = defineStore('map', () => {
|
|||||||
|
|
||||||
processItems(items);
|
processItems(items);
|
||||||
|
|
||||||
try {
|
const allTasks = [...checkedTasks, ...uncheckedTasks];
|
||||||
// 首页初始化:所有图层接口一次性并发下发,避免分批串行等待。
|
|
||||||
const loadResults = await Promise.allSettled(
|
|
||||||
[...checkedTasks, ...uncheckedTasks].map(task => task())
|
|
||||||
);
|
|
||||||
const failedResults = loadResults.filter(
|
|
||||||
result => result.status === 'rejected'
|
|
||||||
);
|
|
||||||
|
|
||||||
if (failedResults.length > 0) {
|
// 方案三:逐个处理 —— 内部不 await 批量完成,每个请求独立释放 HTTP 连接
|
||||||
console.warn(
|
void (async () => {
|
||||||
'部分图层数据加载失败,但不会阻断其他图层:',
|
try {
|
||||||
failedResults
|
const loadResults = await Promise.allSettled(
|
||||||
|
allTasks.map(task => task())
|
||||||
|
);
|
||||||
|
const failedResults = loadResults.filter(
|
||||||
|
result => result.status === 'rejected'
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
if (failedResults.length > 0) {
|
||||||
(typeof currentSessionId === 'number' &&
|
console.warn(
|
||||||
!isCurrentLoadSession(currentSessionId)) ||
|
'部分图层数据加载失败,但不会阻断其他图层:',
|
||||||
!isActivePageToken(options.pageToken)
|
failedResults
|
||||||
) {
|
);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const layerPoints = getPointLayerData(key);
|
if (
|
||||||
const displayData = filterPointLayerDataForDisplay(key, layerPoints);
|
(typeof currentSessionId === 'number' &&
|
||||||
const cacheChecked = mapDataStore.getPointLayerCache(key)?.checked;
|
!isCurrentLoadSession(currentSessionId)) ||
|
||||||
const shouldRestoreVisible =
|
!isActivePageToken(options.pageToken)
|
||||||
typeof cacheChecked === 'boolean'
|
) {
|
||||||
? cacheChecked
|
return;
|
||||||
: layer.checked === 1 || finalCheckedKeys.includes(key);
|
|
||||||
layer.data = layerPoints;
|
|
||||||
mapClass.addInitDataLayer(displayData, key);
|
|
||||||
if (shouldRestoreVisible) {
|
|
||||||
restoreLayerLegendVisibility(key);
|
|
||||||
}
|
}
|
||||||
mapClass.mdLayerTreeShowOrHidden(key, shouldRestoreVisible);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设置选中的图例数据(确保图例在所有锚点加载完成后才显示)
|
if (
|
||||||
setSelectedLegendData();
|
options.pageKey &&
|
||||||
|
hasPendingPageNavigationAwayFrom(options.pageKey)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// 备注:初始化 loading 期间用户可能已经手动改过图层勾选,这里必须以最新运行态收尾,
|
// 确保所有图层的数据都已经缓存到 pointDataCache 和 layer.data
|
||||||
// 不能再回放 load 启动瞬间的默认 checked 快照。
|
const allLayerKeys = getAllLayerKeys(items);
|
||||||
await updateLayerData(finalCheckedKeys, true);
|
for (const key of allLayerKeys) {
|
||||||
} finally {
|
const layer = findLayerByKey(items, key);
|
||||||
const shouldFinishLoading =
|
if (layer && getEffectivePointLayerData(key).length > 0) {
|
||||||
(typeof currentSessionId !== 'number' ||
|
syncBackgroundCacheToReactiveStore(key);
|
||||||
isCurrentLoadSession(currentSessionId)) &&
|
layer.data = getEffectivePointLayerData(key);
|
||||||
(options.pageToken === undefined ||
|
}
|
||||||
activePageRenderToken === options.pageToken);
|
}
|
||||||
if (shouldFinishLoading) {
|
|
||||||
finishPageLoading(options.pageToken);
|
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[],
|
items: any[],
|
||||||
checkedKeys: string[] = [],
|
checkedKeys: string[] = [],
|
||||||
options: {
|
options: {
|
||||||
@ -993,14 +996,12 @@ export const useMapStore = defineStore('map', () => {
|
|||||||
|
|
||||||
beginPageLoading(options.pageToken);
|
beginPageLoading(options.pageToken);
|
||||||
|
|
||||||
try {
|
// 方案三:逐个处理 —— 不 await 批量完成,每个请求独立释放 HTTP 连接
|
||||||
// 菜单切换:仅当前页的勾选图层一次性并发下发,旧请求在后台继续运行。
|
void Promise.allSettled(tasks.map(task => task())).then(() => {
|
||||||
await Promise.allSettled(tasks.map(task => task()));
|
|
||||||
} finally {
|
|
||||||
if (isActivePageToken(options.pageToken)) {
|
if (isActivePageToken(options.pageToken)) {
|
||||||
finishPageLoading(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,
|
loadCurrentPageLayerData,
|
||||||
loadLayerData,
|
loadLayerData,
|
||||||
setSelectedLegendData,
|
setSelectedLegendData,
|
||||||
|
abortAllInFlightRequests,
|
||||||
legendData,
|
legendData,
|
||||||
setLegendData,
|
setLegendData,
|
||||||
legendDataSelected,
|
legendDataSelected,
|
||||||
|
|||||||
@ -8,6 +8,8 @@ export const useUiStore = defineStore('ui', () => {
|
|||||||
const mapSwitchCompletedTick = ref(0);
|
const mapSwitchCompletedTick = ref(0);
|
||||||
// 3d 流域三维漫游
|
// 3d 流域三维漫游
|
||||||
const isRoaming = ref(false);
|
const isRoaming = ref(false);
|
||||||
|
// dim 切换时跳过漫游相机还原(由地图模式切换自行处理)
|
||||||
|
const skipRoamCameraRestore = ref(false);
|
||||||
|
|
||||||
// 切换抽屉状态
|
// 切换抽屉状态
|
||||||
const toggleDrawer = () => {
|
const toggleDrawer = () => {
|
||||||
@ -26,6 +28,7 @@ export const useUiStore = defineStore('ui', () => {
|
|||||||
return {
|
return {
|
||||||
drawerOpen,
|
drawerOpen,
|
||||||
isRoaming,
|
isRoaming,
|
||||||
|
skipRoamCameraRestore,
|
||||||
toggleDrawer,
|
toggleDrawer,
|
||||||
setDrawerOpen,
|
setDrawerOpen,
|
||||||
mapType,
|
mapType,
|
||||||
|
|||||||
@ -56,10 +56,9 @@ service.interceptors.response.use(
|
|||||||
if (status === 200) {
|
if (status === 200) {
|
||||||
if (response.data.code == 401) {
|
if (response.data.code == 401) {
|
||||||
message.error(response.data.msg || '请求失败');
|
message.error(response.data.msg || '请求失败');
|
||||||
setTimeout(() => {
|
localStorage.clear();
|
||||||
localStorage.clear();
|
useUserStoreHook().resetToken();
|
||||||
window.location.href = '/login';
|
router.push('/login');
|
||||||
}, 1000);
|
|
||||||
return;
|
return;
|
||||||
} else if (response.data.code == 1) {
|
} else if (response.data.code == 1) {
|
||||||
message.error(response.data.msg);
|
message.error(response.data.msg);
|
||||||
|
|||||||
@ -148,7 +148,7 @@ const handleDelete = (record: any) => {
|
|||||||
zIndex: 2002,
|
zIndex: 2002,
|
||||||
onOk: async () => {
|
onOk: async () => {
|
||||||
try {
|
try {
|
||||||
let res = await deleteTiltPhoto({ id: record.id });
|
let res = await deleteTiltPhoto({ stcd: record.stcd });
|
||||||
message.success('删除成功');
|
message.success('删除成功');
|
||||||
basicTable.value.refresh();
|
basicTable.value.refresh();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user