前端:新增对Markdown解析的支持,优化文章内容的排版和样式,添加emoji支持。
This commit is contained in:
parent
14f50467f0
commit
2ffa4883ae
@ -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";
|
||||||
|
@ -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"
|
||||||
|
@ -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>
|
||||||
);
|
),
|
||||||
},
|
|
||||||
// 修改表格相关组件的响应式设计
|
th: ({ children, ...props }: ComponentPropsWithoutRef<'th'>) => (
|
||||||
table: ({ children, ...props }: ComponentPropsWithoutRef<'table'>) => (
|
<th
|
||||||
<div className="w-full my-4 sm:my-6 -mx-4 sm:mx-0 overflow-hidden">
|
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
|
||||||
<div className="scroll-container overflow-x-auto">
|
text-[--gray-12] break-words hyphens-auto
|
||||||
<div className="min-w-[640px] sm:min-w-0">
|
bg-[--gray-3] dark:bg-[--gray-3]
|
||||||
<div className="border border-[--gray-6] rounded-lg bg-white dark:bg-[--gray-1]">
|
first:rounded-tl-lg last:rounded-tr-lg
|
||||||
<table className="w-full border-collapse text-xs sm:text-sm" {...props}>
|
border-b border-[--gray-6]"
|
||||||
{children}
|
{...props}
|
||||||
</table>
|
>
|
||||||
</div>
|
{children}
|
||||||
</div>
|
</th>
|
||||||
</div>
|
),
|
||||||
</div>
|
|
||||||
),
|
td: ({ children, ...props }: ComponentPropsWithoutRef<'td'>) => (
|
||||||
|
<td
|
||||||
th: ({ children, ...props }: ComponentPropsWithoutRef<'th'>) => (
|
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
|
||||||
<th
|
[&:first-child]:font-medium [&:first-child]:text-[--gray-12]"
|
||||||
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
|
{...props}
|
||||||
text-[--gray-12] break-words hyphens-auto
|
>
|
||||||
bg-[--gray-3] dark:bg-[--gray-3]
|
{children}
|
||||||
first:rounded-tl-lg last:rounded-tr-lg
|
</td>
|
||||||
border-b border-[--gray-6]"
|
),
|
||||||
{...props}
|
// 修改 details 组件
|
||||||
>
|
details: ({ children, ...props }: ComponentPropsWithoutRef<'details'>) => (
|
||||||
{children}
|
<details
|
||||||
</th>
|
className="my-4 rounded-lg border border-[--gray-6] bg-[--gray-2] overflow-hidden
|
||||||
),
|
marker:text-[--gray-11] [&[open]]:bg-[--gray-1]"
|
||||||
|
{...props}
|
||||||
td: ({ children, ...props }: ComponentPropsWithoutRef<'td'>) => (
|
>
|
||||||
<td
|
{children}
|
||||||
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
|
</details>
|
||||||
[&:first-child]:font-medium [&:first-child]:text-[--gray-12]"
|
),
|
||||||
{...props}
|
|
||||||
>
|
// 修改 summary 组件
|
||||||
{children}
|
summary: ({ children, ...props }: ComponentPropsWithoutRef<'summary'>) => (
|
||||||
</td>
|
<summary
|
||||||
),
|
className="px-4 py-3 cursor-pointer hover:bg-[--gray-3] transition-colors
|
||||||
}), [mockPost.id, headingIdsArrays]);
|
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");
|
|
||||||
|
|
||||||
if (!container || !contentBox) return;
|
|
||||||
|
|
||||||
// 找出所有进入可视区域的标题
|
// 清除之前的定时器
|
||||||
const intersectingEntries = entries.filter(entry => entry.isIntersecting);
|
clearTimeout(scrollTimeout);
|
||||||
|
|
||||||
if (intersectingEntries.length > 0) {
|
// 添加防抖,等待滚动结束后再更新高亮
|
||||||
// 获取所有可见标题的位置信息
|
scrollTimeout = setTimeout(() => {
|
||||||
const visibleHeadings = intersectingEntries.map(entry => ({
|
const visibleEntries = entries.filter(entry => entry.isIntersecting);
|
||||||
id: entry.target.id,
|
|
||||||
top: entry.boundingClientRect.top
|
|
||||||
}));
|
|
||||||
|
|
||||||
// 选择靠近视口顶部的标题
|
if (visibleEntries.length > 0) {
|
||||||
const closestHeading = visibleHeadings.reduce((prev, current) => {
|
const visibleHeadings = visibleEntries
|
||||||
return Math.abs(current.top) < Math.abs(prev.top) ? current : prev;
|
.map(entry => ({
|
||||||
});
|
id: entry.target.id,
|
||||||
|
top: entry.boundingClientRect.top,
|
||||||
setActiveId(closestHeading.id);
|
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;
|
||||||
|
});
|
||||||
|
|
||||||
|
const mostVisible = visibleHeadings[0];
|
||||||
|
|
||||||
|
setActiveId(currentActiveId => {
|
||||||
|
if (mostVisible.id !== currentActiveId) {
|
||||||
|
return mostVisible.id;
|
||||||
|
}
|
||||||
|
return currentActiveId;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, 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"
|
||||||
|
Loading…
Reference in New Issue
Block a user