small-package/luci-app-nekobox/htdocs/nekobox/ping.php

968 lines
31 KiB
PHP

<?php
ob_start();
include './cfg.php';
$translate = [
'Argentina' => '阿根廷',
'Australia' => '澳大利亚',
'Austria' => '奥地利',
'Belgium' => '比利时',
'Brazil' => '巴西',
'Canada' => '加拿大',
'Chile' => '智利',
'China' => '中国',
'Colombia' => '哥伦比亚',
'Denmark' => '丹麦',
'Egypt' => '埃及',
'Finland' => '芬兰',
'France' => '法国',
'Germany' => '德国',
'Greece' => '希腊',
'Hong Kong' => '中国香港',
'India' => '印度',
'Indonesia' => '印度尼西亚',
'Iran' => '伊朗',
'Ireland' => '爱尔兰',
'Israel' => '以色列',
'Italy' => '意大利',
'Japan' => '日本',
'Kazakhstan' => '哈萨克斯坦',
'Kenya' => '肯尼亚',
'Macao' => '中国澳门',
'Malaysia' => '马来西亚',
'Mexico' => '墨西哥',
'Morocco' => '摩洛哥',
'The Netherlands' => '荷兰',
'New Zealand' => '新西兰',
'Nigeria' => '尼日利亚',
'Norway' => '挪威',
'Pakistan' => '巴基斯坦',
'Philippines' => '菲律宾',
'Poland' => '波兰',
'Portugal' => '葡萄牙',
'Russia' => '俄罗斯',
'Saudi Arabia' => '沙特阿拉伯',
'Singapore' => '新加坡',
'South Africa' => '南非',
'South Korea' => '韩国',
'Spain' => '西班牙',
'Sweden' => '瑞典',
'Switzerland' => '瑞士',
'Taiwan' => '中国台湾',
'Thailand' => '泰国',
'Turkey' => '土耳其',
'United Arab Emirates' => '阿拉伯联合酋长国',
'United Kingdom' => '英国',
'United States' => '美国',
'Vietnam' => '越南',
'Afghanistan' => '阿富汗',
'Albania' => '阿尔巴尼亚',
'Armenia' => '亚美尼亚',
'Bahrain' => '巴林',
'Bangladesh' => '孟加拉国',
'Barbados' => '巴巴多斯',
'Belarus' => '白俄罗斯',
'Bhutan' => '不丹',
'Bolivia' => '玻利维亚',
'Bosnia and Herzegovina' => '波斯尼亚和黑塞哥维那',
'Botswana' => '博茨瓦纳',
'Brunei' => '文莱',
'Bulgaria' => '保加利亚',
'Burkina Faso' => '布基纳法索',
'Burundi' => '布隆迪',
'Cambodia' => '柬埔寨',
'Cameroon' => '喀麦隆',
'Central African Republic' => '中非共和国',
'Chad' => '乍得',
'Comoros' => '科摩罗',
'Congo' => '刚果',
'Czech Republic' => '捷克共和国',
'Dominica' => '多米尼加',
'Dominican Republic' => '多米尼加共和国',
'Ecuador' => '厄瓜多尔',
'El Salvador' => '萨尔瓦多',
'Equatorial Guinea' => '赤道几内亚',
'Ethiopia' => '埃塞俄比亚',
'Fiji' => '斐济',
'Gabon' => '加蓬',
'Gambia' => '冈比亚',
'Georgia' => '格鲁吉亚',
'Ghana' => '加纳',
'Grenada' => '格林纳达',
'Guatemala' => '危地马拉',
'Guinea' => '几内亚',
'Guinea-Bissau' => '几内亚比绍',
'Haiti' => '海地',
'Honduras' => '洪都拉斯',
'Hungary' => '匈牙利',
'Iceland' => '冰岛',
'Jamaica' => '牙买加',
'Jordan' => '约旦',
'Kazakhstan' => '哈萨克斯坦',
'Kuwait' => '科威特',
'Kyrgyzstan' => '吉尔吉斯斯坦',
'Laos' => '老挝',
'Latvia' => '拉脱维亚',
'Lebanon' => '黎巴嫩',
'Lesotho' => '莱索托',
'Liberia' => '利比里亚',
'Libya' => '利比亚',
'Liechtenstein' => '列支敦士登',
'Lithuania' => '立陶宛',
'Luxembourg' => '卢森堡',
'Madagascar' => '马达加斯加',
'Malawi' => '马拉维',
'Maldives' => '马尔代夫',
'Mali' => '马里',
'Malta' => '马耳他',
'Mauritania' => '毛里塔尼亚',
'Mauritius' => '毛里求斯',
'Moldova' => '摩尔多瓦',
'Monaco' => '摩纳哥',
'Mongolia' => '蒙古',
'Montenegro' => '黑山',
'Morocco' => '摩洛哥',
'Mozambique' => '莫桑比克',
'Myanmar' => '缅甸',
'Namibia' => '纳米比亚',
'Nauru' => '瑙鲁',
'Nepal' => '尼泊尔',
'Nicaragua' => '尼加拉瓜',
'Niger' => '尼日尔',
'Nigeria' => '尼日利亚',
'North Korea' => '朝鲜',
'North Macedonia' => '北马其顿',
'Norway' => '挪威',
'Oman' => '阿曼',
'Pakistan' => '巴基斯坦',
'Palau' => '帕劳',
'Panama' => '巴拿马',
'Papua New Guinea' => '巴布亚新几内亚',
'Paraguay' => '巴拉圭',
'Peru' => '秘鲁',
'Philippines' => '菲律宾',
'Poland' => '波兰',
'Portugal' => '葡萄牙',
'Qatar' => '卡塔尔',
'Romania' => '罗马尼亚',
'Russia' => '俄罗斯',
'Rwanda' => '卢旺达',
'Saint Kitts and Nevis' => '圣基茨和尼维斯',
'Saint Lucia' => '圣卢西亚',
'Saint Vincent and the Grenadines' => '圣文森特和格林纳丁斯',
'Samoa' => '萨摩亚',
'San Marino' => '圣马力诺',
'Sao Tome and Principe' => '圣多美和普林西比',
'Saudi Arabia' => '沙特阿拉伯',
'Senegal' => '塞内加尔',
'Serbia' => '塞尔维亚',
'Seychelles' => '塞舌尔',
'Sierra Leone' => '塞拉利昂',
'Singapore' => '新加坡',
'Slovakia' => '斯洛伐克',
'Slovenia' => '斯洛文尼亚',
'Solomon Islands' => '所罗门群岛',
'Somalia' => '索马里',
'South Africa' => '南非',
'South Korea' => '韩国',
'South Sudan' => '南苏丹',
'Spain' => '西班牙',
'Sri Lanka' => '斯里兰卡',
'Sudan' => '苏丹',
'Suriname' => '苏里南',
'Sweden' => '瑞典',
'Switzerland' => '瑞士',
'Syria' => '叙利亚',
'Taiwan' => '中国台湾',
'Tajikistan' => '塔吉克斯坦',
'Tanzania' => '坦桑尼亚',
'Thailand' => '泰国',
'Timor-Leste' => '东帝汶',
'Togo' => '多哥',
'Tonga' => '汤加',
'Trinidad and Tobago' => '特立尼达和多巴哥',
'Tunisia' => '突尼斯',
'Turkey' => '土耳其',
'Turkmenistan' => '土库曼斯坦',
'Tuvalu' => '图瓦卢',
'Uganda' => '乌干达',
'Ukraine' => '乌克兰',
'United Arab Emirates' => '阿拉伯联合酋长国',
'United Kingdom' => '英国',
'United States' => '美国',
'Uruguay' => '乌拉圭',
'Uzbekistan' => '乌兹别克斯坦',
'Vanuatu' => '瓦努阿图',
'Vatican City' => '梵蒂冈',
'Venezuela' => '委内瑞拉',
'Vietnam' => '越南',
'Yemen' => '也门',
'Zambia' => '赞比亚',
'Zimbabwe' => '津巴布韦'
];
$lang = $_GET['lang'] ?? 'en';
?>
<!DOCTYPE html>
<html lang="<?php echo htmlspecialchars($lang); ?>">
<head>
<meta charset="utf-8">
<meta http-equiv="x-dns-prefetch-control" content="on">
<link rel="dns-prefetch" href="//cdn.jsdelivr.net">
<link rel="dns-prefetch" href="//whois.pconline.com.cn">
<link rel="dns-prefetch" href="//forge.speedtest.cn">
<link rel="dns-prefetch" href="//api-ipv4.ip.sb">
<link rel="dns-prefetch" href="//api.ipify.org">
<link rel="dns-prefetch" href="//api.ttt.sh">
<link rel="dns-prefetch" href="//qqwry.api.skk.moe">
<link rel="dns-prefetch" href="//d.skk.moe">
<link rel="preconnect" href="https://forge.speedtest.cn">
<link rel="preconnect" href="https://whois.pconline.com.cn">
<link rel="preconnect" href="https://api-ipv4.ip.sb">
<link rel="preconnect" href="https://api.ipify.org">
<link rel="preconnect" href="https://api.ttt.sh">
<link rel="preconnect" href="https://qqwry.api.skk.moe">
<link rel="preconnect" href="https://d.skk.moe">
<style>
.img-con {
width: 65px;
height: 55px;
display: flex;
justify-content: center;
overflow: visible;
}
#flag {
width: auto;
height: auto;
max-width: 65px;
max-height: 55px;
object-fit: contain;
}
.status-icon {
width: 58px;
height: 58px;
object-fit: contain;
display: block;
}
.status-icons {
display: flex;
height: 55px;
margin-left: auto;
}
.site-icon {
display: flex;
justify-content: center;
height: 55px;
margin: 0 6px;
}
.mx-1 {
margin: 0 4px;
}
.site-icon[onclick*="github"] .status-icon {
width: 61px;
height: 59px;
}
.site-icon[onclick*="github"] {
width: 60px;
height: 57px;
display: flex;
justify-content: center;
}
.container-sm.container-bg.callout.border {
padding: 12px 15px;
min-height: 70px;
display: flex;
margin-bottom: 15px;
}
.row.align-items-center {
width: 100%;
margin: 0;
display: flex;
gap: 15px;
height: 55px; /
}
.col-3 {
height: 55px;
display: flex;
flex-direction: column;
justify-content: center;
}
.col.text-center {
position: static;
left: auto;
transform: none;
}
.container-sm .row .col-4 {
position: static !important;
order: 2 !important;
width: 100% !important;
padding-left: 54px !important;
margin-top: 5px !important;
text-align: left !important;
}
#ping-result {
font-weight: bold;
}
#d-ip {
color: #09B63F;
font-weight: 700 !important;
}
#d-ip > .ip-main {
font-size: 15px !important;
}
#d-ip .badge-primary {
font-size: 13px !important;
}
.info.small {
color: #ff69b4;
font-weight: 600;
white-space: nowrap;
}
.site-icon, .img-con {
cursor: pointer !important;
transition: all 0.2s ease !important;
position: relative !important;
user-select: none !important;
}
.site-icon:hover, .img-con:hover {
transform: translateY(-2px) !important;
}
.site-icon:active, .img-con:active {
transform: translateY(1px) !important;
opacity: 0.8 !important;
}
@media (max-width: 1206px) {
.site-icon[onclick*="baidu"],
.site-icon[onclick*="taobao"],
.site-icon[onclick*="google"],
.site-icon[onclick*="youtube"],
.site-icon[onclick*="github"] {
display: none !important;
}
}
</style>
<?php if (in_array($lang, ['zh-cn', 'en', 'auto'])): ?>
<div id="status-bar-component" class="container-sm container-bg callout border">
<div class="row align-items-center">
<div class="col-auto">
<div class="img-con">
<img src="./assets/neko/img/loading.svg" id="flag" title="国旗" onclick="IP.getIpipnetIP()">
</div>
</div>
<div class="col-3">
<p id="d-ip" class="ip-address mb-0">Checking...</p>
<p id="ipip" class="info small mb-0"></p>
</div>
<div class="col text-center">
<p id="ping-result" class="mb-0"></p>
</div>
<div class="col-auto ms-auto">
<div class="status-icons d-flex">
<div class="site-icon mx-1" onclick="pingHost('baidu', 'Baidu')">
<img src="./assets/neko/img/site_icon_01.png" id="baidu-normal" class="status-icon" style="display: none;">
<img src="./assets/neko/img/site_icon1_01.png" id="baidu-gray" class="status-icon">
</div>
<div class="site-icon mx-1" onclick="pingHost('taobao', 'Taobao')">
<img src="./assets/neko/img/site_icon_02.png" id="taobao-normal" class="status-icon" style="display: none;">
<img src="./assets/neko/img/site_icon1_02.png" id="taobao-gray" class="status-icon">
</div>
<div class="site-icon mx-1" onclick="pingHost('google', 'Google')">
<img src="./assets/neko/img/site_icon_03.png" id="google-normal" class="status-icon" style="display: none;">
<img src="./assets/neko/img/site_icon1_03.png" id="google-gray" class="status-icon">
</div>
<div class="site-icon mx-1" onclick="pingHost('youtube', 'YouTube')">
<img src="./assets/neko/img/site_icon_04.png" id="youtube-normal" class="status-icon" style="display: none;">
<img src="./assets/neko/img/site_icon1_04.png" id="youtube-gray" class="status-icon">
</div>
<div class="site-icon mx-1" onclick="pingHost('github', 'GitHub')">
<img src="./assets/neko/img/site_icon_05.png" id="github-normal" title="测试 GitHub 延迟" class="status-icon" style="display: none;">
<img src="./assets/neko/img/site_icon1_05.png" id="github-gray" class="status-icon">
</div>
</div>
</div>
</div>
</div>
<?php endif; ?>
<script src="./assets/neko/js/jquery.min.js"></script>
<script type="text/javascript">
const _IMG = './assets/neko/';
const translate = <?php echo json_encode($translate, JSON_UNESCAPED_UNICODE); ?>;
let cachedIP = null;
let cachedInfo = null;
let random = parseInt(Math.random() * 100000000);
const sitesToPing = {
baidu: { url: 'https://www.baidu.com', name: 'Baidu' },
taobao: { url: 'https://www.taobao.com', name: 'Taobao' },
google: { url: 'https://www.google.com', name: 'Google' },
youtube: { url: 'https://www.youtube.com', name: 'YouTube' },
github: { url: 'https://www.github.com', name: 'GitHub' }
};
async function checkAllPings() {
const pingResults = {};
for (const [key, site] of Object.entries(sitesToPing)) {
const { url, name } = site;
try {
const startTime = performance.now();
await fetch(url, { mode: 'no-cors', cache: 'no-cache' });
const endTime = performance.now();
const pingTime = Math.round(endTime - startTime);
pingResults[key] = { name, pingTime };
} catch (error) {
pingResults[key] = { name, pingTime: '超时' };
}
}
return pingResults;
}
const checkSiteStatus = {
sites: {
baidu: 'https://www.baidu.com',
taobao: 'https://www.taobao.com',
google: 'https://www.google.com',
youtube: 'https://www.youtube.com',
github: 'https://www.github.com'
},
check: async function() {
for (let [site, url] of Object.entries(this.sites)) {
try {
const response = await fetch(url, {
mode: 'no-cors',
cache: 'no-cache'
});
document.getElementById(`${site}-normal`).style.display = 'inline';
document.getElementById(`${site}-gray`).style.display = 'none';
} catch (error) {
document.getElementById(`${site}-normal`).style.display = 'none';
document.getElementById(`${site}-gray`).style.display = 'inline';
}
}
}
};
async function pingHost(site, siteName) {
const url = checkSiteStatus.sites[site];
const resultElement = document.getElementById('ping-result');
try {
resultElement.innerHTML = `<span style="font-size: 22px">正在测试 ${siteName} 的连接延迟...`;
resultElement.style.color = '#87CEFA';
const startTime = performance.now();
await fetch(url, {
mode: 'no-cors',
cache: 'no-cache'
});
const endTime = performance.now();
const pingTime = Math.round(endTime - startTime);
resultElement.innerHTML = `<span style="font-size: 22px">${siteName} 连接延迟: ${pingTime}ms</span>`;
if(pingTime <= 100) {
resultElement.style.color = '#09B63F';
} else if(pingTime <= 200) {
resultElement.style.color = '#FFA500';
} else {
resultElement.style.color = '#ff6b6b';
}
} catch (error) {
resultElement.innerHTML = `<span style="font-size: 22px">${siteName} 连接超时`;
resultElement.style.color = '#ff6b6b';
}
}
async function onlineTranslate(text, targetLang = 'zh') {
if (!text || typeof text !== 'string' || text.trim() === '') {
return text;
}
const cacheKey = `trans_${text}_${targetLang}`;
const cachedTranslation = localStorage.getItem(cacheKey);
if (cachedTranslation) {
return cachedTranslation;
}
const apis = [
{
url: 'https://api.mymemory.translated.net/get?q=' + encodeURIComponent(text) + '&langpair=en|' + targetLang,
method: 'GET',
parseResponse: (data) => data.responseData.translatedText
},
{
url: 'https://libretranslate.com/translate',
method: 'POST',
body: JSON.stringify({
q: text,
source: 'en',
target: targetLang,
format: 'text'
}),
headers: {
'Content-Type': 'application/json'
},
parseResponse: (data) => data.translatedText
},
{
url: `https://lingva.ml/api/v1/en/${targetLang}/${encodeURIComponent(text)}`,
method: 'GET',
parseResponse: (data) => data.translation
},
{
url: `https://simplytranslate.org/api/translate?engine=google&from=en&to=${targetLang}&text=${encodeURIComponent(text)}`,
method: 'GET',
parseResponse: (data) => data.translatedText
}
];
for (const api of apis) {
try {
const response = await fetch(api.url, {
method: api.method,
headers: api.headers || {},
body: api.body || null
});
if (response.ok) {
const data = await response.json();
const translatedText = api.parseResponse(data);
try {
localStorage.setItem(cacheKey, translatedText);
} catch (e) {
clearOldCache();
localStorage.setItem(cacheKey, translatedText);
}
return translatedText;
}
} catch (error) {
continue;
}
}
return text;
}
function clearOldCache() {
const cachePrefix = 'trans_';
const cacheKeys = Object.keys(localStorage).filter(key =>
key.startsWith(cachePrefix)
);
if (cacheKeys.length > 1000) {
const itemsToRemove = cacheKeys.slice(0, cacheKeys.length - 1000);
itemsToRemove.forEach(key => localStorage.removeItem(key));
}
}
async function translateText(text, targetLang = 'zh') {
if (translate[text]) {
return translate[text];
}
return await onlineTranslate(text, targetLang);
}
let IP = {
isRefreshing: false,
lastGeoData: null,
ipApis: [
{url: 'https://api.ipify.org?format=json', type: 'json', key: 'ip'},
{url: 'https://api-ipv4.ip.sb/geoip', type: 'json', key: 'ip'},
{url: 'https://myip.ipip.net', type: 'text'},
{url: 'http://pv.sohu.com/cityjson', type: 'text'},
{url: 'https://ipinfo.io/json', type: 'json', key: 'ip'},
{url: 'https://ipapi.co/json/', type: 'json'},
{url: 'https://freegeoip.app/json/', type: 'json'}
],
fetchIP: async () => {
let error;
for(let api of IP.ipApis) {
try {
const response = await IP.get(api.url, api.type);
if(api.type === 'json') {
const ipData = api.key ? response.data[api.key] : response.data;
cachedIP = ipData;
document.getElementById('d-ip').innerHTML = ipData;
return ipData;
} else {
const ipData = response.data.match(/\d+\.\d+\.\d+\.\d+/)?.[0];
if(ipData) {
cachedIP = ipData;
document.getElementById('d-ip').innerHTML = ipData;
return ipData;
}
}
} catch(e) {
error = e;
console.error(`Error with ${api.url}:`, e);
continue;
}
}
throw error || new Error("All IP APIs failed");
},
get: (url, type) =>
fetch(url, {
method: 'GET',
cache: 'no-store'
}).then((resp) => {
if (type === 'text')
return Promise.all([resp.ok, resp.status, resp.text(), resp.headers]);
else
return Promise.all([resp.ok, resp.status, resp.json(), resp.headers]);
}).then(([ok, status, data, headers]) => {
if (ok) {
return { ok, status, data, headers };
} else {
throw new Error(JSON.stringify(data.error));
}
}).catch(error => {
console.error("Error fetching data:", error);
throw error;
}),
Ipip: async (ip, elID) => {
const geoApis = [
{url: `https://api.ip.sb/geoip/${ip}`, type: 'json'},
{url: 'https://myip.ipip.net', type: 'text'},
{url: `http://ip-api.com/json/${ip}`, type: 'json'},
{url: `https://ipinfo.io/${ip}/json`, type: 'json'},
{url: `https://ipapi.co/${ip}/json/`, type: 'json'},
{url: `https://freegeoip.app/json/${ip}`, type: 'json'}
];
let geoData = null;
let error;
for(let api of geoApis) {
try {
const response = await IP.get(api.url, api.type);
geoData = response.data;
break;
} catch(e) {
error = e;
console.error(`Error with ${api.url}:`, e);
continue;
}
}
if(!geoData) {
throw error || new Error("All Geo APIs failed");
}
cachedIP = ip;
IP.lastGeoData = geoData;
IP.updateUI(geoData, elID);
},
updateUI: async (data, elID) => {
try {
const country = await translateText(data.country || "未知");
const region = await translateText(data.region || "");
const city = await translateText(data.city || "");
const isp = await translateText(data.isp || "");
const asnOrganization = await translateText(data.asn_organization || "");
let location = `${region && city && region !== city ? `${region} ${city}` : region || city || ''}`;
let simpleDisplay = `
<div class="ip-main" style="cursor: pointer;" onclick="IP.showDetailModal()">
${cachedIP} <span class="badge badge-primary" style="color: #333;">${country}</span>
</div>`;
let locationInfo = `<span style="margin-left: 8px;">${location} ${isp} ${data.asn || ''} ${asnOrganization}</span>`;
document.getElementById('d-ip').innerHTML = simpleDisplay;
document.getElementById('ipip').innerHTML = locationInfo;
const countryCode = data.country_code || 'unknown';
const flagSrc = (countryCode !== 'unknown') ? _IMG + "flags/" + countryCode.toLowerCase() + ".png" : './assets/neko/flags/mo.png';
$("#flag").attr("src", flagSrc);
} catch (error) {
console.error("Error in updateUI:", error);
document.getElementById('d-ip').innerHTML = "更新 IP 信息失败";
$("#flag").attr("src", "./assets/neko/flags/mo.png");
}
},
showDetailModal: async () => {
const data = IP.lastGeoData;
if (!data) return;
const translatedCountry = await translateText(data.country, 'zh');
const translatedRegion = await translateText(data.region, 'zh');
const translatedCity = await translateText(data.city, 'zh');
const translatedIsp = await translateText(data.isp, 'zh');
const translatedAsnOrganization = await translateText(data.asn_organization, 'zh');
let country = translatedCountry || data.country || "未知";
let region = translatedRegion || data.region || "";
let city = translatedCity || data.city || "";
let isp = translatedIsp || data.isp || "";
let asnOrganization = translatedAsnOrganization || data.asn_organization || "";
let timezone = data.timezone || "";
let asn = data.asn || "";
let ipSupport;
const ipv4Regex = /^(\d{1,3}\.){3}\d{1,3}$/;
const ipv6Regex = /^[a-fA-F0-9:]+$/;
if (ipv4Regex.test(cachedIP)) {
ipSupport = 'IPv4 支持';
} else if (ipv6Regex.test(cachedIP)) {
ipSupport = 'IPv6 支持';
} else {
ipSupport = '未检测到 IPv4 或 IPv6 支持';
}
const pingResults = await checkAllPings();
const delayInfoHTML = Object.entries(pingResults).map(([key, { name, pingTime }]) => {
let color = '#ff6b6b';
if (typeof pingTime === 'number') {
color = pingTime <= 100 ? '#09B63F' : pingTime <= 200 ? '#FFA500' : '#ff6b6b';
}
return `<span style="margin-right: 20px; font-size: 18px; color: ${color};">${name}: ${pingTime === '超时' ? '超时' : `${pingTime}ms`}</span>`;
}).join('');
const modalHTML = `
<div class="modal fade custom-modal" id="ipDetailModal" tabindex="-1" role="dialog" aria-labelledby="ipDetailModalLabel" aria-hidden="true" data-bs-backdrop="static" data-bs-keyboard="false">
<div class="modal-dialog modal-dialog-centered modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="ipDetailModalLabel">IP详细信息</h5>
<button type="button" class="close" data-bs-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<div class="ip-details">
<div class="detail-row">
<span class="detail-label">IP支持:</span>
<span class="detail-value">${ipSupport}</span>
</div>
<div class="detail-row">
<span class="detail-label">IP地址:</span>
<span class="detail-value">${cachedIP}</span>
</div>
<div class="detail-row">
<span class="detail-label">地区:</span>
<span class="detail-value">${country} ${region} ${city}</span>
</div>
<div class="detail-row">
<span class="detail-label">运营商:</span>
<span class="detail-value">${isp}</span>
</div>
<div class="detail-row">
<span class="detail-label">ASN:</span>
<span class="detail-value">${asn} ${asnOrganization}</span>
</div>
<div class="detail-row">
<span class="detail-label">时区:</span>
<span class="detail-value">${timezone}</span>
</div>
${data.latitude && data.longitude ? `
<div class="detail-row">
<span class="detail-label">经纬度:</span>
<span class="detail-value">${data.latitude}, ${data.longitude}</span>
</div>` : ''}
<h5 style="margin-top: 15px;">延迟信息:</h5>
<div class="detail-row" style="display: flex; flex-wrap: wrap;">
${delayInfoHTML}
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
</div>
</div>
</div>
</div>
`;
$('#ipDetailModal').remove();
$('body').append(modalHTML);
$('#ipDetailModal').modal('show');
},
getIpipnetIP: async () => {
if(IP.isRefreshing) return;
try {
IP.isRefreshing = true;
document.getElementById('d-ip').innerHTML = `
<div class="ip-main">
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
检查中...
</div>
`;
document.getElementById('ipip').innerHTML = "";
$("#flag").attr("src", _IMG + "img/loading.svg");
const ip = await IP.fetchIP();
await IP.Ipip(ip, 'ipip');
} catch (error) {
console.error("Error in getIpipnetIP function:", error);
document.getElementById('ipip').innerHTML = "获取IP信息失败";
} finally {
IP.isRefreshing = false;
}
}
};
const style = document.createElement('style');
style.textContent = `
.ip-main {
font-size: 14px;
padding: 5px;
transition: all 0.3s;
display: inline-flex;
align-items: center;
gap: 8px;
}
.badge-primary {
color: #ff69b4 !important;
background-color: #f8f9fa !important;
border: 1px solid #dee2e6;
}
#ipip {
margin-left: -3px;
}
.ip-main:hover {
background: #f0f0f0;
border-radius: 4px;
}
.ip-details {
font-size: 18px !important;
line-height: 1.6;
}
.detail-row {
margin-bottom: 12px;
display: flex;
}
.detail-label {
font-weight: 500;
color: #666;
flex: 0 0 80px;
}
.detail-value {
color: #333;
flex: 1;
}
.modal-content {
border-radius: 8px;
}
.modal-header {
background: #f8f9fa;
border-radius: 8px 8px 0 0;
}
.modal-body {
padding: 20px;
}
.custom-modal .modal-header {
background-color: #007bff;
color: #fff;
padding: 16px 20px;
border-bottom: 1px solid #ddd;
border-top-left-radius: 8px;
border-top-right-radius: 8px;
}
.custom-modal .custom-close {
color: #fff;
font-size: 1.5rem;
opacity: 0.7;
}
.custom-modal .custom-close:hover {
color: #ddd;
opacity: 1;
}
.custom-modal .modal-body {
padding: 20px;
font-size: 1rem;
color: #333;
line-height: 1.6;
}
.custom-modal .detail-row {
display: flex;
justify-content: space-between;
padding: 8px 0;
border-bottom: 1px solid #eee;
}
.custom-modal .detail-label {
font-weight: 600;
color: #555;
}
.custom-modal .detail-value {
font-weight: 400;
color: #333;
}
.custom-modal .modal-footer {
background-color: #f7f7f7;
padding: 12px 16px;
display: flex;
justify-content: flex-end;
border-top: 1px solid #ddd;
}
.custom-modal .custom-close-btn {
background-color: #007bff;
color: #fff;
border: none;
padding: 8px 16px;
font-size: 1rem;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.3s ease;
}
.custom-modal .custom-close-btn:hover {
background-color: #0056b3;
}
`;
document.head.appendChild(style);
IP.getIpipnetIP();
if(typeof checkSiteStatus !== 'undefined') {
checkSiteStatus.check();
setInterval(() => checkSiteStatus.check(), 30000);
}
setInterval(IP.getIpipnetIP, 180000);
</script>