1906 lines
71 KiB
HTML
1906 lines
71 KiB
HTML
<!--
|
||
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,'&').replace(/</g,'<').replace(/>/g,'>');
|
||
}
|
||
|
||
/* 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>
|