OpenLayers 综合案例-台风风场模拟

看过的知识不等于学会。唯有用心总结、系统记录,并通过温故知新反复实践,才能真正掌握一二

作为一名摸爬滚打三年的前端开发,开源社区给了我饭碗,我也将所学的知识体系回馈给大家,助你少走弯路!
OpenLayers、Leaflet 快速入门 ,每周保持更新 2 个案例
Cesium 快速入门,每周保持更新 4 个案例

OpenLayers 综合案例-台风风场模拟

Vue 3 + OpenLayers 实现的 WebGIS 应用提供了完整的台风风场模拟功能

主要功能

  1. 台风的完整路径以红色虚线在地图上绘制,路径上的历史和预测点用绿色圆点标记
  2. 当前台风中心点使用一个自定义图标(风圈.png)表示,并会沿着预设路径移动
  3. 应用通过大量蓝色线条模拟风场粒子,这些粒子围绕台风中心以漩涡状向内移动,并带有阻尼效果,模拟真实的台风风场
  4. 风场动画使用 requestAnimationFrame 实现平滑的粒子运动
  5. 用户在地图上按下鼠标时,台风路径动画会暂停;鼠标抬起时,动画会恢复,方便用户观察地图

MP4效果动画链接地址

技术栈

该环境下代码即拿即用

bash 复制代码
Vue 3.5.13+
Openlayers 10.5.0+
Vite 6.3.5+
vue 复制代码
<template>
  <div class="container">
    <div id="map" class="map"></div>
    <!-- 控制面板 -->
    <div class="controls-panel">
      <h2 class="panel-title">图层控制</h2>
      <!-- 台风路径不再有开关,始终显示 -->
      <label class="checkbox-label">
        <!-- 仅作显示,无实际控制功能 -->
        <input type="checkbox" checked disabled class="checkbox-input" />
        <span class="checkbox-text">台风路径 (始终显示)</span>
      </label>
    </div>
    <!-- 台风信息卡片 -->
    <div v-if="selectedTyphoon" class="info-card">
      <h3 class="card-title">
        {{ selectedTyphoon.name }} - {{ selectedTyphoon.time }}
      </h3>
      <div class="card-content">
        <p>
          <strong>中心位置:</strong> 东经{{ selectedTyphoon.lon }}° 北纬{{
            selectedTyphoon.lat
          }}°
        </p>
        <p><strong>中心气压:</strong> {{ selectedTyphoon.pressure }}百帕</p>
        <p>
          <strong>风速风力:</strong> {{ selectedTyphoon.windSpeed }}米/秒,
          {{ selectedTyphoon.windForce }}级({{ selectedTyphoon.windType }})
        </p>
        <p><strong>当前位置:</strong> {{ selectedTyphoon.currentLocation }}</p>
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, onMounted, onUnmounted, watch } from "vue";
import Map from "ol/Map.js";
import View from "ol/View.js";
import TileLayer from "ol/layer/Tile.js";
import VectorLayer from "ol/layer/Vector.js";
import XYZ from "ol/source/XYZ.js";
import VectorSource from "ol/source/Vector.js";
import Feature from "ol/Feature.js";
import Point from "ol/geom/Point.js";
import LineString from "ol/geom/LineString.js";
import Circle from "ol/geom/Circle.js";
import { fromLonLat } from "ol/proj.js"; // 用于经纬度坐标与地图投影坐标之间的转换
import { Style, Stroke, Fill, Circle as CircleStyle, Icon } from "ol/style.js"; // OpenLayers 样式相关模块,新增 Icon
import "ol/ol.css"; // OpenLayers 默认样式

// OpenLayers 地图实例
let map = null;
// 台风路径和中心点图层
let typhoonTrackLayer = null;
// 风场模拟图层
let windFieldLayer = null;
// 风场数据源,用于存储风场粒子要素
let windFieldSource = null;
// 风场动画帧请求ID,用于取消 requestAnimationFrame 动画
let animationFrameId = null;
// 台风路径动画的定时器ID,用于清除 setInterval 定时器
let typhoonAnimationIntervalId = null;

// 当前选中的台风信息,用于信息卡片显示,响应式更新
const selectedTyphoon = ref(null);
// 台风路径动画的当前索引,用于追踪台风在路径上的位置
const typhoonPathIndex = ref(0);

