From a5b96eed6e019c2798fac6a390044c2b35f6101a Mon Sep 17 00:00:00 2001 From: Pine Date: Wed, 13 May 2026 00:45:50 +0800 Subject: [PATCH] =?UTF-8?q?-=20=E4=BF=AE=E5=A4=8D=E6=9C=8D=E5=8A=A1?= =?UTF-8?q?=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src-tauri/Cargo.lock | 35 +++++++++++++ src-tauri/Cargo.toml | 1 + src-tauri/src/lib.rs | 2 +- src-tauri/src/server.rs | 100 ++++++++++++++++++++++++++++++++------ src-tauri/tauri.conf.json | 4 +- 5 files changed, 123 insertions(+), 19 deletions(-) diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 0747d68..87d923f 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -869,6 +869,7 @@ dependencies = [ "dirs", "futures", "once_cell", + "rust-embed", "serde", "serde_json", "tauri", @@ -2911,6 +2912,40 @@ dependencies = [ "web-sys", ] +[[package]] +name = "rust-embed" +version = "8.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04113cb9355a377d83f06ef1f0a45b8ab8cd7d8b1288160717d66df5c7988d27" +dependencies = [ + "rust-embed-impl", + "rust-embed-utils", + "walkdir", +] + +[[package]] +name = "rust-embed-impl" +version = "8.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0902e4c7c8e997159ab384e6d0fc91c221375f6894346ae107f47dd0f3ccaa" +dependencies = [ + "proc-macro2", + "quote", + "rust-embed-utils", + "syn 2.0.117", + "walkdir", +] + +[[package]] +name = "rust-embed-utils" +version = "8.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bcdef0be6fe7f6fa333b1073c949729274b05f123a0ad7efcb8efd878e5c3b1" +dependencies = [ + "sha2", + "walkdir", +] + [[package]] name = "rustc-hash" version = "2.1.2" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index bff4b95..ec9e132 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -27,3 +27,4 @@ once_cell = "1" dirs = "6" futures = "0.3" tokio-stream = { version = "0.1", features = ["sync"] } +rust-embed = "8" diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 3e91d9d..ce4a20d 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -19,7 +19,7 @@ pub fn run() { // 等待 HTTP 服务器就绪后再打开窗口 for i in 0..50 { if let Ok(mut s) = std::net::TcpStream::connect_timeout( - &"127.0.0.1:10801".parse().unwrap(), + &"0.0.0.0:10801".parse().unwrap(), Duration::from_millis(500), ) { // 发送一个简单的 HTTP 请求测试 diff --git a/src-tauri/src/server.rs b/src-tauri/src/server.rs index 6f5fb2a..83a3b58 100644 --- a/src-tauri/src/server.rs +++ b/src-tauri/src/server.rs @@ -2,8 +2,9 @@ use crate::events::EVENTS; use crate::models::*; use crate::storage::{get_media_type, is_allowed_extension, STORAGE}; use axum::{ + body::Body, extract::{Form, Multipart, State}, - http::StatusCode, + http::{Response, StatusCode, Uri}, response::sse::{Event, KeepAlive, Sse}, response::Json, routing::{get, post}, @@ -11,6 +12,7 @@ use axum::{ }; use chrono::Local; use futures::stream::Stream; +use rust_embed::RustEmbed; use std::convert::Infallible; use std::path::PathBuf; use std::sync::Arc; @@ -20,6 +22,77 @@ use tokio_stream::StreamExt; use tower_http::cors::CorsLayer; use tower_http::services::ServeDir; +// ===================== 嵌入式前端文件 ===================== +// dist/ 编译进二进制,无需文件系统依赖 +#[derive(RustEmbed)] +#[folder = "../dist"] +struct Asset; + +fn mime_type(path: &str) -> &'static str { + let ext = path.rsplit('.').next().unwrap_or(""); + match ext { + "html" => "text/html; charset=utf-8", + "css" => "text/css; charset=utf-8", + "js" => "application/javascript; charset=utf-8", + "png" => "image/png", + "jpg" | "jpeg" => "image/jpeg", + "svg" => "image/svg+xml", + "ico" => "image/x-icon", + "json" => "application/json", + "woff2" => "font/woff2", + "woff" => "font/woff", + "ttf" => "font/ttf", + _ => "application/octet-stream", + } +} + +async fn spa_fallback(uri: Uri) -> Response { + let path = uri.path().trim_start_matches('/'); + + // 1. 尝试从嵌入式文件提供 + if !path.is_empty() && !path.starts_with("api/") { + if let Some(file) = Asset::get(path) { + return Response::builder() + .header("Content-Type", mime_type(path)) + .body(Body::from(file.data.into_owned())) + .unwrap(); + } + } + + // 2. SPA 回退:所有非 API 路径都返回 index.html + if !path.starts_with("api/") { + if let Some(file) = Asset::get("index.html") { + return Response::builder() + .header("Content-Type", "text/html; charset=utf-8") + .body(Body::from(file.data.into_owned())) + .unwrap(); + } + } + + // 3. 文件系统回退(开发模式可用) + let dist = dist_dir(); + let file_path = dist.join(path); + if let Ok(data) = fs::read(&file_path) { + return Response::builder() + .header("Content-Type", mime_type(path)) + .body(Body::from(data)) + .unwrap(); + } + let index = dist.join("index.html"); + if let Ok(data) = fs::read(&index) { + return Response::builder() + .header("Content-Type", "text/html; charset=utf-8") + .body(Body::from(data)) + .unwrap(); + } + + Response::builder() + .status(StatusCode::NOT_FOUND) + .header("Content-Type", "text/plain; charset=utf-8") + .body(Body::from("Not Found")) + .unwrap() +} + // ===================== 媒体目录 ===================== fn media_dir() -> PathBuf { let mut path = dirs::home_dir().unwrap_or_else(|| PathBuf::from(".")); @@ -60,10 +133,15 @@ fn dist_dir() -> PathBuf { // 2. 相对于可执行文件路径 if let Ok(exe) = std::env::current_exe() { if let Some(dir) = exe.parent() { - // 二进制在 src-tauri/target/release/dpm → dist 在 dpm/dist/ + // 开发模式: 二进制在 src-tauri/target/debug/dpm → dist 在项目根目录 c.push(dir.join("../../../dist")); - // macOS .app bundle: dpm.app/Contents/MacOS/dpm + c.push(dir.join("../../dist")); + // macOS .app bundle 资源目录: dpm.app/Contents/Resources/dist/ c.push(dir.join("../Resources/dist")); + // Windows/Linux: 资源在二进制同目录下的 dist/ + c.push(dir.join("dist")); + // Windows NSIS 安装: 资源在 resources/dist/ + c.push(dir.join("resources/dist")); } } c @@ -78,7 +156,7 @@ fn dist_dir() -> PathBuf { } } - // Fallback:直接尝试最常见的路径 + // Fallback let fallback = PathBuf::from("../dist"); eprintln!("[dpm] 警告: 找不到 dist 目录 (已尝试 {:?}),使用: {:?}", candidates, fallback); fallback @@ -86,10 +164,6 @@ fn dist_dir() -> PathBuf { // ===================== 路由构建 ===================== pub fn create_router(state: AppState) -> Router { - let dist = dist_dir(); - println!("[dpm] 静态文件目录: {:?}", dist); - let dist_str = dist.to_string_lossy().to_string(); - Router::new() .route("/api/login", post(login_handler)) .route("/api/settings", get(get_settings).post(update_settings)) @@ -105,14 +179,8 @@ pub fn create_router(state: AppState) -> Router { .route("/api/events", get(sse_handler)) // 媒体文件:用户目录 ~/Downloads/Media/ .nest_service("/file", ServeDir::new(media_dir_str())) - // SPA 静待文件回退 - .fallback_service( - ServeDir::new(&dist_str) - .append_index_html_on_directories(true) - .fallback(tower_http::services::ServeFile::new( - format!("{}/index.html", dist_str), - )), - ) + // SPA 回退 → 嵌入式文件(跨平台,编译到二进制中) + .fallback_service(get(spa_fallback)) .layer(CorsLayer::permissive()) .with_state(state) } diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index f5e05d8..8e0ef97 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -4,7 +4,7 @@ "version": "0.1.0", "identifier": "com.pine.dpm", "build": { - "beforeDevCommand": "yarn dev", + "beforeDevCommand": "yarn build && yarn dev", "devUrl": "http://localhost:1420", "beforeBuildCommand": "yarn build", "frontendDist": "../dist" @@ -12,7 +12,7 @@ "app": { "windows": [ { - "title": "大屏媒体轮播系统", + "title": "昆明大学生创业园展播系统", "width": 1200, "height": 800 }