实战项目5:天气应用

项目介绍

在这个项目中,我们将创建一个功能完整的天气应用,该应用允许用户获取实时天气数据、查看天气预报、搜索不同城市的天气信息,并支持地理位置定位。这个项目将帮助我们学习JavaScript的API集成、地理位置定位、异步编程、DOM操作和响应式设计等核心概念。

项目需求

  1. 显示当前城市的实时天气信息
  2. 支持地理位置定位,自动获取用户所在城市的天气
  3. 支持通过搜索框搜索不同城市的天气
  4. 显示当前天气的详细信息:温度、湿度、风速、气压等
  5. 显示未来几天的天气预报
  6. 支持温度单位切换(摄氏度/华氏度)
  7. 根据天气状况显示相应的图标和背景
  8. 显示日出日落时间
  9. 支持刷新天气数据
  10. 处理各种边界情况(如网络错误、城市不存在等)
  11. 页面布局简洁美观,响应式设计

技术栈

  • HTML5
  • CSS3
  • JavaScript (ES6+)
  • 天气API(OpenWeatherMap API)

项目结构

weather-app-project/
├── index.html      # 主页面
├── styles.css      # 样式文件
└── script.js       # JavaScript文件

实现步骤

1. 创建HTML结构

首先,我们需要创建HTML结构,包括天气应用的各个组件:标题、搜索框、当前天气信息、详细天气数据和天气预报。

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>天气应用</title>
    <link rel="stylesheet" href="styles.css">
    <!-- 引入天气图标库 -->
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/weather-icons/2.0.12/css/weather-icons.min.css">
</head>
<body>
    <div class="weather-app">
        <!-- 头部区域 -->
        <header class="app-header">
            <h1 class="app-title">天气应用</h1>
            <div class="search-container">
                <input 
                    type="text" 
                    id="search-input" 
                    placeholder="搜索城市..."
                    autocomplete="off"
                >
                <button id="search-btn" class="search-btn">
                    <i class="wi wi-search"></i>
                </button>
            </div>
            <button id="location-btn" class="location-btn">
                <i class="wi wi-location"></i>
            </button>
        </header>
        
        <!-- 主要内容区域 -->
        <main class="app-main">
            <!-- 加载状态 -->
            <div class="loading" id="loading">
                <div class="loading-spinner"></div>
                <p>加载天气数据中...</p>
            </div>
            
            <!-- 错误状态 -->
            <div class="error" id="error" style="display: none;">
                <i class="wi wi-alerts-danger"></i>
                <p id="error-message">无法获取天气数据</p>
                <button id="retry-btn" class="retry-btn">重试</button>
            </div>
            
            <!-- 天气信息区域 -->
            <div class="weather-info" id="weather-info" style="display: none;">
                <!-- 当前天气 -->
                <section class="current-weather">
                    <div class="location">
                        <h2 id="city-name">北京</h2>
                        <p id="date-time">2023年10月1日 星期日 14:30</p>
                    </div>
                    
                    <div class="weather-main">
                        <div class="temperature">
                            <span id="current-temp">22</span>
                            <span class="temp-unit">
                                <span id="celsius" class="active">°C</span>
                                <span class="divider">/</span>
                                <span id="fahrenheit">°F</span>
                            </span>
                        </div>
                        
                        <div class="weather-details">
                            <div class="weather-icon">
                                <i id="weather-icon" class="wi wi-day-sunny"></i>
                            </div>
                            <div class="weather-desc">
                                <p id="weather-description">晴天</p>
                            </div>
                        </div>
                    </div>
                    
                    <div class="weather-stats">
                        <div class="stat-item">
                            <i class="wi wi-humidity"></i>
                            <span id="humidity">50%</span>
                            <span class="stat-label">湿度</span>
                        </div>
                        <div class="stat-item">
                            <i class="wi wi-strong-wind"></i>
                            <span id="wind-speed">10 km/h</span>
                            <span class="stat-label">风速</span>
                        </div>
                        <div class="stat-item">
                            <i class="wi wi-barometer"></i>
                            <span id="pressure">1013 hPa</span>
                            <span class="stat-label">气压</span>
                        </div>
                        <div class="stat-item">
                            <i class="wi wi-cloud"></i>
                            <span id="cloudiness">0%</span>
                            <span class="stat-label">云量</span>
                        </div>
                    </div>
                </section>
                
                <!-- 日出日落信息 -->
                <section class="sun-info">
                    <div class="sun-item">
                        <i class="wi wi-sunrise"></i>
                        <div class="sun-time">
                            <span id="sunrise">06:30</span>
                            <span class="sun-label">日出</span>
                        </div>
                    </div>
                    <div class="sun-divider"></div>
                    <div class="sun-item">
                        <i class="wi wi-sunset"></i>
                        <div class="sun-time">
                            <span id="sunset">18:30</span>
                            <span class="sun-label">日落</span>
                        </div>
                    </div>
                </section>
                
                <!-- 天气预报 -->
                <section class="forecast">
                    <h3 class="forecast-title">未来天气预报</h3>
                    <div class="forecast-container" id="forecast-container"></div>
                </section>
            </div>
        </main>
        
        <!-- 页脚区域 -->
        <footer class="app-footer">
            <p>数据来源: <a href="https://openweathermap.org/" target="_blank">OpenWeatherMap API</a></p>
        </footer>
    </div>
    
    <script src="script.js"></script>
