DEJA_VU3D - Cesium功能集 之 083-Cesium热力图实现完整版

前言 

编写这个专栏主要目的是对工作之中基于Cesium实现过的功能进行整合,有自己琢磨实现的,也有参考其他大神后整理实现的,初步算了算现在有差不多实现小130个左右的功能,后续也会不断的追加,所以暂时打算一周2-3更的样子来更新本专栏(尽可能把代码简洁一些)。博文内容如存在错误或者有可改进之处,也希望在这里和各位大佬交流提高一下。

更多内容/样例/demo说明:DEJA_VU3D完整功能目录 

专栏内容本着尽可能简洁的原则,上一篇文章

DEJA_VU3D - Cesium功能集 之 082-热力图绘制原理_总要学点什么的博客-CSDN博客

我们有讲过热力图实现的原理,本篇我们就来详细的介绍基于Cesium来实现热力图的绘制,文章中包含了所有的源代码,最终的实现效果大致如下:

原理上一篇已经说过了,废话不多说,直接上代码: 

关键代码 

/**
   * 创建热力图对象
   * @param {*} box 范围对象,经纬度值-{west,south,east,north}
   * @param {*} data 待绘制热力图的数据-[{x1,y1,value1},{x2,y2,value2},...]
   * @returns 热力图结果对象
   */
 createHeatmap(box, data) {
  const mercator_WestSouth = this.WGS84ToWebMercator(box.west, box.south, 0); //左下位置(墨卡托)
  const mercator_EastNorth = this.WGS84ToWebMercator(box.east, box.north, 0); //右上位置(墨卡托)
  const diffDegrees_X = mercator_EastNorth.x - mercator_WestSouth.x;
  const diffDegrees_Y = mercator_EastNorth.y - mercator_WestSouth.y;
  const diffMax = Math.max(diffDegrees_X, diffDegrees_Y);
  let multiple = diffMax / 300; //适当扩大倍数,以便绘制canvas
  const width = Math.ceil(diffDegrees_X / multiple);
  const height = Math.ceil(diffDegrees_Y / multiple);
  this.mercator_WestSouth = mercator_WestSouth;
  this.mercator_EastNorth = mercator_EastNorth;
  this.diffDegrees_X = diffDegrees_X;
  this.diffDegrees_Y = diffDegrees_Y;
  let canvasData = [];
  data.forEach((element) => {
    const curMercator = this.WGS84ToWebMercator(
      Number(element.x),
      Number(element.y),
      0
    );
    const per_X = (curMercator.x - mercator_WestSouth.x) / diffDegrees_X;
    const currentPix_X = Math.ceil(per_X * width);
    const per_Y = (curMercator.y - mercator_WestSouth.y) / diffDegrees_Y;
    const currentPix_Y = Math.ceil(per_Y * height);
    const currentValue = Number(element.value);
    canvasData.push({
      x: currentPix_X,
      y: height - currentPix_Y - 1,
      value: currentValue,
    });
  });
  let canvas = new Canvas(width, height);
  let context = canvas.getContext("2d");
  context.clearRect(0, 0, canvas.width, canvas.height);
  let circle = this.createCircle(this._size);
  let circleHalfWidth = circle.width / 2;
  let circleHalfHeight = circle.height / 2;

  // 按透明度分类
  let dataOrderByAlpha = {};
  canvasData.forEach((item) => {
    let alpha =
      item.value < this._min
        ? 0
        : Math.min(1, item.value / this._max).toFixed(2);
    dataOrderByAlpha[alpha] = dataOrderByAlpha[alpha] || [];
    dataOrderByAlpha[alpha].push(item);
  });

  // 绘制不同透明度的圆形
  for (let i in dataOrderByAlpha) {
    if (isNaN(i)) continue;
    let _data = dataOrderByAlpha[i];
    context.beginPath();
    context.globalAlpha = i;
    _data.forEach((item) => {
      context.drawImage(
        circle,
        item.x - circleHalfWidth,
        item.y - circleHalfHeight
      );
    });
  }
  // 圆形着色
  let intensity = new Intensity();
  let colored = context.getImageData(
    0,
    0,
    context.canvas.width,
    context.canvas.height
  );
  const options = { min: 0, max: this._max, size: this._size };
  this.colorize(options, colored.data, intensity.getImageData());
  context.clearRect(0, 0, context.canvas.width, context.canvas.height);
  context.putImageData(colored, 0, 0);
  let entity = this._viewer.entities.add({
    name: "rectangle",
    rectangle: {
      coordinates: Cesium.Rectangle.fromDegrees(
        box.west,
        box.south,
        box.east,
        box.north
      ),
      material: new Cesium.ImageMaterialProperty({
        image: canvas,
        transparent: true,
      }),
      // heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
    },
  });
  this._canvas = canvas;
  this._result = { box, data, canvasData, entity };
  return this._result;
}

