天气

天气.可输入经纬度获取天气信息

适配设备:Pro

作者:rikka

下载量:41 · 点赞:3

可信源码风险提示:保存或下载前请阅读源码和权限声明。
SHA-256f81d3ac93b62d7f24cc3d7a1748c344dd0bb8c9ab4048783d73fecb690c2e245
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);
})();