MapBox实现框选查询,多边形范围查询

还是老规矩先来看效果:

 mapbox官方没有为我们提供框选查询的案例,所以这个功能需要我们自己写。在openlayers框架中是有一个矩形范围查询的例子,但是在maobox没有。

那么我们就来说一下如何来做这个效果吧,首先这个效果可以分为两个部分,第一个部分是绘制图形,第二部分是利用绘制的图形去查询指定的图层数据。

绘制图形在mapbox中也没有固定的案例,因此也需要我们自己写。

绘制一个图形的思路其实是通过一个关键的函数setData()这个函数的作用是为图层源source设置数据。在mapbox中我们通常可以这样操作。先建立一个空的source和layer,然后后续再为这个图层指定数据。就像下面这样的写法:

map.addSource("selected-source", {
        type: "geojson",
        data: {
          type: "FeatureCollection",
          features: [],
        },
      });
      map.addLayer({
        id: "selected-layer",
        type: "circle",
        source: "selected-source",
        paint: {
          "circle-radius": 8,
          "circle-color": color,
        },
      });
      map.getSource("selected-source").setData(features);

因此顺着这个思路我们可以先建立图层,后更新数据。如何来更新数据?我们可以监听地图的“onmousemove”事件,初始状态我们可以鼠标点击确定多边形的起点,然后鼠标移动的时候,我们让鼠标当前的位置和第一次点下的位置进行连线,这样就画成了一条线,顺着这个思路我们就可以不断的使用点击和鼠标移动的方式来绘制多边形。当然如果你是矩形或者圆形 的话思路是一样的,只不过画线的数学计算不一样,例如圆形的第一次点击是确定圆心的位置,鼠标的移动是为了确定圆的半径,第二次鼠标落下圆就画好了。

绘制多边形的核心代码如下:

function boxSelect(targetLayer) {
      return new Promise((resolve, reject) => {
        var Selected = true;
        map.doubleClickZoom.disable();
        map.getCanvas().style.cursor = "default";
        clearSelect();
        var jsonPoint = {
          type: "FeatureCollection",
          features: [],
        };
        var jsonLine = {
          type: "FeatureCollection",
          features: [],
        };
        var points = [];
        var ele = document.createElement("div");
        ele.setAttribute("class", "measure-result");
        const option = {
          element: ele,
          anchor: "left",
          offset: [8, 0],
        };
        var markers = [];
        var tooltip = new mapboxgl.Marker(option).setLngLat([0, 0]).addTo(map);
        markers.push(tooltip);
        var source = map.getSource("points-area");
        if (source) {
          map.getSource("points-area").setData(jsonPoint);
          map.getSource("line-area").setData(jsonLine);
        } else {
          map.addSource("points-area", {
            type: "geojson",
            data: jsonPoint,
          });
          map.addSource("line-area", {
            type: "geojson",
            data: jsonLine,
          });
          map.addLayer({
            id: "line-area",
            type: "fill",
            source: "line-area",
            paint: {
              "fill-color": "#006aff",
              "fill-opacity": 0.1,
            },
          });
          map.addLayer({
            id: "line-area-stroke",
            type: "line",
            source: "line-area",
            paint: {
              "line-color": "#006aff",
              "line-width": 2,
              "line-opacity": 0.65,
            },
          });
          map.addLayer({
            id: "points-area",
            type: "circle",
            source: "points-area",
            paint: {
              "circle-color": "#006aff",
              "circle-radius": 6,
            },
          });
        }

        function addPoint(coords) {
          jsonPoint.features.push({
            type: "Feature",
            geometry: {
              type: "Point",
              coordinates: coords,
            },
          });
          map.getSource("points-area").setData(jsonPoint);
        }

        map.on("click", function (_e) {
          if (Selected) {
            var coords = [_e.lngLat.lng, _e.lngLat.lat];
            points.push(coords);
            addPoint(coords);
          }
        });
        var boxResult = {};
        map.on("dblclick", function (_e) {
          if (Selected) {
            var coords = [_e.lngLat.lng, _e.lngLat.lat];
            points.push(coords);
            Selected = false;
            markers.forEach((f) => {
              f.remove();
            });
            boxResult.boxGeometry = map.getSource("line-area")["_data"];
            boxResult.area = turf.area(boxResult.boxGeometry);
            boxResult.selectedFeatures = execSelect(
              boxResult.boxGeometry,
              targetLayer
            );
            hightLightFeature(boxResult.selectedFeatures);
            resolve(boxResult);
          }
        });

        map.on("mousemove", function (_e) {
          if (Selected) {
            var coords = [_e.lngLat.lng, _e.lngLat.lat];
            var len = jsonPoint.features.length;
            if (len === 0) {
              ele.innerHTML = "点击绘制多边形";
            } else if (len === 1) {
              ele.innerHTML = "点击继续绘制";
            } else {
              var pts = points.concat([coords]);
              pts = pts.concat([points[0]]);
              var json = {
                type: "Feature",
                geometry: {
                  type: "Polygon",
                  coordinates: [pts],
                },
              };
              map.getSource("line-area").setData(json);
            }
            tooltip.setLngLat(coords);
          }
        });
      });
    }


