@@ -145,6 +145,9 @@ struct App {
145145
146146 /// Phase 7:上一帧的 CPU pass / 网格化统计,用于 HUD。
147147 perf : FramePerfStats ,
148+
149+ /// 游戏内通知队列(timestamp_ms, 消息)。用于在 InGame 状态下显示信令错误等浮窗提示。
150+ notifications : Vec < ( f64 , String ) > ,
148151}
149152
150153#[ wasm_bindgen( start) ]
@@ -211,6 +214,7 @@ pub async fn start() -> Result<(), JsValue> {
211214 preload_state : None ,
212215 relayed_peers : HashSet :: new ( ) ,
213216 perf : FramePerfStats :: default ( ) ,
217+ notifications : Vec :: new ( ) ,
214218 } ) ) ;
215219
216220 install_event_listeners ( & canvas, & document, input. clone ( ) , egui_events, app. clone ( ) ) ?;
@@ -677,20 +681,21 @@ fn render_lobby_frame(app: &Rc<RefCell<App>>, cw: u32, ch: u32) -> Result<(), St
677681 // —— 异步加载存档列表(仅首次进入 Lobby 时触发)——
678682 {
679683 let mut a = app. borrow_mut ( ) ;
680- if a. lobby_state . saved_worlds . is_empty ( ) && !a. lobby_state . saves_loading {
684+ if ! a. lobby_state . saves_loaded && !a. lobby_state . saves_loading {
681685 a. lobby_state . saves_loading = true ;
682686 let app_ref = app. clone ( ) ;
683687 wasm_bindgen_futures:: spawn_local ( async move {
684688 let result = crate :: storage:: list_saved_worlds ( ) . await ;
685689 let mut a = app_ref. borrow_mut ( ) ;
686690 a. lobby_state . saves_loading = false ;
691+ a. lobby_state . saves_loaded = true ;
687692 match result {
688693 Ok ( worlds) => {
689694 a. lobby_state . saved_worlds = worlds;
690695 }
691696 Err ( e) => {
692697 log:: warn!( "[lobby] 加载存档列表失败: {e:?}" ) ;
693- a. lobby_state . error_message = Some ( format ! ( "加载存档失败 : {e:?}" ) ) ;
698+ a. lobby_state . error_message = Some ( format ! ( "Failed to load saves : {e:?}" ) ) ;
694699 }
695700 }
696701 } ) ;
@@ -788,6 +793,9 @@ fn render_lobby_frame(app: &Rc<RefCell<App>>, cw: u32, ch: u32) -> Result<(), St
788793 wasm_bindgen_futures:: spawn_local ( async move {
789794 if let Err ( e) = crate :: storage:: delete_world_by_key ( & key) . await {
790795 log:: warn!( "[lobby] 删除存档失败: {e:?}" ) ;
796+ let mut a = app_ref. borrow_mut ( ) ;
797+ a. lobby_state . error_message = Some ( format ! ( "Failed to delete save: {e:?}" ) ) ;
798+ return ;
791799 }
792800 // 刷新列表
793801 let result = crate :: storage:: list_saved_worlds ( ) . await ;
@@ -799,6 +807,8 @@ fn render_lobby_frame(app: &Rc<RefCell<App>>, cw: u32, ch: u32) -> Result<(), St
799807 }
800808 Err ( e) => {
801809 log:: warn!( "[lobby] 刷新存档列表失败: {e:?}" ) ;
810+ a. lobby_state . error_message =
811+ Some ( format ! ( "Failed to refresh saves: {e:?}" ) ) ;
802812 }
803813 }
804814 } ) ;
@@ -814,6 +824,8 @@ fn render_lobby_frame(app: &Rc<RefCell<App>>, cw: u32, ch: u32) -> Result<(), St
814824 }
815825 Err ( e) => {
816826 log:: warn!( "[lobby] 刷新存档列表失败: {e:?}" ) ;
827+ a. lobby_state . error_message =
828+ Some ( format ! ( "Failed to refresh saves: {e:?}" ) ) ;
817829 }
818830 }
819831 } ) ;
@@ -1770,6 +1782,15 @@ fn apply_room_event(app: &Rc<RefCell<App>>, ev: RoomEvent) {
17701782 }
17711783 RoomEvent :: SignalingError ( msg) => {
17721784 log:: warn!( "[net] signaling error: {msg}" ) ;
1785+ // InGame 状态下将错误推入通知队列,让玩家在游戏内看到浮窗提示
1786+ if matches ! ( a. state, AppState :: InGame { .. } ) {
1787+ let now = now_ms ( ) ;
1788+ a. notifications . push ( ( now, msg. clone ( ) ) ) ;
1789+ // 最多保留 8 条通知,超出时移除最旧的
1790+ if a. notifications . len ( ) > 8 {
1791+ a. notifications . remove ( 0 ) ;
1792+ }
1793+ }
17731794 a. connecting_error = Some ( msg) ;
17741795 }
17751796 RoomEvent :: PeerCount ( n) => {
@@ -2155,6 +2176,16 @@ fn render_game_frame(
21552176 }
21562177 }
21572178
2179+ // 提取游戏内通知(5 秒内有效),供 egui 闭包渲染浮窗
2180+ let active_notifications: Vec < String > = a
2181+ . notifications
2182+ . iter ( )
2183+ . filter ( |( ts, _) | now_local - ts < 5000.0 )
2184+ . map ( |( _, msg) | msg. clone ( ) )
2185+ . collect ( ) ;
2186+ // 清理过期通知
2187+ a. notifications . retain ( |( ts, _) | now_local - ts < 5000.0 ) ;
2188+
21582189 // —— 跑 egui:在同一 ctx.run 内绘制 HUD + 玩家列表 + 名牌 + 聊天浮窗 + 聊天框 + 暂停菜单 ——
21592190 let App {
21602191 ref egui_ctx,
@@ -2170,6 +2201,10 @@ fn render_game_frame(
21702201 if let Some ( hud) = hud_data. as_ref ( ) {
21712202 draw_hud ( ctx, hud. clone ( ) ) ;
21722203 }
2204+ // 1b) 游戏内通知浮窗(信令错误等,5 秒自动消失)
2205+ if !active_notifications. is_empty ( ) {
2206+ draw_toast_notifications ( ctx, & active_notifications) ;
2207+ }
21732208 // 2) 玩家列表
21742209 ui:: players:: draw_player_list ( ctx, & player_list_entries) ;
21752210 // 3) 远端玩家名牌
@@ -3068,6 +3103,33 @@ fn draw_hud(ctx: &egui::Context, data: HudData) {
30683103 } ) ;
30693104}
30703105
3106+ /// 在屏幕顶部居中绘制通知浮窗(信令错误等),5 秒自动消失。
3107+ /// 多条通知从上到下堆叠,半透明深色背景 + 橙红色文字。
3108+ fn draw_toast_notifications ( ctx : & egui:: Context , messages : & [ String ] ) {
3109+ egui:: Area :: new ( egui:: Id :: new ( "toast_notifications" ) )
3110+ . anchor ( egui:: Align2 :: CENTER_TOP , egui:: vec2 ( 0.0 , 60.0 ) )
3111+ . order ( egui:: Order :: Foreground )
3112+ . show ( ctx, |ui| {
3113+ ui. vertical_centered ( |ui| {
3114+ for msg in messages {
3115+ egui:: Frame :: default ( )
3116+ . fill ( egui:: Color32 :: from_rgba_unmultiplied ( 40 , 20 , 20 , 200 ) )
3117+ . corner_radius ( egui:: CornerRadius :: same ( 6 ) )
3118+ . inner_margin ( egui:: Margin :: symmetric ( 16 , 8 ) )
3119+ . show ( ui, |ui| {
3120+ ui. set_max_width ( 420.0 ) ;
3121+ ui. label (
3122+ egui:: RichText :: new ( msg)
3123+ . color ( egui:: Color32 :: from_rgb ( 240 , 140 , 120 ) )
3124+ . size ( 14.0 ) ,
3125+ ) ;
3126+ } ) ;
3127+ ui. add_space ( 4.0 ) ;
3128+ }
3129+ } ) ;
3130+ } ) ;
3131+ }
3132+
30713133// ============================================================
30723134// 工具
30733135// ============================================================
0 commit comments