first commit

This commit is contained in:
PC-202306242200\Administrator
2026-03-28 23:15:40 +08:00
commit 10ca82f012
157 changed files with 159116 additions and 0 deletions

29
.commitlintrc.js Normal file
View File

@@ -0,0 +1,29 @@
module.exports = {
extends: ["@commitlint/config-conventional"],
rules: {
"type-enum": [
2,
"always",
[
"feat", // 功能
"fix", // bug
"test", // 测试
"perf", // 优化
"refactor", // 重构
"docs", // 文档
"chore", // 辅助工具配置
"style", // 格式 适合lint fix...
"revert", // 回滚
"merge", // 合并
"sync", // 同步同步主线或分支上的fix修复等
],
],
"type-case": [2, "always", "lower-case"],
"type-empty": [2, "never"],
"scope-empty": [0],
"scope-case": [0],
"subject-full-stop": [0, "never"],
"subject-case": [0, "never"],
"header-max-length": [0, "always", 72],
},
};

1
.env.local Normal file
View File

@@ -0,0 +1 @@
VITE_BASE_API=http://192.168.1.194:1020

2
.env.production Normal file
View File

@@ -0,0 +1,2 @@
VITE_BASE_API=http://dvapi.prod.zhongshuai2023.com

16
.eslintrc.js Normal file
View File

@@ -0,0 +1,16 @@
module.exports = {
"plugins": ['@typescript-eslint'],
extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
rules: {
eqeqeq: 0, // 必须使用全等
'no-unused-vars': 1, // 不能有声明后未被使用的变量或参数
'no-throw-literal': 0, // 0可以/2不可以 抛出字面量错误 throw "error";
'no-sparse-arrays': 2, // 数组中不允许出现空位置
'no-empty': 0, // 禁止出现空语句块
'no-console': ['error', { allow: ['warn', 'error', 'info', "log"] }],
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-useless-escape': 0,
"@typescript-eslint/no-explicit-any": "off",
"no-async-promise-executor": 0
},
}

1
.gitattributes vendored Normal file
View File

@@ -0,0 +1 @@
*.js linguist-detectable=false

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
node_modules

5
.husky/pre-commit Normal file
View File

@@ -0,0 +1,5 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npx lint-staged

8
.prettierrc.js Normal file
View File

@@ -0,0 +1,8 @@
export default {
"printWidth": 80,
"semi": false,
"singleQuote": true,
"trailingComma": "es5",
"bracketSpacing": true,
"spaceBeforeFunctionParen": true
}

BIN
3d-earth.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 GhostCat
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

6
README.en.md Normal file
View File

@@ -0,0 +1,6 @@
# Webpack 5 + Typescript 4 + Three.js 基础模板
- Webpack 5
- Typescript 4
- Three.js 130
- lodash

14
README.md Normal file
View File