// 模拟台风数据
const typhoonData = {
  name: "竹节草", // 台风名称
  // 模拟的台风路径点数组,每个点包含经纬度、时间、气压、风速风力等信息
  path: [
    {
      lon: 118.0,
      lat: 20.0,
      time: "7月28日0时",
      pressure: 1000,
      windSpeed: 15,
      windForce: 7,
      windType: "热带低压",
      currentLocation: "位于南海中部",
    },
    {
      lon: 119.5,
      lat: 21.5,
      time: "7月28日8时",
      pressure: 998,
      windSpeed: 16,
      windForce: 7,
      windType: "热带低压",
      currentLocation: "位于南海北部",
    },
    {
      lon: 121.0,
      lat: 23.0,
      time: "7月28日16时",
      pressure: 995,
      windSpeed: 17,
      windForce: 8,
      windType: "热带风暴",
      currentLocation: "位于巴士海峡",
    },
    {
      lon: 122.5,
      lat: 24.5,
      time: "7月29日0时",
      pressure: 993,
      windSpeed: 18,
      windForce: 8,
      windType: "热带风暴",
      currentLocation: "位于台湾以东洋面",
    },
    {
      lon: 123.8,
      lat: 26.0,
      time: "7月29日4时",
      pressure: 992,
      windSpeed: 18,
      windForce: 8,
      windType: "热带风暴",
      currentLocation: "位于台湾东北部洋面",
    },
    {
      lon: 124.9,
      lat: 27.3,
      time: "7月29日8时",
      pressure: 992,
      windSpeed: 18,
      windForce: 8,
      windType: "热带风暴",
      currentLocation: "当前位于东海,距离登陆点上海市东南方向549公里",
    }, // 当前位置
    {
      lon: 126.0,
      lat: 28.5,
      time: "7月29日16时",
      pressure: 990,
      windSpeed: 20,
      windForce: 9,
      windType: "热带风暴",
      currentLocation: "预计向西北方向移动",
    }, // 预测
    {
      lon: 127.5,
      lat: 29.5,
      time: "7月30日0时",
      pressure: 988,
      windSpeed: 22,
      windForce: 9,
      windType: "热带风暴",
      currentLocation: "预计在东海北部",
    },
    {
      lon: 129.0,
      lat: 30.0,
      time: "7月30日8时",
      pressure: 985,
      windSpeed: 25,
      windForce: 10,
      windType: "强热带风暴",
      currentLocation: "预计在黄海南部",
    },
  ],
  windRadius: 300000, // 7级风圈半径,单位米
};

// 风场粒子数组,存储每个粒子的属性和对应的 OpenLayers Feature
const windParticles = [];
// 风场粒子数量
const NUM_WIND_PARTICLES = 1500; // 增加粒子数量以提高密度
// 风场粒子线段长度(模拟),增加长度使其更明显
const WIND_PARTICLE_LENGTH = 10000; // 米 (10公里)

// 风场行为的新常量(实现更强的漩涡和向内拉力)
const WIND_FIELD_MAX_RADIUS = typhoonData.windRadius * 3.5; // 粒子最大半径 (1050公里)
const WIND_FIELD_MIN_RADIUS = typhoonData.windRadius * 0.05; // 粒子最小半径 (15公里),允许粒子更靠近中心
const INWARD_ACCELERATION = 2.0; // 每帧粒子向内拉力的强度(已增加)
const SWIRL_ACCELERATION = 4.0; // 每帧粒子漩涡(切向力)的强度(已增加)
const MAX_PARTICLE_SPEED = 1000; // 粒子最大速度(米/帧)(已增加)
const MIN_PARTICLE_SPEED = 100; // 粒子最小速度(米/帧)(已增加)
const DRAG_FACTOR = 0.97; // 阻尼/拖曳因子,防止无限加速(略微减小阻力)

// 用于更轻松更新的点要素引用
let typhoonPointFeatures = [];
let windCircleFeature = null;

// 台风中心点图片 URL,请替换为您的图片路径
const TYPHOON_ICON_URL = "/src/assets/风圈.png"; // 示例占位符图片