完整demo

热力图主要实现类HeatmapIntensity.js

/*
 * Cesium 热力图主类
 * @Author: Wang jianLei
 * @Date: 2022-10-20 15:12:27
 * @Last Modified by: Wang JianLei
 * @Last Modified time: 2022-10-23 22:53:01
 */
import { Intensity, Canvas } from "./Intensity";
const Cesium = window.Cesium;
class HeatmapIntensity {
  constructor(viewer, option = {}) {
    if (!viewer) throw new Error("no viewer object!");
    this._viewer = viewer;
    this._min = option.min || 0; //最小值
    this._max = option.max || 100; //最大值
    this._size = option.size || 20; //光圈大小,像素值
    this._result = undefined; //热力图结果
    this._canvas = undefined; //离屏canvas
  }

  get min() {
    return this._min;
  }
  set min(val) {
    this._min = val;
    this.updateHeatmap(this._result.canvasData);
  }
  get max() {
    return this._max;
  }
  set max(val) {
    this._max = val;
    this.updateHeatmap(this._result.canvasData);
  }
  get size() {
    return this._size;
  }
  set size(val) {
    this._size = val;
    this.updateHeatmap(this._result.canvasData);
  }
  get result() {
    return this._result;
  }
  /**
   * 创建热力图对象
   * @param {*} box 范围对象,经纬度值-{west,south,east,north}
   * @param {*} data 待绘制热力图的数据-[{x1,y1,value1},{x2,y2,value2},...]
   * @returns 热力图结果对象
   */
  createHeatmap(box, data) {
    const mercator_WestSouth = this.WGS84ToWebMercator(box.west, box.south, 0); //左下位置(墨卡托)
    const mercator_EastNorth = this.WGS84ToWebMercator(box.east, box.north, 0); //右上位置(墨卡托)
    const diffDegrees_X = mercator_EastNorth.x - mercator_WestSouth.x;
    const diffDegrees_Y = mercator_EastNorth.y - mercator_WestSouth.y;
    const diffMax = Math.max(diffDegrees_X, diffDegrees_Y);
    let multiple = diffMax / 300; //适当扩大倍数,以便绘制canvas
    const width = Math.ceil(diffDegrees_X / multiple);
    const height = Math.ceil(diffDegrees_Y / multiple);
    this.mercator_WestSouth = mercator_WestSouth;
    this.mercator_EastNorth = mercator_EastNorth;
    this.diffDegrees_X = diffDegrees_X;
    this.diffDegrees_Y = diffDegrees_Y;
    let canvasData = [];
    data.forEach((element) => {
      const curMercator = this.WGS84ToWebMercator(
        Number(element.x),
        Number(element.y),
        0
      );
      const per_X = (curMercator.x - mercator_WestSouth.x) / diffDegrees_X;
      const currentPix_X = Math.ceil(per_X * width);
      const per_Y = (curMercator.y - mercator_WestSouth.y) / diffDegrees_Y;
      const currentPix_Y = Math.ceil(per_Y * height);
      const currentValue = Number(element.value);
      canvasData.push({
        x: currentPix_X,
        y: height - currentPix_Y - 1,
        value: currentValue,
      });
    });
    let canvas = new Canvas(width, height);
    let context = canvas.getContext("2d");
    context.clearRect(0, 0, canvas.width, canvas.height);
    let circle = this.createCircle(this._size);
    let circleHalfWidth = circle.width / 2;
    let circleHalfHeight = circle.height / 2;

    // 按透明度分类
    let dataOrderByAlpha = {};
    canvasData.forEach((item) => {
      let alpha =
        item.value < this._min
          ? 0
          : Math.min(1, item.value / this._max).toFixed(2);
      dataOrderByAlpha[alpha] = dataOrderByAlpha[alpha] || [];
      dataOrderByAlpha[alpha].push(item);
    });

    // 绘制不同透明度的圆形
    for (let i in dataOrderByAlpha) {
      if (isNaN(i)) continue;
      let _data = dataOrderByAlpha[i];
      context.beginPath();
      context.globalAlpha = i;
      _data.forEach((item) => {
        context.drawImage(
          circle,
          item.x - circleHalfWidth,
          item.y - circleHalfHeight
        );
      });
    }
    // 圆形着色
    let intensity = new Intensity();
    let colored = context.getImageData(
      0,
      0,
      context.canvas.width,
      context.canvas.height
    );
    const options = { min: 0, max: this._max, size: this._size };
    this.colorize(options, colored.data, intensity.getImageData());
    context.clearRect(0, 0, context.canvas.width, context.canvas.height);
    context.putImageData(colored, 0, 0);
    let entity = this._viewer.entities.add({
      name: "rectangle",
      rectangle: {
        coordinates: Cesium.Rectangle.fromDegrees(
          box.west,
          box.south,
          box.east,
          box.north
        ),
        material: new Cesium.ImageMaterialProperty({
          image: canvas,
          transparent: true,
        }),
        // heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
      },
    });
    this._canvas = canvas;
    this._result = { box, data, canvasData, entity };
    return this._result;
  }
  /**
   * 更新热力图数据
   * @param {*} data 待绘制热力图的数据-[{x1,y1,value1},{x2,y2,value2},...]
   */
  setData(data) {
    if (!this._result) {
      return;
    }
    let canvasData = [];
    const height = this._canvas.height;
    const width = this._canvas.width;
    data.forEach((element) => {
      const curMercator = this.WGS84ToWebMercator(
        Number(element.x),
        Number(element.y),
        0
      );
      const per_X =
        (curMercator.x - this.mercator_WestSouth.x) / this.diffDegrees_X;
      const currentPix_X = Math.ceil(per_X * width);
      const per_Y =
        (curMercator.y - this.mercator_WestSouth.y) / this.diffDegrees_Y;
      const currentPix_Y = Math.ceil(per_Y * height);
      const currentValue = Number(element.value);
      canvasData.push({
        x: currentPix_X,
        y: height - currentPix_Y - 1,
        value: currentValue,
      });
    });
    this._result.data = data;
    this._result.canvasData = canvasData;
    this.updateHeatmap(canvasData);
  }
  /**
   * 更新热力图
   * @param {*} data 参考canvasData
   * @returns 热力图结果对象
   */
  updateHeatmap(data) {
    let canvas = this._canvas;
    let context = canvas.getContext("2d");
    context.clearRect(0, 0, canvas.width, canvas.height);
    let circle = this.createCircle(this._size);
    let circleHalfWidth = circle.width / 2;
    let circleHalfHeight = circle.height / 2;
    // 按透明度分类
    let dataOrderByAlpha = {};
    data.forEach((item) => {
      let alpha =
        item.value < this._min
          ? 0
          : Math.min(1, item.value / this._max).toFixed(2);
      dataOrderByAlpha[alpha] = dataOrderByAlpha[alpha] || [];
      dataOrderByAlpha[alpha].push(item);
    });
    // 绘制不同透明度的圆形
    for (let i in dataOrderByAlpha) {
      if (isNaN(i)) continue;
      let _data = dataOrderByAlpha[i];
      context.beginPath();
      context.globalAlpha = i;
      _data.forEach((item) => {
        context.drawImage(
          circle,
          item.x - circleHalfWidth,
          item.y - circleHalfHeight
        );
      });
    }
    // 圆形着色
    let intensity = new Intensity();
    let colored = context.getImageData(
      0,
      0,
      context.canvas.width,
      context.canvas.height
    );
    const options = { min: 0, max: this._max, size: this._size };
    this.colorize(options, colored.data, intensity.getImageData());
    context.clearRect(0, 0, context.canvas.width, context.canvas.height);
    context.putImageData(colored, 0, 0);
    this._result.entity.rectangle.material = new Cesium.ImageMaterialProperty({
      image: canvas,
      transparent: true,
    });
    return this._result;
  }
  createCircle(size) {
    let shadowBlur = size / 2;
    let r2 = size + shadowBlur;
    let offsetDistance = 10000;
    let circle = new Canvas(r2 * 2, r2 * 2);
    let context = circle.getContext("2d");
    context.shadowBlur = shadowBlur;
    context.shadowColor = "black";
    context.shadowOffsetX = context.shadowOffsetY = offsetDistance;
    context.beginPath();
    context.arc(
      r2 - offsetDistance,
      r2 - offsetDistance,
      size,
      0,
      Math.PI * 2,
      true
    );
    context.closePath();
    context.fill();
    return circle;
  }
  colorize(options, pixels, gradient) {
    let max = options.max;
    let min = options.min;
    let diff = max - min;
    let range = options.range || null;
    let jMin = 0;
    let jMax = 1024;
    if (range && range.length === 2) {
      jMin = ((range[0] - min) / diff) * 1024;
    }
    if (range && range.length === 2) {
      jMax = ((range[1] - min) / diff) * 1024;
    }
    let maxOpacity = options.maxOpacity || 0.8;
    for (let i = 3, len = pixels.length, j; i < len; i += 4) {
      j = pixels[i] * 4; // get gradient color from opacity value
      if (pixels[i] / 256 > maxOpacity) {
        pixels[i] = 256 * maxOpacity;
      }
      if (j && j >= jMin && j <= jMax) {
        pixels[i - 3] = gradient[j];
        pixels[i - 2] = gradient[j + 1];
        pixels[i - 1] = gradient[j + 2];
      } else {
        pixels[i] = 0;
      }
    }
  }
  WGS84ToWebMercator(lng, lat, height) {
    let mercator = {};
    let x = (lng * 20037508.34) / 180;
    let y = Math.log(Math.tan(((90 + lat) * Math.PI) / 360)) / (Math.PI / 180);
    y = (y * 20037508.34) / 180;
    mercator.x = x;
    mercator.y = y;
    mercator.z = height;
    return mercator;
  }
  clearAll() {
    this._result && this._viewer.entities.remove(this._result.entity);
    this._result = undefined;
  }
}
export default HeatmapIntensity;

