feat(earth): Modularize 3D Earth page with ES Modules

## Changelog

### New Features
- Modularized 3D earth HTML page from single 1918-line file into ES Modules
- Split CSS into separate module files (base, info-panel, coordinates-display, legend, earth-stats)
- Split JS into separate modules (constants, utils, ui, earth, cables, controls, main)

### 3D Earth Rendering
- Use Three.js r128 (via esm.sh CDN) for color consistency with original
- Earth with 8K satellite texture and proper material settings
- Cloud layer with transparency and additive blending
- Starfield background (8000 stars)
- Latitude/longitude grid lines that rotate with Earth

### Cable System
- Load cable data from geo.json with great circle path calculation
- Support for MultiLineString and LineString geometry types
- Cable color from geo.json properties.color field
- Landing points loading from landing-point-geo.geojson

### User Interactions
- Mouse hover: highlight cable and show details
- Mouse click: lock cable with pulsing glow effect
- Click cable to pause rotation, click elsewhere to resume
- Click rotation toggle button to resume rotation and clear highlight
- Reset view with smooth animation (800ms cubic ease-out)
- Mouse wheel zoom support
- Drag to rotate Earth

### UI/UX Improvements
- Tooltip shows latitude, longitude, and altitude
- Prevent tooltip text selection during drag
- Hide tooltip during drag operation
- Blue border tooltip styling matching original
- Cursor changes to grabbing during drag
- Front-facing cable detection (only detect cables facing camera)

### Bug Fixes
- Grid lines now rotate with Earth (added as Earth child)
- Reset view button now works correctly
- Fixed camera reference in reset view
- Fixed autoRotate state management when clearing locked cable

### Original HTML
- Copied original 3dearthmult.html to public folder for reference
This commit is contained in:
rayd1o
2026-03-11 15:54:50 +08:00
parent 4ada75ca14
commit 6cb4398f3a
15 changed files with 1805 additions and 44 deletions

View File

@@ -0,0 +1,149 @@
/* base.css - 公共基础样式 */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: #0a0a1a;
color: #fff;
overflow: hidden;
}
#container {
position: relative;
width: 100vw;
height: 100vh;
/* user-select: none;
-webkit-user-select: none; */
}
#container.dragging {
cursor: grabbing;
}
#loading {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 1.2rem;
color: #4db8ff;
z-index: 100;
text-align: center;
background-color: rgba(10, 10, 30, 0.95);
padding: 30px;
border-radius: 10px;
border: 1px solid #4db8ff;
box-shadow: 0 0 30px rgba(77,184,255,0.3);
}
#loading-spinner {
border: 4px solid rgba(77, 184, 255, 0.3);
border-top: 4px solid #4db8ff;
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
margin: 0 auto 15px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.error-message {
color: #ff4444;
margin-top: 10px;
font-size: 0.9rem;
display: none;
padding: 10px;
background-color: rgba(255, 68, 68, 0.1);
border-radius: 5px;
border-left: 3px solid #ff4444;
}
.terrain-controls {
margin-top: 15px;
padding-top: 15px;
border-top: 1px solid rgba(255, 255, 255, 0.1);
}
.slider-container {
margin-bottom: 10px;
}
.slider-label {
display: flex;
justify-content: space-between;
margin-bottom: 5px;
font-size: 0.9rem;
}
input[type="range"] {
width: 100%;
height: 8px;
-webkit-appearance: none;
background: rgba(0, 102, 204, 0.3);
border-radius: 4px;
outline: none;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
width: 16px;
height: 16px;
border-radius: 50%;
background: #4db8ff;
cursor: pointer;
box-shadow: 0 0 10px #4db8ff;
}
.status-message {
position: absolute;
top: 20px;
right: 20px;
background-color: rgba(10, 10, 30, 0.85);
border-radius: 10px;
padding: 10px 15px;
z-index: 10;
box-shadow: 0 0 20px rgba(0, 150, 255, 0.3);
border: 1px solid rgba(0, 150, 255, 0.2);
font-size: 0.9rem;
display: none;
backdrop-filter: blur(5px);
}
.status-message.success {
color: #44ff44;
border-left: 3px solid #44ff44;
}
.status-message.warning {
color: #ffff44;
border-left: 3px solid #ffff44;
}
.status-message.error {
color: #ff4444;
border-left: 3px solid #ff4444;
}
.tooltip {
position: absolute;
background-color: rgba(10, 10, 30, 0.95);
border: 1px solid #4db8ff;
border-radius: 5px;
padding: 5px 10px;
font-size: 0.8rem;
color: #fff;
pointer-events: none;
z-index: 100;
box-shadow: 0 0 10px rgba(77, 184, 255, 0.3);
display: none;
user-select: none;
}

View File