onMounted(() => {
  // 基础瓦片图层 (高德地图)
  const gaodeLayer = new TileLayer({
    source: new XYZ({
      url: "https://webrd04.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=1&style=7&x={x}&y={y}&z={z}",
      crossOrigin: "anonymous", // 允许跨域加载瓦片,避免图片加载问题
    }),
  });

  // 台风路径图层的数据源
  const typhoonSource = new VectorSource();
  typhoonTrackLayer = new VectorLayer({
    source: typhoonSource,
    // 根据要素类型设置不同的样式
    style: (feature) => {
      if (feature.get("type") === "path") {
        // 台风路径线样式
        return new Style({
          stroke: new Stroke({
            color: "red",
            width: 3,
            lineDash: [10, 10], // 虚线效果
          }),
        });
      } else if (feature.get("type") === "current") {
        // 当前台风中心点样式(使用circle代替)
        // return new Style({
        //   image: new CircleStyle({
        //     radius: 10,
        //     fill: new Fill({ color: "rgba(0, 150, 255, 0.8)" }), // 蓝色填充
        //     stroke: new Stroke({ color: "white", width: 2 }), // 白色边框
        //   }),
        // });
        // 当前台风中心点样式 (使用图片)
        return new Style({
          image: new Icon({
            anchor: [0.5, 0.5], // 图片中心与坐标点对齐
            anchorXUnits: "fraction",
            anchorYUnits: "fraction",
            src: TYPHOON_ICON_URL, // 您的图片 URL
            scale: 3.5, // 根据图片实际大小调整缩放比例
          }),
        });
      } else if (feature.get("type") === "point") {
        // 路径上的历史/预测点样式
        return new Style({
          image: new CircleStyle({
            radius: 5,
            fill: new Fill({ color: "rgba(0, 200, 0, 0.8)" }), // 绿色填充
            stroke: new Stroke({ color: "white", width: 1 }), // 白色边框
          }),
        });
      } else if (feature.get("type") === "windCircle") {
        // 风圈样式
        return new Style({
          stroke: new Stroke({
            color: "rgba(0, 255, 0, 0.6)", // 绿色边框
            width: 2,
          }),
          fill: new Fill({
            color: "rgba(0, 255, 0, 0.1)", // 半透明绿色填充
          }),
        });
      }
      return null;
    },
  });

  // 初始化台风路径和点要素
  const initialPathCoordinates = typhoonData.path.map((p) =>
    fromLonLat([p.lon, p.lat])
  );
  // 创建台风路径线要素
  const pathFeature = new Feature({
    geometry: new LineString(initialPathCoordinates),
    type: "path",
  });
  typhoonSource.addFeature(pathFeature);

  // 创建台风路径上的所有点要素并存储引用
  typhoonData.path.forEach((p, i) => {
    const pointFeature = new Feature({
      geometry: new Point(fromLonLat([p.lon, p.lat])),
      type: "point", // 初始都设为普通点
    });
    typhoonSource.addFeature(pointFeature);
    typhoonPointFeatures.push(pointFeature);
  });

  // 获取当前台风中心点要素和风圈要素的引用,方便后续动画更新
  if (typhoonPointFeatures[typhoonPathIndex.value]) {
    typhoonPointFeatures[typhoonPathIndex.value].set("type", "current"); // 设置初始当前点类型
  }

  // 创建风圈要素
  windCircleFeature = new Feature({
    geometry: new Circle(
      fromLonLat([
        typhoonData.path[typhoonPathIndex.value].lon,
        typhoonData.path[typhoonPathIndex.value].lat,
      ]),
      typhoonData.windRadius
    ),
    type: "windCircle",
  });
  typhoonSource.addFeature(windCircleFeature);

  // 初始化台风信息卡片显示为路径的第一个点数据
  selectedTyphoon.value = {
    ...typhoonData.path[typhoonPathIndex.value],
    name: typhoonData.name,
  };

  // 风场图层的数据源
  windFieldSource = new VectorSource();
  windFieldLayer = new VectorLayer({
    source: windFieldSource,
    style: new Style({
      stroke: new Stroke({
        color: "rgba(0, 150, 255, 0.8)", // 风线颜色略微加深
        width: 2, // 增加风线宽度
      }),
    }),
  });

  // 初始化风场粒子,并添加到风场数据源
  for (let i = 0; i < NUM_WIND_PARTICLES; i++) {
    resetWindParticle(i); // 调用重置函数,它会创建并添加新的Feature
  }

  // 创建地图实例
  map = new Map({
    layers: [gaodeLayer, typhoonTrackLayer, windFieldLayer], // 添加所有图层到地图
    target: "map", // 地图容器的DOM元素ID
    view: new View({
      center: fromLonLat([
        typhoonData.path[typhoonPathIndex.value].lon,
        typhoonData.path[typhoonPathIndex.value].lat,
      ]), // 初始地图中心为台风起始点
      zoom: 6, // 初始缩放级别
      maxZoom: 18, // 最大缩放级别
    }),
  });

  // 鼠标按下时停止定时器,抬起时启用定时器
  map.on("pointerdown", () => {
    if (typhoonAnimationIntervalId) {
      clearInterval(typhoonAnimationIntervalId);
      typhoonAnimationIntervalId = null;
    }
  });
  map.on("pointerup", () => {
    if (!typhoonAnimationIntervalId) {
      startTyphoonAnimation(); // 恢复台风路径动画
    }
  });

  // 初始状态下,启动风场动画
  startWindAnimation();
  // 启动台风路径动画
  startTyphoonAnimation();
});