@@ -0,0 +1,14 @@
# 3d-earth
本项目使用 [three-ts-webpack](https://github.com/GhostCatcg/three-ts-webpack) 构建
[Live Demo](https://gcat.cc/demo/earth)
![alt](./3d-earth.png)
## Todolist
1. - [x] 加载效果[loading...]
2. - [x] 地球、以及星空背景🌏
3. - [x] 辉光以及大气层✨
4. - [x] 地球标点以及城市标签🇨🇳
5. - [x] 卫星环绕旋转🛰
6. - [x] 国家/城市之前的飞线🪐
7. - [ ] 飞机沿飞线飞行🛫

View File

@@ -0,0 +1,144 @@
import { CatmullRomCurve3, DoubleSide, Group, Mesh, MeshBasicMaterial, PlaneGeometry, Texture, TubeGeometry, Vector3 } from "three";
import { punctuation } from "../world/Earth";
/**
* 经纬度坐标转球面坐标
* @param {地球半径} R
* @param {经度(角度值)} longitude
* @param {维度(角度值)} latitude
*/
export const lon2xyz = (R:number, longitude:number, latitude:number): Vector3 => {
let lon = longitude * Math.PI / 180; // 转弧度值
const lat = latitude * Math.PI / 180; // 转弧度值
lon = -lon; // js坐标系z坐标轴对应经度-90度而不是90度
// 经纬度坐标转球面坐标计算公式
const x = R * Math.cos(lat) * Math.cos(lon);
const y = R * Math.sin(lat);
const z = R * Math.cos(lat) * Math.sin(lon);
// 返回球面坐标
return new Vector3(x, y, z);
}
// 创建波动光圈
export const createWaveMesh = (options: { radius: number, lon: number, lat: number, textures: Record<string, Texture> }) => {
const geometry = new PlaneGeometry(1, 1); //默认在XOY平面上
const texture = options.textures.aperture;
const material = new MeshBasicMaterial({
color: 0xe99f68,
map: texture,
transparent: true, //使用背景透明的png贴图注意开启透明计算
opacity: 1.0,
depthWrite: false, //禁止写入深度缓冲区数据
});
const mesh = new Mesh(geometry, material);
// 经纬度转球面坐标
const coord = lon2xyz(options.radius * 1.001, options.lon, options.lat);
const size = options.radius * 0.12; //矩形平面Mesh的尺寸
mesh.scale.set(size, size, size); //设置mesh大小
mesh.userData['size'] = size; //自顶一个属性表示mesh静态大小
mesh.userData['scale'] = Math.random() * 1.0; //自定义属性._s表示mesh在原始大小基础上放大倍数 光圈在原来mesh.size基础上1~2倍之间变化
mesh.position.set(coord.x, coord.y, coord.z);
const coordVec3 = new Vector3(coord.x, coord.y, coord.z).normalize();
const meshNormal = new Vector3(0, 0, 1);
mesh.quaternion.setFromUnitVectors(meshNormal, coordVec3);
return mesh;
}
// 创建柱状
export const createLightPillar = (options: { radius: number, lon: number, lat: number, index: number, textures: Record<string, Texture>, punctuation: punctuation }) => {
const height = options.radius * 0.3;
const geometry = new PlaneGeometry(options.radius * 0.05, height);
geometry.rotateX(Math.PI / 2);
geometry.translate(0, 0, height / 2);
const material = new MeshBasicMaterial({
map: options.textures.light_column,
color:
options.index == 0
? options.punctuation.lightColumn.startColor
: options.punctuation.lightColumn.endColor,
transparent: true,
side: DoubleSide,
depthWrite: false, //是否对深度缓冲区有任何的影响
});
const mesh = new Mesh(geometry, material);
const group = new Group();
// 两个光柱交叉叠加
group.add(mesh, mesh.clone().rotateZ(Math.PI / 2)); //几何体绕x轴旋转了所以mesh旋转轴变为z
// 经纬度转球面坐标
const SphereCoord = lon2xyz(options.radius, options.lon, options.lat); //SphereCoord球面坐标
group.position.set(SphereCoord.x, SphereCoord.y, SphereCoord.z); //设置mesh位置
const coordVec3 = new Vector3(
SphereCoord.x,
SphereCoord.y,
SphereCoord.z
).normalize();
const meshNormal = new Vector3(0, 0, 1);
group.quaternion.setFromUnitVectors(meshNormal, coordVec3);
return group;
}
// 光柱底座矩形平面
export const createPointMesh = (options: {
radius: number, lon: number,
lat: number, material: MeshBasicMaterial
}) => {
const geometry = new PlaneGeometry(1, 1); //默认在XOY平面上
const mesh = new Mesh(geometry, options.material);
// 经纬度转球面坐标
const coord = lon2xyz(options.radius * 1.001, options.lon, options.lat);
const size = options.radius * 0.05; // 矩形平面Mesh的尺寸
mesh.scale.set(size, size, size); // 设置mesh大小
// 设置mesh位置
mesh.position.set(coord.x, coord.y, coord.z);
const coordVec3 = new Vector3(coord.x, coord.y, coord.z).normalize();
const meshNormal = new Vector3(0, 0, 1);
mesh.quaternion.setFromUnitVectors(meshNormal, coordVec3);
return mesh;
}
// 获取点
export const getCirclePoints = (option: { number?: number, radius?: number, closed?: boolean }) => {
const list = [];
for (
let j = 0;
j < 2 * Math.PI - 0.1;
j += (2 * Math.PI) / (option.number || 100)
) {
list.push([
parseFloat((Math.cos(j) * (option.radius || 10)).toFixed(2)),
0,
parseFloat((Math.sin(j) * (option.radius || 10)).toFixed(2)),
]);
}
if (option.closed) list.push(list[0]);
return list;
}
// 创建线
/**
* 创建动态的线
*/
export const createAnimateLine = (option: { pointList: number[][], number?: number, radius?: number, radialSegments?: number, material: MeshBasicMaterial }) => {
// 由多个点数组构成的曲线 通常用于道路
const l: Vector3[] = [];
option.pointList.forEach((e) =>
l.push(new Vector3(e[0], e[1], e[2]))
);
const curve = new CatmullRomCurve3(l); // 曲线路径
// 管道体
const tubeGeometry = new TubeGeometry(
curve,
option.number || 50,
option.radius || 1,
option.radialSegments
);
return new Mesh(tubeGeometry, option.material);
}

View File

@@ -0,0 +1,50 @@
/**
* 资源文件
* 把模型和图片分开进行加载
*/
interface ITextures {
name: string
url: string
}
export interface IResources {
textures?: ITextures[],
}
// 创建基础纹理数据
const createBasicTexture = (name: string): string => {
// 创建一个基础的canvas纹理
const canvas = document.createElement('canvas');
canvas.width = 256;
canvas.height = 256;
const ctx = canvas.getContext('2d');
if (ctx) {
ctx.fillStyle = '#ffffff';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = '#000000';
ctx.font = '20px Arial';
ctx.textAlign = 'center';
ctx.fillText(name, canvas.width / 2, canvas.height / 2);
}
return canvas.toDataURL();
};
const textures = [
{ name: 'gradient', url: createBasicTexture('gradient') },
{ name: 'redCircle', url: createBasicTexture('redCircle') },
{ name: 'label', url: createBasicTexture('label') },
{ name: 'aperture', url: createBasicTexture('aperture') },
{ name: 'glow', url: createBasicTexture('glow') },
{ name: 'light_column', url: createBasicTexture('light_column') },
{ name: 'aircraft', url: createBasicTexture('aircraft') },
{ name: 'earth', url: createBasicTexture('earth') }
];
const resources: IResources = {
textures
}
export {
resources
}

View File

@@ -0,0 +1,91 @@
/**
* 创建 threejs 四大天王
* 场景、相机、渲染器、控制器
*/
import * as THREE from 'three';
import {
OrbitControls
} from "three/examples/jsm/controls/OrbitControls";
class Basic {
public scene!: THREE.Scene;
public camera!: THREE.PerspectiveCamera;
public renderer!: THREE.WebGLRenderer
public controls!: OrbitControls;
public dom: HTMLElement;
constructor(dom: HTMLElement) {
this.dom = dom
this.initScenes()
this.setControls()
}
/**
* 初始化场景
*/
initScenes() {
this.scene = new THREE.Scene();
this.camera = new THREE.PerspectiveCamera(
45,
window.innerWidth / window.innerHeight,
1,
100000
);
this.camera.position.set(0, 30, -250)
this.renderer = new THREE.WebGLRenderer({
alpha: true, // 透明
antialias: true, // 抗锯齿
});
this.renderer.setPixelRatio(window.devicePixelRatio); // 设置屏幕像素比
this.renderer.setSize(window.innerWidth, window.innerHeight); // 设置渲染器宽高
this.dom.appendChild(this.renderer.domElement); // 添加到dom中
}
/**
* 设置控制器
*/
setControls() {
// 鼠标控制 相机渲染dom
this.controls = new OrbitControls(this.camera, this.renderer.domElement);
this.controls.autoRotateSpeed = 3
// 使动画循环使用时阻尼或自转 意思是否有惯性
this.controls.enableDamping = true;
// 动态阻尼系数 就是鼠标拖拽旋转灵敏度
this.controls.dampingFactor = 0.05;
// 是否可以缩放
this.controls.enableZoom = true;
// 设置相机距离原点的最远距离
this.controls.minDistance = 100;
// 设置相机距离原点的最远距离
this.controls.maxDistance = 300;
// 是否开启右键拖拽
this.controls.enablePan = false;
}
/**
* 销毁方法,用于清理资源
*/
destroy() {
// 移除DOM中的渲染器
if (this.dom && this.renderer && this.renderer.domElement && this.dom.contains(this.renderer.domElement)) {
this.dom.removeChild(this.renderer.domElement);
}
// 清理渲染器
if (this.renderer) {
this.renderer.dispose();
}
// 清理控制器
if (this.controls) {
this.controls.dispose();
}
}
}
export default Basic; // 添加默认导出

View File

@@ -0,0 +1,200 @@
import {
BufferAttribute, BufferGeometry, Color, DoubleSide, Group, Material, Mesh, MeshBasicMaterial, NormalBlending,
Object3D,
Points, PointsMaterial, ShaderMaterial,
SphereGeometry, Sprite, SpriteMaterial, Texture, TextureLoader, Vector3
} from "three";
import html2canvas from "html2canvas";
// import img_bg from '../../../static/images/earth/gradient.png'
import img_earth from '../../../static/images/earth/earth.jpg'
// import img_redCircle from '../../../static/images/earth/redCircle.png'
import img_a from '../../../static/images/earth/aircraft.png'
import { createAnimateLine, createLightPillar, createPointMesh, createWaveMesh, getCirclePoints, lon2xyz } from "../Utils/common";
import gsap from "gsap";
import { flyArc } from "../Utils/arc";
// 直接嵌入着色器代码
const earthVertex = `
varying vec2 vUv;
varying vec3 vNormal;
varying vec3 vp;
varying vec3 vPositionNormal;
void main(void){
vUv = uv;
vNormal = normalize( normalMatrix * normal ); // 转换到视图空间
vp = position;
vPositionNormal = normalize(( modelViewMatrix * vec4(position, 1.0) ).xyz);
gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
}
`;
const earthFragment = `
uniform vec3 glowColor;
uniform float bias;
uniform float power;
uniform float time;
varying vec3 vp;
varying vec3 vNormal;
varying vec3 vPositionNormal;
uniform float scale;
// 获取纹理
uniform sampler2D map;
// 纹理坐标
varying vec2 vUv;
void main(void){
float a = pow( bias + scale * abs(dot(vNormal, vPositionNormal)), power );
if(vp.y > time && vp.y < time + 20.0) {
float t = smoothstep(0.0, 0.8, (1.0 - abs(0.5 - (vp.y - time) / 20.0)) / 3.0 );
gl_FragColor = mix(gl_FragColor, vec4(glowColor, 1.0), t * t );
}
gl_FragColor = mix(gl_FragColor, vec4( glowColor, 1.0 ), a);
float b = 0.8;
gl_FragColor = gl_FragColor + texture2D( map, vUv );
}
`;
export type punctuation = {
circleColor: number,
lightColumn: {
startColor: number, // 起点颜色
endColor: number, // 终点颜色
},
}
type options = {
data: {
startArray: {
name: string,
E: number, // 经度
N: number, // 维度
},
endArray: {
name: string,
E: number, // 经度
N: number, // 维度
}[]
}[]
dom: HTMLElement,
textures: Record<string, Texture>, // 贴图
earth: {
radius: number, // 地球半径
rotateSpeed: number, // 地球旋转速度
isRotation: boolean // 地球组是否自转
}
satellite: {
show: boolean, // 是否显示卫星
rotateSpeed: number, // 旋转速度
size: number, // 卫星大小
number: number, // 一个圆环几个球
},
punctuation: punctuation,
flyLine: {
color: number, // 飞线的颜色
speed: number, // 飞机拖尾线速度
flyLineColor: number // 飞行线的颜色
},
}
type uniforms = {
glowColor: { value: Color; }
scale: { type: string; value: number; }
bias: { type: string; value: number; }
power: { type: string; value: number; }
time: { type: string; value: any; }
isHover: { value: boolean; };
map: { value: Texture }
}
export default class Earth {
public group: Group;
public earthGroup: Group;
public around: BufferGeometry
public aroundPoints: Points<BufferGeometry, PointsMaterial>;
public options: options;
public uniforms: uniforms
public timeValue: number;
public earth: Mesh<SphereGeometry, ShaderMaterial>;
public punctuationMaterial: MeshBasicMaterial;
public markupPoint: Group;
public waveMeshArr: Object3D[];
public circleLineList: any[];
public circleList: any[];
public x: number;
public n: number;
public isRotation: boolean;
public flyLineArcGroup: Group;
constructor(options: options) {
this.options = options;
this.group = new Group()
this.group.name = "group";
this.group.scale.set(0, 0, 0)
this.earthGroup = new Group()
this.group.add(this.earthGroup)
this.earthGroup.name = "EarthGroup";
// 标注点效果
this.markupPoint = new Group()
this.markupPoint.name = "markupPoint"
this.waveMeshArr = []
// 卫星和标签
this.circleLineList = []
this.circleList = [];
this.x = 0;
this.n = 0;
// 地球自转
this.isRotation = this.options.earth.isRotation
// 扫光动画 shader
this.timeValue = 100
this.uniforms = {
glowColor: {
value: new Color(0x0cd1eb),
},
scale: {
type: "f",
value: -1.0,
},
bias: {
type: "f",
value: 1.0,
},
power: {
type: "f",
value: 3.3,
},
time: {
type: "f",
value: this.timeValue,
},
isHover: {
value: false,
},
map: {
value: null,
},
};
}
async init(): Promise<void> {
return new Promise(async (resolve) => {
this.createEarth(); // 创建地球
this.createStars(); // 添加星星
this.createEarthGlow() // 创建地球辉光
this.createEarthAperture() // 创建地球的大气层
await this.createMarkupPoint() // 创建柱状点位

View File

@@ -0,0 +1,73 @@
/**
* 资源管理和加载
*/
import { LoadingManager, Texture, TextureLoader } from 'three';
import { resources } from './Assets'
export class Resources {
private manager!: LoadingManager
private callback: () => void;
private textureLoader!: InstanceType<typeof TextureLoader>;
public textures: Record<string, Texture>;
constructor(callback: () => void) {
this.callback = callback // 资源加载完成的回调
this.textures = {} // 贴图对象
this.setLoadingManager()
this.loadResources()
}
/**
* 管理加载状态
*/
private setLoadingManager() {
this.manager = new LoadingManager()
// 开始加载
this.manager.onStart = () => {
console.log('开始加载资源文件')
}
// 加载完成
this.manager.onLoad = () => {
this.callback()
}
// 正在进行中
this.manager.onProgress = (url) => {
console.log(`正在加载:${url}`)
}
this.manager.onError = url => {
console.log('加载失败:' + url)
}
}
/**
* 加载资源
*/
private loadResources(): void {
this.textureLoader = new TextureLoader(this.manager)
resources.textures?.forEach((item) => {
this.textureLoader.load(item.url, (t) => {
this.textures[item.name] = t
})
})
}
/**
* 销毁方法,用于清理资源
*/
destroy() {
// 清理所有纹理
Object.keys(this.textures).forEach(key => {
const texture = this.textures[key];
if (texture && texture.dispose) {
texture.dispose();
}
});
// 清空纹理对象
this.textures = {};
}
}

View File

@@ -0,0 +1,119 @@
import { resources } from "./Assets";
import { IWord } from "../interfaces/IWord";
import { Resources } from "./Resources";
import Basic from "./Basic"; // 修改导入,从命名导入改为默认导入
import Earth from "./Earth";
import Sizes from "../Utils/Sizes";
export default class World extends Basic {
public sizes: Sizes;
public res: Resources;
public earth: Earth;
constructor(option: IWord) {
super(option.dom)
this.sizes = new Sizes({ dom: option.dom })
this.res = new Resources(() => {
this.earth = new Earth({
data: [
{
startArray: {
name: "北京",
E: 116.404,
N: 39.915,
},
endArray: [
{
name: "上海",
E: 121.4737,
N: 31.2304,
},
{
name: "广州",
E: 113.2806,
N: 23.1258,
},
{
name: "杭州",
E: 120.1614,
N: 30.2792,
},
],
},
],
dom: option.dom,
textures: this.res.textures,
earth: {
radius: 50, // 地球半径
rotateSpeed: 0.001, // 地球旋转速度
isRotation: true, // 地球组是否自转
},
satellite: {
show: true, // 是否显示卫星
rotateSpeed: 0.002, // 旋转速度
size: 2, // 卫星大小
number: 3, // 一个圆环几个球
},
punctuation: {
circleColor: 0x0cd1eb,
lightColumn: {
startColor: 0x0cd1eb, // 起点颜色
endColor: 0x00aaff, // 终点颜色
},
},
flyLine: {
color: 0xff0000, // 飞线的颜色
speed: 0.05, // 飞机拖尾线速度
flyLineColor: 0xffffff // 飞行线的颜色
},
});
this.earth.init().then(() => {
this.scene.add(this.earth.earthGroup); // 使用 this.scene 而不是 this.earthGroup
this.tick();
});
});
}
/**
* 渲染函数
*/
tick = () => {
this.controls.update(); // 更新控制器
if (this.earth) {
this.earth.render();
}
this.renderer.render(this.scene, this.camera); // 渲染页面
requestAnimationFrame(this.tick); // 使页面一直执行
}
/**
* 销毁方法,用于清理资源
*/
destroy() {
// 停止渲染循环
cancelAnimationFrame(this.tick as any);
// 销毁子组件
if (this.earth) {
this.earth.destroy();
}
if (this.res) {
this.res.destroy();
}
if (this.sizes) {
this.sizes.destroy();
}
// 销毁基础组件
super.destroy();
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

BIN
dist/assets/earth-74238849.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

BIN
dist/assets/favicon-6c6593f1.ico vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

BIN
dist/assets/glow-ef19d813.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

BIN
dist/assets/gradient-b59efedb.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

15
dist/assets/gsap-54c24ddd.js vendored Normal file

File diff suppressed because one or more lines are too long

313
dist/assets/index-766f4bf0.js vendored Normal file

File diff suppressed because one or more lines are too long

1
dist/assets/index-9962e5e1.css vendored Normal file

File diff suppressed because one or more lines are too long

BIN
dist/assets/jitu-0ae999cf.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
dist/assets/label-f2c3f6ed.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

BIN
dist/assets/light_column-aa132c89.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

BIN
dist/assets/order1-85843c41.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

BIN
dist/assets/order2-dcefcc30.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

BIN
dist/assets/order3-c3910ed9.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

BIN
dist/assets/pageBg-0f2bff15.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 289 KiB

BIN
dist/assets/redCircle-61b74946.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

BIN
dist/assets/ssssss-c7e62842.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

3112
dist/assets/three-6ab3b202.js vendored Normal file

File diff suppressed because one or more lines are too long

BIN
dist/assets/titles-0941b966.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

17
dist/assets/vue-3f711dbc.js vendored Normal file

File diff suppressed because one or more lines are too long

BIN
dist/images/20251231114626_961_154.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

BIN
dist/images/earth/aircraft.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

BIN
dist/images/earth/aperture.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
dist/images/earth/earth.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

BIN
dist/images/earth/earths.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 MiB

BIN
dist/images/earth/glow.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

BIN
dist/images/earth/gradient.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

BIN
dist/images/earth/label-old.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 504 KiB

BIN
dist/images/earth/label.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
dist/images/earth/light_column.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

BIN
dist/images/earth/redCircle.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

BIN
dist/images/light_bg-DEu33aaa.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

BIN
dist/images/light_bg-DEu33aaas.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

BIN
dist/images/light_bg-DEu33pwq.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

BIN
dist/images/pageBg.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 289 KiB

BIN
dist/images/title.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

BIN
dist/images/titles.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

22
dist/index.html vendored Normal file
View File

@@ -0,0 +1,22 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/assets/favicon-6c6593f1.ico">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/qweather-icons@1.3.0/font/qweather-icons.css">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>即途充电站大数据可视化系统</title>
<script type="module" crossorigin src="/assets/index-766f4bf0.js"></script>
<link rel="modulepreload" crossorigin href="/assets/vue-3f711dbc.js">
<link rel="modulepreload" crossorigin href="/assets/three-6ab3b202.js">
<link rel="modulepreload" crossorigin href="/assets/gsap-54c24ddd.js">
<link rel="stylesheet" href="/assets/index-9962e5e1.css">
</head>
<body>
<div id="app"></div>
</body>
</html>

BIN
favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

141
flexible.js Normal file
View File

@@ -0,0 +1,141 @@
(function() {
// flexible.css
var cssText =
'' +
'@charset "utf-8";html{color:#000;background:#fff;overflow-y:scroll;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;-webkit-overflow-scrolling:touch}html *{outline:0;-webkit-text-size-adjust:none;-webkit-tap-highlight-color:transparent}body,html{font-family:"Microsoft YaHei",sans-serif,Tahoma,Arial}article,aside,blockquote,body,button,code,dd,details,div,dl,dt,fieldset,figcaption,figure,footer,form,h1,h2,h3,h4,h5,h6,header,hgroup,hr,input,legend,li,menu,nav,ol,p,pre,section,td,textarea,th,ul{margin:0;padding:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}article,aside,details,figcaption,figure,footer,header,hgroup,menu,nav,section{display:block}input,input[type=button],input[type=reset],input[type=submit]{resize:none;border:none;-webkit-appearance:none;border-radius:0}input,select,textarea{font-size:100%}table{border-collapse:collapse;border-spacing:0}fieldset,img{border:0}abbr,acronym{border:0;font-variant:normal}del{text-decoration:line-through}address,caption,cite,code,dfn,em,th,var{font-style:normal;font-weight:500}ol,ul{list-style:none}caption,th{text-align:left}h1,h2,h3,h4,h5,h6{font-size:100%;font-weight:500}q:after,q:before{content:\'\'}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}a:hover{text-decoration:underline}a,ins{text-decoration:none}a:active,a:hover,a:link,a:visited{background:0 0;-webkit-tap-highlight-color:transparent;-webkit-tap-highlight-color:transparent;outline:0;text-decoration:none}';
// cssText end
var styleEl = document.createElement('style');
document.getElementsByTagName('head')[0].appendChild(styleEl);
if (styleEl.styleSheet) {
if (!styleEl.styleSheet.disabled) {
styleEl.styleSheet.cssText = cssText;
}
} else {
try {
styleEl.innerHTML = cssText;
} catch (e) {
styleEl.innerText = cssText;
}
}
})();
;
(function(win, lib) {
var doc = win.document;
var docEl = doc.documentElement;
var metaEl = doc.querySelector('meta[name="viewport"]');
var flexibleEl = doc.querySelector('meta[name="flexible"]');
var dpr = 0;
var scale = 0;
var tid;
var flexible = lib.flexible || (lib.flexible = {});
if (metaEl) {
console.warn('将根据已有的meta标签来设置缩放比例');
var match = metaEl.getAttribute('content').match(/initial\-scale=([\d\.]+)/);
if (match) {
scale = parseFloat(match[1]);
dpr = parseInt(1 / scale);
}
} else if (flexibleEl) {
var content = flexibleEl.getAttribute('content');
if (content) {
var initialDpr = content.match(/initial\-dpr=([\d\.]+)/);
var maximumDpr = content.match(/maximum\-dpr=([\d\.]+)/);
if (initialDpr) {
dpr = parseFloat(initialDpr[1]);
scale = parseFloat((1 / dpr).toFixed(2));
}
if (maximumDpr) {
dpr = parseFloat(maximumDpr[1]);
scale = parseFloat((1 / dpr).toFixed(2));
}
}
}
if (!dpr && !scale) {
var isAndroid = win.navigator.appVersion.match(/android/gi);
var isIPhone = win.navigator.appVersion.match(/iphone/gi);
var devicePixelRatio = win.devicePixelRatio;
if (isIPhone) {
// iOS下对于2和3的屏用2倍的方案其余的用1倍方案
if (devicePixelRatio >= 3 && (!dpr || dpr >= 3)) {
dpr = 3;
} else if (devicePixelRatio >= 2 && (!dpr || dpr >= 2)) {
dpr = 2;
} else {
dpr = 1;
}
} else {
// 其他设备下仍旧使用1倍的方案
dpr = 1;
}
scale = 1 / dpr;
}
docEl.setAttribute('data-dpr', dpr);
if (!metaEl) {
metaEl = doc.createElement('meta');
metaEl.setAttribute('name', 'viewport');
metaEl.setAttribute('content', 'initial-scale=' + scale + ', maximum-scale=' + scale + ', minimum-scale=' + scale + ', user-scalable=no');
if (docEl.firstElementChild) {
docEl.firstElementChild.appendChild(metaEl);
} else {
var wrap = doc.createElement('div');
wrap.appendChild(metaEl);
doc.write(wrap.innerHTML);
}
}
function refreshRem() {
var width = docEl.getBoundingClientRect().width;
if (width / dpr > 540) {
width = width * dpr;
}
var rem = width / 10;
docEl.style.fontSize = rem + 'px';
flexible.rem = win.rem = rem;
}
win.addEventListener('resize', function() {
clearTimeout(tid);
tid = setTimeout(refreshRem, 300);
}, false);
win.addEventListener('pageshow', function(e) {
if (e.persisted) {
clearTimeout(tid);
tid = setTimeout(refreshRem, 300);
}
}, false);
if (doc.readyState === 'complete') {
doc.body.style.fontSize = 12 * dpr + 'px';
} else {
doc.addEventListener('DOMContentLoaded', function(e) {
doc.body.style.fontSize = 12 * dpr + 'px';
}, false);
}
refreshRem();
flexible.dpr = win.dpr = dpr;
flexible.refreshRem = refreshRem;
flexible.rem2px = function(d) {
var val = parseFloat(d) * this.rem;
if (typeof d === 'string' && d.match(/rem$/)) {
val += 'px';
}
return val;
}
flexible.px2rem = function(d) {
var val = parseFloat(d) / this.rem;
if (typeof d === 'string' && d.match(/px$/)) {
val += 'rem';
}
return val;
}
})(window, window['lib'] || (window['lib'] = {}));

17
index.html Normal file
View File

@@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/qweather-icons@1.3.0/font/qweather-icons.css">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>即途充电站大数据可视化系统</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

16832
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

77
package.json Normal file
View File

@@ -0,0 +1,77 @@
{
"name": "3d-earth",
"version": "1.0.0",
"description": "3d-earth",
"keywords": [
"GhostCat",
"3d",
"threejs",
"typescript",
"vue"
],
"type": "module",
"author": "GhostCat",
"license": "MIT",
"scripts": {
"dev": "vite",
"build": "vue-tsc --noEmit && vite build",
"serve": "vite preview",
"lint": "eslint",
"prepare": "husky install"
},
"lint-staged": {
"src/*.{js,jsx,tsx,ts,vue}": "eslint --fix"
},
"homepage": "https://gcat.cc",
"devDependencies": {
"@babel/core": "^7.12.10",
"@babel/preset-env": "^7.12.11",
"@commitlint/config-conventional": "^15.0.0",
"@size-limit/preset-small-lib": "^7.0.3",
"@types/lodash": "^4.14.172",
"@types/minimatch": "^6.0.0",
"@types/three": "^0.144.0",
"@typescript-eslint/eslint-plugin": "^5.13.0",
"@typescript-eslint/parser": "^5.13.0",
"@vitejs/plugin-vue": "^4.0.0",
"babel-loader": "^8.2.2",
"clean-webpack-plugin": "^3.0.0",
"copy-webpack-plugin": "^9.0.1",
"core-js": "^3.8.1",
"css-loader": "^6.2.0",
"eslint": "^8.10.0",
"eslint-webpack-plugin": "^3.1.1",
"html-webpack-plugin": "^4.5.0",
"husky": "^7.0.0",
"lint-staged": "^12.1.2",
"style-loader": "^3.2.1",
"ts-loader": "^8.0.12",
"ts-shader-loader": "^1.0.6",
"typescript": "^4.1.3",
"vite": "^4.0.0",
"vue-tsc": "^1.0.11"
},
"dependencies": {
"@dataview/datav-vue3": "^0.0.0-test.1672506674342",
"@jiaminghi/data-view": "^2.10.0",
"@kjgl77/datav-vue3": "^1.7.4",
"@tweakpane/core": "^1.0.6",
"autofit.js": "^3.2.8",
"axios": "^1.13.2",
"echarts": "^6.0.0",
"element-plus": "^2.13.0",
"eslint-webpack-plugin": "^3.1.1",
"gsap": "^3.7.1",
"html2canvas": "^1.4.1",
"lodash": "^4.17.21",
"pietile-eventemitter": "^1.0.1",
"postcss-px-to-viewport": "^1.1.1",
"postcss-pxtorem": "^6.1.0",
"sass": "^1.97.1",
"three": "^0.145.0",
"vue": "^3.2.45",
"vue-seamless-scroll": "^1.1.23",
"vue3-scale-box": "^0.1.9",
"vue3-seamless-scroll": "^3.0.2"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 504 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

BIN
public/images/pageBg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 289 KiB

BIN
public/images/title.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

BIN
public/images/titles.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

257
src/App.vue Normal file
View File

@@ -0,0 +1,257 @@
<template>
<div id="app">
<!-- <ScaleBox :width="2220" :height="1050" bgc="transparent" :delay="100" :isFlat="false"> -->
<!-- <sals> -->
<div style="position: fixed;top: 0;left: 0;width: 100%;">
<img src="/public/images/titles.png" style="width: 100%;height: 55px;">
<img src="/public/images/light_bg-DEu33pwq.png"
style="width: 500px;height: 100px;position: absolute;left: 400px;animation: light-go-503410de 3s ease-in-out infinite forwards;">
<!-- 添加的时间显示 -->
<div
style="position: absolute;right: 30px;top: 5px;color:white;font-size:20px;display: flex;align-items: center;color: #3060F7;"
:key="currentTime">
<span v-if="now.text"> <i :class="`qi-${now.icon}`"></i>
&nbsp;
&nbsp;
{{ now.text }} &nbsp;
&nbsp;
{{ now.windDir }} &nbsp;
&nbsp;
{{ now.temp }}°c &nbsp;
&nbsp;</span>
{{ currentTime }} &nbsp;
</div>
<img src="/public/images/20251231114626_961_154.png" style="position: absolute;left: 5%;top: 5px;" alt="">
</div>
<left style="position: fixed;top: 3%;left: 30px;" />
<center ></center>
<right style="position: fixed;top: 3%;right: 30px;" />
<div style="position: fixed;top: 25%;right: 26%;">
<img src="/public/images/light_bg-DEu33aaa.png"
style="width: 100%;height: 10px;position: absolute;left: -3%;animation: light-go-5034104 3s ease-in-out infinite forwards;">
<img style="width: 80%;" src="@/assets/img/jitu.png" alt="">
</div>
<div>
<div id="html2canvas" style="position: absolute; left: -9999px; top: -9999px;"></div>
<div ref="earthContainer" id="earth-container"
style="width: 100%; height: 100%;display: flex;align-items: center;justify-content: center;"></div>
</div>
<!-- </sals> -->
<!-- </ScaleBox> -->
</div>
</template>
<script setup lang="ts">
import autofit from 'autofit.js'
import { ref, onMounted, onUnmounted } from 'vue'
import World from './ts/world/World'
import left from './left.vue'
import right from './right.vue'
import center from './center.vue'
import Api from '@/api/index';
import axios from 'axios';
const earthContainer = ref<HTMLElement | null>(null)
let worldInstance: World | null = null
// 新增:用于保存当前时间的响应式变量
const currentTime = ref<string>(new Date().toLocaleTimeString())
const now = ref<any>({})
function getRandomRecords(arr: any, count = 10) {
if (count >= arr.length) return [...arr];
const result = [];
const usedIndices = new Set();
while (result.length < count) {
const index = Math.floor(Math.random() * arr.length);
if (!usedIndices.has(index)) {
usedIndices.add(index);
result.push(arr[index]);
}
}
return result;
}
onMounted(async () => {
getLocation().then(async (res: any) => {
const response = await axios({
url: `https://devapi.qweather.com/v7/weather/now?location=${res.longitude},${res.latitude}&key=fd9afe7f4325414c809baef7e86b907c`,
method: 'GET',
});
now.value = response.data.now
})
if (earthContainer.value) {
const res = await Api.get('/cityStats/list')
worldInstance = new World({
dom: earthContainer.value,
data: getRandomRecords(res)
})
const interval = setInterval(async () => {
try {
worldInstance?.updateEarthData(getRandomRecords(res));
} catch (error) {
console.error('Failed to update earth data:', error);
}
}, 10000);
}
// 新增:每秒钟更新一次时间
setInterval(() => {
currentTime.value = new Date().toLocaleTimeString()
}, 1000)
})
function getLocation() {
return new Promise((resolve, reject) => {
if (!navigator.geolocation) {
reject(new Error('浏览器不支持地理定位'))
return
}
navigator.geolocation.getCurrentPosition(
(position) => {
resolve({
latitude: position.coords.latitude,
longitude: position.coords.longitude,
accuracy: position.coords.accuracy
})
},
(error) => {
let message = '获取位置失败'
switch (error.code) {
case error.PERMISSION_DENIED:
message = '用户拒绝授权'
break
case error.POSITION_UNAVAILABLE:
message = '位置不可用'
break
case error.TIMEOUT:
message = '请求超时'
break
}
reject(new Error(message))
},
{
enableHighAccuracy: true,
timeout: 10000,
maximumAge: 60000
}
)
})
}
onUnmounted(() => {
if (worldInstance) {
worldInstance.destroy()
worldInstance = null
}
})
autofit.init({
dh: 1080,
dw: 1920,
el: "body",
resize: true
})
</script>
<style>
*,
body {
padding: 0;
margin: 0;
box-sizing: border-box;
}
body {
margin: 0;
padding: 0;
width: 100vw;
height: 100vh;
background-image: url('/public/images/pageBg.png');
/* background-repeat: no-repeat; */
background-size: 100%;
}
.container {
width: 500px;
height: 200px;
}
@keyframes light-go-503410de {
0% {
left: 400px
}
to {
left: 1100px;
opacity: 0
}
}
@keyframes light-go-5034104 {
0% {
top: 0px;
}
30% {
top: 100px;
opacity: 0
}
100% {
top: 0px;
opacity: 0
}
}
.fire-div {
font-size: 20px;
font-weight: 600;
border-top: 3px solid #0cd1eb;
padding: 6px 8px;
min-width: 50px;
background: rgba(40, 108, 181, 0.5);
display: flex;
justify-content: center;
align-items: center;
}
canvas {
width: 100% !important;
height: 100% !important;
}
</style>

39
src/api/index.ts Normal file
View File

@@ -0,0 +1,39 @@
// src/utils/request.js
import axios from 'axios';
import { ElMessage } from 'element-plus' // 引入Element Plus消息组件如果使用Element UI
// 创建 axios 实例
const Api = axios.create({
baseURL: 'http://39.105.28.231:1020', // 后端 API 基础路径
timeout: 5000 // 请求超时时间
});
// 请求拦截器
Api.interceptors.request.use(
(config) => {
// 在发送请求之前做一些操作,比如添加 token
const token = localStorage.getItem('token');
if (token) {
config.headers['Authorization'] = `Bearer ${token}`;
}
return config;
},
(error) => {
// 请求错误处理
return Promise.reject(error);
}
);
// 响应拦截器
Api.interceptors.response.use(
(response) => {
const res = response.data;
return res;
},
(error) => {
// ElMessage.error(error.message || '请求失败');
return Promise.reject(error);
}
);
export default Api;

BIN
src/assets/bei.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 674 KiB

29674
src/assets/china.json Normal file

File diff suppressed because it is too large Load Diff

103310
src/assets/chinaProvince.json Normal file

File diff suppressed because it is too large Load Diff

BIN
src/assets/img/-s-bg_.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
src/assets/img/1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 595 B

BIN
src/assets/img/2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Some files were not shown because too many files have changed in this diff Show More