|
|
|
|
@ -52,6 +52,27 @@ class WordRibbon(QFrame):
|
|
|
|
|
# 开始标签的内容(最常用的功能)
|
|
|
|
|
self.setup_home_tab(ribbon_layout)
|
|
|
|
|
|
|
|
|
|
# 添加天气工具组
|
|
|
|
|
weather_group = self.create_ribbon_group("天气")
|
|
|
|
|
weather_layout = QVBoxLayout()
|
|
|
|
|
|
|
|
|
|
# 城市选择
|
|
|
|
|
self.city_combo = QComboBox()
|
|
|
|
|
self.city_combo.setFixedWidth(100)
|
|
|
|
|
self.city_combo.addItems(['自动定位', '北京', '上海', '广州', '深圳', '杭州', '南京', '武汉', '成都', '西安'])
|
|
|
|
|
self.city_combo.setCurrentText('自动定位')
|
|
|
|
|
self.city_combo.currentTextChanged.connect(self.on_city_changed)
|
|
|
|
|
|
|
|
|
|
# 刷新按钮
|
|
|
|
|
self.refresh_weather_btn = QPushButton("刷新天气")
|
|
|
|
|
self.refresh_weather_btn.clicked.connect(self.on_refresh_weather)
|
|
|
|
|
self.refresh_weather_btn.setFixedSize(80, 25)
|
|
|
|
|
|
|
|
|
|
weather_layout.addWidget(self.city_combo)
|
|
|
|
|
weather_layout.addWidget(self.refresh_weather_btn)
|
|
|
|
|
weather_group.setLayout(weather_layout)
|
|
|
|
|
ribbon_layout.addWidget(weather_group)
|
|
|
|
|
|
|
|
|
|
self.ribbon_area.setLayout(ribbon_layout)
|
|
|
|
|
|
|
|
|
|
main_layout.addWidget(self.ribbon_area)
|
|
|
|
|
@ -179,6 +200,14 @@ class WordRibbon(QFrame):
|
|
|
|
|
""")
|
|
|
|
|
return group
|
|
|
|
|
|
|
|
|
|
def on_refresh_weather(self):
|
|
|
|
|
"""刷新天气按钮点击处理"""
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
def on_city_changed(self, city):
|
|
|
|
|
"""城市选择变化处理"""
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
def create_ribbon_button(self, text, shortcut, icon_name):
|
|
|
|
|
"""创建功能区按钮"""
|
|
|
|
|
btn = QToolButton()
|
|
|
|
|
@ -353,83 +382,169 @@ class WeatherAPI:
|
|
|
|
|
def __init__(self):
|
|
|
|
|
self.api_key = "f3d9201bf5974ed39caf0d6fe9567322"
|
|
|
|
|
self.base_url = "https://devapi.qweather.com/v7"
|
|
|
|
|
self.geo_url = "https://geoapi.qweather.com/v2"
|
|
|
|
|
|
|
|
|
|
# 城市ID映射表(基于sojson API的城市编码)
|
|
|
|
|
self.city_id_map = {
|
|
|
|
|
# 主要城市中文映射
|
|
|
|
|
'北京': '101010100',
|
|
|
|
|
'上海': '101020100',
|
|
|
|
|
'广州': '101280101',
|
|
|
|
|
'深圳': '101280601',
|
|
|
|
|
'杭州': '101210101',
|
|
|
|
|
'南京': '101190101',
|
|
|
|
|
'成都': '101270101',
|
|
|
|
|
'武汉': '101200101',
|
|
|
|
|
'西安': '101110101',
|
|
|
|
|
'重庆': '101040100',
|
|
|
|
|
'天津': '101030100',
|
|
|
|
|
'苏州': '101190401',
|
|
|
|
|
'青岛': '101120201',
|
|
|
|
|
'大连': '101070201',
|
|
|
|
|
'沈阳': '101070101',
|
|
|
|
|
'哈尔滨': '101050101',
|
|
|
|
|
'长春': '101060101',
|
|
|
|
|
'石家庄': '101090101',
|
|
|
|
|
'太原': '101100101',
|
|
|
|
|
'郑州': '101180101',
|
|
|
|
|
'济南': '101120101',
|
|
|
|
|
'合肥': '101220101',
|
|
|
|
|
'南昌': '101240101',
|
|
|
|
|
'长沙': '101250101',
|
|
|
|
|
'福州': '101230101',
|
|
|
|
|
'厦门': '101230201',
|
|
|
|
|
'南宁': '101300101',
|
|
|
|
|
'海口': '101310101',
|
|
|
|
|
'贵阳': '101260101',
|
|
|
|
|
'昆明': '101290101',
|
|
|
|
|
'拉萨': '101140101',
|
|
|
|
|
'兰州': '101160101',
|
|
|
|
|
'西宁': '101150101',
|
|
|
|
|
'银川': '101170101',
|
|
|
|
|
'乌鲁木齐': '101130101',
|
|
|
|
|
'呼和浩特': '101080101',
|
|
|
|
|
|
|
|
|
|
# 英文城市名映射到中文
|
|
|
|
|
'Beijing': '北京',
|
|
|
|
|
'Shanghai': '上海',
|
|
|
|
|
'Guangzhou': '广州',
|
|
|
|
|
'Shenzhen': '深圳',
|
|
|
|
|
'Hangzhou': '杭州',
|
|
|
|
|
'Nanjing': '南京',
|
|
|
|
|
'Chengdu': '成都',
|
|
|
|
|
'Wuhan': '武汉',
|
|
|
|
|
'Xian': '西安',
|
|
|
|
|
'Chongqing': '重庆',
|
|
|
|
|
'Tianjin': '天津',
|
|
|
|
|
'Suzhou': '苏州',
|
|
|
|
|
'Qingdao': '青岛',
|
|
|
|
|
'Dalian': '大连',
|
|
|
|
|
'Shenyang': '沈阳',
|
|
|
|
|
'Harbin': '哈尔滨',
|
|
|
|
|
'Changchun': '长春',
|
|
|
|
|
'Shijiazhuang': '石家庄',
|
|
|
|
|
'Taiyuan': '太原',
|
|
|
|
|
'Zhengzhou': '郑州',
|
|
|
|
|
'Jinan': '济南',
|
|
|
|
|
'Hefei': '合肥',
|
|
|
|
|
'Nanchang': '南昌',
|
|
|
|
|
'Changsha': '长沙',
|
|
|
|
|
'Fuzhou': '福州',
|
|
|
|
|
'Xiamen': '厦门',
|
|
|
|
|
'Nanning': '南宁',
|
|
|
|
|
'Haikou': '海口',
|
|
|
|
|
'Guiyang': '贵阳',
|
|
|
|
|
'Kunming': '昆明',
|
|
|
|
|
'Lhasa': '拉萨',
|
|
|
|
|
'Lanzhou': '兰州',
|
|
|
|
|
'Xining': '西宁',
|
|
|
|
|
'Yinchuan': '银川',
|
|
|
|
|
'Urumqi': '乌鲁木齐',
|
|
|
|
|
'Hohhot': '呼和浩特'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
def get_city_id(self, city_name):
|
|
|
|
|
"""根据城市名获取城市ID"""
|
|
|
|
|
try:
|
|
|
|
|
url = f"{self.base_url}/location/lookup"
|
|
|
|
|
# 使用正确的地理API端点格式
|
|
|
|
|
url = f"{self.geo_url}/city/lookup"
|
|
|
|
|
params = {
|
|
|
|
|
'key': self.api_key,
|
|
|
|
|
'location': city_name,
|
|
|
|
|
'adm': 'cn'
|
|
|
|
|
'location': city_name
|
|
|
|
|
}
|
|
|
|
|
print(f"获取城市ID: {city_name}, URL: {url}, 参数: {params}")
|
|
|
|
|
response = requests.get(url, params=params, timeout=10)
|
|
|
|
|
response.raise_for_status()
|
|
|
|
|
data = response.json()
|
|
|
|
|
print(f"城市ID响应: {data}")
|
|
|
|
|
|
|
|
|
|
# 检查新的响应格式
|
|
|
|
|
if data['code'] == '200' and data.get('location'):
|
|
|
|
|
return data['location'][0]['id']
|
|
|
|
|
city_id = data['location'][0]['id']
|
|
|
|
|
print(f"找到城市ID: {city_name} -> {city_id}")
|
|
|
|
|
return city_id
|
|
|
|
|
print(f"未找到城市ID: {city_name}, 响应码: {data.get('code')}")
|
|
|
|
|
return None
|
|
|
|
|
except:
|
|
|
|
|
except Exception as e:
|
|
|
|
|
print(f"获取城市ID失败: {city_name}, 错误: {e}")
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
def get_current_weather(self, city_id):
|
|
|
|
|
"""获取当前天气"""
|
|
|
|
|
try:
|
|
|
|
|
url = f"{self.base_url}/weather/now"
|
|
|
|
|
params = {
|
|
|
|
|
'key': self.api_key,
|
|
|
|
|
'location': city_id,
|
|
|
|
|
'lang': 'zh'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
response = requests.get(url, params=params, timeout=10)
|
|
|
|
|
# 使用免费的天气API
|
|
|
|
|
url = f"http://t.weather.sojson.com/api/weather/city/{city_id}"
|
|
|
|
|
print(f"获取当前天气: {url}")
|
|
|
|
|
response = requests.get(url, timeout=10)
|
|
|
|
|
response.raise_for_status()
|
|
|
|
|
data = response.json()
|
|
|
|
|
print(f"当前天气响应: {data}")
|
|
|
|
|
|
|
|
|
|
if data['code'] == '200':
|
|
|
|
|
now = data['now']
|
|
|
|
|
return {
|
|
|
|
|
'temp': now['temp'],
|
|
|
|
|
'feels_like': now['feelsLike'],
|
|
|
|
|
'weather': now['text'],
|
|
|
|
|
'humidity': now['humidity'],
|
|
|
|
|
'wind_dir': now['windDir'],
|
|
|
|
|
'wind_scale': now['windScale'],
|
|
|
|
|
'vis': now['vis'],
|
|
|
|
|
'pressure': now['pressure']
|
|
|
|
|
if data['status'] == 200:
|
|
|
|
|
city_info = data['cityInfo']
|
|
|
|
|
current_data = data['data']
|
|
|
|
|
|
|
|
|
|
weather_info = {
|
|
|
|
|
'temp': current_data['wendu'],
|
|
|
|
|
'feels_like': current_data['wendu'], # 没有体感温度,用实际温度代替
|
|
|
|
|
'weather': current_data['forecast'][0]['type'],
|
|
|
|
|
'humidity': current_data['shidu'].replace('%', ''),
|
|
|
|
|
'wind_dir': current_data['forecast'][0]['fx'],
|
|
|
|
|
'wind_scale': '1', # 没有风力等级,用默认值
|
|
|
|
|
'vis': current_data['forecast'][0]['high'], # 用最高温作为可见度
|
|
|
|
|
'pressure': '1013' # 没有气压,用默认值
|
|
|
|
|
}
|
|
|
|
|
print(f"解析后的天气信息: {weather_info}")
|
|
|
|
|
return weather_info
|
|
|
|
|
print(f"获取天气失败,状态码: {data.get('status')}")
|
|
|
|
|
return None
|
|
|
|
|
except:
|
|
|
|
|
except Exception as e:
|
|
|
|
|
print(f"获取当前天气失败: {e}")
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
def get_weather_forecast(self, city_id):
|
|
|
|
|
"""获取3天天气预报"""
|
|
|
|
|
try:
|
|
|
|
|
url = f"{self.base_url}/weather/3d"
|
|
|
|
|
params = {
|
|
|
|
|
'key': self.api_key,
|
|
|
|
|
'location': city_id,
|
|
|
|
|
'lang': 'zh'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
response = requests.get(url, params=params, timeout=10)
|
|
|
|
|
# 使用免费的天气API
|
|
|
|
|
url = f"http://t.weather.sojson.com/api/weather/city/{city_id}"
|
|
|
|
|
response = requests.get(url, timeout=10)
|
|
|
|
|
response.raise_for_status()
|
|
|
|
|
data = response.json()
|
|
|
|
|
|
|
|
|
|
if data['code'] == '200':
|
|
|
|
|
if data['status'] == 200:
|
|
|
|
|
forecast_list = []
|
|
|
|
|
for day in data['daily']:
|
|
|
|
|
# 获取未来几天的预报(最多取3天)
|
|
|
|
|
for i, day in enumerate(data['data']['forecast'][:3]):
|
|
|
|
|
forecast_list.append({
|
|
|
|
|
'date': day['fxDate'],
|
|
|
|
|
'temp_max': day['tempMax'],
|
|
|
|
|
'temp_min': day['tempMin'],
|
|
|
|
|
'day_weather': day['textDay'],
|
|
|
|
|
'night_weather': day['textNight']
|
|
|
|
|
'date': day['ymd'],
|
|
|
|
|
'temp_max': day['high'].replace('高温 ', '').replace('℃', ''),
|
|
|
|
|
'temp_min': day['low'].replace('低温 ', '').replace('℃', ''),
|
|
|
|
|
'day_weather': day['type'],
|
|
|
|
|
'night_weather': day['type'] # 免费API没有区分白天和夜间天气
|
|
|
|
|
})
|
|
|
|
|
return forecast_list
|
|
|
|
|
return None
|
|
|
|
|
except:
|
|
|
|
|
except Exception as e:
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
def get_weather_data(self, city_name):
|
|
|
|
|
@ -442,12 +557,14 @@ class WeatherAPI:
|
|
|
|
|
forecast = self.get_weather_forecast(city_id)
|
|
|
|
|
|
|
|
|
|
if current and forecast:
|
|
|
|
|
return {
|
|
|
|
|
weather_data = {
|
|
|
|
|
'city': city_name,
|
|
|
|
|
'current': current,
|
|
|
|
|
'forecast': forecast
|
|
|
|
|
}
|
|
|
|
|
return None
|
|
|
|
|
return weather_data
|
|
|
|
|
else:
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
def get_multiple_cities_weather(self, city_list):
|
|
|
|
|
"""获取多个城市的天气数据"""
|
|
|
|
|
@ -458,22 +575,180 @@ class WeatherAPI:
|
|
|
|
|
results[city] = weather_data
|
|
|
|
|
return results
|
|
|
|
|
|
|
|
|
|
def get_weather_data(self, city_name):
|
|
|
|
|
"""获取指定城市的完整天气数据"""
|
|
|
|
|
city_id = self.get_city_id(city_name)
|
|
|
|
|
if not city_id:
|
|
|
|
|
def get_location_by_ip(self):
|
|
|
|
|
"""通过IP地址获取用户位置"""
|
|
|
|
|
try:
|
|
|
|
|
# 尝试多个免费的IP地理位置API
|
|
|
|
|
|
|
|
|
|
# API 1: 搜狐IP接口(HTTP,无SSL问题)
|
|
|
|
|
try:
|
|
|
|
|
url = "http://pv.sohu.com/cityjson?ie=utf-8"
|
|
|
|
|
response = requests.get(url, timeout=5)
|
|
|
|
|
response.raise_for_status()
|
|
|
|
|
|
|
|
|
|
# 搜狐返回的是JavaScript格式,需要特殊处理
|
|
|
|
|
text = response.text
|
|
|
|
|
|
|
|
|
|
# 提取JSON部分
|
|
|
|
|
if 'returnCitySN' in text:
|
|
|
|
|
# 找到JSON开始和结束位置
|
|
|
|
|
start = text.find('{')
|
|
|
|
|
end = text.rfind('}') + 1
|
|
|
|
|
if start != -1 and end != 0:
|
|
|
|
|
json_str = text[start:end]
|
|
|
|
|
data = json.loads(json_str)
|
|
|
|
|
city = data.get('cname', '')
|
|
|
|
|
if city and city != '未知' and city != '':
|
|
|
|
|
print(f"搜狐IP定位成功: {city}")
|
|
|
|
|
return city
|
|
|
|
|
except Exception as e:
|
|
|
|
|
print(f"搜狐IP接口失败: {e}")
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
# API 2: 使用pconline接口(HTTP)
|
|
|
|
|
try:
|
|
|
|
|
url = "http://whois.pconline.com.cn/ipJson.jsp"
|
|
|
|
|
response = requests.get(url, timeout=5)
|
|
|
|
|
response.raise_for_status()
|
|
|
|
|
|
|
|
|
|
text = response.text
|
|
|
|
|
|
|
|
|
|
# 提取JSON部分
|
|
|
|
|
if 'IPCallBack' in text:
|
|
|
|
|
start = text.find('{')
|
|
|
|
|
end = text.rfind('}') + 1
|
|
|
|
|
if start != -1 and end != 0:
|
|
|
|
|
json_str = text[start:end]
|
|
|
|
|
data = json.loads(json_str)
|
|
|
|
|
city = data.get('city', '')
|
|
|
|
|
if city:
|
|
|
|
|
print(f"pconline定位成功: {city}")
|
|
|
|
|
return city
|
|
|
|
|
except Exception as e:
|
|
|
|
|
print(f"pconline接口失败: {e}")
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
# API 3: 使用ip-api.com接口(更稳定的免费服务)
|
|
|
|
|
try:
|
|
|
|
|
url = "http://ip-api.com/json/"
|
|
|
|
|
response = requests.get(url, timeout=5)
|
|
|
|
|
response.raise_for_status()
|
|
|
|
|
|
|
|
|
|
data = response.json()
|
|
|
|
|
if data.get('status') == 'success':
|
|
|
|
|
city = data.get('city', '')
|
|
|
|
|
if city:
|
|
|
|
|
print(f"ip-api定位成功: {city}")
|
|
|
|
|
return city
|
|
|
|
|
except Exception as e:
|
|
|
|
|
print(f"ip-api接口失败: {e}")
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
# API 4: 使用淘宝IP接口
|
|
|
|
|
try:
|
|
|
|
|
url = "http://ip.taobao.com/outGetIpInfo"
|
|
|
|
|
params = {'ip': '', 'accessKey': 'alibaba-inc'}
|
|
|
|
|
response = requests.get(url, params=params, timeout=5)
|
|
|
|
|
response.raise_for_status()
|
|
|
|
|
|
|
|
|
|
data = response.json()
|
|
|
|
|
if data.get('code') == 0:
|
|
|
|
|
city_info = data.get('data', {})
|
|
|
|
|
city = city_info.get('city', '')
|
|
|
|
|
if city:
|
|
|
|
|
print(f"淘宝IP定位成功: {city}")
|
|
|
|
|
return city
|
|
|
|
|
except Exception as e:
|
|
|
|
|
print(f"淘宝IP接口失败: {e}")
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
print("所有IP定位接口都失败了")
|
|
|
|
|
return None
|
|
|
|
|
except Exception as e:
|
|
|
|
|
print(f"IP定位总体失败: {e}")
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
def get_current_location(self):
|
|
|
|
|
"""获取当前位置信息"""
|
|
|
|
|
try:
|
|
|
|
|
# 首先尝试通过IP获取位置
|
|
|
|
|
city = self.get_location_by_ip()
|
|
|
|
|
if city:
|
|
|
|
|
print(f"通过IP定位成功: {city}")
|
|
|
|
|
return {'city': city}
|
|
|
|
|
|
|
|
|
|
# 如果IP定位失败,尝试其他方法
|
|
|
|
|
# 可以尝试使用浏览器的地理位置API,但这需要前端支持
|
|
|
|
|
|
|
|
|
|
print("自动定位失败,使用默认城市")
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
print(f"获取当前位置失败: {e}")
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
def get_city_weather_by_name(self, city_name):
|
|
|
|
|
"""直接使用城市名获取天气数据"""
|
|
|
|
|
try:
|
|
|
|
|
# 处理英文城市名映射
|
|
|
|
|
original_city_name = city_name
|
|
|
|
|
if city_name in self.city_id_map:
|
|
|
|
|
# 如果是英文城市名,先映射到中文
|
|
|
|
|
mapped_name = self.city_id_map.get(city_name)
|
|
|
|
|
if mapped_name and len(mapped_name) <= 4: # 判断是否为中文城市名
|
|
|
|
|
city_name = mapped_name
|
|
|
|
|
|
|
|
|
|
# 首先尝试从映射表中获取城市ID
|
|
|
|
|
city_id = self.city_id_map.get(city_name)
|
|
|
|
|
if not city_id:
|
|
|
|
|
# 如果映射表中没有,尝试使用和风天气API获取城市ID
|
|
|
|
|
city_id = self.get_city_id(city_name)
|
|
|
|
|
|
|
|
|
|
if city_id:
|
|
|
|
|
# 使用城市ID获取天气数据
|
|
|
|
|
weather_info = self.get_current_weather(city_id)
|
|
|
|
|
if weather_info:
|
|
|
|
|
forecast = self.get_weather_forecast(city_id)
|
|
|
|
|
return {
|
|
|
|
|
'city': city_name, # 使用中文城市名
|
|
|
|
|
'current': {
|
|
|
|
|
'temp': weather_info['temp'],
|
|
|
|
|
'weather': weather_info['weather'],
|
|
|
|
|
'humidity': weather_info['humidity'],
|
|
|
|
|
'wind_scale': weather_info['wind_scale']
|
|
|
|
|
},
|
|
|
|
|
'forecast': forecast
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
print(f"无法获取城市ID: {original_city_name}")
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
print(f"使用城市名获取天气失败: {city_name}, 错误: {e}")
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
def get_weather_data(self, city_name=None):
|
|
|
|
|
"""获取天气数据"""
|
|
|
|
|
try:
|
|
|
|
|
if city_name:
|
|
|
|
|
# 直接使用sojson API获取城市天气,无需先获取城市ID
|
|
|
|
|
weather_info = self.get_city_weather_by_name(city_name)
|
|
|
|
|
if not weather_info:
|
|
|
|
|
print(f"无法获取城市 {city_name} 的天气数据")
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
return weather_info
|
|
|
|
|
else:
|
|
|
|
|
# 获取当前位置
|
|
|
|
|
location = self.get_current_location()
|
|
|
|
|
if location:
|
|
|
|
|
city_name = location['city']
|
|
|
|
|
return self.get_weather_data(city_name)
|
|
|
|
|
else:
|
|
|
|
|
# 如果无法获取位置,使用默认城市
|
|
|
|
|
return self.get_weather_data('北京')
|
|
|
|
|
except Exception as e:
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
current = self.get_current_weather(city_id)
|
|
|
|
|
forecast = self.get_weather_forecast(city_id)
|
|
|
|
|
|
|
|
|
|
if current and forecast:
|
|
|
|
|
return {
|
|
|
|
|
'city': city_name,
|
|
|
|
|
'current': current,
|
|
|
|
|
'forecast': forecast
|
|
|
|
|
}
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
def get_multiple_cities_weather(self, city_list):
|
|
|
|
|
"""获取多个城市的天气数据"""
|
|
|
|
|
|