CesiumTransition.vue 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528
  1. <template>
  2. <div class="cesium-transition-wrapper">
  3. <div ref="cesiumContainer" class="cesium-container"></div>
  4. <div class="ui-layer">
  5. <div
  6. v-if="poi"
  7. ref="poiLabel"
  8. class="html-label"
  9. :style="{ color: poi.fontColor || '#00ffff', fontSize: (poi.fontSize || 20) + 'px', opacity: 0, display: 'none' }"
  10. >
  11. {{ poi.label }}
  12. </div>
  13. </div>
  14. </div>
  15. </template>
  16. <script>
  17. import CesiumPreloader from '@/utils/cesiumPreloader';
  18. const Cesium = window.Cesium;
  19. function createOuterRingTexture(color) {
  20. const size = 1024, cx = size / 2, cy = size / 2, radius = size / 2 - 20;
  21. const lineWidth = 16, baseColorStr = color.toCssColorString();
  22. const canvas = document.createElement('canvas'); canvas.width = size; canvas.height = size; const ctx = canvas.getContext('2d');
  23. const solidCanvas = document.createElement('canvas'); solidCanvas.width = size; solidCanvas.height = size; const solidCtx = solidCanvas.getContext('2d'); solidCtx.lineWidth = lineWidth; solidCtx.strokeStyle = baseColorStr; solidCtx.shadowBlur = 10; solidCtx.shadowColor = baseColorStr; solidCtx.beginPath(); solidCtx.arc(cx, cy, radius, 0, Math.PI * 2); solidCtx.stroke();
  24. const dashedCanvas = document.createElement('canvas'); dashedCanvas.width = size; dashedCanvas.height = size; const dashedCtx = dashedCanvas.getContext('2d'); dashedCtx.lineWidth = lineWidth; dashedCtx.strokeStyle = baseColorStr; dashedCtx.setLineDash([12, 12]); dashedCtx.shadowBlur = 10; dashedCtx.shadowColor = baseColorStr; dashedCtx.beginPath(); dashedCtx.arc(cx, cy, radius, 0, Math.PI * 2); dashedCtx.stroke();
  25. const solidMask = ctx.createConicGradient(0, cx, cy); solidMask.addColorStop(0.0, 'rgba(0,0,0,0)'); solidMask.addColorStop(0.10, 'rgba(0,0,0,0)'); solidMask.addColorStop(0.18, 'rgba(0,0,0,1)'); solidMask.addColorStop(0.48, 'rgba(0,0,0,1)'); solidMask.addColorStop(0.50, 'rgba(0,0,0,0)'); solidMask.addColorStop(0.60, 'rgba(0,0,0,0)'); solidMask.addColorStop(0.68, 'rgba(0,0,0,1)'); solidMask.addColorStop(0.98, 'rgba(0,0,0,1)'); solidMask.addColorStop(1.0, 'rgba(0,0,0,0)');
  26. const dashedMask = ctx.createConicGradient(0, cx, cy); dashedMask.addColorStop(0.0, 'rgba(0,0,0,0)'); dashedMask.addColorStop(0.02, 'rgba(0,0,0,1)'); dashedMask.addColorStop(0.10, 'rgba(0,0,0,1)'); dashedMask.addColorStop(0.18, 'rgba(0,0,0,0)'); dashedMask.addColorStop(0.50, 'rgba(0,0,0,0)'); dashedMask.addColorStop(0.52, 'rgba(0,0,0,1)'); dashedMask.addColorStop(0.60, 'rgba(0,0,0,1)'); dashedMask.addColorStop(0.68, 'rgba(0,0,0,0)'); dashedMask.addColorStop(1.0, 'rgba(0,0,0,0)');
  27. ctx.drawImage(solidCanvas, 0, 0); ctx.globalCompositeOperation = 'destination-in'; ctx.fillStyle = solidMask; ctx.fillRect(0, 0, size, size);
  28. const tempCanvas = document.createElement('canvas'); tempCanvas.width = size; tempCanvas.height = size; const tempCtx = tempCanvas.getContext('2d'); tempCtx.drawImage(dashedCanvas, 0, 0); tempCtx.globalCompositeOperation = 'destination-in'; tempCtx.fillStyle = dashedMask; tempCtx.fillRect(0, 0, size, size);
  29. ctx.globalCompositeOperation = 'lighter'; ctx.drawImage(tempCanvas, 0, 0);
  30. ctx.globalCompositeOperation = 'destination-over'; ctx.shadowBlur = 40; ctx.shadowColor = color.withAlpha(0.3).toCssColorString(); ctx.strokeStyle = color.withAlpha(0.08).toCssColorString(); ctx.lineWidth = 20; ctx.beginPath(); ctx.arc(cx, cy, radius, 0, Math.PI * 2); ctx.stroke();
  31. return canvas.toDataURL();
  32. }
  33. function createScannerTexture(color) {
  34. const canvas = document.createElement('canvas'); canvas.width = 512; canvas.height = 512; const ctx = canvas.getContext('2d');
  35. const sharpColor = color.withAlpha(1.0).toCssColorString(); ctx.lineWidth = 3; ctx.strokeStyle = sharpColor; ctx.shadowBlur = 20; ctx.shadowColor = sharpColor; ctx.beginPath(); ctx.moveTo(256, 256); ctx.lineTo(506, 256); ctx.stroke(); return canvas.toDataURL();
  36. }
  37. function createLightBeamTexture(color) {
  38. const canvas = document.createElement('canvas'); canvas.width = 64; canvas.height = 256; const ctx = canvas.getContext('2d');
  39. const gradient = ctx.createLinearGradient(0, 256, 0, 0);
  40. gradient.addColorStop(0, color.withAlpha(0.6).toCssColorString()); gradient.addColorStop(0.4, color.withAlpha(0.1).toCssColorString()); gradient.addColorStop(1, color.withAlpha(0.0).toCssColorString());
  41. ctx.fillStyle = gradient; ctx.fillRect(0, 0, 64, 256); return canvas.toDataURL();
  42. }
  43. const TWO_PI = Math.PI * 2;
  44. export default {
  45. name: 'CesiumTransition',
  46. props: {
  47. // 要高亮的省份名称
  48. province: {
  49. type: String,
  50. default: '北京市'
  51. },
  52. // 微观标记点(null 则不显示POI)
  53. poi: {
  54. type: Object,
  55. default: null
  56. },
  57. // 区域中心坐标 [lon, lat](无POI时用于相机定位)
  58. districtCenter: {
  59. type: Array,
  60. default: null
  61. },
  62. // 路网数据
  63. roads: {
  64. type: Array,
  65. default: () => [
  66. { name: "阜石路-阜成路快速路", width: 12, glowPower: 0.3, color: '#00ffff', path: [[116.18, 39.93], [116.22, 39.93], [116.26, 39.928], [116.30, 39.925]] },
  67. { name: "石景山路-复兴路", width: 12, glowPower: 0.3, color: '#ff3333', path: [[116.18, 39.907], [116.224, 39.907], [116.25, 39.906], [116.30, 39.905]] },
  68. { name: "莲石东路快速路", width: 12, glowPower: 0.3, color: '#00ff00', path: [[116.18, 39.89], [116.22, 39.888], [116.26, 39.885], [116.30, 39.88]] },
  69. { name: "西五环路", width: 10, glowPower: 0.25, color: '#ffaa00', path: [[116.205, 39.95], [116.203, 39.92], [116.202, 39.89], [116.20, 39.86]] },
  70. { name: "玉泉路", width: 10, glowPower: 0.25, color: '#cc00ff', path: [[116.25, 39.94], [116.25, 39.907], [116.248, 39.88], [116.245, 39.86]] },
  71. { name: "西四环路", width: 10, glowPower: 0.25, color: '#ffff00', path: [[116.285, 39.94], [116.283, 39.91], [116.28, 39.88], [116.278, 39.85]] }
  72. ]
  73. },
  74. // 微观区域卫星底图
  75. satelliteImage: {
  76. type: Object,
  77. default: () => ({
  78. url: './beijing-satellite.jpg',
  79. bounds: [116.10, 39.80, 116.38, 39.98] // [west, south, east, north]
  80. })
  81. },
  82. // 中国边界 GeoJSON 路径
  83. boundaryUrl: {
  84. type: String,
  85. default: './china.json'
  86. },
  87. // 微观俯冲视角高度(米)
  88. microViewRange: {
  89. type: Number,
  90. default: 10000
  91. }
  92. },
  93. data() {
  94. return {
  95. isAnimating: false
  96. };
  97. },
  98. mounted() {
  99. this._viewer = null;
  100. this._preRenderListener = null;
  101. this._chinaDataSource = null;
  102. this._chinaEffectsSource = null;
  103. this._microEffectsSource = null;
  104. this._macroAlpha = 0.0;
  105. this._destroyed = false;
  106. this._pendingTimers = [];
  107. this._coords = { start: [-15.0, 35.86, 45000000], china: [104.19, 35.86, 14000000] };
  108. this.$nextTick(async () => {
  109. if (!this.$refs.cesiumContainer) return;
  110. // 尝试复用预加载的 Viewer
  111. const preloaded = await CesiumPreloader.acquire(this.$refs.cesiumContainer);
  112. this.initCesium(preloaded);
  113. if (!preloaded) {
  114. await this.waitForGlobeReady();
  115. }
  116. if (!this._destroyed) this.startTransition();
  117. });
  118. },
  119. beforeDestroy() {
  120. this._destroyed = true;
  121. this._pendingTimers.forEach(id => clearTimeout(id));
  122. this._pendingTimers = [];
  123. if (this._viewer) {
  124. if (this._preRenderListener) {
  125. this._viewer.scene.preRender.removeEventListener(this._preRenderListener);
  126. }
  127. // Cesium destroy() 内部会 removeChild,但 Vue 可能已移除 DOM,需要先确保容器在文档中
  128. const cesiumWidget = this._viewer.cesiumWidget && this._viewer.cesiumWidget.container;
  129. if (cesiumWidget && !cesiumWidget.parentNode) {
  130. document.body.appendChild(cesiumWidget);
  131. }
  132. try {
  133. this._viewer.destroy();
  134. } catch (e) {
  135. // 忽略 DOM 已被 Vue 移除导致的 removeChild 错误
  136. }
  137. this._viewer = null;
  138. }
  139. },
  140. methods: {
  141. waitForGlobeReady() {
  142. return new Promise(resolve => {
  143. const startTime = Date.now();
  144. const maxWait = 3000; // 最多等待3秒,避免卡住
  145. const check = () => {
  146. if (this._destroyed) { resolve(); return; }
  147. if ((this._viewer && this._viewer.scene.globe.tilesLoaded) || (Date.now() - startTime > maxWait)) {
  148. resolve();
  149. } else {
  150. requestAnimationFrame(check);
  151. }
  152. };
  153. requestAnimationFrame(check);
  154. });
  155. },
  156. initCesium(preloadedViewer) {
  157. this._baseGold = Cesium.Color.GOLD;
  158. this._baseWhite = Cesium.Color.WHITE;
  159. this._baseBlack = Cesium.Color.BLACK;
  160. this._dynamicGold = this._baseGold.withAlpha(0);
  161. this._dynamicWhite = this._baseWhite.withAlpha(0);
  162. this._dynamicBlack = this._baseBlack.withAlpha(0);
  163. this._poiThemeColor = Cesium.Color.fromCssColorString(
  164. (this.poi && this.poi.themeColor) || '#00bfff'
  165. );
  166. // 将 props 中的 roads 转为 Cesium 颜色对象
  167. this._majorRoads = this.roads.map(r => ({
  168. ...r,
  169. color: Cesium.Color.fromCssColorString(r.color)
  170. }));
  171. if (preloadedViewer) {
  172. // 复用预加载的 Viewer(瓦片已渲染好)
  173. this._viewer = preloadedViewer;
  174. } else {
  175. // 降级:从头创建
  176. this._viewer = new Cesium.Viewer(this.$refs.cesiumContainer, {
  177. animation: false, timeline: false, baseLayerPicker: false, geocoder: false,
  178. homeButton: false, sceneModePicker: false, navigationHelpButton: false, infoBox: false,
  179. fullscreenButton: false, selectionIndicator: false, shadows: false, shouldAnimate: false,
  180. requestRenderMode: false,
  181. imageryProvider: new Cesium.UrlTemplateImageryProvider({
  182. url: './tiles/{z}/{y}/{x}.jpg',
  183. maximumLevel: 12
  184. })
  185. });
  186. this._viewer.cesiumWidget.creditContainer.style.display = "none";
  187. }
  188. const scene = this._viewer.scene;
  189. scene.fog.enabled = false;
  190. scene.skyAtmosphere.show = false;
  191. scene.globe.showGroundAtmosphere = false;
  192. scene.globe.enableLighting = true;
  193. scene.globe.tileCacheSize = 300;
  194. scene.globe.maximumScreenSpaceError = 1.5;
  195. this._viewer.clock.currentTime = Cesium.JulianDate.fromDate(new Date('2026-01-01T01:30:00Z'));
  196. this._viewer.clock.shouldAnimate = false;
  197. const baseLayer = this._viewer.imageryLayers.get(0);
  198. if (baseLayer) {
  199. baseLayer.brightness = 0.75; // 亮度:1.0 是原图,小于 1.0 变暗,大于 1.0 变亮
  200. baseLayer.contrast = 1.3; // 对比度
  201. baseLayer.gamma = 1; // 新增这一行:默认值是 1.0,调高可以显著提亮暗部环境
  202. }
  203. const cam = this._viewer.scene.screenSpaceCameraController;
  204. cam.enableRotate = false;
  205. cam.enableTranslate = false;
  206. cam.enableZoom = false;
  207. cam.enableTilt = false;
  208. cam.enableLook = false;
  209. // 微观区域高清卫星底图
  210. if (this.satelliteImage && this.satelliteImage.url) {
  211. const b = this.satelliteImage.bounds;
  212. this._viewer.imageryLayers.addImageryProvider(
  213. new Cesium.SingleTileImageryProvider({
  214. url: this.satelliteImage.url,
  215. rectangle: Cesium.Rectangle.fromDegrees(b[0], b[1], b[2], b[3])
  216. })
  217. );
  218. // 针对本地大图的专项优化
  219. this._viewer.scene.globe.maximumScreenSpaceError = 1.0; // 降低误差,强制渲染高清
  220. this._viewer.scene.globe.tileCacheSize = 200; // 增加缓存
  221. }
  222. this._viewer.camera.setView({ destination: Cesium.Cartesian3.fromDegrees(...this._coords.start) });
  223. this.buildMacroLayer();
  224. this.buildMicroLayer();
  225. this._preRenderListener = this._viewer.scene.preRender.addEventListener(() => {
  226. if (!this._microEffectsSource || !this._microEffectsSource.show) return;
  227. // POI 标签跟踪
  228. if (this.poi && this.$refs.poiLabel && this.$refs.poiLabel.style.opacity !== "0") {
  229. const pos3D = Cesium.Cartesian3.fromDegrees(this.poi.lon, this.poi.lat);
  230. const screenPos = Cesium.SceneTransforms.wgs84ToWindowCoordinates(this._viewer.scene, pos3D);
  231. if (screenPos && screenPos.y > 0) {
  232. this.$refs.poiLabel.style.display = 'flex';
  233. this.$refs.poiLabel.style.left = screenPos.x + 'px';
  234. this.$refs.poiLabel.style.top = (screenPos.y - 150) + 'px';
  235. } else {
  236. this.$refs.poiLabel.style.display = 'none';
  237. }
  238. }
  239. });
  240. },
  241. buildMacroLayer() {
  242. this._chinaEffectsSource = new Cesium.CustomDataSource('chinaEffects');
  243. this._viewer.dataSources.add(this._chinaEffectsSource);
  244. this._chinaEffectsSource.show = false;
  245. Cesium.GeoJsonDataSource.load(this.boundaryUrl).then(ds => {
  246. if (this._destroyed) return;
  247. this._chinaDataSource = ds;
  248. const entities = ds.entities.values;
  249. const provinceMainLand = {};
  250. for (let i = 0; i < entities.length; i++) {
  251. const entity = entities[i];
  252. if (entity.polygon) {
  253. entity.polygon.fill = false;
  254. entity.polygon.outline = false;
  255. const positions = entity.polygon.hierarchy.getValue(Cesium.JulianDate.now()).positions;
  256. entity.polyline = new Cesium.PolylineGraphics({
  257. positions, width: 2,
  258. material: new Cesium.PolylineGlowMaterialProperty({
  259. glowPower: 0.05,
  260. color: new Cesium.CallbackProperty(() => this._dynamicGold, false)
  261. })
  262. });
  263. if (entity.name === this.province) {
  264. const pointCount = positions.length;
  265. if (!provinceMainLand[entity.name] || pointCount > provinceMainLand[entity.name].pointCount) {
  266. provinceMainLand[entity.name] = { pointCount, center: Cesium.BoundingSphere.fromPoints(positions).center };
  267. }
  268. }
  269. }
  270. }
  271. this._viewer.dataSources.add(ds);
  272. for (const name in provinceMainLand) {
  273. const mainCenter = provinceMainLand[name].center;
  274. this._chinaEffectsSource.entities.add({
  275. position: mainCenter,
  276. label: {
  277. text: name, font: 'bold 28px Microsoft YaHei',
  278. fillColor: new Cesium.CallbackProperty(() => this._dynamicWhite, false),
  279. outlineColor: new Cesium.CallbackProperty(() => this._dynamicBlack, false),
  280. outlineWidth: 3, style: Cesium.LabelStyle.FILL_AND_OUTLINE,
  281. heightReference: Cesium.HeightReference.RELATIVE_TO_GROUND,
  282. pixelOffset: new Cesium.Cartesian2(0, -10),
  283. distanceDisplayCondition: new Cesium.DistanceDisplayCondition(0, 20000000)
  284. }
  285. });
  286. for (let i = 0; i < 3; i++) {
  287. const offset = (4000 / 3) * i, startTime = Date.now() + offset;
  288. this._chinaEffectsSource.entities.add({
  289. position: mainCenter,
  290. ellipse: {
  291. semiMinorAxis: new Cesium.CallbackProperty(() => 350000 * (((Date.now() - startTime) % 4000) / 4000), false),
  292. semiMajorAxis: new Cesium.CallbackProperty(() => 350000 * (((Date.now() - startTime) % 4000) / 4000), false),
  293. material: new Cesium.ColorMaterialProperty(new Cesium.CallbackProperty(() => {
  294. const t = ((Date.now() - startTime) % 4000) / 4000;
  295. return Cesium.Color.DEEPSKYBLUE.withAlpha((1.0 - t) * 0.8 * this._macroAlpha);
  296. }, false)), height: 5000 + i * 100
  297. }
  298. });
  299. }
  300. }
  301. });
  302. },
  303. buildMicroLayer() {
  304. this._microEffectsSource = new Cesium.CustomDataSource('microEffects');
  305. this._viewer.dataSources.add(this._microEffectsSource);
  306. this._microEffectsSource.show = false;
  307. // POI 标记(可选)
  308. if (this.poi) {
  309. const pos3D = Cesium.Cartesian3.fromDegrees(this.poi.lon, this.poi.lat);
  310. const slowRingImg = createOuterRingTexture(this._poiThemeColor);
  311. const fastScannerImg = createScannerTexture(this._poiThemeColor);
  312. const beamImg = createLightBeamTexture(this._poiThemeColor);
  313. this._microEffectsSource.entities.add({ position: pos3D, ellipse: { semiMinorAxis: this.poi.radarRadius, semiMajorAxis: this.poi.radarRadius, material: new Cesium.ImageMaterialProperty({ image: slowRingImg, transparent: true }), stRotation: new Cesium.CallbackProperty(() => (Date.now() / 5000.0) % TWO_PI, false), height: 20 } });
  314. this._microEffectsSource.entities.add({ position: pos3D, ellipse: { semiMinorAxis: this.poi.radarRadius, semiMajorAxis: this.poi.radarRadius, material: new Cesium.ImageMaterialProperty({ image: fastScannerImg, transparent: true }), stRotation: new Cesium.CallbackProperty(() => (Date.now() / 300.0) % TWO_PI, false), height: 21 } });
  315. this._microEffectsSource.entities.add({ position: Cesium.Cartesian3.fromDegrees(this.poi.lon, this.poi.lat, 2000.0), cylinder: { length: 4000, topRadius: 20, bottomRadius: 200, material: new Cesium.ImageMaterialProperty({ image: beamImg, transparent: true }) } });
  316. this._microEffectsSource.entities.add({ position: Cesium.Cartesian3.fromDegrees(this.poi.lon, this.poi.lat, 50), point: { pixelSize: 15, color: Cesium.Color.WHITE, outlineColor: this._poiThemeColor, outlineWidth: 3 } });
  317. }
  318. },
  319. fadeMacroLayer(startAlpha, endAlpha, durationMs) {
  320. return new Promise(resolve => {
  321. if (startAlpha < endAlpha && this._chinaDataSource) {
  322. this._chinaDataSource.show = true;
  323. this._chinaEffectsSource.show = true;
  324. }
  325. const startTime = Date.now();
  326. const animateFade = () => {
  327. if (this._destroyed) { resolve(); return; }
  328. let p = (Date.now() - startTime) / durationMs; if (p >= 1.0) p = 1.0;
  329. this._macroAlpha = startAlpha + (endAlpha - startAlpha) * p;
  330. this._dynamicGold = this._baseGold.withAlpha(this._macroAlpha);
  331. this._dynamicWhite = this._baseWhite.withAlpha(this._macroAlpha);
  332. this._dynamicBlack = this._baseBlack.withAlpha(this._macroAlpha);
  333. if (p < 1.0) {
  334. requestAnimationFrame(animateFade);
  335. } else {
  336. if (endAlpha === 0 && this._chinaDataSource) {
  337. this._chinaDataSource.show = false;
  338. this._chinaEffectsSource.show = false;
  339. }
  340. resolve();
  341. }
  342. };
  343. requestAnimationFrame(animateFade);
  344. });
  345. },
  346. animatePathGrowing(roadObj, delayTime) {
  347. return new Promise(resolve => {
  348. const timerId = setTimeout(() => {
  349. if (this._destroyed) { resolve(); return; }
  350. const positions = roadObj.path.map(p => Cesium.Cartesian3.fromDegrees(p[0], p[1]));
  351. const distances = [0]; let totalLength = 0;
  352. for (let i = 1; i < positions.length; i++) {
  353. totalLength += Cesium.Cartesian3.distance(positions[i - 1], positions[i]);
  354. distances.push(totalLength);
  355. }
  356. let progress = 0.0, isFinished = false;
  357. this._microEffectsSource.entities.add({
  358. polyline: {
  359. positions: new Cesium.CallbackProperty(function () {
  360. if (isFinished) return positions;
  361. progress += 0.025;
  362. if (progress >= 1.0) { isFinished = true; resolve(); return positions; }
  363. const targetDist = progress * totalLength;
  364. const pts = [];
  365. for (let i = 1; i < positions.length; i++) {
  366. pts.push(positions[i - 1]);
  367. if (targetDist <= distances[i]) {
  368. const seg = (targetDist - distances[i - 1]) / (distances[i] - distances[i - 1]);
  369. const p = new Cesium.Cartesian3();
  370. Cesium.Cartesian3.lerp(positions[i - 1], positions[i], seg, p);
  371. pts.push(p); break;
  372. }
  373. }
  374. return pts;
  375. }, false),
  376. width: roadObj.width,
  377. material: new Cesium.PolylineGlowMaterialProperty({ glowPower: roadObj.glowPower, color: roadObj.color })
  378. }
  379. });
  380. }, delayTime);
  381. this._pendingTimers.push(timerId);
  382. });
  383. },
  384. _safeDelay(ms) {
  385. return new Promise(resolve => {
  386. const id = setTimeout(resolve, ms);
  387. this._pendingTimers.push(id);
  388. });
  389. },
  390. async startTransition() {
  391. if (this.isAnimating) return;
  392. this.isAnimating = true;
  393. // 阶段1: 飞向中国 (2s)
  394. this._viewer.camera.flyTo({
  395. destination: Cesium.Cartesian3.fromDegrees(...this._coords.china), duration: 2
  396. });
  397. const fadeInId = setTimeout(() => {
  398. if (!this._destroyed) this.fadeMacroLayer(0.0, 1.0, 1000);
  399. }, 800);
  400. this._pendingTimers.push(fadeInId);
  401. await this._safeDelay(3000);
  402. if (this._destroyed) return;
  403. // 阶段2: 俯冲到微观
  404. const targetCenter = this.poi
  405. ? [this.poi.lon, this.poi.lat]
  406. : (this.districtCenter || [116.70, 39.80]);
  407. const targetSphere = Cesium.BoundingSphere.fromPoints([Cesium.Cartesian3.fromDegrees(targetCenter[0], targetCenter[1])]);
  408. this._viewer.camera.flyToBoundingSphere(targetSphere, {
  409. offset: new Cesium.HeadingPitchRange(0.0, Cesium.Math.toRadians(-45), this.microViewRange),
  410. duration: 2, easingFunction: Cesium.EasingFunction.CUBIC_IN_OUT
  411. });
  412. this.fadeMacroLayer(1.0, 0.0, 800);
  413. // 阶段3: 显示微观效果
  414. await this._safeDelay(1500);
  415. if (this._destroyed) return;
  416. this._microEffectsSource.show = true;
  417. if (this.poi && this.$refs.poiLabel) this.$refs.poiLabel.style.opacity = 1;
  418. await this._safeDelay(1000);
  419. if (this._destroyed) return;
  420. this.$emit('complete');
  421. }
  422. }
  423. };
  424. </script>
  425. <style scoped>
  426. .cesium-transition-wrapper {
  427. position: fixed;
  428. top: 0;
  429. left: 0;
  430. width: 100vw;
  431. height: 100vh;
  432. z-index: 9999;
  433. background: #020813;
  434. overflow: hidden;
  435. font-family: "Microsoft YaHei", sans-serif;
  436. }
  437. .cesium-container {
  438. width: 100%;
  439. height: 100%;
  440. margin: 0;
  441. padding: 0;
  442. }
  443. .ui-layer {
  444. position: absolute;
  445. top: 0;
  446. left: 0;
  447. width: 100%;
  448. height: 100%;
  449. pointer-events: none;
  450. }
  451. .html-label {
  452. position: absolute;
  453. padding: 4px 10px;
  454. border-radius: 2px;
  455. pointer-events: none;
  456. transition: opacity 0.5s;
  457. transform: translate(-50%, -100%);
  458. display: flex;
  459. flex-direction: column;
  460. align-items: center;
  461. white-space: nowrap;
  462. font-weight: bold;
  463. z-index: 5;
  464. border: 1px solid currentColor;
  465. background: rgba(0, 20, 30, 0.7);
  466. box-shadow: inset 0 0 10px currentColor;
  467. text-shadow: 0 0 5px #000;
  468. }
  469. .html-label::after {
  470. content: '';
  471. width: 2px;
  472. height: 35px;
  473. background: currentColor;
  474. margin-top: 5px;
  475. box-shadow: 0 0 5px currentColor;
  476. }
  477. </style>