<template> <div id="WangJiaPIng" ref="threeDom"> <div class="YuanDian" v-for="(item, i) in labelData" :key="item.id" :id="item.id" style="display: none"> <div class="iconBox" v-show="item.show"> <img src="@/assets/images/ljx.png" alt="" class="iconImg" /> <div class="ydBox"> <el-icon color="#fff" class="iconClose" @click="closeWindow(item)"><CircleClose /></el-icon> <div class="YuanDianText">{{ item.name }}</div> <div class="ydData" v-for="(val, k) in item.data" :key="k" v-show="val.data && item.type !== 'Camera'"> <div class="leftData">{{ val.phy + ':' }}</div> <div class="rightData">{{ val.data + ' ' + val.unit }}</div> </div> <div class="ydData" v-show="item.type !== 'Camera'"> <div class="leftData">{{ '检测时间:' }}</div> <div class="rightData">{{ item.dataTime }}</div> </div> </div> </div> <!-- <img class="YuanDianIcon" :src="item.icon" @mouseenter="item.show = true" @mouseleave="item.show = false" /> --> <img class="YuanDianIcon" :src="item.icon" @click="ydClick(item)" /> </div> </div> </template> <script setup name="WangJiaPIng"> import { ref, reactive, toRefs, onMounted } from 'vue'; import * as THREE from 'three'; import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'; //控制器 import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'; // gltf加载器 import { RGBELoader } from 'three/examples/jsm/loaders/RGBELoader'; import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader'; import { Water } from 'three/examples/jsm/objects/Water.js'; import { CSS3DRenderer, CSS3DObject } from 'three/examples/jsm/renderers/CSS3DRenderer.js'; import gsap from 'gsap'; import { CSSRulePlugin } from 'gsap/CSSRulePlugin'; import bus from '@/bus'; import { pointGetDataList } from '@/api/system/tanchuang'; const AllData = reactive({}); const show = ref(''); const width = ref(null); const height = ref(null); const Camera = ref(null); const Renderer = ref(null); const Controls = ref(null); const granaryArr = ref([]); const LabelRenderer = ref(null); const animationFrameId = ref(null); let Twater = null; const labelData = ref([ { name: '静力水准仪', id: `YuanDian1`, type: 'noCamera', // Camera position: { x: -0.5, y: 0, z: -0.5, }, icon: '/Three/icon/jlszy_icon.png', data: [], show: false, pointCode: 'WL62zX5', dataTime: '', toP: { x: -2.6309215167693845, y: 4.332423113638442, z: 21.572960056289727, duration: 2, ease: 'power4.out', }, toC: { x: 5.2676576739806205, y: 1.083337105388434, z: -19.44099716858533, duration: 2, ease: 'power4.out', }, }, { name: '静力水准仪', id: `YuanDian2`, type: 'noCamera', position: { x: -40, y: 0, z: -0.5, }, icon: '/Three/icon/jlszy_icon.png', data: [], show: false, pointCode: 'qGxgnkX', dataTime: '', toP: { x: -58.221584999469584, y: 6.590211176632594, z: 9.665579736149148, duration: 2, ease: 'power4.out', }, toC: { x: 1, y: 1, z: -20, duration: 2, ease: 'power4.out', }, }, { name: '应变计', id: `YuanDian3`, type: 'noCamera', position: { x: -37.5, y: 0, z: -0.2, }, icon: '/Three/icon/ybj_icon.png', data: [], show: false, // dsZCOmK pointCode: 'dsZCOmK', dataTime: '', toP: { x: -51.3485606944383, y: 5.243577935608563, z: 8.991560624060426, duration: 3, ease: 'power4.out', }, toC: { x: 1, y: 1, z: -20, duration: 3, ease: 'power4.out' }, }, { name: '应变计', id: `YuanDian4`, type: 'noCamera', position: { x: 37, y: 0, z: -0.2, }, icon: '/Three/icon/ybj_icon.png', data: [], show: false, pointCode: 'yKBFH23', dataTime: '', toP: { x: 22.461896619811345, y: 4.9018705165338625, z: 11.610714039725684, duration: 2, ease: 'power4.out', }, toC: { x: 46.94567624487662, y: 1.9550975524650114, z: -5.473227670824349, duration: 2, ease: 'power4.out', }, }, { name: '加速度计', id: `YuanDian5`, type: 'noCamera', position: { x: 0.5, y: 0, z: -0.5, }, icon: '/Three/icon/jsdj_icon.png', data: [], show: false, pointCode: 'nS34TXN', dataTime: '', toP: { x: -2.6309215167693845, y: 4.332423113638442, z: 21.572960056289727, duration: 2, ease: 'power4.out', }, toC: { x: 5.2676576739806205, y: 1.083337105388434, z: -19.44099716858533, duration: 2, ease: 'power4.out', }, }, { name: '加速度计', id: `YuanDian6`, type: 'noCamera', position: { x: -1.5, y: 0, z: -0.5, }, icon: '/Three/icon/jsdj_icon.png', data: [], show: false, pointCode: 'a60toBt', dataTime: '', toP: { x: -2.6309215167693845, y: 4.332423113638442, z: 21.572960056289727, duration: 2, ease: 'power4.out', }, toC: { x: 5.2676576739806205, y: 1.083337105388434, z: -19.44099716858533, duration: 2, ease: 'power4.out', }, }, { name: '风速风向仪', id: `YuanDian7`, type: 'noCamera', position: { x: -45.5, y: 2.5, z: -0.2, }, icon: '/Three/icon/fsfx_icon.png', data: [], show: false, pointCode: 'W5gfQor', dataTime: '', toP: { x: -58.221584999469584, y: 6.590211176632594, z: 9.665579736149148, duration: 2, ease: 'power4.out', }, toC: { x: 1, y: 1, z: -20, duration: 2, ease: 'power4.out', }, }, { name: '裂缝计', id: `YuanDian8`, type: 'noCamera', position: { x: -43.5, y: -0.3, z: 2, }, icon: '/Three/icon/lfj_mx.png', data: [], show: false, pointCode: 'tbd1HZw', dataTime: '', toP: { x: -57.02391699702596, y: 4.679545725497521, z: 12.416539848914276, duration: 2, ease: 'power4.out', }, toC: { x: 2.197668002974112, y: -0.9106652745024885, z: -17.24904015108576, duration: 2, ease: 'power4.out', }, }, { name: '彩虹桥延安中学方向摄像机', id: `YuanDian9`, type: 'Camera', position: { x: -50.5, y: 4, z: -5, }, icon: '/Three/icon/sxj_icon.png', data: [], show: false, pointCode: '114', dataTime: '', toP: { x: -70.27433095597907, y: 8.753833164595864, z: -5.218871727447189, duration: 2, ease: 'power4.out', }, toC: { x: -1.1466023223195003, y: -1.8988402785746414, z: -3.2914248069834957, duration: 2, ease: 'power4.out', }, }, { name: '彩虹桥纪念馆方向摄像机', id: `YuanDian10`, type: 'Camera', position: { x: 50.5, y: 4, z: -5, }, icon: '/Three/icon/sxj_icon.png', data: [], show: false, pointCode: '114', dataTime: '', toP: { x: 69.1252383332412, y: 8.632133195958298, z: -4.822438477532835, duration: 2, ease: 'power4.out', }, toC: { x: -0.38725392744080417, y: 0.8244778362443704, z: -6.522822745737244, duration: 2, ease: 'power4.out', }, }, ]); const deviceCode = ref(null); const stCode = ref(null); const MiMa = ref(null); const dialogVisible = ref(false); const loading = ref(false); const status = ref(false); const timer = ref(null); const Scene = new THREE.Scene(); const clock = new THREE.Clock(); const threeDom = ref(null); const cameraPosition = { // x: 457.66301930145875, // y: 397.5473109836439, // z: 943.8703438418463, x: -58.221584999469584, y: 6.590211176632594, z: 9.665579736149148, }; const cameraLookat = { x: 1, y: 1, z: -20, }; const closeWindow = item => { item.show = false; // 摄像机位置 gsap.to(Camera.value.position, cameraPosition); // 视角 gsap.to(Controls.value.target, cameraLookat); }; // 点位点击 const ydClick = point => { // 清除其他点位 再开 labelData.value.forEach(item => { item.show = false; }); point.show = true; // 摄像机位置 gsap.to(Camera.value.position, point.toP); // 视角 gsap.to(Controls.value.target, point.toC); if (point.type == 'Camera') { let data = { title: point.name, comIDs: ['Imou'], // getSiteId: item.id, }; bus.emit('publicDialog', data); } }; /** * 初始化模态对话框 * @param {string} name - 模型的名称 * @param {string} url - 模型的URL */ const initModal = (name, url) => { const Gltfloader = new GLTFLoader(); var dracoLoader = new DRACOLoader(); // dracoLoader.setDecoderPath("https://zhzz.hongshan.gov.cn:8865/file/hongshan/Three_Gltf/"); //设置解压库文件路径 dracoLoader.setDecoderPath('/draco/'); //设置解压库文件路径 Gltfloader.setDRACOLoader(dracoLoader); Gltfloader.load( url, gltf => { gltf.scene.name = name; gltf.scene.scale.set(1, 1, 1); // gltf.scene.position.x = -70; gltf.scene.position.y = 2; cameraReset(cameraPosition, cameraLookat); // Scene.add(gltf.scene); Scene.traverse(function (child) { if (child.isMesh) { child.frustumCulled = false; child.material.side = THREE.DoubleSide; granaryArr.value.push(child); } }); console.log(gltf); // gltf.scene.forEach((element) => { // if ((element.name = "布尔")) { // let material = new THREE.MeshBasicMaterial({ color: 0xCCCCCC }); // 设置路面颜色为灰色 // element.children[0] // // 路面 // // element.children // } // }); // 遍历模型中的所有网格 gltf.scene.traverse(child => { if (child.name == '立方体1' || child.name == '立方体2') { // 修改或替换材质 // child.material = new THREE.MeshBasicMaterial({ color: 0x545d0c}); // 将材质替换为红色基本材质 } }); // 将模型添加到场景中 Scene.add(gltf.scene); LoadWater(); }, function (xhr) { // console.log(Math.floor((xhr.loaded / xhr.total) * 100)); } ); }; /** * 恢复相机位置和视角 * * 此函数通过GSAP动画库来调整相机的位置和视角,以实现 Smooth Transition * * @param {Object} position - 相机的新位置,包含x, y, z坐标 * @param {Object} lookAt - 相机的新视角目标,包含x, y, z坐标,目前未使用 * @param {number} time - 动画过渡时间,单位为秒,默认为3秒 */ const cameraReset = (position, lookAt, time = 3) => { gsap.to(Camera.value.position, { x: position.x, y: position.y, z: position.z, duration: time, ease: 'power4.out', }); // gsap.to(Camera.value.lookAt, { // x: lookAt.x, // y: lookAt.y, // z: lookAt.z, // duration: time, // ease: "power4.out", // }); gsap.to(Controls.value.target, { x: lookAt.x, y: lookAt.y, z: lookAt.z, duration: time, ease: 'power4.out', }); }; /** * 初始化渲染器 * 该函数负责创建和配置Three.js的WebGL渲染器,包括设置视口大小、抗锯齿、阴影、像素比、色调映射等 */ const initRenderer = () => { width.value = document.getElementById('WangJiaPIng').clientWidth; height.value = document.getElementById('WangJiaPIng').clientHeight; Renderer.value = new THREE.WebGLRenderer({ antialias: true, // alpha: true, //开启alpha }); Renderer.value.shadowMap.enabled = true; Renderer.value.setPixelRatio(window.devicePixelRatio); Renderer.value.setSize(width.value, height.value, true); // Renderer.value.toneMapping = THREE.ACESFilmicToneMapping; Renderer.value.toneMappingExposure = 1; // 曝光系数 threeDom.value.appendChild(Renderer.value.domElement); }; /** * 初始化标签渲染器 * 该函数用于创建和配置一个CSS3D渲染器,用于渲染标签对象 * 它设置了渲染器的大小,位置,并将其添加到三维场景的DOM元素中 */ const initLabelRenderer = () => { LabelRenderer.value = new CSS3DRenderer(); LabelRenderer.value.setSize(width.value, height.value); LabelRenderer.value.domElement.style.position = 'absolute'; LabelRenderer.value.domElement.style.top = '0px'; threeDom.value.appendChild(LabelRenderer.value.domElement); // LabelRenderer.value.domElement.addEventListener("click", meshOnClick); }; /** * 初始化摄像机 * 创建一个透视摄像机,并设置其位置 */ const initCamera = () => { Camera.value = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 1, 4000); Camera.value.position.set(500, 500, 500); }; /** * 初始化控制对象 * 设置相机控制属性,以实现特定的相机行为 */ const initControls = () => { Controls.value = new OrbitControls(Camera.value, LabelRenderer.value.domElement); //上下翻转的最大角度 Controls.value.maxPolarAngle = 1.5; // //上下翻转的最小角度 Controls.value.minPolarAngle = 0.2; // 使动画循环使用时阻尼或自转 意思是否有惯性 Controls.value.enableDamping = false; // 动态阻尼系数 就是鼠标拖拽旋转灵敏度 Controls.value.dampingFactor = 0.04; // 是否可以旋转 Controls.value.enableRotate = true; // 是否可以缩放与速度 Controls.value.enableZoom = true; // 设置相机距离原点的最远距离 Controls.value.minDistance = 30; // 设置相机距离原点的最远距离 Controls.value.maxDistance = 3000; // 是否开启右键拖拽 Controls.value.enablePan = true; // AxesHelper:辅助观察的坐标系 // const axesHelper = new THREE.AxesHelper(3000); // Scene.add(axesHelper); }; /** * 初始化灯光 * 创建并添加环境光到场景中,以模拟全局照明效果 */ const initLight = () => { // // // 设置场景的背景颜色,即天空颜色 // // Scene.background = new THREE.Color(0x999999); // 设置背景,可以更换为你想要的颜色 // // // 设置天空的雾气颜色和雾气近距离 // Scene.fog = new THREE.Fog(0xffffff, 50, 200); // 距离10开始,到200结束 // // // 创建平行光源 // const directionalLight = new THREE.DirectionalLight(0xffffff, 0.6); // Scene.add(directionalLight); // // 创建环境光 // const ambientLight = new THREE.AmbientLight("#fff", 0.8); // Scene.add(ambientLight); // // // 创建平行光辅助线 // // const directionalLightHelper = new THREE.DirectionalLightHelper(directionalLight, 0.5); // // Scene.add(directionalLightHelper); // // 创建点光源 // const PointLight = new THREE.PointLight(0x1784f9, 1, 300); // PointLight.position.set(100, -100, 10); // Scene.add(PointLight); }; /** * 初始化天空盒 * 使用PMREMGenerator生成环境贴图,并设置为场景的环境光和背景 * 通过RGBELoader加载HDR纹理,用于创建环境贴图 * 注意:此函数未使用TextureLoader加载背景图片,而是使用HDR纹理 */ const initSky = () => { var pmremGenerator = new THREE.PMREMGenerator(Renderer.value); // 使用hdr作为背景色 pmremGenerator.compileEquirectangularShader(); new RGBELoader().load('/Gltf/Skey_1K.hdr', function (texture) { const envMap = pmremGenerator.fromEquirectangular(texture).texture; envMap.isPmremTexture = true; Scene.environment = envMap; Scene.background = envMap; pmremGenerator.dispose(); }); // new THREE.TextureLoader().load("/Gltf/bg.png", function (texture) { // Scene.background = texture; // }); }; /** * 渲染函数,用于不断更新和渲染场景 */ const Render = () => { animationFrameId.value = requestAnimationFrame(Render); Controls.value.update(); // 轨道控制器的更新 Renderer.value.clear(); // 清除画布 updateLabelOrientation(Camera.value.position); Renderer.value.render(Scene, Camera.value); LabelRenderer.value.render(Scene, Camera.value); const delta = clock.getDelta(); // 如需调试请打开这个获取Camera,Controls 的值 console.log('Camera.value', Camera.value); console.log('Controls.value', Controls.value); }; // 创建气泡窗 function createLable() { labelData.value.forEach((item, index) => { let labelCSS3D = Scene.getObjectByName(`YuanDian${index + 1}`); if (labelCSS3D === undefined) { let lableDiv = document.getElementById(`YuanDian${index + 1}`); lableDiv.style.display = 'block'; labelCSS3D = new CSS3DObject(lableDiv); labelCSS3D.position.set(item.position.x, item.position.y, item.position.z); labelCSS3D.scale.set(0.1, 0.1, 0.1); labelCSS3D.name = `YuanDian${index + 1}`; // labelCSS3D.rotation.y = - Math.PI / 6; // 30度转换为弧度 Scene.add(labelCSS3D); } else { labelCSS3D.visible = true; } }); } /** * 更新所有气泡框的方向,使其始终面向相机 */ function updateLabelOrientation(cameraPosition) { labelData.value.forEach((item, index) => { let labelCSS3D = Scene.getObjectByName(`YuanDian${index + 1}`); if (labelCSS3D) { const direction = new THREE.Vector3(); direction.subVectors(cameraPosition, labelCSS3D.position).normalize(); const angle = Math.atan2(direction.x, direction.z); labelCSS3D.rotation.y = angle; // 调整角度使其始终面向相机 } }); } const flyTo = data => { // 静立水准仪 if (data.name[0] == '静力水准仪') { // 摄像机位置 gsap.to(Camera.value.position, { x: -57.41155667894531, y: 6.7641604399912865, z: 11.781994826043844, duration: 3, ease: 'power4.out', }); // 视角 gsap.to(Controls.value.target, { x: -0.6994858490355302, y: 0.5764769980367779, z: -15.294633781993175, duration: 3, ease: 'power4.out', }); } else if (data.name[0] == '应变计') { // 摄像机位置 gsap.to(Camera.value.position, { x: -53.61576836842107, y: 7.710859842105263, z: 17.230486105263143, duration: 3, ease: 'power4.out', }); // 视角 gsap.to(Controls.value.target, { x: 29.03625918562505, y: -0.3313374072727649, z: -29.291435662949137, duration: 3, ease: 'power4.out', }); } else if (data.name.includes('加速度计')) { gsap.to(Camera.value.position, { x: -11.465032004199559, y: 5.053503359128684, z: 15.647675383755246, duration: 3, ease: 'power4.out', }); gsap.to(Controls.value.target, { x: 12.070445610968184, y: -0.5540312430818636, z: -12.12358097277393, duration: 3, ease: 'power4.out', }); } else if (data.name.includes('风速风向仪')) { gsap.to(Camera.value.position, { x: -58.221584999469584, y: 6.590211176632594, z: 9.665579736149148, duration: 3, ease: 'power4.out', }); gsap.to(Controls.value.target, { x: 1, y: 1, z: -20, duration: 3, ease: 'power4.out', }); } else if (data.name.includes('裂缝计')) { gsap.to(Camera.value.position, { x: -57.02391699702596, y: 4.679545725497521, z: 12.416539848914276, duration: 3, ease: 'power4.out', }); gsap.to(Controls.value.target, { x: 2.197668002974112, y: -0.9106652745024885, z: -17.24904015108576, duration: 3, ease: 'power4.out', }); } else if (data.name.includes('摄像机')) { gsap.to(Camera.value.position, { x: -74.57145168733881, y: 14.5437957001853, z: 18.62897991542918, duration: 3, ease: 'power4.out', }); gsap.to(Controls.value.target, { x: 0.6482923084213158, y: -1.5888868202391535, z: -19.59950474716774, duration: 3, ease: 'power4.out', }); } // x: -74.57145168733881, y: 14.5437957001853, z: 18.62897991542918, // x: 0.6482923084213158, y: -1.5888868202391535, z: -19.59950474716774, // 打开弹窗 labelData.value.forEach(element => { let val = element.type == 'Camera' ? '摄像机' : element.name; if (data.name.includes(val)) { element.show = true; } else { element.show = false; } }); }; // const BIMClick = () => { // console.log("触发点击事件,隐藏弹窗"); // labelData.value.forEach((element) => { // element.show = false; // }); // }; // 获取点位信息 const getPonintInfo = () => { const promises = labelData.value.map(item => { return pointGetDataList({ pointCode: item.pointCode }).then(res => { // 检测时间 const timeWithDate = res.data.find(time => time.dataTime); if (timeWithDate) { item.dataTime = timeWithDate.dataTime; } return { ...item, data: res.data }; }); }); Promise.all(promises) .then(updatedItems => { labelData.value = updatedItems; }) .catch(error => { console.error('请求出错:labelData.value', error); }); }; // 加入水体 const LoadWater = () => { const waterGeometry = new THREE.PlaneGeometry(100, 330, 32, 32); //水体大小 长宽 Twater = new Water(waterGeometry, { textureWidth: 512, textureHeight: 512, waterNormals: new THREE.TextureLoader().load('/Gltf/waternormals.jpg', function (texture) { texture.wrapS = texture.wrapT = THREE.RepeatWrapping; }), sunDirection: new THREE.Vector3(), sunColor: 0xffffff, waterColor: 0x001e0f, distortionScale: 1, fog: Scene.fog !== undefined, }); Twater.position.set(0, -1.5, 0); //水体位置x Twater.rotation.x = -Math.PI / 2; // Twater.rotateZ(-110); Scene.add(Twater); animate(); }; const animate = () => { // 配置动画 requestAnimationFrame(animate); Twater.material.uniforms['time'].value += 1.0 / 100.0; }; onBeforeMount(() => { // initGltfFloor(); // initModal('ChangJing', '/Gltf/ChuWangCheng.gltf'); // initModal('ChangJing', '/Gltf/qiaoliang.glb'); // initModal('daqiao', 'https://newfiber-cloud-1255570142.cos.ap-chengdu.myqcloud.com/yanan/daqiao.glb'); initModal('daqiao', 'https://newfiber-cloud-1255570142.cos.ap-chengdu.myqcloud.com/yanan/daqiao.gltf'); // 共同弹窗触发事件 bus.on('WJP_BIM_flyTo', params => { // 打开弹窗 flyTo(params); }); }); onMounted(() => { getPonintInfo(); nextTick(() => { if (document.readyState === 'complete') { createLable(); // createWaterLevel(); } }); initRenderer(); initLabelRenderer(); initCamera(); initControls(); initLight(); initSky(); createLable(); Render(); // 添加点击事件 //监听点击事件 // window.addEventListener("click", BIMClick, false); }); onBeforeUnmount(() => { cancelAnimationFrame(animationFrameId.value); bus.off('WJP_BIM_flyTo'); // window.removeEventListener("click", BIMClick); // document.removeEventListener('click', meshOnClick); // window.removeEventListener("resize", onWindowResize, false); Scene.traverse(e => { if (e.BufferGeometry) e.BufferGeometry.dispose(); if (e.material) { if (Array.isArray(e.material)) { e.material.forEach(m => { m.dispose(); }); } else { e.material.dispose(); } } if (e.isMesh) { e.remove(); } }); Scene.remove(); Renderer.value.dispose(); Renderer.value.content = null; clearInterval(timer.value); // 清理渲染器和相机 Camera.value = null; }); </script> <style lang="scss" scoped> #WangJiaPIng { width: 100%; height: 100%; position: absolute; // background: url("@/assets/images/Sponge_screen/QiaoLiang/BIM.png") no-repeat center center; .YuanDian { z-index: 9999 !important; position: relative; // min-height: 20px; .iconBox { position: relative; // border: 1px solid red; } .iconImg { position: absolute; left: 50%; // transform: translateX(-50%); bottom: 5px; width: 19px; height: 12px; } .ydBox { // width: 134px; // height: 70px; position: absolute; left: calc(50% + 19px); bottom: 8px; width: 60px; // height: 26px; background: linear-gradient(0deg, rgba(12, 54, 92, 0.6) 0%, rgba(12, 54, 92, 0.6) 100%); border-radius: 2px; border: 1px solid #04d8ff; .iconClose { width: 5px; height: 5px; position: absolute; right: 1px; top: 0; cursor: pointer; } .ydData { display: flex; align-items: center; justify-content: space-between; padding: 0 3px; font-size: 3px; height: 7px; line-height: 7px; color: #04d8ff; // .leftData { // } // .rightData { // } } } .YuanDianText { // padding-bottom: 10px; height: 7px; line-height: 7px; color: #fff; text-shadow: 0px 1px 3px rgba(0, 0, 0, 0.9); white-space: nowrap; word-break: keep-all; // cursor: pointer; pointer-events: none; font-size: 3px; text-align: center; background: linear-gradient(90deg, rgba(4, 216, 255, 0.8) 0%, rgba(4, 216, 255, 0.2) 100%); } .YuanDianIcon { width: 10px; height: 10px; position: absolute; left: 50%; transform: translateX(-50%); bottom: 0; pointer-events: auto; cursor: pointer; } } } </style>