Skip to content

Commit 7789321

Browse files
committed
feat: add share button with permalink copying and support for URL-based world loading
1 parent 76ca19d commit 7789321

1 file changed

Lines changed: 140 additions & 12 deletions

File tree

src/main/resources/web/index.html

Lines changed: 140 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,57 @@
182182
.health-fill.high { background: var(--success); }
183183
.health-fill.medium { background: var(--warning); }
184184
.health-fill.low { background: var(--danger); }
185+
186+
#share-btn {
187+
position: absolute;
188+
bottom: 60px; /* 位于坐标显示上方 */
189+
left: 20px;
190+
z-index: 1000;
191+
width: 40px;
192+
height: 40px;
193+
border-radius: 50%;
194+
background: var(--panel-bg);
195+
border: 1px solid rgba(255,255,255,0.1);
196+
color: var(--text-primary);
197+
cursor: pointer;
198+
display: flex;
199+
align-items: center;
200+
justify-content: center;
201+
box-shadow: 0 4px 10px rgba(0,0,0,0.3);
202+
transition: all 0.2s;
203+
}
204+
205+
#share-btn:hover {
206+
background: var(--border);
207+
transform: translateY(-2px);
208+
color: var(--accent);
209+
}
210+
211+
#share-btn:active {
212+
transform: translateY(0);
213+
}
214+
215+
/* 提示气泡 (默认隐藏) */
216+
#share-btn .tooltip {
217+
position: absolute;
218+
left: 50px;
219+
background: var(--success);
220+
color: #1a1a2e;
221+
padding: 5px 10px;
222+
border-radius: 4px;
223+
font-size: 12px;
224+
font-weight: bold;
225+
white-space: nowrap;
226+
opacity: 0;
227+
pointer-events: none;
228+
transition: opacity 0.3s;
229+
transform: translateX(10px);
230+
}
231+
232+
#share-btn.copied .tooltip {
233+
opacity: 1;
234+
transform: translateX(0);
235+
}
185236
</style>
186237
</head>
187238
<body>
@@ -199,6 +250,14 @@ <h3>Online Players</h3>
199250
</div>
200251
</div>
201252

253+
<button id="share-btn" title="Copy Link to Current View">
254+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
255+
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
256+
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
257+
</svg>
258+
<span class="tooltip">Link Copied!</span>
259+
</button>
260+
202261
<div class="coords" id="coords" style="opacity: 0;">X: 0, Z: 0</div>
203262

204263
<script>
@@ -226,7 +285,8 @@ <h3>Online Players</h3>
226285
this.dom = {
227286
worldSelect: document.getElementById('world-select'),
228287
playerList: document.getElementById('player-list'),
229-
coords: document.getElementById('coords')
288+
coords: document.getElementById('coords'),
289+
shareBtn: document.getElementById('share-btn')
230290
};
231291

232292
this.initMap();
@@ -274,6 +334,8 @@ <h3>Online Players</h3>
274334
}
275335
});
276336

337+
this.dom.shareBtn.addEventListener('click', () => this.copyPermalink());
338+
277339
// Coords Update (using requestAnimationFrame for performance)
278340
this.map.on('mousemove', (e) => {
279341
if (this.coordReq) cancelAnimationFrame(this.coordReq);
@@ -290,6 +352,33 @@ <h3>Online Players</h3>
290352
});
291353
}
292354

355+
copyPermalink() {
356+
const center = this.map.getCenter();
357+
const zoom = this.map.getZoom();
358+
const x = Math.round(center.lng);
359+
const z = Math.round(-center.lat); // Leaflet Lat 是反转的 Z
360+
const world = this.state.currentWorld;
361+
362+
// 构建 URL
363+
const url = new URL(window.location.href);
364+
url.searchParams.set('world', world);
365+
url.searchParams.set('x', x);
366+
url.searchParams.set('z', z);
367+
url.searchParams.set('zoom', zoom);
368+
369+
// 复制到剪贴板
370+
navigator.clipboard.writeText(url.toString()).then(() => {
371+
// 显示成功提示
372+
this.dom.shareBtn.classList.add('copied');
373+
setTimeout(() => {
374+
this.dom.shareBtn.classList.remove('copied');
375+
}, 2000);
376+
}).catch(err => {
377+
console.error('Failed to copy: ', err);
378+
alert('Failed to copy URL to clipboard');
379+
});
380+
}
381+
293382
async startLoop() {
294383
await this.loadWorlds();
295384
this.updatePlayers(); // Initial call
@@ -309,6 +398,18 @@ <h3>Online Players</h3>
309398
return;
310399
}
311400

