use serde::Serialize; use std::path::Path; const INDEX_TEMPLATE: &str = include_str!("../assets/index.html"); const NOT_FOUND_TEMPLATE: &str = include_str!("../assets/404.html"); #[derive(Debug, Serialize, Clone)] pub struct AppProject { pub name: String, pub route_path: String, } pub fn scan_projects(root: &Path) -> Vec { let mut projects = Vec::new(); let Ok(read_dir) = std::fs::read_dir(root) else { return projects; }; for item in read_dir.flatten() { let path = item.path(); let name = item.file_name().to_string_lossy().to_string(); if name.starts_with('.') { continue; } if path.is_dir() { let has_index = path.join("index.html").exists() || path.join("index.htm").exists(); if has_index { let route_path = format!("/{}/", name); projects.push(AppProject { name, route_path }); } } } projects.sort_by(|a, b| a.name.to_lowercase().cmp(&b.name.to_lowercase())); projects } pub fn render_dashboard(projects: &[AppProject]) -> String { let mut cards_html = String::new(); for proj in projects { // 提取首字符作为圆形头像字母 let initial = proj.name.chars().next().unwrap_or('A').to_uppercase().to_string(); cards_html.push_str(&format!( r#"
{initial}
{name} 本地托管应用 • {url}
"#, url = proj.route_path, name = proj.name, initial = initial )); } let final_cards = if cards_html.is_empty() { r#"
当前目录下暂无含有 index.html 的前端项目
"#.to_string() } else { cards_html }; INDEX_TEMPLATE.replace("__CARDS__", &final_cards) } pub fn render_not_found(path: &str) -> String { NOT_FOUND_TEMPLATE.replace("__PATH__", path) }