画完多边形之后,就要进入查询的阶段了,查询的方式有很多,第一种你可以把画完的多边形的经纬度边界范围提交到服务端(自己的数据服务或者geoserver)进行查询,然后把结果返回到前端,第二种方式你可以采用turf.js直接在前端做查询,其实mapbox官方虽然没有提供范围查询的示例但是却提供了范围查询的api:

 

 只需要传入一个边界范围就可以进行查询。不过这个api是有两个坑的地方。

第一是这个api你传入的边界范围坐标必须是canvas坐标。经纬度是不行的。因此在传递范围的时候要这样写:

  var extent = turf.bbox(polygon);
      const features = map.queryRenderedFeatures(
        [
          map.project([extent[0], extent[1]]),
          map.project([extent[2], extent[3]]),
        ],
        { layers: [targetLayer] }
      );

第二个比较坑的地方是按照这个接口查询出来的结果并不准确,因为他选择的是外部边界的范围。就像下图的情况是很容易发生的:

 可以看到五边形之外的点也能够被选中高亮,因为这个接口他是按照多变形的外接矩形(图中橙色虚线)来选择的。外接矩形范围内的点都能够被选中。所以为了准确的计算结果,我们最好是接入turf.js的一个函数,叫做:pointsWithinPolygon。这个函数能够准确的获取多边形内部的点。所以我们可以采用这个函数来查询,也可以采用mapbox 的接口第一次检索一批结果。然后再使用这个函数进行过滤。看个人设计。

