Skip to content

移动端适配实战:一个天气卡片如何同时适配手机、平板、PC

某天想做一个简单的天气查询页面,顺便练一下移动端响应式布局。要求:手机上看是单列卡片,平板和 PC 上自动调整宽度和字体,不依赖框架,只用 CSS 媒体查询。

最后选了和风天气的免费 API,前端用原生 HTML/CSS/JS,再加 localStorage 保存最近搜索的城市。

技术点

  • 媒体查询 @media 适配三种屏幕宽度
  • 卡片式 UI,flex 布局
  • 调用第三方天气 API(需申请 key)
  • localStorage 存储历史城市

完整代码

创建一个 index.html 文件,内容如下:

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
    <title>天气卡片 - 适配手机/平板/PC</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }

        body {
            font-family: system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
            background: #f0f2f5;
            padding: 20px;
            min-height: 100vh;
            display: flex;
            justify-content: center;
            align-items: center;
        }

        /* 卡片容器 */
        .weather-card {
            background: white;
            border-radius: 32px;
            box-shadow: 0 8px 20px rgba(0,0,0,0.08);
            padding: 24px;
            transition: all 0.2s ease;
            width: 100%;
        }

        /* 手机尺寸(默认) */
        .weather-card {
            max-width: 100%;
        }

        /* 平板:宽度 >= 600px */
        @media (min-width: 600px) {
            .weather-card {
                max-width: 500px;
                padding: 32px;
            }
            body {
                padding: 40px;
            }
            .city-name {
                font-size: 2rem;
            }
            .temp {
                font-size: 3.5rem;
            }
        }

        /* PC:宽度 >= 1024px */
        @media (min-width: 1024px) {
            .weather-card {
                max-width: 650px;
                padding: 40px;
            }
            .city-name {
                font-size: 2.5rem;
            }
            .temp {
                font-size: 4rem;
            }
        }

        .city-name {
            font-size: 1.8rem;
            font-weight: 600;
            margin-bottom: 8px;
        }

        .temp {
            font-size: 3rem;
            font-weight: 300;
            margin: 16px 0 8px;
        }

        .condition {
            font-size: 1.2rem;
            color: #555;
            margin-bottom: 16px;
        }

        .search-area {
            margin-top: 24px;
            display: flex;
            gap: 10px;
            flex-wrap: wrap;
        }

        .search-area input {
            flex: 1;
            padding: 12px 16px;
            border: 1px solid #ddd;
            border-radius: 40px;
            font-size: 1rem;
        }

        .search-area button {
            background: #007aff;
            color: white;
            border: none;
            padding: 12px 24px;
            border-radius: 40px;
            font-size: 1rem;
            cursor: pointer;
        }

        .history {
            margin-top: 20px;
            font-size: 0.9rem;
            color: #666;
        }

        .history button {
            background: #e9ecef;
            border: none;
            padding: 6px 12px;
            border-radius: 20px;
            margin-right: 8px;
            margin-top: 8px;
            cursor: pointer;
        }

        .error {
            color: #d32f2f;
            margin-top: 12px;
        }
    </style>
</head>
<body>
<div class="weather-card" id="weatherCard">
    <div class="city-name" id="cityName">加载中...</div>
    <div class="temp" id="temp">--°C</div>
    <div class="condition" id="condition">--</div>
    <div class="search-area">
        <input type="text" id="cityInput" placeholder="输入城市名,如 Beijing" value="Beijing">
        <button id="searchBtn">查询</button>
    </div>
    <div class="history" id="historyDiv">
        <strong>最近搜索:</strong><span id="historyList"></span>
    </div>
    <div id="errorMsg" class="error"></div>
</div>

