跳到主要内容

Doucsaurus自定义blog列表的分页

· 阅读需 6 分钟
zgh
Front-end Engineer

默认情况下,Docusaurus 的 blog 列表分页是「较新的博文/较旧的博文」的形式。如果想改成数字按钮形式,需要自定义分页组件。

我的需求如下:

  1. 需要一个除了 /blog 以外的路径,例如:/code
  2. code 列表页可以使用自定义的分页组件,blog 列表页使用默认的分页组件

定义独立的 blog 类型

Docusaurus 允许通过 @docusaurus/plugin-content-blog 为不同路径定义多个独立的博客类型。

参考 自定义 blog 类型的路径

修改默认的分页组件

pnpm docusaurus swizzle @docusaurus/theme-classic BlogListPaginator --eject --typescript

修改 src/theme/BlogListPaginator/index.tsx 文件:

import React from 'react';
import type { Props } from '@theme/BlogListPaginator';
import CustomPaginator from '../../components/CustomBlogPaginator';

export default function BlogListPaginator(props: Props): JSX.Element {
return <CustomPaginator {...props} />;
}

创建自定义分页组件

src/components中创建一个 CustomBlogPaginator 组件,目录结构如下:

- src
- components
- CustomBlogPaginator
- CustomPaginator.tsx
- index.module.css
- index.tsx

判断路径中是否包含 /code,如果是,则使用自定义分页组件,否则使用默认的分页组件。

index.tsx
import React from 'react';
import BlogListPaginator from '@theme-original/BlogListPaginator';
import CustomPaginator from './CustomPaginator';

export default function CodePaginator({ metadata }) {
if (metadata.permalink.includes('/code')) {
return <CustomPaginator metadata={metadata} />;
}
return <BlogListPaginator metadata={metadata} />;
}

定义分页组件

CustomPaginator.tsx
import React, { useState } from 'react';
import type { Props } from '@theme/BlogListPaginator';
import clsx from 'clsx';
import Link from '@docusaurus/Link';
import styles from './index.module.css';

interface ChevronIconProps {
left: string;
right: string;
doubleLeft: string;
doubleRight: string;
ellipsis: string;
}
type ChevronIconType = keyof ChevronIconProps;

const dataMap: ChevronIconProps = {
left: 'M15.75 19.5 8.25 12l7.5-7.5',
right: 'm8.25 4.5 7.5 7.5-7.5 7.5',
doubleLeft: 'm18.75 4.5-7.5 7.5 7.5 7.5m-6-15L5.25 12l7.5 7.5',
doubleRight: 'm5.25 4.5 7.5 7.5-7.5 7.5m6-15 7.5 7.5-7.5 7.5',
ellipsis:
'M6.75 12a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0ZM12.75 12a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0ZM18.75 12a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0Z'
};

const chevronIcon = (type: ChevronIconType) => {
let data = dataMap[type];
return (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
style={{ width: '20px', height: '20px', verticalAlign: 'middle' }}
>
<path strokeLinecap="round" strokeLinejoin="round" d={data} />;
</svg>
);
};

