diff --git a/src/components/Countdown.tsx b/src/components/Countdown.tsx index 94c38e6..12fcb44 100644 --- a/src/components/Countdown.tsx +++ b/src/components/Countdown.tsx @@ -81,11 +81,9 @@ export const Countdown: React.FC = ({ targetDate, className = '' setTimeLeft(newTimeLeft); // 如果已经到期,清除计时器 - if (newTimeLeft.expired) { - if (timerRef.current !== null) { - clearInterval(timerRef.current); - timerRef.current = null; - } + if (newTimeLeft.expired && timerRef.current !== null) { + clearInterval(timerRef.current); + timerRef.current = null; } }, 1000); diff --git a/src/components/Header.astro b/src/components/Header.astro index 633dee5..7c6f674 100644 --- a/src/components/Header.astro +++ b/src/components/Header.astro @@ -286,7 +286,63 @@ const normalizedPath = + diff --git a/src/components/Layout.astro b/src/components/Layout.astro index 47e9a21..8ec4673 100644 --- a/src/components/Layout.astro +++ b/src/components/Layout.astro @@ -55,25 +55,46 @@ const { title = SITE_NAME, description = SITE_DESCRIPTION, date, author, tags, i ))} diff --git a/src/components/MediaGrid.tsx b/src/components/MediaGrid.tsx index 1819395..5e324fb 100644 --- a/src/components/MediaGrid.tsx +++ b/src/components/MediaGrid.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useRef, useState } from "react"; +import React, { useEffect, useRef, useState, useCallback } from "react"; interface MediaGridProps { type: "movie" | "book"; @@ -21,6 +21,9 @@ const MediaGrid: React.FC = ({ type, title, doubanId }) => { const itemsPerPage = 15; const mediaListRef = useRef(null); const lastScrollTime = useRef(0); + const abortControllerRef = useRef(null); + const observerRef = useRef(null); + const scrollDetectorRef = useRef(null); // 使用ref来跟踪关键状态,避免闭包问题 const stateRef = useRef({ @@ -30,8 +33,8 @@ const MediaGrid: React.FC = ({ type, title, doubanId }) => { error: null as string | null, }); - // 封装fetch函数但不使用useCallback以避免依赖循环 - const fetchMedia = async (page = 1, append = false) => { + // 封装fetch函数使用useCallback避免重新创建 + const fetchMedia = useCallback(async (page = 1, append = false) => { // 使用ref中的最新状态 if ( stateRef.current.isLoading || @@ -41,6 +44,14 @@ const MediaGrid: React.FC = ({ type, title, doubanId }) => { return; } + // 取消之前的请求 + if (abortControllerRef.current) { + abortControllerRef.current.abort(); + } + + // 创建新的AbortController + abortControllerRef.current = new AbortController(); + // 更新状态和ref setIsLoading(true); stateRef.current.isLoading = true; @@ -55,6 +66,7 @@ const MediaGrid: React.FC = ({ type, title, doubanId }) => { try { const response = await fetch( `/api/douban?type=${type}&start=${start}&doubanId=${doubanId}`, + { signal: abortControllerRef.current.signal } ); if (!response.ok) { // 解析响应内容,获取详细错误信息 @@ -127,6 +139,11 @@ const MediaGrid: React.FC = ({ type, title, doubanId }) => { stateRef.current.hasMoreContent = newHasMoreContent; } } catch (error) { + // 如果是取消的请求,不显示错误 + if (error instanceof Error && error.name === 'AbortError') { + return; + } + // 只有在非追加模式下才清空已加载的内容 if (!append) { setItems([]); @@ -136,10 +153,10 @@ const MediaGrid: React.FC = ({ type, title, doubanId }) => { setIsLoading(false); stateRef.current.isLoading = false; } - }; + }, [type, doubanId]); // 处理滚动事件 - const handleScroll = () => { + const handleScroll = useCallback(() => { // 获取关键滚动值 const scrollY = window.scrollY; const windowHeight = window.innerHeight; @@ -167,7 +184,7 @@ const MediaGrid: React.FC = ({ type, title, doubanId }) => { if (scrollPosition >= threshold) { fetchMedia(stateRef.current.currentPage + 1, true); } - }; + }, [fetchMedia]); // 更新ref值以跟踪状态变化 useEffect(() => { @@ -186,6 +203,58 @@ const MediaGrid: React.FC = ({ type, title, doubanId }) => { stateRef.current.error = error; }, [error]); + // 设置和清理IntersectionObserver + const setupIntersectionObserver = useCallback(() => { + // 清理旧的Observer + if (observerRef.current) { + observerRef.current.disconnect(); + observerRef.current = null; + } + + // 清理旧的检测元素 + if (scrollDetectorRef.current) { + scrollDetectorRef.current.remove(); + scrollDetectorRef.current = null; + } + + // 创建新的IntersectionObserver + const observerOptions = { + root: null, + rootMargin: "300px", + threshold: 0.1, + }; + + observerRef.current = new IntersectionObserver((entries) => { + const entry = entries[0]; + + if ( + entry.isIntersecting && + !stateRef.current.isLoading && + stateRef.current.hasMoreContent && + !stateRef.current.error + ) { + fetchMedia(stateRef.current.currentPage + 1, true); + } + }, observerOptions); + + // 创建并添加检测底部的元素 + const footer = document.createElement("div"); + footer.id = "scroll-detector"; + footer.style.width = "100%"; + footer.style.height = "10px"; + scrollDetectorRef.current = footer; + + // 确保mediaListRef有父元素 + if (mediaListRef.current && mediaListRef.current.parentElement) { + // 插入到grid后面而不是内部 + mediaListRef.current.parentElement.insertBefore( + footer, + mediaListRef.current.nextSibling, + ); + observerRef.current.observe(footer); + } + }, [fetchMedia]); + // 组件初始化和依赖变化时重置 useEffect(() => { // 重置状态 @@ -204,69 +273,50 @@ const MediaGrid: React.FC = ({ type, title, doubanId }) => { // 清空列表 setItems([]); + // 取消可能存在的请求 + if (abortControllerRef.current) { + abortControllerRef.current.abort(); + } + // 加载第一页数据 fetchMedia(1, false); - // 管理滚动事件 - const scrollListener = handleScroll; - - // 移除任何现有监听器 - window.removeEventListener("scroll", scrollListener); - - // 添加滚动事件监听器 - 使用passive: true可提高滚动性能 - window.addEventListener("scroll", scrollListener, { passive: true }); - - // 创建一个IntersectionObserver作为备选检测方案 - const observerOptions = { - root: null, - rootMargin: "300px", - threshold: 0.1, - }; - - const intersectionObserver = new IntersectionObserver((entries) => { - const entry = entries[0]; - - if ( - entry.isIntersecting && - !stateRef.current.isLoading && - stateRef.current.hasMoreContent && - !stateRef.current.error - ) { - fetchMedia(stateRef.current.currentPage + 1, true); - } - }, observerOptions); - - // 添加检测底部的元素 - 放在grid容器的后面而不是内部 - const footer = document.createElement("div"); - footer.id = "scroll-detector"; - footer.style.width = "100%"; - footer.style.height = "10px"; - - // 确保mediaListRef有父元素 - if (mediaListRef.current && mediaListRef.current.parentElement) { - // 插入到grid后面而不是内部 - mediaListRef.current.parentElement.insertBefore( - footer, - mediaListRef.current.nextSibling, - ); - intersectionObserver.observe(footer); - } + // 设置滚动事件监听器 + window.addEventListener("scroll", handleScroll, { passive: true }); + + // 设置IntersectionObserver + setupIntersectionObserver(); // 初始检查一次,以防内容不足一屏 const timeoutId = setTimeout(() => { if (stateRef.current.hasMoreContent && !stateRef.current.isLoading) { - scrollListener(); + handleScroll(); } }, 500); // 清理函数 return () => { clearTimeout(timeoutId); - window.removeEventListener("scroll", scrollListener); - intersectionObserver.disconnect(); - document.getElementById("scroll-detector")?.remove(); + window.removeEventListener("scroll", handleScroll); + + // 清理IntersectionObserver + if (observerRef.current) { + observerRef.current.disconnect(); + observerRef.current = null; + } + + // 清理scroll detector元素 + if (scrollDetectorRef.current) { + scrollDetectorRef.current.remove(); + scrollDetectorRef.current = null; + } + + // 取消正在进行的请求 + if (abortControllerRef.current) { + abortControllerRef.current.abort(); + } }; - }, [type, doubanId]); // 只在关键属性变化时执行 + }, [type, doubanId, handleScroll, fetchMedia, setupIntersectionObserver]); // 错误提示组件 const ErrorMessage = () => { @@ -345,6 +395,7 @@ const MediaGrid: React.FC = ({ type, title, doubanId }) => { src={item.imageUrl} alt={item.title} className="absolute top-0 left-0 w-full h-full object-cover hover:scale-105" + loading="lazy" />

diff --git a/src/components/ThemeToggle.astro b/src/components/ThemeToggle.astro index e83f5eb..31fb008 100644 --- a/src/components/ThemeToggle.astro +++ b/src/components/ThemeToggle.astro @@ -47,7 +47,58 @@ const { \ No newline at end of file diff --git a/src/consts.ts b/src/consts.ts index 4d50a42..ae0c3d8 100644 --- a/src/consts.ts +++ b/src/consts.ts @@ -8,7 +8,7 @@ export const NAV_LINKS = [ { href: '/movies', text: '观影' }, { href: '/books', text: '读书' }, { href: '/projects', text: '项目' }, - { href: '/other', text: '其他' } + { href: '/other', text: '其他' }, ]; export const ICP = '渝ICP备2022009272号'; diff --git a/src/content/技术日志/web/echoes博客使用说明.md b/src/content/技术日志/web/echoes博客使用说明.md index b1fdcd6..ed6250f 100644 --- a/src/content/技术日志/web/echoes博客使用说明.md +++ b/src/content/技术日志/web/echoes博客使用说明.md @@ -15,6 +15,7 @@ tags: [] 5. **观影记录**:集成豆瓣观影数据 6. **读书记录**:集成豆瓣读书数据 7. **旅行足迹**:支持展示全球旅行足迹热力图 +8. **丝滑页面过渡**:使用 Swup 集成实现页面间无缝过渡动画,提供类似 SPA 的浏览体验,保留静态站点的所有优势 ## 基础配置 diff --git a/src/pages/404.astro b/src/pages/404.astro index 1c742d6..9bfc081 100644 --- a/src/pages/404.astro +++ b/src/pages/404.astro @@ -1,6 +1,10 @@ --- import Layout from '@/components/Layout.astro'; import { SITE_NAME } from '@/consts'; + +// 启用静态预渲染 +export const prerender = true; + --- diff --git a/src/pages/api/articles.ts b/src/pages/api/articles.ts index 5f184e2..014ebdb 100644 --- a/src/pages/api/articles.ts +++ b/src/pages/api/articles.ts @@ -32,7 +32,6 @@ export const GET: APIRoute = async ({ request }) => { if (path) { const normalizedPath = path.toLowerCase(); filteredArticles = filteredArticles.filter(article => { - const articlePath = article.id.split('/'); return article.id.toLowerCase().includes(normalizedPath); }); } diff --git a/src/pages/articles/[...id].astro b/src/pages/articles/[...id].astro index be717c2..475022a 100644 --- a/src/pages/articles/[...id].astro +++ b/src/pages/articles/[...id].astro @@ -115,7 +115,7 @@ function getArticleUrl(articleId: string) {
- - + diff --git a/src/pages/articles/[...path].astro b/src/pages/articles/[...path].astro index d9ca901..c75375a 100644 --- a/src/pages/articles/[...path].astro +++ b/src/pages/articles/[...path].astro @@ -1,6 +1,9 @@ --- import ArticlesPage, { getStaticPaths as getOriginalPaths } from './index.astro'; +// 启用静态预渲染 +export const prerender = true; + // 重新导出 getStaticPaths,处理所有路径模式 export async function getStaticPaths() { const paths = await getOriginalPaths(); @@ -98,12 +101,6 @@ const mergedProps = { view }; -// 生成页面标题 -let pageTitle = path ? path : '文章列表'; -if (tag) { - pageTitle = `标签: ${tag}`; -} --- -

{pageTitle}

\ No newline at end of file