Compare commits
7 Commits
c2e662cbf4
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 3e0c8a70c8 | |||
| 13d25763df | |||
| 49c8171dc8 | |||
| dbb9026bc4 | |||
| 94830006bc | |||
| f3de05d7dc | |||
| 6e0e447fa3 |
8
package-lock.json
generated
@@ -9,7 +9,7 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"element-ui": "^2.15.13",
|
"element-ui": "^2.15.13",
|
||||||
"three": "^0.124.0",
|
"three": "^0.184.0",
|
||||||
"vue": "^2.6.14",
|
"vue": "^2.6.14",
|
||||||
"vue-router": "^3.5.3"
|
"vue-router": "^3.5.3"
|
||||||
},
|
},
|
||||||
@@ -6996,9 +6996,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/three": {
|
"node_modules/three": {
|
||||||
"version": "0.124.0",
|
"version": "0.184.0",
|
||||||
"resolved": "https://registry.npmmirror.com/three/-/three-0.124.0.tgz",
|
"resolved": "https://registry.npmmirror.com/three/-/three-0.184.0.tgz",
|
||||||
"integrity": "sha512-ROXp1Ly7YyF+jC910DQyAWj++Qlw2lQv0qwYLNQwdDbjk4bsOXAfGO92wYTMPNei1GMJUmCxSxc3MjGBTS09Rg=="
|
"integrity": "sha512-wtTRjG92pM5eUg/KuUnHsqSAlPM296brTOcLgMRqEeylYTh/CdtvKUvCyyCQTzFuStieWxvZb8mVTMvdPyUpxg=="
|
||||||
},
|
},
|
||||||
"node_modules/throttle-debounce": {
|
"node_modules/throttle-debounce": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
|
|||||||
@@ -8,9 +8,9 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"element-ui": "^2.15.13",
|
"element-ui": "^2.15.13",
|
||||||
|
"three": "^0.184.0",
|
||||||
"vue": "^2.6.14",
|
"vue": "^2.6.14",
|
||||||
"vue-router": "^3.5.3",
|
"vue-router": "^3.5.3"
|
||||||
"three": "^0.124.0"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vue/cli-service": "^5.0.8",
|
"@vue/cli-service": "^5.0.8",
|
||||||
|
|||||||
BIN
public/山海防灾模型展示.zip
Normal file
BIN
public/山海防灾模型展示/三相隔离开关5.png
Normal file
|
After Width: | Height: | Size: 139 KiB |
BIN
public/山海防灾模型展示/三相隔离开关7.png
Normal file
|
After Width: | Height: | Size: 116 KiB |
BIN
public/山海防灾模型展示/三相隔离开关_1.png
Normal file
|
After Width: | Height: | Size: 114 KiB |
BIN
public/山海防灾模型展示/三相隔离开关_2.png
Normal file
|
After Width: | Height: | Size: 132 KiB |
BIN
public/山海防灾模型展示/三相隔离开关_3.png
Normal file
|
After Width: | Height: | Size: 110 KiB |
BIN
public/山海防灾模型展示/三相隔离开关_4.png
Normal file
|
After Width: | Height: | Size: 167 KiB |
BIN
public/山海防灾模型展示/三相隔离开关_6.png
Normal file
|
After Width: | Height: | Size: 135 KiB |
BIN
public/山海防灾模型展示/三相隔离开关_move_2.png
Normal file
|
After Width: | Height: | Size: 39 KiB |
BIN
public/山海防灾模型展示/三相隔离开关move_2.png
Normal file
|
After Width: | Height: | Size: 38 KiB |
BIN
public/山海防灾模型展示/交流棒形悬式复合绝缘子_1.png
Normal file
|
After Width: | Height: | Size: 81 KiB |
BIN
public/山海防灾模型展示/交流棒形悬式复合绝缘子_2.png
Normal file
|
After Width: | Height: | Size: 82 KiB |
BIN
public/山海防灾模型展示/交流棒形悬式复合绝缘子_3.png
Normal file
|
After Width: | Height: | Size: 99 KiB |
BIN
public/山海防灾模型展示/交流棒形悬式复合绝缘子_4.png
Normal file
|
After Width: | Height: | Size: 76 KiB |
BIN
public/山海防灾模型展示/交流棒形悬式复合绝缘子_move_1.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
public/山海防灾模型展示/交流棒形悬式复合绝缘子_move_2.png
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
public/山海防灾模型展示/交流避雷器_1.png
Normal file
|
After Width: | Height: | Size: 92 KiB |
BIN
public/山海防灾模型展示/交流避雷器_2.png
Normal file
|
After Width: | Height: | Size: 91 KiB |
BIN
public/山海防灾模型展示/交流避雷器_3.png
Normal file
|
After Width: | Height: | Size: 108 KiB |
BIN
public/山海防灾模型展示/交流避雷器_4.png
Normal file
|
After Width: | Height: | Size: 74 KiB |
BIN
public/山海防灾模型展示/交流避雷器_5.png
Normal file
|
After Width: | Height: | Size: 85 KiB |
BIN
public/山海防灾模型展示/交流避雷器_move_1.png
Normal file
|
After Width: | Height: | Size: 38 KiB |
BIN
public/山海防灾模型展示/交流避雷器_move_2.png
Normal file
|
After Width: | Height: | Size: 42 KiB |
BIN
public/山海防灾模型展示/交流避雷器_move_3.png
Normal file
|
After Width: | Height: | Size: 44 KiB |
@@ -55,6 +55,9 @@
|
|||||||
</el-collapse>
|
</el-collapse>
|
||||||
</div>
|
</div>
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
|
<!-- 关键元器件检查 -->
|
||||||
|
<el-tab-pane label="附件" name="file">
|
||||||
|
</el-tab-pane>
|
||||||
</el-tabs>
|
</el-tabs>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -217,6 +220,10 @@ export default {
|
|||||||
padding: 10px;
|
padding: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.el-tabs >>> .el-tabs__item{
|
||||||
|
padding: 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
.check-item {
|
.check-item {
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,27 @@
|
|||||||
<div class="three-viewer">
|
<div class="three-viewer">
|
||||||
<div ref="container" class="canvas-container"></div>
|
<div ref="container" class="canvas-container"></div>
|
||||||
<div class="annotations-container" ref="annotationsContainer"></div>
|
<div class="annotations-container" ref="annotationsContainer"></div>
|
||||||
|
<!-- 调试面板 -->
|
||||||
|
<div v-if="debugMode" class="debug-panel">
|
||||||
|
<h3>调试模式</h3>
|
||||||
|
<div class="debug-info">
|
||||||
|
<p>点击模型查看部件信息</p>
|
||||||
|
<div v-if="selectedMeshInfo">
|
||||||
|
<p><strong>部件名称:</strong> {{ selectedMeshInfo.name || '未命名' }}</p>
|
||||||
|
<p><strong>坐标:</strong></p>
|
||||||
|
<pre>{{ JSON.stringify(selectedMeshInfo.position, null, 2) }}</pre>
|
||||||
|
<button @click="copyPosition">复制坐标</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mesh-list">
|
||||||
|
<h4>模型部件列表:</h4>
|
||||||
|
<ul>
|
||||||
|
<li v-for="(mesh, index) in meshList" :key="index">
|
||||||
|
{{ mesh.name || `未命名_${index}` }}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -79,17 +100,17 @@ export default {
|
|||||||
pmremGenerator: null,
|
pmremGenerator: null,
|
||||||
labelRenderer: null,
|
labelRenderer: null,
|
||||||
annotationObjects: [],
|
annotationObjects: [],
|
||||||
selectedPart: null
|
selectedPart: null,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
mounted () {
|
mounted () {
|
||||||
// this.onWindowResize(); // 初始化后立即调用一次
|
// this.onWindowResize(); // 初始化后立即调用一次
|
||||||
// 添加窗口大小变化监听
|
// 添加窗口大小变化监听
|
||||||
// window.addEventListener('resize', this.onWindowResize);
|
// window.addEventListener('resize', this.onWindowResize);
|
||||||
this.resizeObserver = new ResizeObserver(this.onWindowResize);
|
this.resizeObserver = new ResizeObserver(this.onWindowResize);
|
||||||
this.resizeObserver.observe(this.$refs.container);
|
this.resizeObserver.observe(this.$refs.container);
|
||||||
|
|
||||||
// window.addEventListener('click', this.onMouseClick, false);
|
window.addEventListener('click', this.onMouseClick, false);
|
||||||
this.highlightColor = new THREE.Color(0xff0000); // 高亮颜色为红色
|
this.highlightColor = new THREE.Color(0xff0000); // 高亮颜色为红色
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
@@ -277,10 +298,10 @@ export default {
|
|||||||
this.camera.updateProjectionMatrix();
|
this.camera.updateProjectionMatrix();
|
||||||
|
|
||||||
// 创建一个父对象用于旋转
|
// 创建一个父对象用于旋转
|
||||||
this.parent = new THREE.Object3D(); // 创建一个新的 Object3D 作为父对象
|
// this.parent = new THREE.Object3D(); // 创建一个新的 Object3D 作为父对象
|
||||||
this.parent.add(this.model); // 将模型添加到父对象
|
// this.parent.add(this.model); // 将模型添加到父对象
|
||||||
|
|
||||||
this.scene.add(this.parent); // 将父对象添加到场景
|
this.scene.add(this.model); // 将父对象添加到场景
|
||||||
|
|
||||||
// 让控制器聚焦到模型中心 聚焦中心
|
// 让控制器聚焦到模型中心 聚焦中心
|
||||||
this.controls.target.copy(center);
|
this.controls.target.copy(center);
|
||||||
@@ -347,40 +368,7 @@ export default {
|
|||||||
// 点击射线
|
// 点击射线
|
||||||
onMouseClick(event) {
|
onMouseClick(event) {
|
||||||
console.log('我点击了')
|
console.log('我点击了')
|
||||||
// const raycaster = new THREE.Raycaster();
|
|
||||||
// const mouse = new THREE.Vector2();
|
|
||||||
// // 计算鼠标在屏幕上的位置并转换为Normalized device coordinates (NDC)
|
|
||||||
// console.log(this.sceneDomElement.clientWidth, window.innerWidth)
|
|
||||||
// mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
|
|
||||||
// mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
|
|
||||||
|
|
||||||
// // 更新Raycaster的射线位置和方向
|
|
||||||
// raycaster.setFromCamera(mouse, this.camera);
|
|
||||||
if(!this.sceneDomElement) return
|
|
||||||
let getBoundingClientRect = this.sceneDomElement.getBoundingClientRect()
|
|
||||||
// 屏幕坐标转标准设备坐标
|
|
||||||
let x = ((event.clientX - getBoundingClientRect .left) / this.sceneDomElement.offsetWidth) * 2 - 1;// 标准设备横坐标
|
|
||||||
let y = -((event.clientY - getBoundingClientRect .top) / this.sceneDomElement.offsetHeight) * 2 + 1;// 标准设备纵坐标
|
|
||||||
let standardVector = new THREE.Vector3(x, y, 1);// 标准设备坐标
|
|
||||||
// 标准设备坐标转世界坐标
|
|
||||||
let worldVector = standardVector.unproject(this.camera);
|
|
||||||
// 射线投射方向单位向量(worldVector坐标减相机位置坐标)
|
|
||||||
let ray = worldVector.sub(this.camera.position).normalize();
|
|
||||||
// 创建射线投射器对象
|
|
||||||
let rayCaster = new THREE.Raycaster(this.camera.position, ray);
|
|
||||||
|
|
||||||
// 计算物体和射线的交点
|
|
||||||
const intersects = rayCaster.intersectObjects(this.model.children, true);
|
|
||||||
if (intersects.length !== 0 && intersects[0].object instanceof THREE.Mesh) {
|
|
||||||
// this.controls.target.copy(intersects[0].point);
|
|
||||||
let selectedObject = intersects[0].object;
|
|
||||||
let selectedObjects = [];
|
|
||||||
selectedObjects.push(selectedObject.parent);
|
|
||||||
this.outlineObj(selectedObjects);
|
|
||||||
} else {
|
|
||||||
console.log('没找到', this.composer)
|
|
||||||
this.composer = null
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
outlineObj(selectedObjects) {
|
outlineObj(selectedObjects) {
|
||||||
let outlinePass;
|
let outlinePass;
|
||||||
|
|||||||
610
src/components/ThreeViewerDebug copy.vue
Normal file
@@ -0,0 +1,610 @@
|
|||||||
|
<template>
|
||||||
|
<div class="three-viewer">
|
||||||
|
<div ref="container" class="canvas-container"></div>
|
||||||
|
<div class="annotations-container" ref="annotationsContainer"></div>
|
||||||
|
|
||||||
|
<!-- 调试面板 -->
|
||||||
|
<div v-if="debugMode" class="debug-panel">
|
||||||
|
<h3>调试模式</h3>
|
||||||
|
<div class="debug-info">
|
||||||
|
<p>点击模型查看部件信息</p>
|
||||||
|
<div v-if="selectedMeshInfo">
|
||||||
|
<p><strong>部件名称:</strong> {{ selectedMeshInfo.name || '未命名' }}</p>
|
||||||
|
<p><strong>坐标:</strong></p>
|
||||||
|
<pre>{{ JSON.stringify(selectedMeshInfo.position, null, 2) }}</pre>
|
||||||
|
<button @click="copyPosition">复制坐标</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mesh-list">
|
||||||
|
<h4>模型部件列表:</h4>
|
||||||
|
<ul>
|
||||||
|
<li v-for="(mesh, index) in meshList" :key="index">
|
||||||
|
{{ mesh.name || `未命名_${index}` }}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import * as THREE from 'three';
|
||||||
|
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
|
||||||
|
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
|
||||||
|
import { CSS2DRenderer, CSS2DObject } from 'three/examples/jsm/renderers/CSS2DRenderer';
|
||||||
|
import { EffectComposer } from "three/examples/jsm/postprocessing/EffectComposer.js";
|
||||||
|
import { RenderPass } from "three/examples/jsm/postprocessing/RenderPass.js";
|
||||||
|
import { OutlinePass } from "three/examples/jsm/postprocessing/OutlinePass.js";
|
||||||
|
import { RoomEnvironment } from 'three/examples/jsm/environments/RoomEnvironment.js';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'ThreeViewerDebug',
|
||||||
|
props: {
|
||||||
|
modelPath: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
annotations: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
debugMode: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
scene: null,
|
||||||
|
camera: null,
|
||||||
|
renderer: null,
|
||||||
|
labelRenderer: null,
|
||||||
|
controls: null,
|
||||||
|
model: null,
|
||||||
|
composer: null,
|
||||||
|
outlinePass: null,
|
||||||
|
frameId: null,
|
||||||
|
sceneDomElement: null,
|
||||||
|
annotationObjects: [],
|
||||||
|
selectedPart: null,
|
||||||
|
partMeshMap: {},
|
||||||
|
meshList: [],
|
||||||
|
selectedMeshInfo: null,
|
||||||
|
raycaster: new THREE.Raycaster(),
|
||||||
|
mouse: new THREE.Vector2()
|
||||||
|
};
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
modelPath: {
|
||||||
|
handler(v) {
|
||||||
|
if (v) {
|
||||||
|
this.cleanup();
|
||||||
|
setTimeout(() => {
|
||||||
|
this.init();
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
immediate: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.resizeObserver = new ResizeObserver(this.onWindowResize);
|
||||||
|
this.resizeObserver.observe(this.$refs.container);
|
||||||
|
|
||||||
|
// 添加点击事件
|
||||||
|
window.addEventListener('click', this.onMouseClick, false);
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
this.cleanup();
|
||||||
|
window.removeEventListener('click', this.onMouseClick, false);
|
||||||
|
if (this.resizeObserver) {
|
||||||
|
this.resizeObserver.disconnect();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
cleanup() {
|
||||||
|
if (this.frameId) {
|
||||||
|
cancelAnimationFrame(this.frameId);
|
||||||
|
}
|
||||||
|
if (this.scene) {
|
||||||
|
this.scene.clear();
|
||||||
|
this.scene = null;
|
||||||
|
}
|
||||||
|
if (this.renderer) {
|
||||||
|
this.renderer.dispose();
|
||||||
|
this.renderer = null;
|
||||||
|
}
|
||||||
|
if (this.labelRenderer) {
|
||||||
|
this.labelRenderer.domElement.remove();
|
||||||
|
this.labelRenderer = null;
|
||||||
|
}
|
||||||
|
if (this.sceneDomElement) {
|
||||||
|
this.sceneDomElement.innerHTML = '';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.sceneDomElement = this.$refs.container;
|
||||||
|
|
||||||
|
// 创建场景
|
||||||
|
this.scene = new THREE.Scene();
|
||||||
|
this.scene.background = new THREE.Color(0xf0f0f0);
|
||||||
|
|
||||||
|
// 创建相机
|
||||||
|
const width = this.sceneDomElement.clientWidth;
|
||||||
|
const height = this.sceneDomElement.clientHeight;
|
||||||
|
this.camera = new THREE.PerspectiveCamera(45, width / height, 0.1, 1000);
|
||||||
|
this.camera.position.set(5, 5, 5);
|
||||||
|
|
||||||
|
// WebGL渲染器
|
||||||
|
this.renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
|
||||||
|
this.renderer.setPixelRatio(window.devicePixelRatio);
|
||||||
|
this.renderer.setSize(width, height);
|
||||||
|
this.renderer.shadowMap.enabled = true;
|
||||||
|
this.renderer.outputEncoding = THREE.sRGBEncoding;
|
||||||
|
this.sceneDomElement.appendChild(this.renderer.domElement);
|
||||||
|
|
||||||
|
// CSS2D渲染器
|
||||||
|
this.labelRenderer = new CSS2DRenderer();
|
||||||
|
this.labelRenderer.setSize(width, height);
|
||||||
|
this.labelRenderer.domElement.style.position = 'absolute';
|
||||||
|
this.labelRenderer.domElement.style.top = '0';
|
||||||
|
this.labelRenderer.domElement.style.pointerEvents = 'none';
|
||||||
|
this.$refs.annotationsContainer.appendChild(this.labelRenderer.domElement);
|
||||||
|
|
||||||
|
// 光照
|
||||||
|
const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
|
||||||
|
this.scene.add(ambientLight);
|
||||||
|
|
||||||
|
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
|
||||||
|
directionalLight.position.set(5, 10, 5);
|
||||||
|
directionalLight.castShadow = true;
|
||||||
|
this.scene.add(directionalLight);
|
||||||
|
|
||||||
|
// 环境光
|
||||||
|
const pmremGenerator = new THREE.PMREMGenerator(this.renderer);
|
||||||
|
const env = pmremGenerator.fromScene(new RoomEnvironment(this.renderer), 0.04).texture;
|
||||||
|
this.scene.environment = env;
|
||||||
|
|
||||||
|
// 控制器
|
||||||
|
this.controls = new OrbitControls(this.camera, this.renderer.domElement);
|
||||||
|
this.controls.enableDamping = true;
|
||||||
|
this.controls.dampingFactor = 0.05;
|
||||||
|
|
||||||
|
// 加载模型
|
||||||
|
this.loadModel();
|
||||||
|
|
||||||
|
// 开始动画
|
||||||
|
this.animate();
|
||||||
|
},
|
||||||
|
|
||||||
|
loadModel() {
|
||||||
|
const loader = new GLTFLoader();
|
||||||
|
|
||||||
|
loader.load(
|
||||||
|
this.modelPath,
|
||||||
|
(gltf) => {
|
||||||
|
this.model = gltf.scene;
|
||||||
|
|
||||||
|
// 收集所有mesh信息
|
||||||
|
this.partMeshMap = {};
|
||||||
|
this.meshList = [];
|
||||||
|
|
||||||
|
this.model.traverse((child) => {
|
||||||
|
if (child.isMesh) {
|
||||||
|
child.castShadow = true;
|
||||||
|
child.receiveShadow = true;
|
||||||
|
|
||||||
|
// 存储mesh引用
|
||||||
|
const meshInfo = {
|
||||||
|
name: child.name,
|
||||||
|
mesh: child,
|
||||||
|
parent: child.parent
|
||||||
|
};
|
||||||
|
|
||||||
|
this.meshList.push(meshInfo);
|
||||||
|
|
||||||
|
if (child.name) {
|
||||||
|
this.partMeshMap[child.name] = child;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('模型部件列表:', this.meshList.map(m => m.name || '未命名'));
|
||||||
|
console.log('部件映射表:', Object.keys(this.partMeshMap));
|
||||||
|
|
||||||
|
// 计算模型包围盒并居中
|
||||||
|
const box = new THREE.Box3().setFromObject(this.model);
|
||||||
|
const center = box.getCenter(new THREE.Vector3());
|
||||||
|
const size = box.getSize(new THREE.Vector3());
|
||||||
|
|
||||||
|
// 根据模型大小调整相机
|
||||||
|
const maxDim = Math.max(size.x, size.y, size.z);
|
||||||
|
this.maxDim = maxDim
|
||||||
|
this.camera.position.z = this.maxDim * 1;
|
||||||
|
this.camera.near = maxDim / 100;
|
||||||
|
this.camera.far = maxDim * 100;
|
||||||
|
this.camera.updateProjectionMatrix();
|
||||||
|
|
||||||
|
// // 计算模型边界并居中
|
||||||
|
// const box = new THREE.Box3().setFromObject(this.model);
|
||||||
|
// const center = box.getCenter(new THREE.Vector3());
|
||||||
|
// const size = box.getSize(new THREE.Vector3());
|
||||||
|
|
||||||
|
// this.model.position.sub(center);
|
||||||
|
|
||||||
|
// // 调整相机
|
||||||
|
// const maxDim = Math.max(size.x, size.y, size.z);
|
||||||
|
// const fov = this.camera.fov * (Math.PI / 180);
|
||||||
|
// let cameraZ = Math.abs(maxDim / 2 / Math.tan(fov / 2));
|
||||||
|
// cameraZ *= 2.5;
|
||||||
|
|
||||||
|
// this.camera.position.set(cameraZ, cameraZ, cameraZ);
|
||||||
|
// this.camera.lookAt(0, 0, 0);
|
||||||
|
// this.controls.target.set(0, 0, 0);
|
||||||
|
|
||||||
|
this.scene.add(this.model);
|
||||||
|
|
||||||
|
// 创建标注
|
||||||
|
this.createAnnotations();
|
||||||
|
|
||||||
|
this.$emit('model-loaded', {
|
||||||
|
meshList: this.meshList,
|
||||||
|
partMeshMap: Object.keys(this.partMeshMap)
|
||||||
|
});
|
||||||
|
},
|
||||||
|
(progress) => {
|
||||||
|
const percent = (progress.loaded / progress.total) * 100;
|
||||||
|
this.$emit('loading-progress', percent);
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
console.error('模型加载失败:', error);
|
||||||
|
this.$emit('model-error', error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
createAnnotations() {
|
||||||
|
// 清除旧标注
|
||||||
|
this.annotationObjects.forEach(obj => {
|
||||||
|
this.scene.remove(obj);
|
||||||
|
});
|
||||||
|
this.annotationObjects = [];
|
||||||
|
|
||||||
|
this.annotations.forEach((annotation, index) => {
|
||||||
|
// 尝试通过名称匹配找到对应的mesh
|
||||||
|
const mesh = this.findMeshByName(annotation.name);
|
||||||
|
|
||||||
|
let position = new THREE.Vector3();
|
||||||
|
|
||||||
|
if (mesh) {
|
||||||
|
// 如果找到对应mesh,使用其位置
|
||||||
|
mesh.getWorldPosition(position);
|
||||||
|
|
||||||
|
// 获取mesh的包围盒,标注放在上方
|
||||||
|
const box = new THREE.Box3().setFromObject(mesh);
|
||||||
|
const size = box.getSize(new THREE.Vector3());
|
||||||
|
position.y += size.y / 2 + 0.3;
|
||||||
|
|
||||||
|
console.log(`找到匹配: ${annotation.name} -> ${mesh.name}`, position);
|
||||||
|
} else {
|
||||||
|
// 如果没找到,使用圆形分布
|
||||||
|
const radius = 2;
|
||||||
|
const angleStep = (Math.PI * 2) / this.annotations.length;
|
||||||
|
const angle = angleStep * index;
|
||||||
|
position.set(
|
||||||
|
Math.cos(angle) * radius,
|
||||||
|
1 + Math.sin(index) * 0.5,
|
||||||
|
Math.sin(angle) * radius
|
||||||
|
);
|
||||||
|
|
||||||
|
console.warn(`未找到匹配: ${annotation.name},使用默认位置`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建标注标签
|
||||||
|
const labelDiv = document.createElement('div');
|
||||||
|
labelDiv.className = 'annotation-label';
|
||||||
|
labelDiv.textContent = `${index + 1} ${annotation.name}`;
|
||||||
|
labelDiv.style.cssText = `
|
||||||
|
background: rgba(64, 158, 255, 0.9);
|
||||||
|
color: white;
|
||||||
|
padding: 5px 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
white-space: nowrap;
|
||||||
|
pointer-events: auto;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
|
||||||
|
`;
|
||||||
|
|
||||||
|
labelDiv.addEventListener('click', () => {
|
||||||
|
this.onAnnotationClick(annotation, index, mesh);
|
||||||
|
});
|
||||||
|
|
||||||
|
const label = new CSS2DObject(labelDiv);
|
||||||
|
label.position.copy(position);
|
||||||
|
label.userData = { annotation, index, mesh };
|
||||||
|
|
||||||
|
this.scene.add(label);
|
||||||
|
this.annotationObjects.push(label);
|
||||||
|
|
||||||
|
// 创建连接线
|
||||||
|
if (mesh) {
|
||||||
|
const meshPos = new THREE.Vector3();
|
||||||
|
mesh.getWorldPosition(meshPos);
|
||||||
|
|
||||||
|
const points = [meshPos, position];
|
||||||
|
const geometry = new THREE.BufferGeometry().setFromPoints(points);
|
||||||
|
const material = new THREE.LineBasicMaterial({
|
||||||
|
color: 0x409eff,
|
||||||
|
transparent: true,
|
||||||
|
opacity: 0.6
|
||||||
|
});
|
||||||
|
const line = new THREE.Line(geometry, material);
|
||||||
|
this.scene.add(line);
|
||||||
|
this.annotationObjects.push(line);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
findMeshByName(annotationName) {
|
||||||
|
// 精确匹配
|
||||||
|
if (this.partMeshMap[annotationName]) {
|
||||||
|
return this.partMeshMap[annotationName];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 模糊匹配
|
||||||
|
for (let meshName in this.partMeshMap) {
|
||||||
|
if (meshName.includes(annotationName) || annotationName.includes(meshName)) {
|
||||||
|
return this.partMeshMap[meshName];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试匹配关键词
|
||||||
|
const keywords = annotationName.split(/[,、\s]+/);
|
||||||
|
for (let keyword of keywords) {
|
||||||
|
if (keyword.length > 1) {
|
||||||
|
for (let meshName in this.partMeshMap) {
|
||||||
|
if (meshName.includes(keyword)) {
|
||||||
|
return this.partMeshMap[meshName];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
|
||||||
|
onAnnotationClick(annotation, index, mesh) {
|
||||||
|
this.highlightPart(index, mesh);
|
||||||
|
this.$emit('annotation-click', annotation, index);
|
||||||
|
},
|
||||||
|
|
||||||
|
highlightPart(index, mesh) {
|
||||||
|
// 重置之前的高亮
|
||||||
|
this.annotationObjects.forEach((obj) => {
|
||||||
|
if (obj.isLine) {
|
||||||
|
obj.material.opacity = 0.6;
|
||||||
|
obj.material.color.setHex(0x409eff);
|
||||||
|
} else if (obj.element) {
|
||||||
|
obj.element.style.background = 'rgba(64, 158, 255, 0.9)';
|
||||||
|
obj.element.style.transform = 'scale(1)';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 高亮选中的标注
|
||||||
|
this.selectedPart = index;
|
||||||
|
this.annotationObjects.forEach((obj, i) => {
|
||||||
|
const objIndex = Math.floor(i / 2);
|
||||||
|
if (objIndex === index) {
|
||||||
|
if (obj.isLine) {
|
||||||
|
obj.material.opacity = 1;
|
||||||
|
obj.material.color.setHex(0xff6b00);
|
||||||
|
} else if (obj.element) {
|
||||||
|
obj.element.style.background = 'rgba(255, 107, 0, 0.95)';
|
||||||
|
obj.element.style.transform = 'scale(1.1)';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 高亮3D部件
|
||||||
|
if (mesh) {
|
||||||
|
this.highlightMesh(mesh);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
highlightMesh(mesh) {
|
||||||
|
// 创建高亮效果
|
||||||
|
if (!this.composer) {
|
||||||
|
this.composer = new EffectComposer(this.renderer);
|
||||||
|
const renderPass = new RenderPass(this.scene, this.camera);
|
||||||
|
this.composer.addPass(renderPass);
|
||||||
|
|
||||||
|
this.outlinePass = new OutlinePass(
|
||||||
|
new THREE.Vector2(this.sceneDomElement.clientWidth, this.sceneDomElement.clientHeight),
|
||||||
|
this.scene,
|
||||||
|
this.camera
|
||||||
|
);
|
||||||
|
this.outlinePass.edgeStrength = 5.0;
|
||||||
|
this.outlinePass.edgeGlow = 0.5;
|
||||||
|
this.outlinePass.edgeThickness = 2.0;
|
||||||
|
this.outlinePass.pulsePeriod = 2;
|
||||||
|
this.outlinePass.visibleEdgeColor.set(0xff6b00);
|
||||||
|
this.composer.addPass(this.outlinePass);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.outlinePass.selectedObjects = [mesh.parent || mesh];
|
||||||
|
},
|
||||||
|
|
||||||
|
onMouseClick(event) {
|
||||||
|
if (!this.debugMode || !this.sceneDomElement) return;
|
||||||
|
|
||||||
|
const rect = this.sceneDomElement.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.model.children, true);
|
||||||
|
|
||||||
|
if (intersects.length > 0) {
|
||||||
|
const mesh = intersects[0].object;
|
||||||
|
const point = intersects[0].point;
|
||||||
|
|
||||||
|
this.selectedMeshInfo = {
|
||||||
|
name: mesh.name,
|
||||||
|
position: {
|
||||||
|
x: parseFloat(point.x.toFixed(3)),
|
||||||
|
y: parseFloat(point.y.toFixed(3)),
|
||||||
|
z: parseFloat(point.z.toFixed(3))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('点击的部件:', this.selectedMeshInfo);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
copyPosition() {
|
||||||
|
if (this.selectedMeshInfo) {
|
||||||
|
const text = JSON.stringify(this.selectedMeshInfo.position, null, 2);
|
||||||
|
navigator.clipboard.writeText(text).then(() => {
|
||||||
|
this.$message.success('坐标已复制到剪贴板');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
animate() {
|
||||||
|
this.frameId = requestAnimationFrame(this.animate);
|
||||||
|
|
||||||
|
if (this.controls) {
|
||||||
|
this.controls.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.renderer && this.scene && this.camera) {
|
||||||
|
this.renderer.render(this.scene, this.camera);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.composer) {
|
||||||
|
this.composer.render(this.scene, this.camera);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.labelRenderer && this.scene && this.camera) {
|
||||||
|
this.labelRenderer.render(this.scene, this.camera);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onWindowResize() {
|
||||||
|
if (this.camera && this.renderer && this.sceneDomElement) {
|
||||||
|
const width = this.sceneDomElement.clientWidth;
|
||||||
|
const height = this.sceneDomElement.clientHeight;
|
||||||
|
|
||||||
|
this.camera.aspect = width / height;
|
||||||
|
this.camera.updateProjectionMatrix();
|
||||||
|
|
||||||
|
this.renderer.setSize(width, height);
|
||||||
|
this.labelRenderer.setSize(width, height);
|
||||||
|
|
||||||
|
if (this.composer) {
|
||||||
|
this.composer.setSize(width, height);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.three-viewer {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.canvas-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.annotations-container {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.debug-panel {
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
right: 10px;
|
||||||
|
background: rgba(255, 255, 255, 0.95);
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 8px;
|
||||||
|
max-width: 300px;
|
||||||
|
max-height: 80%;
|
||||||
|
overflow-y: auto;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.debug-panel h3 {
|
||||||
|
margin: 0 0 10px 0;
|
||||||
|
font-size: 16px;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.debug-panel h4 {
|
||||||
|
margin: 10px 0 5px 0;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #606266;
|
||||||
|
}
|
||||||
|
|
||||||
|
.debug-info {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
padding-bottom: 15px;
|
||||||
|
border-bottom: 1px solid #dcdfe6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.debug-info p {
|
||||||
|
margin: 5px 0;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #606266;
|
||||||
|
}
|
||||||
|
|
||||||
|
.debug-info pre {
|
||||||
|
background: #f5f7fa;
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
margin: 5px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.debug-info button {
|
||||||
|
margin-top: 8px;
|
||||||
|
padding: 5px 12px;
|
||||||
|
background: #409eff;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.debug-info button:hover {
|
||||||
|
background: #66b1ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mesh-list ul {
|
||||||
|
margin: 0;
|
||||||
|
padding-left: 20px;
|
||||||
|
max-height: 300px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mesh-list li {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #606266;
|
||||||
|
margin: 3px 0;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
<button @click="copyPosition">复制坐标</button>
|
<button @click="copyPosition">复制坐标</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mesh-list">
|
<div class="mesh-list" @click="save">
|
||||||
<h4>模型部件列表:</h4>
|
<h4>模型部件列表:</h4>
|
||||||
<ul>
|
<ul>
|
||||||
<li v-for="(mesh, index) in meshList" :key="index">
|
<li v-for="(mesh, index) in meshList" :key="index">
|
||||||
@@ -29,20 +29,24 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import * as THREE from 'three';
|
import * as THREE from 'three';
|
||||||
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
|
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'; // 导入 GLTFLoader 用于加载 GLTF 模型
|
||||||
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
|
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'; // 导入 OrbitControls 用于控制相机
|
||||||
import { CSS2DRenderer, CSS2DObject } from 'three/examples/jsm/renderers/CSS2DRenderer';
|
// 高亮
|
||||||
import { EffectComposer } from "three/examples/jsm/postprocessing/EffectComposer.js";
|
import { EffectComposer } from "three/examples/jsm/postprocessing/EffectComposer.js";
|
||||||
import { RenderPass } from "three/examples/jsm/postprocessing/RenderPass.js";
|
import { RenderPass } from "three/examples/jsm/postprocessing/RenderPass.js";
|
||||||
import { OutlinePass } from "three/examples/jsm/postprocessing/OutlinePass.js";
|
import { OutlinePass } from "three/examples/jsm/postprocessing/OutlinePass.js";
|
||||||
import { RoomEnvironment } from 'three/examples/jsm/environments/RoomEnvironment.js';
|
import { UnrealBloomPass } from "three/examples/jsm/postprocessing/UnrealBloomPass.js";
|
||||||
|
import TextureName from '../../public/models/textureName.json'
|
||||||
|
import { RoomEnvironment } from 'three/examples/jsm/environments/RoomEnvironment.js'
|
||||||
|
import { CSS2DRenderer, CSS2DObject } from 'three/examples/jsm/renderers/CSS2DRenderer'
|
||||||
|
import { cloneDeep } from 'lodash';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'ThreeViewerDebug',
|
name: 'ThreeViewerDebug',
|
||||||
props: {
|
props: {
|
||||||
modelPath: {
|
modelPath: {
|
||||||
type: String,
|
type: [String, Number],
|
||||||
required: true
|
default: '',
|
||||||
},
|
},
|
||||||
annotations: {
|
annotations: {
|
||||||
type: Array,
|
type: Array,
|
||||||
@@ -51,55 +55,104 @@ export default {
|
|||||||
debugMode: {
|
debugMode: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false
|
default: false
|
||||||
|
},
|
||||||
|
viewStr: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
textureObj() {
|
||||||
|
return this.$store && this.$store.state.textureObj || {}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
modelPath: {
|
||||||
|
handler(v) {
|
||||||
|
if(v) {
|
||||||
|
if (this.scene) {
|
||||||
|
this.scene.clear();
|
||||||
|
this.scene = null;
|
||||||
|
}
|
||||||
|
// 2. 销毁渲染器
|
||||||
|
if (this.renderer) {
|
||||||
|
this.renderer.dispose();
|
||||||
|
this.renderer.forceContextLoss();
|
||||||
|
this.renderer = null;
|
||||||
|
}
|
||||||
|
// 3. 销毁相机
|
||||||
|
this.camera = null;
|
||||||
|
// 4. 清空画布DOM容器的内容
|
||||||
|
if(this.sceneDomElement){
|
||||||
|
this.sceneDomElement.innerHTML = '';
|
||||||
|
}
|
||||||
|
this.cleanup();
|
||||||
|
setTimeout(() => {
|
||||||
|
this.init()
|
||||||
|
}, 300)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
immediate: true
|
||||||
|
},
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
scene: null,
|
scene: null, // Three.js场景
|
||||||
camera: null,
|
camera: null, // 相机
|
||||||
renderer: null,
|
renderer: null, // 渲染器
|
||||||
labelRenderer: null,
|
controls: null, // 轨道控制器
|
||||||
controls: null,
|
model: null, // 加载的3D模型
|
||||||
model: null,
|
frameId: null, // 动画帧ID
|
||||||
|
sceneDomElement: null, // 容器DOM元素
|
||||||
|
parent: null, // 容器
|
||||||
|
maxDim: null,
|
||||||
|
highlightColor: null, //高亮颜色
|
||||||
composer: null,
|
composer: null,
|
||||||
outlinePass: null,
|
pmremGenerator: null,
|
||||||
frameId: null,
|
labelRenderer: null,
|
||||||
sceneDomElement: null,
|
|
||||||
annotationObjects: [],
|
annotationObjects: [],
|
||||||
selectedPart: null,
|
selectedPart: null,
|
||||||
partMeshMap: {},
|
partMeshMap: {},
|
||||||
meshList: [],
|
meshList: [],
|
||||||
selectedMeshInfo: null,
|
selectedMeshInfo: null,
|
||||||
raycaster: new THREE.Raycaster(),
|
raycaster: new THREE.Raycaster(),
|
||||||
mouse: new THREE.Vector2()
|
mouse: new THREE.Vector2(),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
watch: {
|
mounted () {
|
||||||
modelPath: {
|
// this.onWindowResize(); // 初始化后立即调用一次
|
||||||
handler(v) {
|
// 添加窗口大小变化监听
|
||||||
if (v) {
|
// window.addEventListener('resize', this.onWindowResize);
|
||||||
this.cleanup();
|
|
||||||
setTimeout(() => {
|
|
||||||
this.init();
|
|
||||||
}, 300);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
immediate: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
this.resizeObserver = new ResizeObserver(this.onWindowResize);
|
this.resizeObserver = new ResizeObserver(this.onWindowResize);
|
||||||
this.resizeObserver.observe(this.$refs.container);
|
this.resizeObserver.observe(this.$refs.container);
|
||||||
|
|
||||||
// 添加点击事件
|
|
||||||
window.addEventListener('click', this.onMouseClick, false);
|
window.addEventListener('click', this.onMouseClick, false);
|
||||||
|
this.highlightColor = new THREE.Color(0xff0000); // 高亮颜色为红色
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
this.cleanup();
|
this.cleanup();
|
||||||
window.removeEventListener('click', this.onMouseClick, false);
|
window.removeEventListener('click', this.onMouseClick, false);
|
||||||
|
window.removeEventListener('resize', this.onWindowResize);
|
||||||
|
|
||||||
|
// 断开ResizeObserver
|
||||||
if (this.resizeObserver) {
|
if (this.resizeObserver) {
|
||||||
this.resizeObserver.disconnect();
|
this.resizeObserver.disconnect();
|
||||||
}
|
}
|
||||||
|
if (this.labelRenderer) {
|
||||||
|
this.labelRenderer.domElement.remove()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
destroyed() {
|
||||||
|
cancelAnimationFrame(this.frameId);
|
||||||
|
if (this.sceneDomElement && this.renderer) {
|
||||||
|
this.sceneDomElement.removeChild(this.renderer.domElement);
|
||||||
|
}
|
||||||
|
if (this.renderer) {
|
||||||
|
this.renderer.dispose();
|
||||||
|
}
|
||||||
|
if (this.scene) {
|
||||||
|
this.scene.clear();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
cleanup() {
|
cleanup() {
|
||||||
@@ -122,79 +175,142 @@ export default {
|
|||||||
this.sceneDomElement.innerHTML = '';
|
this.sceneDomElement.innerHTML = '';
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
|
if (this.sceneDomElement && this.renderer) {
|
||||||
|
this.sceneDomElement.removeChild(this.renderer.domElement);
|
||||||
|
}
|
||||||
|
// 获取容器元素
|
||||||
this.sceneDomElement = this.$refs.container;
|
this.sceneDomElement = this.$refs.container;
|
||||||
|
// 1. 创建一个新的场景
|
||||||
// 创建场景
|
|
||||||
this.scene = new THREE.Scene();
|
this.scene = new THREE.Scene();
|
||||||
this.scene.background = new THREE.Color(0xf0f0f0);
|
|
||||||
|
|
||||||
// 创建相机
|
// 2. 创建相机
|
||||||
const width = this.sceneDomElement.clientWidth;
|
const width = this.sceneDomElement.clientWidth;
|
||||||
const height = this.sceneDomElement.clientHeight;
|
const height = this.sceneDomElement.clientHeight;
|
||||||
this.camera = new THREE.PerspectiveCamera(45, width / height, 0.1, 1000);
|
this.camera = new THREE.PerspectiveCamera(55, width / height, 0.1, 1000);
|
||||||
this.camera.position.set(5, 5, 5);
|
|
||||||
|
|
||||||
// WebGL渲染器
|
// 3. 创建渲染器
|
||||||
this.renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
|
this.renderer = new THREE.WebGLRenderer({
|
||||||
|
antialias: true,
|
||||||
|
// alpha: true,
|
||||||
|
});
|
||||||
|
this.renderer.shadowMap.enabled = true
|
||||||
|
|
||||||
|
// CSS2D渲染器(用于标注)
|
||||||
|
this.labelRenderer = new CSS2DRenderer()
|
||||||
|
this.labelRenderer.setSize(width, height)
|
||||||
|
this.labelRenderer.domElement.style.position = 'absolute'
|
||||||
|
this.labelRenderer.domElement.style.top = '0'
|
||||||
|
this.labelRenderer.domElement.style.pointerEvents = 'none'
|
||||||
|
this.$refs.annotationsContainer.appendChild(this.labelRenderer.domElement)
|
||||||
|
|
||||||
|
// 设置画布分辨率 提高画质
|
||||||
this.renderer.setPixelRatio(window.devicePixelRatio);
|
this.renderer.setPixelRatio(window.devicePixelRatio);
|
||||||
|
// 渲染器开启阴影效果
|
||||||
|
this.renderer.shadowMap.enabled = true
|
||||||
|
|
||||||
this.renderer.setSize(width, height);
|
this.renderer.setSize(width, height);
|
||||||
this.renderer.shadowMap.enabled = true;
|
this.renderer.outputEncoding = THREE.sRGBEncoding; // 设置颜色编码
|
||||||
this.renderer.outputEncoding = THREE.sRGBEncoding;
|
|
||||||
this.sceneDomElement.appendChild(this.renderer.domElement);
|
this.sceneDomElement.appendChild(this.renderer.domElement);
|
||||||
|
|
||||||
// CSS2D渲染器
|
this.scene.background = new THREE.Color( 0xcccccc );
|
||||||
this.labelRenderer = new CSS2DRenderer();
|
this.scene.fog = new THREE.Fog( 0xa0a0a0, 10, 50 );
|
||||||
this.labelRenderer.setSize(width, height);
|
|
||||||
this.labelRenderer.domElement.style.position = 'absolute';
|
|
||||||
this.labelRenderer.domElement.style.top = '0';
|
|
||||||
this.labelRenderer.domElement.style.pointerEvents = 'none';
|
|
||||||
this.$refs.annotationsContainer.appendChild(this.labelRenderer.domElement);
|
|
||||||
|
|
||||||
// 光照
|
const hemiLight = new THREE.HemisphereLight( 0xffffff, 0x8d8d8d, 3 );
|
||||||
const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
|
hemiLight.position.set( 0, 20, 0 );
|
||||||
this.scene.add(ambientLight);
|
this.scene.add( hemiLight );
|
||||||
|
|
||||||
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
|
const dirLight = new THREE.DirectionalLight( 0xffffff, 3 );
|
||||||
directionalLight.position.set(5, 10, 5);
|
dirLight.position.set( 3, 10, 10 );
|
||||||
directionalLight.castShadow = true;
|
dirLight.castShadow = true;
|
||||||
this.scene.add(directionalLight);
|
dirLight.shadow.camera.top = 2;
|
||||||
|
dirLight.shadow.camera.bottom = - 2;
|
||||||
|
dirLight.shadow.camera.left = - 2;
|
||||||
|
dirLight.shadow.camera.right = 2;
|
||||||
|
dirLight.shadow.camera.near = 0.1;
|
||||||
|
dirLight.shadow.camera.far = 40;
|
||||||
|
this.scene.add( dirLight );
|
||||||
|
|
||||||
// 环境光
|
// 添加地面
|
||||||
const pmremGenerator = new THREE.PMREMGenerator(this.renderer);
|
this.createGround()
|
||||||
const env = pmremGenerator.fromScene(new RoomEnvironment(this.renderer), 0.04).texture;
|
|
||||||
this.scene.environment = env;
|
|
||||||
|
|
||||||
// 控制器
|
// 5. 添加轨道控制器
|
||||||
this.controls = new OrbitControls(this.camera, this.renderer.domElement);
|
this.controls = new OrbitControls(this.camera, this.renderer.domElement);
|
||||||
this.controls.enableDamping = true;
|
this.controls.enableDamping = true; // 启用阻尼效果
|
||||||
this.controls.dampingFactor = 0.05;
|
this.controls.dampingFactor = 0.25; // 阻尼系数
|
||||||
|
|
||||||
// 加载模型
|
// 6. 加载3D模型
|
||||||
this.loadModel();
|
this.loadModel();
|
||||||
|
|
||||||
// 开始动画
|
// 7. 开始动画循环
|
||||||
this.animate();
|
this.animate();
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
loadModel() {
|
loadModel() {
|
||||||
const loader = new GLTFLoader();
|
const loader = new GLTFLoader();
|
||||||
|
const textureLoader = new THREE.TextureLoader();
|
||||||
loader.load(
|
loader.load(
|
||||||
this.modelPath,
|
this.modelPath,
|
||||||
|
// 'https://skeps-bucket.oss-cn-shenzhen.aliyuncs.com/2026%2FFile%2F4d3a9525-c415-4ab2-b916-d87b4b86ebcbSK010020001001BLQ_%E4%BA%A4%E6%B5%81%E9%81%BF%E9%9B%B7%E5%99%A8%2C%E5%B8%A6%E6%94%AF%E6%92%91%E4%BB%B6%E5%9B%BA%E5%AE%9A%E9%97%B4%E9%9A%99.glb',
|
||||||
(gltf) => {
|
(gltf) => {
|
||||||
|
// 移除旧模型
|
||||||
|
if (this.model) {
|
||||||
|
this.scene.remove(this.model);
|
||||||
|
}
|
||||||
this.model = gltf.scene;
|
this.model = gltf.scene;
|
||||||
|
|
||||||
|
this.scene.add(this.model);
|
||||||
|
|
||||||
// 收集所有mesh信息
|
// 收集所有mesh信息
|
||||||
this.partMeshMap = {};
|
this.partMeshMap = {};
|
||||||
this.meshList = [];
|
this.meshList = [];
|
||||||
|
|
||||||
|
// 遍历模型所有子元素
|
||||||
this.model.traverse((child) => {
|
this.model.traverse((child) => {
|
||||||
if (child.isMesh) {
|
if (child.isMesh) {
|
||||||
child.castShadow = true;
|
child.material.emissiveMap = child.material.map;
|
||||||
child.receiveShadow = true;
|
child.castShadow = true; // 让模型产生阴影
|
||||||
|
child.receiveShadow = true; // 让模型接受阴影
|
||||||
|
|
||||||
|
// child.material.emissive = child.material.color;
|
||||||
|
let name = child.material.name.split('.')[0]
|
||||||
|
let path = TextureName[name]
|
||||||
|
|
||||||
|
if(path) {
|
||||||
|
console.log(7777, child, name, path)
|
||||||
|
if(this.textureObj[name]) {
|
||||||
|
child.material = this.textureObj[name]
|
||||||
|
child.material.needsUpdate = true;
|
||||||
|
} else {
|
||||||
|
let texture = textureLoader.load(`/models/texture/${encodeURIComponent(path)}.jpg`)
|
||||||
|
texture.wrapS = THREE.RepeatWrapping;
|
||||||
|
texture.wrapT = THREE.RepeatWrapping;
|
||||||
|
texture.repeat.set( 1, 1 );
|
||||||
|
let material = null
|
||||||
|
if(name.includes('金属')) {
|
||||||
|
material = new THREE.MeshStandardMaterial({
|
||||||
|
map: texture,
|
||||||
|
// color: 0x049EF4,
|
||||||
|
// emissive: 0x000000,
|
||||||
|
metalness: 0.55, // 金属感
|
||||||
|
roughness: 0.05 // 粗糙度,0为镜面反射
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
material = new THREE.MeshPhongMaterial( {
|
||||||
|
// ...child.material,
|
||||||
|
map: texture,
|
||||||
|
emissiveMap: child.material.map,
|
||||||
|
shininess: 0,
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
child.material = material
|
||||||
|
child.material.needsUpdate = true;
|
||||||
|
this.textureObj[name] = material
|
||||||
|
// this.$store.commit('set_textureObj', this.textureObj)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 存储mesh引用
|
// 存储mesh引用
|
||||||
const meshInfo = {
|
const meshInfo = {
|
||||||
name: child.name,
|
name: child.name,
|
||||||
@@ -210,219 +326,262 @@ export default {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('模型部件列表:', this.meshList.map(m => m.name || '未命名'));
|
// 计算模型包围盒并居中
|
||||||
console.log('部件映射表:', Object.keys(this.partMeshMap));
|
|
||||||
|
|
||||||
// 计算模型边界并居中
|
|
||||||
const box = new THREE.Box3().setFromObject(this.model);
|
const box = new THREE.Box3().setFromObject(this.model);
|
||||||
const center = box.getCenter(new THREE.Vector3());
|
const center = box.getCenter(new THREE.Vector3());
|
||||||
const size = box.getSize(new THREE.Vector3());
|
const size = box.getSize(new THREE.Vector3());
|
||||||
|
|
||||||
this.model.position.sub(center);
|
// 根据模型大小调整相机
|
||||||
|
|
||||||
// 调整相机
|
|
||||||
const maxDim = Math.max(size.x, size.y, size.z);
|
const maxDim = Math.max(size.x, size.y, size.z);
|
||||||
const fov = this.camera.fov * (Math.PI / 180);
|
this.maxDim = maxDim
|
||||||
let cameraZ = Math.abs(maxDim / 2 / Math.tan(fov / 2));
|
this.camera.position.z = this.maxDim * 1;
|
||||||
cameraZ *= 2.5;
|
this.camera.near = maxDim / 100;
|
||||||
|
this.camera.far = maxDim * 100;
|
||||||
this.camera.position.set(cameraZ, cameraZ, cameraZ);
|
this.camera.updateProjectionMatrix();
|
||||||
this.camera.lookAt(0, 0, 0);
|
|
||||||
this.controls.target.set(0, 0, 0);
|
// 创建一个父对象用于旋转
|
||||||
|
// this.parent = new THREE.Object3D(); // 创建一个新的 Object3D 作为父对象
|
||||||
this.scene.add(this.model);
|
// this.parent.add(this.model); // 将模型添加到父对象
|
||||||
|
|
||||||
// 创建标注
|
this.scene.add(this.model); // 将父对象添加到场景
|
||||||
this.createAnnotations();
|
|
||||||
|
// 让控制器聚焦到模型中心 聚焦中心
|
||||||
this.$emit('model-loaded', {
|
this.controls.target.copy(center);
|
||||||
meshList: this.meshList,
|
|
||||||
partMeshMap: Object.keys(this.partMeshMap)
|
this.controls.enableRotate = true;
|
||||||
});
|
// this.controls.minPolarAngle = Math.PI / 2; // 限制向下旋转的角度
|
||||||
|
// this.controls.maxPolarAngle = Math.PI / 2; // 限制向上旋转的角度
|
||||||
|
|
||||||
|
this.controls.update();
|
||||||
|
this.loadCameraState()
|
||||||
|
|
||||||
|
// 调整地面位置到模型底部
|
||||||
|
if (this.ground) {
|
||||||
|
this.ground.position.y = box.min.y - 0.01
|
||||||
|
}
|
||||||
|
|
||||||
|
this.createAnnotations()
|
||||||
|
this.$emit('model-loaded')
|
||||||
},
|
},
|
||||||
(progress) => {
|
(progress) => {
|
||||||
const percent = (progress.loaded / progress.total) * 100;
|
const percent = (progress.loaded / progress.total) * 100
|
||||||
this.$emit('loading-progress', percent);
|
this.$emit('loading-progress', percent)
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
console.error('模型加载失败:', error);
|
console.error('模型加载失败:', error);
|
||||||
this.$emit('model-error', error);
|
this.$emit('model-error', error)
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
animate() {
|
||||||
|
this.frameId = requestAnimationFrame(this.animate);
|
||||||
|
if(this.parent && this.isPlay) {
|
||||||
|
this.parent.rotation.y += 0.001; // 沿 Y 轴旋转父对象
|
||||||
|
}
|
||||||
|
// 更新控制器
|
||||||
|
if (this.controls) {
|
||||||
|
this.controls.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 渲染场景
|
||||||
|
if (this.renderer && this.scene && this.camera) {
|
||||||
|
this.renderer.render(this.scene, this.camera);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.composer) {
|
||||||
|
this.composer.render(this.scene, this.camera);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(this.labelRenderer && this.scene && this.camera) {
|
||||||
|
this.labelRenderer.render(this.scene, this.camera)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 响应式调整大小
|
||||||
|
onWindowResize() {
|
||||||
|
if (this.camera && this.renderer) {
|
||||||
|
const width = this.sceneDomElement.clientWidth;
|
||||||
|
const height = this.sceneDomElement.clientHeight;
|
||||||
|
this.camera.aspect = width / height;
|
||||||
|
this.camera.updateProjectionMatrix();
|
||||||
|
this.renderer.setSize(width, height);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
outlineObj(selectedObjects) {
|
||||||
|
let outlinePass;
|
||||||
|
let renderPass;
|
||||||
|
let unrealBloomPass;
|
||||||
|
|
||||||
|
// 创建一个EffectComposer(效果组合器)对象,然后在该对象上添加后期处理通道。
|
||||||
|
// 用于模型边缘高亮
|
||||||
|
this.composer = new EffectComposer(this.renderer);
|
||||||
|
this.composer.renderTarget1.texture.outputColorSpace = THREE.sRGBEncoding;
|
||||||
|
this.composer.renderTarget2.texture.outputColorSpace = THREE.sRGBEncoding;
|
||||||
|
this.composer.renderTarget1.texture.encoding = THREE.sRGBEncoding;
|
||||||
|
this.composer.renderTarget2.texture.encoding = THREE.sRGBEncoding;
|
||||||
|
|
||||||
|
// 新建一个场景通道 为了覆盖到原来的场景上
|
||||||
|
renderPass = new RenderPass(this.scene, this.camera);
|
||||||
|
this.composer.addPass(renderPass);
|
||||||
|
// 物体边缘发光通道
|
||||||
|
outlinePass = new OutlinePass(
|
||||||
|
new THREE.Vector2(this.sceneDomElement.clientWidth, this.sceneDomElement.clientHeight),
|
||||||
|
this.scene,
|
||||||
|
this.camera,
|
||||||
|
selectedObjects
|
||||||
|
);
|
||||||
|
outlinePass.selectedObjects = selectedObjects;
|
||||||
|
outlinePass.edgeStrength = 2.0; // 边框的亮度
|
||||||
|
outlinePass.edgeGlow = 0.5; // 光晕[0,1]
|
||||||
|
outlinePass.usePatternTexture = false; // 是否使用父级的材质
|
||||||
|
outlinePass.edgeThickness = 1.0; // 边框宽度
|
||||||
|
outlinePass.downSampleRatio = 1; // 边框弯曲度
|
||||||
|
outlinePass.pulsePeriod = 5; // 呼吸闪烁的速度
|
||||||
|
outlinePass.visibleEdgeColor.set(parseInt(0x00ff00)); // 呼吸显示的颜色
|
||||||
|
outlinePass.hiddenEdgeColor = new THREE.Color(0, 0, 0); // 呼吸消失的颜色
|
||||||
|
outlinePass.clear = true;
|
||||||
|
this.composer.addPass(outlinePass);
|
||||||
|
// 自定义的着色器通道 作为参数
|
||||||
|
// effectFXAA = new ShaderPass(FXAAShader);
|
||||||
|
// effectFXAA.uniforms.resolution.value.set(
|
||||||
|
// 1 / window.innerWidth,
|
||||||
|
// 1 / window.innerHeight
|
||||||
|
// );
|
||||||
|
// effectFXAA.renderToScreen = true;
|
||||||
|
// composer.addPass(effectFXAA);
|
||||||
|
// // 抗锯齿
|
||||||
|
// smaaPass = new SMAAPass();
|
||||||
|
// composer.addPass(smaaPass);
|
||||||
|
// // 发光效果
|
||||||
|
unrealBloomPass = new UnrealBloomPass();
|
||||||
|
unrealBloomPass.strength = 0.1;
|
||||||
|
unrealBloomPass.radius = 0;
|
||||||
|
unrealBloomPass.threshold = 1;
|
||||||
|
this.composer.addPass(unrealBloomPass);
|
||||||
|
|
||||||
|
// this.scene.background = new THREE.Color(0x1b1824);
|
||||||
|
},
|
||||||
|
// 创建标签
|
||||||
createAnnotations() {
|
createAnnotations() {
|
||||||
// 清除旧标注
|
// 清除旧标注
|
||||||
this.annotationObjects.forEach(obj => {
|
this.annotationObjects.forEach(obj => {
|
||||||
this.scene.remove(obj);
|
this.scene.remove(obj)
|
||||||
});
|
})
|
||||||
this.annotationObjects = [];
|
this.annotationObjects = []
|
||||||
|
|
||||||
|
// 创建标注点的预设位置(圆形分布)
|
||||||
|
const radius = 2
|
||||||
|
const angleStep = (Math.PI * 2) / this.annotations.length
|
||||||
|
|
||||||
this.annotations.forEach((annotation, index) => {
|
this.annotations.forEach((annotation, index) => {
|
||||||
// 尝试通过名称匹配找到对应的mesh
|
if(annotation.position) {
|
||||||
const mesh = this.findMeshByName(annotation.name);
|
let position = annotation.position && annotation.position[0] ? annotation.position[0] : { x: 0, y: 0, z: 0 }
|
||||||
|
let position2 = annotation.position && annotation.position[1] ? annotation.position[1] : { x: 0, y: 0, z: 0 }
|
||||||
let position = new THREE.Vector3();
|
// 创建标注标签
|
||||||
|
const labelDiv = document.createElement('div')
|
||||||
if (mesh) {
|
labelDiv.className = 'annotation-label'
|
||||||
// 如果找到对应mesh,使用其位置
|
labelDiv.textContent = `${annotation.name}`
|
||||||
mesh.getWorldPosition(position);
|
labelDiv.style.cssText = `
|
||||||
|
background: rgba(64, 158, 255, 0.9);
|
||||||
// 获取mesh的包围盒,标注放在上方
|
color: white;
|
||||||
const box = new THREE.Box3().setFromObject(mesh);
|
padding: 5px 10px;
|
||||||
const size = box.getSize(new THREE.Vector3());
|
border-radius: 4px;
|
||||||
position.y += size.y / 2 + 0.3;
|
font-size: 12px;
|
||||||
|
cursor: pointer;
|
||||||
console.log(`找到匹配: ${annotation.name} -> ${mesh.name}`, position);
|
white-space: nowrap;
|
||||||
} else {
|
pointer-events: auto;
|
||||||
// 如果没找到,使用圆形分布
|
`
|
||||||
const radius = 2;
|
labelDiv.addEventListener('click', () => {
|
||||||
const angleStep = (Math.PI * 2) / this.annotations.length;
|
this.onAnnotationClick(annotation, index)
|
||||||
const angle = angleStep * index;
|
})
|
||||||
position.set(
|
|
||||||
Math.cos(angle) * radius,
|
|
||||||
1 + Math.sin(index) * 0.5,
|
|
||||||
Math.sin(angle) * radius
|
|
||||||
);
|
|
||||||
|
|
||||||
console.warn(`未找到匹配: ${annotation.name},使用默认位置`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建标注标签
|
const label = new CSS2DObject(labelDiv)
|
||||||
const labelDiv = document.createElement('div');
|
label.position.set(position2.x, position2.y, position2.z)
|
||||||
labelDiv.className = 'annotation-label';
|
label.userData = { annotation, index }
|
||||||
labelDiv.textContent = `${index + 1} ${annotation.name}`;
|
|
||||||
labelDiv.style.cssText = `
|
|
||||||
background: rgba(64, 158, 255, 0.9);
|
|
||||||
color: white;
|
|
||||||
padding: 5px 10px;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 12px;
|
|
||||||
cursor: pointer;
|
|
||||||
white-space: nowrap;
|
|
||||||
pointer-events: auto;
|
|
||||||
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
|
|
||||||
`;
|
|
||||||
|
|
||||||
labelDiv.addEventListener('click', () => {
|
|
||||||
this.onAnnotationClick(annotation, index, mesh);
|
|
||||||
});
|
|
||||||
|
|
||||||
const label = new CSS2DObject(labelDiv);
|
|
||||||
label.position.copy(position);
|
|
||||||
label.userData = { annotation, index, mesh };
|
|
||||||
|
|
||||||
this.scene.add(label);
|
|
||||||
this.annotationObjects.push(label);
|
|
||||||
|
|
||||||
// 创建连接线
|
|
||||||
if (mesh) {
|
|
||||||
const meshPos = new THREE.Vector3();
|
|
||||||
mesh.getWorldPosition(meshPos);
|
|
||||||
|
|
||||||
const points = [meshPos, position];
|
this.scene.add(label)
|
||||||
const geometry = new THREE.BufferGeometry().setFromPoints(points);
|
this.annotationObjects.push(label)
|
||||||
|
|
||||||
|
// 创建连接线
|
||||||
|
const points = [
|
||||||
|
new THREE.Vector3(position.x, position.y, position.z),
|
||||||
|
new THREE.Vector3(position2.x, position2.y, position2.z)
|
||||||
|
]
|
||||||
|
const geometry = new THREE.BufferGeometry().setFromPoints(points)
|
||||||
const material = new THREE.LineBasicMaterial({
|
const material = new THREE.LineBasicMaterial({
|
||||||
color: 0x409eff,
|
color: 0x409eff,
|
||||||
transparent: true,
|
transparent: true,
|
||||||
opacity: 0.6
|
opacity: 0.5
|
||||||
});
|
})
|
||||||
const line = new THREE.Line(geometry, material);
|
const line = new THREE.Line(geometry, material)
|
||||||
this.scene.add(line);
|
this.scene.add(line)
|
||||||
this.annotationObjects.push(line);
|
this.annotationObjects.push(line)
|
||||||
|
|
||||||
|
}else {
|
||||||
|
this.annotationObjects.push('')
|
||||||
|
this.annotationObjects.push('')
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
},
|
||||||
|
onAnnotationClick(annotation, index) {
|
||||||
|
this.highlightPart(index)
|
||||||
|
this.$emit('annotation-click', annotation, index)
|
||||||
},
|
},
|
||||||
|
|
||||||
findMeshByName(annotationName) {
|
// 创建地面
|
||||||
// 精确匹配
|
createGround() {
|
||||||
if (this.partMeshMap[annotationName]) {
|
// 创建圆形地面几何体,避免菱形格子
|
||||||
return this.partMeshMap[annotationName];
|
const groundGeometry = new THREE.CircleGeometry(25, 64)
|
||||||
}
|
|
||||||
|
|
||||||
// 模糊匹配
|
// 创建地面材质 - 使用 ShadowMaterial 只显示阴影
|
||||||
for (let meshName in this.partMeshMap) {
|
const groundMaterial = new THREE.ShadowMaterial({
|
||||||
if (meshName.includes(annotationName) || annotationName.includes(meshName)) {
|
opacity: 0.25,
|
||||||
return this.partMeshMap[meshName];
|
color: 0x000000
|
||||||
}
|
})
|
||||||
}
|
|
||||||
|
|
||||||
// 尝试匹配关键词
|
// 创建地面网格
|
||||||
const keywords = annotationName.split(/[,、\s]+/);
|
this.ground = new THREE.Mesh(groundGeometry, groundMaterial)
|
||||||
for (let keyword of keywords) {
|
this.ground.rotation.x = -Math.PI / 2 // 旋转地面使其水平
|
||||||
if (keyword.length > 1) {
|
this.ground.position.y = 0
|
||||||
for (let meshName in this.partMeshMap) {
|
this.ground.receiveShadow = true // 地面接收阴影
|
||||||
if (meshName.includes(keyword)) {
|
|
||||||
return this.partMeshMap[meshName];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
this.scene.add(this.ground)
|
||||||
},
|
},
|
||||||
|
|
||||||
onAnnotationClick(annotation, index, mesh) {
|
highlightPart(index) {
|
||||||
this.highlightPart(index, mesh);
|
|
||||||
this.$emit('annotation-click', annotation, index);
|
|
||||||
},
|
|
||||||
|
|
||||||
highlightPart(index, mesh) {
|
|
||||||
// 重置之前的高亮
|
// 重置之前的高亮
|
||||||
this.annotationObjects.forEach((obj) => {
|
if (this.selectedPart !== null) {
|
||||||
if (obj.isLine) {
|
this.annotationObjects.forEach((obj, i) => {
|
||||||
obj.material.opacity = 0.6;
|
if (obj.isLine) {
|
||||||
obj.material.color.setHex(0x409eff);
|
obj.material.opacity = 0.5
|
||||||
} else if (obj.element) {
|
obj.material.color.setHex(0x409eff)
|
||||||
obj.element.style.background = 'rgba(64, 158, 255, 0.9)';
|
// obj.material.color = 'rgba(64, 158, 255, 0.9)'
|
||||||
obj.element.style.transform = 'scale(1)';
|
} else if (obj.element) {
|
||||||
}
|
obj.element.style.background = 'rgba(64, 158, 255, 0.9)'
|
||||||
});
|
obj.element.style.transform = 'scale(1)'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// 高亮选中的标注
|
// 高亮选中的部件
|
||||||
this.selectedPart = index;
|
|
||||||
|
console.log(8888, this.annotationObjects, index)
|
||||||
|
this.selectedPart = index
|
||||||
this.annotationObjects.forEach((obj, i) => {
|
this.annotationObjects.forEach((obj, i) => {
|
||||||
const objIndex = Math.floor(i / 2);
|
const objIndex = Math.floor(i / 2)
|
||||||
if (objIndex === index) {
|
if (objIndex === index) {
|
||||||
if (obj.isLine) {
|
if (obj.isLine) {
|
||||||
obj.material.opacity = 1;
|
obj.material.opacity = 1
|
||||||
obj.material.color.setHex(0xff6b00);
|
obj.material.color.setHex(0xff6b00)
|
||||||
} else if (obj.element) {
|
} else if (obj.element) {
|
||||||
obj.element.style.background = 'rgba(255, 107, 0, 0.95)';
|
obj.element.style.background = 'rgba(255, 107, 0, 0.95)'
|
||||||
obj.element.style.transform = 'scale(1.1)';
|
obj.element.style.transform = 'scale(1.1)'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
|
||||||
// 高亮3D部件
|
|
||||||
if (mesh) {
|
|
||||||
this.highlightMesh(mesh);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
highlightMesh(mesh) {
|
|
||||||
// 创建高亮效果
|
|
||||||
if (!this.composer) {
|
|
||||||
this.composer = new EffectComposer(this.renderer);
|
|
||||||
const renderPass = new RenderPass(this.scene, this.camera);
|
|
||||||
this.composer.addPass(renderPass);
|
|
||||||
|
|
||||||
this.outlinePass = new OutlinePass(
|
|
||||||
new THREE.Vector2(this.sceneDomElement.clientWidth, this.sceneDomElement.clientHeight),
|
|
||||||
this.scene,
|
|
||||||
this.camera
|
|
||||||
);
|
|
||||||
this.outlinePass.edgeStrength = 5.0;
|
|
||||||
this.outlinePass.edgeGlow = 0.5;
|
|
||||||
this.outlinePass.edgeThickness = 2.0;
|
|
||||||
this.outlinePass.pulsePeriod = 2;
|
|
||||||
this.outlinePass.visibleEdgeColor.set(0xff6b00);
|
|
||||||
this.composer.addPass(this.outlinePass);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.outlinePass.selectedObjects = [mesh.parent || mesh];
|
|
||||||
},
|
|
||||||
|
|
||||||
onMouseClick(event) {
|
onMouseClick(event) {
|
||||||
if (!this.debugMode || !this.sceneDomElement) return;
|
if (!this.debugMode || !this.sceneDomElement) return;
|
||||||
|
|
||||||
@@ -448,6 +607,42 @@ export default {
|
|||||||
|
|
||||||
console.log('点击的部件:', this.selectedMeshInfo);
|
console.log('点击的部件:', this.selectedMeshInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// console.log('我点击了')
|
||||||
|
// // const raycaster = new THREE.Raycaster();
|
||||||
|
// // const mouse = new THREE.Vector2();
|
||||||
|
// // // 计算鼠标在屏幕上的位置并转换为Normalized device coordinates (NDC)
|
||||||
|
// // console.log(this.sceneDomElement.clientWidth, window.innerWidth)
|
||||||
|
// // mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
|
||||||
|
// // mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
|
||||||
|
|
||||||
|
// // // 更新Raycaster的射线位置和方向
|
||||||
|
// // raycaster.setFromCamera(mouse, this.camera);
|
||||||
|
// if(!this.sceneDomElement) return
|
||||||
|
// let getBoundingClientRect = this.sceneDomElement.getBoundingClientRect()
|
||||||
|
// // 屏幕坐标转标准设备坐标
|
||||||
|
// let x = ((event.clientX - getBoundingClientRect .left) / this.sceneDomElement.offsetWidth) * 2 - 1;// 标准设备横坐标
|
||||||
|
// let y = -((event.clientY - getBoundingClientRect .top) / this.sceneDomElement.offsetHeight) * 2 + 1;// 标准设备纵坐标
|
||||||
|
// let standardVector = new THREE.Vector3(x, y, 1);// 标准设备坐标
|
||||||
|
// // 标准设备坐标转世界坐标
|
||||||
|
// let worldVector = standardVector.unproject(this.camera);
|
||||||
|
// // 射线投射方向单位向量(worldVector坐标减相机位置坐标)
|
||||||
|
// let ray = worldVector.sub(this.camera.position).normalize();
|
||||||
|
// // 创建射线投射器对象
|
||||||
|
// let rayCaster = new THREE.Raycaster(this.camera.position, ray);
|
||||||
|
|
||||||
|
// // 计算物体和射线的交点
|
||||||
|
// const intersects = rayCaster.intersectObjects(this.model.children, true);
|
||||||
|
// if (intersects.length !== 0 && intersects[0].object instanceof THREE.Mesh) {
|
||||||
|
// // this.controls.target.copy(intersects[0].point);
|
||||||
|
// let selectedObject = intersects[0].object;
|
||||||
|
// let selectedObjects = [];
|
||||||
|
// selectedObjects.push(selectedObject.parent);
|
||||||
|
// this.outlineObj(selectedObjects);
|
||||||
|
// } else {
|
||||||
|
// console.log('没找到', this.composer)
|
||||||
|
// this.composer = null
|
||||||
|
// }
|
||||||
},
|
},
|
||||||
|
|
||||||
copyPosition() {
|
copyPosition() {
|
||||||
@@ -459,40 +654,34 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
animate() {
|
save() {
|
||||||
this.frameId = requestAnimationFrame(this.animate);
|
let str = {
|
||||||
|
position: this.camera.position.toArray(), // 保存位置
|
||||||
if (this.controls) {
|
quaternion: this.camera.quaternion.toArray(), // 保存朝向
|
||||||
this.controls.update();
|
fov: this.camera.fov, // 如果是透视摄像机,保存焦距
|
||||||
}
|
target: this.controls.target.toArray(), // 控制器目标点
|
||||||
|
};
|
||||||
if (this.renderer && this.scene && this.camera) {
|
console.log(9999, JSON.stringify(str))
|
||||||
if (this.composer && this.outlinePass && this.outlinePass.selectedObjects.length > 0) {
|
|
||||||
this.composer.render();
|
|
||||||
} else {
|
|
||||||
this.renderer.render(this.scene, this.camera);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.labelRenderer && this.scene && this.camera) {
|
|
||||||
this.labelRenderer.render(this.scene, this.camera);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
onWindowResize() {
|
loadCameraState() {
|
||||||
if (this.camera && this.renderer && this.sceneDomElement) {
|
const cameraData = this.viewStr ? this.viewStr : {};
|
||||||
const width = this.sceneDomElement.clientWidth;
|
console.log('cameraData', cameraData)
|
||||||
const height = this.sceneDomElement.clientHeight;
|
if(cameraData && cameraData.position && cameraData.quaternion) {
|
||||||
|
console.log(888, cameraData, cloneDeep(this.camera.position))
|
||||||
this.camera.aspect = width / height;
|
this.camera.position.fromArray(cameraData.position); // 恢复位置
|
||||||
this.camera.updateProjectionMatrix();
|
this.camera.quaternion.fromArray(cameraData.quaternion); // 恢复朝向
|
||||||
|
if (this.camera instanceof THREE.PerspectiveCamera) { // 如果是透视摄像机,恢复焦距
|
||||||
this.renderer.setSize(width, height);
|
this.camera.fov = cameraData.fov;
|
||||||
this.labelRenderer.setSize(width, height);
|
this.camera.updateProjectionMatrix(); // 更新投影矩阵
|
||||||
|
|
||||||
if (this.composer) {
|
|
||||||
this.composer.setSize(width, height);
|
|
||||||
}
|
}
|
||||||
|
this.controls.target.fromArray(cameraData.target); // 恢复目标点
|
||||||
|
this.controls.update(); // 更新控制器状态以应用新的目标点(如果有的话)
|
||||||
|
|
||||||
|
} else {
|
||||||
|
this.camera.position.z = this.maxDim * 1;
|
||||||
|
this.camera.position.x = 0;
|
||||||
|
this.camera.position.y = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,6 +65,18 @@
|
|||||||
"content": [
|
"content": [
|
||||||
"不低于 80cm,直径不低于 16mm 的软铜线",
|
"不低于 80cm,直径不低于 16mm 的软铜线",
|
||||||
"一次端引线应采用绝缘铜线,引线绝缘层的工频电压绝缘耐受水平不低于 18kV"
|
"一次端引线应采用绝缘铜线,引线绝缘层的工频电压绝缘耐受水平不低于 18kV"
|
||||||
|
],
|
||||||
|
"position": [
|
||||||
|
{
|
||||||
|
"x": 0.409,
|
||||||
|
"y": 0.193,
|
||||||
|
"z": 0.084
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 0.259,
|
||||||
|
"y": 0.353,
|
||||||
|
"z": 0.084
|
||||||
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -77,12 +89,36 @@
|
|||||||
"name": "螺栓螺母",
|
"name": "螺栓螺母",
|
||||||
"content": [
|
"content": [
|
||||||
"避雷器的金属材料(连接板、螺栓螺母紧固件等金属部位)均应采用热浸镀锌钢(镀锌层厚度应不小于 86µm)或 316L 不锈钢以防腐防锈"
|
"避雷器的金属材料(连接板、螺栓螺母紧固件等金属部位)均应采用热浸镀锌钢(镀锌层厚度应不小于 86µm)或 316L 不锈钢以防腐防锈"
|
||||||
|
],
|
||||||
|
"position": [
|
||||||
|
{
|
||||||
|
"x": -0.024,
|
||||||
|
"y": 0.263,
|
||||||
|
"z": 0.001
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": -0.45,
|
||||||
|
"y": 0.363,
|
||||||
|
"z": 0.001
|
||||||
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "线夹",
|
"name": "线夹",
|
||||||
"content": [
|
"content": [
|
||||||
"配置 JLG 挂钩引流线夹"
|
"配置 JLG 挂钩引流线夹"
|
||||||
|
],
|
||||||
|
"position": [
|
||||||
|
{
|
||||||
|
"x": 0.612,
|
||||||
|
"y": 0.422,
|
||||||
|
"z": -0.012
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 0.512,
|
||||||
|
"y": 0.522,
|
||||||
|
"z": -0.02
|
||||||
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -90,9 +126,22 @@
|
|||||||
"content": [
|
"content": [
|
||||||
"引线两端需要配置绝缘罩,位置 1:避雷器与引线连接位置(支柱绝缘子与引线连接位置)",
|
"引线两端需要配置绝缘罩,位置 1:避雷器与引线连接位置(支柱绝缘子与引线连接位置)",
|
||||||
"位置 2:引线与导线连接位置(JLG 挂钩引流线夹位置)"
|
"位置 2:引线与导线连接位置(JLG 挂钩引流线夹位置)"
|
||||||
|
],
|
||||||
|
"position": [
|
||||||
|
{
|
||||||
|
"x": -0.069,
|
||||||
|
"y": 0.111,
|
||||||
|
"z": -0.011
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": -0.35,
|
||||||
|
"y": 0.161,
|
||||||
|
"z": -0.011
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"viewStr": {"position":[-0.028319151734416986,0.44690071186209945,-1.060153541579475],"quaternion":[0.010099016171466932,0.9892156310872932,0.12015805821630032,-0.08314136233322726],"fov":55,"target":[0.14773152364529415,0.18683321728723215,-0.02022672204783433]}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"modelName": "交流棒形悬式复合绝缘子,FXBW-10/70",
|
"modelName": "交流棒形悬式复合绝缘子,FXBW-10/70",
|
||||||
@@ -145,6 +194,18 @@
|
|||||||
"content": [
|
"content": [
|
||||||
"护套与芯棒之间以及伞裙与护套之间的界面应是永久性粘接",
|
"护套与芯棒之间以及伞裙与护套之间的界面应是永久性粘接",
|
||||||
"粘接部分应牢固密实,没有气泡和缝隙,防止污秽物和水汽进入"
|
"粘接部分应牢固密实,没有气泡和缝隙,防止污秽物和水汽进入"
|
||||||
|
],
|
||||||
|
"position": [
|
||||||
|
{
|
||||||
|
"x": 0,
|
||||||
|
"y": 0.245,
|
||||||
|
"z": 0.015
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 0.1,
|
||||||
|
"y": 0.315,
|
||||||
|
"z": 0.015
|
||||||
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -152,6 +213,18 @@
|
|||||||
"content": [
|
"content": [
|
||||||
"采用锡青铜、黄铜、奥氏体不锈钢或其他耐锈蚀性材料制作,不采用有防腐蚀表层而本身不耐锈蚀的材料,与绝缘子成套供应",
|
"采用锡青铜、黄铜、奥氏体不锈钢或其他耐锈蚀性材料制作,不采用有防腐蚀表层而本身不耐锈蚀的材料,与绝缘子成套供应",
|
||||||
"销腿末端弯曲部分尺寸严格满足标准规定,末端分开到 180° 再扳回原位无裂纹"
|
"销腿末端弯曲部分尺寸严格满足标准规定,末端分开到 180° 再扳回原位无裂纹"
|
||||||
|
],
|
||||||
|
"position": [
|
||||||
|
{
|
||||||
|
"x": 0.019,
|
||||||
|
"y": -0.055,
|
||||||
|
"z": 0.019
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 0.119,
|
||||||
|
"y": -0.005,
|
||||||
|
"z": 0.019
|
||||||
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -223,6 +296,18 @@
|
|||||||
"name": "基座材质",
|
"name": "基座材质",
|
||||||
"content": [
|
"content": [
|
||||||
"不锈钢 (不低于 S304,厚度不小于 5mm) 或热镀锌钢板 (镀层不小于 70μm,厚度不小于 3mm)"
|
"不锈钢 (不低于 S304,厚度不小于 5mm) 或热镀锌钢板 (镀层不小于 70μm,厚度不小于 3mm)"
|
||||||
|
],
|
||||||
|
"position": [
|
||||||
|
{
|
||||||
|
"x": 0.2,
|
||||||
|
"y": -0.006,
|
||||||
|
"z": 0.043
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 0.4,
|
||||||
|
"y": -0.006,
|
||||||
|
"z": 0.043
|
||||||
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="model-detail" :class="{ 'mobile': isMobile }">
|
<div class="model-detail" :class="{ 'mobile': isMobile }">
|
||||||
<!-- 顶部标题栏 -->
|
<!-- 顶部标题栏 -->
|
||||||
<div class="header">
|
<div :class="['header', isMobile ? 'header-move' : '']">
|
||||||
<el-button
|
<el-button
|
||||||
icon="el-icon-arrow-left"
|
icon="el-icon-arrow-left"
|
||||||
circle
|
size="medium"
|
||||||
size="small"
|
|
||||||
@click="goBack"
|
@click="goBack"
|
||||||
class="back-btn"
|
class="back-btn"
|
||||||
></el-button>
|
></el-button>
|
||||||
@@ -17,21 +16,22 @@
|
|||||||
<!-- PC端布局:左右分栏 -->
|
<!-- PC端布局:左右分栏 -->
|
||||||
<template v-if="!isMobile">
|
<template v-if="!isMobile">
|
||||||
<div class="viewer-section">
|
<div class="viewer-section">
|
||||||
<three-viewer
|
<!-- <three-viewer
|
||||||
v-if="modelPath"
|
v-if="modelPath"
|
||||||
:model-path="modelPath"
|
:model-path="modelPath"
|
||||||
:annotations="modelData.componentCheck"
|
:annotations="modelData.componentCheck"
|
||||||
@annotation-click="handleAnnotationClick"
|
@annotation-click="handleAnnotationClick"
|
||||||
@model-loaded="handleModelLoaded"
|
@model-loaded="handleModelLoaded"
|
||||||
@model-error="handleModelError"
|
@model-error="handleModelError"
|
||||||
|
/> -->
|
||||||
|
<three-viewer-debug
|
||||||
|
:model-path="modelPath"
|
||||||
|
:annotations="modelData.componentCheck"
|
||||||
|
:viewStr="modelData.viewStr"
|
||||||
|
:debug-mode="pageTyep == 'show' ? false : true"
|
||||||
|
@annotation-click="handleAnnotationClick"
|
||||||
|
@model-loaded="handleModelLoaded"
|
||||||
/>
|
/>
|
||||||
<!-- <three-viewer-debug
|
|
||||||
:model-path="modelPath"
|
|
||||||
:annotations="modelData.componentCheck"
|
|
||||||
:debug-mode="true"
|
|
||||||
@annotation-click="handleAnnotationClick"
|
|
||||||
@model-loaded="handleModelLoaded"
|
|
||||||
/> -->
|
|
||||||
<div v-if="loading" class="loading-overlay">
|
<div v-if="loading" class="loading-overlay">
|
||||||
<el-progress
|
<el-progress
|
||||||
type="circle"
|
type="circle"
|
||||||
@@ -54,13 +54,21 @@
|
|||||||
<!-- 移动端布局:上中下 -->
|
<!-- 移动端布局:上中下 -->
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<div class="viewer-section-mobile">
|
<div class="viewer-section-mobile">
|
||||||
<three-viewer
|
<!-- <three-viewer
|
||||||
v-if="modelPath"
|
v-if="modelPath"
|
||||||
:model-path="modelPath"
|
:model-path="modelPath"
|
||||||
:annotations="modelData.componentCheck"
|
:annotations="modelData.componentCheck"
|
||||||
@annotation-click="handleAnnotationClick"
|
@annotation-click="handleAnnotationClick"
|
||||||
@model-loaded="handleModelLoaded"
|
@model-loaded="handleModelLoaded"
|
||||||
@model-error="handleModelError"
|
@model-error="handleModelError"
|
||||||
|
/> -->
|
||||||
|
<three-viewer-debug
|
||||||
|
:model-path="modelPath"
|
||||||
|
:annotations="modelData.componentCheck"
|
||||||
|
:viewStr="modelData.viewStr"
|
||||||
|
:debug-mode="pageTyep == 'show' ? false : true"
|
||||||
|
@annotation-click="handleAnnotationClick"
|
||||||
|
@model-loaded="handleModelLoaded"
|
||||||
/>
|
/>
|
||||||
<div v-if="loading" class="loading-overlay">
|
<div v-if="loading" class="loading-overlay">
|
||||||
<el-progress
|
<el-progress
|
||||||
@@ -125,13 +133,15 @@ export default {
|
|||||||
selectedComponentIndex: -1,
|
selectedComponentIndex: -1,
|
||||||
selectedComponent: null,
|
selectedComponent: null,
|
||||||
drawerVisible: false,
|
drawerVisible: false,
|
||||||
isMobile: false
|
isMobile: false,
|
||||||
|
pageTyep: 'show'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
this.checkDevice()
|
this.checkDevice()
|
||||||
this.loadModelData()
|
this.loadModelData()
|
||||||
window.addEventListener('resize', this.checkDevice)
|
window.addEventListener('resize', this.checkDevice)
|
||||||
|
this.pageTyep = this.$route.query.pageTyep || 'show'
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
window.removeEventListener('resize', this.checkDevice)
|
window.removeEventListener('resize', this.checkDevice)
|
||||||
@@ -191,26 +201,37 @@ export default {
|
|||||||
|
|
||||||
.header {
|
.header {
|
||||||
height: 60px;
|
height: 60px;
|
||||||
background: #409eff;
|
color: #303133;
|
||||||
color: white;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 0 20px;
|
padding-right: 10px;
|
||||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
.header-move{
|
||||||
|
background: #409eff;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
.back-btn {
|
.back-btn {
|
||||||
background: rgba(255, 255, 255, 0.2);
|
|
||||||
border: none;
|
border: none;
|
||||||
color: white;
|
color: #303133;
|
||||||
margin-right: 15px;
|
background-color: transparent;
|
||||||
|
/* margin-right: 10px; */
|
||||||
|
}
|
||||||
|
.el-button:focus, .el-button:hover{
|
||||||
|
background-color: transparent;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
.el-button--medium{
|
||||||
|
font-size: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.back-btn:hover {
|
.header-move .back-btn {
|
||||||
background: rgba(255, 255, 255, 0.3);
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
@@ -235,7 +256,7 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.table-section {
|
.table-section {
|
||||||
width: 450px;
|
width: 40%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -308,18 +329,26 @@ export default {
|
|||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.el-tabs--border-card{
|
||||||
|
border-left: none;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.header {
|
.header {
|
||||||
height: 50px;
|
height: 50px;
|
||||||
padding: 0 15px;
|
/* padding: 0 15px; */
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.back-btn {
|
.el-tabs--border-card{
|
||||||
margin-right: 10px;
|
border-top: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* .back-btn {
|
||||||
|
margin-right: 10px;
|
||||||
|
} */
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -9,12 +9,12 @@
|
|||||||
:key="index"
|
:key="index"
|
||||||
class="model-card"
|
class="model-card"
|
||||||
shadow="hover"
|
shadow="hover"
|
||||||
@click.native="goToDetail(index)"
|
|
||||||
>
|
>
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
<i class="el-icon-box"></i>
|
<i class="el-icon-box"></i>
|
||||||
<h3>{{ item.modelName }}</h3>
|
<h3>{{ item.modelName }}</h3>
|
||||||
<p>点击查看详情</p>
|
<p @click="goToDetail(index, 'show')">点击查看详情</p>
|
||||||
|
<p @click="goToDetail(index, 'edit')">编辑</p>
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
</div>
|
</div>
|
||||||
@@ -32,8 +32,8 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
goToDetail(id) {
|
goToDetail(id, pageTyep) {
|
||||||
this.$router.push({ name: 'ModelDetail', params: { id } })
|
this.$router.push({ name: 'ModelDetail', params: { id }, query: { pageTyep: pageTyep } })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||