吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 2685|回复: 50
收起左侧

[其他原创] 简单天气网页(免费api接口+高德底图)

  [复制链接]
noforgvie 发表于 2026-7-14 16:05
本帖最后由 noforgvie 于 2026-7-15 09:05 编辑

调用 Open-Meteo 免费天气 API 获取天气预报。

底图是高德的,保存为html即可。

点击地图上任意一点,弹出天气信息。能获取到的信息很多,可自行探索该接口。

Open-Meteo 是一个开源且完全免费的天气 API,不需要注册,不需要 API Key,也没有隐藏收费。

最近觉得西安是大火炉,点吐鲁番看了下,顿时感觉西安还挺凉快。

关于接口的一些问题,官网在这里:https://open-meteo.com/

(以下均为官网介绍,翻译采用了最简单的edge浏览器自带的)

1、预测的天气模型:来自ECMWF、NOAA、DWD、MÉTÉO-France、jma、kma、knmi、DMI的30+天气模型, MeteoSwiss、英国气象局、BOM、CMA等——全球范围内,分辨率最高可达1公里

2、来自15个以上国家气象服务机构的气象模型——ECMWF、DWD、NOAA、法国气象局, JMA、KMA、KNMI、DMI、MeteoSwiss、英国气象局、英国气象局、英国气象局、CMA和GeoSphere奥地利。

best_match选项会自动为任意地点选择分辨率最高的模型。 单个模型可以直接被选用于模型比较或模型专用管道。
分辨率范围为1–2公里(区域中尺度模型)到9–11公里(全球模型)。全部输出经过归一化至小时分辨率,并优化至所需坐标。

3、至于说的不准,可能的原因是(更新频率?):

大多数全球模型每6小时更新一次。高分辨率区域模型,如ICON-D2, HRRR,AROME每1至3小时更新一次。每次更新都会接收来自的最新观测数据 气象站、无线电探空仪、飞机、雷达和卫星。
中欧和北美提供15分钟一级数据,插值自 小时模型产出则在其他地方。

4、测试了几个大城市,还比较准。小城市可能不准吧,应该是分辨率或者更新问题或者是外国接口的问题?maybe  maybe

5、Asia/Shanghai  指的是 时区 ,说明你点的位置都在中国,而不是所有的天气预报都是上海

参照:https://www.52pojie.cn/forum.php?mod=viewthread&tid=2115277&highlight=claude(Claude Code 隐蔽遥测行为分析)  这个贴里的  检测用户是否使用代理和所在时区(n = t === "Asia/Shanghai" || t === "Asia/Urumqi")

小玩具一个,源码如下





1.jpg
2.jpg


