最後活躍 4 days ago

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

修訂 de54070f279c7ca420f92b15458df2e26851da1e

tq.js 原始檔案
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个方面提供具体、实用的建议,要求:
5231. 个性化定制:结合用户年龄、性别、通勤方式
5242. 数据融合:综合温度、湿度、风速、降水等多种要素
5253. 趋势提醒:结合短期预报给出动态建议
5264. 情景化:具体到出行、着装、健康等实际场景
5275. 简洁实用:每个建议控制在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})();
详细文档 原始檔案
1// 智能天气插件文档:https://www.yuque.com/shanshanerchuan-leyrb/wsa1gw/fiple6ihym1gy4b8
21. 获取API密钥
3 ○ 注册和风天气开发者账号获取API Key
4 ○ 注册高德地图开放平台获取API Key
5 ○ (可选)配置AI服务API(采用open AI标准,暂未测试过其他API)
62. 修改配置
7 ○ 将上述配置变量替换为您自己的API密钥
8 ○ 根据个人情况修改USER_PROFILE信息
93. 集成到项目
10 ○ 将完整JS代码添加到Sun-Panel项目中
11 ○ (具体添加方法请参考作者“红烧猎人”的“sun-panel-js-plugins”项目)
12 ○ 确保网络环境可访问相关API服务
配置说明.js 原始檔案
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 };