基于openlayers 开发vue地图组件

先看效果
在这里插入图片描述
在这里插入图片描述

主要功能如下:

  • 测量
  • 图源更换
  • 放大缩小
  • 地图添加点
  • hover点数据
  • 切换到地图位置;也设定层级
  • 2D3D切换,3D为cesium开发,
  • 技术交流可以加V:bloxed
    地图工具做了插槽,分为toolbar(左上角工具) 和mode(右下角功能区),对应mapTools.vue和mapMode.vue 文件
    上代码:
    commonmap.vue文件
<!--
 * @Author: shangyc
 * @Date: 2024-011-07 09:54:54
 * @LastEditors: shangyc
 * @LastEditTime: 2024-011-07 09:54:54
 * @Description: file content
-->
<template>
  <div class="map-container h100 w100">
    <div class="mouseInfo" v-show="mouseInfo.scale">
      <span  class="btn-item">坐标:{{ mouseInfo.coords }}</span>
      <span class="btn-item">比例尺:{{ mouseInfo.scale }}</span>
      <span class="btn-item">视野:{{ mouseInfo.zoom }}</span>
    </div>
    <div id="map" class="map h100"></div>
    <slot name="toolbar" ></slot>
    <slot name="mode"></slot>
    <!-- tooltip -->
    <div ref="tooltip" class="tooltip" v-if="tooltipVisible">
      <el-row v-if="tooltipContents.length">
        <el-col v-for="item in tooltipContents" :key="item.value" @click="tooltipClick" class="content-col">
          <span>{{ item.name }}</span>
          <span>{{ item.value }}</span>
          <span>&nbsp;{{ item.unit ?? "" }}</span>
        </el-col>
      </el-row>
      <el-row v-else>
        <el-col class="content-col">暂无数据</el-col>
      </el-row>
    </div>
    <!--end  -->
  </div>
</template>

<script setup lang="ts">
import { onMounted, reactive, ref } from "vue";
import Map from "ol/Map";
import TileLayer from "ol/layer/Tile";
import View from "ol/View";
import { defaults as defaultControls } from "ol/control";

import { Overlay } from "ol";
import { fromLonLat, get as getProjection, toLonLat } from "ol/proj";
import { Cluster, Vector as SourceVector, XYZ } from "ol/source";
import { Vector as LayerVector } from "ol/layer";
import GeoJSON from "ol/format/GeoJSON";
import { Fill, Icon, Stroke, Style, Text } from "ol/style";
import CircleStyle from "ol/style/Circle";
import { Polygon } from "ol/geom";
import LineString from "ol/geom/LineString";
import { getArea, getLength } from "ol/sphere";
import GeometryType from "ol/geom/GeometryType";
import { Draw } from "ol/interaction";
import { unByKey } from "ol/Observable";
import OverlayPositioning from "ol/OverlayPositioning";
import areaLabel from "/@/assets/map/areaLabel.png";

import Mask from "ol-ext/filter/Mask";
import Crop from "ol-ext/filter/Crop";
import { getVectorContext } from "ol/render";

const { VITE_APP_2DMAP_URL } = import.meta.env;
const tooltipVisible = ref<boolean>(false);
const props = defineProps({
  imageSource: {
    type: String,
    default: "img",
  },
  toolBox: {
    type: Boolean,
    default: true,
  },
});
const imgSource = ref<string>("yx");
const emit = defineEmits(["mapSingleClick"]); // 自定义事件 会回传也是point数据
const center:any = [];
// start地图以及图层显示
const map = ref<any>(null);
//鼠标信息
const mouseInfo = reactive<any>({
  coords: "",
  scale: "",
  zoom: "",
});
let vector: any;
const drawObj = ref<any>(null);
const helpTooltipElement = ref<any>(null);
const helpTooltip = ref<any>(null);
const measureTooltipElement = ref<any>(null);
const measureTooltip = ref<any>(null);
let timer = null;
let flashTimer = null;
const clickPoint = ref<any>();