透明度转换方法文件Intensity.js

function Intensity(options) {
  options = options || {};
  this.gradient = options.gradient || {
    0.25: "rgba(0, 0, 255, 1)",
    0.55: "rgba(0, 255, 0, 1)",
    0.85: "rgba(255, 255, 0, 1)",
    1.0: "rgba(255, 0, 0, 1)",
  };
  this.maxSize = options.maxSize || 35;
  this.minSize = options.minSize || 0;
  this.max = options.max || 100;
  this.min = options.min || 0;
  this.initPalette();
}

Intensity.prototype.setMax = function (value) {
  this.max = value || 100;
};

Intensity.prototype.setMin = function (value) {
  this.min = value || 0;
};

Intensity.prototype.setMaxSize = function (maxSize) {
  this.maxSize = maxSize || 35;
};

Intensity.prototype.setMinSize = function (minSize) {
  this.minSize = minSize || 0;
};

Intensity.prototype.initPalette = function () {
  let gradient = this.gradient;
  let canvas = new Canvas(256, 1);
  let paletteCtx = (this.paletteCtx = canvas.getContext("2d"));
  let lineGradient = paletteCtx.createLinearGradient(0, 0, 256, 1);
  for (let key in gradient) {
    lineGradient.addColorStop(parseFloat(key), gradient[key]);
  }
  paletteCtx.fillStyle = lineGradient;
  paletteCtx.fillRect(0, 0, 256, 1);
};