@@ -0,0 +1,47 @@
/* coordinates-display */
#coordinates-display {
position: absolute;
top: 20px;
right: 250px;
background-color: rgba(10, 10, 30, 0.85);
border-radius: 10px;
padding: 10px 15px;
z-index: 10;
box-shadow: 0 0 20px rgba(0, 150, 255, 0.3);
border: 1px solid rgba(0, 150, 255, 0.2);
font-size: 0.9rem;
min-width: 180px;
backdrop-filter: blur(5px);
}
#coordinates-display .coord-item {
margin-bottom: 5px;
display: flex;
justify-content: space-between;
}
#coordinates-display .coord-label {
color: #aaa;
}
#coordinates-display .coord-value {
color: #4db8ff;
font-weight: 500;
}
#coordinates-display #zoom-level {
margin-top: 5px;
color: #ffff44;
font-weight: 500;
text-align: center;
font-size: 1rem;
}
#coordinates-display .mouse-coords {
font-size: 0.8rem;
color: #aaa;
margin-top: 5px;
padding-top: 5px;
border-top: 1px solid rgba(255, 255, 255, 0.1);
}

View File

@@ -0,0 +1,31 @@
/* earth-stats */
#earth-stats {
position: absolute;
bottom: 20px;
right: 20px;
background-color: rgba(10, 10, 30, 0.85);
border-radius: 10px;
padding: 15px;
width: 250px;
z-index: 10;
box-shadow: 0 0 20px rgba(0, 150, 255, 0.3);
border: 1px solid rgba(0, 150, 255, 0.2);
font-size: 0.9rem;
backdrop-filter: blur(5px);
}
#earth-stats .stats-item {
margin-bottom: 8px;
display: flex;
justify-content: space-between;
}
#earth-stats .stats-label {
color: #aaa;
}
#earth-stats .stats-value {
color: #4db8ff;
font-weight: 500;
}

View File

@@ -0,0 +1,105 @@
/* info-panel */
#info-panel {
position: absolute;
top: 20px;
left: 20px;
background-color: rgba(10, 10, 30, 0.85);
border-radius: 10px;
padding: 20px;
width: 320px;
z-index: 10;
box-shadow: 0 0 20px rgba(0, 150, 255, 0.3);
border: 1px solid rgba(0, 150, 255, 0.2);
backdrop-filter: blur(5px);
}
#info-panel h1 {
font-size: 1.8rem;
margin-bottom: 5px;
color: #4db8ff;
text-shadow: 0 0 10px rgba(77, 184, 255, 0.5);
}
#info-panel .subtitle {
color: #aaa;
margin-bottom: 20px;
font-size: 0.9rem;
border-bottom: 1px solid rgba(255,255,255,0.1);
padding-bottom: 10px;
}
#info-panel .cable-info {
margin-top: 15px;
padding-top: 15px;
border-top: 1px solid rgba(255, 255, 255, 0.1);
}
#info-panel .cable-info h3 {
color: #4db8ff;
margin-bottom: 8px;
font-size: 1.2rem;
}
#info-panel .cable-property {
display: flex;
justify-content: space-between;
margin-bottom: 5px;
font-size: 0.9rem;
}
#info-panel .property-label {
color: #aaa;
}
#info-panel .property-value {
color: #fff;
font-weight: 500;
}
#info-panel .controls {
display: flex;
justify-content: space-between;
margin-top: 20px;
flex-wrap: wrap;
gap: 10px;
}
#info-panel button {
background: linear-gradient(135deg, #0066cc, #004c99);
color: white;
border: none;
padding: 8px 15px;
border-radius: 5px;
cursor: pointer;
font-size: 0.9rem;
transition: all 0.3s;
flex: 1;
min-width: 120px;
box-shadow: 0 2px 5px rgba(0,0,0,0.3);
}
#info-panel button:hover {
background: linear-gradient(135deg, #0088ff, #0066cc);
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(0,102,204,0.4);
}
#info-panel .zoom-controls {
display: flex;
align-items: center;
margin-top: 15px;
padding-top: 15px;
border-top: 1px solid rgba(255, 255, 255, 0.1);
}
#info-panel .zoom-buttons {
display: flex;
gap: 10px;
margin-top: 10px;
}
#info-panel .zoom-buttons button {
flex: 1;
min-width: 60px;
}

View File

@@ -0,0 +1,28 @@
/* legend */
#legend {
position: absolute;
bottom: 20px;
left: 20px;
background-color: rgba(10, 10, 30, 0.85);
border-radius: 10px;
padding: 15px;
width: 220px;
z-index: 10;
box-shadow: 0 0 20px rgba(0, 150, 255, 0.3);
border: 1px solid rgba(0, 150, 255, 0.2);
backdrop-filter: blur(5px);
}
#legend .legend-item {
display: flex;
align-items: center;
margin-bottom: 8px;
}
#legend .legend-color {
width: 20px;
height: 20px;
border-radius: 3px;
margin-right: 10px;
}