update code level

This commit is contained in:
lychang
2026-03-25 04:49:36 +08:00
parent 8a7156128b
commit af15d9f3d1
9 changed files with 337 additions and 80 deletions

View File

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

View File

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

View File

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

View File

@@ -37,6 +37,6 @@ impl Config {
}
pub fn server_address(&self) -> String {
format!("{}:{}", self.server.host, self.server.port)
format!("{}:{}", "localhost", self.server.port)
}
}

View File

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

View File

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

View File

@@ -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(
"/",

View File

@@ -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,
}

View File

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