This commit is contained in:
2025-11-21 11:35:47 +08:00
parent b4683674b9
commit 7f6e727b9c
11 changed files with 2215 additions and 8 deletions

View 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 类型提示

View 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,
}
}

View File

@@ -1,2 +1,7 @@
export * from './environment'
export * from './lighting'
export * from './loader' export * from './loader'
export * from './particles'
export * from './postprocessing'
export * from './scene'
export * from './texture' export * from './texture'

View 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,
}
}

View 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,
}
}

View 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,
}
}

View File

@@ -1,9 +1,117 @@
import * as THREE from 'three' 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() { export function useThreeScene() {
function createScene(options) { /**
* 创建场景
*/
function createScene(options: SceneOptions = {}) {
const scene = new THREE.Scene() const scene = new THREE.Scene()
if (options.backgroundColor !== undefined) {
scene.background = new THREE.Color(options.backgroundColor)
}
if (options.enableFog) {
scene.fog = new THREE.Fog(
options.fogColor || 0x000000,
options.fogNear || 10,
options.fogFar || 50,
)
}
return scene
} }
return { createScene } /**
* 创建相机
*/
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,
}
} }

View File

@@ -64,6 +64,11 @@ const router = createRouter({
name: 'three-11', name: 'three-11',
component: () => import('../views/three/11/index.vue'), component: () => import('../views/three/11/index.vue'),
}, },
{
path: '/three/12',
name: 'three-12',
component: () => import('../views/three/12/index.vue'),
},
{ {
path: '/3d-point', path: '/3d-point',
name: '3d-point', name: '3d-point',

View File

@@ -3,7 +3,7 @@
* @Autor: 相卿 * @Autor: 相卿
* @Date: 2025-11-17 16:07:33 * @Date: 2025-11-17 16:07:33
* @LastEditors: 相卿 * @LastEditors: 相卿
* @LastEditTime: 2025-11-21 09:26:39 * @LastEditTime: 2025-11-21 10:04:12
--> -->
<script lang="ts" setup> <script lang="ts" setup>
import * as THREE from 'three' import * as THREE from 'three'

View File

@@ -3,11 +3,577 @@
* @Autor: 相卿 * @Autor: 相卿
* @Date: 2025-11-17 16:07:33 * @Date: 2025-11-17 16:07:33
* @LastEditors: 相卿 * @LastEditors: 相卿
* @LastEditTime: 2025-11-20 11:45:46 * @LastEditTime: 2025-11-21 09:43:16
--> -->
<script lang="ts" setup> <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> </script>
<template> <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> </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>

View File

@@ -1,13 +1,858 @@
<!-- <!--
* @Description: 公共函数抽象 * @Description: 公共函数抽象 - 综合方案
* @Autor: 相卿 * @Autor: 相卿
* @Date: 2025-11-17 16:08:50 * @Date: 2025-11-17 16:08:50
* @LastEditors: 相卿 * @LastEditors: 相卿
* @LastEditTime: 2025-11-20 11:45:56 * @LastEditTime: 2025-11-21 11:05:07
--> -->
<script lang="ts" setup> <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> </script>
<template> <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> </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>