- main.go: 入口+单实例互斥锁 - win32.go: Win32 API/embed/evalJS/全局变量 - config.go: 配置读写+图标生成 - weather.go: 天气API/定位/天气循环 - systray.go: 托盘菜单/WebView/全屏监控 - wallpaper.html → web/wallpaper.html
307 lines
7.1 KiB
Go
307 lines
7.1 KiB
Go
package main
|
||
|
||
import (
|
||
"encoding/json"
|
||
"fmt"
|
||
"io"
|
||
"log"
|
||
"net/http"
|
||
"regexp"
|
||
"strings"
|
||
"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", "成都", "成都", "四川"},
|
||
}
|
||
|
||
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 := locateByQWeather(); 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 locateByQWeather() *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"`
|
||
}
|
||
|
||
func fetchAndPushWeather(city City) {
|
||
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)
|
||
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 ¤tWeather{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)
|
||
}
|
||
}
|