// 组件卸载时清理资源,防止内存泄漏
onUnmounted(() => {
  if (map) {
    map.setTarget(null); // 解除地图与DOM元素的绑定
    map = null;
  }
  stopWindAnimation(); // 停止风场动画
  stopTyphoonAnimation(); // 停止台风路径动画
});

/**
 * 重置单个风场粒子位置
 * 当粒子超出范围或需要重新生成时调用
 * @param {number} index - 粒子在 windParticles 数组中的索引
 */
function resetWindParticle(index) {
  const currentTyphoonCenter = fromLonLat([
    selectedTyphoon.value.lon,
    selectedTyphoon.value.lat,
  ]);

  // 在环形区域 [最小半径, 最大半径] 内生成随机位置
  const angle = Math.random() * 2 * Math.PI;
  const distance =
    WIND_FIELD_MIN_RADIUS +
    Math.random() * (WIND_FIELD_MAX_RADIUS - WIND_FIELD_MIN_RADIUS);

  const startX = currentTyphoonCenter[0] + distance * Math.cos(angle);
  const startY = currentTyphoonCenter[1] + distance * Math.sin(angle);

  // 初始速度(在范围内随机速度,随机方向)
  let initialSpeed =
    MIN_PARTICLE_SPEED +
    Math.random() * (MAX_PARTICLE_SPEED - MIN_PARTICLE_SPEED);
  let initialAngle = Math.random() * 2 * Math.PI;

  let initialVx = initialSpeed * Math.cos(initialAngle);
  let initialVy = initialSpeed * Math.sin(initialAngle);

  // 应用一些初始的向内/漩涡趋势,以实现即时视觉效果
  const dxToCenter = currentTyphoonCenter[0] - startX;
  const dyToCenter = currentTyphoonCenter[1] - startY;
  const distToCenter = Math.sqrt(
    dxToCenter * dxToCenter + dyToCenter * dyToCenter
  );

  if (distToCenter > 0) {
    // 初始向内拉力
    initialVx += (dxToCenter / distToCenter) * INWARD_ACCELERATION * 5; // 增强初始向内力
    initialVy += (dyToCenter / distToCenter) * INWARD_ACCELERATION * 5;

    // 初始漩涡
    initialVx += (-dyToCenter / distToCenter) * SWIRL_ACCELERATION * 5; // 增强初始漩涡力
    initialVy += (dxToCenter / distToCenter) * SWIRL_ACCELERATION * 5;
  }

  const particleRenderAngle = Math.atan2(initialVy, initialVx);
  const endX = startX + WIND_PARTICLE_LENGTH * Math.cos(particleRenderAngle);
  const endY = startY + WIND_PARTICLE_LENGTH * Math.sin(particleRenderAngle);

  if (windParticles[index]) {
    windParticles[index].x = startX;
    windParticles[index].y = startY;
    windParticles[index].vx = initialVx;
    windParticles[index].vy = initialVy;
    windParticles[index].feature.getGeometry().setCoordinates([
      [startX, startY],
      [endX, endY],
    ]);
  } else {
    const feature = new Feature({
      geometry: new LineString([
        [startX, startY],
        [endX, endY],
      ]),
    });
    windParticles.push({
      feature: feature, // 存储 Feature 对象
      x: startX,
      y: startY,
      vx: initialVx,
      vy: initialVy,
    });
    windFieldSource.addFeature(feature); // 将新创建的 Feature 添加到数据源
  }
}