最后为大家附上框选范围查询的源码,大家只需要配置自己的mapbox的token就可以直接使用:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>框选查询</title>
    <link
      href="https://api.mapbox.com/mapbox-gl-js/v2.10.0/mapbox-gl.css"
      rel="stylesheet"
    />
    <script src="https://api.mapbox.com/mapbox-gl-js/v2.10.0/mapbox-gl.js"></script>
    <script src="./turf.min.js"></script>
    <script src="./token.js"></script>
    <script src="./poi.js"></script>
    <style>
      body {
        padding: 0;
        margin: 0;
      }
      .mapboxgl-ctrl-bottom-left {
        display: none !important;
      }
      #map {
        width: 100%;
        height: 100vh;
        top: 0;
        left: 0;
        position: absolute;
        z-index: 2;
      }
      #result {
        width: 500px;
        height: 450px;
        position: absolute;
        left: 20px;
        bottom: 30px;
        z-index: 10;
        background: rgba(255, 255, 255, 0.8);
        overflow: auto;
      }
      .btns {
        position: absolute;
        top: 50px;
        left: 50px;
        z-index: 10;
      }
      .measure-result {
        background-color: white;
        border-radius: 3px;
        height: 16px;
        line-height: 16px;
        padding: 0 3px;
        font-size: 12px;
        box-shadow: 0 0 0 1px #ccc;
        &.close {
          cursor: pointer;
          width: 20px;
          height: 20px;
          display: flex;
          align-items: center;
          justify-content: center;
        }
      }
    </style>
  </head>

  <body>
    <div id="map"></div>
    <div id="result"></div>
    <div class="btns">
      <button onclick="test()">开始框选</button
      ><button onclick="clearSelect()">清除结果</button>
    </div>
  </body>
  <script>
    mapboxgl.accessToken = token;
    const map = new mapboxgl.Map({
      center: [120.34233, 30.324342],
      attributionControl: false,
      style: "mapbox://styles/mapbox/satellite-v9",
      container: "map",
      zoom: 3,
    });
    map.on("load", (e) => {
      map.addSource("poi", { type: "geojson", data: poi });
      map.addLayer({
        id: "poi-layer",
        source: "poi",
        type: "circle",
        paint: { "circle-radius": 5, "circle-color": "yellow" },
      });
    });
    async function test() {
      let re = await boxSelect("poi-layer");
      document.getElementById("result").innerText = JSON.stringify(re, 2, null);
    }
    function boxSelect(targetLayer) {
      return new Promise((resolve, reject) => {
        var Selected = true;
        map.doubleClickZoom.disable();
        map.getCanvas().style.cursor = "default";
        clearSelect();
        var jsonPoint = {
          type: "FeatureCollection",
          features: [],
        };
        var jsonLine = {
          type: "FeatureCollection",
          features: [],
        };
        var points = [];
        var ele = document.createElement("div");
        ele.setAttribute("class", "measure-result");
        const option = {
          element: ele,
          anchor: "left",
          offset: [8, 0],
        };
        var markers = [];
        var tooltip = new mapboxgl.Marker(option).setLngLat([0, 0]).addTo(map);
        markers.push(tooltip);
        var source = map.getSource("points-area");
        if (source) {
          map.getSource("points-area").setData(jsonPoint);
          map.getSource("line-area").setData(jsonLine);
        } else {
          map.addSource("points-area", {
            type: "geojson",
            data: jsonPoint,
          });
          map.addSource("line-area", {
            type: "geojson",
            data: jsonLine,
          });
          map.addLayer({
            id: "line-area",
            type: "fill",
            source: "line-area",
            paint: {
              "fill-color": "#006aff",
              "fill-opacity": 0.1,
            },
          });
          map.addLayer({
            id: "line-area-stroke",
            type: "line",
            source: "line-area",
            paint: {
              "line-color": "#006aff",
              "line-width": 2,
              "line-opacity": 0.65,
            },
          });
          map.addLayer({
            id: "points-area",
            type: "circle",
            source: "points-area",
            paint: {
              "circle-color": "#006aff",
              "circle-radius": 6,
            },
          });
        }

        function addPoint(coords) {
          jsonPoint.features.push({
            type: "Feature",
            geometry: {
              type: "Point",
              coordinates: coords,
            },
          });
          map.getSource("points-area").setData(jsonPoint);
        }

        map.on("click", function (_e) {
          if (Selected) {
            var coords = [_e.lngLat.lng, _e.lngLat.lat];
            points.push(coords);
            addPoint(coords);
          }
        });
        var boxResult = {};
        map.on("dblclick", function (_e) {
          if (Selected) {
            var coords = [_e.lngLat.lng, _e.lngLat.lat];
            points.push(coords);
            Selected = false;
            markers.forEach((f) => {
              f.remove();
            });
            boxResult.boxGeometry = map.getSource("line-area")["_data"];
            boxResult.area = turf.area(boxResult.boxGeometry);
            boxResult.selectedFeatures = execSelect(
              boxResult.boxGeometry,
              targetLayer
            );
            hightLightFeature(boxResult.selectedFeatures);
            resolve(boxResult);
          }
        });

        map.on("mousemove", function (_e) {
          if (Selected) {
            var coords = [_e.lngLat.lng, _e.lngLat.lat];
            var len = jsonPoint.features.length;
            if (len === 0) {
              ele.innerHTML = "点击绘制多边形";
            } else if (len === 1) {
              ele.innerHTML = "点击继续绘制";
            } else {
              var pts = points.concat([coords]);
              pts = pts.concat([points[0]]);
              var json = {
                type: "Feature",
                geometry: {
                  type: "Polygon",
                  coordinates: [pts],
                },
              };
              map.getSource("line-area").setData(json);
            }
            tooltip.setLngLat(coords);
          }
        });
      });
    }
    function hightLightFeature(features, color = "#06ceff") {
      map.addSource("selected-source", {
        type: "geojson",
        data: {
          type: "FeatureCollection",
          features: [],
        },
      });
      map.addLayer({
        id: "selected-layer",
        type: "circle",
        source: "selected-source",
        paint: {
          "circle-radius": 8,
          "circle-color": color,
        },
      });
      map.getSource("selected-source").setData(features);
    }
    function execSelect(polygon, targetLayer) {
      var extent = turf.bbox(polygon);
      const features = map.queryRenderedFeatures(
        [
          map.project([extent[0], extent[1]]),
          map.project([extent[2], extent[3]]),
        ],
        { layers: [targetLayer] }
      );
      //优化一下,因为有的点不在多边形内,但是还是被选上了;
      const points = features.map((f) => f.geometry.coordinates);
      let withinFeature = turf.pointsWithinPolygon(
        turf.points(points),
        polygon
      );
      return withinFeature;
    }
    function clearSelect() {
      const dom = document.getElementsByClassName("measure-result");
      if (dom.length > 0) {
        for (let index = dom.length - 1; index > -1; index--) {
          dom[index].parentNode.removeChild(dom[index]);
        }
      }
      var source = map.getSource("points");
      var json = {
        type: "FeatureCollection",
        features: [],
      };
      if (source) {
        map.getSource("points").setData(json);
        map.getSource("line-move").setData(json);
        map.getSource("line").setData(json);
      }
      var sourceArea = map.getSource("points-area");
      if (sourceArea) {
        map.getSource("points-area").setData(json);
        map.getSource("line-area").setData(json);
      }
      if (map.getLayer("selected-layer")) {
        map.removeLayer("selected-layer");
        map.removeSource("selected-source");
      }
    }
  </script>
