用three.js+echarts给公司写了一个站点数据大屏系统经验总结

发布于:2024-05-08 ⋅ 阅读:(133) ⋅ 点赞:(0)

时间过的好快,参加公司的新项目研发快一年了,五一机器人项目首秀,我们遇到了高并发集中下单情景,然后海量数据处理场景来了,给我在后端领域的高并发实践业务上画上了漂亮的一笔经验。人都是在磨练中成长,我很感谢这次给我的机会,虽然有点累,但也有点小成就。正好现在有空,我先聊聊首秀后给领导们做的大屏数据展示吧,领导等着看漂亮数据呢!

大屏重点是贼啦炫酷的动态特效加持

业务核心运营场景:各大地上地下停车场

这里我用的是three.js去实现的实际业务场景的场站模拟三维图,废话不多说,直接上图吧!先说一下,这里截图是看起来像2维,但实际是3维的,可以滑动翻转地图的。

截屏2024-05-07 18.35.07.png

如图所示,这是p4停车场的全景图,整个停车场的鸟瞰图一览无余,可以滚动鼠标放大看====》

这是我用three.js渲染的每个停车位的车位标记,这里记录了这个车场的每个车位的坐标点,方便后期,观察我们投放的100台机器人智能驾驶实时模拟。听起来是不是很牛逼plus?我告诉你,事情没那么简单!由于数据太多,既要有3D-map,也要有实时动态数据滚动展示和各种echarts图表,比如:

4A71FC88-CCB1-4DD4-82B9-612DE98328B8.png ps:不好意思,本人还要严格遵守劳动合同执行公司数据保密,相关数据已经打码。

然后一堆实时数据数据和图上来后,不出意外的意外来了,性能出现了问题,我遇到了内存泄漏的情况。这还怎么继续二期的机器人动态运行场景研发呢?别慌,一步一步排查代码。

1.先介绍下大屏顶部的总数统计动态数字翻牌器

这里我用了第三方插件:动态数字翻牌器vue-count-to,只要有数据变化,就会实时看到动态增长的效果

首先要在框架中安装npm install vue-count-to,并在项目入口文件中引入

import CountTo from 'vue-count-to';
Vue.use(CountTo)
new Vue({
  el: '#app',
  router,
  store,
  render: h => h(App)
})

实际业务开发模块中代码:

                <count-to
                    class="count-to"
                    :startVal="0" //开始数值
                    :endVal="687573.74" //结束数值
                    :duration="500000" //动态变化的时间设定
                    :decimals="1"  //每次动态增长的数量
                ></count-to>

2.然后在大屏的左侧,我写了一个实时从下到上无限滚动的动态订单列表,可以让领导看到最新的订单情况。


<template>
    <div class="scrolling-list" :style="{ transform: `translate(0px,-${scrollTop}px)` }">
      <ul ref="scrollItemBox">
        <li v-for="(item, index) in items" :key="index" class="item-li">
            <div><span class="name">订单编号:</span><span class="content"><i class="el-icon-receiving">  {{ item.order_id }}</i></span></div>
            <div><span class="name">订单金额:</span><span class="content"><i class="el-icon-s-finance" style="color:#1989fa" >  {{ item.pre_total_amount }}</i></span><span class="name">手机号:</span><span class="content"><i class="el-icon-mobile-phone" style="color:#1989fa">  {{ item.phone }}</i></span></div>
            <div><span class="name">车牌号:</span><span class="content"> <i class="el-icon-truck" style="color:#1989fa">  {{ item.vehicle_no }}</i></span><span class="name">车位号:</span><span class="content"><i class="el-icon-map-location" style="color:#1989fa">  {{ item.target_slot_no }}</i></span></div>
            <div><span class="name">订单来源:</span><span class="content">{{ item.order_from == 1 ? '小程序' : 'APP' }}</span><span class="name">下单时间:</span><span class="content"><i class="el-icon-time">  {{ item.created_at }}</i></span></div>
        </li>
      </ul>
      <div v-html="copyHtml"></div>
    </div>
</template>
<script>
  
  export default {
    
  data() {
      return {
        name: "InfiniteScroll",
        scrollTop: 0, //列表滚动高度
        speed: 15, //滚动的速度
        copyHtml: '',
        items:[],
        intervalId: null
      };
    },
  mounted() {
    this.initScroll()
  },
  beforeDestroy() {
    // 清除定时任务
      clearInterval(this.intervalId);
    },
  methods: {
     initScroll() {
            this.$nextTick(() => {
                this.copyHtml = this.$refs.scrollItemBox.innerHTML
                this.startScroll()
            })
      },
      // 开始滚动
      startScroll() {
          setInterval(this.scroll, this.speed);
      },
      // 滚动处理方法
      scroll() {
            this.scrollTop++
            // 获取需要滚动的盒子的高度
            let scrollItemBox = this.$refs.scrollItemBox?.offsetHeight || 1000
            // 当判断滚动的高度大于等于盒子高度时,从头开始滚动
            if (this.scrollTop >= scrollItemBox) {
                this.scrollTop = 0
            }
      }
    }
  };
  </script>

3.然后在大屏的右侧,用echarts写了两个饼图和折线图表,可以直观的的看到数据统计


