Ultima attività 4 days ago

一款功能丰富的天气显示插件,集成和风天气API和高德地图定位服务,提供实时天气信息、24小时预报、7天预报,并内置AI生活建议功能,为您的日常出行和生活规划提供智能化参考

wojack's Avatar wojack ha revisionato questo gist 4 days ago. Vai alla revisione

2 files changed, 13 insertions, 12 deletions

详细文档 (file eliminato)

@@ -1,12 +0,0 @@
1 - // 智能天气插件文档:https://www.yuque.com/shanshanerchuan-leyrb/wsa1gw/fiple6ihym1gy4b8
2 - 1. 获取API密钥
3 - ○ 注册和风天气开发者账号获取API Key
4 - ○ 注册高德地图开放平台获取API Key
5 - ○ (可选)配置AI服务API(采用open AI标准,暂未测试过其他API)
6 - 2. 修改配置
7 - ○ 将上述配置变量替换为您自己的API密钥
8 - ○ 根据个人情况修改USER_PROFILE信息
9 - 3. 集成到项目
10 - ○ 将完整JS代码添加到Sun-Panel项目中
11 - ○ (具体添加方法请参考作者“红烧猎人”的“sun-panel-js-plugins”项目)
12 - ○ 确保网络环境可访问相关API服务

详细文档.md(file creato)

@@ -0,0 +1,13 @@
1 + 智能天气插件文档:https://www.yuque.com/shanshanerchuan-leyrb/wsa1gw/fiple6ihym1gy4b8
2 +
3 + ## 1. 获取API密钥
4 + * 注册和风天气开发者账号获取API Key
5 + * 注册高德地图开放平台获取API Key
6 + * (可选)配置AI服务API(采用open AI标准,暂未测试过其他API)
7 + ## 2. 修改配置
8 + * 将上述配置变量替换为您自己的API密钥
9 + * 根据个人情况修改USER_PROFILE信息
10 + ## 3. 集成到项目
11 + * 将完整JS代码添加到Sun-Panel项目中
12 + * (具体添加方法请参考作者“红烧猎人”的“sun-panel-js-plugins”项目)
13 + * 确保网络环境可访问相关API服务

wojack's Avatar wojack ha revisionato questo gist 4 days ago. Vai alla revisione

1 file changed, 1 insertion, 2 deletions

详细文档

@@ -1,5 +1,4 @@
1 - # 智能天气插件文档:https://www.yuque.com/shanshanerchuan-leyrb/wsa1gw/fiple6ihym1gy4b8
2 -
1 + // 智能天气插件文档:https://www.yuque.com/shanshanerchuan-leyrb/wsa1gw/fiple6ihym1gy4b8
3 2 1. 获取API密钥
4 3 ○ 注册和风天气开发者账号获取API Key
5 4 ○ 注册高德地图开放平台获取API Key

wojack's Avatar wojack ha revisionato questo gist 4 days ago. Vai alla revisione

2 files changed, 33 insertions

详细文档(file creato)

@@ -0,0 +1,13 @@
1 + # 智能天气插件文档:https://www.yuque.com/shanshanerchuan-leyrb/wsa1gw/fiple6ihym1gy4b8
2 +
3 + 1. 获取API密钥
4 + ○ 注册和风天气开发者账号获取API Key
5 + ○ 注册高德地图开放平台获取API Key
6 + ○ (可选)配置AI服务API(采用open AI标准,暂未测试过其他API)
7 + 2. 修改配置
8 + ○ 将上述配置变量替换为您自己的API密钥
9 + ○ 根据个人情况修改USER_PROFILE信息
10 + 3. 集成到项目
11 + ○ 将完整JS代码添加到Sun-Panel项目中
12 + ○ (具体添加方法请参考作者“红烧猎人”的“sun-panel-js-plugins”项目)
13 + ○ 确保网络环境可访问相关API服务

配置说明.js(file creato)

@@ -0,0 +1,20 @@
1 + // 和风天气配置(每月限额免费)地址:“https://id.qweather.com/”
2 + const QWEATHER_API_KEY = "XXXXXXXXXX"; // 请替换为您的和风天气API Key
3 + const QWEATHER_API_HOST = "XXXXXXXXXX"; // 请替换为您的和风天气API Host
4 +
5 + // 高德地图配置(高德开放平台获取个人版本,有限额度免费)地址“https://lbs.amap.com/”
6 + const AMAP_API_KEY = "XXXXXXXXXX"; // 请替换为您的高德地图API Key
7 +
8 + // AI助手配置(建议使用魔搭社区,每日限额免费)地址:“https://www.modelscope.cn”
9 + const OPENAI_API_KEY = "XXXXXXXXXX"; // 请替换为您的AI API Key
10 + const OPENAI_MODEL = "XXXXXXXXXX"; // 请替换为您的AI模型名称,例如"Qwen/Qwen3-VL-235B-A22B-Instruct"
11 + const OPENAI_BASE_URL = "XXXXXXXXXX"; // 请替换为您的AI API地址,例如“https://api-inference.modelscope.cn/v1”
12 +
13 + // 个人信息配置 - 用于AI生活建议定制
14 + const USER_PROFILE = {
15 + age: 26, // 您的年龄
16 + gender: "男", // 您的性别
17 + commuteDays: "周一至周六", // 通勤需求
18 + commuteMethod: "地铁或公交", // 通勤方式
19 + coreNeeds: "舒适生活、保持健康、便利出行、优化安排" // 核心需求
20 + };

wojack's Avatar wojack ha revisionato questo gist 4 days ago. Vai alla revisione

1 file changed, 2282 insertions

tq.js(file creato)