Intensity.prototype.getColor = function (value) {
  let imageData = this.getImageData(value);
  return (
    "rgba(" +
    imageData[0] +
    ", " +
    imageData[1] +
    ", " +
    imageData[2] +
    ", " +
    imageData[3] / 256 +
    ")"
  );
};

Intensity.prototype.getImageData = function (value) {
  let imageData = this.paletteCtx.getImageData(0, 0, 256, 1).data;
  if (value === undefined) {
    return imageData;
  }
  let max = this.max;
  let min = this.min;
  if (value > max) {
    value = max;
  }
  if (value < min) {
    value = min;
  }
  let index = Math.floor(((value - min) / (max - min)) * (256 - 1)) * 4;
  return [
    imageData[index],
    imageData[index + 1],
    imageData[index + 2],
    imageData[index + 3],
  ];
};

/**
 * @param Number value
 * @param Number max of value
 * @param Number max of size
 * @param Object other options
 */
Intensity.prototype.getSize = function (value) {
  let size = 0;
  let max = this.max;
  let min = this.min;
  let maxSize = this.maxSize;
  let minSize = this.minSize;
  if (value > max) {
    value = max;
  }
  if (value < min) {
    value = min;
  }
  size = minSize + ((value - min) / (max - min)) * (maxSize - minSize);
  return size;
};