</html>

测试数据:

var poi = {
  type: "FeatureCollection",
  features: [
    {
      type: "Feature",
      properties: { type: "school", name: "susu学校" },
      geometry: {
        type: "Point",
        coordinates: [125.384449, 46.578032],
      },
    },
    {
      type: "Feature",
      properties: { type: "school", name: "混过你学校" },
      geometry: {
        type: "Point",
        coordinates: [122.58806, 50.438486],
      },
    },
    {
      type: "Feature",
      properties: { type: "hospital", name: "司机医院" },
      geometry: {
        type: "Point",
        coordinates: [117.583995, 44.877376],
      },
    },
    {
      type: "Feature",
      properties: { type: "school", name: "损害学校" },
      geometry: {
        type: "Point",
        coordinates: [125.384449, 46.578032],
      },
    },
    {
      type: "Feature",
      properties: { type: "hospital", name: "同样是医院" },
      geometry: {
        type: "Point",
        coordinates: [116.112212, 41.430537],
      },
    },
    {
      type: "Feature",
      properties: { type: "hospital", name: "输液医院" },
      geometry: {
        type: "Point",
        coordinates: [108.311758, 40.538283],
      },
    },
    {
      type: "Feature",
      properties: { type: "school", name: "USB学校" },
      geometry: {
        type: "Point",
        coordinates: [116.700925, 31.783546],
      },
    },
    {
      type: "Feature",
      properties: { type: "market", name: "哈哈超市" },
      geometry: {
        type: "Point",
        coordinates: [97.935682, 37.554957],
      },
    },
    {
      type: "Feature",
      properties: { type: "market", name: "低估超市" },
      geometry: {
        type: "Point",
        coordinates: [86.014234, 44.193101],
      },
    },
    {
      type: "Feature",
      properties: { type: "school", name: "嘎哈学校" },
      geometry: {
        type: "Point",
        coordinates: [85.351931, 32.783785],
      },
    },
    {
      type: "Feature",
      properties: { type: "hospital", name: "六医院" },
      geometry: {
        type: "Point",
        coordinates: [105.44178, 28.00127],
      },
    },
    {
      type: "Feature",
      properties: { type: "market", name: "超市不过" },
      geometry: {
        type: "Point",
        coordinates: [115.008374, 23.944745],
      },
    },
    {
      type: "Feature",
      properties: { type: "hospital", name: "玉兔医院" },
      geometry: {
        type: "Point",
        coordinates: [111.77045, 33.341497],
      },
    },
  ],
};


 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/27998.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

