使用技术:
Vue+Cesium
产品需求:
需要在地球上指定经纬度展示项目数据,展示内容包含项目进度 、项目类型 、项目名称 、项目负责人。点击不同的信息,展示不同的弹窗。并且地球视角转动,展示地球上展示的数据也要随之转动。
UI设计图:

关于文字:
Cesium提供的label只能实现简单的文字加载,支持修改文字大小、边框、背景等
关于Icon:
Cesium提供的billboard可以加载指定图片,对于项目类型这种可枚举的字段倒是可以通过if判断来加载指定类型的图片,但是类似于项目进度这种可选值数量巨大的字段,总不至于要切100张图片吧 🤯
开发思路:
经过度娘的一番搜索,发现大部分思路都是先写自定义的<div>标签,然后使用绝对定位,给定位到指定经纬度的屏幕坐标上,当地球发生视角变化时,重新计算定位位置。
这种方法当数据量小的时候还好说,需要展示的数据增多,获取最新位置的计算量也会随之增长。😟
🤔 实现思路:
因为项目中有将Dom元素保存为本地图片的需求和实现方式。所以就想到了,那么可以将自定义的<div>标签转成image,然后Cesium通过billboard直接把图片加载到地球上么?
经过一番尝试,证明这个思路是可行的。
Cesium加载图片
引入自定义
Vue组件,初始化所需数据,并挂载到Dom树上
            
            
              js
              
              
            
          
          import ProjectProgress from './ProjectProgress.vue'
const ProjectProgressConstructor = Vue.extend(ProjectProgress);
const ProjectProgressDom = new ProjectProgressConstructor({
  data: { progress: progress }
}).$mount();
document.body.appendChild(ProjectProgressDom.$el);将挂载在Dom树上的元素转换为图片,并清除元素
            
            
              js
              
              
            
          
          const progressImage = await domConvertToImage(ProjectProgressDom.$el); // 进度组件Image
ProjectProgressDom.$el.remove(); // 将使用完成的DOM元素清除
Cesium加载billboard
            
            
              js
              
              
            
          
          app.$Viewer.entities.add({
  name: "mapPorjectProgressElement",
  useful: item,
  position,
  billboard: {
    image: progressImage,
    width: 26, // 宽度(以像素为单位)
    height: 26, // 高度(以像素为单位)
    verticalOrigin: Cesium.VerticalOrigin.BOTTOM, // 相对于坐标的垂直位置
    horizontalOrigin: Cesium.HorizontalOrigin.CENTER, // 相对于坐标的水平位置
    pixelOffset: new Cesium.Cartesian2(0, -26) // 该属性指定标签在屏幕空间中距此标签原点的像素偏移量
  }
});Dom元素转image
使用了
html2canvas依赖
            
            
              js
              
              
            
          
          import html2canvas from 'html2canvas'
/**
 * 将DOM元素转换为图片
 * @param {DOM} dom
 * @returns {String} ImageUrl
 */
export async function domConvertToImage(dom) {
  let saveUrl
  await html2canvas(dom, {
    backgroundColor: null // 设置图片背景为透明
  })
    .then((canvas) => {
      saveUrl = canvas.toDataURL('image/png')
    })
    .catch((err) => {
      console.error('组件转换图片失败:', err)
    })
  return saveUrl
}实现效果图

🧐 存在问题
当然这种方式也有缺点,因为是将元素转换为image加载的,所有无法实现加载数据的动画效果 和鼠标交互效果。
实际开发,根据需求的情况择优选择。
完整代码
自定组件
进度百分比的圆环使用了
element的组件,也可以自己实现
            
            
              html
              
              
            
          
          <template>
  <div class="project_progress_main_content">
    <el-progress
      type="circle"
      :percentage="progress"
      :stroke-width="3"
      color="#FCB718"
      define-back-color="rgba(255, 255, 255, 0.2)"
      :width="26"
      :show-text="false"
    />
    <div class="progress_number">
      {{ progress }}
    </div>
  </div>
</template>
<script>
export default {
  data() {
    return {
      progress: 0
    }
  }
}
</script>
<style lang="scss" scoped>
.project_progress_main_content {
  position: fixed;
  top: -999px;
  left: -999px;
  width: 26px;
  height: 26px;
  .progress_number {
    position: absolute;
    top: 3px;
    left: 3px;
    width: 20px;
    height: 20px;
    background: rgba(0, 0, 0, 0.4);
    border-radius: 50%;
    font-family: D-DIN;
    font-size: 12px;
    font-weight: bold;
    line-height: 10px;
    color: #ffffff;
    text-align: center;
    line-height: 20px;
  }
}
</style>Cesium加载元素
因为项目的每个属性都需要单独的点击交互,所以把四个信息拆成独立的元素。
如果不需要多个点击事件,也可把所有信息都写到自定义的
Vue组件中
            
            
              js
              
              
            
          
          import Vue from "vue";