export default function BlogListPaginator(props: Props): JSX.Element {
const { totalPages, page, permalink } = props.metadata;
const [hoveredEllipsis, setHoveredEllipsis] = useState(null);

// 生成分页按钮的路径
const getPageLink = pageNumber => {
if (pageNumber === 1) {
return permalink.replace(/\/page\/\d+$/, ''); // 第一页为根路径
}
return `${permalink.replace(/\/page\/\d+$/, '')}/page/${pageNumber}`;
};

// 动态生成分页显示范围
const handlePageRange = () => {
const maxButtons = 5; // 默认显示的按钮数量
const range = [];

// 始终显示第一页
range.push(1);

if (totalPages <= maxButtons) {
// 如果总页数少于等于 5,则直接显示所有页码
for (let i = 2; i <= totalPages; i++) {
range.push(i);
}
} else if (page <= 3) {
// 当前页在 1-3 时,显示前 5 页和省略号
for (let i = 2; i <= Math.min(5, totalPages - 1); i++) {
range.push(i);
}
range.push('next-ellipsis');
range.push(totalPages);
} else if (page === 4) {
// 当前页为第 4 页时,显示前 6 页和省略号
for (let i = 2; i <= 6; i++) {
range.push(i);
}
range.push('next-ellipsis');
range.push(totalPages);
} else if (page >= 5 && page <= totalPages - 4) {
// 当前页在中间时,显示左右省略号和前后 2 页
range.push('prev-ellipsis');
for (let i = page - 2; i <= page + 2; i++) {
range.push(i);
}
range.push('next-ellipsis');
range.push(totalPages);
} else {
// 当前页接近末尾时,显示最后 5 页和省略号
range.push('prev-ellipsis');
for (let i = totalPages - 4; i < totalPages; i++) {
range.push(i);
}
range.push(totalPages);
}

return range;
};

const pageNumbers = handlePageRange();

return (
<nav className="pagination-nav">
<ul className={styles.pagination}>
{/* 左箭头 */}
<li
className={clsx(styles.pageItem, page === 1 && styles.disabled)}
title={page === 1 ? '没有上一页' : '上一页'}
>
<Link to={page > 1 ? getPageLink(page - 1) : '#'} className={styles.pageLink} aria-disabled={page === 1}>
{chevronIcon('left')}
</Link>
</li>

{/* 数字按钮和省略号 */}
{pageNumbers.map((pageNumber, index) => {
if (pageNumber === 'prev-ellipsis' || pageNumber === 'next-ellipsis') {
const isPrev = pageNumber === 'prev-ellipsis';
return (
<li
key={`${pageNumber}-${index}`}
className={styles.pageItem}
onMouseEnter={() => setHoveredEllipsis(pageNumber)}
onMouseLeave={() => setHoveredEllipsis(null)}
>
<Link
to={getPageLink(isPrev ? Math.max(1, page - 5) : Math.min(totalPages, page + 5))}
title={isPrev ? '向前 5 页' : '向后 5 页'}
className={styles.pageLink}
>
{hoveredEllipsis === pageNumber
? isPrev
? chevronIcon('doubleLeft')
: chevronIcon('doubleRight')
: chevronIcon('ellipsis')}
</Link>
</li>
);
}
return (
<li key={pageNumber} className={clsx(styles.pageItem, pageNumber === page && styles.active)}>
<Link to={getPageLink(pageNumber)} className={styles.pageLink}>
{pageNumber}
</Link>
</li>
);
})}

{/* 右箭头 */}
<li
className={clsx(styles.pageItem, page === totalPages && styles.disabled)}
title={page === totalPages ? '没有下一页' : '下一页'}
>
<Link
to={page < totalPages ? getPageLink(page + 1) : '#'}
className={styles.pageLink}
aria-disabled={page === totalPages}
>
{chevronIcon('right')}
</Link>
</li>
</ul>
</nav>
);
}

定义分页样式

index.module.css
.pagination {
display: flex;
list-style: none;
padding: 0;
margin: 0;
}

.pageItem {
width: 32px;
height: 32px;
margin: 0 5px;
}

.pageItem a {
display: block;
width: 100%;
height: 100%;
text-align: center;
line-height: 30px;
text-decoration: none;
padding: 0 6px;
font-size: 14px;
color: var(--ifm-color-primary);
border: 1px solid #ddd;
border-radius: 4px;
transition: all 0.3s ease;
}

.pageItem a:hover {
background-color: rgba(0, 0, 0, 0.06);
border-color: var(--ifm-color-primary);
}

.pageItem.disabled a {
color: #aaa;
cursor: not-allowed;
border-color: #eee;
background-color: var(--paginator-item-disabled-bg);
}

.pageItem.active a {
font-weight: bold;
background-color: var(--ifm-color-primary);
color: white;
border-color: var(--ifm-color-primary);
}

定义全局变量

src/css/custom.css中定义变量:

src/css/custom.css
:root {
--ifm-color-primary: #338bff;
--paginator-item-disabled-bg: #f9f9f9;
}

[data-theme='dark'] {
--ifm-color-primary: #25c2a0;
--paginator-item-disabled-bg: #393737;
}