feat: 添加双屏控制功能,更新相关逻辑以支持双屏窗口全屏展示;新增全局错误边界组件以处理渲染错误
This commit is contained in:
@@ -480,6 +480,8 @@ _VALID_DISPLAY_ACTIONS = {
|
|||||||
"voice_start", "voice_stop", "voice_refresh",
|
"voice_start", "voice_stop", "voice_refresh",
|
||||||
# 媒体
|
# 媒体
|
||||||
"play", "pause", "next", "prev", "set_mode", "play_target",
|
"play", "pause", "next", "prev", "set_mode", "play_target",
|
||||||
|
# 双屏
|
||||||
|
"dual_screen",
|
||||||
# 全局
|
# 全局
|
||||||
"alert", "show_card", "minimize", "settings_changed", "playlist_changed",
|
"alert", "show_card", "minimize", "settings_changed", "playlist_changed",
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-2
@@ -24,15 +24,17 @@ fn set_dual_screen(app: AppHandle, on: bool) -> Result<String, String> {
|
|||||||
let _ = w.set_focus();
|
let _ = w.set_focus();
|
||||||
return Ok("on".into());
|
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 运营中心 · 双屏");
|
.title("OPC 运营中心 · 双屏");
|
||||||
// 定位到第二块屏(非主屏;无第二屏则用第一块)
|
// 定位到第二块屏(非主屏;无第二屏则用第一块)
|
||||||
if let Ok(monitors) = app.available_monitors() {
|
if let Ok(monitors) = app.available_monitors() {
|
||||||
|
eprintln!("[dual] available_monitors={}", monitors.len());
|
||||||
let primary_pos = app
|
let primary_pos = app
|
||||||
.primary_monitor()
|
.primary_monitor()
|
||||||
.ok()
|
.ok()
|
||||||
.flatten()
|
.flatten()
|
||||||
.map(|pm| { let p = pm.position(); (p.x, p.y) });
|
.map(|pm| { let p = pm.position(); (p.x, p.y) });
|
||||||
|
eprintln!("[dual] primary_pos={:?}", primary_pos);
|
||||||
let target = monitors
|
let target = monitors
|
||||||
.iter()
|
.iter()
|
||||||
.find(|m| { let p = m.position(); Some((p.x, p.y)) != primary_pos })
|
.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<String, String> {
|
|||||||
if let Some(mon) = target {
|
if let Some(mon) = target {
|
||||||
let p = mon.position();
|
let p = mon.position();
|
||||||
let s = mon.size();
|
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);
|
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 = 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())
|
Ok("on".into())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
<div style={{ padding: 40, fontFamily: 'sans-serif', color: '#333' }}>
|
||||||
|
<h3>页面加载出错</h3>
|
||||||
|
<pre style={{ whiteSpace: 'pre-wrap', fontSize: 12, background: '#f5f5f5', padding: 12, borderRadius: 8 }}>
|
||||||
|
{String(this.state.err && (this.state.err.message || this.state.err))}
|
||||||
|
</pre>
|
||||||
|
<button
|
||||||
|
style={{ marginTop: 12, padding: '8px 16px', cursor: 'pointer' }}
|
||||||
|
onClick={() => { this.setState({ err: null }); window.location.reload(); }}
|
||||||
|
>
|
||||||
|
重载
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return this.props.children;
|
||||||
|
}
|
||||||
|
}
|
||||||
+6
-3
@@ -1,6 +1,7 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import ReactDOM from "react-dom/client";
|
import ReactDOM from "react-dom/client";
|
||||||
import App from "./App";
|
import App from "./App";
|
||||||
|
import ErrorBoundary from "./components/ErrorBoundary";
|
||||||
import { ThemeProvider } from "@appica/ui-react/providers/theme-provider";
|
import { ThemeProvider } from "@appica/ui-react/providers/theme-provider";
|
||||||
import { getApiBase } from "./config";
|
import { getApiBase } from "./config";
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
@@ -60,9 +61,11 @@ async function bootstrapRuntimeConfig() {
|
|||||||
bootstrapRuntimeConfig().then(() => {
|
bootstrapRuntimeConfig().then(() => {
|
||||||
ReactDOM.createRoot(document.getElementById("root")).render(
|
ReactDOM.createRoot(document.getElementById("root")).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<ThemeProvider>
|
<ErrorBoundary>
|
||||||
<App />
|
<ThemeProvider>
|
||||||
</ThemeProvider>
|
<App />
|
||||||
|
</ThemeProvider>
|
||||||
|
</ErrorBoundary>
|
||||||
</React.StrictMode>,
|
</React.StrictMode>,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user