<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>智慧园区数字孪生系统</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: #0a0e27;
color: white;
overflow: hidden;
}
#app {
width: 100vw;
height: 100vh;
position: relative;
}
#canvas-container {
width: 100%;
height: 100%;
}
.overlay {
position: absolute;
pointer-events: none;
}
.header {
top: 0;
left: 0;
right: 0;
padding: 20px 30px;
background: linear-gradient(180deg, rgba(10,14,39,0.95) 0%, rgba(10,14,39,0) 100%);
pointer-events: auto;
}
.header h1 {
font-size: 28px;
font-weight: 700;
color: #00d4ff;
margin-bottom: 5px;
text-shadow: 0 0 20px rgba(0,212,255,0.5);
}
.header p {
font-size: 14px;
color: rgba(255,255,255,0.7);
}
.data-panels {
position: absolute;
top: 100px;
right: 20px;
width: 320px;
display: flex;
flex-direction: column;
gap: 15px;
pointer-events: auto;
}
.data-card {
background: rgba(10,30,60,0.85);
border: 1px solid rgba(0,212,255,0.3);
border-radius: 12px;
padding: 20px;
backdrop-filter: blur(10px);
box-shadow: 0 8px 32px rgba(0,0,0,0.3);
}
.data-card h3 {
font-size: 16px;
color: #00d4ff;
margin-bottom: 15px;
display: flex;
align-items: center;
gap: 8px;
}
.data-card h3::before {
content: '';
width: 4px;
height: 16px;
background: linear-gradient(180deg, #00d4ff, #0066ff);
border-radius: 2px;
}
.data-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 0;
border-bottom: 1px solid rgba(255,255,255,0.1);
}
.data-item:last-child {
border-bottom: none;
}
.data-label {
color: rgba(255,255,255,0.7);
font-size: 14px;
}
.data-value {
font-size: 18px;
font-weight: 700;
color: #00d4ff;
}
.data-value.warning {
color: #ffd700;
}
.data-value.danger {
color: #ff4757;
}
.status-indicator {
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
margin-left: 8px;
animation: pulse 2s ease-in-out infinite;
}
.status-indicator.normal {
background: #2ecc71;
box-shadow: 0 0 10px #2ecc71;
}
.status-indicator.warning {
background: #ffd700;
box-shadow: 0 0 10px #ffd700;
}
.status-indicator.danger {
background: #ff4757;
box-shadow: 0 0 10px #ff4757;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.building-list {
max-height: 250px;
overflow-y: auto;
}
.building-item {
padding: 12px;
margin-bottom: 8px;
background: rgba(0,212,255,0.05);
border-left: 3px solid #00d4ff;
border-radius: 4px;
cursor: pointer;
transition: all 0.3s;
}
.building-item:hover {
background: rgba(0,212,255,0.15);
transform: translateX(5px);
}
.building-item.selected {
background: rgba(0,212,255,0.2);
border-left-color: #ffd700;
}
.building-name {
font-size: 14px;
font-weight: 600;
margin-bottom: 5px;
}
.building-info {
font-size: 12px;
color: rgba(255,255,255,0.6);
}
.controls {
position: absolute;
bottom: 30px;
left: 50%;
transform: translateX(-50%);
display: flex;
gap: 15px;
pointer-events: auto;
}
.btn {
padding: 12px 24px;
background: rgba(0,212,255,0.15);
border: 1px solid rgba(0,212,255,0.5);
border-radius: 8px;
color: #00d4ff;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
backdrop-filter: blur(10px);
}
.btn:hover {
background: rgba(0,212,255,0.3);
transform: translateY(-2px);
box-shadow: 0 5px 20px rgba(0,212,255,0.3);
}
.btn.active {
background: rgba(0,212,255,0.4);
border-color: #00d4ff;
}
.legend {
position: absolute;
bottom: 30px;
right: 20px;
background: rgba(10,30,60,0.85);
border: 1px solid rgba(0,212,255,0.3);
border-radius: 12px;
padding: 15px;
backdrop-filter: blur(10px);
pointer-events: auto;
}
.legend h4 {
font-size: 14px;
color: #00d4ff;
margin-bottom: 10px;
}
.legend-item {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 8px;
font-size: 12px;
}
.legend-color {
width: 20px;
height: 20px;
border-radius: 4px;
}
::-webkit-scrollbar {
width: 6px;
}
::-webkit-scrollbar-track {
background: rgba(255,255,255,0.05);
border-radius: 3px;
}
::-webkit-scrollbar-thumb {
background: rgba(0,212,255,0.3);
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: rgba(0,212,255,0.5);
}
</style>
</head>
<body>
<div id="app">
<div id="canvas-container"></div>
<div class="overlay header">
<h1>智慧园区数字孪生系统</h1>
<p>实时监控园区运行状态 | 数据更新频率: 实时</p>
</div>
<div class="data-panels">
<div class="data-card">
<h3>实时数据总览</h3>
<div class="data-item">
<span class="data-label">在线人数</span>
<span class="data-value">{{ realtimeData.people }}</span>
</div>
<div class="data-item">
<span class="data-label">能耗 (kW)</span>
<span class="data-value" :class="energyStatus">{{ realtimeData.energy }}</span>
</div>
<div class="data-item">
<span class="data-label">设备在线率</span>
<span class="data-value">{{ realtimeData.deviceOnline }}%</span>
</div>
<div class="data-item">
<span class="data-label">告警数量</span>
<span class="data-value danger">{{ realtimeData.alarms }}</span>
</div>
</div>
<div class="data-card">
<h3>建筑列表</h3>
<div class="building-list">
<div
v-for="building in buildings"
:key="building.id"
class="building-item"
:class="{ selected: selectedBuilding === building.id }"
@click="selectBuilding(building)"
>
<div class="building-name">
{{ building.name }}
<span
class="status-indicator"
:class="building.status"
></span>
</div>
<div class="building-info">
人数: {{ building.people }} | 能耗: {{ building.energy }}kW
</div>
</div>
</div>
</div>
</div>
<div class="controls">
<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" :class="{ active: showHeatmap }" @click="toggleHeatmap">
{{ showHeatmap ? '关闭' : '显示' }}热力图
</button>
</div>
<div class="legend" v-if="showHeatmap">
<h4>能耗热力图</h4>
<div class="legend-item">
<div class="legend-color" style="background: #2ecc71"></div>
<span>正常 (0-60kW)</span>
</div>
<div class="legend-item">
<div class="legend-color" style="background: #ffd700"></div>
<span>偏高 (60-90kW)</span>
</div>
<div class="legend-item">
<div class="legend-color" style="background: #ff4757"></div>
<span>过高 (90kW+)</span>
</div>
</div>
</div>
<script>
const { createApp } = Vue;
const { Scene, PerspectiveCamera, WebGLRenderer, AmbientLight, DirectionalLight,
BoxGeometry, MeshStandardMaterial, Mesh, Group, Color, Fog } = THREE;
createApp({
data() {
return {
scene: null,
camera: null,
renderer: null,
controls: null,
animationId: null,
buildingMeshes: [],
viewMode: 'overview',
showHeatmap: false,
selectedBuilding: null,
realtimeData: {
people: 2847,
energy: 1256,
deviceOnline: 98.5,
alarms: 3
},
buildings: [
{ id: 1, name: 'A栋办公楼', people: 856, energy: 342, status: 'normal', position: { x: -6, z: -6 } },
{ id: 2, name: 'B栋研发楼', people: 632, energy: 278, status: 'normal', position: { x: -6, z: 0 } },
{ id: 3, name: 'C栋实验楼', people: 421, energy: 456, status: 'warning', position: { x: -6, z: 6 } },
{ id: 4, name: 'D栋生产车间', people: 538, energy: 1089, status: 'danger', position: { x: 0, z: -6 } },
{ id: 5, name: 'E栋仓储楼', people: 145, energy: 123, status: 'normal', position: { x: 0, z: 0 } },
{ id: 6, name: 'F栋展示中心', people: 255, energy: 267, status: 'normal', position: { x: 0, z: 6 } }
],
dataUpdateInterval: null
};
},
computed: {
energyStatus() {
const energy = this.realtimeData.energy;
if (energy > 1500) return 'danger';
if (energy > 1200) return 'warning';
return '';
}
},
mounted() {
this.initScene();
this.createPark();
this.animate();
this.startDataUpdate();
window.addEventListener('resize', this.onWindowResize);
},
beforeUnmount() {
window.removeEventListener('resize', this.onWindowResize);
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(0x0a0e27);
this.scene.fog = new Fog(0x0a0e27, 20, 50);
this.camera = new PerspectiveCamera(60, width / height, 0.1, 1000);
this.camera.position.set(0, 25, 25);
this.camera.lookAt(0, 0, 0);
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.2;
this.controls.minDistance = 10;
this.controls.maxDistance = 60;
const ambientLight = new AmbientLight(0xffffff, 0.4);
this.scene.add(ambientLight);
const dirLight = new DirectionalLight(0xffffff, 0.8);
dirLight.position.set(10, 20, 10);
dirLight.castShadow = true;
dirLight.shadow.camera.left = -30;
dirLight.shadow.camera.right = 30;
dirLight.shadow.camera.top = 30;
dirLight.shadow.camera.bottom = -30;
this.scene.add(dirLight);
// 创建地面
const groundGeometry = new BoxGeometry(40, 0.2, 40);
const groundMaterial = new MeshStandardMaterial({
color: 0x1a2332,
roughness: 0.8,
metalness: 0.2
});
const ground = new Mesh(groundGeometry, groundMaterial);
ground.receiveShadow = true;
ground.position.y = -0.1;
this.scene.add(ground);
// 添加网格线
this.createGrid();
},
createGrid() {
const gridSize = 40;
const divisions = 20;
const gridHelper = new THREE.GridHelper(gridSize, divisions, 0x00d4ff, 0x1a3a52);
gridHelper.material.transparent = true;
gridHelper.material.opacity = 0.3;
this.scene.add(gridHelper);
},
createPark() {
this.buildings.forEach(building => {
const buildingMesh = this.createBuilding(building);
buildingMesh.userData = building;
this.buildingMeshes.push(buildingMesh);
this.scene.add(buildingMesh);
});
},
createBuilding(data) {
const group = new Group();
// 建筑高度根据能耗决定
const height = 3 + (data.energy / 200);
// 主体建筑
const geometry = new BoxGeometry(4, height, 4);
const material = new MeshStandardMaterial({
color: this.getBuildingColor(data.status),
roughness: 0.7,
metalness: 0.3,
emissive: this.getBuildingColor(data.status),
emissiveIntensity: 0.2
});
const mesh = new Mesh(geometry, material);
mesh.castShadow = true;
mesh.receiveShadow = true;
mesh.position.y = height / 2;
group.add(mesh);
// 顶部标识
const topGeometry = new BoxGeometry(4.2, 0.2, 4.2);
const topMaterial = new MeshStandardMaterial({
color: 0x00d4ff,
emissive: 0x00d4ff,
emissiveIntensity: 0.5
});
const top = new Mesh(topGeometry, topMaterial);
top.position.y = height + 0.1;
group.add(top);
group.position.set(data.position.x, 0, data.position.z);
group.userData.material = material;
group.userData.originalColor = material.color.getHex();
return group;
},
getBuildingColor(status) {
const colors = {
normal: 0x2ecc71,
warning: 0xffd700,
danger: 0xff4757
};
return colors[status] || colors.normal;
},
selectBuilding(building) {
// 重置之前选中的建筑
this.buildingMeshes.forEach(mesh => {
if (mesh.userData.material) {
mesh.userData.material.emissiveIntensity = 0.2;
mesh.userData.material.color.setHex(mesh.userData.originalColor);
}
});
// 高亮选中的建筑
this.selectedBuilding = building.id;
const selectedMesh = this.buildingMeshes.find(m => m.userData.id === building.id);
if (selectedMesh && selectedMesh.userData.material) {
selectedMesh.userData.material.emissiveIntensity = 0.6;
selectedMesh.userData.material.color.setHex(0x00d4ff);
}
// 相机聚焦到选中的建筑
if (this.controls) {
this.controls.target.set(building.position.x, 0, building.position.z);
this.controls.update();
}
},
setViewMode(mode) {
this.viewMode = mode;
if (mode === 'overview') {
this.camera.position.set(0, 25, 25);
this.controls.target.set(0, 0, 0);
} else {
this.camera.position.set(0, 15, 15);
this.controls.target.set(0, 0, 0);
}
this.controls.update();
},
toggleHeatmap() {
this.showHeatmap = !this.showHeatmap;
this.buildingMeshes.forEach(mesh => {
if (mesh.userData.material) {
if (this.showHeatmap) {
const energy = mesh.userData.energy;
let color;
if (energy > 900) color = 0xff4757;
else if (energy > 600) color = 0xffd700;
else color = 0x2ecc71;
mesh.userData.material.color.setHex(color);
mesh.userData.material.emissiveIntensity = 0.4;
} else {
mesh.userData.material.color.setHex(mesh.userData.originalColor);
mesh.userData.material.emissiveIntensity = 0.2;
}
}
});
},
startDataUpdate() {
this.dataUpdateInterval = setInterval(() => {
// 模拟实时数据更新
this.realtimeData.people = 2800 + Math.floor(Math.random() * 100);
this.realtimeData.energy = 1200 + Math.floor(Math.random() * 200);
this.realtimeData.deviceOnline = 98 + Math.random() * 2;
// 更新建筑数据
this.buildings.forEach(building => {
building.people += Math.floor((Math.random() - 0.5) * 20);
building.people = Math.max(50, Math.min(building.people, 1000));
building.energy += Math.floor((Math.random() - 0.5) * 50);
building.energy = Math.max(50, Math.min(building.energy, 1500));
});
}, 3000);
},
animate() {
this.animationId = requestAnimationFrame(this.animate);
if (this.controls) {
this.controls.update();
}
// 建筑脉冲动画
this.buildingMeshes.forEach((mesh, i) => {
const time = Date.now() * 0.001;
if (mesh.userData.material) {
mesh.userData.material.emissiveIntensity =
0.2 + Math.sin(time * 2 + 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>
返回笔记首页
智慧园区数字孪生系统
主题配置