Master the sharp tool for efficient map drawing - LeafletJs


foreword

As an important tool for acquiring, storing, analyzing and managing geospatial data, GIS is more flexible to draw maps with GIS technology than traditional manual or automatic mapping tools. Today I would like to share with you a JavaScript class library package for GIS client development leafletJs .


1. What is leafletJs?

To quote the official words, Leaflet is an open source and mobile-friendly interactive map JavaScript library. And it has all the map features that most developers need. Moreover, Leaflet can efficiently run on desktop and mobile platforms, and has a large number of extensions, excellent documentation, easy-to-use API , complete cases, and better readable source code. Chinese document address: https://leafletjs.cn/reference.html


2. Quick start

1. Installation

leafletJs provides two ways to introduce, you can choose one according to your personal needs.

Method 1: Direct import and use

<link rel = "stylesheet" href = "http://cdn.leafletjs.com/leaflet-0.7.3/leaflet.css" />
<script src = "http://cdn.leafletjs.com/leaflet-0.7.3/leaflet.js"></script>

Method 2: npm installation

npm install leaflet --save
cnpm install leaflet --save

main.js import and register

import Leaflet from 'leaflet'
import "leaflet/dist/leaflet.css"
Vue.use(Leaflet);

2. Quick start

2.1. Prepare a div container, which must have width and height.

<div class="oilMap" id="oilMap"></div>

2.2. Define a map instance in data

data() {
    
    
  return {
    
    
    map: null,
  };
},

2.3. Initialize map instance

mounted() {
    
    
  this.initMap(); 
},

// 地图初始化
initMap() {
    
    
  // 使用 id 为 map 的 div 容器初始化地图,同时指定地图的中心点和缩放级别
  this.map = L.map("oilMap", {
    
    
    center: [39.91, 116.91], //中心经纬度
    zoom: 8, //初始缩放大小
  });
  // 引入图层
  L.tileLayer("http://192.168.0.138/OSM_BLUE/{z}/{x}/{y}.png").addTo(
    this.map
  );
},

2.4. Don't forget to set the width and height for the map

<style lang="scss" scoped>
.oilMap {
    
    
  width: 100%;
  height: 100vh;
  z-index: 9;
  background: #00192e;
}
</style>

From this, you can get an initialized map as follows

insert image description here


3. Advanced Learning

1. Map control

MapControls are mainly tools used to control the map display.

1.1 Scale bar control

L.control.scale({
    
    
    metric: true, // 显示米
    imperial: false, // 不显示英尺
  }).addTo(this.map);

1.2 Disable double-click to zoom in

this.map = L.map("oilMap", {
    
    
  center: [39.91, 116.91], //中心经纬度
  zoom: 8, //初始缩放大小
  doubleClickZoom: false, //禁用双击放大
});

1.3 Whether the zoom control is displayed

this.map = L.map("oilMap", {
    
    
  center: [39.91, 116.91], //中心经纬度
  zoom: 8, //初始缩放大小
  zoomControl: true, //缩放控件是否显示
});

1.4 Whether the copyright control is displayed

this.map = L.map("oilMap", {
    
    
  center: [39.91, 116.91], //中心经纬度
  zoom: 8, //初始缩放大小
  attributionControl: false, //版权控件是否显示
});

1.5 Map maximum/minimum zoom level

this.map = L.map("oilMap", {
    
    
  center: [39.91, 116.91], //中心经纬度
  zoom: 8, //初始缩放大小
  minZoom: 6,//地图最小缩放层级
  maxZoom: 10,//地图最大缩放层级
});

2. Marker mark

L.MarkerUsed to display clickable/dragable icons on the map.

2.1 Add icon mark points

// 定义图标属性
let markerIcon = L.icon({
    
    
  iconUrl:"https://unpkg.com/[email protected]/dist/images/marker-icon-2x.png",
  iconSize: [20, 34],//图标图像的尺寸,单位是像素。
  iconAnchor: [7, 9],//图标 "tip" 的坐标(相对于其左上角)。图标将被对齐,使该点位于标记的地理位置。如果指定了尺寸,默认为居中,也可以在CSS中设置负的边距。
  popupAnchor: [-3, -3],//弹出窗口(popup)的坐标,相对于图标锚点而言,将从该点打开。
});
// 生成标记点
L.marker([39.91, 116.91], {
    
    icon: markerIcon,}).addTo(this.map);

