Files
u-desktop/weather.go
绝尘 9fd3acede3 新增: 星座运势+AI资讯+知识卡片+桌面设置窗口+秒显示开关
- 星座运势: 天聚数行API集成,5维进度条+幸运标签+今日概述
- AI资讯: 天聚数行API,图文布局5条展示,文件缓存2小时刷新
- 知识卡片: AI生成,关键字+提示词配置,30分钟刷新
- 桌面设置: 独立WebView2窗口,760x1350,含壁纸/布局/城市/颜色等配置
- 显示控制: 壁纸/时间/天气/星座/知识/AI资讯独立开关,秒显示开关
- 文件缓存: 星座运势+AI资讯缓存到本地,启动即显示上次数据
- initDone防抖: 防止设置窗口初始化触发卡片重载
2026-05-26 04:34:00 +08:00

340 lines
8.1 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package main
import (
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"regexp"
"strings"
"sync"
"time"
)
const qweatherKey = "3b67b65a53c04170b602d2d1a7e6096f"
const qweatherHost = "https://pb4nmv4qnu.re.qweatherapi.com"
type City struct {
ID string `json:"id"`
Name string `json:"name"`
Adm2 string `json:"adm2"`
Adm1 string `json:"adm1"`
}
var cities = []City{
{"101010100", "北京", "北京", "北京"},
{"101020100", "上海", "上海", "上海"},
{"101280101", "广州", "广州", "广东"},
{"101280601", "深圳", "深圳", "广东"},
{"101200101", "武汉", "武汉", "湖北"},
{"101110101", "西安", "西安", "陕西"},
{"101210101", "杭州", "杭州", "浙江"},
{"101190101", "南京", "南京", "江苏"},
{"101070101", "沈阳", "沈阳", "辽宁"},
{"101050101", "哈尔滨", "哈尔滨", "黑龙江"},
{"101250101", "长沙", "长沙", "湖南"},
{"101270101", "成都", "成都", "四川"},
{"101090101", "石家庄", "石家庄", "河北"},
{"101090206", "任丘", "任丘", "河北"},
{"101090301", "邯郸", "邯郸", "河北"},
{"101290106", "宣威", "宣威", "云南"},
{"101290101", "昆明", "昆明", "云南"},
{"101260101", "贵阳", "贵阳", "贵州"},
}
var defaultCity = City{"101200101", "武汉", "武汉", "湖北"}
var httpClient = &http.Client{Timeout: 10 * time.Second}
var weatherIcons = map[string]string{
"晴": "☀️", "多云": "⛅", "阴": "☁️",
"雨": "🌧️", "雪": "❄️", "雷": "⛈️",
"雾": "🌫️", "霾": "😷", "风": "💨",
}
func getWeatherIcon(text string) string {
for k, v := range weatherIcons {
if strings.Contains(text, k) {
return v
}
}
return "🌤️"
}
func httpGet(url string) ([]byte, error) {
resp, err := httpClient.Get(url)
if err != nil {
return nil, err
}
defer resp.Body.Close()
return io.ReadAll(resp.Body)
}
// ── 定位 ──
func getLocation() City {
if city := locateByIPIP(); city != nil {
return *city
}
if city := locateByGeoAPI(); city != nil {
return *city
}
log.Println("所有定位失败,使用默认城市:", defaultCity.Name)
return defaultCity
}
func locateByIPIP() *City {
data, err := httpGet("https://myip.ipip.net")
if err != nil {
return nil
}
text := string(data)
re := regexp.MustCompile(`来自于[:]\s*(.+?)\s*$`)
m := re.FindStringSubmatch(text)
if m == nil {
return nil
}
parts := strings.Fields(m[1])
cityName := parts[0]
if len(parts) >= 3 {
cityName = parts[2]
} else if len(parts) >= 2 {
cityName = parts[1]
}
for _, c := range cities {
if c.Name == cityName {
log.Println("ipip.net 匹配:", c.Name)
return &c
}
}
return nil
}
func locateByGeoAPI() *City {
data, err := httpGet("https://myip.ipip.net")
if err == nil {
re := regexp.MustCompile(`(\d+\.\d+\.\d+\.\d+)`)
if m := re.FindStringSubmatch(string(data)); m != nil {
return geoLookup(m[1])
}
}
return nil
}
func geoLookup(ip string) *City {
url := fmt.Sprintf(qweatherHost+"/v2/city/lookup?location=%s&key=%s", ip, qweatherKey)
data, err := httpGet(url)
if err != nil {
return nil
}
var resp struct {
Code string `json:"code"`
Location []struct {
ID string `json:"id"`
Name string `json:"name"`
Adm2 string `json:"adm2"`
Adm1 string `json:"adm1"`
} `json:"location"`
}
if json.Unmarshal(data, &resp) != nil || resp.Code != "200" || len(resp.Location) == 0 {
return nil
}
loc := resp.Location[0]
for _, c := range cities {
if c.Name == loc.Name {
return &c
}
}
return &City{loc.ID, loc.Name, loc.Adm2, loc.Adm1}
}
func getCurrentCity() City {
cfg := loadConfig()
if cfg.City != "" {
for _, c := range cities {
if c.ID == cfg.City {
return c
}
}
}
return getLocation()
}
// ── 天气数据 ──
type hourlyItem struct {
Time string `json:"time"`
Temp string `json:"temp"`
Icon string `json:"icon"`
}
type dailyItem struct {
Date string `json:"date"`
Icon string `json:"icon"`
TempMin string `json:"tempMin"`
TempMax string `json:"tempMax"`
}
type currentWeather struct {
Text string `json:"text"`
Temp string `json:"temp"`
}
var (
weatherCacheMu sync.Mutex
weatherCache map[string]time.Time
weatherCacheData map[string]string
)
func init() {
weatherCache = make(map[string]time.Time)
weatherCacheData = make(map[string]string)
}
func fetchAndPushWeather(city City) {
// 5 分钟内同一城市不重复请求
weatherCacheMu.Lock()
if last, ok := weatherCache[city.ID]; ok && time.Since(last) < 5*time.Minute {
if cached, ok := weatherCacheData[city.ID]; ok {
weatherCacheMu.Unlock()
evalJS(fmt.Sprintf(`if(window.updateWeatherFromGo) window.updateWeatherFromGo(%s)`, cached))
return
}
}
weatherCacheMu.Unlock()
type weatherData struct {
Current string `json:"current"`
Hourly []hourlyItem `json:"hourly"`
Daily []dailyItem `json:"daily"`
City string `json:"city"`
}
wd := weatherData{}
cityName := city.Name
if city.Adm2 != city.Name {
cityName = city.Adm2 + " " + city.Name
}
wd.City = cityName
if now := fetchCurrentWeather(city.ID); now != nil {
icon := getWeatherIcon(now.Text)
wd.Current = fmt.Sprintf("📍 %s %s %s %s°C", cityName, icon, now.Text, now.Temp)
} else {
wd.Current = fmt.Sprintf("📍 %s ☀️ 晴 24°C", cityName)
}
wd.Hourly = fetchHourlyForecast(city.ID)
wd.Daily = fetchDailyForecast(city.ID)
jsonData, _ := json.Marshal(wd)
weatherCacheMu.Lock()
weatherCache[city.ID] = time.Now()
weatherCacheData[city.ID] = string(jsonData)
weatherCacheMu.Unlock()
evalJS(fmt.Sprintf(`if(window.updateWeatherFromGo) window.updateWeatherFromGo(%s)`, string(jsonData)))
log.Println("天气已推送:", wd.Current)
}
func fetchCurrentWeather(cityID string) *currentWeather {
url := fmt.Sprintf(qweatherHost+"/v7/weather/now?location=%s&key=%s", cityID, qweatherKey)
data, err := httpGet(url)
if err != nil {
return nil
}
var resp struct {
Code string `json:"code"`
Now struct {
Text string `json:"text"`
Temp string `json:"temp"`
} `json:"now"`
}
if json.Unmarshal(data, &resp) != nil || resp.Code != "200" {
return nil
}
return &currentWeather{resp.Now.Text, resp.Now.Temp}
}
func fetchHourlyForecast(cityID string) []hourlyItem {
url := fmt.Sprintf(qweatherHost+"/v7/weather/24h?location=%s&key=%s", cityID, qweatherKey)
data, err := httpGet(url)
if err != nil {
return nil
}
var resp struct {
Code string `json:"code"`
Hourly []struct {
FxTime string `json:"fxTime"`
Temp string `json:"temp"`
Text string `json:"text"`
} `json:"hourly"`
}
if json.Unmarshal(data, &resp) != nil || resp.Code != "200" {
return nil
}
var result []hourlyItem
for i, h := range resp.Hourly {
if i >= 8 {
break
}
t := h.FxTime
if idx := strings.Index(t, "T"); idx >= 0 {
t = t[idx+1:]
if len(t) >= 5 {
t = t[:5]
}
}
result = append(result, hourlyItem{t, h.Temp + "°", getWeatherIcon(h.Text)})
}
return result
}
func fetchDailyForecast(cityID string) []dailyItem {
url := fmt.Sprintf(qweatherHost+"/v7/weather/7d?location=%s&key=%s", cityID, qweatherKey)
data, err := httpGet(url)
if err != nil {
return nil
}
var resp struct {
Code string `json:"code"`
Daily []struct {
FxDate string `json:"fxDate"`
TextDay string `json:"textDay"`
TempMin string `json:"tempMin"`
TempMax string `json:"tempMax"`
} `json:"daily"`
}
if json.Unmarshal(data, &resp) != nil || resp.Code != "200" {
return nil
}
weekdays := []string{"周日", "周一", "周二", "周三", "周四", "周五", "周六"}
today := time.Now().Format("2006-01-02")
var result []dailyItem
for _, d := range resp.Daily {
date := d.FxDate
if date == today {
date = "今天"
} else {
if t, err := time.Parse("2006-01-02", date); err == nil {
date = weekdays[t.Weekday()]
}
}
result = append(result, dailyItem{date, getWeatherIcon(d.TextDay), d.TempMin, d.TempMax})
}
return result
}
func weatherLoop() {
time.Sleep(3 * time.Second)
city := getCurrentCity()
log.Println("初始城市:", city.Name)
fetchAndPushWeather(city)
ticker := time.NewTicker(10 * time.Minute)
for range ticker.C {
city := getCurrentCity()
fetchAndPushWeather(city)
}
}