/**
 * 风场粒子动画循环函数
 * 使用 requestAnimationFrame 实现平滑动画
 */
function animateWindField() {
  // 获取当前台风中心点,风场粒子会围绕此中心移动
  const currentTyphoonCenter = fromLonLat([
    selectedTyphoon.value.lon,
    selectedTyphoon.value.lat,
  ]);

  windParticles.forEach((p, index) => {
    const dx = currentTyphoonCenter[0] - p.x;
    const dy = currentTyphoonCenter[1] - p.y;
    const distance = Math.sqrt(dx * dx + dy * dy);

    // 如果粒子太近或太远,则重置
    if (distance < WIND_FIELD_MIN_RADIUS || distance > WIND_FIELD_MAX_RADIUS) {
      resetWindParticle(index);
      return; // 本帧跳过此粒子的后续处理
    }

    // 计算力(加速度)
    let ax = 0;
    let ay = 0;

    if (distance > 0) {
      // 向内拉力(越靠近中心越强)
      // 使用更平滑的向内力衰减,可能类似反平方定律
      const inwardFactor = INWARD_ACCELERATION / (distance / 50000 + 1); // 调整除数以控制力强度
      ax += (dx / distance) * inwardFactor;
      ay += (dy / distance) * inwardFactor;

      // 切向漩涡(顺时针,越靠近中心越强)
      const swirlFactor = SWIRL_ACCELERATION / (distance / 50000 + 1); // 调整除数以控制力强度
      ax += (-dy / distance) * swirlFactor; // 垂直于 (dx, dy)
      ay += (dx / distance) * swirlFactor;
    }

    // 更新速度
    p.vx += ax;
    p.vy += ay;

    // 应用阻力/阻尼
    p.vx *= DRAG_FACTOR;
    p.vy *= DRAG_FACTOR;

    // 限制速度
    const currentSpeed = Math.sqrt(p.vx * p.vx + p.vy * p.vy);
    if (currentSpeed > MAX_PARTICLE_SPEED) {
      p.vx = (p.vx / currentSpeed) * MAX_PARTICLE_SPEED;
      p.vy = (p.vy / currentSpeed) * MAX_PARTICLE_SPEED;
    } else if (currentSpeed < MIN_PARTICLE_SPEED && currentSpeed > 0) {
      // 确保粒子不会完全停止,除非速度为0
      p.vx = (p.vx / currentSpeed) * MIN_PARTICLE_SPEED;
      p.vy = (p.vy / currentSpeed) * MIN_PARTICLE_SPEED;
    }

    // 更新位置
    p.x += p.vx;
    p.y += p.vy;

    // 更新要素几何形状(线段方向基于当前速度)
    const particleAngle = Math.atan2(p.vy, p.vx);
    const endX = p.x + WIND_PARTICLE_LENGTH * Math.cos(particleAngle);
    const endY = p.y + WIND_PARTICLE_LENGTH * Math.sin(particleAngle);
    p.feature.getGeometry().setCoordinates([
      [p.x, p.y],
      [endX, endY],
    ]);
  });

  windFieldSource.changed(); // 强制重绘风场图层,显示粒子移动效果
  animationFrameId = requestAnimationFrame(animateWindField); // 请求下一帧动画
}

/**
 * 启动风场动画
 */
function startWindAnimation() {
  if (!animationFrameId) {
    // 避免重复启动动画
    animateWindField();
  }
}

/**
 * 停止风场动画
 */
function stopWindAnimation() {
  if (animationFrameId) {
    cancelAnimationFrame(animationFrameId); // 取消当前的动画帧请求
    animationFrameId = null;
  }
}

/**
 * 启动台风路径动画
 * 使用 setInterval 定时器模拟台风的移动
 */
function startTyphoonAnimation() {
  // 每隔1秒更新一次台风位置
  typhoonAnimationIntervalId = setInterval(() => {
    // 获取上一个当前点要素并重置其类型
    if (typhoonPointFeatures[typhoonPathIndex.value]) {
      typhoonPointFeatures[typhoonPathIndex.value].set("type", "point");
    }

    // 更新台风路径索引,循环遍历路径
    typhoonPathIndex.value =
      (typhoonPathIndex.value + 1) % typhoonData.path.length;
    const currentPointData = typhoonData.path[typhoonPathIndex.value];
    const newCenterCoordinates = fromLonLat([
      currentPointData.lon,
      currentPointData.lat,
    ]);

    // 更新台风信息卡片显示的数据
    selectedTyphoon.value = { ...currentPointData, name: typhoonData.name };

    // 设置新的当前点要素类型
    if (typhoonPointFeatures[typhoonPathIndex.value]) {
      typhoonPointFeatures[typhoonPathIndex.value].set("type", "current");
    }

    // 更新风圈位置
    if (windCircleFeature) {
      windCircleFeature.setGeometry(
        new Circle(newCenterCoordinates, typhoonData.windRadius)
      );
    }

    // 强制重绘台风路径图层,显示移动的台风中心和风圈
    typhoonTrackLayer.getSource().changed();

    // 确保风场粒子根据新的台风中心重置
    // 遍历所有风场粒子,确保它们对新的台风中心做出反应
    windParticles.forEach((p, index) => {
      // 当台风移动时,粒子也应调整
      // 相对于新的台风中心重新初始化粒子,以防止视觉"跳跃"
      const oldCenter = fromLonLat([
        typhoonData.path[
          (typhoonPathIndex.value - 1 + typhoonData.path.length) %
            typhoonData.path.length
        ].lon,
        typhoonData.path[
          (typhoonPathIndex.value - 1 + typhoonData.path.length) %
            typhoonData.path.length
        ].lat,
      ]);

      // 计算粒子相对于旧中心的位置
      const relativeX = p.x - oldCenter[0];
      const relativeY = p.y - oldCenter[1];

      // 将相同的相对位置应用于新中心
      p.x = newCenterCoordinates[0] + relativeX;
      p.y = newCenterCoordinates[1] + relativeY;

      // 重置速度,避免携带可能与新力冲突的旧动量
      const initialSpeed =
        MIN_PARTICLE_SPEED +
        Math.random() * (MAX_PARTICLE_SPEED - MIN_PARTICLE_SPEED);
      const initialAngle = Math.atan2(p.vy, p.vx); // 保持大致方向
      p.vx = initialSpeed * Math.cos(initialAngle);
      p.vy = initialSpeed * Math.sin(initialAngle);

      // 更新其要素几何形状
      const particleAngle = Math.atan2(p.vy, p.vx);
      const endX = p.x + WIND_PARTICLE_LENGTH * Math.cos(particleAngle);
      const endY = p.y + WIND_PARTICLE_LENGTH * Math.sin(particleAngle);
      p.feature.getGeometry().setCoordinates([
        [p.x, p.y],
        [endX, endY],
      ]);
    });
    windFieldSource.changed(); // 强制重绘风场图层
  }, 1000); // 每1秒更新一次台风位置
}

/**
 * 停止台风路径动画
 */
function stopTyphoonAnimation() {
  if (typhoonAnimationIntervalId) {
    clearInterval(typhoonAnimationIntervalId); // 清除定时器
    typhoonAnimationIntervalId = null;
  }
}
</script>

<style scoped>
/* 容器样式 */
.container {
  position: relative;
  width: 100vw; /* 视口宽度 */
  height: 100vh; /* 视口高度 */
  overflow: hidden; /* 隐藏溢出内容 */
}

/* 地图容器样式 */
.map {
  width: 100%;
  height: 100%;
  background-color: #e0f2f7; /* 浅蓝色背景,模拟海洋区域 */
}

/* 控制面板样式 */
.controls-panel {
  position: absolute;
  top: 16px; /* 距离顶部16px */
  right: 16px; /* 距离右侧16px */
  background-color: rgba(255, 255, 255, 0.9); /* 半透明白色背景 */
  padding: 16px; /* 内边距 */
  border-radius: 8px; /* 圆角 */
  box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); /* 阴影效果 */
  z-index: 10; /* 确保在地图上方 */
  display: flex;
  flex-direction: column; /* 垂直布局 */
  gap: 12px; /* 元素间距 */
}