</body>
</html>

2. 添加CSS样式

接下来,我们需要添加CSS样式,使天气应用看起来更加美观和易用。

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

body {
    font-family: 'Arial', sans-serif;
    background-color: #f0f0f0;
    color: #333;
    line-height: 1.6;
}

/* 天气应用容器 */
.weather-app {
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    min-height: 100vh;
    color: white;
    display: flex;
    flex-direction: column;
}

/* 头部区域样式 */
.app-header {
    padding: 20px;
    background-color: rgba(0, 0, 0, 0.2);
    backdrop-filter: blur(10px);
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 15px;
}

.app-title {
    font-size: 28px;
    font-weight: bold;
    text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
}

/* 搜索容器样式 */
.search-container {
    display: flex;
    width: 100%;
    max-width: 500px;
    gap: 10px;
}

#search-input {
    flex: 1;
    padding: 12px 20px;
    border: none;
    border-radius: 25px;
    font-size: 16px;
    background-color: rgba(255, 255, 255, 0.9);
    color: #333;
    box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
    transition: all 0.3s ease;
}

#search-input:focus {
    outline: none;
    background-color: white;
    transform: translateY(-2px);
    box-shadow: 0 6px 20px rgba(0, 0, 0, 0.15);
}

.search-btn, .location-btn {
    background-color: rgba(255, 255, 255, 0.9);
    color: #333;
    border: none;
    border-radius: 50%;
    width: 50px;
    height: 50px;
    font-size: 18px;
    cursor: pointer;
    transition: all 0.3s ease;
    display: flex;
    justify-content: center;
    align-items: center;
    box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
}

.search-btn:hover, .location-btn:hover {
    background-color: white;
    transform: translateY(-2px);
    box-shadow: 0 6px 20px rgba(0, 0, 0, 0.15);
}

.search-btn:active, .location-btn:active {
    transform: translateY(0);
    box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}

/* 主要内容区域样式 */
.app-main {
    flex: 1;
    padding: 20px;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
}

/* 加载状态样式 */
.loading {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 20px;
}

.loading-spinner {
    width: 60px;
    height: 60px;
    border: 4px solid rgba(255, 255, 255, 0.3);
    border-top: 4px solid white;
    border-radius: 50%;
    animation: spin 1s linear infinite;
}

@keyframes spin {
    0% { transform: rotate(0deg); }
    100% { transform: rotate(360deg); }
}