@@ -0,0 +1,2282 @@
1 + (function() {
2 + 'use strict';
3 +
4 + // =========== 用户配置区域 - 请根据实际情况修改以下配置 ===========
5 +
6 + // 和风天气配置(每月限额免费)地址:“https://id.qweather.com/”
7 + const QWEATHER_API_KEY = "XXXXXXXXXX"; // 请替换为您的和风天气API Key
8 + const QWEATHER_API_HOST = "XXXXXXXXXX"; // 请替换为您的和风天气API Host
9 +
10 + // 高德地图配置(高德开放平台获取个人版本,有限额度免费)地址“https://lbs.amap.com/”
11 + const AMAP_API_KEY = "XXXXXXXXXX"; // 请替换为您的高德地图API Key
12 +
13 + // AI助手配置(建议使用魔搭社区,每日限额免费)地址:“https://www.modelscope.cn”
14 + const OPENAI_API_KEY = "XXXXXXXXXX"; // 请替换为您的AI API Key
15 + const OPENAI_MODEL = "XXXXXXXXXX"; // 请替换为您的AI模型名称,例如"Qwen/Qwen3-VL-235B-A22B-Instruct"
16 + const OPENAI_BASE_URL = "XXXXXXXXXX"; // 请替换为您的AI API地址,例如“https://api-inference.modelscope.cn/v1”
17 +
18 + // 个人信息配置 - 用于AI生活建议定制
19 + const USER_PROFILE = {
20 + age: 26, // 您的年龄
21 + gender: "男", // 您的性别
22 + commuteDays: "周一至周六", // 通勤需求
23 + commuteMethod: "地铁或公交", // 通勤方式
24 + coreNeeds: "舒适生活、保持健康、便利出行、优化安排" // 核心需求
25 + };
26 +
27 + // 默认位置配置
28 + const DEFAULT_LOCATION = "116.41,39.92"; // 默认经纬度坐标
29 + const DEFAULT_LOCATION_NAME = "北京"; // 默认位置名称
30 +
31 + // =========== 插件配置 - 以下配置一般无需修改 ===========
32 + const weatherConfig = {
33 + apiKey: QWEATHER_API_KEY,
34 + apiHost: QWEATHER_API_HOST,
35 + defaultLocation: DEFAULT_LOCATION,
36 + locationName: DEFAULT_LOCATION_NAME,
37 + mobileWidth: 768,
38 + autoRefresh: true,
39 + refreshInterval: 300000, // 5分钟
40 + showOnPC: true,
41 + showOnMobile: false,
42 + enableAutoLocation: true,
43 + amapKey: AMAP_API_KEY,
44 + openaiApiKey: OPENAI_API_KEY,
45 + openaiModel: OPENAI_MODEL,
46 + openaiBaseUrl: OPENAI_BASE_URL,
47 + userProfile: USER_PROFILE // 新增用户配置
48 + };
49 + // =========== 配置结束 ===========
50 +
51 + // 工具函数
52 + const utils = {
53 + isMobile: () => window.innerWidth <= weatherConfig.mobileWidth,
54 +
55 + debounce: (func, wait) => {
56 + let timeout;
57 + return function executedFunction(...args) {
58 + const later = () => {
59 + clearTimeout(timeout);
60 + func(...args);
61 + };
62 + clearTimeout(timeout);
63 + timeout = setTimeout(later, wait);
64 + };
65 + },
66 +
67 + // 保存设置到本地存储
68 + saveSettings(location, locationName) {
69 + try {
70 + localStorage.setItem('weather_location', location);
71 + localStorage.setItem('weather_location_name', locationName);
72 + localStorage.setItem('weather_auto_location_enabled', 'true');
73 + } catch (e) {
74 + console.log('本地存储不可用');
75 + }
76 + },
77 +
78 + // 从本地存储加载设置
79 + loadSettings() {
80 + try {
81 + const autoLocationEnabled = localStorage.getItem('weather_auto_location_enabled') === 'true';
82 + return {
83 + location: localStorage.getItem('weather_location') || weatherConfig.defaultLocation,
84 + locationName: localStorage.getItem('weather_location_name') || weatherConfig.locationName,
85 + autoLocationEnabled: autoLocationEnabled
86 + };
87 + } catch (e) {
88 + return {
89 + location: weatherConfig.defaultLocation,
90 + locationName: weatherConfig.locationName,
91 + autoLocationEnabled: false
92 + };
93 + }
94 + },
95 +
96 + // 获取星期几
97 + getWeekday(dateStr) {
98 + const date = new Date(dateStr);
99 + const weekdays = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
100 + return weekdays[date.getDay()];
101 + },
102 +
103 + // 格式化时间 - 从和风天气API的时间格式中提取小时
104 + formatTime(fxTime) {
105 + if (!fxTime) return '';
106 + // 和风天气API返回格式: "2024-01-01T12:00+08:00"
107 + const timeMatch = fxTime.match(/T(\d{2}):/);
108 + return timeMatch ? `${timeMatch[1]}:00` : '00:00';
109 + },
110 +
111 + // 生成折线图的SVG路径 - 完全复刻原有逻辑
112 + generateTemperatureLine(points, totalWidth, height, itemWidth) {
113 + if (points.length < 2) return '';
114 +
115 + const allTemps = points.map(item => parseInt(item.temp));
116 + const minTemp = Math.min(...allTemps);
117 + const maxTemp = Math.max(...allTemps);
118 + const range = maxTemp - minTemp;
119 +
120 + let path = '';
121 +
122 + points.forEach((item, index) => {
123 + const temp = parseInt(item.temp);
124 + const heightPercent = range === 0 ? 50 : ((temp - minTemp) / range) * 75 + 17.5;
125 + const x = (index * itemWidth) + (itemWidth / 2);
126 + const y = height - (heightPercent / 100) * (height * 0.8);
127 +
128 + if (index === 0) {
129 + path = `M ${x} ${y}`;
130 + } else {
131 + path += ` L ${x} ${y}`;
132 + }
133 + });
134 +
135 + return path;
136 + },
137 +
138 + // 和风天气图标映射
139 + getWeatherIcon(code) {
140 + const iconMap = {
141 + '100': 'fas fa-sun', // 晴
142 + '101': 'fas fa-cloud-sun', // 多云
143 + '102': 'fas fa-cloud-sun', // 少云
144 + '103': 'fas fa-cloud', // 晴间多云
145 + '104': 'fas fa-cloud', // 阴
146 + '150': 'fas fa-moon', // 晴(夜)
147 + '151': 'fas fa-cloud-moon', // 多云(夜)
148 + '152': 'fas fa-cloud-moon', // 少云(夜)
149 + '153': 'fas fa-cloud', // 晴间多云(夜)
150 + '300': 'fas fa-cloud-rain', // 阵雨
151 + '301': 'fas fa-cloud-rain', // 强阵雨
152 + '302': 'fas fa-bolt', // 雷阵雨
153 + '303': 'fas fa-bolt', // 强雷阵雨
154 + '304': 'fas fa-cloud-showers-heavy', // 雷阵雨伴有冰雹
155 + '305': 'fas fa-cloud-rain', // 小雨
156 + '306': 'fas fa-cloud-rain', // 中雨
157 + '307': 'fas fa-cloud-showers-heavy', // 大雨
158 + '308': 'fas fa-cloud-showers-heavy', // 极端降雨
159 + '309': 'fas fa-cloud-rain', // 毛毛雨
160 + '310': 'fas fa-cloud-rain', // 暴雨
161 + '311': 'fas fa-cloud-showers-heavy', // 大暴雨
162 + '312': 'fas fa-cloud-showers-heavy', // 特大暴雨
163 + '313': 'fas fa-cloud-rain', // 冻雨
164 + '314': 'fas fa-cloud-rain', // 小到中雨
165 + '315': 'fas fa-cloud-rain', // 中到大雨
166 + '316': 'fas fa-cloud-showers-heavy', // 大到暴雨
167 + '317': 'fas fa-cloud-showers-heavy', // 暴雨到大暴雨
168 + '318': 'fas fa-cloud-showers-heavy', // 大暴雨到特大暴雨
169 + '399': 'fas fa-cloud-rain', // 雨
170 + '400': 'fas fa-snowflake', // 小雪
171 + '401': 'fas fa-snowflake', // 中雪
172 + '402': 'fas fa-snowflake', // 大雪
173 + '403': 'fas fa-snowflake', // 暴雪
174 + '404': 'fas fa-cloud-rain', // 雨夹雪
175 + '405': 'fas fa-cloud-rain', // 雨雪天气
176 + '406': 'fas fa-cloud-rain', // 阵雨夹雪
177 + '407': 'fas fa-snowflake', // 阵雪
178 + '408': 'fas fa-snowflake', // 小到中雪
179 + '409': 'fas fa-snowflake', // 中到大雪
180 + '410': 'fas fa-snowflake', // 大到暴雪
181 + '499': 'fas fa-snowflake', // 雪
182 + '500': 'fas fa-smog', // 薄雾
183 + '501': 'fas fa-smog', // 雾
184 + '502': 'fas fa-smog', // 霾
185 + '503': 'fas fa-wind', // 扬沙
186 + '504': 'fas fa-wind', // 浮尘
187 + '507': 'fas fa-wind', // 沙尘暴
188 + '508': 'fas fa-wind', // 强沙尘暴
189 + '509': 'fas fa-smog', // 浓雾
190 + '510': 'fas fa-smog', // 强浓雾
191 + '511': 'fas fa-smog', // 中度霾
192 + '512': 'fas fa-smog', // 重度霾
193 + '513': 'fas fa-smog', // 严重霾
194 + '514': 'fas fa-smog', // 大雾
195 + '515': 'fas fa-smog', // 特强浓雾
196 + '900': 'fas fa-temperature-high', // 热
197 + '901': 'fas fa-temperature-low', // 冷
198 + '999': 'fas fa-cloud' // 未知
199 + };
200 + return iconMap[code] || 'fas fa-cloud';
201 + },
202 +
203 + // 新增:通过IP定位获取位置信息
204 + async getLocationByIP() {
205 + try {
206 + console.log('开始IP定位...');
207 + // 使用高德地图IP定位API
208 + const response = await fetch(`https://restapi.amap.com/v3/ip?key=${weatherConfig.amapKey}`);
209 +
210 + if (!response.ok) {
211 + throw new Error(`HTTP ${response.status}`);
212 + }
213 +
214 + const data = await response.json();
215 + console.log('高德IP定位返回:', data);
216 +
217 + if (data.status === '1' && data.province && data.city) {
218 + // 高德IP定位返回省份和城市
219 + const province = data.province;
220 + const city = data.city;
221 + const rectangle = data.rectangle; // 格式:"左下角经度,左下角纬度;右上角经度,右上角纬度"
222 +
223 + // 计算矩形中心点作为坐标
224 + let longitude, latitude;
225 + if (rectangle) {
226 + const coords = rectangle.split(';');
227 + const bottomLeft = coords[0].split(',').map(Number);
228 + const topRight = coords[1].split(',').map(Number);
229 + longitude = ((bottomLeft[0] + topRight[0]) / 2).toFixed(6);
230 + latitude = ((bottomLeft[1] + topRight[1]) / 2).toFixed(6);
231 + } else {
232 + // 如果没有矩形区域,使用默认坐标
233 + longitude = "116.397428";
234 + latitude = "39.90923";
235 + }
236 +
237 + const locationName = city === province ? city : `${city}, ${province}`;
238 +
239 + return {
240 + location: `${longitude},${latitude}`,
241 + locationName: locationName,
242 + source: 'ip'
243 + };
244 + } else {
245 + throw new Error('高德IP定位未返回有效数据');
246 + }
247 + } catch (error) {
248 + console.warn('高德IP定位失败:', error);
249 + // 如果高德IP定位失败,尝试使用其他IP定位服务作为备选
250 + return await this.getLocationByIPFallback();
251 + }
252 + },
253 +
254 + // 新增:备用的IP定位服务
255 + async getLocationByIPFallback() {
256 + try {
257 + console.log('尝试备用IP定位...');
258 + const ipServices = [
259 + 'https://ipapi.co/json/',
260 + 'https://api.ip.sb/geoip'
261 + ];
262 +
263 + for (const serviceUrl of ipServices) {
264 + try {
265 + const response = await fetch(serviceUrl);
266 +
267 + if (!response.ok) {
268 + continue;
269 + }
270 +
271 + const data = await response.json();
272 + console.log('备用IP定位服务返回:', data);
273 +
274 + let latitude, longitude, cityName;
275 +
276 + if (serviceUrl.includes('ipapi.co')) {
277 + latitude = data.latitude;
278 + longitude = data.longitude;
279 + cityName = data.city;
280 + } else if (serviceUrl.includes('ip.sb')) {
281 + latitude = data.latitude;
282 + longitude = data.longitude;
283 + cityName = data.city;
284 + }
285 +
286 + if (latitude && longitude) {
287 + // 使用高德逆地理编码获取城市名称
288 + const chineseName = await this.reverseGeocodeAMap(longitude, latitude);
289 + return {
290 + location: `${longitude.toFixed(6)},${latitude.toFixed(6)}`,
291 + locationName: chineseName,
292 + source: 'ip_fallback'
293 + };
294 + }
295 + } catch (error) {
296 + console.warn(`备用IP定位服务 ${serviceUrl} 失败:`, error);
297 + continue;
298 + }
299 + }
300 +
301 + throw new Error('所有IP定位服务都失败了');
302 + } catch (error) {
303 + console.warn('备用IP定位失败:', error);
304 + throw error;
305 + }
306 + },
307 +
308 + // 新增:通过浏览器定位获取位置信息
309 + async getLocationByBrowser() {
310 + return new Promise((resolve, reject) => {
311 + if (!navigator.geolocation) {
312 + reject(new Error('浏览器不支持定位功能'));
313 + return;
314 + }
315 +
316 + console.log('请求浏览器定位权限...');
317 +
318 + navigator.geolocation.getCurrentPosition(
319 + async (position) => {
320 + console.log('浏览器定位成功:', position);
321 + const latitude = position.coords.latitude;
322 + const longitude = position.coords.longitude;
323 +
324 + try {
325 + // 使用高德逆地理编码获取详细位置信息
326 + const locationName = await this.reverseGeocodeAMap(longitude, latitude);
327 + resolve({
328 + location: `${longitude.toFixed(6)},${latitude.toFixed(6)}`,
329 + locationName: locationName,
330 + source: 'browser'
331 + });
332 + } catch (error) {
333 + console.warn('高德逆地理编码失败:', error);
334 + // 即使逆地理编码失败,也返回坐标信息
335 + resolve({
336 + location: `${longitude.toFixed(6)},${latitude.toFixed(6)}`,
337 + locationName: await this.getCityNameFromCoords(longitude, latitude),
338 + source: 'browser'
339 + });
340 + }
341 + },
342 + (error) => {
343 + console.error('浏览器定位失败:', error);
344 + let errorMessage = '浏览器定位失败';
345 + switch (error.code) {
346 + case error.PERMISSION_DENIED:
347 + errorMessage = '定位权限被拒绝,请允许浏览器定位权限';
348 + break;
349 + case error.POSITION_UNAVAILABLE:
350 + errorMessage = '无法获取位置信息';
351 + break;
352 + case error.TIMEOUT:
353 + errorMessage = '定位请求超时';
354 + break;
355 + }
356 + reject(new Error(errorMessage));
357 + },
358 + {
359 + enableHighAccuracy: true,
360 + timeout: 10000,
361 + maximumAge: 300000 // 5分钟缓存
362 + }
363 + );
364 + });
365 + },
366 +
367 + // 新增:使用高德地图逆地理编码API获取城市名称
368 + async reverseGeocodeAMap(longitude, latitude) {
369 + try {
370 + console.log('开始高德逆地理编码:', longitude, latitude);
371 + const response = await fetch(`https://restapi.amap.com/v3/geocode/regeo?key=${weatherConfig.amapKey}&location=${longitude},${latitude}&extensions=base`);
372 +
373 + if (!response.ok) {
374 + throw new Error(`高德逆地理编码请求失败: HTTP ${response.status}`);
375 + }
376 +
377 + const data = await response.json();
378 + console.log('高德逆地理编码返回:', data);
379 +
380 + if (data.status === '1' && data.regeocode) {
381 + const addressComponent = data.regeocode.addressComponent;
382 + const province = addressComponent.province;
383 + const city = addressComponent.city;
384 + const district = addressComponent.district;
385 +
386 + // 构建位置名称
387 + let locationName;
388 + if (city && city !== province) {
389 + // 普通城市:城市, 省份
390 + locationName = `${city}, ${province}`;
391 + } else if (city && city === province) {
392 + // 直辖市:城市
393 + locationName = city;
394 + } else {
395 + // 其他情况:省份
396 + locationName = province;
397 + }
398 +
399 + console.log('高德逆地理编码成功,位置名称:', locationName);
400 + return locationName;
401 + } else {
402 + throw new Error('高德逆地理编码未返回有效数据');
403 + }
404 + } catch (error) {
405 + console.warn('高德逆地理编码失败:', error);
406 + throw error;
407 + }
408 + },
409 +
410 + // 新增:从坐标生成简单的城市名称(备用方法)
411 + async getCityNameFromCoords(longitude, latitude) {
412 + try {
413 + // 使用高德逆地理编码作为主要方法
414 + const locationName = await this.reverseGeocodeAMap(longitude, latitude);
415 + return locationName;
416 + } catch (e) {
417 + // 如果高德API失败,返回坐标信息
418 + return `${latitude.toFixed(2)}, ${longitude.toFixed(2)}`;
419 + }
420 + },
421 +
422 + // 新增:自动定位功能
423 + async autoLocate() {
424 + try {
425 + // 首先尝试浏览器精确定位
426 + console.log('尝试浏览器定位...');
427 + const browserLocation = await this.getLocationByBrowser();
428 + console.log('浏览器定位成功:', browserLocation);
429 + return browserLocation;
430 + } catch (browserError) {
431 + console.warn('浏览器定位失败,尝试IP定位:', browserError);
432 +
433 + try {
434 + // 浏览器定位失败后尝试IP定位
435 + const ipLocation = await this.getLocationByIP();
436 + console.log('IP定位成功:', ipLocation);
437 + return ipLocation;
438 + } catch (ipError) {
439 + console.error('所有定位方式都失败了:', ipError);
440 + throw new Error('自动定位失败:请检查定位权限或手动设置位置');
441 + }
442 + }
443 + },
444 +
445 + // 新增:AI建议API调用
446 + async getAIAdvice(weatherData) {
447 + try {
448 + console.log('开始获取AI建议...');
449 +
450 + // 构建用户信息和天气数据的提示词
451 + const prompt = this.buildAIPrompt(weatherData);
452 +
453 + const response = await fetch(`${weatherConfig.openaiBaseUrl}/chat/completions`, {
454 + method: 'POST',
455 + headers: {
456 + 'Content-Type': 'application/json',
457 + 'Authorization': `Bearer ${weatherConfig.openaiApiKey}`
458 + },
459 + body: JSON.stringify({
460 + model: weatherConfig.openaiModel,
461 + messages: [
462 + {
463 + role: 'user',
464 + content: prompt
465 + }
466 + ],
467 + temperature: 0.7,
468 + max_tokens: 1000
469 + })
470 + });
471 +
472 + if (!response.ok) {
473 + throw new Error(`AI API请求失败: HTTP ${response.status}`);
474 + }
475 +
476 + const data = await response.json();
477 +
478 + if (data.choices && data.choices[0] && data.choices[0].message) {
479 + const adviceText = data.choices[0].message.content;
480 + return this.parseAIResponse(adviceText);
481 + } else {
482 + throw new Error('AI API返回数据格式错误');
483 + }
484 + } catch (error) {
485 + console.error('获取AI建议失败:', error);
486 + // 返回默认建议作为降级方案
487 + return this.getDefaultAdvice(weatherData);
488 + }
489 + },
490 +
491 + // 新增:构建AI提示词 - 使用配置中的用户信息
492 + buildAIPrompt(weatherData) {
493 + const userInfo = `个人信息:
494 + - 年龄:${weatherConfig.userProfile.age}岁
495 + - 性别:${weatherConfig.userProfile.gender}
496 + - 通勤需求:${weatherConfig.userProfile.commuteDays}需要通勤
497 + - 通勤方式:${weatherConfig.userProfile.commuteMethod}
498 +
499 + 核心需求:${weatherConfig.userProfile.coreNeeds}`;
500 +
501 + const weatherInfo = `当前天气数据:
502 + - 位置:${weatherData.location}
503 + - 当前温度:${weatherData.temperature}°C
504 + - 天气状况:${weatherData.weather}
505 + - 体感温度:${weatherData.feelsLike}°C
506 + - 湿度:${weatherData.humidity}%
507 + - 风速:${weatherData.windSpeed} km/h,风向:${weatherData.windDirection}
508 + - 今日温度范围:${weatherData.todayLow}°C ~ ${weatherData.todayHigh}°C
509 + - 日出时间:${weatherData.sunriseTime || '未知'}
510 + - 日落时间:${weatherData.sunsetTime || '未知'}
511 +
512 + 未来24小时趋势:${weatherData.hourlyForecast.slice(0, 8).map(h => `${h.time} ${h.temperature}°C ${h.weather}`).join('; ')}
513 +
514 + 未来7天预报:${weatherData.dailyForecast.slice(0, 3).map(d => `${d.weekday} ${d.lowTemp}~${d.highTemp}°C ${d.weather}`).join('; ')}`;
515 +
516 + return `你是一个专业的天气生活助手,请根据以下信息为一位${weatherConfig.userProfile.age}岁${weatherConfig.userProfile.gender}提供个性化天气建议。
517 +
518 + ${userInfo}
519 +
520 + ${weatherInfo}
521 +
522 + 请针对以下5个方面提供具体、实用的建议,要求:
523 + 1. 个性化定制:结合用户年龄、性别、通勤方式
524 + 2. 数据融合:综合温度、湿度、风速、降水等多种要素
525 + 3. 趋势提醒:结合短期预报给出动态建议
526 + 4. 情景化:具体到出行、着装、健康等实际场景
527 + 5. 简洁实用:每个建议控制在40字以内
528 +
529 + 请严格按照以下格式返回,不要添加其他内容:
530 +
531 + 出行准备建议:[具体建议]
532 + 着装建议:[具体建议]
533 + 健康防护建议:[具体建议]
534 + 户外活动可行性建议:[具体建议]
535 + 通勤/交通提醒:[具体建议]`;
536 + },
537 +
538 + // 新增:解析AI响应
539 + parseAIResponse(responseText) {
540 + const adviceTypes = [
541 + '出行准备建议',
542 + '着装建议',
543 + '健康防护建议',
544 + '户外活动可行性建议',
545 + '通勤/交通提醒'
546 + ];
547 +
548 + const advice = {};
549 +
550 + adviceTypes.forEach(type => {
551 + const regex = new RegExp(`${type}:([^\\n]+)`);
552 + const match = responseText.match(regex);
553 + advice[type] = match ? match[1].trim() : this.getDefaultAdviceForType(type);
554 + });
555 +
556 + return advice;
557 + },
558 +
559 + // 新增:获取默认建议(降级方案)
560 + getDefaultAdvice(weatherData) {
561 + const temp = parseInt(weatherData.temperature);
562 + const weather = weatherData.weather;
563 + const humidity = parseInt(weatherData.humidity);
564 +
565 + return {
566 + '出行准备建议': this.getTravelAdvice(temp, weather, humidity),
567 + '着装建议': this.getDressingAdvice(temp, weather),
568 + '健康防护建议': this.getHealthAdvice(temp, weather, humidity),
569 + '户外活动可行性建议': this.getOutdoorAdvice(temp, weather),
570 + '通勤/交通提醒': this.getCommuteAdvice(temp, weather)
571 + };
572 + },
573 +
574 + // 新增:根据温度天气生成出行建议
575 + getTravelAdvice(temp, weather, humidity) {
576 + if (weather.includes('雨')) {
577 + return '今天有雨,建议携带雨具,选择防水背包和鞋子';
578 + } else if (temp > 30) {
579 + return '天气炎热,建议避开正午高温时段出行,携带防晒用品';
580 + } else if (temp < 5) {
581 + return '天气寒冷,外出注意保暖,建议使用保温杯携带热水';
582 + } else {
583 + return '天气适宜,正常出行即可,建议携带轻便外套备用';
584 + }
585 + },
586 +
587 + // 新增:着装建议
588 + getDressingAdvice(temp, weather) {
589 + if (temp > 28) {
590 + return '建议穿短袖、短裤等夏季服装,选择透气吸汗面料';
591 + } else if (temp > 20) {
592 + return '建议穿长袖T恤、薄外套,搭配长裤,舒适透气';
593 + } else if (temp > 10) {
594 + return '建议穿夹克、卫衣等秋季服装,注意早晚温差';
595 + } else {
596 + return '建议穿羽绒服、厚外套,戴围巾手套,注意防寒保暖';
597 + }
598 + },
599 +
600 + // 新增:健康防护建议
601 + getHealthAdvice(temp, weather, humidity) {
602 + const advice = [];
603 +
604 + if (humidity > 80) {
605 + advice.push('湿度较高,注意防潮除湿,保持室内通风');
606 + } else if (humidity < 30) {
607 + advice.push('空气干燥,注意补水保湿,可使用加湿器');
608 + }
609 +
610 + if (temp > 30) {
611 + advice.push('注意防暑降温,及时补充水分和电解质');
612 + } else if (temp < 5) {
613 + advice.push('注意防寒保暖,预防感冒和呼吸道疾病');
614 + }
615 +
616 + if (weather.includes('霾') || weather.includes('沙尘')) {
617 + advice.push('空气质量较差,建议佩戴防护口罩,减少户外活动');
618 + }
619 +
620 + return advice.length > 0 ? advice.join(';') : '天气条件良好,保持正常生活习惯即可';
621 + },
622 +
623 + // 新增:户外活动建议
624 + getOutdoorAdvice(temp, weather) {
625 + if (weather.includes('雨') || weather.includes('雪')) {
626 + return '不适宜户外活动,建议选择室内运动或改期进行';
627 + } else if (weather.includes('雷')) {
628 + return '有雷电活动,严禁户外活动,务必待在室内';
629 + } else if (temp > 35 || temp < -10) {
630 + return '温度极端,不适宜长时间户外活动,注意安全';
631 + } else if (temp > 25 && temp < 30) {
632 + return '天气适宜户外活动,建议在早晚凉爽时段进行';
633 + } else {
634 + return '条件允许户外活动,注意适时休息和补充水分';
635 + }
636 + },
637 +
638 + // 新增:通勤建议
639 + getCommuteAdvice(temp, weather) {
640 + const advice = [];
641 +
642 + if (weather.includes('雨') || weather.includes('雪')) {
643 + advice.push('雨天路滑,骑行请注意安全,建议选择公共交通');
644 + advice.push('预留额外通勤时间,注意车辆行驶安全');
645 + } else if (weather.includes('雾') || weather.includes('霾')) {
646 + advice.push('能见度较低,骑行请开启车灯,减速慢行');
647 + } else if (temp < 0) {
648 + advice.push('路面可能结冰,骑行需特别小心,建议选择地铁');
649 + } else {
650 + advice.push('通勤条件良好,共享电单车+地铁是理想选择');
651 + }
652 +
653 + return advice.join(';');
654 + },
655 +
656 + // 新增:获取默认类型建议
657 + getDefaultAdviceForType(type) {
658 + const defaults = {
659 + '出行准备建议': '根据天气情况准备相应物品,注意温度变化',
660 + '着装建议': '根据温度选择合适的服装,注意舒适度',
661 + '健康防护建议': '关注天气变化,做好相应健康防护',
662 + '户外活动可行性建议': '根据天气条件评估户外活动适宜度',
663 + '通勤/交通提醒': '合理安排通勤方式,注意交通安全'
664 + };
665 + return defaults[type] || '建议信息暂不可用';
666 + }
667 + };
668 +
669 + // 天气数据管理 - 保持原有代码100%不变,只新增日出日落数据
670 + const weatherData = {
671 + current: null,
672 + currentLocation: utils.loadSettings(),
673 +
674 + async fetchWeatherData() {
675 + const location = this.currentLocation.location;
676 +
677 + try {
678 + // 并行获取所有天气数据
679 + const [nowData, hourlyData, dailyData] = await Promise.all([
680 + this.fetchApiData('/weather/now', location),
681 + this.fetchApiData('/weather/24h', location),
682 + this.fetchApiData('/weather/7d', location)
683 + ]);
684 +
685 + // 保存当前设置
686 + utils.saveSettings(location, this.currentLocation.locationName);
687 +
688 + return this.formatWeatherData(nowData, hourlyData, dailyData);
689 + } catch (error) {
690 + console.error('天气API请求失败:', error);
691 + throw error;
692 + }
693 + },
694 +
695 + async fetchApiData(endpoint, location) {
696 + const url = `https://${weatherConfig.apiHost}/v7${endpoint}?location=${location}`;
697 +
698 + const response = await fetch(url, {
699 + headers: {
700 + 'X-QW-Api-Key': weatherConfig.apiKey
701 + }
702 + });
703 +
704 + if (!response.ok) {
705 + throw new Error(`HTTP ${response.status}: ${response.statusText}`);
706 + }
707 +
708 + const data = await response.json();
709 +
710 + if (data.code === '200') {
711 + return data;
712 + } else {
713 + throw new Error(data.code || 'API返回错误');
714 + }
715 + },
716 +
717 + formatWeatherData(nowData, hourlyData, dailyData) {
718 + const now = nowData.now;
719 + const today = dailyData.daily[0];
720 +
721 + return {
722 + location: this.currentLocation.locationName,
723 + temperature: now.temp || '--',
724 + weather: now.text || '未知',
725 + description: now.text || '未知天气',
726 + humidity: now.humidity || '--',
727 + windSpeed: now.windSpeed || '--',
728 + windDirection: now.windDir || '--',
729 + feelsLike: now.feelsLike || '--',
730 + windScale: now.windScale || '--',
731 + icon: utils.getWeatherIcon(now.icon),
732 + updateTime: this.formatUpdateTime(),
733 + todayHigh: dailyData.daily[0].tempMax || '--',
734 + todayLow: dailyData.daily[0].tempMin || '--',
735 + // 新增:日出日落时间
736 + sunriseTime: today.sunrise || '--',
737 + sunsetTime: today.sunset || '--',
738 + hourlyForecast: this.formatHourlyData(hourlyData.hourly || []),
739 + dailyForecast: this.formatDailyData(dailyData.daily || []),
740 + rawData: { nowData, hourlyData, dailyData }
741 + };
742 + },
743 +
744 + formatHourlyData(hourlyData) {
745 + if (!hourlyData || !Array.isArray(hourlyData)) {
746 + return this.generateMockHourlyData();
747 + }
748 +
749 + console.log('和风天气API返回的小时数据点数量:', hourlyData.length);
750 +
751 + // 使用和风天气返回的24小时数据,确保每个小时一个数据点
752 + return hourlyData.slice(0, 24).map((hour, index) => ({
753 + time: utils.formatTime(hour.fxTime),
754 + temperature: parseInt(hour.temp),
755 + weather: hour.text,
756 + icon: utils.getWeatherIcon(hour.icon),
757 + precipitation: hour.precip || '0mm'
758 + }));
759 + },
760 +
761 + formatDailyData(dailyData) {
762 + const days = [];
763 + const today = new Date();
764 +
765 + dailyData.slice(0, 7).forEach((day, index) => {
766 + const date = new Date(today);
767 + date.setDate(today.getDate() + index);
768 +
769 + days.push({
770 + weekday: index === 0 ? '今天' : utils.getWeekday(day.fxDate),
771 + highTemp: day.tempMax,
772 + lowTemp: day.tempMin,
773 + weather: day.textDay,
774 + weatherText: day.textDay,
775 + icon: utils.getWeatherIcon(day.iconDay)
776 + });
777 + });
778 +
779 + return days;
780 + },
781 +
782 + generateMockHourlyData() {
783 + const hours = [];
784 + const now = new Date();
785 + const currentHour = now.getHours();
786 +
787 + // 生成24小时模拟数据 - 每个小时一个点
788 + let baseTemp = 20;
789 + for (let i = 0; i < 24; i++) {
790 + const hour = (currentHour + i) % 24;
791 + // 模拟温度变化:白天高,夜晚低
792 + const variation = Math.sin((i / 24) * Math.PI * 2) * 8;
793 + const temperature = Math.round(baseTemp + variation);
794 +
795 + // 模拟天气变化
796 + let weather = '晴';
797 + let iconCode = '100';
798 + if (hour >= 18 || hour <= 6) {
799 + weather = '晴';
800 + iconCode = '150'; // 夜间晴
801 + } else if (hour >= 12 && hour <= 15) {
802 + weather = '多云';
803 + iconCode = '101';
804 + } else if (hour >= 8 && hour <= 11) {
805 + weather = '晴';
806 + iconCode = '100';
807 + }
808 +
809 + hours.push({
810 + time: `${hour.toString().padStart(2, '0')}:00`,
811 + temperature: temperature,
812 + weather: weather,
813 + icon: utils.getWeatherIcon(iconCode),
814 + precipitation: '0mm'
815 + });
816 + }
817 +
818 + return hours;
819 + },
820 +
821 + formatUpdateTime() {
822 + const now = new Date();
823 + return `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}`;
824 + },
825 +
826 + // 模拟数据作为备用
827 + getMockData() {
828 + return {
829 + location: this.currentLocation.locationName,
830 + temperature: 15 + Math.floor(Math.random() * 15),
831 + weather: '晴',
832 + description: '晴朗',
833 + humidity: 40 + Math.floor(Math.random() * 40),
834 + windSpeed: (2 + Math.random() * 5).toFixed(1),
835 + windDirection: '东南风',
836 + feelsLike: 13 + Math.floor(Math.random() * 15),
837 + windScale: '2-3级',
838 + icon: utils.getWeatherIcon('100'),
839 + updateTime: '刚刚',
840 + todayHigh: 25,
841 + todayLow: 15,
842 + sunriseTime: '06:30',
843 + sunsetTime: '18:45',
844 + hourlyForecast: this.generateMockHourlyData(),
845 + dailyForecast: this.generateMockDailyData(),
846 + isMock: true
847 + };
848 + },
849 +
850 + generateMockDailyData() {
851 + const days = [];
852 + const today = new Date();
853 + const conditions = ['100', '101', '104', '305']; // 晴, 多云, 阴, 小雨
854 +
855 + for (let i = 0; i < 7; i++) {
856 + const date = new Date(today);
857 + date.setDate(today.getDate() + i);
858 + const condition = conditions[Math.floor(Math.random() * conditions.length)];
859 +
860 + days.push({
861 + weekday: i === 0 ? '今天' : utils.getWeekday(date),
862 + highTemp: 20 + Math.floor(Math.random() * 10),
863 + lowTemp: 10 + Math.floor(Math.random() * 8),
864 + weather: this.getWeatherText(condition),
865 + weatherText: this.getWeatherText(condition),
866 + icon: utils.getWeatherIcon(condition)
867 + });
868 + }
869 +
870 + return days;
871 + },
872 +
873 + getWeatherText(iconCode) {
874 + const textMap = {
875 + '100': '晴',
876 + '101': '多云',
877 + '104': '阴',
878 + '305': '小雨'
879 + };
880 + return textMap[iconCode] || '晴';
881 + }
882 + };
883 +
884 + // UI管理 - 新增日出日落显示和AI建议面板
885 + const weatherUI = {
886 + init() {
887 + this.createContainer();
888 + this.addStyles();
889 + this.bindGlobalEvents();
890 + },
891 +
892 + createContainer() {
893 + if (document.getElementById('qweather-widget')) return;
894 +
895 + const container = document.createElement('div');
896 + container.id = 'qweather-widget';
897 + container.innerHTML = `
898 + <!-- 移动端和PC端按钮 -->
899 + <div class="weather-toggle-btn ${utils.isMobile() ? 'weather-mobile-btn' : 'weather-pc-btn'}">
900 + <i class="fas fa-cloud-sun"></i>
901 + </div>
902 +
903 + <!-- 天气面板 -->
904 + <div class="weather-panel ${utils.isMobile() ? 'weather-panel-mobile' : 'weather-panel-pc'} hidden">
905 + <div class="weather-header">
906 + <div class="location" title="点击切换城市">
907 + <i class="fas fa-map-marker-alt"></i>
908 + <span class="city-name">加载中...</span>
909 + </div>
910 + <div class="weather-actions">
911 + <button class="weather-auto-locate-btn" title="自动定位">
912 + <i class="fas fa-crosshairs"></i>
913 + </button>
914 + <button class="weather-refresh-btn" title="刷新天气">
915 + <i class="fas fa-sync-alt"></i>
916 + </button>
917 + <button class="weather-close-btn" title="关闭面板">
918 + <i class="fas fa-times"></i>
919 + </button>
920 + </div>
921 + </div>
922 +
923 + <div class="weather-main">
924 + <div class="temperature-container">
925 + <div class="temperature">--°</div>
926 + <div class="temp-range">
927 + <span class="high-temp">--°</span>
928 + <span class="low-temp">--°</span>
929 + </div>
930 + </div>
931 + <div class="weather-icon">
932 + <i class="fas fa-sun"></i>
933 + </div>
934 + </div>
935 +
936 + <div class="weather-description">加载中...</div>
937 +
938 + <!-- 简化的小信息栏 -->
939 + <div class="weather-mini-info">
940 + <div class="mini-item">
941 + <i class="fas fa-wind"></i>
942 + <span class="mini-value" id="wind-speed">--</span>
943 + <span class="mini-label">风速</span>
944 + </div>
945 + <div class="mini-item">
946 + <i class="fas fa-tint"></i>
947 + <span class="mini-value" id="humidity">--</span>
948 + <span class="mini-label">湿度</span>
949 + </div>
950 + <div class="mini-item">
951 + <i class="fas fa-temperature-low"></i>
952 + <span class="mini-value" id="feels-like">--</span>
953 + <span class="mini-label">体感</span>
954 + </div>
955 + </div>
956 +
957 + <!-- 24小时天气预报 - 带折线图 -->
958 + <div class="hourly-forecast">
959 + <div class="hourly-header">
960 + <div class="section-title">24小时预报</div>
961 + <div class="sun-times">
962 + <span class="sunrise">
963 + <i class="fas fa-sun"></i>
964 + <span class="sun-time-text">日出 --:--</span>
965 + </span>
966 + <span class="sunset">
967 + <i class="fas fa-moon"></i>
968 + <span class="sun-time-text">日落 --:--</span>
969 + </span>
970 + </div>
971 + </div>
972 + <div class="hourly-scroll-container">
973 + <div class="hourly-chart-container">
974 + <svg class="temperature-line-chart" width="1200" height="120">
975 + <!-- 折线图路径将由JS动态生成 -->
976 + </svg>
977 + <div class="hourly-list">
978 + <!-- 小时数据将由JS动态生成 -->
979 + </div>
980 + </div>
981 + </div>
982 + </div>
983 +
984 + <!-- 7天天气预报 -->
985 + <div class="daily-forecast">
986 + <div class="section-title">7天预报</div>
987 + <div class="daily-list">
988 + <!-- 每日数据将由JS动态生成 -->
989 + </div>
990 + </div>
991 +
992 + <!-- 新增:AI建议面板 -->
993 + <div class="ai-advice-panel">
994 + <div class="section-title">
995 + <i class="fas fa-robot"></i>
996 + AI生活建议
997 + <span class="ai-loading hidden">分析中...</span>
998 + </div>
999 + <div class="advice-grid">
1000 + <div class="advice-item">
1001 + <div class="advice-icon">
1002 + <i class="fas fa-walking"></i>
1003 + </div>
1004 + <div class="advice-content">
1005 + <div class="advice-title">出行准备</div>
1006 + <div class="advice-text">分析中...</div>
1007 + </div>
1008 + </div>
1009 + <div class="advice-item">
1010 + <div class="advice-icon">
1011 + <i class="fas fa-tshirt"></i>
1012 + </div>
1013 + <div class="advice-content">
1014 + <div class="advice-title">着装建议</div>
1015 + <div class="advice-text">分析中...</div>
1016 + </div>
1017 + </div>
1018 + <div class="advice-item">
1019 + <div class="advice-icon">
1020 + <i class="fas fa-heartbeat"></i>
1021 + </div>
1022 + <div class="advice-content">
1023 + <div class="advice-title">健康防护</div>
1024 + <div class="advice-text">分析中...</div>
1025 + </div>
1026 + </div>
1027 + <div class="advice-item">
1028 + <div class="advice-icon">
1029 + <i class="fas fa-running"></i>
1030 + </div>
1031 + <div class="advice-content">
1032 + <div class="advice-title">户外活动</div>
1033 + <div class="advice-text">分析中...</div>
1034 + </div>
1035 + </div>
1036 + <div class="advice-item">
1037 + <div class="advice-icon">
1038 + <i class="fas fa-subway"></i>
1039 + </div>
1040 + <div class="advice-content">
1041 + <div class="advice-title">通勤提醒</div>
1042 + <div class="advice-text">分析中...</div>
1043 + </div>
1044 + </div>
1045 + </div>
1046 + </div>
1047 +
1048 + <div class="weather-footer">
1049 + <span class="update-time">更新于: 刚刚</span>
1050 + <span class="mock-indicator hidden">演示数据</span>
1051 + </div>
1052 + </div>
1053 +
1054 + <!-- 移动端遮罩 -->
1055 + <div class="weather-mobile-overlay hidden"></div>
1056 +
1057 + <!-- 城市选择弹窗 -->
1058 + <div class="city-select-modal hidden">
1059 + <div class="modal-content">
1060 + <h3>选择位置</h3>
1061 + <div class="input-group">
1062 + <label for="location-name-input">位置名称:</label>
1063 + <input type="text" id="location-name-input" placeholder="例如: 北京" value="${weatherData.currentLocation.locationName}">
1064 + </div>
1065 + <div class="input-group">
1066 + <label for="location-coord-input">经纬度:</label>
1067 + <input type="text" id="location-coord-input" placeholder="例如: 116.41,39.92" value="${weatherData.currentLocation.location}">
1068 + </div>
1069 + <div class="modal-tips">
1070 + <p><strong>提示:</strong> 使用经纬度坐标获取精确天气数据</p>
1071 + <p>格式: 经度,纬度 (例如: 116.41,39.92)</p>
1072 + </div>
1073 + <div class="modal-actions">
1074 + <button class="modal-auto-locate">自动定位</button>
1075 + <button class="modal-cancel">取消</button>
1076 + <button class="modal-confirm">确认</button>
1077 + </div>
1078 + </div>
1079 + </div>
1080 + `;
1081 +
1082 + document.body.appendChild(container);
1083 + this.bindEvents();
1084 + },
1085 +
1086 + addStyles() {
1087 + const styles = `
1088 + <style>
1089 + #qweather-widget {
1090 + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
1091 + z-index: 10000;
1092 + }
1093 +
1094 + /* 移动端和PC端按钮 - 调整到导航按钮右侧 */
1095 + .weather-toggle-btn {
1096 + position: fixed;
1097 + top: 20px !important;
1098 + left: 76px !important;
1099 + width: 44px;
1100 + height: 44px;
1101 + background: rgba(255, 255, 255, 0.15);
1102 + backdrop-filter: blur(10px);
1103 + border-radius: 12px;
1104 + display: flex;
1105 + align-items: center;
1106 + justify-content: center;
1107 + cursor: pointer;
1108 + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
1109 + border: 1px solid rgba(255, 255, 255, 0.2);
1110 + z-index: 10001;
1111 + transition: all 0.3s ease;
1112 + }
1113 +
1114 + .weather-toggle-btn:hover {
1115 + transform: scale(1.05);
1116 + background: rgba(255, 255, 255, 0.25);
1117 + }
1118 +
1119 + .weather-toggle-btn i {
1120 + font-size: 1.4rem;
1121 + color: rgba(255, 255, 255, 0.9);
1122 + }
1123 +
1124 + /* 天气面板 - PC端 */
1125 + .weather-panel-pc {
1126 + position: fixed;
1127 + top: 20px;
1128 + left: 20px;
1129 + width: 350px; /* 保持原有宽度 */
1130 + background: rgba(255, 255, 255, 0.15);
1131 + backdrop-filter: blur(15px);
1132 + border-radius: 16px;
1133 + padding: 20px;
1134 + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
1135 + border: 1px solid rgba(255, 255, 255, 0.2);
1136 + color: white;
1137 + max-height: 80vh;
1138 + overflow-y: auto;
1139 + z-index: 10002;
1140 + transform-origin: 56px 0;
1141 + transform: scale(0);
1142 + opacity: 0;
1143 + transition: all 1.5s cubic-bezier(0.34, 1.56, 0.64, 1);
1144 + pointer-events: none;
1145 + }
1146 +
1147 + /* PC端面板激活状态 - 展开 */
1148 + .weather-panel-pc.weather-panel-active {
1149 + transform: scale(1);
1150 + opacity: 1;
1151 + pointer-events: auto;
1152 + }
1153 +
1154 + /* 天气面板 - 移动端 */
1155 + .weather-panel-mobile {
1156 + position: fixed;
1157 + top: 50%;
1158 + left: 50%;
1159 + transform: translate(-50%, -50%);
1160 + width: 90%;
1161 + max-width: 350px; /* 保持原有宽度 */
1162 + max-height: 80vh;
1163 + background: rgba(255, 255, 255, 0.15);
1164 + backdrop-filter: blur(15px);
1165 + border-radius: 16px;
1166 + padding: 20px;
1167 + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
1168 + border: 1px solid rgba(255, 255, 255, 0.2);
1169 + color: white;
1170 + overflow-y: auto;
1171 + z-index: 10003;
1172 + display: none;
1173 + }
1174 +
1175 + .weather-panel-mobile-active {
1176 + display: block !important;
1177 + }
1178 +
1179 + /* 移动端遮罩 */
1180 + .weather-mobile-overlay {
1181 + position: fixed;
1182 + top: 0;
1183 + left: 0;
1184 + width: 100%;
1185 + height: 100%;
1186 + background: rgba(0, 0, 0, 0.5);
1187 + z-index: 10002;
1188 + display: none;
1189 + transition: opacity 0.3s ease;
1190 + }
1191 +
1192 + .weather-mobile-overlay-active {
1193 + display: block !important;
1194 + }
1195 +
1196 + /* 24小时天气预报头部 - 新增日出日落 */
1197 + .hourly-header {
1198 + display: flex;
1199 + justify-content: space-between;
1200 + align-items: center;
1201 + margin-bottom: 8px;
1202 + }
1203 +
1204 + .sun-times {
1205 + display: flex;
1206 + gap: 12px;
1207 + font-size: 0.75rem;
1208 + opacity: 0.8;
1209 + }
1210 +
1211 + .sunrise, .sunset {
1212 + display: flex;
1213 + align-items: center;
1214 + gap: 4px;
1215 + }
1216 +
1217 + .sunrise i {
1218 + color: #ffb74d;
1219 + }
1220 +
1221 + .sunset i {
1222 + color: #ff9800;
1223 + }
1224 +
1225 + .sun-time-text {
1226 + font-size: 0.7rem;
1227 + }
1228 +
1229 + /* 24小时天气预报 - 带折线图 - 完全复刻原有样式 */
1230 + .hourly-forecast {
1231 + margin-bottom: 15px;
1232 + }
1233 +
1234 + .hourly-scroll-container {
1235 + overflow-x: auto;
1236 + padding-bottom: 8px;
1237 + }
1238 +
1239 + .hourly-chart-container {
1240 + position: relative;
1241 + min-width: 1200px; /* 增加宽度以适应24个数据点 */
1242 + height: 180px;
1243 + }
1244 +
1245 + .temperature-line-chart {
1246 + position: absolute;
1247 + top: 0;
1248 + left: 0;
1249 + width: 1200px; /* 增加宽度以适应24个数据点 */
1250 + height: 140px;
1251 + }
1252 +
1253 + .temperature-line {
1254 + fill: none;
1255 + stroke: rgba(255, 255, 255, 0.8);
1256 + stroke-width: 2;
1257 + stroke-linecap: round;
1258 + stroke-linejoin: round;
1259 + }
1260 +
1261 + .temperature-point {
1262 + fill: rgba(255, 255, 255, 0.9);
1263 + stroke: rgba(255, 255, 255, 0.3);
1264 + stroke-width: 1;
1265 + r: 4;
1266 + transition: all 0.3s ease;
1267 + }
1268 +
1269 + .temperature-point:hover {
1270 + r: 6;
1271 + fill: #fff;
1272 + }
1273 +
1274 + .hourly-list {
1275 + position: absolute;
1276 + bottom: 0;
1277 + left: 0;
1278 + display: flex;
1279 + width: 1200px; /* 增加宽度以适应24个数据点 */
1280 + justify-content: space-between; /* 均匀分布24个点 */
1281 + }
1282 +
1283 + .hourly-item {
1284 + display: flex;
1285 + flex-direction: column;
1286 + align-items: center;
1287 + gap: 4px;
1288 + width: 50px; /* 保持原有宽度 */
1289 + position: relative;
1290 + }
1291 +
1292 + .hourly-time {
1293 + font-size: 0.7rem;
1294 + opacity: 0.8;
1295 + white-space: nowrap;
1296 + }
1297 +
1298 + .hourly-icon {
1299 + font-size: 1rem;
1300 + opacity: 0.9;
1301 + }
1302 +
1303 + .hourly-temp {
1304 + font-size: 0.8rem;
1305 + font-weight: 600;
1306 + white-space: nowrap;
1307 + }
1308 +
1309 + /* 7天天气预报 */
1310 + .daily-forecast {
1311 + margin-bottom: 15px;
1312 + }
1313 +
1314 + .daily-list {
1315 + display: flex;
1316 + flex-direction: column;
1317 + gap: 8px;
1318 + }
1319 +
1320 + .daily-item {
1321 + display: flex;
1322 + justify-content: space-between;
1323 + align-items: center;
1324 + padding: 6px 0;
1325 + border-bottom: 1px solid rgba(255, 255, 255, 0.1);
1326 + }
1327 +
1328 + .daily-weekday {
1329 + min-width: 35px;
1330 + font-size: 0.85rem;
1331 + opacity: 0.9;
1332 + }
1333 +
1334 + .daily-weather {
1335 + display: flex;
1336 + align-items: center;
1337 + gap: 8px;
1338 + min-width: 80px;
1339 + justify-content: center;
1340 + }
1341 +
1342 + .daily-weather-text {
1343 + font-size: 0.8rem;
1344 + opacity: 0.9;
1345 + }
1346 +
1347 + .daily-icon {
1348 + font-size: 1rem;
1349 + opacity: 0.9;
1350 + min-width: 20px;
1351 + text-align: center;
1352 + }
1353 +
1354 + .daily-temp {
1355 + display: flex;
1356 + gap: 12px;
1357 + font-size: 0.85rem;
1358 + min-width: 60px;
1359 + justify-content: flex-end;
1360 + }
1361 +
1362 + .daily-high {
1363 + font-weight: 600;
1364 + }
1365 +
1366 + .daily-low {
1367 + opacity: 0.7;
1368 + }
1369 +
1370 + /* 新增:AI建议面板样式 */
1371 + .ai-advice-panel {
1372 + margin-bottom: 10px;
1373 + }
1374 +
1375 + .ai-advice-panel .section-title {
1376 + display: flex;
1377 + align-items: center;
1378 + gap: 6px;
1379 + margin-bottom: 10px;
1380 + }
1381 +
1382 + .ai-advice-panel .section-title i {
1383 + color: #4fc3f7;
1384 + }
1385 +
1386 + .ai-loading {
1387 + font-size: 0.7rem;
1388 + opacity: 0.7;
1389 + margin-left: auto;
1390 + }
1391 +
1392 + .advice-grid {
1393 + display: grid;
1394 + grid-template-columns: 1fr;
1395 + gap: 8px;
1396 + }
1397 +
1398 + .advice-item {
1399 + display: flex;
1400 + align-items: flex-start;
1401 + gap: 10px;
1402 + padding: 10px;
1403 + background: rgba(255, 255, 255, 0.08);
1404 + border-radius: 8px;
1405 + border: 1px solid rgba(255, 255, 255, 0.1);
1406 + transition: all 0.3s ease;
1407 + }
1408 +
1409 + .advice-item:hover {
1410 + background: rgba(255, 255, 255, 0.12);
1411 + transform: translateY(-1px);
1412 + }
1413 +
1414 + .advice-icon {
1415 + width: 32px;
1416 + height: 32px;
1417 + display: flex;
1418 + align-items: center;
1419 + justify-content: center;
1420 + background: rgba(79, 195, 247, 0.2);
1421 + border-radius: 6px;
1422 + flex-shrink: 0;
1423 + }
1424 +
1425 + .advice-icon i {
1426 + color: #4fc3f7;
1427 + font-size: 0.9rem;
1428 + }
1429 +
1430 + .advice-content {
1431 + flex: 1;
1432 + min-width: 0;
1433 + }
1434 +
1435 + .advice-title {
1436 + font-size: 0.8rem;
1437 + font-weight: 600;
1438 + margin-bottom: 4px;
1439 + color: rgba(255, 255, 255, 0.9);
1440 + }
1441 +
1442 + .advice-text {
1443 + font-size: 0.75rem;
1444 + line-height: 1.3;
1445 + opacity: 0.8;
1446 + word-wrap: break-word;
1447 + }
1448 +
1449 + /* 其他样式保持不变... */
1450 + .weather-header {
1451 + display: flex;
1452 + justify-content: space-between;
1453 + align-items: center;
1454 + margin-bottom: 15px;
1455 + }
1456 +
1457 + .location {
1458 + display: flex;
1459 + align-items: center;
1460 + gap: 8px;
1461 + font-size: 0.95rem;
1462 + font-weight: 600;
1463 + cursor: pointer;
1464 + transition: all 0.3s ease;
1465 + max-width: 70%;
1466 + }
1467 +
1468 + .location:hover {
1469 + opacity: 0.8;
1470 + transform: translateX(2px);
1471 + }
1472 +
1473 + .location i {
1474 + font-size: 0.9rem;
1475 + opacity: 0.9;
1476 + }
1477 +
1478 + .weather-actions {
1479 + display: flex;
1480 + gap: 4px;
1481 + }
1482 +
1483 + .weather-auto-locate-btn,
1484 + .weather-refresh-btn,
1485 + .weather-close-btn {
1486 + background: none;
1487 + border: none;
1488 + padding: 6px;
1489 + border-radius: 6px;
1490 + cursor: pointer;
1491 + color: rgba(255, 255, 255, 0.8);
1492 + transition: all 0.3s ease;
1493 + display: flex;
1494 + align-items: center;
1495 + justify-content: center;
1496 + }
1497 +
1498 + .weather-auto-locate-btn:hover {
1499 + background: rgba(255, 255, 255, 0.15);
1500 + color: #4fc3f7;
1501 + }
1502 +
1503 + .weather-refresh-btn:hover {
1504 + background: rgba(255, 255, 255, 0.15);
1505 + color: white;
1506 + }
1507 +
1508 + .weather-close-btn:hover {
1509 + background: rgba(255, 255, 255, 0.15);
1510 + color: white;
1511 + }
1512 +
1513 + .weather-main {
1514 + display: flex;
1515 + justify-content: space-between;
1516 + align-items: center;
1517 + margin-bottom: 12px;
1518 + }
1519 +
1520 + .temperature-container {
1521 + display: flex;
1522 + flex-direction: column;
1523 + gap: 4px;
1524 + }
1525 +
1526 + .temperature {
1527 + font-size: 2.8rem;
1528 + font-weight: 300;
1529 + line-height: 1;
1530 + text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
1531 + }
1532 +
1533 + .temp-range {
1534 + display: flex;
1535 + gap: 8px;
1536 + font-size: 0.9rem;
1537 + opacity: 0.8;
1538 + }
1539 +
1540 + .high-temp::after {
1541 + content: "/";
1542 + margin-left: 4px;
1543 + }
1544 +
1545 + .weather-icon {
1546 + font-size: 3.2rem;
1547 + opacity: 0.9;
1548 + text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
1549 + }
1550 +
1551 + .weather-description {
1552 + text-align: center;
1553 + font-size: 1rem;
1554 + margin-bottom: 15px;
1555 + opacity: 0.9;
1556 + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
1557 + }
1558 +
1559 + /* 简化的小信息栏 */
1560 + .weather-mini-info {
1561 + display: flex;
1562 + justify-content: space-around;
1563 + margin-bottom: 15px;
1564 + padding: 10px 0;
1565 + border-top: 1px solid rgba(255, 255, 255, 0.1);
1566 + border-bottom: 1px solid rgba(255, 255, 255, 0.1);
1567 + }
1568 +
1569 + .mini-item {
1570 + display: flex;
1571 + flex-direction: column;
1572 + align-items: center;
1573 + gap: 2px;
1574 + }
1575 +
1576 + .mini-item i {
1577 + font-size: 0.9rem;
1578 + opacity: 0.8;
1579 + }
1580 +
1581 + .mini-value {
1582 + font-size: 0.85rem;
1583 + font-weight: 600;
1584 + }
1585 +
1586 + .mini-label {
1587 + font-size: 0.7rem;
1588 + opacity: 0.7;
1589 + }
1590 +
1591 + .section-title {
1592 + font-size: 0.9rem;
1593 + font-weight: 600;
1594 + margin-bottom: 8px;
1595 + opacity: 0.9;
1596 + }
1597 +
1598 + .weather-footer {
1599 + display: flex;
1600 + justify-content: space-between;
1601 + align-items: center;
1602 + font-size: 0.7rem;
1603 + opacity: 0.6;
1604 + padding-top: 8px;
1605 + border-top: 1px solid rgba(255, 255, 255, 0.1);
1606 + }
1607 +
1608 + .mock-indicator {
1609 + color: #ff6b6b;
1610 + font-weight: 500;
1611 + }
1612 +
1613 + /* 滚动条样式 */
1614 + .hourly-scroll-container::-webkit-scrollbar {
1615 + height: 4px;
1616 + }
1617 +
1618 + .hourly-scroll-container::-webkit-scrollbar-track {
1619 + background: rgba(255, 255, 255, 0.1);
1620 + border-radius: 2px;
1621 + }
1622 +
1623 + .hourly-scroll-container::-webkit-scrollbar-thumb {
1624 + background: rgba(255, 255, 255, 0.3);
1625 + border-radius: 2px;
1626 + }
1627 +
1628 + .hourly-scroll-container::-webkit-scrollbar-thumb:hover {
1629 + background: rgba(255, 255, 255, 0.5);
1630 + }
1631 +
1632 + /* 工具类 */
1633 + .hidden {
1634 + display: none !important;
1635 + }
1636 +
1637 + /* 加载动画 */
1638 + .weather-loading .temperature,
1639 + .weather-loading .weather-description,
1640 + .weather-loading .mini-value,
1641 + .weather-loading .temp-range span,
1642 + .weather-loading .hourly-temp,
1643 + .weather-loading .daily-temp span {
1644 + color: transparent;
1645 + background: linear-gradient(90deg, rgba(255,255,255,0.1) 25%, rgba(255,255,255,0.2) 50%, rgba(255,255,255,0.1) 75%);
1646 + background-size: 200% 100%;
1647 + animation: loading 1.5s infinite;
1648 + border-radius: 4px;
1649 + }
1650 +
1651 + @keyframes loading {
1652 + 0% { background-position: 200% 0; }
1653 + 100% { background-position: -200% 0; }
1654 + }
1655 +
1656 + /* 响应式调整 */
1657 + @media (max-width: 480px) {
1658 + .weather-panel-mobile {
1659 + width: 95%;
1660 + padding: 16px;
1661 + }
1662 +
1663 + .temperature {
1664 + font-size: 2.5rem;
1665 + }
1666 +
1667 + .weather-icon {
1668 + font-size: 2.8rem;
1669 + }
1670 +
1671 + .hourly-header {
1672 + flex-direction: row;
1673 + align-items: center;
1674 + justify-content: space-between;
1675 + gap: 4px;
1676 + }
1677 +
1678 + .sun-times {
1679 + display: flex;
1680 + gap: 12px;
1681 + font-size: 0.7rem;
1682 + align-self: auto;
1683 + }
1684 + }
1685 + </style>
1686 + `;
1687 +
1688 + document.head.insertAdjacentHTML('beforeend', styles);
1689 + },
1690 +
1691 + updateHourlyForecast(hourlyData, sunriseTime, sunsetTime) {
1692 + const hourlyList = document.querySelector('.hourly-list');
1693 + const chartSvg = document.querySelector('.temperature-line-chart');
1694 +
1695 + // 清空现有内容
1696 + hourlyList.innerHTML = '';
1697 + chartSvg.innerHTML = '';
1698 +
1699 + console.log('更新24小时预报,数据点数量:', hourlyData.length);
1700 +
1701 + // 计算每个项目的宽度 - 完全复刻原有逻辑
1702 + const dataPointCount = hourlyData.length;
1703 + const containerWidth = 1200;
1704 + const itemWidth = containerWidth / dataPointCount;
1705 +
1706 + // 设置SVG宽度
1707 + chartSvg.setAttribute('width', containerWidth);
1708 +
1709 + // 生成折线图数据点 - 完全复刻原有逻辑
1710 + const points = hourlyData.map((hour, index) => ({
1711 + x: index,
1712 + temp: hour.temperature
1713 + }));
1714 +
1715 + // 生成折线路径 - 完全复刻原有逻辑
1716 + const linePath = utils.generateTemperatureLine(points, containerWidth, 120, itemWidth);
1717 + const pathElement = document.createElementNS('http://www.w3.org/2000/svg', 'path');
1718 + pathElement.setAttribute('d', linePath);
1719 + pathElement.setAttribute('class', 'temperature-line');
1720 + chartSvg.appendChild(pathElement);
1721 +
1722 + // 生成小时项目和温度点 - 完全复刻原有逻辑
1723 + hourlyData.forEach((hour, index) => {
1724 + // 计算x坐标 - 在每个项目的中心位置
1725 + const x = (index * itemWidth) + (itemWidth / 2);
1726 +
1727 + // 计算y坐标 - 使用相同的计算方法确保对齐
1728 + const allTemps = hourlyData.map(item => parseInt(item.temperature));
1729 + const minTemp = Math.min(...allTemps);
1730 + const maxTemp = Math.max(...allTemps);
1731 + const range = maxTemp - minTemp;
1732 + const heightPercent = range === 0 ? 50 : ((hour.temperature - minTemp) / range) * 60 + 30;
1733 + const y = 140 - (heightPercent / 100) * 120;
1734 +
1735 + // 添加温度点
1736 + const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
1737 + circle.setAttribute('cx', x);
1738 + circle.setAttribute('cy', y);
1739 + circle.setAttribute('class', 'temperature-point');
1740 + circle.setAttribute('data-temp', hour.temperature);
1741 + circle.setAttribute('data-index', index);
1742 + chartSvg.appendChild(circle);
1743 +
1744 + // 添加小时项目
1745 + const hourlyItem = document.createElement('div');
1746 + hourlyItem.className = 'hourly-item';
1747 + hourlyItem.style.width = `${itemWidth}px`;
1748 + hourlyItem.innerHTML = `
1749 + <div class="hourly-time">${hour.time}</div>
1750 + <i class="${hour.icon} hourly-icon"></i>
1751 + <div class="hourly-temp">${hour.temperature}°</div>
1752 + `;
1753 + hourlyList.appendChild(hourlyItem);
1754 + });
1755 +
1756 + // 更新日出日落时间
1757 + this.updateSunTimes(sunriseTime, sunsetTime);
1758 +
1759 + console.log('24小时折线图更新完成,数据点:', dataPointCount, '项目宽度:', itemWidth);
1760 + },
1761 +
1762 + // 新增:更新日出日落时间
1763 + updateSunTimes(sunriseTime, sunsetTime) {
1764 + const sunriseElement = document.querySelector('.sunrise .sun-time-text');
1765 + const sunsetElement = document.querySelector('.sunset .sun-time-text');
1766 +
1767 + if (sunriseElement && sunsetElement) {
1768 + sunriseElement.textContent = `日出 ${sunriseTime}`;
1769 + sunsetElement.textContent = `日落 ${sunsetTime}`;
1770 + }
1771 + },
1772 +
1773 + updateDailyForecast(dailyData) {
1774 + const dailyList = document.querySelector('.daily-list');
1775 + dailyList.innerHTML = '';
1776 +
1777 + dailyData.forEach(day => {
1778 + const dailyItem = document.createElement('div');
1779 + dailyItem.className = 'daily-item';
1780 + dailyItem.innerHTML = `
1781 + <div class="daily-weekday">${day.weekday}</div>
1782 + <div class="daily-weather">
1783 + <span class="daily-weather-text">${day.weatherText}</span>
1784 + <i class="${day.icon} daily-icon"></i>
1785 + </div>
1786 + <div class="daily-temp">
1787 + <span class="daily-high">${day.highTemp}°</span>
1788 + <span class="daily-low">${day.lowTemp}°</span>
1789 + </div>
1790 + `;
1791 + dailyList.appendChild(dailyItem);
1792 + });
1793 + },
1794 +
1795 + // 新增:更新AI建议面板
1796 + updateAIAdvice(adviceData) {
1797 + const adviceItems = document.querySelectorAll('.advice-item');
1798 + const aiLoading = document.querySelector('.ai-loading');
1799 +
1800 + if (aiLoading) {
1801 + aiLoading.classList.add('hidden');
1802 + }
1803 +
1804 + // 更新每个建议项
1805 + adviceItems.forEach(item => {
1806 + const titleElement = item.querySelector('.advice-title');
1807 + const textElement = item.querySelector('.advice-text');
1808 +
1809 + if (titleElement && textElement) {
1810 + const title = titleElement.textContent;
1811 + let adviceText = '';
1812 +
1813 + switch (title) {
1814 + case '出行准备':
1815 + adviceText = adviceData['出行准备建议'] || '建议信息暂不可用';
1816 + break;
1817 + case '着装建议':
1818 + adviceText = adviceData['着装建议'] || '建议信息暂不可用';
1819 + break;
1820 + case '健康防护':
1821 + adviceText = adviceData['健康防护建议'] || '建议信息暂不可用';
1822 + break;
1823 + case '户外活动':
1824 + adviceText = adviceData['户外活动可行性建议'] || '建议信息暂不可用';
1825 + break;
1826 + case '通勤提醒':
1827 + adviceText = adviceData['通勤/交通提醒'] || '建议信息暂不可用';
1828 + break;
1829 + }
1830 +
1831 + textElement.textContent = adviceText;
1832 + }
1833 + });
1834 + },
1835 +
1836 + // 新增:设置AI建议加载状态
1837 + setAIAdviceLoading(loading) {
1838 + const aiLoading = document.querySelector('.ai-loading');
1839 + const adviceTexts = document.querySelectorAll('.advice-text');
1840 +
1841 + if (loading) {
1842 + if (aiLoading) aiLoading.classList.remove('hidden');
1843 + adviceTexts.forEach(text => {
1844 + text.textContent = '分析中...';
1845 + });
1846 + } else {
1847 + if (aiLoading) aiLoading.classList.add('hidden');
1848 + }
1849 + },
1850 +
1851 + bindEvents() {
1852 + const container = document.getElementById('qweather-widget');
1853 + const toggleBtn = container.querySelector('.weather-toggle-btn');
1854 + const panel = container.querySelector('.weather-panel');
1855 +
1856 + // 移动端:保持点击切换逻辑
1857 + if (utils.isMobile()) {
1858 + toggleBtn.addEventListener('click', (e) => {
1859 + e.stopPropagation();
1860 + this.togglePanel();
1861 + });
1862 + } else {
1863 + // PC端:使用悬停显示逻辑
1864 + let hideTimeout;
1865 +
1866 + // 鼠标进入按钮时显示面板
1867 + toggleBtn.addEventListener('mouseenter', () => {
1868 + clearTimeout(hideTimeout);
1869 + this.showPanel();
1870 + });
1871 +
1872 + // 鼠标离开按钮时设置延迟隐藏
1873 + toggleBtn.addEventListener('mouseleave', () => {
1874 + hideTimeout = setTimeout(() => {
1875 + this.hidePanel();
1876 + }, 600);
1877 + });
1878 +
1879 + // 鼠标进入面板时取消隐藏
1880 + panel.addEventListener('mouseenter', () => {
1881 + clearTimeout(hideTimeout);
1882 + });
1883 +
1884 + // 鼠标离开面板时隐藏
1885 + panel.addEventListener('mouseleave', () => {
1886 + hideTimeout = setTimeout(() => {
1887 + this.hidePanel();
1888 + }, 300);
1889 + });
1890 + }
1891 +
1892 + // 位置点击事件
1893 + container.querySelector('.location').addEventListener('click', () => {
1894 + this.showCitySelectModal();
1895 + });
1896 +
1897 + // 自动定位按钮
1898 + container.querySelector('.weather-auto-locate-btn').addEventListener('click', () => {
1899 + this.autoLocate();
1900 + });
1901 +
1902 + // 刷新按钮
1903 + container.querySelector('.weather-refresh-btn').addEventListener('click', () => {
1904 + weatherApp.refreshWeather();
1905 + });
1906 +
1907 + // 关闭按钮
1908 + container.querySelector('.weather-close-btn').addEventListener('click', (e) => {
1909 + e.stopPropagation();
1910 + this.hidePanel();
1911 + });
1912 +
1913 + // 移动端遮罩
1914 + const overlay = container.querySelector('.weather-mobile-overlay');
1915 + if (overlay) {
1916 + overlay.addEventListener('click', (e) => {
1917 + e.stopPropagation();
1918 + this.hideMobilePanel();
1919 + });
1920 + }
1921 +
1922 + this.bindModalEvents();
1923 + },
1924 +
1925 + bindModalEvents() {
1926 + const container = document.getElementById('qweather-widget');
1927 + const modal = container.querySelector('.city-select-modal');
1928 +
1929 + // 自动定位按钮
1930 + container.querySelector('.modal-auto-locate').addEventListener('click', async () => {
1931 + await this.autoLocate(true); // true表示在模态框中执行
1932 + });
1933 +
1934 + // 确认按钮
1935 + container.querySelector('.modal-confirm').addEventListener('click', () => {
1936 + const locationName = container.querySelector('#location-name-input').value.trim();
1937 + const location = container.querySelector('#location-coord-input').value.trim();
1938 +
1939 + if (locationName && location) {
1940 + weatherData.currentLocation = { location, locationName };
1941 + weatherApp.loadWeatherData();
1942 + this.hideCitySelectModal();
1943 + } else {
1944 + alert('请输入完整的位置信息');
1945 + }
1946 + });
1947 +
1948 + // 取消按钮
1949 + container.querySelector('.modal-cancel').addEventListener('click', () => {
1950 + this.hideCitySelectModal();
1951 + });
1952 +
1953 + // 输入框回车事件
1954 + const inputs = container.querySelectorAll('#location-name-input, #location-coord-input');
1955 + inputs.forEach(input => {
1956 + input.addEventListener('keypress', (e) => {
1957 + if (e.key === 'Enter') {
1958 + container.querySelector('.modal-confirm').click();
1959 + }
1960 + });
1961 + });
1962 +
1963 + // 点击模态框外部关闭
1964 + modal.addEventListener('click', (e) => {
1965 + if (e.target === modal) {
1966 + this.hideCitySelectModal();
1967 + }
1968 + });
1969 + },
1970 +
1971 + bindGlobalEvents() {
1972 + // 窗口大小变化
1973 + window.addEventListener('resize', utils.debounce(() => {
1974 + this.handleResize();
1975 + }, 250));
1976 +
1977 + // ESC键关闭
1978 + document.addEventListener('keydown', (e) => {
1979 + if (e.key === 'Escape') {
1980 + this.hidePanel();
1981 + }
1982 + });
1983 + },
1984 +
1985 + handleResize() {
1986 + const container = document.getElementById('qweather-widget');
1987 + const toggleBtn = container.querySelector('.weather-toggle-btn');
1988 + const panel = container.querySelector('.weather-panel');
1989 +
1990 + if (utils.isMobile()) {
1991 + toggleBtn.classList.add('weather-mobile-btn');
1992 + toggleBtn.classList.remove('weather-pc-btn');
1993 + panel.classList.add('weather-panel-mobile');
1994 + panel.classList.remove('weather-panel-pc');
1995 + } else {
1996 + toggleBtn.classList.remove('weather-mobile-btn');
1997 + toggleBtn.classList.add('weather-pc-btn');
1998 + panel.classList.remove('weather-panel-mobile');
1999 + panel.classList.add('weather-panel-pc');
2000 + }
2001 + },
2002 +
2003 + togglePanel() {
2004 + const container = document.getElementById('qweather-widget');
2005 + const panel = container.querySelector('.weather-panel');
2006 +
2007 + if (panel.classList.contains('hidden') || (utils.isMobile() && !panel.classList.contains('weather-panel-mobile-active'))) {
2008 + this.showPanel();
2009 + } else {
2010 + this.hidePanel();
2011 + }
2012 + },
2013 +
2014 + showPanel() {
2015 + const container = document.getElementById('qweather-widget');
2016 + const panel = container.querySelector('.weather-panel');
2017 + const overlay = container.querySelector('.weather-mobile-overlay');
2018 + const toggleBtn = container.querySelector('.weather-toggle-btn');
2019 +
2020 + // 移除隐藏类
2021 + panel.classList.remove('hidden');
2022 +
2023 + if (utils.isMobile()) {
2024 + // 移动端特殊处理
2025 + panel.classList.add('weather-panel-mobile-active');
2026 + overlay.classList.add('weather-mobile-overlay-active');
2027 + // 按钮在下层
2028 + toggleBtn.style.zIndex = '10001';
2029 + // 禁用背景滚动
2030 + document.body.style.overflow = 'hidden';
2031 + } else {
2032 + // PC端:使用requestAnimationFrame确保动画正确触发
2033 + // 先强制重绘,确保初始状态被应用
2034 + panel.offsetHeight;
2035 + // 然后添加激活类以触发展开动画
2036 + panel.classList.add('weather-panel-active');
2037 + }
2038 +
2039 + console.log('显示天气面板');
2040 + },
2041 +
2042 + hidePanel() {
2043 + const container = document.getElementById('qweather-widget');
2044 + const panel = container.querySelector('.weather-panel');
2045 + const overlay = container.querySelector('.weather-mobile-overlay');
2046 + const toggleBtn = container.querySelector('.weather-toggle-btn');
2047 +
2048 + if (utils.isMobile()) {
2049 + this.hideMobilePanel();
2050 + } else {
2051 + // PC端:移除激活类以触发收缩动画
2052 + panel.classList.remove('weather-panel-active');
2053 +
2054 + // 等待过渡动画完成后完全隐藏
2055 + setTimeout(() => {
2056 + if (!panel.classList.contains('weather-panel-active')) {
2057 + panel.classList.add('hidden');
2058 + }
2059 + }, 300);
2060 + }
2061 +
2062 + console.log('隐藏天气面板');
2063 + },
2064 +
2065 + hideMobilePanel() {
2066 + const container = document.getElementById('qweather-widget');
2067 + const panel = container.querySelector('.weather-panel');
2068 + const overlay = container.querySelector('.weather-mobile-overlay');
2069 + const toggleBtn = container.querySelector('.weather-toggle-btn');
2070 +
2071 + panel.classList.remove('weather-panel-mobile-active');
2072 + panel.classList.add('hidden');
2073 + overlay.classList.remove('weather-mobile-overlay-active');
2074 +
2075 + // 恢复按钮z-index
2076 + toggleBtn.style.zIndex = '10001';
2077 +
2078 + // 恢复背景滚动
2079 + document.body.style.overflow = '';
2080 + },
2081 +
2082 + showCitySelectModal() {
2083 + const container = document.getElementById('qweather-widget');
2084 + const modal = container.querySelector('.city-select-modal');
2085 + modal.classList.remove('hidden');
2086 +
2087 + // 聚焦到位置输入框
2088 + setTimeout(() => {
2089 + container.querySelector('#location-coord-input').focus();
2090 + }, 100);
2091 + },
2092 +
2093 + hideCitySelectModal() {
2094 + const container = document.getElementById('qweather-widget');
2095 + const modal = container.querySelector('.city-select-modal');
2096 + modal.classList.add('hidden');
2097 + },
2098 +
2099 + // 新增:自动定位功能
2100 + async autoLocate(inModal = false) {
2101 + const container = document.getElementById('qweather-widget');
2102 + const autoLocateBtn = inModal ?
2103 + container.querySelector('.modal-auto-locate') :
2104 + container.querySelector('.weather-auto-locate-btn');
2105 +
2106 + // 保存原始按钮状态
2107 + const originalHTML = autoLocateBtn.innerHTML;
2108 + autoLocateBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i>';
2109 + autoLocateBtn.disabled = true;
2110 +
2111 + try {
2112 + this.setLoading(true);
2113 + const locationInfo = await utils.autoLocate();
2114 +
2115 + if (inModal) {
2116 + // 在模态框中自动填写坐标
2117 + container.querySelector('#location-name-input').value = locationInfo.locationName;
2118 + container.querySelector('#location-coord-input').value = locationInfo.location;
2119 + } else {
2120 + // 直接更新天气数据
2121 + weatherData.currentLocation = {
2122 + location: locationInfo.location,
2123 + locationName: locationInfo.locationName
2124 + };
2125 + await weatherApp.loadWeatherData();
2126 + }
2127 +
2128 + console.log('自动定位成功:', locationInfo);
2129 +
2130 + } catch (error) {
2131 + console.error('自动定位失败:', error);
2132 + alert(`自动定位失败: ${error.message}`);
2133 + } finally {
2134 + // 恢复按钮状态
2135 + autoLocateBtn.innerHTML = originalHTML;
2136 + autoLocateBtn.disabled = false;
2137 + this.setLoading(false);
2138 + }
2139 + },
2140 +
2141 + updateUI(data) {
2142 + const container = document.getElementById('qweather-widget');
2143 +
2144 + // 更新基础信息
2145 + container.querySelector('.city-name').textContent = data.location;
2146 + container.querySelector('.temperature').textContent = `${data.temperature}°`;
2147 + container.querySelector('.high-temp').textContent = `${data.todayHigh}°`;
2148 + container.querySelector('.low-temp').textContent = `${data.todayLow}°`;
2149 + container.querySelector('.weather-description').textContent = data.description;
2150 + container.querySelector('#wind-speed').textContent = `${data.windSpeed} km/h`;
2151 + container.querySelector('#humidity').textContent = `${data.humidity}%`;
2152 + container.querySelector('#feels-like').textContent = `${data.feelsLike}°`;
2153 + container.querySelector('.update-time').textContent = `更新于: ${data.updateTime}`;
2154 +
2155 + const iconElement = container.querySelector('.weather-icon i');
2156 + iconElement.className = data.icon;
2157 +
2158 + // 更新24小时预报和日出日落时间
2159 + this.updateHourlyForecast(data.hourlyForecast, data.sunriseTime, data.sunsetTime);
2160 +
2161 + // 更新7天预报
2162 + this.updateDailyForecast(data.dailyForecast);
2163 +
2164 + // 显示模拟数据标识
2165 + const mockIndicator = container.querySelector('.mock-indicator');
2166 + if (data.isMock) {
2167 + mockIndicator.classList.remove('hidden');
2168 + } else {
2169 + mockIndicator.classList.add('hidden');
2170 + }
2171 +
2172 + // 触发AI建议获取
2173 + this.getAIAdvice(data);
2174 + },
2175 +
2176 + // 新增:获取AI建议
2177 + async getAIAdvice(weatherData) {
2178 + this.setAIAdviceLoading(true);
2179 +
2180 + try {
2181 + const adviceData = await utils.getAIAdvice(weatherData);
2182 + this.updateAIAdvice(adviceData);
2183 + console.log('AI建议更新完成:', adviceData);
2184 + } catch (error) {
2185 + console.error('获取AI建议失败:', error);
2186 + // 使用默认建议作为降级
2187 + const defaultAdvice = utils.getDefaultAdvice(weatherData);
2188 + this.updateAIAdvice(defaultAdvice);
2189 + } finally {
2190 + this.setAIAdviceLoading(false);
2191 + }
2192 + },
2193 +
2194 + setLoading(loading) {
2195 + const container = document.getElementById('qweather-widget');
2196 + const panel = container.querySelector('.weather-panel');
2197 +
2198 + if (loading) {
2199 + panel.classList.add('weather-loading');
2200 + } else {
2201 + panel.classList.remove('weather-loading');
2202 + }
2203 + }
2204 + };
2205 +
2206 + // 主应用 - 保持原有代码100%不变
2207 + const weatherApp = {
2208 + async init() {
2209 + // 确保Font Awesome已加载
2210 + await this.loadFontAwesome();
2211 +
2212 + weatherUI.init();
2213 +
2214 + // 新增:如果启用了自动定位且没有保存的位置,尝试自动定位
2215 + const settings = utils.loadSettings();
2216 + if (weatherConfig.enableAutoLocation &&
2217 + (settings.location === weatherConfig.defaultLocation ||
2218 + !localStorage.getItem('weather_location'))) {
2219 + try {
2220 + console.log('尝试自动定位...');
2221 + const locationInfo = await utils.autoLocate();
2222 + weatherData.currentLocation = {
2223 + location: locationInfo.location,
2224 + locationName: locationInfo.locationName
2225 + };
2226 + console.log('初始化自动定位成功:', locationInfo);
2227 + } catch (error) {
2228 + console.warn('初始化自动定位失败,使用默认位置:', error);
2229 + }
2230 + }
2231 +
2232 + await this.loadWeatherData();
2233 +
2234 + if (weatherConfig.autoRefresh) {
2235 + setInterval(() => this.loadWeatherData(), weatherConfig.refreshInterval);
2236 + }
2237 + },
2238 +
2239 + async loadFontAwesome() {
2240 + if (document.querySelector('link[href*="font-awesome"]')) return;
2241 +
2242 + return new Promise((resolve, reject) => {
2243 + const link = document.createElement('link');
2244 + link.rel = 'stylesheet';
2245 + link.href = 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css';
2246 + link.onload = resolve;
2247 + link.onerror = reject;
2248 + document.head.appendChild(link);
2249 + });
2250 + },
2251 +
2252 + async loadWeatherData() {
2253 + weatherUI.setLoading(true);
2254 +
2255 + try {
2256 + const data = await weatherData.fetchWeatherData();
2257 + weatherData.current = data;
2258 + weatherUI.updateUI(data);
2259 +
2260 + } catch (error) {
2261 + console.error('和风天气数据加载失败,使用模拟数据:', error);
2262 +
2263 + // 使用模拟数据作为降级方案
2264 + const mockData = weatherData.getMockData();
2265 + weatherUI.updateUI(mockData);
2266 + } finally {
2267 + weatherUI.setLoading(false);
2268 + }
2269 + },
2270 +
2271 + refreshWeather() {
2272 + this.loadWeatherData();
2273 + }
2274 + };
2275 +
2276 + // 初始化
2277 + if (document.readyState === 'loading') {
2278 + document.addEventListener('DOMContentLoaded', () => weatherApp.init());
2279 + } else {
2280 + weatherApp.init();
2281 + }
2282 + })();
Più nuovi Più vecchi