export const timeStaticsOption = (xData,tipsArr) => {
  return {
    title: {
      text: '',
      subtext:'当天时间段充电订单数',
      subtextStyle:{
        color:'#fff',
      }
    },
    tooltip: {
      trigger: 'axis',
      axisPointer: {
        type: 'cross'
      }
    },
    xAxis: {
      type: 'category',
      data: xData,
      boundaryGap: false,
      axisLine: {
        show: false,
        lineStyle: {
          color: '#73B131',
          type: 'dashed'
        }
      },
    },
    yAxis: {
      type: 'value',
      axisPointer: {
        snap: true
      }
    },
    series: [
      {
        name: '时间段充电订单数',
        type: 'line',
        smooth: true,
        data: tipsArr,
      }
    ]
  }
}
export const botDataPieEcharts = (total,a,b)=>{
  console.log(total,a,b)
  return {
    title: {
      text: '',
      subtext: "Bot总数:"+ total+ '台',
      left: 'center',
      subtextStyle:{
        color:'#fff',
      }
    },
    tooltip: {
      trigger: 'item'
    },
    // legend: {
    //   orient: 'vertical',
    //   left: 'left'
    // },
    series: [
      {
        name: 'Bot数量',
        type: 'pie',
        radius: '50%',
        data: [
          { value: a, name: '在线:'+ a +'台' },
          { value: b, name: '空闲:'+ b +'台'},
        ],
        emphasis: {
          itemStyle: {
            shadowBlur: 10,
            shadowOffsetX: 0,
            shadowColor: 'rgba(0, 0, 0, 0.5)'
          }
        }
      }
    ]
  }
}

4.最后也是最耗性能的部分,用three.js写的停车场模拟实景鸟瞰图,3d-map

这里我直接把相关的方法和类,全部封装好,单独引入文件,用到以下文件,

import '../public/threejs/controls/OrbitControls.js'; 
import '../public/threejs/lines/LineSegmentsGeometry.js';
import '../public/threejs/lines/LineGeometry.js';
import '../public/threejs/lines/LineMaterial.js';
import '../public/threejs/lines/LineSegments2.js';
import '../public/threejs/lines/Line2.js';

在业务中相关代码写好工具方法,

import myWorker from './map.worker';
import FONT_JSON_DATA from './helvetiker_bold.typeface.json';
class basicThree {
    constructor(props) {
        this.from = props.from
        this.callbackSlotNo = props.callback
        console.log(props, 'props')
        this.LineGeometry
        // three 3要素
        this.renderer; //渲染器
        this.camera; //摄像头
        this.scene; //场景
        //光源
        this.ambientLight; //环境光
        this.pointLight; //平行光
        this.DirectionalLight
        //触屏开始时间
        this.touchTime = 0
        //摄像头控制
        this.controls;
        this.init()
        this.onmousedbclick = this.onMouseDblclick.bind(this);
        this.selectObject
        this.rawOption
        this.materialLine = Object()
        this.box = document.createElement("div")

        this.donX
        this.donY
        this.dataNumber
        this.originX; // 偏移量x坐标
        this.originZ; // 偏移量z坐标
        this.thinLine;
        this.wideLine;

        // 定义模型组
        this.initModalGroup();

        this.mapParams;

        this.drawModalFunc = {
            '0': {
                'func': this.RoadLineSigns.bind(this),
                'group': 'RoadLineSigns_group',
                'z': 0
            },
            '1': {
                'func': this.RoadLineSigns.bind(this),
                'group': 'RoadLineSigns_group',
                'z': 0
            },
            '2': {
                'func': this.RoadLineSigns.bind(this),
                'group': 'RoadLineSigns_group',
                'z': 2
            },
            '3': {
                'func': this.initNoAndArea.bind(this),
                'group': null,
                'z': 3
            },
            '4': {
                'func': this.basicWall.bind(this),
                'group': 'barrier_group',
                'z': 3
            },
            '5': {
                'func': this.initSlotLine.bind(this),
                'group': 'initSlotLine_group',
                'z': 3
            },
            '6': {
                'func': this.RoadLineSigns.bind(this),
                'group': 'RoadLineSigns_group',
                'z': 3
            },
            '7': {
                'func': this.RoadLineSigns.bind(this),
                'group': 'RoadLineSigns_group',
                'z': 3
            },
            '8': {
                'func': this.RoadLineSigns.bind(this),
                'group': 'RoadLineSigns_group',
                'z': 3
            },
            '9': {
                'func': this.initSlotLine.bind(this),
                'group': 'initSlotLine_group',
                'z': 3
            },
            '10': {
                'func': this.initSlotLine.bind(this),
                'group': 'initSlotLine_group',
                'z': 3
            },
            '11': {
                'func': this.RoadLineSigns.bind(this),
                'group': 'RoadLineSigns_group',
                'z': 3
            },
            '13': {
                'func': this.initSlotLine.bind(this),
                'group': 'initSlotLine_group',
                'z': 3
            },
            '14': {
                'func': this.RoadLineSigns.bind(this),
                'group': 'RoadLineSigns_group',
                'z': 3
            },
            '16': {
                'func': this.RoadLineSigns.bind(this),
                'group': 'RoadLineSigns_group',
                'z': 3
            },
            '1001': {
                'func': this.initSlotLine.bind(this),
                'group': 'initSlotLine_group',
                'z': 4
            }
        }
        //字体loader
        this.FontLoader;
        this.getFontLoaderInit()
    }

