diff --git a/backend/app/routers.py b/backend/app/routers.py index 53dd9ce..86a470d 100644 --- a/backend/app/routers.py +++ b/backend/app/routers.py @@ -480,6 +480,8 @@ _VALID_DISPLAY_ACTIONS = { "voice_start", "voice_stop", "voice_refresh", # 媒体 "play", "pause", "next", "prev", "set_mode", "play_target", + # 双屏 + "dual_screen", # 全局 "alert", "show_card", "minimize", "settings_changed", "playlist_changed", } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index ad3e6ae..02acfd9 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -24,15 +24,17 @@ fn set_dual_screen(app: AppHandle, on: bool) -> Result { let _ = w.set_focus(); return Ok("on".into()); } - let mut builder = WebviewWindowBuilder::new(&app, "secondary", WebviewUrl::App("/".into())) + let mut builder = WebviewWindowBuilder::new(&app, "secondary", WebviewUrl::default()) .title("OPC 运营中心 · 双屏"); // 定位到第二块屏(非主屏;无第二屏则用第一块) if let Ok(monitors) = app.available_monitors() { + eprintln!("[dual] available_monitors={}", monitors.len()); let primary_pos = app .primary_monitor() .ok() .flatten() .map(|pm| { let p = pm.position(); (p.x, p.y) }); + eprintln!("[dual] primary_pos={:?}", primary_pos); let target = monitors .iter() .find(|m| { let p = m.position(); Some((p.x, p.y)) != primary_pos }) @@ -40,11 +42,18 @@ fn set_dual_screen(app: AppHandle, on: bool) -> Result { if let Some(mon) = target { let p = mon.position(); let s = mon.size(); + eprintln!("[dual] target {}x{} @ {},{}", s.width, s.height, p.x, p.y); builder = builder.position(p.x as f64, p.y as f64).inner_size(s.width as f64, s.height as f64); } } let win = builder.build().map_err(|e| e.to_string())?; - let _ = win.set_fullscreen(true); + // 等窗口内容就绪后再全屏(避免过早全屏导致白屏/布局异常) + let win2 = win.clone(); + std::thread::spawn(move || { + std::thread::sleep(std::time::Duration::from_millis(400)); + let _ = win2.set_fullscreen(true); + let _ = win2.set_focus(); + }); Ok("on".into()) } diff --git a/src/components/ErrorBoundary.jsx b/src/components/ErrorBoundary.jsx new file mode 100644 index 0000000..8dff8b5 --- /dev/null +++ b/src/components/ErrorBoundary.jsx @@ -0,0 +1,35 @@ +import { Component } from 'react'; + +/* 全局错误边界:渲染崩溃时不白屏,显示具体错误 + 重载按钮,便于双屏/异常定位 */ +export default class ErrorBoundary extends Component { + constructor(props) { + super(props); + this.state = { err: null }; + } + static getDerivedStateFromError(err) { + return { err }; + } + componentDidCatch(err, info) { + // eslint-disable-next-line no-console + console.error('[AppError]', err, info); + } + render() { + if (this.state.err) { + return ( +
+

页面加载出错

+
+            {String(this.state.err && (this.state.err.message || this.state.err))}
+          
+ +
+ ); + } + return this.props.children; + } +} diff --git a/src/main.jsx b/src/main.jsx index 00e94d7..f14ce1d 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -1,6 +1,7 @@ import React from "react"; import ReactDOM from "react-dom/client"; import App from "./App"; +import ErrorBoundary from "./components/ErrorBoundary"; import { ThemeProvider } from "@appica/ui-react/providers/theme-provider"; import { getApiBase } from "./config"; import { invoke } from "@tauri-apps/api/core"; @@ -60,9 +61,11 @@ async function bootstrapRuntimeConfig() { bootstrapRuntimeConfig().then(() => { ReactDOM.createRoot(document.getElementById("root")).render( - - - + + + + + , ); });