const tooltip = ref(null);
const tooltipContents = ref<any[]>([]);
//底图图源
const env = import.meta.env.VITE_APP_ENV;
const imgpublicUrl =  `${VITE_APP_2DMAP_URL}/yx/{z}/{x}/{-y}.png`;
const cvapublicUrl =  `${VITE_APP_2DMAP_URL}/dz/{z}/{x}/{-y}.png`;
const imgLayer = ref<any>(
  new TileLayer({
    source: new XYZ({
      // wrapX: true,
      url: imgpublicUrl
    }),
    visible: true
  })
);
const vecLayer = ref<any>(
  new TileLayer({
    source: new XYZ({
      url: cvapublicUrl,
      projection: getProjection("EPSG:3857"),
      // wrapX: true
    }),
    visible: true
  })
);

// 用于存储事件监听器键的数组
const eventKeys = ref<any[]>([]);

let start = new Date().getTime();
const disableMapEvent = ref<boolean>(false); // 关闭地图上面事件

//地图操作事件
const zoomInOut = (number: any) => {
  if (map.value) {
    const view = map.value.getView();
    const zoom = view.getZoom();
    view.setZoom(zoom + number);
  }
};
const toCenter = (coordinates?: any[]) => {
  if (map.value) {
    map.value.getView().animate({
      center: fromLonLat(coordinates ?? center),
      duration: 2500, // 动画持续时间,单位为毫秒(这里设置为2秒)
    });
  }
};
const points: any = {
  type: "FeatureCollection",
  features: [],
};
//查询到站点添加到地图
const addStation = (stations: any[]) => {
  mapInteractionSource.value.clear();
  map.value?.getOverlays().clear();
  points.features = [
  ];
  stations.forEach((point, index) => {
    point.lng &&
      point.lat &&
      points.features.push({
        type: "Feature",
        geometry: {
          type: "Point",
          coordinates: fromLonLat([point.lng, point.lat]),
        },
        properties: {
          id: index + point.stcd,
          name: point.stnm,
          type: point.sttp,
          info: point,
          icon: point.icon,
        },
        id: index + point.stcd,
      });
  });
  new GeoJSON({ featureProjection: "EPSG:4326" })
    .readFeatures({
      type: "FeatureCollection",
      features: [...points.features],
    })
    .forEach((item: any) => {
      mapInteractionSource.value.addFeature(item);
    });
};
const mapInteractionSource = ref<any>(new SourceVector({ wrapX: false }));
const imagSourceClick = (type: any) => {
  imgSource.value = type;
  if (type == "yx") {
    imgLayer.value.setVisible(true);
    vecLayer.value.setVisible(false);
  } else {
    imgLayer.value.setVisible(false);
    vecLayer.value.setVisible(true);
  }
};
const initMap = () => {
  if (map.value) {
    map.value.setTarget(null);
  }

  // 聚合
  var cluster = new Cluster({
    source: mapInteractionSource.value,
    distance: 30,
  });
  const cluster1Style = (feature: any, resolution: any) => {
    const { name, icon } = feature.get("features")[0].getProperties();
    return new Style({
      //把点的样式换成ICON图标
      fill: new Fill({
        //填充颜色
        color: "rgba(37,241,239,0.2)",
      }),
      //图形样式,主要适用于点样式
      image: new Icon({
        opacity: 1,
        scale: 0.5,
        src: icon ?? areaLabel,
      }),
      text: new Text({
        // 字体与大小
        font: "bold 13px Microsoft YaHei",
        //文字填充色
        fill: new Fill({
          color: "#fff",
        }),
        // 显示文本,数字需要转换为文本string类型!
        text: name,
        offsetY: -35,
        
      }),
    });
  };


  map.value = new Map({
    layers: [
      
      vecLayer.value,
      imgLayer.value,
      new LayerVector({
        source: cluster,
        style: cluster1Style,
      }),
    ],
    keyboardEventTarget: document,
    target: "map", // 对应页面里 id 为 map 的元素
    view: new View({
      center: fromLonLat([110.105931,22.422299]),
      zoom: 15,
    }),
    //控件初始默认不显示
    controls: defaultControls({
      attribution: false,
      zoom: false,
      rotate: false,
    }).extend([]),
  });

  const tdtStyle = new Style({
    fill: new Fill({
      color: "black",
    }),
  });
  imgLayer.value.on("postrender", (e: any) => {
    const vectorContext = getVectorContext(e);
    e.context.globalCompositeOperation = "destination-in";
    e.context.globalCompositeOperation = "source-over";
  });

  map.value.on("pointermove", handleMouseMove);
  map.value.on("pointermove", (evt: any) => {
    const pixel = map?.value?.getEventPixel(evt.originalEvent);
    const hit = map?.value?.hasFeatureAtPixel(pixel);
    map.value.forEachFeatureAtPixel(evt.pixel, function (feature: any) {
      let geometry: any = null;
      try {
        geometry = JSON.parse(new GeoJSON().writeFeature(feature));
      } catch (error) {
        geometry = feature;
      }
      if (geometry.geometry.type == "Point") {
        const coordinate = geometry.geometry.coordinates;
        const data =
          geometry.properties?.features[0]?.values_?.info?.data || [];
        if (data.length) {
          clickPoint.value = geometry.properties?.features[0]?.values_?.info;
          tooltipContents.value = data;
          tooltipVisible.value = true;
          try {
            map.value.addOverlay(
              new Overlay({
                position: coordinate,
                offset: [0, 0],
                element: tooltip.value,
                stopEvent: true,
              })
            );
          } catch (error) {
            console.warn(error);
          }
        }
      }
    });
  });
  map.value.on("singleclick", (evt: any) => {
    var pixel = map?.value?.getEventPixel(evt.originalEvent);
    var hit = map?.value?.hasFeatureAtPixel(pixel);
    map.value.forEachFeatureAtPixel(evt.pixel, function (feature: any) {
      let geometry: any = null;
      try {
        geometry = JSON.parse(new GeoJSON().writeFeature(feature));
      } catch (error) {
        geometry = feature;
      }
      if (geometry.geometry.type == "Point") {
        const coordinate = geometry.geometry.coordinates;
        mapSingleClick(geometry.properties?.features[0]?.values_?.info);
        const data =
          geometry.properties?.features[0]?.values_?.info?.data || [];
        if (data.length) {
          clickPoint.value = geometry.properties?.features[0]?.values_?.info;
          tooltipClick();
        }
      }
    });
  });
};
const mapSingleClick = (point: any) => {
  emit("mapSingleClick", point);
};
const handleMouseMove = (event: any) => {
  const coordinate = event.coordinate;
  const lonLat = toLonLat(map.value?.getCoordinateFromPixel(event.pixel));
  const scale = map.value?.getView().getResolution(); // 近似计算比例尺(米)
  const zoom = map.value?.getView().getZoom();
  mouseInfo.coords = ` ${lonLat[0].toFixed(5)},  ${lonLat[1].toFixed(5)}`;
  mouseInfo.scale = `1:${ Math.round(scale * 10000) / 100} m`;
  mouseInfo.zoom = `${zoom.toFixed(0)}`;
};
/**
 * 绘画
 * @param measureType line
 */
