From eb43c145cc24f1810d07202dcbd71f2a0535a3fe Mon Sep 17 00:00:00 2001
From: lychang <632945494@qq.com>
Date: Sat, 28 Mar 2026 12:56:30 +0800
Subject: [PATCH] update code level
---
Cargo.lock | 26 ++++++-
Cargo.toml | 1 +
assets/js/album.js | 188 +++++++++++++++++++++++++++++++++++++++++++-
assets/js/app.js | 189 ++++++++++++++++++++++++++++++++++++++++++++-
src/queue.rs | 20 +----
src/routes.rs | 55 +++++++++++--
6 files changed, 446 insertions(+), 33 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
index d4e9fdf..8bfb85b 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -54,7 +54,7 @@ dependencies = [
"actix-utils",
"base64 0.22.1",
"bitflags 2.11.0",
- "brotli",
+ "brotli 8.0.2",
"bytes",
"bytestring",
"derive_more",
@@ -357,6 +357,17 @@ dependencies = [
"generic-array",
]
+[[package]]
+name = "brotli"
+version = "3.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d640d25bc63c50fb1f0b545ffd80207d2e10a4c965530809b40ba3386825c391"
+dependencies = [
+ "alloc-no-stdlib",
+ "alloc-stdlib",
+ "brotli-decompressor 2.5.1",
+]
+
[[package]]
name = "brotli"
version = "8.0.2"
@@ -365,7 +376,17 @@ checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560"
dependencies = [
"alloc-no-stdlib",
"alloc-stdlib",
- "brotli-decompressor",
+ "brotli-decompressor 5.0.0",
+]
+
+[[package]]
+name = "brotli-decompressor"
+version = "2.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4e2e4afe60d7dd600fdd3de8d0f08c2b7ec039712e3b6137ff98b7004e82de4f"
+dependencies = [
+ "alloc-no-stdlib",
+ "alloc-stdlib",
]
[[package]]
@@ -1228,6 +1249,7 @@ version = "1.0.0"
dependencies = [
"actix-files",
"actix-web",
+ "brotli 3.5.0",
"chrono",
"config",
"image",
diff --git a/Cargo.toml b/Cargo.toml
index 8ac156f..0b2d592 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -20,3 +20,4 @@ tracing = "0.1"
tracing-subscriber = "0.3"
thiserror = "1.0"
mimalloc = "0.1.48"
+brotli = "3.4"
\ No newline at end of file
diff --git a/assets/js/album.js b/assets/js/album.js
index 6864aa3..ff0fe01 100644
--- a/assets/js/album.js
+++ b/assets/js/album.js
@@ -10,6 +10,118 @@ const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
// API基础URL
const API_BASE = '/api';
+// 图片加载器类(与app.js中的保持一致)
+class ImageLoader {
+ constructor() {
+ this.cache = new Map();
+ }
+
+ // 加载并解压图片
+ async loadImage(imageId, element) {
+ // 检查缓存
+ if (this.cache.has(imageId)) {
+ const cachedData = this.cache.get(imageId);
+ this.setImageSrc(element, cachedData.blobUrl, cachedData.mimeType);
+ return;
+ }
+
+ try {
+ const response = await fetch(`${API_BASE}/image/${imageId}`);
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+
+ const contentType = response.headers.get('content-type');
+ const contentEncoding = response.headers.get('content-encoding');
+ const originalContentType = response.headers.get('x-original-content-type');
+
+ let imageData;
+
+ if (contentEncoding === 'br') {
+ // Brotli压缩数据,需要解压
+ const compressedData = await response.arrayBuffer();
+ imageData = await this.decompressBrotli(compressedData);
+ } else {
+ // 未压缩数据
+ imageData = await response.blob();
+ }
+
+ // 创建Blob URL
+ const mimeType = originalContentType || contentType || 'image/jpeg';
+ const blob = new Blob([imageData], { type: mimeType });
+ const blobUrl = URL.createObjectURL(blob);
+
+ // 缓存结果
+ this.cache.set(imageId, { blobUrl, mimeType });
+
+ // 设置图片源
+ this.setImageSrc(element, blobUrl, mimeType);
+
+ } catch (error) {
+ console.error('图片加载失败:', error);
+ // 加载失败时使用占位图
+ element.src = '/assets/placeholder.svg';
+ }
+ }
+
+ // 设置图片源
+ setImageSrc(element, blobUrl, mimeType) {
+ // 创建新的Image对象进行预加载
+ const img = new Image();
+ img.onload = () => {
+ element.src = blobUrl;
+ element.classList.add('loaded');
+ };
+ img.onerror = () => {
+ console.error('图片解码失败');
+ element.src = '/assets/placeholder.svg';
+ };
+ img.src = blobUrl;
+ }
+
+ // Brotli解压
+ async decompressBrotli(compressedData) {
+ // 检查浏览器是否支持Brotli解压
+ if (window.TextDecoder && window.TextEncoder) {
+ try {
+ // 使用浏览器的Brotli解压(如果支持)
+ if (window.DecompressionStream && 'br' in window) {
+ const decompressionStream = new DecompressionStream('br');
+ const decompressedStream = new Response(compressedData).body.pipeThrough(decompressionStream);
+ return new Response(decompressedStream).arrayBuffer();
+ }
+ } catch (error) {
+ console.warn('浏览器Brotli解压失败,使用JavaScript解压:', error);
+ }
+ }
+
+ // 使用JavaScript实现的Brotli解压(备用方案)
+ return await this.decompressBrotliJS(compressedData);
+ }
+
+ // JavaScript实现的Brotli解压(备用方案)
+ async decompressBrotliJS(compressedData) {
+ // 这里可以集成一个轻量级的Brotli解压库
+ // 由于Brotli解压比较复杂,这里先返回原始数据
+ // 在实际项目中,可以使用第三方库如brotli-wasm
+ console.warn('Brotli解压未实现,返回原始数据');
+ return compressedData;
+ }
+
+ // 清理缓存
+ clearCache() {
+ // 清理所有Blob URL
+ for (const [imageId, cached] of this.cache.entries()) {
+ URL.revokeObjectURL(cached.blobUrl);
+ }
+ this.cache.clear();
+ }
+}
+
+// 创建全局图片加载器实例
+const imageLoader = new ImageLoader();
+
// 页面加载完成后初始化
document.addEventListener('DOMContentLoaded', function() {
// 添加设备类到body
@@ -31,8 +143,77 @@ document.addEventListener('DOMContentLoaded', function() {
// 初始化键盘导航和触摸手势
initLightboxControls();
+
+ // 初始化图片懒加载
+ initAlbumLazyLoading();
});
+// 初始化相册图片懒加载
+function initAlbumLazyLoading() {
+ // 使用Intersection Observer实现懒加载
+ if ('IntersectionObserver' in window) {
+ const observer = new IntersectionObserver((entries) => {
+ entries.forEach(entry => {
+ if (entry.isIntersecting) {
+ const img = entry.target;
+ const imageId = img.getAttribute('data-image-id');
+
+ if (imageId && !img.classList.contains('loaded')) {
+ imageLoader.loadImage(imageId, img);
+ }
+
+ observer.unobserve(img);
+ }
+ });
+ }, {
+ rootMargin: '200px 0px', // 提前200px开始加载
+ threshold: 0.1
+ });
+
+ // 观察所有带有data-image-id的图片
+ document.querySelectorAll('img[data-image-id]').forEach(img => {
+ if (!img.classList.contains('loaded')) {
+ observer.observe(img);
+ }
+ });
+ } else {
+ // 不支持Intersection Observer的回退方案
+ console.warn('浏览器不支持Intersection Observer,使用回退懒加载方案');
+ initAlbumFallbackLazyLoading();
+ }
+}
+
+// 相册回退懒加载方案
+function initAlbumFallbackLazyLoading() {
+ let scrollTimeout;
+
+ function checkImagesInView() {
+ const images = document.querySelectorAll('img[data-image-id]:not(.loaded)');
+ const viewportHeight = window.innerHeight;
+ const scrollTop = window.scrollY;
+
+ images.forEach(img => {
+ const rect = img.getBoundingClientRect();
+ const isInView = rect.top < viewportHeight + 300 && rect.bottom > -300;
+
+ if (isInView) {
+ const imageId = img.getAttribute('data-image-id');
+ if (imageId) {
+ imageLoader.loadImage(imageId, img);
+ }
+ }
+ });
+ }
+
+ window.addEventListener('scroll', () => {
+ clearTimeout(scrollTimeout);
+ scrollTimeout = setTimeout(checkImagesInView, 100);
+ });
+
+ // 初始检查
+ checkImagesInView();
+}
+
// 显示图片骨架屏
function showImageSkeleton(count = 8) {
const container = document.getElementById('images-container');
@@ -133,9 +314,10 @@ function displayImages() {
@@ -230,7 +412,7 @@ function updateLightboxContent(newIndex) {
// 在lightbox中优先使用原始图片以获得更好的显示效果
let imageUrl = '/assets/placeholder.svg';
if (image.id) {
- imageUrl = `/api/original/${image.id}`;
+ imageUrl = `/api/image/${image.id}`;
} else if (image.original_url) {
imageUrl = image.original_url;
}
diff --git a/assets/js/app.js b/assets/js/app.js
index 2d52ba3..6a8a5bc 100644
--- a/assets/js/app.js
+++ b/assets/js/app.js
@@ -13,6 +13,118 @@ let isInFavoritesPage = false; // 标记当前是否在收藏页
// API基础URL
const API_BASE = '/api';
+// 图片加载器类
+class ImageLoader {
+ constructor() {
+ this.cache = new Map();
+ }
+
+ // 加载并解压图片
+ async loadImage(imageId, element) {
+ // 检查缓存
+ if (this.cache.has(imageId)) {
+ const cachedData = this.cache.get(imageId);
+ this.setImageSrc(element, cachedData.blobUrl, cachedData.mimeType);
+ return;
+ }
+
+ try {
+ const response = await fetch(`${API_BASE}/image/${imageId}`);
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+
+ const contentType = response.headers.get('content-type');
+ const contentEncoding = response.headers.get('content-encoding');
+ const originalContentType = response.headers.get('x-original-content-type');
+
+ let imageData;
+
+ if (contentEncoding === 'br') {
+ // Brotli压缩数据,需要解压
+ const compressedData = await response.arrayBuffer();
+ imageData = await this.decompressBrotli(compressedData);
+ } else {
+ // 未压缩数据
+ imageData = await response.blob();
+ }
+
+ // 创建Blob URL
+ const mimeType = originalContentType || contentType || 'image/jpeg';
+ const blob = new Blob([imageData], { type: mimeType });
+ const blobUrl = URL.createObjectURL(blob);
+
+ // 缓存结果
+ this.cache.set(imageId, { blobUrl, mimeType });
+
+ // 设置图片源
+ this.setImageSrc(element, blobUrl, mimeType);
+
+ } catch (error) {
+ console.error('图片加载失败:', error);
+ // 加载失败时使用占位图
+ element.src = '/assets/placeholder.svg';
+ }
+ }
+
+ // 设置图片源
+ setImageSrc(element, blobUrl, mimeType) {
+ // 创建新的Image对象进行预加载
+ const img = new Image();
+ img.onload = () => {
+ element.src = blobUrl;
+ element.classList.add('loaded');
+ };
+ img.onerror = () => {
+ console.error('图片解码失败');
+ element.src = '/assets/placeholder.svg';
+ };
+ img.src = blobUrl;
+ }
+
+ // Brotli解压
+ async decompressBrotli(compressedData) {
+ // 检查浏览器是否支持Brotli解压
+ if (window.TextDecoder && window.TextEncoder) {
+ try {
+ // 使用浏览器的Brotli解压(如果支持)
+ if (window.DecompressionStream && 'br' in window) {
+ const decompressionStream = new DecompressionStream('br');
+ const decompressedStream = new Response(compressedData).body.pipeThrough(decompressionStream);
+ return new Response(decompressedStream).arrayBuffer();
+ }
+ } catch (error) {
+ console.warn('浏览器Brotli解压失败,使用JavaScript解压:', error);
+ }
+ }
+
+ // 使用JavaScript实现的Brotli解压(备用方案)
+ return await this.decompressBrotliJS(compressedData);
+ }
+
+ // JavaScript实现的Brotli解压(备用方案)
+ async decompressBrotliJS(compressedData) {
+ // 这里可以集成一个轻量级的Brotli解压库
+ // 由于Brotli解压比较复杂,这里先返回原始数据
+ // 在实际项目中,可以使用第三方库如brotli-wasm
+ console.warn('Brotli解压未实现,返回原始数据');
+ return compressedData;
+ }
+
+ // 清理缓存
+ clearCache() {
+ // 清理所有Blob URL
+ for (const [imageId, cached] of this.cache.entries()) {
+ URL.revokeObjectURL(cached.blobUrl);
+ }
+ this.cache.clear();
+ }
+}
+
+// 创建全局图片加载器实例
+const imageLoader = new ImageLoader();
+
// 设备检测
const isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
@@ -68,6 +180,76 @@ function initInfiniteScroll() {
});
}
+// 初始化图片懒加载
+function initLazyLoading() {
+ // 使用Intersection Observer实现懒加载
+ if ('IntersectionObserver' in window) {
+ if (lazyLoadObserver) {
+ lazyLoadObserver.disconnect();
+ }
+
+ lazyLoadObserver = new IntersectionObserver((entries) => {
+ entries.forEach(entry => {
+ if (entry.isIntersecting) {
+ const img = entry.target;
+ const imageId = img.getAttribute('data-image-id');
+
+ if (imageId && !img.classList.contains('loaded')) {
+ imageLoader.loadImage(imageId, img);
+ }
+
+ lazyLoadObserver.unobserve(img);
+ }
+ });
+ }, {
+ rootMargin: '100px 0px', // 提前100px开始加载
+ threshold: 0.1
+ });
+
+ // 观察所有带有data-image-id的图片
+ document.querySelectorAll('img[data-image-id]').forEach(img => {
+ if (!img.classList.contains('loaded')) {
+ lazyLoadObserver.observe(img);
+ }
+ });
+ } else {
+ // 不支持Intersection Observer的回退方案
+ console.warn('浏览器不支持Intersection Observer,使用回退懒加载方案');
+ initFallbackLazyLoading();
+ }
+}
+
+// 回退懒加载方案
+function initFallbackLazyLoading() {
+ let scrollTimeout;
+
+ function checkImagesInView() {
+ const images = document.querySelectorAll('img[data-image-id]:not(.loaded)');
+ const viewportHeight = window.innerHeight;
+ const scrollTop = window.scrollY;
+
+ images.forEach(img => {
+ const rect = img.getBoundingClientRect();
+ const isInView = rect.top < viewportHeight + 200 && rect.bottom > -200;
+
+ if (isInView) {
+ const imageId = img.getAttribute('data-image-id');
+ if (imageId) {
+ imageLoader.loadImage(imageId, img);
+ }
+ }
+ });
+ }
+
+ window.addEventListener('scroll', () => {
+ clearTimeout(scrollTimeout);
+ scrollTimeout = setTimeout(checkImagesInView, 100);
+ });
+
+ // 初始检查
+ checkImagesInView();
+}
+
// 处理滚动事件
function handleScroll() {
// 如果在收藏页,禁用无限滚动
@@ -217,7 +399,7 @@ function appendAlbums(albums) {

`
-
diff --git a/src/queue.rs b/src/queue.rs
index 6f7d3b2..521587a 100644
--- a/src/queue.rs
+++ b/src/queue.rs
@@ -386,7 +386,7 @@ impl TaskQueue {
false
}
-
+
/// 启动定期清理过期任务
pub async fn start_periodic_cleanup(self: Arc
, interval_duration: Duration) {
let queue = Arc::clone(&self);
@@ -790,24 +790,6 @@ impl AlbumImportWorker {
pub struct TaskFactory;
impl TaskFactory {
- /// 创建缩略图生成任务
- pub fn create_thumbnail_task(
- image_id: Uuid,
- original_data: Vec,
- priority: TaskPriority,
- ) -> Task {
- Task {
- id: Uuid::new_v4(),
- task_type: TaskType::ThumbnailGeneration,
- priority,
- created_at: Instant::now(),
- data: TaskData::ThumbnailGeneration {
- image_id,
- original_data,
- },
- }
- }
-
/// 创建相册导入任务
pub fn create_album_import_task(album_path: String, album_data: Option>) -> Task {
Task {
diff --git a/src/routes.rs b/src/routes.rs
index 4eb9648..effe833 100644
--- a/src/routes.rs
+++ b/src/routes.rs
@@ -384,7 +384,7 @@ pub async fn create_album(
}
}
-/// 获取原始图片(已移除缩略图功能)
+/// 获取压缩图片(后端压缩,前端解压)
pub async fn get_image(
db: web::Data>,
image_processor: web::Data>,
@@ -406,11 +406,39 @@ pub async fn get_image(
if original_path.exists() {
match fs::read(&original_path) {
Ok(data) => {
- HttpResponse::Ok()
- .content_type(image.mime_type.clone())
- .insert_header(("Cache-Control", "public, max-age=31536000")) // 1年缓存
- .insert_header(("ETag", format!("\"{}\"", image_id)))
- .body(data)
+ // 检查是否支持压缩(仅对JPEG/PNG图片进行压缩)
+ let should_compress = image.mime_type == "image/jpeg" || image.mime_type == "image/png";
+
+ if should_compress {
+ // 使用Brotli压缩图片数据
+ match compress_image_data(&data) {
+ Ok(compressed_data) => {
+ HttpResponse::Ok()
+ .content_type("application/octet-stream") // 压缩数据使用二进制格式
+ .insert_header(("Content-Encoding", "br")) // Brotli压缩
+ .insert_header(("X-Original-Content-Type", image.mime_type.clone())) // 保留原始类型信息
+ .insert_header(("Cache-Control", "public, max-age=31536000")) // 1年缓存
+ .insert_header(("ETag", format!("\"{}\"", image_id)))
+ .body(compressed_data)
+ }
+ Err(e) => {
+ eprintln!("图片压缩失败: {}", e);
+ // 压缩失败时返回原始数据
+ HttpResponse::Ok()
+ .content_type(image.mime_type.clone())
+ .insert_header(("Cache-Control", "public, max-age=31536000"))
+ .insert_header(("ETag", format!("\"{}\"", image_id)))
+ .body(data)
+ }
+ }
+ } else {
+ // 不支持压缩的图片类型直接返回
+ HttpResponse::Ok()
+ .content_type(image.mime_type.clone())
+ .insert_header(("Cache-Control", "public, max-age=31536000"))
+ .insert_header(("ETag", format!("\"{}\"", image_id)))
+ .body(data)
+ }
}
Err(e) => {
eprintln!("读取原始图片失败: {}", e);
@@ -435,4 +463,19 @@ pub async fn get_image(
}))
}
}
+}
+
+/// 使用Brotli压缩图片数据
+fn compress_image_data(data: &[u8]) -> Result, Box> {
+ use brotli::CompressorWriter;
+ use std::io::Write;
+
+ let mut compressed = Vec::new();
+ {
+ let mut compressor = CompressorWriter::new(&mut compressed, 4096, 11, 22); // 高质量压缩
+ Write::write_all(&mut compressor, data)?;
+ compressor.flush()?;
+ }
+
+ Ok(compressed)
}
\ No newline at end of file