----优化登录界面
This commit is contained in:
@ -47,7 +47,7 @@ VITE_GLOB_API_URL_PREFIX =
|
||||
# 打包是否开启pwa功能
|
||||
VITE_USE_PWA = false
|
||||
#租户开关
|
||||
VITE_GLOB_TENANT_ENABLED = false
|
||||
#VITE_GLOB_TENANT_ENABLED = false
|
||||
VITE_GLOB_APP_TITLE = 供应链平台
|
||||
#登录时是否需要输入租户码
|
||||
# VITE_GLOB_TENANT_INPUT_REQUIRED = true
|
||||
|
||||
@ -88,6 +88,7 @@
|
||||
"showdown": "^2.1.0",
|
||||
"snowflake-id": "^1.1.0",
|
||||
"sortablejs": "^1.14.0",
|
||||
"three": "0.152.0",
|
||||
"tinymce": "^5.10.3",
|
||||
"vditor": "^3.9.2",
|
||||
"vue": "~3.3.4",
|
||||
|
||||
BIN
src/assets/images/earth-blue-marble.jpg
Normal file
BIN
src/assets/images/earth-blue-marble.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 MiB |
BIN
src/assets/images/particle-sprite.png
Normal file
BIN
src/assets/images/particle-sprite.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
335
src/views/secondDev/Earth3D.vue
Normal file
335
src/views/secondDev/Earth3D.vue
Normal file
@ -0,0 +1,335 @@
|
||||
<template>
|
||||
<div ref="earthContainer" class="earth-container"></div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted, nextTick } from 'vue';
|
||||
import * as THREE from 'three';
|
||||
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';
|
||||
// 地球纹理URL
|
||||
import earthBlueMarble from '../../assets/images/earth-blue-marble.jpg';
|
||||
|
||||
const earthContainer = ref<HTMLDivElement | null>(null);
|
||||
|
||||
let scene: THREE.Scene | null = null;
|
||||
let camera: THREE.PerspectiveCamera | null = null;
|
||||
let renderer: THREE.WebGLRenderer | null = null;
|
||||
let controls: OrbitControls | null = null;
|
||||
let earth: THREE.Mesh | null = null;
|
||||
let markers: THREE.Mesh[] = [];
|
||||
let animationId: number | null = null;
|
||||
|
||||
// 城市数据 - 红色主要城市
|
||||
const primaryCities = [
|
||||
{ name: '广州', lat: 23.1291, lng: 113.2644, color: 0xFA2D30 },
|
||||
];
|
||||
|
||||
// 城市数据 - 黄色次要城市
|
||||
const secondaryCities = [
|
||||
{ name: '北京', lat: 39.9042, lng: 116.4074, color: 0xFFF963 },
|
||||
{ name: '上海', lat: 31.2304, lng: 121.4737, color: 0xFFF963 },
|
||||
{ name: '香港', lat: 22.3964, lng: 114.1095, color: 0xFFF963 },
|
||||
{ name: '新加坡', lat: 1.3521, lng: 103.8198, color: 0xFFF963 },
|
||||
{ name: '东京', lat: 35.6824, lng: 139.759, color: 0xFFF963 },
|
||||
{ name: '伦敦', lat: 51.5074, lng: -0.1278, color: 0xFFF963 },
|
||||
{ name: '迪拜', lat: 25.276987, lng: 55.296249, color: 0xFFF963 },
|
||||
{ name: '纽约', lat: 40.748817, lng: -73.985428, color: 0xFFF963 },
|
||||
{ name: '旧金山', lat: 37.7749, lng: -122.4194, color: 0xFFF963 },
|
||||
{ name: '惉尼', lat: -33.8688, lng: 151.2093, color: 0xFFF963 },
|
||||
{ name: '巴黎', lat: 48.8566, lng: 2.3522, color: 0xFFF963 },
|
||||
];
|
||||
|
||||
const EARTH_RADIUS = 1.0;
|
||||
|
||||
const initScene = () => {
|
||||
if (!earthContainer.value) return;
|
||||
|
||||
const width = earthContainer.value.clientWidth;
|
||||
const height = earthContainer.value.clientHeight;
|
||||
|
||||
// 创建场景
|
||||
scene = new THREE.Scene();
|
||||
|
||||
// 创建相机
|
||||
camera = new THREE.PerspectiveCamera(45, width / height, 0.1, 100);
|
||||
camera.position.set(0.0, 1.5, 3.0);
|
||||
|
||||
// 创建渲染器
|
||||
renderer = new THREE.WebGLRenderer({
|
||||
antialias: true,
|
||||
alpha: true,
|
||||
});
|
||||
renderer.setSize(width, height);
|
||||
renderer.setPixelRatio(window.devicePixelRatio);
|
||||
earthContainer.value.appendChild(renderer.domElement);
|
||||
|
||||
// 创建 OrbitControls
|
||||
controls = new OrbitControls(camera, renderer.domElement);
|
||||
controls.autoRotate = true;
|
||||
controls.autoRotateSpeed = -1.0;
|
||||
controls.enablePan = false;
|
||||
controls.enableZoom = false;
|
||||
controls.enableDamping = true;
|
||||
controls.dampingFactor = 0.05;
|
||||
|
||||
// 添加光源
|
||||
addLights();
|
||||
|
||||
// 创建地球
|
||||
createEarth();
|
||||
|
||||
// 创建大气层
|
||||
createAtmosphere();
|
||||
};
|
||||
|
||||
const addLights = () => {
|
||||
if (!scene) return;
|
||||
|
||||
// 环境光
|
||||
const ambient = new THREE.AmbientLight(0xffffff, 1);
|
||||
scene.add(ambient);
|
||||
|
||||
// 方向光
|
||||
const directional = new THREE.DirectionalLight(0xffffff, 0.5);
|
||||
directional.position.set(5.0, 2.0, 5.0).normalize();
|
||||
scene.add(directional);
|
||||
};
|
||||
|
||||
const createEarth = () => {
|
||||
if (!scene) return;
|
||||
|
||||
const loader = new THREE.TextureLoader();
|
||||
|
||||
loader.load(earthBlueMarble, (texture) => {
|
||||
if (!scene) return;
|
||||
|
||||
// 设置各向异性过滤
|
||||
if (renderer) {
|
||||
texture.anisotropy = renderer.capabilities.getMaxAnisotropy();
|
||||
}
|
||||
|
||||
const geometry = new THREE.SphereGeometry(EARTH_RADIUS, 64, 48);
|
||||
const material = new THREE.MeshStandardMaterial({
|
||||
color: 0xFFFFFF,
|
||||
map: texture,
|
||||
roughness: 0.6,
|
||||
metalness: 0.1,
|
||||
side: THREE.DoubleSide,
|
||||
});
|
||||
|
||||
earth = new THREE.Mesh(geometry, material);
|
||||
scene.add(earth);
|
||||
|
||||
// 地球加载完成后添加标记点
|
||||
createMarkers();
|
||||
});
|
||||
};
|
||||
|
||||
const createAtmosphere = () => {
|
||||
if (!scene) return;
|
||||
|
||||
const atmosphereGeometry = new THREE.SphereGeometry(1.05, 64, 48);
|
||||
const atmosphereMaterial = new THREE.ShaderMaterial({
|
||||
vertexShader: `
|
||||
varying vec3 vertexNormal;
|
||||
void main() {
|
||||
vertexNormal = normalize(normalMatrix * normal);
|
||||
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
||||
}
|
||||
`,
|
||||
fragmentShader: `
|
||||
varying vec3 vertexNormal;
|
||||
void main() {
|
||||
float intensity = pow(0.6 - dot(vertexNormal, vec3(0, 0, 1.0)), 2.0);
|
||||
gl_FragColor = vec4(0.3, 0.6, 1.0, 0.8) * intensity;
|
||||
}
|
||||
`,
|
||||
blending: THREE.AdditiveBlending,
|
||||
side: THREE.BackSide,
|
||||
transparent: true,
|
||||
});
|
||||
|
||||
const atmosphere = new THREE.Mesh(atmosphereGeometry, atmosphereMaterial);
|
||||
atmosphere.scale.set(1.1, 1.1, 1.1);
|
||||
atmosphere.position.x = -0.02;
|
||||
atmosphere.position.y = 0.02;
|
||||
scene.add(atmosphere);
|
||||
};
|
||||
|
||||
// 经纬度转换为3D坐标
|
||||
const latLngToVector3 = (lat: number, lng: number, radius: number): THREE.Vector3 => {
|
||||
const phi = (90 - lat) * (Math.PI / 180);
|
||||
const theta = (lng + 180) * (Math.PI / 180);
|
||||
|
||||
const x = -radius * Math.sin(phi) * Math.cos(theta);
|
||||
const y = radius * Math.cos(phi);
|
||||
const z = radius * Math.sin(phi) * Math.sin(theta);
|
||||
|
||||
return new THREE.Vector3(x, y, z);
|
||||
};
|
||||
|
||||
// 创建标记点
|
||||
const createMarkers = () => {
|
||||
if (!scene) return;
|
||||
|
||||
const markerRadius = 1.06;
|
||||
|
||||
// 创建主要城市标记(红色,较大)
|
||||
primaryCities.forEach((city) => {
|
||||
const marker = createCityMarker(city.lat, city.lng, markerRadius, city.color, 0.02, true);
|
||||
if (marker) {
|
||||
scene!.add(marker);
|
||||
markers.push(marker);
|
||||
}
|
||||
});
|
||||
|
||||
// 创建次要城市标记(黄色,较小)
|
||||
secondaryCities.forEach((city) => {
|
||||
const marker = createCityMarker(city.lat, city.lng, markerRadius, city.color, 0.01, false);
|
||||
if (marker) {
|
||||
scene!.add(marker);
|
||||
markers.push(marker);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 创建城市标记
|
||||
const createCityMarker = (
|
||||
lat: number,
|
||||
lng: number,
|
||||
radius: number,
|
||||
color: number,
|
||||
size: number,
|
||||
hasRing: boolean
|
||||
): THREE.Mesh | null => {
|
||||
// 创建圆形标记
|
||||
const geometry = new THREE.CircleGeometry(size, 32);
|
||||
const material = new THREE.MeshBasicMaterial({
|
||||
color: color,
|
||||
side: THREE.DoubleSide,
|
||||
});
|
||||
const marker = new THREE.Mesh(geometry, material);
|
||||
|
||||
// 添加外环
|
||||
if (hasRing) {
|
||||
const ringGeometry = new THREE.RingGeometry(size * 1.2, size * 1.7, 32);
|
||||
const ringMaterial = new THREE.MeshBasicMaterial({
|
||||
color: color,
|
||||
side: THREE.DoubleSide,
|
||||
});
|
||||
const ring = new THREE.Mesh(ringGeometry, ringMaterial);
|
||||
marker.add(ring);
|
||||
} else {
|
||||
// 次要城市添加较小的外环
|
||||
const ringGeometry = new THREE.RingGeometry(size * 1.5, size * 1.8, 32);
|
||||
const ringMaterial = new THREE.MeshBasicMaterial({
|
||||
color: 0x898A51,
|
||||
side: THREE.DoubleSide,
|
||||
});
|
||||
const ring = new THREE.Mesh(ringGeometry, ringMaterial);
|
||||
marker.add(ring);
|
||||
}
|
||||
|
||||
// 计算位置
|
||||
const position = latLngToVector3(lat, lng, radius);
|
||||
marker.position.copy(position);
|
||||
|
||||
// 让标记朝向球心外侧
|
||||
marker.lookAt(new THREE.Vector3(0, 0, 0));
|
||||
marker.rotateX(Math.PI);
|
||||
|
||||
return marker;
|
||||
};
|
||||
|
||||
const animate = () => {
|
||||
if (!scene || !camera || !renderer) return;
|
||||
|
||||
animationId = requestAnimationFrame(animate);
|
||||
|
||||
// 更新控制器
|
||||
if (controls) {
|
||||
controls.update();
|
||||
}
|
||||
|
||||
// 让标记点始终面向相机
|
||||
markers.forEach((marker) => {
|
||||
marker.quaternion.copy(camera!.quaternion);
|
||||
});
|
||||
|
||||
renderer.render(scene, camera);
|
||||
};
|
||||
|
||||
const onResize = () => {
|
||||
if (!earthContainer.value || !camera || !renderer) return;
|
||||
|
||||
const width = earthContainer.value.clientWidth;
|
||||
const height = earthContainer.value.clientHeight;
|
||||
|
||||
camera.aspect = width / height;
|
||||
camera.updateProjectionMatrix();
|
||||
renderer.setSize(width, height);
|
||||
};
|
||||
|
||||
const dispose = () => {
|
||||
if (animationId) {
|
||||
cancelAnimationFrame(animationId);
|
||||
}
|
||||
|
||||
if (controls) {
|
||||
controls.dispose();
|
||||
}
|
||||
|
||||
// 清理标记
|
||||
markers.forEach((marker) => {
|
||||
marker.geometry?.dispose();
|
||||
if (marker.material instanceof THREE.Material) {
|
||||
marker.material.dispose();
|
||||
}
|
||||
});
|
||||
markers = [];
|
||||
|
||||
// 清理地球
|
||||
if (earth) {
|
||||
earth.geometry?.dispose();
|
||||
if (earth.material instanceof THREE.Material) {
|
||||
earth.material.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
if (renderer) {
|
||||
renderer.dispose();
|
||||
if (earthContainer.value && renderer.domElement) {
|
||||
earthContainer.value.removeChild(renderer.domElement);
|
||||
}
|
||||
}
|
||||
|
||||
scene = null;
|
||||
camera = null;
|
||||
renderer = null;
|
||||
controls = null;
|
||||
earth = null;
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
await nextTick();
|
||||
initScene();
|
||||
animate();
|
||||
window.addEventListener('resize', onResize);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
dispose();
|
||||
window.removeEventListener('resize', onResize);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.earth-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 50;
|
||||
}
|
||||
</style>
|
||||
@ -53,6 +53,8 @@
|
||||
itemRefs.value.push(el);
|
||||
};
|
||||
|
||||
|
||||
|
||||
onBeforeUpdate(() => {
|
||||
itemRefs.value = [];
|
||||
});
|
||||
|
||||
@ -1,16 +1,25 @@
|
||||
<template>
|
||||
<div :class="prefixCls" class="login-box relative w-full h-full" v-if="!isSingleLogin">
|
||||
<div class="center-box">
|
||||
<div class="login-left-title">
|
||||
<div class="sub-title">{{ sysName }}</div>
|
||||
<div class="t-container">
|
||||
<StarBackground3 />
|
||||
<div :class="prefixCls" class="login-box relative w-full h-full" v-if="!isSingleLogin">
|
||||
<div class="center-box">
|
||||
<div class="login-left-title">
|
||||
<div class="sub-title">{{ sysName }}</div>
|
||||
</div>
|
||||
<div :class="`${prefixCls}-form`">
|
||||
<LoginForm />
|
||||
</div>
|
||||
</div>
|
||||
<div :class="`${prefixCls}-form`">
|
||||
<LoginForm />
|
||||
<div class="earth-wrapper">
|
||||
<Earth3D />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="width: 100%; height: 300px; display: flex; justify-content: center; align-items: center" v-else>
|
||||
<a-spin />
|
||||
<div style="width: 100%; height: 300px; display: flex; justify-content: center; align-items: center" v-else>
|
||||
<a-spin />
|
||||
</div>
|
||||
<div v-if="isSingleLogin" class="absolute top-0 left-0 right-0 bottom-0 flex justify-center items-center">
|
||||
<a-spin />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
@ -26,6 +35,8 @@
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
import { useI18n } from '/@/hooks/web/useI18n';
|
||||
import StarBackground3 from './StarBackground3.vue';
|
||||
import Earth3D from './Earth3D.vue';
|
||||
|
||||
const { currentRoute } = useRouter();
|
||||
const userStore = useUserStore();
|
||||
@ -89,11 +100,29 @@
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
.t-container{
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
background: #000;
|
||||
}
|
||||
|
||||
.login-box {
|
||||
display: flex;
|
||||
background: linear-gradient(180deg, #00356d 0%, rgb(0 53 109 / 0%) 100%), linear-gradient(180deg, #0074d3 2%, #011853 100%);
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
// display: flex;
|
||||
// //background: linear-gradient(180deg, #00356d 0%, rgb(0 53 109 / 0%) 100%), linear-gradient(180deg, #0074d3 2%, #011853 100%);
|
||||
// align-items: center;
|
||||
// justify-content: center;
|
||||
|
||||
|
||||
}
|
||||
|
||||
.earth-wrapper {
|
||||
position: absolute;
|
||||
left: 30%;
|
||||
top: 50%;
|
||||
transform: translate(-70%, -50%);
|
||||
width: 700px;
|
||||
height: 700px;
|
||||
}
|
||||
|
||||
.logo-box {
|
||||
@ -107,7 +136,14 @@
|
||||
}
|
||||
|
||||
.center-box {
|
||||
//background-color: #fff;
|
||||
border: 1px solid #eaeaea;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
background-color: #fff;
|
||||
position: absolute;
|
||||
right: 15%;
|
||||
top: 20%;
|
||||
}
|
||||
|
||||
.login-left-title {
|
||||
|
||||
@ -68,14 +68,14 @@
|
||||
</Checkbox>
|
||||
</FormItem>
|
||||
</ACol>
|
||||
<ACol :span="12" style="text-align: right">
|
||||
<FormItem>
|
||||
<!-- <ACol :span="12" style="text-align: right">
|
||||
<FormItem> -->
|
||||
<!-- No logic, you need to deal with it yourself -->
|
||||
<Button type="link" class="f-16" @click="changeLoginType" size="small">
|
||||
<!-- <Button type="link" class="f-16" @click="changeLoginType" size="small">
|
||||
{{ loginType == 'mobile' ? t('账号密码登录') : t('手机登录') }}
|
||||
</Button>
|
||||
</FormItem>
|
||||
</ACol>
|
||||
</ACol> -->
|
||||
<!-- <ACol :span="12">
|
||||
<FormItem :style="{ 'text-align': 'right' }">
|
||||
No logic, you need to deal with it yourself
|
||||
|
||||
272
src/views/secondDev/StarBackground3.vue
Normal file
272
src/views/secondDev/StarBackground3.vue
Normal file
@ -0,0 +1,272 @@
|
||||
<template>
|
||||
<div class="star-background">
|
||||
<div ref="starContainer" class="star-container"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted, nextTick } from 'vue';
|
||||
import * as THREE from 'three';
|
||||
import particleSprite from '../../assets/images/particle-sprite.png';
|
||||
|
||||
const starContainer = ref(null);
|
||||
let scene = null;
|
||||
let camera = null;
|
||||
let renderer = null;
|
||||
let mesh = null;
|
||||
let animationId = null;
|
||||
let texture = null;
|
||||
const fieldRadius = 20;
|
||||
const fieldZLength = 40;
|
||||
|
||||
const initScene = async () => {
|
||||
if (!starContainer.value) return;
|
||||
|
||||
// 创建场景和相机
|
||||
scene = new THREE.Scene();
|
||||
camera = new THREE.PerspectiveCamera(
|
||||
75,
|
||||
starContainer.value.clientWidth / starContainer.value.clientHeight,
|
||||
0.1,
|
||||
1000
|
||||
);
|
||||
camera.position.z = 5;
|
||||
|
||||
// 创建渲染器
|
||||
renderer = new THREE.WebGLRenderer({
|
||||
antialias: true,
|
||||
alpha: true,
|
||||
preserveDrawingBuffer: true // 保留缓冲区以确保正确渲染
|
||||
});
|
||||
renderer.setSize(
|
||||
starContainer.value.clientWidth,
|
||||
starContainer.value.clientHeight
|
||||
);
|
||||
renderer.setPixelRatio(window.devicePixelRatio); // 处理高DPI屏幕
|
||||
starContainer.value.appendChild(renderer.domElement);
|
||||
|
||||
// 创建星空几何体
|
||||
createStars();
|
||||
};
|
||||
|
||||
const createStars = () => {
|
||||
if (!scene) return;
|
||||
|
||||
const corner = [];
|
||||
const colorMix = [];
|
||||
const geoIndex = [];
|
||||
const posArr = [];
|
||||
const uvArr = [];
|
||||
|
||||
const geo = new THREE.BufferGeometry();
|
||||
|
||||
// 循环生成星星
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
const [x, y, z] = [
|
||||
THREE.MathUtils.mapLinear(Math.random(), 0, 1, -fieldRadius, fieldRadius),
|
||||
THREE.MathUtils.mapLinear(Math.random(), 0, 1, -fieldRadius, fieldRadius),
|
||||
THREE.MathUtils.mapLinear(Math.random(), 0, 1, -fieldZLength / 2, fieldZLength / 2)
|
||||
];
|
||||
|
||||
const mix = Math.random();
|
||||
for (let j = 0; j < 4; j++) {
|
||||
posArr.push(x, y, z);
|
||||
corner.push(j);
|
||||
colorMix.push(mix);
|
||||
}
|
||||
|
||||
uvArr.push(0, 1);
|
||||
uvArr.push(1, 1);
|
||||
uvArr.push(0, 0);
|
||||
uvArr.push(1, 0);
|
||||
|
||||
const index = 4 * i;
|
||||
geoIndex.push(index, index + 1, index + 2);
|
||||
geoIndex.push(index + 1, index + 3, index + 2);
|
||||
}
|
||||
|
||||
geo.setAttribute('position', new THREE.BufferAttribute(new Float32Array(posArr), 3));
|
||||
geo.setAttribute('uv', new THREE.BufferAttribute(new Float32Array(uvArr), 2));
|
||||
geo.setAttribute('corner', new THREE.BufferAttribute(new Float32Array(corner), 1));
|
||||
geo.setAttribute('colorMix', new THREE.BufferAttribute(new Float32Array(colorMix), 1));
|
||||
geo.setIndex(geoIndex);
|
||||
geo.needsUpdate = true;
|
||||
|
||||
// 顶点着色器
|
||||
const vertexShader = `
|
||||
attribute float corner;
|
||||
attribute float colorMix;
|
||||
uniform float u_time;
|
||||
varying vec2 vUv;
|
||||
varying vec3 vColor;
|
||||
|
||||
uniform float zMin;
|
||||
uniform float zMax;
|
||||
uniform vec3 color1;
|
||||
uniform vec3 color2;
|
||||
|
||||
void main() {
|
||||
vUv = uv;
|
||||
vec3 pos = position;
|
||||
pos.z += u_time;
|
||||
pos.z = mod(pos.z, zMax - zMin) + zMin; // 修复模运算
|
||||
|
||||
|
||||
vec4 worldPos = modelMatrix * vec4(pos, 1.0);
|
||||
vec4 viewPosition = viewMatrix * worldPos;
|
||||
|
||||
float offset = 0.1;
|
||||
|
||||
if (corner == 0.0) {
|
||||
viewPosition.xy += vec2(-offset, -offset);
|
||||
}
|
||||
if (corner == 1.0) {
|
||||
viewPosition.xy += vec2(offset, -offset);
|
||||
}
|
||||
if (corner == 2.0) {
|
||||
viewPosition.xy += vec2(-offset, offset);
|
||||
}
|
||||
if (corner == 3.0) {
|
||||
viewPosition.xy += vec2(offset, offset);
|
||||
}
|
||||
|
||||
vColor = mix(color1, color2, colorMix);
|
||||
|
||||
gl_Position = projectionMatrix * viewPosition;
|
||||
}
|
||||
`;
|
||||
|
||||
// 片元着色器
|
||||
const fragmentShader = `
|
||||
varying vec2 vUv;
|
||||
varying vec3 vColor;
|
||||
uniform sampler2D u_texture;
|
||||
void main() {
|
||||
vec4 texel = texture2D(u_texture, vUv);
|
||||
|
||||
float alpha = texel.r;
|
||||
|
||||
vec3 color = mix(vColor, vec3(1.0), texel.r);
|
||||
|
||||
gl_FragColor = vec4(color, alpha);
|
||||
}
|
||||
`;
|
||||
|
||||
// 加载粒子纹理
|
||||
const loader = new THREE.TextureLoader();
|
||||
const particleTexture = loader.load(particleSprite);
|
||||
|
||||
// 创建材质
|
||||
const material = new THREE.ShaderMaterial({
|
||||
vertexShader: vertexShader,
|
||||
fragmentShader: fragmentShader,
|
||||
uniforms: {
|
||||
u_texture: {
|
||||
value: particleTexture // 使用粒子纹理
|
||||
},
|
||||
color1: {
|
||||
value: new THREE.Color(0x3068FF)
|
||||
},
|
||||
color2: {
|
||||
value: new THREE.Color(0xF34F94)
|
||||
},
|
||||
zMin: {
|
||||
value: -fieldZLength / 2
|
||||
},
|
||||
zMax: {
|
||||
value: fieldZLength / 2 // 修复zMax值
|
||||
},
|
||||
u_time: {
|
||||
value: 0
|
||||
},
|
||||
},
|
||||
transparent: true,
|
||||
blending: THREE.AdditiveBlending // 添加混合模式
|
||||
});
|
||||
|
||||
// 创建网格
|
||||
mesh = new THREE.Mesh(geo, material);
|
||||
scene.add(mesh);
|
||||
};
|
||||
|
||||
const animate = () => {
|
||||
if (!scene || !camera || !renderer) return;
|
||||
|
||||
animationId = requestAnimationFrame(animate);
|
||||
|
||||
if (mesh) {
|
||||
mesh.material.uniforms.u_time.value += 0.02;
|
||||
}
|
||||
|
||||
renderer.render(scene, camera);
|
||||
};
|
||||
|
||||
const onResize = () => {
|
||||
if (!starContainer.value || !camera || !renderer) return;
|
||||
|
||||
camera.aspect = starContainer.value.clientWidth / starContainer.value.clientHeight;
|
||||
camera.updateProjectionMatrix();
|
||||
renderer.setSize(
|
||||
starContainer.value.clientWidth,
|
||||
starContainer.value.clientHeight
|
||||
);
|
||||
};
|
||||
|
||||
const dispose = () => {
|
||||
if (animationId) {
|
||||
cancelAnimationFrame(animationId);
|
||||
}
|
||||
|
||||
// 清理几何体
|
||||
if (mesh) {
|
||||
if (mesh.geometry) mesh.geometry.dispose();
|
||||
if (mesh.material) {
|
||||
if (Array.isArray(mesh.material)) {
|
||||
mesh.material.forEach(material => material.dispose());
|
||||
} else {
|
||||
mesh.material.dispose();
|
||||
}
|
||||
}
|
||||
scene.remove(mesh);
|
||||
}
|
||||
|
||||
// 清理渲染器
|
||||
if (renderer) {
|
||||
renderer.dispose();
|
||||
}
|
||||
|
||||
// 清理dom元素
|
||||
if (renderer && renderer.domElement && starContainer.value && starContainer.value.contains(renderer.domElement)) {
|
||||
starContainer.value.removeChild(renderer.domElement);
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
await nextTick(); // 确保DOM已渲染
|
||||
initScene();
|
||||
animate();
|
||||
window.addEventListener('resize', onResize);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
dispose();
|
||||
window.removeEventListener('resize', onResize);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.star-background {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.star-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden; /* 防止出现滚动条 */
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user