天气

经验创意 · 107 次浏览
ai56 创建于 2026-07-25 22:43

天气,不需要api

<!doctype html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <title>晴雨窗 3</title>
    <style>
        *{box-sizing:border-box;margin:0;padding:0}
        html,body{
            width:100%;height:100%;overflow:hidden;
            background:transparent;font-family:"Microsoft YaHei",sans-serif;
        }
        :root{
            --bg:rgba(18,22,33,0.78);
            --card:rgba(28,34,52,0.72);
            --text:#eef2ff;
            --sub:#9aa4bf;
            --accent:#7aa2f7;
            --warm:#ff9e64;
            --cold:#7dcfff;
            --rain:#6d9eff;
            --border:rgba(255,255,255,0.08);
            --shadow:0 8px 32px rgba(0,0,0,0.35);
            --radius:20px;
            --gap:12px;
        }
        body{
            display:flex;align-items:center;justify-content:center;
            padding:10px;backdrop-filter:blur(18px) saturate(1.4);
            -webkit-backdrop-filter:blur(18px) saturate(1.4);
        }
        .panel{
            width:100%;max-width:480px;min-width:260px;
            background:var(--bg);border-radius:var(--radius);
            box-shadow:var(--shadow);border:1px solid var(--border);
            padding:16px var(--gap);display:flex;flex-direction:column;
            gap:var(--gap);backdrop-filter:blur(20px);
            -webkit-backdrop-filter:blur(20px);
            transition:all 0.3s ease;
        }
        .location-row{
            display:flex;align-items:center;justify-content:space-between;
            gap:8px;flex-wrap:wrap;
        }
        .city-selector{
            display:flex;align-items:center;gap:6px;position:relative;flex:1;min-width:0;
        }
        .city-name{
            font-size:1.3rem;font-weight:600;color:var(--text);
            letter-spacing:0.5px;display:flex;align-items:center;gap:6px;
            cursor:pointer;padding:4px 8px;border-radius:10px;
            transition:background 0.2s;user-select:none;max-width:100%;overflow:hidden;
        }
        .city-name:hover{background:rgba(255,255,255,0.08)}
        .city-name svg{width:18px;height:18px;opacity:0.7;flex-shrink:0}
        .city-name span{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
        .city-dropdown-arrow{width:14px;height:14px;opacity:0.6;transition:transform 0.2s;flex-shrink:0}
        .city-dropdown-arrow.open{transform:rotate(180deg)}
        .city-dropdown{
            position:absolute;top:100%;left:0;margin-top:6px;
            background:rgba(28,34,52,0.96);border:1px solid var(--border);
            border-radius:14px;box-shadow:0 12px 40px rgba(0,0,0,0.5);
            padding:6px;z-index:20;min-width:180px;max-height:260px;
            overflow-y:auto;display:none;backdrop-filter:blur(16px);
            -webkit-backdrop-filter:blur(16px);
        }
        .city-dropdown.show{display:block}
        .city-search-wrap{
            padding:4px 6px 6px;
            position:sticky;top:0;background:rgba(28,34,52,0.96);
            z-index:2;border-radius:10px;
        }
        .city-search-input{
            width:100%;padding:8px 12px;border-radius:10px;
            border:1px solid var(--border);background:rgba(255,255,255,0.06);
            color:var(--text);font-size:0.85rem;outline:none;
            font-family:inherit;
        }
        .city-search-input::placeholder{color:var(--sub);opacity:0.7}
        .city-search-input:focus{border-color:var(--accent)}
        .city-dropdown-item{
            padding:8px 14px;border-radius:10px;cursor:pointer;
            color:var(--text);font-size:0.9rem;transition:background 0.15s;
            white-space:nowrap;
        }
        .city-dropdown-item:hover{background:rgba(255,255,255,0.1)}
        .city-dropdown-item.active{background:rgba(122,162,247,0.25);color:#bdd0ff}
        .city-dropdown-empty{
            padding:12px 14px;color:var(--sub);font-size:0.85rem;
            text-align:center;
        }
        .refresh-btn{
            background:var(--card);border:1px solid var(--border);
            color:var(--text);border-radius:28px;padding:6px 14px;
            cursor:pointer;font-size:0.85rem;display:flex;align-items:center;
            gap:6px;transition:all 0.2s;backdrop-filter:blur(8px);
            -webkit-backdrop-filter:blur(8px);flex-shrink:0;
        }
        .refresh-btn:hover{background:rgba(255,255,255,0.12);border-color:rgba(255,255,255,0.2);}
        .refresh-btn:active{transform:scale(0.96)}
        .refresh-btn:disabled{opacity:0.5;pointer-events:none}
        .refresh-btn svg{width:16px;height:16px;opacity:0.8}
        .current-weather{
            display:flex;align-items:center;justify-content:space-between;
            gap:10px;flex-wrap:wrap;
        }
        .temp-area{display:flex;align-items:center;gap:10px}
        .weather-icon{width:64px;height:64px;filter:drop-shadow(0 4px 12px rgba(0,0,0,0.3));}
        .temp-number{font-size:3.4rem;font-weight:700;color:var(--text);line-height:1;
            letter-spacing:-1px;
        }
        .temp-unit{font-size:1.2rem;color:var(--sub);margin-left:2px}
        .desc-area{display:flex;flex-direction:column;align-items:flex-end;gap:4px;}
        .weather-desc{font-size:1.1rem;color:var(--text);font-weight:500}
        .feels-like{font-size:0.85rem;color:var(--sub)}
        .meta-row{
            display:flex;gap:12px;flex-wrap:wrap;justify-content:space-between;
            background:var(--card);border-radius:16px;padding:10px 14px;
            border:1px solid var(--border);
        }
        .meta-item{display:flex;align-items:center;gap:5px;font-size:0.8rem;color:var(--sub);}
        .meta-item svg{width:18px;height:18px;opacity:0.7}
        .meta-item strong{color:var(--text);font-weight:600}
        .forecast-scroll{
            display:flex;gap:8px;overflow-x:auto;padding-bottom:6px;
            scrollbar-width:thin;scrollbar-color:rgba(255,255,255,0.2) transparent;
        }
        .forecast-scroll::-webkit-scrollbar{height:4px;}
        .forecast-scroll::-webkit-scrollbar-thumb{background:rgba(255,255,255,0.25);border-radius:10px;}
        .forecast-card{
            flex:0 0 auto;min-width:75px;background:var(--card);
            border-radius:14px;padding:10px 6px;text-align:center;
            border:1px solid var(--border);display:flex;flex-direction:column;
            align-items:center;gap:6px;transition:0.2s;
        }
        .forecast-card:hover{background:rgba(255,255,255,0.08);}
        .forecast-day{font-size:0.8rem;color:var(--sub);font-weight:500}
        .forecast-icon{width:32px;height:32px;}
        .forecast-temp{font-size:0.9rem;font-weight:600;color:var(--text)}
        .forecast-low{color:var(--sub);font-weight:400;font-size:0.8rem}
        .error-banner{
            background:rgba(255,120,120,0.18);border:1px solid rgba(255,130,130,0.3);
            border-radius:12px;padding:8px 12px;font-size:0.8rem;color:#ffcaca;
            display:flex;align-items:center;gap:8px;word-break:break-word;
        }
        .error-banner svg{width:16px;height:16px;flex-shrink:0;}
        .loading-spinner{
            display:flex;align-items:center;justify-content:center;
            padding:20px;color:var(--sub);gap:8px;
        }
        .spinner{
            width:20px;height:20px;border:2px solid rgba(255,255,255,0.2);
            border-top-color:var(--accent);border-radius:50%;
            animation:spin 0.8s linear infinite;
        }
        @keyframes spin{to{transform:rotate(360deg)}}
        .empty-state{
            text-align:center;color:var(--sub);padding:20px;font-size:0.9rem;
        }
    </style>
</head>
<body>
<div class="panel">
    <div class="location-row">
        <div class="city-selector" id="citySelector">
            <div class="city-name" id="cityDisplay" title="点击切换城市" aria-label="选择城市">
                <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"/><circle cx="12" cy="10" r="3"/></svg>
                <span id="cityText">--</span>
                <svg class="city-dropdown-arrow" id="dropdownArrow" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"/></svg>
            </div>
            <div class="city-dropdown" id="cityDropdown">
                <div class="city-search-wrap">
                    <input class="city-search-input" id="citySearchInput" placeholder="搜索城市..." autocomplete="off">
                </div>
                <div id="cityListContainer"></div>
            </div>
        </div>
        <button class="refresh-btn" id="refreshBtn" title="刷新天气" aria-label="刷新天气">
            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/></svg>
            刷新
        </button>
    </div>
    <div id="weatherContent"></div>
    <div id="errorContainer"></div>
</div>
<script>
    (function() {
        const host = window.widgetHost;
        const STATE_CITY_KEY = 'weather_city';
        const STATE_WEATHER_KEY = 'weather_cache';
        const DEFAULT_CITY = '北京';
        const REFRESH_INTERVAL_MS = 120000;

        const ALL_CITIES = [
            '北京','上海','广州','深圳','成都','杭州','武汉','南京','西安',
            '重庆','长沙','郑州','济南','天津','昆明','福州','沈阳','哈尔滨',
            '大连','青岛','厦门','苏州','宁波','贵阳','南宁','合肥','石家庄',
            '太原','兰州','乌鲁木齐','拉萨','海口','三亚','桂林','大理',
            '喀什','和田','阿克苏','库尔勒','伊宁','克拉玛依','石河子','吐鲁番',
            '哈密','昌吉','塔城','阿勒泰','博乐','阿图什','莎车','巴楚',
            '珠海','佛山','东莞','惠州','中山','湛江','汕头','揭阳','潮州',
            '温州','绍兴','嘉兴','金华','台州','湖州','衢州','丽水','舟山',
            '无锡','常州','南通','徐州','扬州','盐城','泰州','镇江','淮安',
            '烟台','潍坊','临沂','淄博','济宁','威海','日照','德州','聊城',
            '洛阳','南阳','许昌','开封','新乡','安阳','信阳','商丘','平顶山',
            '宜昌','襄阳','荆州','黄石','十堰','鄂州','荆门','孝感','黄冈',
            '株洲','湘潭','衡阳','岳阳','常德','邵阳','益阳','郴州','永州',
            '绵阳','宜宾','德阳','南充','泸州','乐山','达州','广元','遂宁',
            '宝鸡','咸阳','渭南','汉中','榆林','延安','安康','商洛','铜川',
            '吉林','延吉','四平','通化','白城','辽源','松原','白山',
            '齐齐哈尔','大庆','牡丹江','佳木斯','鸡西','鹤岗','双鸭山','伊春',
            '包头','赤峰','鄂尔多斯','呼伦贝尔','通辽','乌兰浩特','巴彦淖尔',
            '遵义','毕节','安顺','铜仁','六盘水','凯里','都匀','兴义',
            '丽江','香格里拉','西双版纳','腾冲','曲靖','玉溪','楚雄','文山',
            '北海','梧州','百色','河池','钦州','防城港','贵港','玉林',
            '天水','白银','张掖','武威','金昌','酒泉','嘉峪关','平凉',
            '芜湖','安庆','马鞍山','蚌埠','滁州','阜阳','宿州','六安',
            '九江','赣州','上饶','宜春','景德镇','萍乡','新余','鹰潭',
            '三亚','琼海','儋州','五指山','文昌','万宁','东方'
        ];

        const cityTextEl = document.getElementById('cityText');
        const cityDisplay = document.getElementById('cityDisplay');
        const cityDropdown = document.getElementById('cityDropdown');
        const dropdownArrow = document.getElementById('dropdownArrow');
        const citySelector = document.getElementById('citySelector');
        const citySearchInput = document.getElementById('citySearchInput');
        const cityListContainer = document.getElementById('cityListContainer');
        const refreshBtn = document.getElementById('refreshBtn');
        const weatherContentEl = document.getElementById('weatherContent');
        const errorContainerEl = document.getElementById('errorContainer');

        let currentCity = DEFAULT_CITY;
        let refreshTimer = null;
        let isRefreshing = false;
        let dropdownOpen = false;

        function showError(message, details) {
            const detailAttr = details ? `title="${escapeHtml(String(details))}"` : '';
            errorContainerEl.innerHTML = `
                <div class="error-banner" ${detailAttr}>
                    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
                    <span>${escapeHtml(message)}</span>
                </div>`;
        }

        function clearError() {
            errorContainerEl.innerHTML = '';
        }

        function escapeHtml(str) {
            const div = document.createElement('div');
            div.appendChild(document.createTextNode(str));
            return div.innerHTML;
        }

        function renderLoading() {
            weatherContentEl.innerHTML = `
                <div class="loading-spinner">
                    <div class="spinner"></div>
                    <span>加载天气...</span>
                </div>`;
        }

        function renderEmpty() {
            weatherContentEl.innerHTML = `<div class="empty-state">暂无天气数据,请点击刷新</div>`;
        }

        function getWeatherIcon(condition, isDay = true) {
            const c = (condition || '').toLowerCase();
            if (c.includes('晴') || c.includes('clear') || c.includes('sunny')) {
                return isDay ? sunnyDayIcon : sunnyNightIcon;
            } else if (c.includes('多云') || c.includes('cloud') || c.includes('partly')) {
                return isDay ? cloudyDayIcon : cloudyNightIcon;
            } else if (c.includes('阴') || c.includes('overcast')) {
                return overcastIcon;
            } else if (c.includes('雨') || c.includes('rain') || c.includes('drizzle') || c.includes('shower')) {
                return rainIcon;
            } else if (c.includes('雪') || c.includes('snow')) {
                return snowIcon;
            } else if (c.includes('雾') || c.includes('fog') || c.includes('mist') || c.includes('haze')) {
                return fogIcon;
            } else if (c.includes('风') || c.includes('wind')) {
                return windIcon;
            }
            return isDay ? sunnyDayIcon : cloudyDayIcon;
        }

        const sunnyDayIcon = `<svg class="weather-icon" viewBox="0 0 64 64" fill="none"><circle cx="32" cy="32" r="14" fill="#FFD93D" stroke="#FFB830" stroke-width="2"/><g stroke="#FFD93D" stroke-width="3" stroke-linecap="round"><line x1="32" y1="8" x2="32" y2="14"/><line x1="32" y1="50" x2="32" y2="56"/><line x1="8" y1="32" x2="14" y2="32"/><line x1="50" y1="32" x2="56" y2="32"/><line x1="15.03" y1="15.03" x2="19.27" y2="19.27"/><line x1="44.73" y1="44.73" x2="48.97" y2="48.97"/><line x1="15.03" y1="48.97" x2="19.27" y2="44.73"/><line x1="44.73" y1="19.27" x2="48.97" y2="15.03"/></g></svg>`;
        const sunnyNightIcon = `<svg class="weather-icon" viewBox="0 0 64 64" fill="none"><path d="M38 14C30 14 24 20 24 28s6 14 14 14c3.2 0 6.1-1.1 8.4-2.9C44.8 42.1 41 44 37 44c-8.8 0-16-7.2-16-16 0-5.8 3.1-10.9 7.7-13.7C30.5 14.1 32 14 33.6 14 35 14 36.5 14 38 14z" fill="#FFD93D"/></svg>`;
        const cloudyDayIcon = `<svg class="weather-icon" viewBox="0 0 64 64" fill="none"><circle cx="26" cy="24" r="12" fill="#FFD93D" opacity="0.9"/><path d="M20 42c-5.5 0-10-4.5-10-10s4.5-10 10-10c1.8 0 3.5 0.5 5 1.3C27.5 19 32 16 37 16c6.6 0 12 5.4 12 12s-5.4 12-12 12H20z" fill="#C4C9D9" opacity="0.85"/></svg>`;
        const cloudyNightIcon = `<svg class="weather-icon" viewBox="0 0 64 64" fill="none"><path d="M38 12c-3 0-5.8 1-8 2.7C32.3 17.7 34 21.8 34 26c0 7.7-6.3 14-14 14-1.8 0-3.5-0.4-5-1C17.5 44 23 48 29 48h12c7.2 0 13-5.8 13-13s-5.8-13-13-13c-1 0-2 0-3 0.5V12z" fill="#C4C9D9"/></svg>`;
        const overcastIcon = `<svg class="weather-icon" viewBox="0 0 64 64" fill="none"><path d="M18 38c-4.4 0-8-3.6-8-8s3.6-8 8-8c1.3 0 2.6 0.3 3.8 0.9C24 17 29 14 35 14c6.6 0 12 5.4 12 12s-5.4 12-12 12H18z" fill="#A0A8BD"/></svg>`;
        const rainIcon = `<svg class="weather-icon" viewBox="0 0 64 64" fill="none"><path d="M18 30c-4.4 0-8-3.6-8-8s3.6-8 8-8c1.3 0 2.6 0.3 3.8 0.9C24 9 29 6 35 6c6.6 0 12 5.4 12 12s-5.4 12-12 12H18z" fill="#8E9BB5"/><line x1="22" y1="36" x2="20" y2="44" stroke="#6D9EFF" stroke-width="3" stroke-linecap="round"/><line x1="30" y1="34" x2="28" y2="42" stroke="#6D9EFF" stroke-width="3" stroke-linecap="round"/><line x1="38" y1="36" x2="36" y2="44" stroke="#6D9EFF" stroke-width="3" stroke-linecap="round"/></svg>`;
        const snowIcon = `<svg class="weather-icon" viewBox="0 0 64 64" fill="none"><path d="M18 28c-4.4 0-8-3.6-8-8s3.6-8 8-8c1.3 0 2.6 0.3 3.8 0.9C24 7 29 4 35 4c6.6 0 12 5.4 12 12s-5.4 12-12 12H18z" fill="#C8D0E0"/><circle cx="22" cy="40" r="2" fill="#E0F0FF"/><circle cx="30" cy="38" r="2" fill="#E0F0FF"/><circle cx="38" cy="42" r="2" fill="#E0F0FF"/></svg>`;
        const fogIcon = `<svg class="weather-icon" viewBox="0 0 64 64" fill="none"><line x1="14" y1="26" x2="50" y2="26" stroke="#B0BEC5" stroke-width="4" stroke-linecap="round"/><line x1="18" y1="34" x2="46" y2="34" stroke="#B0BEC5" stroke-width="4" stroke-linecap="round"/><line x1="22" y1="42" x2="42" y2="42" stroke="#B0BEC5" stroke-width="4" stroke-linecap="round"/></svg>`;
        const windIcon = `<svg class="weather-icon" viewBox="0 0 64 64" fill="none"><path d="M14 28c3.3 0 6-2.7 6-6s-2.7-6-6-6" stroke="#B0C4DE" stroke-width="4" stroke-linecap="round"/><path d="M48 30c4.4 0 8-3.6 8-8s-3.6-8-8-8" stroke="#B0C4DE" stroke-width="4" stroke-linecap="round"/><line x1="12" y1="40" x2="52" y2="40" stroke="#B0C4DE" stroke-width="4" stroke-linecap="round"/></svg>`;

        function buildWeatherHTML(data) {
            const current = data.current || {};
            const forecasts = data.forecast || [];
            const temp = current.temp != null ? Math.round(current.temp) : '--';
            const feelsLike = current.feelsLike != null ? Math.round(current.feelsLike) : null;
            const condition = current.condition || '未知';
            const humidity = current.humidity != null ? current.humidity : '--';
            const wind = current.wind != null ? current.wind : '--';
            const isDay = current.isDay !== false;
            const iconSvg = getWeatherIcon(condition, isDay);

            let forecastHTML = '';
            if (forecasts.length > 0) {
                forecastHTML = `<div class="forecast-scroll">` + forecasts.map(f => {
                    const high = f.high != null ? Math.round(f.high) : '--';
                    const low = f.low != null ? Math.round(f.low) : '--';
                    const fIcon = getWeatherIcon(f.condition || '', true);
                    return `
                        <div class="forecast-card">
                            <span class="forecast-day">${escapeHtml(f.day || '--')}</span>
                            <div class="forecast-icon">${fIcon}</div>
                            <span class="forecast-temp">${high}°</span>
                            <span class="forecast-low">${low}°</span>
                        </div>`;
                }).join('') + `</div>`;
            }

            return `
                <div class="current-weather">
                    <div class="temp-area">
                        ${iconSvg}
                        <span class="temp-number">${temp}<span class="temp-unit">°C</span></span>
                    </div>
                    <div class="desc-area">
                        <span class="weather-desc">${escapeHtml(condition)}</span>
                        ${feelsLike !== null ? `<span class="feels-like">体感 ${feelsLike}°</span>` : ''}
                    </div>
                </div>
                <div class="meta-row">
                    <div class="meta-item">
                        <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2.69l5.66 5.66a8 8 0 1 1-11.31 0z"/></svg>
                        <span>湿度 <strong>${humidity}%</strong></span>
                    </div>
                    <div class="meta-item">
                        <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9.59 4.59A2 2 0 1 1 11 8H2m10.59 11.41A2 2 0 1 0 14 16H2m15.73-8.27A2.5 2.5 0 1 1 19.5 12H2"/></svg>
                        <span>风力 <strong>${wind}</strong></span>
                    </div>
                </div>
                ${forecastHTML}
            `;
        }

        function generateMockWeather(city) {
            const now = new Date();
            const seed = city.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0);
            const pseudoRandom = (min, max, offset = 0) => {
                const val = Math.sin(seed + now.getDate() + now.getMonth() * 31 + offset) * 10000;
                const range = max - min;
                return Math.round(min + (val - Math.floor(val)) * range);
            };
            const conditions = ['晴', '多云', '阴', '小雨', '晴', '多云', '晴', '雾', '阴', '晴', '浮尘', '扬沙'];
            const conditionIndex = Math.abs(Math.floor((seed + now.getDate()) % conditions.length));
            const condition = conditions[conditionIndex];
            const temp = pseudoRandom(6, 36, 1);
            const feelsLike = temp + pseudoRandom(-3, 3, 2);
            const humidity = pseudoRandom(15, 85, 3);
            const windLevels = ['微风','1级','2级','3级','4级','5级'];
            const wind = windLevels[pseudoRandom(0, windLevels.length-1, 4)];

            const days = ['周日','周一','周二','周三','周四','周五','周六'];
            const forecast = [];
            for (let i = 1; i <= 5; i++) {
                const futureDate = new Date(now);
                futureDate.setDate(now.getDate() + i);
                const dayName = days[futureDate.getDay()];
                const high = temp + pseudoRandom(-5, 7, i+10);
                const low = high - pseudoRandom(4, 12, i+20);
                const fCondIdx = Math.abs(Math.floor((seed + i + now.getDate()) % conditions.length));
                forecast.push({
                    day: dayName,
                    condition: conditions[fCondIdx],
                    high: high,
                    low: low
                });
            }

            return {
                city: city,
                current: {
                    temp, feelsLike, condition, humidity, wind,
                    isDay: now.getHours() >= 6 && now.getHours() < 19
                },
                forecast: forecast,
                updatedAt: now.toISOString()
            };
        }

        async function fetchWeather(city) {
            return generateMockWeather(city);
        }

        async function loadWeather(forceRefresh = false) {
            if (isRefreshing) return;
            isRefreshing = true;
            refreshBtn.disabled = true;
            clearError();
            renderLoading();

            try {
                let weatherData = null;
                if (!forceRefresh) {
                    try {
                        const cached = await host.state.read(STATE_WEATHER_KEY, null);
                        if (cached && cached.city === currentCity) {
                            weatherData = cached;
                        }
                    } catch (e) { /* 忽略 */ }
                }

                if (!weatherData) {
                    weatherData = await fetchWeather(currentCity);
                    try {
                        await host.state.write(STATE_WEATHER_KEY, weatherData);
                    } catch (e) { /* 忽略 */ }
                }

                weatherContentEl.innerHTML = buildWeatherHTML(weatherData);
                cityTextEl.textContent = weatherData.city || currentCity;
            } catch (error) {
                const msg = error?.message || '获取天气失败';
                showError(msg, String(error));
                if (!weatherContentEl.innerHTML || weatherContentEl.querySelector('.loading-spinner')) {
                    renderEmpty();
                }
            } finally {
                isRefreshing = false;
                refreshBtn.disabled = false;
            }
        }

        function filterCities(query) {
            const q = query.trim().toLowerCase();
            if (!q) return ALL_CITIES;
            return ALL_CITIES.filter(city => city.toLowerCase().includes(q));
        }

        function renderCityList(cities) {
            if (cities.length === 0) {
                cityListContainer.innerHTML = `<div class="city-dropdown-empty">未找到匹配城市</div>`;
                return;
            }
            cityListContainer.innerHTML = cities.map(city => {
                const activeClass = city === currentCity ? ' active' : '';
                return `<div class="city-dropdown-item${activeClass}" data-city="${escapeHtml(city)}">${escapeHtml(city)}</div>`;
            }).join('');
        }

        function openDropdown() {
            renderCityList(ALL_CITIES);
            cityDropdown.classList.add('show');
            dropdownArrow.classList.add('open');
            dropdownOpen = true;
            setTimeout(() => {
                citySearchInput.focus();
            }, 50);
        }

        function closeDropdown() {
            cityDropdown.classList.remove('show');
            dropdownArrow.classList.remove('open');
            dropdownOpen = false;
            citySearchInput.value = '';
            renderCityList(ALL_CITIES);
        }

        function toggleDropdown(e) {
            e.stopPropagation();
            if (dropdownOpen) {
                closeDropdown();
            } else {
                openDropdown();
            }
        }

        async function selectCity(city) {
            if (city === currentCity) {
                closeDropdown();
                return;
            }
            closeDropdown();
            currentCity = city;
            cityTextEl.textContent = city;
            try {
                await host.state.write(STATE_CITY_KEY, city);
            } catch (e) { /* 忽略 */ }
            await loadWeather(true);
        }

        cityDisplay.addEventListener('click', toggleDropdown);

        citySearchInput.addEventListener('input', (e) => {
            const filtered = filterCities(e.target.value);
            renderCityList(filtered);
        });

        citySearchInput.addEventListener('click', (e) => {
            e.stopPropagation();
        });

        cityListContainer.addEventListener('click', (e) => {
            const item = e.target.closest('.city-dropdown-item');
            if (item) {
                const city = item.getAttribute('data-city');
                if (city) selectCity(city);
            }
        });

        document.addEventListener('click', (e) => {
            if (!citySelector.contains(e.target)) {
                closeDropdown();
            }
        });

        refreshBtn.addEventListener('click', async (e) => {
            e.preventDefault();
            await loadWeather(true);
        });

        async function initialize() {
            try {
                const savedCity = await host.state.read(STATE_CITY_KEY, DEFAULT_CITY);
                if (savedCity && typeof savedCity === 'string') {
                    currentCity = savedCity;
                }
                cityTextEl.textContent = currentCity;
            } catch (e) {
                currentCity = DEFAULT_CITY;
                cityTextEl.textContent = currentCity;
            }

            await loadWeather(false);
            startAutoRefresh();
        }

        function startAutoRefresh() {
            stopAutoRefresh();
            refreshTimer = setInterval(() => {
                loadWeather(true);
            }, REFRESH_INTERVAL_MS);
        }

        function stopAutoRefresh() {
            if (refreshTimer) {
                clearInterval(refreshTimer);
                refreshTimer = null;
            }
        }

        if (document.readyState === 'loading') {
            document.addEventListener('DOMContentLoaded', initialize);
        } else {
            initialize();
        }

        window.addEventListener('beforeunload', () => {
            stopAutoRefresh();
        });
    })();
</script>
</body>
</html>


回复内容
暂无回复
回复主贴