Newer
Older
urbanLifeline_YanAn / src / views / oneMap / BIM / Tunnel.vue
@zhangzhihui zhangzhihui on 28 Oct 18 KB 站点对接接口
  1. <template>
  2. <div id="Tunnel" ref="threeDom">
  3. <div class="YuanDian" v-for="(item, i) in labelData" :key="item.id" :id="item.id" style="display: none">
  4. <div class="iconBox" v-show="item.show">
  5. <img src="@/assets/images/ljx.png" alt="" class="iconImg" />
  6. <div class="ydBox">
  7. <div class="YuanDianText">{{ item.name }}</div>
  8. <div class="ydData" v-for="(val, k) in item.data" :key="k" v-show="val.data">
  9. <div class="leftData">{{ val.phy + ':' }}</div>
  10. <div class="rightData">{{ val.data + ' ' + val.unit }}</div>
  11. </div>
  12. <div class="ydData">
  13. <div class="leftData">{{ '检测时间:' }}</div>
  14. <div class="rightData">{{ item.dataTime }}</div>
  15. </div>
  16. </div>
  17. </div>
  18. <img class="YuanDianIcon" :src="item.icon" @mouseenter="item.show = true" @mouseleave="item.show = false" />
  19. </div>
  20. </div>
  21. </template>
  22.  
  23. <script setup name="Tunnel">
  24. import { ref, reactive, toRefs, onMounted } from 'vue';
  25. import * as THREE from 'three';
  26. import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'; //控制器
  27. import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'; // gltf加载器
  28. import { RGBELoader } from 'three/examples/jsm/loaders/RGBELoader';
  29. import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader';
  30. import { Water } from 'three/examples/jsm/objects/Water.js';
  31.  
  32. import { CSS3DRenderer, CSS3DObject } from 'three/examples/jsm/renderers/CSS3DRenderer.js';
  33. import gsap from 'gsap';
  34. import { CSSRulePlugin } from 'gsap/CSSRulePlugin';
  35. import bus from '@/bus';
  36.  
  37. import { pointGetDataList } from '@/api/system/tanchuang';
  38.  
  39. const show = ref('');
  40. const width = ref(null);
  41. const height = ref(null);
  42. const Camera = ref(null);
  43. const Renderer = ref(null);
  44. const Controls = ref(null);
  45. const granaryArr = ref([]);
  46. const LabelRenderer = ref(null);
  47. const animationFrameId = ref(null);
  48. let Twater = null;
  49. const labelData = ref([
  50. {
  51. name: '激光位移计',
  52. id: `YuanDian1`,
  53. position: {
  54. // x: -1400,
  55. x: -900,
  56. y: 30,
  57. z: -140,
  58. },
  59. icon: '/Three/icon/wyj_mx.png',
  60. data: [],
  61. show: false,
  62. pointCode: 'kohqORm',
  63. dataTime: '',
  64. },
  65. {
  66. name: '激光位移计',
  67. id: `YuanDian2`,
  68. position: {
  69. // x: -1400,
  70. x: -900,
  71. y: 30,
  72. z: -85,
  73. },
  74. icon: '/Three/icon/wyj_mx.png',
  75. data: [],
  76. show: false,
  77. pointCode: 'LtfAqBh',
  78. dataTime: '',
  79. },
  80. {
  81. name: '静力水准仪',
  82. id: `YuanDian3`,
  83. position: {
  84. x: -1390,
  85. y: -50,
  86. z: -90,
  87. },
  88. icon: '/Three/icon/jlszy_icon.png',
  89. data: [],
  90. show: false,
  91. pointCode: 'xr8JcRb',
  92. dataTime: '',
  93. },
  94. {
  95. name: '静力水准仪',
  96. id: `YuanDian4`,
  97. position: {
  98. x: -900,
  99. y: -50,
  100. z: -90,
  101. },
  102. icon: '/Three/icon/jlszy_icon.png',
  103. data: [],
  104. show: false,
  105. pointCode: 'zkbTwJW',
  106. dataTime: '',
  107. },
  108. {
  109. name: '应变计',
  110. id: `YuanDian5`,
  111. position: {
  112. x: -900,
  113. y: 80,
  114. z: 0,
  115. },
  116. icon: '/Three/icon/ybj_icon.png',
  117. data: [],
  118. show: false,
  119. pointCode: '13G1Hnc',
  120. dataTime: '',
  121. },
  122. {
  123. name: '裂缝计',
  124. id: `YuanDian6`,
  125. position: {
  126. x: -900,
  127. y: 80,
  128. z: -210,
  129. },
  130. icon: '/Three/icon/lfj_mx.png',
  131. data: [],
  132. show: false,
  133. pointCode: 'WAB89Dk',
  134. dataTime: '',
  135. },
  136. ]);
  137. const deviceCode = ref(null);
  138. const stCode = ref(null);
  139. const MiMa = ref(null);
  140. const dialogVisible = ref(false);
  141. const loading = ref(false);
  142. const status = ref(false);
  143. const timer = ref(null);
  144.  
  145. const Scene = new THREE.Scene();
  146. const clock = new THREE.Clock();
  147. const threeDom = ref(null);
  148. const cameraPosition = {
  149. x: -2200.221584999469584,
  150. y: 200.590211176632594,
  151. z: 600.665579736149148,
  152. };
  153. const cameraLookat = {
  154. x: 1,
  155. y: 1,
  156. z: -700,
  157. };
  158.  
  159. /**
  160. * 初始化模态对话框
  161. * @param {string} name - 模型的名称
  162. * @param {string} url - 模型的URL
  163. */
  164. const initModal = (name, url) => {
  165. const Gltfloader = new GLTFLoader();
  166. var dracoLoader = new DRACOLoader();
  167. // dracoLoader.setDecoderPath("https://zhzz.hongshan.gov.cn:8865/file/hongshan/Three_Gltf/"); //设置解压库文件路径
  168. dracoLoader.setDecoderPath('/draco/'); //设置解压库文件路径
  169. Gltfloader.setDRACOLoader(dracoLoader);
  170. Gltfloader.load(
  171. url,
  172. gltf => {
  173. gltf.scene.name = name;
  174. gltf.scene.scale.set(1, 1, 1);
  175. // gltf.scene.position.x = -70;
  176. gltf.scene.position.y = 2;
  177.  
  178. cameraReset(cameraPosition, cameraLookat);
  179.  
  180. Scene.add(gltf.scene);
  181. Scene.traverse(function (child) {
  182. if (child.isMesh) {
  183. child.frustumCulled = false;
  184. child.material.side = THREE.DoubleSide;
  185. child.material.emissive = child.material.color;
  186. child.material.emissiveMap = child.material.map;
  187. granaryArr.value.push(child);
  188. }
  189. });
  190. },
  191. function (xhr) {
  192. console.log(Math.floor((xhr.loaded / xhr.total) * 100));
  193. }
  194. );
  195. };
  196.  
  197. /**
  198. * 恢复相机位置和视角
  199. *
  200. * 此函数通过GSAP动画库来调整相机的位置和视角,以实现 Smooth Transition
  201. *
  202. * @param {Object} position - 相机的新位置,包含x, y, z坐标
  203. * @param {Object} lookAt - 相机的新视角目标,包含x, y, z坐标,目前未使用
  204. * @param {number} time - 动画过渡时间,单位为秒,默认为3秒
  205. */
  206. const cameraReset = (position, lookAt, time = 3) => {
  207. gsap.to(Camera.value.position, {
  208. x: position.x,
  209. y: position.y,
  210. z: position.z,
  211. duration: time,
  212. ease: 'power4.out',
  213. });
  214. // gsap.to(Camera.value.lookAt, {
  215. // x: lookAt.x,
  216. // y: lookAt.y,
  217. // z: lookAt.z,
  218. // duration: time,
  219. // ease: "power4.out",
  220. // });
  221. gsap.to(Controls.value.target, {
  222. x: lookAt.x,
  223. y: lookAt.y,
  224. z: lookAt.z,
  225. duration: time,
  226. ease: 'power4.out',
  227. });
  228. };
  229.  
  230. /**
  231. * 初始化渲染器
  232. * 该函数负责创建和配置Three.js的WebGL渲染器,包括设置视口大小、抗锯齿、阴影、像素比、色调映射等
  233. */
  234. const initRenderer = () => {
  235. width.value = document.getElementById('Tunnel').clientWidth;
  236. height.value = document.getElementById('Tunnel').clientHeight;
  237. Renderer.value = new THREE.WebGLRenderer({
  238. antialias: true,
  239. // alpha: true, //开启alpha
  240. });
  241. Renderer.value.shadowMap.enabled = true;
  242. Renderer.value.setPixelRatio(window.devicePixelRatio);
  243. Renderer.value.setSize(width.value, height.value, true);
  244. // Renderer.value.toneMapping = THREE.ACESFilmicToneMapping;
  245. Renderer.value.toneMappingExposure = 1; // 曝光系数
  246. threeDom.value.appendChild(Renderer.value.domElement);
  247. };
  248.  
  249. /**
  250. * 初始化标签渲染器
  251. * 该函数用于创建和配置一个CSS3D渲染器,用于渲染标签对象
  252. * 它设置了渲染器的大小,位置,并将其添加到三维场景的DOM元素中
  253. */
  254. const initLabelRenderer = () => {
  255. LabelRenderer.value = new CSS3DRenderer();
  256. LabelRenderer.value.setSize(width.value, height.value);
  257. LabelRenderer.value.domElement.style.position = 'absolute';
  258. LabelRenderer.value.domElement.style.top = '0px';
  259. threeDom.value.appendChild(LabelRenderer.value.domElement);
  260. // LabelRenderer.value.domElement.addEventListener("click", meshOnClick);
  261. };
  262.  
  263. /**
  264. * 初始化摄像机
  265. * 创建一个透视摄像机,并设置其位置
  266. */
  267. const initCamera = () => {
  268. Camera.value = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 1, 4000);
  269. Camera.value.position.set(500, 500, 500);
  270. };
  271.  
  272. /**
  273. * 初始化控制对象
  274. * 设置相机控制属性,以实现特定的相机行为
  275. */
  276. const initControls = () => {
  277. Controls.value = new OrbitControls(Camera.value, LabelRenderer.value.domElement);
  278. //上下翻转的最大角度
  279. Controls.value.maxPolarAngle = 1.5;
  280. // //上下翻转的最小角度
  281. Controls.value.minPolarAngle = 0.2;
  282. //是否允许缩放
  283. Controls.value.enableZoom = true;
  284. // 使动画循环使用时阻尼或自转 意思是否有惯性
  285. Controls.value.enableDamping = false;
  286. // 动态阻尼系数 就是鼠标拖拽旋转灵敏度
  287. Controls.value.dampingFactor = 0.04;
  288. // 是否可以旋转
  289. Controls.value.enableRotate = true;
  290. // 是否可以缩放与速度
  291. Controls.value.enableZoom = true;
  292. // 设置相机距离原点的最远距离
  293. Controls.value.minDistance = 50;
  294. // 设置相机距离原点的最远距离
  295. Controls.value.maxDistance = 3000;
  296. // 是否开启右键拖拽
  297. Controls.value.enablePan = true;
  298. // AxesHelper:辅助观察的坐标系
  299. // const axesHelper = new THREE.AxesHelper(3000);
  300. // Scene.add(axesHelper);
  301. };
  302.  
  303. /**
  304. * 初始化灯光
  305. * 创建并添加环境光到场景中,以模拟全局照明效果
  306. */
  307. const initLight = () => {
  308. // 设置场景的背景颜色,即天空颜色
  309. // Scene.background = new THREE.Color(0x999999); // 设置背景,可以更换为你想要的颜色
  310. // 设置天空的雾气颜色和雾气近距离
  311. // Scene.fog = new THREE.Fog(0xffffff, 50, 200); // 距离10开始,到200结束
  312. // Scene.fog = new THREE.Fog(0x1784f9, 50, 2000); // 距离10开始,到200结束
  313. // 创建平行光源
  314. // const directionalLight = new THREE.DirectionalLight(0xffffff, 0.6);
  315. // const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
  316. // // directionalLight.position.set(-2500, 1, 1);
  317. // Scene.add(directionalLight);
  318. // 创建环境光
  319. const ambientLight = new THREE.AmbientLight('#ffffff', 0.5);
  320. ambientLight.visible = true;
  321. // const ambientLight = new THREE.AmbientLight('#1784f9', 0.8);
  322. Scene.add(ambientLight);
  323. // 创建平行光辅助线
  324. // const directionalLightHelper = new THREE.DirectionalLightHelper(directionalLight, 0.5);
  325. // Scene.add(directionalLightHelper);
  326. // 创建点光源
  327. // const PointLight = new THREE.PointLight(0x1784f9, 1, 300);
  328. // PointLight.position.set(100, -100, 10);
  329. // Scene.add(PointLight);
  330. // const spotLight = new THREE.SpotLight(0xffffff);
  331. // spotLight.position.set(-1000, 1000, 0);
  332. // spotLight.castShadow = true;
  333. // spotLight.intensity = 800;
  334. // Scene.add(spotLight);
  335. };
  336.  
  337. /**
  338. * 初始化天空盒
  339. * 使用PMREMGenerator生成环境贴图,并设置为场景的环境光和背景
  340. * 通过RGBELoader加载HDR纹理,用于创建环境贴图
  341. * 注意:此函数未使用TextureLoader加载背景图片,而是使用HDR纹理
  342. */
  343. const initSky = () => {
  344. // var pmremGenerator = new THREE.PMREMGenerator(Renderer.value); // 使用hdr作为背景色
  345. // pmremGenerator.compileEquirectangularShader();
  346. // new RGBELoader().load("/Gltf/Skey_1K.hdr", function (texture) {
  347. // const envMap = pmremGenerator.fromEquirectangular(texture).texture;
  348. // envMap.isPmremTexture = true;
  349. // Scene.environment = envMap;
  350. // // Scene.background = envMap;
  351. // pmremGenerator.dispose();
  352. // });
  353. new THREE.TextureLoader().load('/Gltf/bg.png', function (texture) {
  354. Scene.background = texture;
  355. });
  356. };
  357.  
  358. /**
  359. * 渲染函数,用于不断更新和渲染场景
  360. */
  361.  
  362. const Render = () => {
  363. animationFrameId.value = requestAnimationFrame(Render);
  364. Controls.value.update(); // 轨道控制器的更新
  365. Renderer.value.clear(); // 清除画布
  366. updateLabelOrientation(Camera.value.position);
  367. Renderer.value.render(Scene, Camera.value);
  368. LabelRenderer.value.render(Scene, Camera.value);
  369. const delta = clock.getDelta();
  370. // 如需调试请打开这个获取Camera,Controls 的值
  371. // console.log('Camera.value', Camera.value);
  372. // console.log('Controls.value', Controls.value);
  373. };
  374.  
  375. // 创建气泡窗
  376. function createLable() {
  377. labelData.value.forEach((item, index) => {
  378. let labelCSS3D = Scene.getObjectByName(`YuanDian${index + 1}`);
  379. if (labelCSS3D === undefined) {
  380. let lableDiv = document.getElementById(`YuanDian${index + 1}`);
  381. lableDiv.style.display = 'block';
  382. labelCSS3D = new CSS3DObject(lableDiv);
  383. labelCSS3D.position.set(item.position.x, item.position.y, item.position.z);
  384. // labelCSS3D.scale.set(0.1, 0.1, 0.1);
  385. labelCSS3D.scale.set(3, 3, 3);
  386. labelCSS3D.name = `YuanDian${index + 1}`;
  387. // labelCSS3D.rotation.y = - Math.PI / 6; // 30度转换为弧度
  388. Scene.add(labelCSS3D);
  389. } else {
  390. labelCSS3D.visible = true;
  391. }
  392. });
  393. }
  394.  
  395. /**
  396. * 更新所有气泡框的方向,使其始终面向相机
  397. */
  398. function updateLabelOrientation(cameraPosition) {
  399. labelData.value.forEach((item, index) => {
  400. let labelCSS3D = Scene.getObjectByName(`YuanDian${index + 1}`);
  401. if (labelCSS3D) {
  402. const direction = new THREE.Vector3();
  403. direction.subVectors(cameraPosition, labelCSS3D.position).normalize();
  404. const angle = Math.atan2(direction.x, direction.z);
  405. labelCSS3D.rotation.y = angle; // 调整角度使其始终面向相机
  406. }
  407. });
  408. }
  409.  
  410. const flyTo = data => {
  411. // 静立水准仪
  412. if (data.name[0] == '激光位移计') {
  413. // 摄像机位置
  414. gsap.to(Camera.value.position, {
  415. x: -1509.9225299741292,
  416. y: 437.8471459592194,
  417. z: 345.1272978647253,
  418. duration: 3,
  419. ease: 'power4.out',
  420. });
  421. // 视角
  422. gsap.to(Controls.value.target, {
  423. x: -148.03951167756108,
  424. y: -406.06002918439316,
  425. z: -648.358072973929,
  426. duration: 3,
  427. ease: 'power4.out',
  428. });
  429. } else if (data.name[0] == '静力水准仪') {
  430. // 摄像机位置
  431. gsap.to(Camera.value.position, {
  432. x: -1797.8251896016222,
  433. y: 191.14387148699308,
  434. z: 635.4949495785738,
  435. duration: 3,
  436. ease: 'power4.out',
  437. });
  438. // 视角
  439. gsap.to(Controls.value.target, {
  440. x: -318.0579715399841,
  441. y: 35.607933619702905,
  442. z: -983.3798167321406,
  443. duration: 3,
  444. ease: 'power4.out',
  445. });
  446. } else if (data.name.includes('应变计')) {
  447. gsap.to(Camera.value.position, {
  448. x: -1694.6408790629657,
  449. y: 259.7822136553308,
  450. z: 583.3976961218505,
  451. duration: 3,
  452. ease: 'power4.out',
  453. });
  454. gsap.to(Controls.value.target, {
  455. x: -546.8674967113623,
  456. y: 24.916489480624683,
  457. z: -409.17274087310204,
  458. duration: 3,
  459. ease: 'power4.out',
  460. });
  461. } else if (data.name.includes('裂缝计')) {
  462. gsap.to(Camera.value.position, {
  463. x: -1530.357885036637,
  464. y: 356.99171737793193,
  465. z: 332.61457706089254,
  466. duration: 3,
  467. ease: 'power4.out',
  468. });
  469. gsap.to(Controls.value.target, {
  470. x: -80.11715403668808,
  471. y: -84.77688943101725,
  472. z: -787.9032398615595,
  473. duration: 3,
  474. ease: 'power4.out',
  475. });
  476. }
  477.  
  478. // 打开弹窗
  479. labelData.value.forEach(element => {
  480. // if (element.name == data.name[0]) {
  481. if (data.name.includes(element.name)) {
  482. element.show = true;
  483. } else {
  484. element.show = false;
  485. }
  486. });
  487. };
  488.  
  489. // 获取点位信息
  490. const getPonintInfo = () => {
  491. const promises = labelData.value.map(item => {
  492. return pointGetDataList({ pointCode: item.pointCode }).then(res => {
  493. // 检测时间
  494. const timeWithDate = res.data.find(time => time.dataTime);
  495. if (timeWithDate) {
  496. item.dataTime = timeWithDate.dataTime;
  497. }
  498. return { ...item, data: res.data };
  499. });
  500. });
  501.  
  502. Promise.all(promises)
  503. .then(updatedItems => {
  504. labelData.value = updatedItems;
  505. console.log("🚀 ~ getPonintInfo ~ updatedItems:", updatedItems)
  506. })
  507. .catch(error => {
  508. console.error('请求出错:labelData.value', error);
  509. });
  510. };
  511.  
  512. onBeforeMount(() => {
  513. // initGltfFloor();
  514. // initModal('ChangJing', '/Gltf/ChuWangCheng.gltf');
  515. initModal('yjlSD', 'https://newfiber-cloud-1255570142.cos.ap-chengdu.myqcloud.com/yanan/yjlSD3.gltf');
  516. // 共同弹窗触发事件
  517. bus.on('Tunnel_flyTo', params => {
  518. // 打开弹窗
  519. flyTo(params);
  520. });
  521. });
  522. onMounted(() => {
  523. getPonintInfo();
  524. nextTick(() => {
  525. if (document.readyState === 'complete') {
  526. createLable();
  527. // createWaterLevel();
  528. }
  529. });
  530. initRenderer();
  531. initLabelRenderer();
  532. initCamera();
  533. initControls();
  534. initLight();
  535. initSky();
  536. createLable();
  537. // LoadWater();
  538. Render();
  539. });
  540.  
  541. onBeforeUnmount(() => {
  542. bus.off('Tunnel_flyTo');
  543. // document.removeEventListener('click', meshOnClick);
  544. // window.removeEventListener("resize", onWindowResize, false);
  545.  
  546. Scene.traverse(e => {
  547. if (e.BufferGeometry) e.BufferGeometry.dispose();
  548. if (e.material) {
  549. if (Array.isArray(e.material)) {
  550. e.material.forEach(m => {
  551. m.dispose();
  552. });
  553. } else {
  554. e.material.dispose();
  555. }
  556. }
  557. if (e.isMesh) {
  558. e.remove();
  559. }
  560. });
  561. Scene.remove();
  562. Renderer.value.dispose();
  563. Renderer.value.content = null;
  564. clearInterval(timer.value);
  565.  
  566. // 清理渲染器和相机
  567. Camera.value = null;
  568.  
  569. cancelAnimationFrame(animationFrameId.value);
  570. });
  571. </script>
  572.  
  573. <style lang="scss" scoped>
  574. #Tunnel {
  575. width: 100%;
  576. height: 100%;
  577.  
  578. .YuanDian {
  579. // z-index: 9999;
  580. position: relative;
  581. // min-height: 20px;
  582. .iconBox {
  583. position: relative;
  584. // border: 1px solid red;
  585. }
  586. .iconImg {
  587. position: absolute;
  588. left: 50%;
  589. // transform: translateX(-50%);
  590. bottom: 5px;
  591. width: 19px;
  592. height: 12px;
  593. }
  594. .ydBox {
  595. z-index: 999999;
  596. // width: 134px;
  597. // height: 70px;
  598. position: absolute;
  599. left: calc(50% + 19px);
  600. bottom: 12px;
  601. width: 60px;
  602. // height: 26px;
  603. background: linear-gradient(0deg, rgba(12, 54, 92, 0.6) 0%, rgba(12, 54, 92, 0.6) 100%);
  604. border-radius: 2px;
  605. border: 1px solid #04d8ff;
  606. .ydData {
  607. display: flex;
  608. align-items: center;
  609. justify-content: space-between;
  610. padding: 0 3px;
  611. font-size: 3px;
  612. height: 7px;
  613. line-height: 7px;
  614. color: #04d8ff;
  615. // .leftData {
  616. // }
  617. // .rightData {
  618. // }
  619. }
  620. }
  621. .YuanDianText {
  622. // padding-bottom: 10px;
  623. height: 7px;
  624. line-height: 7px;
  625. color: #fff;
  626. text-shadow: 0px 1px 3px rgba(0, 0, 0, 0.9);
  627. white-space: nowrap;
  628. word-break: keep-all;
  629. // cursor: pointer;
  630. pointer-events: none;
  631. font-size: 3px;
  632. text-align: center;
  633. background: linear-gradient(90deg, rgba(4, 216, 255, 0.8) 0%, rgba(4, 216, 255, 0.2) 100%);
  634. }
  635.  
  636. .YuanDianIcon {
  637. z-index: 999;
  638. width: 10px;
  639. height: 10px;
  640. position: absolute;
  641. left: 50%;
  642. transform: translateX(-50%);
  643. bottom: 0;
  644. pointer-events: auto;
  645. }
  646. }
  647. }
  648. </style>