Files
iOS-Location-Spoofer-Web/public/index.html
T
2026-09-09 18:10:20 +08:00

1906 lines
71 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!--
iOS Location Spoofer Web
Copyright (c) 2026 akudamatata (https://github.com/akudamatata/iOS-Location-Spoofer-Web)
Licensed under Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International (CC BY-NC-SA 4.0)
⚠️【特别声明】:本项目完全免费开源,严禁以任何形式进行二次售卖、转售、商业收费代搭建、打包牟利等行为!
若您是通过付费渠道获取本项目的,请立即申请退款并举报不良商家!
-->
<!doctype html>
<html lang="zh">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)">
<meta name="theme-color" content="#1c1c1e" media="(prefers-color-scheme: dark)">
<script>
// In PWA (standalone) mode, remove theme-color so iOS uses black-translucent
// This allows the map to render truly edge-to-edge under the status bar.
if (window.matchMedia('(display-mode: standalone)').matches || window.navigator.standalone) {
document.write('<style>#pwa-top-shadow { display: block !important; }</style>');
document.addEventListener('DOMContentLoaded', function() {
document.querySelectorAll('meta[name="theme-color"]').forEach(function(el) { el.remove(); });
});
}
</script>
<title>GPS Spoofer</title>
<link rel="manifest" href="/manifest.json">
<link rel="apple-touch-icon" href="/icon-512.png">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap">
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css">
<style>
/* ═══════════════════════════════════════════════
RESET & BASE
═══════════════════════════════════════════════ */
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0;-webkit-tap-highlight-color:transparent}
html,body{width:100vw;height:100vh;height:100dvh;height:-webkit-fill-available;overflow:hidden;font-family:'Inter','SF Pro Display',-apple-system,BlinkMacSystemFont,sans-serif;background:#ffffff}
/* ═══════════════════════════════════════════════
LIQUID GLASS TOKENS (iOS 26 / Apple 2026)
═══════════════════════════════════════════════ */
:root{
--glass-light:rgba(255,255,255,0.72);
--glass-light-border:rgba(255,255,255,0.90);
--glass-mid:rgba(255,255,255,0.55);
--glass-dark:rgba(0,0,0,0.18);
--blur-sm:blur(12px)saturate(160%);
--blur-md:blur(20px)saturate(180%);
--blur-lg:blur(32px)saturate(200%);
--shadow-float:0 8px 32px rgba(0,0,0,0.14),0 2px 8px rgba(0,0,0,0.10),inset 0 1px 0 rgba(255,255,255,0.95);
--shadow-press:0 2px 8px rgba(0,0,0,0.10),inset 0 1px 0 rgba(255,255,255,0.8);
--accent:#007AFF;
--accent-hover:#0071E3;
--green:#30D158;
--green-hover:#28B14D;
--red:#FF453A;
--text:#1C1C1E;
--text-secondary:rgba(60,60,67,0.6);
--r-sm:14px;
--r-md:20px;
--r-lg:26px;
--r-xl:32px;
--safe-top:env(safe-area-inset-top,0px);
--safe-bottom:env(safe-area-inset-bottom,0px);
--map-bg:#e8e8e8;
}
/* ── Dark Mode ────────────────────────────────── */
html.dark{
--glass-light:rgba(28,28,30,0.82);
--glass-light-border:rgba(255,255,255,0.12);
--glass-mid:rgba(28,28,30,0.65);
--glass-dark:rgba(0,0,0,0.55);
--shadow-float:0 8px 32px rgba(0,0,0,0.45),0 2px 8px rgba(0,0,0,0.35),inset 0 1px 0 rgba(255,255,255,0.08);
--shadow-press:0 2px 8px rgba(0,0,0,0.35),inset 0 1px 0 rgba(255,255,255,0.06);
--text:#F2F2F7;
--text-secondary:rgba(235,235,245,0.55);
--map-bg:#1c1c1e;
}
html.dark body{background:#1c1c1e;}
html.dark #login-overlay{background:rgba(0,0,0,0.7);}
html.dark #login-input{background:rgba(44,44,46,0.8);color:var(--text);border-color:rgba(255,255,255,0.12);}
html.dark .param-input{background:rgba(44,44,46,0.7);color:var(--text);border-color:rgba(255,255,255,0.12);}
html.dark #modal-name{background:rgba(44,44,46,0.8);color:var(--text);border-color:rgba(255,255,255,0.12);}
html.dark #search-input{color:var(--text);}
html.dark #status-pill{background:rgba(255,255,255,0.08);}
html.dark .result-row{color:var(--text);border-bottom-color:rgba(255,255,255,0.06);}
html.dark .result-row:active{background:rgba(0,122,255,0.15);}
html.dark .config-code-box{background:rgba(0,0,0,0.35);color:var(--text);border-color:rgba(255,255,255,0.1);}
/* dark mode toggle button */
#dark-toggle{
display:flex;align-items:center;justify-content:space-between;
padding:14px 20px;
font-size:14px;font-weight:500;color:var(--text);
cursor:default;
}
#dark-toggle-label{display:flex;align-items:center;gap:8px;}
.toggle-switch{
position:relative;width:44px;height:26px;
background:rgba(120,120,128,0.32);
border-radius:13px;
cursor:pointer;
transition:background .25s;
flex-shrink:0;
}
.toggle-switch.on{background:var(--accent);}
.toggle-knob{
position:absolute;
top:3px;left:3px;
width:20px;height:20px;
background:#fff;
border-radius:50%;
box-shadow:0 1px 4px rgba(0,0,0,0.25);
transition:transform .25s cubic-bezier(.34,1.3,.64,1);
}
.toggle-switch.on .toggle-knob{transform:translateX(18px);}
/* Locked pin marker */
.locked-pin{
width:28px;height:36px;
position:relative;
filter:drop-shadow(0 3px 8px rgba(0,122,255,0.5));
}
.locked-pin-inner{
width:28px;height:28px;
background:var(--accent);
border-radius:50% 50% 50% 0;
transform:rotate(-45deg);
border:3px solid #fff;
}
.locked-pin-dot{
position:absolute;
top:7px;left:7px;
width:10px;height:10px;
background:#fff;
border-radius:50%;
}
/* ═══════════════════════════════════════════════
LOGIN SCREEN (Liquid Glass)
═══════════════════════════════════════════════ */
#login-overlay{
position:fixed;inset:0;z-index:9999;
background:rgba(0,0,0,0.45);
backdrop-filter:blur(30px)saturate(160%);
-webkit-backdrop-filter:blur(30px)saturate(160%);
display:flex;align-items:center;justify-content:center;
padding:20px;
}
#login-card{
width:100%;max-width:340px;
background:var(--glass-light);
backdrop-filter:var(--blur-lg);
-webkit-backdrop-filter:var(--blur-lg);
border:1px solid var(--glass-light-border);
box-shadow:0 24px 60px rgba(0,0,0,0.25);
border-radius:var(--r-lg);
padding:28px 24px;
text-align:center;
}
#login-card h2{font-size:21px;font-weight:700;color:var(--text);margin-bottom:8px;}
#login-card p{font-size:13px;color:var(--text-secondary);line-height:1.5;margin-bottom:20px;}
#login-input{
width:100%;height:46px;padding:0 14px;
font-size:16px;color:var(--text);
background:rgba(255,255,255,0.7);
border:1px solid rgba(60,60,67,0.2);
border-radius:12px;outline:none;
text-align:center;margin-bottom:16px;
transition:border-color .15s;
}
#login-input:focus{border-color:var(--accent);}
#login-submit{
width:100%;height:46px;background:var(--accent);color:#fff;
border:none;border-radius:12px;font-size:15px;font-weight:600;
cursor:pointer;box-shadow:0 4px 16px rgba(0,122,255,0.3);
transition:background .15s,transform .15s;
}
#login-submit:active{transform:scale(.97);background:var(--accent-hover);}
/* ═══════════════════════════════════════════════
CONFIG PANEL & BUTTON
═══════════════════════════════════════════════ */
#config-btn{
position:fixed;
top:calc(var(--safe-top) + 126px);
left:16px;
z-index:20;
width:44px;height:44px;
background:var(--glass-light);
backdrop-filter:var(--blur-md);
-webkit-backdrop-filter:var(--blur-md);
border:1px solid var(--glass-light-border);
box-shadow:var(--shadow-float);
border-radius:var(--r-sm);
cursor:pointer;display:flex;align-items:center;justify-content:center;
transition:transform .15s;
color:var(--text);
}
#config-btn:active{transform:scale(.92);}
#config-panel{
position:fixed;
top:0;left:0;bottom:0;
width:min(350px,90vw);
z-index:40;
background:var(--glass-light);
backdrop-filter:var(--blur-lg);
-webkit-backdrop-filter:var(--blur-lg);
border-right:1px solid var(--glass-light-border);
box-shadow:4px 0 40px rgba(0,0,0,0.15);
transform:translateX(-110%);
transition:transform .35s cubic-bezier(.32,0,.67,0);
display:flex;flex-direction:column;
}
#config-panel.open{
transform:translateX(0);
transition-timing-function:cubic-bezier(.34,1.2,.64,1);
}
#config-panel-header{
padding:calc(var(--safe-top) + 16px) 16px 16px;
display:flex;align-items:center;justify-content:space-between;
border-bottom:1px solid rgba(60,60,67,.1);
}
#config-panel-header h2{font-size:20px;font-weight:700;color:var(--text);}
#config-close{
width:32px;height:32px;border-radius:50%;
background:rgba(60,60,67,.1);border:none;cursor:pointer;
display:flex;align-items:center;justify-content:center;
transition:background .15s;
}
#config-close:active{background:rgba(60,60,67,.2);}
#config-content{flex:1;overflow-y:auto;padding:8px 0;}
.config-section{
padding:16px 20px;
border-bottom:1px solid rgba(60,60,67,.08);
}
.config-section:last-child{border-bottom:none;}
.config-title{
font-size:14px;font-weight:600;color:var(--text);margin-bottom:8px;
}
.config-desc{
font-size:12px;color:var(--text-secondary);line-height:1.5;margin-bottom:12px;
}
.config-action-btn{
width:100%;height:40px;background:var(--accent);color:#fff;
border:none;border-radius:10px;font-size:13px;font-weight:600;
cursor:pointer;display:flex;align-items:center;justify-content:center;gap:6px;
transition:background .15s,transform .15s;
}
.config-action-btn:active{transform:scale(.97);background:var(--accent-hover);}
.config-code-box{
background:rgba(0,0,0,0.05);
border:1px solid rgba(60,60,67,0.12);
border-radius:8px;padding:10px;
font-family:monospace;font-size:11px;color:var(--text);
word-break:break-all;white-space:pre-wrap;
max-height:110px;overflow-y:auto;margin-bottom:10px;
}
/* ═══════════════════════════════════════════════
MAP (full viewport)
═══════════════════════════════════════════════ */
#map{
position:fixed;inset:0;width:100vw;height:100vh;height:100dvh;height:-webkit-fill-available;z-index:0;
}
#pwa-top-shadow {
display: none; /* Only shown in PWA mode via JS */
position: fixed;
top: 0; left: 0; right: 0;
height: max(env(safe-area-inset-top), 47px);
background: linear-gradient(to bottom, rgba(0,0,0,0.25) 0%, transparent 100%);
z-index: 10;
pointer-events: none;
}
.leaflet-control-zoom{display:none}
.leaflet-control-attribution{
background:rgba(255,255,255,0.55)!important;
backdrop-filter:var(--blur-sm)!important;
-webkit-backdrop-filter:var(--blur-sm)!important;
border-radius:8px!important;
font-size:10px!important;
color:var(--text-secondary)!important;
border:1px solid var(--glass-light-border)!important;
}
/* ═══════════════════════════════════════════════
CROSSHAIR (fixed viewport center)
═══════════════════════════════════════════════ */
#crosshair{
position:fixed;
top:50%;left:50%;
transform:translate(-50%,-50%);
z-index:10;
pointer-events:none;
width:60px;height:60px;
}
#crosshair svg{
width:100%;height:100%;
filter:drop-shadow(0 2px 6px rgba(0,0,0,0.3));
transition:transform .2s cubic-bezier(.34,1.56,.64,1);
}
#crosshair.locked svg{transform:scale(0.8);}
/* Pulse ring */
#ch-ring{
position:absolute;
top:50%;left:50%;
transform:translate(-50%,-50%);
width:60px;height:60px;
border:2px solid rgba(0,122,255,0.5);
border-radius:50%;
opacity:0;
animation:pulse 2.5s ease-out infinite;
}
@keyframes pulse{
0%{opacity:.7;transform:translate(-50%,-50%)scale(0.6);}
70%{opacity:0;transform:translate(-50%,-50%)scale(1.6);}
100%{opacity:0;}
}
/* ═══════════════════════════════════════════════
COORDINATE BUBBLE
═══════════════════════════════════════════════ */
#coord-bubble{
position:fixed;
left:50%;
bottom:calc(50% + 46px);
transform:translateX(-50%);
z-index:11;
background:var(--glass-light);
backdrop-filter:var(--blur-md);
-webkit-backdrop-filter:var(--blur-md);
border:1px solid var(--glass-light-border);
box-shadow:var(--shadow-float);
border-radius:var(--r-sm);
padding:8px 14px 8px 16px;
white-space:nowrap;
pointer-events:auto;
transition:opacity .25s,transform .25s;
display:flex;
align-items:center;
gap:12px;
}
#bubble-content{
display:flex;
flex-direction:column;
align-items:center;
}
#coord-bubble .coords{
font-size:13px;font-weight:600;
color:var(--text);letter-spacing:-.3px;
}
#coord-bubble .alt{
font-size:11px;font-weight:500;
color:var(--text-secondary);
margin-top:1px;
text-align:center;
}
#bubble-fav-btn{
background:transparent;
border:none;
font-size:18px;
cursor:pointer;
display:flex;
align-items:center;
justify-content:center;
width:28px;
height:28px;
border-radius:50%;
transition:transform .12s,background .12s;
}
#bubble-fav-btn:active{
transform:scale(.88);
background:rgba(0,0,0,.08);
}
/* Arrow pointing down */
#coord-bubble::after{
content:'';position:absolute;
bottom:-6px;left:50%;
transform:translateX(-50%) rotate(45deg);
width:10px;height:10px;
background:inherit;
border-right:1px solid var(--glass-light-border);
border-bottom:1px solid var(--glass-light-border);
z-index:-1;
}
/* ═══════════════════════════════════════════════
TOP SEARCH BAR
═══════════════════════════════════════════════ */
#search-bar{
position:fixed;
top:calc(var(--safe-top) + 12px);
left:16px;right:16px;
z-index:35;
display:flex;gap:8px;align-items:center;
}
#search-input{
flex:1;
height:44px;
padding:0 16px;
font-size:16px;font-weight:400;
color:var(--text);
background:var(--glass-light);
backdrop-filter:var(--blur-md);
-webkit-backdrop-filter:var(--blur-md);
border:1px solid var(--glass-light-border);
box-shadow:var(--shadow-float);
border-radius:var(--r-sm);
transition:all 0.3s var(--bounce);
}
#search-input::placeholder{color:var(--text-secondary);}
#search-input:focus{
outline:none;
background:var(--glass-light-hover);
border-color:var(--accent);
box-shadow:0 0 0 3px rgba(0,122,255,.15),var(--shadow-float);
}
html.dark #search-input{color:var(--text);}
html.dark #search-input:focus{background:var(--glass-dark-hover);}
#search-history {
position: absolute;
top: 100%;
left: 0;
right: 0;
margin-top: 8px;
background: var(--glass-light);
backdrop-filter: var(--blur-md);
-webkit-backdrop-filter: var(--blur-md);
border: 1px solid var(--border-light);
border-radius: 16px;
box-shadow: 0 8px 24px rgba(0,0,0,0.1);
overflow: hidden;
display: flex;
flex-direction: column;
z-index: 10;
transition: all 0.2s var(--bounce);
transform-origin: top;
}
#search-history.hidden {
opacity: 0;
transform: scaleY(0.95);
pointer-events: none;
}
.history-item {
padding: 12px 16px;
font-size: 14px;
color: var(--text);
display: flex;
align-items: center;
gap: 12px;
cursor: pointer;
transition: background 0.2s ease;
border-bottom: 1px solid var(--border-light);
}
.history-item:active {
background: var(--bg-hover);
}
.history-icon {
color: var(--text-secondary);
flex-shrink: 0;
}
.history-clear {
padding: 10px;
font-size: 13px;
color: var(--system-red);
text-align: center;
cursor: pointer;
background: rgba(255, 59, 48, 0.05);
font-weight: 500;
transition: background 0.2s ease;
}
.history-clear:active {
background: rgba(255, 59, 48, 0.15);
}
html.dark #search-history {
background: var(--glass-dark);
box-shadow: 0 8px 24px rgba(0,0,0,0.4);
}
#search-btn{
width:44px;height:44px;flex-shrink:0;
background:var(--accent);
border:none;border-radius:var(--r-sm);
cursor:pointer;display:flex;align-items:center;justify-content:center;
box-shadow:0 4px 16px rgba(0,122,255,.35),var(--shadow-float);
transition:transform .15s,background .15s;
}
#search-btn:active{transform:scale(.93);background:var(--accent-hover);}
#search-btn svg{width:20px;height:20px;fill:none;stroke:#fff;stroke-width:2.2;stroke-linecap:round;stroke-linejoin:round;}
/* Search results dropdown */
#search-results{
position:fixed;
top:calc(var(--safe-top) + 68px);
left:16px;right:16px;
z-index:36;
background:var(--glass-light);
backdrop-filter:var(--blur-lg);
-webkit-backdrop-filter:var(--blur-lg);
border:1px solid var(--glass-light-border);
box-shadow:var(--shadow-float);
border-radius:var(--r-md);
overflow:hidden;
max-height:260px;overflow-y:auto;
/* Scrollbar styles for search results */
scrollbar-width: thin;
scrollbar-color: rgba(150, 150, 150, 0.4) transparent;
}
#search-results::-webkit-scrollbar {
width: 6px;
}
#search-results::-webkit-scrollbar-track {
background: transparent;
}
#search-results::-webkit-scrollbar-thumb {
background: rgba(150, 150, 150, 0.4);
border-radius: 6px;
}
html.dark #search-results::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.3);
}
#search-results {
display:none;
animation:slideDown .2s cubic-bezier(.34,1.2,.64,1);
}
@keyframes slideDown{from{opacity:0;transform:translateY(-8px);}to{opacity:1;transform:translateY(0);}}
.result-row{
padding:13px 16px;
font-size:14px;font-weight:400;
color:var(--text);
border-bottom:1px solid rgba(60,60,67,.1);
cursor:pointer;transition:background .12s;
}
.result-row:last-child{border-bottom:none;}
.result-row:active{background:rgba(0,122,255,.08);}
.result-row .name{font-weight:500;}
.result-row .sub{font-size:12px;color:var(--text-secondary);margin-top:2px;}
/* ═══════════════════════════════════════════════
LAYER SWITCHER (top right)
═══════════════════════════════════════════════ */
#layer-btn{
position:fixed;
top:calc(var(--safe-top) + 72px);
right:16px;
z-index:20;
width:44px;height:44px;
background:var(--glass-light);
backdrop-filter:var(--blur-md);
-webkit-backdrop-filter:var(--blur-md);
border:1px solid var(--glass-light-border);
box-shadow:var(--shadow-float);
border-radius:var(--r-sm);
cursor:pointer;
display:flex;align-items:center;justify-content:center;
transition:transform .15s;font-size:20px;
}
#layer-btn:active{transform:scale(.92);}
#layer-menu{
position:fixed;
top:calc(var(--safe-top) + 72px);
right:68px;
z-index:20;
background:var(--glass-light);
backdrop-filter:var(--blur-lg);
-webkit-backdrop-filter:var(--blur-lg);
border:1px solid var(--glass-light-border);
box-shadow:var(--shadow-float);
border-radius:var(--r-md);
overflow:hidden;
display:none;
min-width:140px;
animation:slideLeft .2s cubic-bezier(.34,1.2,.64,1);
}
@keyframes slideLeft{from{opacity:0;transform:translateX(8px);}to{opacity:1;transform:translateX(0);}}
.layer-item{
padding:12px 16px;font-size:14px;font-weight:500;
color:var(--text);cursor:pointer;transition:background .1s;
display:flex;align-items:center;gap:8px;
border-bottom:1px solid rgba(60,60,67,.08);
}
.layer-item:last-child{border-bottom:none;}
.layer-item:active{background:rgba(0,122,255,.08);}
.layer-item.active{color:var(--accent);}
.layer-item .check{margin-left:auto;color:var(--accent);font-size:16px;opacity:0;}
.layer-item.active .check{opacity:1;}
/* ═══════════════════════════════════════════════
FAVORITES BUTTON (left side, floating)
═══════════════════════════════════════════════ */
#fav-btn{
position:fixed;
top:calc(var(--safe-top) + 72px);
left:16px;
z-index:20;
width:44px;height:44px;
background:var(--glass-light);
backdrop-filter:var(--blur-md);
-webkit-backdrop-filter:var(--blur-md);
border:1px solid var(--glass-light-border);
box-shadow:var(--shadow-float);
border-radius:var(--r-sm);
cursor:pointer;display:flex;align-items:center;justify-content:center;
font-size:20px;transition:transform .15s;
}
#fav-btn:active{transform:scale(.92);}
#fav-badge{
position:absolute;top:-4px;right:-4px;
background:var(--accent);color:#fff;
font-size:9px;font-weight:700;
min-width:16px;height:16px;
border-radius:8px;padding:0 3px;
display:flex;align-items:center;justify-content:center;
border:2px solid #fff;
opacity:0;transition:opacity .2s;
}
/* ═══════════════════════════════════════════════
FAVORITES PANEL (slide from left)
═══════════════════════════════════════════════ */
#fav-panel{
position:fixed;
top:0;left:0;bottom:0;
width:min(320px,90vw);
z-index:40;
background:var(--glass-light);
backdrop-filter:var(--blur-lg);
-webkit-backdrop-filter:var(--blur-lg);
border-right:1px solid var(--glass-light-border);
box-shadow:4px 0 40px rgba(0,0,0,0.15);
transform:translateX(-110%);
transition:transform .35s cubic-bezier(.32,0,.67,0);
display:flex;flex-direction:column;
}
#fav-panel.open{
transform:translateX(0);
transition-timing-function:cubic-bezier(.34,1.2,.64,1);
}
#fav-panel-header{
padding:calc(var(--safe-top) + 16px) 16px 16px;
display:flex;align-items:center;justify-content:space-between;
border-bottom:1px solid rgba(60,60,67,.1);
}
#fav-panel-header h2{font-size:20px;font-weight:700;color:var(--text);}
#fav-close{
width:32px;height:32px;border-radius:50%;
background:rgba(60,60,67,.1);border:none;cursor:pointer;
display:flex;align-items:center;justify-content:center;font-size:16px;
transition:background .15s;
}
#fav-close:active{background:rgba(60,60,67,.2);}
#fav-list{flex:1;overflow-y:auto;padding:8px 0;}
.fav-item{
padding:14px 16px;
display:flex;align-items:center;gap:12px;
cursor:pointer;transition:background .1s;
}
.fav-item:active{background:rgba(0,122,255,.06);}
.fav-icon{
width:40px;height:40px;flex-shrink:0;
background:rgba(0,122,255,.1);border-radius:12px;
display:flex;align-items:center;justify-content:center;font-size:18px;
}
.fav-info{flex:1;min-width:0;}
.fav-name{font-size:15px;font-weight:600;color:var(--text);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
.fav-coords{font-size:12px;color:var(--text-secondary);margin-top:2px;}
.fav-del{
width:30px;height:30px;border-radius:8px;flex-shrink:0;
background:rgba(255,69,58,.1);border:none;cursor:pointer;
display:flex;align-items:center;justify-content:center;font-size:16px;
transition:background .15s;
}
.fav-del:active{background:rgba(255,69,58,.25);}
#fav-empty{
text-align:center;padding:48px 24px;
color:var(--text-secondary);font-size:14px;line-height:1.6;
}
#fav-empty svg{
display:block;
margin:0 auto 12px;
}
/* Overlay */
#overlay{
position:fixed;inset:0;z-index:39;
background:rgba(0,0,0,0);
pointer-events:none;
transition:background .35s;
}
#overlay.visible{
background:rgba(0,0,0,0.3);
pointer-events:all;
}
/* ═══════════════════════════════════════════════
BOTTOM DRAWER
═══════════════════════════════════════════════ */
#drawer{
position:fixed;
left:0;right:0;
bottom:0;
z-index:30;
background:var(--glass-light);
backdrop-filter:var(--blur-lg);
-webkit-backdrop-filter:var(--blur-lg);
border-top:1px solid var(--glass-light-border);
border-radius:var(--r-xl) var(--r-xl) 0 0;
box-shadow:0 -8px 40px rgba(0,0,0,0.12),inset 0 1px 0 rgba(255,255,255,0.9);
transition:transform .35s cubic-bezier(.34,1.15,.64,1);
padding-bottom:var(--safe-bottom);
/* Default: show just the handle + buttons */
transform:translateY(0);
}
/* Drag handle */
#drawer-handle{
display:flex;justify-content:center;padding:10px 0 4px;
cursor:pointer;
}
#drawer-handle::after{
content:'';
width:36px;height:4px;
background:rgba(60,60,67,.25);
border-radius:2px;
}
/* Collapsed section (always visible) */
#drawer-collapsed{
padding:4px 16px 16px;
display:flex;gap:10px;align-items:center;
}
/* Saved status pill */
#status-pill{
display:flex;align-items:center;gap:6px;
padding:6px 12px;
background:rgba(60,60,67,.08);
border-radius:20px;flex:1;min-width:0;
transition:background .2s;
}
#status-dot{
width:8px;height:8px;border-radius:50%;flex-shrink:0;
background:var(--text-secondary);
transition:background .3s;
}
#status-dot.saved{background:var(--green);}
#status-dot.unsaved{background:#FF9F0A;animation:blink 1.5s ease-in-out infinite;}
@keyframes blink{0%,100%{opacity:1;}50%{opacity:.4;}}
#status-text{font-size:12px;font-weight:500;color:var(--text-secondary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
#btn-lock{
height:48px;padding:0 22px;
background:var(--green);
color:#fff;font-size:15px;font-weight:600;
border:none;border-radius:var(--r-md);cursor:pointer;
box-shadow:0 4px 16px rgba(48,209,88,.35);
display:flex;align-items:center;gap:7px;flex-shrink:0;
transition:transform .15s,background .15s,box-shadow .15s;
}
#btn-lock:active{transform:scale(.95);background:var(--green-hover);box-shadow:none;}
#btn-lock.locking{background:#aaa;pointer-events:none;}
/* Expanded section (hidden by default) */
#drawer-expanded{
padding:0 16px 8px;
max-height:0;overflow:hidden;
transition:max-height .35s cubic-bezier(.34,1.1,.64,1),
padding .35s cubic-bezier(.34,1.1,.64,1);
}
#drawer-expanded.open{
max-height:220px;
padding:0 16px 16px;
}
.param-row{
display:flex;align-items:center;gap:10px;
padding:8px 0;
border-bottom:1px solid rgba(60,60,67,.08);
}
.param-row:last-child{border-bottom:none;}
.param-label{
font-size:13px;font-weight:500;color:var(--text-secondary);
min-width:68px;
}
.param-input{
flex:1;height:36px;padding:0 12px;
font-size:14px;font-weight:500;color:var(--text);
background:rgba(255,255,255,.6);
border:1px solid rgba(60,60,67,.15);
border-radius:10px;outline:none;
transition:border-color .15s;
}
.param-input:focus{border-color:var(--accent);}
.param-unit{font-size:13px;color:var(--text-secondary);min-width:18px;}
/* ═══════════════════════════════════════════════
TOAST
═══════════════════════════════════════════════ */
#toast{
position:fixed;
bottom:calc(var(--safe-bottom) + 120px);
left:50%;transform:translateX(-50%) translateY(12px);
z-index:99;
background:rgba(30,30,32,.88);
backdrop-filter:var(--blur-sm);
-webkit-backdrop-filter:var(--blur-sm);
color:#fff;font-size:14px;font-weight:500;
padding:11px 20px;border-radius:var(--r-md);
white-space:nowrap;
opacity:0;pointer-events:none;
transition:opacity .25s,transform .25s;
box-shadow:0 4px 20px rgba(0,0,0,.25);
}
#toast.show{opacity:1;transform:translateX(-50%) translateY(0);}
/* ═══════════════════════════════════════════════
SAVE NAME MODAL
═══════════════════════════════════════════════ */
#modal-backdrop{
position:fixed;inset:0;z-index:50;
background:rgba(0,0,0,.4);
backdrop-filter:var(--blur-sm);
-webkit-backdrop-filter:var(--blur-sm);
display:none;align-items:center;justify-content:center;
padding:20px;
}
#modal-backdrop.open{display:flex;animation:fadeIn .2s ease;}
@keyframes fadeIn{from{opacity:0;}to{opacity:1;}}
#modal{
width:100%;max-width:320px;
background:var(--glass-light);
backdrop-filter:var(--blur-lg);
-webkit-backdrop-filter:var(--blur-lg);
border:1px solid var(--glass-light-border);
box-shadow:0 24px 60px rgba(0,0,0,.2);
border-radius:var(--r-lg);
overflow:hidden;
animation:modalIn .3s cubic-bezier(.34,1.3,.64,1);
}
@keyframes modalIn{from{opacity:0;transform:scale(.88)translateY(20px);}to{opacity:1;transform:none;}}
#modal-header{padding:22px 20px 12px;border-bottom:1px solid rgba(60,60,67,.1);}
#modal-header h3{font-size:17px;font-weight:700;color:var(--text);}
#modal-header p{font-size:13px;color:var(--text-secondary);margin-top:4px;}
#modal-body{padding:16px 20px;}
#modal-name{
width:100%;height:44px;padding:0 14px;
font-size:16px;font-weight:400;color:var(--text);
background:rgba(255,255,255,.7);
border:1px solid rgba(60,60,67,.2);
border-radius:12px;outline:none;
transition:border-color .15s;
}
#modal-name:focus{border-color:var(--accent);}
#modal-footer{
display:flex;border-top:1px solid rgba(60,60,67,.1);
}
.modal-btn{
flex:1;height:52px;border:none;cursor:pointer;
font-size:16px;font-weight:500;transition:background .1s;
}
.modal-btn.cancel{
background:transparent;color:var(--text-secondary);
border-right:1px solid rgba(60,60,67,.1);
}
.modal-btn.cancel:active{background:rgba(60,60,67,.06);}
.modal-btn.confirm{background:transparent;color:var(--accent);font-weight:600;}
.modal-btn.confirm:active{background:rgba(0,122,255,.06);}
/* ═══════════════════════════════════════════════
ZOOM CONTROLS (custom, top right below layer)
═══════════════════════════════════════════════ */
#zoom-controls{
position:fixed;
right:16px;
top:calc(var(--safe-top) + 126px);
z-index:20;
display:flex;flex-direction:column;gap:6px;
}
.zoom-btn{
width:44px;height:44px;
background:var(--glass-light);
backdrop-filter:var(--blur-md);
-webkit-backdrop-filter:var(--blur-md);
border:1px solid var(--glass-light-border);
box-shadow:var(--shadow-float);
border-radius:var(--r-sm);cursor:pointer;
font-size:22px;font-weight:300;color:var(--text);
display:flex;align-items:center;justify-content:center;
transition:transform .12s,background .12s;user-select:none;
}
.zoom-btn:active{transform:scale(.92);background:rgba(255,255,255,.9);}
</style>
</head>
<body>
<script>
if(window.matchMedia('(display-mode: standalone)').matches){
const meta = document.querySelector('meta[name="theme-color"]');
if(meta) meta.remove();
}
</script>
<!-- Top shadow for PWA status bar readability -->
<div id="pwa-top-shadow"></div>
<!-- Search Bar -->
<div id="map"></div>
<!-- Crosshair -->
<div id="crosshair">
<div id="ch-ring"></div>
<svg viewBox="0 0 60 60" xmlns="http://www.w3.org/2000/svg">
<!-- Horizontal line -->
<line x1="2" y1="30" x2="22" y2="30" stroke="#007AFF" stroke-width="2.5" stroke-linecap="round"/>
<line x1="38" y1="30" x2="58" y2="30" stroke="#007AFF" stroke-width="2.5" stroke-linecap="round"/>
<!-- Vertical line -->
<line x1="30" y1="2" x2="30" y2="22" stroke="#007AFF" stroke-width="2.5" stroke-linecap="round"/>
<line x1="30" y1="38" x2="30" y2="58" stroke="#007AFF" stroke-width="2.5" stroke-linecap="round"/>
<!-- Center ring -->
<circle cx="30" cy="30" r="5.5" fill="none" stroke="#007AFF" stroke-width="2.2"/>
<!-- Center dot -->
<circle cx="30" cy="30" r="2" fill="#007AFF"/>
</svg>
</div>
<!-- Coordinate bubble -->
<div id="coord-bubble">
<div id="bubble-content">
<div class="coords" id="bubble-coords">---, ---</div>
<div class="alt" id="bubble-alt">海拔 — m</div>
</div>
<button id="bubble-fav-btn" aria-label="收藏当前位置">
<svg id="bubble-fav-icon" viewBox="0 0 24 24" width="20" height="20" stroke="currentColor" stroke-width="2.2" fill="none" stroke-linecap="round" stroke-linejoin="round" style="color: #FFB800;"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"></polygon></svg>
</button>
</div>
<!-- Top search bar -->
<div id="search-bar">
<input id="search-input" type="text" placeholder="搜索地点…" autocomplete="off" spellcheck="false">
<button id="search-btn" aria-label="搜索">
<svg viewBox="0 0 24 24"><circle cx="11" cy="11" r="7"/><path d="M21 21l-4.35-4.35"/></svg>
</button>
<div id="search-history" class="hidden"></div>
</div>
<div id="search-results"></div>
<!-- Overlay (for panels) -->
<div id="overlay"></div>
<!-- Left: Favorites button -->
<button id="fav-btn" aria-label="收藏夹">
<svg viewBox="0 0 24 24" width="20" height="20" stroke="currentColor" stroke-width="2.2" fill="currentColor" stroke-linecap="round" stroke-linejoin="round" style="color: #FFB800;"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"></polygon></svg>
<span id="fav-badge">0</span>
</button>
<!-- Left: Config button -->
<button id="config-btn" aria-label="小火箭配置">
<svg viewBox="0 0 24 24" width="20" height="20" stroke="currentColor" stroke-width="2.2" fill="none" stroke-linecap="round" stroke-linejoin="round"><path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.1a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"></path><circle cx="12" cy="12" r="3"></circle></svg>
</button>
<!-- Right: Layer switcher -->
<button id="layer-btn" aria-label="切换地图">
<svg viewBox="0 0 24 24" width="20" height="20" stroke="currentColor" stroke-width="2.2" fill="none" stroke-linecap="round" stroke-linejoin="round"><polygon points="12 2 2 7 12 12 22 7 12 2"></polygon><polyline points="2 17 12 22 22 17"></polyline><polyline points="2 12 12 17 22 12"></polyline></svg>
</button>
<div id="layer-menu">
<div class="layer-item" data-layer="amap" id="layer-amap">
高德地图<span class="check">✓</span>
</div>
<div class="layer-item" data-layer="sat" id="layer-sat">
高德卫星<span class="check">✓</span>
</div>
<div class="layer-item" data-layer="osm" id="layer-osm">
国际地图<span class="check">✓</span>
</div>
<div class="layer-item" data-layer="esri" id="layer-esri">
国际卫星<span class="check">✓</span>
</div>
</div>
<!-- Right: Zoom controls -->
<div id="zoom-controls">
<button class="zoom-btn" id="btn-zoom-in">+</button>
<button class="zoom-btn" id="btn-zoom-out">−</button>
<button class="zoom-btn" id="btn-locate" aria-label="定位当前位置">
<svg viewBox="0 0 24 24" width="20" height="20" stroke="currentColor" stroke-width="2.2" fill="none" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><circle cx="12" cy="12" r="3"></circle><line x1="12" y1="1" x2="12" y2="3"></line><line x1="12" y1="21" x2="12" y2="23"></line><line x1="1" y1="12" x2="3" y2="12"></line><line x1="21" y1="12" x2="23" y2="12"></line></svg>
</button>
</div>
<!-- Login overlay -->
<div id="login-overlay" style="display: none;">
<div id="login-card">
<h2>安全登录验证</h2>
<p>自建定位服务受密码保护,请输入您的访问密钥 (Token) 以解锁地图面板。</p>
<input id="login-input" type="password" placeholder="请输入密码 (Token)" autocomplete="off">
<button id="login-submit">进入系统</button>
</div>
</div>
<!-- Config panel -->
<div id="config-panel">
<div id="config-panel-header">
<h2>Shadowrocket 配置</h2>
<button id="config-close">
<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2.5" fill="none" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
</button>
</div>
<div id="config-content">
<div class="config-section">
<div class="config-title">方法 A:一键导入小火箭模块</div>
<div class="config-desc">最推荐的方式。点击下方按钮即可唤醒手机上的 Shadowrocket 自动导入到【模块】列表中。</div>
<button class="config-action-btn" id="btn-config-import">一键导入模块</button>
</div>
<div class="config-section">
<div class="config-title">方法 B:手动复制模块链接</div>
<div class="config-desc">如果在微信或第三方浏览器无法唤醒,请复制下方完整的模块链接,前往 Shadowrocket 的【配置】页面 -> 点击进入【模块】 -> 点击右上角【+】号 -> 粘贴此 URL 下载即可。</div>
<div class="config-code-box" id="config-url-text">...</div>
<button class="config-action-btn" id="btn-config-copy-url">复制模块链接</button>
</div>
<div class="config-section">
<div class="config-title">方法 C:一键复制 argument 参数</div>
<div class="config-desc">如果您已导入了模块,只需将下方为您拼装好的 argument 覆盖原模块参数即可。</div>
<div class="config-code-box" id="config-arg-text">...</div>
<button class="config-action-btn" id="btn-config-copy-arg">复制 argument 参数</button>
</div>
<div class="config-section" style="border-bottom:none;">
<div id="dark-toggle">
<div id="dark-toggle-label">
<svg viewBox="0 0 24 24" width="18" height="18" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path></svg>
深色模式
</div>
<div class="toggle-switch" id="dark-toggle-switch"><div class="toggle-knob"></div></div>
</div>
</div>
</div>
</div>
<!-- Favorites panel -->
<div id="fav-panel">
<div id="fav-panel-header">
<h2>收藏夹</h2>
<button id="fav-close" aria-label="关闭">
<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2.5" fill="none" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
</button>
</div>
<div id="fav-list">
<div id="fav-empty">
<svg viewBox="0 0 24 24" width="40" height="40" stroke="#8E8E93" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round" style="margin-bottom: 12px;"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"></polygon></svg>
暂无收藏<br>锁定位置后点「收藏」即可保存
</div>
</div>
</div>
<!-- Bottom drawer -->
<div id="drawer">
<div id="drawer-handle"></div>
<!-- Expanded params (shown when drawer is dragged up) -->
<div id="drawer-expanded">
<div class="param-row">
<span class="param-label">海拔</span>
<input class="param-input" id="inp-alt" type="number" inputmode="decimal" placeholder="自动">
<span class="param-unit">m</span>
</div>
<div class="param-row">
<span class="param-label">水平精度</span>
<input class="param-input" id="inp-hacc" type="number" inputmode="decimal" placeholder="39">
<span class="param-unit">m</span>
</div>
<div class="param-row">
<span class="param-label">垂直精度</span>
<input class="param-input" id="inp-vacc" type="number" inputmode="decimal" placeholder="1000">
<span class="param-unit">m</span>
</div>
</div>
<!-- Collapsed actions (always visible) -->
<div id="drawer-collapsed">
<div id="status-pill">
<span id="status-dot"></span>
<span id="status-text">点击地图选择位置</span>
</div>
<button id="btn-lock">
<svg viewBox="0 0 24 24" width="18" height="18" stroke="currentColor" stroke-width="2.5" fill="none" stroke-linecap="round" stroke-linejoin="round" style="margin-right: 4px;"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"></path><circle cx="12" cy="10" r="3"></circle></svg>
<span id="btn-lock-label">锁定</span>
</button>
</div>
</div>
<!-- Save-name modal -->
<div id="modal-backdrop">
<div id="modal">
<div id="modal-header">
<h3>命名收藏</h3>
<p id="modal-coords-hint">--</p>
</div>
<div id="modal-body">
<input id="modal-name" type="text" maxlength="30" placeholder="输入名称(如:家、公司、咖啡厅)">
</div>
<div id="modal-footer">
<button class="modal-btn cancel" id="modal-cancel">取消</button>
<button class="modal-btn confirm" id="modal-confirm">保存</button>
</div>
</div>
</div>
<!-- Toast -->
<div id="toast"></div>
<!-- Scripts -->
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script>
(function(){
'use strict';
/* ─── Runtime config injected by server ─────────────────── */
var CFG = window.__CFG__ || {};
var HAS_TOKEN = CFG.hasToken === true;
var AMAP_KEY = CFG.amapKey || '';
var API = function(p){
var t = localStorage.getItem('gps_token') || '';
return p + (t ? '?token=' + encodeURIComponent(t) : '');
};
/* ─── GCJ-02 ↔ WGS-84 (China map offset correction) ─────── */
var GCJ = (function(){
var PI=Math.PI, a=6378245.0, ee=0.00669342162296594323;
function out(lat,lng){return(lng<72.004||lng>137.8347)||(lat<0.8293||lat>55.8271);}
function tLat(x,y){
var r=-100+2*x+3*y+.2*y*y+.1*x*y+.2*Math.sqrt(Math.abs(x));
r+=(20*Math.sin(6*x*PI)+20*Math.sin(2*x*PI))*2/3;
r+=(20*Math.sin(y*PI)+40*Math.sin(y/3*PI))*2/3;
r+=(160*Math.sin(y/12*PI)+320*Math.sin(y*PI/30))*2/3;return r;
}
function tLng(x,y){
var r=300+x+2*y+.1*x*x+.1*x*y+.1*Math.sqrt(Math.abs(x));
r+=(20*Math.sin(6*x*PI)+20*Math.sin(2*x*PI))*2/3;
r+=(20*Math.sin(x*PI)+40*Math.sin(x/3*PI))*2/3;
r+=(150*Math.sin(x/12*PI)+300*Math.sin(x/30*PI))*2/3;return r;
}
function w2g(lat,lng){
if(out(lat,lng))return[lat,lng];
var dLat=tLat(lng-105,lat-35),dLng=tLng(lng-105,lat-35);
var rLat=lat/180*PI,m=Math.sin(rLat);m=1-ee*m*m;var sm=Math.sqrt(m);
dLat=dLat*180/((a*(1-ee))/(m*sm)*PI);
dLng=dLng*180/(a/sm*Math.cos(rLat)*PI);
return[lat+dLat,lng+dLng];
}
function g2w(lat,lng){
if(out(lat,lng))return[lat,lng];
var g=w2g(lat,lng);return[lat*2-g[0],lng*2-g[1]];
}
return{w2g:w2g,g2w:g2w};
})();
/* ─── State ──────────────────────────────────────────────── */
var map, currentLayer=(AMAP_KEY?'amap':'osm');
var WGS={lat:39.90872,lng:116.39748}; // current center in WGS-84
var lockedWGS=null; // last saved to server
var altitude=null,hacc=39,vacc=1000;
var altFetchTimer=null;
var drawerExpanded=false;
var drawerDragStartY=0,drawerDragStartExpanded=false;
var favorites=[];
var favPanelOpen=false;
/* ─── DOM refs ───────────────────────────────────────────── */
function $id(id){return document.getElementById(id);}
var elBubbleCoords=$id('bubble-coords'),
elBubbleAlt=$id('bubble-alt'),
elStatusDot=$id('status-dot'),
elStatusText=$id('status-text'),
elBtnLock=$id('btn-lock'),
elBtnLockLabel=$id('btn-lock-label'),
elDrawerExpanded=$id('drawer-expanded'),
elFavList=$id('fav-list'),
elFavBadge=$id('fav-badge'),
elFavEmpty=$id('fav-empty'),
elOverlay=$id('overlay');
/* ─── Toast ──────────────────────────────────────────────── */
var toastTimer=null;
function toast(msg){
var el=$id('toast');
el.textContent=msg;el.classList.add('show');
if(toastTimer)clearTimeout(toastTimer);
toastTimer=setTimeout(function(){el.classList.remove('show');},2200);
}
/* ─── Coordinate display ─────────────────────────────────── */
function fmtCoord(v,pos,neg){
var dir=v>=0?pos:neg;
return Math.abs(v).toFixed(5)+'° '+dir;
}
function updateBubble(){
elBubbleCoords.textContent=fmtCoord(WGS.lat,'N','S')+' '+fmtCoord(WGS.lng,'E','W');
elBubbleAlt.textContent=altitude!==null?('海拔 '+Math.round(altitude)+' m'):'海拔 — m';
}
function updateStatus(){
if(!lockedWGS){
elStatusDot.className='status-dot';
elStatusText.textContent='拖动地图,然后点「锁定」';
return;
}
var same=Math.abs(WGS.lat-lockedWGS.lat)<0.00001&&Math.abs(WGS.lng-lockedWGS.lng)<0.00001;
if(same){
elStatusDot.className='status-dot saved';
elStatusText.textContent='已锁定 · '+WGS.lat.toFixed(4)+', '+WGS.lng.toFixed(4);
}else{
elStatusDot.className='status-dot unsaved';
elStatusText.textContent='位置已移动,点「锁定」生效';
}
}
/* ─── Elevation fetch ────────────────────────────────────── */
function fetchElevation(lat,lng){
fetch('https://api.open-meteo.com/v1/elevation?latitude='+lat+'&longitude='+lng)
.then(function(r){return r.json();})
.then(function(d){
if(d&&d.elevation&&d.elevation.length){
altitude=d.elevation[0];
$id('inp-alt').placeholder=Math.round(altitude);
updateBubble();
}
}).catch(function(){});
}
function scheduleElevation(){
if(altFetchTimer)clearTimeout(altFetchTimer);
altFetchTimer=setTimeout(function(){fetchElevation(WGS.lat,WGS.lng);},800);
}
/* ─── Map center → WGS-84 ────────────────────────────────── */
function onMapMove(){
var c=map.getCenter();
var w=(currentLayer==='osm'||currentLayer==='esri')?[c.lat,c.lng]:GCJ.g2w(c.lat,c.lng);
WGS={lat:w[0],lng:w[1]};
updateBubble();
updateStatus();
scheduleElevation();
updateFavStar();
}
/* ─── Map setup ──────────────────────────────────────────── */
var layers={};
function buildLayers(){
layers.amap=L.tileLayer(
'https://wprd0{s}.is.autonavi.com/appmaptile?x={x}&y={y}&z={z}&lang=zh_cn&size=1&scl=1&style=7',
{subdomains:'1234',maxZoom:19,attribution:'© 高德地图'});
layers.sat=L.layerGroup([
L.tileLayer('https://webst0{s}.is.autonavi.com/appmaptile?style=6&x={x}&y={y}&z={z}',{subdomains:'1234',maxZoom:19}),
L.tileLayer('https://wprd0{s}.is.autonavi.com/appmaptile?x={x}&y={y}&z={z}&lang=zh_cn&size=1&scl=1&style=8',{subdomains:'1234',maxZoom:19,attribution:'© 高德地图'})
]);
layers.osm=L.tileLayer(
'https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}{r}.png',
{maxZoom:20,attribution:'© OpenStreetMap contributors, © CartoDB'});
layers.esri=L.tileLayer(
'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}',
{maxZoom:19,attribution:'Tiles © Esri — Source: Esri, i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community'});
}
function initMap(lat,lng){
var dispLat,dispLng;
if(currentLayer==='osm'||currentLayer==='esri'){dispLat=lat;dispLng=lng;}
else{var g=GCJ.w2g(lat,lng);dispLat=g[0];dispLng=g[1];}
map=L.map('map',{zoomControl:false,attributionControl:true});
buildLayers();
layers[currentLayer].addTo(map);
map.setView([dispLat,dispLng],13);
map.on('move',onMapMove);
map.on('moveend',onMapMove);
// Sync layer checkmark to currentLayer
document.querySelectorAll('.layer-item').forEach(function(el){
el.classList.toggle('active', el.dataset.layer===currentLayer);
});
// Zoom buttons
$id('btn-zoom-in').addEventListener('click',function(){map.zoomIn();});
$id('btn-zoom-out').addEventListener('click',function(){map.zoomOut();});
}
/* ─── Layer switching ────────────────────────────────────── */
function switchLayer(name){
if(name===currentLayer)return;
var c=map.getCenter();
// Convert current display coords → WGS-84 → new display coords
var wgs=(currentLayer==='osm'||currentLayer==='esri')?[c.lat,c.lng]:GCJ.g2w(c.lat,c.lng);
var newDisp=(name==='osm'||name==='esri')?wgs:GCJ.w2g(wgs[0],wgs[1]);
map.removeLayer(layers[currentLayer]);
currentLayer=name;
layers[name].addTo(map);
map.setView([newDisp[0],newDisp[1]],map.getZoom(),{animate:false});
// Update UI
document.querySelectorAll('.layer-item').forEach(function(el){
el.classList.toggle('active',el.dataset.layer===name);
});
$id('layer-menu').style.display='none';
}
/* ─── Search ─────────────────────────────────────────────── */
var searchDebounce=null;
var searchHistory = [];
var HISTORY_KEY = 'ios_loc_spoofer_history';
function loadHistory() {
try {
var raw = localStorage.getItem(HISTORY_KEY);
if(raw) searchHistory = JSON.parse(raw);
} catch(e){}
}
function saveHistory(query) {
if(!query.trim()) return;
query = query.trim();
searchHistory = searchHistory.filter(function(q) { return q !== query; });
searchHistory.unshift(query);
if(searchHistory.length > 10) searchHistory.pop();
localStorage.setItem(HISTORY_KEY, JSON.stringify(searchHistory));
renderHistory();
}
function renderHistory() {
var container = $id('search-history');
if(searchHistory.length === 0) {
container.innerHTML = '';
return;
}
var html = '';
searchHistory.forEach(function(q) {
html += '<div class="history-item" onclick="onHistoryClick(event, \'' + q.replace(/'/g, "\\'") + '\')">'
+ '<svg class="history-icon" viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></svg>'
+ '<span>' + escHtml(q) + '</span>'
+ '</div>';
});
html += '<div class="history-clear" onclick="clearHistory(event)">清除历史记录</div>';
container.innerHTML = html;
}
window.onHistoryClick = function(e, query) {
if (e) e.stopPropagation();
$id('search-input').value = query;
doSearch();
}
window.clearHistory = function(e) {
if (e) e.stopPropagation();
searchHistory = [];
localStorage.removeItem(HISTORY_KEY);
renderHistory();
$id('search-input').focus();
}
function doSearch(){
var q=$id('search-input').value.trim();
if(!q)return;
saveHistory(q);
$id('search-input').blur(); // 取消焦点
$id('search-history').classList.add('hidden'); // 隐藏历史记录下拉
// 坐标直接跳转:支持 "lat, lng" 或 "lat lng" 格式
var coordMatch=q.match(/^\s*(-?\d+\.?\d*)\s*[,\s]\s*(-?\d+\.?\d*)\s*$/);
if(coordMatch){
var cLat=parseFloat(coordMatch[1]),cLng=parseFloat(coordMatch[2]);
if(cLat>=-90&&cLat<=90&&cLng>=-180&&cLng<=180){
var disp=(currentLayer==='osm'||currentLayer==='esri')?[cLat,cLng]:GCJ.w2g(cLat,cLng);
map.setView([disp[0],disp[1]],16,{animate:true});
$id('search-results').style.display='none';
$id('search-input').value='';
toast('已跳转到坐标 '+cLat.toFixed(5)+', '+cLng.toFixed(5));
return;
}
}
var box=$id('search-results');
box.innerHTML='<div class="result-row"><span class="name">搜索中…</span></div>';
box.style.display='block';
if (AMAP_KEY) {
fetch('https://restapi.amap.com/v3/assistant/inputtips?keywords='+encodeURIComponent(q)+'&key='+AMAP_KEY)
.then(function(r){return r.json();})
.then(function(d){
box.innerHTML='';
if(d.status !== '1' || !d.tips || !d.tips.length){
box.innerHTML='<div class="result-row"><span class="name">未找到结果</span></div>';
return;
}
d.tips.forEach(function(it){
if (!it.location || typeof it.location !== 'string') return;
var row=document.createElement('div');row.className='result-row';
var locParts = it.location.split(',');
var gcjLng = parseFloat(locParts[0]);
var gcjLat = parseFloat(locParts[1]);
row.innerHTML='<div class="name">'+escHtml(it.name)+'</div>'
+'<div class="sub">'+escHtml((it.district || '') + (it.address || ''))+'</div>';
row.addEventListener('click',function(){
var disp = (currentLayer === 'osm'||currentLayer === 'esri') ? GCJ.g2w(gcjLat, gcjLng) : [gcjLat, gcjLng];
map.setView([disp[0], disp[1]], 16, {animate:true});
box.style.display='none';
$id('search-input').value='';
});
box.appendChild(row);
});
if (!box.children.length) {
box.innerHTML='<div class="result-row"><span class="name">无有效定位结果</span></div>';
}
}).catch(function(){
box.innerHTML='<div class="result-row"><span class="name">搜索失败,请重试</span></div>';
});
} else {
fetch('https://nominatim.openstreetmap.org/search?format=json&limit=6&q='+encodeURIComponent(q))
.then(function(r){return r.json();})
.then(function(a){
box.innerHTML='';
if(!a||!a.length){
box.innerHTML='<div class="result-row"><span class="name">未找到结果 (国内搜索建议配置高德 Key)</span></div>';
return;
}
a.forEach(function(it){
var row=document.createElement('div');row.className='result-row';
var parts=it.display_name.split(',');
row.innerHTML='<div class="name">'+parts[0]+'</div>'
+'<div class="sub">'+parts.slice(1,3).join(',')+'</div>';
row.addEventListener('click',function(){
var wLat=+it.lat,wLng=+it.lon;
var disp=(currentLayer==='osm'||currentLayer==='esri')?[wLat,wLng]:GCJ.w2g(wLat,wLng);
map.setView([disp[0], disp[1]], 16, {animate:true});
box.style.display='none';
$id('search-input').value='';
});
box.appendChild(row);
});
}).catch(function(){
box.innerHTML='<div class="result-row"><span class="name">搜索失败,请重试</span></div>';
});
}
}
$id('search-btn').addEventListener('click',doSearch);
$id('search-input').addEventListener('keydown',function(e){if(e.key==='Enter')doSearch();});
$id('search-input').addEventListener('focus', function() {
if(searchHistory.length > 0) {
$id('search-history').classList.remove('hidden');
}
});
document.addEventListener('click',function(e){
if(!$id('search-bar').contains(e.target)&&!$id('search-results').contains(e.target)){
$id('search-results').style.display='none';
$id('search-history').classList.add('hidden');
}
});
/* ─── Lock location ──────────────────────────────────────── */
function lockLocation(){
elBtnLock.classList.add('locking');
elBtnLockLabel.textContent='保存中…';
var altVal=$id('inp-alt').value?parseFloat($id('inp-alt').value):altitude;
var haccVal=$id('inp-hacc').value?parseFloat($id('inp-hacc').value):hacc;
var vaccVal=$id('inp-vacc').value?parseFloat($id('inp-vacc').value):vacc;
var payload={
latitude:WGS.lat,longitude:WGS.lng,
altitude:altVal||0,
horizontalAccuracy:haccVal||39,
verticalAccuracy:vaccVal||1000
};
fetch(API('/set'),{
method:'POST',
headers:{'Content-Type':'application/json'},
body:JSON.stringify(payload)
}).then(function(r){
if(!r.ok)throw new Error(r.status);
lockedWGS={lat:WGS.lat,lng:WGS.lng};
$id('crosshair').classList.add('locked');
setTimeout(function(){$id('crosshair').classList.remove('locked');},400);
toast('位置已锁定!请前往系统 定位服务 重新开关生效');
updateStatus();
updateLockedPin(WGS.lat,WGS.lng);
}).catch(function(){
toast('保存失败,请检查网络或 Token');
}).finally(function(){
elBtnLock.classList.remove('locking');
elBtnLockLabel.textContent='锁定';
});
}
elBtnLock.addEventListener('click',lockLocation);
/* ─── Locked position pin ────────────────────────── */
var lockedMarker = null;
function updateLockedPin(lat, lng) {
var disp = (currentLayer==='osm'||currentLayer==='esri') ? [lat, lng] : GCJ.w2g(lat, lng);
var pinHtml = '<div class="locked-pin"><div class="locked-pin-inner"></div><div class="locked-pin-dot"></div></div>';
var icon = L.divIcon({ html: pinHtml, className: '', iconSize: [28, 36], iconAnchor: [14, 36] });
if (lockedMarker) {
lockedMarker.setLatLng([disp[0], disp[1]]).setIcon(icon);
} else {
lockedMarker = L.marker([disp[0], disp[1]], { icon: icon, interactive: false, zIndexOffset: -100 }).addTo(map);
}
}
/* ─── Dark mode ──────────────────────────────────── */
var darkSwitch = $id('dark-toggle-switch');
var isDark = localStorage.getItem('darkMode') === '1';
function applyDark(v) {
isDark = v;
document.documentElement.classList.toggle('dark', v);
darkSwitch.classList.toggle('on', v);
localStorage.setItem('darkMode', v ? '1' : '0');
}
applyDark(isDark);
darkSwitch.addEventListener('click', function() { applyDark(!isDark); });
/* ─── Drawer expand/collapse ─────────────────────────────── */
var drawer=$id('drawer');
var drawerHandle=$id('drawer-handle');
function setDrawerExpanded(v){
drawerExpanded=v;
elDrawerExpanded.classList.toggle('open',v);
}
// Tap handle to toggle
drawerHandle.addEventListener('click',function(){setDrawerExpanded(!drawerExpanded);});
// Touch drag on drawer
var touchStartY=0,touchDragging=false;
drawer.addEventListener('touchstart',function(e){
touchStartY=e.touches[0].clientY;
touchDragging=true;
},{ passive:true });
drawer.addEventListener('touchend',function(e){
if(!touchDragging)return;
touchDragging=false;
var delta=touchStartY-e.changedTouches[0].clientY;
if(Math.abs(delta)>30){setDrawerExpanded(delta>0);}
},{ passive:true });
/* ─── Favorites ──────────────────────────────────────────── */
function loadFavorites(){
fetch(API('/favorites'))
.then(function(r){return r.json();})
.then(function(a){favorites=a||[];renderFavorites();updateFavStar();})
.catch(function(){});
}
/* Update the bubble star icon: solid if current position is already a favorite */
// 坐标匹配:容差 0.000015° ≈ 1.7m,用于消除 WGS84↔GCJ02 转换的浮点误差
function coordMatch(f, wgs) {
return Math.abs(f.latitude - wgs.lat) < 0.000015
&& Math.abs(f.longitude - wgs.lng) < 0.000015;
}
function updateFavStar(){
var icon=$id('bubble-fav-icon');
if(!icon)return;
var isFav=favorites.some(function(f){ return coordMatch(f, WGS); });
icon.setAttribute('fill', isFav ? 'currentColor' : 'none');
}
function renderFavorites(){
elFavBadge.textContent=favorites.length;
elFavBadge.style.opacity=favorites.length?'1':'0';
if(!favorites.length){
elFavList.innerHTML='';elFavList.appendChild(elFavEmpty);
return;
}
elFavList.innerHTML='';
favorites.forEach(function(f){
var item=document.createElement('div');item.className='fav-item';
item.innerHTML=
'<div class="fav-icon">'
+'<svg viewBox="0 0 24 24" width="20" height="20" stroke="#007AFF" stroke-width="2.2" fill="none" stroke-linecap="round" stroke-linejoin="round"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"></path><circle cx="12" cy="10" r="3"></circle></svg>'
+'</div>'
+'<div class="fav-info">'
+'<div class="fav-name">'+escHtml(f.name)+'</div>'
+'<div class="fav-coords">'+f.latitude.toFixed(5)+', '+f.longitude.toFixed(5)+'</div>'
+'</div>'
+'<button class="fav-del" data-id="'+f.id+'">'
+'<svg viewBox="0 0 24 24" width="16" height="16" stroke="#FF453A" stroke-width="2.2" fill="none" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path><line x1="10" y1="11" x2="10" y2="17"></line><line x1="14" y1="11" x2="14" y2="17"></line></svg>'
+'</button>';
// Jump to location on tap
item.addEventListener('click',function(e){
if(e.target.classList.contains('fav-del'))return;
var disp=currentLayer==='osm'?[f.latitude,f.longitude]:GCJ.w2g(f.latitude,f.longitude);
map.setView([disp[0],disp[1]],15,{animate:true});
closeFavPanel();
});
// Delete
item.querySelector('.fav-del').addEventListener('click',function(e){
e.stopPropagation();
deleteFavorite(f.id);
});
elFavList.appendChild(item);
});
}
function deleteFavorite(id){
fetch(API('/favorites/'+id),{method:'DELETE'})
.then(function(){loadFavorites();toast('已删除');})
.catch(function(){toast('删除失败');});
}
function escHtml(s){
return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
}
/* Favorites panel open/close */
function openFavPanel(){
favPanelOpen=true;
$id('fav-panel').classList.add('open');
elOverlay.classList.add('visible');
}
function closeFavPanel(){
favPanelOpen=false;
$id('fav-panel').classList.remove('open');
elOverlay.classList.remove('visible');
}
$id('fav-btn').addEventListener('click',function(){favPanelOpen?closeFavPanel():openFavPanel();});
$id('fav-close').addEventListener('click',closeFavPanel);
elOverlay.addEventListener('click',function(){
closeFavPanel();
$id('layer-menu').style.display='none';
});
/* Add favorite flow */
$id('bubble-fav-btn').addEventListener('click', function(e){
e.stopPropagation();
// 已收藏 → 取消收藏;未收藏 → 弹出新增窗口
var existing=favorites.find(function(f){ return coordMatch(f, WGS); });
if(existing){
deleteFavorite(existing.id);
toast('已取消收藏');
} else {
showSaveModal();
}
});
// Locate current position
$id('btn-locate').addEventListener('click', function(e){
e.stopPropagation();
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(function(pos) {
var lat = pos.coords.latitude;
var lng = pos.coords.longitude;
var disp = (currentLayer === 'osm'||currentLayer === 'esri') ? [lat, lng] : GCJ.w2g(lat, lng);
map.setView([disp[0], disp[1]], 16, {animate: true});
toast('已定位到当前真实位置');
}, function(err) {
toast('定位失败: ' + err.message);
}, {enableHighAccuracy: true, timeout: 6000});
} else {
toast('您的浏览器不支持定位功能');
}
});
function showSaveModal(){
$id('modal-coords-hint').textContent=WGS.lat.toFixed(5)+', '+WGS.lng.toFixed(5);
$id('modal-name').value='';
$id('modal-backdrop').classList.add('open');
setTimeout(function(){$id('modal-name').focus();},200);
}
$id('modal-cancel').addEventListener('click',function(){$id('modal-backdrop').classList.remove('open');});
$id('modal-confirm').addEventListener('click',saveFavorite);
$id('modal-name').addEventListener('keydown',function(e){if(e.key==='Enter')saveFavorite();});
function saveFavorite(){
var name=$id('modal-name').value.trim()||'未命名';
var altVal=$id('inp-alt').value?parseFloat($id('inp-alt').value):altitude;
// 去重:相同位置(容差 0.000015°)不重复收藏
var duplicate=favorites.some(function(f){ return coordMatch(f, WGS); });
if(duplicate){
$id('modal-backdrop').classList.remove('open');
toast('该位置已在收藏夹中');
return;
}
var payload={
name:name,latitude:WGS.lat,longitude:WGS.lng,
altitude:altVal||null,
horizontalAccuracy:parseFloat($id('inp-hacc').value)||39,
verticalAccuracy:parseFloat($id('inp-vacc').value)||1000
};
fetch(API('/favorites'),{
method:'POST',
headers:{'Content-Type':'application/json'},
body:JSON.stringify(payload)
}).then(function(r){return r.json();})
.then(function(){
$id('modal-backdrop').classList.remove('open');
loadFavorites();
toast('已加入收藏');
updateFavStar();
}).catch(function(){toast('收藏失败');});
}
/* Layer menu */
$id('layer-btn').addEventListener('click',function(e){
e.stopPropagation();
var m=$id('layer-menu');
m.style.display=m.style.display==='block'?'none':'block';
});
document.querySelectorAll('.layer-item').forEach(function(el){
el.addEventListener('click',function(){switchLayer(el.dataset.layer);});
});
document.addEventListener('click',function(e){
if(!$id('layer-btn').contains(e.target)&&!$id('layer-menu').contains(e.target)){
$id('layer-menu').style.display='none';
}
});
/* ─── Config Panel logic ────────────────────────────────── */
var configPanelOpen = false;
function openConfigPanel() {
configPanelOpen = true;
$id('config-panel').classList.add('open');
elOverlay.classList.add('visible');
// Dynamically build paths
var domain = window.location.origin;
var tokenVal = localStorage.getItem('gps_token') || '';
// Format links
var moduleUrl = domain + '/ios-location-spoofer.sgmodule' + (tokenVal ? '?token=' + encodeURIComponent(tokenVal) : '');
var argText = 'configUrl=' + domain + '/loc.json' + (tokenVal ? '?token=' + encodeURIComponent(tokenVal) : '');
$id('config-url-text').textContent = moduleUrl;
$id('config-arg-text').textContent = argText;
}
function closeConfigPanel() {
configPanelOpen = false;
$id('config-panel').classList.remove('open');
if (!favPanelOpen) elOverlay.classList.remove('visible');
}
$id('config-btn').addEventListener('click', function(e) {
e.stopPropagation();
configPanelOpen ? closeConfigPanel() : openConfigPanel();
});
$id('config-close').addEventListener('click', closeConfigPanel);
$id('btn-config-import').addEventListener('click', function() {
var url = $id('config-url-text').textContent;
var importUrl = 'shadowrocket://install/module?url=' + encodeURIComponent(url);
window.location.href = importUrl;
toast('已唤起 Shadowrocket 导入模块');
});
$id('btn-config-copy-url').addEventListener('click', function() {
navigator.clipboard.writeText($id('config-url-text').textContent)
.then(function() { toast('模块链接已复制'); })
.catch(function() { toast('复制失败,请手动选择复制'); });
});
$id('btn-config-copy-arg').addEventListener('click', function() {
navigator.clipboard.writeText($id('config-arg-text').textContent)
.then(function() { toast('参数已复制'); })
.catch(function() { toast('复制失败,请手动选择复制'); });
});
// Click overlay closes everything
elOverlay.addEventListener('click', function(){
closeFavPanel();
closeConfigPanel();
$id('layer-menu').style.display='none';
});
/* ─── Security login checks (30 days expiration) ─────────── */
function verifyToken(token, callback) {
fetch(window.location.origin + '/verify?token=' + encodeURIComponent(token))
.then(function(r) {
if (r.status === 200) callback(true);
else callback(false);
})
.catch(function() { callback(false); });
}
function showLoginScreen(onSuccess) {
var overlay = $id('login-overlay');
var input = $id('login-input');
var submit = $id('login-submit');
overlay.style.display = 'flex';
input.value = '';
function attemptLogin() {
var val = input.value.trim();
if (!val) {
toast('请输入密码');
return;
}
submit.disabled = true;
submit.textContent = '验证中...';
verifyToken(val, function(ok) {
submit.disabled = false;
submit.textContent = '进入系统';
if (ok) {
// Cache token for 30 days
var session = {
token: val,
expiresAt: Date.now() + 30 * 24 * 60 * 60 * 1000 // 30 days
};
localStorage.setItem('gps_token', val);
localStorage.setItem('gps_session', JSON.stringify(session));
overlay.style.display = 'none';
onSuccess(val);
toast('登录成功,有效期30天');
} else {
toast('密码错误,请重新输入');
input.value = '';
input.focus();
}
});
}
submit.onclick = attemptLogin;
input.onkeydown = function(e) {
if (e.key === 'Enter') attemptLogin();
};
}
function handleAuthentication(onAuthenticated) {
if (!HAS_TOKEN) {
// Open access, no token required on server
$id('login-overlay').style.display = 'none';
localStorage.removeItem('gps_token');
localStorage.removeItem('gps_session');
onAuthenticated('');
return;
}
var session = {};
try {
session = JSON.parse(localStorage.getItem('gps_session') || '{}');
} catch(e) {}
if (session.token && session.expiresAt && session.expiresAt > Date.now()) {
verifyToken(session.token, function(ok) {
if (ok) {
$id('login-overlay').style.display = 'none';
onAuthenticated(session.token);
} else {
showLoginScreen(onAuthenticated);
}
});
} else {
showLoginScreen(onAuthenticated);
}
}
/* ─── Init: load current coords from server ─────────────── */
handleAuthentication(function(activeToken) {
// Update API URL helper with cached token dynamically
API = function(p) {
var t = activeToken || localStorage.getItem('gps_token') || '';
return p + (t ? '?token=' + encodeURIComponent(t) : '');
};
fetch(API('/loc.json'))
.then(function(r){return r.json();})
.then(function(d){
WGS={lat:d.latitude||39.90872,lng:d.longitude||116.39748};
lockedWGS={lat:WGS.lat,lng:WGS.lng};
altitude=d.altitude||null;
hacc=d.horizontalAccuracy||39;
vacc=d.verticalAccuracy||1000;
$id('inp-hacc').placeholder=hacc;
$id('inp-vacc').placeholder=vacc;
if(altitude!==null)$id('inp-alt').placeholder=Math.round(altitude);
initMap(WGS.lat,WGS.lng);
updateLockedPin(lockedWGS.lat, lockedWGS.lng);
updateBubble();
updateStatus();
loadHistory();
renderHistory();
loadFavorites();
})
.catch(function(){
// fallback: Beijing
initMap(39.90872,116.39748);
updateLockedPin(39.90872,116.39748);
updateBubble();
updateStatus();
loadHistory();
renderHistory();
loadFavorites();
toast('无法连接服务器,请检查 Token');
});
});
if (!AMAP_KEY) {
$id('search-input').placeholder = '搜索地点 (国内搜索建议配置高德 Key)';
}
})();
</script>
</body>
</html>