refactor(pages, components): 移除冗余组件并优化页面结构

- 删除不再使用的 HomeContent 组件,简化主页逻辑。
- 更新文章和分类页面,使用 Suspense 组件优化加载状态,提升用户体验。
- 修改 API 调用逻辑,确保在记录文章访问量时处理异常情况。
- 更新分页组件,支持从父组件传递 basePath,增强灵活性。
This commit is contained in:
刘宇阳
2026-07-08 13:25:47 +08:00
parent 4f2935e37e
commit 595f0afabe
19 changed files with 285 additions and 156 deletions

View File

@@ -23,7 +23,6 @@ const nextConfig = {
reactCompiler: true,
// 启用 Turbopack 文件系统缓存,加快开发时候的构建速度
experimental: {
dynamicIO: true,
turbopackFileSystemCacheForDev: true,
},
// 配置图片来源

View File

@@ -0,0 +1,38 @@
import ArticleLayout from '@/components/ArticleLayout';
import { getArticlePagingCacheAPI } from '@/lib/article';
import { getThemeConfigCacheAPI, getThemeCoversCacheAPI } from '@/lib/theme';
import { getSwiperListCacheAPI } from '@/lib/swiper';
interface Props {
searchParams: Promise<{ page?: number }>;
}
export default async ({ searchParams }: Props) => {
const { page: pageParam } = await searchParams;
const page = Number(pageParam) || 1;
const [theme, covers, { data: swiper }] = await Promise.all([
getThemeConfigCacheAPI(),
getThemeCoversCacheAPI(),
getSwiperListCacheAPI(),
]);
swiper.result = swiper.result?.sort((a, b) => (a.order || 0) - (b.order || 0)) ?? [];
const { data } = await getArticlePagingCacheAPI({
pageNum: page,
pageSize: theme.is_article_layout === 'waterfall' ? 28 : 8,
});
data.result = data?.result?.filter((item) => item.config.status !== 'no_home') ?? [];
return (
<ArticleLayout
page={page}
basePath="/"
theme={theme}
covers={covers}
swiper={swiper}
data={data}
/>
);
};

View File

@@ -0,0 +1,21 @@
import Slide from '@/components/Slide';
import Typed from '@/components/Typed';
import Starry from '@/components/Starry';
import { getThemeConfigCacheAPI, getThemeCoversCacheAPI } from '@/lib/theme';
export default async () => {
const [theme, covers] = await Promise.all([
getThemeConfigCacheAPI(),
getThemeCoversCacheAPI(),
]);
return (
<Slide src={theme?.swiper_image} covers={covers}>
<Starry />
<Typed
swiperText={theme?.swiper_text}
className="absolute top-[45%] sm:top-[40%] left-[50%] transform -translate-x-1/2 w-[80%] text-center text-white xs:text-xl sm:text-[30px] leading-7 sm:leading-[40px] md:leading-[50px] custom_text_shadow"
/>
</Slide>
);
};

View File

@@ -0,0 +1,14 @@
import Sidebar from '@/components/Sidebar';
import { getThemeConfigCacheAPI } from '@/lib/theme';
export default async () => {
const theme = await getThemeConfigCacheAPI();
return (
<Sidebar
sidebar={theme?.right_sidebar ?? []}
social={theme?.social ?? []}
recoArticleIds={theme?.reco_article ?? []}
/>
);
};

25
src/app/(home)/page.tsx Normal file
View File

@@ -0,0 +1,25 @@
import { Suspense } from 'react';
import Container from '@/components/Container';
import ArticlesFallback from '@/components/ArticlesFallback';
import HomeShell from './components/HomeShell';
import HomeArticles from './components/HomeArticles';
import HomeSidebar from './components/HomeSidebar';
interface Props {
searchParams: Promise<{ page?: number }>;
}
export default (props: Props) => (
<>
<HomeShell />
<Container>
<Suspense fallback={<ArticlesFallback />}>
<HomeArticles searchParams={props.searchParams} />
</Suspense>
<Suspense fallback={null}>
<HomeSidebar />
</Suspense>
</Container>
</>
);

View File

@@ -104,8 +104,12 @@ export default async (props: Props) => {
}
// 记录文章访问量
after(() => {
void recordViewAPI(id);
after(async () => {
try {
await recordViewAPI(id);
} catch (error) {
if (error instanceof Error && error.name === 'AbortError') return;
}
});
const headings = extractArticleHeadings(data?.content);

View File

@@ -0,0 +1,30 @@
import Classics from '@/components/ArticleLayout/Classics';
import Pagination from '@/components/Pagination';
import { getCateArticleListCacheAPI } from '@/lib/cate';
interface Props {
id: number;
searchParams: Promise<{ page?: number; name?: string }>;
}
export default async ({ id, searchParams }: Props) => {
const { page: pageParam, name } = await searchParams;
const page = Number(pageParam) || 1;
const { data } = await getCateArticleListCacheAPI(id, { pageNum: page, pageSize: 8 });
return (
<>
<Classics data={data} />
{data?.total > 0 && (
<Pagination
total={data.pages}
page={page}
basePath={`/cate/${id}`}
path={name ? `?name=${name}` : undefined}
className="flex justify-center mt-5"
/>
)}
</>
);
};

View File

@@ -0,0 +1,30 @@
import { getCateArticleListCacheAPI, getCateListCacheAPI } from '@/lib/cate';
import CateHero from '../../../components/CateHero';
import CateHeroContent from '../../../components/CateHeroContent';
import { CATE_HERO_IMAGE, findCateById } from '../../../utils';
interface Props {
params: Promise<{ id: number }>;
searchParams: Promise<{ name?: string }>;
}
export default async ({ params, searchParams }: Props) => {
const [{ id }, { name }] = await Promise.all([params, searchParams]);
const [{ data }, { data: cateListData }] = await Promise.all([
getCateArticleListCacheAPI(id, { pageNum: 1, pageSize: 8 }),
getCateListCacheAPI(),
]);
const cateInfo = findCateById(cateListData?.result ?? [], id);
return (
<CateHero image={CATE_HERO_IMAGE}>
<CateHeroContent
name={cateInfo?.name ?? name ?? '分类'}
icon={cateInfo?.icon}
articleCount={data?.total ?? 0}
/>
</CateHero>
);
};

View File

@@ -1,14 +1,15 @@
import { Suspense } from 'react';
import { Metadata } from 'next';
import { getCateArticleListCacheAPI, getCateListCacheAPI } from '@/lib/cate';
import Classics from '@/components/ArticleLayout/Classics';
import Pagination from '@/components/Pagination';
import CateHero from '../components/CateHero';
import CateHeroContent from '../components/CateHeroContent';
import { CATE_HERO_IMAGE, findCateById } from '../utils';
import ArticlesFallback from '@/components/ArticlesFallback';
import { getCateListCacheAPI } from '@/lib/cate';
import CateHeroSection from './components/CateHeroSection';
import CateArticles from './components/CateArticles';
import { findCateById } from '../utils';
interface Props {
params: Promise<{ id: number }>;
searchParams: Promise<{ page: number; name: string }>;
searchParams: Promise<{ page?: number; name?: string }>;
}
export async function generateMetadata(props: Props): Promise<Metadata> {
@@ -24,38 +25,18 @@ export async function generateMetadata(props: Props): Promise<Metadata> {
}
export default async (props: Props) => {
const searchParams = await props.searchParams;
const params = await props.params;
const id = params.id;
const page = +(searchParams.page ?? 1);
const name = searchParams.name;
const [{ data }, { data: cateListData }] = await Promise.all([
getCateArticleListCacheAPI(id, { pageNum: page, pageSize: 8 }),
getCateListCacheAPI(),
]);
const cateInfo = findCateById(cateListData?.result ?? [], id);
return (
<>
<div className="min-h-screen bg-background text-foreground dark:bg-black-a">
<CateHero image={CATE_HERO_IMAGE}>
<CateHeroContent
name={cateInfo?.name ?? name}
icon={cateInfo?.icon}
articleCount={data?.total ?? 0}
/>
</CateHero>
<div className="min-h-screen bg-background text-foreground dark:bg-black-a">
<CateHeroSection params={props.params} searchParams={props.searchParams} />
<main className="relative mx-auto max-w-[900px] px-3 pb-10 sm:mt-8 sm:px-6 sm:pb-12 lg:px-8">
<Classics data={data} />
{data?.total > 0 && (
<Pagination total={data?.pages} page={page} path={`?name=${name}`} className="flex justify-center mt-5" />
)}
</main>
</div>
</>
<main className="relative mx-auto max-w-[900px] px-3 pb-10 sm:mt-8 sm:px-6 sm:pb-12 lg:px-8">
<Suspense fallback={<ArticlesFallback count={3} />}>
<CateArticles id={id} searchParams={props.searchParams} />
</Suspense>
</main>
</div>
);
};

View File

@@ -1,43 +0,0 @@
import Slide from '@/components/Slide';
import Typed from '@/components/Typed';
import Starry from '@/components/Starry';
import Container from '@/components/Container';
import ArticleLayout from '@/components/ArticleLayout';
import Sidebar from '@/components/Sidebar';
import { getArticlePagingCacheAPI } from '@/lib/article';
import { getThemeConfigCacheAPI, getThemeCoversCacheAPI } from '@/lib/theme';
import { getSwiperListCacheAPI } from '@/lib/swiper';
export default async ({ page }: { page: number }) => {
const [theme, covers, { data: swiper }] = await Promise.all([
getThemeConfigCacheAPI(),
getThemeCoversCacheAPI(),
getSwiperListCacheAPI(),
]);
swiper.result = swiper.result?.sort((a, b) => (a.order || 0) - (b.order || 0)) ?? [];
const { data } = await getArticlePagingCacheAPI({
pageNum: page || 1,
pageSize: theme.is_article_layout === 'waterfall' ? 28 : 8,
});
data.result = data?.result?.filter((item) => item.config.status !== 'no_home') ?? [];
return (
<>
<Slide src={theme?.swiper_image} covers={covers}>
<Starry />
<Typed
swiperText={theme?.swiper_text}
className="absolute top-[45%] sm:top-[40%] left-[50%] transform -translate-x-1/2 w-[80%] text-center text-white xs:text-xl sm:text-[30px] leading-7 sm:leading-[40px] md:leading-[50px] custom_text_shadow"
/>
</Slide>
<Container>
<ArticleLayout page={page} theme={theme} covers={covers} swiper={swiper} data={data} />
<Sidebar sidebar={theme?.right_sidebar ?? []} social={theme?.social ?? []} recoArticleIds={theme?.reco_article ?? []} />
</Container>
</>
);
};

View File

@@ -1,22 +0,0 @@
import { Suspense } from 'react';
import HomeContent from './components/HomeContent';
interface Props {
searchParams: Promise<{ page?: number }>;
}
export default async (props: Props) => {
const searchParams = await props.searchParams;
const page = Number(searchParams.page) || 1;
return (
<Suspense
fallback={
<div className="min-h-screen animate-pulse bg-neutral-100 dark:bg-neutral-900" aria-hidden />
}
>
<HomeContent page={page} />
</Suspense>
);
};

View File

@@ -0,0 +1,30 @@
import Classics from '@/components/ArticleLayout/Classics';
import Pagination from '@/components/Pagination';
import { getTagArticleListCacheAPI } from '@/lib/tag';
interface Props {
id: number;
searchParams: Promise<{ page?: number; name?: string }>;
}
export default async ({ id, searchParams }: Props) => {
const { page: pageParam, name } = await searchParams;
const page = Number(pageParam) || 1;
const { data } = await getTagArticleListCacheAPI(id, { pageNum: page, pageSize: 8 });
return (
<>
<Classics data={data} />
{data?.total > 0 && (
<Pagination
total={data.pages}
page={page}
basePath={`/tag/${id}`}
path={name ? `?name=${name}` : undefined}
className="flex justify-center mt-5"
/>
)}
</>
);
};

View File

@@ -0,0 +1,29 @@
import Starry from '@/components/Starry';
import Slide from '@/components/Slide';
import { getTagArticleListCacheAPI } from '@/lib/tag';
import { getThemeCoversCacheAPI } from '@/lib/theme';
interface Props {
id: number;
searchParams: Promise<{ name?: string }>;
}
export default async ({ id, searchParams }: Props) => {
const name = (await searchParams).name ?? '标签';
const [{ data }, covers] = await Promise.all([
getTagArticleListCacheAPI(id, { pageNum: 1, pageSize: 8 }),
getThemeCoversCacheAPI(),
]);
return (
<Slide isRipple={false} covers={covers}>
<Starry />
<div className="absolute top-[40%] left-[50%] transform -translate-x-1/2 w-[80%] text-center text-white text-[20px] xs:text-[25px] sm:text-[30px] custom_text_shadow">
<span>
{name} ~ {data?.total}
</span>
</div>
</Slide>
);
};

View File

@@ -1,14 +1,13 @@
import { Suspense } from 'react';
import { Metadata } from 'next';
import Starry from '@/components/Starry';
import Slide from '@/components/Slide';
import Classics from '@/components/ArticleLayout/Classics';
import Pagination from '@/components/Pagination';
import { getTagArticleListCacheAPI } from '@/lib/tag';
import { getThemeCoversCacheAPI } from '@/lib/theme';
import ArticlesFallback from '@/components/ArticlesFallback';
import TagHero from './components/TagHero';
import TagArticles from './components/TagArticles';
interface Props {
params: Promise<{ id: number }>;
searchParams: Promise<{ page: number; name: string }>;
searchParams: Promise<{ page?: number; name?: string }>;
}
export async function generateMetadata(props: Props): Promise<Metadata> {
@@ -22,37 +21,17 @@ export async function generateMetadata(props: Props): Promise<Metadata> {
}
export default async (props: Props) => {
const searchParams = await props.searchParams;
const params = await props.params;
const id = params.id;
const page = searchParams.page ?? 1;
const name = searchParams.name;
const [{ data }, covers] = await Promise.all([
getTagArticleListCacheAPI(id, { pageNum: page, pageSize: 8 }),
getThemeCoversCacheAPI(),
]);
return (
<>
<div>
<Slide isRipple={false} covers={covers}>
{/* 星空背景组件 */}
<Starry />
<TagHero id={id} searchParams={props.searchParams} />
{/* 标签信息 */}
<div className="absolute top-[40%] left-[50%] transform -translate-x-1/2 w-[80%] text-center text-white text-[20px] xs:text-[25px] sm:text-[30px] custom_text_shadow">
<span>
{name} ~ {data?.total}
</span>
</div>
</Slide>
<div className="md:w-full lg:w-[900px] lg:mx-auto px-4 lg:p-0 my-5">
<Classics data={data} />
{data?.total > 0 && <Pagination total={data?.pages} page={page} path={`?name=${name}`} className="flex justify-center mt-5" />}
</div>
<div className="md:w-full lg:w-[900px] lg:mx-auto px-4 lg:p-0 my-5">
<Suspense fallback={<ArticlesFallback count={3} />}>
<TagArticles id={id} searchParams={props.searchParams} />
</Suspense>
</div>
</>
);

View File

@@ -11,13 +11,14 @@ import { Article } from '@/types/app/article';
interface Props {
page: number;
basePath: string;
theme: Theme;
covers: string[];
swiper: { result?: SwiperItem[] };
data: Paginate<Article[]>;
}
export default ({ page, theme, covers, swiper, data }: Props) => {
export default ({ page, basePath, theme, covers, swiper, data }: Props) => {
const sidebar = theme?.right_sidebar ?? [];
return (
@@ -29,7 +30,9 @@ export default ({ page, theme, covers, swiper, data }: Props) => {
{theme.is_article_layout === 'card' && <Card data={data} covers={covers} />}
{theme.is_article_layout === 'waterfall' && <Waterfall data={data} covers={covers} />}
{!!data.total && <Pagination total={data?.pages} page={page} className="flex justify-center mt-5" />}
{!!data.total && (
<Pagination total={data.pages} page={page} basePath={basePath} className="flex justify-center mt-5" />
)}
</div>
);
};

View File

@@ -0,0 +1,12 @@
export default ({ count = 4 }: { count?: number }) => {
return (
<div className="space-y-2" aria-hidden>
{Array.from({ length: count }, (_, i) => (
<div
key={i}
className="panel h-[190px] animate-pulse bg-neutral-200/60 md:h-60 lg:h-52 xl:h-60 dark:bg-neutral-800/60"
/>
))}
</div>
);
};

View File

@@ -1,11 +1,9 @@
'use client';
import { usePathname } from 'next/navigation';
import Pagination from '@/ThriveUI/Pagination';
interface Props {
total: number;
page: number;
basePath: string;
path?: string;
className?: string;
}
@@ -16,16 +14,12 @@ function parseQuery(path?: string): Record<string, string> {
return Object.fromEntries(new URLSearchParams(qs));
}
export default ({ total, page, path, className }: Props) => {
const pathname = usePathname();
return (
<Pagination
current={+page}
totalPages={total}
basePath={pathname}
query={parseQuery(path)}
className={className}
/>
);
};
export default ({ total, page, basePath, path, className }: Props) => (
<Pagination
current={+page}
totalPages={total}
basePath={basePath}
query={parseQuery(path)}
className={className}
/>
);

View File

@@ -1,13 +1,15 @@
'use client';
import { useEffect } from 'react';
import { usePathname } from 'next/navigation';
import { usePathname, useSearchParams } from 'next/navigation';
// 监听路由变化
const RouteChangeHandler: React.FC = () => {
const pathname = usePathname();
const searchParams = useSearchParams();
const query = searchParams.toString();
// 每次切换页面滚动到顶部
// 每次切换页面或分页参数变化时滚动到顶部
useEffect(() => {
// 尊重开源,禁止删除此版权信息!!!
console.log(`%c 博客系统 %c ThriveX `, 'background: #35495e; padding: 4px; border-radius: 3px 0 0 3px; color: #fff', 'background: #539dfd; padding: 4px; border-radius: 0 3px 3px 0; color: #fff');
@@ -17,7 +19,7 @@ const RouteChangeHandler: React.FC = () => {
console.log('🌟 觉得好用的话记得点个 Star 哦 🙏');
window.scrollTo(0, 0);
}, [pathname]);
}, [pathname, query]);
return null;
};

View File

@@ -16,6 +16,9 @@ export const Request = async <T>(method: string, api: string, data?: any) => {
return res?.json() as Promise<ResponseData<T>>;
} catch (error) {
if (error instanceof Error && error.name === 'AbortError') {
throw error;
}
console.log('捕获到异常:', error);
return { code: 500, message: 'Request failed', data: {} as T };
}