401+
// 解析 URL 参数
402+
const params = new URLSearchParams(window.location.search);
403+
const paramWorld = params.get('world');
404+
const paramX = params.get('x');
405+
const paramZ = params.get('z');
406+
const paramZoom = params.get('zoom');
407+
408+
let initialWorld = null;
409+
let initialX = 0;
410+
let initialZ = 0;
411+
let initialZoom = 2; // 默认缩放
412+
312413
data.worlds.forEach((w, index) => {
313414
const opt = document.createElement('option');
314415
opt.value = w.id;
@@ -317,27 +418,54 @@ <h3>Online Players</h3>
317418
opt.dataset.z = w.spawn.z;
318419
this.dom.worldSelect.appendChild(opt);
319420

320-
// Default select first world
321-
if (index === 0) this.setWorld(w.id, w.spawn.x, w.spawn.z);
421+
// 逻辑:如果 URL 指定了世界且存在,则使用它;否则默认使用第一个世界
422+
if (paramWorld && w.id === paramWorld) {
423+
initialWorld = w.id;
424+
// 如果 URL 有坐标,用 URL 的,否则用出生点
425+
initialX = paramX ? parseFloat(paramX) : w.spawn.x;
426+
initialZ = paramZ ? parseFloat(paramZ) : w.spawn.z;
427+
initialZoom = paramZoom ? parseFloat(paramZoom) : this.config.maxZoom;
428+
429+
// 同步 Select 的选中状态
430+
setTimeout(() => { this.dom.worldSelect.value = w.id; }, 0);
431+
} else if (index === 0 && !initialWorld) {
432+
// 默认回退
433+
initialWorld = w.id;
434+
initialX = w.spawn.x;
435+
initialZ = w.spawn.z;
436+
}
322437
});
438+
439+
// 初始化地图视图
440+
if (initialWorld) {
441+
this.setWorld(initialWorld, initialX, initialZ, initialZoom);
442+
}
443+
323444
} catch (e) {
324445
console.error('World load failed', e);
325-
setTimeout(() => this.loadWorlds(), 5000); // Retry
446+
setTimeout(() => this.loadWorlds(), 5000);
326447
}
327448
}
328449

329-
setWorld(id, spawnX, spawnZ) {
330-
if (this.state.currentWorld === id) return;
450+
setWorld(id, x, z, zoom = 2) {
451+
// 即使是同一个世界,如果是通过 URL 跳转来的,我们也允许重新定位视图
452+
// 但如果是普通切换,我们通常只想加载瓦片
453+
454+
const isWorldChanged = this.state.currentWorld !== id;
331455
this.state.currentWorld = id;
332456

333-
// Refresh tiles
334-
this.tileLayer.redraw();
457+
// 刷新 Select UI (防止程序调用 setWorld 时 UI 没变)
458+
if (this.dom.worldSelect.value !== id) {
459+
this.dom.worldSelect.value = id;
460+
}
335461

336-
// Fly to spawn
337-
this.map.setView([-spawnZ, spawnX], 2);
462+
if (isWorldChanged) {
463+
this.tileLayer.redraw();
464+
this.refreshMarkerVisibility();
465+
}
338466

339-
// Update markers visibility
340-
this.refreshMarkerVisibility();
467+
// 移动视角
468+
this.map.setView([-z, x], zoom);
341469
}
342470

343471
async updatePlayers() {

0 commit comments

Comments
 (0)