Icon mark points to achieve the effect

insert image description here


2.2 Add a circle marker

L.circle([40.91, 116.91], {
    
    
  color: "red", //描边颜色
  fillColor: "yellow", //填充颜色
  fillOpacity: 0.5, //填充的不透明度
  radius: 5000, //圆的半径,以米为单位
}).addTo(this.map);

Round markers to achieve the effect

insert image description here


2.3 Adding polygon markers

let latlngs = [
  [39.91, 116.91],
  [39.87, 116.11],
  [39.51, 116.51],
  [39.51, 116.81],
  [39.91, 116.91],
];
L.polygon(latlngs,{
    
    color:'rgb(51,136,255)',fillColor:'red', fillOpacity:'0.4'}).addTo(this.map);

Polygon markers to achieve the effect

insert image description here

3. Popup pop-up window

bindPopup("弹出内容").openPopup()Used to open popups at certain locations on the map.

chestnut

// 定义图标属性
let markerIcon = L.icon({
    
    
  iconUrl:"https://unpkg.com/[email protected]/dist/images/marker-icon-2x.png",
  iconSize: [20, 34],//图标图像的尺寸,单位是像素。
  iconAnchor: [7, 9],//图标 "tip" 的坐标(相对于其左上角)。图标将被对齐,使该点位于标记的地理位置。如果指定了尺寸,默认为居中,也可以在CSS中设置负的边距。
  popupAnchor: [-3, -3],//弹出窗口(popup)的坐标,相对于图标锚点而言,将从该点打开。
});
// 生成标记点
L.marker([39.91, 116.91], {
    
    icon: markerIcon,}).addTo(this.map).bindPopup("弹出内容").openPopup();

achieve effect

insert image description here


4. Layers

LeafletIn , anything that can be added to a map is a layer. Therefore, the markers, circles, and polygons above all belong to the layer, and can also be understood as the elements of the map.

4.1 Clear layers

//示例图层
let lineSet = L.polygon(latlngs,{
    
    color:'rgb(51,136,255)',fillColor:'red', fillOpacity:'0.4'}).addTo(this.map);
//清除指定layer
this.map.removeLayer(lineSet)
//清除地图上所有layer
this.map.clearLayers()

4.2 Map level judgment

this.map.on("zoom", function (e) {
    
    
  if (e.target._zoom > 8) {
    
    
    // 执行操作
  }
});

4.3 Monitor map zoom

this.map.on("zoom", function () {
    
    
 // 执行操作
});

4.4 Binding click event

let marker = L.marker([39.91, 116.91]).addTo(this.map);
marker.on("click", function (e) {
    
    
  // 执行操作
});

4.5 Add GeoJson layer

//jsondata必须为指定格式为GeoJson格式
let GeoJsonlayer = L.geoJSON(jsondata, {
    
    
  style: {
    
    
    color: "#4e98f444",
    fillOpacity: 1,
    weight: 1,
  },
});
GeoJsonlayer.setZIndex(1);// 改变网格图层的 zIndex 
this.map.addLayer(GeoJsonlayer);// 将给定的图层添加到组中

4. Project actual combat

The above are the main points of using Leafletthe map . Next, we will combine the above mentioned points with the needs of the project to realize various interesting operations on the map.

package file

// 地图公共方法封装 /utils/index
import * as L from 'leaflet'
import {
    
     Message, Notification } from 'element-ui'
