first commit
29
.commitlintrc.js
Normal 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
@@ -0,0 +1 @@
|
||||
VITE_BASE_API=http://192.168.1.194:1020
|
||||
2
.env.production
Normal file
@@ -0,0 +1,2 @@
|
||||
VITE_BASE_API=http://dvapi.prod.zhongshuai2023.com
|
||||
|
||||
16
.eslintrc.js
Normal 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
@@ -0,0 +1 @@
|
||||
*.js linguist-detectable=false
|
||||
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
node_modules
|
||||
5
.husky/pre-commit
Normal file
@@ -0,0 +1,5 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
npx lint-staged
|
||||
|
||||
8
.prettierrc.js
Normal 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
|
After Width: | Height: | Size: 1.8 MiB |
21
LICENSE
Normal 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
@@ -0,0 +1,6 @@
|
||||
# Webpack 5 + Typescript 4 + Three.js 基础模板
|
||||
|
||||
- Webpack 5
|
||||
- Typescript 4
|
||||
- Three.js 130
|
||||
- lodash
|
||||
14
README.md
Normal file
@@ -0,0 +1,14 @@
|
||||
# 3d-earth
|
||||
本项目使用 [three-ts-webpack](https://github.com/GhostCatcg/three-ts-webpack) 构建
|
||||
|
||||
[Live Demo](https://gcat.cc/demo/earth)
|
||||
|
||||

|
||||
## Todolist
|
||||
1. - [x] 加载效果[loading...]
|
||||
2. - [x] 地球、以及星空背景🌏
|
||||
3. - [x] 辉光以及大气层✨
|
||||
4. - [x] 地球标点以及城市标签🇨🇳
|
||||
5. - [x] 卫星环绕旋转🛰
|
||||
6. - [x] 国家/城市之前的飞线🪐
|
||||
7. - [ ] 飞机沿飞线飞行🛫
|
||||
144
d/zy/three/3d-earth/src/ts/Utils/common.ts
Normal 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);
|
||||
}
|
||||
50
d/zy/three/3d-earth/src/ts/world/Assets.ts
Normal 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
|
||||
}
|
||||
91
d/zy/three/3d-earth/src/ts/world/Basic.ts
Normal 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; // 添加默认导出
|
||||
200
d/zy/three/3d-earth/src/ts/world/Earth.ts
Normal 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() // 创建柱状点位
|
||||
73
d/zy/three/3d-earth/src/ts/world/Resources.ts
Normal 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 = {};
|
||||
}
|
||||
}
|
||||
119
d/zy/three/3d-earth/src/ts/world/World.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
BIN
dist/assets/20251231114626_961_154-3c8fdbef.png
vendored
Normal file
|
After Width: | Height: | Size: 4.5 KiB |
BIN
dist/assets/earth-74238849.jpg
vendored
Normal file
|
After Width: | Height: | Size: 2.4 MiB |
BIN
dist/assets/favicon-6c6593f1.ico
vendored
Normal file
|
After Width: | Height: | Size: 110 KiB |
BIN
dist/assets/glow-ef19d813.png
vendored
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
dist/assets/gradient-b59efedb.png
vendored
Normal file
|
After Width: | Height: | Size: 5.5 KiB |
15
dist/assets/gsap-54c24ddd.js
vendored
Normal file
313
dist/assets/index-766f4bf0.js
vendored
Normal file
1
dist/assets/index-9962e5e1.css
vendored
Normal file
BIN
dist/assets/jitu-0ae999cf.png
vendored
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
dist/assets/label-f2c3f6ed.png
vendored
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
dist/assets/light_bg-DEu33pwq-09f9d8c7.png
vendored
Normal file
|
After Width: | Height: | Size: 4.8 KiB |
BIN
dist/assets/light_column-aa132c89.png
vendored
Normal file
|
After Width: | Height: | Size: 4.6 KiB |
BIN
dist/assets/order1-85843c41.png
vendored
Normal file
|
After Width: | Height: | Size: 5.4 KiB |
BIN
dist/assets/order2-dcefcc30.png
vendored
Normal file
|
After Width: | Height: | Size: 5.1 KiB |
BIN
dist/assets/order3-c3910ed9.png
vendored
Normal file
|
After Width: | Height: | Size: 5.3 KiB |
BIN
dist/assets/pageBg-0f2bff15.png
vendored
Normal file
|
After Width: | Height: | Size: 289 KiB |
BIN
dist/assets/redCircle-61b74946.png
vendored
Normal file
|
After Width: | Height: | Size: 42 KiB |
BIN
dist/assets/ssssss-c7e62842.png
vendored
Normal file
|
After Width: | Height: | Size: 4.0 KiB |
3112
dist/assets/three-6ab3b202.js
vendored
Normal file
BIN
dist/assets/titles-0941b966.png
vendored
Normal file
|
After Width: | Height: | Size: 126 KiB |
17
dist/assets/vue-3f711dbc.js
vendored
Normal file
BIN
dist/images/20251231114626_961_154.png
vendored
Normal file
|
After Width: | Height: | Size: 4.5 KiB |
BIN
dist/images/earth/aircraft.png
vendored
Normal file
|
After Width: | Height: | Size: 3.6 KiB |
BIN
dist/images/earth/aperture.png
vendored
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
dist/images/earth/earth.jpg
vendored
Normal file
|
After Width: | Height: | Size: 2.4 MiB |
BIN
dist/images/earth/earths.jpg
vendored
Normal file
|
After Width: | Height: | Size: 3.1 MiB |
BIN
dist/images/earth/glow.png
vendored
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
dist/images/earth/gradient.png
vendored
Normal file
|
After Width: | Height: | Size: 5.5 KiB |
BIN
dist/images/earth/label-old.png
vendored
Normal file
|
After Width: | Height: | Size: 504 KiB |
BIN
dist/images/earth/label.png
vendored
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
dist/images/earth/light_column.png
vendored
Normal file
|
After Width: | Height: | Size: 4.6 KiB |
BIN
dist/images/earth/redCircle.png
vendored
Normal file
|
After Width: | Height: | Size: 42 KiB |
BIN
dist/images/light_bg-DEu33aaa.png
vendored
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
dist/images/light_bg-DEu33aaas.png
vendored
Normal file
|
After Width: | Height: | Size: 47 KiB |
BIN
dist/images/light_bg-DEu33pwq.png
vendored
Normal file
|
After Width: | Height: | Size: 4.8 KiB |
BIN
dist/images/pageBg.png
vendored
Normal file
|
After Width: | Height: | Size: 289 KiB |
BIN
dist/images/title.png
vendored
Normal file
|
After Width: | Height: | Size: 108 KiB |
BIN
dist/images/titles.png
vendored
Normal file
|
After Width: | Height: | Size: 126 KiB |
22
dist/index.html
vendored
Normal 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
|
After Width: | Height: | Size: 110 KiB |
141
flexible.js
Normal 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
@@ -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
77
package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
BIN
public/images/20251231114626_961_154.png
Normal file
|
After Width: | Height: | Size: 4.5 KiB |
BIN
public/images/earth/aircraft.png
Normal file
|
After Width: | Height: | Size: 3.6 KiB |
BIN
public/images/earth/aperture.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
public/images/earth/earth.jpg
Normal file
|
After Width: | Height: | Size: 2.4 MiB |
BIN
public/images/earth/earths.jpg
Normal file
|
After Width: | Height: | Size: 3.1 MiB |
BIN
public/images/earth/glow.png
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
public/images/earth/gradient.png
Normal file
|
After Width: | Height: | Size: 5.5 KiB |
BIN
public/images/earth/label-old.png
Normal file
|
After Width: | Height: | Size: 504 KiB |
BIN
public/images/earth/label.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
public/images/earth/light_column.png
Normal file
|
After Width: | Height: | Size: 4.6 KiB |
BIN
public/images/earth/redCircle.png
Normal file
|
After Width: | Height: | Size: 42 KiB |
BIN
public/images/light_bg-DEu33aaa.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
public/images/light_bg-DEu33aaas.png
Normal file
|
After Width: | Height: | Size: 47 KiB |
BIN
public/images/light_bg-DEu33pwq.png
Normal file
|
After Width: | Height: | Size: 4.8 KiB |
BIN
public/images/pageBg.png
Normal file
|
After Width: | Height: | Size: 289 KiB |
BIN
public/images/title.png
Normal file
|
After Width: | Height: | Size: 108 KiB |
BIN
public/images/titles.png
Normal file
|
After Width: | Height: | Size: 126 KiB |
257
src/App.vue
Normal 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>
|
||||
|
||||
丨
|
||||
{{ now.text }}
|
||||
丨
|
||||
{{ now.windDir }}
|
||||
丨
|
||||
{{ now.temp }}°c
|
||||
丨 </span>
|
||||
{{ currentTime }}
|
||||
</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
@@ -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
|
After Width: | Height: | Size: 674 KiB |
29674
src/assets/china.json
Normal file
103310
src/assets/chinaProvince.json
Normal file
BIN
src/assets/img/-s-bg_.png
Normal file
|
After Width: | Height: | Size: 8.7 KiB |
BIN
src/assets/img/-s-icon-保安队.png
Normal file
|
After Width: | Height: | Size: 7.2 KiB |
BIN
src/assets/img/-s-icon-城管员.png
Normal file
|
After Width: | Height: | Size: 6.6 KiB |
BIN
src/assets/img/-s-icon-巡防队.png
Normal file
|
After Width: | Height: | Size: 8.2 KiB |
BIN
src/assets/img/-s-点缀-1.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
src/assets/img/-s-点缀.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
src/assets/img/1.png
Normal file
|
After Width: | Height: | Size: 595 B |
BIN
src/assets/img/2.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
src/assets/img/big-data/center-details-data1.png
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
src/assets/img/big-data/center-details-data2.png
Normal file
|
After Width: | Height: | Size: 44 KiB |
BIN
src/assets/img/big-data/center-details-data3.png
Normal file
|
After Width: | Height: | Size: 38 KiB |
BIN
src/assets/img/big-data/center-details-data4.png
Normal file
|
After Width: | Height: | Size: 44 KiB |
BIN
src/assets/img/big-data/center-details-data5.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
src/assets/img/big-data/center-details-data6.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
src/assets/img/chatKuang.png
Normal file
|
After Width: | Height: | Size: 8.8 KiB |
BIN
src/assets/img/headers/juxing1.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |