feat: 图片lazy loading

This commit is contained in:
2026-08-12 18:15:41 +08:00
parent 67070e5fc4
commit 60131d6fa9
5 changed files with 135 additions and 9 deletions

View File

View File

@@ -0,0 +1,131 @@
---
interface Props {
src: string;
alt?: string;
width?: number;
height?: number;
class?: string;
}
const {
src,
alt = "",
width,
height,
class: className = ""
} = Astro.props;
---
<div
class={`lazy-image ${className}`}
data-src={src}
style={`aspect-ratio:${width}/${height}`}
>
<div class="placeholder"></div>
<img
class="real-image"
alt={alt}
width={width}
height={height}
loading="lazy"
decoding="async"
/>
</div>
<style>
.lazy-image {
position: relative;
overflow: hidden;
width: 100%;
height: 100%;
}
.lazy-image img {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
}
.placeholder {
position: absolute;
inset: 0;
background: #f3f4f6;
background-image: linear-gradient(
90deg,
#f3f4f6 0%,
#e5e7eb 50%,
#f3f4f6 100%
);
background-size: 200% 100%;
animation: loading 1.5s infinite;
}
@keyframes loading {
from {
background-position: 200% 0;
}
to {
background-position: -200% 0;
}
}
.real-image {
opacity: 0;
transition: opacity .35s ease;
}
.real-image.loaded {
opacity: 1;
}
</style>
<script>
function initLazyImages() {
const images = document.querySelectorAll(".lazy-image");
const observer = new IntersectionObserver(
entries => {
entries.forEach(entry => {
if (!entry.isIntersecting) return;
const container = entry.target as HTMLElement;
const img = container.querySelector(
".real-image"
) as HTMLImageElement;
const src = container.dataset.src;
if (!src) return;
img.src = src;
img.onload = () => {
img.classList.add("loaded");
const placeholder =
container.querySelector(".placeholder");
placeholder?.remove();
};
observer.unobserve(container);
});
},
{
rootMargin: "300px"
}
);
images.forEach(img => observer.observe(img));
}
initLazyImages();
</script>

View File

@@ -4,6 +4,7 @@ import Folder from "@/assets/icon/folder.png?url";
import Calendar from "@/assets/icon/calendar.png?url"; import Calendar from "@/assets/icon/calendar.png?url";
import type {Post} from "@/types/post"; import type {Post} from "@/types/post";
import {formatDatetime} from "@/lib/format.ts"; import {formatDatetime} from "@/lib/format.ts";
import LazyImage from "@/components/LazyImage.astro";
interface Props { interface Props {
posts: Post[] posts: Post[]
@@ -23,7 +24,7 @@ posts.map((post) => {
<a href={`/post/${post.slug}`}> <a href={`/post/${post.slug}`}>
<article class="post-card"> <article class="post-card">
<div class="post-cover"> <div class="post-cover">
<img src={post.cover} alt={post.title} loading="lazy"/> <LazyImage src={post.cover} alt={post.title} />
</div> </div>
<div class="post-body"> <div class="post-body">
<div class="post-meta"> <div class="post-meta">
@@ -112,12 +113,6 @@ posts.map((post) => {
overflow: hidden; overflow: hidden;
} }
.post-cover img {
width: 100%;
height: 100%;
object-fit: cover;
}
.post-body { .post-body {
flex: 1; flex: 1;
min-width: 0; min-width: 0;

View File

@@ -16,7 +16,7 @@ const { list, total } = await http.get<ApiPageResponse<Post>>("posts", {
const totalPage = Math.ceil(total / metadata.postPageSize); const totalPage = Math.ceil(total / metadata.postPageSize);
--- ---
<Layout> <Layout description="天若有情天亦老,人间正道是沧桑。" title="博客首页">
<IndexView <IndexView
posts={list} posts={list}
page={1} page={1}

View File

@@ -27,7 +27,7 @@ if (totalPage < Number(page)) {
} }
--- ---
<Layout> <Layout description="天若有情天亦老,人间正道是沧桑。" title="博客首页">
<IndexView <IndexView
posts={list} posts={list}
page={Number(page)} page={Number(page)}