export default class LMap {
    
    
    // 初始化地图
    /***
     * 【options】参数合集
     * [tileUrl]瓦片地图地址
     * [minZoom]最小级别
     * [maxZoom]最大级别
     * [zoom]初始化级别
     * [crs]坐标系
     * [center]初始化视图中心点
     * [fullscreenControl]是否显示全屏控件
     * [zoomControl]是否显示缩放控件
     * [inertia]是否开启惯性,在拖动和在某一时间段内持续朝同一方向移动建有动力的地图时,会有惯性的效果
     * [attributionControl]属性控制是否默认加载在地图上
     * [renderer]// 渲染方式 (默认使用canvas提高性能)
     * [bounds]// 地图限制拖动区域 [下,上,右,左]
     *
     *
     * ***/
    async initMap(divId, options) {
    
    
        // let url = process.env.VUE_APP_MAP_URL + '/OSM_BLUE/{z}/{x}/{y}.png'
        const {
    
    
            tileUrl,
            minZoom,
            maxZoom,
            zoom,
            crs,
            center,
            fullscreenControl,
            zoomControl,
            inertia,
            attributionControl,
            renderer,
            bounds
        } = options
        this.map = L.map(divId, {
    
    
            minZoom: minZoom ? minZoom : 8, // 地图最小缩放层级
            maxZoom: maxZoom ? maxZoom : 15, // 地图最大缩放层级,
            // crs: crs ? crs : L.CRS.EPSG3857, // 坐标系
            center: center ? center : [39.7506, 118.3423], // 地图中心
            zoom: zoom ? zoom : 8, // 地图默认缩放层级
            fullscreenControl: fullscreenControl ? fullscreenControl : false, // 是否显示全屏控件
            zoomControl: zoomControl ? zoomControl : false, // 是否显示缩放控件
            inertia: inertia ? inertia : true, // 是否开启惯性,在拖动和在某一时间段内持续朝同一方向移动建有动力的地图时,会有惯性的效果
            attributionControl: attributionControl ? attributionControl : false, // 属性控制是否默认加载在地图上
            renderer: renderer ? renderer : L.canvas() //使用canvas 渲染 提高性能
        })
        if (tileUrl) {
    
    
            // 将mapbox自定义样式添加到地图中
            // this.map.addLayer(L.tileLayer(tileUrl, { 
            //     tileSize: 256,
            //     zoomOffset: 1, 
            //     attribution: 'stamen',
            //   }))
            this.map.addLayer(L.tileLayer(tileUrl))
        } else {
    
    
            console.error('---请配置瓦片图层地址---')
        }
        // 给地图区域做限制的上下左右四部分
        if (bounds) {
    
    
            const mapBounds = bounds
            // L.latLngBounds([
            //     [40.7844, 118.2935], // 下
            //     [38.4575, 118.4979], // 上
            //     [39.782, 115.778], // 左
            //     [39.8041, 121.583] // 右
            // ])
            this.map.setMaxBounds(mapBounds)
        }
        // 渲染完成返回true
        return this.map
    }
    getMap() {
    
    
        return this.map || {
    
    }
    }
    mapArea() {
    
    }
    mapDistance() {
    
    }
}


4.1 Basic bitmap

<template>
  <div>
    <div class="zMap" id="map"></div>
    <!-- 点位详情 -->
    <div ref="detailsObj" v-if="showPup">
      <div class="dataBox">
        <div class="item">
          <div>点位名称:</div>
          <div>{
    
    {
    
     markerData.name ? markerData.name : "--" }}</div>
        </div>
        <div class="item">
          <div>车牌号:</div>
          <div> {
    
    {
    
     markerData.cph ? markerData.cph : "--" }}</div>
        </div>
        <div class="item">
          <div>排放阶段:</div>
          <div>{
    
    {
    
     markerData.pfjd ? markerData.pfjd : "--" }}</div>
        </div>
      </div>
    </div>
  </div>
