3d文字碰撞修改,添加漫游功能,3d整体优化

This commit is contained in:
扈兆增 2026-07-22 16:23:10 +08:00
parent 2f0bc9f67d
commit f1f232cb2d
18 changed files with 2066 additions and 441 deletions

View File

@ -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'
# 汤伟

Binary file not shown.

View File

@ -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',

View File

@ -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

View File

@ -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();
}

View File

@ -78,6 +78,11 @@ export interface MapInterface {
*/
initPopupOverlay(popupContainer: HTMLDivElement): void;
/**
* 3D
*/
flyToDefaultView(): Promise<void>;
/**
*
* @param layer

View File

@ -2499,6 +2499,11 @@ export class MapOl implements MapInterface {
return this.view?.getZoom();
}
/** 3D 专用飞入默认视角2D 下无需操作 */
async flyToDefaultView(): Promise<void> {
// 2D 无动画,空实现
}
/**
* Canvas
* @param radius

View 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();
}
}

View 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>

View 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: []
}
}
]

View File

@ -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) {
// 2D3D
@ -405,7 +410,6 @@ const handleControllerClick = (item: any) => {
} else {
// 3D2D
uiStore.drawerOpen = true;
uiStore.isRoaming = false;
cleanup3DTransition();
activeKey.value = null;
//

View File

@ -647,7 +647,7 @@ const focusSelectedAnchorPoint = () => {
mapOrchestrator.focusPoint(
selectedValue,
uiStore.mapType === '3D' ? 9 : 14,
uiStore.mapType === '3D' ? 8 : 14,
isFishSurveyMode.value ? anchorPointOptions.value : undefined
);
};

View File

@ -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();

View File

@ -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,

View File

@ -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,

View File

@ -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);

View File

@ -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) {