Created
April 10, 2026 09:35
-
-
Save rxliuli/77ddc47e55b2d1b00b195f73acbc1b3a to your computer and use it in GitHub Desktop.
Free weather widget for iOS Scriptable — powered by Open-Meteo, no API key, no subscription, no location access needed. Just set your city name.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ===== 配置区:只需要改城市名 ===== | |
| const CITY_NAME = "杭州" | |
| // ================================ | |
| const geoReq = new Request( | |
| `https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(CITY_NAME)}&count=1&language=zh` | |
| ) | |
| const geoData = await geoReq.loadJSON() | |
| const LATITUDE = geoData.results[0].latitude | |
| const LONGITUDE = geoData.results[0].longitude | |
| const TIMEZONE = geoData.results[0].timezone | |
| // Open-Meteo 免费 API,无需 key | |
| const url = `https://api.open-meteo.com/v1/forecast` | |
| + `?latitude=${LATITUDE}&longitude=${LONGITUDE}` | |
| + `¤t=temperature_2m,weather_code,apparent_temperature` | |
| + `&hourly=temperature_2m,weather_code` | |
| + `&daily=temperature_2m_max,temperature_2m_min` | |
| + `&timezone=${encodeURIComponent(TIMEZONE)}` | |
| + `&forecast_days=2` | |
| const req = new Request(url) | |
| const data = await req.loadJSON() | |
| const current = data.current | |
| const daily = data.daily | |
| const hourly = data.hourly | |
| // WMO weather code -> 中文描述 + SF Symbol | |
| function decodeWeather(code) { | |
| const map = { | |
| 0: ["晴", "sun.max.fill"], | |
| 1: ["大致晴朗", "sun.max.fill"], | |
| 2: ["局部多云", "cloud.sun.fill"], | |
| 3: ["阴", "cloud.fill"], | |
| 45: ["有雾", "cloud.fog.fill"], | |
| 48: ["雾凇", "cloud.fog.fill"], | |
| 51: ["小毛毛雨", "cloud.drizzle.fill"], | |
| 53: ["毛毛雨", "cloud.drizzle.fill"], | |
| 55: ["大毛毛雨", "cloud.drizzle.fill"], | |
| 61: ["小雨", "cloud.rain.fill"], | |
| 63: ["中雨", "cloud.rain.fill"], | |
| 65: ["大雨", "cloud.heavyrain.fill"], | |
| 71: ["小雪", "cloud.snow.fill"], | |
| 73: ["中雪", "cloud.snow.fill"], | |
| 75: ["大雪", "cloud.snow.fill"], | |
| 80: ["阵雨", "cloud.rain.fill"], | |
| 81: ["强阵雨", "cloud.heavyrain.fill"], | |
| 82: ["暴雨", "cloud.heavyrain.fill"], | |
| 95: ["雷暴", "cloud.bolt.rain.fill"], | |
| 96: ["雷暴伴冰雹","cloud.bolt.rain.fill"], | |
| 99: ["强雷暴", "cloud.bolt.rain.fill"], | |
| } | |
| return map[code] || ["未知", "questionmark.circle"] | |
| } | |
| // 用本地时间匹配 Open-Meteo 返回的本地时间 | |
| const now = new Date() | |
| const pad = n => String(n).padStart(2, "0") | |
| const localPrefix = `${now.getFullYear()}-${pad(now.getMonth()+1)}-${pad(now.getDate())}T${pad(now.getHours())}` | |
| let startIdx = hourly.time.findIndex(t => t.startsWith(localPrefix)) | |
| if (startIdx < 0) startIdx = 0 | |
| // ===== 构建小组件 ===== | |
| const widget = new ListWidget() | |
| widget.backgroundGradient = (() => { | |
| const g = new LinearGradient() | |
| g.colors = [new Color("#1e3c72"), new Color("#2a5298")] | |
| g.locations = [0, 1] | |
| return g | |
| })() | |
| widget.setPadding(14, 16, 14, 16) | |
| // --- 顶部:城市 + 当前温度 + 状况 --- | |
| const topStack = widget.addStack() | |
| topStack.layoutHorizontally() | |
| topStack.centerAlignContent() | |
| const leftStack = topStack.addStack() | |
| leftStack.layoutVertically() | |
| const cityText = leftStack.addText(CITY_NAME) | |
| cityText.font = Font.mediumSystemFont(14) | |
| cityText.textColor = Color.white() | |
| const tempText = leftStack.addText(`${Math.round(current.temperature_2m)}°`) | |
| tempText.font = Font.lightSystemFont(42) | |
| tempText.textColor = Color.white() | |
| const [desc, symbolName] = decodeWeather(current.weather_code) | |
| const descText = leftStack.addText(desc) | |
| descText.font = Font.systemFont(12) | |
| descText.textColor = new Color("#ffffff", 0.85) | |
| topStack.addSpacer() | |
| const rightStack = topStack.addStack() | |
| rightStack.layoutVertically() | |
| const symbol = SFSymbol.named(symbolName) | |
| symbol.applyFont(Font.systemFont(36)) | |
| const symbolImg = rightStack.addImage(symbol.image) | |
| symbolImg.imageSize = new Size(44, 44) | |
| symbolImg.tintColor = Color.white() | |
| rightStack.addSpacer(4) | |
| const hiLo = rightStack.addText( | |
| `H:${Math.round(daily.temperature_2m_max[0])}° L:${Math.round(daily.temperature_2m_min[0])}°` | |
| ) | |
| hiLo.font = Font.systemFont(11) | |
| hiLo.textColor = new Color("#ffffff", 0.85) | |
| hiLo.rightAlignText() | |
| widget.addSpacer(10) | |
| // --- 底部:未来 6 小时逐时预报 --- | |
| const hourlyStack = widget.addStack() | |
| hourlyStack.layoutHorizontally() | |
| hourlyStack.spacing = 4 | |
| for (let i = 0; i < 6; i++) { | |
| const idx = startIdx + i | |
| if (idx >= hourly.time.length) break | |
| const hStack = hourlyStack.addStack() | |
| hStack.layoutVertically() | |
| hStack.centerAlignContent() | |
| const rawHour = parseInt(hourly.time[idx].slice(11, 13), 10) | |
| const hourLabel = i === 0 ? "现在" : rawHour + "时" | |
| const hText = hStack.addText(hourLabel) | |
| hText.font = Font.systemFont(10) | |
| hText.textColor = new Color("#ffffff", 0.8) | |
| hText.centerAlignText() | |
| hStack.addSpacer(2) | |
| const [, hSymName] = decodeWeather(hourly.weather_code[idx]) | |
| const hSym = SFSymbol.named(hSymName) | |
| hSym.applyFont(Font.systemFont(16)) | |
| const hImg = hStack.addImage(hSym.image) | |
| hImg.imageSize = new Size(20, 20) | |
| hImg.tintColor = Color.white() | |
| hStack.addSpacer(2) | |
| const hTemp = hStack.addText(`${Math.round(hourly.temperature_2m[idx])}°`) | |
| hTemp.font = Font.mediumSystemFont(12) | |
| hTemp.textColor = Color.white() | |
| hTemp.centerAlignText() | |
| if (i < 5) hourlyStack.addSpacer() | |
| } | |
| // 刷新策略:30 分钟后再请求一次 | |
| widget.refreshAfterDate = new Date(Date.now() + 30 * 60 * 1000) | |
| if (config.runsInWidget) { | |
| Script.setWidget(widget) | |
| } else { | |
| widget.presentMedium() | |
| } | |
| Script.complete() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment