天气模块修改

>> ·[FIX]以韩小韩API替代失效的教书先生API
>> ·[ADD]添加腾讯位置服务支持,以解决高德接口次数不足以及不支持 IPV6 的问题
>> ·[ADD]修改天气模块策略,在一个接口失效时切换另一个接口
>> ·[FIX]修复天气模块中的错误,处理气温平均数时自动删除多余符号单位,处理分量时先判断是否携带级字再考虑添加
This commit is contained in:
NanoRocky 2025-04-04 01:19:48 +08:00
parent 74c7558d91
commit 5ae2d86855
6 changed files with 867 additions and 545 deletions

View File

@ -22,12 +22,13 @@ VITE_DESC_TEXT_OTHER = "哎呀,这都被你发现了( 再点击一次可关
## 网站链接的图标名称可前往 https://www.xicons.org 自行挑选并在 src/components/Links/index.vue 中引入
# 天气 Key
## 请前往 高德开放平台注册 Web服务 Key
## 请注意不是 Web端 (JS API),免费申请,每日上限 5000 次
## 此处提供的服务可能会超量从而无法访问,请自行申请!请自行申请!请自行申请!
## 若此处设为空则调用 教书先生 API https://api.oioweb.cn/doc/weather/GetWeather (已失效)
## 备注:如需使用天气,请自行获取 token 。几乎所有的天气api都需要token并且限制频率或收费。
VITE_WEATHER_KEY = ""
## 请前往 腾讯位置服务 [https://lbs.qq.com/] 或 高德开放平台 [https://lbs.amap.com/] 注册并获得 Web 服务端(WebServiceAPI) Key
## 请注意不是 Web端 (JS API),免费申请。腾讯每日上限 10000 次,高德每日上限 5000 次。
## 目前首推腾讯位置服务,因为高德的 API 不支持 IPV6需要付费找客服额外开通高级版本在有 IPV6 的网络环境会出现 IP 定位异常。
## 可以同时填写两个服务的 Key若一个服务无法访问则会自动切换到另一个服务。
## 若此处设为空则调用 韩小韩 API 与 教书先生 API此处提供的服务可能会超量从而无法访问请自行申请请自行申请请自行申请
VITE_TX_WEATHER_KEY = "" # 腾讯位置服务 Key
VITE_GD_WEATHER_KEY = "" # 高德开放平台 Key
# 建站日期
## 若不需要,请设为空即可
@ -57,4 +58,12 @@ VITE_SONG_ID = "9379831714"
## (更多参数设置可以修改 \src\utils\speech.js 或自行补充)
VITE_TTS_API = ""
VITE_TTS_Voice = "zh-CN-YunxiaNeural"
VITE_TTS_Style = "cheerful"
VITE_TTS_Style = "cheerful"
# 鉴权参数
## 这是一些用于鉴权用的 Key正常只有 VITE_TX_WEATHER_SKEY 有效。其它高级功能如需使用请自行配置功能!
## 腾讯位置服务鉴权 SKEY 是签名校验 KEY不强制要求。如需使用可在控制台生成密钥并填写在这里填写将启用鉴权模式。
VITE_TX_WEATHER_SKEY = "" # 腾讯位置服务鉴权SKEY
VITE_TTS_SKEY = "" # 文字转语音鉴权 SKEY (自行配置)
VITE_METING_SKEY = "" # METING API 鉴权 SKEY (自行配置)
VITE_SFILE_SKEY = "" # 特殊文件 API 鉴权 SKEY (自行配置)

View File

