
[推广] [抽奖-固态硬盘] --聊聊 AI Agent 与移动编程,附福利互动!(第二轮二次推广)
[程序员] 我终于可以骄傲地说:直接从源码分析出程序完整的数据流向图谱,现在成为可能了!
<!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 位参与者