Skip to content

Commit d000474

Browse files
committed
feat: 阅读进度
1 parent df6ee64 commit d000474

File tree

5 files changed

+82
-4
lines changed

5 files changed

+82
-4
lines changed

src/components/icons/Progress.tsx

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
export function MaterialSymbolsProgressActivity(props: { progress: number }) {
2+
const offset = 87.92 * ((100 - props.progress) / 100);
3+
4+
return (
5+
<svg style={{ transform: 'rotate(-90deg)' }} width="1em" height="1em" viewBox="0 0 36 36">
6+
{/* 第一圈 */}
7+
<circle r="14" cx="18" cy="18" fill="transparent" stroke="#e0e0e0" strokeWidth="4"></circle>
8+
{/* 第二圈,进度条 */}
9+
<circle
10+
r="14"
11+
cx="18"
12+
cy="18"
13+
fill="transparent"
14+
stroke="var(--accent-color)"
15+
strokeWidth="4"
16+
strokeDasharray="87.92"
17+
strokeDashoffset={`${offset}`}
18+
></circle>
19+
</svg>
20+
);
21+
}

src/components/modules/shared/ArticleRightAside.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import React from 'react';
22
import { PropsWithChildren } from 'react';
33

44
import { TocAside } from '../toc/TocAside';
5+
import { ReadIndicator } from './ReadIndicator';
56

67
export const ArticleRightAside = ({ children }: PropsWithChildren) => {
78
return (
@@ -11,6 +12,7 @@ export const ArticleRightAside = ({ children }: PropsWithChildren) => {
1112
as="div"
1213
className="static ml-4"
1314
treeClassName="absolute h-full min-h-[120px] flex flex-col"
15+
accessory={<ReadIndicator />}
1416
/>
1517
</div>
1618
{!!children &&
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
'use client';
2+
3+
import { useEffect, useState } from 'react';
4+
5+
import { MaterialSymbolsProgressActivity } from '@/components/icons/Progress';
6+
import { MotionButtonBase } from '@/components/ui/button';
7+
import { cn } from '@/lib/helper';
8+
import { springScrollToTop } from '@/lib/scroller';
9+
import { useMainArticleStore } from '@/store/mainArticleStore';
10+
11+
export const ReadIndicator = () => {
12+
const { Element } = useMainArticleStore();
13+
const [readPercent, setReadPercent] = useState(0);
14+
15+
useEffect(() => {
16+
const handleScroll = () => {
17+
if (!Element) return;
18+
19+
const elementTop = Element.getBoundingClientRect().top + window.scrollY;
20+
const scrollTop = window.scrollY;
21+
const winHeight = window.innerHeight;
22+
const elementHeight = Element.offsetHeight;
23+
// 为了解决一开始进度不为0的问题
24+
const readHeight = scrollTop > winHeight ? scrollTop + winHeight - elementTop : scrollTop;
25+
const scrollPosition = (readHeight / elementHeight) * 100;
26+
setReadPercent(Math.floor(Math.min(Math.max(0, scrollPosition), 100)));
27+
};
28+
29+
window.addEventListener('scroll', handleScroll);
30+
handleScroll();
31+
32+
return () => {
33+
window.removeEventListener('scroll', handleScroll);
34+
};
35+
}, [Element]);
36+
37+
return (
38+
<div className="text-gray-800 dark:text-neutral-300">
39+
<div className="flex items-center gap-2">
40+
<MaterialSymbolsProgressActivity progress={readPercent} />
41+
{readPercent}%<br />
42+
</div>
43+
<MotionButtonBase
44+
onClick={springScrollToTop}
45+
className={cn(
46+
'mt-1 flex flex-nowrap items-center gap-2 opacity-50 transition-all duration-500 hover:opacity-100',
47+
readPercent > 10 ? '' : 'pointer-events-none opacity-0',
48+
)}
49+
>
50+
<i className="i-mingcute-arrow-up-circle-line" />
51+
<span className="whitespace-nowrap">回到顶部</span>
52+
</MotionButtonBase>
53+
</div>
54+
);
55+
};

src/components/modules/toc/TocAside.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { useMainArticleStore } from '@/store/mainArticleStore';
1111

1212
export type TocAsideProps = {
1313
treeClassName?: string;
14-
accessory?: React.ReactNode | React.FC;
14+
accessory?: React.ReactNode;
1515
as?: React.ElementType;
1616
className?: string;
1717
};
@@ -21,6 +21,7 @@ export const TocAside = ({
2121
children,
2222
treeClassName,
2323
as: As = 'aside',
24+
accessory,
2425
}: TocAsideProps & PropsWithChildren) => {
2526
const containerRef = useRef<HTMLUListElement>(null);
2627
const $article = useMainArticleStore(useShallow((state) => state.Element));
@@ -62,6 +63,7 @@ export const TocAside = ({
6263
$headings={$headings}
6364
containerRef={containerRef}
6465
className={cn('absolute max-h-[75vh]', treeClassName)}
66+
accessory={accessory}
6567
/>
6668
{children}
6769
</As>

src/components/ui/divider/index.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,7 @@ export const DividerVertical: FC<
2828
className,
2929
)}
3030
{...rest}
31-
>
32-
w
33-
</span>
31+
></span>
3432
);
3533
};
3634

0 commit comments

Comments
 (0)