const draw = (measureType: any) => {
  // 移除所有事件监听器
  // eventKeys.value.forEach(key => unByKey(key));
  // eventKeys.value = []; // 清空数组,以便后续可能重新添加事件
  if (disableMapEvent.value) {
    return ElMessage.warning("请先结束之前的绘制");
  }
  drawObj.value && map.value.removeInteraction(drawObj.value);
  const source = new SourceVector();
  // if (!vector) {
  vector = new LayerVector({
    name: "drawLayer",
    zIndex: 10000,
    source: source,
    style: new Style({
      fill: new Fill({
        color: "rgba(255, 255, 255, 0.2)",
      }),
      stroke: new Stroke({
        color: "#ffcc33",
        width: 2,
      }),
      image: new CircleStyle({
        radius: 7,
        fill: new Fill({
          color: "#ffcc33",
        }),
      }),
    }),
  });
  map.value.addLayer(vector);
  // }
  /**
   * Currently drawn feature.
   * @type {module:ol/Feature~Feature}
   */
  let sketch: any = null;

  /**
   * Message to show when the user is drawing a polygon.
   * @type {string}
   */
  const continuePolygonMsg = "继续点击绘制多边形";

  /**
   * Message to show when the user is drawing a line.
   * @type {string}
   */
  const continueLineMsg = "继续点击绘制线";

  /**
   * Handle pointer move.
   * @param {module:ol/MapBrowserEvent~MapBrowserEvent} evt The event.
   */
  const pointerMoveHandler = (evt: any) => {
    if (evt.dragging) {
      return;
    }
    /** @type {string} */
    let helpMsg = "请点击开始绘制";

    if (sketch) {
      const geom = sketch.getGeometry();
      if (geom instanceof Polygon) {
        helpMsg = continuePolygonMsg;
      } else if (geom instanceof LineString) {
        helpMsg = continueLineMsg;
      }
    }

    helpTooltipElement.value.innerHTML = `<div style="color:#fff;background-color: rgba(0,0,0,0.6);padding: 4px;border-radius: 6px">${helpMsg}</div>`;
    helpTooltip.value.setPosition(evt.coordinate);

    helpTooltipElement.value?.lassList?.remove("hidden");
  };

  map.value.on("pointermove", pointerMoveHandler);
  map.value.getViewport().addEventListener("mouseout", function () {
    helpTooltipElement.value?.classList?.add("hidden");
  });

  const formatLength = (line: any) => {
    const sourceProj = map.value.getView().getProjection();
    // @ts-ignore
    const length = getLength(line, { projection: sourceProj });
    let output;
    if (length > 100) {
      output = Math.round((length / 1000) * 100) / 100 + " " + "km";
    } else {
      output = Math.round(length * 100) / 100 + " " + "m";
    }
    return output;
  };
  const formatArea = (polygon: any) => {
    const area = getArea(polygon, {
      projection: "EPSG:4326",
    });
    let output;
    if (area > 10000) {
      output =
        Math.round((area / 1000000) * 100) / 100 + " " + "km<sup>2</sup>";
    } else {
      output = Math.round(area * 100) / 100 + " " + "m<sup>2</sup>";
    }
    return output;
  };

  const addInteraction = () => {
    const type =
      measureType == "area" ? GeometryType.POLYGON : GeometryType.LINE_STRING;
    drawObj.value = new Draw({
      source: source,
      type: type,
      // condition: mouseOnly,
      // freehandCondition: noModifierKeys,
      style: new Style({
        fill: new Fill({
          color: "rgba(255, 255, 255, 0.2)",
        }),
        stroke: new Stroke({
          color: "red",
          lineDash: [10, 10],
          width: 2,
        }),
        image: new CircleStyle({
          radius: 5,
          stroke: new Stroke({
            color: "red",
          }),
          fill: new Fill({
            color: "rgba(255, 255, 255, 0.2)",
          }),
        }),
      }),
    });
    map.value.addInteraction(drawObj.value);
    createHelpTooltip();

    let listener: any;
    drawObj.value.on("drawstart", function (evt: any) {
      createMeasureTooltip();
      disableMapEvent.value = true;
      // set sketch
      sketch = evt.feature;

      /** @type {module:ol/coordinate~Coordinate|undefined} */
      let tooltipCoord: any;

      listener = sketch.getGeometry()?.on("change", function (evt: any) {
        const geom = evt.target;
        let output = "";
        if (geom instanceof Polygon) {
          output = formatArea(geom);
          tooltipCoord = geom.getInteriorPoint().getCoordinates();
        } else if (geom instanceof LineString) {
          output = formatLength(geom);
          tooltipCoord = geom.getLastCoordinate();
        }
        measureTooltipElement.value.innerHTML = output;
        measureTooltip.value.setPosition(tooltipCoord);
      });
    });

    drawObj.value.on("drawend", function () {
      disableMapEvent.value = false;
      measureTooltip.value.setOffset([0, -7]);
      // unset sketch
      sketch.dispose();
      // unset tooltip so that a new one can be created
      measureTooltipElement.value = document.createElement("div");
      listener && unByKey(listener);
    });
  };

  const createHelpTooltip = () => {
    helpTooltipElement.value?.parentNode?.removeChild(helpTooltipElement.value);
    helpTooltipElement.value = document.createElement("div");
    helpTooltipElement.value.className = "tooltip hidden";
    helpTooltip.value = new Overlay({
      id: "helpTooltip",
      element: helpTooltipElement.value,
      offset: [15, 0],
      zIndex: 10000,
      positioning: OverlayPositioning.CENTER_LEFT,
    });
    map.value.addOverlay(helpTooltip.value);
  };

  const createMeasureTooltip = () => {
    // measureTooltipElement?.parentNode?.removeChild(measureTooltipElement);
    measureTooltipElement.value = document.createElement("div");
    measureTooltipElement.value.className = "ol-tooltip ol-tooltip-measure";
    // measureTooltipElement.setAttribute('class', comStyle['ol-tooltip-measure']);
    measureTooltip.value = new Overlay({
      oId: "measureTooltip",
      element: measureTooltipElement.value,
      offset: [0, -15],
      zIndex: 10000,
      positioning: OverlayPositioning.BOTTOM_CENTER,
    });
    map.value.addOverlay(measureTooltip.value);
    //@ts-ignore
    window["measureTooltip"] = measureTooltip.value;
  };

  addInteraction();
};
/**
 * 清除
 */