.loading p {
    font-size: 18px;
    color: rgba(255, 255, 255, 0.9);
}

/* 错误状态样式 */
.error {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 20px;
    text-align: center;
    max-width: 500px;
}

.error i {
    font-size: 64px;
    color: #ff6b6b;
}

.error p {
    font-size: 18px;
    color: rgba(255, 255, 255, 0.9);
}

.retry-btn {
    background-color: rgba(255, 255, 255, 0.9);
    color: #333;
    border: none;
    border-radius: 25px;
    padding: 12px 24px;
    font-size: 16px;
    font-weight: bold;
    cursor: pointer;
    transition: all 0.3s ease;
    box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
}

.retry-btn:hover {
    background-color: white;
    transform: translateY(-2px);
    box-shadow: 0 6px 20px rgba(0, 0, 0, 0.15);
}

/* 天气信息区域样式 */
.weather-info {
    width: 100%;
    max-width: 1200px;
}

/* 当前天气样式 */
.current-weather {
    background-color: rgba(255, 255, 255, 0.1);
    backdrop-filter: blur(10px);
    border-radius: 20px;
    padding: 30px;
    margin-bottom: 20px;
    box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
}

.location {
    text-align: center;
    margin-bottom: 20px;
}

#city-name {
    font-size: 32px;
    font-weight: bold;
    margin-bottom: 5px;
    text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
}

#date-time {
    font-size: 16px;
    color: rgba(255, 255, 255, 0.8);
}

.weather-main {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 30px;
    flex-wrap: wrap;
    gap: 20px;
}

.temperature {
    font-size: 72px;
    font-weight: bold;
    display: flex;
    align-items: baseline;
    gap: 10px;
    text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
}

#current-temp {
    font-size: 72px;
}

.temp-unit {
    font-size: 32px;
    color: rgba(255, 255, 255, 0.8);
    cursor: pointer;
    user-select: none;
}

.temp-unit span {
    transition: all 0.3s ease;
}

.temp-unit span.active {
    color: white;
    font-weight: bold;
}

.temp-unit span:hover {
    color: white;
}

.weather-details {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 10px;
}

.weather-icon {
    font-size: 64px;
    filter: drop-shadow(2px 2px 4px rgba(0, 0, 0, 0.3));
}

#weather-description {
    font-size: 24px;
    text-transform: capitalize;
    color: rgba(255, 255, 255, 0.9);
}

/* 天气统计信息样式 */
.weather-stats {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
    gap: 20px;
    margin-top: 30px;
}

.stat-item {
    background-color: rgba(255, 255, 255, 0.1);
    border-radius: 10px;
    padding: 20px;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 10px;
    transition: all 0.3s ease;
}

.stat-item:hover {
    background-color: rgba(255, 255, 255, 0.2);
    transform: translateY(-5px);
}

.stat-item i {
    font-size: 32px;
    color: rgba(255, 255, 255, 0.9);
}

.stat-item span:first-of-type {
    font-size: 24px;
    font-weight: bold;
}

.stat-label {
    font-size: 14px;
    color: rgba(255, 255, 255, 0.7);
    text-transform: uppercase;
    letter-spacing: 1px;
}

/* 日出日落信息样式 */
.sun-info {
    background-color: rgba(255, 255, 255, 0.1);
    backdrop-filter: blur(10px);
    border-radius: 20px;
    padding: 20px;
    margin-bottom: 20px;
    display: flex;
    justify-content: space-around;
    align-items: center;
    box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
}

.sun-item {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 10px;
}

.sun-item i {
    font-size: 48px;
    color: rgba(255, 255, 255, 0.9);
    filter: drop-shadow(2px 2px 4px rgba(0, 0, 0, 0.3));
}

.sun-time {
    display: flex;
    flex-direction: column;
    align-items: center;
}

.sun-time span:first-of-type {
    font-size: 24px;
    font-weight: bold;
}

