前端:新增对Markdown解析的支持,优化文章内容的排版和样式,添加emoji支持。

This commit is contained in:
lsy 2024-12-09 13:22:39 +08:00
parent 14f50467f0
commit 2ffa4883ae
4 changed files with 462 additions and 254 deletions

View File

@ -1,6 +1,6 @@
import ErrorPage from "hooks/Error"; import ErrorPage from "hooks/Error";
import layout from "themes/echoes/layout"; import layout from "themes/echoes/layout";
import article from "themes/echoes/article"; import article from "themes/echoes/posts";
import about from "themes/echoes/about"; import about from "themes/echoes/about";
import { useLocation } from "react-router-dom"; import { useLocation } from "react-router-dom";
import post from "themes/echoes/post"; import post from "themes/echoes/post";

View File

@ -37,6 +37,7 @@
"react-markdown": "^9.0.1", "react-markdown": "^9.0.1",
"react-syntax-highlighter": "^15.6.1", "react-syntax-highlighter": "^15.6.1",
"rehype-raw": "^7.0.0", "rehype-raw": "^7.0.0",
"remark-emoji": "^5.0.1",
"remark-gfm": "^4.0.0", "remark-gfm": "^4.0.0",
"remark-parse": "^11.0.0", "remark-parse": "^11.0.0",
"three": "^0.171.0" "three": "^0.171.0"

View File

