feat: 大屏桌面壳(Tauri 2)
This commit is contained in:
@@ -0,0 +1,151 @@
|
||||
// 说明:早期 Rust 内嵌后端(server/storage/models/events,axum :10801)已随架构迁移到
|
||||
// Python 后端(FastAPI :10085)而移除;当前 Rust 侧仅负责窗口与生命周期,
|
||||
// 外加一个「运行时配置读取」命令(供前端读取 exe 旁 config.json,部署免重打包改地址)。
|
||||
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use tauri::{AppHandle, Manager, WebviewUrl, WebviewWindowBuilder};
|
||||
|
||||
/// 双屏控制:on=true 在第二块屏幕上新建一个全屏窗口(加载前端首页),on=false 关闭它。
|
||||
/// 用于 admin 端「启动双屏」,让展播内容同时投到电脑的另一块显示器。
|
||||
#[tauri::command]
|
||||
fn set_dual_screen(app: AppHandle, on: bool, parent_id: Option<String>) -> Result<String, String> {
|
||||
if !on {
|
||||
if let Some(w) = app.get_webview_window("secondary") {
|
||||
let _ = w.close();
|
||||
}
|
||||
return Ok("off".into());
|
||||
}
|
||||
// 已存在则直接显示/全屏/聚焦
|
||||
if let Some(w) = app.get_webview_window("secondary") {
|
||||
let _ = w.show();
|
||||
let _ = w.unminimize();
|
||||
let _ = w.set_fullscreen(true);
|
||||
let _ = w.set_focus();
|
||||
return Ok("on".into());
|
||||
}
|
||||
// 主屏位置(用于排除主屏;必须把副窗放到主屏之外)
|
||||
let primary_pos = app
|
||||
.primary_monitor()
|
||||
.ok()
|
||||
.flatten()
|
||||
.map(|pm| { let p = pm.position(); (p.x, p.y) });
|
||||
// 获取全部显示器,寻找一块与主屏位置不同的副屏
|
||||
let monitors = app.available_monitors().map_err(|e| format!("读取显示器失败: {e}"))?;
|
||||
eprintln!("[dual] monitors={} primary_pos={:?}", monitors.len(), primary_pos);
|
||||
let mut target: Option<(i32, i32, u32, u32)> = None; // (x, y, w, h)
|
||||
for m in &monitors {
|
||||
let p = m.position();
|
||||
if Some((p.x, p.y)) != primary_pos {
|
||||
let s = m.size();
|
||||
target = Some((p.x, p.y, s.width, s.height));
|
||||
break;
|
||||
}
|
||||
}
|
||||
// 绝对不允许在主屏桌面上开启;没有副屏则拒绝
|
||||
let (x, y, w, h) = match target {
|
||||
Some(t) => t,
|
||||
None => return Err("未检测到副屏(需连接第二块显示器),无法开启双屏".into()),
|
||||
};
|
||||
eprintln!("[dual] secondary {}x{} @ {},{}", w, h, x, y);
|
||||
let win = WebviewWindowBuilder::new(&app, "secondary", WebviewUrl::default())
|
||||
.title("OPC 运营中心 · 双屏")
|
||||
.position(x as f64, y as f64)
|
||||
.inner_size(w as f64, h as f64)
|
||||
// 由 Rust 明确告知副窗:它的身份是 secondary,且从属于主屏 device_id(parent_id)
|
||||
.initialization_script(format!(
|
||||
"window.__DPM_WINDOW_LABEL__='secondary';window.__DPM_PARENT_ID__='{}';",
|
||||
parent_id.unwrap_or_default().replace('\'', "")
|
||||
))
|
||||
.build()
|
||||
.map_err(|e| format!("创建副窗失败: {e}"))?;
|
||||
// 等窗口内容就绪后再全屏(避免过早全屏导致白屏/布局异常)
|
||||
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())
|
||||
}
|
||||
|
||||
/// 安装更新:把前端已下载的安装包字节写入磁盘 → 启动安装程序 → 退出应用。
|
||||
/// bytes 由前端 fetch /download 得到;启动安装程序后进程独立,应用退出不影响它。
|
||||
#[tauri::command]
|
||||
fn install_update(app: AppHandle, bytes: Vec<u8>, filename: String) -> Result<String, String> {
|
||||
if bytes.len() < 1000 {
|
||||
return Err("下载内容无效或为空".into());
|
||||
}
|
||||
let dest = std::env::temp_dir().join(filename);
|
||||
std::fs::write(&dest, &bytes).map_err(|e| format!("写入安装包失败: {e}"))?;
|
||||
// 启动安装程序(Windows 下 Command::spawn 为独立进程,应用退出后仍在运行)
|
||||
std::process::Command::new(&dest)
|
||||
.spawn()
|
||||
.map_err(|e| format!("启动安装程序失败: {e}"))?;
|
||||
// 稍后退出应用,让安装程序接管
|
||||
let app2 = app.clone();
|
||||
std::thread::spawn(move || {
|
||||
std::thread::sleep(std::time::Duration::from_millis(800));
|
||||
app2.exit(0);
|
||||
});
|
||||
Ok(dest.to_string_lossy().into_owned())
|
||||
}
|
||||
|
||||
/// 读取运行时配置(部署用,免重新打包):
|
||||
/// 1) exe 同目录 config.json / dpm.config.json(最高优先)
|
||||
/// 2) 应用配置目录 config.json
|
||||
/// 内容示例:
|
||||
/// { "api_base": "http://192.168.1.9:10085",
|
||||
/// "mqtt_url": "ws://192.168.1.3:8083/mqtt",
|
||||
/// "mqtt_username": "dpm", "mqtt_password": "123456" }
|
||||
/// 返回 JSON 字符串;未配置返回 null。
|
||||
#[tauri::command]
|
||||
fn read_runtime_config(app: AppHandle) -> Option<String> {
|
||||
let mut candidates: Vec<PathBuf> = Vec::new();
|
||||
if let Ok(exe) = std::env::current_exe() {
|
||||
if let Some(dir) = exe.parent() {
|
||||
candidates.push(dir.join("config.json"));
|
||||
candidates.push(dir.join("dpm.config.json"));
|
||||
}
|
||||
}
|
||||
if let Ok(dir) = app.path().app_config_dir() {
|
||||
candidates.push(dir.join("config.json"));
|
||||
}
|
||||
for p in candidates {
|
||||
if p.exists() {
|
||||
if let Ok(s) = fs::read_to_string(&p) {
|
||||
let t = s.trim();
|
||||
if t.starts_with('{') {
|
||||
return Some(t.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_opener::init())
|
||||
.plugin(tauri_plugin_autostart::init(
|
||||
tauri_plugin_autostart::MacosLauncher::LaunchAgent,
|
||||
Some(vec![]),
|
||||
))
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
read_runtime_config,
|
||||
set_dual_screen,
|
||||
install_update
|
||||
])
|
||||
.on_window_event(|window, event| {
|
||||
if let tauri::WindowEvent::CloseRequested { api, .. } = event {
|
||||
// 仅主窗口关闭=最小化(进程常驻);双屏副窗口可正常关闭
|
||||
if window.label() == "main" {
|
||||
api.prevent_close();
|
||||
let _ = window.minimize();
|
||||
}
|
||||
}
|
||||
})
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
fn main() {
|
||||
dpm_lib::run()
|
||||
}
|
||||
Reference in New Issue
Block a user