.sun-label {
    font-size: 14px;
    color: rgba(255, 255, 255, 0.7);
    text-transform: uppercase;
    letter-spacing: 1px;
}

.sun-divider {
    width: 2px;
    height: 80px;
    background-color: rgba(255, 255, 255, 0.3);
}

/* 天气预报样式 */
.forecast {
    background-color: rgba(255, 255, 255, 0.1);
    backdrop-filter: blur(10px);
    border-radius: 20px;
    padding: 30px;
    box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
}

.forecast-title {
    font-size: 24px;
    font-weight: bold;
    margin-bottom: 20px;
    text-align: center;
    text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
}

.forecast-container {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
    gap: 15px;
}

.forecast-item {
    background-color: rgba(255, 255, 255, 0.1);
    border-radius: 10px;
    padding: 20px;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 10px;
    transition: all 0.3s ease;
}

.forecast-item:hover {
    background-color: rgba(255, 255, 255, 0.2);
    transform: translateY(-5px);
}

.forecast-date {
    font-size: 16px;
    font-weight: bold;
    color: rgba(255, 255, 255, 0.9);
}

.forecast-icon {
    font-size: 32px;
    filter: drop-shadow(2px 2px 4px rgba(0, 0, 0, 0.3));
}

.forecast-temp {
    font-size: 18px;
    font-weight: bold;
}

.forecast-desc {
    font-size: 14px;
    text-transform: capitalize;
    color: rgba(255, 255, 255, 0.8);
    text-align: center;
}

/* 页脚样式 */
.app-footer {
    padding: 20px;
    background-color: rgba(0, 0, 0, 0.2);
    text-align: center;
    font-size: 14px;
    color: rgba(255, 255, 255, 0.7);
}

.app-footer a {
    color: white;
    text-decoration: none;
    font-weight: bold;
    transition: all 0.3s ease;
}

.app-footer a:hover {
    text-decoration: underline;
}

/* 响应式设计 */
@media (max-width: 768px) {
    .weather-main {
        flex-direction: column;
        text-align: center;
    }
    
    .temperature {
        font-size: 56px;
    }
    
    #current-temp {
        font-size: 56px;
    }
    
    .temp-unit {
        font-size: 24px;
    }
    
    .weather-icon {
        font-size: 48px;
    }
    
    #weather-description {
        font-size: 20px;
    }
    
    .weather-stats {
        grid-template-columns: repeat(2, 1fr);
    }
    
    .sun-info {
        flex-direction: column;
        gap: 20px;
    }
    
    .sun-divider {
        width: 80px;
        height: 2px;
    }
    
    .forecast-container {
        grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
    }
    
    .forecast-item {
        padding: 15px;
    }
}

@media (max-width: 480px) {
    .app-title {
        font-size: 24px;
    }
    
    #city-name {
        font-size: 24px;
    }
    
    .temperature {
        font-size: 48px;
    }
    
    #current-temp {
        font-size: 48px;
    }
    
    .temp-unit {
        font-size: 20px;
    }
    
    .weather-stats {
        grid-template-columns: 1fr;
    }
    
    .forecast-container {
        grid-template-columns: repeat(2, 1fr);
    }
}

/* 天气背景样式 */
.weather-app.clear {
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}

