update code level

This commit is contained in:
lychang
2026-03-25 07:29:57 +08:00
parent af15d9f3d1
commit e53ee4b2d0
7 changed files with 397 additions and 94 deletions

1
Cargo.lock generated
View File

@@ -1231,6 +1231,7 @@ dependencies = [
"chrono",
"config",
"image",
"rand 0.8.5",
"reqwest",
"serde",
"serde_json",

View File

@@ -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"

View File

@@ -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 {

View File

@@ -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 = `
<div class="search-hint">
<i class="fas fa-search"></i>
<h3>未在当前页面找到 "${searchTerm}"</h3>
<p>尝试:</p>
<ul>
<li>继续滚动加载更多相册</li>
<li>使用更精确的搜索词</li>
<li><button class="btn-primary" onclick="performServerSearch('${searchTerm}')">在全站搜索 "${searchTerm}"</button></li>
</ul>
</div>
`;
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);
});
}
// 显示收藏夹

View File

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

View File

@@ -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<Uuid>,
}
// 内存缓存结构
#[derive(Debug, Clone)]
pub struct AlbumsCache {
pub albums: Vec<AlbumInfo>,
pub search_index: std::collections::HashMap<String, Vec<AlbumInfo>>,
pub last_updated: chrono::DateTime<chrono::Utc>,
}
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<AlbumInfo>) {
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<AlbumInfo> {
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
}
}

View File

@@ -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<Arc<Database>>,
query: web::Query<std::collections::HashMap<String, String>>,
) -> impl Responder {
let page = query.get("page").and_then(|s| s.parse::<usize>().ok());
let limit = query.get("limit").and_then(|s| s.parse::<usize>().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<Arc<Database>>,
) -> impl Responder {
pub async fn get_favorites(db: web::Data<Arc<Database>>) -> 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<Arc<RwLock<crate::models::AlbumsCache>>>,
query: web::Query<HashMap<String, String>>,
) -> impl Responder {
let page = query.get("page").and_then(|s| s.parse::<usize>().ok()).unwrap_or(1);
let limit = query.get("limit").and_then(|s| s.parse::<usize>().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<Arc<Database>>,
albums_cache: web::Data<Arc<RwLock<crate::models::AlbumsCache>>>,
query: web::Query<std::collections::HashMap<String, String>>,
) -> impl Responder {
let q = query.get("q").map(|s| s.as_str()).unwrap_or("");
let page = query.get("page").and_then(|s| s.parse::<usize>().ok());
let limit = query.get("limit").and_then(|s| s.parse::<usize>().ok());
let page = query
.get("page")
.and_then(|s| s.parse::<usize>().ok())
.unwrap_or(1);
let limit = query
.get("limit")
.and_then(|s| s.parse::<usize>().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<Arc<RwLock<crate::models::AlbumsCache>>>,
) -> 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)
}
}
}