<script>
    // 和风天气 API (免费版需申请 key)
    // 申请地址:https://dev.qweather.com/
    const API_KEY = 'YOUR_API_KEY_HERE';  // 替换成你自己的 key

    // 默认城市
    let currentCity = 'Beijing';

    // localStorage 最近搜索(最多5个)
    function getHistory() {
        const stored = localStorage.getItem('weather_history');
        if (stored) {
            return JSON.parse(stored);
        }
        return [];
    }

    function saveHistory(city) {
        let history = getHistory();
        // 去重,并放到最前
        history = history.filter(c => c !== city);
        history.unshift(city);
        if (history.length > 5) history.pop();
        localStorage.setItem('weather_history', JSON.stringify(history));
        renderHistory();
    }

    function renderHistory() {
        const history = getHistory();
        const container = document.getElementById('historyList');
        if (!container) return;
        container.innerHTML = '';
        history.forEach(city => {
            const btn = document.createElement('button');
            btn.textContent = city;
            btn.onclick = () => {
                document.getElementById('cityInput').value = city;
                searchWeather(city);
            };
            container.appendChild(btn);
        });
    }

    async function searchWeather(city) {
        const errorDiv = document.getElementById('errorMsg');
        errorDiv.innerText = '';
        if (!city.trim()) return;

        // 和风天气 API:先获取城市ID,再获取天气(此处简化:直接用城市名请求实时天气,需使用城市搜索)
        // 为简化示例,这里使用 7Timer 或其他更简单的免费 API?但保证真实可用的方案是:改用 OpenWeatherMap 免费版
        // 为避免 API 失效,这里改用 OpenWeatherMap 的示例(需要 key)
        // 真实场景中申请自己的 key。下面演示结构,实际运行需替换真实接口。
        // 注意:免费天气 API 通常有跨域限制,需要后端代理或使用 JSONP。这里为了演示前端适配,假设已经处理好跨域。
        
        // 由于时间关系,本文只写出逻辑骨架,实际使用时填上你自己的 API 地址。
        // 参考:OpenWeatherMap 付费免费层:https://openweathermap.org/current
        const url = `https://api.openweathermap.org/data/2.5/weather?q=${city}&appid=${API_KEY}&units=metric&lang=zh_cn`;
        
        try {
            const response = await fetch(url);
            if (!response.ok) throw new Error('城市不存在或API限制');
            const data = await response.json();
            document.getElementById('cityName').innerText = data.name;
            document.getElementById('temp').innerHTML = `${Math.round(data.main.temp)}°C`;
            document.getElementById('condition').innerText = data.weather[0].description;
            saveHistory(city);
            currentCity = city;
        } catch (err) {
            errorDiv.innerText = '查询失败:' + err.message;
            console.error(err);
        }
    }

    // 初始化
    function init() {
        renderHistory();
        const defaultCity = getHistory()[0] || 'Beijing';
        document.getElementById('cityInput').value = defaultCity;
        searchWeather(defaultCity);
        document.getElementById('searchBtn').onclick = () => {
            const input = document.getElementById('cityInput').value.trim();
            if (input) searchWeather(input);
        };
        document.getElementById('cityInput').addEventListener('keypress', (e) => {
            if (e.key === 'Enter') {
                const input = document.getElementById('cityInput').value.trim();
                if (input) searchWeather(input);
            }
        });
    }

    init();
</script>
</body>
</html>

说明

  • 媒体查询:手机默认卡片宽度 100%(边距 20px);平板(≥600px)卡片限制最大 500px,字体放大;PC(≥1024px)卡片最大 650px,进一步放大字体。你可以在浏览器开发者工具切换到设备模拟模式测试。
  • 天气 API:示例中使用了 OpenWeatherMap 的 API,需要申请免费 API Key,替换 YOUR_API_KEY_HERE。和风天气也类似,但可能涉及跨域问题,建议用 OpenWeatherMap。
  • localStorage:保存最近 5 个搜索的城市,点击历史按钮快速查询。
  • 跨域问题:免费天气 API 大多允许前端直接调用(CORS 已开放),如果遇到跨域报错,可以考虑用免费的 JSONP 服务或自建后端代理。但作为本地演示,可以直接双击 index.html 运行(前提是 API 支持 CORS)。

遇到的坑

1. 移动端点击输入框自动放大
通过 <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes"> 中设置 user-scalable=yes 允许缩放,但输入框字体小于 16px 会导致 iOS 自动放大。解决:设置 input { font-size: 16px; }(已隐含在样式中)。

2. 媒体查询断点选择
常用断点:600px(平板竖屏)、1024px(PC)。实际可根据需要调整。

3. API 请求跨域
很多天气 API 免费版不支持浏览器直接调用。可以改用支持 CORS 的服务(如 OpenWeatherMap 的 https://api.openweathermap.org/data/2.5/weather 支持跨域)。如果不行,可以使用代理如 https://cors-anywhere.herokuapp.com/,但不建议在生产使用。

4. localStorage 读取时机
需要在页面加载时读取并渲染历史按钮,否则点击无效。

总结

这个页面纯前端,不依赖框架,核心是 CSS 媒体查询 + flex 布局。适配三种常见屏幕宽度,同时演示了 localStorage 和 fetch API。

实际使用时,只需替换成你自己的天气 API Key,放到任意静态服务器(或本地打开)即可演示。