Intensity.prototype.getLegend = function (options) {
  let gradient = this.gradient;
  let width = options.width || 20;
  let height = options.height || 180;
  let canvas = new Canvas(width, height);
  let paletteCtx = canvas.getContext("2d");
  let lineGradient = paletteCtx.createLinearGradient(0, height, 0, 0);
  for (let key in gradient) {
    lineGradient.addColorStop(parseFloat(key), gradient[key]);
  }
  paletteCtx.fillStyle = lineGradient;
  paletteCtx.fillRect(0, 0, width, height);
  return canvas;
};
// 构造一个离屏canvas
function Canvas(width, height) {
  let canvas = document.createElement("canvas");
  canvas.width = width;
  canvas.height = height;
  return canvas;
}

export { Intensity, Canvas };

前端界面调用HeatmapCustom.vue

<template>
  <div id="cesiumContainer">
    <div class="canvas-main">
      <el-button size="mini" @click="startDraw" :disabled="bool"
        >开始绘制</el-button
      >
      <el-button size="mini" @click="autoUpdate" :disabled="!bool"
        >自动更新</el-button
      >
    </div>
  </div>
</template>
<script>
import HeatmapIntensity from "./HeatmapIntensity";
const Cesium = window.Cesium;
let viewer = undefined;
export default {
  data() {
    return {
      mapData: [],
      bool: false,
    };
  },
  mounted() {
    let key =
      "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiIwZDhhOThhNy0zMzUzLTRiZDktYWM3Ni00NGI5MGY2N2UwZDUiLCJpZCI6MjQzMjYsInNjb3BlcyI6WyJhc3IiLCJnYyJdLCJpYXQiOjE1ODUwMzUwNDh9.DYuDF_RPKe5_8w849_y-sutM68LM51O9o3bTt_3rF1w";
    Cesium.Ion.defaultAccessToken = key;
    window.viewer = viewer = new Cesium.Viewer("cesiumContainer", {
      imageryProvider: new Cesium.ArcGisMapServerImageryProvider({
        url: "https://services.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer",
      }),
      terrainProvider: Cesium.createWorldTerrain(),
      geocoder: true,
      homeButton: true,
      sceneModePicker: true,
      baseLayerPicker: true,
      navigationHelpButton: true,
      animation: true,
      timeline: true,
      fullscreenButton: true,
      vrButton: true,
      //关闭点选出现的提示框
      selectionIndicator: true,
      infoBox: true,
    });
    viewer._cesiumWidget._creditContainer.style.display = "none"; // 隐藏版权
    this.initData();
  },
  methods: {
    startDraw() {
      const option = {
        min: 0,
        max: 100,
        size: 20,
      };
      window.heatmapObj = new HeatmapIntensity(viewer, option);
      const box = {
        west: 110,
        south: 40.5,
        east: 110.5,
        north: 41,
      };
      window.heatmapObj.createHeatmap(box, this.mapData);
      this.bool = true;
    },
    autoUpdate() {
      setInterval(() => {
        let data = [];
        for (let i = 0; i < 100; i++) {
          let obj = {};
          obj.x = this.randomNum(110, 110.5, 5);
          obj.y = this.randomNum(40.5, 41, 5);
          obj.value = this.randomNum(0, 100, 2);
          data.push(obj);
        }
        window.heatmapObj.setData(data);
      }, 1000);
    },
    initData() {
      this.mapData = [];
      for (let i = 0; i < 100; i++) {
        let obj = {};
        obj.x = this.randomNum(110, 110.5, 5);
        obj.y = this.randomNum(40.5, 41, 5);
        obj.value = this.randomNum(0, 100, 2);
        this.mapData.push(obj);
      }
      this.mapData.forEach((element) => {
        viewer.entities.add({
          position: Cesium.Cartesian3.fromDegrees(
            Number(element.x),
            Number(element.y)
          ),
          point: {
            pixelSize: 5,
            heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
          },
        });
      });
      viewer.flyTo(viewer.entities);
    },
    randomNum(maxNum, minNum, decimalNum) {
      // 获取指定范围内的随机数, decimalNum指小数保留多少位
      let max = 0,
        min = 0;
      minNum <= maxNum
        ? ((min = minNum), (max = maxNum))
        : ((min = maxNum), (max = minNum));
      let result = undefined;
      switch (arguments.length) {
        case 1:
          result = Math.floor(Math.random() * (max + 1));
          break;
        case 2:
          result = Math.floor(Math.random() * (max - min + 1) + min);
          break;
        case 3:
          result = (Math.random() * (max - min) + min).toFixed(decimalNum);
          break;
        default:
          result = Math.random();
          break;
      }
      return result;
    },
  },
};
</script>
<style lang="scss" scoped>
#cesiumContainer {
  width: 100%;
  height: 100%;
  position: relative;
  background: rgba(255, 0, 0, 0.322);
  .canvas-main {
    position: absolute;
    margin: 10px;
    padding: 10px;
    z-index: 10;
    background: rgba(255, 255, 0, 0.329);
  }
}
</style>