import app from "@/main";
// 绘制项目打卡icon
export function drawPorjectRecordIcon(projectRecordList) {
  projectRecordList.forEach(async (item) => {
    const position = Cesium.Cartesian3.fromDegrees(
      parseFloat(item.longitude),
      parseFloat(item.latitude),
      0
    ); // 项目坐标
    /* ---------------------------------- 绘制项目Icon ----------------------------------- */
    const image = ""; // 项目类别图标 --自定义补充
    app.$Viewer.entities.add({
      name: "mapPorjectRecordIconElement",
      useful: item,
      position,
      billboard: {
        image,
        show: true,
        width: 24, // 宽度(以像素为单位)
        height: 30, // 高度(以像素为单位)
        verticalOrigin: Cesium.VerticalOrigin.BOTTOM, // 相对于坐标的垂直位置
        horizontalOrigin: Cesium.HorizontalOrigin.CENTER // 相对于坐标的水平位置
      }
    });
    /* ---------------------------------- 绘制进度组件 ----------------------------------- */
    const ProjectProgressConstructor = Vue.extend(ProjectProgress);
    const ProjectProgressDom = new ProjectProgressConstructor({
      data: { progress: item.progress }
    }).$mount();
    document.body.appendChild(ProjectProgressDom.$el);
    const progressImage = await domConvertToImage(ProjectProgressDom.$el); // 进度组件Image
    ProjectProgressDom.$el.remove(); // 将使用完成的DOM元素清除
    const progressBillboard = {
      image: progressImage,
      width: 26, // 宽度(以像素为单位)
      height: 26, // 高度(以像素为单位)
      verticalOrigin: Cesium.VerticalOrigin.BOTTOM, // 相对于坐标的垂直位置
      horizontalOrigin: Cesium.HorizontalOrigin.CENTER, // 相对于坐标的水平位置
      pixelOffset: new Cesium.Cartesian2(0, -26) // 该属性指定标签在屏幕空间中距此标签原点的像素偏移量
    };
    app.$Viewer.entities.add({
      name: "mapPorjectProgressElement",
      useful: item,
      position,
      billboard: progressBillboard
    });
    /* ---------------------------------- 绘制人员信息 ----------------------------------- */
    const userLabel = {
      text: item.responsibleName,
      font: "14px Source Han Sans CN", // 字体样式
      fillColor: Cesium.Color.WHITE, // 字体颜色
      backgroundColor: Cesium.Color.BLACK.withAlpha(0.4), // 背景颜色
      showBackground: true, // 是否显示背景颜色
      outlineWidth: 20, // 文字外边框宽度
      verticalOrigin: Cesium.VerticalOrigin.BOTTOM, // 相对于坐标的水平位置
      horizontalOrigin: Cesium.HorizontalOrigin.LEFT, // 相对于坐标的垂直位置
      pixelOffset: new Cesium.Cartesian2(20, -22) // 该属性指定标签在屏幕空间中距此标签原点的像素偏移量
    };
    app.$Viewer.entities.add({
      name: "mapProjectResponsibleElement",
      useful: item,
      position,
      label: userLabel
    });
    /* ---------------------------------- 绘制项目名称 ----------------------------------- */
    app.$Viewer.entities.add({
      name: "mapProjectNameElement",
      useful: item,
      position,
      label: {
        text: item.projectName,
        font: "16px Source Han Sans CN", // 字体样式
        style: Cesium.LabelStyle.FILL_AND_OUTLINE, // 文字描边
        fillColor: Cesium.Color.WHITE, // 字体颜色
        outlineWidth: 20, // 文字外边框宽度
        outlineColor: Cesium.Color.BLACK.withAlpha(0.6), // 文字外边框颜色
        verticalOrigin: Cesium.VerticalOrigin.TOP, // 相对于坐标的水平位置
        horizontalOrigin: Cesium.HorizontalOrigin.LEFT, // 相对于坐标的垂直位置
        pixelOffset: new Cesium.Cartesian2(20, -30) // 该属性指定标签在屏幕空间中距此标签原点的像素偏移量
      }
    });
  });
  /* ------------------------------ 项目点击事件 ------------------------------ */
  const mapPorjectRecordHandler = new Cesium.ScreenSpaceEventHandler(
    app.$Viewer.scene.canvas
  );
  mapPorjectRecordHandler.setInputAction((click) => {
    var pick = app.$Viewer.scene.pick(click.position);
    if (pick && pick.id && pick.id.name === "mapPorjectRecordIconElement") {
      console.log("点击了项目类别图标,项目信息为:", pick.id.useful);
    }
    if (pick && pick.id && pick.id.name === "mapPorjectProgressElement") {
      console.log("点击了绘制进度组件,项目信息为:", pick.id.useful);
    }
    if (pick && pick.id && pick.id.name === "mapProjectResponsibleElement") {
      console.log("点击了项目人员信息,项目信息为:", pick.id.useful);
    }
    if (pick && pick.id && pick.id.name === "mapProjectNameElement") {
      console.log("点击了项目名称,项目信息为:", pick.id.useful);
    }
  }, Cesium.ScreenSpaceEventType.LEFT_CLICK);
}