Openlayers调用ArcGis地图服务之五 —— 要素识别(/identify)

1.5 Openlayers调用ArcGis地图服务之要素识别(/identify)

下面使用ArcGis官方服务作为示例直接调用(如果使用自己的私有服务,可能先要获取token)

各个库版本如下:

javascript 复制代码
    "ol": "^10.8.0",
    "proj4": "^2.20.8",
    "vue3-openlayers": "^12.2.2"

目录

  • [1.5 要素识别【地图服务的查询接口(/identify)】](#1.5 要素识别【地图服务的查询接口(/identify)】 "#15-%E8%A6%81%E7%B4%A0%E8%AF%86%E5%88%AB%E5%9C%B0%E5%9B%BE%E6%9C%8D%E5%8A%A1%E7%9A%84%E6%9F%A5%E8%AF%A2%E6%8E%A5%E5%8F%A3identify")
    • [1.5.1 介绍](#1.5.1 介绍 "#151-%E4%BB%8B%E7%BB%8D")
    • [1.5.2 判断](#1.5.2 判断 "#152-%E5%88%A4%E6%96%AD")
    • [1.5.3 调用](#1.5.3 调用 "#153-%E8%B0%83%E7%94%A8")
      • [1.5.3.1 在线调用](#1.5.3.1 在线调用 "#1531-%E5%9C%A8%E7%BA%BF%E8%B0%83%E7%94%A8")
      • [1.5.3.2 Openlayers调用](#1.5.3.2 Openlayers调用 "#1532-openlayers%E8%B0%83%E7%94%A8")
      • [1.5.3.3 Vue3-Openlayers调用](#1.5.3.3 Vue3-Openlayers调用 "#1533-vue3-openlayers%E8%B0%83%E7%94%A8")
  • [1.6 简单对比(/find vs /identify vs /query)](#1.6 简单对比(/find vs /identify vs /query) "#16-%E7%AE%80%E5%8D%95%E5%AF%B9%E6%AF%94find-vs-identify-vs-query")

1.5 要素识别【地图服务的查询接口(/identify)】

1.5.1 介绍

要素识别主要用给定一个点(或几何),查找该位置上的要素,是"点击查询"功能的标准实现,可跨图层

1.5.1.1 核心用法

点选识别,查询地图上某一点或某一小区域内的要素,

1.5.1.2 关键参数
  • geometry:要查询的点的坐标(JSON格式)。

  • geometryType:几何类型,通常为 esriGeometryPoint。

  • layers:要查询的图层,可以是 all 或特定ID。

  • tolerance:容差(像素),即点击位置周围多大范围内的要素被视为命中。

  • mapExtent:当前地图范围,用于提高查询效率。

  • ImageDisplay:描述当前地图显示窗口的屏幕参数

1.5.2 判断

在服务信息页面ArcGis官方服务3转存失败,建议直接上传图片文件

可以看到Supported Operations中有Identify可以判断支持使用identify查询要素

1.5.3 调用
1.5.3.1 在线调用

点击Identify,进入在线调用页面

可以看到根据参数返回的要素信息

1.5.3.2 Openlayers调用

地图上点击到不同的要素,会红色高亮

javascript 复制代码
<template>
  <div class="map-page">
    <h1>OpenLayers - ArcGIS Identify 服务</h1>
    <p class="hint">点击地图查询要素信息</p>

    <!-- 要素信息面板 -->
    <div v-if="selectedFeature" class="info-panel">
      <div class="info-header">
        <h3>要素信息</h3>
        <button @click="closeInfo" class="close-btn">×</button>
      </div>
      <div class="info-content">
        <div class="info-item">
          <span class="info-label">图层:</span>
          <span class="info-value">{{ selectedFeature.layerName }}</span>
        </div>
        <div class="info-item">
          <span class="info-label">图层ID:</span>
          <span class="info-value">{{ selectedFeature.layerId }}</span>
        </div>
        <div v-if="selectedFeature.attributes" class="attributes">
          <h4>属性</h4>
          <div
            v-for="(value, key) in selectedFeature.attributes"
            :key="key"
            class="attr-item"
          >
            <span class="attr-key">{{ key }}:</span>
            <span class="attr-value">{{ value }}</span>
          </div>
        </div>
      </div>
    </div>

    <div id="ol-identify-map" ref="mapContainer" class="map-container"></div>
  </div>
</template>

<script setup lang="ts">
import { onMounted, onUnmounted, ref } from "vue";
import axios from "axios";
import Map from "ol/Map";
import View from "ol/View";
import VectorLayer from "ol/layer/Vector";
import VectorSource from "ol/source/Vector";
import ImageLayer from "ol/layer/Image";
import ImageArcGISRest from "ol/source/ImageArcGISRest";
import { Style, Circle, Fill, Stroke } from "ol/style";
import type { Geometry } from "ol/geom";
import Polygon from "ol/geom/Polygon";
import Feature from "ol/Feature";
import Point from "ol/geom/Point";
import LineString from "ol/geom/LineString";

const mapContainer = ref<HTMLDivElement>();
const selectedFeature = ref<any>(null);

let map: Map | null = null;

// ArcGIS MapServer 动态地图底图图层
const baseLayer = new ImageLayer({
  source: new ImageArcGISRest({
    ratio: 1,
    params: {},
    url: "https://sampleserver6.arcgisonline.com/arcgis/rest/services/USA/MapServer/export",
  }),
});

const resultLayer = new VectorLayer({
  source: new VectorSource(),
  style: (feature) => {
    const geometry = feature.getGeometry();
    if (!geometry) return undefined;

    const type = geometry.getType();

    // 点样式
    if (type === "Point") {
      return new Style({
        image: new Circle({
          radius: 12,
          fill: new Fill({ color: "rgba(255, 0, 0, 0.6)" }),
          stroke: new Stroke({ color: "#FF0000", width: 3 }),
        }),
      });
    }
    // 多点样式
    if (type === "MultiPoint") {
      return new Style({
        image: new Circle({
          radius: 12,
          fill: new Fill({ color: "rgba(255, 0, 0, 0.6)" }),
          stroke: new Stroke({ color: "#FF0000", width: 3 }),
        }),
      });
    }
    // 线样式
    if (type === "LineString") {
      return new Style({
        stroke: new Stroke({
          color: "#FF0000",
          width: 4,
        }),
      });
    }
    // 多线样式
    if (type === "MultiLineString") {
      return new Style({
        stroke: new Stroke({
          color: "#FF0000",
          width: 4,
        }),
      });
    }
    // 多边形样式
    if (type === "Polygon") {
      return new Style({
        stroke: new Stroke({
          color: "#FF0000",
          width: 3,
        }),
        fill: new Fill({
          color: "rgba(255, 0, 0, 0.3)",
        }),
      });
    }
    // 多多边形样式
    if (type === "MultiPolygon") {
      return new Style({
        stroke: new Stroke({
          color: "#FF0000",
          width: 3,
        }),
        fill: new Fill({
          color: "rgba(255, 0, 0, 0.3)",
        }),
      });
    }

    return new Style({
      stroke: new Stroke({ color: "#FF0000", width: 3 }),
      fill: new Fill({ color: "rgba(255, 0, 0, 0.3)" }),
      image: new Circle({
        radius: 10,
        fill: new Fill({ color: "rgba(255, 0, 0, 0.5)" }),
        stroke: new Stroke({ color: "#FF0000", width: 3 }),
      }),
    });
  },
  zIndex: 1000,
});

// Identify 功能
const identify = async (coordinate: number[]) => {
  try {
    const formData = new FormData();
    formData.append(
      "geometry",
      JSON.stringify({ x: coordinate[0], y: coordinate[1] }),
    );
    formData.append("geometryType", "esriGeometryPoint");
    formData.append("sr", "4326");
    formData.append("layers", "0,1,2,3");
    formData.append("layerDefs", "");
    formData.append("time", "");
    formData.append("timeRelation", "esriTimeRelationOverlaps");
    formData.append("layerTimeOptions", "");
    formData.append("tolerance", "1");
    formData.append(
      "mapExtent",
      "173.24789851593732,-69.43544422133593,-16.2692137558099,109.10900234289849",
    );
    formData.append("imageDisplay", "1920,960,96");
    formData.append("returnGeometry", "true");
    formData.append("maxAllowableOffset", "");
    formData.append("geometryPrecision", "");
    formData.append("dynamicLayers", "");
    formData.append("returnZ", "false");
    formData.append("returnM", "false");
    formData.append("gdbVersion", "");
    formData.append("historicMoment", "");
    formData.append("returnUnformattedValues", "false");
    formData.append("returnFieldName", "false");
    formData.append("datumTransformations", "");
    formData.append("layerParameterValues", "");
    formData.append("mapRangeValues", "");
    formData.append("layerRangeValues", "");
    formData.append("clipping", "");
    formData.append("spatialFilter", "");
    formData.append("f", "pjson");

    const response = await axios.post(
      "https://sampleserver6.arcgisonline.com/arcgis/rest/services/USA/MapServer/identify",
      formData,
    );

    const results = response.data.results || [];

    // 清除之前的高亮
    const source = resultLayer.getSource();
    source?.clear();

    if (results.length > 0) {
      // 显示第一个结果
      const result = results[0];
      selectedFeature.value = {
        layerName: result.layerName,
        layerId: result.layerId,
        attributes: result.attributes,
      };

      console.log("Identify result:", result);
      console.log("Geometry type:", result.geometryType);
      console.log("Geometry:", result.geometry);

      // 高亮显示要素
      if (result.geometry) {
        const geometry = result.geometry;
        const geometryType = result.geometryType;

        if (
          geometryType === "esriGeometryPoint" &&
          geometry.x != null &&
          geometry.y != null
        ) {
          const feature = new Feature({
            geometry: new Point([Number(geometry.x), Number(geometry.y)]),
          });
          source?.addFeature(feature);
          console.log("Added point feature at:", [geometry.x, geometry.y]);
        } else if (geometryType === "esriGeometryPolygon" && geometry.rings) {
          geometry.rings.forEach((ring: number[][]) => {
            const coords = ring.map((coord) => [
              Number(coord[0]),
              Number(coord[1]),
            ]);
            const polygonFeature = new Feature({
              geometry: new Polygon([coords]),
            });
            source?.addFeature(polygonFeature);
          });
          console.log("Added polygon features");
        } else if (geometryType === "esriGeometryPolyline" && geometry.paths) {
          geometry.paths.forEach((path: number[][]) => {
            const coords = path.map((coord) => [
              Number(coord[0]),
              Number(coord[1]),
            ]);
            const lineFeature = new Feature({
              geometry: new LineString(coords),
            });
            source?.addFeature(lineFeature);
          });
          console.log("Added line features");
        } else if (
          geometryType === "esriGeometryMultipoint" &&
          geometry.points
        ) {
          // 处理多点几何
          geometry.points.forEach((point: number[]) => {
            const pointFeature = new Feature({
              geometry: new Point([Number(point[0]), Number(point[1])]),
            });
            source?.addFeature(pointFeature);
          });
          console.log("Added multipoint features");
        }
      }

      // 输出添加后的要素数量
      console.log("Total features in source:", source?.getFeatures().length);
    } else {
      selectedFeature.value = null;
      console.log("No results found");
    }
  } catch (err: any) {
    console.error("Identify error:", err);
  }
};

// 关闭信息面板
const closeInfo = () => {
  selectedFeature.value = null;
  const source = resultLayer.getSource();
  source?.clear();
};

onMounted(() => {
  // Create map
  map = new Map({
    target: mapContainer.value!,
    layers: [baseLayer, resultLayer],
    view: new View({
      projection: "EPSG:4326",
      center: [-98.5795, 39.8283], // USA center (lon, lat)
      zoom: 4,
    }),
  });

  // 点击事件
  map.on("singleclick", (evt) => {
    const coordinate = evt.coordinate;
    identify(coordinate);
  });
});

onUnmounted(() => {
  if (map) {
    map.setTarget(undefined);
    map = null;
  }
});
</script>

<style scoped>
.map-page {
  padding: 20px;
}

h1 {
  margin-bottom: 10px;
  color: #333;
}

.hint {
  margin-bottom: 15px;
  color: #666;
  font-size: 14px;
}

.info-panel {
  position: absolute;
  top: 80px;
  right: 30px;
  width: 320px;
  max-height: 500px;
  background: white;
  border-radius: 8px;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
  overflow: hidden;
  z-index: 1000;
}

.info-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 15px;
  background-color: #42b983;
  color: white;
}

.info-header h3 {
  margin: 0;
  font-size: 16px;
}

.close-btn {
  background: none;
  border: none;
  color: white;
  font-size: 24px;
  cursor: pointer;
  padding: 0;
  width: 24px;
  height: 24px;
  line-height: 1;
}

.close-btn:hover {
  opacity: 0.8;
}

.info-content {
  padding: 15px;
  max-height: 420px;
  overflow-y: auto;
}

.info-item {
  display: flex;
  padding: 8px 0;
  border-bottom: 1px solid #eee;
}

.info-label {
  font-weight: 600;
  color: #666;
  min-width: 80px;
}

.info-value {
  color: #333;
  flex: 1;
}

.attributes h4 {
  margin: 15px 0 10px 0;
  font-size: 14px;
  color: #666;
  border-bottom: 1px solid #ddd;
  padding-bottom: 5px;
}

.attr-item {
  display: flex;
  padding: 6px 0;
  font-size: 13px;
}

.attr-key {
  font-weight: 500;
  color: #666;
  min-width: 100px;
  word-break: break-word;
}

.attr-value {
  color: #333;
  flex: 1;
  word-break: break-word;
}

.map-container {
  width: 100%;
  height: 600px;
  border: 2px solid #ddd;
  border-radius: 8px;
  background-color: #f5f5f5;
}
</style>
1.5.3.3 Vue3-Openlayers调用

地图上点击到不同的要素,会红色高亮

javascript 复制代码
<template>
  <div class="map-page">
    <h1>Vue3-OpenLayers - ArcGIS Identify 服务</h1>
    <p class="hint">点击地图查询要素信息</p>

    <!-- 要素信息面板 -->
    <div v-if="selectedFeature" class="info-panel">
      <div class="info-header">
        <h3>要素信息</h3>
        <button @click="closeInfo" class="close-btn">×</button>
      </div>
      <div class="info-content">
        <div class="info-item">
          <span class="info-label">图层:</span>
          <span class="info-value">{{ selectedFeature.layerName }}</span>
        </div>
        <div class="info-item">
          <span class="info-label">图层ID:</span>
          <span class="info-value">{{ selectedFeature.layerId }}</span>
        </div>
        <div v-if="selectedFeature.attributes" class="attributes">
          <h4>属性</h4>
          <div
            v-for="(value, key) in selectedFeature.attributes"
            :key="key"
            class="attr-item"
          >
            <span class="attr-key">{{ key }}:</span>
            <span class="attr-value">{{ value }}</span>
          </div>
        </div>
      </div>
    </div>

    <ol-map
      ref="mapRef"
      :loadTilesWhileAnimating="true"
      :loadTilesWhileInteracting="true"
      style="
        height: 600px;
        width: 100%;
        border: 2px solid #ddd;
        border-radius: 8px;
      "
      @singleclick="handleMapClick"
    >
      <ol-view
        ref="viewRef"
        :center="center"
        :zoom="4"
        :projection="projection"
      />

      <!-- ArcGIS MapServer 动态地图底图图层 -->
      <ol-tile-layer>
        <OlSourceTileArcGISRest
          url="https://sampleserver6.arcgisonline.com/arcgis/rest/services/USA/MapServer/export"
        />
      </ol-tile-layer>

      <ol-vector-layer :z-index="1000">
        <ol-source-vector ref="vectorSourceRef" />
      </ol-vector-layer>
    </ol-map>
  </div>
</template>

<script setup lang="ts">
import { ref } from "vue";
import axios from "axios";
import Point from "ol/geom/Point";
import Polygon from "ol/geom/Polygon";
import LineString from "ol/geom/LineString";
import Feature from "ol/Feature";
import type { MapBrowserEvent } from "ol";
import { Style, Circle, Fill, Stroke } from "ol/style";

const projection = "EPSG:4326";
const center = ref([-98.5795, 39.8283]); // USA center (lon, lat)
const selectedFeature = ref<any>(null);
const vectorSourceRef = ref();
const viewRef = ref();

// Identify 功能
const identify = async (coordinate: number[]) => {
  try {
    const formData = new FormData();
    formData.append(
      "geometry",
      JSON.stringify({ x: coordinate[0], y: coordinate[1] }),
    );
    formData.append("geometryType", "esriGeometryPoint");
    formData.append("sr", "4326");
    formData.append("layers", "0,1,2,3");
    formData.append("layerDefs", "");
    formData.append("time", "");
    formData.append("timeRelation", "esriTimeRelationOverlaps");
    formData.append("layerTimeOptions", "");
    formData.append("tolerance", "1");
    formData.append(
      "mapExtent",
      "173.24789851593732,-69.43544422133593,-16.2692137558099,109.10900234289849",
    );
    formData.append("imageDisplay", "1920,960,96");
    formData.append("returnGeometry", "true");
    formData.append("maxAllowableOffset", "");
    formData.append("geometryPrecision", "");
    formData.append("dynamicLayers", "");
    formData.append("returnZ", "false");
    formData.append("returnM", "false");
    formData.append("gdbVersion", "");
    formData.append("historicMoment", "");
    formData.append("returnUnformattedValues", "false");
    formData.append("returnFieldName", "false");
    formData.append("datumTransformations", "");
    formData.append("layerParameterValues", "");
    formData.append("mapRangeValues", "");
    formData.append("layerRangeValues", "");
    formData.append("clipping", "");
    formData.append("spatialFilter", "");
    formData.append("f", "pjson");

    const response = await axios.post(
      "https://sampleserver6.arcgisonline.com/arcgis/rest/services/USA/MapServer/identify",
      formData,
    );

    const results = response.data.results || [];

    // 清除之前的高亮
    const source = vectorSourceRef.value?.source;
    if (source) {
      source.clear();
    }

    if (results.length > 0) {
      // 显示第一个结果
      const result = results[0];
      selectedFeature.value = {
        layerName: result.layerName,
        layerId: result.layerId,
        attributes: result.attributes,
      };

      console.log("Identify result:", result);
      console.log("Geometry type:", result.geometryType);
      console.log("Geometry:", result.geometry);

      // 高亮显示要素
      if (result.geometry) {
        const geometry = result.geometry;
        const geometryType = result.geometryType;

        const features: Feature[] = [];

        if (
          geometryType === "esriGeometryPoint" &&
          geometry.x != null &&
          geometry.y != null
        ) {
          const pointFeature = new Feature({
            geometry: new Point([Number(geometry.x), Number(geometry.y)]),
          });
          features.push(pointFeature);
          console.log("Added point feature at:", [geometry.x, geometry.y]);
        } else if (geometryType === "esriGeometryPolygon" && geometry.rings) {
          geometry.rings.forEach((ring: number[][]) => {
            const coords = ring.map((coord) => [
              Number(coord[0]),
              Number(coord[1]),
            ]);
            const polygonFeature = new Feature({
              geometry: new Polygon([coords]),
            });
            features.push(polygonFeature);
          });
          console.log("Added polygon features");
        } else if (geometryType === "esriGeometryPolyline" && geometry.paths) {
          geometry.paths.forEach((path: number[][]) => {
            const coords = path.map((coord) => [
              Number(coord[0]),
              Number(coord[1]),
            ]);
            const lineFeature = new Feature({
              geometry: new LineString(coords),
            });
            features.push(lineFeature);
          });
          console.log("Added line features");
        } else if (
          geometryType === "esriGeometryMultipoint" &&
          geometry.points
        ) {
          // 处理多点几何
          geometry.points.forEach((point: number[]) => {
            const pointFeature = new Feature({
              geometry: new Point([Number(point[0]), Number(point[1])]),
            });
            features.push(pointFeature);
          });
          console.log("Added multipoint features");
        }

        // 为每个要素设置样式
        features.forEach(setFeatureStyle);

        if (source) {
          source.addFeatures(features);
        }

        console.log("Total features in source:", source.getFeatures().length);
      }
    } else {
      selectedFeature.value = null;
      console.log("No results found");
    }
  } catch (err: any) {
    console.error("Identify error:", err);
  }
};

// 处理地图点击
const handleMapClick = (event: MapBrowserEvent<any>) => {
  const coordinate = event.coordinate;
  identify(coordinate);
};

// 关闭信息面板
const closeInfo = () => {
  selectedFeature.value = null;
  const source = vectorSourceRef.value?.source;
  if (source) {
    source.clear();
  }
};

// 为要素设置样式的函数
const setFeatureStyle = (feature: Feature) => {
  const geometry = feature.getGeometry();
  if (!geometry) return;

  const type = geometry.getType();

  // 点样式
  if (type === "Point") {
    feature.setStyle(
      new Style({
        image: new Circle({
          radius: 12,
          fill: new Fill({ color: "rgba(255, 0, 0, 0.6)" }),
          stroke: new Stroke({ color: "#FF0000", width: 3 }),
        }),
      }),
    );
  }
  // 多点样式
  else if (type === "MultiPoint") {
    feature.setStyle(
      new Style({
        image: new Circle({
          radius: 12,
          fill: new Fill({ color: "rgba(255, 0, 0, 0.6)" }),
          stroke: new Stroke({ color: "#FF0000", width: 3 }),
        }),
      }),
    );
  }
  // 线样式
  else if (type === "LineString" || type === "MultiLineString") {
    feature.setStyle(
      new Style({
        stroke: new Stroke({
          color: "#FF0000",
          width: 4,
        }),
      }),
    );
  }
  // 多边形样式
  else if (type === "Polygon" || type === "MultiPolygon") {
    feature.setStyle(
      new Style({
        stroke: new Stroke({
          color: "#FF0000",
          width: 3,
        }),
        fill: new Fill({
          color: "rgba(255, 0, 0, 0.3)",
        }),
      }),
    );
  }
};
</script>

<style scoped>
.map-page {
  padding: 20px;
  position: relative;
}

h1 {
  margin-bottom: 10px;
  color: #333;
}

.hint {
  margin-bottom: 15px;
  color: #666;
  font-size: 14px;
}

.info-panel {
  position: absolute;
  top: 80px;
  right: 30px;
  width: 320px;
  max-height: 500px;
  background: white;
  border-radius: 8px;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
  overflow: hidden;
  z-index: 1000;
}

.info-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 15px;
  background-color: #42b983;
  color: white;
}

.info-header h3 {
  margin: 0;
  font-size: 16px;
}

.close-btn {
  background: none;
  border: none;
  color: white;
  font-size: 24px;
  cursor: pointer;
  padding: 0;
  width: 24px;
  height: 24px;
  line-height: 1;
}

.close-btn:hover {
  opacity: 0.8;
}

.info-content {
  padding: 15px;
  max-height: 420px;
  overflow-y: auto;
}

.info-item {
  display: flex;
  padding: 8px 0;
  border-bottom: 1px solid #eee;
}

.info-label {
  font-weight: 600;
  color: #666;
  min-width: 80px;
}

.info-value {
  color: #333;
  flex: 1;
}

.attributes h4 {
  margin: 15px 0 10px 0;
  font-size: 14px;
  color: #666;
  border-bottom: 1px solid #ddd;
  padding-bottom: 5px;
}

.attr-item {
  display: flex;
  padding: 6px 0;
  font-size: 13px;
}

.attr-key {
  font-weight: 500;
  color: #666;
  min-width: 100px;
  word-break: break-word;
}

.attr-value {
  color: #333;
  flex: 1;
  word-break: break-word;
}
</style>

1.6 简单对比(/find vs /identify vs /query)

特性 /find (查找) /identify (识别) /query (查询)
核心功能 文本匹配,在多个图层的多个字段中查找关键词 点选识别,查询地图上某一点或某一小区域内的要素 结构化查询,支持复杂的属性、空间及统计分析
主要场景 实现"搜索框"功能,例如按名称、代码模糊搜索地物 实现"点击查询"功能,例如点击地图查看该位置的POI信息 实现"条件筛选"、"分类统计"、"空间分析"等功能
空间过滤 不支持 支持,以点或小范围几何作为过滤条件 支持,功能强大且灵活
跨图层 支持,一次可搜索多个图层 支持,可指定一个或多个图层 不支持,一次只能查询单个图层

简单来说:

  • 想要一个类似百度地图的搜索框?用 /find。

  • 想要实现点击地图查看信息?用 /identify。

  • 想要做复杂的条件筛选、统计或空间分析?用 /query

相关推荐
Dxy12393102162 小时前
HTML 如何设置 Div 阴影悬浮边框:从基础到进阶
前端·html·css3
好运的阿财2 小时前
OpenClaw工具拆解之browser+agents_list
前端·人工智能·机器学习·开源软件·ai编程·openclaw·openclaw工具
JarvanMo2 小时前
八个开源Flutter应用,让你成为更好的开发者
前端
ZC跨境爬虫2 小时前
Apple官网复刻第二阶段day_2:(前端模块化还原苹果官网WATCH海报)
前端·ui·重构·html·状态模式
Rabbit_QL2 小时前
【前端基础】npm install 是干嘛的(带参数 vs 不带参数)
前端·npm·node.js
这是程序猿2 小时前
MySQL 索引一篇讲透:原理、分类、优化与面试总结
java·前端·mysql
IT_陈寒2 小时前
被JavaScript的隐式类型转换坑到怀疑人生
前端·人工智能·后端
Highcharts.js2 小时前
实战指南:如何构建一套全平台适配的响应式图表系统?
前端·javascript·highcharts·实战代码·响应式图表
lihaozecq2 小时前
我用 1 个月写了一个 Web AI Coding Agent,今天开源 —— code-artisan
前端·agent·ai编程