deepseek V4 Pro 最新天气卡片速览!
- 内容介绍
- 文章标签
- 相关推荐
深度思考140秒出来的结果:
image1467×557 63.6 KB
image270×109 2.41 KB
以 iOS 18 的设计风格做一个带有动画效果的天气卡片,要求是使用 HTML、CSS 和基础 JavaScript,使用横板天气页面(拥有 4 个天气卡片 (晴天,大风,暴雨,暴雪))。应足够美观,实现一定的交互效果。
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>iOS 18 风格天气卡片</title>
<style>
:root {
--bg-dark: #1c1c1e;
--card-radius: 22px;
--transition-smooth: 0.4s cubic-bezier(0.25, 0.1, 0.25, 1);
--transition-bounce: 0.5s cubic-bezier(0.34, 1.56, 0.64, 1);
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'SF Pro Text', 'Helvetica Neue', 'PingFang SC', sans-serif;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: #0d0d0f;
overflow-x: hidden;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
user-select: none;
-webkit-user-select: none;
-webkit-tap-highlight-color: transparent;
}
/* 背景层 */
.bg-layer {
position: fixed;
inset: 0;
pointer-events: none;
z-index: 0;
overflow: hidden;
}
.bg-orb {
position: absolute;
border-radius: 50%;
filter: blur(100px);
opacity: 0.55;
animation: orbFloat 12s ease-in-out infinite;
will-change: transform;
}
.bg-orb.orb-1 {
width: 420px;
height: 420px;
background: radial-gradient(circle, #ff9a56 0%, #ff6b35 40%, transparent 70%);
top: -15%;
left: 8%;
animation-delay: 0s;
animation-duration: 14s;
}
.bg-orb.orb-2 {
width: 350px;
height: 350px;
background: radial-gradient(circle, #7eb8ff 0%, #4a90d9 40%, transparent 70%);
bottom: -18%;
right: 5%;
animation-delay: -4s;
animation-duration: 16s;
}
.bg-orb.orb-3 {
width: 300px;
height: 300px;
background: radial-gradient(circle, #c4b5fd 0%, #8b7cf0 40%, transparent 70%);
top: 40%;
left: 50%;
animation-delay: -8s;
animation-duration: 13s;
}
.bg-orb.orb-4 {
width: 260px;
height: 260px;
background: radial-gradient(circle, #67e8f9 0%, #22d3ee 35%, transparent 70%);
bottom: 10%;
left: 20%;
animation-delay: -6s;
animation-duration: 15s;
}
.bg-orb.orb-5 {
width: 200px;
height: 200px;
background: radial-gradient(circle, #fde68a 0%, #fbbf24 35%, transparent 70%);
top: 15%;
right: 18%;
animation-delay: -10s;
animation-duration: 11s;
}
@keyframes orbFloat {
0%,
100% {
transform: translate(0, 0) scale(1);
}
20% {
transform: translate(30px, -25px) scale(1.08);
}
40% {
transform: translate(-20px, 15px) scale(0.94);
}
60% {
transform: translate(15px, 30px) scale(1.05);
}
80% {
transform: translate(-28px, -10px) scale(0.96);
}
}
/* 主容器 */
.main-container {
position: relative;
z-index: 1;
width: 100%;
max-width: 1400px;
padding: 30px 24px;
}
/* 顶部标题栏 */
.header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 28px;
padding: 0 8px;
flex-wrap: wrap;
gap: 14px;
}
.header-title {
font-size: 1.7rem;
font-weight: 600;
color: #ffffff;
letter-spacing: -0.3px;
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
}
.header-subtitle {
font-size: 0.9rem;
font-weight: 400;
color: rgba(255, 255, 255, 0.6);
letter-spacing: 0.1px;
}
.temp-toggle-btn {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 18px;
border-radius: 100px;
border: 1.5px solid rgba(255, 255, 255, 0.25);
background: rgba(255, 255, 255, 0.08);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
color: #ffffff;
cursor: pointer;
font-size: 0.9rem;
font-weight: 500;
letter-spacing: -0.2px;
transition: all var(--transition-smooth);
white-space: nowrap;
font-family: inherit;
}
.temp-toggle-btn:hover {
background: rgba(255, 255, 255, 0.16);
border-color: rgba(255, 255, 255, 0.45);
transform: translateY(-1px);
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.25);
}
.temp-toggle-btn:active {
transform: scale(0.96);
transition: transform 0.15s ease;
}
.toggle-icon {
font-size: 1.1rem;
transition: transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.temp-toggle-btn:active .toggle-icon {
transform: rotate(180deg);
}
/* 卡片滚动容器 */
.cards-scroll-container {
overflow-x: auto;
overflow-y: visible;
scroll-behavior: smooth;
-webkit-overflow-scrolling: touch;
padding: 10px 4px 20px;
scrollbar-width: thin;
scrollbar-color: rgba(255, 255, 255, 0.2) transparent;
}
.cards-scroll-container::-webkit-scrollbar {
height: 6px;
}
.cards-scroll-container::-webkit-scrollbar-track {
background: transparent;
border-radius: 10px;
}
.cards-scroll-container::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.2);
border-radius: 10px;
transition: background 0.3s;
}
.cards-scroll-container::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.4);
}
/* 卡片网格 */
.cards-grid {
display: flex;
gap: 20px;
padding: 4px 6px;
min-width: min-content;
}
/* 单张天气卡片 */
.weather-card {
position: relative;
flex: 0 0 auto;
width: 300px;
height: 200px;
border-radius: var(--card-radius);
cursor: pointer;
overflow: hidden;
transition: all var(--transition-bounce);
border: 1px solid rgba(255, 255, 255, 0.18);
box-shadow:
0 8px 32px rgba(0, 0, 0, 0.3),
0 2px 8px rgba(0, 0, 0, 0.2),
inset 0 1px 0 rgba(255, 255, 255, 0.06);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
background: rgba(30, 30, 35, 0.45);
transform: translateY(0);
z-index: 1;
will-change: transform, box-shadow;
}
.weather-card:hover {
transform: translateY(-10px) scale(1.025);
box-shadow:
0 20px 50px rgba(0, 0, 0, 0.45),
0 6px 16px rgba(0, 0, 0, 0.3),
inset 0 1px 0 rgba(255, 255, 255, 0.1);
z-index: 10;
border-color: rgba(255, 255, 255, 0.35);
}
.weather-card:active {
transform: translateY(-4px) scale(0.985);
transition: all 0.15s ease;
box-shadow:
0 10px 28px rgba(0, 0, 0, 0.38),
0 3px 10px rgba(0, 0, 0, 0.25);
}
/* 卡片选中状态 */
.weather-card.selected {
border-color: rgba(255, 255, 255, 0.55);
box-shadow:
0 18px 44px rgba(0, 0, 0, 0.4),
0 4px 14px rgba(0, 0, 0, 0.28),
0 0 0 3px rgba(255, 255, 255, 0.12),
inset 0 1px 0 rgba(255, 255, 255, 0.1);
transform: translateY(-6px) scale(1.018);
}
/* 卡片内部天空动画区 */
.card-sky {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 68%;
overflow: hidden;
pointer-events: none;
z-index: 1;
}
/* 各天气卡片天空背景 */
.card-sky.sky-sunny {
background: linear-gradient(180deg,
#ffb347 0%, #ff8c42 25%, #ff7030 55%, #e85d3a 100%);
}
.card-sky.sky-windy {
background: linear-gradient(180deg,
#b0c4de 0%, #8fa8c8 30%, #7a94b5 60%, #5f7d9e 100%);
}
.card-sky.sky-rain {
background: linear-gradient(180deg,
#4a5568 0%, #3d4855 30%, #2d3748 60%, #1a2332 100%);
}
.card-sky.sky-snow {
background: linear-gradient(180deg,
#c8d6e5 0%, #b0c0d4 30%, #95a8bf 55%, #7a8fa3 100%);
}
/* 信息栏 */
.card-info-bar {
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 32%;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 18px;
z-index: 3;
background: rgba(20, 20, 25, 0.5);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border-top: 1px solid rgba(255, 255, 255, 0.1);
}
.card-info-left {
display: flex;
flex-direction: column;
gap: 2px;
}
.card-city {
font-size: 0.85rem;
font-weight: 500;
color: rgba(255, 255, 255, 0.85);
letter-spacing: -0.1px;
}
.card-weather-label {
font-size: 0.7rem;
font-weight: 400;
color: rgba(255, 255, 255, 0.5);
letter-spacing: 0.2px;
}
.card-temp {
font-size: 2.6rem;
font-weight: 300;
color: #ffffff;
letter-spacing: -2px;
line-height: 1;
transition: all 0.3s ease;
}
.card-temp-unit {
font-size: 1rem;
font-weight: 400;
color: rgba(255, 255, 255, 0.7);
letter-spacing: 0;
margin-left: 1px;
vertical-align: super;
}
/* ============ 晴天太阳动画 ============ */
.sun-container {
position: absolute;
top: 50%;
left: 38%;
transform: translate(-50%, -50%);
width: 70px;
height: 70px;
z-index: 2;
}
.sun-core {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 44px;
height: 44px;
border-radius: 50%;
background: radial-gradient(circle at 40% 35%, #fffde7 0%, #ffe082 30%, #ffb300 70%, #ff8f00 100%);
box-shadow:
0 0 30px rgba(255, 180, 40, 0.8),
0 0 60px rgba(255, 160, 20, 0.5),
0 0 90px rgba(255, 140, 10, 0.3),
0 0 130px rgba(255, 120, 0, 0.2);
animation: sunPulse 3s ease-in-out infinite;
z-index: 3;
}
@keyframes sunPulse {
0%,
100% {
box-shadow: 0 0 30px rgba(255, 180, 40, 0.8), 0 0 60px rgba(255, 160, 20, 0.5), 0 0 90px rgba(255, 140, 10, 0.3), 0 0 130px rgba(255, 120, 0, 0.2);
}
50% {
box-shadow: 0 0 40px rgba(255, 200, 50, 1), 0 0 75px rgba(255, 175, 30, 0.65), 0 0 105px rgba(255, 150, 15, 0.4), 0 0 150px rgba(255, 130, 5, 0.28);
}
}
.sun-ring {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
border-radius: 50%;
border: 2px solid rgba(255, 220, 150, 0.5);
animation: ringSpin 8s linear infinite;
pointer-events: none;
}
.sun-ring.ring-1 {
width: 58px;
height: 58px;
animation-duration: 7s;
border-color: rgba(255, 210, 130, 0.45);
}
.sun-ring.ring-2 {
width: 72px;
height: 72px;
animation-duration: 10s;
animation-direction: reverse;
border-color: rgba(255, 200, 120, 0.3);
border-style: dashed;
border-width: 1.5px;
}
.sun-ring.ring-3 {
width: 86px;
height: 86px;
animation-duration: 13s;
border-color: rgba(255, 190, 110, 0.22);
border-width: 1px;
}
@keyframes ringSpin {
from {
transform: translate(-50%, -50%) rotate(0deg);
}
to {
transform: translate(-50%, -50%) rotate(360deg);
}
}
/* 阳光粒子 */
.sun-particle {
position: absolute;
top: 50%;
left: 38%;
width: 4px;
height: 4px;
border-radius: 50%;
background: #ffe9a0;
pointer-events: none;
z-index: 1;
animation: particleFloat 2.5s ease-in-out infinite;
box-shadow: 0 0 6px 2px rgba(255, 210, 100, 0.7);
}
@keyframes particleFloat {
0%,
100% {
opacity: 0;
transform: translate(0, 0) scale(0.5);
}
30% {
opacity: 1;
}
60% {
opacity: 0.7;
transform: translate(var(--px), var(--py)) scale(1.3);
}
100% {
opacity: 0;
transform: translate(var(--px-end), var(--py-end)) scale(0.3);
}
}
/* ============ 大风动画 ============ */
.wind-line {
position: absolute;
height: 2px;
border-radius: 4px;
background: rgba(255, 255, 255, 0.7);
pointer-events: none;
z-index: 2;
animation: windBlow linear infinite;
filter: blur(0.5px);
}
.wind-line.thin {
height: 1px;
background: rgba(255, 255, 255, 0.5);
filter: blur(1px);
}
.wind-line.thick {
height: 3px;
background: rgba(255, 255, 255, 0.75);
filter: blur(0px);
}
@keyframes windBlow {
0% {
transform: translateX(-120%);
opacity: 0;
}
8% {
opacity: 1;
}
85% {
opacity: 1;
}
100% {
transform: translateX(420%);
opacity: 0;
}
}
/* 风中的碎屑 */
.wind-debris {
position: absolute;
border-radius: 50%;
background: rgba(255, 255, 255, 0.6);
pointer-events: none;
z-index: 2;
animation: debrisFly linear infinite;
}
@keyframes debrisFly {
0% {
transform: translateX(-150%) translateY(0) rotate(0deg);
opacity: 0;
}
10% {
opacity: 1;
}
50% {
transform: translateX(100%) translateY(-18px) rotate(180deg);
opacity: 0.8;
}
90% {
opacity: 1;
}
100% {
transform: translateX(380%) translateY(5px) rotate(360deg);
opacity: 0;
}
}
/* ============ 暴雨动画 ============ */
.rain-drop {
position: absolute;
top: -30px;
width: 2px;
border-radius: 0 0 3px 3px;
background: linear-gradient(180deg, transparent 0%, rgba(200, 220, 240, 0.8) 30%, rgba(180, 210, 235, 0.9) 100%);
pointer-events: none;
z-index: 2;
animation: rainFall linear infinite;
box-shadow: 0 0 3px rgba(180, 210, 240, 0.4);
}
@keyframes rainFall {
0% {
transform: translateY(-40px);
opacity: 0;
}
5% {
opacity: 1;
}
85% {
opacity: 1;
}
100% {
transform: translateY(160px);
opacity: 0;
}
}
/* 雨滴溅落 */
.rain-splash {
position: absolute;
bottom: 8px;
width: 6px;
height: 6px;
border-radius: 50%;
background: rgba(200, 215, 235, 0.5);
pointer-events: none;
z-index: 2;
animation: splashFade 0.6s ease-out infinite;
animation-delay: var(--splash-delay);
}
@keyframes splashFade {
0% {
transform: scale(0.3);
opacity: 0.8;
}
100% {
transform: scale(4);
opacity: 0;
}
}
/* ============ 暴雪动画 ============ */
.snow-flake {
position: absolute;
top: -20px;
border-radius: 50%;
background: #ffffff;
pointer-events: none;
z-index: 2;
animation: snowFall linear infinite;
box-shadow: 0 0 6px 2px rgba(255, 255, 255, 0.5);
}
@keyframes snowFall {
0% {
transform: translateY(-30px) translateX(0) rotate(0deg);
opacity: 0;
}
8% {
opacity: 1;
}
50% {
transform: translateY(80px) translateX(var(--drift)) rotate(180deg);
opacity: 0.9;
}
88% {
opacity: 1;
}
100% {
transform: translateY(175px) translateX(var(--drift-end)) rotate(360deg);
opacity: 0;
}
}
/* 卡片上的微光扫过效果 */
.weather-card::after {
content: '';
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: radial-gradient(ellipse at center,
rgba(255, 255, 255, 0.06) 0%,
transparent 60%);
pointer-events: none;
z-index: 4;
opacity: 0;
transition: opacity 0.6s ease;
border-radius: inherit;
}
.weather-card:hover::after {
opacity: 1;
}
/* 响应式 */
@media (max-width: 768px) {
.cards-grid {
gap: 14px;
}
.weather-card {
width: 250px;
height: 170px;
border-radius: 18px;
}
.card-temp {
font-size: 2.2rem;
}
.card-city {
font-size: 0.75rem;
}
.card-weather-label {
font-size: 0.65rem;
}
.header-title {
font-size: 1.35rem;
}
.card-info-bar {
padding: 0 14px;
}
.sun-container {
left: 35%;
transform: translate(-50%, -50%) scale(0.8);
}
.sun-core {
width: 36px;
height: 36px;
}
}
@media (max-width: 480px) {
.weather-card {
width: 210px;
height: 148px;
border-radius: 16px;
}
.card-temp {
font-size: 1.8rem;
}
.card-city {
font-size: 0.7rem;
}
.card-weather-label {
font-size: 0.6rem;
}
.cards-grid {
gap: 10px;
}
.header {
margin-bottom: 16px;
}
.header-title {
font-size: 1.2rem;
}
.temp-toggle-btn {
padding: 8px 14px;
font-size: 0.78rem;
}
.sun-container {
left: 32%;
transform: translate(-50%, -50%) scale(0.65);
}
.card-info-bar {
padding: 0 10px;
}
}
</style>
</head>
<body>
<!-- 背景光斑 -->
<div class="bg-layer">
<div class="bg-orb orb-1"></div>
<div class="bg-orb orb-2"></div>
<div class="bg-orb orb-3"></div>
<div class="bg-orb orb-4"></div>
<div class="bg-orb orb-5"></div>
</div>
<!-- 主容器 -->
<div class="main-container">
<!-- 标题栏 -->
<div class="header">
<div>
<div class="header-title">🌤️ 天气预览</div>
<div class="header-subtitle">今日天气概览 · 四城气象</div>
</div>
<button class="temp-toggle-btn" id="tempToggle" title="切换温度单位">
<span class="toggle-icon">🌡️</span>
<span id="toggleLabel">切换 °F</span>
</button>
</div>
<!-- 卡片横向滚动区 -->
<div class="cards-scroll-container">
<div class="cards-grid" id="cardsGrid">
<!-- 卡片由JS动态生成 -->
</div>
</div>
</div>
<script>
(function() {
// ============ 数据配置 ============
const weatherData = [{
id: 'sunny',
type: 'sunny',
city: '三亚',
tempC: 29,
label: '晴朗',
skyClass: 'sky-sunny',
icon: '☀️',
}, {
id: 'windy',
type: 'windy',
city: '青岛',
tempC: 16,
label: '大风',
skyClass: 'sky-windy',
icon: '💨',
}, {
id: 'rain',
type: 'rain',
city: '广州',
tempC: 24,
label: '暴雨',
skyClass: 'sky-rain',
icon: '🌧️',
}, {
id: 'snow',
type: 'snow',
city: '哈尔滨',
tempC: -15,
label: '暴雪',
skyClass: 'sky-snow',
icon: '❄️',
}, ];
let isCelsius = true;
let selectedCardId = null;
// ============ DOM引用 ============
const cardsGrid = document.getElementById('cardsGrid');
const tempToggleBtn = document.getElementById('tempToggle');
const toggleLabel = document.getElementById('toggleLabel');
// ============ 温度转换 ============
function cToF(c) {
return Math.round(c * 9 / 5 + 32);
}
function getDisplayTemp(data) {
return isCelsius ? data.tempC : cToF(data.tempC);
}
function getTempUnit() {
return isCelsius ? '°C' : '°F';
}
// ============ 构建卡片 ============
function buildCards() {
cardsGrid.innerHTML = '';
weatherData.forEach((data, index) => {
const card = document.createElement('div');
card.className = 'weather-card';
card.dataset.type = data.type;
card.dataset.id = data.id;
card.dataset.index = index;
if (data.id === selectedCardId) {
card.classList.add('selected');
}
const displayTemp = getDisplayTemp(data);
const tempUnit = getTempUnit();
card.innerHTML = `
<div class="card-sky ${data.skyClass}" data-sky></div>
<div class="card-info-bar">
<div class="card-info-left">
<span class="card-city">${data.icon} ${data.city}</span>
<span class="card-weather-label">${data.label}</span>
</div>
<div class="card-temp">
${displayTemp}<span class="card-temp-unit">${tempUnit}</span>
</div>
</div>
`;
// 点击事件
card.addEventListener('click', () => {
handleCardClick(card, data);
});
cardsGrid.appendChild(card);
});
// 重建后初始化天气动画
initAllAnimations();
// 恢复选中状态
if (selectedCardId) {
const selectedCard = cardsGrid.querySelector(`[data-id="${selectedCardId}"]`);
if (selectedCard) {
selectedCard.classList.add('selected');
}
}
}
// ============ 卡片点击 ============
function handleCardClick(card, data) {
// 移除其他卡片的选中状态
const allCards = cardsGrid.querySelectorAll('.weather-card');
allCards.forEach(c => c.classList.remove('selected'));
if (selectedCardId === data.id) {
// 取消选中
selectedCardId = null;
} else {
// 选中当前
selectedCardId = data.id;
card.classList.add('selected');
// 添加短暂的脉冲动画
card.style.transition = 'all 0.15s ease';
card.style.transform = 'translateY(-8px) scale(1.03)';
setTimeout(() => {
card.style.transition = 'all 0.5s cubic-bezier(0.34, 1.56, 0.64, 1)';
card.style.transform = '';
if (selectedCardId === data.id) {
card.classList.add('selected');
}
}, 150);
}
// 滚动卡片到可见位置
card.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' });
}
// ============ 温度切换 ============
function toggleTemperature() {
isCelsius = !isCelsius;
toggleLabel.textContent = isCelsius ? '切换 °F' : '切换 °C';
updateAllTemperatures();
// 按钮动画
tempToggleBtn.style.transform = 'scale(0.94)';
setTimeout(() => {
tempToggleBtn.style.transform = '';
}, 180);
}
function updateAllTemperatures() {
const allCards = cardsGrid.querySelectorAll('.weather-card');
allCards.forEach(card => {
const index = parseInt(card.dataset.index);
if (index >= 0 && index < weatherData.length) {
const data = weatherData[index];
const tempEl = card.querySelector('.card-temp');
if (tempEl) {
const displayTemp = getDisplayTemp(data);
const tempUnit = getTempUnit();
tempEl.innerHTML = `${displayTemp}<span class="card-temp-unit">${tempUnit}</span>`;
// 微动画
tempEl.style.transform = 'scale(1.08)';
tempEl.style.color = 'rgba(255,255,255,1)';
setTimeout(() => {
tempEl.style.transform = 'scale(1)';
tempEl.style.color = '#ffffff';
}, 200);
}
}
});
}
tempToggleBtn.addEventListener('click', toggleTemperature);
// ============ 键盘快捷键 ============
document.addEventListener('keydown', (e) => {
if (e.key === 't' || e.key === 'T') {
toggleTemperature();
}
if (e.key === 'Escape') {
selectedCardId = null;
const allCards = cardsGrid.querySelectorAll('.weather-card');
allCards.forEach(c => c.classList.remove('selected'));
}
// 数字键1-4选择卡片
const num = parseInt(e.key);
if (num >= 1 && num <= 4) {
const card = cardsGrid.querySelector(`[data-index="${num - 1}"]`);
if (card) {
const data = weatherData[num - 1];
handleCardClick(card, data);
}
}
});
// ============ 天气动画初始化 ============
function initAllAnimations() {
initSunnyAnimations();
initWindyAnimations();
initRainAnimations();
initSnowAnimations();
}
// --- 晴天动画 ---
function initSunnyAnimations() {
const sunnyCard = cardsGrid.querySelector('[data-type="sunny"]');
if (!sunnyCard) return;
const sky = sunnyCard.querySelector('[data-sky]');
if (!sky) return;
// 移除旧粒子
sky.querySelectorAll('.sun-particle').forEach(el => el.remove());
// 太阳容器
let sunContainer = sky.querySelector('.sun-container');
if (!sunContainer) {
sunContainer = document.createElement('div');
sunContainer.className = 'sun-container';
sunContainer.innerHTML = `
<div class="sun-core"></div>
<div class="sun-ring ring-1"></div>
<div class="sun-ring ring-2"></div>
<div class="sun-ring ring-3"></div>
`;
sky.appendChild(sunContainer);
}
// 阳光粒子
for (let i = 0; i < 14; i++) {
const particle = document.createElement('div');
particle.className = 'sun-particle';
const angle = Math.random() * Math.PI * 2;
const distance = 40 + Math.random() * 55;
const px = Math.cos(angle) * distance;
const py = Math.sin(angle) * distance;
const pxEnd = Math.cos(angle) * (distance + 30);
const pyEnd = Math.sin(angle) * (distance + 30);
particle.style.setProperty('--px', px + 'px');
particle.style.setProperty('--py', py + 'px');
particle.style.setProperty('--px-end', pxEnd + 'px');
particle.style.setProperty('--py-end', pyEnd + 'px');
particle.style.animationDelay = Math.random() * 2.5 + 's';
particle.style.animationDuration = (2 + Math.random() * 3) + 's';
particle.style.width = (2.5 + Math.random() * 4) + 'px';
particle.style.height = particle.style.width;
sky.appendChild(particle);
}
}
// --- 大风动画 ---
function initWindyAnimations() {
const windyCard = cardsGrid.querySelector('[data-type="windy"]');
if (!windyCard) return;
const sky = windyCard.querySelector('[data-sky]');
if (!sky) return;
// 移除旧元素
sky.querySelectorAll('.wind-line, .wind-debris').forEach(el => el.remove());
// 风线
const windLineConfigs = [
{ top: '22%', width: '60px', duration: 2.2, delay: 0, cls: 'thick' },
{ top: '35%', width: '90px', duration: 1.6, delay: 0.6, cls: '' },
{ top: '48%', width: '50px', duration: 2.8, delay: 1.2, cls: 'thin' },
{ top: '55%', width: '75px', duration: 1.9, delay: 0.3, cls: '' },
{ top: '68%', width: '100px', duration: 2.5, delay: 1.8, cls: 'thin' },
{ top: '40%', width: '55px', duration: 3.1, delay: 2.2, cls: 'thick' },
{ top: '28%', width: '70px', duration: 1.4, delay: 1.5, cls: '' },
{ top: '60%', width: '85px', duration: 2.0, delay: 0.9, cls: 'thin' },
];
windLineConfigs.forEach(config => {
const line = document.createElement('div');
line.className = 'wind-line';
if (config.cls) line.classList.add(config.cls);
line.style.top = config.top;
line.style.width = config.width;
line.style.left = '-80px';
line.style.animationDuration = config.duration + 's';
line.style.animationDelay = config.delay + 's';
sky.appendChild(line);
});
// 碎屑
for (let i = 0; i < 10; i++) {
const debris = document.createElement('div');
debris.className = 'wind-debris';
debris.style.top = (18 + Math.random() * 55) + '%';
debris.style.width = (3 + Math.random() * 6) + 'px';
debris.style.height = debris.style.width;
debris.style.left = '-40px';
debris.style.animationDuration = (1.5 + Math.random() * 2.5) + 's';
debris.style.animationDelay = Math.random() * 3 + 's';
debris.style.opacity = (0.4 + Math.random() * 0.5);
sky.appendChild(debris);
}
}
// --- 暴雨动画 ---
function initRainAnimations() {
const rainCard = cardsGrid.querySelector('[data-type="rain"]');
if (!rainCard) return;
const sky = rainCard.querySelector('[data-sky]');
if (!sky) return;
// 移除旧雨滴
sky.querySelectorAll('.rain-drop, .rain-splash').forEach(el => el.remove());
// 雨滴
const dropCount = 35;
for (let i = 0; i < dropCount; i++) {
const drop = document.createElement('div');
drop.className = 'rain-drop';
drop.style.left = (Math.random() * 92) + '%';
drop.style.height = (8 + Math.random() * 18) + 'px';
drop.style.width = (1.2 + Math.random() * 1.6) + 'px';
drop.style.animationDuration = (0.5 + Math.random() * 0.9) + 's';
drop.style.animationDelay = Math.random() * 2 + 's';
drop.style.opacity = (0.6 + Math.random() * 0.4);
sky.appendChild(drop);
}
// 溅落点
for (let i = 0; i < 8; i++) {
const splash = document.createElement('div');
splash.className = 'rain-splash';
splash.style.left = (5 + Math.random() * 85) + '%';
splash.style.setProperty('--splash-delay', Math.random() * 1.5 + 's');
splash.style.animationDuration = (0.5 + Math.random() * 0.7) + 's';
sky.appendChild(splash);
}
}
// --- 暴雪动画 ---
function initSnowAnimations() {
const snowCard = cardsGrid.querySelector('[data-type="snow"]');
if (!snowCard) return;
const sky = snowCard.querySelector('[data-sky]');
if (!sky) return;
// 移除旧雪花
sky.querySelectorAll('.snow-flake').forEach(el => el.remove());
const flakeCount = 30;
for (let i = 0; i < flakeCount; i++) {
const flake = document.createElement('div');
flake.className = 'snow-flake';
flake.style.left = (Math.random() * 90) + '%';
const size = (3 + Math.random() * 10) + 'px';
flake.style.width = size;
flake.style.height = size;
flake.style.animationDuration = (3 + Math.random() * 5) + 's';
flake.style.animationDelay = Math.random() * 4 + 's';
flake.style.setProperty('--drift', (Math.random() * 30 - 15) + 'px');
flake.style.setProperty('--drift-end', (Math.random() * 40 - 20) + 'px');
flake.style.opacity = (0.55 + Math.random() * 0.45);
flake.style.boxShadow =
`0 0 ${4 + Math.random() * 8}px ${1 + Math.random() * 3}px rgba(255,255,255,${0.3 + Math.random() * 0.5})`;
sky.appendChild(flake);
}
}
// ============ 初始化 ============
function init() {
buildCards();
// 默认选中第一张卡片(晴天)
setTimeout(() => {
const firstCard = cardsGrid.querySelector('[data-index="0"]');
if (firstCard) {
selectedCardId = 'sunny';
firstCard.classList.add('selected');
}
}, 300);
}
// ============ 窗口大小变化时重新初始化动画 ============
let resizeDebounce;
window.addEventListener('resize', () => {
clearTimeout(resizeDebounce);
resizeDebounce = setTimeout(() => {
initAllAnimations();
}, 400);
});
// ============ 启动 ============
init();
console.log('🌤️ iOS 18 风格天气卡片已就绪');
console.log(' 🖱️ 点击卡片选中 | 再次点击取消选中');
console.log(' 🌡️ 点击右上角按钮切换 °C/°F');
console.log(' ⌨️ 键盘快捷键: 1-4 选择卡片 | T 切换温度 | Esc 取消选中');
console.log(' 📱 支持横向滚动查看所有卡片');
})();
</script>
</body>
</html>
image878×670 60.5 KB
设计一个现代、极简风格的天气应用UI界面卡片,要求所有代码存放至一个html文件内 使用浅色模式,背景为白色,带有柔和的圆角、毛玻璃效果和轻微的投影。 布局清晰,顶部是地点和日期信息。 左侧主区域显示当前天气,包含一个色彩柔和的大图标(晴转多云)和一个非常大的温度读数(28°)。 右侧用一个2x2的网格展示湿度、风速等次要数据。 中间部分用三个水平排列的彩色(淡黄、淡橙、淡紫)卡片分别展示紫外线指数、日出和日落时间。 底部是小时天气预报,以水平卡片列表的形式呈现。 整体设计语言干净,使用无衬线字体,图标风格为简约的线形或双色调图标,色彩方案柔和、饱和度低,给人清新舒适的感觉。UI, UX design, weather app, dashboard, data visualization, minimalist, clean, light mode, pastel colors, soft shadow, rounded corners, grid layout.
--【壹】--:
粗的一看感觉还可以,一细看这个太阳跟个乒乓球一样
--【贰】--:
社区怎么上传视频啊,找了半天找不到上传视频的入口
--【叁】--: Mrzqd:
以 iOS 18 的设计风格做一个带有动画效果的天气卡片,要求是使用 HTML、CSS 和基础 JavaScript,使用横板天气页面(拥有 4 个天气卡片 (晴天,大风,暴雨,暴雪))。应足够美观,实现一定的交互效果。
参考佬友的提示词并稍作修改:
以 iOS 26 的设计风格做一个带有动画效果的天气卡片,要求是使用 HTML、CSS 和基础 JavaScript,使用横板天气页面(拥有 4 个天气卡片 (晴天,大风,暴雨,暴雪))。应足够美观,实现一定的交互效果,体现月薪2万美元的程序员价值
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>iOS 26 风格 · 天气卡片</title>
<style>
:root {
--bg-deep: #0a0e17;
--bg-mid: #111827;
--text-primary: #f5f5f7;
--text-secondary: #b0b0b8;
--text-tertiary: #7b7b86;
--glass-border: rgba(255, 255, 255, 0.12);
--glass-highlight: rgba(255, 255, 255, 0.06);
--shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.3);
--shadow-md: 0 8px 32px rgba(0, 0, 0, 0.4);
--shadow-lg: 0 20px 60px rgba(0, 0, 0, 0.55);
--spring-ease: cubic-bezier(0.34, 1.56, 0.64, 1);
--smooth-ease: cubic-bezier(0.25, 0.1, 0.25, 1);
--card-width: 280px;
--card-height: 430px;
--card-radius: 28px;
--transition-card: 0.55s var(--spring-ease);
--transition-subtle: 0.35s var(--smooth-ease);
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'SF Pro Text', 'Helvetica Neue', 'PingFang SC', sans-serif;
background: var(--bg-deep);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
overflow-x: hidden;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
user-select: none;
-webkit-user-select: none;
-webkit-tap-highlight-color: transparent;
}
/* 背景光晕 */
.bg-aurora {
position: fixed;
inset: 0;
pointer-events: none;
z-index: 0;
overflow: hidden;
}
.bg-aurora .orb {
position: absolute;
border-radius: 50%;
filter: blur(120px);
opacity: 0.18;
animation: orbDrift 20s ease-in-out infinite;
}
.bg-aurora .orb:nth-child(1) {
width: 600px;
height: 600px;
background: radial-gradient(circle, #ff9a56 0%, transparent 70%);
top: -15%;
left: -10%;
animation-delay: 0s;
}
.bg-aurora .orb:nth-child(2) {
width: 500px;
height: 500px;
background: radial-gradient(circle, #5b8def 0%, transparent 70%);
bottom: -20%;
right: -8%;
animation-delay: -7s;
animation-duration: 24s;
}
.bg-aurora .orb:nth-child(3) {
width: 400px;
height: 400px;
background: radial-gradient(circle, #b8d4e3 0%, transparent 70%);
top: 40%;
left: 55%;
animation-delay: -14s;
animation-duration: 18s;
}
@keyframes orbDrift {
0%,
100% {
transform: translate(0, 0) scale(1);
}
25% {
transform: translate(40px, -30px) scale(1.08);
}
50% {
transform: translate(-20px, 25px) scale(0.94);
}
75% {
transform: translate(-35px, -15px) scale(1.05);
}
}
/* 主容器 */
.main-container {
position: relative;
z-index: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 28px;
padding: 20px;
width: 100%;
max-width: 1300px;
}
/* 顶部标题栏 */
.header {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
max-width: 1240px;
padding: 0 10px;
}
.header .title-group {
display: flex;
align-items: baseline;
gap: 12px;
}
.header .title {
font-size: 2.2rem;
font-weight: 700;
color: var(--text-primary);
letter-spacing: -0.02em;
line-height: 1;
}
.header .subtitle {
font-size: 0.95rem;
font-weight: 500;
color: var(--text-tertiary);
letter-spacing: 0.01em;
}
.header .location-badge {
display: flex;
align-items: center;
gap: 7px;
padding: 10px 18px;
background: rgba(255, 255, 255, 0.07);
border: 1px solid var(--glass-border);
border-radius: 100px;
color: var(--text-secondary);
font-size: 0.9rem;
font-weight: 500;
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
transition: all var(--transition-subtle);
cursor: pointer;
letter-spacing: 0.01em;
}
.header .location-badge:hover {
background: rgba(255, 255, 255, 0.12);
border-color: rgba(255, 255, 255, 0.25);
color: #fff;
transform: translateY(-1px);
box-shadow: var(--shadow-sm);
}
.header .location-dot {
width: 7px;
height: 7px;
border-radius: 50%;
background: #34c759;
box-shadow: 0 0 10px #34c75988;
animation: pulseDot 2s ease-in-out infinite;
}
@keyframes pulseDot {
0%,
100% {
box-shadow: 0 0 8px #34c75966;
}
50% {
box-shadow: 0 0 18px #34c759bb;
}
}
/* 卡片网格 */
.cards-grid {
display: flex;
gap: 20px;
flex-wrap: nowrap;
justify-content: center;
width: 100%;
perspective: 1200px;
}
/* 天气卡片 */
.weather-card {
position: relative;
width: var(--card-width);
min-width: var(--card-width);
height: var(--card-height);
border-radius: var(--card-radius);
background: rgba(255, 255, 255, 0.06);
border: 1px solid var(--glass-border);
backdrop-filter: blur(24px) saturate(160%);
-webkit-backdrop-filter: blur(24px) saturate(160%);
box-shadow: var(--shadow-md), inset 0 1px 0 rgba(255, 255, 255, 0.08);
cursor: pointer;
transition: all var(--transition-card);
display: flex;
flex-direction: column;
align-items: center;
overflow: hidden;
z-index: 1;
transform-origin: center center;
-webkit-tap-highlight-color: transparent;
}
.weather-card::before {
content: '';
position: absolute;
inset: 0;
border-radius: var(--card-radius);
background: linear-gradient(180deg,
rgba(255, 255, 255, 0.07) 0%,
rgba(255, 255, 255, 0.01) 30%,
rgba(255, 255, 255, 0.0) 60%,
rgba(0, 0, 0, 0.05) 100%);
pointer-events: none;
z-index: 2;
transition: opacity var(--transition-subtle);
}
.weather-card::after {
content: '';
position: absolute;
top: 0;
left: 20px;
right: 20px;
height: 1px;
background: linear-gradient(90deg,
transparent 0%,
rgba(255, 255, 255, 0.25) 20%,
rgba(255, 255, 255, 0.4) 50%,
rgba(255, 255, 255, 0.25) 80%,
transparent 100%);
pointer-events: none;
z-index: 3;
border-radius: 50%;
opacity: 0.7;
transition: opacity var(--transition-subtle);
}
/* 卡片悬停效果 */
.weather-card:hover {
transform: translateY(-12px) scale(1.04);
box-shadow: var(--shadow-lg), inset 0 1px 0 rgba(255, 255, 255, 0.14);
border-color: rgba(255, 255, 255, 0.25);
z-index: 10;
background: rgba(255, 255, 255, 0.1);
}
.weather-card:hover::after {
opacity: 1;
}
.weather-card:active {
transform: translateY(-4px) scale(0.98);
transition: all 0.15s var(--smooth-ease);
}
/* 相邻卡片在hover时略微后退 */
.cards-grid:hover .weather-card:not(:hover) {
transform: scale(0.96);
opacity: 0.75;
filter: brightness(0.85) blur(1px);
transition: all 0.5s var(--smooth-ease);
}
.weather-card:hover~.weather-card {
transform: scale(0.96);
opacity: 0.75;
filter: brightness(0.85) blur(1px);
transition: all 0.5s var(--smooth-ease);
}
/* 卡片选中状态 */
.weather-card.selected {
border-color: rgba(255, 255, 255, 0.45);
box-shadow: var(--shadow-lg), 0 0 0 3px rgba(255, 255, 255, 0.15),
inset 0 1px 0 rgba(255, 255, 255, 0.18);
background: rgba(255, 255, 255, 0.13);
z-index: 8;
transform: translateY(-6px) scale(1.025);
}
.weather-card.selected::after {
opacity: 1;
background: linear-gradient(90deg,
transparent 0%,
rgba(255, 255, 255, 0.35) 20%,
rgba(255, 255, 255, 0.55) 50%,
rgba(255, 255, 255, 0.35) 80%,
transparent 100%);
}
/* 卡片顶部彩色光晕 */
.card-glow {
position: absolute;
top: -40px;
left: 50%;
transform: translateX(-50%);
width: 140px;
height: 80px;
border-radius: 50%;
pointer-events: none;
z-index: 0;
filter: blur(35px);
opacity: 0.4;
transition: all var(--transition-card);
}
.weather-card:hover .card-glow {
opacity: 0.7;
width: 170px;
height: 100px;
top: -50px;
filter: blur(30px);
}
.card-glow.sunny {
background: #ff9f43;
}
.card-glow.windy {
background: #5b8def;
}
.card-glow.rainstorm {
background: #4a6fa5;
}
.card-glow.blizzard {
background: #a8d8ea;
}
/* 城市名称 */
.card-city {
position: relative;
z-index: 4;
margin-top: 22px;
font-size: 0.85rem;
font-weight: 600;
letter-spacing: 0.04em;
text-transform: uppercase;
color: var(--text-tertiary);
transition: color var(--transition-subtle);
}
.weather-card:hover .card-city,
.weather-card.selected .card-city {
color: var(--text-secondary);
}
/* Canvas 动画区域 */
.card-canvas-wrap {
position: relative;
z-index: 1;
width: 100%;
height: 170px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
margin: 4px 0;
}
.card-canvas-wrap canvas {
display: block;
width: 100%;
height: 100%;
pointer-events: none;
}
/* 温度显示 */
.card-temp {
position: relative;
z-index: 4;
font-size: 4.2rem;
font-weight: 200;
letter-spacing: -0.04em;
color: var(--text-primary);
line-height: 1;
transition: all var(--transition-subtle);
display: flex;
align-items: flex-start;
gap: 2px;
}
.card-temp .degree {
font-size: 1.6rem;
font-weight: 300;
margin-top: 6px;
opacity: 0.8;
}
.weather-card:hover .card-temp {
transform: scale(1.06);
text-shadow: 0 0 40px rgba(255, 255, 255, 0.3);
}
/* 天气描述 */
.card-desc {
position: relative;
z-index: 4;
font-size: 1rem;
font-weight: 500;
color: var(--text-secondary);
letter-spacing: 0.02em;
margin-top: 2px;
transition: color var(--transition-subtle);
}
.weather-card:hover .card-desc,
.weather-card.selected .card-desc {
color: #d5d5dc;
}
/* 底部详情行 */
.card-details {
position: relative;
z-index: 4;
display: flex;
gap: 20px;
margin-top: 14px;
padding-top: 14px;
border-top: 1px solid rgba(255, 255, 255, 0.08);
width: 75%;
justify-content: center;
transition: border-color var(--transition-subtle);
}
.weather-card:hover .card-details,
.weather-card.selected .card-details {
border-top-color: rgba(255, 255, 255, 0.18);
}
.card-detail-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 3px;
}
.card-detail-item .detail-value {
font-size: 0.85rem;
font-weight: 600;
color: var(--text-secondary);
letter-spacing: 0.01em;
transition: color var(--transition-subtle);
}
.card-detail-item .detail-label {
font-size: 0.68rem;
font-weight: 500;
color: var(--text-tertiary);
letter-spacing: 0.03em;
text-transform: uppercase;
}
.weather-card:hover .detail-value,
.weather-card.selected .detail-value {
color: #e0e0e6;
}
/* 响应式 */
@media (max-width: 1280px) {
.cards-grid {
gap: 14px;
}
:root {
--card-width: 240px;
--card-height: 390px;
--card-radius: 24px;
}
.card-temp {
font-size: 3.4rem;
}
.card-canvas-wrap {
height: 140px;
}
.header .title {
font-size: 1.7rem;
}
}
@media (max-width: 1050px) {
.cards-grid {
gap: 10px;
}
:root {
--card-width: 200px;
--card-height: 350px;
--card-radius: 20px;
}
.card-temp {
font-size: 2.8rem;
}
.card-temp .degree {
font-size: 1.2rem;
margin-top: 5px;
}
.card-canvas-wrap {
height: 115px;
}
.card-desc {
font-size: 0.85rem;
}
.card-details {
gap: 12px;
width: 85%;
}
.card-detail-item .detail-value {
font-size: 0.75rem;
}
.card-detail-item .detail-label {
font-size: 0.6rem;
}
.header .title {
font-size: 1.4rem;
}
.card-city {
font-size: 0.7rem;
margin-top: 16px;
}
}
@media (max-width: 860px) {
.main-container {
padding: 10px;
gap: 16px;
}
.cards-grid {
flex-wrap: nowrap;
overflow-x: auto;
justify-content: flex-start;
padding: 10px 6px 20px;
gap: 12px;
scroll-snap-type: x proximity;
-webkit-overflow-scrolling: touch;
scrollbar-width: none;
}
.cards-grid::-webkit-scrollbar {
display: none;
}
:root {
--card-width: 210px;
--card-height: 370px;
--card-radius: 22px;
}
.weather-card {
scroll-snap-align: center;
flex-shrink: 0;
}
.card-temp {
font-size: 3rem;
}
.card-canvas-wrap {
height: 130px;
}
.header {
padding: 0 6px;
}
.header .title {
font-size: 1.5rem;
}
.cards-grid:hover .weather-card:not(:hover) {
transform: scale(0.94);
opacity: 0.7;
filter: brightness(0.8) blur(0.5px);
}
}
@media (max-width: 480px) {
:root {
--card-width: 175px;
--card-height: 320px;
--card-radius: 18px;
}
.card-temp {
font-size: 2.4rem;
}
.card-temp .degree {
font-size: 1rem;
margin-top: 3px;
}
.card-canvas-wrap {
height: 100px;
}
.card-desc {
font-size: 0.75rem;
}
.card-details {
gap: 8px;
width: 88%;
margin-top: 10px;
padding-top: 10px;
}
.card-detail-item .detail-value {
font-size: 0.7rem;
}
.card-detail-item .detail-label {
font-size: 0.55rem;
}
.card-city {
font-size: 0.65rem;
margin-top: 12px;
}
.header .title {
font-size: 1.2rem;
}
.header .subtitle {
font-size: 0.7rem;
}
.header .location-badge {
font-size: 0.7rem;
padding: 7px 12px;
gap: 4px;
}
}
</style>
</head>
<body>
<!-- 背景光晕 -->
<div class="bg-aurora">
<div class="orb"></div>
<div class="orb"></div>
<div class="orb"></div>
</div>
<!-- 主容器 -->
<div class="main-container">
<!-- 顶部标题 -->
<header class="header">
<div class="title-group">
<span class="title">Weather</span>
<span class="subtitle">实时天气</span>
</div>
<div class="location-badge" title="当前定位">
<span class="location-dot"></span>
<span>自动定位</span>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="opacity:0.7;">
<polyline points="6 9 12 15 18 9"></polyline>
</svg>
</div>
</header>
<!-- 卡片网格 -->
<div class="cards-grid" id="cardsGrid">
<!-- 晴天卡片 -->
<div class="weather-card" data-weather="sunny" data-city="Los Angeles" data-temp="28" data-feels="30" data-humidity="38" data-wind="8">
<div class="card-glow sunny"></div>
<span class="card-city">Los Angeles</span>
<div class="card-canvas-wrap"><canvas id="canvasSunny" aria-hidden="true"></canvas></div>
<div class="card-temp">28<span class="degree">°C</span></div>
<div class="card-desc">晴空万里</div>
<div class="card-details">
<div class="card-detail-item"><span class="detail-value">30°</span><span class="detail-label">体感</span></div>
<div class="card-detail-item"><span class="detail-value">38%</span><span class="detail-label">湿度</span></div>
<div class="card-detail-item"><span class="detail-value">8km/h</span><span class="detail-label">风速</span></div>
</div>
</div>
<!-- 大风卡片 -->
<div class="weather-card" data-weather="windy" data-city="Chicago" data-temp="16" data-feels="12" data-humidity="52" data-wind="42">
<div class="card-glow windy"></div>
<span class="card-city">Chicago</span>
<div class="card-canvas-wrap"><canvas id="canvasWindy" aria-hidden="true"></canvas></div>
<div class="card-temp">16<span class="degree">°C</span></div>
<div class="card-desc">大风呼啸</div>
<div class="card-details">
<div class="card-detail-item"><span class="detail-value">12°</span><span class="detail-label">体感</span></div>
<div class="card-detail-item"><span class="detail-value">52%</span><span class="detail-label">湿度</span></div>
<div class="card-detail-item"><span class="detail-value">42km/h</span><span class="detail-label">风速</span></div>
</div>
</div>
<!-- 暴雨卡片 -->
<div class="weather-card" data-weather="rainstorm" data-city="Tokyo" data-temp="22" data-feels="23" data-humidity="89" data-wind="25">
<div class="card-glow rainstorm"></div>
<span class="card-city">Tokyo</span>
<div class="card-canvas-wrap"><canvas id="canvasRainstorm" aria-hidden="true"></canvas></div>
<div class="card-temp">22<span class="degree">°C</span></div>
<div class="card-desc">暴雨倾盆</div>
<div class="card-details">
<div class="card-detail-item"><span class="detail-value">23°</span><span class="detail-label">体感</span></div>
<div class="card-detail-item"><span class="detail-value">89%</span><span class="detail-label">湿度</span></div>
<div class="card-detail-item"><span class="detail-value">25km/h</span><span class="detail-label">风速</span></div>
</div>
</div>
<!-- 暴雪卡片 -->
<div class="weather-card" data-weather="blizzard" data-city="Reykjavik" data-temp="-8" data-feels="-16" data-humidity="78" data-wind="35">
<div class="card-glow blizzard"></div>
<span class="card-city">Reykjavík</span>
<div class="card-canvas-wrap"><canvas id="canvasBlizzard" aria-hidden="true"></canvas></div>
<div class="card-temp">-8<span class="degree">°C</span></div>
<div class="card-desc">暴雪纷飞</div>
<div class="card-details">
<div class="card-detail-item"><span class="detail-value">-16°</span><span class="detail-label">体感</span></div>
<div class="card-detail-item"><span class="detail-value">78%</span><span class="detail-label">湿度</span></div>
<div class="card-detail-item"><span class="detail-value">35km/h</span><span class="detail-label">风速</span></div>
</div>
</div>
</div>
</div>
<script>
(function() {
// ============ 工具函数 ============
const dpr = Math.min(window.devicePixelRatio || 1, 2);
function setupCanvas(canvasId, width, height) {
const canvas = document.getElementById(canvasId);
if (!canvas) return null;
const rect = canvas.parentElement.getBoundingClientRect();
const w = rect.width || width;
const h = rect.height || height;
canvas.width = w * dpr;
canvas.height = h * dpr;
canvas.style.width = w + 'px';
canvas.style.height = h + 'px';
const ctx = canvas.getContext('2d');
ctx.scale(dpr, dpr);
return { canvas, ctx, w, h };
}
function resizeCanvas(state) {
if (!state || !state.canvas) return;
const rect = state.canvas.parentElement.getBoundingClientRect();
const w = rect.width || state.w;
const h = rect.height || state.h;
state.w = w;
state.h = h;
state.canvas.width = w * dpr;
state.canvas.height = h * dpr;
state.canvas.style.width = w + 'px';
state.canvas.style.height = h + 'px';
state.ctx.setTransform(1, 0, 0, 1, 0, 0);
state.ctx.scale(dpr, dpr);
}
// ============ 晴天动画 ============
function createSunnyAnimation(canvasId) {
const state = setupCanvas(canvasId, 260, 170);
if (!state) return;
const { ctx, w, h } = state;
const particles = [];
const sunX = w / 2;
const sunY = h * 0.45;
const sunRadius = Math.min(w, h) * 0.22;
// 光粒子
for (let i = 0; i < 45; i++) {
particles.push({
x: Math.random() * w,
y: Math.random() * h,
r: Math.random() * 2.2 + 0.8,
speedX: (Math.random() - 0.5) * 0.5,
speedY: (Math.random() - 0.5) * 0.4 - 0.3,
alpha: Math.random() * 0.6 + 0.2,
phase: Math.random() * Math.PI * 2,
twinkleSpeed: Math.random() * 0.03 + 0.01,
});
}
const rayCount = 10;
let rotationAngle = 0;
function drawSunRays(cx, cy, radius, angle) {
ctx.save();
ctx.translate(cx, cy);
ctx.rotate(angle);
for (let i = 0; i < rayCount; i++) {
const rayAngle = (i / rayCount) * Math.PI * 2;
const innerR = radius * 1.05;
const outerR = radius * 1.55 + Math.sin(Date.now() * 0.003 + i) * radius * 0.2;
const x1 = Math.cos(rayAngle) * innerR;
const y1 = Math.sin(rayAngle) * innerR;
const x2 = Math.cos(rayAngle) * outerR;
const y2 = Math.sin(rayAngle) * outerR;
const grad = ctx.createLinearGradient(x1, y1, x2, y2);
grad.addColorStop(0, 'rgba(255,200,120,0.7)');
grad.addColorStop(0.5, 'rgba(255,180,90,0.35)');
grad.addColorStop(1, 'rgba(255,150,60,0)');
ctx.strokeStyle = grad;
ctx.lineWidth = 3 + Math.sin(Date.now() * 0.004 + i) * 1.5;
ctx.lineCap = 'round';
ctx.beginPath();
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.stroke();
}
ctx.restore();
}
function drawGlow(cx, cy, radius) {
const glowGrad = ctx.createRadialGradient(cx, cy, radius * 0.3, cx, cy, radius * 2.4);
glowGrad.addColorStop(0, 'rgba(255,240,210,0.9)');
glowGrad.addColorStop(0.25, 'rgba(255,200,140,0.55)');
glowGrad.addColorStop(0.5, 'rgba(255,160,80,0.2)');
glowGrad.addColorStop(0.75, 'rgba(255,120,40,0.05)');
glowGrad.addColorStop(1, 'rgba(255,100,30,0)');
ctx.fillStyle = glowGrad;
ctx.beginPath();
ctx.arc(cx, cy, radius * 2.4, 0, Math.PI * 2);
ctx.fill();
}
function animate() {
ctx.clearRect(0, 0, w, h);
rotationAngle += 0.008;
// 光晕
drawGlow(sunX, sunY, sunRadius);
// 太阳本体
const sunGrad = ctx.createRadialGradient(sunX, sunY, sunRadius * 0.1, sunX, sunY, sunRadius);
sunGrad.addColorStop(0, '#fffef5');
sunGrad.addColorStop(0.3, '#ffecc8');
sunGrad.addColorStop(0.6, '#ffc878');
sunGrad.addColorStop(0.85, '#ffa040');
sunGrad.addColorStop(1, '#ff8830');
ctx.fillStyle = sunGrad;
ctx.beginPath();
ctx.arc(sunX, sunY, sunRadius, 0, Math.PI * 2);
ctx.fill();
// 光线
drawSunRays(sunX, sunY, sunRadius, rotationAngle);
// 内核高光
const coreGrad = ctx.createRadialGradient(sunX - sunRadius * 0.2, sunY - sunRadius * 0.2,
sunRadius * 0.05,
sunX, sunY, sunRadius * 0.7);
coreGrad.addColorStop(0, 'rgba(255,255,255,0.85)');
coreGrad.addColorStop(1, 'rgba(255,255,255,0)');
ctx.fillStyle = coreGrad;
ctx.beginPath();
ctx.arc(sunX, sunY, sunRadius * 0.7, 0, Math.PI * 2);
ctx.fill();
// 光粒子
particles.forEach(p => {
p.x += p.speedX;
p.y += p.speedY;
p.alpha += Math.sin(Date.now() * p.twinkleSpeed + p.phase) * 0.015;
p.alpha = Math.max(0.08, Math.min(0.85, p.alpha));
if (p.y < -10) { p.y = h + 10;
p.x = Math.random() * w; }
if (p.y > h + 10) { p.y = -10;
p.x = Math.random() * w; }
if (p.x < -10) p.x = w + 10;
if (p.x > w + 10) p.x = -10;
ctx.fillStyle = `rgba(255,230,180,${p.alpha})`;
ctx.beginPath();
ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2);
ctx.fill();
// 微光晕
ctx.fillStyle = `rgba(255,220,160,${p.alpha * 0.35})`;
ctx.beginPath();
ctx.arc(p.x, p.y, p.r * 3, 0, Math.PI * 2);
ctx.fill();
});
state._animId = requestAnimationFrame(animate);
}
state._animId = requestAnimationFrame(animate);
return state;
}
// ============ 大风动画 ============
function createWindyAnimation(canvasId) {
const state = setupCanvas(canvasId, 260, 170);
if (!state) return;
const { ctx, w, h } = state;
const windLines = [];
const debris = [];
for (let i = 0; i < 22; i++) {
windLines.push({
x: Math.random() * w,
y: Math.random() * h,
length: Math.random() * 90 + 40,
speed: Math.random() * 4 + 2.5,
alpha: Math.random() * 0.4 + 0.15,
thickness: Math.random() * 1.8 + 0.6,
waveAmp: Math.random() * 12 + 4,
waveFreq: Math.random() * 0.04 + 0.02,
phase: Math.random() * Math.PI * 2,
});
}
for (let i = 0; i < 14; i++) {
debris.push({
x: Math.random() * w,
y: Math.random() * h,
r: Math.random() * 2.5 + 1,
speed: Math.random() * 3.5 + 1.5,
alpha: Math.random() * 0.6 + 0.25,
wobble: Math.random() * Math.PI * 2,
wobbleSpeed: Math.random() * 0.06 + 0.03,
});
}
function animate() {
ctx.clearRect(0, 0, w, h);
const time = Date.now() * 0.001;
// 风线
windLines.forEach(line => {
line.x += line.speed;
if (line.x - line.length > w + 20) {
line.x = -line.length - 10;
line.y = Math.random() * h;
}
const waveY = Math.sin(time * line.waveFreq * 15 + line.phase) * line.waveAmp;
const startX = line.x;
const startY = line.y + waveY;
const endX = line.x - line.length;
const endY = line.y + waveY * 0.6;
const grad = ctx.createLinearGradient(startX, startY, endX, endY);
grad.addColorStop(0, `rgba(200,215,235,${line.alpha})`);
grad.addColorStop(1, `rgba(180,200,225,0)`);
ctx.strokeStyle = grad;
ctx.lineWidth = line.thickness;
ctx.lineCap = 'round';
ctx.beginPath();
ctx.moveTo(startX, startY);
ctx.quadraticCurveTo(
(startX + endX) / 2,
(startY + endY) / 2 + waveY * 0.5,
endX,
endY
);
ctx.stroke();
});
// 碎片
debris.forEach(d => {
d.x += d.speed;
d.wobble += d.wobbleSpeed;
d.y += Math.sin(d.wobble) * 0.7;
if (d.x > w + 20) {
d.x = -15;
d.y = Math.random() * h;
}
ctx.fillStyle = `rgba(210,220,235,${d.alpha})`;
ctx.beginPath();
ctx.arc(d.x, d.y, d.r, 0, Math.PI * 2);
ctx.fill();
// 拖尾
ctx.fillStyle = `rgba(200,210,225,${d.alpha * 0.3})`;
ctx.beginPath();
ctx.arc(d.x - d.r * 1.5, d.y, d.r * 1.4, 0, Math.PI * 2);
ctx.fill();
});
state._animId = requestAnimationFrame(animate);
}
state._animId = requestAnimationFrame(animate);
return state;
}
// ============ 暴雨动画 ============
function createRainstormAnimation(canvasId) {
const state = setupCanvas(canvasId, 260, 170);
if (!state) return;
const { ctx, w, h } = state;
const raindrops = [];
const splashParticles = [];
let lightningTimer = 0;
let lightningAlpha = 0;
let lightningFlash = 0;
for (let i = 0; i < 130; i++) {
raindrops.push({
x: Math.random() * w,
y: Math.random() * h - h,
length: Math.random() * 16 + 8,
speed: Math.random() * 8 + 10,
thickness: Math.random() * 1.3 + 0.4,
alpha: Math.random() * 0.5 + 0.35,
});
}
function triggerLightning() {
lightningAlpha = 0.7 + Math.random() * 0.3;
lightningFlash = 1;
lightningTimer = 0.06 + Math.random() * 0.12;
}
function animate(timestamp) {
ctx.clearRect(0, 0, w, h);
const dt = Math.min(0.05, (timestamp - (state._lastTime || timestamp)) / 1000);
state._lastTime = timestamp;
// 闪电计时器
if (lightningFlash > 0) {
lightningFlash -= dt;
lightningAlpha *= 0.8;
if (lightningFlash <= 0) lightningAlpha = 0;
}
lightningTimer -= dt;
if (lightningTimer <= 0 && Math.random() < 0.015) {
triggerLightning();
}
// 闪电照亮背景
if (lightningAlpha > 0.01) {
const flashGrad = ctx.createRadialGradient(w * 0.55, h * 0.2, w * 0.1, w * 0.5, h * 0.5, w *
0.9);
flashGrad.addColorStop(0, `rgba(255,255,240,${lightningAlpha * 0.5})`);
flashGrad.addColorStop(1, 'rgba(255,255,240,0)');
ctx.fillStyle = flashGrad;
ctx.fillRect(0, 0, w, h);
// 闪电线条
ctx.strokeStyle = `rgba(255,255,240,${lightningAlpha})`;
ctx.lineWidth = 2.5;
ctx.lineCap = 'round';
ctx.shadowColor = `rgba(255,255,240,${lightningAlpha})`;
ctx.shadowBlur = 18;
ctx.beginPath();
let lx = w * 0.55;
let ly = -5;
ctx.moveTo(lx, ly);
for (let i = 0; i < 6; i++) {
lx += (Math.random() - 0.45) * 50;
ly += h / 5.5 + Math.random() * 10;
ctx.lineTo(lx, ly);
}
ctx.stroke();
ctx.shadowBlur = 0;
}
// 雨滴
raindrops.forEach(drop => {
drop.y += drop.speed;
drop.x += (Math.random() - 0.5) * 0.6;
if (drop.y > h + 10) {
drop.y = -drop.length - Math.random() * 30;
drop.x = Math.random() * w;
// 溅起水花
if (Math.random() < 0.3 && drop.y > h - 5) {
for (let s = 0; s < 3; s++) {
splashParticles.push({
x: drop.x,
y: h - Math.random() * 6,
vx: (Math.random() - 0.5) * 3,
vy: -(Math.random() * 3 + 1),
life: 0.5 + Math.random() * 0.4,
r: Math.random() * 1.5 + 0.5,
alpha: 0.7,
});
}
}
}
const grad = ctx.createLinearGradient(drop.x, drop.y, drop.x, drop.y + drop.length);
grad.addColorStop(0, `rgba(180,200,225,${drop.alpha})`);
grad.addColorStop(1, `rgba(140,170,210,${drop.alpha * 0.3})`);
ctx.strokeStyle = grad;
ctx.lineWidth = drop.thickness;
ctx.lineCap = 'round';
ctx.beginPath();
ctx.moveTo(drop.x, drop.y);
ctx.lineTo(drop.x, drop.y + drop.length);
ctx.stroke();
});
// 水花粒子
for (let i = splashParticles.length - 1; i >= 0; i--) {
const sp = splashParticles[i];
sp.x += sp.vx;
sp.y += sp.vy;
sp.vy += 0.25;
sp.life -= dt;
sp.alpha = sp.life / 0.9;
if (sp.life <= 0 || sp.y > h + 5) {
splashParticles.splice(i, 1);
continue;
}
ctx.fillStyle = `rgba(190,210,230,${sp.alpha})`;
ctx.beginPath();
ctx.arc(sp.x, sp.y, sp.r, 0, Math.PI * 2);
ctx.fill();
}
// 限制水花数量
if (splashParticles.length > 60) splashParticles.splice(0, splashParticles.length - 60);
state._animId = requestAnimationFrame(animate);
}
state._lastTime = performance.now();
state._animId = requestAnimationFrame(animate);
return state;
}
// ============ 暴雪动画 ============
function createBlizzardAnimation(canvasId) {
const state = setupCanvas(canvasId, 260, 170);
if (!state) return;
const { ctx, w, h } = state;
const snowflakes = [];
const snowAccumulation = [];
const maxAccum = 70;
for (let i = 0; i < 75; i++) {
snowflakes.push({
x: Math.random() * w,
y: Math.random() * h - h,
r: Math.random() * 3.5 + 1.2,
speedY: Math.random() * 1.6 + 0.6,
speedX: Math.random() * 2.5 + 1.2,
alpha: Math.random() * 0.7 + 0.3,
wobbleAmp: Math.random() * 1.8 + 0.4,
wobbleFreq: Math.random() * 0.05 + 0.02,
phase: Math.random() * Math.PI * 2,
crystal: Math.random() < 0.35,
});
}
function animate(timestamp) {
ctx.clearRect(0, 0, w, h);
const time = timestamp * 0.001;
// 积雪
if (snowAccumulation.length < maxAccum && Math.random() < 0.25) {
snowAccumulation.push({
x: Math.random() * w,
y: h - Math.random() * 8,
r: Math.random() * 4 + 1.5,
alpha: 0.7 + Math.random() * 0.3,
});
}
if (snowAccumulation.length > maxAccum) {
snowAccumulation.splice(0, snowAccumulation.length - maxAccum);
}
// 绘制积雪
snowAccumulation.forEach(sa => {
ctx.fillStyle = `rgba(235,245,255,${sa.alpha})`;
ctx.beginPath();
ctx.arc(sa.x, sa.y, sa.r, 0, Math.PI * 2);
ctx.fill();
});
// 积雪连成片
ctx.fillStyle = 'rgba(225,238,250,0.5)';
ctx.beginPath();
const baseY = h - 2;
ctx.moveTo(0, baseY);
for (let x = 0; x <= w; x += 8) {
const yOff = Math.sin(x * 0.06 + time * 0.3) * 5 + Math.cos(x * 0.13) * 3;
ctx.lineTo(x, baseY - 6 + yOff);
}
ctx.lineTo(w, h + 5);
ctx.lineTo(0, h + 5);
ctx.closePath();
ctx.fill();
// 雪花
snowflakes.forEach(sf => {
sf.y += sf.speedY;
sf.x += sf.speedX + Math.sin(time * sf.wobbleFreq * 10 + sf.phase) * sf.wobbleAmp;
if (sf.y > h + 10) {
sf.y = -sf.r * 3 - Math.random() * 20;
sf.x = Math.random() * w;
}
if (sf.x > w + 15) sf.x = -15;
if (sf.x < -15) sf.x = w + 15;
ctx.fillStyle = `rgba(240,245,255,${sf.alpha})`;
ctx.beginPath();
if (sf.crystal) {
// 小星形雪花
const cx = sf.x;
const cy = sf.y;
const rr = sf.r;
for (let j = 0; j < 6; j++) {
const angle = (j / 6) * Math.PI * 2 + time * 0.4;
const tipX = cx + Math.cos(angle) * rr;
const tipY = cy + Math.sin(angle) * rr;
const midX = cx + Math.cos(angle + 0.35) * rr * 0.4;
const midY = cy + Math.sin(angle + 0.35) * rr * 0.4;
if (j === 0) ctx.moveTo(tipX, tipY);
else ctx.lineTo(tipX, tipY);
ctx.lineTo(midX, midY);
}
ctx.closePath();
ctx.fill();
ctx.fillStyle = `rgba(255,255,255,${sf.alpha * 0.6})`;
ctx.beginPath();
ctx.arc(sf.x, sf.y, sf.r * 0.45, 0, Math.PI * 2);
ctx.fill();
} else {
ctx.arc(sf.x, sf.y, sf.r, 0, Math.PI * 2);
ctx.fill();
// 微光
ctx.fillStyle = `rgba(255,255,255,${sf.alpha * 0.35})`;
ctx.beginPath();
ctx.arc(sf.x, sf.y, sf.r * 2.2, 0, Math.PI * 2);
ctx.fill();
}
});
state._animId = requestAnimationFrame(animate);
}
state._animId = requestAnimationFrame(animate);
return state;
}
// ============ 初始化所有动画 ============
const animationStates = {};
function initAllAnimations() {
// 清理旧动画
Object.values(animationStates).forEach(s => {
if (s && s._animId) cancelAnimationFrame(s._animId);
});
animationStates.sunny = createSunnyAnimation('canvasSunny');
animationStates.windy = createWindyAnimation('canvasWindy');
animationStates.rainstorm = createRainstormAnimation('canvasRainstorm');
animationStates.blizzard = createBlizzardAnimation('canvasBlizzard');
}
initAllAnimations();
// ============ 响应式处理 ============
let resizeTimeout;
function handleResize() {
clearTimeout(resizeTimeout);
resizeTimeout = setTimeout(() => {
Object.values(animationStates).forEach(s => {
if (s && s.canvas) resizeCanvas(s);
});
}, 250);
}
window.addEventListener('resize', handleResize);
window.addEventListener('orientationchange', () => setTimeout(handleResize, 400));
// ============ 卡片交互 ============
const cardsGrid = document.getElementById('cardsGrid');
const allCards = cardsGrid.querySelectorAll('.weather-card');
allCards.forEach(card => {
// 点击选中/取消
card.addEventListener('click', function(e) {
const wasSelected = this.classList.contains('selected');
// 取消所有选中
allCards.forEach(c => c.classList.remove('selected'));
// 如果之前未选中,则选中当前卡片
if (!wasSelected) {
this.classList.add('selected');
// 轻微振动反馈模拟(视觉)
this.style.transition = 'all 0.12s cubic-bezier(0.34,1.56,0.64,1)';
this.style.transform = 'translateY(-10px) scale(1.05)';
setTimeout(() => {
this.style.transition = '';
this.style.transform = '';
}, 150);
}
});
// 触摸设备的长按效果
let pressTimer;
card.addEventListener('touchstart', function() {
pressTimer = setTimeout(() => {
this.style.transform = 'translateY(-6px) scale(1.03)';
this.style.transition = 'all 0.3s cubic-bezier(0.34,1.56,0.64,1)';
}, 150);
}, { passive: true });
card.addEventListener('touchend', function() {
clearTimeout(pressTimer);
this.style.transition = 'all 0.5s cubic-bezier(0.34,1.56,0.64,1)';
this.style.transform = '';
});
card.addEventListener('touchcancel', function() {
clearTimeout(pressTimer);
this.style.transition = 'all 0.5s cubic-bezier(0.34,1.56,0.64,1)';
this.style.transform = '';
});
});
// 键盘导航
let focusedCardIndex = -1;
document.addEventListener('keydown', function(e) {
if (e.key === 'ArrowRight' || e.key === 'ArrowLeft') {
e.preventDefault();
allCards.forEach(c => c.classList.remove('selected'));
if (e.key === 'ArrowRight') {
focusedCardIndex = Math.min(focusedCardIndex + 1, allCards.length - 1);
} else {
focusedCardIndex = Math.max(focusedCardIndex - 1, 0);
}
if (focusedCardIndex >= 0 && focusedCardIndex < allCards.length) {
allCards[focusedCardIndex].classList.add('selected');
allCards[focusedCardIndex].scrollIntoView({ behavior: 'smooth', block: 'nearest',
inline: 'center' });
}
}
if (e.key === 'Escape') {
allCards.forEach(c => c.classList.remove('selected'));
focusedCardIndex = -1;
}
});
// ============ 性能监控 & 页面可见性 ============
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
// 暂停所有动画
Object.values(animationStates).forEach(s => {
if (s && s._animId) {
cancelAnimationFrame(s._animId);
s._animId = null;
s._paused = true;
}
});
} else {
// 恢复动画
Object.entries(animationStates).forEach(([key, s]) => {
if (s && s._paused) {
s._paused = false;
if (key === 'sunny') s._animId = requestAnimationFrame(function loop(
t) { createSunnyAnimation('canvasSunny'); });
// 简化恢复:重新初始化
}
});
if (Object.values(animationStates).some(s => s && s._paused)) {
initAllAnimations();
}
}
});
console.log('%c🌤️ iOS 26 Weather Cards %c已就绪 %c| %c晴天 · 大风 · 暴雨 · 暴雪',
'font-size:1.1em;font-weight:700;',
'color:#888;',
'',
'color:#aaa;');
console.log('%c✨ 点击卡片切换选中 · 悬停查看动效 · 方向键导航',
'color:#b0b0b8;font-style:italic;');
})();
</script>
</body>
</html>
--【肆】--:
你这一说我想起来了 gpt 给我做的挂灯也用的这个黄色圆球
--【伍】--:
赞同老哥的想法,逻辑确实好的话能解决一些问题了就
--【陆】--:
可以把视频压缩转换成avif动图,非常省空间还能上传。
--【柒】--:
压缩的太狠了,旋转效果都压缩没有了,不过我这个不是v4写的,这是上一个他出思考模式后写的
--【捌】--:
效果还不如前员工萝莉的小米v2.5做的呀:
image1333×853 127 KB
--【玖】--:
有点拉看着,感觉前端不是很行,而且佬是咋确定是v4 pro的?
--【拾】--:
佬:做一个3D效果的,6阶魔方的打乱和还原动态演示html 试试效果
--【拾壹】--:
宣传说的比claude opus 4.6还强,看起来还不如claude 4.5,用的不对吗
--【拾贰】--:
image1614×901 167 KB
GPT5.5
--【拾叁】--:
感觉依然是很普通,没什么大的进步啊这和3.2没区别
--【拾肆】--:
关键看逻辑能力,前端能力没那么重要,openai系的模型前端也不行但不影响SOTA
--【拾伍】--:
开深度思考,四个天气都有动效,有鼠标悬停动画
SnowShot2026-04-2411-20-071920×1036 150 KB
关闭深度思考,第一个太阳无动效,鼠标也是太阳没有悬停动画
SnowShot2026-04-2411-22-381920×1026 139 KB
深度思考140秒出来的结果:
image1467×557 63.6 KB
image270×109 2.41 KB
以 iOS 18 的设计风格做一个带有动画效果的天气卡片,要求是使用 HTML、CSS 和基础 JavaScript,使用横板天气页面(拥有 4 个天气卡片 (晴天,大风,暴雨,暴雪))。应足够美观,实现一定的交互效果。
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>iOS 18 风格天气卡片</title>
<style>
:root {
--bg-dark: #1c1c1e;
--card-radius: 22px;
--transition-smooth: 0.4s cubic-bezier(0.25, 0.1, 0.25, 1);
--transition-bounce: 0.5s cubic-bezier(0.34, 1.56, 0.64, 1);
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'SF Pro Text', 'Helvetica Neue', 'PingFang SC', sans-serif;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: #0d0d0f;
overflow-x: hidden;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
user-select: none;
-webkit-user-select: none;
-webkit-tap-highlight-color: transparent;
}
/* 背景层 */
.bg-layer {
position: fixed;
inset: 0;
pointer-events: none;
z-index: 0;
overflow: hidden;
}
.bg-orb {
position: absolute;
border-radius: 50%;
filter: blur(100px);
opacity: 0.55;
animation: orbFloat 12s ease-in-out infinite;
will-change: transform;
}
.bg-orb.orb-1 {
width: 420px;
height: 420px;
background: radial-gradient(circle, #ff9a56 0%, #ff6b35 40%, transparent 70%);
top: -15%;
left: 8%;
animation-delay: 0s;
animation-duration: 14s;
}
.bg-orb.orb-2 {
width: 350px;
height: 350px;
background: radial-gradient(circle, #7eb8ff 0%, #4a90d9 40%, transparent 70%);
bottom: -18%;
right: 5%;
animation-delay: -4s;
animation-duration: 16s;
}
.bg-orb.orb-3 {
width: 300px;
height: 300px;
background: radial-gradient(circle, #c4b5fd 0%, #8b7cf0 40%, transparent 70%);
top: 40%;
left: 50%;
animation-delay: -8s;
animation-duration: 13s;
}
.bg-orb.orb-4 {
width: 260px;
height: 260px;
background: radial-gradient(circle, #67e8f9 0%, #22d3ee 35%, transparent 70%);
bottom: 10%;
left: 20%;
animation-delay: -6s;
animation-duration: 15s;
}
.bg-orb.orb-5 {
width: 200px;
height: 200px;
background: radial-gradient(circle, #fde68a 0%, #fbbf24 35%, transparent 70%);
top: 15%;
right: 18%;
animation-delay: -10s;
animation-duration: 11s;
}
@keyframes orbFloat {
0%,
100% {
transform: translate(0, 0) scale(1);
}
20% {
transform: translate(30px, -25px) scale(1.08);
}
40% {
transform: translate(-20px, 15px) scale(0.94);
}
60% {
transform: translate(15px, 30px) scale(1.05);
}
80% {
transform: translate(-28px, -10px) scale(0.96);
}
}
/* 主容器 */
.main-container {
position: relative;
z-index: 1;
width: 100%;
max-width: 1400px;
padding: 30px 24px;
}
/* 顶部标题栏 */
.header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 28px;
padding: 0 8px;
flex-wrap: wrap;
gap: 14px;
}
.header-title {
font-size: 1.7rem;
font-weight: 600;
color: #ffffff;
letter-spacing: -0.3px;
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
}
.header-subtitle {
font-size: 0.9rem;
font-weight: 400;
color: rgba(255, 255, 255, 0.6);
letter-spacing: 0.1px;
}
.temp-toggle-btn {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 18px;
border-radius: 100px;
border: 1.5px solid rgba(255, 255, 255, 0.25);
background: rgba(255, 255, 255, 0.08);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
color: #ffffff;
cursor: pointer;
font-size: 0.9rem;
font-weight: 500;
letter-spacing: -0.2px;
transition: all var(--transition-smooth);
white-space: nowrap;
font-family: inherit;
}
.temp-toggle-btn:hover {
background: rgba(255, 255, 255, 0.16);
border-color: rgba(255, 255, 255, 0.45);
transform: translateY(-1px);
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.25);
}
.temp-toggle-btn:active {
transform: scale(0.96);
transition: transform 0.15s ease;
}
.toggle-icon {
font-size: 1.1rem;
transition: transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.temp-toggle-btn:active .toggle-icon {
transform: rotate(180deg);
}
/* 卡片滚动容器 */
.cards-scroll-container {
overflow-x: auto;
overflow-y: visible;
scroll-behavior: smooth;
-webkit-overflow-scrolling: touch;
padding: 10px 4px 20px;
scrollbar-width: thin;
scrollbar-color: rgba(255, 255, 255, 0.2) transparent;
}
.cards-scroll-container::-webkit-scrollbar {
height: 6px;
}
.cards-scroll-container::-webkit-scrollbar-track {
background: transparent;
border-radius: 10px;
}
.cards-scroll-container::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.2);
border-radius: 10px;
transition: background 0.3s;
}
.cards-scroll-container::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.4);
}
/* 卡片网格 */
.cards-grid {
display: flex;
gap: 20px;
padding: 4px 6px;
min-width: min-content;
}
/* 单张天气卡片 */
.weather-card {
position: relative;
flex: 0 0 auto;
width: 300px;
height: 200px;
border-radius: var(--card-radius);
cursor: pointer;
overflow: hidden;
transition: all var(--transition-bounce);
border: 1px solid rgba(255, 255, 255, 0.18);
box-shadow:
0 8px 32px rgba(0, 0, 0, 0.3),
0 2px 8px rgba(0, 0, 0, 0.2),
inset 0 1px 0 rgba(255, 255, 255, 0.06);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
background: rgba(30, 30, 35, 0.45);
transform: translateY(0);
z-index: 1;
will-change: transform, box-shadow;
}
.weather-card:hover {
transform: translateY(-10px) scale(1.025);
box-shadow:
0 20px 50px rgba(0, 0, 0, 0.45),
0 6px 16px rgba(0, 0, 0, 0.3),
inset 0 1px 0 rgba(255, 255, 255, 0.1);
z-index: 10;
border-color: rgba(255, 255, 255, 0.35);
}
.weather-card:active {
transform: translateY(-4px) scale(0.985);
transition: all 0.15s ease;
box-shadow:
0 10px 28px rgba(0, 0, 0, 0.38),
0 3px 10px rgba(0, 0, 0, 0.25);
}
/* 卡片选中状态 */
.weather-card.selected {
border-color: rgba(255, 255, 255, 0.55);
box-shadow:
0 18px 44px rgba(0, 0, 0, 0.4),
0 4px 14px rgba(0, 0, 0, 0.28),
0 0 0 3px rgba(255, 255, 255, 0.12),
inset 0 1px 0 rgba(255, 255, 255, 0.1);
transform: translateY(-6px) scale(1.018);
}
/* 卡片内部天空动画区 */
.card-sky {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 68%;
overflow: hidden;
pointer-events: none;
z-index: 1;
}
/* 各天气卡片天空背景 */
.card-sky.sky-sunny {
background: linear-gradient(180deg,
#ffb347 0%, #ff8c42 25%, #ff7030 55%, #e85d3a 100%);
}
.card-sky.sky-windy {
background: linear-gradient(180deg,
#b0c4de 0%, #8fa8c8 30%, #7a94b5 60%, #5f7d9e 100%);
}
.card-sky.sky-rain {
background: linear-gradient(180deg,
#4a5568 0%, #3d4855 30%, #2d3748 60%, #1a2332 100%);
}
.card-sky.sky-snow {
background: linear-gradient(180deg,
#c8d6e5 0%, #b0c0d4 30%, #95a8bf 55%, #7a8fa3 100%);
}
/* 信息栏 */
.card-info-bar {
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 32%;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 18px;
z-index: 3;
background: rgba(20, 20, 25, 0.5);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border-top: 1px solid rgba(255, 255, 255, 0.1);
}
.card-info-left {
display: flex;
flex-direction: column;
gap: 2px;
}
.card-city {
font-size: 0.85rem;
font-weight: 500;
color: rgba(255, 255, 255, 0.85);
letter-spacing: -0.1px;
}
.card-weather-label {
font-size: 0.7rem;
font-weight: 400;
color: rgba(255, 255, 255, 0.5);
letter-spacing: 0.2px;
}
.card-temp {
font-size: 2.6rem;
font-weight: 300;
color: #ffffff;
letter-spacing: -2px;
line-height: 1;
transition: all 0.3s ease;
}
.card-temp-unit {
font-size: 1rem;
font-weight: 400;
color: rgba(255, 255, 255, 0.7);
letter-spacing: 0;
margin-left: 1px;
vertical-align: super;
}
/* ============ 晴天太阳动画 ============ */
.sun-container {
position: absolute;
top: 50%;
left: 38%;
transform: translate(-50%, -50%);
width: 70px;
height: 70px;
z-index: 2;
}
.sun-core {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 44px;
height: 44px;
border-radius: 50%;
background: radial-gradient(circle at 40% 35%, #fffde7 0%, #ffe082 30%, #ffb300 70%, #ff8f00 100%);
box-shadow:
0 0 30px rgba(255, 180, 40, 0.8),
0 0 60px rgba(255, 160, 20, 0.5),
0 0 90px rgba(255, 140, 10, 0.3),
0 0 130px rgba(255, 120, 0, 0.2);
animation: sunPulse 3s ease-in-out infinite;
z-index: 3;
}
@keyframes sunPulse {
0%,
100% {
box-shadow: 0 0 30px rgba(255, 180, 40, 0.8), 0 0 60px rgba(255, 160, 20, 0.5), 0 0 90px rgba(255, 140, 10, 0.3), 0 0 130px rgba(255, 120, 0, 0.2);
}
50% {
box-shadow: 0 0 40px rgba(255, 200, 50, 1), 0 0 75px rgba(255, 175, 30, 0.65), 0 0 105px rgba(255, 150, 15, 0.4), 0 0 150px rgba(255, 130, 5, 0.28);
}
}
.sun-ring {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
border-radius: 50%;
border: 2px solid rgba(255, 220, 150, 0.5);
animation: ringSpin 8s linear infinite;
pointer-events: none;
}
.sun-ring.ring-1 {
width: 58px;
height: 58px;
animation-duration: 7s;
border-color: rgba(255, 210, 130, 0.45);
}
.sun-ring.ring-2 {
width: 72px;
height: 72px;
animation-duration: 10s;
animation-direction: reverse;
border-color: rgba(255, 200, 120, 0.3);
border-style: dashed;
border-width: 1.5px;
}
.sun-ring.ring-3 {
width: 86px;
height: 86px;
animation-duration: 13s;
border-color: rgba(255, 190, 110, 0.22);
border-width: 1px;
}
@keyframes ringSpin {
from {
transform: translate(-50%, -50%) rotate(0deg);
}
to {
transform: translate(-50%, -50%) rotate(360deg);
}
}
/* 阳光粒子 */
.sun-particle {
position: absolute;
top: 50%;
left: 38%;
width: 4px;
height: 4px;
border-radius: 50%;
background: #ffe9a0;
pointer-events: none;
z-index: 1;
animation: particleFloat 2.5s ease-in-out infinite;
box-shadow: 0 0 6px 2px rgba(255, 210, 100, 0.7);
}
@keyframes particleFloat {
0%,
100% {
opacity: 0;
transform: translate(0, 0) scale(0.5);
}
30% {
opacity: 1;
}
60% {
opacity: 0.7;
transform: translate(var(--px), var(--py)) scale(1.3);
}
100% {
opacity: 0;
transform: translate(var(--px-end), var(--py-end)) scale(0.3);
}
}
/* ============ 大风动画 ============ */
.wind-line {
position: absolute;
height: 2px;
border-radius: 4px;
background: rgba(255, 255, 255, 0.7);
pointer-events: none;
z-index: 2;
animation: windBlow linear infinite;
filter: blur(0.5px);
}
.wind-line.thin {
height: 1px;
background: rgba(255, 255, 255, 0.5);
filter: blur(1px);
}
.wind-line.thick {
height: 3px;
background: rgba(255, 255, 255, 0.75);
filter: blur(0px);
}
@keyframes windBlow {
0% {
transform: translateX(-120%);
opacity: 0;
}
8% {
opacity: 1;
}
85% {
opacity: 1;
}
100% {
transform: translateX(420%);
opacity: 0;
}
}
/* 风中的碎屑 */
.wind-debris {
position: absolute;
border-radius: 50%;
background: rgba(255, 255, 255, 0.6);
pointer-events: none;
z-index: 2;
animation: debrisFly linear infinite;
}
@keyframes debrisFly {
0% {
transform: translateX(-150%) translateY(0) rotate(0deg);
opacity: 0;
}
10% {
opacity: 1;
}
50% {
transform: translateX(100%) translateY(-18px) rotate(180deg);
opacity: 0.8;
}
90% {
opacity: 1;
}
100% {
transform: translateX(380%) translateY(5px) rotate(360deg);
opacity: 0;
}
}
/* ============ 暴雨动画 ============ */
.rain-drop {
position: absolute;
top: -30px;
width: 2px;
border-radius: 0 0 3px 3px;
background: linear-gradient(180deg, transparent 0%, rgba(200, 220, 240, 0.8) 30%, rgba(180, 210, 235, 0.9) 100%);
pointer-events: none;
z-index: 2;
animation: rainFall linear infinite;
box-shadow: 0 0 3px rgba(180, 210, 240, 0.4);
}
@keyframes rainFall {
0% {
transform: translateY(-40px);
opacity: 0;
}
5% {
opacity: 1;
}
85% {
opacity: 1;
}
100% {
transform: translateY(160px);
opacity: 0;
}
}
/* 雨滴溅落 */
.rain-splash {
position: absolute;
bottom: 8px;
width: 6px;
height: 6px;
border-radius: 50%;
background: rgba(200, 215, 235, 0.5);
pointer-events: none;
z-index: 2;
animation: splashFade 0.6s ease-out infinite;
animation-delay: var(--splash-delay);
}
@keyframes splashFade {
0% {
transform: scale(0.3);
opacity: 0.8;
}
100% {
transform: scale(4);
opacity: 0;
}
}
/* ============ 暴雪动画 ============ */
.snow-flake {
position: absolute;
top: -20px;
border-radius: 50%;
background: #ffffff;
pointer-events: none;
z-index: 2;
animation: snowFall linear infinite;
box-shadow: 0 0 6px 2px rgba(255, 255, 255, 0.5);
}
@keyframes snowFall {
0% {
transform: translateY(-30px) translateX(0) rotate(0deg);
opacity: 0;
}
8% {
opacity: 1;
}
50% {
transform: translateY(80px) translateX(var(--drift)) rotate(180deg);
opacity: 0.9;
}
88% {
opacity: 1;
}
100% {
transform: translateY(175px) translateX(var(--drift-end)) rotate(360deg);
opacity: 0;
}
}
/* 卡片上的微光扫过效果 */
.weather-card::after {
content: '';
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: radial-gradient(ellipse at center,
rgba(255, 255, 255, 0.06) 0%,
transparent 60%);
pointer-events: none;
z-index: 4;
opacity: 0;
transition: opacity 0.6s ease;
border-radius: inherit;
}
.weather-card:hover::after {
opacity: 1;
}
/* 响应式 */
@media (max-width: 768px) {
.cards-grid {
gap: 14px;
}
.weather-card {
width: 250px;
height: 170px;
border-radius: 18px;
}
.card-temp {
font-size: 2.2rem;
}
.card-city {
font-size: 0.75rem;
}
.card-weather-label {
font-size: 0.65rem;
}
.header-title {
font-size: 1.35rem;
}
.card-info-bar {
padding: 0 14px;
}
.sun-container {
left: 35%;
transform: translate(-50%, -50%) scale(0.8);
}
.sun-core {
width: 36px;
height: 36px;
}
}
@media (max-width: 480px) {
.weather-card {
width: 210px;
height: 148px;
border-radius: 16px;
}
.card-temp {
font-size: 1.8rem;
}
.card-city {
font-size: 0.7rem;
}
.card-weather-label {
font-size: 0.6rem;
}
.cards-grid {
gap: 10px;
}
.header {
margin-bottom: 16px;
}
.header-title {
font-size: 1.2rem;
}
.temp-toggle-btn {
padding: 8px 14px;
font-size: 0.78rem;
}
.sun-container {
left: 32%;
transform: translate(-50%, -50%) scale(0.65);
}
.card-info-bar {
padding: 0 10px;
}
}
</style>
</head>
<body>
<!-- 背景光斑 -->
<div class="bg-layer">
<div class="bg-orb orb-1"></div>
<div class="bg-orb orb-2"></div>
<div class="bg-orb orb-3"></div>
<div class="bg-orb orb-4"></div>
<div class="bg-orb orb-5"></div>
</div>
<!-- 主容器 -->
<div class="main-container">
<!-- 标题栏 -->
<div class="header">
<div>
<div class="header-title">🌤️ 天气预览</div>
<div class="header-subtitle">今日天气概览 · 四城气象</div>
</div>
<button class="temp-toggle-btn" id="tempToggle" title="切换温度单位">
<span class="toggle-icon">🌡️</span>
<span id="toggleLabel">切换 °F</span>
</button>
</div>
<!-- 卡片横向滚动区 -->
<div class="cards-scroll-container">
<div class="cards-grid" id="cardsGrid">
<!-- 卡片由JS动态生成 -->
</div>
</div>
</div>
<script>
(function() {
// ============ 数据配置 ============
const weatherData = [{
id: 'sunny',
type: 'sunny',
city: '三亚',
tempC: 29,
label: '晴朗',
skyClass: 'sky-sunny',
icon: '☀️',
}, {
id: 'windy',
type: 'windy',
city: '青岛',
tempC: 16,
label: '大风',
skyClass: 'sky-windy',
icon: '💨',
}, {
id: 'rain',
type: 'rain',
city: '广州',
tempC: 24,
label: '暴雨',
skyClass: 'sky-rain',
icon: '🌧️',
}, {
id: 'snow',
type: 'snow',
city: '哈尔滨',
tempC: -15,
label: '暴雪',
skyClass: 'sky-snow',
icon: '❄️',
}, ];
let isCelsius = true;
let selectedCardId = null;
// ============ DOM引用 ============
const cardsGrid = document.getElementById('cardsGrid');
const tempToggleBtn = document.getElementById('tempToggle');
const toggleLabel = document.getElementById('toggleLabel');
// ============ 温度转换 ============
function cToF(c) {
return Math.round(c * 9 / 5 + 32);
}
function getDisplayTemp(data) {
return isCelsius ? data.tempC : cToF(data.tempC);
}
function getTempUnit() {
return isCelsius ? '°C' : '°F';
}
// ============ 构建卡片 ============
function buildCards() {
cardsGrid.innerHTML = '';
weatherData.forEach((data, index) => {
const card = document.createElement('div');
card.className = 'weather-card';
card.dataset.type = data.type;
card.dataset.id = data.id;
card.dataset.index = index;
if (data.id === selectedCardId) {
card.classList.add('selected');
}
const displayTemp = getDisplayTemp(data);
const tempUnit = getTempUnit();
card.innerHTML = `
<div class="card-sky ${data.skyClass}" data-sky></div>
<div class="card-info-bar">
<div class="card-info-left">
<span class="card-city">${data.icon} ${data.city}</span>
<span class="card-weather-label">${data.label}</span>
</div>
<div class="card-temp">
${displayTemp}<span class="card-temp-unit">${tempUnit}</span>
</div>
</div>
`;
// 点击事件
card.addEventListener('click', () => {
handleCardClick(card, data);
});
cardsGrid.appendChild(card);
});
// 重建后初始化天气动画
initAllAnimations();
// 恢复选中状态
if (selectedCardId) {
const selectedCard = cardsGrid.querySelector(`[data-id="${selectedCardId}"]`);
if (selectedCard) {
selectedCard.classList.add('selected');
}
}
}
// ============ 卡片点击 ============
function handleCardClick(card, data) {
// 移除其他卡片的选中状态
const allCards = cardsGrid.querySelectorAll('.weather-card');
allCards.forEach(c => c.classList.remove('selected'));
if (selectedCardId === data.id) {
// 取消选中
selectedCardId = null;
} else {
// 选中当前
selectedCardId = data.id;
card.classList.add('selected');
// 添加短暂的脉冲动画
card.style.transition = 'all 0.15s ease';
card.style.transform = 'translateY(-8px) scale(1.03)';
setTimeout(() => {
card.style.transition = 'all 0.5s cubic-bezier(0.34, 1.56, 0.64, 1)';
card.style.transform = '';
if (selectedCardId === data.id) {
card.classList.add('selected');
}
}, 150);
}
// 滚动卡片到可见位置
card.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' });
}
// ============ 温度切换 ============
function toggleTemperature() {
isCelsius = !isCelsius;
toggleLabel.textContent = isCelsius ? '切换 °F' : '切换 °C';
updateAllTemperatures();
// 按钮动画
tempToggleBtn.style.transform = 'scale(0.94)';
setTimeout(() => {
tempToggleBtn.style.transform = '';
}, 180);
}
function updateAllTemperatures() {
const allCards = cardsGrid.querySelectorAll('.weather-card');
allCards.forEach(card => {
const index = parseInt(card.dataset.index);
if (index >= 0 && index < weatherData.length) {
const data = weatherData[index];
const tempEl = card.querySelector('.card-temp');
if (tempEl) {
const displayTemp = getDisplayTemp(data);
const tempUnit = getTempUnit();
tempEl.innerHTML = `${displayTemp}<span class="card-temp-unit">${tempUnit}</span>`;
// 微动画
tempEl.style.transform = 'scale(1.08)';
tempEl.style.color = 'rgba(255,255,255,1)';
setTimeout(() => {
tempEl.style.transform = 'scale(1)';
tempEl.style.color = '#ffffff';
}, 200);
}
}
});
}
tempToggleBtn.addEventListener('click', toggleTemperature);
// ============ 键盘快捷键 ============
document.addEventListener('keydown', (e) => {
if (e.key === 't' || e.key === 'T') {
toggleTemperature();
}
if (e.key === 'Escape') {
selectedCardId = null;
const allCards = cardsGrid.querySelectorAll('.weather-card');
allCards.forEach(c => c.classList.remove('selected'));
}
// 数字键1-4选择卡片
const num = parseInt(e.key);
if (num >= 1 && num <= 4) {
const card = cardsGrid.querySelector(`[data-index="${num - 1}"]`);
if (card) {
const data = weatherData[num - 1];
handleCardClick(card, data);
}
}
});
// ============ 天气动画初始化 ============
function initAllAnimations() {
initSunnyAnimations();
initWindyAnimations();
initRainAnimations();
initSnowAnimations();
}
// --- 晴天动画 ---
function initSunnyAnimations() {
const sunnyCard = cardsGrid.querySelector('[data-type="sunny"]');
if (!sunnyCard) return;
const sky = sunnyCard.querySelector('[data-sky]');
if (!sky) return;
// 移除旧粒子
sky.querySelectorAll('.sun-particle').forEach(el => el.remove());
// 太阳容器
let sunContainer = sky.querySelector('.sun-container');
if (!sunContainer) {
sunContainer = document.createElement('div');
sunContainer.className = 'sun-container';
sunContainer.innerHTML = `
<div class="sun-core"></div>
<div class="sun-ring ring-1"></div>
<div class="sun-ring ring-2"></div>
<div class="sun-ring ring-3"></div>
`;
sky.appendChild(sunContainer);
}
// 阳光粒子
for (let i = 0; i < 14; i++) {
const particle = document.createElement('div');
particle.className = 'sun-particle';
const angle = Math.random() * Math.PI * 2;
const distance = 40 + Math.random() * 55;
const px = Math.cos(angle) * distance;
const py = Math.sin(angle) * distance;
const pxEnd = Math.cos(angle) * (distance + 30);
const pyEnd = Math.sin(angle) * (distance + 30);
particle.style.setProperty('--px', px + 'px');
particle.style.setProperty('--py', py + 'px');
particle.style.setProperty('--px-end', pxEnd + 'px');
particle.style.setProperty('--py-end', pyEnd + 'px');
particle.style.animationDelay = Math.random() * 2.5 + 's';
particle.style.animationDuration = (2 + Math.random() * 3) + 's';
particle.style.width = (2.5 + Math.random() * 4) + 'px';
particle.style.height = particle.style.width;
sky.appendChild(particle);
}
}
// --- 大风动画 ---
function initWindyAnimations() {
const windyCard = cardsGrid.querySelector('[data-type="windy"]');
if (!windyCard) return;
const sky = windyCard.querySelector('[data-sky]');
if (!sky) return;
// 移除旧元素
sky.querySelectorAll('.wind-line, .wind-debris').forEach(el => el.remove());
// 风线
const windLineConfigs = [
{ top: '22%', width: '60px', duration: 2.2, delay: 0, cls: 'thick' },
{ top: '35%', width: '90px', duration: 1.6, delay: 0.6, cls: '' },
{ top: '48%', width: '50px', duration: 2.8, delay: 1.2, cls: 'thin' },
{ top: '55%', width: '75px', duration: 1.9, delay: 0.3, cls: '' },
{ top: '68%', width: '100px', duration: 2.5, delay: 1.8, cls: 'thin' },
{ top: '40%', width: '55px', duration: 3.1, delay: 2.2, cls: 'thick' },
{ top: '28%', width: '70px', duration: 1.4, delay: 1.5, cls: '' },
{ top: '60%', width: '85px', duration: 2.0, delay: 0.9, cls: 'thin' },
];
windLineConfigs.forEach(config => {
const line = document.createElement('div');
line.className = 'wind-line';
if (config.cls) line.classList.add(config.cls);
line.style.top = config.top;
line.style.width = config.width;
line.style.left = '-80px';
line.style.animationDuration = config.duration + 's';
line.style.animationDelay = config.delay + 's';
sky.appendChild(line);
});
// 碎屑
for (let i = 0; i < 10; i++) {
const debris = document.createElement('div');
debris.className = 'wind-debris';
debris.style.top = (18 + Math.random() * 55) + '%';
debris.style.width = (3 + Math.random() * 6) + 'px';
debris.style.height = debris.style.width;
debris.style.left = '-40px';
debris.style.animationDuration = (1.5 + Math.random() * 2.5) + 's';
debris.style.animationDelay = Math.random() * 3 + 's';
debris.style.opacity = (0.4 + Math.random() * 0.5);
sky.appendChild(debris);
}
}
// --- 暴雨动画 ---
function initRainAnimations() {
const rainCard = cardsGrid.querySelector('[data-type="rain"]');
if (!rainCard) return;
const sky = rainCard.querySelector('[data-sky]');
if (!sky) return;
// 移除旧雨滴
sky.querySelectorAll('.rain-drop, .rain-splash').forEach(el => el.remove());
// 雨滴
const dropCount = 35;
for (let i = 0; i < dropCount; i++) {
const drop = document.createElement('div');
drop.className = 'rain-drop';
drop.style.left = (Math.random() * 92) + '%';
drop.style.height = (8 + Math.random() * 18) + 'px';
drop.style.width = (1.2 + Math.random() * 1.6) + 'px';
drop.style.animationDuration = (0.5 + Math.random() * 0.9) + 's';
drop.style.animationDelay = Math.random() * 2 + 's';
drop.style.opacity = (0.6 + Math.random() * 0.4);
sky.appendChild(drop);
}
// 溅落点
for (let i = 0; i < 8; i++) {
const splash = document.createElement('div');
splash.className = 'rain-splash';
splash.style.left = (5 + Math.random() * 85) + '%';
splash.style.setProperty('--splash-delay', Math.random() * 1.5 + 's');
splash.style.animationDuration = (0.5 + Math.random() * 0.7) + 's';
sky.appendChild(splash);
}
}
// --- 暴雪动画 ---
function initSnowAnimations() {
const snowCard = cardsGrid.querySelector('[data-type="snow"]');
if (!snowCard) return;
const sky = snowCard.querySelector('[data-sky]');
if (!sky) return;
// 移除旧雪花
sky.querySelectorAll('.snow-flake').forEach(el => el.remove());
const flakeCount = 30;
for (let i = 0; i < flakeCount; i++) {
const flake = document.createElement('div');
flake.className = 'snow-flake';
flake.style.left = (Math.random() * 90) + '%';
const size = (3 + Math.random() * 10) + 'px';
flake.style.width = size;
flake.style.height = size;
flake.style.animationDuration = (3 + Math.random() * 5) + 's';
flake.style.animationDelay = Math.random() * 4 + 's';
flake.style.setProperty('--drift', (Math.random() * 30 - 15) + 'px');
flake.style.setProperty('--drift-end', (Math.random() * 40 - 20) + 'px');
flake.style.opacity = (0.55 + Math.random() * 0.45);
flake.style.boxShadow =
`0 0 ${4 + Math.random() * 8}px ${1 + Math.random() * 3}px rgba(255,255,255,${0.3 + Math.random() * 0.5})`;
sky.appendChild(flake);
}
}
// ============ 初始化 ============
function init() {
buildCards();
// 默认选中第一张卡片(晴天)
setTimeout(() => {
const firstCard = cardsGrid.querySelector('[data-index="0"]');
if (firstCard) {
selectedCardId = 'sunny';
firstCard.classList.add('selected');
}
}, 300);
}
// ============ 窗口大小变化时重新初始化动画 ============
let resizeDebounce;
window.addEventListener('resize', () => {
clearTimeout(resizeDebounce);
resizeDebounce = setTimeout(() => {
initAllAnimations();
}, 400);
});
// ============ 启动 ============
init();
console.log('🌤️ iOS 18 风格天气卡片已就绪');
console.log(' 🖱️ 点击卡片选中 | 再次点击取消选中');
console.log(' 🌡️ 点击右上角按钮切换 °C/°F');
console.log(' ⌨️ 键盘快捷键: 1-4 选择卡片 | T 切换温度 | Esc 取消选中');
console.log(' 📱 支持横向滚动查看所有卡片');
})();
</script>
</body>
</html>
image878×670 60.5 KB
设计一个现代、极简风格的天气应用UI界面卡片,要求所有代码存放至一个html文件内 使用浅色模式,背景为白色,带有柔和的圆角、毛玻璃效果和轻微的投影。 布局清晰,顶部是地点和日期信息。 左侧主区域显示当前天气,包含一个色彩柔和的大图标(晴转多云)和一个非常大的温度读数(28°)。 右侧用一个2x2的网格展示湿度、风速等次要数据。 中间部分用三个水平排列的彩色(淡黄、淡橙、淡紫)卡片分别展示紫外线指数、日出和日落时间。 底部是小时天气预报,以水平卡片列表的形式呈现。 整体设计语言干净,使用无衬线字体,图标风格为简约的线形或双色调图标,色彩方案柔和、饱和度低,给人清新舒适的感觉。UI, UX design, weather app, dashboard, data visualization, minimalist, clean, light mode, pastel colors, soft shadow, rounded corners, grid layout.
--【壹】--:
粗的一看感觉还可以,一细看这个太阳跟个乒乓球一样
--【贰】--:
社区怎么上传视频啊,找了半天找不到上传视频的入口
--【叁】--: Mrzqd:
以 iOS 18 的设计风格做一个带有动画效果的天气卡片,要求是使用 HTML、CSS 和基础 JavaScript,使用横板天气页面(拥有 4 个天气卡片 (晴天,大风,暴雨,暴雪))。应足够美观,实现一定的交互效果。
参考佬友的提示词并稍作修改:
以 iOS 26 的设计风格做一个带有动画效果的天气卡片,要求是使用 HTML、CSS 和基础 JavaScript,使用横板天气页面(拥有 4 个天气卡片 (晴天,大风,暴雨,暴雪))。应足够美观,实现一定的交互效果,体现月薪2万美元的程序员价值
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>iOS 26 风格 · 天气卡片</title>
<style>
:root {
--bg-deep: #0a0e17;
--bg-mid: #111827;
--text-primary: #f5f5f7;
--text-secondary: #b0b0b8;
--text-tertiary: #7b7b86;
--glass-border: rgba(255, 255, 255, 0.12);
--glass-highlight: rgba(255, 255, 255, 0.06);
--shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.3);
--shadow-md: 0 8px 32px rgba(0, 0, 0, 0.4);
--shadow-lg: 0 20px 60px rgba(0, 0, 0, 0.55);
--spring-ease: cubic-bezier(0.34, 1.56, 0.64, 1);
--smooth-ease: cubic-bezier(0.25, 0.1, 0.25, 1);
--card-width: 280px;
--card-height: 430px;
--card-radius: 28px;
--transition-card: 0.55s var(--spring-ease);
--transition-subtle: 0.35s var(--smooth-ease);
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'SF Pro Text', 'Helvetica Neue', 'PingFang SC', sans-serif;
background: var(--bg-deep);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
overflow-x: hidden;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
user-select: none;
-webkit-user-select: none;
-webkit-tap-highlight-color: transparent;
}
/* 背景光晕 */
.bg-aurora {
position: fixed;
inset: 0;
pointer-events: none;
z-index: 0;
overflow: hidden;
}
.bg-aurora .orb {
position: absolute;
border-radius: 50%;
filter: blur(120px);
opacity: 0.18;
animation: orbDrift 20s ease-in-out infinite;
}
.bg-aurora .orb:nth-child(1) {
width: 600px;
height: 600px;
background: radial-gradient(circle, #ff9a56 0%, transparent 70%);
top: -15%;
left: -10%;
animation-delay: 0s;
}
.bg-aurora .orb:nth-child(2) {
width: 500px;
height: 500px;
background: radial-gradient(circle, #5b8def 0%, transparent 70%);
bottom: -20%;
right: -8%;
animation-delay: -7s;
animation-duration: 24s;
}
.bg-aurora .orb:nth-child(3) {
width: 400px;
height: 400px;
background: radial-gradient(circle, #b8d4e3 0%, transparent 70%);
top: 40%;
left: 55%;
animation-delay: -14s;
animation-duration: 18s;
}
@keyframes orbDrift {
0%,
100% {
transform: translate(0, 0) scale(1);
}
25% {
transform: translate(40px, -30px) scale(1.08);
}
50% {
transform: translate(-20px, 25px) scale(0.94);
}
75% {
transform: translate(-35px, -15px) scale(1.05);
}
}
/* 主容器 */
.main-container {
position: relative;
z-index: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 28px;
padding: 20px;
width: 100%;
max-width: 1300px;
}
/* 顶部标题栏 */
.header {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
max-width: 1240px;
padding: 0 10px;
}
.header .title-group {
display: flex;
align-items: baseline;
gap: 12px;
}
.header .title {
font-size: 2.2rem;
font-weight: 700;
color: var(--text-primary);
letter-spacing: -0.02em;
line-height: 1;
}
.header .subtitle {
font-size: 0.95rem;
font-weight: 500;
color: var(--text-tertiary);
letter-spacing: 0.01em;
}
.header .location-badge {
display: flex;
align-items: center;
gap: 7px;
padding: 10px 18px;
background: rgba(255, 255, 255, 0.07);
border: 1px solid var(--glass-border);
border-radius: 100px;
color: var(--text-secondary);
font-size: 0.9rem;
font-weight: 500;
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
transition: all var(--transition-subtle);
cursor: pointer;
letter-spacing: 0.01em;
}
.header .location-badge:hover {
background: rgba(255, 255, 255, 0.12);
border-color: rgba(255, 255, 255, 0.25);
color: #fff;
transform: translateY(-1px);
box-shadow: var(--shadow-sm);
}
.header .location-dot {
width: 7px;
height: 7px;
border-radius: 50%;
background: #34c759;
box-shadow: 0 0 10px #34c75988;
animation: pulseDot 2s ease-in-out infinite;
}
@keyframes pulseDot {
0%,
100% {
box-shadow: 0 0 8px #34c75966;
}
50% {
box-shadow: 0 0 18px #34c759bb;
}
}
/* 卡片网格 */
.cards-grid {
display: flex;
gap: 20px;
flex-wrap: nowrap;
justify-content: center;
width: 100%;
perspective: 1200px;
}
/* 天气卡片 */
.weather-card {
position: relative;
width: var(--card-width);
min-width: var(--card-width);
height: var(--card-height);
border-radius: var(--card-radius);
background: rgba(255, 255, 255, 0.06);
border: 1px solid var(--glass-border);
backdrop-filter: blur(24px) saturate(160%);
-webkit-backdrop-filter: blur(24px) saturate(160%);
box-shadow: var(--shadow-md), inset 0 1px 0 rgba(255, 255, 255, 0.08);
cursor: pointer;
transition: all var(--transition-card);
display: flex;
flex-direction: column;
align-items: center;
overflow: hidden;
z-index: 1;
transform-origin: center center;
-webkit-tap-highlight-color: transparent;
}
.weather-card::before {
content: '';
position: absolute;
inset: 0;
border-radius: var(--card-radius);
background: linear-gradient(180deg,
rgba(255, 255, 255, 0.07) 0%,
rgba(255, 255, 255, 0.01) 30%,
rgba(255, 255, 255, 0.0) 60%,
rgba(0, 0, 0, 0.05) 100%);
pointer-events: none;
z-index: 2;
transition: opacity var(--transition-subtle);
}
.weather-card::after {
content: '';
position: absolute;
top: 0;
left: 20px;
right: 20px;
height: 1px;
background: linear-gradient(90deg,
transparent 0%,
rgba(255, 255, 255, 0.25) 20%,
rgba(255, 255, 255, 0.4) 50%,
rgba(255, 255, 255, 0.25) 80%,
transparent 100%);
pointer-events: none;
z-index: 3;
border-radius: 50%;
opacity: 0.7;
transition: opacity var(--transition-subtle);
}
/* 卡片悬停效果 */
.weather-card:hover {
transform: translateY(-12px) scale(1.04);
box-shadow: var(--shadow-lg), inset 0 1px 0 rgba(255, 255, 255, 0.14);
border-color: rgba(255, 255, 255, 0.25);
z-index: 10;
background: rgba(255, 255, 255, 0.1);
}
.weather-card:hover::after {
opacity: 1;
}
.weather-card:active {
transform: translateY(-4px) scale(0.98);
transition: all 0.15s var(--smooth-ease);
}
/* 相邻卡片在hover时略微后退 */
.cards-grid:hover .weather-card:not(:hover) {
transform: scale(0.96);
opacity: 0.75;
filter: brightness(0.85) blur(1px);
transition: all 0.5s var(--smooth-ease);
}
.weather-card:hover~.weather-card {
transform: scale(0.96);
opacity: 0.75;
filter: brightness(0.85) blur(1px);
transition: all 0.5s var(--smooth-ease);
}
/* 卡片选中状态 */
.weather-card.selected {
border-color: rgba(255, 255, 255, 0.45);
box-shadow: var(--shadow-lg), 0 0 0 3px rgba(255, 255, 255, 0.15),
inset 0 1px 0 rgba(255, 255, 255, 0.18);
background: rgba(255, 255, 255, 0.13);
z-index: 8;
transform: translateY(-6px) scale(1.025);
}
.weather-card.selected::after {
opacity: 1;
background: linear-gradient(90deg,
transparent 0%,
rgba(255, 255, 255, 0.35) 20%,
rgba(255, 255, 255, 0.55) 50%,
rgba(255, 255, 255, 0.35) 80%,
transparent 100%);
}
/* 卡片顶部彩色光晕 */
.card-glow {
position: absolute;
top: -40px;
left: 50%;
transform: translateX(-50%);
width: 140px;
height: 80px;
border-radius: 50%;
pointer-events: none;
z-index: 0;
filter: blur(35px);
opacity: 0.4;
transition: all var(--transition-card);
}
.weather-card:hover .card-glow {
opacity: 0.7;
width: 170px;
height: 100px;
top: -50px;
filter: blur(30px);
}
.card-glow.sunny {
background: #ff9f43;
}
.card-glow.windy {
background: #5b8def;
}
.card-glow.rainstorm {
background: #4a6fa5;
}
.card-glow.blizzard {
background: #a8d8ea;
}
/* 城市名称 */
.card-city {
position: relative;
z-index: 4;
margin-top: 22px;
font-size: 0.85rem;
font-weight: 600;
letter-spacing: 0.04em;
text-transform: uppercase;
color: var(--text-tertiary);
transition: color var(--transition-subtle);
}
.weather-card:hover .card-city,
.weather-card.selected .card-city {
color: var(--text-secondary);
}
/* Canvas 动画区域 */
.card-canvas-wrap {
position: relative;
z-index: 1;
width: 100%;
height: 170px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
margin: 4px 0;
}
.card-canvas-wrap canvas {
display: block;
width: 100%;
height: 100%;
pointer-events: none;
}
/* 温度显示 */
.card-temp {
position: relative;
z-index: 4;
font-size: 4.2rem;
font-weight: 200;
letter-spacing: -0.04em;
color: var(--text-primary);
line-height: 1;
transition: all var(--transition-subtle);
display: flex;
align-items: flex-start;
gap: 2px;
}
.card-temp .degree {
font-size: 1.6rem;
font-weight: 300;
margin-top: 6px;
opacity: 0.8;
}
.weather-card:hover .card-temp {
transform: scale(1.06);
text-shadow: 0 0 40px rgba(255, 255, 255, 0.3);
}
/* 天气描述 */
.card-desc {
position: relative;
z-index: 4;
font-size: 1rem;
font-weight: 500;
color: var(--text-secondary);
letter-spacing: 0.02em;
margin-top: 2px;
transition: color var(--transition-subtle);
}
.weather-card:hover .card-desc,
.weather-card.selected .card-desc {
color: #d5d5dc;
}
/* 底部详情行 */
.card-details {
position: relative;
z-index: 4;
display: flex;
gap: 20px;
margin-top: 14px;
padding-top: 14px;
border-top: 1px solid rgba(255, 255, 255, 0.08);
width: 75%;
justify-content: center;
transition: border-color var(--transition-subtle);
}
.weather-card:hover .card-details,
.weather-card.selected .card-details {
border-top-color: rgba(255, 255, 255, 0.18);
}
.card-detail-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 3px;
}
.card-detail-item .detail-value {
font-size: 0.85rem;
font-weight: 600;
color: var(--text-secondary);
letter-spacing: 0.01em;
transition: color var(--transition-subtle);
}
.card-detail-item .detail-label {
font-size: 0.68rem;
font-weight: 500;
color: var(--text-tertiary);
letter-spacing: 0.03em;
text-transform: uppercase;
}
.weather-card:hover .detail-value,
.weather-card.selected .detail-value {
color: #e0e0e6;
}
/* 响应式 */
@media (max-width: 1280px) {
.cards-grid {
gap: 14px;
}
:root {
--card-width: 240px;
--card-height: 390px;
--card-radius: 24px;
}
.card-temp {
font-size: 3.4rem;
}
.card-canvas-wrap {
height: 140px;
}
.header .title {
font-size: 1.7rem;
}
}
@media (max-width: 1050px) {
.cards-grid {
gap: 10px;
}
:root {
--card-width: 200px;
--card-height: 350px;
--card-radius: 20px;
}
.card-temp {
font-size: 2.8rem;
}
.card-temp .degree {
font-size: 1.2rem;
margin-top: 5px;
}
.card-canvas-wrap {
height: 115px;
}
.card-desc {
font-size: 0.85rem;
}
.card-details {
gap: 12px;
width: 85%;
}
.card-detail-item .detail-value {
font-size: 0.75rem;
}
.card-detail-item .detail-label {
font-size: 0.6rem;
}
.header .title {
font-size: 1.4rem;
}
.card-city {
font-size: 0.7rem;
margin-top: 16px;
}
}
@media (max-width: 860px) {
.main-container {
padding: 10px;
gap: 16px;
}
.cards-grid {
flex-wrap: nowrap;
overflow-x: auto;
justify-content: flex-start;
padding: 10px 6px 20px;
gap: 12px;
scroll-snap-type: x proximity;
-webkit-overflow-scrolling: touch;
scrollbar-width: none;
}
.cards-grid::-webkit-scrollbar {
display: none;
}
:root {
--card-width: 210px;
--card-height: 370px;
--card-radius: 22px;
}
.weather-card {
scroll-snap-align: center;
flex-shrink: 0;
}
.card-temp {
font-size: 3rem;
}
.card-canvas-wrap {
height: 130px;
}
.header {
padding: 0 6px;
}
.header .title {
font-size: 1.5rem;
}
.cards-grid:hover .weather-card:not(:hover) {
transform: scale(0.94);
opacity: 0.7;
filter: brightness(0.8) blur(0.5px);
}
}
@media (max-width: 480px) {
:root {
--card-width: 175px;
--card-height: 320px;
--card-radius: 18px;
}
.card-temp {
font-size: 2.4rem;
}
.card-temp .degree {
font-size: 1rem;
margin-top: 3px;
}
.card-canvas-wrap {
height: 100px;
}
.card-desc {
font-size: 0.75rem;
}
.card-details {
gap: 8px;
width: 88%;
margin-top: 10px;
padding-top: 10px;
}
.card-detail-item .detail-value {
font-size: 0.7rem;
}
.card-detail-item .detail-label {
font-size: 0.55rem;
}
.card-city {
font-size: 0.65rem;
margin-top: 12px;
}
.header .title {
font-size: 1.2rem;
}
.header .subtitle {
font-size: 0.7rem;
}
.header .location-badge {
font-size: 0.7rem;
padding: 7px 12px;
gap: 4px;
}
}
</style>
</head>
<body>
<!-- 背景光晕 -->
<div class="bg-aurora">
<div class="orb"></div>
<div class="orb"></div>
<div class="orb"></div>
</div>
<!-- 主容器 -->
<div class="main-container">
<!-- 顶部标题 -->
<header class="header">
<div class="title-group">
<span class="title">Weather</span>
<span class="subtitle">实时天气</span>
</div>
<div class="location-badge" title="当前定位">
<span class="location-dot"></span>
<span>自动定位</span>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="opacity:0.7;">
<polyline points="6 9 12 15 18 9"></polyline>
</svg>
</div>
</header>
<!-- 卡片网格 -->
<div class="cards-grid" id="cardsGrid">
<!-- 晴天卡片 -->
<div class="weather-card" data-weather="sunny" data-city="Los Angeles" data-temp="28" data-feels="30" data-humidity="38" data-wind="8">
<div class="card-glow sunny"></div>
<span class="card-city">Los Angeles</span>
<div class="card-canvas-wrap"><canvas id="canvasSunny" aria-hidden="true"></canvas></div>
<div class="card-temp">28<span class="degree">°C</span></div>
<div class="card-desc">晴空万里</div>
<div class="card-details">
<div class="card-detail-item"><span class="detail-value">30°</span><span class="detail-label">体感</span></div>
<div class="card-detail-item"><span class="detail-value">38%</span><span class="detail-label">湿度</span></div>
<div class="card-detail-item"><span class="detail-value">8km/h</span><span class="detail-label">风速</span></div>
</div>
</div>
<!-- 大风卡片 -->
<div class="weather-card" data-weather="windy" data-city="Chicago" data-temp="16" data-feels="12" data-humidity="52" data-wind="42">
<div class="card-glow windy"></div>
<span class="card-city">Chicago</span>
<div class="card-canvas-wrap"><canvas id="canvasWindy" aria-hidden="true"></canvas></div>
<div class="card-temp">16<span class="degree">°C</span></div>
<div class="card-desc">大风呼啸</div>
<div class="card-details">
<div class="card-detail-item"><span class="detail-value">12°</span><span class="detail-label">体感</span></div>
<div class="card-detail-item"><span class="detail-value">52%</span><span class="detail-label">湿度</span></div>
<div class="card-detail-item"><span class="detail-value">42km/h</span><span class="detail-label">风速</span></div>
</div>
</div>
<!-- 暴雨卡片 -->
<div class="weather-card" data-weather="rainstorm" data-city="Tokyo" data-temp="22" data-feels="23" data-humidity="89" data-wind="25">
<div class="card-glow rainstorm"></div>
<span class="card-city">Tokyo</span>
<div class="card-canvas-wrap"><canvas id="canvasRainstorm" aria-hidden="true"></canvas></div>
<div class="card-temp">22<span class="degree">°C</span></div>
<div class="card-desc">暴雨倾盆</div>
<div class="card-details">
<div class="card-detail-item"><span class="detail-value">23°</span><span class="detail-label">体感</span></div>
<div class="card-detail-item"><span class="detail-value">89%</span><span class="detail-label">湿度</span></div>
<div class="card-detail-item"><span class="detail-value">25km/h</span><span class="detail-label">风速</span></div>
</div>
</div>
<!-- 暴雪卡片 -->
<div class="weather-card" data-weather="blizzard" data-city="Reykjavik" data-temp="-8" data-feels="-16" data-humidity="78" data-wind="35">
<div class="card-glow blizzard"></div>
<span class="card-city">Reykjavík</span>
<div class="card-canvas-wrap"><canvas id="canvasBlizzard" aria-hidden="true"></canvas></div>
<div class="card-temp">-8<span class="degree">°C</span></div>
<div class="card-desc">暴雪纷飞</div>
<div class="card-details">
<div class="card-detail-item"><span class="detail-value">-16°</span><span class="detail-label">体感</span></div>
<div class="card-detail-item"><span class="detail-value">78%</span><span class="detail-label">湿度</span></div>
<div class="card-detail-item"><span class="detail-value">35km/h</span><span class="detail-label">风速</span></div>
</div>
</div>
</div>
</div>
<script>
(function() {
// ============ 工具函数 ============
const dpr = Math.min(window.devicePixelRatio || 1, 2);
function setupCanvas(canvasId, width, height) {
const canvas = document.getElementById(canvasId);
if (!canvas) return null;
const rect = canvas.parentElement.getBoundingClientRect();
const w = rect.width || width;
const h = rect.height || height;
canvas.width = w * dpr;
canvas.height = h * dpr;
canvas.style.width = w + 'px';
canvas.style.height = h + 'px';
const ctx = canvas.getContext('2d');
ctx.scale(dpr, dpr);
return { canvas, ctx, w, h };
}
function resizeCanvas(state) {
if (!state || !state.canvas) return;
const rect = state.canvas.parentElement.getBoundingClientRect();
const w = rect.width || state.w;
const h = rect.height || state.h;
state.w = w;
state.h = h;
state.canvas.width = w * dpr;
state.canvas.height = h * dpr;
state.canvas.style.width = w + 'px';
state.canvas.style.height = h + 'px';
state.ctx.setTransform(1, 0, 0, 1, 0, 0);
state.ctx.scale(dpr, dpr);
}
// ============ 晴天动画 ============
function createSunnyAnimation(canvasId) {
const state = setupCanvas(canvasId, 260, 170);
if (!state) return;
const { ctx, w, h } = state;
const particles = [];
const sunX = w / 2;
const sunY = h * 0.45;
const sunRadius = Math.min(w, h) * 0.22;
// 光粒子
for (let i = 0; i < 45; i++) {
particles.push({
x: Math.random() * w,
y: Math.random() * h,
r: Math.random() * 2.2 + 0.8,
speedX: (Math.random() - 0.5) * 0.5,
speedY: (Math.random() - 0.5) * 0.4 - 0.3,
alpha: Math.random() * 0.6 + 0.2,
phase: Math.random() * Math.PI * 2,
twinkleSpeed: Math.random() * 0.03 + 0.01,
});
}
const rayCount = 10;
let rotationAngle = 0;
function drawSunRays(cx, cy, radius, angle) {
ctx.save();
ctx.translate(cx, cy);
ctx.rotate(angle);
for (let i = 0; i < rayCount; i++) {
const rayAngle = (i / rayCount) * Math.PI * 2;
const innerR = radius * 1.05;
const outerR = radius * 1.55 + Math.sin(Date.now() * 0.003 + i) * radius * 0.2;
const x1 = Math.cos(rayAngle) * innerR;
const y1 = Math.sin(rayAngle) * innerR;
const x2 = Math.cos(rayAngle) * outerR;
const y2 = Math.sin(rayAngle) * outerR;
const grad = ctx.createLinearGradient(x1, y1, x2, y2);
grad.addColorStop(0, 'rgba(255,200,120,0.7)');
grad.addColorStop(0.5, 'rgba(255,180,90,0.35)');
grad.addColorStop(1, 'rgba(255,150,60,0)');
ctx.strokeStyle = grad;
ctx.lineWidth = 3 + Math.sin(Date.now() * 0.004 + i) * 1.5;
ctx.lineCap = 'round';
ctx.beginPath();
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.stroke();
}
ctx.restore();
}
function drawGlow(cx, cy, radius) {
const glowGrad = ctx.createRadialGradient(cx, cy, radius * 0.3, cx, cy, radius * 2.4);
glowGrad.addColorStop(0, 'rgba(255,240,210,0.9)');
glowGrad.addColorStop(0.25, 'rgba(255,200,140,0.55)');
glowGrad.addColorStop(0.5, 'rgba(255,160,80,0.2)');
glowGrad.addColorStop(0.75, 'rgba(255,120,40,0.05)');
glowGrad.addColorStop(1, 'rgba(255,100,30,0)');
ctx.fillStyle = glowGrad;
ctx.beginPath();
ctx.arc(cx, cy, radius * 2.4, 0, Math.PI * 2);
ctx.fill();
}
function animate() {
ctx.clearRect(0, 0, w, h);
rotationAngle += 0.008;
// 光晕
drawGlow(sunX, sunY, sunRadius);
// 太阳本体
const sunGrad = ctx.createRadialGradient(sunX, sunY, sunRadius * 0.1, sunX, sunY, sunRadius);
sunGrad.addColorStop(0, '#fffef5');
sunGrad.addColorStop(0.3, '#ffecc8');
sunGrad.addColorStop(0.6, '#ffc878');
sunGrad.addColorStop(0.85, '#ffa040');
sunGrad.addColorStop(1, '#ff8830');
ctx.fillStyle = sunGrad;
ctx.beginPath();
ctx.arc(sunX, sunY, sunRadius, 0, Math.PI * 2);
ctx.fill();
// 光线
drawSunRays(sunX, sunY, sunRadius, rotationAngle);
// 内核高光
const coreGrad = ctx.createRadialGradient(sunX - sunRadius * 0.2, sunY - sunRadius * 0.2,
sunRadius * 0.05,
sunX, sunY, sunRadius * 0.7);
coreGrad.addColorStop(0, 'rgba(255,255,255,0.85)');
coreGrad.addColorStop(1, 'rgba(255,255,255,0)');
ctx.fillStyle = coreGrad;
ctx.beginPath();
ctx.arc(sunX, sunY, sunRadius * 0.7, 0, Math.PI * 2);
ctx.fill();
// 光粒子
particles.forEach(p => {
p.x += p.speedX;
p.y += p.speedY;
p.alpha += Math.sin(Date.now() * p.twinkleSpeed + p.phase) * 0.015;
p.alpha = Math.max(0.08, Math.min(0.85, p.alpha));
if (p.y < -10) { p.y = h + 10;
p.x = Math.random() * w; }
if (p.y > h + 10) { p.y = -10;
p.x = Math.random() * w; }
if (p.x < -10) p.x = w + 10;
if (p.x > w + 10) p.x = -10;
ctx.fillStyle = `rgba(255,230,180,${p.alpha})`;
ctx.beginPath();
ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2);
ctx.fill();
// 微光晕
ctx.fillStyle = `rgba(255,220,160,${p.alpha * 0.35})`;
ctx.beginPath();
ctx.arc(p.x, p.y, p.r * 3, 0, Math.PI * 2);
ctx.fill();
});
state._animId = requestAnimationFrame(animate);
}
state._animId = requestAnimationFrame(animate);
return state;
}
// ============ 大风动画 ============
function createWindyAnimation(canvasId) {
const state = setupCanvas(canvasId, 260, 170);
if (!state) return;
const { ctx, w, h } = state;
const windLines = [];
const debris = [];
for (let i = 0; i < 22; i++) {
windLines.push({
x: Math.random() * w,
y: Math.random() * h,
length: Math.random() * 90 + 40,
speed: Math.random() * 4 + 2.5,
alpha: Math.random() * 0.4 + 0.15,
thickness: Math.random() * 1.8 + 0.6,
waveAmp: Math.random() * 12 + 4,
waveFreq: Math.random() * 0.04 + 0.02,
phase: Math.random() * Math.PI * 2,
});
}
for (let i = 0; i < 14; i++) {
debris.push({
x: Math.random() * w,
y: Math.random() * h,
r: Math.random() * 2.5 + 1,
speed: Math.random() * 3.5 + 1.5,
alpha: Math.random() * 0.6 + 0.25,
wobble: Math.random() * Math.PI * 2,
wobbleSpeed: Math.random() * 0.06 + 0.03,
});
}
function animate() {
ctx.clearRect(0, 0, w, h);
const time = Date.now() * 0.001;
// 风线
windLines.forEach(line => {
line.x += line.speed;
if (line.x - line.length > w + 20) {
line.x = -line.length - 10;
line.y = Math.random() * h;
}
const waveY = Math.sin(time * line.waveFreq * 15 + line.phase) * line.waveAmp;
const startX = line.x;
const startY = line.y + waveY;
const endX = line.x - line.length;
const endY = line.y + waveY * 0.6;
const grad = ctx.createLinearGradient(startX, startY, endX, endY);
grad.addColorStop(0, `rgba(200,215,235,${line.alpha})`);
grad.addColorStop(1, `rgba(180,200,225,0)`);
ctx.strokeStyle = grad;
ctx.lineWidth = line.thickness;
ctx.lineCap = 'round';
ctx.beginPath();
ctx.moveTo(startX, startY);
ctx.quadraticCurveTo(
(startX + endX) / 2,
(startY + endY) / 2 + waveY * 0.5,
endX,
endY
);
ctx.stroke();
});
// 碎片
debris.forEach(d => {
d.x += d.speed;
d.wobble += d.wobbleSpeed;
d.y += Math.sin(d.wobble) * 0.7;
if (d.x > w + 20) {
d.x = -15;
d.y = Math.random() * h;
}
ctx.fillStyle = `rgba(210,220,235,${d.alpha})`;
ctx.beginPath();
ctx.arc(d.x, d.y, d.r, 0, Math.PI * 2);
ctx.fill();
// 拖尾
ctx.fillStyle = `rgba(200,210,225,${d.alpha * 0.3})`;
ctx.beginPath();
ctx.arc(d.x - d.r * 1.5, d.y, d.r * 1.4, 0, Math.PI * 2);
ctx.fill();
});
state._animId = requestAnimationFrame(animate);
}
state._animId = requestAnimationFrame(animate);
return state;
}
// ============ 暴雨动画 ============
function createRainstormAnimation(canvasId) {
const state = setupCanvas(canvasId, 260, 170);
if (!state) return;
const { ctx, w, h } = state;
const raindrops = [];
const splashParticles = [];
let lightningTimer = 0;
let lightningAlpha = 0;
let lightningFlash = 0;
for (let i = 0; i < 130; i++) {
raindrops.push({
x: Math.random() * w,
y: Math.random() * h - h,
length: Math.random() * 16 + 8,
speed: Math.random() * 8 + 10,
thickness: Math.random() * 1.3 + 0.4,
alpha: Math.random() * 0.5 + 0.35,
});
}
function triggerLightning() {
lightningAlpha = 0.7 + Math.random() * 0.3;
lightningFlash = 1;
lightningTimer = 0.06 + Math.random() * 0.12;
}
function animate(timestamp) {
ctx.clearRect(0, 0, w, h);
const dt = Math.min(0.05, (timestamp - (state._lastTime || timestamp)) / 1000);
state._lastTime = timestamp;
// 闪电计时器
if (lightningFlash > 0) {
lightningFlash -= dt;
lightningAlpha *= 0.8;
if (lightningFlash <= 0) lightningAlpha = 0;
}
lightningTimer -= dt;
if (lightningTimer <= 0 && Math.random() < 0.015) {
triggerLightning();
}
// 闪电照亮背景
if (lightningAlpha > 0.01) {
const flashGrad = ctx.createRadialGradient(w * 0.55, h * 0.2, w * 0.1, w * 0.5, h * 0.5, w *
0.9);
flashGrad.addColorStop(0, `rgba(255,255,240,${lightningAlpha * 0.5})`);
flashGrad.addColorStop(1, 'rgba(255,255,240,0)');
ctx.fillStyle = flashGrad;
ctx.fillRect(0, 0, w, h);
// 闪电线条
ctx.strokeStyle = `rgba(255,255,240,${lightningAlpha})`;
ctx.lineWidth = 2.5;
ctx.lineCap = 'round';
ctx.shadowColor = `rgba(255,255,240,${lightningAlpha})`;
ctx.shadowBlur = 18;
ctx.beginPath();
let lx = w * 0.55;
let ly = -5;
ctx.moveTo(lx, ly);
for (let i = 0; i < 6; i++) {
lx += (Math.random() - 0.45) * 50;
ly += h / 5.5 + Math.random() * 10;
ctx.lineTo(lx, ly);
}
ctx.stroke();
ctx.shadowBlur = 0;
}
// 雨滴
raindrops.forEach(drop => {
drop.y += drop.speed;
drop.x += (Math.random() - 0.5) * 0.6;
if (drop.y > h + 10) {
drop.y = -drop.length - Math.random() * 30;
drop.x = Math.random() * w;
// 溅起水花
if (Math.random() < 0.3 && drop.y > h - 5) {
for (let s = 0; s < 3; s++) {
splashParticles.push({
x: drop.x,
y: h - Math.random() * 6,
vx: (Math.random() - 0.5) * 3,
vy: -(Math.random() * 3 + 1),
life: 0.5 + Math.random() * 0.4,
r: Math.random() * 1.5 + 0.5,
alpha: 0.7,
});
}
}
}
const grad = ctx.createLinearGradient(drop.x, drop.y, drop.x, drop.y + drop.length);
grad.addColorStop(0, `rgba(180,200,225,${drop.alpha})`);
grad.addColorStop(1, `rgba(140,170,210,${drop.alpha * 0.3})`);
ctx.strokeStyle = grad;
ctx.lineWidth = drop.thickness;
ctx.lineCap = 'round';
ctx.beginPath();
ctx.moveTo(drop.x, drop.y);
ctx.lineTo(drop.x, drop.y + drop.length);
ctx.stroke();
});
// 水花粒子
for (let i = splashParticles.length - 1; i >= 0; i--) {
const sp = splashParticles[i];
sp.x += sp.vx;
sp.y += sp.vy;
sp.vy += 0.25;
sp.life -= dt;
sp.alpha = sp.life / 0.9;
if (sp.life <= 0 || sp.y > h + 5) {
splashParticles.splice(i, 1);
continue;
}
ctx.fillStyle = `rgba(190,210,230,${sp.alpha})`;
ctx.beginPath();
ctx.arc(sp.x, sp.y, sp.r, 0, Math.PI * 2);
ctx.fill();
}
// 限制水花数量
if (splashParticles.length > 60) splashParticles.splice(0, splashParticles.length - 60);
state._animId = requestAnimationFrame(animate);
}
state._lastTime = performance.now();
state._animId = requestAnimationFrame(animate);
return state;
}
// ============ 暴雪动画 ============
function createBlizzardAnimation(canvasId) {
const state = setupCanvas(canvasId, 260, 170);
if (!state) return;
const { ctx, w, h } = state;
const snowflakes = [];
const snowAccumulation = [];
const maxAccum = 70;
for (let i = 0; i < 75; i++) {
snowflakes.push({
x: Math.random() * w,
y: Math.random() * h - h,
r: Math.random() * 3.5 + 1.2,
speedY: Math.random() * 1.6 + 0.6,
speedX: Math.random() * 2.5 + 1.2,
alpha: Math.random() * 0.7 + 0.3,
wobbleAmp: Math.random() * 1.8 + 0.4,
wobbleFreq: Math.random() * 0.05 + 0.02,
phase: Math.random() * Math.PI * 2,
crystal: Math.random() < 0.35,
});
}
function animate(timestamp) {
ctx.clearRect(0, 0, w, h);
const time = timestamp * 0.001;
// 积雪
if (snowAccumulation.length < maxAccum && Math.random() < 0.25) {
snowAccumulation.push({
x: Math.random() * w,
y: h - Math.random() * 8,
r: Math.random() * 4 + 1.5,
alpha: 0.7 + Math.random() * 0.3,
});
}
if (snowAccumulation.length > maxAccum) {
snowAccumulation.splice(0, snowAccumulation.length - maxAccum);
}
// 绘制积雪
snowAccumulation.forEach(sa => {
ctx.fillStyle = `rgba(235,245,255,${sa.alpha})`;
ctx.beginPath();
ctx.arc(sa.x, sa.y, sa.r, 0, Math.PI * 2);
ctx.fill();
});
// 积雪连成片
ctx.fillStyle = 'rgba(225,238,250,0.5)';
ctx.beginPath();
const baseY = h - 2;
ctx.moveTo(0, baseY);
for (let x = 0; x <= w; x += 8) {
const yOff = Math.sin(x * 0.06 + time * 0.3) * 5 + Math.cos(x * 0.13) * 3;
ctx.lineTo(x, baseY - 6 + yOff);
}
ctx.lineTo(w, h + 5);
ctx.lineTo(0, h + 5);
ctx.closePath();
ctx.fill();
// 雪花
snowflakes.forEach(sf => {
sf.y += sf.speedY;
sf.x += sf.speedX + Math.sin(time * sf.wobbleFreq * 10 + sf.phase) * sf.wobbleAmp;
if (sf.y > h + 10) {
sf.y = -sf.r * 3 - Math.random() * 20;
sf.x = Math.random() * w;
}
if (sf.x > w + 15) sf.x = -15;
if (sf.x < -15) sf.x = w + 15;
ctx.fillStyle = `rgba(240,245,255,${sf.alpha})`;
ctx.beginPath();
if (sf.crystal) {
// 小星形雪花
const cx = sf.x;
const cy = sf.y;
const rr = sf.r;
for (let j = 0; j < 6; j++) {
const angle = (j / 6) * Math.PI * 2 + time * 0.4;
const tipX = cx + Math.cos(angle) * rr;
const tipY = cy + Math.sin(angle) * rr;
const midX = cx + Math.cos(angle + 0.35) * rr * 0.4;
const midY = cy + Math.sin(angle + 0.35) * rr * 0.4;
if (j === 0) ctx.moveTo(tipX, tipY);
else ctx.lineTo(tipX, tipY);
ctx.lineTo(midX, midY);
}
ctx.closePath();
ctx.fill();
ctx.fillStyle = `rgba(255,255,255,${sf.alpha * 0.6})`;
ctx.beginPath();
ctx.arc(sf.x, sf.y, sf.r * 0.45, 0, Math.PI * 2);
ctx.fill();
} else {
ctx.arc(sf.x, sf.y, sf.r, 0, Math.PI * 2);
ctx.fill();
// 微光
ctx.fillStyle = `rgba(255,255,255,${sf.alpha * 0.35})`;
ctx.beginPath();
ctx.arc(sf.x, sf.y, sf.r * 2.2, 0, Math.PI * 2);
ctx.fill();
}
});
state._animId = requestAnimationFrame(animate);
}
state._animId = requestAnimationFrame(animate);
return state;
}
// ============ 初始化所有动画 ============
const animationStates = {};
function initAllAnimations() {
// 清理旧动画
Object.values(animationStates).forEach(s => {
if (s && s._animId) cancelAnimationFrame(s._animId);
});
animationStates.sunny = createSunnyAnimation('canvasSunny');
animationStates.windy = createWindyAnimation('canvasWindy');
animationStates.rainstorm = createRainstormAnimation('canvasRainstorm');
animationStates.blizzard = createBlizzardAnimation('canvasBlizzard');
}
initAllAnimations();
// ============ 响应式处理 ============
let resizeTimeout;
function handleResize() {
clearTimeout(resizeTimeout);
resizeTimeout = setTimeout(() => {
Object.values(animationStates).forEach(s => {
if (s && s.canvas) resizeCanvas(s);
});
}, 250);
}
window.addEventListener('resize', handleResize);
window.addEventListener('orientationchange', () => setTimeout(handleResize, 400));
// ============ 卡片交互 ============
const cardsGrid = document.getElementById('cardsGrid');
const allCards = cardsGrid.querySelectorAll('.weather-card');
allCards.forEach(card => {
// 点击选中/取消
card.addEventListener('click', function(e) {
const wasSelected = this.classList.contains('selected');
// 取消所有选中
allCards.forEach(c => c.classList.remove('selected'));
// 如果之前未选中,则选中当前卡片
if (!wasSelected) {
this.classList.add('selected');
// 轻微振动反馈模拟(视觉)
this.style.transition = 'all 0.12s cubic-bezier(0.34,1.56,0.64,1)';
this.style.transform = 'translateY(-10px) scale(1.05)';
setTimeout(() => {
this.style.transition = '';
this.style.transform = '';
}, 150);
}
});
// 触摸设备的长按效果
let pressTimer;
card.addEventListener('touchstart', function() {
pressTimer = setTimeout(() => {
this.style.transform = 'translateY(-6px) scale(1.03)';
this.style.transition = 'all 0.3s cubic-bezier(0.34,1.56,0.64,1)';
}, 150);
}, { passive: true });
card.addEventListener('touchend', function() {
clearTimeout(pressTimer);
this.style.transition = 'all 0.5s cubic-bezier(0.34,1.56,0.64,1)';
this.style.transform = '';
});
card.addEventListener('touchcancel', function() {
clearTimeout(pressTimer);
this.style.transition = 'all 0.5s cubic-bezier(0.34,1.56,0.64,1)';
this.style.transform = '';
});
});
// 键盘导航
let focusedCardIndex = -1;
document.addEventListener('keydown', function(e) {
if (e.key === 'ArrowRight' || e.key === 'ArrowLeft') {
e.preventDefault();
allCards.forEach(c => c.classList.remove('selected'));
if (e.key === 'ArrowRight') {
focusedCardIndex = Math.min(focusedCardIndex + 1, allCards.length - 1);
} else {
focusedCardIndex = Math.max(focusedCardIndex - 1, 0);
}
if (focusedCardIndex >= 0 && focusedCardIndex < allCards.length) {
allCards[focusedCardIndex].classList.add('selected');
allCards[focusedCardIndex].scrollIntoView({ behavior: 'smooth', block: 'nearest',
inline: 'center' });
}
}
if (e.key === 'Escape') {
allCards.forEach(c => c.classList.remove('selected'));
focusedCardIndex = -1;
}
});
// ============ 性能监控 & 页面可见性 ============
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
// 暂停所有动画
Object.values(animationStates).forEach(s => {
if (s && s._animId) {
cancelAnimationFrame(s._animId);
s._animId = null;
s._paused = true;
}
});
} else {
// 恢复动画
Object.entries(animationStates).forEach(([key, s]) => {
if (s && s._paused) {
s._paused = false;
if (key === 'sunny') s._animId = requestAnimationFrame(function loop(
t) { createSunnyAnimation('canvasSunny'); });
// 简化恢复:重新初始化
}
});
if (Object.values(animationStates).some(s => s && s._paused)) {
initAllAnimations();
}
}
});
console.log('%c🌤️ iOS 26 Weather Cards %c已就绪 %c| %c晴天 · 大风 · 暴雨 · 暴雪',
'font-size:1.1em;font-weight:700;',
'color:#888;',
'',
'color:#aaa;');
console.log('%c✨ 点击卡片切换选中 · 悬停查看动效 · 方向键导航',
'color:#b0b0b8;font-style:italic;');
})();
</script>
</body>
</html>
--【肆】--:
你这一说我想起来了 gpt 给我做的挂灯也用的这个黄色圆球
--【伍】--:
赞同老哥的想法,逻辑确实好的话能解决一些问题了就
--【陆】--:
可以把视频压缩转换成avif动图,非常省空间还能上传。
--【柒】--:
压缩的太狠了,旋转效果都压缩没有了,不过我这个不是v4写的,这是上一个他出思考模式后写的
--【捌】--:
效果还不如前员工萝莉的小米v2.5做的呀:
image1333×853 127 KB
--【玖】--:
有点拉看着,感觉前端不是很行,而且佬是咋确定是v4 pro的?
--【拾】--:
佬:做一个3D效果的,6阶魔方的打乱和还原动态演示html 试试效果
--【拾壹】--:
宣传说的比claude opus 4.6还强,看起来还不如claude 4.5,用的不对吗
--【拾贰】--:
image1614×901 167 KB
GPT5.5
--【拾叁】--:
感觉依然是很普通,没什么大的进步啊这和3.2没区别
--【拾肆】--:
关键看逻辑能力,前端能力没那么重要,openai系的模型前端也不行但不影响SOTA
--【拾伍】--:
开深度思考,四个天气都有动效,有鼠标悬停动画
SnowShot2026-04-2411-20-071920×1036 150 KB
关闭深度思考,第一个太阳无动效,鼠标也是太阳没有悬停动画
SnowShot2026-04-2411-22-381920×1026 139 KB