<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>天气地图</title>
  <script src="https://cdn.tailwindcss.com"></script>
  <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
  <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
  <link rel="preconnect" href="https://fonts.googleapis.com">
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
  <link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;700&family=Noto+Sans+SC:wght@400;500;700&display=swap" rel="stylesheet">
  <style>
    :root {
      --bg: #1a1f2e;
      --fg: #f0f4f8;
      --muted: #8b9cb3;
      --accent: #ff6b35;
      --accent-glow: rgba(255, 107, 53, 0.4);
      --card: rgba(26, 31, 46, 0.95);
      --card-border: rgba(255, 255, 255, 0.1);
    }

    * { margin: 0; padding: 0; box-sizing: border-box; }

    body {
      font-family: 'Noto Sans SC', 'Space Grotesk', sans-serif;
      background: var(--bg);
      color: var(--fg);
      height: 100vh;
      overflow: hidden;
    }

    #map {
      width: 100%;
      height: 100%;
      z-index: 1;
    }

    .leaflet-container {
      background: #1e2636;
      font-family: inherit;
    }

    .leaflet-control-zoom {
      border: none !important;
      box-shadow: 0 4px 20px rgba(0,0,0,0.3) !important;
    }

    .leaflet-control-zoom a {
      background: var(--card) !important;
      color: var(--fg) !important;
      border: 1px solid var(--card-border) !important;
      width: 36px !important;
      height: 36px !important;
      line-height: 36px !important;
      font-size: 18px !important;
    }

    .leaflet-control-zoom a:hover {
      background: rgba(255, 107, 53, 0.2) !important;
      color: var(--accent) !important;
    }

    .custom-marker {
      width: 36px;
      height: 36px;
      border-radius: 50%;
      background: linear-gradient(135deg, var(--accent), #ff8c5a);
      border: 3px solid #fff;
      box-shadow: 0 4px 24px var(--accent-glow);
      display: flex;
      align-items: center;
      justify-content: center;
      animation: markerPulse 2s ease-in-out infinite;
    }

    @keyframes markerPulse {
      0%, 100% { transform: scale(1); }
      50% { transform: scale(1.08); }
    }

    .weather-panel {
      position: fixed;
      top: 20px;
      right: 20px;
      width: 400px;
      max-height: calc(100vh - 40px);
      background: var(--card);
      backdrop-filter: blur(24px);
      border: 1px solid var(--card-border);
      border-radius: 24px;
      padding: 28px;
      z-index: 1000;
      transform: translateX(460px);
      opacity: 0;
      transition: all 0.4s cubic-bezier(0.16, 1, 0.3, 1);
      overflow-y: auto;
      box-shadow: 0 20px 60px rgba(0,0,0,0.4);
    }

    .weather-panel.active {
      transform: translateX(0);
      opacity: 1;
    }

    /* 自定义滚动条 */
    .weather-panel::-webkit-scrollbar {
      width: 4px;
    }
    .weather-panel::-webkit-scrollbar-track {
      background: transparent;
    }
    .weather-panel::-webkit-scrollbar-thumb {
      background: var(--accent);
      border-radius: 10px;
    }

    .close-btn {
      position: sticky;
      top: 0;
      float: right;
      width: 36px;
      height: 36px;
      border-radius: 50%;
      background: rgba(255, 255, 255, 0.06);
      border: none;
      color: var(--muted);
      cursor: pointer;
      display: flex;
      align-items: center;
      justify-content: center;
      transition: all 0.2s ease;
      z-index: 10;
      margin-bottom: 8px;
    }

    .close-btn:hover {
      background: rgba(255, 107, 53, 0.2);
      color: var(--accent);
      transform: rotate(90deg);
    }

    .location-name {
      font-size: 1.6rem;
      font-weight: 700;
      margin-bottom: 4px;
      letter-spacing: -0.02em;
      line-height: 1.2;
    }

    .location-coords {
      font-size: 0.85rem;
      color: var(--muted);
      font-family: 'Space Grotesk', monospace;
    }

    .elevation-info {
      font-size: 0.8rem;
      color: var(--muted);
      opacity: 0.7;
      margin-top: 2px;
    }

    /* 当前天气 */
    .current-weather {
      margin-top: 20px;
      padding: 24px;
      background: linear-gradient(135deg, rgba(255, 107, 53, 0.15), rgba(255, 107, 53, 0.05));
      border-radius: 20px;
      display: flex;
      align-items: center;
      gap: 20px;
      border: 1px solid rgba(255, 107, 53, 0.1);
    }

    .weather-icon {
      width: 88px;
      height: 88px;
      flex-shrink: 0;
    }

    .weather-icon svg {
      width: 100%;
      height: 100%;
    }

    .temp-current {
      font-size: 3.5rem;
      font-weight: 700;
      font-family: 'Space Grotesk', sans-serif;
      line-height: 1;
    }

    .weather-desc {
      font-size: 1.05rem;
      color: var(--muted);
      margin-top: 4px;
    }

    .feels-like {
      font-size: 0.85rem;
      color: var(--muted);
      opacity: 0.7;
      margin-top: 2px;
    }

    /* 所有详情网格 */
    .weather-details {
      display: grid;
      grid-template-columns: repeat(3, 1fr);
      gap: 10px;
      margin-top: 20px;
    }

    .detail-item {
      padding: 14px 12px;
      background: rgba(255, 255, 255, 0.03);
      border-radius: 14px;
      border: 1px solid var(--card-border);
      transition: all 0.2s ease;
      text-align: center;
    }

    .detail-item:hover {
      background: rgba(255, 255, 255, 0.05);
      border-color: rgba(255, 107, 53, 0.2);
    }

    .detail-label {
      font-size: 0.65rem;
      color: var(--muted);
      text-transform: uppercase;
      letter-spacing: 0.06em;
    }

    .detail-value {
      font-size: 1.1rem;
      font-weight: 600;
      margin-top: 4px;
      font-family: 'Space Grotesk', sans-serif;
    }

    .detail-value .unit {
      font-size: 0.7rem;
      font-weight: 400;
      color: var(--muted);
      margin-left: 2px;
    }

    /* 日出日落 */
    .sun-section {
      display: grid;
      grid-template-columns: 1fr 1fr;
      gap: 10px;
      margin-top: 16px;
    }

    .sun-item {
      padding: 14px;
      background: rgba(255, 255, 255, 0.03);
      border-radius: 14px;
      border: 1px solid var(--card-border);
      text-align: center;
    }

    .sun-item .label {
      font-size: 0.7rem;
      color: var(--muted);
      text-transform: uppercase;
      letter-spacing: 0.06em;
    }

    .sun-item .value {
      font-size: 1.1rem;
      font-weight: 600;
      font-family: 'Space Grotesk', monospace;
      margin-top: 4px;
    }

    .sun-item .value.sunrise { color: #FFB347; }
    .sun-item .value.sunset { color: #FF6B6B; }

    .forecast-title {
      margin-top: 24px;
      font-size: 0.85rem;
      font-weight: 600;
      color: var(--muted);
      text-transform: uppercase;
      letter-spacing: 0.12em;
    }

    .forecast-list {
      margin-top: 14px;
      display: flex;
      flex-direction: column;
      gap: 10px;
    }

    .forecast-item {
      display: flex;
      align-items: center;
      padding: 14px 16px;
      background: rgba(255, 255, 255, 0.02);
      border-radius: 14px;
      border: 1px solid var(--card-border);
      transition: all 0.2s ease;
    }

    .forecast-item:hover {
      background: rgba(255, 255, 255, 0.04);
      border-color: rgba(255, 107, 53, 0.25);
      transform: translateX(4px);
    }

    .forecast-day {
      width: 60px;
      font-weight: 600;
      font-size: 0.9rem;
    }

    .forecast-icon {
      width: 36px;
      height: 36px;
      margin: 0 12px;
      flex-shrink: 0;
    }

    .forecast-icon svg {
      width: 100%;
      height: 100%;
    }

    .forecast-desc {
      flex: 1;
      font-size: 0.85rem;
      color: var(--muted);
    }

    .forecast-temps {
      display: flex;
      gap: 10px;
      font-family: 'Space Grotesk', sans-serif;
    }

    .temp-high {
      font-weight: 700;
      font-size: 1rem;
    }

    .temp-low {
      color: var(--muted);
      font-size: 1rem;
    }

    .forecast-extra {
      font-size: 0.7rem;
      color: var(--muted);
      opacity: 0.6;
      margin-top: 2px;
    }

    /* 小时预报 */
    .hourly-title {
      margin-top: 20px;
      font-size: 0.85rem;
      font-weight: 600;
      color: var(--muted);
      text-transform: uppercase;
      letter-spacing: 0.12em;
    }

    .hourly-scroll {
      display: flex;
      gap: 10px;
      overflow-x: auto;
      margin-top: 12px;
      padding-bottom: 8px;
    }

    .hourly-scroll::-webkit-scrollbar {
      height: 3px;
    }
    .hourly-scroll::-webkit-scrollbar-thumb {
      background: var(--accent);
      border-radius: 10px;
    }

    .hourly-item {
      flex: 0 0 72px;
      text-align: center;
      padding: 12px 6px;
      background: rgba(255, 255, 255, 0.03);
      border-radius: 12px;
      border: 1px solid var(--card-border);
      transition: all 0.2s ease;
    }

    .hourly-item:hover {
      background: rgba(255, 255, 255, 0.06);
      border-color: rgba(255, 107, 53, 0.2);
    }

    .hourly-item .time {
      font-size: 0.7rem;
      color: var(--muted);
      font-family: 'Space Grotesk', monospace;
    }

    .hourly-item .h-icon {
      width: 28px;
      height: 28px;
      margin: 4px auto;
    }

    .hourly-item .h-icon svg {
      width: 100%;
      height: 100%;
    }

    .hourly-item .h-temp {
      font-size: 0.95rem;
      font-weight: 600;
      font-family: 'Space Grotesk', sans-serif;
    }

    .hourly-item .h-precip {
      font-size: 0.6rem;
      color: #4A90D9;
      margin-top: 2px;
    }

    .hint-overlay {
      position: fixed;
      top: 50%;
      left: 50%;
      transform: translate(-50%, -50%);
      text-align: center;
      z-index: 999;
      pointer-events: none;
      opacity: 1;
      transition: opacity 0.4s ease;
    }

    .hint-overlay.hidden {
      opacity: 0;
    }

    .hint-icon {
      width: 72px;
      height: 72px;
      margin: 0 auto 20px;
      opacity: 0.5;
      animation: hintBounce 2.5s ease-in-out infinite;
    }

    @keyframes hintBounce {
      0%, 100% { transform: translateY(0); }
      50% { transform: translateY(-12px); }
    }

    .hint-text {
      font-size: 1.05rem;
      color: var(--fg);
      background: var(--card);
      padding: 16px 32px;
      border-radius: 100px;
      border: 1px solid var(--card-border);
      box-shadow: 0 10px 40px rgba(0,0,0,0.3);
    }

    .loading-spinner {
      width: 44px;
      height: 44px;
      border: 3px solid rgba(255, 107, 53, 0.15);
      border-top-color: var(--accent);
      border-radius: 50%;
      animation: spin 0.8s linear infinite;
      margin: 48px auto;
    }

    @keyframes spin {
      to { transform: rotate(360deg); }
    }

    .error-msg {
      text-align: center;
      padding: 48px 20px;
      color: var(--muted);
    }

    .data-source {
      margin-top: 16px;
      font-size: 0.6rem;
      color: var(--muted);
      opacity: 0.4;
      text-align: center;
      border-top: 1px solid var(--card-border);
      padding-top: 14px;
    }

    @media (max-width: 640px) {
      .weather-panel {
        width: calc(100% - 16px);
        right: 8px;
        top: auto;
        bottom: 8px;
        max-height: 60vh;
        padding: 20px;
        transform: translateY(100%);
        border-radius: 20px;
      }

      .weather-panel.active {
        transform: translateY(0);
      }

      .temp-current {
        font-size: 2.8rem;
      }

      .weather-icon {
        width: 70px;
        height: 70px;
      }

      .weather-details {
        grid-template-columns: repeat(2, 1fr);
      }
    }

    @media (prefers-reduced-motion: reduce) {
      *, *::before, *::after {
        animation-duration: 0.01ms !important;
        transition-duration: 0.01ms !important;
      }
    }
  </style>
</head>
<body>
  <div id="map"></div>

  <div class="hint-overlay" id="hint">
    <div class="hint-icon">
      <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
        <path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"/>
        <circle cx="12" cy="10" r="3"/>
      </svg>
    </div>
    <div class="hint-text">点击地图查看完整天气</div>
  </div>

  <div class="weather-panel" id="weatherPanel">
    <button class="close-btn" id="closeBtn" aria-label="关闭面板">
      <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round">
        <path d="M18 6L6 18M6 6l12 12"/>
      </svg>
    </button>
    <div id="panelContent">
      <div class="loading-spinner"></div>
    </div>
  </div>

  <script>
    // ============================================================
    // 1. 初始化地图
    // ============================================================
    const map = L.map('map', {
      center: [35, 105],
      zoom: 5,
      zoomControl: false,
      attributionControl: false
    });

    L.tileLayer('https://webrd0{s}.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=1&style=8&x={x}&y={y}&z={z}', {
      subdomains: ['1', '2', '3', '4'],
      maxZoom: 18
    }).addTo(map);

    L.control.zoom({ position: 'bottomleft' }).addTo(map);

    let currentMarker = null;
    const weatherPanel = document.getElementById('weatherPanel');
    const panelContent = document.getElementById('panelContent');
    const closeBtn = document.getElementById('closeBtn');
    const hint = document.getElementById('hint');

    // ============================================================
    // 2. 天气代码映射 (WMO 标准)
    // ============================================================
    const weatherCodes = {
      0: { desc: '晴天', icon: 'clear' },
      1: { desc: '基本晴朗', icon: 'partly-cloudy' },
      2: { desc: '局部多云', icon: 'partly-cloudy' },
      3: { desc: '阴天', icon: 'cloudy' },
      45: { desc: '雾', icon: 'fog' },
      48: { desc: '霜雾', icon: 'fog' },
      51: { desc: '小毛毛雨', icon: 'drizzle' },
      53: { desc: '毛毛雨', icon: 'drizzle' },
      55: { desc: '大毛毛雨', icon: 'drizzle' },
      56: { desc: '冻毛毛雨', icon: 'sleet' },
      57: { desc: '大冻毛毛雨', icon: 'sleet' },
      61: { desc: '小雨', icon: 'rain' },
      63: { desc: '中雨', icon: 'rain' },
      65: { desc: '大雨', icon: 'rain' },
      66: { desc: '冻雨', icon: 'sleet' },
      67: { desc: '大冻雨', icon: 'sleet' },
      71: { desc: '小雪', icon: 'snow' },
      73: { desc: '中雪', icon: 'snow' },
      75: { desc: '大雪', icon: 'snow' },
      77: { desc: '雪粒', icon: 'snow' },
      80: { desc: '小阵雨', icon: 'showers' },
      81: { desc: '阵雨', icon: 'showers' },
      82: { desc: '大阵雨', icon: 'showers' },
      85: { desc: '小阵雪', icon: 'snow' },
      86: { desc: '大阵雪', icon: 'snow' },
      95: { desc: '雷暴', icon: 'thunderstorm' },
      96: { desc: '雷暴伴小冰雹', icon: 'thunderstorm' },
      99: { desc: '雷暴伴大冰雹', icon: 'thunderstorm' }
    };

    // ============================================================
    // 3. SVG 天气图标生成器
    // ============================================================
    function getWeatherIcon(type, size = 64) {
      const icons = {
        'clear': `<svg viewBox="0 0 64 64" fill="none">
          <circle cx="32" cy="32" r="12" fill="#FFD93D"/>
          <g stroke="#FFD93D" stroke-width="2.5" stroke-linecap="round" opacity="0.8">
            <line x1="32" y1="8" x2="32" y2="14"/><line x1="32" y1="50" x2="32" y2="56"/>
            <line x1="8" y1="32" x2="14" y2="32"/><line x1="50" y1="32" x2="56" y2="32"/>
            <line x1="15.3" y1="15.3" x2="19.5" y2="19.5"/><line x1="44.5" y1="44.5" x2="48.7" y2="48.7"/>
            <line x1="15.3" y1="48.7" x2="19.5" y2="44.5"/><line x1="44.5" y1="19.5" x2="48.7" y2="15.3"/>
          </g>
        </svg>`,
        'partly-cloudy': `<svg viewBox="0 0 64 64" fill="none">
          <circle cx="22" cy="22" r="9" fill="#FFD93D"/>
          <path d="M46 48H20a11 11 0 1 1 1-22h1a14 14 0 0 1 27 5c0 4-1 8-4 11h1z" fill="#E8EDF5"/>
        </svg>`,
        'cloudy': `<svg viewBox="0 0 64 64" fill="none">
          <path d="M48 44H18a13 13 0 1 1 2-26 16 16 0 0 1 32 4c0 5-2 10-5 14h1z" fill="#D1D9E3"/>
        </svg>`,
        'fog': `<svg viewBox="0 0 64 64" fill="none">
          <g stroke="#B8C4D4" stroke-width="4" stroke-linecap="round"><line x1="10" y1="26" x2="54" y2="26"/><line x1="8" y1="38" x2="56" y2="38"/><line x1="10" y1="50" x2="54" y2="50"/></g>
        </svg>`,
        'drizzle': `<svg viewBox="0 0 64 64" fill="none">
          <path d="M46 32H20a11 11 0 1 1 1-22 14 14 0 0 1 27 5c0 4-1 7-3 10h1z" fill="#D1D9E3"/>
          <g stroke="#6BB3F0" stroke-width="2" stroke-linecap="round">
            <line x1="20" y1="42" x2="18" y2="50"/><line x1="32" y1="42" x2="30" y2="50"/><line x1="44" y1="42" x2="42" y2="50"/>
          </g>
        </svg>`,
        'rain': `<svg viewBox="0 0 64 64" fill="none">
          <path d="M46 30H20a11 11 0 1 1 1-22 14 14 0 0 1 27 5c0 4-1 7-3 10h1z" fill="#B8C4D4"/>
          <g stroke="#4A90D9" stroke-width="3" stroke-linecap="round">
            <line x1="18" y1="40" x2="14" y2="56"/><line x1="32" y1="40" x2="28" y2="56"/><line x1="46" y1="40" x2="42" y2="56"/>
          </g>
        </svg>`,
        'sleet': `<svg viewBox="0 0 64 64" fill="none">
          <path d="M46 28H20a11 11 0 1 1 1-22 14 14 0 0 1 27 5c0 4-1 7-3 10h1z" fill="#B8C4D4"/>
          <g stroke="#6BB3F0" stroke-width="2" stroke-linecap="round">
            <line x1="18" y1="38" x2="16" y2="46"/><line x1="46" y1="38" x2="44" y2="46"/>
          </g>
          <g fill="#A8C8E8"><circle cx="28" cy="50" r="3"/><circle cx="40" cy="54" r="3"/></g>
        </svg>`,
        'snow': `<svg viewBox="0 0 64 64" fill="none">
          <path d="M46 28H20a11 11 0 1 1 1-22 14 14 0 0 1 27 5c0 4-1 7-3 10h1z" fill="#D1D9E3"/>
          <g fill="#A8C8E8">
            <circle cx="18" cy="44" r="4"/><circle cx="32" cy="50" r="4"/><circle cx="46" cy="44" r="4"/>
            <circle cx="25" cy="56" r="3"/><circle cx="39" cy="56" r="3"/>
          </g>
        </svg>`,
        'showers': `<svg viewBox="0 0 64 64" fill="none">
          <path d="M46 26H20a11 11 0 1 1 1-22 14 14 0 0 1 27 5c0 4-1 7-3 10h1z" fill="#98A8B8"/>
          <g stroke="#4A90D9" stroke-width="4" stroke-linecap="round">
            <line x1="16" y1="38" x2="10" y2="58"/><line x1="32" y1="38" x2="26" y2="58"/><line x1="48" y1="38" x2="42" y2="58"/>
          </g>
        </svg>`,
        'thunderstorm': `<svg viewBox="0 0 64 64" fill="none">
          <path d="M46 24H20a11 11 0 1 1 1-22 14 14 0 0 1 27 5c0 4-1 7-3 10h1z" fill="#6B7A8C"/>
          <polygon points="36,32 28,44 34,44 30,58 44,40 38,40 42,32" fill="#FFD93D"/>
        </svg>`
      };
      return icons[type] || icons['partly-cloudy'];
    }

    // ============================================================
    // 4. 获取完整天气数据 (包含所有可用参数)
    // ============================================================
    async function getFullWeather(lat, lon) {
      // 包含 Open-Meteo 所有可用参数
      const params = {
        latitude: lat,
        longitude: lon,
        // 当前数据
        current: [
          'temperature_2m',
          'relative_humidity_2m',
          'apparent_temperature',
          'is_day',
          'precipitation',
          'rain',
          'snowfall',
          'weather_code',
          'cloud_cover',
          'wind_speed_10m',
          'wind_direction_10m',
          'wind_gusts_10m',
          'pressure_msl',
          'surface_pressure',
          'visibility'
        ].join(','),
        // 每日数据
        daily: [
          'weather_code',
          'temperature_2m_max',
          'temperature_2m_min',
          'apparent_temperature_max',
          'apparent_temperature_min',
          'sunrise',
          'sunset',
          'daylight_duration',
          'sunshine_duration',
          'uv_index_max',
          'precipitation_sum',
          'rain_sum',
          'snowfall_sum',
          'precipitation_hours',
          'precipitation_probability_max',
          'wind_speed_10m_max',
          'wind_gusts_10m_max',
          'wind_direction_10m_dominant'
        ].join(','),
        // 逐小时数据
        hourly: [
          'temperature_2m',
          'relative_humidity_2m',
          'apparent_temperature',
          'precipitation_probability',
          'precipitation',
          'rain',
          'snowfall',
          'weather_code',
          'cloud_cover',
          'visibility',
          'wind_speed_10m',
          'wind_direction_10m',
          'wind_gusts_10m',
          'uv_index'
        ].join(','),
        timezone: 'auto',
        forecast_days: '4'
      };

      const url = `https://api.open-meteo.com/v1/forecast?` + 
        Object.entries(params).map(([k, v]) => `${k}=${v}`).join('&');

      const res = await fetch(url);
      if (!res.ok) throw new Error(`HTTP ${res.status}`);
      return res.json();
    }

    // ============================================================
    // 5. 工具函数
    // ============================================================
    function formatDate(dateStr, index) {
      if (index === 0) return '今天';
      if (index === 1) return '明天';
      const date = new Date(dateStr);
      const weekdays = ['日', '一', '二', '三', '四', '五', '六'];
      return `${date.getMonth()+1}/${date.getDate()} 周${weekdays[date.getDay()]}`;
    }

    function formatTime(dateStr) {
      const d = new Date(dateStr);
      return d.toTimeString().slice(0, 5);
    }

    function formatHour(dateStr) {
      return new Date(dateStr).toTimeString().slice(0, 5);
    }

    function getDayName(dateStr) {
      const weekdays = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'];
      return weekdays[new Date(dateStr).getDay()];
    }

    // ============================================================
    // 6. 渲染完整天气面板
    // ============================================================
    async function renderWeatherPanel(lat, lon) {
      panelContent.innerHTML = '<div class="loading-spinner"></div>';
      weatherPanel.classList.add('active');
      hint.classList.add('hidden');

      try {
        const data = await getFullWeather(lat, lon);
        const current = data.current;
        const daily = data.daily;
        const hourly = data.hourly;

        // 当前天气
        const currentWeather = weatherCodes[current.weather_code] || { desc: '未知', icon: 'partly-cloudy' };
        const isDay = current.is_day === 1;

        // ---- 构建三日预报 ----
        let forecastHTML = '';
        for (let i = 0; i < Math.min(3, daily.time.length); i++) {
          const dayWeather = weatherCodes[daily.weather_code[i]] || { desc: '未知', icon: 'partly-cloudy' };
          const precipProb = daily.precipitation_probability_max ? Math.round(daily.precipitation_probability_max[i]) : null;
          const uvMax = daily.uv_index_max ? daily.uv_index_max[i].toFixed(1) : null;

          forecastHTML += `
            <div class="forecast-item">
              <div class="forecast-day">${formatDate(daily.time[i], i)}</div>
              <div class="forecast-icon">${getWeatherIcon(dayWeather.icon)}</div>
              <div style="flex:1">
                <div class="forecast-desc">${dayWeather.desc}</div>
                <div class="forecast-extra">
                  ${precipProb !== null ? `降水 ${precipProb}%` : ''}
                  ${uvMax !== null ? ` · UV ${uvMax}` : ''}
                </div>
              </div>
              <div class="forecast-temps">
                <span class="temp-high">${Math.round(daily.temperature_2m_max[i])}°</span>
                <span class="temp-low">${Math.round(daily.temperature_2m_min[i])}°</span>
              </div>
            </div>
          `;
        }

        // ---- 构建逐小时预报 (未来12小时) ----
        let hourlyHTML = '';
        const now = new Date();
        const hourStart = new Date(now);
        hourStart.setMinutes(0, 0, 0);

        let hourlyCount = 0;
        for (let i = 0; i < hourly.time.length && hourlyCount < 12; i++) {
          const hTime = new Date(hourly.time[i]);
          if (hTime >= now) {
            const hWeather = weatherCodes[hourly.weather_code[i]] || { desc: '未知', icon: 'partly-cloudy' };
            const precip = hourly.precipitation ? hourly.precipitation[i] : 0;
            hourlyHTML += `
              <div class="hourly-item">
                <div class="time">${formatHour(hourly.time[i])}</div>
                <div class="h-icon">${getWeatherIcon(hWeather.icon)}</div>
                <div class="h-temp">${Math.round(hourly.temperature_2m[i])}°</div>
                ${precip > 0 ? `<div class="h-precip">${precip.toFixed(1)}mm</div>` : ''}
              </div>
            `;
            hourlyCount++;
          }
        }

        // 如果没有未来小时数据(可能已过午夜),取前12个
        if (!hourlyHTML) {
          for (let i = 0; i < Math.min(12, hourly.time.length); i++) {
            const hWeather = weatherCodes[hourly.weather_code[i]] || { desc: '未知', icon: 'partly-cloudy' };
            const precip = hourly.precipitation ? hourly.precipitation[i] : 0;
            hourlyHTML += `
              <div class="hourly-item">
                <div class="time">${formatHour(hourly.time[i])}</div>
                <div class="h-icon">${getWeatherIcon(hWeather.icon)}</div>
                <div class="h-temp">${Math.round(hourly.temperature_2m[i])}°</div>
                ${precip > 0 ? `<div class="h-precip">${precip.toFixed(1)}mm</div>` : ''}
              </div>
            `;
          }
        }

        // ---- 组装主面板 ----
        panelContent.innerHTML = `
          <!-- 位置信息 -->
          <div style="display:flex;justify-content:space-between;align-items:start;">
            <div>
              <div class="location-name">${lat.toFixed(3)}°N, ${lon.toFixed(3)}°E</div>
              <div class="location-coords">${data.timezone || '未知时区'}</div>
              ${data.elevation ? `<div class="elevation-info">海拔 ${data.elevation}m</div>` : ''}
            </div>
            <div style="text-align:right;font-size:0.75rem;color:var(--muted);opacity:0.6;">
              ${isDay ? '☀️ 白天' : '🌙 夜间'}
            </div>
          </div>

          <!-- 当前天气 -->
          <div class="current-weather">
            <div class="weather-icon">${getWeatherIcon(currentWeather.icon)}</div>
            <div>
              <div class="temp-current">${Math.round(current.temperature_2m)}°</div>
              <div class="weather-desc">${currentWeather.desc}</div>
              <div class="feels-like">体感 ${Math.round(current.apparent_temperature)}°</div>
            </div>
          </div>

          <!-- 天气详情 (3列网格) -->
          <div class="weather-details">
            <div class="detail-item">
              <div class="detail-label">湿度</div>
              <div class="detail-value">${current.relative_humidity_2m}%</div>
            </div>
            <div class="detail-item">
              <div class="detail-label">风速</div>
              <div class="detail-value">${Math.round(current.wind_speed_10m)} <span class="unit">km/h</span></div>
            </div>
            <div class="detail-item">
              <div class="detail-label">阵风</div>
              <div class="detail-value">${Math.round(current.wind_gusts_10m || 0)} <span class="unit">km/h</span></div>
            </div>
            <div class="detail-item">
              <div class="detail-label">风向</div>
              <div class="detail-value">${current.wind_direction_10m !== undefined ? Math.round(current.wind_direction_10m) + '°' : '--'}</div>
            </div>
            <div class="detail-item">
              <div class="detail-label">云量</div>
              <div class="detail-value">${current.cloud_cover !== undefined ? Math.round(current.cloud_cover) + '%' : '--'}</div>
            </div>
            <div class="detail-item">
              <div class="detail-label">气压</div>
              <div class="detail-value">${current.pressure_msl !== undefined ? Math.round(current.pressure_msl) : '--'} <span class="unit">hPa</span></div>
            </div>
            <div class="detail-item">
              <div class="detail-label">降水</div>
              <div class="detail-value">${current.precipitation !== undefined ? current.precipitation.toFixed(1) : '0.0'} <span class="unit">mm</span></div>
            </div>
            <div class="detail-item">
              <div class="detail-label">能见度</div>
              <div class="detail-value">${current.visibility !== undefined ? Math.round(current.visibility/1000) : '--'} <span class="unit">km</span></div>
            </div>
            <div class="detail-item">
              <div class="detail-label">紫外线</div>
              <div class="detail-value">${current.uv_index !== undefined ? current.uv_index.toFixed(1) : '--'}</div>
            </div>
          </div>

          <!-- 日出日落 -->
          ${daily.sunrise && daily.sunset ? `
          <div class="sun-section">
            <div class="sun-item">
              <div class="label">🌅 日出</div>
              <div class="value sunrise">${formatTime(daily.sunrise[0])}</div>
            </div>
            <div class="sun-item">
              <div class="label">🌇 日落</div>
              <div class="value sunset">${formatTime(daily.sunset[0])}</div>
            </div>
          </div>
          ` : ''}

          <!-- 三日预报 -->
          <div class="forecast-title">📅 三日预报</div>
          <div class="forecast-list">${forecastHTML}</div>

          <!-- 逐小时预报 -->
          <div class="hourly-title">⏰ 逐小时预报</div>
          <div class="hourly-scroll">${hourlyHTML}</div>

          <!-- 数据来源 -->
          <div class="data-source">
            数据来源: Open-Meteo.com · 免费开源天气 API
          </div>
        `;

      } catch (error) {
        console.error('获取天气失败:', error);
        panelContent.innerHTML = `
          <div class="error-msg">
            <svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" style="margin: 0 auto 16px; opacity: 0.4;">
              <circle cx="12" cy="12" r="10"/><path d="M12 8v4M12 16h.01"/>
            </svg>
            <p>获取天气失败</p>
            <p style="font-size:0.8rem;margin-top:8px;opacity:0.6;">${error.message}</p>
          </div>
        `;
      }
    }

    // ============================================================
    // 7. 地图交互
    // ============================================================
    function createMarker(lat, lon) {
      if (currentMarker) map.removeLayer(currentMarker);

      const markerIcon = L.divIcon({
        className: 'custom-marker-wrapper',
        html: '<div class="custom-marker"><svg width="18" height="18" viewBox="0 0 24 24" fill="white"><path d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7z"/></svg></div>',
        iconSize: [36, 36],
        iconAnchor: [18, 18]
      });

      currentMarker = L.marker([lat, lon], { icon: markerIcon }).addTo(map);
    }

    map.on('click', function(e) {
      const lat = e.latlng.lat;
      const lon = e.latlng.lng;
      createMarker(lat, lon);
      renderWeatherPanel(lat, lon);
    });

    // ============================================================
    // 8. 关闭面板
    // ============================================================
    closeBtn.addEventListener('click', function() {
      weatherPanel.classList.remove('active');
      hint.classList.remove('hidden');
      if (currentMarker) {
        map.removeLayer(currentMarker);
        currentMarker = null;
      }
    });

    document.addEventListener('keydown', function(e) {
      if (e.key === 'Escape' && weatherPanel.classList.contains('active')) {
        closeBtn.click();
      }
    });
  </script>
</body>
</html>

免费评分

参与人数 9吾爱币 +15 热心值 +5 收起 理由
逐雅斋 + 1 已上线:https://ico5.net/tianqi/
苏紫方璇 + 7 + 1 欢迎分析讨论交流,吾爱破解论坛有你更精彩!
wapjsx + 1 谢谢@Thanks!
gonethen + 1 谢谢@Thanks!
lwj1367 + 1 + 1 谢谢@Thanks!
OrData + 1 + 1 谢谢@Thanks!
☆木木可可★ + 1 + 1 谢谢@Thanks!
13594611244 + 1 + 1 我很赞同!
qu1eRo + 1 谢谢@Thanks!

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

批注 发表于 2026-7-14 20:15
13388157215 发表于 2026-7-14 19:59
申请高德web服务key,怎么操作呢?麻烦大佬出个教程,谢谢!

5.66 q@e.oQ :6pm xFh:/ 06/18 如何配置个人高德地图 API KEY # 高德 # 函证 # 审计 # 年审# 电脑知识  https://v.douyin.com/zO0ofbnBTms/ 复制此链接,打开Dou音搜索,直接观看视频!
批注 发表于 2026-7-14 19:03
[HTML] 纯文本查看 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>天气地图</title>
  <script src="https://cdn.tailwindcss.com"></script>
  <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
  <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
  <link rel="preconnect" href="https://fonts.googleapis.com">
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
  <link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;700&family=Noto+Sans+SC:wght@400;500;700&display=swap" rel="stylesheet">
  <style>
    :root {
      --bg: #1a1f2e;
      --fg: #f0f4f8;
      --muted: #8b9cb3;
      --accent: #ff6b35;
      --accent-glow: rgba(255, 107, 53, 0.4);
      --card: rgba(26, 31, 46, 0.95);
      --card-border: rgba(255, 255, 255, 0.1);
    }

    * { margin: 0; padding: 0; box-sizing: border-box; }

    body {
      font-family: 'Noto Sans SC', 'Space Grotesk', sans-serif;
      background: var(--bg);
      color: var(--fg);
      height: 100vh;
      overflow: hidden;
    }

    #map {
      width: 100%;
      height: 100%;
      z-index: 1;
    }

    .leaflet-container {
      background: #1e2636;
      font-family: inherit;
    }

    .leaflet-control-zoom {
      border: none !important;
      box-shadow: 0 4px 20px rgba(0,0,0,0.3) !important;
    }

    .leaflet-control-zoom a {
      background: var(--card) !important;
      color: var(--fg) !important;
      border: 1px solid var(--card-border) !important;
      width: 36px !important;
      height: 36px !important;
      line-height: 36px !important;
      font-size: 18px !important;
    }

    .leaflet-control-zoom a:hover {
      background: rgba(255, 107, 53, 0.2) !important;
      color: var(--accent) !important;
    }

    .custom-marker {
      width: 36px;
      height: 36px;
      border-radius: 50%;
      background: linear-gradient(135deg, var(--accent), #ff8c5a);
      border: 3px solid #fff;
      box-shadow: 0 4px 24px var(--accent-glow);
      display: flex;
      align-items: center;
      justify-content: center;
      animation: markerPulse 2s ease-in-out infinite;
    }

    @keyframes markerPulse {
      0%, 100% { transform: scale(1); }
      50% { transform: scale(1.08); }
    }

    .weather-panel {
      position: fixed;
      top: 20px;
      right: 20px;
      width: 400px;
      max-height: calc(100vh - 40px);
      background: var(--card);
      backdrop-filter: blur(24px);
      border: 1px solid var(--card-border);
      border-radius: 24px;
      padding: 28px;
      z-index: 1000;
      transform: translateX(460px);
      opacity: 0;
      transition: all 0.4s cubic-bezier(0.16, 1, 0.3, 1);
      overflow-y: auto;
      box-shadow: 0 20px 60px rgba(0,0,0,0.4);
    }

    .weather-panel.active {
      transform: translateX(0);
      opacity: 1;
    }

    /* 自定义滚动条 */
    .weather-panel::-webkit-scrollbar {
      width: 4px;
    }
    .weather-panel::-webkit-scrollbar-track {
      background: transparent;
    }
    .weather-panel::-webkit-scrollbar-thumb {
      background: var(--accent);
      border-radius: 10px;
    }

    .close-btn {
      position: sticky;
      top: 0;
      float: right;
      width: 36px;
      height: 36px;
      border-radius: 50%;
      background: rgba(255, 255, 255, 0.06);
      border: none;
      color: var(--muted);
      cursor: pointer;
      display: flex;
      align-items: center;
      justify-content: center;
      transition: all 0.2s ease;
      z-index: 10;
      margin-bottom: 8px;
    }

    .close-btn:hover {
      background: rgba(255, 107, 53, 0.2);
      color: var(--accent);
      transform: rotate(90deg);
    }

    .location-name {
      font-size: 1.6rem;
      font-weight: 700;
      margin-bottom: 4px;
      letter-spacing: -0.02em;
      line-height: 1.2;
    }

    .location-coords {
      font-size: 0.85rem;
      color: var(--muted);
      font-family: 'Space Grotesk', monospace;
    }

    .elevation-info {
      font-size: 0.8rem;
      color: var(--muted);
      opacity: 0.7;
      margin-top: 2px;
    }

    /* 当前天气 */
    .current-weather {
      margin-top: 20px;
      padding: 24px;
      background: linear-gradient(135deg, rgba(255, 107, 53, 0.15), rgba(255, 107, 53, 0.05));
      border-radius: 20px;
      display: flex;
      align-items: center;
      gap: 20px;
      border: 1px solid rgba(255, 107, 53, 0.1);
    }

    .weather-icon {
      width: 88px;
      height: 88px;
      flex-shrink: 0;
    }

    .weather-icon svg {
      width: 100%;
      height: 100%;
    }

    .temp-current {
      font-size: 3.5rem;
      font-weight: 700;
      font-family: 'Space Grotesk', sans-serif;
      line-height: 1;
    }

    .weather-desc {
      font-size: 1.05rem;
      color: var(--muted);
      margin-top: 4px;
    }

    .feels-like {
      font-size: 0.85rem;
      color: var(--muted);
      opacity: 0.7;
      margin-top: 2px;
    }

    /* 所有详情网格 */
    .weather-details {
      display: grid;
      grid-template-columns: repeat(3, 1fr);
      gap: 10px;
      margin-top: 20px;
    }

    .detail-item {
      padding: 14px 12px;
      background: rgba(255, 255, 255, 0.03);
      border-radius: 14px;
      border: 1px solid var(--card-border);
      transition: all 0.2s ease;
      text-align: center;
    }

    .detail-item:hover {
      background: rgba(255, 255, 255, 0.05);
      border-color: rgba(255, 107, 53, 0.2);
    }

    .detail-label {
      font-size: 0.65rem;
      color: var(--muted);
      text-transform: uppercase;
      letter-spacing: 0.06em;
    }

    .detail-value {
      font-size: 1.1rem;
      font-weight: 600;
      margin-top: 4px;
      font-family: 'Space Grotesk', sans-serif;
    }

    .detail-value .unit {
      font-size: 0.7rem;
      font-weight: 400;
      color: var(--muted);
      margin-left: 2px;
    }

    /* 日出日落 */
    .sun-section {
      display: grid;
      grid-template-columns: 1fr 1fr;
      gap: 10px;
      margin-top: 16px;
    }

    .sun-item {
      padding: 14px;
      background: rgba(255, 255, 255, 0.03);
      border-radius: 14px;
      border: 1px solid var(--card-border);
      text-align: center;
    }

    .sun-item .label {
      font-size: 0.7rem;
      color: var(--muted);
      text-transform: uppercase;
      letter-spacing: 0.06em;
    }

    .sun-item .value {
      font-size: 1.1rem;
      font-weight: 600;
      font-family: 'Space Grotesk', monospace;
      margin-top: 4px;
    }

    .sun-item .value.sunrise { color: #FFB347; }
    .sun-item .value.sunset { color: #FF6B6B; }

    .forecast-title {
      margin-top: 24px;
      font-size: 0.85rem;
      font-weight: 600;
      color: var(--muted);
      text-transform: uppercase;
      letter-spacing: 0.12em;
    }

    .forecast-list {
      margin-top: 14px;
      display: flex;
      flex-direction: column;
      gap: 10px;
    }

    .forecast-item {
      display: flex;
      align-items: center;
      padding: 14px 16px;
      background: rgba(255, 255, 255, 0.02);
      border-radius: 14px;
      border: 1px solid var(--card-border);
      transition: all 0.2s ease;
    }

    .forecast-item:hover {
      background: rgba(255, 255, 255, 0.04);
      border-color: rgba(255, 107, 53, 0.25);
      transform: translateX(4px);
    }

    .forecast-day {
      width: 60px;
      font-weight: 600;
      font-size: 0.9rem;
    }

    .forecast-icon {
      width: 36px;
      height: 36px;
      margin: 0 12px;
      flex-shrink: 0;
    }

    .forecast-icon svg {
      width: 100%;
      height: 100%;
    }

    .forecast-desc {
      flex: 1;
      font-size: 0.85rem;
      color: var(--muted);
    }

    .forecast-temps {
      display: flex;
      gap: 10px;
      font-family: 'Space Grotesk', sans-serif;
    }

    .temp-high {
      font-weight: 700;
      font-size: 1rem;
    }

    .temp-low {
      color: var(--muted);
      font-size: 1rem;
    }

    .forecast-extra {
      font-size: 0.7rem;
      color: var(--muted);
      opacity: 0.6;
      margin-top: 2px;
    }

    /* 小时预报 */
    .hourly-title {
      margin-top: 20px;
      font-size: 0.85rem;
      font-weight: 600;
      color: var(--muted);
      text-transform: uppercase;
      letter-spacing: 0.12em;
    }

    .hourly-scroll {
      display: flex;
      gap: 10px;
      overflow-x: auto;
      margin-top: 12px;
      padding-bottom: 8px;
    }

    .hourly-scroll::-webkit-scrollbar {
      height: 3px;
    }
    .hourly-scroll::-webkit-scrollbar-thumb {
      background: var(--accent);
      border-radius: 10px;
    }

    .hourly-item {
      flex: 0 0 72px;
      text-align: center;
      padding: 12px 6px;
      background: rgba(255, 255, 255, 0.03);
      border-radius: 12px;
      border: 1px solid var(--card-border);
      transition: all 0.2s ease;
    }

    .hourly-item:hover {
      background: rgba(255, 255, 255, 0.06);
      border-color: rgba(255, 107, 53, 0.2);
    }

    .hourly-item .time {
      font-size: 0.7rem;
      color: var(--muted);
      font-family: 'Space Grotesk', monospace;
    }

    .hourly-item .h-icon {
      width: 28px;
      height: 28px;
      margin: 4px auto;
    }

    .hourly-item .h-icon svg {
      width: 100%;
      height: 100%;
    }

    .hourly-item .h-temp {
      font-size: 0.95rem;
      font-weight: 600;
      font-family: 'Space Grotesk', sans-serif;
    }

    .hourly-item .h-precip {
      font-size: 0.6rem;
      color: #4A90D9;
      margin-top: 2px;
    }

    .hint-overlay {
      position: fixed;
      top: 50%;
      left: 50%;
      transform: translate(-50%, -50%);
      text-align: center;
      z-index: 999;
      pointer-events: none;
      opacity: 1;
      transition: opacity 0.4s ease;
    }

    .hint-overlay.hidden {
      opacity: 0;
    }

    .hint-icon {
      width: 72px;
      height: 72px;
      margin: 0 auto 20px;
      opacity: 0.5;
      animation: hintBounce 2.5s ease-in-out infinite;
    }

    @keyframes hintBounce {
      0%, 100% { transform: translateY(0); }
      50% { transform: translateY(-12px); }
    }

    .hint-text {
      font-size: 1.05rem;
      color: var(--fg);
      background: var(--card);
      padding: 16px 32px;
      border-radius: 100px;
      border: 1px solid var(--card-border);
      box-shadow: 0 10px 40px rgba(0,0,0,0.3);
    }

    .loading-spinner {
      width: 44px;
      height: 44px;
      border: 3px solid rgba(255, 107, 53, 0.15);
      border-top-color: var(--accent);
      border-radius: 50%;
      animation: spin 0.8s linear infinite;
      margin: 48px auto;
    }

    @keyframes spin {
      to { transform: rotate(360deg); }
    }

    .error-msg {
      text-align: center;
      padding: 48px 20px;
      color: var(--muted);
    }

    .data-source {
      margin-top: 16px;
      font-size: 0.6rem;
      color: var(--muted);
      opacity: 0.4;
      text-align: center;
      border-top: 1px solid var(--card-border);
      padding-top: 14px;
    }

    [url=home.php?mod=space&uid=945662]@media[/url] (max-width: 640px) {
      .weather-panel {
        width: calc(100% - 16px);
        right: 8px;
        top: auto;
        bottom: 8px;
        max-height: 60vh;
        padding: 20px;
        transform: translateY(100%);
        border-radius: 20px;
      }

      .weather-panel.active {
        transform: translateY(0);
      }

      .temp-current {
        font-size: 2.8rem;
      }

      .weather-icon {
        width: 70px;
        height: 70px;
      }

      .weather-details {
        grid-template-columns: repeat(2, 1fr);
      }
    }

    @media (prefers-reduced-motion: reduce) {
      *, *::before, *::after {
        animation-duration: 0.01ms !important;
        transition-duration: 0.01ms !important;
      }
    }
  </style>
</head>
<body>
  <div id="map"></div>

  <div class="hint-overlay" id="hint">
    <div class="hint-icon">
      <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
        <path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"/>
        <circle cx="12" cy="10" r="3"/>
      </svg>
    </div>
    <div class="hint-text">点击地图查看完整天气</div>
  </div>

  <div class="weather-panel" id="weatherPanel">
    <button class="close-btn" id="closeBtn" aria-label="关闭面板">
      <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round">
        <path d="M18 6L6 18M6 6l12 12"/>
      </svg>
    </button>
    <div id="panelContent">
      <div class="loading-spinner"></div>
    </div>
  </div>

  <script>
    // ============================================================
    // 1. 初始化地图
    // ============================================================
    const map = L.map('map', {
      center: [35, 105],
      zoom: 5,
      zoomControl: false,
      attributionControl: false
    });

    L.tileLayer('https://webrd0{s}.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=1&style=8&x={x}&y={y}&z={z}', {
      subdomains: ['1', '2', '3', '4'],
      maxZoom: 18
    }).addTo(map);

    L.control.zoom({ position: 'bottomleft' }).addTo(map);

    let currentMarker = null;
    const weatherPanel = document.getElementById('weatherPanel');
    const panelContent = document.getElementById('panelContent');
    const closeBtn = document.getElementById('closeBtn');
    const hint = document.getElementById('hint');

    // ============================================================
    // 2. 天气代码映射 (WMO 标准)
    // ============================================================
    const weatherCodes = {
      0: { desc: '晴天', icon: 'clear' },
      1: { desc: '基本晴朗', icon: 'partly-cloudy' },
      2: { desc: '局部多云', icon: 'partly-cloudy' },
      3: { desc: '阴天', icon: 'cloudy' },
      45: { desc: '雾', icon: 'fog' },
      48: { desc: '霜雾', icon: 'fog' },
      51: { desc: '小毛毛雨', icon: 'drizzle' },
      53: { desc: '毛毛雨', icon: 'drizzle' },
      55: { desc: '大毛毛雨', icon: 'drizzle' },
      56: { desc: '冻毛毛雨', icon: 'sleet' },
      57: { desc: '大冻毛毛雨', icon: 'sleet' },
      61: { desc: '小雨', icon: 'rain' },
      63: { desc: '中雨', icon: 'rain' },
      65: { desc: '大雨', icon: 'rain' },
      66: { desc: '冻雨', icon: 'sleet' },
      67: { desc: '大冻雨', icon: 'sleet' },
      71: { desc: '小雪', icon: 'snow' },
      73: { desc: '中雪', icon: 'snow' },
      75: { desc: '大雪', icon: 'snow' },
      77: { desc: '雪粒', icon: 'snow' },
      80: { desc: '小阵雨', icon: 'showers' },
      81: { desc: '阵雨', icon: 'showers' },
      82: { desc: '大阵雨', icon: 'showers' },
      85: { desc: '小阵雪', icon: 'snow' },
      86: { desc: '大阵雪', icon: 'snow' },
      95: { desc: '雷暴', icon: 'thunderstorm' },
      96: { desc: '雷暴伴小冰雹', icon: 'thunderstorm' },
      99: { desc: '雷暴伴大冰雹', icon: 'thunderstorm' }
    };

    // ============================================================
    // 3. SVG 天气图标生成器
    // ============================================================
    function getWeatherIcon(type, size = 64) {
      const icons = {
        'clear': `<svg viewBox="0 0 64 64" fill="none">
          <circle cx="32" cy="32" r="12" fill="#FFD93D"/>
          <g stroke="#FFD93D" stroke-width="2.5" stroke-linecap="round" opacity="0.8">
            <line x1="32" y1="8" x2="32" y2="14"/><line x1="32" y1="50" x2="32" y2="56"/>
            <line x1="8" y1="32" x2="14" y2="32"/><line x1="50" y1="32" x2="56" y2="32"/>
            <line x1="15.3" y1="15.3" x2="19.5" y2="19.5"/><line x1="44.5" y1="44.5" x2="48.7" y2="48.7"/>
            <line x1="15.3" y1="48.7" x2="19.5" y2="44.5"/><line x1="44.5" y1="19.5" x2="48.7" y2="15.3"/>
          </g>
        </svg>`,
        'partly-cloudy': `<svg viewBox="0 0 64 64" fill="none">
          <circle cx="22" cy="22" r="9" fill="#FFD93D"/>
          <path d="M46 48H20a11 11 0 1 1 1-22h1a14 14 0 0 1 27 5c0 4-1 8-4 11h1z" fill="#E8EDF5"/>
        </svg>`,
        'cloudy': `<svg viewBox="0 0 64 64" fill="none">
          <path d="M48 44H18a13 13 0 1 1 2-26 16 16 0 0 1 32 4c0 5-2 10-5 14h1z" fill="#D1D9E3"/>
        </svg>`,
        'fog': `<svg viewBox="0 0 64 64" fill="none">
          <g stroke="#B8C4D4" stroke-width="4" stroke-linecap="round"><line x1="10" y1="26" x2="54" y2="26"/><line x1="8" y1="38" x2="56" y2="38"/><line x1="10" y1="50" x2="54" y2="50"/></g>
        </svg>`,
        'drizzle': `<svg viewBox="0 0 64 64" fill="none">
          <path d="M46 32H20a11 11 0 1 1 1-22 14 14 0 0 1 27 5c0 4-1 7-3 10h1z" fill="#D1D9E3"/>
          <g stroke="#6BB3F0" stroke-width="2" stroke-linecap="round">
            <line x1="20" y1="42" x2="18" y2="50"/><line x1="32" y1="42" x2="30" y2="50"/><line x1="44" y1="42" x2="42" y2="50"/>
          </g>
        </svg>`,
        'rain': `<svg viewBox="0 0 64 64" fill="none">
          <path d="M46 30H20a11 11 0 1 1 1-22 14 14 0 0 1 27 5c0 4-1 7-3 10h1z" fill="#B8C4D4"/>
          <g stroke="#4A90D9" stroke-width="3" stroke-linecap="round">
            <line x1="18" y1="40" x2="14" y2="56"/><line x1="32" y1="40" x2="28" y2="56"/><line x1="46" y1="40" x2="42" y2="56"/>
          </g>
        </svg>`,
        'sleet': `<svg viewBox="0 0 64 64" fill="none">
          <path d="M46 28H20a11 11 0 1 1 1-22 14 14 0 0 1 27 5c0 4-1 7-3 10h1z" fill="#B8C4D4"/>
          <g stroke="#6BB3F0" stroke-width="2" stroke-linecap="round">
            <line x1="18" y1="38" x2="16" y2="46"/><line x1="46" y1="38" x2="44" y2="46"/>
          </g>
          <g fill="#A8C8E8"><circle cx="28" cy="50" r="3"/><circle cx="40" cy="54" r="3"/></g>
        </svg>`,
        'snow': `<svg viewBox="0 0 64 64" fill="none">
          <path d="M46 28H20a11 11 0 1 1 1-22 14 14 0 0 1 27 5c0 4-1 7-3 10h1z" fill="#D1D9E3"/>
          <g fill="#A8C8E8">
            <circle cx="18" cy="44" r="4"/><circle cx="32" cy="50" r="4"/><circle cx="46" cy="44" r="4"/>
            <circle cx="25" cy="56" r="3"/><circle cx="39" cy="56" r="3"/>
          </g>
        </svg>`,
        'showers': `<svg viewBox="0 0 64 64" fill="none">
          <path d="M46 26H20a11 11 0 1 1 1-22 14 14 0 0 1 27 5c0 4-1 7-3 10h1z" fill="#98A8B8"/>
          <g stroke="#4A90D9" stroke-width="4" stroke-linecap="round">
            <line x1="16" y1="38" x2="10" y2="58"/><line x1="32" y1="38" x2="26" y2="58"/><line x1="48" y1="38" x2="42" y2="58"/>
          </g>
        </svg>`,
        'thunderstorm': `<svg viewBox="0 0 64 64" fill="none">
          <path d="M46 24H20a11 11 0 1 1 1-22 14 14 0 0 1 27 5c0 4-1 7-3 10h1z" fill="#6B7A8C"/>
          <polygon points="36,32 28,44 34,44 30,58 44,40 38,40 42,32" fill="#FFD93D"/>
        </svg>`
      };
      return icons[type] || icons['partly-cloudy'];
    }

    // ============================================================
    // 4. 获取完整天气数据 (包含所有可用参数)
    // ============================================================
    async function getFullWeather(lat, lon) {
      // 包含 Open-Meteo 所有可用参数
      const params = {
        latitude: lat,
        longitude: lon,
        // 当前数据
        current: [
          'temperature_2m',
          'relative_humidity_2m',
          'apparent_temperature',
          'is_day',
          'precipitation',
          'rain',
          'snowfall',
          'weather_code',
          'cloud_cover',
          'wind_speed_10m',
          'wind_direction_10m',
          'wind_gusts_10m',
          'pressure_msl',
          'surface_pressure',
          'visibility'
        ].join(','),
        // 每日数据
        daily: [
          'weather_code',
          'temperature_2m_max',
          'temperature_2m_min',
          'apparent_temperature_max',
          'apparent_temperature_min',
          'sunrise',
          'sunset',
          'daylight_duration',
          'sunshine_duration',
          'uv_index_max',
          'precipitation_sum',
          'rain_sum',
          'snowfall_sum',
          'precipitation_hours',
          'precipitation_probability_max',
          'wind_speed_10m_max',
          'wind_gusts_10m_max',
          'wind_direction_10m_dominant'
        ].join(','),
        // 逐小时数据
        hourly: [
          'temperature_2m',
          'relative_humidity_2m',
          'apparent_temperature',
          'precipitation_probability',
          'precipitation',
          'rain',
          'snowfall',
          'weather_code',
          'cloud_cover',
          'visibility',
          'wind_speed_10m',
          'wind_direction_10m',
          'wind_gusts_10m',
          'uv_index'
        ].join(','),
        timezone: 'auto',
        forecast_days: '4'
      };

      const url = `https://api.open-meteo.com/v1/forecast?` + 
        Object.entries(params).map(([k, v]) => `${k}=${v}`).join('&');

      const res = await fetch(url);
      if (!res.ok) throw new Error(`HTTP ${res.status}`);
      return res.json();
    }

    // ============================================================
    // 5. 工具函数
    // ============================================================
    function formatDate(dateStr, index) {
      if (index === 0) return '今天';
      if (index === 1) return '明天';
      const date = new Date(dateStr);
      const weekdays = ['日', '一', '二', '三', '四', '五', '六'];
      return `${date.getMonth()+1}/${date.getDate()} 周${weekdays[date.getDay()]}`;
    }

    function formatTime(dateStr) {
      const d = new Date(dateStr);
      return d.toTimeString().slice(0, 5);
    }

    function formatHour(dateStr) {
      return new Date(dateStr).toTimeString().slice(0, 5);
    }

    function getDayName(dateStr) {
      const weekdays = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'];
      return weekdays[new Date(dateStr).getDay()];
    }

    // ============================================================
    // 6. 渲染完整天气面板
    // ============================================================
    async function renderWeatherPanel(lat, lon) {
      panelContent.innerHTML = '<div class="loading-spinner"></div>';
      weatherPanel.classList.add('active');
      hint.classList.add('hidden');

      try {
        const data = await getFullWeather(lat, lon);
        const current = data.current;
        const daily = data.daily;
        const hourly = data.hourly;

        // 当前天气
        const currentWeather = weatherCodes[current.weather_code] || { desc: '未知', icon: 'partly-cloudy' };
        const isDay = current.is_day === 1;

        // ---- 构建三日预报 ----
        let forecastHTML = '';
        for (let i = 0; i < Math.min(3, daily.time.length); i++) {
          const dayWeather = weatherCodes[daily.weather_code[i]] || { desc: '未知', icon: 'partly-cloudy' };
          const precipProb = daily.precipitation_probability_max ? Math.round(daily.precipitation_probability_max[i]) : null;
          const uvMax = daily.uv_index_max ? daily.uv_index_max[i].toFixed(1) : null;

          forecastHTML += `
            <div class="forecast-item">
              <div class="forecast-day">${formatDate(daily.time[i], i)}</div>
              <div class="forecast-icon">${getWeatherIcon(dayWeather.icon)}</div>
              <div style="flex:1">
                <div class="forecast-desc">${dayWeather.desc}</div>
                <div class="forecast-extra">
                  ${precipProb !== null ? `降水 ${precipProb}%` : ''}
                  ${uvMax !== null ? ` · UV ${uvMax}` : ''}
                </div>
              </div>
              <div class="forecast-temps">
                <span class="temp-high">${Math.round(daily.temperature_2m_max[i])}°</span>
                <span class="temp-low">${Math.round(daily.temperature_2m_min[i])}°</span>
              </div>
            </div>
          `;
        }

        // ---- 构建逐小时预报 (未来12小时) ----
        let hourlyHTML = '';
        const now = new Date();
        const hourStart = new Date(now);
        hourStart.setMinutes(0, 0, 0);

        let hourlyCount = 0;
        for (let i = 0; i < hourly.time.length && hourlyCount < 12; i++) {
          const hTime = new Date(hourly.time[i]);
          if (hTime >= now) {
            const hWeather = weatherCodes[hourly.weather_code[i]] || { desc: '未知', icon: 'partly-cloudy' };
            const precip = hourly.precipitation ? hourly.precipitation[i] : 0;
            hourlyHTML += `
              <div class="hourly-item">
                <div class="time">${formatHour(hourly.time[i])}</div>
                <div class="h-icon">${getWeatherIcon(hWeather.icon)}</div>
                <div class="h-temp">${Math.round(hourly.temperature_2m[i])}°</div>
                ${precip > 0 ? `<div class="h-precip">${precip.toFixed(1)}mm</div>` : ''}
              </div>
            `;
            hourlyCount++;
          }
        }

        // 如果没有未来小时数据(可能已过午夜),取前12个
        if (!hourlyHTML) {
          for (let i = 0; i < Math.min(12, hourly.time.length); i++) {
            const hWeather = weatherCodes[hourly.weather_code[i]] || { desc: '未知', icon: 'partly-cloudy' };
            const precip = hourly.precipitation ? hourly.precipitation[i] : 0;
            hourlyHTML += `
              <div class="hourly-item">
                <div class="time">${formatHour(hourly.time[i])}</div>
                <div class="h-icon">${getWeatherIcon(hWeather.icon)}</div>
                <div class="h-temp">${Math.round(hourly.temperature_2m[i])}°</div>
                ${precip > 0 ? `<div class="h-precip">${precip.toFixed(1)}mm</div>` : ''}
              </div>
            `;
          }
        }

      // ---- 组装主面板 ----
     // 新增:通过高德逆地理编码获取当前经纬度对应的地点名称
let locationName = '定位中...';
try {
  // &#9888;&#65039; 请将下方 YOUR_AMAP_KEY 替换为你自己申请的高德Web服务Key(免费额度足够个人使用)
  const amapKey = 'YOUR_AMAP_KEY';
  const geoRes = await fetch(`https://restapi.amap.com/v3/geocode/regeo?location=${lon},${lat}&key=${amapKey}&extensions=base`);
  const geoData = await geoRes.json();
  if (geoData.status === '1' && geoData.regeocode) {
    // 优先取格式化地址中的商圈/乡镇/街道,兜底取完整格式化地址
    locationName = geoData.regeocode.addressComponent?.township || geoData.regeocode.addressComponent?.streetNumber?.street || geoData.regeocode.formatted_address || '未知地点';
  } else {
    locationName = '未知地点';
  }
} catch (e) {
  console.warn('逆地理编码失败', e);
  locationName = '未知地点';
}

panelContent.innerHTML = `
  <!-- 位置信息 -->
  <div style="display:flex;justify-content:space-between;align-items:start;">
    <div>
      <div class="location-name">${lat.toFixed(3)}°N, ${lon.toFixed(3)}°E</div>
      <div class="location-coords">${locationName}</div>
      ${data.elevation ? `<div class="elevation-info">海拔 ${data.elevation}m</div>` : ''}
    </div>
    <div style="text-align:right;font-size:0.75rem;color:var(--muted);opacity:0.6;">
      ${isDay ? '&#9728;&#65039; 白天' : '&#127769; 夜间'}
    </div>
  </div>

          <!-- 当前天气 -->
          <div class="current-weather">
            <div class="weather-icon">${getWeatherIcon(currentWeather.icon)}</div>
            <div>
              <div class="temp-current">${Math.round(current.temperature_2m)}°</div>
              <div class="weather-desc">${currentWeather.desc}</div>
              <div class="feels-like">体感 ${Math.round(current.apparent_temperature)}°</div>
            </div>
          </div>

          <!-- 天气详情 (3列网格) -->
          <div class="weather-details">
            <div class="detail-item">
              <div class="detail-label">湿度</div>
              <div class="detail-value">${current.relative_humidity_2m}%</div>
            </div>
            <div class="detail-item">
              <div class="detail-label">风速</div>
              <div class="detail-value">${Math.round(current.wind_speed_10m)} <span class="unit">km/h</span></div>
            </div>
            <div class="detail-item">
              <div class="detail-label">阵风</div>
              <div class="detail-value">${Math.round(current.wind_gusts_10m || 0)} <span class="unit">km/h</span></div>
            </div>
            <div class="detail-item">
              <div class="detail-label">风向</div>
              <div class="detail-value">${current.wind_direction_10m !== undefined ? Math.round(current.wind_direction_10m) + '°' : '--'}</div>
            </div>
            <div class="detail-item">
              <div class="detail-label">云量</div>
              <div class="detail-value">${current.cloud_cover !== undefined ? Math.round(current.cloud_cover) + '%' : '--'}</div>
            </div>
            <div class="detail-item">
              <div class="detail-label">气压</div>
              <div class="detail-value">${current.pressure_msl !== undefined ? Math.round(current.pressure_msl) : '--'} <span class="unit">hPa</span></div>
            </div>
            <div class="detail-item">
              <div class="detail-label">降水</div>
              <div class="detail-value">${current.precipitation !== undefined ? current.precipitation.toFixed(1) : '0.0'} <span class="unit">mm</span></div>
            </div>
            <div class="detail-item">
              <div class="detail-label">能见度</div>
              <div class="detail-value">${current.visibility !== undefined ? Math.round(current.visibility/1000) : '--'} <span class="unit">km</span></div>
            </div>
            <div class="detail-item">
              <div class="detail-label">紫外线</div>
              <div class="detail-value">${current.uv_index !== undefined ? current.uv_index.toFixed(1) : '--'}</div>
            </div>
          </div>

          <!-- 日出日落 -->
          ${daily.sunrise && daily.sunset ? `
          <div class="sun-section">
            <div class="sun-item">
              <div class="label">&#127749; 日出</div>
              <div class="value sunrise">${formatTime(daily.sunrise[0])}</div>
            </div>
            <div class="sun-item">
              <div class="label">&#127751; 日落</div>
              <div class="value sunset">${formatTime(daily.sunset[0])}</div>
            </div>
          </div>
          ` : ''}

          <!-- 三日预报 -->
          <div class="forecast-title">&#128197; 三日预报</div>
          <div class="forecast-list">${forecastHTML}</div>

          <!-- 逐小时预报 -->
          <div class="hourly-title">&#9200; 逐小时预报</div>
          <div class="hourly-scroll">${hourlyHTML}</div>

          <!-- 数据来源 -->
          <div class="data-source">
            数据来源: Open-Meteo.com · 免费开源天气 API
          </div>
        `;

      } catch (error) {
        console.error('获取天气失败:', error);
        panelContent.innerHTML = `
          <div class="error-msg">
            <svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" style="margin: 0 auto 16px; opacity: 0.4;">
              <circle cx="12" cy="12" r="10"/><path d="M12 8v4M12 16h.01"/>
            </svg>
            <p>获取天气失败</p>
            <p style="font-size:0.8rem;margin-top:8px;opacity:0.6;">${error.message}</p>
          </div>
        `;
      }
    }

    // ============================================================
    // 7. 地图交互
    // ============================================================
    function createMarker(lat, lon) {
      if (currentMarker) map.removeLayer(currentMarker);

      const markerIcon = L.divIcon({
        className: 'custom-marker-wrapper',
        html: '<div class="custom-marker"><svg width="18" height="18" viewBox="0 0 24 24" fill="white"><path d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7z"/></svg></div>',
        iconSize: [36, 36],
        iconAnchor: [18, 18]
      });

      currentMarker = L.marker([lat, lon], { icon: markerIcon }).addTo(map);
    }

    map.on('click', function(e) {
      const lat = e.latlng.lat;
      const lon = e.latlng.lng;
      createMarker(lat, lon);
      renderWeatherPanel(lat, lon);
    });

    // ============================================================
    // 8. 关闭面板
    // ============================================================
    closeBtn.addEventListener('click', function() {
      weatherPanel.classList.remove('active');
      hint.classList.remove('hidden');
      if (currentMarker) {
        map.removeLayer(currentMarker);
        currentMarker = null;
      }
    });

    document.addEventListener('keydown', function(e) {
      if (e.key === 'Escape' && weatherPanel.classList.contains('active')) {
        closeBtn.click();
      }
    });
  </script>
</body>
</html>




我用AI帮着改了一下,可以在经纬度下面显示地址了,但是得用高德的逆地理编码获取当前经纬度对应的地点名称,需要自行申请高德web服务key,免费额度足够个人使用(月配额150000)。

免费评分

参与人数 1热心值 +1 收起 理由
wapjsx + 1 谢谢@Thanks!

查看全部评分

 楼主| noforgvie 发表于 2026-7-14 17:00
hualuoqixia 发表于 2026-7-14 16:19
厉害了我的哥
Seven_2017 发表于 2026-7-14 16:20
再装个tomcat 完美
dugupie 发表于 2026-7-14 16:31
实测好像不行, 天气预报不准, 出大太阳, 可预报的是雷暴~~~
qu1eRo 发表于 2026-7-14 16:31
还挺方便&#129761;&#129761;
yclyjb_001 发表于 2026-7-14 16:37
为什么我的天气乱码
shadow123456 发表于 2026-7-14 16:45
看了下,预报并不准
宇智波灬劫 发表于 2026-7-14 16:45
为啥一直显示的是上海
wahahehe 发表于 2026-7-14 17:06
试一下是否支持手机
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - 52pojie.cn ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2026-8-31 23:54

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表