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