效果图1:

效果图2:

cesium和leaflet用的都是同一个影像图层,中间的红色分屏线可以左右滑动,但是目前只能是左边的cesium图层控制右边的leaflet图层,反之不行(还未做该功能),如果分屏线离中间越远,则分屏线两边相差越大,如下图所示:

实现原理:左右两边其实都是100%宽度,通过css的clip-path属性设置了各自的显示范围,通过cesium的屏幕中心点设置leaflet的地图中心点,从而实现联动,但是由于leaflet和cesium的缩放层级无法控制一致,zoom的层级定义不一样,所以这个层级是通过微调实现大体一致,缩放过曾中辉看到由于层级的不一样导致左右两边相差很大,如下图所示:

<template><div class="el-main"><!-- Cesium 容器 --><div id="cesiumContainer"></div><!-- 卷帘滑块 --><div id="slider"></div><!-- Leaflet 地图容器 --><div id="leafletContainer"></div></div>
</template><script setup>
import { onMounted, ref } from "vue";
import * as Cesium from "cesium";
import L from "leaflet"; // 引入 Leaflet// 设置 Cesium Ion 的默认访问令牌
Cesium.Ion.defaultAccessToken = "youKey";onMounted(() => {init();
});
let leafletUpdating = ref(false); // 标志位,防止循环触发
let leafletMap;
const init = () => {const viewer = initializeCesiumViewer();const leafletMap = initializeLeafletMap();const slider = document.getElementById("slider");// 监听cesium相机变化事件viewer.camera.percentageChanged = 0.00001;viewer.camera.changed.addEventListener(() => {leafletUpdating.value = false;const centerPosition = getCenterPosition(viewer);if (centerPosition) {const zoomLevel = calculateZoomLevel(viewer.camera.positionCartographic.height);leafletMap.setView([centerPosition.lat, centerPosition.lng],zoomLevel + 4);leafletUpdating.value = true;}});// 监听leaflet相机变化事件let splitPosition = 0.5; // 初始分屏位置为 50%updateSplitPosition(splitPosition);const handler = new Cesium.ScreenSpaceEventHandler(slider);let moveActive = false;// 滑块移动逻辑const move = (movement) => {// 如果没有激活移动状态,则直接返回if (!moveActive) return;// 获取滑块父元素的宽度const parentWidth = slider.parentElement.offsetWidth;// 计算新的分割位置,确保其在0到1之间const newSplitPosition = Math.min(// slider.offsetLeft 获取滑块当前的左边距(以像素为单位)。// movement.endPosition.x 获取鼠标或触摸事件的移动距离(以像素为单位)。Math.max((slider.offsetLeft + movement.endPosition.x) / parentWidth, 0),1);// 更新分割位置updateSplitPosition(newSplitPosition);};// 监听鼠标按下事件(开始拖动)handler.setInputAction(() => {moveActive = true;}, Cesium.ScreenSpaceEventType.LEFT_DOWN);// 监听鼠标移动事件(拖动过程中更新分屏位置)handler.setInputAction(move, Cesium.ScreenSpaceEventType.MOUSE_MOVE);// 监听鼠标松开事件(结束拖动)handler.setInputAction(() => {moveActive = false;}, Cesium.ScreenSpaceEventType.LEFT_UP);// 监听 Leaflet 的 zoomend 事件leafletMap.on("zoomend", (e) => {const center = leafletMap.getCenter();const scale = e.target.getZoom();console.log("leaflet坐标和层级:", center, scale);});// 将 viewer 挂载到全局 window 对象上,便于调试window.viewer = viewer;
};/*** 初始化 Cesium Viewer 实例* @returns {Cesium.Viewer} Cesium Viewer 实例*/
const initializeCesiumViewer = () => {const viewer = new Cesium.Viewer("cesiumContainer", {infoBox: false,geocoder: false,timeline: false,animation: false,});viewer.camera.setView({destination: Cesium.Cartesian3.fromDegrees(116.38225078582765,39.90710270565395,4500),orientation: {heading: 6.283185307179581,pitch: -1.5688168484696687,roll: 0.0,},});var layer = new Cesium.UrlTemplateImageryProvider({url: "http://webrd02.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=1&style=8&x={x}&y={y}&z={z}",minimumLevel: 4,maximumLevel: 18,});viewer.imageryLayers.addImageryProvider(layer);viewer.camera.constrainedAxis = Cesium.Cartesian3.UNIT_Z; // 约束相机只能垂直缩放viewer.scene.screenSpaceCameraController.zoomFactor = 0.1; // 设置缩放速率return viewer;
};/*** 初始化 Leaflet 地图* @returns {L.Map} Leaflet 地图实例*/
const initializeLeafletMap = () => {leafletMap = L.map("leafletContainer").setView([39.90710270565395, 116.38225078582765],15);const layer = L.tileLayer("http://webrd0{s}.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=1&style=8&x={x}&y={y}&z={z}",{subdomains: ["1", "2", "3", "4"],minZoom: 1,maxZoom: 19,});leafletMap.addLayer(layer);return leafletMap;
};/*** 更新分屏位置* @param {number} position - 分屏位置(0 到 1 之间的值)*/
const updateSplitPosition = (position) => {const slider = document.getElementById("slider");slider.style.left = `${100 * position}%`;document.getElementById("leafletContainer").style.width = `${100 * (1 - position)}%`;document.getElementById("leafletContainer").style.clipPath = `inset(0 0 0 ${100 * position}%)`;document.getElementById("cesiumContainer").style.width = `${100 * position}%`;document.getElementById("cesiumContainer").style.clipPath = `inset(0 ${100 * (1 - position)}% 0 0)`;
};/*** 获取屏幕中心点的地理坐标* @param {Cesium.Viewer} viewer - Cesium Viewer 实例* @returns {Object|null} 包含经度和纬度的对象,或 null*/
const getCenterPosition = (viewer) => {const centerResult = viewer.camera.pickEllipsoid(new Cesium.Cartesian2(viewer.canvas.clientWidth / 2,viewer.canvas.clientHeight / 2));if (centerResult) {const cartographic =Cesium.Ellipsoid.WGS84.cartesianToCartographic(centerResult);return {lng: Cesium.Math.toDegrees(cartographic.longitude),lat: Cesium.Math.toDegrees(cartographic.latitude),};}return null;
};/*** 计算近似的缩放级别* @param {number} height - 相机高度(米)* @returns {number} 缩放级别*/
const calculateZoomLevel = (height) => {const earthRadius = 6378137; // 地球半径(米)return Math.round(Math.log2((2 * earthRadius) / height));
};
// 将Leaflet的缩放级别转换为Cesium的相机高度
const calculateHeightFromZoom = (zoomLevel) => {const earthRadius = 6378137; // 地球半径(米)return (2 * earthRadius) / Math.pow(2, zoomLevel);
};
</script><style scoped>
/* 卷帘滑块样式 */
#slider {position: absolute;left: 50%;top: 0;background-color: red;width: 3px;height: 100%;cursor: ew-resize;z-index: 10;
}/* Leaflet 地图容器样式 */
#leafletContainer {position: absolute;top: 0;bottom: 0;right: 0;width: 100% !important;clip-path: inset(0 0 0 50%);
}/* Cesium 地图容器样式 */
#cesiumContainer {position: absolute;top: 0;bottom: 0;left: 0;/* width: 50%; */z-index: 10;width: 100% !important;clip-path: inset(0 50% 0 0);
}/* 主内容区域样式 */
.el-main {position: relative;width: 100%;height: 100%;
}
</style>