@ -19,8 +19,9 @@ import { getColorScheme } from "themes/echoes/utils/colorScheme";
import MarkdownIt from 'markdown-it'; import MarkdownIt from 'markdown-it';
import { ComponentPropsWithoutRef } from 'react'; import { ComponentPropsWithoutRef } from 'react';
import remarkGfm from 'remark-gfm'; import remarkGfm from 'remark-gfm';
import type { Components } from "react-markdown";
import { toast } from "hooks/Notification"; import { toast } from "hooks/Notification";
import rehypeRaw from 'rehype-raw';
import remarkEmoji from 'remark-emoji';
// 示例文章数据 // 示例文章数据
const mockPost: PostDisplay = { const mockPost: PostDisplay = {
@ -29,12 +30,19 @@ const mockPost: PostDisplay = {
content: ` content: `
# Markdown # Markdown
Markdown Markdown
## 1. ## 1.
### 1.1 ### 1.1
<pre>
****
**
******
~~线~~
</pre>
**** ****
@ -44,6 +52,27 @@ const mockPost: PostDisplay = {
### 1.2 ### 1.2
<pre>
####
-
- 1
- 2
-
-
####
1.
1. 1
2. 2
2.
3.
####
- [x]
- [ ]
- [x]
</pre>
#### ####
- -
- 1 - 1
@ -65,8 +94,28 @@ const mockPost: PostDisplay = {
### 1.3 ### 1.3
<pre>
\`const greeting = "Hello World";\` \`const greeting = "Hello World";\`
</pre>
\`const greeting = "Hello World";\`
<pre>
\`\`\`typescript
interface User {
id: number;
name: string;
email: string;
}
function greet(user: User): string {
return \`Hello, \${user.name}!\`;
}
\`\`\`
</pre>
\`\`\`typescript \`\`\`typescript
interface User { interface User {
@ -82,6 +131,14 @@ function greet(user: User): string {
### 1.4 ### 1.4
<pre>
| | | |
|:-----|:------:|-------:|
| | | |
| | | |
| | 2 | 5 |
</pre>
| | | | | | | |
|:-----|:------:|-------:| |:-----|:------:|-------:|
| | | | | | | |
@ -92,7 +149,17 @@ function greet(user: User): string {
### 2.1 ### 2.1
#### <pre>
<div class="flex items-center gap-6 my-8">
<img src="https://images.unsplash.com/photo-1516116216624-53e697fedbea?w=400&h=400"
alt="写作工具"
class="w-1/3 rounded-lg shadow-lg" />
<div class="flex-1">
<h4 class="text-xl font-bold mb-2"></h4>
<p>使使</p>
</div>
</div>
</pre>
<div class="flex items-center gap-6 my-8"> <div class="flex items-center gap-6 my-8">
<img src="https://images.unsplash.com/photo-1516116216624-53e697fedbea?w=400&h=400" <img src="https://images.unsplash.com/photo-1516116216624-53e697fedbea?w=400&h=400"
@ -104,20 +171,23 @@ function greet(user: User): string {
</div> </div>
</div> </div>
####
<div class="flex items-center gap-6 my-8">
<div class="flex-1">
<h4 class="text-xl font-bold mb-2"></h4>
<p>使</p>
</div>
<img src="https://images.unsplash.com/photo-1499951360447-b19be8fe80f5?w=400&h=400"
alt="设计工具"
class="w-1/3 rounded-lg shadow-lg" />
</div>
### 2.2 ### 2.2
<pre>
<details class="my-4">
<summary class="cursor-pointer p-4 bg-gray-100 rounded-lg font-medium hover:bg-gray-200 transition-colors">
🎯
</summary>
1. **** - 访
2. **** - Markdown
3. **** -
4. **** -
</details>
</pre>
<details class="my-4"> <details class="my-4">
<summary class="cursor-pointer p-4 bg-gray-100 rounded-lg font-medium hover:bg-gray-200 transition-colors"> <summary class="cursor-pointer p-4 bg-gray-100 rounded-lg font-medium hover:bg-gray-200 transition-colors">
🎯 🎯
@ -133,6 +203,19 @@ function greet(user: User): string {
### 2.3 ### 2.3
<pre>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 my-8">
<div class="p-6 bg-gray-100 rounded-lg">
<h4 class="text-lg font-bold mb-2">🚀 </h4>
<p></p>
</div>
<div class="p-6 bg-gray-100 rounded-lg">
<h4 class="text-lg font-bold mb-2"> </h4>
<p></p>
</div>
</div>
</pre>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 my-8"> <div class="grid grid-cols-1 md:grid-cols-2 gap-6 my-8">
<div class="p-6 bg-gray-100 rounded-lg"> <div class="p-6 bg-gray-100 rounded-lg">
<h4 class="text-lg font-bold mb-2">🚀 </h4> <h4 class="text-lg font-bold mb-2">🚀 </h4>
@ -146,18 +229,36 @@ function greet(user: User): string {
### 2.4 ### 2.4
<pre>
<div class="p-6 bg-blue-50 border-l-4 border-blue-500 rounded-lg my-8">
<h4 class="text-lg font-bold text-blue-700 mb-2">💡 </h4>
<p class="text-blue-600"></p>
</div>
</pre>
<div class="p-6 bg-blue-50 border-l-4 border-blue-500 rounded-lg my-8"> <div class="p-6 bg-blue-50 border-l-4 border-blue-500 rounded-lg my-8">
<h4 class="text-lg font-bold text-blue-700 mb-2">💡 </h4> <h4 class="text-lg font-bold text-blue-700 mb-2">💡 </h4>
<p class="text-blue-600"></p> <p class="text-blue-600"></p>
</div> </div>
<div class="p-6 bg-yellow-50 border-l-4 border-yellow-500 rounded-lg my-8">
<h4 class="text-lg font-bold text-yellow-700 mb-2"> </h4>
<p class="text-yellow-600">使</p>
</div>
### 2.5 线 ### 2.5 线
<pre>
<div class="relative pl-8 my-8 border-l-2 border-gray-200">
<div class="mb-8 relative">
<div class="absolute -left-10 w-4 h-4 bg-blue-500 rounded-full"></div>
<div class="font-bold mb-2">1. </div>
<p></p>
</div>
<div class="mb-8 relative">
<div class="absolute -left-10 w-4 h-4 bg-blue-500 rounded-full"></div>
<div class="font-bold mb-2">2. </div>
<p>广</p>
</div>
</div>
</pre>
<div class="relative pl-8 my-8 border-l-2 border-gray-200"> <div class="relative pl-8 my-8 border-l-2 border-gray-200">
<div class="mb-8 relative"> <div class="mb-8 relative">
<div class="absolute -left-10 w-4 h-4 bg-blue-500 rounded-full"></div> <div class="absolute -left-10 w-4 h-4 bg-blue-500 rounded-full"></div>
@ -168,18 +269,24 @@ function greet(user: User): string {
<div class="mb-8 relative"> <div class="mb-8 relative">
<div class="absolute -left-10 w-4 h-4 bg-blue-500 rounded-full"></div> <div class="absolute -left-10 w-4 h-4 bg-blue-500 rounded-full"></div>
<div class="font-bold mb-2">2. </div> <div class="font-bold mb-2">2. </div>
<p>广</p> <p>广</p>
</div>
<div class="relative">
<div class="absolute -left-10 w-4 h-4 bg-blue-500 rounded-full"></div>
<div class="font-bold mb-2">3. </div>
<p></p>
</div> </div>
</div> </div>
### 2.6 ### 2.6
<pre>
> 📌 ****
>
>
>
> 1.
> 2.
> 3.
>
> * *
</pre>
> 📌 **** > 📌 ****
> >
> >
@ -192,25 +299,25 @@ function greet(user: User): string {
## 3. ## 3.
### 3.1 ### 3.1
$E = mc^2$ <pre>
[^1]
[^1]:
</pre>
$$
\\frac{n!}{k!(n-k)!} = \\binom{n}{k}
$$
### 3.2
[^1] [^1]
[^1]: [^1]:
### 3.3 ### 3.2
:smile: :heart: :thumbsup: :star: :rocket: <pre>
:smile: :heart: :star: :rocket:
</pre>
:smile: :heart: :star: :rocket:
## 4. ## 4.
@ -220,7 +327,7 @@ $$
2. 2.
3. 3.
> 💡 **** Markdown 使 > 💡 **** Markdown
`, `,
authorName: "Markdown 专家", authorName: "Markdown 专家",
publishedAt: new Date("2024-03-15"), publishedAt: new Date("2024-03-15"),
@ -315,19 +422,21 @@ const generateSequentialId = (() => {
} }
const counter = idMap.get(postId)!; const counter = idMap.get(postId)!;
const id = `heading-${postId}-${counter}`; const id = `heading-${counter}`;
idMap.set(postId, counter + 1); idMap.set(postId, counter + 1);
return id; return id;
}; };
})(); })();
export default new Template({}, ({ http, args }) => { export default new Template({}, ({ http, args }) => {
const [toc, setToc] = useState<string[]>([]); const [toc, setToc] = useState<string[]>([]);
const [tocItems, setTocItems] = useState<TocItem[]>([]); const [tocItems, setTocItems] = useState<TocItem[]>([]);
const [activeId, setActiveId] = useState<string>(""); const [activeId, setActiveId] = useState<string>("");
const contentRef = useRef<HTMLDivElement>(null); const contentRef = useRef<HTMLDivElement>(null);
const [showToc, setShowToc] = useState(false); const [showToc, setShowToc] = useState(false);
const [isMounted, setIsMounted] = useState(true); const [isMounted, setIsMounted] = useState(false);
const [headingIdsArrays, setHeadingIdsArrays] = useState<{[key: string]: string[]}>({}); const [headingIdsArrays, setHeadingIdsArrays] = useState<{[key: string]: string[]}>({});
const headingIds = useRef<string[]>([]); // 保持原有的 ref const headingIds = useRef<string[]>([]); // 保持原有的 ref
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
@ -343,7 +452,7 @@ export default new Template({}, ({ http, args }) => {
const md = new MarkdownIt(); const md = new MarkdownIt();
const tocArray: TocItem[] = []; const tocArray: TocItem[] = [];
// 重计数器,传入文章ID // 重计数器,传入文章ID
generateSequentialId(mockPost.id.toString(), true); generateSequentialId(mockPost.id.toString(), true);
let isInCodeBlock = false; let isInCodeBlock = false;
@ -361,7 +470,6 @@ export default new Template({}, ({ http, args }) => {
if (level <= 3 && !isInCodeBlock) { if (level <= 3 && !isInCodeBlock) {
const content = tokens[idx + 1].content; const content = tokens[idx + 1].content;
// 生成ID时传入文章ID
const id = generateSequentialId(mockPost.id.toString()); const id = generateSequentialId(mockPost.id.toString());
token.attrSet('id', id); token.attrSet('id', id);
@ -376,213 +484,293 @@ export default new Template({}, ({ http, args }) => {
md.render(mockPost.content); md.render(mockPost.content);
// 只在 ID 数组发生变化时更新
const newIds = tocArray.map(item => item.id); const newIds = tocArray.map(item => item.id);
headingIds.current = [...newIds]; if (JSON.stringify(headingIds.current) !== JSON.stringify(newIds)) {
setHeadingIdsArrays(prev => ({ headingIds.current = [...newIds];
...prev, setHeadingIdsArrays(prev => ({
[mockPost.id]: [...newIds] ...prev,
})); [mockPost.id]: [...newIds]
}));
}
setToc(newIds); setToc(newIds);
setTocItems(tocArray); setTocItems(tocArray);
if (tocArray.length > 0) { if (tocArray.length > 0 && !activeId) {
setActiveId(tocArray[0].id); setActiveId(tocArray[0].id);
} }
}, [mockPost.content, mockPost.id]); }, [mockPost.content, mockPost.id, activeId]);
const components = useMemo(() => ({ useEffect(() => {
h1: ({ children, ...props }: ComponentPropsWithoutRef<'h1'> & { node?: any }) => { if (headingIdsArrays[mockPost.id] && headingIds.current.length === 0) {
if (headingIdsArrays[mockPost.id] && headingIds.current.length === 0) { headingIds.current = [...headingIdsArrays[mockPost.id]];
headingIds.current = [...headingIdsArrays[mockPost.id]]; }
} }, [headingIdsArrays, mockPost.id]);
const headingId = headingIds.current.shift();
return (
<h1 id={headingId} className="text-xl sm:text-2xl md:text-3xl lg:text-4xl font-bold mt-6 sm:mt-8 mb-3 sm:mb-4" {...props}>
{children}
</h1>
);
},
h2: ({ children, ...props }: ComponentPropsWithoutRef<'h2'> & { node?: any }) => {
if (headingIdsArrays[mockPost.id] && headingIds.current.length === 0) {
headingIds.current = [...headingIdsArrays[mockPost.id]];
}
const headingId = headingIds.current.shift();
return (
<h2 id={headingId} className="text-lg sm:text-xl md:text-2xl lg:text-3xl font-semibold mt-5 sm:mt-6 mb-2 sm:mb-3" {...props}>
{children}
</h2>
);
},
h3: ({ children, ...props }: ComponentPropsWithoutRef<'h3'> & { node?: any }) => {
if (headingIdsArrays[mockPost.id] && headingIds.current.length === 0) {
headingIds.current = [...headingIdsArrays[mockPost.id]];
}
const headingId = headingIds.current.shift();
return (
<h3 id={headingId} className="text-base sm:text-lg md:text-xl lg:text-2xl font-medium mt-4 mb-2" {...props}>
{children}
</h3>
);
},
p: ({ children, ...props }: ComponentPropsWithoutRef<'p'>) => (
<p className="text-sm sm:text-base md:text-lg leading-relaxed mb-3 sm:mb-4 text-[--gray-11]" {...props}>
{children}
</p>
),
ul: ({ children, ...props }: ComponentPropsWithoutRef<'ul'>) => (
<ul className="list-disc pl-4 sm:pl-6 mb-3 sm:mb-4 space-y-1.5 sm:space-y-2 text-[--gray-11]" {...props}>
{children}
</ul>
),
ol: ({ children, ...props }: ComponentPropsWithoutRef<'ol'>) => (
<ol className="list-decimal pl-4 sm:pl-6 mb-3 sm:mb-4 space-y-1.5 sm:space-y-2 text-[--gray-11]" {...props}>
{children}
</ol>
),
li: ({ children, ...props }: ComponentPropsWithoutRef<'li'>) => (
<li className="text-sm sm:text-base md:text-lg leading-relaxed" {...props}>
{children}
</li>
),
blockquote: ({ children, ...props }: ComponentPropsWithoutRef<'blockquote'>) => (
<blockquote className="border-l-4 border-[--gray-6] pl-4 sm:pl-6 py-2 my-3 sm:my-4 text-[--gray-11] italic" {...props}>
{children}
</blockquote>
),
code: ({ inline, className, children, ...props }: ComponentPropsWithoutRef<'code'> & { inline?: boolean }) => {
const match = /language-(\w+)/.exec(className || "");
const lang = match ? match[1].toLowerCase() : "";
return inline ? ( const components = useMemo(() => {
<code className="px-1.5 py-0.5 rounded bg-[--gray-3] text-[--gray-12] text-[0.85em] sm:text-[0.9em]" {...props}> return {
h1: ({ children, node, ...props }: ComponentPropsWithoutRef<'h1'> & { node?: any }) => {
const headingId = headingIds.current.shift();
return (
<h1 id={headingId} className="text-xl sm:text-2xl md:text-3xl lg:text-4xl font-bold mt-6 sm:mt-8 mb-3 sm:mb-4" {...props}>
{children}
</h1>
);
},
h2: ({ children, node, ...props }: ComponentPropsWithoutRef<'h2'> & { node?: any }) => {
const headingId = headingIds.current.shift();
return (
<h2 id={headingId} className="text-lg sm:text-xl md:text-2xl lg:text-3xl font-semibold mt-5 sm:mt-6 mb-2 sm:mb-3" {...props}>
{children}
</h2>
);
},
h3: ({ children, node, ...props }: ComponentPropsWithoutRef<'h3'> & { node?: any }) => {
const headingId = headingIds.current.shift();
return (
<h3 id={headingId} className="text-base sm:text-lg md:text-xl lg:text-2xl font-medium mt-4 mb-2" {...props}>
{children}
</h3>
);
},
p: ({ children, node, ...props }: ComponentPropsWithoutRef<'p'> & { node?: any }) => (
<p className="text-sm sm:text-base md:text-lg leading-relaxed mb-3 sm:mb-4 text-[--gray-11]" {...props}>
{children} {children}
</code> </p>
) : ( ),
<div className="my-4 sm:my-6"> ul: ({ children, node, ...props }: ComponentPropsWithoutRef<'ul'> & { node?: any }) => (
<div className="flex justify-between items-center h-9 sm:h-10 px-4 sm:px-6 <ul className="list-disc pl-4 sm:pl-6 mb-3 sm:mb-4 space-y-1.5 sm:space-y-2 text-[--gray-11]" {...props}>
border-t border-x border-[--gray-6] {children}
bg-[--gray-3] dark:bg-[--gray-3] </ul>
rounded-t-lg ),
mx-0" ol: ({ children, node, ...props }: ComponentPropsWithoutRef<'ol'> & { node?: any }) => (
> <ol className="list-decimal pl-4 sm:pl-6 mb-3 sm:mb-4 space-y-1.5 sm:space-y-2 text-[--gray-11]" {...props}>
<div className="text-sm text-[--gray-12] dark:text-[--gray-12] font-medium">{lang || "text"}</div> {children}
<CopyButton code={String(children)} /> </ol>
),
li: ({ children, node, ...props }: ComponentPropsWithoutRef<'li'> & { node?: any }) => (
<li className="text-sm sm:text-base md:text-lg leading-relaxed" {...props}>
{children}
</li>
),
blockquote: ({ children, node, ...props }: ComponentPropsWithoutRef<'blockquote'> & { node?: any }) => (
<blockquote className="border-l-4 border-[--gray-6] pl-4 sm:pl-6 py-2 my-3 sm:my-4 text-[--gray-11] italic" {...props}>
{children}
</blockquote>
),
code: ({ inline, className, children, node, ...props }: ComponentPropsWithoutRef<'code'> & {
inline?: boolean,
node?: any
}) => {
// 使用多个条件来确保服务端和客户端渲染一致
const isInPre = Boolean(
className?.includes('language-')
);
// 如果是行内代码(不在 pre 标签内),使用行内样式
if (!isInPre) {
return (
<code
className="px-2 py-1 rounded-md bg-[--gray-4] text-[--accent-11] font-medium text-[0.85em] sm:text-[0.9em]"
{...props}
>
{children}
</code>
);
}
// 以下是代码块的处理逻辑
const match = /language-(\w+)/.exec(className || "");
const lang = match ? match[1].toLowerCase() : "";
return (
<div className="my-4 sm:my-6">
<div className="flex justify-between items-center h-9 sm:h-10 px-4 sm:px-6
border-t border-x border-[--gray-6]
bg-[--gray-3] dark:bg-[--gray-3]
rounded-t-lg
mx-0"
>
<div className="text-sm text-[--gray-12] dark:text-[--gray-12] font-medium">{lang || "text"}</div>
<CopyButton code={String(children)} />
</div>
<div className="border border-[--gray-6] rounded-b-lg bg-white dark:bg-[--gray-1] mx-0">
<div className="overflow-x-auto">
<div className="p-4 sm:p-6">
<SyntaxHighlighter
language={lang || "text"}
style={{
...oneLight,
'punctuation': {
color: 'var(--gray-12)'
},
'operator': {
color: 'var(--gray-12)'
},
'symbol': {
color: 'var(--gray-12)'
}
}}
customStyle={{
margin: 0,
padding: 0,
background: "none",
fontSize: "0.9rem",
lineHeight: 1.6,
}}
codeTagProps={{
className: "dark:text-[--gray-12]",
style: {
color: "inherit"
}
}}
>
{String(children).replace(/\n$/, "")}
</SyntaxHighlighter>
</div>
</div>
</div>
</div> </div>
);
<div className="border border-[--gray-6] rounded-b-lg bg-white dark:bg-[--gray-1] mx-0"> },
<div className="overflow-x-auto"> // 修改表格相关组件的响应式设计
<div className="p-4 sm:p-6"> table: ({ children, ...props }: ComponentPropsWithoutRef<'table'>) => (
<SyntaxHighlighter <div className="w-full my-4 sm:my-6 -mx-4 sm:mx-0 overflow-hidden">
language={lang || "text"} <div className="scroll-container overflow-x-auto">
style={{ <div className="min-w-[640px] sm:min-w-0">
...oneLight, <div className="border border-[--gray-6] rounded-lg bg-white dark:bg-[--gray-1]">
'punctuation': { <table className="w-full border-collapse text-xs sm:text-sm" {...props}>
color: 'var(--gray-12)' {children}
}, </table>
'operator': {
color: 'var(--gray-12)'
},
'symbol': {
color: 'var(--gray-12)'
}
}}
customStyle={{
margin: 0,
padding: 0,
background: "none",
fontSize: "0.9rem",
lineHeight: 1.6,
}}
codeTagProps={{
className: "dark:text-[--gray-12]",
style: {
color: "inherit"
}
}}
>
{String(children).replace(/\n$/, "")}
</SyntaxHighlighter>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
); ),
},
// 修改表格相关组件的响应式设计
table: ({ children, ...props }: ComponentPropsWithoutRef<'table'>) => (
<div className="w-full my-4 sm:my-6 -mx-4 sm:mx-0 overflow-hidden">
<div className="scroll-container overflow-x-auto">
<div className="min-w-[640px] sm:min-w-0">
<div className="border border-[--gray-6] rounded-lg bg-white dark:bg-[--gray-1]">
<table className="w-full border-collapse text-xs sm:text-sm" {...props}>
{children}
</table>
</div>
</div>
</div>
</div>
),
th: ({ children, ...props }: ComponentPropsWithoutRef<'th'>) => ( th: ({ children, ...props }: ComponentPropsWithoutRef<'th'>) => (
<th <th
className="px-4 sm:px-4 md:px-6 py-2 sm:py-3 text-left text-[10px] sm:text-xs font-medium uppercase tracking-wider className="px-4 sm:px-4 md:px-6 py-2 sm:py-3 text-left text-[10px] sm:text-xs font-medium uppercase tracking-wider
text-[--gray-12] break-words hyphens-auto text-[--gray-12] break-words hyphens-auto
bg-[--gray-3] dark:bg-[--gray-3] bg-[--gray-3] dark:bg-[--gray-3]
first:rounded-tl-lg last:rounded-tr-lg first:rounded-tl-lg last:rounded-tr-lg
border-b border-[--gray-6]" border-b border-[--gray-6]"
{...props} {...props}
> >
{children} {children}
</th> </th>
), ),
td: ({ children, ...props }: ComponentPropsWithoutRef<'td'>) => ( td: ({ children, ...props }: ComponentPropsWithoutRef<'td'>) => (
<td <td
className="px-4 sm:px-4 md:px-6 py-2 sm:py-3 md:py-4 text-xs sm:text-sm text-[--gray-11] break-words hyphens-auto className="px-4 sm:px-4 md:px-6 py-2 sm:py-3 md:py-4 text-xs sm:text-sm text-[--gray-11] break-words hyphens-auto
[&:first-child]:font-medium [&:first-child]:text-[--gray-12]" [&:first-child]:font-medium [&:first-child]:text-[--gray-12]"
{...props} {...props}
> >
{children} {children}
</td> </td>
), ),
}), [mockPost.id, headingIdsArrays]); // 修改 details 组件
details: ({ children, ...props }: ComponentPropsWithoutRef<'details'>) => (
<details
className="my-4 rounded-lg border border-[--gray-6] bg-[--gray-2] overflow-hidden
marker:text-[--gray-11] [&[open]]:bg-[--gray-1]"
{...props}
>
{children}
</details>
),
// 修改 summary 组件
summary: ({ children, ...props }: ComponentPropsWithoutRef<'summary'>) => (
<summary
className="px-4 py-3 cursor-pointer hover:bg-[--gray-3] transition-colors
text-[--gray-12] font-medium select-none
marker:text-[--gray-11]"
{...props}
>
{children}
</summary>
),
pre: ({ children, ...props }: ComponentPropsWithoutRef<'pre'>) => {
// 添加调试日志
console.log('Pre Component Props:', props);
console.log('Pre Component Children:', children);
// 检查children的具体结构
if (Array.isArray(children)) {
children.forEach((child, index) => {
console.log(`Child ${index}:`, child);
console.log(`Child ${index} props:`, (child as any)?.props);
});
}
const content = (children as any)?.[0]?.props?.children || '';
console.log('Extracted content:', content);
return (
<pre
className="my-4 p-4 bg-[--gray-3] rounded-lg overflow-x-auto text-sm
border border-[--gray-6] text-[--gray-12]"
{...props}
>
{/* 直接输出原始内容,不经过 markdown 解析 */}
{typeof content === 'string' ? content : children}
</pre>
);
},
};
}, []);
// 修改滚动监听逻辑 // 修改滚动监听逻辑
useEffect(() => { useEffect(() => {
if (typeof window === 'undefined') return; if (typeof window === 'undefined') return;
let scrollTimeout: NodeJS.Timeout;
const observer = new IntersectionObserver( const observer = new IntersectionObserver(
(entries) => { (entries) => {
if (!isMounted) return; // 如果是点击触发的滚动,不处理高亮更新
if (!isMounted || isClickScrolling.current) return;
const container = document.querySelector("#main-content"); // 清除之前的定时器
const contentBox = document.querySelector(".prose"); clearTimeout(scrollTimeout);
if (!container || !contentBox) return; // 添加防抖,等待滚动结束后再更新高亮
scrollTimeout = setTimeout(() => {
const visibleEntries = entries.filter(entry => entry.isIntersecting);
// 找出所有进入可视区域的标题 if (visibleEntries.length > 0) {
const intersectingEntries = entries.filter(entry => entry.isIntersecting); const visibleHeadings = visibleEntries
.map(entry => ({
id: entry.target.id,
top: entry.boundingClientRect.top,
y: entry.intersectionRatio
}))
.sort((a, b) => {
if (Math.abs(a.y - b.y) < 0.1) {
return a.top - b.top;
}
return b.y - a.y;
});
if (intersectingEntries.length > 0) { const mostVisible = visibleHeadings[0];
// 获取所有可见标题的位置信息
const visibleHeadings = intersectingEntries.map(entry => ({
id: entry.target.id,
top: entry.boundingClientRect.top
}));
// 选择靠近视口顶部的标题 setActiveId(currentActiveId => {
const closestHeading = visibleHeadings.reduce((prev, current) => { if (mostVisible.id !== currentActiveId) {
return Math.abs(current.top) < Math.abs(prev.top) ? current : prev; return mostVisible.id;
}); }
return currentActiveId;
setActiveId(closestHeading.id); });
} }
}, 100); // 100ms 的防抖延迟
}, },
{ {
root: document.querySelector("#main-content"), root: document.querySelector("#main-content"),
rootMargin: '-20px 0px -80% 0px', rootMargin: '-10% 0px -70% 0px',
threshold: [0, 1] threshold: [0, 0.25, 0.5, 0.75, 1]
} }
); );
@ -596,6 +784,7 @@ export default new Template({}, ({ http, args }) => {
} }
return () => { return () => {
clearTimeout(scrollTimeout);
if (isMounted) { if (isMounted) {
tocItems.forEach((item) => { tocItems.forEach((item) => {
const element = document.getElementById(item.id); const element = document.getElementById(item.id);
@ -610,36 +799,38 @@ export default new Template({}, ({ http, args }) => {
// 修改点击处理函数 // 修改点击处理函数
const handleTocClick = useCallback((e: React.MouseEvent, itemId: string) => { const handleTocClick = useCallback((e: React.MouseEvent, itemId: string) => {
e.preventDefault(); e.preventDefault();
const element = document.getElementById(itemId); const element = document.getElementById(itemId);
const container = document.querySelector("#main-content"); const container = document.querySelector("#main-content");
const contentBox = document.querySelector(".prose"); // 获取实际内容容器 const contentBox = document.querySelector(".prose");
if (element && container && contentBox) { if (element && container && contentBox) {
// 设置点击滚动标志
isClickScrolling.current = true; isClickScrolling.current = true;
// 计算元素相对于内容容器的偏移量 // 立即更新高亮,不等待滚动
setActiveId(itemId);
// 计算滚动位置
const elementRect = element.getBoundingClientRect(); const elementRect = element.getBoundingClientRect();
const contentBoxRect = contentBox.getBoundingClientRect(); const contentBoxRect = contentBox.getBoundingClientRect();
const containerRect = container.getBoundingClientRect(); const containerRect = container.getBoundingClientRect();
// 计算元素相对于内容容器的偏移量
const relativeTop = elementRect.top - contentBoxRect.top; const relativeTop = elementRect.top - contentBoxRect.top;
// 计算内容容器相对于滚动容器的偏移量
const contentOffset = contentBoxRect.top - containerRect.top; const contentOffset = contentBoxRect.top - containerRect.top;
// 计算最终滚动距离
const scrollDistance = container.scrollTop + relativeTop + contentOffset; const scrollDistance = container.scrollTop + relativeTop + contentOffset;
// 执行滚动
container.scrollTo({ container.scrollTo({
top: scrollDistance, top: scrollDistance,
behavior: "smooth", behavior: "smooth",
}); });
// 滚动完成后重置标记 // 延迟重置 isClickScrolling 标志
// 增加延迟时间,确保滚动完全结束
const resetTimeout = setTimeout(() => { const resetTimeout = setTimeout(() => {
isClickScrolling.current = false; isClickScrolling.current = false;
}, 100); }, 1500); // 增加到 1.5 秒
return () => clearTimeout(resetTimeout); return () => clearTimeout(resetTimeout);
} }
@ -733,6 +924,37 @@ export default new Template({}, ({ http, args }) => {
</> </>
); );
// 在组件顶部添加 useMemo 包裹静态内容
const PostContent = useMemo(() => {
// 在渲染内容前重置 headingIds
if (headingIdsArrays[mockPost.id]) {
headingIds.current = [...headingIdsArrays[mockPost.id]];
}
return (
<Box className="prose dark:prose-invert max-w-none
[&_pre]:!bg-transparent [&_pre]:!p-0 [&_pre]:!m-0 [&_pre]:!border-0
[&_.prism-code]:!bg-transparent [&_.prism-code]:!shadow-none
[&_pre_.prism-code]:!bg-transparent [&_pre_.prism-code]:!shadow-none
[&_pre_code]:!bg-transparent [&_pre_code]:!shadow-none
[&_table]:!m-0
[&_:not(pre)>code]:![&::before]:hidden [&_:not(pre)>code]:![&::after]:hidden
[&_:not(pre)>code]:[&::before]:content-none [&_:not(pre)>code]:[&::after]:content-none
[&_:not(pre)>code]:!bg-[--gray-4] [&_:not(pre)>code]:!text-[--accent-11]
">
<div ref={contentRef}>
<ReactMarkdown
components={components}
remarkPlugins={[remarkGfm, remarkEmoji]}
rehypePlugins={[rehypeRaw]}
>
{mockPost.content}
</ReactMarkdown>
</div>
</Box>
);
}, [mockPost.content, components, mockPost.id, headingIdsArrays]); // 添加必要的依赖
return ( return (
<Container <Container
ref={containerRef} ref={containerRef}
@ -745,8 +967,8 @@ export default new Template({}, ({ http, args }) => {
className="relative flex-col lg:flex-row" className="relative flex-col lg:flex-row"
gap={{initial: "4", lg: "8"}} gap={{initial: "4", lg: "8"}}
> >
{/* 文章主体 */} {/* 文章主体 - 调整宽度计算 */}
<Box className="w-full lg:flex-1"> <Box className="w-full lg:w-[calc(100%-12rem)] xl:w-[calc(100%-13rem)]">
<Box className="p-4 sm:p-6 md:p-8"> <Box className="p-4 sm:p-6 md:p-8">
{/* 头部 */} {/* 头部 */}
<Box className="mb-4 sm:mb-8"> <Box className="mb-4 sm:mb-8">
@ -849,28 +1071,13 @@ export default new Template({}, ({ http, args }) => {
</Box> </Box>
)} )}
{/* 内容区域 */} {/* 内容区域使用记忆化的组件 */}
<Box className="prose dark:prose-invert max-w-none {PostContent}
[&_pre]:!bg-transparent [&_pre]:!p-0 [&_pre]:!m-0 [&_pre]:!border-0
[&_.prism-code]:!bg-transparent [&_.prism-code]:!shadow-none
[&_pre_.prism-code]:!bg-transparent [&_pre_.prism-code]:!shadow-none
[&_code]:!bg-transparent [&_code]:!shadow-none
[&_table]:!m-0
">
<div ref={contentRef}>
<ReactMarkdown
components={components}
remarkPlugins={[remarkGfm]}
>
{mockPost.content}
</ReactMarkdown>
</div>
</Box>
</Box> </Box>
</Box> </Box>
{/* 侧边目录 */} {/* 侧边目录 - 减小宽度 */}
<Box className="hidden lg:block w-48 xl:w-56 relative"> <Box className="hidden lg:block w-40 xl:w-44 flex-shrink-0">
<Box className="sticky top-8"> <Box className="sticky top-8">
<Text <Text
size="2" size="2"