/* 面板标题样式 */
.panel-title {
  font-size: 1.125rem; /* 字体大小 */
  font-weight: 600; /* 字体粗细 */
  color: #2d3748; /* 深灰色字体 */
  margin-bottom: 8px; /* 底部外边距 */
}

/* 复选框标签样式 */
.checkbox-label {
  display: flex;
  align-items: center; /* 垂直居中对齐 */
  cursor: pointer; /* 鼠标指针变为手型 */
}

/* 复选框输入框样式 */
.checkbox-input {
  height: 20px;
  width: 20px;
  accent-color: #2563eb; /* 改变复选框的颜色 */
  border-radius: 4px;
  border: 1px solid #ccc;
  appearance: none; /* 隐藏默认复选框样式 */
  -webkit-appearance: none; /* 兼容WebKit浏览器 */
  outline: none; /* 移除焦点轮廓 */
  cursor: pointer;
  position: relative;
}

/* 复选框选中时的样式 */
.checkbox-input:checked {
  background-color: #2563eb; /* 选中时背景色 */
  border-color: #2563eb; /* 选中时边框色 */
}

/* 复选框选中时显示对勾 */
.checkbox-input:checked::before {
  content: "✔"; /* 对勾符号 */
  display: block;
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%); /* 居中对勾 */
  font-size: 14px;
  color: white; /* 对勾颜色 */
}

/* 复选框文本样式 */
.checkbox-text {
  margin-left: 8px; /* 左侧外边距 */
  color: #4a5568; /* 灰色字体 */
}

/* 信息卡片样式 */
.info-card {
  position: absolute;
  bottom: 16px; /* 距离底部16px */
  left: 16px; /* 距离左侧16px */
  background-color: rgba(255, 255, 255, 0.9); /* 半透明白色背景 */
  padding: 24px; /* 内边距 */
  border-radius: 8px; /* 圆角 */
  box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); /* 阴影效果 */
  z-index: 10; /* 确保在地图上方 */
  width: 384px; /* 固定宽度 */
}

/* 卡片标题样式 */
.card-title {
  font-size: 1.25rem; /* 字体大小 */
  font-weight: 700; /* 字体粗细 */
  color: #1d4ed8; /* 蓝色字体 */
  margin-bottom: 12px; /* 底部外边距 */
}

/* 卡片内容样式 */
.card-content {
  display: flex;
  flex-direction: column; /* 垂直布局 */
  gap: 8px; /* 元素间距 */
  color: #4a5568; /* 灰色字体 */
}

/* 卡片内容中加粗文本的样式 */
.card-content strong {
  font-weight: bold;
}
</style>

如果暂时没有图片,没关系,将完整代码中这段代码注释解开

js 复制代码
// return new Style({
//   image: new CircleStyle({
//     radius: 10,
//     fill: new Fill({ color: "rgba(0, 150, 255, 0.8)" }), // 蓝色填充
//     stroke: new Stroke({ color: "white", width: 2 }), // 白色边框
//   }),
// });
相关推荐
鱼樱前端17 小时前
uni-app开发app之前提须知(IOS/安卓)
前端·uni-app
V***u45317 小时前
【学术会议论文投稿】Spring Boot实战:零基础打造你的Web应用新纪元
前端·spring boot·后端
i听风逝夜17 小时前
Web 3D地球实时统计访问来源
前端·后端
iMonster17 小时前
React 组件的组合模式之道 (Composition Pattern)
前端
呐呐呐呐呢18 小时前
antd渐变色边框按钮
前端
元直数字电路验证18 小时前
Jakarta EE Web 聊天室技术梳理
前端
wadesir18 小时前
Nginx配置文件CPU优化(从零开始提升Web服务器性能)
服务器·前端·nginx
牧码岛18 小时前
Web前端之canvas实现图片融合与清晰度介绍、合并
前端·javascript·css·html·web·canvas·web前端
灵犀坠18 小时前
前端面试八股复习心得
开发语言·前端·javascript
9***Y4818 小时前
前端动画性能优化
前端