</template>
<script>
import areaGeo from "@/utils/area.json"; //引入离线本地json
import LMap from "@/utils/index";//地图公共方法封装
import * as L from "leaflet";
import onlineMarker from "../assets/zaix.png";//不同状态的图片
import offlineMarker from "../assets/lix.png";//不同状态的图片
export default {
    
    
  name: "lMap",
  data() {
    
    
    return {
    
    
      map: null, //地图对象
      lmap: new LMap(), //地图公共方法封装
      labelGroup: null, //文本图层
      markerLayer: null, //marker 图层
      bondarylayer: null, //geojson 图层
      layerPup: null, //弹窗
      markerData: {
    
    }, //详情数据
      showPup: false, //详情弹框是否弹出
    };
  },
  mounted() {
    
    
    this.initMap(); //初始化地图方法
  },
  methods: {
    
    
    //初始化地图
    async initMap() {
    
    
      let _this = this;
      let {
    
     lmap } = this;
      //配置地图项
      const mapConfig = {
    
    
        tileUrl: "http://192.168.0.138/OSM_BLUE/{z}/{x}/{y}.png", //瓦片图层地址
        minZoom: 8, //允许缩放最小级别
        maxZoom: 10, //允许缩放最大级别
        zoom: 8, //初始化级别
        center: [30.536095454317287, 104.15024279778913], //中心经纬度
        bounds: L.latLngBounds([
          [30.536095454317287, 104.15024279778913], // 下
          [30.536095454317287, 104.15024279778913], // 上
          [30.536095454317287, 104.15024279778913], // 左
          [30.536095454317287, 104.15024279778913], // 右
        ]), //限制拖动的区域
      };
      this.map = await lmap.initMap("map", mapConfig);
      if (this.map) {
    
    
        this.labelGroup = new L.layerGroup(); //文本图层
        this.labelGroup.addTo(this.map);
        this.markerLayer = new L.layerGroup(); //标记
        this.markerLayer.addTo(this.map);
        this.getGeojsonByName(areaGeo, true);
        this.maptopPoint();
      } else {
    
    
        console.error("--地图加载失败!!!--");
      }
    },
    //加载geojson数据
    getGeojsonByName(data, flag) {
    
    
      let _this = this;
      this.bondarylayer = L.geoJSON(data, {
    
    
        style: {
    
    
          color: "#4e98f444",
          fillOpacity: 1,
          weight: 2,
        },
        pane: "overlayPane",
        onEachFeature: (feature, layer) => {
    
    
          if (!flag) {
    
    
            _this.addText(feature, layer);
          }
        },
      });
      this.bondarylayer.setZIndex(1);
      this.map.addLayer(this.bondarylayer);
    },
    addText(feature, layer) {
    
    
      let name = feature.properties.NAME;
      let location = layer._bounds.getCenter();
      //显示文字
      var content = `${
    
    name}`;
      // 区县名称文字
      var text = L.divIcon({
    
    
        html: "<div>" + content + "</div>",
        iconSize: [60, 20],
        iconAnchor: [0, 10],
        className: "labelStyle",
      });
      let marker = new L.marker(location, {
    
     icon: text });
      //中心点位
      this.labelGroup.addLayer(marker);
    },
    //模拟地图点位接口
    maptopPoint() {
    
    
      let dataList = [
        {
    
    
          lng: "104.882",
          lat: "30.436",
          name: "点位1",
          cph: "川A31255",
          pfjd: "国五",
          status: "0",
        },
        {
    
    
          lng: "103.652",
          lat: "30.565",
          name: "点位2",
          cph: "川C32585",
          pfjd: "国四",
          status: "1",
        },
        {
    
    
          lng: "104.352",
          lat: "31.136",
          name: "点位3",
          cph: "川X36985",
          pfjd: "国三",
          status: "0",
        },
        {
    
    
          lng: "104.452",
          lat: "30.536",
          name: "点位4",
          cph: "川D31255",
          pfjd: "国二",
          status: "1",
        },
        {
    
    
          lng: "104.202",
          lat: "30.136",
          name: "点位5",
          cph: "川E31125",
          pfjd: "国一",
          status: "1",
        },
      ];
      this.checkMarkerList(dataList);
    },
    //监测数据对marker进行渲染
    checkMarkerList(list) {
    
    
      let _this = this;
      if (!list.length) {
    
    
        return;
      }
      list.map((item) => {
    
    
        _this.addMarker(item);
      });
    },
    addMarker(item) {
    
    
      let _this = this;
      let Icon = _this.getMarkerIcon(item);
      if (Icon) {
    
    
        var marker = new L.marker(L.latLng(item.lat, item.lng), {
    
    
          icon: Icon,
        });
        //区县编码
        marker["markerInfo"] = item;
        marker.bindTooltip(item.name); //鼠标触摸显示提示信息
        marker.on("click", function (e) {
    
    
          let MarkerTarget = e.target; //获取 点击marker
          _this.showMarkerPup(MarkerTarget);
        });
        this.markerLayer.addLayer(marker);
      }
    },
    //显示marker 弹窗信息
    showMarkerPup(marker) {
    
    
      let _this = this;
      _this.markerData = marker["markerInfo"];
      let location = marker._latlng;
      _this.showPup = true;
      _this.$nextTick(() => {
    
    
        _this.layerPup = L.popup({
    
    
          className: "district-pup",
          closeButton: false,
        })
          .setLatLng(location)
          .setContent(_this.$refs.detailsObj)
          .openOn(_this.map);
      });
    },
    //获取marker图标
    getMarkerIcon(data) {
    
    
      let markerIcon, imgUrl;
      let {
    
     status } = data; //1在线 2离线

      if (status) {
    
    
        status = Number(status);
      }
      if (status == 1) {
    
    
        imgUrl = onlineMarker;
      } else {
    
    
        imgUrl = offlineMarker;
      }
      markerIcon = L.icon({
    
    
        iconUrl: imgUrl,
        iconSize: [20, 20],
        iconAnchor: [12, 12],
        popupAnchor: [-3, -3],
      });
      return markerIcon;
    },
  },
};
</script>