    init() {
        this.initScene()
        this.initCamera()
        this.initRender()
        this.orbitHelper() //摄像头辅助
        this.animate()
        window.onresize = this.onWindowResize.bind(this);
    }
    initScene() { //场景
        this.scene = new THREE.Scene();
    }
    // // 三维坐标系辅助
    axesHelper() {
        this.scene.add(new THREE.AxesHelper(200))
    }
    initCamera() { //摄像头
        var width = window.innerWidth; //窗口宽度
        var height = window.innerHeight; //窗口高度
        var k = width / height; //窗口宽高比
        var s = 30; //三维场景显示范围控制系数,系数越大,显示的范围越大
        var camera = new THREE.OrthographicCamera(-s * k, s * k, s, -s, 1, 1000);
        camera.position.set(0, 200, 0); //设置相机位置
        camera.lookAt(this.scene.position); //设置相机方向(指向的场景对象)
        this.camera = camera;
    }
    initRender() { //渲染器
        let renderer = new THREE.WebGLRenderer({
            antialias: true
        });
        let container = document.getElementById('room');
        renderer.setSize(container.offsetWidth, container.offsetHeight);
        container.appendChild(renderer.domElement);
        //告诉渲染器需要阴影效果
        // renderer.shadowMap.enabled = true;
        // renderer.shadowMap.type = THREE.PCFSoftShadowMap;
        renderer.setClearColor(new THREE.Color('#212129'));
        this.renderer = renderer;
    }
    initModalGroup() {
        this.slot_no_group = null; // 车位号组
        this.slot_area_group = null; // 车位方框组
        this.barrier_group = null; // barrier组
        this.initSlotLine_group = null;
        this.RoadLineSigns_group = null;
    }
    initData(option, index, mapParams, callback, originX, originZ) {//初始化渲染
        this.slot_no_group = new THREE.Group(); // 车位号组
        this.slot_area_group = new THREE.Group(); // 车位方框组
        this.barrier_group = new THREE.Group(); // barrier组
        this.initSlotLine_group = new THREE.Group();
        this.RoadLineSigns_group = new THREE.Group();

        this.originX = originX;
        this.originZ = originZ;
        var data = option['entities_groups'];
        this.rawOption = option;
        this.mapParams = mapParams;
        console.log(data);
        // initSlotLine-画线,RoadLineSigns-画区域,initSlotNumber-车位号,slotAreaRegion-画车位区域(专用),initSlotArea-画块,basicWall-画块(墙体)
        let dataType = null
        for (let i = 0; i < data.length; i++) {
            if (data[i]['entities']) {
                dataType = data[i]['entities'][0]['type'] + '';
                if (this.drawModalFunc[dataType]) {
                    this.drawModalFunc[dataType]['func'](data[i]['entities'], this[this.drawModalFunc[dataType]['group']], this.drawModalFunc[dataType]['z'], true, mapParams['bg'][data[i]['name']])
                }
            }
        }
        if (callback) callback()
    }
    // 清空模型
    clearModal() {
        if (this.scene && this.scene.children) {
            this.initModalGroup();
            if (this.wideLine && this.wideLine.geometry) {
                this.wideLine.geometry.dispose();
                this.wideLine.material.dispose();
                this.scene.remove(this.wideLine);
            }
            if (this.thinLine && this.thinLine.geometry) {
                this.thinLine.geometry.dispose();
                this.thinLine.material.dispose();
                this.scene.remove(this.thinLine);
            }
            let xlType = this.scene.children.filter(function (obj) {
                return obj.type == 'Group'
            })
            for (let i = 0; i < xlType.length; i++) {
                for (let j = 0; j < xlType[i]['children'].length; j++) {
                    if (xlType[i]['children'][j]['type'] == 'Mesh') {
                        xlType[i]['children'][j].geometry.dispose();
                        xlType[i]['children'][j].material.dispose();
                    }
                }
                this.scene.remove(xlType[i]);
            }
        }
    }
    // 清除 type 类型的 obj
    clearSlot(type) {
        if (this.scene && this.scene.children) {
            this.scene.children.forEach(obj => {
                if (obj.userData && obj.userData.slot == type) {
                    this.scene.remove(obj);
                }
                if (obj.children.length > 0) {
                    obj.children.forEach(item => {
                        if (item.userData && item.userData.slot == type) {
                            this.scene.remove(obj);
                        }
                    })
                }
            })
        }
    }
    /**
     * 清除模型,模型中有 group 和 scene,需要进行判断
     * @param scene
     * @returns
     */
    clearScene() {
        // 从scene中删除模型并释放内存
        if (this.scene.length > 0) {
            for (var i = 0; i < this.scene.length; i++) {
                var currObj = this.scene[i];

                // 判断类型
                if (currObj instanceof THREE.Scene) {
                    var children = currObj.children;
                    for (var i = 0; i < children.length; i++) {
                        deleteGroup(children[i]);
                    }
                } else {
                    deleteGroup(currObj);
                }
                this.scene.remove(currObj);
            }
        }
        console.log(this.scene, '清空后的 scene')
    }