@ -7,7 +7,7 @@
"home": "https://imsyy.top",
"efuh": "https://nanorocky.top",
"private": true,
"version": "4.2.1 [EFU]",
"version": "4.2.2 [EFU]",
"type": "module",
"scripts": {
"dev": "vite --host",
@ -24,12 +24,13 @@
"element-plus": "^2.9.7",
"fetch-jsonp": "^1.3.0",
"jparticles": "^3.5.0",
"js-md5": "^0.8.3",
"lodash-es": "^4.17.21",
"pinia": "^3.0.1",
"pinia-plugin-persistedstate": "^4.2.0",
"pinia-plugin-persistedstate-2": "^2.0.29",
"pinia-plugin-persistedstate-2": "^2.0.30",
"swiper": "^11.2.6",
"three": "^0.174.0",
"three": "^0.175.0",
"vue": "^3.5.13",
"vuex": "^4.1.0"
},
@ -42,16 +43,17 @@
"@vicons/tabler": "^0.13.0",
"@vicons/utils": "^0.1.4",
"@vitejs/plugin-vue": "^5.2.3",
"eslint": "^9.22.0",
"eslint-plugin-vue": "^9.33.0",
"eslint": "^9.23.0",
"eslint-plugin-vue": "^10.0.0",
"vue-eslint-parser": "^10.1.1",
"prettier": "^3.5.3",
"sass": "^1.86.0",
"sass": "^1.86.2",
"terser": "^5.39.0",
"unplugin-auto-import": "^19.1.1",
"unplugin-auto-import": "^19.1.2",
"unplugin-vue-components": "^28.4.1",
"vite": "^6.2.2",
"vite": "^6.2.5",
"vite-plugin-compression2": "^1.3.3",
"vite-plugin-pwa": "^0.21.1"
"vite-plugin-pwa": "^1.0.0"
},
"pnpm": {
"overrides": {
@ -59,6 +61,9 @@
"@jridgewell/sourcemap-codec": "^1.5.0",
"magic-string": "^0.30.17",
"workbox-build": "^7.3.0"
}
},
"ignoredBuiltDependencies": [
"esbuild"
]
}
}

