From af15d9f3d167394471d7a09a7bc732b893cee55c Mon Sep 17 00:00:00 2001 From: lychang <632945494@qq.com> Date: Wed, 25 Mar 2026 04:49:36 +0800 Subject: [PATCH] update code level --- assets/css/style.css | 5 ++ assets/js/app.js | 142 +++++++++++++++++++++++++----- migrations/001_initial.sql | 1 + src/config.rs | 2 +- src/database.rs | 173 ++++++++++++++++++++++++++++--------- src/image_processor.rs | 40 ++++++++- src/main.rs | 14 ++- src/models.rs | 1 + src/routes.rs | 39 ++++++--- 9 files changed, 337 insertions(+), 80 deletions(-) diff --git a/assets/css/style.css b/assets/css/style.css index d877e4c..bb4cda4 100644 --- a/assets/css/style.css +++ b/assets/css/style.css @@ -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; diff --git a/assets/js/app.js b/assets/js/app.js index db48857..60bc351 100644 --- a/assets/js/app.js +++ b/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) { diff --git a/migrations/001_initial.sql b/migrations/001_initial.sql index 65e1707..4d12173 100644 --- a/migrations/001_initial.sql +++ b/migrations/001_initial.sql @@ -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 ); diff --git a/src/config.rs b/src/config.rs index fd7f43e..394a006 100644 --- a/src/config.rs +++ b/src/config.rs @@ -37,6 +37,6 @@ impl Config { } pub fn server_address(&self) -> String { - format!("{}:{}", self.server.host, self.server.port) + format!("{}:{}", "localhost", self.server.port) } } \ No newline at end of file diff --git a/src/database.rs b/src/database.rs index 3b10fd5..75333db 100644 --- a/src/database.rs +++ b/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, limit: Option) -> Result, 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, limit: Option) -> Result, 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::("created_at"), updated_at: row.get::("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) -> Result, 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 { 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 { + // 先获取图片所属的相册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) -> Result<(), sqlx::Error> { let cover_id_str = cover_image_id.map(|id| id.to_string()); diff --git a/src/image_processor.rs b/src/image_processor.rs index a8d0f62..b9bddd2 100644 --- a/src/image_processor.rs +++ b/src/image_processor.rs @@ -80,6 +80,44 @@ impl ImageProcessor { } } + /// 快速生成缩略图(低质量但快速) + pub async fn generate_fast_thumbnail(&self, image_data: &[u8], _filename: &str) -> Result, Box> { + // 使用快速解码和缩放 + 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 { + 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()); diff --git a/src/main.rs b/src/main.rs index b45f180..a5fa98b 100644 --- a/src/main.rs +++ b/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( "/", diff --git a/src/models.rs b/src/models.rs index fa28d69..9f09e8a 100644 --- a/src/models.rs +++ b/src/models.rs @@ -7,6 +7,7 @@ pub struct Album { pub title: String, pub description: Option, pub cover_image_id: Option, + pub image_count: i64, // 新增图片数量字段 pub created_at: String, pub updated_at: String, } diff --git a/src/routes.rs b/src/routes.rs index e47deee..e790d92 100644 --- a/src/routes.rs +++ b/src/routes.rs @@ -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>, @@ -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) } } }