6道常见hadoop面试题及答案解析

Q1.什么是Hadoop&#xff1f;   Hadoop是一个开源软件框架&#xff0c;用于存储大量数据&#xff0c;并发处理/查询在具有多个商用硬件&#xff08;即低成本硬件&#xff09;节点的集群上的那些数据。总之&#xff0c;Hadoop包括以下内容&#xff1a;   HDFS&#xff08;Ha…

红外人体感应灯单片机开发方案

近来&#xff0c;红外人体感应灯受到了居家人们关注和喜爱。为此&#xff0c;宇凡微推出了一款低成本红外人体感应灯单片机方案。红外人体感应灯可应用于走廊、床边、楼梯、衣柜等地方&#xff0c;提供柔和照明作用。人来即亮&#xff0c;人走即灭&#xff0c;不受强光影响睡眠…

位姿估计 | 空间目标位姿估计方法分类总结

目录 前言位姿估计方法分类一、传统位姿估计方法1. 基于特征的位姿估计2. 基于模型的位姿估计 二、深度学习位姿估计方法 总结 前言 本文接着分享空间目标位姿跟踪和滤波算法中用到的一些常用内容&#xff0c;希望为后来者减少一些基础性内容的工作时间。以往分享总结见文章&a…

【C语言】整,浮点型数据存储,大小端。细节拉满!!

目录 一. 整型 1. C语言内置整型家族 类型的意义&#xff1a; 2.整型在内存如何存储的呢&#xff1f; 3. 原码&#xff0c;反码&#xff0c; 补码 原码 反码 补码 4. 当 整型遇上unsigned 会发生什么呢&#xff1f; 1. unsigned 与 signed 解析 2. printf 输出 有无…

【新版】系统架构设计师 - 信息安全技术基础知识

个人总结&#xff0c;仅供参考&#xff0c;欢迎加好友一起讨论 文章目录 架构 - 信息安全技术基础知识考点摘要信息安全基础知识信息安全系统的组成框架信息加密技术对称加密&#xff08;共享密钥&#xff09;非对称加密&#xff08;公开密钥&#xff09;信息摘要数字签名数字信…

IDEA安装教程2023

在本文中&#xff0c;我们将提供关于如何安装 IntelliJ IDEA 的详细步骤。如果您是初学者或只是想尝试一下 IDEA&#xff0c;我们建议您下载 Community 版。如果您需要更多高级功能&#xff0c;可以选择 Ultimate 版。 步骤一&#xff1a;下载 IntelliJ IDEA 首先&#xff0c;…

路漫漫其修远兮

其实不仅是专业&#xff0c;AI冲击波才刚刚开启&#xff0c;包括博客、自媒体作用也在大幅度下降呢。 很多人看过如下这幅图&#xff1a; 提示工程师确实是在当前大型语言模型不够完善的情况下&#xff0c;通过微调输入的方式来提高模型的性能。随着模型的迭代&#xff0c;这些…

功能测试如何转型自动化测试

在互联网行业&#xff0c;我们是那些被遗忘的技术人。 很多人都觉得&#xff0c;传统开发、运维才是技术含量的一个工作。 但是测试的入门门槛比较低&#xff0c;所做的事情相对有限&#xff0c; 这是我之前跟一些大型互联网软件测试负责人大牛们聊天的时候发现&#xff0c;…

lora,固定模特+固定衣服,怎么实现?

在电商行业&#xff0c;经常会有一个需求&#xff0c;就是把固定的衣服让模型穿上&#xff0c;然后拍很多的图片&#xff0c;放在商品主图、详情页、买家秀...... 人工智能发展到现在&#xff0c;最近aigc也挺热门的&#xff0c;有没有办法用“人工智能”算法就实现这个功能&a…

从1万到1亿需要多少个涨停板?(python)

如果本金只有1万元&#xff0c;需要多少个涨停板才可以到达一亿元呢&#xff1f; 亦或者&#xff0c;如果有一亿元本金&#xff0c;需要多少个跌停板才可以到达一万元。 注&#xff1a;涨停板&#xff08;10%&#xff09;&#xff0c;跌停板&#xff08;-10%&#xff09; 用到的…

