MiniMax M3 free 天气卡片测试

<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Weathe...
MiniMax M3 free 天气卡片测试
MiniMax M3 free 天气卡片测试

image
image
image

image

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Weather · 天气</title>
<style>
  * { margin: 0; padding: 0; box-sizing: border-box; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; }

  :root {
    --glass-bg: rgba(255, 255, 255, 0.1);
    --glass-border: rgba(255, 255, 255, 0.18);
    --glass-shadow: 0 8px 32px rgba(0, 0, 0, 0.15);
    --text-1: rgba(255, 255, 255, 1);
    --text-2: rgba(255, 255, 255, 0.72);
    --text-3: rgba(255, 255, 255, 0.5);
  }

  html, body { height: 100%; }

  body {
    font-family: -apple-system, BlinkMacSystemFont, "SF Pro Display", "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
    background: linear-gradient(135deg, #4F46E5 0%, #7C3AED 50%, #EC4899 100%);
    background-attachment: fixed;
    min-height: 100vh;
    color: var(--text-1);
    overflow-x: hidden;
    position: relative;
  }

  /* ===== 动态背景光斑 ===== */
  .bg { position: fixed; inset: 0; z-index: 0; overflow: hidden; pointer-events: none; }
  .orb {
    position: absolute; border-radius: 50%; filter: blur(80px); opacity: 0.55;
    animation: orbFloat 22s ease-in-out infinite;
  }
  .orb-1 { width: 520px; height: 520px; background: radial-gradient(circle, #fbbf24, transparent 70%); top: -160px; right: -120px; }
  .orb-2 { width: 460px; height: 460px; background: radial-gradient(circle, #ec4899, transparent 70%); bottom: -160px; left: -120px; animation-delay: -8s; }
  .orb-3 { width: 420px; height: 420px; background: radial-gradient(circle, #06b6d4, transparent 70%); top: 40%; left: 30%; animation-delay: -16s; }
  @keyframes orbFloat {
    0%, 100% { transform: translate(0, 0) scale(1); }
    33% { transform: translate(40px, -60px) scale(1.1); }
    66% { transform: translate(-30px, 40px) scale(0.95); }
  }

  /* 颗粒纹理 */
  .grain {
    position: fixed; inset: 0; z-index: 1; pointer-events: none; opacity: 0.04;
    background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='3'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
  }

  .app { position: relative; z-index: 2; max-width: 1280px; margin: 0 auto; padding: 32px 24px; }

  /* ===== 顶部 ===== */
  .header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 26px; flex-wrap: wrap; gap: 16px; }
  .logo { display: flex; align-items: center; gap: 12px; font-size: 20px; font-weight: 600; letter-spacing: -0.02em; }
  .logo-mark {
    width: 38px; height: 38px; border-radius: 11px;
    background: linear-gradient(135deg, #60a5fa, #a78bfa);
    display: grid; place-items: center;
    box-shadow: 0 6px 20px rgba(96, 165, 250, 0.4);
  }
  .logo-mark svg { width: 20px; height: 20px; }

  .city-tabs {
    display: flex; gap: 4px; padding: 5px;
    background: rgba(255, 255, 255, 0.08);
    backdrop-filter: blur(20px) saturate(180%);
    -webkit-backdrop-filter: blur(20px) saturate(180%);
    border: 1px solid rgba(255, 255, 255, 0.14);
    border-radius: 100px;
  }
  .tab {
    padding: 8px 18px; background: transparent; border: none;
    color: var(--text-2); font-size: 13.5px; font-weight: 500;
    cursor: pointer; border-radius: 100px; transition: all 0.3s ease;
    font-family: inherit; white-space: nowrap; letter-spacing: 0.02em;
  }
  .tab:hover { color: var(--text-1); }
  .tab.active { background: rgba(255, 255, 255, 0.2); color: var(--text-1); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); }

  /* ===== 布局 ===== */
  .main-grid { display: grid; grid-template-columns: 1.45fr 1fr; gap: 20px; }
  @media (max-width: 900px) { .main-grid { grid-template-columns: 1fr; } }

  /* ===== 玻璃卡片基础 ===== */
  .glass {
    background: rgba(255, 255, 255, 0.1);
    backdrop-filter: blur(20px) saturate(180%);
    -webkit-backdrop-filter: blur(20px) saturate(180%);
    border: 1px solid var(--glass-border);
    border-radius: 28px;
    box-shadow: var(--glass-shadow);
  }

  /* ===== 主卡片 ===== */
  .hero {
    padding: 30px;
    position: relative; overflow: hidden;
    min-height: 560px;
    display: flex; flex-direction: column;
    transition: opacity 0.3s ease;
  }
  .hero-glow {
    position: absolute; top: -30%; right: -30%; width: 80%; height: 130%;
    filter: blur(60px); pointer-events: none; z-index: 0;
    transition: background 0.8s ease;
  }
  .hero-header { display: flex; align-items: flex-start; justify-content: space-between; position: relative; z-index: 2; }
  .location { display: flex; align-items: center; gap: 10px; }
  .location-pin { width: 16px; height: 16px; color: var(--text-2); flex-shrink: 0; }
  .location-info h2 { font-size: 28px; font-weight: 600; letter-spacing: -0.02em; line-height: 1.1; }
  .location-sub { font-size: 13px; color: var(--text-2); margin-top: 4px; letter-spacing: 0.01em; }

  .time-block { text-align: right; }
  .time-now { font-size: 36px; font-weight: 200; font-variant-numeric: tabular-nums; letter-spacing: -0.02em; line-height: 1; }
  .time-label { font-size: 11px; color: var(--text-2); margin-top: 6px; letter-spacing: 0.1em; text-transform: uppercase; }

  .hero-main {
    flex: 1; display: flex; align-items: center; justify-content: center;
    gap: 36px; margin: 14px 0 20px; position: relative; z-index: 2;
  }
  .weather-icon { width: 200px; height: 200px; filter: drop-shadow(0 20px 40px rgba(0, 0, 0, 0.18)); }
  .weather-icon svg { width: 100%; height: 100%; }

  .temperature { display: flex; align-items: flex-start; position: relative; }
  .temp-value { font-size: 148px; font-weight: 200; line-height: 0.85; letter-spacing: -0.06em; font-variant-numeric: tabular-nums; }
  .temp-unit { font-size: 64px; font-weight: 200; margin-top: 14px; color: var(--text-2); line-height: 1; }

  .hero-info { text-align: center; margin-bottom: 22px; position: relative; z-index: 2; }
  .condition { font-size: 20px; font-weight: 500; margin-bottom: 6px; letter-spacing: -0.01em; }
  .hi-lo { font-size: 14px; color: var(--text-2); font-weight: 400; }
  .hi-lo span { margin: 0 6px; }
  .hi-lo .dot { color: var(--text-3); }

  .divider { height: 1px; background: rgba(255, 255, 255, 0.12); margin: 0 0 18px; position: relative; z-index: 2; }

  .hourly-label { font-size: 11px; color: var(--text-3); text-transform: uppercase; letter-spacing: 0.12em; font-weight: 600; margin-bottom: 10px; position: relative; z-index: 2; }
  .hourly { display: flex; gap: 4px; overflow-x: auto; padding-bottom: 4px; position: relative; z-index: 2; }
  .hourly::-webkit-scrollbar { height: 0; display: none; }
  .hour {
    flex: 1; min-width: 58px; display: flex; flex-direction: column; align-items: center; gap: 8px;
    padding: 10px 4px; border-radius: 16px; transition: background 0.2s; border: 1px solid transparent;
  }
  .hour.now { background: rgba(255, 255, 255, 0.14); border-color: rgba(255, 255, 255, 0.1); }
  .hour:hover { background: rgba(255, 255, 255, 0.08); }
  .hour-time { font-size: 12px; color: var(--text-2); font-weight: 500; }
  .hour.now .hour-time { color: var(--text-1); font-weight: 600; }
  .hour-icon { width: 32px; height: 32px; }
  .hour-icon svg { width: 100%; height: 100%; }
  .hour-temp { font-size: 15px; font-weight: 600; font-variant-numeric: tabular-nums; }
  .hour-rain { font-size: 10px; color: #93c5fd; font-weight: 500; min-height: 12px; display: flex; align-items: center; gap: 2px; }
  .hour-rain svg { width: 9px; height: 9px; }

  /* ===== 侧边 ===== */
  .side { display: flex; flex-direction: column; gap: 20px; }
  .stats-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }
  .stat {
    padding: 18px; border-radius: 20px;
    background: rgba(255, 255, 255, 0.1);
    backdrop-filter: blur(20px) saturate(180%);
    -webkit-backdrop-filter: blur(20px) saturate(180%);
    border: 1px solid var(--glass-border);
    box-shadow: var(--glass-shadow);
    transition: all 0.3s;
  }
  .stat:hover { background: rgba(255, 255, 255, 0.14); transform: translateY(-2px); }
  .stat-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; }
  .stat-label { font-size: 11px; color: var(--text-2); text-transform: uppercase; letter-spacing: 0.12em; font-weight: 600; }
  .stat-icon { width: 18px; height: 18px; color: rgba(255, 255, 255, 0.7); }
  .stat-value { font-size: 28px; font-weight: 300; letter-spacing: -0.02em; line-height: 1; font-variant-numeric: tabular-nums; }
  .stat-value .unit { font-size: 15px; color: var(--text-2); margin-left: 2px; }
  .stat-sub { font-size: 11px; color: var(--text-3); margin-top: 6px; }

  .bar { margin-top: 10px; height: 4px; background: rgba(255, 255, 255, 0.1); border-radius: 2px; overflow: hidden; }
  .bar-fill { height: 100%; border-radius: 2px; transition: width 0.8s cubic-bezier(0.4, 0, 0.2, 1); }

  /* 风罗盘 */
  .wind-info { display: flex; align-items: center; gap: 12px; margin-top: 6px; }
  .wind-compass { width: 44px; height: 44px; flex-shrink: 0; }
  .wind-compass svg { width: 100%; height: 100%; }
  .wind-detail { flex: 1; }
  .wind-speed { font-size: 24px; font-weight: 300; letter-spacing: -0.02em; line-height: 1; font-variant-numeric: tabular-nums; }
  .wind-dir { font-size: 11px; color: var(--text-3); margin-top: 4px; letter-spacing: 0.02em; }

  /* ===== 7 天预报 ===== */
  .forecast {
    padding: 22px; flex: 1;
    background: rgba(255, 255, 255, 0.1);
    backdrop-filter: blur(20px) saturate(180%);
    -webkit-backdrop-filter: blur(20px) saturate(180%);
    border: 1px solid var(--glass-border);
    border-radius: 24px;
    box-shadow: var(--glass-shadow);
  }
  .forecast-title { font-size: 11px; font-weight: 600; color: var(--text-2); text-transform: uppercase; letter-spacing: 0.12em; margin-bottom: 14px; }
  .forecast-list { display: flex; flex-direction: column; gap: 2px; }
  .forecast-item {
    display: grid; grid-template-columns: 50px 30px 1fr 72px; align-items: center; gap: 10px;
    padding: 9px 6px; border-radius: 12px; transition: background 0.2s;
  }
  .forecast-item:hover { background: rgba(255, 255, 255, 0.05); }
  .forecast-item.today { background: rgba(255, 255, 255, 0.07); }
  .forecast-day { font-size: 14px; font-weight: 500; }
  .forecast-day.today { font-weight: 600; }
  .forecast-icon { width: 28px; height: 28px; }
  .forecast-icon svg { width: 100%; height: 100%; }
  .forecast-bar { height: 4px; background: rgba(255, 255, 255, 0.08); border-radius: 2px; position: relative; overflow: hidden; }
  .forecast-bar-fill { position: absolute; height: 100%; border-radius: 2px; background: linear-gradient(90deg, #60a5fa, #fbbf24, #f87171); }
  .forecast-temp { font-size: 14px; font-weight: 500; text-align: right; font-variant-numeric: tabular-nums; display: flex; justify-content: flex-end; gap: 8px; }
  .forecast-temp .low { color: var(--text-3); }

  /* ===== 动画 ===== */
  @keyframes rotate { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
  @keyframes float-slow { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-6px); } }
  @keyframes rain-fall {
    0% { transform: translateY(-12px); opacity: 0; }
    20% { opacity: 1; }
    100% { transform: translateY(14px); opacity: 0; }
  }
  @keyframes twinkle { 0%, 100% { opacity: 0.35; } 50% { opacity: 1; } }
  @keyframes fadeIn { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: translateY(0); } }

  .sun-rays { animation: rotate 40s linear infinite; transform-origin: 100px 100px; }
  .float-slow { animation: float-slow 4s ease-in-out infinite; transform-origin: center; }
  .rain-drop { animation: rain-fall 1.2s ease-in infinite; }
  .rain-drop:nth-child(2) { animation-delay: -0.2s; }
  .rain-drop:nth-child(3) { animation-delay: -0.4s; }
  .rain-drop:nth-child(4) { animation-delay: -0.6s; }
  .rain-drop:nth-child(5) { animation-delay: -0.8s; }
  .rain-drop:nth-child(6) { animation-delay: -1.0s; }
  .twinkle { animation: twinkle 3s ease-in-out infinite; }
  .twinkle-2 { animation: twinkle 2.4s ease-in-out infinite; animation-delay: -1s; }

  /* ===== 响应式 ===== */
  @media (max-width: 640px) {
    .app { padding: 20px 16px; }
    .header { flex-direction: column; align-items: stretch; }
    .city-tabs { justify-content: space-between; }
    .hero { padding: 22px; min-height: auto; }
    .hero-main { flex-direction: column; gap: 6px; margin: 10px 0 16px; }
    .temp-value { font-size: 100px; }
    .temp-unit { font-size: 44px; margin-top: 8px; }
    .weather-icon { width: 140px; height: 140px; }
    .location-info h2 { font-size: 22px; }
    .time-now { font-size: 28px; }
    .hero-header { flex-direction: column; gap: 10px; }
    .time-block { text-align: left; }
    .stat-value { font-size: 24px; }
    .stat { padding: 14px; }
    .forecast { padding: 18px; }
    .forecast-item { grid-template-columns: 42px 26px 1fr 64px; gap: 8px; padding: 8px 4px; }
  }
</style>
</head>
<body>
<div class="bg">
  <div class="orb orb-1"></div>
  <div class="orb orb-2"></div>
  <div class="orb orb-3"></div>
</div>
<div class="grain"></div>

<div class="app">
  <header class="header">
    <div class="logo">
      <div class="logo-mark">
        <svg viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
          <circle cx="12" cy="12" r="4"/>
          <path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"/>
        </svg>
      </div>
      <span>Weather</span>
    </div>
    <nav class="city-tabs" id="cityTabs">
      <button class="tab active" data-city="tokyo">东京</button>
      <button class="tab" data-city="paris">巴黎</button>
      <button class="tab" data-city="newyork">纽约</button>
      <button class="tab" data-city="sydney">悉尼</button>
    </nav>
  </header>

  <main class="main-grid">
    <section class="hero glass" id="hero"></section>
    <aside class="side">
      <div class="stats-grid" id="statsGrid"></div>
      <div class="forecast" id="forecast"></div>
    </aside>
  </main>
</div>

<script>
/* ============ 城市数据 ============ */
const cityData = {
  tokyo: {
    name: '东京', country: '日本', timezone: 'Asia/Tokyo', dateStr: '星期二, 3月15日',
    temp: 24, condition: '晴朗', conditionKey: 'sunny', hi: 28, lo: 18, feels: 26,
    humidity: 68, wind: 12, windDir: '东北', windDeg: 45,
    uv: 5, uvLabel: '中等', visibility: 16,
    sunrise: '05:42', sunset: '17:58',
    hourly: [
      { time: '现在', icon: 'sunny', temp: 24, rain: 0 },
      { time: '14时', icon: 'sunny', temp: 25, rain: 0 },
      { time: '15时', icon: 'partly-cloudy', temp: 26, rain: 0 },
      { time: '16时', icon: 'partly-cloudy', temp: 25, rain: 10 },
      { time: '17时', icon: 'cloudy', temp: 24, rain: 20 },
      { time: '18时', icon: 'cloudy', temp: 22, rain: 20 },
      { time: '19时', icon: 'partly-cloudy', temp: 20, rain: 10 }
    ],
    forecast: [
      { day: '今天', icon: 'sunny', hi: 28, lo: 18, today: true },
      { day: '周三', icon: 'partly-cloudy', hi: 25, lo: 16 },
      { day: '周四', icon: 'rainy', hi: 20, lo: 14 },
      { day: '周五', icon: 'partly-cloudy', hi: 22, lo: 15 },
      { day: '周六', icon: 'sunny', hi: 26, lo: 17 },
      { day: '周日', icon: 'sunny', hi: 28, lo: 19 },
      { day: '周一', icon: 'partly-cloudy', hi: 24, lo: 16 }
    ]
  },
  paris: {
    name: '巴黎', country: '法国', timezone: 'Europe/Paris', dateStr: '星期二, 3月15日',
    temp: 14, condition: '多云', conditionKey: 'cloudy', hi: 17, lo: 9, feels: 12,
    humidity: 78, wind: 18, windDir: '西风', windDeg: 270,
    uv: 2, uvLabel: '低', visibility: 10,
    sunrise: '07:08', sunset: '18:54',
    hourly: [
      { time: '现在', icon: 'cloudy', temp: 14, rain: 30 },
      { time: '14时', icon: 'cloudy', temp: 14, rain: 30 },
      { time: '15时', icon: 'cloudy', temp: 15, rain: 40 },
      { time: '16时', icon: 'rainy', temp: 14, rain: 60 },
      { time: '17时', icon: 'rainy', temp: 13, rain: 70 },
      { time: '18时', icon: 'rainy', temp: 12, rain: 60 },
      { time: '19时', icon: 'cloudy', temp: 11, rain: 40 }
    ],
    forecast: [
      { day: '今天', icon: 'cloudy', hi: 17, lo: 9, today: true },
      { day: '周三', icon: 'rainy', hi: 12, lo: 7 },
      { day: '周四', icon: 'rainy', hi: 11, lo: 6 },
      { day: '周五', icon: 'cloudy', hi: 13, lo: 7 },
      { day: '周六', icon: 'partly-cloudy', hi: 15, lo: 8 },
      { day: '周日', icon: 'sunny', hi: 17, lo: 9 },
      { day: '周一', icon: 'partly-cloudy', hi: 16, lo: 8 }
    ]
  },
  newyork: {
    name: '纽约', country: '美国', timezone: 'America/New_York', dateStr: '星期二, 3月15日',
    temp: 8, condition: '雷阵雨', conditionKey: 'thunder', hi: 11, lo: 4, feels: 5,
    humidity: 85, wind: 22, windDir: '北风', windDeg: 0,
    uv: 1, uvLabel: '低', visibility: 6,
    sunrise: '06:54', sunset: '19:02',
    hourly: [
      { time: '现在', icon: 'thunder', temp: 8, rain: 80 },
      { time: '14时', icon: 'rainy', temp: 9, rain: 90 },
      { time: '15时', icon: 'thunder', temp: 9, rain: 90 },
      { time: '16时', icon: 'rainy', temp: 8, rain: 80 },
      { time: '17时', icon: 'rainy', temp: 7, rain: 70 },
      { time: '18时', icon: 'cloudy', temp: 6, rain: 50 },
      { time: '19时', icon: 'cloudy', temp: 5, rain: 40 }
    ],
    forecast: [
      { day: '今天', icon: 'thunder', hi: 11, lo: 4, today: true },
      { day: '周三', icon: 'rainy', hi: 9, lo: 3 },
      { day: '周四', icon: 'cloudy', hi: 10, lo: 4 },
      { day: '周五', icon: 'partly-cloudy', hi: 12, lo: 5 },
      { day: '周六', icon: 'sunny', hi: 14, lo: 6 },
      { day: '周日', icon: 'partly-cloudy', hi: 13, lo: 7 },
      { day: '周一', icon: 'rainy', hi: 11, lo: 6 }
    ]
  },
  sydney: {
    name: '悉尼', country: '澳大利亚', timezone: 'Australia/Sydney', dateStr: '星期二, 3月15日',
    temp: 19, condition: '夜晚晴朗', conditionKey: 'clear-night', hi: 23, lo: 15, feels: 18,
    humidity: 62, wind: 15, windDir: '东南', windDeg: 135,
    uv: 0, uvLabel: '无', visibility: 20,
    sunrise: '06:48', sunset: '19:12',
    hourly: [
      { time: '现在', icon: 'clear-night', temp: 19, rain: 0 },
      { time: '22时', icon: 'clear-night', temp: 18, rain: 0 },
      { time: '23时', icon: 'clear-night', temp: 17, rain: 0 },
      { time: '00时', icon: 'partly-night', temp: 16, rain: 0 },
      { time: '01时', icon: 'partly-night', temp: 16, rain: 0 },
      { time: '02时', icon: 'partly-night', temp: 15, rain: 0 },
      { time: '03时', icon: 'partly-night', temp: 15, rain: 0 }
    ],
    forecast: [
      { day: '今天', icon: 'clear-night', hi: 23, lo: 15, today: true },
      { day: '周三', icon: 'sunny', hi: 25, lo: 16 },
      { day: '周四', icon: 'sunny', hi: 27, lo: 17 },
      { day: '周五', icon: 'partly-cloudy', hi: 24, lo: 17 },
      { day: '周六', icon: 'cloudy', hi: 21, lo: 16 },
      { day: '周日', icon: 'rainy', hi: 19, lo: 14 },
      { day: '周一', icon: 'partly-cloudy', hi: 22, lo: 15 }
    ]
  }
};

/* ============ 通用图标 SVG ============ */
const ICONS_SVG = {
  location: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" 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>`,
  humidity: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2.69l5.66 5.66a8 8 0 1 1-11.31 0z"/></svg>`,
  wind: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9.59 4.59A2 2 0 1 1 11 8H2m10.59 11.41A2 2 0 1 0 14 16H2m15.73-8.27A2.5 2.5 0 1 1 19.5 12H2"/></svg>`,
  uv: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"/></svg>`,
  visibility: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>`,
  rain: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="16" y1="13" x2="16" y2="20"/><line x1="8" y1="13" x2="8" y2="20"/><line x1="12" y1="15" x2="12" y2="22"/></svg>`
};

/* ============ 天气图标生成器 ============ */
let _ic = 0;
function icon(name) {
  const i = ++_ic;
  switch (name) {
    case 'sunny':
      return `<svg viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
        <defs>
          <radialGradient id="sg${i}"><stop offset="0%" stop-color="#FFF3B0" stop-opacity="0.55"/><stop offset="100%" stop-color="#FFD93D" stop-opacity="0"/></radialGradient>
          <radialGradient id="sc${i}"><stop offset="0%" stop-color="#FFFCEB"/><stop offset="60%" stop-color="#FFD93D"/><stop offset="100%" stop-color="#FF9A3C"/></radialGradient>
        </defs>
        <circle cx="100" cy="100" r="80" fill="url(#sg${i})"/>
        <g class="sun-rays">
          <line x1="100" y1="20" x2="100" y2="42" stroke="#FFD93D" stroke-width="5" stroke-linecap="round"/>
          <line x1="100" y1="158" x2="100" y2="180" stroke="#FFD93D" stroke-width="5" stroke-linecap="round"/>
          <line x1="20" y1="100" x2="42" y2="100" stroke="#FFD93D" stroke-width="5" stroke-linecap="round"/>
          <line x1="158" y1="100" x2="180" y2="100" stroke="#FFD93D" stroke-width="5" stroke-linecap="round"/>
          <line x1="43" y1="43" x2="58" y2="58" stroke="#FFD93D" stroke-width="5" stroke-linecap="round"/>
          <line x1="142" y1="142" x2="157" y2="157" stroke="#FFD93D" stroke-width="5" stroke-linecap="round"/>
          <line x1="157" y1="43" x2="142" y2="58" stroke="#FFD93D" stroke-width="5" stroke-linecap="round"/>
          <line x1="58" y1="142" x2="43" y2="157" stroke="#FFD93D" stroke-width="5" stroke-linecap="round"/>
        </g>
        <circle cx="100" cy="100" r="38" fill="url(#sc${i})"/>
      </svg>`;
    case 'partly-cloudy':
      return `<svg viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
        <defs>
          <radialGradient id="pc${i}"><stop offset="0%" stop-color="#FFFCEB"/><stop offset="100%" stop-color="#FFD93D"/></radialGradient>
          <linearGradient id="cl${i}" x1="0%" y1="0%" x2="0%" y2="100%"><stop offset="0%" stop-color="#FFFFFF"/><stop offset="100%" stop-color="#D6DEE9"/></linearGradient>
        </defs>
        <g class="float-slow">
          <circle cx="70" cy="70" r="28" fill="url(#pc${i})"/>
          <g class="sun-rays">
            <line x1="70" y1="22" x2="70" y2="32" stroke="#FFD93D" stroke-width="3" stroke-linecap="round"/>
            <line x1="70" y1="108" x2="70" y2="118" stroke="#FFD93D" stroke-width="3" stroke-linecap="round"/>
            <line x1="22" y1="70" x2="32" y2="70" stroke="#FFD93D" stroke-width="3" stroke-linecap="round"/>
            <line x1="108" y1="70" x2="118" y2="70" stroke="#FFD93D" stroke-width="3" stroke-linecap="round"/>
            <line x1="37" y1="37" x2="44" y2="44" stroke="#FFD93D" stroke-width="3" stroke-linecap="round"/>
            <line x1="96" y1="96" x2="103" y2="103" stroke="#FFD93D" stroke-width="3" stroke-linecap="round"/>
            <line x1="103" y1="37" x2="96" y2="44" stroke="#FFD93D" stroke-width="3" stroke-linecap="round"/>
            <line x1="44" y1="96" x2="37" y2="103" stroke="#FFD93D" stroke-width="3" stroke-linecap="round"/>
          </g>
        </g>
        <path d="M 75 150 Q 42 150 42 122 Q 42 96 70 96 Q 76 74 102 74 Q 132 74 138 102 Q 168 102 168 125 Q 168 150 145 150 Z" fill="url(#cl${i})"/>
      </svg>`;
    case 'cloudy':
      return `<svg viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
        <defs>
          <linearGradient id="cc${i}" x1="0%" y1="0%" x2="0%" y2="100%"><stop offset="0%" stop-color="#FFFFFF"/><stop offset="100%" stop-color="#C7D2E0"/></linearGradient>
        </defs>
        <g class="float-slow">
          <path d="M 60 125 Q 28 125 28 100 Q 28 73 55 73 Q 60 46 92 46 Q 122 46 132 73 Q 168 73 168 105 Q 168 125 142 125 Z" fill="url(#cc${i})"/>
          <path d="M 50 160 Q 22 160 22 142 Q 22 125 45 125 Q 50 110 72 110 Q 96 110 102 128 Q 126 128 126 146 Q 126 160 108 160 Z" fill="url(#cc${i})" opacity="0.78"/>
        </g>
      </svg>`;
    case 'rainy':
      return `<svg viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
        <defs>
          <linearGradient id="rc${i}" x1="0%" y1="0%" x2="0%" y2="100%"><stop offset="0%" stop-color="#E2E8F0"/><stop offset="100%" stop-color="#94A3B8"/></linearGradient>
        </defs>
        <g class="float-slow">
          <path d="M 60 105 Q 28 105 28 80 Q 28 53 55 53 Q 60 28 92 28 Q 122 28 132 53 Q 168 53 168 85 Q 168 105 142 105 Z" fill="url(#rc${i})"/>
        </g>
        <line x1="55" y1="125" x2="50" y2="155" stroke="#60A5FA" stroke-width="4" stroke-linecap="round" class="rain-drop"/>
        <line x1="80" y1="130" x2="75" y2="160" stroke="#60A5FA" stroke-width="4" stroke-linecap="round" class="rain-drop"/>
        <line x1="105" y1="125" x2="100" y2="155" stroke="#60A5FA" stroke-width="4" stroke-linecap="round" class="rain-drop"/>
        <line x1="130" y1="130" x2="125" y2="160" stroke="#60A5FA" stroke-width="4" stroke-linecap="round" class="rain-drop"/>
        <line x1="65" y1="158" x2="60" y2="180" stroke="#60A5FA" stroke-width="3" stroke-linecap="round" opacity="0.5" class="rain-drop"/>
        <line x1="115" y1="158" x2="110" y2="180" stroke="#60A5FA" stroke-width="3" stroke-linecap="round" opacity="0.5" class="rain-drop"/>
      </svg>`;
    case 'thunder':
      return `<svg viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
        <defs>
          <linearGradient id="tc${i}" x1="0%" y1="0%" x2="0%" y2="100%"><stop offset="0%" stop-color="#CBD5E1"/><stop offset="100%" stop-color="#64748B"/></linearGradient>
        </defs>
        <g class="float-slow">
          <path d="M 60 100 Q 28 100 28 75 Q 28 48 55 48 Q 60 23 92 23 Q 122 23 132 48 Q 168 48 168 80 Q 168 100 142 100 Z" fill="url(#tc${i})"/>
        </g>
        <path d="M 102 105 L 75 142 L 95 142 L 80 178 L 128 130 L 108 130 L 122 105 Z" fill="#FCD34D" stroke="#F59E0B" stroke-width="1.5" stroke-linejoin="round"/>
      </svg>`;
    case 'clear-night':
      return `<svg viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
        <defs>
          <radialGradient id="mc${i}"><stop offset="0%" stop-color="#FFFFFF"/><stop offset="60%" stop-color="#E0E7FF"/><stop offset="100%" stop-color="#A5B4FC"/></radialGradient>
        </defs>
        <circle cx="125" cy="100" r="55" fill="url(#mc${i})"/>
        <circle cx="105" cy="90" r="50" fill="#4F46E5" opacity="0.4"/>
        <circle cx="55" cy="50" r="3" fill="#FFFFFF" class="twinkle"/>
        <circle cx="165" cy="55" r="2.5" fill="#FFFFFF" class="twinkle-2"/>
        <circle cx="50" cy="158" r="3" fill="#FFFFFF" class="twinkle-2"/>
        <circle cx="170" cy="160" r="2" fill="#FFFFFF" class="twinkle"/>
        <circle cx="35" cy="105" r="2" fill="#FFFFFF" class="twinkle"/>
        <circle cx="180" cy="105" r="2" fill="#FFFFFF" class="twinkle-2"/>
      </svg>`;
    case 'partly-night':
      return `<svg viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
        <defs>
          <radialGradient id="mc2${i}"><stop offset="0%" stop-color="#FFFFFF"/><stop offset="100%" stop-color="#A5B4FC"/></radialGradient>
          <linearGradient id="cl2${i}" x1="0%" y1="0%" x2="0%" y2="100%"><stop offset="0%" stop-color="#FFFFFF"/><stop offset="100%" stop-color="#C7D2E0"/></linearGradient>
        </defs>
        <circle cx="80" cy="65" r="32" fill="url(#mc2${i})"/>
        <circle cx="65" cy="58" r="28" fill="#4F46E5" opacity="0.5"/>
        <circle cx="155" cy="50" r="2" fill="#FFFFFF" class="twinkle"/>
        <circle cx="40" cy="105" r="2" fill="#FFFFFF" class="twinkle-2"/>
        <circle cx="170" cy="130" r="2" fill="#FFFFFF" class="twinkle"/>
        <g class="float-slow">
          <path d="M 65 150 Q 38 150 38 128 Q 38 106 60 106 Q 65 90 88 90 Q 113 90 118 112 Q 142 112 142 132 Q 142 150 122 150 Z" fill="url(#cl2${i})"/>
        </g>
      </svg>`;
    default: return '';
  }
}

/* ============ 工具 ============ */
function glowFor(key) {
  return {
    'sunny': 'radial-gradient(circle, rgba(255, 217, 61, 0.32) 0%, transparent 60%)',
    'partly-cloudy': 'radial-gradient(circle, rgba(255, 200, 100, 0.28) 0%, transparent 60%)',
    'cloudy': 'radial-gradient(circle, rgba(200, 215, 230, 0.22) 0%, transparent 60%)',
    'rainy': 'radial-gradient(circle, rgba(96, 165, 250, 0.28) 0%, transparent 60%)',
    'thunder': 'radial-gradient(circle, rgba(252, 211, 77, 0.25) 0%, transparent 60%)',
    'clear-night': 'radial-gradient(circle, rgba(165, 180, 252, 0.28) 0%, transparent 60%)',
    'partly-night': 'radial-gradient(circle, rgba(165, 180, 252, 0.22) 0%, transparent 60%)'
  }[key] || 'radial-gradient(circle, rgba(255, 217, 61, 0.3) 0%, transparent 60%)';
}

function uvBar(uv) {
  if (uv <= 2) return 'linear-gradient(90deg, #4ade80, #facc15)';
  if (uv <= 5) return 'linear-gradient(90deg, #facc15, #fb923c)';
  if (uv <= 7) return 'linear-gradient(90deg, #fb923c, #f87171)';
  return 'linear-gradient(90deg, #f87171, #a855f7)';
}

/* ============ 渲染 ============ */
function renderHero(city) {
  const d = cityData[city];
  const el = document.getElementById('hero');
  el.style.opacity = '0';
  setTimeout(() => {
    el.innerHTML = `
      <div class="hero-glow" style="background:${glowFor(d.conditionKey)};"></div>
      <div class="hero-header">
        <div class="location">
          <div class="location-pin">${ICONS_SVG.location}</div>
          <div class="location-info">
            <h2>${d.name}</h2>
            <div class="location-sub">${d.country} · ${d.dateStr}</div>
          </div>
        </div>
        <div class="time-block">
          <div class="time-now" id="timeNow">--:--</div>
          <div class="time-label">当地时间</div>
        </div>
      </div>
      <div class="hero-main">
        <div class="weather-icon">${icon(d.conditionKey)}</div>
        <div class="temperature">
          <span class="temp-value">${d.temp}</span>
          <span class="temp-unit">°</span>
        </div>
      </div>
      <div class="hero-info">
        <div class="condition">${d.condition}</div>
        <div class="hi-lo">最高 <span>${d.hi}°</span><span class="dot">·</span>最低 <span>${d.lo}°</span><span class="dot">·</span>体感 ${d.feels}°</div>
      </div>
      <div class="divider"></div>
      <div class="hourly-label">小时预报</div>
      <div class="hourly">
        ${d.hourly.map((h, idx) => `
          <div class="hour ${idx === 0 ? 'now' : ''}">
            <div class="hour-time">${h.time}</div>
            <div class="hour-icon">${icon(h.icon)}</div>
            <div class="hour-temp">${h.temp}°</div>
            <div class="hour-rain">${h.rain > 0 ? `${ICONS_SVG.rain}${h.rain}%` : ''}</div>
          </div>
        `).join('')}
      </div>
    `;
    el.style.opacity = '1';
    updateClock();
  }, 180);
}

function renderStats(city) {
  const d = cityData[city];
  document.getElementById('statsGrid').innerHTML = `
    <div class="stat">
      <div class="stat-header">
        <div class="stat-label">湿度</div>
        <div class="stat-icon">${ICONS_SVG.humidity}</div>
      </div>
      <div class="stat-value">${d.humidity}<span class="unit">%</span></div>
      <div class="stat-sub">${d.humidity > 70 ? '较为潮湿' : d.humidity > 40 ? '舒适宜人' : '较为干燥'}</div>
      <div class="bar"><div class="bar-fill" style="width:${d.humidity}%; background:linear-gradient(90deg, #60a5fa, #a78bfa);"></div></div>
    </div>
    <div class="stat">
      <div class="stat-header">
        <div class="stat-label">风速</div>
        <div class="stat-icon">${ICONS_SVG.wind}</div>
      </div>
      <div class="wind-info">
        <div class="wind-compass">
          <svg viewBox="0 0 50 50" xmlns="http://www.w3.org/2000/svg">
            <circle cx="25" cy="25" r="20" fill="none" stroke="rgba(255,255,255,0.15)" stroke-width="1"/>
            <circle cx="25" cy="25" r="14" fill="none" stroke="rgba(255,255,255,0.08)" stroke-width="0.5" stroke-dasharray="2,2"/>
            <text x="25" y="7" text-anchor="middle" font-size="5" fill="rgba(255,255,255,0.45)" font-family="sans-serif" font-weight="600">N</text>
            <text x="25" y="48" text-anchor="middle" font-size="5" fill="rgba(255,255,255,0.3)" font-family="sans-serif">S</text>
            <text x="4" y="28" text-anchor="middle" font-size="5" fill="rgba(255,255,255,0.3)" font-family="sans-serif">W</text>
            <text x="46" y="28" text-anchor="middle" font-size="5" fill="rgba(255,255,255,0.3)" font-family="sans-serif">E</text>
            <g transform="rotate(${d.windDeg} 25 25)">
              <path d="M 25 11 L 21.5 27 L 25 24 L 28.5 27 Z" fill="#60A5FA"/>
              <path d="M 25 39 L 22 28 L 28 28 Z" fill="rgba(255,255,255,0.25)"/>
              <circle cx="25" cy="25" r="2" fill="rgba(96,165,250,0.4)"/>
            </g>
          </svg>
        </div>
        <div class="wind-detail">
          <div class="wind-speed">${d.wind}<span class="unit"> km/h</span></div>
          <div class="wind-dir">${d.windDir} · ${d.wind > 20 ? '强风' : d.wind > 10 ? '和风' : '微风'}</div>
        </div>
      </div>
    </div>
    <div class="stat">
      <div class="stat-header">
        <div class="stat-label">紫外线</div>
        <div class="stat-icon">${ICONS_SVG.uv}</div>
      </div>
      <div class="stat-value">${d.uv}</div>
      <div class="stat-sub">${d.uvLabel}${d.uv >= 6 ? ' · 注意防晒' : ''}</div>
      <div class="bar"><div class="bar-fill" style="width:${Math.max((d.uv / 11) * 100, 4)}%; background:${uvBar(d.uv)};"></div></div>
    </div>
    <div class="stat">
      <div class="stat-header">
        <div class="stat-label">能见度</div>
        <div class="stat-icon">${ICONS_SVG.visibility}</div>
      </div>
      <div class="stat-value">${d.visibility}<span class="unit"> km</span></div>
      <div class="stat-sub">${d.visibility > 15 ? '极佳视野' : d.visibility > 10 ? '视野良好' : d.visibility > 5 ? '视野一般' : '视野较差'}</div>
    </div>
  `;
}

function renderForecast(city) {
  const d = cityData[city];
  const allHi = Math.max(...d.forecast.map(f => f.hi));
  const allLo = Math.min(...d.forecast.map(f => f.lo));
  const range = Math.max(allHi - allLo, 1);
  document.getElementById('forecast').innerHTML = `
    <div class="forecast-title">7 天预报</div>
    <div class="forecast-list">
      ${d.forecast.map(f => {
        const left = ((f.lo - allLo) / range) * 100;
        const width = ((f.hi - f.lo) / range) * 100;
        return `
          <div class="forecast-item ${f.today ? 'today' : ''}">
            <div class="forecast-day ${f.today ? 'today' : ''}">${f.day}</div>
            <div class="forecast-icon">${icon(f.icon)}</div>
            <div class="forecast-bar">
              <div class="forecast-bar-fill" style="left:${left}%; width:${Math.max(width, 6)}%;"></div>
            </div>
            <div class="forecast-temp">
              <span class="low">${f.lo}°</span>
              <span>${f.hi}°</span>
            </div>
          </div>
        `;
      }).join('')}
    </div>
  `;
}

function render(city) {
  renderHero(city);
  renderStats(city);
  renderForecast(city);
}

function updateClock() {
  const el = document.getElementById('timeNow');
  if (!el) return;
  try {
    el.textContent = new Date().toLocaleTimeString('zh-CN', {
      timeZone: cityData[currentCity].timezone,
      hour: '2-digit', minute: '2-digit', hour12: false
    });
  } catch (e) {}
}

/* ============ 初始化 ============ */
let currentCity = 'tokyo';
document.querySelectorAll('.tab').forEach(tab => {
  tab.addEventListener('click', () => {
    if (tab.classList.contains('active')) return;
    document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
    tab.classList.add('active');
    currentCity = tab.dataset.city;
    render(currentCity);
  });
});

render('tokyo');
setInterval(updateClock, 1000);
</script>
</body>
</html>

8 个帖子 - 7 位参与者

阅读完整话题

来源: LinuxDo 最新话题查看原文