天气,不需要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>