905
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,28 @@
// import axios from "axios";
import fetchJsonp from "fetch-jsonp";
import { gwg } from "@/utils/authServer";
/**
* JSONP 请求模块
*/
// JSONP 请求函数,并返回 JSON 【关于为什么要有这个呢...请腾讯自觉扫一下x
const loadJSONP = (url, callbackName) => {
return new Promise((resolve, reject) => {
// 定义 JSONP 回调函数
window[callbackName] = (data) => {
resolve(data); // 解析 JSON 数据
delete window[callbackName]; // 清理全局变量,防止污染
};
// 创建 script 标签
const script = document.createElement('script');
script.src = url;
script.onerror = () => {
reject(new Error('JSONP 请求失败'));
delete window[callbackName]; // 出错时也要清理
};
document.body.appendChild(script);
});
};
/**
* 音乐播放器
@ -52,18 +75,72 @@ export const getHitokoto = async () => {
/**
* 天气
*/
// 获取腾讯地理位置信息JSONP 方式)
export const getTXAdcode = async (key) => {
const callback = `jsonpCallback_${Date.now()}_${Math.floor(Math.random() * 100000)}`;
const url = `https://apis.map.qq.com/ws/location/v1/ip?key=${key}&output=jsonp&callback=${callback}`;
return await loadJSONP(url, callback);
};
// 获取腾讯地理天气信息JSONP 方式)
export const getTXWeather = async (key, adcode) => {
const callback = `jsonpCallback_${Date.now()}_${Math.floor(Math.random() * 100000)}`;
const url = `https://apis.map.qq.com/ws/weather/v1/?key=${key}&adcode=${adcode}&type=now&output=jsonp&callback=${callback}`;
return await loadJSONP(url, callback);
};
// 获取腾讯地理位置信息(鉴权模式 JSONP 方式)
export const getTXAdcodeS = async (key, skey) => {
const callback = `jsonpCallback_${Date.now()}_${Math.floor(Math.random() * 100000)}`;
const url = `https://apis.map.qq.com/ws/location/v1/ip?key=${key}&output=jsonp&callback=${callback}`;
const urls = await gwg(url, skey);
return await loadJSONP(urls, callback);
};
// 获取腾讯地理天气信息(鉴权模式 JSONP 方式)
export const getTXWeatherS = async (key, adcode, skey) => {
const callback = `jsonpCallback_${Date.now()}_${Math.floor(Math.random() * 100000)}`;
const url = `https://apis.map.qq.com/ws/weather/v1/?key=${key}&adcode=${adcode}&type=now&output=jsonp&callback=${callback}`;
const urls = await gwg(url, skey);
return await loadJSONP(urls, callback);
};
// 获取高德地理位置信息
export const getAdcode = async (key) => {
export const getGDAdcode = async (key) => {
const res = await fetch(`https://restapi.amap.com/v3/ip?key=${key}`);
return await res.json();
};
// 获取高德地理位置信息带IP
export const getGDAdcodeI = async (ipv4, key) => {
const res = await fetch(`https://restapi.amap.com/v3/ip?ip=${ipv4}&key=${key}`);
return await res.json();
};
// 获取高德地理天气信息
export const getWeather = async (key, city) => {
const res = await fetch(
`https://restapi.amap.com/v3/weather/weatherInfo?key=${key}&city=${city}`,
);
export const getGDWeather = async (key, city) => {
const res = await fetch(`https://restapi.amap.com/v3/weather/weatherInfo?key=${key}&city=${city}`);
return await res.json();
};
// 补充的获取 IPV4 地址的 API
export const getIPV4Addr = async () => {
const res = await fetch(`https://api4.ipify.org?format=json`);
return await res.json();
};
// 补充的获取 IPV6 地址的 API
export const getIPV6Addr = async () => {
const res = await fetch(`https://api6.ipify.org?format=json`);
return await res.json();
};
// 免 KEY 区域
// 强烈建议自己注册腾讯或高德的 API
// 获取韩小韩天气 API
export const getHXHWeather = async () => {
const res = await fetch("https://api.vvhan.com/api/weather");
return await res.json();
};
@ -74,27 +151,9 @@ export const getOtherWeather = async () => {
return await res.json();
};
// 这是一个待完善的新免 token 天气模块
// // 获取位置信息
// export const getLocation = async () => {
// const res = await fetch("http://inip.in/ip.json");
// return await res.json();
// };
// // 转换城市 ID
// export const getCityId = async (city) => {
// const res = await fetch(
// `https://api.songzixian.com/api/china-city?dataSource=LOCAL_CHINA_CITY&district=${city}`,
// );
// return await res.json();
// };
// // 获取小米天气 API
// export const getOtherWeather = async () => {
// const res = await fetch(
// `https://weatherapi.market.xiaomi.com/wtr-v3/weather/all?latitude=0&longitude=0&isLocated=true&locationKey=weathercn%3A${city}&days=2&appKey=weather20151024&sign=zUFJoAR2ZVrDy1vF3D07&locale=zh_cn&alpha=false&isGlobal=false`,
// );
// return await res.json();
// };
// 获取小米天气 API
// 这个接口或许会比上面两个稳的多,但是它需要自己定位并转换 Adcode ...
export const getXMWeather = async () => {
const res = await fetch(`https://weatherapi.market.xiaomi.com/wtr-v3/weather/all?latitude=0&longitude=0&isLocated=true&locationKey=weathercn%3A${city}&days=2&appKey=weather20151024&sign=zUFJoAR2ZVrDy1vF3D07&locale=zh_cn&alpha=false&isGlobal=false`);
return await res.json();
};

View File

@ -10,7 +10,9 @@
: weatherData.weather.winddirection + "风"
}} 
</span>
<span class="sm-hidden">{{ weatherData.weather.windpower }}&nbsp;</span>
<span class="sm-hidden">{{ weatherData.weather.windpower?.endsWith("级")
? weatherData.weather.windpower
: weatherData.weather.windpower + "级" }}&nbsp;</span>
</div>
<div class="weather" v-else>
<span>天气数据获取失败</span>
@ -18,14 +20,16 @@
</template>
<script setup>
import { getAdcode, getWeather, getOtherWeather } from "@/api";
import { getTXAdcode, getTXWeather, getTXAdcodeS, getTXWeatherS, getGDAdcode, getGDAdcodeI, getGDWeather, getIPV4Addr, getIPV6Addr, getOtherWeather, getHXHWeather, getXMWeather } from "@/api";
import { Error } from "@icon-park/vue-next";
import { mainStore } from "@/store";
import { Speech, stopSpeech, SpeechLocal } from "@/utils/speech";
const store = mainStore();
// Key
const mainKey = import.meta.env.VITE_WEATHER_KEY;
//
const txkey = import.meta.env.VITE_TX_WEATHER_KEY; //
const txskey = import.meta.env.VITE_TX_WEATHER_SKEY; //
const gdkey = import.meta.env.VITE_GD_WEATHER_KEY; //
//
const weatherData = reactive({
@ -44,8 +48,12 @@ const weatherData = reactive({
//
const getTemperature = (min, max) => {
try {
//
const average = (Number(min) + Number(max)) / 2;
const cleanMin = parseFloat(min.toString().replace(/[^\d.-]/g, ""));
const cleanMax = parseFloat(max.toString().replace(/[^\d.-]/g, ""));
if (isNaN(cleanMin) || isNaN(cleanMax)) {
throw new Error("无法解析温度数据");
};
const average = (cleanMin + cleanMax) / 2;
return Math.round(average);
} catch (error) {
console.error("计算温度出现错误:", error);
@ -59,51 +67,228 @@ const getTemperature = (min, max) => {
}
};
const getTXW = async () => {
if (!txskey) {
console.log("正在使用腾讯天气接口");
// Adcode
const adCode = await getTXAdcode(txkey);
if (String(adCode.status) !== "0") {
if (store.webSpeech) {
stopSpeech();
const voice = import.meta.env.VITE_TTS_Voice;
const vstyle = import.meta.env.VITE_TTS_Style;
SpeechLocal("位置信息获取失败.mp3");
};
throw "地区查询失败";
};
weatherData.adCode = {
city: adCode.result.ad_info.district || adCode.result.ad_info.city || adCode.result.ad_info.province || "未知地区",
adcode: adCode.result.ad_info.adcode,
};
//
const txWeather = await getTXWeather(txkey, weatherData.adCode.adcode);
if (String(txWeather.status) !== "0") {
if (store.webSpeech) {
stopSpeech();
const voice = import.meta.env.VITE_TTS_Voice;
const vstyle = import.meta.env.VITE_TTS_Style;
SpeechLocal("天气加载失败.mp3");
};
throw "天气信息获取失败";
};
const realtimeData = txWeather.result.realtime?.[0];
if (!realtimeData?.infos) {
if (store.webSpeech) {
stopSpeech();
const voice = import.meta.env.VITE_TTS_Voice;
const vstyle = import.meta.env.VITE_TTS_Style;
SpeechLocal("天气加载失败.mp3");
};
throw "天气信息获取失败";
};
weatherData.weather = {
weather: realtimeData.infos.weather,
temperature: realtimeData.infos.temperature,
winddirection: realtimeData.infos.wind_direction,
windpower: realtimeData.infos.wind_power,
};
} else {
console.log("正在使用腾讯天气接口,鉴权模式已启用");
// Adcode
const adCode = await getTXAdcodeS(txkey, txskey);
if (String(adCode?.status) !== "0") {
if (store.webSpeech) {
stopSpeech();
const voice = import.meta.env.VITE_TTS_Voice;
const vstyle = import.meta.env.VITE_TTS_Style;
SpeechLocal("位置信息获取失败.mp3");
};
throw "地区查询失败";
};
weatherData.adCode = {
city: adCode.result.ad_info.district || adCode.result.ad_info.city || adCode.result.ad_info.province || "未知地区",
adcode: adCode.result.ad_info.adcode,
};
//
const txWeather = await getTXWeatherS(txkey, weatherData.adCode.adcode, txskey);
if (String(txWeather.status) !== "0") {
if (store.webSpeech) {
stopSpeech();
const voice = import.meta.env.VITE_TTS_Voice;
const vstyle = import.meta.env.VITE_TTS_Style;
SpeechLocal("天气加载失败.mp3");
};
throw "天气信息获取失败";
};
const realtimeData = txWeather.result.realtime?.[0];
if (!realtimeData?.infos) {
if (store.webSpeech) {
stopSpeech();
const voice = import.meta.env.VITE_TTS_Voice;
const vstyle = import.meta.env.VITE_TTS_Style;
SpeechLocal("天气加载失败.mp3");
};
throw "天气信息获取失败";
};
weatherData.weather = {
weather: realtimeData.infos.weather,
temperature: realtimeData.infos.temperature,
winddirection: realtimeData.infos.wind_direction,
windpower: realtimeData.infos.wind_power,
};
};
};
const getGDW = async () => {
// Adcode
const adCode = await getGDAdcode(gdkey);
let adCodei = null;
if (String(adCode?.infocode) !== "10000" || String(adCode?.status) !== "1") {
console.log("检测到高德接口 IP 获取失败,调用额外接口获取 IPV4 地址...");
const ipV4addr = await getIPV4Addr();
adCodei = await getGDAdcodeI(ipV4addr.ip, gdkey);
if (String(adCodei?.infocode) !== "10000" || String(adCodei?.status) !== "1") {
if (store.webSpeech) {
stopSpeech();
const voice = import.meta.env.VITE_TTS_Voice;
const vstyle = import.meta.env.VITE_TTS_Style;
SpeechLocal("位置信息获取失败.mp3");
};
throw "地区查询失败";
};
};
if (!adCodei) {
weatherData.adCode = {
city: adCode.city || adCode.province || "未知地区",
adcode: adCode.adcode,
};
} else {
weatherData.adCode = {
city: adCodei.city || adCodei.province || "未知地区",
adcode: adCodei.adcode,
};
};
//
const result = await getGDWeather(gdkey, weatherData.adCode.adcode);
if (String(result?.status) !== "1" || String(result?.infocode) !== "10000") {
if (store.webSpeech) {
stopSpeech();
const voice = import.meta.env.VITE_TTS_Voice;
const vstyle = import.meta.env.VITE_TTS_Style;
SpeechLocal("天气加载失败.mp3");
};
throw "天气信息获取失败";
};
weatherData.weather = {
weather: result.lives[0].weather,
temperature: result.lives[0].temperature,
winddirection: result.lives[0].winddirection,
windpower: result.lives[0].windpower,
};
};
const getOW = async () => {
const result = await getOtherWeather();
const data = result.result;
weatherData.adCode = {
city: data.city.City || "未知地区",
};
weatherData.weather = {
weather: data.condition.day_weather,
temperature: getTemperature(data.condition.min_degree, data.condition.max_degree),
winddirection: data.condition.day_wind_direction,
windpower: data.condition.day_wind_power,
};
};
const getHXHW = async () => {
const result = await getHXHWeather();
if (String(result?.success) !== "true") {
if (store.webSpeech) {
stopSpeech();
const voice = import.meta.env.VITE_TTS_Voice;
const vstyle = import.meta.env.VITE_TTS_Style;
SpeechLocal("天气加载失败.mp3");
};
throw "天气信息获取失败";
};
weatherData.adCode = {
city: result.city || "未知地区",
};
weatherData.weather = {
weather: result.data.type || result.data.night.type,
temperature: getTemperature(result.data.low || result.data.night.low, result.data.high || result.data.night.high),
winddirection: result.data.fengxiang || result.data.night.fengxiang,
windpower: result.data.fengli || result.data.night.fengli,
};
};
const getXMW = async () => {
//
};
//
const getWeatherData = async () => {
try {
//
if (!mainKey) {
console.log("未配置,使用备用天气接口");
const result = await getOtherWeather();
console.log(result);
const data = result.result;
weatherData.adCode = {
city: data.city.City || "未知地区",
// adcode: data.city.cityId,
if (!gdkey && !txkey) {
console.log("未配置天气接口密钥,使用备用天气接口");
try {
await getHXHW();
} catch (error) {
await getOW();
};
weatherData.weather = {
weather: data.condition.day_weather,
temperature: getTemperature(data.condition.min_degree, data.condition.max_degree),
winddirection: data.condition.day_wind_direction,
windpower: data.condition.day_wind_power,
} else if (!txkey) {
// API
console.log("正在使用高德天气接口");
try {
await getGDW();
} catch (error) {
console.error("高德天气接口获取失败,尝试调用备用接口");
try {
await getHXHW();
} catch (error) {
await getOW();
};
};
} else {
// Adcode
const adCode = await getAdcode(mainKey);
console.log(adCode);
if (adCode.infocode !== "10000") {
if (store.webSpeech) {
stopSpeech();
const voice = import.meta.env.VITE_TTS_Voice;
const vstyle = import.meta.env.VITE_TTS_Style;
SpeechLocal("位置信息获取失败.mp3");
// API
try {
await getTXW();
} catch (error) {
console.error("腾讯天气接口获取失败,尝试使用高德天气接口");
try {
await getGDW();
} catch (error) {
console.error("高德天气接口获取失败,尝试调用备用接口");
try {
await getHXHW();
} catch (error) {
await getOW();
};
};
throw "地区查询失败";
}
weatherData.adCode = {
city: adCode.city,
adcode: adCode.adcode,
};
//
const result = await getWeather(mainKey, weatherData.adCode.adcode);
weatherData.weather = {
weather: result.lives[0].weather,
temperature: result.lives[0].temperature,
winddirection: result.lives[0].winddirection,
windpower: result.lives[0].windpower,
};
}
};
} catch (error) {
console.error("天气信息获取失败:" + error);
onError("天气信息获取失败");
@ -113,7 +298,7 @@ const getWeatherData = async () => {
const vstyle = import.meta.env.VITE_TTS_Style;
SpeechLocal("天气加载失败.mp3");
};
}
};
};
//

71
src/utils/authServer.js Normal file
View File

@ -0,0 +1,71 @@
/**
* authServer
* Made by NanoRocky
* 这个模块除了腾讯地图的签名认证外其余函数并不对其他人有任何鸟用大概叭
* 涉及到接口加密的东西怎么能不整点代码混淆对叭对叭~x
* 该说不说写点答辩还挺有意思嘟bushi
*/
import { md5 as d } from "js-md5";
let x = null, y = null;
const f = () => Math.floor(Date.now() / 1000);
const o = (v) => v.toString(16);
async function gst() {
if (!x) {
try {
const { timestamp: t } = await (await fetch("https://nanorocky.top/time/")).json();
x = t;
y = f();
} catch (error) {
x = y = f();
};
};
return x + (f() - y);
};
export async function gwp(u, s, b) {
// 备注POST 方法需要传入 body
const { origin: ul, pathname: p } = new URL(u);
return `${ul}${p}?sig=${d(`${p}?${Object.keys(b).sort().map(key => `${key}=${JSON.stringify(b[key])}`).join("&")}${s}`).toLowerCase()}`;
};
export async function gwg(u, s) {
const { origin: ul, pathname: p } = new URL(u), q = new URLSearchParams(new URL(u).search);
q.set("sig", d(`${p}?${[...q.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([key, value]) => `${key}=${value}`).join("&")}${s}`).toLowerCase());
return `${ul}${p}?${q.toString()}`;
};
export async function gasA(p, s) {
const t = await gst(), r = Math.random().toString(36).substring(2, 12);
return [t, r, "0", d(`${p}-${t}-${r}-0-${s}`)].join("-");
};
export async function gasB(u, s) {
const { origin: ul, pathname: p } = new URL(u);
const t = new Date((await gst()) * 1000 + 8 * 3600 * 1000).toISOString().replace(/[-T:]|(\..*)/g, "").substring(0, 12);
return `${ul}/${t}/${d(`${s}${t}/${p}`)}/${p}`;
};
export async function gasC(u, s) {
const { origin: ul, pathname: p } = new URL(u);
const t = o((await gst()));
return `${ul}/${d(`${s}/${p}${t}`)}/${t}/${p}`;
};
export async function gasDH(u, s) {
const ul = new URL(u), p = ul.pathname, t = await gst();
ul.searchParams.set("sign", d(`${s}/${p}${t}`));
ul.searchParams.set("t", t);
return ul.toString();
};
export async function gasDI(u, s) {
const ul = new URL(u), p = ul.pathname, t = o(await gst());
ul.searchParams.set("sign", d(`${s}/${p}${t}`));
ul.searchParams.set("t", t);
return ul.toString();
};