<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>3D机房可视化监控系统</title>
<script src="https://cdn.jsdelivr.net/npm/vue@3.3.4/dist/vue.global.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.156.1/build/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.156.1/examples/js/controls/OrbitControls.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: #1a1a2e;
color: white;
overflow: hidden;
}
#app {
width: 100vw;
height: 100vh;
display: flex;
}
.sidebar {
width: 350px;
background: #16213e;
border-right: 2px solid #0f3460;
display: flex;
flex-direction: column;
overflow: hidden;
}
.sidebar-header {
padding: 25px;
background: linear-gradient(135deg, #0f3460 0%, #16213e 100%);
border-bottom: 2px solid #00ff88;
}
.sidebar-header h1 {
font-size: 20px;
color: #00ff88;
margin-bottom: 8px;
}
.sidebar-header p {
font-size: 13px;
color: rgba(255,255,255,0.6);
}
.device-list {
flex: 1;
overflow-y: auto;
padding: 15px;
}
.device-category {
margin-bottom: 20px;
}
.category-title {
font-size: 14px;
color: #00ff88;
padding: 10px 12px;
background: rgba(0,255,136,0.1);
border-left: 3px solid #00ff88;
margin-bottom: 10px;
}
.device-item {
padding: 15px;
margin-bottom: 8px;
background: rgba(15,52,96,0.5);
border-radius: 8px;
cursor: pointer;
transition: all 0.3s;
border-left: 3px solid transparent;
}
.device-item:hover {
background: rgba(15,52,96,0.8);
transform: translateX(5px);
}
.device-item.selected {
background: rgba(0,255,136,0.2);
border-left-color: #00ff88;
}
.device-item.status-warning {
border-left-color: #ffd93d;
}
.device-item.status-error {
border-left-color: #ff6b6b;
}
.device-item.status-normal {
border-left-color: #00ff88;
}
.device-name {
font-size: 14px;
font-weight: 600;
margin-bottom: 8px;
display: flex;
align-items: center;
gap: 8px;
}
.device-status {
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
animation: pulse 2s ease-in-out infinite;
}
.device-status.normal { background: #00ff88; box-shadow: 0 0 8px #00ff88; }
.device-status.warning { background: #ffd93d; box-shadow: 0 0 8px #ffd93d; }
.device-status.error { background: #ff6b6b; box-shadow: 0 0 8px #ff6b6b; }
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.device-info {
font-size: 12px;
color: rgba(255,255,255,0.6);
line-height: 1.6;
}
.main-view {
flex: 1;
position: relative;
display: flex;
flex-direction: column;
}
.top-bar {
padding: 20px 30px;
background: linear-gradient(180deg, rgba(22,33,62,0.95) 0%, rgba(22,33,62,0) 100%);
display: flex;
justify-content: space-between;
align-items: center;
position: absolute;
top: 0;
left: 0;
right: 0;
z-index: 10;
}
.stats-bar {
display: flex;
gap: 30px;
}
.stat-item {
text-align: center;
}
.stat-value {
font-size: 24px;
font-weight: 700;
color: #00ff88;
display: block;
}
.stat-label {
font-size: 12px;
color: rgba(255,255,255,0.6);
margin-top: 5px;
}
.control-buttons {
display: flex;
gap: 10px;
}
.btn {
padding: 10px 20px;
background: rgba(0,255,136,0.1);
border: 1px solid rgba(0,255,136,0.5);
border-radius: 6px;
color: #00ff88;
font-size: 13px;
cursor: pointer;
transition: all 0.3s;
}
.btn:hover {
background: rgba(0,255,136,0.2);
transform: translateY(-2px);
}
.btn.active {
background: rgba(0,255,136,0.3);
border-color: #00ff88;
}
#canvas-container {
flex: 1;
}
.detail-panel {
position: absolute;
top: 50%;
right: 20px;
transform: translateY(-50%);
width: 320px;
background: rgba(22,33,62,0.95);
border: 2px solid #00ff88;
border-radius: 12px;
padding: 20px;
backdrop-filter: blur(10px);
box-shadow: 0 10px 40px rgba(0,0,0,0.5);
max-height: 70vh;
overflow-y: auto;
}
.detail-panel h3 {
font-size: 18px;
color: #00ff88;
margin-bottom: 20px;
padding-bottom: 15px;
border-bottom: 2px solid rgba(0,255,136,0.3);
}
.detail-row {
display: flex;
justify-content: space-between;
padding: 12px 0;
border-bottom: 1px solid rgba(255,255,255,0.1);
}
.detail-row:last-child {
border-bottom: none;
}
.detail-label {
color: rgba(255,255,255,0.7);
font-size: 13px;
}
.detail-value {
color: white;
font-weight: 600;
font-size: 14px;
}
.detail-value.warning {
color: #ffd93d;
}
.detail-value.error {
color: #ff6b6b;
}
.progress-bar {
width: 100%;
height: 8px;
background: rgba(255,255,255,0.1);
border-radius: 4px;
overflow: hidden;
margin-top: 8px;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #00ff88, #00d4ff);
transition: width 0.3s;
border-radius: 4px;
}
.progress-fill.warning {
background: linear-gradient(90deg, #ffd93d, #ff9f43);
}
.progress-fill.error {
background: linear-gradient(90deg, #ff6b6b, #ee5a6f);
}
::-webkit-scrollbar {
width: 6px;
}
::-webkit-scrollbar-track {
background: rgba(255,255,255,0.05);
}
::-webkit-scrollbar-thumb {
background: rgba(0,255,136,0.3);
border-radius: 3px;
}
</style>
</head>
<body>
<div id="app">
<div class="sidebar">
<div class="sidebar-header">
<h1>机房设备列表</h1>
<p>实时监控 {{ totalDevices }} 台设备</p>
</div>
<div class="device-list">
<div v-for="category in deviceCategories" :key="category.name" class="device-category">
<div class="category-title">{{ category.name }} ({{ category.devices.length }})</div>
<div
v-for="device in category.devices"
:key="device.id"
class="device-item"
:class="[
'status-' + device.status,
{ selected: selectedDevice && selectedDevice.id === device.id }
]"
@click="selectDevice(device)"
>
<div class="device-name">
<span class="device-status" :class="device.status"></span>
{{ device.name }}
</div>
<div class="device-info">
{{ device.location }} | 机柜{{ device.rack }}
</div>
</div>
</div>
</div>
</div>
<div class="main-view">
<div class="top-bar">
<div class="stats-bar">
<div class="stat-item">
<span class="stat-value">{{ onlineDevices }}</span>
<span class="stat-label">在线设备</span>
</div>
<div class="stat-item">
<span class="stat-value" style="color: #ffd93d">{{ warningDevices }}</span>
<span class="stat-label">预警设备</span>
</div>
<div class="stat-item">
<span class="stat-value" style="color: #ff6b6b">{{ errorDevices }}</span>
<span class="stat-label">故障设备</span>
</div>
</div>
<div class="control-buttons">
<button class="btn" :class="{ active: viewMode === 'overview' }" @click="setViewMode('overview')">
全景视图
</button>
<button class="btn" :class="{ active: viewMode === 'detail' }" @click="setViewMode('detail')">
细节视图
</button>
<button class="btn" @click="resetView">重置视角</button>
</div>
</div>
<div id="canvas-container"></div>
<div class="detail-panel" v-if="selectedDevice">
<h3>{{ selectedDevice.name }}</h3>
<div class="detail-row">
<span class="detail-label">设备状态</span>
<span class="detail-value" :class="selectedDevice.status">
{{ statusText[selectedDevice.status] }}
</span>
</div>
<div class="detail-row">
<span class="detail-label">设备位置</span>
<span class="detail-value">{{ selectedDevice.location }} - 机柜{{ selectedDevice.rack }}</span>
</div>
<div class="detail-row">
<span class="detail-label">IP地址</span>
<span class="detail-value">{{ selectedDevice.ip }}</span>
</div>
<div class="detail-row">
<span class="detail-label">CPU使用率</span>
<span class="detail-value" :class="getCpuClass(selectedDevice.cpu)">
{{ selectedDevice.cpu }}%
</span>
</div>
<div class="progress-bar">
<div
class="progress-fill"
:class="getCpuClass(selectedDevice.cpu)"
:style="{ width: selectedDevice.cpu + '%' }"
></div>
</div>
<div class="detail-row">
<span class="detail-label">内存使用率</span>
<span class="detail-value" :class="getMemoryClass(selectedDevice.memory)">
{{ selectedDevice.memory }}%
</span>
</div>
<div class="progress-bar">
<div
class="progress-fill"
:class="getMemoryClass(selectedDevice.memory)"
:style="{ width: selectedDevice.memory + '%' }"
></div>
</div>
<div class="detail-row">
<span class="detail-label">温度</span>
<span class="detail-value" :class="getTempClass(selectedDevice.temperature)">
{{ selectedDevice.temperature }}°C
</span>
</div>
<div class="detail-row">
<span class="detail-label">运行时长</span>
<span class="detail-value">{{ selectedDevice.uptime }}</span>
</div>
</div>
</div>
</div>
<script>
const { createApp } = Vue;
const { Scene, PerspectiveCamera, WebGLRenderer, AmbientLight, PointLight,
BoxGeometry, MeshStandardMaterial, Mesh, Group, Color, Raycaster, Vector2 } = THREE;
createApp({
data() {
return {
scene: null,
camera: null,
renderer: null,
controls: null,
raycaster: null,
mouse: null,
animationId: null,
rackMeshes: [],
deviceMeshes: [],
viewMode: 'overview',
selectedDevice: null,
statusText: {
normal: '正常运行',
warning: '预警状态',
error: '故障离线'
},
deviceCategories: [
{
name: '服务器',
devices: [
{ id: 1, name: 'Web-Server-01', location: 'A区', rack: 'R1', status: 'normal', ip: '192.168.1.101', cpu: 45, memory: 62, temperature: 42, uptime: '156天', position: { row: 0, col: 0, level: 0 } },
{ id: 2, name: 'Web-Server-02', location: 'A区', rack: 'R1', status: 'normal', ip: '192.168.1.102', cpu: 38, memory: 58, temperature: 41, uptime: '156天', position: { row: 0, col: 0, level: 1 } },
{ id: 3, name: 'DB-Server-01', location: 'A区', rack: 'R2', status: 'warning', ip: '192.168.1.201', cpu: 82, memory: 89, temperature: 58, uptime: '89天', position: { row: 0, col: 1, level: 0 } },
{ id: 4, name: 'DB-Server-02', location: 'A区', rack: 'R2', status: 'error', ip: '192.168.1.202', cpu: 0, memory: 0, temperature: 0, uptime: '0天', position: { row: 0, col: 1, level: 1 } }
]
},
{
name: '网络设备',
devices: [
{ id: 5, name: 'Core-Switch-01', location: 'B区', rack: 'R3', status: 'normal', ip: '192.168.1.1', cpu: 25, memory: 45, temperature: 38, uptime: '298天', position: { row: 1, col: 0, level: 0 } },
{ id: 6, name: 'Core-Switch-02', location: 'B区', rack: 'R3', status: 'normal', ip: '192.168.1.2', cpu: 28, memory: 42, temperature: 39, uptime: '298天', position: { row: 1, col: 0, level: 1 } },
{ id: 7, name: 'Firewall-01', location: 'B区', rack: 'R4', status: 'warning', ip: '192.168.1.254', cpu: 76, memory: 71, temperature: 52, uptime: '178天', position: { row: 1, col: 1, level: 0 } }
]
},
{
name: '存储设备',
devices: [
{ id: 8, name: 'Storage-Array-01', location: 'C区', rack: 'R5', status: 'normal', ip: '192.168.2.100', cpu: 35, memory: 68, temperature: 44, uptime: '234天', position: { row: 2, col: 0, level: 0 } },
{ id: 9, name: 'Backup-Server-01', location: 'C区', rack: 'R6', status: 'normal', ip: '192.168.2.200', cpu: 22, memory: 38, temperature: 36, uptime: '267天', position: { row: 2, col: 1, level: 0 } }
]
}
],
dataUpdateInterval: null
};
},
computed: {
totalDevices() {
return this.deviceCategories.reduce((sum, cat) => sum + cat.devices.length, 0);
},
onlineDevices() {
let count = 0;
this.deviceCategories.forEach(cat => {
cat.devices.forEach(dev => {
if (dev.status !== 'error') count++;
});
});
return count;
},
warningDevices() {
let count = 0;
this.deviceCategories.forEach(cat => {
cat.devices.forEach(dev => {
if (dev.status === 'warning') count++;
});
});
return count;
},
errorDevices() {
let count = 0;
this.deviceCategories.forEach(cat => {
cat.devices.forEach(dev => {
if (dev.status === 'error') count++;
});
});
return count;
}
},
mounted() {
this.initScene();
this.createMachineRoom();
this.animate();
this.startDataUpdate();
window.addEventListener('resize', this.onWindowResize);
window.addEventListener('click', this.onMouseClick);
},
beforeUnmount() {
window.removeEventListener('resize', this.onWindowResize);
window.removeEventListener('click', this.onMouseClick);
if (this.animationId) {
cancelAnimationFrame(this.animationId);
}
if (this.dataUpdateInterval) {
clearInterval(this.dataUpdateInterval);
}
this.dispose();
},
methods: {
initScene() {
const container = document.getElementById('canvas-container');
const width = container.clientWidth;
const height = container.clientHeight;
this.scene = new Scene();
this.scene.background = new Color(0x1a1a2e);
this.camera = new PerspectiveCamera(60, width / height, 0.1, 1000);
this.camera.position.set(10, 8, 10);
this.renderer = new WebGLRenderer({ antialias: true });
this.renderer.setSize(width, height);
this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
this.renderer.shadowMap.enabled = true;
container.appendChild(this.renderer.domElement);
this.controls = new THREE.OrbitControls(this.camera, this.renderer.domElement);
this.controls.enableDamping = true;
this.controls.dampingFactor = 0.05;
this.controls.maxPolarAngle = Math.PI / 2.1;
this.raycaster = new Raycaster();
this.mouse = new Vector2();
const ambientLight = new AmbientLight(0xffffff, 0.5);
this.scene.add(ambientLight);
const pointLight1 = new PointLight(0xffffff, 0.8, 100);
pointLight1.position.set(5, 10, 5);
pointLight1.castShadow = true;
this.scene.add(pointLight1);
const pointLight2 = new PointLight(0xffffff, 0.5, 100);
pointLight2.position.set(-5, 10, -5);
this.scene.add(pointLight2);
// 地面
const groundGeometry = new BoxGeometry(20, 0.2, 15);
const groundMaterial = new MeshStandardMaterial({
color: 0x2a2a3e,
roughness: 0.8,
metalness: 0.2
});
const ground = new Mesh(groundGeometry, groundMaterial);
ground.receiveShadow = true;
ground.position.y = -0.1;
this.scene.add(ground);
},
createMachineRoom() {
// 创建机柜
for (let row = 0; row < 3; row++) {
for (let col = 0; col < 2; col++) {
const rack = this.createRack(row, col);
this.rackMeshes.push(rack);
this.scene.add(rack);
}
}
// 创建设备
this.deviceCategories.forEach(category => {
category.devices.forEach(device => {
const deviceMesh = this.createDevice(device);
deviceMesh.userData = device;
this.deviceMeshes.push(deviceMesh);
this.scene.add(deviceMesh);
});
});
},
createRack(row, col) {
const group = new Group();
// 机柜框架
const frameGeometry = new BoxGeometry(1.8, 4, 1);
const frameMaterial = new MeshStandardMaterial({
color: 0x3a3a4e,
metalness: 0.6,
roughness: 0.4
});
const frame = new Mesh(frameGeometry, frameMaterial);
frame.castShadow = true;
frame.receiveShadow = true;
frame.position.y = 2;
group.add(frame);
// 机柜门
const doorGeometry = new BoxGeometry(1.82, 3.8, 0.05);
const doorMaterial = new MeshStandardMaterial({
color: 0x2a2a3e,
metalness: 0.8,
roughness: 0.3,
transparent: true,
opacity: 0.9
});
const door = new Mesh(doorGeometry, doorMaterial);
door.position.set(0, 2, 0.52);
group.add(door);
group.position.set(col * 3 - 1.5, 0, row * 4 - 4);
return group;
},
createDevice(data) {
const geometry = new BoxGeometry(1.6, 0.3, 0.8);
const material = new MeshStandardMaterial({
color: this.getDeviceColor(data.status),
metalness: 0.7,
roughness: 0.3,
emissive: this.getDeviceColor(data.status),
emissiveIntensity: 0.3
});
const mesh = new Mesh(geometry, material);
mesh.castShadow = true;
const { row, col, level } = data.position;
const x = col * 3 - 1.5;
const y = 0.5 + level * 0.5;
const z = row * 4 - 4;
mesh.position.set(x, y, z);
mesh.userData.material = material;
mesh.userData.originalColor = material.color.getHex();
return mesh;
},
getDeviceColor(status) {
const colors = {
normal: 0x00ff88,
warning: 0xffd93d,
error: 0xff6b6b
};
return colors[status] || colors.normal;
},
selectDevice(device) {
// 重置之前选中的设备
this.deviceMeshes.forEach(mesh => {
if (mesh.userData.material) {
mesh.userData.material.emissiveIntensity = 0.3;
mesh.userData.material.color.setHex(mesh.userData.originalColor);
}
});
this.selectedDevice = device;
// 高亮选中的设备
const selectedMesh = this.deviceMeshes.find(m => m.userData.id === device.id);
if (selectedMesh && selectedMesh.userData.material) {
selectedMesh.userData.material.emissiveIntensity = 0.8;
selectedMesh.userData.material.color.setHex(0x00d4ff);
}
// 聚焦到设备
if (this.controls && selectedMesh) {
this.controls.target.copy(selectedMesh.position);
this.controls.update();
}
},
setViewMode(mode) {
this.viewMode = mode;
if (mode === 'overview') {
this.camera.position.set(10, 8, 10);
this.controls.target.set(0, 2, 0);
} else {
this.camera.position.set(5, 4, 5);
this.controls.target.set(0, 2, 0);
}
this.controls.update();
},
resetView() {
this.camera.position.set(10, 8, 10);
this.controls.target.set(0, 2, 0);
this.controls.update();
},
getCpuClass(cpu) {
if (cpu > 80) return 'error';
if (cpu > 60) return 'warning';
return '';
},
getMemoryClass(memory) {
if (memory > 85) return 'error';
if (memory > 70) return 'warning';
return '';
},
getTempClass(temp) {
if (temp > 55) return 'error';
if (temp > 45) return 'warning';
return '';
},
startDataUpdate() {
this.dataUpdateInterval = setInterval(() => {
this.deviceCategories.forEach(category => {
category.devices.forEach(device => {
if (device.status !== 'error') {
device.cpu += Math.floor((Math.random() - 0.5) * 10);
device.cpu = Math.max(10, Math.min(device.cpu, 95));
device.memory += Math.floor((Math.random() - 0.5) * 5);
device.memory = Math.max(20, Math.min(device.memory, 95));
device.temperature += Math.floor((Math.random() - 0.5) * 3);
device.temperature = Math.max(30, Math.min(device.temperature, 65));
}
});
});
}, 3000);
},
onMouseClick(event) {
const container = document.getElementById('canvas-container');
const rect = container.getBoundingClientRect();
this.mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
this.mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
this.raycaster.setFromCamera(this.mouse, this.camera);
const intersects = this.raycaster.intersectObjects(this.deviceMeshes);
if (intersects.length > 0) {
const device = intersects[0].object.userData;
if (device && device.id) {
this.selectDevice(device);
}
}
},
animate() {
this.animationId = requestAnimationFrame(this.animate);
if (this.controls) {
this.controls.update();
}
// 设备脉冲动画
const time = Date.now() * 0.001;
this.deviceMeshes.forEach((mesh, i) => {
if (mesh.userData.material && mesh.userData.status !== 'error') {
mesh.userData.material.emissiveIntensity =
0.3 + Math.sin(time * 3 + i * 0.5) * 0.1;
}
});
if (this.renderer && this.scene && this.camera) {
this.renderer.render(this.scene, this.camera);
}
},
onWindowResize() {
const container = document.getElementById('canvas-container');
if (!container) return;
const width = container.clientWidth;
const height = container.clientHeight;
if (this.camera) {
this.camera.aspect = width / height;
this.camera.updateProjectionMatrix();
}
if (this.renderer) {
this.renderer.setSize(width, height);
}
},
dispose() {
if (this.scene) {
this.scene.traverse((object) => {
if (object.geometry) object.geometry.dispose();
if (object.material) {
if (Array.isArray(object.material)) {
object.material.forEach(m => m.dispose());
} else {
object.material.dispose();
}
}
});
}
if (this.renderer) this.renderer.dispose();
if (this.controls) this.controls.dispose();
}
}
}).mount('#app');
</script>
</body>
</html>
返回笔记首页
3D机房可视化监控系统
主题配置