// ==UserScript==
// @name JKCXSN-三角洲行动-主页天气
// @namespace https://scriptcat.org/
// @version 1.3
// @description 在主页右侧添加本地天气卡片(仅展示,无查询功能),优化图标文字间距
// @author sulang
// @match https://www.jkcxsn.cn/
// @grant none
// @run-at document-end
// ==/UserScript==
(function() {
'use strict';
// 和风天气配置(建议替换为自己的KEY)
const API_KEY = "35459b2323b940b6b823a00efcac9330";
const CITY_LOOKUP_URL = "https://geoapi.qweather.com/v2/city/lookup";
const WEATHER_URL = "https://devapi.qweather.com/v7/weather/now";
// 天气图标映射
const WEATHER_ICON_MAP = {
'100': 'fa-sun-o', '101': 'fa-cloud', '102': 'fa-cloud', '103': 'fa-cloud', '104': 'fa-cloud',
'200': 'fa-bolt', '201': 'fa-bolt', '202': 'fa-bolt',
'300': 'fa-tint', '301': 'fa-tint', '302': 'fa-tint', '303': 'fa-tint',
'310': 'fa-snowflake-o', '311': 'fa-snowflake-o', '312': 'fa-snowflake-o', '313': 'fa-snowflake-o', '314': 'fa-tint',
'400': 'fa-cloud', '401': 'fa-cloud', '402': 'fa-cloud', '406': 'fa-cloud', '407': 'fa-cloud', '408': 'fa-cloud', '409': 'fa-cloud',
'500': 'fa-thermometer-half', '501': 'fa-thermometer-full', '999': 'fa-question'
};
// 1. 创建天气卡片容器(统一图标文字间距)
function createWeatherCard() {
const card = document.createElement('div');
card.className = 'ui segment';
card.style.cssText = `
width: 100%;
max-width: 320px;
background: rgba(255,255,255,0.95);
border-radius: 12px;
padding: 16px;
box-shadow: 0 4px 20px rgba(0,0,0,0.08);
margin-top: 16px;
`;
card.innerHTML = `
正在获取位置...
-Relink 技术
`;
return card;
}
// 2. 插入到右侧栏
function insertCard() {
// 兼容不同的页面结构
let rightColumn = document.querySelector('.ui.grid > .column:last-child');
if (!rightColumn) {
rightColumn = document.querySelector('.right.column');
}
if (!rightColumn) {
// 如果找不到右侧栏,创建一个固定在页面右侧的容器
rightColumn = document.createElement('div');
rightColumn.style.cssText = `
position: fixed;
top: 100px;
right: 20px;
z-index: 9999;
`;
document.body.appendChild(rightColumn);
}
const weatherCard = createWeatherCard();
rightColumn.insertBefore(weatherCard, rightColumn.firstChild);
return weatherCard;
}
// 3. 安全的Fetch请求(增加超时和错误处理)
async function safeFetch(url, timeout = 10000) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
try {
const res = await fetch(url, { signal: controller.signal });
clearTimeout(timeoutId);
// 检查响应状态
if (!res.ok) throw new Error(`请求失败 (${res.status})`);
// 尝试解析JSON,失败则返回空
try {
return await res.json();
} catch (e) {
throw new Error('数据格式错误');
}
} catch (e) {
clearTimeout(timeoutId);
if (e.name === 'AbortError') throw new Error('请求超时');
throw e;
}
}
// 4. 获取地理位置
function getLocation() {
return new Promise((resolve, reject) => {
if (!navigator.geolocation) reject(new Error('浏览器不支持定位'));
navigator.geolocation.getCurrentPosition(
pos => resolve({ lat: pos.coords.latitude, lon: pos.coords.longitude }),
err => {
let msg = '获取位置失败';
if (err.code === 1) msg = '您拒绝了位置权限';
else if (err.code === 2) msg = '位置信息不可用';
else if (err.code === 3) msg = '获取位置超时';
reject(new Error(msg));
},
{ timeout: 10000, enableHighAccuracy: false }
);
});
}
// 5. 根据经纬度获取城市ID
async function getCityId(lon, lat) {
const url = new URL(CITY_LOOKUP_URL);
url.searchParams.append('key', API_KEY);
url.searchParams.append('location', `${lon},${lat}`);
url.searchParams.append('range', 'cn');
url.searchParams.append('lang', 'zh');
const data = await safeFetch(url);
if (data.code !== '200' || !data.location?.length) throw new Error('未找到对应城市');
return data.location[0];
}
// 6. 获取天气数据
async function getWeather(cityId) {
const url = new URL(WEATHER_URL);
url.searchParams.append('key', API_KEY);
url.searchParams.append('location', cityId);
const data = await safeFetch(url);
if (data.code !== '200') throw new Error(`获取天气失败 (${data.code})`);
return data;
}
// 7. 更新天气UI
function updateUI(card, city, weather) {
const loading = card.querySelector('#weather-loading');
const content = card.querySelector('#weather-content');
loading.style.display = 'none';
content.style.display = 'block';
// 格式化时间
const updateTime = weather.updateTime ?
`${weather.updateTime.slice(0, 10)} ${weather.updateTime.slice(11, 16)}` :
'未知时间';
// 填充数据(增加空值处理)
card.querySelector('#weather-city').textContent = city.name || '未知城市';
card.querySelector('#weather-time').textContent = `更新于: ${updateTime}`;
card.querySelector('#weather-temp').textContent = `${weather.now?.temp || '--'}℃`;
card.querySelector('#weather-text').textContent = weather.now?.text || '未知';
card.querySelector('#weather-feels').textContent = `${weather.now?.feelsLike || '--'}℃`;
card.querySelector('#weather-windDir').textContent = weather.now?.windDir || '未知';
card.querySelector('#weather-wind').textContent = `${weather.now?.windScale || '--'}级 / ${weather.now?.windSpeed || '--'}km/h`;
card.querySelector('#weather-humidity').textContent = `${weather.now?.humidity || '--'}%`;
// 设置图标
const icon = WEATHER_ICON_MAP[weather.now?.icon] || 'fa-question';
card.querySelector('#weather-icon').innerHTML = ``;
}
// 8. 显示错误
function showError(card, msg) {
const loading = card.querySelector('#weather-loading');
const error = card.querySelector('#weather-error');
loading.style.display = 'none';
error.style.display = 'block';
card.querySelector('#weather-error-msg').textContent = msg;
}
// 9. 主流程(增加重试机制)
async function init() {
const card = insertCard();
if (!card) return;
let retryCount = 0;
const maxRetry = 2;
while (retryCount < maxRetry) {
try {
const { lon, lat } = await getLocation();
card.querySelector('#weather-loading').innerHTML = '正在获取城市...';
const city = await getCityId(lon, lat);
card.querySelector('#weather-loading').innerHTML = '正在获取天气...';
const weather = await getWeather(city.id);
updateUI(card, city, weather);
return; // 成功则退出
} catch (err) {
retryCount++;
if (retryCount >= maxRetry) {
showError(card, err.message);
return;
}
// 重试前短暂延迟
await new Promise(resolve => setTimeout(resolve, 1000));
}
}
}
// 引入Font Awesome(如果页面已有则忽略)
if (!document.querySelector('link[href*="font-awesome"]')) {
const fa = document.createElement('link');
fa.rel = 'stylesheet';
fa.href = 'https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css';
document.head.appendChild(fa);
}
// 延迟执行,确保页面加载完成
setTimeout(init, 800);
})();