在VSCode下利用PlateFormIO开发Arduino的MicroROS遇到的一些问题

文章目录 简介1.在第四节编译工程中&#xff0c;教程使用的vscode是有编译、上传的按钮的。但是我的没有。2.在【6.串口通信-接收实验】中&#xff0c;没有串行监视器&#xff08;Serial Monitor&#xff09;。3.关于trajectory_msgs/msg/joint_trajectory.hpp的相关问题4.关于…

PMP项目管理证书是什么?有什么用?

什么是PMP证书&#xff1f; PMP全称是Project Management Professional&#xff0c;中文全称叫项目管理专业人士资格认证&#xff0c;是由美国项目管理协会(PMI)发起&#xff0c;严格评估项目管理人员知识技能是否具有高品质的资格认证考试&#xff0c;目的是为了给项目管理人…

代码随想录|day13| 栈与队列part03 ● 239. 滑动窗口最大值● 347.前 K 个高频元素● 总结

239. 滑动窗口最大值--------知识点&#xff1a;单调队列 链接&#xff1a;代码随想录 自己写的&#xff0c;报错&#xff1a; class DandiaoQueue{//一个栈或者队列&#xff0c;基本要有进栈出栈两种操作&#xff0c;这里再加上pop出最大值一种操作//底层是deque public:deque…

机器学习笔记 - 多实例学习(MIL)弱监督学习

一、多实例学习概述 多实例学习(MIL)是一种弱监督学习形式,其中训练实例被排列在称为袋的集合中,并为整个袋提供标签。这种方式越来越受到人们的关注,因为它自然适合各种问题,并允许利用弱标记数据。因此,它被应用于计算机视觉和文档分类等不同的应用领域。 多实例学习(…

AntDB 企业增强特性介绍——同步异步自适应流复制

AntDB 提供了 hot-standby 的能力&#xff0c;功能与 Oracle 11g 的active standby 类似。并且通过流复制的方式&#xff0c;大大地缩短了备份库与主库的事务间隔。 传统流复制分为同步和异步两种模式。同步复制&#xff0c;即主机的事务要等到备机提交成功后才会提交并结束事…

使用Red Hat Insights注册RedHat系统

文章目录 前因Step 1: 确认所选择的系统Step 2: 将系统注册到Red Hat InsightsStep 3:具体操作演示 前因 使用SSH命令远程连接红帽系统&#xff0c;提示需要使用下面提示的命令进行系统注册订阅。 C:\Users\xyb>ssh -i xybdiy-aws-key.pem ec2-user18.179.118.78 The authen…

Mysql升级8.0后日期类型兼容性问题

背景 最近对原有项目数据库进行升级&#xff0c;从MySQL 5.7 升级到8.0&#xff0c;因此项目种的驱动程序也要做相应升级。 问题 一、 升级后报&#xff1a;java.time.LocalDateTime cannot be cast to java.util.Date 该问题是因为代码中使用Map类型获取查询返回值&#xf…

劝学:Android 14 Framework 引入了哪些“新”技术栈

作者&#xff1a;Mr_万能胶 2023 年 Google I/O 已于 2023 年 5 月 10 日 拉开帷幕&#xff0c;Android 14 Beta 版本近期也已经 释放到 Google partners&#xff0c;本文主要分析 Google 在 Android 14 框架代码中引入了哪些新的技术栈&#xff0c;而对于新功能和 API Change&…

Netty中ServerBootstrap类介绍

一、Netty基本介绍 Netty是由JBOSS提供的一个java开源框架。Netty提供异步的、事件驱动的网络应用程序框架和工具&#xff0c;用以快速开发高性能、高可靠性的网络服务器和客户端程序。Netty 在保证易于开发的同时还保证了其应用的性能&#xff0c;稳定性和伸缩性。 Netty 是一…

OpenShift Route 的实现原理

OpenShift Route 的实现原理 OpenShift 如何使用 HAProxy 实现 Router 和 RouteRouter appHAProxy 配置1. HTTP2. HTTPS OpenShift 中的 Route 解决了从集群外部访问服务的需求&#xff0c;与 Kubernetes 中的 Ingress 类似。 OpenShift 中 Route 资源 API 定义如下&#xff1…
最新文章