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)
}
}
}