async function reverseGeocode(latitude, longitude) {
try {
const response = await fetch(
`https://nominatim.openstreetmap.org/reverse?`
+ `format=json&lat=${latitude}&lon=${longitude}`
+ `&addressdetails=1&zoom=18&accept-language=zh`
);
if (!response.ok) {
throw new Error('逆地理编码服务不可用');
}
const data = await response.json();
if (data.error) {
throw new Error(data.error);
}
const address = data.address || {};
return {
display_name: data.display_name || '未知地址',
road: address.road || address.street || '',
neighborhood: address.neighbourhood || address.suburb || '',
city: address.city || address.town || address.village || '',
county: address.county || '',
state: address.state || address.region || '',
country: address.country || '',
postcode: address.postcode || '',
country_code: address.country_code || '',
full_address: [
address.road,
address.neighbourhood,
address.city || address.town,
address.county,
address.state,
address.country
].filter(Boolean).join(', ')
};
} catch (error) {
console.warn('逆地理编码失败:', error);
return {
display_name: '无法获取详细地址',
full_address: '逆地理编码服务暂时不可用',
is_fallback: true
};
}
}
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>IP 地址定位测试工具</title>
<style>
/* 样式部分已简化,保留核心布局 */
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; line-height: 1.6; color: #333; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 100vh; padding: 20px; }
.container { max-width: 1000px; margin: 0 auto; background: white; border-radius: 20px; box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); overflow: hidden; }
header { background: linear-gradient(90deg, #4facfe 0%, #00f2fe 100%); color: white; padding: 40px 30px; text-align: center; }
h1 { font-size: 2.5rem; margin-bottom: 10px; text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.2); }
.subtitle { font-size: 1.1rem; opacity: 0.9; max-width: 600px; margin: 0 auto; }
.main-content { padding: 30px; }
.card { background: #f8f9fa; border-radius: 15px; padding: 25px; margin-bottom: 25px; border: 1px solid #e9ecef; transition: transform 0.3s ease, box-shadow 0.3s ease; }
.card:hover { transform: translateY(-5px); box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1); }
.card h3 { color: #4facfe; margin-bottom: 15px; display: flex; align-items: center; gap: 10px; }
.data-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 20px; margin-top: 15px; }
.data-item { background: white; padding: 15px; border-radius: 10px; border-left: 4px solid #4facfe; }
.data-item label { display: block; font-size: 0.85rem; color: #6c757d; margin-bottom: 5px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; }
.data-item .value { font-size: 1.1rem; color: #212529; font-weight: 500; word-break: break-all; }
.value.coordinates { font-family: 'Courier New', monospace; color: #e83e8c; }
.value.ip { color: #28a745; font-weight: bold; }
.map-container { height: 300px; background: #e9ecef; border-radius: 10px; overflow: hidden; margin-top: 15px; position: relative; }
#map { width: 100%; height: 100%; }
.buttons { display: flex; gap: 15px; margin-top: 20px; flex-wrap: wrap; }
button { padding: 14px 28px; border: none; border-radius: 50px; font-size: 1rem; font-weight: 600; cursor: pointer; transition: all 0.3s ease; display: flex; align-items: center; justify-content: center; gap: 10px; min-width: 180px; }
.primary-btn { background: linear-gradient(90deg, #4facfe 0%, #00f2fe 100%); color: white; }
.secondary-btn { background: #6c757d; color: white; }
.footer { text-align: center; padding: 20px; color: #6c757d; font-size: 0.9rem; border-top: 1px solid #e9ecef; background: #f8f9fa; }
@media(max-width: 768px) { .container { border-radius: 10px; } header { padding: 30px 20px; } h1 { font-size: 2rem; } .main-content { padding: 20px; } button { width: 100%; } .data-grid { grid-template-columns: 1fr; } }
</style>
<link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/leaflet.css"/>
<script src="https://unpkg.com/[email protected]/dist/leaflet.js"></script>
</head>
<body>
<div class="container">
<header>
<h1>🌍 IP 地址精确定位工具</h1>
<p class="subtitle">通过 IP 地址获取用户的经纬度坐标、详细地址和网络信息</p>
</header>
<div class="main-content">
<div class="privacy-notice" style="background:#e7f3ff;border-radius:10px;padding:20px;margin-bottom:25px;border-left:4px solid #4facfe;">
<h4 style="color:#4facfe;margin-bottom:10px;display:flex;align-items:center;gap:10px;">🔒 隐私提示</h4>
<p>本工具会获取您的 IP 地址和大致地理位置信息。所有处理均在您的浏览器中完成,数据不会被保存到服务器。</p>
</div>
<div id="status" class="status" style="padding:20px;text-align:center;font-size:1.1rem;border-radius:10px;margin-bottom:20px;display:none;"></div>
<div class="card">
<h3>📍 基本信息</h3>
<div class="data-grid">
<div class="data-item"><label>IP 地址</label><div id="ip" class="value ip">正在获取...</div></div>
<div class="data-item"><label>网络提供商</label><div id="isp" class="value">正在获取...</div></div>
<div class="data-item"><label>定位方式</label><div id="method" class="value">IP 地址定位</div></div>
<div class="data-item"><label>数据来源</label><div id="source" class="value">ipapi.co + 逆地理编码</div></div>
</div>
</div>
<div class="card">
<h3>🗺️ 地理位置</h3>
<div class="data-grid">
<div class="data-item"><label>国家/地区</label><div id="country" class="value">正在获取...</div></div>
<div class="data-item"><label>省/州</label><div id="region" class="value">正在获取...</div></div>
<div class="data-item"><label>城市</label><div id="city" class="value">正在获取...</div></div>
<div class="data-item"><label>邮政编码</label><div id="zipcode" class="value">正在获取...</div></div>
</div>
</div>
<div class="card">
<h3>📡 坐标信息</h3>
<div class="data-grid">
<div class="data-item"><label>纬度</label><div id="latitude" class="value coordinates">正在获取...</div></div>
<div class="data-item"><label>经度</label><div id="longitude" class="value coordinates">正在获取...</div></div>
<div class="data-item"><label>时区</label><div id="timezone" class="value">正在获取...</div></div>
<div class="data-item"><label>货币</label><div id="currency" class="value">正在获取...</div></div>
</div>
<div class="accuracy-meter" style="margin-top:15px;padding:15px;background:#fff3cd;border-radius:10px;border-left:4px solid #ffc107;">
<div class="accuracy-label" style="display:flex;justify-content:space-between;margin-bottom:10px;">
<span>定位精度</span><span id="accuracy-text">未知</span>
</div>
<div class="meter-bar" style="height:10px;background:#e9ecef;border-radius:5px;overflow:hidden;">
<div id="accuracy-meter" class="meter-fill" style="height:100%;background:linear-gradient(90deg,#20c997,#28a745);width:0%;transition:width 1.5s ease;"></div>
</div>
<small style="color:#6c757d;display:block;margin-top:5px;">注:IP 定位精度通常为 1-50 公里,受网络类型和 VPN 影响</small>
</div>
</div>
<div class="card">
<h3>🏠 详细地址</h3>
<div id="detailed-address" style="font-size:1.1rem;line-height:1.6;padding:15px;background:white;border-radius:8px;min-height:60px;">正在获取详细地址信息...</div>
</div>
<div class="card">
<h3>🗺️ 位置地图</h3>
<div class="map-container">
<div id="map"></div>
<div id="map-placeholder" class="map-placeholder" style="display:flex;align-items:center;justify-content:center;height:100%;color:#6c757d;font-size:1.1rem;">获取位置后,将在此显示地图</div>
</div>
</div>
<div class="buttons">
<button id="locate-btn" class="primary-btn" onclick="startLocationDetection()"><span class="loading-spinner" style="display:inline-block;width:20px;height:20px;border:3px solid rgba(255,255,255,0.3);border-radius:50%;border-top-color:white;animation:spin 1s ease-in-out infinite;" style="display:none;"></span><span id="btn-text">🚀 开始定位检测</span></button>
<button class="secondary-btn" onclick="copyLocationData()"> 📋 复制位置数据 </button>
<button class="secondary-btn" onclick="refreshLocation()"> 🔄 重新检测 </button>
</div>
</div>
<div class="footer">
<p>⚠️ 注意:此工具仅供学习和测试使用。IP 定位精度有限,不适用于需要精确定位的场景。</p>
<p>📊 最后更新:<span id="update-time"></span></p>
</div>
</div>
<script>
let map = null;
let marker = null;
let currentLocation = null;
document.addEventListener('DOMContentLoaded', function() {
document.getElementById('update-time').textContent = new Date().toLocaleString('zh-CN');
setTimeout(() => {
startLocationDetection();
}, 1000);
});
async function startLocationDetection() {
const btn = document.getElementById('locate-btn');
const btnText = document.getElementById('btn-text');
const spinner = btn.querySelector('.loading-spinner');
btnText.textContent = '正在定位...';
spinner.style.display = 'inline-block';
btn.disabled = true;
showStatus('正在通过 IP 地址获取您的位置信息...', 'loading');
try {
ip = ();
.(). = ip;
ipLocation = (ip);
detailedAddress = (ipLocation., ipLocation.);
currentLocation = {
: ip,
...ipLocation,
: detailedAddress,
: ().()
};
(currentLocation);
(currentLocation., currentLocation.);
(ipLocation. || );
(, );
} (error) {
.(, error);
(, );
();
} {
btnText. = ;
spinner.. = ;
btn. = ;
}
}
() {
{
response = ();
data = response.();
data.;
} (error) {
backupAPIs = [, , ];
( api backupAPIs) {
{
response = (api);
text = response.();
text.();
} (e) {
;
}
}
();
}
}
() {
apis = [
,
,
];
( apiUrl apis) {
{
.();
response = (apiUrl, { : { : } });
(!response.) ;
data = response.();
(data. && data.) {
{
: (data.),
: (data.),
: data. || data.,
: data. || data.,
: data. || data. || data.,
: data. || data.,
: data. || data. || data.,
: data. || data.,
: data. || data.,
: data. || data. || data.,
: data. || (data. ? : )
};
}
} (error) {
.(, error);
;
}
}
();
}
() {
{
response = (
+
+
);
(!response.) {
();
}
data = response.();
(data.) {
(data.);
}
address = data. || {};
{
: data. || ,
: address. || address. || ,
: address. || address. || ,
: address. || address. || address. || ,
: address. || ,
: address. || address. || ,
: address. || ,
: address. || ,
: address. || ,
: [
address.,
address.,
address. || address.,
address.,
address.,
address.
].().()
};
} (error) {
.(, error);
{
: ,
: ,
:
};
}
}
() {
.(). = location. || ;
.(). = location. || ;
.(). = location. || ;
.(). = location. || ;
.(). = location. || ;
.(). = location..();
.(). = location..();
.(). = location. || ;
.(). = location. || ;
addressElement = .();
(location. && location..) {
addressElement. = ;
} {
addressElement. = ;
}
}
() {
mapContainer = .();
mapPlaceholder = .();
mapPlaceholder.. = ;
mapContainer.. = ;
(!map) {
map = L.().([latitude, longitude], );
L.(, {
: ,
:
}).(map);
} {
map.([latitude, longitude], );
}
(marker) {
map.(marker);
}
marker = L.([latitude, longitude]).(map);
marker.(
).();
L.([latitude, longitude], {
: ,
: ,
: ,
:
}).(map);
}
() {
accuracyText = .();
accuracyMeter = .();
percentage = ;
( accuracy === ) {
(accuracy.() || accuracy.()) {
percentage = ;
accuracyText. = ;
} (accuracy.() || accuracy.()) {
percentage = ;
accuracyText. = ;
} (accuracy.()) {
km = (accuracy);
(km <= ) {
percentage = ;
accuracyText. = ;
} (km <= ) {
percentage = ;
accuracyText. = ;
} {
percentage = ;
accuracyText. = ;
}
} {
accuracyText. = ;
}
} {
accuracyText. = ;
}
( {
accuracyMeter.. = ;
}, );
}
() {
statusElement = .();
statusElement. = ;
statusElement. = message;
statusElement..(type);
statusElement.. = ;
(type === || type === ) {
( {
statusElement.. = ;
}, );
}
}
() {
demoLocation = {
: ,
: ,
: ,
: ,
: ,
: ,
: ,
: ,
: ,
: ,
: ,
: ,
: {
: ,
: ,
:
}
};
currentLocation = demoLocation;
(demoLocation);
(demoLocation., demoLocation.);
();
(, );
}
() {
(!currentLocation) {
(, );
;
}
data = {
时间: ().(),
: currentLocation.,
: currentLocation.,
国家:currentLocation.,
省份:currentLocation.,
城市:currentLocation.,
邮政编码:currentLocation.,
纬度:currentLocation.,
经度:currentLocation.,
时区:currentLocation.,
货币:currentLocation.,
详细地址:currentLocation.?. || ,
定位方式:,
精度:.().
};
text = .(data).( ).();
navigator..(text).( {
(, );
}).( {
.(, err);
(, );
});
}
() {
(marker) {
map.(marker);
marker = ;
}
.(). = ;
.(). = ;
.(). = ;
.(). = ;
.(). = ;
.(). = ;
.(). = ;
.(). = ;
.(). = ;
.(). = ;
.(). = ;
.().. = ;
.().. = ;
.().. = ;
.(). = ;
();
}
</script>
</body>
</html>