Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f701862e5a | ||
|
|
0c7990a9a8 | ||
|
|
dac7aefeb4 |
+1
-1
@@ -39,7 +39,7 @@
|
||||
<div class="view-switch" aria-label="视图切换">
|
||||
<button class="view-btn active" data-view="binder">活页本</button>
|
||||
<button class="view-btn" data-view="timeline">时间轴</button>
|
||||
<button class="view-btn" data-view="globe">地理</button>
|
||||
<button class="view-btn" data-view="globe" disabled title="地理视图加载中">地理</button>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
|
||||
+70
-11
@@ -21,6 +21,7 @@ export class App {
|
||||
this.timelineSlide = null;
|
||||
this.zoomTarget = null;
|
||||
this.cameraMemory = {};
|
||||
this._needsRender = true; // 按需渲染:首帧必渲染
|
||||
|
||||
this._initRenderer();
|
||||
this._initLights();
|
||||
@@ -36,6 +37,14 @@ export class App {
|
||||
|
||||
this._initEvents();
|
||||
this._applyRotationLimits(CAMERA_POSES.binder);
|
||||
// 默认新打开时:活页本放到最小(相机沿默认视角拉到最远)
|
||||
{
|
||||
const bp = CAMERA_POSES.binder;
|
||||
const dir = bp.pos.clone().sub(bp.target).normalize();
|
||||
this.camera.position.copy(bp.target).addScaledVector(dir, bp.max);
|
||||
this.controls.target.copy(bp.target);
|
||||
this.controls.update();
|
||||
}
|
||||
this._select(0, { syncBinder: false });
|
||||
this.binder.syncTo(0);
|
||||
|
||||
@@ -45,6 +54,13 @@ export class App {
|
||||
requestAnimationFrame(this._animate);
|
||||
}
|
||||
|
||||
// 首屏加载完成后由 main.js 调用:后台预载地理视图,就绪后激活“地理”按钮
|
||||
preloadGlobe() {
|
||||
const btn = document.querySelector('.view-btn[data-view="globe"]');
|
||||
this.globe.viewerReady.then(() => { if (btn) btn.disabled = false; });
|
||||
this.globe.ensureCreated();
|
||||
}
|
||||
|
||||
_initRenderer() {
|
||||
const canvas = document.getElementById('scene');
|
||||
this.renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
|
||||
@@ -74,6 +90,7 @@ export class App {
|
||||
this.controls.mouseButtons.MIDDLE = -1;
|
||||
this.controls.mouseButtons.RIGHT = -1;
|
||||
this.controls.rotateSpeed = 0.4; // 整体降速(无边缘减速)
|
||||
this.controls.addEventListener('change', () => { this._needsRender = true; });
|
||||
this.controls.update();
|
||||
}
|
||||
|
||||
@@ -84,15 +101,16 @@ export class App {
|
||||
const key = new THREE.DirectionalLight(0xffe6c0, 2.4);
|
||||
key.position.set(3.2, 4.4, 3.6);
|
||||
key.castShadow = true;
|
||||
key.shadow.mapSize.set(1024, 1024);
|
||||
key.shadow.mapSize.set(2048, 2048);
|
||||
key.shadow.camera.near = 0.5;
|
||||
key.shadow.camera.far = 30;
|
||||
key.shadow.camera.left = -5;
|
||||
key.shadow.camera.right = 5;
|
||||
key.shadow.camera.top = 6;
|
||||
key.shadow.camera.bottom = -6;
|
||||
key.shadow.bias = -0.0002;
|
||||
key.shadow.camera.left = -3.5;
|
||||
key.shadow.camera.right = 3.5;
|
||||
key.shadow.camera.top = 4;
|
||||
key.shadow.camera.bottom = -4;
|
||||
key.shadow.bias = -0.0005;
|
||||
this.scene.add(key);
|
||||
this.scene.add(key.target);
|
||||
this.keyLight = key;
|
||||
|
||||
const rim = new THREE.DirectionalLight(0x8fa0d0, 0.7);
|
||||
@@ -116,7 +134,7 @@ export class App {
|
||||
|
||||
this.timeline = new Timeline();
|
||||
this.globe = new CesiumGlobe({ onPick: (i) => this._pickGlobe(i) });
|
||||
this.globe.ensureCreated(); // 打开页面时就渲染缓存好三个视图
|
||||
// Cesium 懒加载:首次进入“地理”视图时才真正创建 viewer
|
||||
|
||||
this.scene.add(this.binder.group);
|
||||
this.scene.add(this.timeline.group);
|
||||
@@ -173,6 +191,7 @@ export class App {
|
||||
this.raycaster = new THREE.Raycaster();
|
||||
this.pointer = new THREE.Vector2();
|
||||
this._drag = null;
|
||||
this._hoverDirty = false;
|
||||
const canvas = this.renderer.domElement;
|
||||
|
||||
const setPointer = (e) => {
|
||||
@@ -207,11 +226,11 @@ export class App {
|
||||
this.timelineOffset = clamp(this._drag.offset + dx * this._worldPerPixel(), -limit, limit);
|
||||
this.timeline.group.position.x = this.timelineOffset;
|
||||
canvas.style.cursor = 'grabbing';
|
||||
this._needsRender = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
const hit = this._hitTest();
|
||||
canvas.style.cursor = hit ? 'pointer' : 'grab';
|
||||
this._hoverDirty = true; // 射线检测移到 _animate 中每帧至多执行一次
|
||||
});
|
||||
|
||||
canvas.addEventListener('pointerup', (e) => {
|
||||
@@ -271,8 +290,21 @@ export class App {
|
||||
}
|
||||
|
||||
_hitTest() {
|
||||
const list = this.interactives[this.view];
|
||||
if (!list || !list.length) return null;
|
||||
let list;
|
||||
if (this.view === 'binder') {
|
||||
// 只检测顶层可点击页(左右页 + 翻页缓冲),避免对全部 182 页求交
|
||||
list = [];
|
||||
const cur = this.binder.current;
|
||||
for (let d = -1; d <= 1; d++) {
|
||||
const i = cur + d;
|
||||
if (i >= 0 && i < this.binder.pages.length) list.push(this.binder.pages[i].mesh);
|
||||
}
|
||||
} else if (this.view === 'timeline') {
|
||||
list = this.timeline.pickableMeshes();
|
||||
} else {
|
||||
return null; // 地理视图由 Cesium 自行拾取
|
||||
}
|
||||
if (!list.length) return null;
|
||||
this.raycaster.setFromCamera(this.pointer, this.camera);
|
||||
const hits = this.raycaster.intersectObjects(list, false);
|
||||
return hits.length ? hits[0] : null;
|
||||
@@ -331,10 +363,13 @@ export class App {
|
||||
this.globe.setSelected(this.selected);
|
||||
if (this.view === 'globe') this.globe.flyToEntry(this.selected); // 地球页切换时自动居中
|
||||
if (syncBinder) this.binder.syncTo(this.selected);
|
||||
this._needsRender = true;
|
||||
}
|
||||
|
||||
switchView(view) {
|
||||
if (view === this.view || !CAMERA_POSES[view]) return;
|
||||
// 地理视图尚未就绪(按钮仍置灰)时忽略切换,键盘也无法绕过
|
||||
if (view === 'globe' && !this.globe.viewer) return;
|
||||
// 离开当前视图前,记住它的相机位置
|
||||
if (this.view !== 'globe') {
|
||||
this.cameraMemory[this.view] = {
|
||||
@@ -416,6 +451,7 @@ export class App {
|
||||
this.controls.target.copy(pose.target);
|
||||
}
|
||||
}
|
||||
this._needsRender = true;
|
||||
}
|
||||
|
||||
_applyRotationLimits(pose) {
|
||||
@@ -449,16 +485,27 @@ export class App {
|
||||
this.camera.updateProjectionMatrix();
|
||||
this.renderer.setSize(window.innerWidth, window.innerHeight);
|
||||
if (this.globe) this.globe.resize();
|
||||
this._needsRender = true;
|
||||
}
|
||||
|
||||
_animate() {
|
||||
requestAnimationFrame(this._animate);
|
||||
const dt = Math.min(this.clock.getDelta(), 0.05);
|
||||
|
||||
// 是否有进行中的动画(决定本帧是否需要重绘)
|
||||
const animating =
|
||||
this.binder.flip !== null ||
|
||||
this.timeline.hasActiveFlip() ||
|
||||
this.cameraTween !== null ||
|
||||
this.zoomTarget !== null ||
|
||||
this.timelineSlide !== null;
|
||||
if (animating) this._needsRender = true;
|
||||
|
||||
this.binder.update(dt);
|
||||
this.timeline.update(dt);
|
||||
this.timeline.drainPending();
|
||||
this._updateTimelineTextures();
|
||||
if (this.timeline.consumeDirty()) this._needsRender = true;
|
||||
|
||||
if (this.cameraTween) {
|
||||
const t = this.cameraTween;
|
||||
@@ -492,7 +539,19 @@ export class App {
|
||||
if (k >= 1) this.timelineSlide = null;
|
||||
}
|
||||
|
||||
// 鼠标悬停检测:每帧至多一次(pointermove 高频事件被合帧)
|
||||
if (this._hoverDirty) {
|
||||
this._hoverDirty = false;
|
||||
const hit = this._hitTest();
|
||||
this.renderer.domElement.style.cursor = hit ? 'pointer' : 'grab';
|
||||
}
|
||||
|
||||
this.controls.update();
|
||||
|
||||
// 按需渲染:仅在状态/动画/交互变化时重绘,静止时停止渲染
|
||||
if (this._needsRender) {
|
||||
this._needsRender = false;
|
||||
this.renderer.render(this.scene, this.camera);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+7
-6
@@ -9,15 +9,16 @@ for (const ev of ['copy', 'cut', 'contextmenu']) {
|
||||
const app = new App();
|
||||
window.__app = app; // 便于调试
|
||||
|
||||
// 加载界面:等三个视图初始化完成(最少展示 1 秒,最多等 8 秒)后淡出
|
||||
// 加载界面:等首帧渲染后立即后台预载地理视图(与 loading 并行,不等其淡出);
|
||||
// loading 最少展示 3 秒(期间 Cesium 预载已同步进行)。
|
||||
const t0 = Date.now();
|
||||
const MIN_DISPLAY = 1000;
|
||||
Promise.race([
|
||||
app.globe.ready,
|
||||
new Promise((r) => setTimeout(r, 8000)),
|
||||
]).then(() => {
|
||||
const MIN_DISPLAY = 8000;
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => {
|
||||
app.preloadGlobe(); // 首帧已渲染,立即预载地理界面
|
||||
const wait = Math.max(0, MIN_DISPLAY - (Date.now() - t0));
|
||||
setTimeout(() => {
|
||||
document.getElementById('loading').classList.add('hidden');
|
||||
}, wait);
|
||||
});
|
||||
});
|
||||
|
||||
+59
-13
@@ -1,6 +1,25 @@
|
||||
import * as Cesium from 'cesium';
|
||||
import { ENTRIES } from '../data/entries.js';
|
||||
|
||||
// Cesium 懒加载:脚本首次进入“地理”视图时才加载;用 Proxy 惰性访问 window.Cesium,
|
||||
// 使下方 50 余处 Cesium.xxx 引用无需逐个改动。
|
||||
const Cesium = new Proxy({}, {
|
||||
get(_t, prop) { return window.Cesium[prop]; },
|
||||
});
|
||||
|
||||
const CESIUM_BASE = '/cesium';
|
||||
|
||||
function loadCesium() {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (window.Cesium) return resolve();
|
||||
window.CESIUM_BASE_URL = CESIUM_BASE;
|
||||
const s = document.createElement('script');
|
||||
s.src = `${CESIUM_BASE}/Cesium.js`;
|
||||
s.onload = () => resolve();
|
||||
s.onerror = () => reject(new Error('Cesium 脚本加载失败'));
|
||||
document.head.appendChild(s);
|
||||
});
|
||||
}
|
||||
|
||||
// 免费、无需 Ion 令牌的影像与地形源
|
||||
const IMAGERY_URL =
|
||||
'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}';
|
||||
@@ -35,7 +54,7 @@ function sphericalAreaKm2(polys) {
|
||||
|
||||
// 生成便签 canvas:黄底圆角 + 左侧箭头 + 黑色加粗文字(整张图,箭头尖端指向坐标)
|
||||
function makeStickyNote(text, fontSize, selected = false) {
|
||||
const padX = 10, padY = 6, radius = 8, arrowW = 12, arrowH = 16, S = 2; // S: 2x 超采样
|
||||
const padX = 8, padY = 5, radius = 7, arrowW = 10, arrowH = 14, S = 2; // S: 2x 超采样
|
||||
const font = `bold ${fontSize}px "Noto Serif SC", "Songti SC", "STSong", "SimSun", Georgia, serif`;
|
||||
const m = document.createElement('canvas');
|
||||
const mctx = m.getContext('2d');
|
||||
@@ -105,12 +124,21 @@ export class CesiumGlobe {
|
||||
this._visible = false;
|
||||
this._pendingData = 2; // 国家边界 + 城市数据
|
||||
this.ready = new Promise((resolve) => { this._resolveReady = resolve; });
|
||||
this.viewerReady = new Promise((resolve) => { this._resolveViewerReady = resolve; });
|
||||
}
|
||||
|
||||
ensureCreated() {
|
||||
async ensureCreated() {
|
||||
if (this._created) return;
|
||||
this._created = true;
|
||||
|
||||
try {
|
||||
await loadCesium();
|
||||
} catch (err) {
|
||||
this._created = false; // 失败可重试
|
||||
console.warn('[CesiumGlobe] Cesium 脚本加载失败:', err);
|
||||
return;
|
||||
}
|
||||
|
||||
// 调整实体渲染顺序:先渲染标签(城市/国家名),再渲染 Billboard(标记便签),
|
||||
// 让词条标记始终显示在城市/国家名的上层。
|
||||
Cesium.DataSourceDisplay.defaultVisualizersCallback = function (
|
||||
@@ -228,13 +256,25 @@ export class CesiumGlobe {
|
||||
viewer.camera.setView({
|
||||
destination: Cesium.Cartesian3.fromDegrees(35, 12, 7.2e6),
|
||||
});
|
||||
|
||||
// 创建完成:同步当前选中/可见/相机状态(首次进入时这些调用可能因 viewer 未建而跳过)
|
||||
this.setSelected(this.selected);
|
||||
this.setVisible(this._visible);
|
||||
this.resize();
|
||||
this.flyToEntry(this.selected);
|
||||
|
||||
// 地球已就绪(影像/地形/城市数据继续后台加载),通知 App 隐藏加载遮罩
|
||||
if (this._resolveViewerReady) {
|
||||
this._resolveViewerReady();
|
||||
this._resolveViewerReady = null;
|
||||
}
|
||||
}
|
||||
|
||||
_buildMarkers() {
|
||||
ENTRIES.forEach((entry, i) => {
|
||||
const position = Cesium.Cartesian3.fromDegrees(entry.lng, entry.lat);
|
||||
const noteNormal = makeStickyNote(entry.name, 19, false);
|
||||
const noteSelected = makeStickyNote(entry.name, 19, true);
|
||||
const noteNormal = makeStickyNote(entry.name, 16, false);
|
||||
const noteSelected = makeStickyNote(entry.name, 16, true);
|
||||
this.markers.push({ position, noteNormal, noteSelected, index: i });
|
||||
});
|
||||
this._initMarkerLayer();
|
||||
@@ -271,7 +311,7 @@ export class CesiumGlobe {
|
||||
_updateMarkerLayer() {
|
||||
const scene = this.viewer.scene;
|
||||
const camera = scene.camera;
|
||||
const outOfRange = camera.positionCartographic.height > 1e7; // 最远 1 万 km 内显示
|
||||
const outOfRange = camera.positionCartographic.height > 8e6; // 最远 8000 km 内显示
|
||||
|
||||
for (const m of this.markers) {
|
||||
const el = m.el;
|
||||
@@ -388,7 +428,7 @@ export class CesiumGlobe {
|
||||
|
||||
// 全部城市:Natural Earth 10m 人口稠密点(7342 个),中文名直接用数据自带的 name_zh
|
||||
async _buildCityLabels() {
|
||||
const viewer = this.viewer;
|
||||
const scene = this.viewer.scene;
|
||||
const textGold = Cesium.Color.fromCssColorString('#e0c46a');
|
||||
const black = Cesium.Color.fromCssColorString('#14100d');
|
||||
const ddc = new Cesium.DistanceDisplayCondition(0, 4e6);
|
||||
@@ -403,10 +443,16 @@ export class CesiumGlobe {
|
||||
cities = [];
|
||||
}
|
||||
|
||||
// 用 primitive 集合替代 7342 个 entity,大幅降低 Cesium 实体系统开销
|
||||
const points = new Cesium.PointPrimitiveCollection();
|
||||
const labels = new Cesium.LabelCollection({ scene });
|
||||
scene.primitives.add(points);
|
||||
scene.primitives.add(labels);
|
||||
|
||||
for (const c of cities) {
|
||||
viewer.entities.add({
|
||||
position: Cesium.Cartesian3.fromDegrees(c.lng, c.lat),
|
||||
point: {
|
||||
const position = Cesium.Cartesian3.fromDegrees(c.lng, c.lat);
|
||||
points.add({
|
||||
position,
|
||||
pixelSize: 3,
|
||||
color: textGold,
|
||||
outlineColor: black,
|
||||
@@ -414,8 +460,9 @@ export class CesiumGlobe {
|
||||
heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
|
||||
distanceDisplayCondition: ddc,
|
||||
translucencyByDistance: tdc,
|
||||
},
|
||||
label: {
|
||||
});
|
||||
labels.add({
|
||||
position,
|
||||
text: c.name,
|
||||
font: 'bold 14px "Noto Serif SC", "Songti SC", "STSong", "SimSun", Georgia, serif',
|
||||
fillColor: black,
|
||||
@@ -426,7 +473,6 @@ export class CesiumGlobe {
|
||||
heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
|
||||
distanceDisplayCondition: ddc,
|
||||
translucencyByDistance: tdc,
|
||||
},
|
||||
});
|
||||
}
|
||||
this._dataReady();
|
||||
|
||||
+3
-3
@@ -5,7 +5,7 @@ import { clamp, easeOutCubic } from '../utils/easing.js';
|
||||
|
||||
const RING_R = 0.085;
|
||||
const RING_TUBE = 0.0115;
|
||||
const STACK_GAP = 0.004;
|
||||
const STACK_GAP = 0.002;
|
||||
const PAGE_GAP = 0.02; // 页内缘离书脊的小空隙
|
||||
|
||||
function makeLeatherTexture({ emblem = false } = {}) {
|
||||
@@ -533,7 +533,7 @@ export class Binder {
|
||||
flipForward() {
|
||||
if (this.flip) this._finalizeFlip(); // 前一页没翻完也能继续翻
|
||||
if (!this.canFlipForward()) return;
|
||||
this.flip = { index: this.current, from: 0, to: -Math.PI, t: 0, duration: 0.6 };
|
||||
this.flip = { index: this.current, from: 0, to: -Math.PI, t: 0, duration: 0.38 };
|
||||
// 优先更新底部导航与信息卡(翻页开始即切换目标页)
|
||||
if (this.onChange) this.onChange((this.current + 1) % this.pages.length);
|
||||
}
|
||||
@@ -541,7 +541,7 @@ export class Binder {
|
||||
flipBackward() {
|
||||
if (this.flip) this._finalizeFlip();
|
||||
if (!this.canFlipBackward()) return;
|
||||
this.flip = { index: this.current - 1, from: -Math.PI, to: 0, t: 0, duration: 0.6 };
|
||||
this.flip = { index: this.current - 1, from: -Math.PI, to: 0, t: 0, duration: 0.38 };
|
||||
// 优先更新底部导航与信息卡
|
||||
if (this.onChange) this.onChange(this.current - 1);
|
||||
}
|
||||
|
||||
+31
-2
@@ -27,6 +27,7 @@ export class Timeline {
|
||||
this._visRange = null;
|
||||
this._ready = false;
|
||||
this._pending = [];
|
||||
this._dirty = false;
|
||||
|
||||
this._buildLine();
|
||||
this._buildCards();
|
||||
@@ -141,17 +142,21 @@ export class Timeline {
|
||||
c.material.setPageMaps(this.placeholder.front, this.placeholder.back);
|
||||
c.isPlaceholder = true;
|
||||
this._textured.delete(i);
|
||||
this._dirty = true;
|
||||
}
|
||||
|
||||
// 进入窗口:分帧异步生成,避免一次性生成 25 页阻塞主线程
|
||||
this._pending = [...win].filter((i) => !this._textured.has(i) && this.cards[i].isPlaceholder);
|
||||
const next = [...win].filter((i) => !this._textured.has(i) && this.cards[i].isPlaceholder);
|
||||
if (next.length) this._dirty = true;
|
||||
this._pending = next;
|
||||
this.drainPending();
|
||||
}
|
||||
|
||||
// 每帧由 App._animate 驱动,逐批生成窗口内纹理(避免阻塞)
|
||||
drainPending() {
|
||||
if (!this._pending || !this._pending.length) return;
|
||||
if (!this._pending || !this._pending.length) return false;
|
||||
const budget = 2; // 每帧最多生成 2 页
|
||||
let did = false;
|
||||
for (let k = 0; k < budget && this._pending.length; k++) {
|
||||
const i = this._pending.shift();
|
||||
const c = this.cards[i];
|
||||
@@ -160,7 +165,21 @@ export class Timeline {
|
||||
c.material.setPageMaps(front, back);
|
||||
c.isPlaceholder = false;
|
||||
this._textured.add(i);
|
||||
this._dirty = true;
|
||||
did = true;
|
||||
}
|
||||
return did;
|
||||
}
|
||||
|
||||
hasActiveFlip() {
|
||||
return this.cards.some((c) => c.flipAnim);
|
||||
}
|
||||
|
||||
// App 每帧消费“纹理窗口变化”标志,用于按需渲染
|
||||
consumeDirty() {
|
||||
const d = this._dirty;
|
||||
this._dirty = false;
|
||||
return d;
|
||||
}
|
||||
|
||||
// 返回某张卡片在时间轴组内的横向坐标(用于点击后居中)
|
||||
@@ -168,6 +187,16 @@ export class Timeline {
|
||||
return cardX(index, ENTRIES.length);
|
||||
}
|
||||
|
||||
// 射线检测用:只返回可视窗口内的卡片 mesh(减少求交对象)
|
||||
pickableMeshes() {
|
||||
if (!this._visRange) return this.cards.map((c) => c.mesh);
|
||||
const list = [];
|
||||
for (let i = this._visRange.min; i <= this._visRange.max; i++) {
|
||||
if (this.cards[i]) list.push(this.cards[i].mesh);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
// 点击翻转:每次点击切换正/反面;选中另一张时,其它翻到背面的卡片先翻回正面
|
||||
flip(index) {
|
||||
this.cards.forEach((o) => {
|
||||
|
||||
+6
-1
@@ -55,7 +55,7 @@ html, body {
|
||||
.vignette {
|
||||
position: absolute; inset: 0; pointer-events: none;
|
||||
background: radial-gradient(ellipse at 50% 42%, transparent 40%, rgba(8, 5, 3, 0.55) 100%);
|
||||
mix-blend-mode: multiply;
|
||||
/* 不启用 mix-blend-mode,避免每帧全屏合成开销(深色半透明覆盖即可) */
|
||||
}
|
||||
.grain {
|
||||
position: absolute; inset: 0; pointer-events: none; opacity: 0.06;
|
||||
@@ -114,6 +114,11 @@ html, body {
|
||||
transition: all 0.25s ease;
|
||||
}
|
||||
.view-btn:hover { border-color: var(--gold); color: #fff; }
|
||||
.view-btn:disabled {
|
||||
opacity: 0.45;
|
||||
cursor: not-allowed;
|
||||
pointer-events: none;
|
||||
}
|
||||
.view-btn.active {
|
||||
background: linear-gradient(180deg, rgba(201,162,39,0.85), rgba(150,115,20,0.85));
|
||||
color: #1c1308; border-color: var(--gold); font-weight: 600;
|
||||
|
||||
+38
-2
@@ -1,6 +1,42 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import cesium from 'vite-plugin-cesium';
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import serveStatic from 'serve-static';
|
||||
|
||||
// Cesium 懒加载:复制静态资源、注入 CSS,但不注入 6MB 的 Cesium.js 脚本——
|
||||
// 脚本由 CesiumGlobe 首次进入“地理”视图时动态加载,避免阻塞首屏。
|
||||
const CESIUM_URL = '/cesium/';
|
||||
|
||||
function cesiumLazy() {
|
||||
let isBuild = false;
|
||||
return {
|
||||
name: 'cesium-lazy',
|
||||
config(_c, { command }) {
|
||||
isBuild = command === 'build';
|
||||
},
|
||||
configureServer({ middlewares }) {
|
||||
// dev:直接静态服务 Cesium 资源(未压缩版便于调试)
|
||||
const dir = path.join(process.cwd(), 'node_modules/cesium/Build/CesiumUnminified');
|
||||
middlewares.use(CESIUM_URL, serveStatic(dir));
|
||||
},
|
||||
async closeBundle() {
|
||||
if (!isBuild) return;
|
||||
const src = path.join(process.cwd(), 'node_modules/cesium/Build/Cesium');
|
||||
const out = path.join(process.cwd(), 'dist', 'cesium');
|
||||
for (const d of ['Assets', 'ThirdParty', 'Workers', 'Widgets']) {
|
||||
await fs.cp(path.join(src, d), path.join(out, d), { recursive: true });
|
||||
}
|
||||
await fs.copyFile(path.join(src, 'Cesium.js'), path.join(out, 'Cesium.js'));
|
||||
},
|
||||
transformIndexHtml() {
|
||||
return [{
|
||||
tag: 'link',
|
||||
attrs: { rel: 'stylesheet', href: `${CESIUM_URL}Widgets/widgets.css` },
|
||||
}];
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [cesium()],
|
||||
plugins: [cesiumLazy()],
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user