- 修复服务器

This commit is contained in:
Pine
2026-05-13 00:45:50 +08:00
parent 2bd68815fd
commit a5b96eed6e
5 changed files with 123 additions and 19 deletions
+35
View File
@@ -869,6 +869,7 @@ dependencies = [
"dirs", "dirs",
"futures", "futures",
"once_cell", "once_cell",
"rust-embed",
"serde", "serde",
"serde_json", "serde_json",
"tauri", "tauri",
@@ -2911,6 +2912,40 @@ dependencies = [
"web-sys", "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]] [[package]]
name = "rustc-hash" name = "rustc-hash"
version = "2.1.2" version = "2.1.2"
+1
View File
@@ -27,3 +27,4 @@ once_cell = "1"
dirs = "6" dirs = "6"
futures = "0.3" futures = "0.3"
tokio-stream = { version = "0.1", features = ["sync"] } tokio-stream = { version = "0.1", features = ["sync"] }
rust-embed = "8"
+1 -1
View File
@@ -19,7 +19,7 @@ pub fn run() {
// 等待 HTTP 服务器就绪后再打开窗口 // 等待 HTTP 服务器就绪后再打开窗口
for i in 0..50 { for i in 0..50 {
if let Ok(mut s) = std::net::TcpStream::connect_timeout( 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), Duration::from_millis(500),
) { ) {
// 发送一个简单的 HTTP 请求测试 // 发送一个简单的 HTTP 请求测试
+84 -16
View File
@@ -2,8 +2,9 @@ use crate::events::EVENTS;
use crate::models::*; use crate::models::*;
use crate::storage::{get_media_type, is_allowed_extension, STORAGE}; use crate::storage::{get_media_type, is_allowed_extension, STORAGE};
use axum::{ use axum::{
body::Body,
extract::{Form, Multipart, State}, extract::{Form, Multipart, State},
http::StatusCode, http::{Response, StatusCode, Uri},
response::sse::{Event, KeepAlive, Sse}, response::sse::{Event, KeepAlive, Sse},
response::Json, response::Json,
routing::{get, post}, routing::{get, post},
@@ -11,6 +12,7 @@ use axum::{
}; };
use chrono::Local; use chrono::Local;
use futures::stream::Stream; use futures::stream::Stream;
use rust_embed::RustEmbed;
use std::convert::Infallible; use std::convert::Infallible;
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::Arc; use std::sync::Arc;
@@ -20,6 +22,77 @@ use tokio_stream::StreamExt;
use tower_http::cors::CorsLayer; use tower_http::cors::CorsLayer;
use tower_http::services::ServeDir; 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<Body> {
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 { fn media_dir() -> PathBuf {
let mut path = dirs::home_dir().unwrap_or_else(|| PathBuf::from(".")); let mut path = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
@@ -60,10 +133,15 @@ fn dist_dir() -> PathBuf {
// 2. 相对于可执行文件路径 // 2. 相对于可执行文件路径
if let Ok(exe) = std::env::current_exe() { if let Ok(exe) = std::env::current_exe() {
if let Some(dir) = exe.parent() { 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")); 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")); 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 c
@@ -78,7 +156,7 @@ fn dist_dir() -> PathBuf {
} }
} }
// Fallback:直接尝试最常见的路径 // Fallback
let fallback = PathBuf::from("../dist"); let fallback = PathBuf::from("../dist");
eprintln!("[dpm] 警告: 找不到 dist 目录 (已尝试 {:?}),使用: {:?}", candidates, fallback); eprintln!("[dpm] 警告: 找不到 dist 目录 (已尝试 {:?}),使用: {:?}", candidates, fallback);
fallback fallback
@@ -86,10 +164,6 @@ fn dist_dir() -> PathBuf {
// ===================== 路由构建 ===================== // ===================== 路由构建 =====================
pub fn create_router(state: AppState) -> Router { 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() Router::new()
.route("/api/login", post(login_handler)) .route("/api/login", post(login_handler))
.route("/api/settings", get(get_settings).post(update_settings)) .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)) .route("/api/events", get(sse_handler))
// 媒体文件:用户目录 ~/Downloads/Media/ // 媒体文件:用户目录 ~/Downloads/Media/
.nest_service("/file", ServeDir::new(media_dir_str())) .nest_service("/file", ServeDir::new(media_dir_str()))
// SPA 静待文件回退 // SPA 回退 → 嵌入式文件(跨平台,编译到二进制中)
.fallback_service( .fallback_service(get(spa_fallback))
ServeDir::new(&dist_str)
.append_index_html_on_directories(true)
.fallback(tower_http::services::ServeFile::new(
format!("{}/index.html", dist_str),
)),
)
.layer(CorsLayer::permissive()) .layer(CorsLayer::permissive())
.with_state(state) .with_state(state)
} }
+2 -2
View File
@@ -4,7 +4,7 @@
"version": "0.1.0", "version": "0.1.0",
"identifier": "com.pine.dpm", "identifier": "com.pine.dpm",
"build": { "build": {
"beforeDevCommand": "yarn dev", "beforeDevCommand": "yarn build && yarn dev",
"devUrl": "http://localhost:1420", "devUrl": "http://localhost:1420",
"beforeBuildCommand": "yarn build", "beforeBuildCommand": "yarn build",
"frontendDist": "../dist" "frontendDist": "../dist"
@@ -12,7 +12,7 @@
"app": { "app": {
"windows": [ "windows": [
{ {
"title": "大屏媒体轮播系统", "title": "昆明大学生创业园展播系统",
"width": 1200, "width": 1200,
"height": 800 "height": 800
} }