update code level
This commit is contained in:
@@ -21,6 +21,7 @@
|
||||
}
|
||||
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
html { scroll-behavior: smooth; }
|
||||
body {
|
||||
font-family: 'Inter', 'Segoe UI', system-ui, -apple-system, sans-serif;
|
||||
background: linear-gradient(135deg, var(--light-blue) 0%, var(--white) 100%);
|
||||
@@ -28,6 +29,10 @@ body {
|
||||
color: var(--text-dark);
|
||||
line-height: 1.6;
|
||||
overflow-x: hidden;
|
||||
/* 优化渲染性能 */
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
.container {
|
||||
max-width: 1400px;
|
||||
|
||||
142
assets/js/app.js
142
assets/js/app.js
@@ -2,9 +2,10 @@
|
||||
let allAlbums = [];
|
||||
let searchTimeout = null;
|
||||
let currentPage = 1;
|
||||
let albumsPerPage = 200;
|
||||
let albumsPerPage = 50; // 减少每页数量,提高加载速度
|
||||
let isLoadingMore = false;
|
||||
let hasMoreAlbums = true;
|
||||
let lazyLoadObserver = null;
|
||||
|
||||
// API基础URL
|
||||
const API_BASE = '/api';
|
||||
@@ -247,6 +248,9 @@ async function loadAlbums(reset = false) {
|
||||
// 初始化图片懒加载
|
||||
initLazyLoading();
|
||||
|
||||
// 预加载下一批图片
|
||||
preloadNextBatchImages();
|
||||
|
||||
currentPage++;
|
||||
|
||||
// 隐藏加载更多指示器
|
||||
@@ -358,37 +362,50 @@ function displayAlbums(albums) {
|
||||
|
||||
// 初始化图片懒加载
|
||||
function initLazyLoading() {
|
||||
if ('IntersectionObserver' in window) {
|
||||
const imageObserver = new IntersectionObserver((entries, observer) => {
|
||||
if (!lazyLoadObserver) {
|
||||
lazyLoadObserver = new IntersectionObserver((entries) => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
const img = entry.target;
|
||||
if (img.dataset.src) {
|
||||
img.src = img.dataset.src;
|
||||
img.classList.remove('lazy');
|
||||
delete img.dataset.src;
|
||||
lazyLoadObserver.unobserve(img);
|
||||
}
|
||||
imageObserver.unobserve(img);
|
||||
}
|
||||
});
|
||||
}, {
|
||||
rootMargin: '50px 0px',
|
||||
threshold: 0.1
|
||||
});
|
||||
|
||||
document.querySelectorAll('img.lazy').forEach(img => {
|
||||
imageObserver.observe(img);
|
||||
});
|
||||
} else {
|
||||
// 不支持IntersectionObserver的浏览器回退方案
|
||||
document.querySelectorAll('img.lazy').forEach(img => {
|
||||
if (img.dataset.src) {
|
||||
img.src = img.dataset.src;
|
||||
img.classList.remove('lazy');
|
||||
delete img.dataset.src;
|
||||
}
|
||||
rootMargin: '100px 0px', // 提前100px开始加载
|
||||
threshold: 0.01
|
||||
});
|
||||
}
|
||||
|
||||
const lazyImages = document.querySelectorAll('img.lazy');
|
||||
lazyImages.forEach(img => {
|
||||
lazyLoadObserver.observe(img);
|
||||
});
|
||||
}
|
||||
|
||||
// 预加载下一批图片
|
||||
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');
|
||||
link.rel = 'preload';
|
||||
link.as = 'image';
|
||||
link.href = src;
|
||||
document.head.appendChild(link);
|
||||
});
|
||||
}
|
||||
|
||||
// 更新统计信息
|
||||
@@ -437,10 +454,95 @@ function preventDoubleTapZoom() {
|
||||
}
|
||||
}
|
||||
|
||||
// 预加载关键资源
|
||||
function preloadCriticalResources() {
|
||||
// 预加载关键CSS和字体
|
||||
const criticalResources = [
|
||||
'/assets/css/font-awesome.css',
|
||||
'/assets/css/style.css',
|
||||
'/assets/webfonts/fa-solid-900.woff2'
|
||||
];
|
||||
|
||||
criticalResources.forEach(resource => {
|
||||
const link = document.createElement('link');
|
||||
link.rel = 'preload';
|
||||
link.href = resource;
|
||||
link.as = resource.endsWith('.css') ? 'style' : 'font';
|
||||
link.crossOrigin = 'anonymous';
|
||||
document.head.appendChild(link);
|
||||
});
|
||||
|
||||
// 预加载第一屏可能需要的图片
|
||||
setTimeout(() => {
|
||||
const firstScreenImages = [
|
||||
'/assets/placeholder.svg'
|
||||
];
|
||||
|
||||
firstScreenImages.forEach(img => {
|
||||
const preload = document.createElement('link');
|
||||
preload.rel = 'preload';
|
||||
preload.href = img;
|
||||
preload.as = 'image';
|
||||
document.head.appendChild(preload);
|
||||
});
|
||||
}, 100);
|
||||
}
|
||||
|
||||
// 优化滚动性能
|
||||
function optimizeScrollPerformance() {
|
||||
// 使用requestAnimationFrame优化滚动
|
||||
let ticking = false;
|
||||
|
||||
function updateOnScroll() {
|
||||
// 在滚动时暂停非关键任务
|
||||
if (!ticking) {
|
||||
requestAnimationFrame(() => {
|
||||
// 延迟加载非关键图片
|
||||
delayNonCriticalImages();
|
||||
ticking = false;
|
||||
});
|
||||
ticking = true;
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('scroll', updateOnScroll, { passive: true });
|
||||
}
|
||||
|
||||
// 延迟加载非关键图片
|
||||
function delayNonCriticalImages() {
|
||||
const nonCriticalImages = document.querySelectorAll('img[data-delayed]');
|
||||
nonCriticalImages.forEach(img => {
|
||||
if (isElementInViewport(img)) {
|
||||
const src = img.getAttribute('data-delayed');
|
||||
if (src) {
|
||||
img.src = src;
|
||||
img.removeAttribute('data-delayed');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 检查元素是否在视口中
|
||||
function isElementInViewport(el) {
|
||||
const rect = el.getBoundingClientRect();
|
||||
return (
|
||||
rect.top >= 0 &&
|
||||
rect.left >= 0 &&
|
||||
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
|
||||
rect.right <= (window.innerWidth || document.documentElement.clientWidth)
|
||||
);
|
||||
}
|
||||
|
||||
// 初始化性能监控和移动端优化
|
||||
logPerformance();
|
||||
preventDoubleTapZoom();
|
||||
|
||||
// 预加载关键资源
|
||||
preloadCriticalResources();
|
||||
|
||||
// 优化滚动性能
|
||||
optimizeScrollPerformance();
|
||||
|
||||
// 随机打开相册
|
||||
function openRandomAlbum() {
|
||||
if (allAlbums.length === 0) {
|
||||
|
||||
@@ -5,6 +5,7 @@ CREATE TABLE IF NOT EXISTS albums (
|
||||
document TEXT NOT NULL,
|
||||
description TEXT,
|
||||
cover_image_id TEXT,
|
||||
image_count INTEGER NOT NULL DEFAULT 0, -- 新增图片数量字段
|
||||
created_at DATETIME NOT NULL,
|
||||
updated_at DATETIME NOT NULL
|
||||
);
|
||||
|
||||
@@ -37,6 +37,6 @@ impl Config {
|
||||
}
|
||||
|
||||
pub fn server_address(&self) -> String {
|
||||
format!("{}:{}", self.server.host, self.server.port)
|
||||
format!("{}:{}", "localhost", self.server.port)
|
||||
}
|
||||
}
|
||||
173
src/database.rs
173
src/database.rs
@@ -33,11 +33,11 @@ impl Database {
|
||||
}
|
||||
|
||||
let pool = SqlitePoolOptions::new()
|
||||
.max_connections(10) // 增加连接数
|
||||
.min_connections(2) // 保持最小连接数
|
||||
.acquire_timeout(std::time::Duration::from_secs(30)) // 增加获取连接超时时间
|
||||
.idle_timeout(std::time::Duration::from_secs(300)) // 增加空闲超时时间
|
||||
.max_lifetime(std::time::Duration::from_secs(1800)) // 增加最大生命周期
|
||||
.max_connections(20) // 增加连接数以支持并发请求
|
||||
.min_connections(5) // 保持更多连接以减少连接建立时间
|
||||
.acquire_timeout(std::time::Duration::from_secs(10)) // 减少获取连接超时时间
|
||||
.idle_timeout(std::time::Duration::from_secs(600)) // 增加空闲超时时间
|
||||
.max_lifetime(std::time::Duration::from_secs(3600)) // 增加最大生命周期
|
||||
.connect(url)
|
||||
.await?;
|
||||
|
||||
@@ -54,7 +54,11 @@ impl Database {
|
||||
// 创建表
|
||||
sqlx::migrate!("./migrations").run(&pool).await?;
|
||||
|
||||
Ok(Database { pool })
|
||||
// 确保image_count字段存在并正确初始化
|
||||
let db = Database { pool };
|
||||
db.ensure_image_count_field().await?;
|
||||
|
||||
Ok(db)
|
||||
}
|
||||
|
||||
|
||||
@@ -106,30 +110,28 @@ impl Database {
|
||||
}
|
||||
pub async fn get_albums(&self, page: Option<usize>, limit: Option<usize>) -> Result<Vec<AlbumInfo>, sqlx::Error> {
|
||||
let page = page.unwrap_or(1);
|
||||
let limit = limit.unwrap_or(200);
|
||||
let limit = limit.unwrap_or(50); // 减少默认加载数量,提高响应速度
|
||||
let offset = (page - 1) * limit;
|
||||
|
||||
let albums = sqlx::query(
|
||||
r#"
|
||||
SELECT
|
||||
a.id,
|
||||
a.title,
|
||||
a.document,
|
||||
a.description,
|
||||
a.cover_image_id,
|
||||
COUNT(img.id) as image_count,
|
||||
a.created_at
|
||||
FROM albums a
|
||||
LEFT JOIN images img ON img.album_id = a.id
|
||||
GROUP BY a.id, a.title, a.document, a.description, a.cover_image_id, a.created_at
|
||||
ORDER BY a.document DESC
|
||||
SELECT
|
||||
id,
|
||||
title,
|
||||
document,
|
||||
description,
|
||||
cover_image_id,
|
||||
image_count,
|
||||
created_at
|
||||
FROM albums
|
||||
ORDER BY created_at DESC
|
||||
LIMIT ? OFFSET ?
|
||||
"#
|
||||
)
|
||||
.bind(limit as i64)
|
||||
.bind(offset as i64)
|
||||
.fetch_all(&self.pool)
|
||||
.await?;
|
||||
.bind(limit as i64)
|
||||
.bind(offset as i64)
|
||||
.fetch_all(&self.pool)
|
||||
.await?;
|
||||
|
||||
let mut result = Vec::new();
|
||||
for row in albums {
|
||||
@@ -166,10 +168,10 @@ impl Database {
|
||||
document,
|
||||
description,
|
||||
cover_image_id,
|
||||
0 as image_count,
|
||||
image_count,
|
||||
created_at
|
||||
FROM albums
|
||||
ORDER BY document DESC
|
||||
ORDER BY created_at DESC
|
||||
"#
|
||||
)
|
||||
.fetch_all(&self.pool)
|
||||
@@ -203,25 +205,22 @@ impl Database {
|
||||
pub async fn search_albums(&self, query: &str, page: Option<usize>, limit: Option<usize>) -> Result<Vec<AlbumInfo>, sqlx::Error> {
|
||||
let search_pattern = format!("%{}%", query);
|
||||
let page = page.unwrap_or(1);
|
||||
let limit = limit.unwrap_or(200);
|
||||
let limit = limit.unwrap_or(50); // 减少默认加载数量,提高响应速度
|
||||
let offset = (page - 1) * limit;
|
||||
|
||||
let albums = sqlx::query(
|
||||
r#"
|
||||
SELECT
|
||||
a.id,
|
||||
a.title,
|
||||
a.document,
|
||||
a.description,
|
||||
a.cover_image_id,
|
||||
COUNT(img.id) as image_count,
|
||||
a.created_at
|
||||
FROM albums a
|
||||
LEFT JOIN images img ON img.album_id = a.id
|
||||
LEFT JOIN images i ON i.id = a.cover_image_id
|
||||
WHERE a.title LIKE ?
|
||||
GROUP BY a.id, a.title, a.document, a.description, a.cover_image_id, a.created_at
|
||||
ORDER BY a.created_at DESC
|
||||
id,
|
||||
title,
|
||||
document,
|
||||
description,
|
||||
cover_image_id,
|
||||
image_count,
|
||||
created_at
|
||||
FROM albums
|
||||
WHERE title LIKE ?
|
||||
ORDER BY created_at DESC
|
||||
LIMIT ? OFFSET ?
|
||||
"#
|
||||
)
|
||||
@@ -291,6 +290,7 @@ impl Database {
|
||||
title: row.get("title"),
|
||||
description: row.get("description"),
|
||||
cover_image_id,
|
||||
image_count: row.get("image_count"),
|
||||
created_at: row.get::<String, _>("created_at"),
|
||||
updated_at: row.get::<String, _>("updated_at"),
|
||||
};
|
||||
@@ -355,7 +355,7 @@ impl Database {
|
||||
let now = chrono::Utc::now().to_rfc3339();
|
||||
|
||||
sqlx::query(
|
||||
"INSERT INTO albums (id, title, document, description, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)"
|
||||
"INSERT INTO albums (id, title, document, description, image_count, created_at, updated_at) VALUES (?, ?, ?, ?, 0, ?, ?)"
|
||||
)
|
||||
.bind(id.to_string())
|
||||
.bind(album.title)
|
||||
@@ -391,6 +391,9 @@ impl Database {
|
||||
.execute(&self.pool)
|
||||
.await?;
|
||||
|
||||
// 更新相册的图片数量
|
||||
self.update_album_image_count(image.album_id).await?;
|
||||
|
||||
// 如果这是相册的第一张图片,设置为封面
|
||||
let image_count = sqlx::query_scalar::<_, i64>(
|
||||
"SELECT COUNT(*) FROM images WHERE album_id = ?"
|
||||
@@ -412,6 +415,26 @@ impl Database {
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
/// 更新相册的图片数量
|
||||
pub async fn update_album_image_count(&self, album_id: Uuid) -> Result<(), sqlx::Error> {
|
||||
sqlx::query(
|
||||
r#"
|
||||
UPDATE albums
|
||||
SET image_count = (
|
||||
SELECT COUNT(*) FROM images WHERE album_id = ?
|
||||
),
|
||||
updated_at = datetime('now')
|
||||
WHERE id = ?
|
||||
"#
|
||||
)
|
||||
.bind(album_id.to_string())
|
||||
.bind(album_id.to_string())
|
||||
.execute(&self.pool)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn add_images(&self, images: Vec<CreateImage>) -> Result<Vec<Uuid>, sqlx::Error> {
|
||||
if images.is_empty() {
|
||||
return Ok(Vec::new());
|
||||
@@ -474,12 +497,15 @@ impl Database {
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
// 更新相册的图片数量
|
||||
self.update_album_image_count(album_id).await?;
|
||||
}
|
||||
|
||||
Ok(ids)
|
||||
}
|
||||
|
||||
/// 获取相册中的图片数量
|
||||
/// 获取相册中的图片数量(直接从image_count字段读取)
|
||||
pub async fn get_album_image_count(&self, album_id: Uuid) -> Result<i64, sqlx::Error> {
|
||||
let count = sqlx::query_scalar::<_, i64>(
|
||||
"SELECT COUNT(*) FROM images WHERE album_id = ?"
|
||||
@@ -491,6 +517,38 @@ impl Database {
|
||||
Ok(count)
|
||||
}
|
||||
|
||||
/// 确保image_count字段存在并正确初始化
|
||||
pub async fn ensure_image_count_field(&self) -> Result<(), sqlx::Error> {
|
||||
// 检查字段是否存在
|
||||
let exists: i64 = sqlx::query_scalar(
|
||||
"SELECT COUNT(*) FROM pragma_table_info('albums') WHERE name = 'image_count'"
|
||||
)
|
||||
.fetch_one(&self.pool)
|
||||
.await?;
|
||||
|
||||
if exists == 0 {
|
||||
// 字段不存在,添加字段
|
||||
sqlx::query("ALTER TABLE albums ADD COLUMN image_count INTEGER NOT NULL DEFAULT 0")
|
||||
.execute(&self.pool)
|
||||
.await?;
|
||||
|
||||
// 更新现有数据的image_count
|
||||
sqlx::query(
|
||||
"UPDATE albums SET image_count = (
|
||||
SELECT COUNT(*) FROM images WHERE images.album_id = albums.id
|
||||
)"
|
||||
)
|
||||
.execute(&self.pool)
|
||||
.await?;
|
||||
|
||||
println!("✅ image_count字段已成功添加并初始化");
|
||||
} else {
|
||||
println!("ℹ️ image_count字段已存在,跳过初始化");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 删除相册及其关联的图片
|
||||
pub async fn delete_album(&self, album_id: Uuid) -> Result<(), sqlx::Error> {
|
||||
let mut tx = self.pool.begin().await?;
|
||||
@@ -511,6 +569,39 @@ impl Database {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn delete_image(&self, image_id: Uuid) -> Result<bool, sqlx::Error> {
|
||||
// 先获取图片所属的相册ID
|
||||
let album_id_result = sqlx::query_scalar::<_, String>(
|
||||
r#"
|
||||
SELECT album_id FROM images WHERE id = ?
|
||||
"#
|
||||
)
|
||||
.bind(image_id.to_string())
|
||||
.fetch_optional(&self.pool)
|
||||
.await?;
|
||||
|
||||
let result = sqlx::query(
|
||||
r#"
|
||||
DELETE FROM images
|
||||
WHERE id = ?
|
||||
"#
|
||||
)
|
||||
.bind(image_id.to_string())
|
||||
.execute(&self.pool)
|
||||
.await?;
|
||||
|
||||
// 如果删除成功且找到了相册ID,更新相册图片数量
|
||||
if result.rows_affected() > 0 {
|
||||
if let Some(album_id_str) = album_id_result {
|
||||
if let Ok(album_id) = Uuid::parse_str(&album_id_str) {
|
||||
self.update_album_image_count(album_id).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(result.rows_affected() > 0)
|
||||
}
|
||||
|
||||
pub async fn update_album_cover(&self, album_id: Uuid, cover_image_id: Option<Uuid>) -> Result<(), sqlx::Error> {
|
||||
let cover_id_str = cover_image_id.map(|id| id.to_string());
|
||||
|
||||
|
||||
@@ -80,6 +80,44 @@ impl ImageProcessor {
|
||||
}
|
||||
}
|
||||
|
||||
/// 快速生成缩略图(低质量但快速)
|
||||
pub async fn generate_fast_thumbnail(&self, image_data: &[u8], _filename: &str) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
|
||||
// 使用快速解码和缩放
|
||||
let img = image::load_from_memory(image_data)?;
|
||||
|
||||
// 快速缩放到小尺寸(200x150)
|
||||
let thumbnail = img.thumbnail(200, 150);
|
||||
|
||||
// 使用低质量JPEG压缩以加快编码速度
|
||||
let mut output = Vec::new();
|
||||
thumbnail.write_to(&mut std::io::Cursor::new(&mut output), image::ImageOutputFormat::Jpeg(50))?;
|
||||
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
/// 快速调整图片尺寸(最大宽度)
|
||||
pub async fn resize_image_to_max(&self, image_data: &[u8], max_width: u32) -> Vec<u8> {
|
||||
match image::load_from_memory(image_data) {
|
||||
Ok(img) => {
|
||||
let (width, height) = img.dimensions();
|
||||
if width <= max_width {
|
||||
return image_data.to_vec(); // 无需调整
|
||||
}
|
||||
|
||||
let new_height = (height as f32 * max_width as f32 / width as f32) as u32;
|
||||
let resized = img.thumbnail(max_width, new_height);
|
||||
|
||||
let mut output = Vec::new();
|
||||
if resized.write_to(&mut std::io::Cursor::new(&mut output), image::ImageOutputFormat::Jpeg(75)).is_ok() {
|
||||
output
|
||||
} else {
|
||||
image_data.to_vec() // 失败时返回原图
|
||||
}
|
||||
}
|
||||
Err(_) => image_data.to_vec(), // 解码失败时返回原图
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_image_url(&self, filename: &str) -> String {
|
||||
format!("/images/{}", filename)
|
||||
}
|
||||
@@ -128,7 +166,7 @@ impl ImageProcessor {
|
||||
let img = image::load_from_memory(image_data)?;
|
||||
|
||||
// 生成缩略图(400x300,保持宽高比)
|
||||
let thumbnail = img.thumbnail(400, 300);
|
||||
let thumbnail = img.thumbnail(600, 400);
|
||||
|
||||
// 将缩略图数据保存到内存缓冲区
|
||||
let mut buffer = std::io::Cursor::new(Vec::new());
|
||||
|
||||
14
src/main.rs
14
src/main.rs
@@ -6,7 +6,7 @@ mod models;
|
||||
mod routes;
|
||||
|
||||
use actix_files as afs;
|
||||
use actix_web::{App, HttpResponse, HttpServer, web};
|
||||
use actix_web::{App, HttpResponse, HttpServer, web, middleware};
|
||||
use std::fs;
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -74,6 +74,10 @@ async fn main() -> std::io::Result<()> {
|
||||
|
||||
HttpServer::new(move || {
|
||||
App::new()
|
||||
// 启用压缩中间件
|
||||
.wrap(middleware::Compress::default())
|
||||
// 启用日志中间件
|
||||
.wrap(middleware::Logger::default())
|
||||
.app_data(web::Data::new(Arc::clone(&db)))
|
||||
.app_data(web::Data::new(Arc::clone(&image_processor)))
|
||||
// API路由
|
||||
@@ -92,8 +96,12 @@ async fn main() -> std::io::Result<()> {
|
||||
.route("/api/favorites", web::post().to(routes::add_favorite))
|
||||
.route("/api/favorites/{id}", web::delete().to(routes::remove_favorite))
|
||||
.route("/api/favorites/image/{id}", web::delete().to(routes::remove_favorite_by_image))
|
||||
// 静态文件服务
|
||||
.service(afs::Files::new("/assets", "./assets").show_files_listing())
|
||||
// 静态文件服务 - 启用缓存
|
||||
.service(afs::Files::new("/assets", "./assets")
|
||||
.show_files_listing()
|
||||
.prefer_utf8(true)
|
||||
.use_last_modified(true)
|
||||
.use_etag(true))
|
||||
// HTML页面路由
|
||||
.route(
|
||||
"/",
|
||||
|
||||
@@ -7,6 +7,7 @@ pub struct Album {
|
||||
pub title: String,
|
||||
pub description: Option<String>,
|
||||
pub cover_image_id: Option<Uuid>,
|
||||
pub image_count: i64, // 新增图片数量字段
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ use actix_web::{HttpResponse, Responder, web};
|
||||
use std::fs;
|
||||
use std::sync::Arc;
|
||||
use uuid::Uuid;
|
||||
use crate::image_processor;
|
||||
|
||||
pub async fn get_albums(
|
||||
db: web::Data<Arc<Database>>,
|
||||
@@ -301,7 +302,11 @@ pub async fn get_thumbnail(
|
||||
match fs::read(&thumbnail_path) {
|
||||
Ok(data) => {
|
||||
let mime_type = image_processor.detect_mime_type("jpg"); // 缩略图总是JPEG格式
|
||||
HttpResponse::Ok().content_type(mime_type).body(data)
|
||||
HttpResponse::Ok()
|
||||
.content_type(mime_type)
|
||||
.insert_header(("Cache-Control", "public, max-age=31536000")) // 1年缓存
|
||||
.insert_header(("ETag", format!("\"{}\"", image_id)))
|
||||
.body(data)
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("读取缩略图失败: {}", e);
|
||||
@@ -311,36 +316,42 @@ pub async fn get_thumbnail(
|
||||
}
|
||||
}
|
||||
} else if original_path.exists() {
|
||||
// 生成缩略图并返回
|
||||
// 先返回原始图片的缩略版本(快速响应)
|
||||
match fs::read(&original_path) {
|
||||
Ok(data) => {
|
||||
// 使用ImageProcessor生成缩略图
|
||||
match image_processor.generate_thumbnail_from_memory(&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();
|
||||
let filename_clone = image.id.clone();
|
||||
let thumbnail_data_clone = thumbnail_data.clone();
|
||||
let original_data_clone = data.clone();
|
||||
tokio::spawn(async move {
|
||||
let thumbnail_path = config_clone
|
||||
.thumbnails_dir
|
||||
.join(filename_clone.to_string());
|
||||
if let Err(e) =
|
||||
fs::write(&thumbnail_path, &thumbnail_data_clone)
|
||||
{
|
||||
eprintln!("保存缩略图文件失败: {}", e);
|
||||
|
||||
// 生成高质量缩略图并保存
|
||||
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) {
|
||||
eprintln!("保存高质量缩略图失败: {}", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 返回缩略图数据
|
||||
// 返回快速生成的缩略图数据
|
||||
HttpResponse::Ok()
|
||||
.content_type("image/jpeg")
|
||||
.insert_header(("Cache-Control", "public, max-age=31536000")) // 1年缓存
|
||||
.insert_header(("ETag", format!("\"{}\"", image_id)))
|
||||
.body(thumbnail_data)
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("生成缩略图失败: {}", e);
|
||||
// 如果生成缩略图失败,返回原始图片
|
||||
HttpResponse::Ok().content_type(image.mime_type).body(data)
|
||||
eprintln!("快速缩略图生成失败: {}", e);
|
||||
// 如果快速生成失败,返回原始图片的小尺寸版本
|
||||
let small_data = image_processor.resize_image_to_max(&data, 800).await;
|
||||
HttpResponse::Ok().content_type(image.mime_type).body(small_data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user