
以 iOS 18 的设计风格做一个带有动画效果的天气卡片,要求是使用 HTML、CSS 和基础 JavaScript,使用横板天气页面(拥有 4 个天气卡片 (晴天,大风,暴雨,暴雪))。应足够美观,实现一定的交互效果。
HTML代码:
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>天气 · Weather</title>
<style>
*{margin:0;padding:0;box-sizing:border-box;-webkit-tap-highlight-color:transparent}
:root{
--ease:cubic-bezier(.34,1.56,.64,1);
--glass:rgba(255,255,255,.18);
--glass-brd:rgba(255,255,255,.35);
}
html,body{height:100%}
body{
font-family:-apple-system,BlinkMacSystemFont,"SF Pro Display","Segoe UI","PingFang SC","Microsoft YaHei",sans-serif;
background:#0a0a14;
color:#fff;
min-height:100vh;
display:flex;
flex-direction:column;
align-items:center;
justify-content:center;
padding:40px 20px;
overflow-x:hidden;
transition:background 1.2s ease;
}
/* 顶部标题 */
.head{
text-align:center;
margin-bottom:32px;
user-select:none;
}
.head h1{
font-size:30px;font-weight:700;letter-spacing:.5px;
text-shadow:0 2px 20px rgba(0,0,0,.4);
}
.head p{
margin-top:6px;font-size:14px;font-weight:500;opacity:.6;
}
/* 横向滚动容器 */
.scroller{
display:flex;
gap:26px;
padding:30px 10px 40px;
max-width:100%;
overflow-x:auto;
scroll-snap-type:x mandatory;
scrollbar-width:none;
}
.scroller::-webkit-scrollbar{display:none}
/* 卡片 */
.card{
position:relative;
flex:0 0 280px;
height:420px;
border-radius:34px;
padding:26px;
overflow:hidden;
cursor:pointer;
scroll-snap-align:center;
display:flex;
flex-direction:column;
justify-content:space-between;
isolation:isolate;
box-shadow:0 24px 50px -12px rgba(0,0,0,.55), inset 0 1px 0 rgba(255,255,255,.25);
transition:transform .55s var(--ease), box-shadow .55s var(--ease);
transform:perspective(900px);
}
.card:hover{
transform:perspective(900px) translateY(-14px) scale(1.03);
box-shadow:0 40px 70px -10px rgba(0,0,0,.6), inset 0 1px 0 rgba(255,255,255,.35);
}
.card:active{transform:perspective(900px) translateY(-6px) scale(.99)}
/* 各卡片背景渐变 */
.sunny{background:linear-gradient(160deg,#48a9ff 0%,#7ec8ff 45%,#ffd86b 120%)}
.windy{background:linear-gradient(160deg,#5a7d8c 0%,#9fb8c4 55%,#cfe0e6 120%)}
.rainy{background:linear-gradient(160deg,#243b55 0%,#3a5a78 55%,#1c2b3a 120%)}
.snowy{background:linear-gradient(160deg,#5d6d8c 0%,#8fa3c4 50%,#dfe8f5 120%)}
/* 玻璃高光层 */
.card::before{
content:"";position:absolute;inset:0;z-index:6;pointer-events:none;
background:radial-gradient(120% 80% at 20% 0%,rgba(255,255,255,.35),transparent 50%);
mix-blend-mode:overlay;
}
.card-top{position:relative;z-index:5}
.city{font-size:13px;font-weight:600;letter-spacing:2px;opacity:.85;text-transform:uppercase}
.cond{font-size:23px;font-weight:700;margin-top:2px;text-shadow:0 2px 10px rgba(0,0,0,.25)}
.temp{
position:relative;z-index:5;
font-size:74px;font-weight:300;line-height:1;
letter-spacing:-2px;
text-shadow:0 4px 24px rgba(0,0,0,.3);
}
.temp sup{font-size:28px;font-weight:400;top:-1.3em}
.meta{
position:relative;z-index:5;
display:flex;gap:18px;font-size:12.5px;font-weight:600;opacity:.9;
}
.meta div{display:flex;flex-direction:column;gap:3px}
.meta span{opacity:.6;font-weight:500;font-size:10.5px;letter-spacing:.5px}
/* 动画画布层 */
.fx{position:absolute;inset:0;z-index:2;pointer-events:none;overflow:hidden}
/* ---------- 晴天:太阳 ---------- */
.sun{
position:absolute;top:50px;right:46px;width:84px;height:84px;border-radius:50%;
background:radial-gradient(circle at 40% 40%,#fff6c2,#ffd23f 60%,#ff9d2f);
box-shadow:0 0 50px 14px rgba(255,210,63,.7);
animation:sun-pulse 4s ease-in-out infinite;
}
.sun::after{
content:"";position:absolute;inset:-26px;border-radius:50%;
background:conic-gradient(from 0deg,transparent 0 12%,rgba(255,225,120,.55) 12% 14%,transparent 14% 25%,rgba(255,225,120,.55) 25% 27%,transparent 27% 37%,rgba(255,225,120,.55) 37% 39%,transparent 39% 50%,rgba(255,225,120,.55) 50% 52%,transparent 52% 62%,rgba(255,225,120,.55) 62% 64%,transparent 64% 75%,rgba(255,225,120,.55) 75% 77%,transparent 77% 87%,rgba(255,225,120,.55) 87% 89%,transparent 89%);
animation:spin 18s linear infinite;
-webkit-mask:radial-gradient(circle,transparent 42px,#000 43px);
mask:radial-gradient(circle,transparent 42px,#000 43px);
}
@keyframes sun-pulse{0%,100%{transform:scale(1)}50%{transform:scale(1.07)}}
@keyframes spin{to{transform:rotate(360deg)}}
.heat{position:absolute;bottom:0;left:0;right:0;height:60%;
background:linear-gradient(transparent,rgba(255,221,128,.25));
animation:heat-wave 5s ease-in-out infinite;}
@keyframes heat-wave{0%,100%{opacity:.4}50%{opacity:.8}}
/* ---------- 大风:流线 ---------- */
.wind-line{
position:absolute;height:3px;border-radius:3px;
background:linear-gradient(90deg,transparent,rgba(255,255,255,.9),transparent);
left:-50%;opacity:0;
animation:gust 2.6s linear infinite;
}
@keyframes gust{
0%{transform:translateX(0);opacity:0}
15%{opacity:.9}
85%{opacity:.9}
100%{transform:translateX(360px);opacity:0}
}
.leaf{
position:absolute;font-size:20px;left:-30px;opacity:0;
animation:leaf-fly 3.4s linear infinite;
}
@keyframes leaf-fly{
0%{transform:translate(0,0) rotate(0);opacity:0}
10%{opacity:1}
90%{opacity:1}
100%{transform:translate(340px,40px) rotate(540deg);opacity:0}
}
/* ---------- 暴雨:雨滴 + 闪电 ---------- */
.drop{
position:absolute;top:-20px;width:2px;height:18px;border-radius:2px;
background:linear-gradient(rgba(255,255,255,.05),rgba(190,220,255,.9));
animation:fall linear infinite;
}
@keyframes fall{
0%{transform:translateY(-20px);opacity:0}
10%{opacity:1}
100%{transform:translateY(440px);opacity:.3}
}
.cloud{
position:absolute;top:42px;right:40px;width:96px;height:34px;border-radius:30px;
background:rgba(255,255,255,.55);filter:blur(1px);
box-shadow:-30px 6px 0 -6px rgba(255,255,255,.45),26px 8px 0 -8px rgba(255,255,255,.4);
animation:cloud-drift 6s ease-in-out infinite;
}
@keyframes cloud-drift{0%,100%{transform:translateX(0)}50%{transform:translateX(-12px)}}
.flash{position:absolute;inset:0;background:rgba(255,255,255,.85);opacity:0;z-index:4;mix-blend-mode:screen}
.flash.bolt{animation:bolt 6s ease-out infinite}
@keyframes bolt{
0%,100%{opacity:0}
1%{opacity:.9}2%{opacity:.1}3%{opacity:.8}5%{opacity:0}
}
/* ---------- 暴雪:雪花 ---------- */
.flake{
position:absolute;top:-12px;border-radius:50%;
background:radial-gradient(circle,#fff,rgba(255,255,255,.6));
box-shadow:0 0 6px rgba(255,255,255,.8);
animation:snow linear infinite;
}
@keyframes snow{
0%{transform:translate(0,-12px);opacity:0}
10%{opacity:1}
100%{transform:translate(var(--sx,20px),440px);opacity:.4}
}
.blizzard{position:absolute;inset:0;
background:linear-gradient(120deg,transparent,rgba(255,255,255,.12),transparent);
background-size:200% 100%;animation:bliz 3s linear infinite;}
@keyframes bliz{to{background-position:200% 0}}
/* 进场动画 */
.card{opacity:0;transform:perspective(900px) translateY(40px) scale(.94);animation:enter .8s var(--ease) forwards}
.card:nth-child(1){animation-delay:.05s}
.card:nth-child(2){animation-delay:.16s}
.card:nth-child(3){animation-delay:.27s}
.card:nth-child(4){animation-delay:.38s}
@keyframes enter{to{opacity:1;transform:perspective(900px) translateY(0) scale(1)}}
.hint{margin-top:6px;font-size:12px;opacity:.45;letter-spacing:1px;user-select:none}
@media(max-width:640px){
.scroller{gap:18px}
.card{flex-basis:240px;height:380px}
.temp{font-size:62px}
}
</style>
</head>
<body>
<div class="head">
<h1>天气预报</h1>
<p id="clock">--:--</p>
</div>
<div class="scroller" id="scroller"></div>
<div class="hint">← 左右滑动 · 点击卡片切换实况 →</div>
<script>
const data = [
{key:"sunny", cls:"sunny", city:"Sanya · 三亚", cond:"晴", temp:32, hi:34, lo:27, hum:"58%", wind:"2级", bg:"linear-gradient(160deg,#48a9ff,#7ec8ff 45%,#ffd86b 120%)"},
{key:"windy", cls:"windy", city:"Dalian · 大连", cond:"大风", temp:18, hi:21, lo:12, hum:"40%", wind:"8级", bg:"linear-gradient(160deg,#5a7d8c,#9fb8c4 55%,#cfe0e6 120%)"},
{key:"rainy", cls:"rainy", city:"Guangzhou · 广州", cond:"暴雨", temp:24, hi:26, lo:22, hum:"95%", wind:"6级", bg:"linear-gradient(160deg,#243b55,#3a5a78 55%,#1c2b3a 120%)"},
{key:"snowy", cls:"snowy", city:"Harbin · 哈尔滨", cond:"暴雪", temp:-12, hi:-8, lo:-19, hum:"82%", wind:"7级", bg:"linear-gradient(160deg,#5d6d8c,#8fa3c4 50%,#dfe8f5 120%)"},
];
const bodyBg = {
sunny:"radial-gradient(circle at 30% 20%,#1a3a5c,#0a0a14 70%)",
windy:"radial-gradient(circle at 30% 20%,#2a3a42,#0a0a14 70%)",
rainy:"radial-gradient(circle at 30% 20%,#0f1f33,#06060c 70%)",
snowy:"radial-gradient(circle at 30% 20%,#28324a,#0a0a14 70%)",
};
function fxFor(key){
const fx = document.createElement('div');
fx.className = 'fx';
if(key==='sunny'){
fx.innerHTML = `<div class="heat"></div><div class="sun"></div>`;
}
if(key==='windy'){
let h='';
for(let i=0;i<7;i++){
const top=20+i*52+Math.random()*20, w=80+Math.random()*120, d=(Math.random()*2).toFixed(2), dur=(2+Math.random()*1.5).toFixed(2);
h+=`<div class="wind-line" style="top:${top}px;width:${w}px;animation-delay:${d}s;animation-duration:${dur}s"></div>`;
}
h+=`<div class="leaf" style="top:90px;animation-delay:.4s">🍃</div><div class="leaf" style="top:240px;animation-delay:1.8s">🍂</div>`;
fx.innerHTML=h;
}
if(key==='rainy'){
let h=`<div class="cloud"></div><div class="flash bolt"></div>`;
for(let i=0;i<60;i++){
const left=Math.random()*100, d=(Math.random()*1.2).toFixed(2), dur=(0.45+Math.random()*0.35).toFixed(2);
h+=`<div class="drop" style="left:${left}%;animation-delay:${d}s;animation-duration:${dur}s"></div>`;
}
fx.innerHTML=h;
}
if(key==='snowy'){
let h=`<div class="blizzard"></div>`;
for(let i=0;i<55;i++){
const left=Math.random()*100, sz=(2+Math.random()*5).toFixed(1), d=(Math.random()*4).toFixed(2),
dur=(3+Math.random()*4).toFixed(2), sx=(Math.random()*120-60).toFixed(0);
h+=`<div class="flake" style="left:${left}%;width:${sz}px;height:${sz}px;--sx:${sx}px;animation-delay:${d}s;animation-duration:${dur}s"></div>`;
}
fx.innerHTML=h;
}
return fx;
}
const scroller = document.getElementById('scroller');
data.forEach(d=>{
const card=document.createElement('div');
card.className='card '+d.cls;
card.dataset.key=d.key;
card.innerHTML=`
<div class="card-top">
<div class="city">${d.city}</div>
<div class="cond">${d.cond}</div>
</div>
<div class="temp">${d.temp}<sup>°</sup></div>
<div class="meta">
<div><span>最高</span>${d.hi}°</div>
<div><span>最低</span>${d.lo}°</div>
<div><span>湿度</span>${d.hum}</div>
<div><span>风力</span>${d.wind}</div>
</div>`;
card.appendChild(fxFor(d.key));
// 点击:切换实时温度 + 主题背景 + 轻微抖动
card.addEventListener('click',()=>{
document.body.style.background = bodyBg[d.key];
const t=card.querySelector('.temp');
const cur=parseInt(t.firstChild.textContent);
const next=cur + (Math.random()>.5?1:-1);
t.firstChild.textContent=next;
card.animate([
{transform:'perspective(900px) translateY(-14px) scale(1.03)'},
{transform:'perspective(900px) translateY(-14px) scale(1.06) rotate(.6deg)'},
{transform:'perspective(900px) translateY(-14px) scale(1.03)'}
],{duration:380,easing:'cubic-bezier(.34,1.56,.64,1)'});
});
// 鼠标视差:跟随指针轻微倾斜
card.addEventListener('mousemove',e=>{
const r=card.getBoundingClientRect();
const px=(e.clientX-r.left)/r.width-0.5, py=(e.clientY-r.top)/r.height-0.5;
card.style.transform=`perspective(900px) translateY(-14px) scale(1.03) rotateY(${px*10}deg) rotateX(${-py*10}deg)`;
});
card.addEventListener('mouseleave',()=>{card.style.transform=''});
scroller.appendChild(card);
});
// 默认应用首张卡片主题
document.body.style.background = bodyBg[data[0].key];
// 时钟
function tick(){
const n=new Date();
document.getElementById('clock').textContent =
n.toLocaleDateString('zh-CN',{month:'long',day:'numeric',weekday:'long'})+' '+
n.toTimeString().slice(0,5);
}
tick();setInterval(tick,1000*30);
</script>
</body>
</html>
4 个帖子 - 4 位参与者