    // 删除group,释放内存
    deleteGroup(group) {
        //console.log(group);
        if (!group) return;
        // 删除掉所有的模型组内的mesh
        group.traverse(function (item) {
            if (item instanceof THREE.Mesh) {
                item.geometry.dispose(); // 删除几何体
                item.material.dispose(); // 删除材质
            }
        });
    }
    initMap(option, index, mapParams, callback, originX, originZ) {
        this.clearScene()
        this.initData(option, index, mapParams, callback, originX, originZ)
    }
    initLight() {
        var light = new THREE.DirectionalLight("#ffffff", 0.28, 100);
        light.position.set(0, 1, 1200); //default; light shining from top
        light.castShadow = true; // default false
        this.scene.add(light);
        //环境光
        // var ambient = new THREE.AmbientLight("#ffffff");
        // this.scene.add(ambient);

        // 点光源
        var point = new THREE.PointLight(0xffffff);
        point.position.set(0, 200, 0); //点光源位置
        this.scene.add(point); //点光源添加到场景中
        //环境光
        var ambient = new THREE.AmbientLight(0x888888);
        this.scene.add(ambient);
    }
    initModel() {
        this.initLight()
        // this.axisHelper()
        // 确认页面需要点选车位
        const _self  = this
        if (_self.from === 'confirm') {
            document.getElementById('room').addEventListener('touchstart', function(){
                _self.touchTime = Date.now()
            }, false);
            document.getElementById('room').addEventListener('touchend', _self.onmousedbclick, false);
        }
    }
    orbitHelper() { //创建摄像头辅助
        let controls = new THREE.OrbitControls(this.camera, this.renderer.domElement);
        controls.target = new THREE.Vector3(0, 0, 0);//控制焦点
        controls.autoRotate = false;//将自动旋转关闭
        // controls.enableRotate = false;
        // controls.minPolarAngle = -Math.PI/2;
        // controls.maxPolarAngle = Math.PI/2;
        // controls.minAzimuthAngle = 0;
        // controls.maxAzimuthAngle = 0;
        // controls.maxZoom = 20;
        // controls.minZoom = 1;
        new THREE.Clock();
        this.controls = controls;
    }
    gridHelper() { // 创建网格辅助
        var gridHelper = new THREE.GridHelper(20, 20, 0x404040, 0x404040);
        this.scene.add(gridHelper);
    }
    statsHelper() { //性能插件
        this.stats = new Stats();
        document.body.appendChild(this.stats.dom);
    }
    axisHelper() { //坐标轴辅助
        var helper = new THREE.AxisHelper(3000);
        this.scene.add(helper);
    }
    onWindowResize() { //自适应
        var parent = document.getElementById("room");
        this.camera.aspect = parent.clientWidth / parent.clientHeight;
        this.camera.updateProjectionMatrix();
        this.renderer.setSize(parent.clientWidth, parent.clientHeight);
    }
    render() { //渲染
        if (this.stats) this.stats.update();
        this.renderer.render(this.scene, this.camera);
    }
    animate() {
        this.render()
        requestAnimationFrame(this.animate.bind(this));
        // if (this.selectObject != undefined && this.selectObject != null) {
        //     this.renderDiv(this.selectObject);
        // }
    }
    basicWall(dataList, floor, y, ground_show, color) { //渲染墙体
        console.log(dataList, floor, y, ground_show, color);
        var str
        for (var i = 0; i < dataList.length; i++) {
            var arryShape = [];
            str = dataList[i].path;
            for (var j = 0; j < str.length; j++) {
                arryShape.push(new THREE.Vector3(str[j].x, str[j].y, str[j].z));
            }
            var californiaShape = new THREE.Shape(arryShape);
            var extrudeSettings = {
                steps: 1,
                amount: 2,
                bevelEnabled: false,
            };
            var geometry = new THREE.ExtrudeBufferGeometry(californiaShape, extrudeSettings);
            var material = new THREE.MeshPhongMaterial({
                color: color,
                depthTest: true
            }); //材质对象
            var mesh = new THREE.Mesh(geometry, material); //网格模型对象
            mesh.position.y = 5;
            mesh.position.x = this.originX;
            mesh.position.z = this.originZ;
            mesh.rotateX(-Math.PI / 2);
            // mesh.rotateZ(Math.PI / 4);
            floor.add(mesh);
        }
        floor.visible = ground_show;
        this.scene.add(floor);
    }
    initNoAndArea(dataList, floor, y, ground_show, color) {
        const self = this
        self.slotAreaRegion(dataList, self.slot_area_group, y, ground_show, color);
        self.initSlotNumber(dataList, self.slot_no_group, y, ground_show, self.mapParams['bg'].slot_number);
    }
    initSlotArea(data, floor, y, ground_show, color) { //渲染车位方块
        if (!data) return
        let dataList = data.filter((item) => {
            return item.slot_name;
        })
        let mesh;
        for (let i = 0; i < dataList.length; i++) {
            let arryShape = []
            let str = dataList[i].path;
            for (let j = 0; j < str.length; j++) {
                arryShape.push(new THREE.Vector3(str[j].x, str[j].y, 0))
            }
            let californiaShape = new THREE.Shape(arryShape);
            let extrudeSettings = {
                steps: 1,
                depth: 0.1,
                amount: 0.1,
                bevelEnabled: false,
            };
            let geometry = new THREE.ExtrudeBufferGeometry(californiaShape, extrudeSettings)
            let material = new THREE.MeshPhongMaterial({
                color: color,
                depthTest: true
            }); //材质对象
            mesh = new THREE.Mesh(geometry, material); //网格模型对象
            mesh.name = dataList[i].slot_name;
            const path = []
            dataList[i].path.filter(item => { path.push(item.x); path.push(item.y); path.push(8); })
            mesh.userData = {
                "slot": 'slot',
                "path": path
            }
            floor.add(mesh);
        }

        floor.rotateX(-Math.PI / 2);
        floor.position.x = this.originX;
        floor.position.z = this.originZ;
        floor.position.y = y;
        floor.visible = ground_show;
        floor.name = 'chewei';
        this.scene.add(floor);
    }
    initSlotLine(dataList, floor, y, ground_show, color) { //渲染车位线
        if (!dataList) return
        for (var i = 0; i < dataList.length; i++) {
            var str = dataList[i].path;
            var geometry = new THREE.Geometry();
            for (var j = 0; j < str.length; j++) {
                geometry.vertices.push(new THREE.Vector3(str[j].x, str[j].y, str[j].z));
            }
            var material = new THREE.LineBasicMaterial({
                color: color
            });
            var line = new THREE.Line(geometry, material);
            line.position.y = y;
            line.position.x = this.originX;
            line.position.z = this.originZ;
            line.rotateX(-Math.PI / 2);
            floor.add(line);
        }
        floor.visible = ground_show;
        this.scene.add(floor)
    }
    getFontLoaderInit (){
        this.FontLoader = new THREE.Font(FONT_JSON_DATA);
    }
    initSlotNumber(dataList, floor, y, ground_show, color) { //渲染车位号
        if (!dataList) return
        let _this = this;
        console.log('dataList.length,floor', dataList.length,floor) 
        for (let i = 0; i < dataList.length; i++) {
            const get_centroid = _this.get_centroid(dataList[i].path)
            // 生成二维字体模型
            var shapes = _this.FontLoader.generateShapes(dataList[i].name, 0.8, 1);
            var fontGeometry = new THREE.ShapeGeometry(shapes);
            // // 材质
            var fontMaterial = new THREE.MeshLambertMaterial({
                color: color,
                side: THREE.DoubleSide
            });

            // // 绑定盒子模型
            fontGeometry.computeBoundingBox();
            fontGeometry.center()
            let FONT = new THREE.Mesh(fontGeometry, fontMaterial);
            // // x = 0,位置
            FONT.position.x = get_centroid[0] + _this.originX;
            FONT.position.y = get_centroid[1] - _this.originZ;
            FONT.position.z = 4;
            FONT.rotateZ(_this.angle(dataList[i].path));
            floor.add(FONT)
        }        
        floor.visible = ground_show;
        floor.rotateX(-Math.PI / 2);
        _this.scene.add(floor);
    }
    get_centroid(cluster) {
        let x = 0;
        let y = 0;
        // let coord_num = cluster.length;
        let coord_num = 4;
        let lat = 0;
        let lon = 0;
        // for (let i = 0; i < cluster.length; i++) {
        for (let i = 0; i < 4; i++) {
            lat = cluster[i]['x'];
            lon = cluster[i]['y'];
            x += lat;
            y += lon;
        }
        x /= coord_num
        y /= coord_num
        let m = [x, y]
        return m
    }
    RoadLineSigns(dataList, floor, y, ground_show, color) {
        if (!dataList) return
        var str;
        for (var i = 0; i < dataList.length; i++) {
            var arryShape = []
            str = dataList[i].path;
            for (var j = 0; j < str.length; j++) {
                arryShape.push(new THREE.Vector3(str[j].x, str[j].y, str[j].z))
            }
            var californiaShape = new THREE.Shape(arryShape);
            var geometry = new THREE.ShapeBufferGeometry(californiaShape)
            var material = new THREE.MeshBasicMaterial({
                color: color
            }); //材质对象
            var mesh = new THREE.Mesh(geometry, material); //网格模型对象
            mesh.rotateX(-Math.PI / 2);
            mesh.position.y = y;
            mesh.position.x = this.originX;
            mesh.position.z = this.originZ;
            floor.add(mesh)
        }
        floor.visible = ground_show
        this.scene.add(floor);
    }
    slotAreaRegion(dataList, floor, y, ground_show, color) {
        if (!dataList) return;
        var str;
        var mesh;
        var material = new THREE.MeshBasicMaterial({
            color: color
        }); //材质对象
        for (var i = 0; i < dataList.length; i++) {
            var arryShape = []
            str = dataList[i].path;
            for (var j = 0; j < str.length; j++) {
                arryShape.push(new THREE.Vector3(str[j].x, str[j].y, str[j].z))
            }
            var californiaShape = new THREE.Shape(arryShape);
            var geometry = new THREE.ShapeBufferGeometry(californiaShape)
            mesh = new THREE.Mesh(geometry, material); //网格模型对象
            mesh.name = dataList[i].name;
            const path = []
            dataList[i].path.filter(item => { path.push(item.x); path.push(item.y); path.push(8); })
            mesh.userData = {
                "slot": 'slot',
                "path": path
            }
            floor.visible = ground_show
            floor.add(mesh);
        }
        floor.rotateX(-Math.PI / 2);
        floor.position.y = y;
        floor.position.x = this.originX;
        floor.position.z = this.originZ;
        this.scene.add(floor);
    }
    // 计算多个坐标点的中心点(地理坐标系--平面直角坐标系)(用于车位号在车位方块内居中显示)
    get_centroid(cluster) {
        let x = 0;
        let y = 0;
        // let coord_num = cluster.length;
        let coord_num = 4;
        let lat = 0;
        let lon = 0;
        // for (let i = 0; i < cluster.length; i++) {
        for (let i = 0; i < 4; i++) {
            lat = cluster[i]['x'];
            lon = cluster[i]['y'];
            x += lat;
            y += lon;
        }
        x /= coord_num
        y /= coord_num
        let m = [x, y]
        return m
    }
    // 计算两点距离
    distance(p1, p2) {
        let dx = Math.abs(p2.x - p1.x);
        let dy = Math.abs(p2.y - p1.y);
        let dis = Math.sqrt(Math.pow(dx, 2) + Math.pow(dy, 2));
        return dis;
    }
    // 确定两点与x轴夹角(选取下标为0、1和1、2两点作比较,选出车位方块长边,根据长边与x轴夹角计算出车位方块相对于x轴的旋转角度,用于设置车位号的旋转,使车位号与车位方块长边保持平行)
    angle(point) {
        if (this.distance(point[0], point[1]) > this.distance(point[1], point[2])) {
            let a = point[0];
            let b = point[1];
            if (a.x > b.x) {
                var radian = Math.atan2(a.y - b.y, a.x - b.x); // 返回来的是弧度
            } else {
                var radian = Math.atan2(b.y - a.y, b.x - a.x); // 返回来的是弧度
            }
            // var angle = 180 / Math.PI * radian; // 根据弧度计算角度
            return radian;
        } else {
            let a = point[1];
            let b = point[2];
            if (a.x > b.x) {
                var radian = Math.atan2(a.y - b.y, a.x - b.x); // 返回来的是弧度
            } else {
                var radian = Math.atan2(b.y - a.y, b.x - a.x); // 返回来的是弧度
            }
            // var angle = 180 / Math.PI * radian; // 根据弧度计算角度
            return radian;
        }
    }
    // 画细线
    drawThinLine(positions) {
        if (this.thinLine && this.thinLine.geometry) {
            this.thinLine.geometry.dispose();
            this.thinLine.material.dispose();
            this.scene.remove(this.thinLine);
        }
        if (!positions) return
        console.log(positions)
        var geometry = new THREE.LineGeometry();
        geometry.setPositions(positions);
        const material = new THREE.LineMaterial({
            color: '#14f408',
            linewidth: 1, // in pixels
            dashed: false
        });
        material.resolution.set(window.innerWidth, window.innerHeight)
        // line
        this.thinLine = new THREE.Line2(geometry, material);
        this.thinLine.rotateX(-Math.PI / 2);
        // this.thinLine.position.x = 300;

        this.thinLine.position.y = 5;
        this.thinLine.position.x = this.originX;
        this.thinLine.position.z = this.originZ;
        this.scene.add(this.thinLine);
    }
    // 画粗线
    drawWideLine(positions) {
        if (this.wideLine && this.wideLine.geometry) {
            this.wideLine.geometry.dispose();
            this.wideLine.material.dispose();
            this.scene.remove(this.wideLine);
        }
        var geometry = new THREE.LineGeometry();
        geometry.setPositions(positions);
        const material = new THREE.LineMaterial({
            color: '#14f408',
            linewidth: 2, // in pixels
            dashed: false
        });
        material.resolution.set(window.innerWidth, window.innerHeight)
        // line
        this.wideLine = new THREE.Line2(geometry, material);
        this.wideLine.rotateX(-Math.PI / 2);
        // this.wideLine.position.x = 300;

        this.wideLine.position.x = this.originX;
        this.wideLine.position.z = this.originZ;
        this.scene.add(this.wideLine);
    }
    renderDiv(dataNumber, object) { //浮动窗口
        console.log('renderDiv',dataNumber, object)
        this.box.id = 'label'
        if (dataNumber === 0) {
            this.box.remove()
            this.callbackSlotNo({ slot_no: '', floor_id: '' })
            return false
        } else {
            document.getElementById('room').appendChild(this.box)
        }
        // 获取窗口的一半高度和宽度
        var halfWidth = window.innerWidth / 2;
        var halfHeight = window.innerHeight / 2;

        // 逆转相机求出二维坐标
        // var vector = object.position.clone().project(this.camera);

        // 修改 div 的位置
        // this.box.style.display = 'block'
        // box.style.left = (vector.x * halfWidth + halfWidth) + 'px'
        // box.style.top = (-vector.y * halfHeight + halfHeight) + 'px'
        this.box.style.left = (this.donX + 15) + 'px'
        this.box.style.top = (this.donY + 15) + 'px'
        // this.box.innerHTML = "车位号:" + object.name
        if (object && object.name) {
            this.box.innerHTML = "车位号:" + object.name
            this.callbackSlotNo({ slot_no: object.name, floor_id: this.rawOption.floor_id })
        } else {
            this.box.innerHTML = '请勾选车位号'
            this.callbackSlotNo({ slot_no: '', floor_id: ''})
        }
    }
    renderPOI(slot_position) { //浮动窗口
        console.log('renderPOI',slot_position)
        this.box.id = 'label'
        document.getElementById('room').appendChild(this.box)
        this.box.style.left = (this.donX + 15) + 'px'
        this.box.style.top = (this.donY + 15) + 'px'
       
        this.box.innerHTML = '已选择'
        this.callbackSlotNo({ slot_no: '', floor_id: this.rawOption.floor_id,slot_position: slot_position})
    }
    //车位点击事件 大头针标记出所选的车位
    onMouseDblclick(event) {
        console.log(event);
        if((Date.now() - this.touchTime) > 300){
            return
        }
        // 获取 raycaster 和所有模型相交的数组,其中的元素按照距离排序,越近的越靠前
        var intersects = this.getIntersects(event);
        console.log('intersects',intersects);
        // 先清除大头针lock(原需求是点击车位号加大头针,所以点击时需先清除大头针再重绘)
        this.clearSlot('lock')
        // 清除细线 车位边框
        this.drawThinLine()
        // 获取选中最近的 Mesh 对象
        if (intersects.length != 0 && intersects[0].object instanceof THREE.Mesh) {
            // 判断和上一次点击的车位相同,则清除selectObject,取消背景颜色等状态
            if (this.selectObject == intersects[0].object) {
                this.dataNumber = 0
                this.selectObject = intersects[0].object;
            } else {
                this.selectObject = intersects[0].object;
                this.dataNumber = 1
                // 根据位置标记大头针
                // const point = intersects[0].point
                // this.initLock({x:point.x - this.originX, y:-point.z+this.originZ, z:point.y}, 'lock')
            }
            console.log('this.dataNumber, this.selectObject',this.dataNumber, this.selectObject,)
            this.renderDiv(this.dataNumber, this.selectObject)
            this.changeMaterial(this.selectObject);
            this.render()
        } else {
            this.selectObject=''
            const slot_p = this.getIntersectsPlane(event) 
            // const slot_position = {
            //     x: slot_p.x,
            //     y: slot_p.z,
            //     z: slot_p.y,
            // }
            const slot_position = {
                x: slot_p.x - this.originX,
                y: slot_p.z + this.originZ,
                z: 0,
            }
            console.log("未选中 Mesh!",slot_position);
            //先清除大头针后添加大头针标记位置
            this.initLock({x:slot_p.x - this.originX, y:-slot_p.z+this.originZ, z:slot_p.y}, 'lock')
            this.render()
            this.renderPOI(slot_position)
        }
    }
    /* 获取射线与平面相交的交点 */
    getIntersectsPlane(event) {
        var raycaster = new THREE.Raycaster();
        var mouse = new THREE.Vector2();
        mouse.x = (event.changedTouches[0].clientX / document.getElementById('room').clientWidth) * 2 - 1;
        mouse.y = -(event.changedTouches[0].clientY / document.getElementById('room').clientHeight) * 2 + 1;
        var normal = new THREE.Vector3(0, 1, 0);
        /* 创建平面 */
        var planeGround = new THREE.Plane(normal, 0);
        /* 从相机发出一条射线经过鼠标点击的位置 */
        raycaster.setFromCamera(mouse, this.camera);
        /* 获取射线 */
        var ray = raycaster.ray;
        /* 计算相机到射线的对象,可能有多个对象,返回一个数组,按照距离相机远近排列 */
        var intersects = ray.intersectPlane(planeGround);
        /* 返回向量 */
        return intersects;
    }
    changeMaterial(object) { //更换材质
        console.log(object, 888888)
        if (this.dataNumber == 0) {
            var material = new THREE.MeshBasicMaterial({
                color: "#212129",
            });
            this.selectObject = ''
        } else {
            var material = new THREE.MeshBasicMaterial({
                color: "#161621",// "#14f408",
            });
            if (object.userData && object.userData.path) {
                this.drawThinLine(object.userData.path)
            }
        }
        object.material = material;
    }
    locateSlot(slot_no) {
        console.log('即将高亮车位号:' + slot_no);
        this.drawThinLine();
        this.scene.traverse(obj => {
            if (obj.type == "Mesh" && obj.name) {
                if (slot_no && obj.name == slot_no) {
                    obj.material = new THREE.MeshBasicMaterial({
                        color: '#161621',
                        side: THREE.DoubleSide
                    });
                    this.drawThinLine(obj.userData.path)
                } else {
                    obj.material = new THREE.MeshBasicMaterial({
                        color: '#212129',
                        side: THREE.DoubleSide
                    });
                    
                }
            }
        })
    }
    initLock(dataList, imgName) { //显示车位大头针
        this.createLockTextureMesh(dataList, new THREE.Group(), imgName)
    }
    initCar(dataList, imgName, offset_x, offset_y) { //显示车辆
        this.clearSlot(imgName)
        this.createTextureMesh(dataList, new THREE.Group(), imgName)
    }
    createLockTextureMesh(dataList, floor, imgName) {
        // var zoom = parseFloat(loaclInfo.map_info.zoom)
        var geometry = new THREE.PlaneGeometry(2, 2, 1, 1);
        //几何体创建纹理坐标 
        console.log(dataList, 'local_map_id一致')
        geometry.vertices[0].uv = new THREE.Vector2(0, 0);
        geometry.vertices[1].uv = new THREE.Vector2(1, 0);
        geometry.vertices[2].uv = new THREE.Vector2(1, 1);
        geometry.vertices[3].uv = new THREE.Vector2(0, 1);
        var texture = THREE.ImageUtils.loadTexture(
             "../images/" + imgName + ".png",
            
            null, function (t) { }
        );
        var material = new THREE.MeshBasicMaterial({
            map: texture,
            transparent: true,
            side: THREE.DoubleSide
        });
        var mesh = new THREE.Mesh(geometry, material);
        mesh.position.set(dataList.x + this.originX, 15, -dataList.y + this.originZ)
        // mesh.position.z = 2
        mesh.rotateX(-Math.PI / 2)
        mesh.userData = {
            "slot": imgName,
        }
        console.log(mesh, 'lock')
        floor.add(mesh)
        this.scene.add(floor);
    }
    createTextureMesh(dataList, floor, imgName, offset_x, offset_y) {
        // var zoom = parseFloat(loaclInfo.map_info.zoom)
        var geometry = new THREE.PlaneGeometry(2, 2, 1, 1);
        if ((dataList.local_map_id && this.rawOption.floor_id == dataList.local_map_id) || !dataList.local_map_id) {
            //几何体创建纹理坐标 
            geometry.vertices[0].uv = new THREE.Vector2(0, 0);
            geometry.vertices[1].uv = new THREE.Vector2(1, 0);
            geometry.vertices[2].uv = new THREE.Vector2(1, 1);
            geometry.vertices[3].uv = new THREE.Vector2(0, 1);
            var texture = THREE.ImageUtils.loadTexture(
                "../../images/" + imgName + ".png",
                null, function (t) { }
            );
            var material = new THREE.MeshBasicMaterial({
                map: texture,
                transparent: true,
                side: THREE.DoubleSide
            });
            var mesh = new THREE.Mesh(geometry, material);
            console.log(dataList, 1111)
            mesh.position.set(dataList.x + this.originX, 15, -dataList.y + this.originZ)
            mesh.rotateX(-Math.PI / 2)
            if (dataList.angle) {
                let piVal = (dataList.angle - 90) * (Math.PI / 180);
                mesh.rotateZ(piVal);
            }
            mesh.userData = {
                "slot": imgName
            }
            console.log(mesh)
            floor.add(mesh)
            this.scene.add(floor);

            // } else {
            //     return false
        }
    }
    getIntersects(event) { // 获取鼠标点击的物体,如果要获取鼠标点击的平面(非物体)的坐标,见mapLink.js的getIntersects方法
        // 声明 raycaster 和 mouse 变量
        var raycaster = new THREE.Raycaster();
        var mouse = new THREE.Vector2();
        // 通过鼠标点击位置,计算出 raycaster 所需点的位置,以屏幕为中心点,范围 -1 到 1
        mouse.x = (event.changedTouches[0].clientX / document.getElementById('room').clientWidth) * 2 - 1;
        mouse.y = -(event.changedTouches[0].clientY / document.getElementById('room').clientHeight) * 2 + 1;
        //通过鼠标点击的位置(二维坐标)和当前相机的矩阵计算出射线位置
        raycaster.setFromCamera(mouse, this.camera);
        // 获取与射线相交的对象数组,其中的元素按照距离排序,越近的越靠前
        var children_ = []
        this.scene.traverse(obj => {
            if (obj.type == "Mesh" && obj.userData.slot == 'slot') {
                obj.material = new THREE.MeshBasicMaterial({
                    color: '#212129',
                });
                children_.push(obj)

            }
        })
        var intersects = raycaster.intersectObjects(children_);
        //返回选中的对象
        return intersects;

    }
    lineNotClosed(dataList, floor, z, ground_show, color) { //渲染线 不闭合类
        if (!dataList) return
        // 5车道分道线 6减速带 7路标
        for (var i = 0; i < dataList.length; i++) {
            var str
            if (dataList[i].data_type == 1001) {
                str = JSON.parse(dataList[i].points)
                var geometry = new THREE.Geometry();
                for (var j = 0; j < str.length; j++) {
                    geometry.vertices.push(new THREE.Vector3(str[j].x, str[j].y, z));
                }
                var material = new THREE.LineBasicMaterial({
                    color: color,
                    linewidth: 35
                });
                var line = new THREE.Line(geometry, material);
                line.position.z = z
                floor.visible = ground_show
                floor.add(line)
                this.scene.add(floor)
            }
        }
    }
    start_car(dataList, loaclInfo, index, floor, z, ground_show) { // 创建导航轨迹 
        if (!dataList) return
        var vector = []
        var zoom = parseFloat(loaclInfo.map_info.zoom)
        var offset_x = parseFloat(loaclInfo.map_info.offset_x)
        var offset_y = parseFloat(loaclInfo.map_info.offset_y)
        for (var i = 0; i < dataList.length; i++) {
            if (loaclInfo.local_map_id[index] === dataList[i].mapId) {
                vector.push(new THREE.Vector3((parseFloat(dataList[i].xPos) + offset_x) * zoom, (parseFloat(dataList[i].yPos) + offset_y) * zoom, z))
            }

        }

        if (vector.length == '') {
            return false
        }
        // var curve = new THREE.CatmullRomCurve3([
        //     new THREE.Vector3(0, 0, 0),
        //     new THREE.Vector3(100, 500, 0),
        //     new THREE.Vector3(500, 500, 0),
        //     new THREE.Vector3(500, 800, 0)
        // ], false /*是否闭合*/ )
        var curve = new THREE.CatmullRomCurve3(vector, false)
        var progress = 0;
        var tubeGeometry = new THREE.TubeGeometry(curve, 64, 15, 100, false);
        var textureLoader = new THREE.TextureLoader();
        // var texture = textureLoader.load('../../../assets/libs/map/track.png');
        // 设置阵列模式为 RepeatWrapping
        texture.wrapS = THREE.RepeatWrapping
        texture.wrapT = THREE.RepeatWrapping
        // 设置x方向的偏移(沿着管道路径方向),y方向默认1
        //等价texture.repeat= new THREE.Vector2(20,1)
        texture.repeat.x = 20;
        var tubeMaterial = new THREE.MeshPhongMaterial({
            map: texture,
            transparent: true,
        });
        var tube = new THREE.Mesh(tubeGeometry, tubeMaterial);
        tube.position.z = z
        //scene.add(tube)
        floor.add(tube)
        floor.visible = ground_show
        this.scene.add(floor)

        nitcar()

        function nitcar() {
            requestAnimationFrame(nitcar);
            texture.offset.x -= 0.02
            if (progress > 1.0) {
                return;
            }
            progress += 0.0005;

            if (curve) {
                let point = curve.getPoint(progress);
                let point_car = curve.getPoint(progress + 0.0003);
                if (point && point.x) {
                    // cubeBox.position.set(point.x, point.y);
                    // cubeBox.lookAt(point_car.x, point_car.y,z);
                    // cubeBox.rotateY(Math.PI /2);
                }
            }
        }

    }
    lambertBox() {
        var cubeGeometry = new THREE.BoxGeometry(10, 10, 10);
        var cubeMaterial = new THREE.MeshLambertMaterial({
            color: 0x00ffff
        });
        let cube = new THREE.Mesh(cubeGeometry, cubeMaterial);
        cube.position.x = 5;
        cube.position.y = 5;
        cube.position.z = -5;
        //告诉立方体需要投射阴影
        cube.castShadow = true;
        this.scene.add(cube);
    }

}

export default basicThree

最后在模块文件中,开始获取数据动态渲染吧

  import basicThree from '@/utils/stationMap/map_type';
  let threeJs = null ;
  let dataList = [];
  threeJs = new basicThree({callback: this.callbackSlotNo });
  threeJs.initModel();
  ...
  //获取数据后创建楼层
  threeJs.initMap(dataList[this.theFloor], 2, mapParams, this.init, this.offset_x, this.offset_y);

由于需要渲染的数据太多,在以前的老vue2项目上继续开发就会遇到问题,升级到vue3吧又会出现兼容性问题,我现在进退两难,最后我决定求稳,不升级框架了,因为我也没时间去测试和把老业务全搞过来,太需要时间了,所以,求稳吧,逐步优化性能。这就用到了,我之前写的另一篇文章,如何进行前端项目优化的一步一步操作。除了及时销毁变量和定时器,少用闭包之外,来看看我的另一篇文章

关于性能优化,我做了哪些?()