运行效果

综上!

如果客官您有问题,可以在本文下留言!

如果客官您有什么建议意见,可以在本文下留言!

如果客官您有批评指正,可以在本文下沟通讨论!

如果实例demo有数据缺失,评论留下您的邮箱地址!

如果客官您有其他的功能需求,可以在本文下留言,不管能不能实现,总会给出回复!

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论
deja_vu3d - cesium是一款功能强大的地球可视化框架。它基于Cesium.js开发,提供了丰富的功能集,可用于创建令人惊叹的三维地球模型。 deja_vu3d - cesium的主要功能包括: 1. 三维地球可视化:deja_vu3d - cesium允许用户展示全球范围内的地理数据,包括地形、海洋、建筑物等。它提供了高度精确的地球模型,可以实时旋转和缩放,让用户以全新的视角探索地球表面。 2. 实时数据可视化:该框架支持对实时数据的可视化,可以将传感器数据、气象数据等实时更新的数据集成到地球模型中。用户可以通过动态的图表、标签和交互式元素来直观地理解数据趋势和模式。 3. 矢量和栅格数据显示:deja_vu3d - cesium支持导入和展示各种矢量和栅格数据。用户可以将自己的地理信息数据集成到地球模型中,进行可视化和空间分析。 4. 相机和视角控制:框架提供了强大的相机和视角控制功能,用户可以自由调整视角、俯瞰地球表面、漫游各个地区。这使得用户能够更好地理解地理空间关系和模式,并更好地展示和交流地理数据。 5. 动画和时间轴:deja_vu3d - cesium允许用户创建动画效果,通过时间轴控制地球模型的演变。这对于展示历史数据、模拟气候变化等具有重要意义。 总而言之,deja_vu3d - cesium是一款功能强大的地球可视化框架,它提供了丰富的功能集,帮助用户创建令人惊叹的三维地球模型,并可可视化和分析各种地理数据。无论是用于科研、教育还是商业应用,deja_vu3d - cesium都能为用户提供出色的地球可视化体验。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

总要学点什么

相信每一个技术人员的惺惺相惜

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值