demos
This commit is contained in:
184
src/composes/three/README.md
Normal file
184
src/composes/three/README.md
Normal file
@@ -0,0 +1,184 @@
|
||||
# Three.js 公共函数库
|
||||
|
||||
这是一个经过抽象的 Three.js 公共函数库,用于快速构建 3D 场景。
|
||||
|
||||
## 模块列表
|
||||
|
||||
### 1. useThreeScene - 场景管理
|
||||
创建和管理基础场景、相机、渲染器和控制器。
|
||||
|
||||
```typescript
|
||||
import { useThreeScene } from '@/composes/three'
|
||||
|
||||
const { createScene, createCamera, createRenderer, createControls } = useThreeScene()
|
||||
|
||||
// 创建场景
|
||||
const scene = createScene({
|
||||
backgroundColor: 0x000000,
|
||||
enableFog: true,
|
||||
fogColor: 0x000000,
|
||||
fogNear: 10,
|
||||
fogFar: 50
|
||||
})
|
||||
|
||||
// 创建相机
|
||||
const camera = createCamera({
|
||||
fov: 75,
|
||||
aspect: width / height,
|
||||
position: { x: 0, y: 5, z: 15 }
|
||||
})
|
||||
|
||||
// 创建渲染器
|
||||
const renderer = createRenderer(container, {
|
||||
antialias: true,
|
||||
shadowMap: true
|
||||
})
|
||||
|
||||
// 创建控制器
|
||||
const controls = createControls(camera, renderer)
|
||||
```
|
||||
|
||||
### 2. useEnvironment - 环境管理
|
||||
创建天空盒、背景、地面和流动效果。
|
||||
|
||||
```typescript
|
||||
import { useEnvironment } from '@/composes/three'
|
||||
|
||||
const envUtils = useEnvironment()
|
||||
|
||||
// 创建海蓝色渐变背景
|
||||
envUtils.createOceanGradientBackground(scene)
|
||||
|
||||
// 创建天空盒
|
||||
envUtils.createSkybox(scene, 0x87CEEB, 0x1A1A2E)
|
||||
|
||||
// 创建有厚度的地板
|
||||
const ground = envUtils.createThickGround(100, 0.8, 0x2C3E50)
|
||||
scene.add(ground)
|
||||
|
||||
// 创建地板流动效果
|
||||
const flowingGround = envUtils.createFlowingGroundEffect(100)
|
||||
scene.add(flowingGround.mesh)
|
||||
|
||||
// 在动画循环中更新
|
||||
function animate() {
|
||||
flowingGround.update(time)
|
||||
}
|
||||
```
|
||||
|
||||
### 3. useThreeLighting - 光照管理
|
||||
批量设置场景光照系统。
|
||||
|
||||
```typescript
|
||||
import { useThreeLighting } from '@/composes/three'
|
||||
|
||||
const lightingUtils = useThreeLighting()
|
||||
|
||||
// 批量设置灯光
|
||||
lightingUtils.setupLighting(scene, {
|
||||
ambientLight: true,
|
||||
ambientIntensity: 0.8,
|
||||
directionalLights: [
|
||||
{
|
||||
color: 0xFFFFFF,
|
||||
intensity: 1.5,
|
||||
position: { x: 10, y: 15, z: 10 },
|
||||
castShadow: true
|
||||
}
|
||||
],
|
||||
pointLights: [
|
||||
{
|
||||
color: 0x00FFFF,
|
||||
intensity: 2,
|
||||
distance: 20,
|
||||
position: { x: 0, y: 5, z: 0 }
|
||||
}
|
||||
]
|
||||
})
|
||||
```
|
||||
|
||||
### 4. usePostProcessing - 后期处理
|
||||
创建后期处理效果。
|
||||
|
||||
```typescript
|
||||
import { usePostProcessing } from '@/composes/three'
|
||||
|
||||
const postUtils = usePostProcessing()
|
||||
|
||||
// 创建后期处理组合器
|
||||
const composer = postUtils.createComposer(renderer, scene, camera)
|
||||
|
||||
// 添加辉光效果
|
||||
const bloomPass = postUtils.createBloomPass(width, height, 1.5, 0.4, 0.85)
|
||||
composer.addPass(bloomPass)
|
||||
|
||||
// 添加轮廓描边
|
||||
const outlinePass = postUtils.createOutlinePass(width, height, scene, camera)
|
||||
outlinePass.selectedObjects = [mesh]
|
||||
composer.addPass(outlinePass)
|
||||
```
|
||||
|
||||
### 5. useParticleSystem - 粒子系统
|
||||
创建和管理粒子效果(如蒸汽、烟雾)。
|
||||
|
||||
```typescript
|
||||
import { useParticleSystem } from '@/composes/three'
|
||||
|
||||
const particleUtils = useParticleSystem()
|
||||
|
||||
// 创建粒子发射器
|
||||
const emitter = particleUtils.createEmitter(
|
||||
scene,
|
||||
new THREE.Vector3(0, 2, 0),
|
||||
5 // 发射速率
|
||||
)
|
||||
|
||||
// 在动画循环中更新
|
||||
function animate() {
|
||||
emitter.update()
|
||||
}
|
||||
|
||||
// 停止发射
|
||||
emitter.stop()
|
||||
```
|
||||
|
||||
## 综合示例
|
||||
|
||||
参见 `src/views/three/12/index.vue` 获取完整的综合应用示例,包含:
|
||||
- ✅ 海蓝色渐变天空
|
||||
- ✅ 立体天空盒
|
||||
- ✅ 有厚度的地板
|
||||
- ✅ 地板流动光效
|
||||
- ✅ 多光源照明系统
|
||||
- ✅ 粒子蒸汽效果
|
||||
- ✅ 设备告警与交互
|
||||
- ✅ 后期处理效果
|
||||
|
||||
## 文件结构
|
||||
|
||||
```
|
||||
src/composes/three/
|
||||
├── index.ts # 统一导出
|
||||
├── scene.ts # 场景管理
|
||||
├── environment.ts # 环境管理
|
||||
├── lighting.ts # 光照管理
|
||||
├── postprocessing.ts # 后期处理
|
||||
├── particles.ts # 粒子系统
|
||||
├── loader.ts # 加载器
|
||||
└── texture.ts # 纹理
|
||||
```
|
||||
|
||||
## 优势
|
||||
|
||||
1. **代码复用**:避免重复编写基础代码
|
||||
2. **类型安全**:完整的 TypeScript 类型定义
|
||||
3. **易于维护**:集中管理公共逻辑
|
||||
4. **快速开发**:通过组合函数快速搭建场景
|
||||
5. **灵活配置**:提供丰富的配置选项
|
||||
|
||||
## 使用建议
|
||||
|
||||
1. 根据项目需求选择需要的模块
|
||||
2. 可以在基础上扩展自定义功能
|
||||
3. 保持函数的纯粹性和可组合性
|
||||
4. 合理使用 TypeScript 类型提示
|
||||
144
src/composes/three/environment.ts
Normal file
144
src/composes/three/environment.ts
Normal file
@@ -0,0 +1,144 @@
|
||||
import * as THREE from 'three'
|
||||
|
||||
export function useEnvironment() {
|
||||
/**
|
||||
* 创建天空盒
|
||||
*/
|
||||
function createSkybox(scene: THREE.Scene, color1 = 0x87CEEB, color2 = 0x1A1A2E) {
|
||||
const skyGeometry = new THREE.BoxGeometry(500, 500, 500)
|
||||
const skyMaterials = [
|
||||
new THREE.MeshBasicMaterial({ color: color1, side: THREE.BackSide }), // right
|
||||
new THREE.MeshBasicMaterial({ color: color1, side: THREE.BackSide }), // left
|
||||
new THREE.MeshBasicMaterial({ color: color2, side: THREE.BackSide }), // top
|
||||
new THREE.MeshBasicMaterial({ color: color1, side: THREE.BackSide }), // bottom
|
||||
new THREE.MeshBasicMaterial({ color: color1, side: THREE.BackSide }), // front
|
||||
new THREE.MeshBasicMaterial({ color: color1, side: THREE.BackSide }), // back
|
||||
]
|
||||
const skybox = new THREE.Mesh(skyGeometry, skyMaterials)
|
||||
scene.add(skybox)
|
||||
return skybox
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建海蓝色渐变背景
|
||||
*/
|
||||
function createOceanGradientBackground(scene: THREE.Scene) {
|
||||
// 使用canvas创建渐变纹理
|
||||
const canvas = document.createElement('canvas')
|
||||
canvas.width = 2
|
||||
canvas.height = 256
|
||||
const ctx = canvas.getContext('2d')!
|
||||
|
||||
const gradient = ctx.createLinearGradient(0, 0, 0, 256)
|
||||
gradient.addColorStop(0, '#001a33') // 深海蓝
|
||||
gradient.addColorStop(0.5, '#004d80') // 中蓝
|
||||
gradient.addColorStop(1, '#0080bf') // 浅海蓝
|
||||
|
||||
ctx.fillStyle = gradient
|
||||
ctx.fillRect(0, 0, 2, 256)
|
||||
|
||||
const texture = new THREE.CanvasTexture(canvas)
|
||||
scene.background = texture
|
||||
return texture
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建有厚度的地板
|
||||
*/
|
||||
function createThickGround(
|
||||
size = 100,
|
||||
thickness = 0.5,
|
||||
color = 0x2C3E50,
|
||||
) {
|
||||
const groundGeometry = new THREE.BoxGeometry(size, thickness, size)
|
||||
const groundMaterial = new THREE.MeshStandardMaterial({
|
||||
color,
|
||||
roughness: 0.8,
|
||||
metalness: 0.2,
|
||||
})
|
||||
|
||||
const ground = new THREE.Mesh(groundGeometry, groundMaterial)
|
||||
ground.position.y = -thickness / 2
|
||||
ground.receiveShadow = true
|
||||
ground.castShadow = true
|
||||
|
||||
return ground
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建地板流动效果
|
||||
*/
|
||||
function createFlowingGroundEffect(size = 100) {
|
||||
// 创建自定义着色器材质
|
||||
const flowMaterial = new THREE.ShaderMaterial({
|
||||
transparent: true,
|
||||
side: THREE.DoubleSide,
|
||||
uniforms: {
|
||||
time: { value: 0 },
|
||||
color1: { value: new THREE.Color(0x00FFFF) },
|
||||
color2: { value: new THREE.Color(0x0080FF) },
|
||||
},
|
||||
vertexShader: `
|
||||
varying vec2 vUv;
|
||||
void main() {
|
||||
vUv = uv;
|
||||
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
||||
}
|
||||
`,
|
||||
fragmentShader: `
|
||||
uniform float time;
|
||||
uniform vec3 color1;
|
||||
uniform vec3 color2;
|
||||
varying vec2 vUv;
|
||||
|
||||
void main() {
|
||||
vec2 uv = vUv;
|
||||
|
||||
// 创建流动的线条效果
|
||||
float line1 = sin(uv.x * 20.0 - time) * 0.5 + 0.5;
|
||||
float line2 = sin(uv.y * 20.0 + time * 0.5) * 0.5 + 0.5;
|
||||
float pattern = line1 * line2;
|
||||
|
||||
// 添加移动的光带
|
||||
float wave = sin(uv.x * 5.0 - time * 2.0) * sin(uv.y * 5.0 + time * 1.5);
|
||||
wave = smoothstep(0.3, 0.7, wave);
|
||||
|
||||
vec3 color = mix(color1, color2, pattern);
|
||||
float alpha = (pattern * 0.3 + wave * 0.5) * 0.4;
|
||||
|
||||
gl_FragColor = vec4(color, alpha);
|
||||
}
|
||||
`,
|
||||
})
|
||||
|
||||
const flowGeometry = new THREE.PlaneGeometry(size, size, 50, 50)
|
||||
const flowMesh = new THREE.Mesh(flowGeometry, flowMaterial)
|
||||
flowMesh.rotation.x = -Math.PI / 2
|
||||
flowMesh.position.y = 0.05 // 略高于地面
|
||||
|
||||
return {
|
||||
mesh: flowMesh,
|
||||
material: flowMaterial,
|
||||
update: (time: number) => {
|
||||
if (flowMaterial.uniforms.time) {
|
||||
flowMaterial.uniforms.time.value = time
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建网格辅助线
|
||||
*/
|
||||
function createGrid(size = 100, divisions = 50, color1 = 0x444444, color2 = 0x222222) {
|
||||
return new THREE.GridHelper(size, divisions, color1, color2)
|
||||
}
|
||||
|
||||
return {
|
||||
createSkybox,
|
||||
createOceanGradientBackground,
|
||||
createThickGround,
|
||||
createFlowingGroundEffect,
|
||||
createGrid,
|
||||
}
|
||||
}
|
||||
@@ -1,2 +1,7 @@
|
||||
export * from './environment'
|
||||
export * from './lighting'
|
||||
export * from './loader'
|
||||
export * from './particles'
|
||||
export * from './postprocessing'
|
||||
export * from './scene'
|
||||
export * from './texture'
|
||||
|
||||
117
src/composes/three/lighting.ts
Normal file
117
src/composes/three/lighting.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
import * as THREE from 'three'
|
||||
|
||||
interface LightingSetup {
|
||||
ambientLight?: boolean
|
||||
ambientIntensity?: number
|
||||
directionalLights?: Array<{
|
||||
color?: number
|
||||
intensity?: number
|
||||
position: { x: number, y: number, z: number }
|
||||
castShadow?: boolean
|
||||
}>
|
||||
pointLights?: Array<{
|
||||
color?: number
|
||||
intensity?: number
|
||||
distance?: number
|
||||
position: { x: number, y: number, z: number }
|
||||
}>
|
||||
}
|
||||
|
||||
export function useThreeLighting() {
|
||||
/**
|
||||
* 创建环境光
|
||||
*/
|
||||
function createAmbientLight(color = 0xFFFFFF, intensity = 1) {
|
||||
return new THREE.AmbientLight(color, intensity)
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建方向光
|
||||
*/
|
||||
function createDirectionalLight(
|
||||
color = 0xFFFFFF,
|
||||
intensity = 1,
|
||||
position = { x: 10, y: 15, z: 10 },
|
||||
castShadow = true,
|
||||
) {
|
||||
const light = new THREE.DirectionalLight(color, intensity)
|
||||
light.position.set(position.x, position.y, position.z)
|
||||
|
||||
if (castShadow) {
|
||||
light.castShadow = true
|
||||
light.shadow.mapSize.width = 2048
|
||||
light.shadow.mapSize.height = 2048
|
||||
light.shadow.camera.left = -20
|
||||
light.shadow.camera.right = 20
|
||||
light.shadow.camera.top = 20
|
||||
light.shadow.camera.bottom = -20
|
||||
}
|
||||
|
||||
return light
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建点光源
|
||||
*/
|
||||
function createPointLight(
|
||||
color = 0xFFFFFF,
|
||||
intensity = 1,
|
||||
distance = 50,
|
||||
position = { x: 0, y: 10, z: 0 },
|
||||
) {
|
||||
const light = new THREE.PointLight(color, intensity, distance)
|
||||
light.position.set(position.x, position.y, position.z)
|
||||
return light
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量设置灯光
|
||||
*/
|
||||
function setupLighting(scene: THREE.Scene, config: LightingSetup) {
|
||||
const lights: THREE.Light[] = []
|
||||
|
||||
// 环境光
|
||||
if (config.ambientLight !== false) {
|
||||
const ambient = createAmbientLight(0xFFFFFF, config.ambientIntensity || 1)
|
||||
scene.add(ambient)
|
||||
lights.push(ambient)
|
||||
}
|
||||
|
||||
// 方向光
|
||||
if (config.directionalLights) {
|
||||
config.directionalLights.forEach((lightConfig) => {
|
||||
const light = createDirectionalLight(
|
||||
lightConfig.color,
|
||||
lightConfig.intensity,
|
||||
lightConfig.position,
|
||||
lightConfig.castShadow,
|
||||
)
|
||||
scene.add(light)
|
||||
lights.push(light)
|
||||
})
|
||||
}
|
||||
|
||||
// 点光源
|
||||
if (config.pointLights) {
|
||||
config.pointLights.forEach((lightConfig) => {
|
||||
const light = createPointLight(
|
||||
lightConfig.color,
|
||||
lightConfig.intensity,
|
||||
lightConfig.distance,
|
||||
lightConfig.position,
|
||||
)
|
||||
scene.add(light)
|
||||
lights.push(light)
|
||||
})
|
||||
}
|
||||
|
||||
return lights
|
||||
}
|
||||
|
||||
return {
|
||||
createAmbientLight,
|
||||
createDirectionalLight,
|
||||
createPointLight,
|
||||
setupLighting,
|
||||
}
|
||||
}
|
||||
162
src/composes/three/particles.ts
Normal file
162
src/composes/three/particles.ts
Normal file
@@ -0,0 +1,162 @@
|
||||
import * as THREE from 'three'
|
||||
|
||||
export interface Particle {
|
||||
sprite: THREE.Sprite
|
||||
velocity: THREE.Vector3
|
||||
age: number
|
||||
maxAge: number
|
||||
}
|
||||
|
||||
export function useParticleSystem() {
|
||||
/**
|
||||
* 创建烟雾纹理
|
||||
*/
|
||||
function createSmokeTexture() {
|
||||
const canvas = document.createElement('canvas')
|
||||
canvas.width = 64
|
||||
canvas.height = 64
|
||||
const ctx = canvas.getContext('2d')!
|
||||
|
||||
const gradient = ctx.createRadialGradient(32, 32, 0, 32, 32, 32)
|
||||
gradient.addColorStop(0, 'rgba(255, 255, 255, 1)')
|
||||
gradient.addColorStop(0.5, 'rgba(255, 255, 255, 0.5)')
|
||||
gradient.addColorStop(1, 'rgba(255, 255, 255, 0)')
|
||||
|
||||
ctx.fillStyle = gradient
|
||||
ctx.fillRect(0, 0, 64, 64)
|
||||
|
||||
const texture = new THREE.Texture(canvas)
|
||||
texture.needsUpdate = true
|
||||
return texture
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建粒子
|
||||
*/
|
||||
function createParticle(
|
||||
texture: THREE.Texture,
|
||||
position: THREE.Vector3,
|
||||
options: {
|
||||
color?: number
|
||||
opacity?: number
|
||||
size?: number
|
||||
velocity?: THREE.Vector3
|
||||
maxAge?: number
|
||||
} = {},
|
||||
): Particle {
|
||||
const material = new THREE.SpriteMaterial({
|
||||
map: texture,
|
||||
transparent: true,
|
||||
opacity: options.opacity || 0.6,
|
||||
color: options.color || 0xAAAAAA,
|
||||
depthWrite: false,
|
||||
blending: THREE.AdditiveBlending,
|
||||
})
|
||||
|
||||
const sprite = new THREE.Sprite(material)
|
||||
sprite.position.copy(position)
|
||||
// 初始大小:可以通过 options.size 控制,默认 0.5
|
||||
sprite.scale.setScalar(options.size || 0.5)
|
||||
|
||||
const velocity = options.velocity || new THREE.Vector3(
|
||||
(Math.random() - 0.5) * 0.05,
|
||||
0.1 + Math.random() * 0.1,
|
||||
(Math.random() - 0.5) * 0.05,
|
||||
)
|
||||
|
||||
return {
|
||||
sprite,
|
||||
velocity,
|
||||
age: 0,
|
||||
maxAge: options.maxAge || 100,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新粒子
|
||||
*/
|
||||
function updateParticle(particle: Particle): boolean {
|
||||
particle.sprite.position.add(particle.velocity)
|
||||
particle.age++
|
||||
|
||||
// 更新大小和透明度
|
||||
const lifeRatio = particle.age / particle.maxAge
|
||||
// 烟雾扩散:从初始大小逐渐变大,最大为初始大小的 3 倍
|
||||
const initialSize = 0.5 // 与 createParticle 中的默认 size 对应
|
||||
particle.sprite.scale.setScalar(initialSize + lifeRatio * initialSize * 2)
|
||||
particle.sprite.material.opacity = 0.6 * (1 - lifeRatio)
|
||||
|
||||
// 返回是否应该移除
|
||||
return particle.age >= particle.maxAge
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建粒子发射器
|
||||
*/
|
||||
function createEmitter(
|
||||
scene: THREE.Scene,
|
||||
position: THREE.Vector3,
|
||||
emissionRate = 5,
|
||||
) {
|
||||
const texture = createSmokeTexture()
|
||||
const particles: Particle[] = []
|
||||
let emissionCounter = 0
|
||||
|
||||
function emit() {
|
||||
for (let i = 0; i < emissionRate; i++) {
|
||||
const particlePos = position.clone()
|
||||
particlePos.x += (Math.random() - 0.5) * 0.3
|
||||
particlePos.z += (Math.random() - 0.5) * 0.3
|
||||
|
||||
const particle = createParticle(texture, particlePos)
|
||||
scene.add(particle.sprite)
|
||||
particles.push(particle)
|
||||
}
|
||||
}
|
||||
|
||||
function update() {
|
||||
// 更新现有粒子
|
||||
for (let i = particles.length - 1; i >= 0; i--) {
|
||||
const particle = particles[i]
|
||||
if (!particle)
|
||||
continue
|
||||
|
||||
const shouldRemove = updateParticle(particle)
|
||||
|
||||
if (shouldRemove) {
|
||||
scene.remove(particle.sprite)
|
||||
particle.sprite.material.dispose()
|
||||
particles.splice(i, 1)
|
||||
}
|
||||
}
|
||||
|
||||
// 控制发射频率
|
||||
emissionCounter++
|
||||
if (emissionCounter >= 2) {
|
||||
emit()
|
||||
emissionCounter = 0
|
||||
}
|
||||
}
|
||||
|
||||
function stop() {
|
||||
particles.forEach((particle) => {
|
||||
scene.remove(particle.sprite)
|
||||
particle.sprite.material.dispose()
|
||||
})
|
||||
particles.length = 0
|
||||
}
|
||||
|
||||
return {
|
||||
update,
|
||||
stop,
|
||||
particles,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
createSmokeTexture,
|
||||
createParticle,
|
||||
updateParticle,
|
||||
createEmitter,
|
||||
}
|
||||
}
|
||||
71
src/composes/three/postprocessing.ts
Normal file
71
src/composes/three/postprocessing.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import * as THREE from 'three'
|
||||
import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js'
|
||||
import { OutlinePass } from 'three/examples/jsm/postprocessing/OutlinePass.js'
|
||||
import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js'
|
||||
import { UnrealBloomPass } from 'three/examples/jsm/postprocessing/UnrealBloomPass.js'
|
||||
|
||||
export function usePostProcessing() {
|
||||
/**
|
||||
* 创建后期处理组合器
|
||||
*/
|
||||
function createComposer(
|
||||
renderer: THREE.WebGLRenderer,
|
||||
scene: THREE.Scene,
|
||||
camera: THREE.Camera,
|
||||
) {
|
||||
const composer = new EffectComposer(renderer)
|
||||
const renderPass = new RenderPass(scene, camera)
|
||||
renderPass.clear = true
|
||||
renderPass.clearDepth = true
|
||||
composer.addPass(renderPass)
|
||||
return composer
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建辉光通道
|
||||
*/
|
||||
function createBloomPass(
|
||||
width: number,
|
||||
height: number,
|
||||
strength = 1.5,
|
||||
radius = 0.4,
|
||||
threshold = 0.85,
|
||||
) {
|
||||
return new UnrealBloomPass(
|
||||
new THREE.Vector2(width, height),
|
||||
strength,
|
||||
radius,
|
||||
threshold,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建轮廓描边通道
|
||||
*/
|
||||
function createOutlinePass(
|
||||
width: number,
|
||||
height: number,
|
||||
scene: THREE.Scene,
|
||||
camera: THREE.Camera,
|
||||
) {
|
||||
const outlinePass = new OutlinePass(
|
||||
new THREE.Vector2(width, height),
|
||||
scene,
|
||||
camera,
|
||||
)
|
||||
outlinePass.edgeStrength = 3
|
||||
outlinePass.edgeGlow = 0.5
|
||||
outlinePass.edgeThickness = 2
|
||||
outlinePass.pulsePeriod = 0
|
||||
outlinePass.visibleEdgeColor.set('#00ff00')
|
||||
outlinePass.hiddenEdgeColor.set('#00ff00')
|
||||
|
||||
return outlinePass
|
||||
}
|
||||
|
||||
return {
|
||||
createComposer,
|
||||
createBloomPass,
|
||||
createOutlinePass,
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,117 @@
|
||||
import * as THREE from 'three'
|
||||
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
|
||||
|
||||
interface SceneOptions {
|
||||
backgroundColor?: number
|
||||
enableFog?: boolean
|
||||
fogColor?: number
|
||||
fogNear?: number
|
||||
fogFar?: number
|
||||
}
|
||||
|
||||
interface CameraOptions {
|
||||
fov?: number
|
||||
aspect: number
|
||||
near?: number
|
||||
far?: number
|
||||
position?: { x: number, y: number, z: number }
|
||||
}
|
||||
|
||||
interface RendererOptions {
|
||||
antialias?: boolean
|
||||
shadowMap?: boolean
|
||||
shadowMapType?: THREE.ShadowMapType
|
||||
toneMapping?: THREE.ToneMapping
|
||||
toneMappingExposure?: number
|
||||
}
|
||||
|
||||
export function useThreeScene() {
|
||||
function createScene(options) {
|
||||
/**
|
||||
* 创建场景
|
||||
*/
|
||||
function createScene(options: SceneOptions = {}) {
|
||||
const scene = new THREE.Scene()
|
||||
|
||||
if (options.backgroundColor !== undefined) {
|
||||
scene.background = new THREE.Color(options.backgroundColor)
|
||||
}
|
||||
|
||||
return { createScene }
|
||||
if (options.enableFog) {
|
||||
scene.fog = new THREE.Fog(
|
||||
options.fogColor || 0x000000,
|
||||
options.fogNear || 10,
|
||||
options.fogFar || 50,
|
||||
)
|
||||
}
|
||||
|
||||
return scene
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建相机
|
||||
*/
|
||||
function createCamera(options: CameraOptions) {
|
||||
const camera = new THREE.PerspectiveCamera(
|
||||
options.fov || 75,
|
||||
options.aspect,
|
||||
options.near || 0.1,
|
||||
options.far || 1000,
|
||||
)
|
||||
|
||||
if (options.position) {
|
||||
camera.position.set(
|
||||
options.position.x,
|
||||
options.position.y,
|
||||
options.position.z,
|
||||
)
|
||||
}
|
||||
|
||||
return camera
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建渲染器
|
||||
*/
|
||||
function createRenderer(container: HTMLElement, options: RendererOptions = {}) {
|
||||
const renderer = new THREE.WebGLRenderer({
|
||||
antialias: options.antialias !== false,
|
||||
})
|
||||
|
||||
const width = container.clientWidth
|
||||
const height = container.clientHeight
|
||||
|
||||
renderer.setSize(width, height)
|
||||
renderer.setPixelRatio(window.devicePixelRatio)
|
||||
|
||||
if (options.shadowMap !== false) {
|
||||
renderer.shadowMap.enabled = true
|
||||
renderer.shadowMap.type = options.shadowMapType || THREE.PCFSoftShadowMap
|
||||
}
|
||||
|
||||
if (options.toneMapping) {
|
||||
renderer.toneMapping = options.toneMapping
|
||||
renderer.toneMappingExposure = options.toneMappingExposure || 1
|
||||
}
|
||||
|
||||
container.appendChild(renderer.domElement)
|
||||
|
||||
return renderer
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建轨道控制器
|
||||
*/
|
||||
function createControls(camera: THREE.Camera, renderer: THREE.WebGLRenderer) {
|
||||
const controls = new OrbitControls(camera, renderer.domElement)
|
||||
controls.enableDamping = true
|
||||
controls.dampingFactor = 0.05
|
||||
return controls
|
||||
}
|
||||
|
||||
return {
|
||||
createScene,
|
||||
createCamera,
|
||||
createRenderer,
|
||||
createControls,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,6 +64,11 @@ const router = createRouter({
|
||||
name: 'three-11',
|
||||
component: () => import('../views/three/11/index.vue'),
|
||||
},
|
||||
{
|
||||
path: '/three/12',
|
||||
name: 'three-12',
|
||||
component: () => import('../views/three/12/index.vue'),
|
||||
},
|
||||
{
|
||||
path: '/3d-point',
|
||||
name: '3d-point',
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* @Autor: 刘 相卿
|
||||
* @Date: 2025-11-17 16:07:33
|
||||
* @LastEditors: 刘 相卿
|
||||
* @LastEditTime: 2025-11-21 09:26:39
|
||||
* @LastEditTime: 2025-11-21 10:04:12
|
||||
-->
|
||||
<script lang="ts" setup>
|
||||
import * as THREE from 'three'
|
||||
|
||||
@@ -3,11 +3,577 @@
|
||||
* @Autor: 刘 相卿
|
||||
* @Date: 2025-11-17 16:07:33
|
||||
* @LastEditors: 刘 相卿
|
||||
* @LastEditTime: 2025-11-20 11:45:46
|
||||
* @LastEditTime: 2025-11-21 09:43:16
|
||||
-->
|
||||
<script lang="ts" setup>
|
||||
import * as THREE from 'three'
|
||||
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
|
||||
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
|
||||
import { DotScreenPass } from 'three/examples/jsm/postprocessing/DotScreenPass.js'
|
||||
import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js'
|
||||
import { GlitchPass } from 'three/examples/jsm/postprocessing/GlitchPass.js'
|
||||
import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js'
|
||||
import { ShaderPass } from 'three/examples/jsm/postprocessing/ShaderPass.js'
|
||||
import { SMAAPass } from 'three/examples/jsm/postprocessing/SMAAPass.js'
|
||||
import { UnrealBloomPass } from 'three/examples/jsm/postprocessing/UnrealBloomPass.js'
|
||||
import { onMounted, onUnmounted, ref, shallowRef, watch } from 'vue'
|
||||
|
||||
const threeRef = shallowRef<HTMLDivElement>()
|
||||
|
||||
let scene: THREE.Scene
|
||||
let camera: THREE.PerspectiveCamera
|
||||
let renderer: THREE.WebGLRenderer
|
||||
let controls: OrbitControls
|
||||
let animationId: number
|
||||
let model: THREE.Group
|
||||
|
||||
// 后期处理相关
|
||||
let composer: EffectComposer
|
||||
let glitchPass: GlitchPass
|
||||
let smaaPass: SMAAPass
|
||||
let dotScreenPass: DotScreenPass
|
||||
let bloomPass: UnrealBloomPass
|
||||
let flickerPass: ShaderPass
|
||||
let waterPass: ShaderPass
|
||||
|
||||
// 控制面板状态
|
||||
const effects = ref({
|
||||
glitch: false,
|
||||
antialiasing: false,
|
||||
dotScreen: false,
|
||||
bloom: false,
|
||||
bloomStrength: 1.5,
|
||||
flicker: false,
|
||||
water: false,
|
||||
})
|
||||
|
||||
// 自定义着色器 - 屏幕闪动效果
|
||||
const FlickerShader = {
|
||||
uniforms: {
|
||||
tDiffuse: { value: null },
|
||||
amount: { value: 0.5 },
|
||||
time: { value: 0 },
|
||||
},
|
||||
vertexShader: `
|
||||
varying vec2 vUv;
|
||||
void main() {
|
||||
vUv = uv;
|
||||
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
||||
}
|
||||
`,
|
||||
fragmentShader: `
|
||||
uniform sampler2D tDiffuse;
|
||||
uniform float amount;
|
||||
uniform float time;
|
||||
varying vec2 vUv;
|
||||
|
||||
void main() {
|
||||
vec4 color = texture2D(tDiffuse, vUv);
|
||||
float flicker = sin(time * 10.0) * 0.5 + 0.5;
|
||||
flicker = pow(flicker, 3.0);
|
||||
color.rgb *= 1.0 - (flicker * amount * 0.3);
|
||||
gl_FragColor = color;
|
||||
}
|
||||
`,
|
||||
}
|
||||
|
||||
// 自定义着色器 - 水底波浪效果
|
||||
const WaterShader = {
|
||||
uniforms: {
|
||||
tDiffuse: { value: null },
|
||||
time: { value: 0 },
|
||||
distortion: { value: 0.5 },
|
||||
},
|
||||
vertexShader: `
|
||||
varying vec2 vUv;
|
||||
void main() {
|
||||
vUv = uv;
|
||||
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
||||
}
|
||||
`,
|
||||
fragmentShader: `
|
||||
uniform sampler2D tDiffuse;
|
||||
uniform float time;
|
||||
uniform float distortion;
|
||||
varying vec2 vUv;
|
||||
|
||||
void main() {
|
||||
vec2 uv = vUv;
|
||||
|
||||
// 创建多层波浪效果
|
||||
float wave1 = sin(uv.x * 10.0 + time) * 0.01;
|
||||
float wave2 = sin(uv.y * 15.0 - time * 1.5) * 0.01;
|
||||
float wave3 = sin((uv.x + uv.y) * 8.0 + time * 0.5) * 0.005;
|
||||
|
||||
uv.x += (wave1 + wave3) * distortion;
|
||||
uv.y += (wave2 + wave3) * distortion;
|
||||
|
||||
// 添加水下的颜色调整
|
||||
vec4 color = texture2D(tDiffuse, uv);
|
||||
color.rgb *= vec3(0.8, 0.95, 1.1); // 蓝绿色调
|
||||
|
||||
gl_FragColor = color;
|
||||
}
|
||||
`,
|
||||
}
|
||||
|
||||
function init() {
|
||||
const width = threeRef.value!.clientWidth
|
||||
const height = threeRef.value!.clientHeight
|
||||
|
||||
scene = new THREE.Scene()
|
||||
scene.background = new THREE.Color(0x1A1A2E)
|
||||
|
||||
camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000)
|
||||
camera.position.set(0, 5, 15)
|
||||
|
||||
renderer = new THREE.WebGLRenderer({ antialias: true })
|
||||
renderer.setSize(width, height)
|
||||
renderer.setPixelRatio(window.devicePixelRatio)
|
||||
renderer.shadowMap.enabled = true
|
||||
renderer.shadowMap.type = THREE.PCFSoftShadowMap
|
||||
threeRef.value!.appendChild(renderer.domElement)
|
||||
|
||||
controls = new OrbitControls(camera, renderer.domElement)
|
||||
controls.enableDamping = true
|
||||
controls.dampingFactor = 0.05
|
||||
|
||||
setupLighting()
|
||||
setupGround()
|
||||
setupPostProcessing()
|
||||
loadModel()
|
||||
|
||||
window.addEventListener('resize', onResize)
|
||||
|
||||
animate()
|
||||
}
|
||||
|
||||
function setupLighting() {
|
||||
// 环境光
|
||||
const ambientLight = new THREE.AmbientLight(0xFFFFFF, 1.0)
|
||||
scene.add(ambientLight)
|
||||
|
||||
// 主方向光
|
||||
const mainLight = new THREE.DirectionalLight(0xFFFFFF, 1.5)
|
||||
mainLight.position.set(10, 15, 10)
|
||||
mainLight.castShadow = true
|
||||
mainLight.shadow.mapSize.width = 2048
|
||||
mainLight.shadow.mapSize.height = 2048
|
||||
scene.add(mainLight)
|
||||
|
||||
// 辅助光
|
||||
const fillLight = new THREE.DirectionalLight(0x4488FF, 0.8)
|
||||
fillLight.position.set(-10, 10, -10)
|
||||
scene.add(fillLight)
|
||||
|
||||
// 添加发光球体(用于演示发光效果)
|
||||
const sphereGeometry = new THREE.SphereGeometry(0.5, 32, 32)
|
||||
const emissiveMaterial = new THREE.MeshStandardMaterial({
|
||||
color: 0x00FFFF,
|
||||
emissive: 0x00FFFF,
|
||||
emissiveIntensity: 2,
|
||||
})
|
||||
|
||||
const positions = [
|
||||
{ x: -8, y: 3, z: -8 },
|
||||
{ x: 8, y: 3, z: -8 },
|
||||
{ x: -8, y: 3, z: 8 },
|
||||
{ x: 8, y: 3, z: 8 },
|
||||
]
|
||||
|
||||
positions.forEach((pos) => {
|
||||
const sphere = new THREE.Mesh(sphereGeometry, emissiveMaterial.clone())
|
||||
sphere.position.set(pos.x, pos.y, pos.z)
|
||||
scene.add(sphere)
|
||||
|
||||
// 添加点光源
|
||||
const pointLight = new THREE.PointLight(0x00FFFF, 2, 20)
|
||||
pointLight.position.copy(sphere.position)
|
||||
scene.add(pointLight)
|
||||
})
|
||||
}
|
||||
|
||||
function setupGround() {
|
||||
const groundGeometry = new THREE.PlaneGeometry(100, 100)
|
||||
const groundMaterial = new THREE.MeshStandardMaterial({
|
||||
color: 0x333333,
|
||||
roughness: 0.8,
|
||||
metalness: 0.2,
|
||||
})
|
||||
const ground = new THREE.Mesh(groundGeometry, groundMaterial)
|
||||
ground.rotation.x = -Math.PI / 2
|
||||
ground.position.y = 0
|
||||
ground.receiveShadow = true
|
||||
scene.add(ground)
|
||||
|
||||
// 网格辅助线
|
||||
const gridHelper = new THREE.GridHelper(100, 50, 0x555555, 0x333333)
|
||||
scene.add(gridHelper)
|
||||
}
|
||||
|
||||
function setupPostProcessing() {
|
||||
composer = new EffectComposer(renderer)
|
||||
|
||||
// 基础渲染通道
|
||||
const renderPass = new RenderPass(scene, camera)
|
||||
composer.addPass(renderPass)
|
||||
|
||||
// 1. 故障效果
|
||||
glitchPass = new GlitchPass()
|
||||
glitchPass.enabled = effects.value.glitch
|
||||
composer.addPass(glitchPass)
|
||||
|
||||
// 2. 抗锯齿
|
||||
smaaPass = new SMAAPass()
|
||||
smaaPass.enabled = effects.value.antialiasing
|
||||
composer.addPass(smaaPass)
|
||||
|
||||
// 3. 点效果
|
||||
dotScreenPass = new DotScreenPass()
|
||||
dotScreenPass.enabled = effects.value.dotScreen
|
||||
composer.addPass(dotScreenPass)
|
||||
|
||||
// 4. 发光效果
|
||||
bloomPass = new UnrealBloomPass(
|
||||
new THREE.Vector2(threeRef.value!.clientWidth, threeRef.value!.clientHeight),
|
||||
effects.value.bloomStrength,
|
||||
0.4,
|
||||
0.85,
|
||||
)
|
||||
bloomPass.enabled = effects.value.bloom
|
||||
composer.addPass(bloomPass)
|
||||
|
||||
// 5. 屏幕闪动效果
|
||||
flickerPass = new ShaderPass(FlickerShader)
|
||||
flickerPass.enabled = effects.value.flicker
|
||||
composer.addPass(flickerPass)
|
||||
|
||||
// 6. 水底波浪效果
|
||||
waterPass = new ShaderPass(WaterShader)
|
||||
waterPass.enabled = effects.value.water
|
||||
composer.addPass(waterPass)
|
||||
}
|
||||
|
||||
function loadModel() {
|
||||
const loader = new GLTFLoader()
|
||||
loader.load('/mzjc_bansw.glb', (gltf) => {
|
||||
model = gltf.scene
|
||||
model.traverse((child) => {
|
||||
if (child instanceof THREE.Mesh) {
|
||||
child.castShadow = true
|
||||
child.receiveShadow = true
|
||||
}
|
||||
})
|
||||
scene.add(model)
|
||||
})
|
||||
}
|
||||
|
||||
function animate() {
|
||||
animationId = requestAnimationFrame(animate)
|
||||
controls.update()
|
||||
|
||||
const time = performance.now() * 0.001
|
||||
|
||||
// 更新自定义着色器的时间
|
||||
if (flickerPass.uniforms.time) {
|
||||
flickerPass.uniforms.time.value = time
|
||||
}
|
||||
if (waterPass.uniforms.time) {
|
||||
waterPass.uniforms.time.value = time
|
||||
}
|
||||
|
||||
// 让装饰球体浮动
|
||||
scene.traverse((object) => {
|
||||
if (object instanceof THREE.Mesh && object.material.emissive) {
|
||||
object.position.y = 3 + Math.sin(time + object.position.x) * 0.5
|
||||
}
|
||||
})
|
||||
|
||||
composer.render()
|
||||
}
|
||||
|
||||
function onResize() {
|
||||
if (!threeRef.value)
|
||||
return
|
||||
const width = threeRef.value.clientWidth
|
||||
const height = threeRef.value.clientHeight
|
||||
camera.aspect = width / height
|
||||
camera.updateProjectionMatrix()
|
||||
renderer.setSize(width, height)
|
||||
composer.setSize(width, height)
|
||||
}
|
||||
|
||||
// 监听效果参数变化
|
||||
watch(() => effects.value.glitch, (value) => {
|
||||
glitchPass.enabled = value
|
||||
})
|
||||
|
||||
watch(() => effects.value.antialiasing, (value) => {
|
||||
smaaPass.enabled = value
|
||||
})
|
||||
|
||||
watch(() => effects.value.dotScreen, (value) => {
|
||||
dotScreenPass.enabled = value
|
||||
})
|
||||
|
||||
watch(() => effects.value.bloom, (value) => {
|
||||
bloomPass.enabled = value
|
||||
})
|
||||
|
||||
watch(() => effects.value.bloomStrength, (value) => {
|
||||
bloomPass.strength = value
|
||||
})
|
||||
|
||||
watch(() => effects.value.flicker, (value) => {
|
||||
flickerPass.enabled = value
|
||||
})
|
||||
|
||||
watch(() => effects.value.water, (value) => {
|
||||
waterPass.enabled = value
|
||||
})
|
||||
|
||||
onMounted(init)
|
||||
onUnmounted(() => {
|
||||
cancelAnimationFrame(animationId)
|
||||
window.removeEventListener('resize', onResize)
|
||||
renderer.dispose()
|
||||
composer.dispose()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>11</div>
|
||||
<div ref="threeRef" class="w-full h-full" />
|
||||
|
||||
<router-link to="/three/12" class="position-fixed left-20px top-20px">
|
||||
公共函数抽象
|
||||
</router-link>
|
||||
|
||||
<!-- 控制面板 -->
|
||||
<div class="control-panel">
|
||||
<h3 class="panel-title">
|
||||
后期处理效果
|
||||
</h3>
|
||||
|
||||
<!-- 1. 故障效果 -->
|
||||
<div class="control-group">
|
||||
<label class="control-label">
|
||||
<input v-model="effects.glitch" type="checkbox">
|
||||
<span>1. 故障效果 (Glitch)</span>
|
||||
</label>
|
||||
<div class="effect-desc">
|
||||
数字故障、屏幕撕裂效果
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 2. 抗锯齿 -->
|
||||
<div class="control-group">
|
||||
<label class="control-label">
|
||||
<input v-model="effects.antialiasing" type="checkbox">
|
||||
<span>2. 抗锯齿 (SMAA)</span>
|
||||
</label>
|
||||
<div class="effect-desc">
|
||||
平滑边缘,减少锯齿
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 3. 点效果 -->
|
||||
<div class="control-group">
|
||||
<label class="control-label">
|
||||
<input v-model="effects.dotScreen" type="checkbox">
|
||||
<span>3. 点效果 (Dot Screen)</span>
|
||||
</label>
|
||||
<div class="effect-desc">
|
||||
像素点阵、印刷效果
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 4. 发光 -->
|
||||
<div class="control-group">
|
||||
<label class="control-label">
|
||||
<input v-model="effects.bloom" type="checkbox">
|
||||
<span>4. 发光效果 (Bloom)</span>
|
||||
</label>
|
||||
<div class="effect-desc">
|
||||
明亮物体辉光效果
|
||||
</div>
|
||||
|
||||
<div v-if="effects.bloom" class="control-sliders">
|
||||
<div class="slider-item">
|
||||
<label>强度</label>
|
||||
<input
|
||||
v-model.number="effects.bloomStrength"
|
||||
type="range"
|
||||
min="0"
|
||||
max="3"
|
||||
step="0.1"
|
||||
>
|
||||
<span class="value">{{ effects.bloomStrength }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 5. 屏幕闪动 -->
|
||||
<div class="control-group">
|
||||
<label class="control-label">
|
||||
<input v-model="effects.flicker" type="checkbox">
|
||||
<span>5. 屏幕闪动 (Flicker)</span>
|
||||
</label>
|
||||
<div class="effect-desc">
|
||||
老旧屏幕闪烁效果
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 6. 水底波浪 -->
|
||||
<div class="control-group">
|
||||
<label class="control-label">
|
||||
<input v-model="effects.water" type="checkbox">
|
||||
<span>6. 水底波浪 (Water)</span>
|
||||
</label>
|
||||
<div class="effect-desc">
|
||||
水下动态扭曲效果
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-text">
|
||||
可以同时开启多个效果组合
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.control-panel {
|
||||
position: fixed;
|
||||
right: 20px;
|
||||
top: 20px;
|
||||
background: rgba(0, 0, 0, 0.9);
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #00FFFF;
|
||||
min-width: 300px;
|
||||
max-height: 85vh;
|
||||
overflow-y: auto;
|
||||
box-shadow: 0 0 20px rgba(0, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.panel-title {
|
||||
margin: 0 0 16px 0;
|
||||
color: #00FFFF;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
border-bottom: 2px solid #00FFFF;
|
||||
padding-bottom: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.control-group {
|
||||
margin-bottom: 16px;
|
||||
padding: 12px;
|
||||
background: rgba(0, 255, 255, 0.05);
|
||||
border-radius: 6px;
|
||||
border-left: 3px solid #00FFFF;
|
||||
}
|
||||
|
||||
.control-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.control-label input[type="checkbox"] {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
cursor: pointer;
|
||||
accent-color: #00FFFF;
|
||||
}
|
||||
|
||||
.effect-desc {
|
||||
margin-top: 6px;
|
||||
margin-left: 28px;
|
||||
color: #888;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.control-sliders {
|
||||
margin-top: 12px;
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
.slider-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.slider-item label {
|
||||
color: #aaa;
|
||||
font-size: 12px;
|
||||
min-width: 40px;
|
||||
}
|
||||
|
||||
.slider-item input[type="range"] {
|
||||
flex: 1;
|
||||
height: 6px;
|
||||
border-radius: 3px;
|
||||
background: #333;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.slider-item input[type="range"]::-webkit-slider-thumb {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
background: #00FFFF;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.slider-item input[type="range"]::-moz-range-thumb {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
background: #00FFFF;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.slider-item .value {
|
||||
color: #00FFFF;
|
||||
font-size: 12px;
|
||||
min-width: 32px;
|
||||
text-align: right;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.info-text {
|
||||
background: rgba(0, 255, 255, 0.1);
|
||||
color: #00FFFF;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
border: 1px dashed #00FFFF;
|
||||
}
|
||||
|
||||
.control-panel::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.control-panel::-webkit-scrollbar-track {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.control-panel::-webkit-scrollbar-thumb {
|
||||
background: #00FFFF;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.control-panel::-webkit-scrollbar-thumb:hover {
|
||||
background: #00CCCC;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,13 +1,858 @@
|
||||
<!--
|
||||
* @Description: 公共函数抽象
|
||||
* @Description: 公共函数抽象 - 综合方案
|
||||
* @Autor: 刘 相卿
|
||||
* @Date: 2025-11-17 16:08:50
|
||||
* @LastEditors: 刘 相卿
|
||||
* @LastEditTime: 2025-11-20 11:45:56
|
||||
* @LastEditTime: 2025-11-21 11:05:07
|
||||
-->
|
||||
<script lang="ts" setup>
|
||||
import * as THREE from 'three'
|
||||
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
|
||||
import { OutputPass } from 'three/examples/jsm/postprocessing/OutputPass.js'
|
||||
import { onMounted, onUnmounted, ref, shallowRef } from 'vue'
|
||||
import {
|
||||
useEnvironment,
|
||||
useParticleSystem,
|
||||
usePostProcessing,
|
||||
useThreeLighting,
|
||||
useThreeScene,
|
||||
} from '@/composes/three'
|
||||
|
||||
const threeRef = shallowRef<HTMLDivElement>()
|
||||
|
||||
let scene: THREE.Scene
|
||||
let camera: THREE.PerspectiveCamera
|
||||
let renderer: THREE.WebGLRenderer
|
||||
let controls: any
|
||||
let animationId: number
|
||||
let model: THREE.Group
|
||||
let composer: any
|
||||
let outlinePass: any
|
||||
|
||||
// 粒子发射器
|
||||
let particleEmitter: any
|
||||
const isEmitting = ref(false)
|
||||
|
||||
// 地板流动效果
|
||||
let flowingGround: any
|
||||
|
||||
// 告警物体
|
||||
let alertMesh: THREE.Mesh | null = null
|
||||
const isAlerting = ref(false)
|
||||
let alertTime = 0
|
||||
|
||||
// 选中物体信息
|
||||
const selectedObject = ref<{
|
||||
name: string
|
||||
position: THREE.Vector3
|
||||
material: string
|
||||
mesh: THREE.Mesh
|
||||
} | null>(null)
|
||||
|
||||
const labelPosition = ref({ x: 0, y: 0 })
|
||||
const showDialog = ref(false)
|
||||
const dialogContent = ref({
|
||||
title: '',
|
||||
description: '',
|
||||
status: 'normal' as 'normal' | 'alert',
|
||||
})
|
||||
|
||||
// 射线检测
|
||||
let raycaster: THREE.Raycaster
|
||||
let mouse: THREE.Vector2
|
||||
|
||||
function init() {
|
||||
if (!threeRef.value)
|
||||
return
|
||||
|
||||
const width = threeRef.value.clientWidth
|
||||
const height = threeRef.value.clientHeight
|
||||
|
||||
// 使用公共函数创建基础场景
|
||||
const sceneUtils = useThreeScene()
|
||||
const envUtils = useEnvironment()
|
||||
const lightingUtils = useThreeLighting()
|
||||
const postUtils = usePostProcessing()
|
||||
const particleUtils = useParticleSystem()
|
||||
|
||||
// 创建场景
|
||||
scene = sceneUtils.createScene({
|
||||
backgroundColor: 0x001A33,
|
||||
enableFog: true,
|
||||
fogColor: 0x001A33,
|
||||
fogNear: 20,
|
||||
fogFar: 100,
|
||||
})
|
||||
|
||||
// 创建海蓝色渐变背景
|
||||
envUtils.createOceanGradientBackground(scene)
|
||||
|
||||
// 创建天空盒
|
||||
envUtils.createSkybox(scene, 0x004D80, 0x001A33)
|
||||
|
||||
// 创建相机
|
||||
camera = sceneUtils.createCamera({
|
||||
fov: 75,
|
||||
aspect: width / height,
|
||||
near: 0.1,
|
||||
far: 1000,
|
||||
position: { x: 0, y: 8, z: 18 },
|
||||
})
|
||||
|
||||
// 创建渲染器
|
||||
renderer = sceneUtils.createRenderer(threeRef.value, {
|
||||
antialias: true,
|
||||
shadowMap: true,
|
||||
shadowMapType: THREE.PCFSoftShadowMap,
|
||||
toneMapping: THREE.ACESFilmicToneMapping,
|
||||
toneMappingExposure: 1.2,
|
||||
})
|
||||
|
||||
// 创建控制器
|
||||
controls = sceneUtils.createControls(camera, renderer)
|
||||
|
||||
// 设置灯光系统
|
||||
lightingUtils.setupLighting(scene, {
|
||||
ambientLight: true,
|
||||
ambientIntensity: 0.8,
|
||||
directionalLights: [
|
||||
{
|
||||
color: 0xFFFFFF,
|
||||
intensity: 1.5,
|
||||
position: { x: 10, y: 15, z: 10 },
|
||||
castShadow: true,
|
||||
},
|
||||
{
|
||||
color: 0x4488FF,
|
||||
intensity: 0.8,
|
||||
position: { x: -10, y: 10, z: -10 },
|
||||
castShadow: false,
|
||||
},
|
||||
{
|
||||
color: 0xFFFFFF,
|
||||
intensity: 0.6,
|
||||
position: { x: 0, y: 5, z: -15 },
|
||||
castShadow: false,
|
||||
},
|
||||
],
|
||||
pointLights: [
|
||||
{
|
||||
color: 0x00FFFF,
|
||||
intensity: 2,
|
||||
distance: 20,
|
||||
position: { x: -8, y: 3, z: -8 },
|
||||
},
|
||||
{
|
||||
color: 0x00FFFF,
|
||||
intensity: 2,
|
||||
distance: 20,
|
||||
position: { x: 8, y: 3, z: -8 },
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
// 创建有厚度的地板
|
||||
const ground = envUtils.createThickGround(100, 0.8, 0x2C3E50)
|
||||
scene.add(ground)
|
||||
|
||||
// 添加地板流动效果
|
||||
flowingGround = envUtils.createFlowingGroundEffect(100)
|
||||
scene.add(flowingGround.mesh)
|
||||
|
||||
// 创建网格辅助线
|
||||
const grid = envUtils.createGrid(100, 50, 0x00FFFF, 0x004D80)
|
||||
grid.position.y = 0.02
|
||||
scene.add(grid)
|
||||
|
||||
// 设置后期处理
|
||||
composer = postUtils.createComposer(renderer, scene, camera)
|
||||
|
||||
// 配置轮廓描边
|
||||
outlinePass = postUtils.createOutlinePass(width, height, scene, camera)
|
||||
outlinePass.renderToScreen = false
|
||||
outlinePass.selectedObjects = [] // 初始化为空数组
|
||||
|
||||
composer.addPass(outlinePass)
|
||||
|
||||
const outputPass = new OutputPass()
|
||||
composer.addPass(outputPass)
|
||||
|
||||
// 初始化射线检测
|
||||
raycaster = new THREE.Raycaster()
|
||||
mouse = new THREE.Vector2()
|
||||
|
||||
// 加载模型
|
||||
loadModel(particleUtils)
|
||||
|
||||
// 事件监听
|
||||
renderer.domElement.addEventListener('click', onCanvasClick)
|
||||
window.addEventListener('resize', onResize)
|
||||
|
||||
animate()
|
||||
}
|
||||
|
||||
function loadModel(particleUtils: any) {
|
||||
const loader = new GLTFLoader()
|
||||
loader.load('/mzjc_bansw.glb', (gltf) => {
|
||||
model = gltf.scene
|
||||
|
||||
model.traverse((child) => {
|
||||
if (child instanceof THREE.Mesh) {
|
||||
child.castShadow = true
|
||||
child.receiveShadow = true
|
||||
child.userData.objectType = '建筑构件'
|
||||
child.userData.objectName = child.name || '未命名物体'
|
||||
|
||||
// 找到特定设备 kzf_peij_18
|
||||
if (child.name === 'kzf_peij_18') {
|
||||
alertMesh = child
|
||||
|
||||
// 为该设备创建粒子发射器
|
||||
const box = new THREE.Box3().setFromObject(child)
|
||||
const center = box.getCenter(new THREE.Vector3())
|
||||
const topPosition = new THREE.Vector3(center.x, box.max.y, center.z)
|
||||
|
||||
particleEmitter = particleUtils.createEmitter(scene, topPosition, 3)
|
||||
isEmitting.value = true // 默认开启蒸汽效果
|
||||
|
||||
// 标记为告警设备
|
||||
child.userData.canAlert = true
|
||||
child.userData.status = 'normal'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
scene.add(model)
|
||||
})
|
||||
}
|
||||
|
||||
function animate() {
|
||||
animationId = requestAnimationFrame(animate)
|
||||
controls.update()
|
||||
|
||||
const time = performance.now() * 0.001
|
||||
|
||||
// 更新地板流动效果
|
||||
if (flowingGround) {
|
||||
flowingGround.update(time)
|
||||
}
|
||||
|
||||
// 更新粒子系统
|
||||
if (isEmitting.value && particleEmitter) {
|
||||
particleEmitter.update()
|
||||
}
|
||||
|
||||
// 更新告警闪烁效果
|
||||
if (isAlerting.value && alertMesh) {
|
||||
alertTime += 0.05
|
||||
const intensity = (Math.sin(alertTime * 5) + 1) / 2
|
||||
|
||||
// 材质闪烁
|
||||
if (alertMesh.material) {
|
||||
const material = alertMesh.material as THREE.MeshStandardMaterial
|
||||
if (material.emissive) {
|
||||
material.emissive.setRGB(intensity, 0, 0)
|
||||
material.emissiveIntensity = intensity * 2
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 更新选中物体标签位置
|
||||
if (selectedObject.value) {
|
||||
updateLabelPosition()
|
||||
}
|
||||
|
||||
composer.render()
|
||||
// if (outlinePass.enabled) {
|
||||
// composer.render()
|
||||
// }
|
||||
// else {
|
||||
// renderer.render(scene, camera)
|
||||
// }
|
||||
}
|
||||
|
||||
function updateLabelPosition() {
|
||||
if (!selectedObject.value)
|
||||
return
|
||||
|
||||
const box = new THREE.Box3().setFromObject(selectedObject.value.mesh)
|
||||
const height = box.max.y - box.min.y
|
||||
const position = selectedObject.value.position.clone()
|
||||
position.y = box.max.y + height * 0.3
|
||||
|
||||
position.project(camera)
|
||||
|
||||
labelPosition.value = {
|
||||
x: (position.x * 0.5 + 0.5) * threeRef.value!.clientWidth,
|
||||
y: (-(position.y * 0.5) + 0.5) * threeRef.value!.clientHeight,
|
||||
}
|
||||
}
|
||||
|
||||
function onCanvasClick(event: MouseEvent) {
|
||||
const rect = renderer.domElement.getBoundingClientRect()
|
||||
mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1
|
||||
mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1
|
||||
|
||||
raycaster.setFromCamera(mouse, camera)
|
||||
|
||||
if (model) {
|
||||
const intersects = raycaster.intersectObject(model, true)
|
||||
|
||||
if (intersects.length > 0 && intersects[0]) {
|
||||
const object = intersects[0].object as THREE.Mesh
|
||||
const point = intersects[0].point
|
||||
|
||||
// 获取材质信息
|
||||
let materialName = '未知材质'
|
||||
if (object.material) {
|
||||
if (Array.isArray(object.material)) {
|
||||
materialName = object.material.map((m: THREE.Material) => m.type).join(', ')
|
||||
}
|
||||
else {
|
||||
materialName = object.material.type
|
||||
}
|
||||
}
|
||||
|
||||
selectedObject.value = {
|
||||
name: object.userData.objectName || object.name || '未命名物体',
|
||||
material: materialName,
|
||||
position: point.clone(),
|
||||
mesh: object,
|
||||
}
|
||||
|
||||
// 添加轮廓描边(绿色)
|
||||
outlinePass.selectedObjects = [object]
|
||||
outlinePass.visibleEdgeColor.set('#00ff00')
|
||||
outlinePass.hiddenEdgeColor.set('#00ff00')
|
||||
// outlinePass.enabled = true
|
||||
|
||||
// 如果点击的是告警设备
|
||||
if (object === alertMesh) {
|
||||
showDeviceDialog(object)
|
||||
}
|
||||
}
|
||||
else {
|
||||
// 点击空白处
|
||||
clearSelection()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function showDeviceDialog(mesh: THREE.Mesh) {
|
||||
const status = isAlerting.value ? 'alert' : 'normal'
|
||||
dialogContent.value = {
|
||||
title: mesh.name || '设备 kzf_peij_18',
|
||||
description: isAlerting.value
|
||||
? '设备温度过高!请立即检查冷却系统。'
|
||||
: '设备运行正常,所有参数在正常范围内。',
|
||||
status,
|
||||
}
|
||||
showDialog.value = true
|
||||
}
|
||||
|
||||
function clearSelection() {
|
||||
selectedObject.value = null
|
||||
outlinePass.selectedObjects = []
|
||||
// outlinePass.enabled = false
|
||||
}
|
||||
|
||||
function toggleAlert() {
|
||||
if (!alertMesh) {
|
||||
console.warn('模型尚未加载或未找到设备 kzf_peij_18')
|
||||
return
|
||||
}
|
||||
|
||||
isAlerting.value = !isAlerting.value
|
||||
alertMesh.userData.status = isAlerting.value ? 'alert' : 'normal'
|
||||
|
||||
if (isAlerting.value) {
|
||||
// 开启告警:红色轮廓
|
||||
outlinePass.selectedObjects = [alertMesh]
|
||||
outlinePass.visibleEdgeColor.set('#ff0000')
|
||||
outlinePass.hiddenEdgeColor.set('#ff0000')
|
||||
// outlinePass.enabled = true
|
||||
|
||||
// 设置材质发光
|
||||
if (alertMesh.material) {
|
||||
const material = alertMesh.material as THREE.MeshStandardMaterial
|
||||
material.emissive = new THREE.Color(0xFF0000)
|
||||
material.emissiveIntensity = 1
|
||||
}
|
||||
}
|
||||
else {
|
||||
// 关闭告警
|
||||
alertTime = 0
|
||||
if (alertMesh.material) {
|
||||
const material = alertMesh.material as THREE.MeshStandardMaterial
|
||||
material.emissive = new THREE.Color(0x000000)
|
||||
material.emissiveIntensity = 0
|
||||
}
|
||||
if (selectedObject.value?.mesh !== alertMesh) {
|
||||
// outlinePass.enabled = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function toggleParticles() {
|
||||
isEmitting.value = !isEmitting.value
|
||||
if (!isEmitting.value && particleEmitter) {
|
||||
particleEmitter.stop()
|
||||
}
|
||||
}
|
||||
|
||||
function closeDialog() {
|
||||
showDialog.value = false
|
||||
}
|
||||
|
||||
function onResize() {
|
||||
if (!threeRef.value)
|
||||
return
|
||||
const width = threeRef.value.clientWidth
|
||||
const height = threeRef.value.clientHeight
|
||||
camera.aspect = width / height
|
||||
camera.updateProjectionMatrix()
|
||||
renderer.setSize(width, height)
|
||||
composer.setSize(width, height)
|
||||
}
|
||||
|
||||
onMounted(init)
|
||||
onUnmounted(() => {
|
||||
cancelAnimationFrame(animationId)
|
||||
window.removeEventListener('resize', onResize)
|
||||
if (renderer.domElement) {
|
||||
renderer.domElement.removeEventListener('click', onCanvasClick)
|
||||
}
|
||||
if (particleEmitter) {
|
||||
particleEmitter.stop()
|
||||
}
|
||||
renderer.dispose()
|
||||
composer.dispose()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>12</div>
|
||||
<div ref="threeRef" class="w-full h-full" />
|
||||
|
||||
<!-- 物体信息标签 -->
|
||||
<div
|
||||
v-if="selectedObject"
|
||||
class="object-label"
|
||||
:style="{
|
||||
left: `${labelPosition.x}px`,
|
||||
top: `${labelPosition.y}px`,
|
||||
}"
|
||||
>
|
||||
<div class="label-content">
|
||||
<div class="label-title">
|
||||
{{ selectedObject.name }}
|
||||
</div>
|
||||
<div class="label-item">
|
||||
<span class="label-key">材质:</span>
|
||||
<span class="label-value">{{ selectedObject.material }}</span>
|
||||
</div>
|
||||
<div class="label-item">
|
||||
<span class="label-key">坐标:</span>
|
||||
<span class="label-value">
|
||||
({{ selectedObject.position.x.toFixed(2) }},
|
||||
{{ selectedObject.position.y.toFixed(2) }},
|
||||
{{ selectedObject.position.z.toFixed(2) }})
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 设备详情弹窗 -->
|
||||
<div v-if="showDialog" class="dialog-overlay" @click="closeDialog">
|
||||
<div class="dialog" :class="{ alert: dialogContent.status === 'alert' }" @click.stop>
|
||||
<div class="dialog-header">
|
||||
<h3>{{ dialogContent.title }}</h3>
|
||||
<div v-if="dialogContent.status === 'alert'" class="alert-badge">
|
||||
告警
|
||||
</div>
|
||||
<button class="close-btn" @click="closeDialog">
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
<div class="dialog-body">
|
||||
<p>{{ dialogContent.description }}</p>
|
||||
<div v-if="dialogContent.status === 'alert'" class="alert-info">
|
||||
<div class="alert-icon">
|
||||
⚠️
|
||||
</div>
|
||||
<div>
|
||||
<div>温度:85°C(正常范围:≤70°C)</div>
|
||||
<div>压力:正常</div>
|
||||
<div>运行时间:152小时</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="normal-info">
|
||||
<div>✓ 温度:65°C</div>
|
||||
<div>✓ 压力:正常</div>
|
||||
<div>✓ 运行时间:152小时</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 控制面板 -->
|
||||
<div class="control-panel">
|
||||
<h3 class="panel-title">
|
||||
综合场景控制
|
||||
</h3>
|
||||
|
||||
<div class="control-section">
|
||||
<h4>场景特性</h4>
|
||||
<ul class="feature-list">
|
||||
<li>✓ 海蓝色渐变天空</li>
|
||||
<li>✓ 立体天空盒</li>
|
||||
<li>✓ 有厚度的地板</li>
|
||||
<li>✓ 地板流动光效</li>
|
||||
<li>✓ 多光源照明系统</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="control-section">
|
||||
<h4>设备控制</h4>
|
||||
<button class="control-btn" @click="toggleAlert">
|
||||
{{ isAlerting ? '关闭' : '触发' }}设备告警
|
||||
</button>
|
||||
<button class="control-btn" @click="toggleParticles">
|
||||
{{ isEmitting ? '关闭' : '开启' }}蒸汽效果
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="control-section">
|
||||
<h4>交互说明</h4>
|
||||
<div class="info-item">
|
||||
点击模型查看物体信息
|
||||
</div>
|
||||
<div class="info-item">
|
||||
点击设备 kzf_peij_18 查看详情
|
||||
</div>
|
||||
<div class="info-item">
|
||||
点击空白处取消选择
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tech-info">
|
||||
<strong>使用的公共函数:</strong>
|
||||
<div>useThreeScene()</div>
|
||||
<div>useEnvironment()</div>
|
||||
<div>useThreeLighting()</div>
|
||||
<div>usePostProcessing()</div>
|
||||
<div>useParticleSystem()</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.object-label {
|
||||
position: absolute;
|
||||
transform: translate(-50%, -100%);
|
||||
pointer-events: none;
|
||||
z-index: 100;
|
||||
animation: fadeIn 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translate(-50%, -100%) scale(0.8);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translate(-50%, -100%) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.label-content {
|
||||
background: rgba(0, 0, 0, 0.9);
|
||||
color: white;
|
||||
padding: 12px 16px;
|
||||
border-radius: 6px;
|
||||
border: 2px solid #00FFFF;
|
||||
box-shadow: 0 4px 12px rgba(0, 255, 255, 0.5);
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.label-title {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 8px;
|
||||
color: #00FFFF;
|
||||
border-bottom: 1px solid rgba(0, 255, 255, 0.3);
|
||||
padding-bottom: 6px;
|
||||
}
|
||||
|
||||
.label-item {
|
||||
font-size: 13px;
|
||||
margin: 6px 0;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.label-key {
|
||||
color: #aaa;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.label-value {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.dialog-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
animation: fadeIn 0.2s;
|
||||
}
|
||||
|
||||
.dialog {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
|
||||
min-width: 450px;
|
||||
max-width: 600px;
|
||||
animation: slideIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
.dialog.alert {
|
||||
border: 3px solid #FF4444;
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
transform: translateY(-50px);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.dialog-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 20px 24px;
|
||||
border-bottom: 2px solid #eee;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.dialog-header h3 {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
color: #333;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.alert-badge {
|
||||
background: #FF4444;
|
||||
color: white;
|
||||
padding: 4px 12px;
|
||||
border-radius: 20px;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
animation: pulse 1.5s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 28px;
|
||||
cursor: pointer;
|
||||
color: #999;
|
||||
padding: 0;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 4px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.close-btn:hover {
|
||||
background: #f0f0f0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.dialog-body {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.dialog-body p {
|
||||
margin: 0 0 16px 0;
|
||||
line-height: 1.6;
|
||||
color: #666;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.alert-info {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
background: #FFF3F3;
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
border-left: 4px solid #FF4444;
|
||||
color: #CC0000;
|
||||
font-size: 14px;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.alert-icon {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.normal-info {
|
||||
background: #F0FFF4;
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
border-left: 4px solid #00CC66;
|
||||
color: #00AA55;
|
||||
font-size: 14px;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.control-panel {
|
||||
position: fixed;
|
||||
left: 20px;
|
||||
top: 20px;
|
||||
background: rgba(0, 0, 0, 0.9);
|
||||
padding: 20px;
|
||||
border-radius: 12px;
|
||||
border: 2px solid #00FFFF;
|
||||
min-width: 280px;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
box-shadow: 0 0 30px rgba(0, 255, 255, 0.4);
|
||||
}
|
||||
|
||||
.panel-title {
|
||||
margin: 0 0 20px 0;
|
||||
color: #00FFFF;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
border-bottom: 2px solid #00FFFF;
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
|
||||
.control-section {
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 20px;
|
||||
border-bottom: 1px solid rgba(0, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.control-section:last-of-type {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.control-section h4 {
|
||||
margin: 0 0 12px 0;
|
||||
color: #00FFFF;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.feature-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.feature-list li {
|
||||
color: #AAAAAA;
|
||||
font-size: 12px;
|
||||
margin: 6px 0;
|
||||
padding-left: 4px;
|
||||
}
|
||||
|
||||
.control-btn {
|
||||
width: 100%;
|
||||
padding: 10px 16px;
|
||||
margin: 6px 0;
|
||||
background: linear-gradient(135deg, #00FFFF 0%, #0080FF 100%);
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
color: white;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
box-shadow: 0 2px 8px rgba(0, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.control-btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.control-btn:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.info-item {
|
||||
background: rgba(0, 255, 255, 0.1);
|
||||
color: #AAAAAA;
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
margin: 6px 0;
|
||||
border-left: 3px solid #00FFFF;
|
||||
}
|
||||
|
||||
.tech-info {
|
||||
background: rgba(0, 255, 255, 0.05);
|
||||
padding: 12px;
|
||||
border-radius: 6px;
|
||||
font-size: 11px;
|
||||
color: #888;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.tech-info strong {
|
||||
color: #00FFFF;
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.tech-info div {
|
||||
margin: 4px 0;
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
.control-panel::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.control-panel::-webkit-scrollbar-track {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.control-panel::-webkit-scrollbar-thumb {
|
||||
background: #00FFFF;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.control-panel::-webkit-scrollbar-thumb:hover {
|
||||
background: #00CCCC;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user