可信源码风险提示:保存或下载前请阅读源码和权限声明。
const CACHE_KEY = 'weather.cache';
const PLACE_KEY = 'weather.place';
const TTL_MS = 15 * 60 * 1000;
const RETRIES = 2;
const CODES = {
0: '晴朗', 1: '大部晴朗', 2: '局部多云', 3: '阴天',
45: '雾', 48: '霜雾', 51: '小雨毛', 53: '雨毛', 55: '大雨毛',
61: '小雨', 63: '中雨', 65: '大雨', 71: '小雪', 73: '中雪',
75: '大雪', 80: '阵雨', 81: '强阵雨', 82: '猛烈阵雨',
95: '雷暴', 96: '雷暴伴冰雹', 99: '强雷暴伴冰雹',
};
const wmo = c => CODES[c] || `天气编码 ${c}`;
const tick = ui.signal(0);
const redraw = () => tick.update(n => n + 1);
const alertBox = msg => dialog.alert({ title: '天气', message: String(msg) });
const state = { place: null, data: null, status: '启动中', fromCache: false, busy: false };
const canFetch = () =>
typeof script.canUse === 'function' &&
(script.canUse('@system.fetch.fetch') || script.canUse('@system.fetch'));
function request(opts) {
return new Promise((resolve, reject) => {
system.http.request(Object.assign({ method: 'GET', responseType: 'json' }, opts, {
success: r => (r.code >= 200 && r.code < 300 ? resolve(r) : reject({ code: r.code, msg: `HTTP ${r.code}` })),
fail: (msg, code) => reject({ code, msg: String(msg) }),
}));
});
}
const sleep = ms => new Promise(r => setTimeout(r, ms));
async function withRetry(fn, tries) {
let last;
for (let i = 0; i <= tries; i++) {
try { return await fn(); } catch (e) {
last = e;
state.status = `重试 ${i + 1}/${tries}...`;
redraw();
await sleep(800 * (i + 1));
}
}
throw last;
}
function buildUrl(p) {
const q = [
'latitude=' + p.lat, 'longitude=' + p.lon,
'current=temperature_2m,apparent_temperature,wind_speed_10m,weather_code',
'daily=weather_code,temperature_2m_max,temperature_2m_min,precipitation_probability_max',
'forecast_days=3', 'timezone=auto',
];
return 'https://api.open-meteo.com/v1/forecast?' + q.join('&');
}
async function readCache() {
try { return JSON.parse(await script.data.get(CACHE_KEY, 'null')); } catch (e) { return null; }
}
async function writeCache(obj) { await script.data.set(CACHE_KEY, JSON.stringify(obj)); }
async function refresh(force) {
if (state.busy || !state.place) return;
state.busy = true;
const cache = await readCache();
const fresh = cache && cache.key === state.place.lat + ',' + state.place.lon &&
Date.now() - cache.at < TTL_MS;
if (fresh && !force) {
state.data = cache.data; state.fromCache = true;
state.status = `缓存 ${Math.round((Date.now() - cache.at) / 60000)} 分钟前`;
state.busy = false; return redraw();
}
if (!canFetch()) {
state.status = '手环不支持网络请求';
if (cache) { state.data = cache.data; state.fromCache = true; state.status += ' (读取缓存)'; }
state.busy = false; return redraw();
}
state.status = '正在加载...'; redraw();
try {
const res = await withRetry(() => request({ url: buildUrl(state.place) }), RETRIES);
state.data = res.data; state.fromCache = false;
state.status = '刚刚更新';
await writeCache({ at: Date.now(), key: state.place.lat + ',' + state.place.lon, data: res.data });
system.vibration.vibrate({ mode: 'short' });
} catch (e) {
state.status = '获取失败:' + (e.msg || e.code);
if (cache) { state.data = cache.data; state.fromCache = true; state.status += ' (读取缓存)'; }
}
state.busy = false;
redraw();
}
const PRESETS = {
'纽约': [40.71, -74.01], '伦敦': [51.51, -0.13], '东京': [35.68, 139.69],
'悉尼': [-33.87, 151.21], '北京': [39.9, 116.4], '自定义经纬度...': null,
};
async function pickPlace() {
const r = await dialog.select({
title: '选择位置',
items: Object.keys(PRESETS).map(k => ({ label: k, value: k })),
});
if (!r || r.action === 'cancel') return;
const name = Array.isArray(r.value) ? r.value[0] : r.value;
let lat, lon;
if (PRESETS[name]) {
[lat, lon] = PRESETS[name];
} else {
const a = await dialog.number({ title: '纬度', value: 0, min: -90, max: 90, decimals: 2 });
if (!a || a.action === 'cancel') return;
const b = await dialog.number({ title: '经度', value: 0, min: -180, max: 180, decimals: 2 });
if (!b || b.action === 'cancel') return;
lat = a.value; lon = b.value;
}
state.place = { name, lat, lon };
await script.data.set(PLACE_KEY, JSON.stringify(state.place));
await refresh(true);
}
const r1 = n => (Math.round(n * 10) / 10).toFixed(1);
function screen() {
tick.get();
const nodes = [ui.heading(state.place ? state.place.name : '未设置位置', { id: 'place' })];
const d = state.data;
if (d && d.current) {
const c = d.current;
nodes.push(ui.text(`${r1(c.temperature_2m)}° ${wmo(c.weather_code)}`, { id: 'now', lines: 1 }));
nodes.push(ui.text(`体感 ${r1(c.apparent_temperature)}° 风速 ${r1(c.wind_speed_10m)}`, { id: 'feels', lines: 1 }));
const dl = d.daily;
for (let i = 0; i < dl.time.length; i++) {
nodes.push(ui.text(
`${dl.time[i].slice(5)} ${r1(dl.temperature_2m_min[i])}/${r1(dl.temperature_2m_max[i])}° ` +
`${dl.precipitation_probability_max[i] ?? 0}% ${wmo(dl.weather_code[i])}`,
{ id: 'day' + i, lines: 1 }));
}
}
nodes.push(ui.text(state.status, { id: 'status', lines: 1 }));
nodes.push(ui.button('刷新天气', () => refresh(true), { id: 'refresh', tone: 'primary', disabled: state.busy }));
nodes.push(ui.button('更换地点', pickPlace, { id: 'place-btn' }));
return nodes;
}
(async function main() {
ui.setTitle('天气');
ui.render(screen);
try { state.place = JSON.parse(await script.data.get(PLACE_KEY, 'null')); } catch (e) {}
if (!state.place) return pickPlace();
await refresh(false);
})();