.weather-app.clouds {
    background: linear-gradient(135deg, #a8c0ff 0%, #3f2b96 100%);
}

.weather-app.rain {
    background: linear-gradient(135deg, #a1c4fd 0%, #c2e9fb 100%);
}

.weather-app.thunderstorm {
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}

.weather-app.snow {
    background: linear-gradient(135deg, #e0eafc 0%, #cfdef3 100%);
}

.weather-app.mist {
    background: linear-gradient(135deg, #a8edea 0%, #fed6e3 100%);
}

3. 编写JavaScript逻辑

最后,我们需要编写JavaScript逻辑,实现天气应用的所有功能,包括API调用、地理位置定位、数据处理和UI更新等。

// API配置
const API_KEY = 'YOUR_API_KEY'; // 请替换为你的OpenWeatherMap API密钥
const BASE_URL = 'https://api.openweathermap.org/data/2.5';

// DOM元素
const loadingElement = document.getElementById('loading');
const errorElement = document.getElementById('error');
const errorMessageElement = document.getElementById('error-message');
const weatherInfoElement = document.getElementById('weather-info');
const searchInputElement = document.getElementById('search-input');
const searchBtnElement = document.getElementById('search-btn');
const locationBtnElement = document.getElementById('location-btn');
const retryBtnElement = document.getElementById('retry-btn');
const celsiusElement = document.getElementById('celsius');
const fahrenheitElement = document.getElementById('fahrenheit');

// 天气数据
let currentWeatherData = null;
let forecastData = null;
let isCelsius = true;

// 初始化应用
function initApp() {
    // 添加事件监听器
    searchBtnElement.addEventListener('click', handleSearch);
    searchInputElement.addEventListener('keypress', (e) => {
        if (e.key === 'Enter') {
            handleSearch();
        }
    });
    locationBtnElement.addEventListener('click', getCurrentLocation);
    retryBtnElement.addEventListener('click', () => {
        if (currentWeatherData) {
            updateWeatherDisplay();
        } else {
            getCurrentLocation();
        }
    });
    celsiusElement.addEventListener('click', () => {
        isCelsius = true;
        updateTemperatureUnit();
        updateWeatherDisplay();
    });
    fahrenheitElement.addEventListener('click', () => {
        isCelsius = false;
        updateTemperatureUnit();
        updateWeatherDisplay();
    });
    
    // 默认获取当前位置的天气
    getCurrentLocation();
}

// 处理搜索
async function handleSearch() {
    const city = searchInputElement.value.trim();
    if (city) {
        await fetchWeatherByCity(city);
    }
}

// 获取当前位置
function getCurrentLocation() {
    if (navigator.geolocation) {
        showLoading();
        navigator.geolocation.getCurrentPosition(
            async (position) => {
                const { latitude, longitude } = position.coords;
                await fetchWeatherByCoords(latitude, longitude);
            },
            (error) => {
                hideLoading();
                showError('无法获取您的位置,请手动搜索城市');
                console.error('Geolocation error:', error);
            }
        );
    } else {
        showError('您的浏览器不支持地理位置定位,请手动搜索城市');
    }
}

// 根据城市名获取天气
async function fetchWeatherByCity(city) {
    showLoading();
    try {
        // 获取当前天气
        const currentWeatherResponse = await fetch(`${BASE_URL}/weather?q=${city}&appid=${API_KEY}&units=metric`);
        if (!currentWeatherResponse.ok) {
            throw new Error('城市不存在或无法获取天气数据');
        }
        currentWeatherData = await currentWeatherResponse.json();
        
        // 获取天气预报
        const forecastResponse = await fetch(`${BASE_URL}/forecast?q=${city}&appid=${API_KEY}&units=metric`);
        if (!forecastResponse.ok) {
            throw new Error('无法获取天气预报数据');
        }
        forecastData = await forecastResponse.json();
        
        // 更新天气显示
        updateWeatherDisplay();
        hideLoading();
    } catch (error) {
        hideLoading();
        showError(error.message);
        console.error('Error fetching weather data:', error);
    }
}

// 根据坐标获取天气
async function fetchWeatherByCoords(latitude, longitude) {
    try {
        // 获取当前天气
        const currentWeatherResponse = await fetch(`${BASE_URL}/weather?lat=${latitude}&lon=${longitude}&appid=${API_KEY}&units=metric`);
        if (!currentWeatherResponse.ok) {
            throw new Error('无法获取天气数据');
        }
        currentWeatherData = await currentWeatherResponse.json();
        
        // 获取天气预报
        const forecastResponse = await fetch(`${BASE_URL}/forecast?lat=${latitude}&lon=${longitude}&appid=${API_KEY}&units=metric`);
        if (!forecastResponse.ok) {
            throw new Error('无法获取天气预报数据');
        }
        forecastData = await forecastResponse.json();
        
        // 更新天气显示
        updateWeatherDisplay();
        hideLoading();
    } catch (error) {
        hideLoading();
        showError(error.message);
        console.error('Error fetching weather data:', error);
    }
}

// 更新温度单位显示
function updateTemperatureUnit() {
    if (isCelsius) {
        celsiusElement.classList.add('active');
        fahrenheitElement.classList.remove('active');
    } else {
        fahrenheitElement.classList.add('active');
        celsiusElement.classList.remove('active');
    }
}

// 更新天气显示
function updateWeatherDisplay() {
    if (!currentWeatherData || !forecastData) return;
    
    // 更新当前天气信息
    updateCurrentWeather();
    
    // 更新天气预报
    updateForecast();
    
    // 更新背景
    updateBackground();
    
    // 显示天气信息,隐藏错误
    weatherInfoElement.style.display = 'block';
    errorElement.style.display = 'none';
}

// 更新当前天气信息
function updateCurrentWeather() {
    // 获取DOM元素
    const cityNameElement = document.getElementById('city-name');
    const dateTimeElement = document.getElementById('date-time');
    const currentTempElement = document.getElementById('current-temp');
    const weatherIconElement = document.getElementById('weather-icon');
    const weatherDescriptionElement = document.getElementById('weather-description');
    const humidityElement = document.getElementById('humidity');
    const windSpeedElement = document.getElementById('wind-speed');
    const pressureElement = document.getElementById('pressure');
    const cloudinessElement = document.getElementById('cloudiness');
    const sunriseElement = document.getElementById('sunrise');
    const sunsetElement = document.getElementById('sunset');
    
    // 更新城市名
    cityNameElement.textContent = `${currentWeatherData.name}, ${currentWeatherData.sys.country}`;
    
    // 更新日期时间
    const now = new Date();
    dateTimeElement.textContent = now.toLocaleString('zh-CN', {
        year: 'numeric',
        month: 'long',
        day: 'numeric',
        weekday: 'long',
        hour: '2-digit',
        minute: '2-digit'
    });
    
    // 更新温度
    let temp = currentWeatherData.main.temp;
    if (!isCelsius) {
        temp = celsiusToFahrenheit(temp);
    }
    currentTempElement.textContent = Math.round(temp);
    
    // 更新天气图标
    const weatherIcon = getWeatherIcon(currentWeatherData.weather[0].icon);
    weatherIconElement.className = `wi ${weatherIcon}`;
    
    // 更新天气描述
    weatherDescriptionElement.textContent = currentWeatherData.weather[0].description;
    
    // 更新湿度
    humidityElement.textContent = `${currentWeatherData.main.humidity}%`;
    
    // 更新风速
    windSpeedElement.textContent = `${currentWeatherData.wind.speed} km/h`;
    
    // 更新气压
    pressureElement.textContent = `${currentWeatherData.main.pressure} hPa`;
    
    // 更新云量
    cloudinessElement.textContent = `${currentWeatherData.clouds.all}%`;
    
    // 更新日出时间
    const sunrise = new Date(currentWeatherData.sys.sunrise * 1000);
    sunriseElement.textContent = sunrise.toLocaleTimeString('zh-CN', {
        hour: '2-digit',
        minute: '2-digit'
    });
    
    // 更新日落时间
    const sunset = new Date(currentWeatherData.sys.sunset * 1000);
    sunsetElement.textContent = sunset.toLocaleTimeString('zh-CN', {
        hour: '2-digit',
        minute: '2-digit'
    });
}

// 更新天气预报
function updateForecast() {
    const forecastContainerElement = document.getElementById('forecast-container');
    
    // 清空天气预报容器
    forecastContainerElement.innerHTML = '';
    
    // 获取每天的天气预报(取每天12点的数据)
    const dailyForecasts = [];
    const forecastMap = new Map();
    
    forecastData.list.forEach(forecast => {
        const date = new Date(forecast.dt * 1000);
        const dateStr = date.toDateString();
        const hour = date.getHours();
        
        // 只取每天12点左右的数据
        if (hour >= 11 && hour <= 13 && !forecastMap.has(dateStr)) {
            forecastMap.set(dateStr, forecast);
            dailyForecasts.push(forecast);
        }
    });
    
    // 只显示未来5天的预报
    const next5Days = dailyForecasts.slice(0, 5);
    
    // 更新天气预报
    next5Days.forEach(forecast => {
        const forecastItem = document.createElement('div');
        forecastItem.className = 'forecast-item';
        
        const date = new Date(forecast.dt * 1000);
        
        forecastItem.innerHTML = `
            <div class="forecast-date">${date.toLocaleDateString('zh-CN', {
                weekday: 'short',
                month: 'short',
                day: 'numeric'
            })}</div>
            <i class="wi ${getWeatherIcon(forecast.weather[0].icon)} forecast-icon"></i>
            <div class="forecast-temp">${Math.round(isCelsius ? forecast.main.temp : celsiusToFahrenheit(forecast.main.temp))}°</div>
            <div class="forecast-desc">${forecast.weather[0].description}</div>
        `;
        
        forecastContainerElement.appendChild(forecastItem);
    });
}

// 更新背景
function updateBackground() {
    const weatherAppElement = document.querySelector('.weather-app');
    const weatherMain = currentWeatherData.weather[0].main.toLowerCase();
    
    // 移除所有天气背景类
    weatherAppElement.className = 'weather-app';
    
    // 添加对应的天气背景类
    if (weatherMain === 'clear') {
        weatherAppElement.classList.add('clear');
    } else if (weatherMain === 'clouds') {
        weatherAppElement.classList.add('clouds');
    } else if (weatherMain === 'rain' || weatherMain === 'drizzle') {
        weatherAppElement.classList.add('rain');
    } else if (weatherMain === 'thunderstorm') {
        weatherAppElement.classList.add('thunderstorm');
    } else if (weatherMain === 'snow') {
        weatherAppElement.classList.add('snow');
    } else {
        weatherAppElement.classList.add('mist');
    }
}

// 获取天气图标
function getWeatherIcon(iconCode) {
    // 天气图标映射
    const iconMap = {
        '01d': 'wi-day-sunny',
        '01n': 'wi-night-clear',
        '02d': 'wi-day-cloudy',
        '02n': 'wi-night-alt-cloudy',
        '03d': 'wi-cloud',
        '03n': 'wi-cloud',
        '04d': 'wi-cloudy',
        '04n': 'wi-cloudy',
        '09d': 'wi-showers',
        '09n': 'wi-showers',
        '10d': 'wi-day-rain',
        '10n': 'wi-night-alt-rain',
        '11d': 'wi-thunderstorm',
        '11n': 'wi-thunderstorm',
        '13d': 'wi-snow',
        '13n': 'wi-snow',
        '50d': 'wi-fog',
        '50n': 'wi-fog'
    };
    
    return iconMap[iconCode] || 'wi-na';
}

// 摄氏度转华氏度
function celsiusToFahrenheit(celsius) {
    return (celsius * 9/5) + 32;
}

// 显示加载状态
function showLoading() {
    loadingElement.style.display = 'flex';
    weatherInfoElement.style.display = 'none';
    errorElement.style.display = 'none';
}

// 隐藏加载状态
function hideLoading() {
    loadingElement.style.display = 'none';
}

// 显示错误信息
function showError(message) {
    errorMessageElement.textContent = message;
    errorElement.style.display = 'flex';
    loadingElement.style.display = 'none';
    weatherInfoElement.style.display = 'none';
}

// 初始化应用
initApp();

功能说明

  1. 实时天气显示:显示当前城市的实时天气信息,包括温度、湿度、风速、气压等。
  2. 地理位置定位:自动获取用户所在城市的天气信息。
  3. 城市搜索:支持通过搜索框搜索不同城市的天气。
  4. 温度单位切换:支持在摄氏度和华氏度之间切换。
  5. 天气预报:显示未来5天的天气预报。
  6. 日出日落时间:显示当地的日出日落时间。
  7. 天气图标和背景:根据天气状况显示相应的图标和背景色。
  8. 错误处理:处理各种边界情况,如网络错误、城市不存在等。
  9. 响应式设计:适配不同屏幕尺寸,在手机和电脑上都能正常使用。

扩展功能

我们可以进一步扩展天气应用的功能,例如:

  1. 支持更多天气数据:空气质量、紫外线指数、能见度等。
  2. 支持小时预报:显示未来24小时的逐小时预报。
  3. 支持天气预警:显示当地的天气预警信息。
  4. 支持多个城市管理:允许用户添加多个城市,方便切换查看。
  5. 支持深色/浅色主题切换:根据用户偏好或时间自动切换主题。
  6. 支持天气动画:添加天气动画效果,增强视觉体验。
  7. 支持语音播报:使用语音播报当前天气信息。
  8. 支持数据缓存:缓存天气数据,在离线时也能查看。

扩展功能示例:空气质量指数

// 获取空气质量数据
async function fetchAirQuality(lat, lon) {
    try {
        const response = await fetch(`${BASE_URL}/air_pollution?lat=${lat}&lon=${lon}&appid=${API_KEY}`);
        if (!response.ok) {
            throw new Error('无法获取空气质量数据');
        }
        const data = await response.json();
        return data;
    } catch (error) {
        console.error('Error fetching air quality data:', error);
        return null;
    }
}

// 显示空气质量指数
function showAirQuality(airQualityData) {
    const aqi = airQualityData.list[0].main.aqi;
    const aqiText = {
        1: '优',
        2: '良',
        3: '轻度污染',
        4: '中度污染',
        5: '重度污染'
    };
    
    const airQualityElement = document.createElement('div');
    airQualityElement.className = 'stat-item';
    airQualityElement.innerHTML = `
        <i class="wi wi-smoke"></i>
        <span>${aqiText[aqi]}</span>
        <span class="stat-label">空气质量</span>
    `;
    
    const weatherStatsElement = document.querySelector('.weather-stats');
    weatherStatsElement.appendChild(airQualityElement);
}

项目运行

  1. 注册OpenWeatherMap账号,获取API密钥
  2. 将API密钥替换到script.js文件中的YOUR_API_KEY
  3. 创建上述三个文件(index.html, styles.css, script.js)
  4. 在浏览器中打开index.html文件
  5. 开始使用天气应用

项目总结

通过这个天气应用项目,我们学习了:

  1. 如何使用JavaScript调用第三方API获取数据
  2. 如何使用地理位置定位获取用户所在位置
  3. 如何使用异步编程(async/await)处理网络请求
  4. 如何使用DOM操作更新页面内容
  5. 如何处理各种边界情况和错误
  6. 如何实现响应式设计
  7. 如何根据数据动态更新页面样式

这个项目涵盖了JavaScript开发中的许多重要概念,为我们后续开发更复杂的应用打下了基础。

练习

  1. 添加空气质量指数显示
  2. 添加小时预报功能
  3. 实现多个城市管理功能
  4. 添加深色/浅色主题切换
  5. 添加天气动画效果
  6. 实现数据缓存功能
  7. 添加语音播报功能
  8. 添加天气预警信息显示

扩展阅读

通过这个实战项目,我们已经掌握了JavaScript的API集成和地理位置定位,接下来我们将继续学习JavaScript的常见问题和调试技巧。

« 上一篇 实战项目4:问答系统 下一篇 » JavaScript常见问题解答