From e53ee4b2d023af2663dea4e4a86b58a03615bd82 Mon Sep 17 00:00:00 2001 From: lychang <632945494@qq.com> Date: Wed, 25 Mar 2026 07:29:57 +0800 Subject: [PATCH] update code level --- Cargo.lock | 1 + Cargo.toml | 1 + assets/css/style.css | 45 +++++++++++ assets/js/app.js | 142 +++++++++++++++++++++++++---------- src/main.rs | 45 +++++++++++ src/models.rs | 84 ++++++++++++++++++++- src/routes.rs | 173 +++++++++++++++++++++++++++++-------------- 7 files changed, 397 insertions(+), 94 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 33e2d6f..972a352 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1231,6 +1231,7 @@ dependencies = [ "chrono", "config", "image", + "rand 0.8.5", "reqwest", "serde", "serde_json", diff --git a/Cargo.toml b/Cargo.toml index f509302..f77feef 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,3 +15,4 @@ uuid = { version = "1.0", features = ["v4", "serde"] } image = { version = "0.24", default-features = false, features = ["jpeg", "png", "gif", "webp"] } reqwest = { version = "0.11", default-features = false, features = ["blocking","rustls-tls"] } config = "0.13" +rand = "0.8" \ No newline at end of file diff --git a/assets/css/style.css b/assets/css/style.css index bb4cda4..b60958b 100644 --- a/assets/css/style.css +++ b/assets/css/style.css @@ -1228,6 +1228,51 @@ body { color: var(--medium-gray); } +/* 搜索提示样式 */ +.search-hint { + text-align: center; + padding: 60px 20px; + color: var(--text-light); + font-size: 1.2rem; + max-width: 600px; + margin: 0 auto; +} + +.search-hint .fa-search { + font-size: 3rem; + margin-bottom: 20px; + color: var(--medium-gray); +} + +.search-hint h3 { + color: var(--text-dark); + margin-bottom: 15px; + font-size: 1.5rem; +} + +.search-hint p { + margin-bottom: 15px; + font-weight: 500; +} + +.search-hint ul { + list-style: none; + text-align: left; + max-width: 400px; + margin: 0 auto; +} + +.search-hint li { + margin-bottom: 10px; + padding-left: 0; +} + +.search-hint .btn-primary { + margin-top: 10px; + padding: 8px 16px; + font-size: 0.9rem; +} + /* 响应式媒体查询 - 移动端优化 */ @media (max-width: 1024px) { .container { diff --git a/assets/js/app.js b/assets/js/app.js index 60bc351..2358298 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -263,52 +263,89 @@ async function loadAlbums(reset = false) { } // 搜索相册 -async function searchAlbums() { +// 防抖搜索函数 +function debouncedSearch() { const searchTerm = document.getElementById('search-input').value.toLowerCase().trim(); - + if (searchTerm === '') { - // 显示所有相册,重置分页 - loadAlbums(true); + // 显示所有已加载的相册 + displayAlbums(allAlbums); + updateStats(); return; } + + // 在当前已加载的数据集中搜索 + const filteredAlbums = searchInLoadedAlbums(searchTerm); + + if (filteredAlbums.length > 0) { + // 如果当前已加载的数据中有匹配项,直接显示 + displayAlbums(filteredAlbums); + document.getElementById('stats').innerHTML = `在当前页面中找到 ${filteredAlbums.length} 个包含 "${searchTerm}" 的相册`; + } else { + // 如果没有找到匹配项,显示提示信息 + showSearchHint(searchTerm); + } +} - try { - // 重置搜索状态 - currentPage = 1; - allAlbums = []; - hasMoreAlbums = true; +// 在当前已加载的数据集中搜索 +function searchInLoadedAlbums(searchTerm) { + if (!allAlbums.length) return []; + + return allAlbums.filter(album => { + // 搜索标题和描述 + const titleMatch = album.title.toLowerCase().includes(searchTerm); + const descriptionMatch = album.description && album.description.toLowerCase().includes(searchTerm); + return titleMatch || descriptionMatch; + }); +} + +// 显示搜索提示 +function showSearchHint(searchTerm) { + const container = document.getElementById('albums-container'); + container.innerHTML = ` +
+ +

未在当前页面找到 "${searchTerm}"

+

尝试:

+ +
+ `; + document.getElementById('stats').innerHTML = `在当前页面中未找到包含 "${searchTerm}" 的相册`; +} + +// 执行服务器端搜索(全站搜索) +async function performServerSearch(searchTerm) { + try { // 显示搜索中的骨架屏 showSkeleton(4); - const response = await fetch(`${API_BASE}/search?q=${encodeURIComponent(searchTerm)}&page=${currentPage}&limit=${albumsPerPage}`); + const response = await fetch(`${API_BASE}/search?q=${encodeURIComponent(searchTerm)}&page=1&limit=${albumsPerPage}`); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } - const filteredAlbums = await response.json(); - allAlbums = filteredAlbums; + const serverResults = await response.json(); - // 检查是否还有更多数据 - if (filteredAlbums.length < albumsPerPage) { - hasMoreAlbums = false; + if (serverResults.length === 0) { + showError(`没有找到包含 "${searchTerm}" 的相册`); + return; } - displayAlbums(filteredAlbums); - - if (filteredAlbums.length === 0) { - document.getElementById('stats').innerHTML = `没有找到包含 "${searchTerm}" 的相册`; - } else { - document.getElementById('stats').innerHTML = `找到 ${filteredAlbums.length} 个包含 "${searchTerm}" 的相册`; - } - + // 显示服务器搜索结果 + displayAlbums(serverResults); + document.getElementById('stats').innerHTML = `在全站找到 ${serverResults.length} 个包含 "${searchTerm}" 的相册`; + // 初始化图片懒加载 initLazyLoading(); - currentPage++; } catch (error) { - console.error('搜索失败:', error); - showError('搜索失败,请重试'); + console.error('全站搜索失败:', error); + showError('全站搜索失败,请重试'); } } @@ -410,6 +447,9 @@ function preloadNextBatchImages() { // 更新统计信息 function updateStats(albums) { + if (!albums) { + albums = allAlbums; + } const totalImages = albums.reduce((sum, album) => sum + album.image_count, 0); document.getElementById('stats').innerHTML = `共发现 ${albums.length} 个相册,包含 ${totalImages} 张图片`; } @@ -545,23 +585,47 @@ optimizeScrollPerformance(); // 随机打开相册 function openRandomAlbum() { - if (allAlbums.length === 0) { - alert('暂无相册数据,请稍后再试'); - return; - } - - const randomIndex = Math.floor(Math.random() * allAlbums.length); - const randomAlbum = allAlbums[randomIndex]; - // 添加随机动画效果 const randomButton = document.querySelector('.random-button'); randomButton.style.transform = 'scale(0.95)'; - setTimeout(() => { - randomButton.style.transform = ''; - }, 200); + randomButton.disabled = true; + + // 显示加载状态 + const originalText = randomButton.textContent; + randomButton.textContent = '随机加载中...'; - // 跳转到随机相册 - window.location.href = `/album?id=${randomAlbum.id}`; + // 调用新的随机相册API + fetch(`${API_BASE}/album/random`) + .then(response => { + if (!response.ok) { + if (response.status === 404) { + throw new Error('暂无相册数据,请稍后再试'); + } + throw new Error('获取随机相册失败'); + } + return response.json(); + }) + .then(randomAlbum => { + // 恢复按钮状态 + setTimeout(() => { + randomButton.style.transform = ''; + randomButton.disabled = false; + randomButton.textContent = originalText; + }, 200); + + // 跳转到随机相册 + window.location.href = `/album?id=${randomAlbum.id}`; + }) + .catch(error => { + // 恢复按钮状态 + setTimeout(() => { + randomButton.style.transform = ''; + randomButton.disabled = false; + randomButton.textContent = originalText; + }, 200); + + alert(error.message); + }); } // 显示收藏夹 diff --git a/src/main.rs b/src/main.rs index a5fa98b..5323896 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,6 +9,8 @@ use actix_files as afs; use actix_web::{App, HttpResponse, HttpServer, web, middleware}; use std::fs; use std::sync::Arc; +use tokio::time::{interval, Duration}; +use crate::models::AlbumsCache; #[actix_web::main] async fn main() -> std::io::Result<()> { @@ -50,6 +52,47 @@ async fn main() -> std::io::Result<()> { // 初始化图片处理器 let image_processor = Arc::new(image_processor::ImageProcessor::new(config.storage.clone())); + // 初始化相册内存缓存 + let albums_cache = Arc::new(tokio::sync::RwLock::new(AlbumsCache::new())); + + // 初始加载相册数据到缓存 + let initial_cache = Arc::clone(&albums_cache); + let initial_db = Arc::clone(&db); + tokio::spawn(async move { + match initial_db.get_all_albums().await { + Ok(albums) => { + let mut cache = initial_cache.write().await; + cache.update(albums); + println!("✅ 初始相册缓存加载完成"); + } + Err(e) => { + eprintln!("❌ 初始缓存加载失败: {}", e); + } + } + }); + + // 设置定时刷新缓存任务(每30分钟) + let refresh_cache = Arc::clone(&albums_cache); + let refresh_db = Arc::clone(&db); + tokio::spawn(async move { + let mut interval = interval(Duration::from_secs(30 * 60)); // 30分钟 + + loop { + interval.tick().await; + + match refresh_db.get_all_albums().await { + Ok(albums) => { + let mut cache = refresh_cache.write().await; + cache.update(albums); + println!("🔄 相册缓存定时刷新完成"); + } + Err(e) => { + eprintln!("❌ 缓存刷新失败: {}", e); + } + } + } + }); + println!("API接口:"); println!(" - GET /api/albums - 获取所有相册"); println!(" - GET /api/search?q=关键词 - 搜索相册"); @@ -80,9 +123,11 @@ async fn main() -> std::io::Result<()> { .wrap(middleware::Logger::default()) .app_data(web::Data::new(Arc::clone(&db))) .app_data(web::Data::new(Arc::clone(&image_processor))) + .app_data(web::Data::new(Arc::clone(&albums_cache))) // API路由 .route("/api/albums", web::get().to(routes::get_albums)) .route("/api/search", web::get().to(routes::search_albums)) + .route("/api/album/random", web::get().to(routes::get_random_album)) .route("/api/album/{id}", web::get().to(routes::get_album_detail)) .route("/api/albums", web::post().to(routes::create_album)) .route("/api/thumbnail/{id}", web::get().to(routes::get_thumbnail)) diff --git a/src/models.rs b/src/models.rs index 9f09e8a..7518fbd 100644 --- a/src/models.rs +++ b/src/models.rs @@ -34,7 +34,7 @@ pub struct AlbumWithImages { pub image_count: i64, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Clone)] pub struct AlbumInfo { pub id: Uuid, pub title: String, @@ -91,4 +91,86 @@ pub struct FavoriteWithImage { pub struct FavoriteStatus { pub is_favorited: bool, pub favorite_id: Option, +} + +// 内存缓存结构 +#[derive(Debug, Clone)] +pub struct AlbumsCache { + pub albums: Vec, + pub search_index: std::collections::HashMap>, + pub last_updated: chrono::DateTime, +} + +impl AlbumsCache { + pub fn new() -> Self { + Self { + albums: Vec::new(), + search_index: std::collections::HashMap::new(), + last_updated: chrono::Utc::now(), + } + } + + pub fn is_expired(&self) -> bool { + let now = chrono::Utc::now(); + let duration = now - self.last_updated; + duration.num_minutes() >= 30 // 30分钟过期 + } + + pub fn update(&mut self, albums: Vec) { + self.albums = albums.clone(); + self.last_updated = chrono::Utc::now(); + + // 构建搜索索引 + self.search_index.clear(); + for album in &self.albums { + let title_lower = album.title.to_lowercase(); + + // 为每个单词创建索引 + for word in title_lower.split_whitespace() { + if word.len() >= 2 { // 只索引长度>=2的单词 + self.search_index + .entry(word.to_string()) + .or_insert_with(Vec::new) + .push(album.clone()); + } + } + + // 为完整标题创建索引 + self.search_index + .entry(title_lower.clone()) + .or_insert_with(Vec::new) + .push(album.clone()); + } + + println!("✅ 相册缓存已更新,共 {} 个相册,{} 个搜索索引项", + self.albums.len(), self.search_index.len()); + } + + pub fn search(&self, query: &str) -> Vec { + let query_lower = query.to_lowercase(); + let mut results = Vec::new(); + let mut seen_ids = std::collections::HashSet::new(); + + // 搜索完整匹配 + if let Some(matches) = self.search_index.get(&query_lower) { + for album in matches { + if seen_ids.insert(album.id) { + results.push(album.clone()); + } + } + } + + // 搜索单词匹配 + for word in query_lower.split_whitespace() { + if let Some(matches) = self.search_index.get(word) { + for album in matches { + if seen_ids.insert(album.id) { + results.push(album.clone()); + } + } + } + } + + results + } } \ No newline at end of file diff --git a/src/routes.rs b/src/routes.rs index e790d92..92f8f76 100644 --- a/src/routes.rs +++ b/src/routes.rs @@ -2,38 +2,19 @@ use crate::database::Database; use crate::image_processor::ImageProcessor; use crate::models::*; use actix_web::{HttpResponse, Responder, web}; +use std::collections::HashMap; use std::fs; use std::sync::Arc; +use tokio::sync::RwLock; use uuid::Uuid; -use crate::image_processor; - -pub async fn get_albums( - db: web::Data>, - query: web::Query>, -) -> impl Responder { - let page = query.get("page").and_then(|s| s.parse::().ok()); - let limit = query.get("limit").and_then(|s| s.parse::().ok()); - - match db.get_albums(page, limit).await { - Ok(albums) => HttpResponse::Ok().json(albums), - Err(e) => { - eprintln!("获取相册列表失败: {}", e); - HttpResponse::InternalServerError().json(serde_json::json!({ - "error": "获取相册列表失败" - })) - } - } -} // 收藏功能API路由 // 获取用户的收藏列表 -pub async fn get_favorites( - db: web::Data>, -) -> impl Responder { +pub async fn get_favorites(db: web::Data>) -> impl Responder { // 这里使用固定用户ID,实际项目中应该从认证信息中获取 let user_id = "default_user".to_string(); - + match db.get_user_favorites(&user_id).await { Ok(favorites) => HttpResponse::Ok().json(favorites), Err(e) => { @@ -58,10 +39,10 @@ pub async fn check_favorite_status( })); } }; - + // 这里使用固定用户ID,实际项目中应该从认证信息中获取 let user_id = "default_user".to_string(); - + match db.get_favorite_status(&user_id, image_id).await { Ok(status) => HttpResponse::Ok().json(status), Err(e) => { @@ -80,7 +61,7 @@ pub async fn add_favorite( ) -> impl Responder { // 这里使用固定用户ID,实际项目中应该从认证信息中获取 let user_id = "default_user".to_string(); - + match db.add_favorite(&user_id, favorite_data.into_inner()).await { Ok(favorite_id) => HttpResponse::Created().json(serde_json::json!({ "id": favorite_id, @@ -108,7 +89,7 @@ pub async fn remove_favorite( })); } }; - + match db.remove_favorite(favorite_id).await { Ok(_) => HttpResponse::Ok().json(serde_json::json!({ "message": "取消收藏成功" @@ -135,10 +116,10 @@ pub async fn remove_favorite_by_image( })); } }; - + // 这里使用固定用户ID,实际项目中应该从认证信息中获取 let user_id = "default_user".to_string(); - + match db.remove_favorite_by_image(&user_id, image_id).await { Ok(_) => HttpResponse::Ok().json(serde_json::json!({ "message": "取消收藏成功" @@ -198,35 +179,106 @@ pub async fn get_original_image( } } +pub async fn get_albums( + albums_cache: web::Data>>, + query: web::Query>, +) -> impl Responder { + let page = query.get("page").and_then(|s| s.parse::().ok()).unwrap_or(1); + let limit = query.get("limit").and_then(|s| s.parse::().ok()).unwrap_or(50); + let offset = (page - 1) * limit; + + // 从内存缓存获取数据 + let cache = albums_cache.read().await; + let total_albums = cache.albums.len(); + + // 应用分页 + let start = offset.min(total_albums); + let end = (offset + limit).min(total_albums); + let paginated_albums = if start < end { + cache.albums[start..end].to_vec() + } else { + Vec::new() + }; + + drop(cache); // 释放读锁 + + HttpResponse::Ok().json(paginated_albums) +} + pub async fn search_albums( - db: web::Data>, + albums_cache: web::Data>>, query: web::Query>, ) -> impl Responder { let q = query.get("q").map(|s| s.as_str()).unwrap_or(""); - let page = query.get("page").and_then(|s| s.parse::().ok()); - let limit = query.get("limit").and_then(|s| s.parse::().ok()); + let page = query + .get("page") + .and_then(|s| s.parse::().ok()) + .unwrap_or(1); + let limit = query + .get("limit") + .and_then(|s| s.parse::().ok()) + .unwrap_or(50); + let offset = (page - 1) * limit; if q.is_empty() { - return match db.get_albums(page, limit).await { - Ok(albums) => HttpResponse::Ok().json(albums), - Err(e) => { - eprintln!("获取相册列表失败: {}", e); - HttpResponse::InternalServerError().json(serde_json::json!({ - "error": "获取相册列表失败" - })) - } + // 如果没有搜索词,返回所有相册(带分页) + let cache = albums_cache.read().await; + let total_albums = cache.albums.len(); + + // 应用分页 + let start = offset.min(total_albums); + let end = (offset + limit).min(total_albums); + let paginated_albums = if start < end { + cache.albums[start..end].to_vec() + } else { + Vec::new() }; + + drop(cache); + return HttpResponse::Ok().json(paginated_albums); } - match db.search_albums(q, page, limit).await { - Ok(albums) => HttpResponse::Ok().json(albums), - Err(e) => { - eprintln!("搜索相册失败: {}", e); - HttpResponse::InternalServerError().json(serde_json::json!({ - "error": "搜索失败" - })) - } + // 从内存缓存搜索 + let cache = albums_cache.read().await; + let mut search_results = cache.search(q); + + // 应用分页到搜索结果 + let total_results = search_results.len(); + let start = offset.min(total_results); + let end = (offset + limit).min(total_results); + let paginated_results = if start < end { + search_results.drain(start..end).collect() + } else { + Vec::new() + }; + + drop(cache); + + HttpResponse::Ok().json(paginated_results) +} + +pub async fn get_random_album( + albums_cache: web::Data>>, +) -> impl Responder { + // 从内存缓存获取数据 + let cache = albums_cache.read().await; + + if cache.albums.is_empty() { + drop(cache); + return HttpResponse::NotFound().json(serde_json::json!({ + "error": "没有可用的相册" + })); } + + // 使用随机数生成器选择一个随机相册 + use rand::Rng; + let mut rng = rand::thread_rng(); + let random_index = rng.gen_range(0..cache.albums.len()); + let random_album = cache.albums[random_index].clone(); + + drop(cache); + + HttpResponse::Ok().json(random_album) } pub async fn get_album_detail( @@ -320,7 +372,10 @@ pub async fn get_thumbnail( match fs::read(&original_path) { Ok(data) => { // 快速生成小尺寸缩略图(不进行复杂处理) - match image_processor.generate_fast_thumbnail(&data, &image.id.to_string()).await { + match image_processor + .generate_fast_thumbnail(&data, &image.id.to_string()) + .await + { Ok(thumbnail_data) => { // 异步保存高质量缩略图到文件(不阻塞响应) let config_clone = image_processor.config.clone(); @@ -330,11 +385,18 @@ pub async fn get_thumbnail( let thumbnail_path = config_clone .thumbnails_dir .join(filename_clone.to_string()); - + // 生成高质量缩略图并保存 if let Ok(high_quality_data) = ImageProcessor::new(config_clone) - .generate_thumbnail_from_memory(&original_data_clone, &filename_clone.to_string()).await { - if let Err(e) = fs::write(&thumbnail_path, &high_quality_data) { + .generate_thumbnail_from_memory( + &original_data_clone, + &filename_clone.to_string(), + ) + .await + { + if let Err(e) = + fs::write(&thumbnail_path, &high_quality_data) + { eprintln!("保存高质量缩略图失败: {}", e); } } @@ -350,8 +412,11 @@ pub async fn get_thumbnail( Err(e) => { eprintln!("快速缩略图生成失败: {}", e); // 如果快速生成失败,返回原始图片的小尺寸版本 - let small_data = image_processor.resize_image_to_max(&data, 800).await; - HttpResponse::Ok().content_type(image.mime_type).body(small_data) + let small_data = + image_processor.resize_image_to_max(&data, 800).await; + HttpResponse::Ok() + .content_type(image.mime_type) + .body(small_data) } } }