update code level

This commit is contained in:
lychang
2026-03-28 12:56:30 +08:00
parent 675a402848
commit eb43c145cc
6 changed files with 446 additions and 33 deletions

26
Cargo.lock generated
View File

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

View File

@@ -20,3 +20,4 @@ tracing = "0.1"
tracing-subscriber = "0.3"
thiserror = "1.0"
mimalloc = "0.1.48"
brotli = "3.4"

View File

@@ -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() {
<div class="image-item ${orientation}" onclick="openLightbox(${index})">
<img
src="${imageUrl}"
${dataSrc ? `data-src="${dataSrc}"` : ''}
alt="${title}"
${image.id ? `data-image-id="${image.id}"` : ''}
alt="${title}"
class="lazy"
loading="lazy"
onerror="this.src='/assets/placeholder.svg'"
>
<div class="image-caption">
@@ -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;
}

View File

@@ -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) {
<div class="album-cover">
<img
src="${coverImageUrl}"
${dataSrc ? `data-src="${dataSrc}"` : ''}
${album.cover_image_id ? `data-image-id="${album.cover_image_id}"` : ''}
alt="${album.title}"
class="lazy"
onerror="this.src='/assets/placeholder.svg'"
@@ -414,7 +596,7 @@ function displayAlbums(albums) {
<div class="album-cover">
<img
src="${coverImageUrl}"
${dataSrc ? `data-src="${dataSrc}"` : ''}
${album.cover_image_id ? `data-image-id="${album.cover_image_id}"` : ''}
alt="${album.title}"
class="lazy"
onerror="this.src='/assets/placeholder.svg'"
@@ -730,7 +912,8 @@ function displayFavorites(favoriteImages) {
` : `${favoriteImages.map((fav, index) => `
<div class="favorite-card" onclick="openFavoriteImage('${fav.favorite.album_id}')">
<div class="card-image">
<img src="${API_BASE}/image/${fav.image.id}"
<img src="/assets/placeholder.svg"
data-image-id="${fav.image.id}"
alt="${fav.image.title || '收藏的图片'}"
loading="lazy">
<div class="card-overlay">

View File

@@ -386,7 +386,7 @@ impl TaskQueue {
false
}
/// 启动定期清理过期任务
pub async fn start_periodic_cleanup(self: Arc<Self>, 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<u8>,
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<Vec<u8>>) -> Task {
Task {

View File

@@ -384,7 +384,7 @@ pub async fn create_album(
}
}
/// 获取原始图片(已移除缩略图功能)
/// 获取压缩图片(后端压缩,前端解压)
pub async fn get_image(
db: web::Data<Arc<Database>>,
image_processor: web::Data<Arc<ImageProcessor>>,
@@ -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<Vec<u8>, Box<dyn std::error::Error>> {
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)
}