const clear = () => {
  disableMapEvent.value = false;
  drawObj.value && map.value.removeInteraction(drawObj.value);
  vector && map.value.removeLayer(vector);
  const overlays = map.value.getOverlays().getArray();
  if (overlays) {
    overlays
      .filter(
        (o: any) =>
          o &&
          (o.options?.oId === "measureTooltip" || o.getId() === "helpTooltip")
      )
      .forEach(function (o: any) {
        map.value.removeOverlay(o);
      });
  }
  const layers = map.value.getLayers();
  if (layers) {
    layers
      .getArray()
      .filter((o: any) => o?.get("name") === "drawLayer")
      .forEach(function (o: any) {
        o.getSource().clear();
        map.value.removeLayer(o);
      });
  }
  drawObj.value = null;
};
const tooltipClick = () => {
};
onMounted(() => {
  initMap();
});
defineExpose({
  map, //地图
  /**
    @desc 添加站点 
    @param [{
      stcd:0000,
      stnm:"xxxx",
      sttp:'',
      icon:'xxxx',
      lng:number,
      lat:number
    }
      data:[{name:'xx',value:'',unit:''}]  //data地图鼠标悬浮展示内容
    }]
  */
  addStation,  
  /**
   * 缩放 地图
   * @param {number} zoom 缩放级别
   */
  zoomInOut, 
  /**
   * 地图中心点
   * @param {number} center 地图中心点参数格式 [lng,lat]
   * @param {number} zoom 缩放级别 15
   */ 
  toCenter,  
  /**
   * 清除地图分析
   */
  clear,
  /**
   * 地图分析
   * @param {string} type 分析类型  'area' 面积  'line' 距离
   */
  draw,  
  /**
   * 图源切换事件
   * @param {string} type 图层类型
   */
  imagSourceClick
  
});
</script>