<style lang="scss" scoped>
.zMap {
    
    
  width: 100%;
  height: 100vh;
  z-index: 9;
}
.dataBox {
    
    
  width: 100%;
  .item {
    
    
    display: flex;
    padding: 4px 0px;
  }
}
::v-deep {
    
    
  .leaflet-popup-content-wrapper,
  .leaflet-popup-tip {
    
    
    border: none;
    background: rgba($color: #020a31, $alpha: 0.7) !important;
    border-radius: 4px;
    box-shadow: 0px 0 6px 0px #33aeff inset;
    color: rgba($color: #fff, $alpha: 0.8);
  }
  .leaflet-container a.leaflet-popup-close-button {
    
    
    color: #fff;
  }
}
</style>

achieve effect

insert image description here


4.2 Driving trajectory map

<template>
  <div>
    <div class="leftRightBox">
      <!-- 左 -->
      <div class="oilMap" id="map"></div>
      <!-- 右 -->
      <div class="rightTxtBox">
        <el-card class="box-card">
          <!-- 播放轨迹 -->
          <div class="trackBox">
            <div>
              <i title="播放" v-if="playIsShow" @click="playPauseOn" class="el-icon-video-play"></i>
              <i title="暂停" v-else @click="playPauseOn" class="el-icon-video-pause"></i>
            </div>
            <div>
              <el-slider :min="0" :max="points.length - 1" @change="sliderOn" v-model="progress"></el-slider>
            </div>
          </div>
          <div class="multipleBox">
            <span>倍速:</span>
            <el-select @change="multipleOn" style="width:70px" size="mini" v-model="multipleNumber" placeholder="请选择倍速">
              <el-option v-for="item in multipleOptions" :key="item.multipleNumber" :label="item.label" :value="item.value">
              </el-option>
            </el-select>
          </div>
        </el-card>
      </div>
    </div>
  </div>
</template>

<script>
import LMap from "@/mapUtils";
import areaGeo from "@/mapUtils/area.json";
import abnormal from "../../../../assets/overview/normal.png"; //地图图标
export default {
      
      
  data() {
      
      
    return {
      
      
      multipleOptions: [
        {
      
      
          value: 800,
          label: "1",
        },
        {
      
      
          value: 550,
          label: "1.5",
        },
        {
      
      
          value: 400,
          label: "2",
        },
      ],
      map: null,
      lmap: new LMap(), //地图公共方法封装
      labelGroup: null, //文本图层
      tipsLayerGroup: null, //tips
      bondarylayer: null, //geojson 图层
      points: [
        //模拟点位数据
        [39.898457, 116.391844],
        [39.898595, 116.377947],
        [39.898341, 116.368001],
        [39.898063, 116.357144],
        [39.899095, 116.351934],
        [39.905871, 116.35067],
        [39.922329, 116.3498],
        [39.931017, 116.349671],
        [39.939104, 116.349225],
        [39.942233, 116.34991],
        [39.947263, 116.366892],
        [39.947568, 116.387537],
        [39.947764, 116.401988],
        [39.947929, 116.410824],
        [39.947558, 116.42674],
        [39.9397, 116.427338],
        [39.932404, 116.427919],
        [39.923109, 116.428377],
        [39.907094, 116.429583],
        [39.906858, 116.41404],
        [39.906622, 116.405321],
        [39.906324, 116.394954],
        [39.906308, 116.391264],
      ],
      playIsShow: true, //播放/暂停
      multipleNumber: 1, //倍速
      progress: 0, //进度
      marker: null,
      polyline: null,
      duration: 0,
      timer: null, //定时器
      time: 800, //定时器速度
    };
  },
  mounted() {
      
      
    this.duration = this.points.length - 1;
    this.initMap();
  },
  watch: {
      
      
    progress() {
      
      
      this.updatePosition();
    },
  },
  destroyed() {
      
      
    clearTimeout(this.timer);
  },
  methods: {
      
      
    updatePosition() {
      
      
      const position = this.points[this.progress];
      this.marker.setLatLng(position);
      this.map.setView(position, 12); //放大比例
    },
    // 滑动进度
    sliderOn(e) {
      
      
      console.log(e, "滑动进度");
    },
    // 选择倍速
    multipleOn(e) {
      
      
      clearTimeout(this.timer);
      this.time = e;
      this.recursion();
    },
    // 点击播放暂停按钮
    playPauseOn() {
      
      
      if (this.progress == this.points.length - 1) {
      
      
        this.progress = 0;
      }
      this.playIsShow = !this.playIsShow;
      this.recursion();
    },
    // 递归
    recursion() {
      
      
      if (!this.playIsShow) {
      
      
        // 模拟数据变化
        this.timer = setTimeout(() => {
      
      
          this.progress++;
          if (this.progress >= this.points.length - 1) {
      
      
            this.playIsShow = true;
            this.polyline = L.polyline(this.points, {
      
       color: "red" }).addTo(
              this.map
            );
            clearTimeout(this.timer);
          }
          this.recursion();
        }, this.time);
      }
    },
    //初始化地图
    async initMap() {
      
      
      let {
      
       lmap } = this;
      //配置地图项
      const mapConfig = {
      
      
        tileUrl: window.api.teilUrl + "/OSM_BLUE/{z}/{x}/{y}.png", //瓦片图层地址
        crs: L.CRS.EPSG4326,
        minZoom: 8, //允许缩放最小级别
        maxZoom: 15, //允许缩放最大级别
        zoom: 10, //初始化级别
        center: [39.91, 116.91], //初始化中心点
      };
      this.map = await lmap.initMap("map", mapConfig);
      if (this.map) {
      
      
        this.labelGroup = new L.layerGroup(); //文本图层
        this.labelGroup.addTo(this.map);
        this.tipsLayerGroup = new L.layerGroup();
        this.tipsLayerGroup.addTo(this.map);
        this.getGeojsonByName(areaGeo, true);
      } else {
      
      
        console.error("--地图加载失败!!!--");
      }
      var icon = L.icon({
      
      
        iconUrl: abnormal,
        iconSize: [16, 13],
        iconAnchor: [8, 7],
      });
      this.marker = L.marker(this.points[0], {
      
       icon: icon }).addTo(this.map);
      this.polyline = L.polyline(this.points, {
      
       color: "red" }).addTo(this.map);
      setInterval(() => {
      
      
        this.marker.setLatLng(this.points[this.progress]);
        this.polyline.setLatLngs(this.points.slice(0, this.progress + 1));
      }, 100);
    },
    //加载geojson数据 falg 是否显示名称
    getGeojsonByName(data, flag) {
      
      
      let _this = this;
      this.bondarylayer = L.geoJSON(data, {
      
      
        style: {
      
      
          color: "#4e98f444",
          fillOpacity: 1,
          weight: 1,
        },
        pane: "overlayPane",
        onEachFeature: (feature, layer) => {
      
      
          // console.log(layer)
          if (flag) {
      
      
            _this.addText(feature, layer);
          }
        },
      });
      this.bondarylayer.setZIndex(1);
      this.map.addLayer(this.bondarylayer);
    },
    //添加市所有区县
    addText(feature, layer) {
      
      
      let name = feature.properties.name;
      // let center = feature.properties.center;
      let location = layer._bounds.getCenter(); //[center[1], center[0]]; //
      //显示文字
      var content = `${ 
        name}`;
      // 区县名称文字
      var text = L.divIcon({
      
      
        html: "<div class='labelStyle'>" + content + "</div>",
        iconSize: [60, 20],
        iconAnchor: [0, 10],
        className: "labelStyle",
      });
      let marker = new L.marker(location, {
      
       icon: text });
      //中心点位
      this.labelGroup.addLayer(marker);
    },
  },
};
</script>
<style scoped>
.oilMap {
      
      
  height: 500px;
}
</style>


achieve effect

insert image description here

持续更新...

Guess you like

Origin blog.csdn.net/Shids_/article/details/127687072