update code level
This commit is contained in:
@@ -16,7 +16,7 @@
|
||||
|
||||
<div class="search-container">
|
||||
<div class="search-box">
|
||||
<label for="search-input"></label><input type="text" id="search-input" placeholder="搜索图片标题..." onkeypress="handleKeyPress(event)">
|
||||
<label for="search-input"></label><input type="text" id="search-input" placeholder="搜索图片标题...">
|
||||
<button onclick="searchAlbums()"><i class="fas fa-search"></i></button>
|
||||
</div>
|
||||
|
||||
|
||||
143
assets/js/app.js
143
assets/js/app.js
@@ -53,8 +53,8 @@ function initSearch() {
|
||||
// 初始化无限滚动
|
||||
function initInfiniteScroll() {
|
||||
let scrollTimeout;
|
||||
|
||||
window.addEventListener('scroll', function() {
|
||||
|
||||
window.addEventListener('scroll', function () {
|
||||
clearTimeout(scrollTimeout);
|
||||
scrollTimeout = setTimeout(handleScroll, 100);
|
||||
});
|
||||
@@ -63,12 +63,12 @@ function initInfiniteScroll() {
|
||||
// 处理滚动事件
|
||||
function handleScroll() {
|
||||
if (isLoadingMore || !hasMoreAlbums) return;
|
||||
|
||||
|
||||
// 检查是否滚动到底部
|
||||
const scrollTop = window.scrollY || document.documentElement.scrollTop;
|
||||
const scrollHeight = document.documentElement.scrollHeight;
|
||||
const clientHeight = window.innerHeight;
|
||||
|
||||
|
||||
// 当距离底部还有200px时开始加载
|
||||
if (scrollTop + clientHeight >= scrollHeight - 200) {
|
||||
loadMoreAlbums();
|
||||
@@ -78,7 +78,7 @@ function handleScroll() {
|
||||
// 加载更多相册
|
||||
async function loadMoreAlbums() {
|
||||
if (isLoadingMore || !hasMoreAlbums) return;
|
||||
|
||||
|
||||
isLoadingMore = true;
|
||||
try {
|
||||
await loadAlbums();
|
||||
@@ -101,18 +101,13 @@ function showLoadMoreError() {
|
||||
<span>加载失败,<a href="javascript:void(0)" onclick="loadMoreAlbums()">点击重试</a></span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
|
||||
const existingError = container.querySelector('.load-more-error');
|
||||
if (existingError) {
|
||||
existingError.remove();
|
||||
}
|
||||
|
||||
container.appendChild(errorElement);
|
||||
}
|
||||
|
||||
// 防抖搜索
|
||||
function debouncedSearch() {
|
||||
searchAlbums();
|
||||
container.appendChild(errorElement);
|
||||
}
|
||||
|
||||
// 显示骨架屏
|
||||
@@ -196,7 +191,7 @@ function appendAlbums(albums) {
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
|
||||
// 移除加载指示器并添加新相册
|
||||
hideLoadingMore();
|
||||
container.insertAdjacentHTML('beforeend', newAlbumsHTML);
|
||||
@@ -222,37 +217,37 @@ async function loadAlbums(reset = false) {
|
||||
showLoadingMore();
|
||||
}
|
||||
|
||||
const response = await fetch(`${API_BASE}/albums?page=${currentPage}&limit=${albumsPerPage}`);
|
||||
const response = await fetch(`${API_BASE}/albums`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
|
||||
const newAlbums = await response.json();
|
||||
|
||||
|
||||
// 检查是否还有更多数据
|
||||
if (newAlbums.length < albumsPerPage) {
|
||||
hasMoreAlbums = false;
|
||||
}
|
||||
|
||||
|
||||
// 合并到现有相册列表
|
||||
allAlbums = [...allAlbums, ...newAlbums];
|
||||
|
||||
|
||||
if (currentPage === 1) {
|
||||
displayAlbums(allAlbums);
|
||||
} else {
|
||||
appendAlbums(newAlbums);
|
||||
}
|
||||
|
||||
|
||||
updateStats(allAlbums);
|
||||
|
||||
// 初始化图片懒加载
|
||||
initLazyLoading();
|
||||
|
||||
|
||||
// 预加载下一批图片
|
||||
preloadNextBatchImages();
|
||||
|
||||
|
||||
currentPage++;
|
||||
|
||||
|
||||
// 隐藏加载更多指示器
|
||||
hideLoadingMore();
|
||||
} catch (error) {
|
||||
@@ -264,7 +259,7 @@ async function loadAlbums(reset = false) {
|
||||
|
||||
// 搜索相册
|
||||
// 防抖搜索函数
|
||||
function debouncedSearch() {
|
||||
async function debouncedSearch() {
|
||||
const searchTerm = document.getElementById('search-input').value.toLowerCase().trim();
|
||||
|
||||
if (searchTerm === '') {
|
||||
@@ -274,28 +269,19 @@ function debouncedSearch() {
|
||||
return;
|
||||
}
|
||||
|
||||
// 在当前已加载的数据集中搜索
|
||||
const filteredAlbums = searchInLoadedAlbums(searchTerm);
|
||||
|
||||
if (filteredAlbums.length > 0) {
|
||||
// 如果当前已加载的数据中有匹配项,直接显示
|
||||
displayAlbums(filteredAlbums);
|
||||
document.getElementById('stats').innerHTML = `在当前页面中找到 ${filteredAlbums.length} 个包含 "${searchTerm}" 的相册`;
|
||||
} else {
|
||||
// 如果没有找到匹配项,显示提示信息
|
||||
showSearchHint(searchTerm);
|
||||
}
|
||||
// 直接使用后端搜索API进行全站搜索
|
||||
await performServerSearch(searchTerm);
|
||||
}
|
||||
|
||||
// 在当前已加载的数据集中搜索
|
||||
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;
|
||||
});
|
||||
}
|
||||
@@ -328,21 +314,21 @@ async function performServerSearch(searchTerm) {
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
|
||||
const serverResults = await response.json();
|
||||
|
||||
|
||||
if (serverResults.length === 0) {
|
||||
showError(`没有找到包含 "${searchTerm}" 的相册`);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// 显示服务器搜索结果
|
||||
displayAlbums(serverResults);
|
||||
document.getElementById('stats').innerHTML = `在全站找到 ${serverResults.length} 个包含 "${searchTerm}" 的相册`;
|
||||
|
||||
|
||||
// 初始化图片懒加载
|
||||
initLazyLoading();
|
||||
|
||||
|
||||
} catch (error) {
|
||||
console.error('全站搜索失败:', error);
|
||||
showError('全站搜索失败,请重试');
|
||||
@@ -416,7 +402,7 @@ function initLazyLoading() {
|
||||
threshold: 0.01
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
const lazyImages = document.querySelectorAll('img.lazy');
|
||||
lazyImages.forEach(img => {
|
||||
lazyLoadObserver.observe(img);
|
||||
@@ -427,14 +413,14 @@ function initLazyLoading() {
|
||||
function preloadNextBatchImages() {
|
||||
const imagesToPreload = [];
|
||||
const visibleImages = document.querySelectorAll('img.lazy');
|
||||
|
||||
|
||||
// 预加载前10张可见图片
|
||||
visibleImages.forEach((img, index) => {
|
||||
if (index < 10 && img.dataset.src) {
|
||||
imagesToPreload.push(img.dataset.src);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// 使用link预加载
|
||||
imagesToPreload.forEach(src => {
|
||||
const link = document.createElement('link');
|
||||
@@ -461,20 +447,24 @@ function showError(message) {
|
||||
document.getElementById('stats').innerHTML = '';
|
||||
}
|
||||
|
||||
// 键盘事件处理
|
||||
function handleKeyPress(event) {
|
||||
if (event.key === 'Enter') {
|
||||
searchAlbums();
|
||||
}
|
||||
// 搜索相册函数
|
||||
async function searchAlbums() {
|
||||
await debouncedSearch();
|
||||
}
|
||||
|
||||
|
||||
// 性能监控
|
||||
function logPerformance() {
|
||||
if ('performance' in window) {
|
||||
window.addEventListener('load', () => {
|
||||
setTimeout(() => {
|
||||
const navigationTiming = performance.getEntriesByType('navigation')[0];
|
||||
console.log('页面加载时间:', navigationTiming.loadEventEnd - navigationTiming.navigationStart, 'ms');
|
||||
if (navigationTiming && navigationTiming.loadEventEnd && navigationTiming.navigationStart) {
|
||||
const loadTime = navigationTiming.loadEventEnd - navigationTiming.navigationStart;
|
||||
console.log('页面加载时间:', loadTime, 'ms');
|
||||
} else {
|
||||
console.log('页面加载时间: 无法计算');
|
||||
}
|
||||
}, 0);
|
||||
});
|
||||
}
|
||||
@@ -502,7 +492,7 @@ function preloadCriticalResources() {
|
||||
'/assets/css/style.css',
|
||||
'/assets/webfonts/fa-solid-900.woff2'
|
||||
];
|
||||
|
||||
|
||||
criticalResources.forEach(resource => {
|
||||
const link = document.createElement('link');
|
||||
link.rel = 'preload';
|
||||
@@ -511,13 +501,13 @@ function preloadCriticalResources() {
|
||||
link.crossOrigin = 'anonymous';
|
||||
document.head.appendChild(link);
|
||||
});
|
||||
|
||||
|
||||
// 预加载第一屏可能需要的图片
|
||||
setTimeout(() => {
|
||||
const firstScreenImages = [
|
||||
'/assets/placeholder.svg'
|
||||
];
|
||||
|
||||
|
||||
firstScreenImages.forEach(img => {
|
||||
const preload = document.createElement('link');
|
||||
preload.rel = 'preload';
|
||||
@@ -532,7 +522,7 @@ function preloadCriticalResources() {
|
||||
function optimizeScrollPerformance() {
|
||||
// 使用requestAnimationFrame优化滚动
|
||||
let ticking = false;
|
||||
|
||||
|
||||
function updateOnScroll() {
|
||||
// 在滚动时暂停非关键任务
|
||||
if (!ticking) {
|
||||
@@ -544,8 +534,8 @@ function optimizeScrollPerformance() {
|
||||
ticking = true;
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('scroll', updateOnScroll, { passive: true });
|
||||
|
||||
window.addEventListener('scroll', updateOnScroll, {passive: true});
|
||||
}
|
||||
|
||||
// 延迟加载非关键图片
|
||||
@@ -589,7 +579,7 @@ function openRandomAlbum() {
|
||||
const randomButton = document.querySelector('.random-button');
|
||||
randomButton.style.transform = 'scale(0.95)';
|
||||
randomButton.disabled = true;
|
||||
|
||||
|
||||
// 显示加载状态
|
||||
const originalText = randomButton.textContent;
|
||||
randomButton.textContent = '随机加载中...';
|
||||
@@ -623,7 +613,7 @@ function openRandomAlbum() {
|
||||
randomButton.disabled = false;
|
||||
randomButton.textContent = originalText;
|
||||
}, 200);
|
||||
|
||||
|
||||
alert(error.message);
|
||||
});
|
||||
}
|
||||
@@ -771,7 +761,7 @@ function previewFavorite(index) {
|
||||
// 显示预览模态框
|
||||
function showPreviewModal(index) {
|
||||
const favorite = currentFavorites[index];
|
||||
|
||||
|
||||
// 构建模态框HTML
|
||||
const modalHtml = `
|
||||
<div class="preview-modal" id="previewModal">
|
||||
@@ -830,16 +820,16 @@ function showPreviewModal(index) {
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
|
||||
// 添加到页面
|
||||
document.body.insertAdjacentHTML('beforeend', modalHtml);
|
||||
|
||||
|
||||
// 显示模态框
|
||||
setTimeout(() => {
|
||||
const modal = document.getElementById('previewModal');
|
||||
modal.classList.add('active');
|
||||
}, 10);
|
||||
|
||||
|
||||
// 监听键盘事件
|
||||
document.addEventListener('keydown', handlePreviewKeydown);
|
||||
}
|
||||
@@ -853,7 +843,7 @@ function closePreviewModal() {
|
||||
modal.remove();
|
||||
}, 300);
|
||||
}
|
||||
|
||||
|
||||
// 移除键盘事件监听
|
||||
document.removeEventListener('keydown', handlePreviewKeydown);
|
||||
}
|
||||
@@ -870,30 +860,30 @@ function navigatePreview(direction) {
|
||||
function updatePreviewContent(newIndex) {
|
||||
const favorite = currentFavorites[newIndex];
|
||||
currentPreviewIndex = newIndex;
|
||||
|
||||
|
||||
// 更新标题和相册信息
|
||||
const titleElement = document.querySelector('.preview-title h3');
|
||||
const infoElement = document.querySelector('.preview-info');
|
||||
if (titleElement) titleElement.textContent = favorite.image.title || '未命名图片';
|
||||
if (infoElement) infoElement.textContent = `来自: ${favorite.album_title}`;
|
||||
|
||||
|
||||
// 更新计数器
|
||||
const counterElement = document.querySelector('.preview-counter');
|
||||
if (counterElement) counterElement.textContent = `${newIndex + 1} / ${currentFavorites.length}`;
|
||||
|
||||
|
||||
// 更新导航按钮状态
|
||||
const prevButton = document.querySelector('.preview-prev');
|
||||
const nextButton = document.querySelector('.preview-next');
|
||||
if (prevButton) prevButton.disabled = newIndex === 0;
|
||||
if (nextButton) nextButton.disabled = newIndex === currentFavorites.length - 1;
|
||||
|
||||
|
||||
// 更新元数据
|
||||
const metaElements = document.querySelectorAll('.preview-meta .meta-item');
|
||||
if (metaElements.length >= 3) {
|
||||
metaElements[0].innerHTML = `<i class="fas fa-calendar"></i> ${formatDate(favorite.favorite.created_at)}`;
|
||||
metaElements[1].innerHTML = `<i class="fas fa-ruler-combined"></i> ${favorite.image.width} × ${favorite.image.height}`;
|
||||
}
|
||||
|
||||
|
||||
// 更新操作按钮
|
||||
const openAlbumButton = document.querySelector('.preview-action:first-child');
|
||||
const removeButton = document.querySelector('.preview-remove');
|
||||
@@ -903,22 +893,22 @@ function updatePreviewContent(newIndex) {
|
||||
if (removeButton) {
|
||||
removeButton.onclick = () => removeFavoriteFromPreview(favorite.favorite.id);
|
||||
}
|
||||
|
||||
|
||||
// 更新图片
|
||||
const imageElement = document.getElementById('preview-image');
|
||||
|
||||
if (imageElement ) {
|
||||
|
||||
if (imageElement) {
|
||||
// 显示加载状态
|
||||
imageElement.style.opacity = '0';
|
||||
|
||||
|
||||
|
||||
// 加载新图片
|
||||
const newImage = new Image();
|
||||
newImage.src = `${API_BASE}/original/${favorite.image.id}`;
|
||||
newImage.alt = favorite.image.title || '预览图片';
|
||||
newImage.className = 'preview-image';
|
||||
|
||||
newImage.onload = function() {
|
||||
|
||||
newImage.onload = function () {
|
||||
// 替换图片
|
||||
if (imageElement.parentNode) {
|
||||
imageElement.parentNode.replaceChild(newImage, imageElement);
|
||||
@@ -928,8 +918,8 @@ function updatePreviewContent(newIndex) {
|
||||
|
||||
newImage.style.opacity = '1';
|
||||
};
|
||||
|
||||
newImage.onerror = function() {
|
||||
|
||||
newImage.onerror = function () {
|
||||
// 图片加载失败
|
||||
imageElement.innerHTML = '<i class="fas fa-exclamation-triangle"></i><span>图片加载失败</span>';
|
||||
};
|
||||
@@ -1010,7 +1000,6 @@ function openFavoriteImage(favoriteId) {
|
||||
}
|
||||
|
||||
// 导出函数供全局使用
|
||||
window.handleKeyPress = handleKeyPress;
|
||||
window.searchAlbums = searchAlbums;
|
||||
window.openRandomAlbum = openRandomAlbum;
|
||||
window.showFavorites = showFavorites;
|
||||
@@ -85,8 +85,8 @@ impl ImageProcessor {
|
||||
// 使用快速解码和缩放
|
||||
let img = image::load_from_memory(image_data)?;
|
||||
|
||||
// 快速缩放到小尺寸(200x150)
|
||||
let thumbnail = img.thumbnail(200, 150);
|
||||
// 快速缩放到小尺寸(200x200)
|
||||
let thumbnail = img.thumbnail(200, 200);
|
||||
|
||||
// 使用低质量JPEG压缩以加快编码速度
|
||||
let mut output = Vec::new();
|
||||
@@ -165,8 +165,8 @@ impl ImageProcessor {
|
||||
// 加载图片
|
||||
let img = image::load_from_memory(image_data)?;
|
||||
|
||||
// 生成缩略图(400x300,保持宽高比)
|
||||
let thumbnail = img.thumbnail(600, 400);
|
||||
// 生成缩略图(600x600,保持宽高比)
|
||||
let thumbnail = img.thumbnail(600, 600);
|
||||
|
||||
// 将缩略图数据保存到内存缓冲区
|
||||
let mut buffer = std::io::Cursor::new(Vec::new());
|
||||
|
||||
126
src/import.rs
126
src/import.rs
@@ -9,6 +9,51 @@ use crate::config::Config;
|
||||
use crate::database::Database;
|
||||
use crate::models::*;
|
||||
|
||||
/// 批量插入图片并设置封面缩略图的辅助函数
|
||||
async fn batch_insert_images_and_set_cover(
|
||||
db: &Database,
|
||||
images_to_insert: Vec<CreateImage>,
|
||||
album_id: Uuid,
|
||||
album_path: &Path,
|
||||
folder_name: &str,
|
||||
) -> Result<usize, Box<dyn std::error::Error>> {
|
||||
if images_to_insert.is_empty() {
|
||||
return Ok(0);
|
||||
}
|
||||
|
||||
println!(" 批量插入 {} 张图片...", images_to_insert.len());
|
||||
match db.add_images(images_to_insert).await {
|
||||
Ok(ids) => {
|
||||
println!(" 批量插入成功,共插入 {} 张图片", ids.len());
|
||||
|
||||
// 设置第一张图片为封面图片并生成缩略图
|
||||
if let Some(first_image_id) = ids.first() {
|
||||
println!(" 设置第一张图片为封面图片 (ID: {})...", first_image_id);
|
||||
|
||||
// 更新相册的封面图片ID
|
||||
if let Err(e) = db.update_album_cover(album_id, Some(*first_image_id)).await {
|
||||
eprintln!(" 更新相册封面失败: {}", e);
|
||||
}
|
||||
|
||||
// 获取第一张图片的信息
|
||||
if let Ok(Some(first_image)) = db.get_image(*first_image_id).await {
|
||||
// 生成缩略图
|
||||
match generate_thumbnail_for_image(&first_image, album_path, folder_name).await {
|
||||
Ok(_) => println!(" 封面缩略图生成成功"),
|
||||
Err(e) => eprintln!(" 封面缩略图生成失败: {}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(ids.len())
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!(" 批量插入失败: {}", e);
|
||||
Ok(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct AlbumIndex {
|
||||
title: String,
|
||||
@@ -125,9 +170,6 @@ pub async fn import_existing_data(config: &Config, db: Arc<Database>) -> Result<
|
||||
// 继续处理下一个相册,不中断整个导入过程
|
||||
}
|
||||
}
|
||||
|
||||
// 在每个相册导入后添加短暂延迟,减少数据库压力
|
||||
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
|
||||
}
|
||||
|
||||
println!("🎉 导入完成!成功导入 {} 个相册,共 {} 张图片", successful_albums, total_imported);
|
||||
@@ -261,42 +303,8 @@ async fn import_single_album(db: &Database, folder_name: &str, path: &Path) -> R
|
||||
}
|
||||
}
|
||||
|
||||
// 批量插入图片
|
||||
if !images_to_insert.is_empty() {
|
||||
println!(" 批量插入 {} 张图片...", images_to_insert.len());
|
||||
match db.add_images(images_to_insert).await {
|
||||
Ok(ids) => {
|
||||
println!(" 批量插入成功,共插入 {} 张图片", ids.len());
|
||||
|
||||
// 设置第一张图片为封面图片并生成缩略图
|
||||
if let Some(first_image_id) = ids.first() {
|
||||
println!(" 设置第一张图片为封面图片 (ID: {})...", first_image_id);
|
||||
|
||||
// 更新相册的封面图片ID
|
||||
if let Err(e) = db.update_album_cover(album_id, Some(*first_image_id)).await {
|
||||
eprintln!(" 更新相册封面失败: {}", e);
|
||||
}
|
||||
|
||||
// 获取第一张图片的信息
|
||||
if let Ok(Some(first_image)) = db.get_image(*first_image_id).await {
|
||||
// 生成缩略图
|
||||
match generate_thumbnail_for_image(&first_image, path, folder_name).await {
|
||||
Ok(_) => println!(" 封面缩略图生成成功"),
|
||||
Err(e) => eprintln!(" 封面缩略图生成失败: {}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(ids.len())
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!(" 批量插入失败: {}", e);
|
||||
Ok(0)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Ok(0)
|
||||
}
|
||||
// 使用辅助函数批量插入图片并设置封面
|
||||
batch_insert_images_and_set_cover(&db, images_to_insert, album_id, path, folder_name).await
|
||||
}
|
||||
|
||||
/// 快速扫描图片文件
|
||||
@@ -438,47 +446,13 @@ async fn reimport_single_album(
|
||||
|
||||
println!(" 发现 {} 张图片需要导入", images_to_insert.len());
|
||||
|
||||
// 批量插入图片
|
||||
if !images_to_insert.is_empty() {
|
||||
println!(" 批量插入 {} 张图片...", images_to_insert.len());
|
||||
match db.add_images(images_to_insert).await {
|
||||
Ok(ids) => {
|
||||
println!(" 批量插入成功,共插入 {} 张图片", ids.len());
|
||||
|
||||
// 设置第一张图片为封面图片并生成缩略图
|
||||
if let Some(first_image_id) = ids.first() {
|
||||
println!(" 设置第一张图片为封面图片 (ID: {})...", first_image_id);
|
||||
|
||||
// 更新相册的封面图片ID
|
||||
if let Err(e) = db.update_album_cover(album_id, Some(*first_image_id)).await {
|
||||
eprintln!(" 更新相册封面失败: {}", e);
|
||||
}
|
||||
|
||||
// 获取第一张图片的信息
|
||||
if let Ok(Some(first_image)) = db.get_image(*first_image_id).await {
|
||||
// 生成缩略图
|
||||
match generate_thumbnail_for_image(&first_image, path, folder_name).await {
|
||||
Ok(_) => println!(" 封面缩略图生成成功"),
|
||||
Err(e) => eprintln!(" 封面缩略图生成失败: {}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(ids.len())
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!(" 批量插入失败: {}", e);
|
||||
Ok(0)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Ok(0)
|
||||
}
|
||||
// 使用辅助函数批量插入图片并设置封面
|
||||
batch_insert_images_and_set_cover(&db, images_to_insert, album_id, path, folder_name).await
|
||||
}
|
||||
|
||||
/// 为图片生成缩略图(使用ImageProcessor)
|
||||
async fn generate_thumbnail_for_image(
|
||||
image: &crate::models::Image,
|
||||
image: &Image,
|
||||
album_path: &Path,
|
||||
_folder_name: &str,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
|
||||
11
src/main.rs
11
src/main.rs
@@ -54,7 +54,7 @@ async fn main() -> std::io::Result<()> {
|
||||
|
||||
// 初始化相册内存缓存
|
||||
let albums_cache = Arc::new(tokio::sync::RwLock::new(AlbumsCache::new()));
|
||||
|
||||
|
||||
// 初始加载相册数据到缓存
|
||||
let initial_cache = Arc::clone(&albums_cache);
|
||||
let initial_db = Arc::clone(&db);
|
||||
@@ -76,10 +76,10 @@ async fn main() -> std::io::Result<()> {
|
||||
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;
|
||||
@@ -131,10 +131,7 @@ async fn main() -> std::io::Result<()> {
|
||||
.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))
|
||||
.route(
|
||||
"/api/original/{id}",
|
||||
web::get().to(routes::get_original_image),
|
||||
)
|
||||
.route("/api/original/{id}", web::get().to(routes::get_original_image))
|
||||
// 收藏功能API路由
|
||||
.route("/api/favorites", web::get().to(routes::get_favorites))
|
||||
.route("/api/favorites/status/{id}", web::get().to(routes::check_favorite_status))
|
||||
|
||||
@@ -12,7 +12,7 @@ pub struct Album {
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct Image {
|
||||
pub id: Uuid,
|
||||
pub album_id: Uuid,
|
||||
@@ -147,11 +147,16 @@ impl AlbumsCache {
|
||||
}
|
||||
|
||||
pub fn search(&self, query: &str) -> Vec<AlbumInfo> {
|
||||
let query_lower = query.to_lowercase();
|
||||
let query_lower = query.to_lowercase().trim().to_string();
|
||||
|
||||
if query_lower.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let mut results = Vec::new();
|
||||
let mut seen_ids = std::collections::HashSet::new();
|
||||
|
||||
// 搜索完整匹配
|
||||
// 1. 搜索完整匹配(最高优先级)
|
||||
if let Some(matches) = self.search_index.get(&query_lower) {
|
||||
for album in matches {
|
||||
if seen_ids.insert(album.id) {
|
||||
@@ -160,10 +165,45 @@ impl AlbumsCache {
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索单词匹配
|
||||
// 2. 搜索单词匹配
|
||||
for word in query_lower.split_whitespace() {
|
||||
if let Some(matches) = self.search_index.get(word) {
|
||||
for album in matches {
|
||||
if word.len() >= 2 { // 只搜索长度>=2的单词
|
||||
if let Some(matches) = self.search_index.get(word) {
|
||||
for album in matches {
|
||||
if seen_ids.insert(album.id) {
|
||||
results.push(album.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 模糊搜索:在相册标题和描述中搜索包含查询词的结果
|
||||
if results.is_empty() {
|
||||
for album in &self.albums {
|
||||
let title_lower = album.title.to_lowercase();
|
||||
let description_lower = album.description.as_ref()
|
||||
.map(|d| d.to_lowercase())
|
||||
.unwrap_or_default();
|
||||
|
||||
// 检查标题或描述是否包含搜索词
|
||||
if title_lower.contains(&query_lower) || description_lower.contains(&query_lower) {
|
||||
if seen_ids.insert(album.id) {
|
||||
results.push(album.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 如果还没有结果,尝试部分匹配(更宽松的搜索)
|
||||
if results.is_empty() && query_lower.len() >= 2 {
|
||||
for album in &self.albums {
|
||||
let title_lower = album.title.to_lowercase();
|
||||
|
||||
// 检查标题是否包含搜索词的任何部分
|
||||
if query_lower.split_whitespace().any(|word|
|
||||
word.len() >= 2 && title_lower.contains(word)
|
||||
) {
|
||||
if seen_ids.insert(album.id) {
|
||||
results.push(album.clone());
|
||||
}
|
||||
|
||||
104
src/routes.rs
104
src/routes.rs
@@ -26,6 +26,60 @@ pub async fn get_favorites(db: web::Data<Arc<Database>>) -> impl Responder {
|
||||
}
|
||||
}
|
||||
|
||||
/// 检查缩略图质量并在需要时升级为高质量版本
|
||||
async fn check_and_upgrade_thumbnail_quality(
|
||||
config: &crate::config::StorageConfig,
|
||||
image: &Image,
|
||||
original_path: &std::path::Path,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let thumbnail_path = config.thumbnails_dir.join(image.id.to_string());
|
||||
|
||||
// 检查原始图片是否存在
|
||||
if !original_path.exists() {
|
||||
return Ok(()); // 原始图片不存在,无法升级
|
||||
}
|
||||
|
||||
// 读取缩略图文件信息
|
||||
let thumbnail_metadata = match std::fs::metadata(&thumbnail_path) {
|
||||
Ok(meta) => meta,
|
||||
Err(_) => return Ok(()), // 无法读取缩略图元数据
|
||||
};
|
||||
|
||||
// 检查缩略图文件大小(简单质量检测)
|
||||
// 如果缩略图小于20KB,可能质量较低,需要升级
|
||||
if thumbnail_metadata.len() < 20 * 1024 {
|
||||
// 读取原始图片数据
|
||||
let original_data = match std::fs::read(original_path) {
|
||||
Ok(data) => data,
|
||||
Err(e) => {
|
||||
eprintln!("读取原始图片失败,无法升级缩略图: {}", e);
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
// 生成高质量缩略图
|
||||
let image_processor = crate::image_processor::ImageProcessor::new(config.clone());
|
||||
match image_processor
|
||||
.generate_thumbnail_from_memory(&original_data, &image.id.to_string())
|
||||
.await
|
||||
{
|
||||
Ok(high_quality_data) => {
|
||||
// 保存高质量缩略图
|
||||
if let Err(e) = std::fs::write(&thumbnail_path, &high_quality_data) {
|
||||
eprintln!("保存高质量缩略图失败: {}", e);
|
||||
} else {
|
||||
println!("✅ 缩略图已升级为高质量版本 (ID: {})", image.id);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("生成高质量缩略图失败: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// 检查图片是否被收藏
|
||||
pub async fn check_favorite_status(
|
||||
db: web::Data<Arc<Database>>,
|
||||
@@ -179,35 +233,28 @@ 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;
|
||||
|
||||
pub async fn get_albums(albums_cache: web::Data<Arc<RwLock<AlbumsCache>>>) -> impl Responder {
|
||||
// 从内存缓存获取数据
|
||||
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 start = 0;
|
||||
let end = 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(
|
||||
albums_cache: web::Data<Arc<RwLock<crate::models::AlbumsCache>>>,
|
||||
query: web::Query<std::collections::HashMap<String, String>>,
|
||||
albums_cache: web::Data<Arc<RwLock<AlbumsCache>>>,
|
||||
query: web::Query<HashMap<String, String>>,
|
||||
) -> impl Responder {
|
||||
let q = query.get("q").map(|s| s.as_str()).unwrap_or("");
|
||||
let page = query
|
||||
@@ -262,22 +309,22 @@ pub async fn get_random_album(
|
||||
) -> 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)
|
||||
}
|
||||
|
||||
@@ -350,10 +397,25 @@ pub async fn get_thumbnail(
|
||||
let original_path = image_processor.config.images_dir.join(&image.filename);
|
||||
|
||||
if thumbnail_path.exists() {
|
||||
// 返回缩略图
|
||||
// 返回现有缩略图,并异步检查是否需要替换为高质量版本
|
||||
match fs::read(&thumbnail_path) {
|
||||
Ok(data) => {
|
||||
let mime_type = image_processor.detect_mime_type("jpg"); // 缩略图总是JPEG格式
|
||||
|
||||
// 异步检查缩略图质量并可能替换为高质量版本
|
||||
let config_clone = image_processor.config.clone();
|
||||
let image_clone = image.clone();
|
||||
let original_path_clone = original_path.clone();
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = check_and_upgrade_thumbnail_quality(
|
||||
&config_clone,
|
||||
&image_clone,
|
||||
&original_path_clone
|
||||
).await {
|
||||
eprintln!("缩略图质量检查失败: {}", e);
|
||||
}
|
||||
});
|
||||
|
||||
HttpResponse::Ok()
|
||||
.content_type(mime_type)
|
||||
.insert_header(("Cache-Control", "public, max-age=31536000")) // 1年缓存
|
||||
@@ -405,7 +467,7 @@ pub async fn get_thumbnail(
|
||||
// 返回快速生成的缩略图数据
|
||||
HttpResponse::Ok()
|
||||
.content_type("image/jpeg")
|
||||
.insert_header(("Cache-Control", "public, max-age=31536000")) // 1年缓存
|
||||
.insert_header(("Cache-Control", "public, max-age=30")) // 0.5分钟缓存
|
||||
.insert_header(("ETag", format!("\"{}\"", image_id)))
|
||||
.body(thumbnail_data)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user