<style scoped lang="less">
.map-container {
  height: 100%;
  position: relative;
  background: url("/@/assets/map/mapbg.png");

  .btn {
    position: absolute;
    z-index: 10;
    top: 20px;
    left: 10px;

    .btn-item {
      background-color: #139eb1;
      margin: 0px 10px;
      color: #fff;
      border: none;
      height: 30px;
      border-radius: 4px 4px 4px 4px;
    }
  }

  .mouseInfo {
    position: absolute;
    z-index: 10;
    left: 40%;
    bottom: 20px;
    pointer-events: none;

    .btn-item {
      margin: 0px 10px;
      color: #fff;
      border: none;
      height: 30px;
      border-radius: 4px 4px 4px 4px;
    }
  }

  .map {
    height: 100%;
  }

  .tooltip {
    position: absolute;
    width: 250px;
    background: rgba(255, 255, 255, 0.9);
    box-shadow: 0px 4px 20px 0px rgba(0, 0, 0, 0.25);
    border-radius: 4px 4px 4px 4px;
    padding: 5px 12px;
    border-top: 4px solid #0fe2ff;

    .content-col {
      margin-top: 8px;
      font-family:
        PingFang SC,
        PingFang SC;
      font-weight: 400;
      font-size: 14px;
      color: #666666;
      line-height: 16px;
      text-align: left;
      font-style: normal;
      text-transform: none;
      border-radius: 4px 4px 0px 0px;
    }
  }

  :deep(.ant-popover, .ant-popover-content) {
    border-radius: 10px;
  }
}

.tyxz {
  border-radius: 10px;

  .tybox {
    cursor: pointer;
    padding-left: 5px;
    padding-top: 5px;
  }

  .desc {
    text-align: center;
    font-size: 12px;
    margin-top: 5px;
  }

  .activeimg {
    background: #139eb1;
    color: #fff;
  }
}
</style>

maptools.vue 文件

<template>
    <div>
      <el-popover  placement="top" :width="160">
          <template #reference>
            <el-button  size="small" type="primary"  v-show="mapType=='2D'" > 图源选择 </el-button>
        </template>
          <el-row :gutter="20" style="width: 280px" class="tyxz">
            <el-col :span="12" :class="'tybox ' + (imgSource == 'dz' ? ' activeimg' : '')"
                   @click="imagSourceClick('dz')">
              <img :src="dzs" alt="" width="120" height="120" srcset="" />
              <p class="desc">电子</p>
            </el-col>
            <el-col :span="12" :class="'tybox ' + (imgSource == 'yx' ? ' activeimg' : '')"
                   @click="imagSourceClick('yx')">
              <img :src="yxs" alt="" srcset="" width="120" height="120" />
              <p class="desc">影像</p>    
            </el-col>
          </el-row>
      </el-popover>
        <el-button size="small" type="primary" v-for="item in tools" v-show="item.type.indexOf(mapType) > -1" @click="item.click" :key="item.id">{{ item.name }}</el-button>
    </div>
</template>
<script lang="ts" setup>
import dzs from "/@/assets/map/dz.png";
import yxs from "/@/assets/map/yx.png";
const props = defineProps({
    map:Object,
    mapType:{
        type:String,
        default:'3D'
    }
})
const mapType = ref(props.mapType);
const tools = [
    {id:1,name:'点位',type:'3D',click:()=>{measurePoint()}},
    {id:2,name:'距离',type:'2D,3D',click:()=>{measureLine()}},
    {id:2,name:'高度',type:'3D',click:()=>{measureHeight()}},
    {id:3,name:'面积',type:'2D,3D',click:()=>{measureArea()}},
    // {id:6,name:"挖坑",type:'3D',click:()=>{console.log('点击了')}},
    {id:7,name:"体积",type:'3D',click:()=>{measureVolume()}},
    {id:8,name:"清除测量",type:'2D,3D',click:()=>{clear()}}

]
const imgSource = ref<string>("yx");
const imagSourceClick = (type:any) => {
  imgSource.value = type;
  props.map?.imagSourceClick(type)
};
const measurePoint = () => {
    props.map?.draw('point');
}
const measureHeight = () => {
    props.map?.draw('height');
}
const measureLine = () => {
    props.map?.draw('line');
}

const measureArea = () => {
    props.map?.draw('area');
}
const measureVolume = () => {
    props.map?.draw('volume');
}
const clear = () => {
    props.map?.clear();
}
</script>
<style scoped lang="scss">
.tyxz {
  border-radius: 10px;

  .tybox {
    cursor: pointer;
    padding-left: 5px;
    padding-top: 5px;
  }

  .desc {
    text-align: center;
    font-size: 12px;
    margin-top: 5px;
  }

  .activeimg {
    background: #139eb1;
    color: #fff;
  }
}
</style>

mapMode.vue文件

<template>
    <div class="map-mode">
        <el-icon class="icon" @click="toCenter"><Promotion /></el-icon>
        <el-icon  class="icon" @click="zoomIn"><CirclePlusFilled /></el-icon>
        <el-icon  class="icon" @click="zoomOut"><RemoveFilled /></el-icon>
        <span  v-if="mapType==='3D'" class="icon D23" @click="changeMapType('2D')">2D</span>
        <span v-if="mapType==='2D'" class="icon D23" @click="changeMapType('3D')">3D</span>
    </div>
</template>
<script lang="ts" setup>
const props = defineProps({
    map:Object,
    mapType:{
        type:String,
        default:'2D'
    }
})
const mapType = ref(props.mapType);
const emit = defineEmits(['changeMapType'])
const changeMapType = (type:string) =>{
    mapType.value = type;
    emit('changeMapType',type)
}
const  zoomIn = () =>{
    props.map?.zoomInOut(0.5);
}
const zoomOut = () =>{
    props.map?.zoomInOut(-0.5);
}
const toCenter = () =>{
    props.map?.toCenter([110.105931,22.422299],16);
}
</script>
<style scoped lang="scss">
.map-mode{
    display: flex;
    flex-direction: column;
    align-items: center;
    background-color: rgba(31,60,113,.66);
    padding: 5px;
    border-radius: 5px;
    .icon{
        font-size: 20px;
        cursor: pointer;
        margin:10px 0px;
        color: #00cdff;
        font-weight: bold;
    }
    .D23{
        font-size: 15px;
    }
}
</style>```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值