react-markdown-typer 1.0.3 → 1.0.4-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +238 -729
- package/README.zh.md +238 -699
- package/es/MarkdownTyperCMD/index.d.ts.map +1 -1
- package/es/MarkdownTyperCMD/index.js +75 -23
- package/es/MarkdownTyperCMD/index.js.map +1 -1
- package/es/components/CursorSpan.d.ts +14 -0
- package/es/components/CursorSpan.d.ts.map +1 -0
- package/es/components/CursorSpan.js +19 -0
- package/es/components/CursorSpan.js.map +1 -0
- package/es/constant.d.ts +1 -1
- package/es/constant.d.ts.map +1 -1
- package/es/constant.js +1 -1
- package/es/constant.js.map +1 -1
- package/es/defined.d.ts +30 -26
- package/es/defined.d.ts.map +1 -1
- package/es/hooks/useTypingTask.d.ts +3 -1
- package/es/hooks/useTypingTask.d.ts.map +1 -1
- package/es/hooks/useTypingTask.js +46 -45
- package/es/hooks/useTypingTask.js.map +1 -1
- package/es/index.d.ts +2 -1
- package/es/index.d.ts.map +1 -1
- package/es/index.js +2 -1
- package/es/index.js.map +1 -1
- package/es/plugins/rehypeCursor.d.ts +8 -0
- package/es/plugins/rehypeCursor.d.ts.map +1 -0
- package/es/plugins/rehypeCursor.js +54 -0
- package/es/plugins/rehypeCursor.js.map +1 -0
- package/package.json +2 -2
package/README.zh.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
**如果您需要带有样式,支持数据公式、mermaid图表渲染,推荐您用 [ds-markdown](https://github.com/onshinpei/ds-markdown)**
|
|
6
6
|
|
|
7
|
-
**🇨🇳 中文 | [🇺🇸 English](./README.md)
|
|
7
|
+
**🇨🇳 中文 | [🇺🇸 English](./README.md)**
|
|
8
8
|
|
|
9
9
|
一个专为现代 AI 应用设计的 React 组件,提供流畅的实时打字动画和完整的 Markdown 渲染能力。
|
|
10
10
|
|
|
@@ -14,825 +14,364 @@
|
|
|
14
14
|
[](https://react.dev)
|
|
15
15
|
[](https://www.typescriptlang.org/)
|
|
16
16
|
|
|
17
|
-
[📖 在线演示](https://onshinpei.github.io/react-markdown-typer/)
|
|
18
|
-
|
|
19
|
-
[DEMO:🔧 StackBlitz 体验](https://stackblitz.com/edit/vitejs-vite-ndgqzcbp?file=README.md)
|
|
17
|
+
[📖 在线演示](https://onshinpei.github.io/react-markdown-typer/) | [🔧 StackBlitz 体验](https://stackblitz.com/edit/vitejs-vite-ndgqzcbp?file=README.md)
|
|
20
18
|
|
|
21
19
|
---
|
|
22
20
|
|
|
23
|
-
##
|
|
24
|
-
|
|
25
|
-
- **后端流式数据完美适配**
|
|
26
|
-
很多 AI/LLM 后端接口(如 OpenAI、DeepSeek 等)推送的数据 chunk 往往一次包含多个字符,普通打字机实现会出现卡顿、跳字等问题。
|
|
27
|
-
**react-markdown-typer 会自动将每个 chunk 拆分为单个字符,逐字流畅渲染动画,无论后端一次推送多少字,都能保证每个字都流畅打字。**
|
|
21
|
+
## 为什么选择 react-markdown-typer?
|
|
28
22
|
|
|
29
|
-
|
|
30
|
-
丰富的命令式 API,支持流式数据、异步回调、插件扩展,开发者可灵活控制动画和内容。
|
|
23
|
+
### 专为 AI 应用优化
|
|
31
24
|
|
|
32
|
-
|
|
33
|
-
体积小、性能优,适配移动端和桌面端。核心依赖 [react-markdown](https://github.com/remarkjs/react-markdown)(业界主流、成熟的 Markdown 渲染库),无其它重量级依赖,开箱即用。
|
|
25
|
+
普通打字机遇到 AI 流式数据会卡顿?我们不会。**自动将每个 chunk 拆分为字符**,无论后端一次推送多少,都能逐字流畅渲染。
|
|
34
26
|
|
|
35
|
-
|
|
36
|
-
兼容 [react-markdown](https://github.com/remarkjs/react-markdown) remark/rehype 插件,满足个性化和高级扩展需求。
|
|
27
|
+
### 轻量但强大
|
|
37
28
|
|
|
38
|
-
-
|
|
39
|
-
|
|
40
|
-
- 实时问答/知识库
|
|
41
|
-
- 教育/数学/编程内容展示
|
|
42
|
-
- 产品演示、交互式文档
|
|
43
|
-
- 任何需要"打字机"动画和流式 Markdown 渲染的场景
|
|
29
|
+
- 基于业界标准 [react-markdown](https://github.com/remarkjs/react-markdown)
|
|
30
|
+
- 零额外依赖,开箱即用
|
|
44
31
|
|
|
45
|
-
|
|
32
|
+
### 完整的打字控制
|
|
46
33
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
- [✨ 核心特性](#-核心特性)
|
|
50
|
-
- [📦 快速安装](#-快速安装)
|
|
51
|
-
- [🚀 5分钟上手](#-5分钟上手)
|
|
52
|
-
- [基础用法](#基础用法)
|
|
53
|
-
- [禁用打字动画](#禁用打字动画)
|
|
54
|
-
- [数学公式支持](#数学公式支持)
|
|
55
|
-
- [AI 对话场景](#ai-对话场景)
|
|
56
|
-
- [🎯 高级回调控制](#-高级回调控制)
|
|
57
|
-
- [🔄 重新开始动画演示](#-重新开始动画演示)
|
|
58
|
-
- [▶️ 手动开始动画演示](#️-手动开始动画演示)
|
|
59
|
-
- [📚 完整 API 文档](#-完整-api-文档)
|
|
60
|
-
- [🧮 数学公式使用指南](#-数学公式使用指南)
|
|
61
|
-
- [🔌 插件系统](#-插件系统)
|
|
62
|
-
- [🎛️ 定时器模式详解](#️-定时器模式详解)
|
|
63
|
-
- [💡 实战示例](#-实战示例)
|
|
64
|
-
|
|
65
|
-
---
|
|
34
|
+
不只是播放动画,还能 **暂停、继续、重新开始、清空**。完全的命令式 API,让你掌控一切。
|
|
66
35
|
|
|
67
|
-
|
|
36
|
+
### 插件生态兼容
|
|
68
37
|
|
|
69
|
-
|
|
38
|
+
兼容整个 remark/rehype 插件生态,轻松扩展功能。支持代码高亮、数学公式、表格、自定义光标等丰富功能。
|
|
70
39
|
|
|
71
|
-
|
|
72
|
-
- 支持思考过程 (`thinking`) 和回答内容 (`answer`) 双模式
|
|
73
|
-
- 流式数据完美适配,零延迟响应用户输入
|
|
40
|
+
### 生产就绪
|
|
74
41
|
|
|
75
|
-
|
|
42
|
+
- TypeScript 原生支持
|
|
43
|
+
- 完整的类型定义
|
|
76
44
|
|
|
77
|
-
|
|
78
|
-
- 数学公式渲染 (KaTeX),支持 `$...$` 和 `\[...\]` 语法
|
|
79
|
-
- 支持亮色/暗色主题,适配不同产品风格
|
|
80
|
-
- 插件化架构,支持 remark/rehype 插件扩展
|
|
45
|
+
**适用场景**
|
|
81
46
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
- 支持打字中断 `stop` 和继续 `resume`
|
|
85
|
-
- 支持打字关闭与开启
|
|
86
|
-
|
|
87
|
-
### 🎬 **流畅动画**
|
|
88
|
-
|
|
89
|
-
- 双模式定时器优化,支持`requestAnimationFrame`和`setTimeout`模式
|
|
90
|
-
- 高频打字支持(`requestAnimationFrame`模式下打字间隔最低可接近于`0ms`)
|
|
91
|
-
- 帧同步渲染,与浏览器刷新完美配合
|
|
92
|
-
- 智能字符批量处理,视觉效果更自然
|
|
47
|
+
AI 聊天助手 · 实时问答系统 · 在线教育平台 · 产品演示 · 交互式文档 · 知识库展示
|
|
93
48
|
|
|
94
49
|
---
|
|
95
50
|
|
|
96
51
|
## 📦 快速安装
|
|
97
52
|
|
|
98
53
|
```bash
|
|
99
|
-
# npm
|
|
100
54
|
npm install react-markdown-typer
|
|
101
|
-
|
|
102
|
-
# yarn
|
|
103
|
-
yarn add react-markdown-typer
|
|
104
|
-
|
|
105
|
-
# pnpm
|
|
106
|
-
pnpm add react-markdown-typer
|
|
107
|
-
```
|
|
108
|
-
|
|
109
|
-
### 通过 ESM CDN 使用
|
|
110
|
-
|
|
111
|
-
无需安装,直接在浏览器中使用:
|
|
112
|
-
|
|
113
|
-
<!-- [DEMO](https://stackblitz.com/edit/stackblitz-starters-7vcclcw7?file=index.html) -->
|
|
114
|
-
|
|
115
|
-
```html
|
|
116
|
-
<!-- 导入组件 -->
|
|
117
|
-
<script type="module">
|
|
118
|
-
import Markdown from 'https://esm.sh/react-markdown-typer';
|
|
119
|
-
</script>
|
|
120
55
|
```
|
|
121
56
|
|
|
122
|
-
## 🚀
|
|
57
|
+
## 🚀 快速开始
|
|
123
58
|
|
|
124
59
|
### 基础用法
|
|
125
60
|
|
|
126
|
-
[DEMO](https://stackblitz.com/edit/vitejs-vite-ndgqzcbp?file=src%2FApp.tsx)
|
|
127
|
-
|
|
128
61
|
```tsx
|
|
129
62
|
import MarkdownTyper from 'react-markdown-typer';
|
|
130
63
|
|
|
131
64
|
function App() {
|
|
132
|
-
return <MarkdownTyper interval={20}># Hello react-markdown-typer 这是一个**高性能**的打字动画组件! ## 特性 - ⚡ 零延迟流式处理 - 🎬 流畅打字动画 - 🎯 完美语法支持</MarkdownTyper>;
|
|
133
|
-
}
|
|
134
|
-
```
|
|
135
|
-
|
|
136
|
-
### 禁用打字动画
|
|
137
|
-
|
|
138
|
-
```tsx
|
|
139
|
-
import MarkdownTyper from 'react-markdown-typer';
|
|
140
|
-
|
|
141
|
-
function StaticDemo() {
|
|
142
|
-
const [disableTyping, setDisableTyping] = useState(false);
|
|
143
|
-
|
|
144
65
|
return (
|
|
145
|
-
<
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
66
|
+
<MarkdownTyper interval={20}>
|
|
67
|
+
# Hello World
|
|
68
|
+
|
|
69
|
+
这是一个**高性能**的打字动画组件!
|
|
70
|
+
|
|
71
|
+
- ⚡ 流畅渲染
|
|
72
|
+
- 🎯 完美语法支持
|
|
73
|
+
</MarkdownTyper>
|
|
152
74
|
);
|
|
153
75
|
}
|
|
154
76
|
```
|
|
155
77
|
|
|
156
|
-
###
|
|
78
|
+
### AI 流式对话
|
|
157
79
|
|
|
158
80
|
```tsx
|
|
159
|
-
import
|
|
81
|
+
import { useRef, useEffect } from 'react';
|
|
82
|
+
import { MarkdownTyperCMD, MarkdownTyperCMDRef } from 'react-markdown-typer';
|
|
160
83
|
|
|
161
|
-
function
|
|
162
|
-
const
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
84
|
+
function ChatDemo() {
|
|
85
|
+
const cmdRef = useRef<MarkdownTyperCMDRef>(null);
|
|
86
|
+
|
|
87
|
+
useEffect(() => {
|
|
88
|
+
// 模拟流式数据
|
|
89
|
+
async function simulateStreaming() {
|
|
90
|
+
const chunks = ['# AI 回答\n\n', '这是', '一个', '流式', '响应'];
|
|
91
|
+
|
|
92
|
+
for (const chunk of chunks) {
|
|
93
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
94
|
+
cmdRef.current?.push(chunk);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
simulateStreaming();
|
|
99
|
+
}, []);
|
|
169
100
|
|
|
170
101
|
return (
|
|
171
|
-
<
|
|
172
|
-
|
|
173
|
-
|
|
102
|
+
<MarkdownTyperCMD
|
|
103
|
+
ref={cmdRef}
|
|
104
|
+
interval={30}
|
|
105
|
+
/>
|
|
174
106
|
);
|
|
175
107
|
}
|
|
176
108
|
```
|
|
177
109
|
|
|
178
|
-
###
|
|
110
|
+
### 光标效果
|
|
179
111
|
|
|
180
112
|
```tsx
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
{answer && <MarkdownTyper interval={15}>{answer}</MarkdownTyper>}
|
|
202
|
-
</div>
|
|
203
|
-
);
|
|
204
|
-
}
|
|
113
|
+
// 字符串光标
|
|
114
|
+
<MarkdownTyperCMD
|
|
115
|
+
ref={cmdRef}
|
|
116
|
+
showCursor={true}
|
|
117
|
+
cursor="|"
|
|
118
|
+
interval={50}
|
|
119
|
+
/>
|
|
120
|
+
|
|
121
|
+
// 自定义 ReactNode 光标
|
|
122
|
+
<MarkdownTyperCMD
|
|
123
|
+
ref={cmdRef}
|
|
124
|
+
showCursor={true}
|
|
125
|
+
cursor={
|
|
126
|
+
<span style={{
|
|
127
|
+
color: '#007acc',
|
|
128
|
+
animation: 'blink 1s infinite'
|
|
129
|
+
}}>|</span>
|
|
130
|
+
}
|
|
131
|
+
interval={50}
|
|
132
|
+
/>
|
|
205
133
|
```
|
|
206
134
|
|
|
207
|
-
###
|
|
135
|
+
### 控制动画
|
|
208
136
|
|
|
209
137
|
```tsx
|
|
210
|
-
|
|
211
|
-
import { MarkdownCMD, MarkdownTyperCMDRef } from 'react-markdown-typer';
|
|
138
|
+
const cmdRef = useRef<MarkdownTyperCMDRef>(null);
|
|
212
139
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
140
|
+
// 控制方法
|
|
141
|
+
cmdRef.current?.stop(); // 暂停
|
|
142
|
+
cmdRef.current?.resume(); // 继续
|
|
143
|
+
cmdRef.current?.restart(); // 重新开始
|
|
144
|
+
cmdRef.current?.clear(); // 清除
|
|
145
|
+
```
|
|
216
146
|
|
|
217
|
-
|
|
218
|
-
// 在字符打字前进行异步操作
|
|
219
|
-
console.log('即将打字:', data.currentChar);
|
|
147
|
+
---
|
|
220
148
|
|
|
221
|
-
|
|
222
|
-
if (data.currentChar === '!') {
|
|
223
|
-
await new Promise((resolve) => setTimeout(resolve, 500)); // 模拟延迟
|
|
224
|
-
}
|
|
225
|
-
};
|
|
226
|
-
|
|
227
|
-
const handleTypedChar = (data) => {
|
|
228
|
-
// 更新打字统计信息
|
|
229
|
-
setTypingStats({
|
|
230
|
-
progress: Math.round(data.percent),
|
|
231
|
-
currentChar: data.currentChar,
|
|
232
|
-
totalChars: data.currentIndex + 1,
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
// 可以在这里添加音效、动画等效果
|
|
236
|
-
if (data.currentChar === '.') {
|
|
237
|
-
// 播放句号音效
|
|
238
|
-
console.log('播放句号音效');
|
|
239
|
-
}
|
|
240
|
-
};
|
|
241
|
-
|
|
242
|
-
const handleStart = (data) => {
|
|
243
|
-
console.log('开始打字:', data.currentChar);
|
|
244
|
-
};
|
|
245
|
-
|
|
246
|
-
const handleEnd = (data) => {
|
|
247
|
-
console.log('打字完成:', data.str);
|
|
248
|
-
};
|
|
249
|
-
|
|
250
|
-
const startDemo = () => {
|
|
251
|
-
markdownRef.current?.clear();
|
|
252
|
-
markdownRef.current?.push(
|
|
253
|
-
'# 高级回调演示\n\n' +
|
|
254
|
-
'这个示例展示了如何使用 `onBeforeTypedChar` 和 `onTypedChar` 回调:\n\n' +
|
|
255
|
-
'- 🎯 **打字前回调**:可以在字符显示前进行异步操作\n' +
|
|
256
|
-
'- 📊 **打字后回调**:可以实时更新进度和添加特效\n' +
|
|
257
|
-
'- ⚡ **性能优化**:支持异步操作,不影响打字流畅度\n\n' +
|
|
258
|
-
'当前进度:' +
|
|
259
|
-
typingStats.progress +
|
|
260
|
-
'%\n' +
|
|
261
|
-
'已打字数:' +
|
|
262
|
-
typingStats.totalChars +
|
|
263
|
-
'\n\n' +
|
|
264
|
-
'这是一个非常强大的功能!',
|
|
265
|
-
'answer',
|
|
266
|
-
);
|
|
267
|
-
};
|
|
149
|
+
## 📚 API 文档
|
|
268
150
|
|
|
269
|
-
|
|
270
|
-
<div>
|
|
271
|
-
<button onClick={startDemo}>🚀 开始高级演示</button>
|
|
151
|
+
### MarkdownTyper Props
|
|
272
152
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
153
|
+
| 属性 | 类型 | 默认值 | 说明 |
|
|
154
|
+
|------|------|--------|------|
|
|
155
|
+
| `children` | `string` | - | Markdown 内容(必需) |
|
|
156
|
+
| `interval` | `number \| IntervalType` | `30` | 打字间隔(毫秒) |
|
|
157
|
+
| `timerType` | `'setTimeout' \| 'requestAnimationFrame'` | `'setTimeout'` | 定时器类型 |
|
|
158
|
+
| `showCursor` | `boolean` | `false` | 是否显示光标 |
|
|
159
|
+
| `cursor` | `React.ReactNode` | `"\|"` | 光标内容 |
|
|
160
|
+
| `disableTyping` | `boolean` | `false` | 禁用打字动画 |
|
|
161
|
+
| `autoStartTyping` | `boolean` | `true` | 是否自动开始 |
|
|
162
|
+
| `onStart` | `(data) => void` | - | 打字开始回调 |
|
|
163
|
+
| `onEnd` | `(data) => void` | - | 打字结束回调 |
|
|
164
|
+
| `onTypedChar` | `(data) => void` | - | 每个字符打字后回调 |
|
|
165
|
+
| `reactMarkdownProps` | `Options` | - | react-markdown 配置 |
|
|
276
166
|
|
|
277
|
-
|
|
278
|
-
</div>
|
|
279
|
-
);
|
|
280
|
-
}
|
|
281
|
-
```
|
|
167
|
+
### MarkdownTyperCMD Props
|
|
282
168
|
|
|
283
|
-
|
|
169
|
+
与 `MarkdownTyper` 相同,但不需要 `children`。
|
|
284
170
|
|
|
285
|
-
|
|
286
|
-
import { useRef, useState } from 'react';
|
|
287
|
-
import { MarkdownCMD, MarkdownTyperCMDRef } from 'react-markdown-typer';
|
|
288
|
-
|
|
289
|
-
function RestartDemo() {
|
|
290
|
-
const markdownRef = useRef<MarkdownTyperCMDRef>(null);
|
|
291
|
-
const [isPlaying, setIsPlaying] = useState(false);
|
|
292
|
-
const [hasStarted, setHasStarted] = useState(false);
|
|
293
|
-
|
|
294
|
-
const startContent = () => {
|
|
295
|
-
markdownRef.current?.clear();
|
|
296
|
-
markdownRef.current?.push(
|
|
297
|
-
'# 重新开始动画演示\n\n' +
|
|
298
|
-
'这个示例展示了如何使用 `restart()` 方法:\n\n' +
|
|
299
|
-
'- 🔄 **重新开始**:从头开始播放当前内容\n' +
|
|
300
|
-
'- ⏸️ **暂停恢复**:可以随时暂停和恢复\n' +
|
|
301
|
-
'- 🎯 **精确控制**:完全控制动画播放状态\n\n' +
|
|
302
|
-
'当前状态:' +
|
|
303
|
-
(isPlaying ? '播放中' : '已暂停') +
|
|
304
|
-
'\n\n' +
|
|
305
|
-
'这是一个非常实用的功能!',
|
|
306
|
-
'answer',
|
|
307
|
-
);
|
|
308
|
-
setIsPlaying(true);
|
|
309
|
-
};
|
|
310
|
-
|
|
311
|
-
const handleStart = () => {
|
|
312
|
-
if (hasStarted) {
|
|
313
|
-
// 如果已经开始过,则重新开始
|
|
314
|
-
markdownRef.current?.restart();
|
|
315
|
-
} else {
|
|
316
|
-
// 第一次开始
|
|
317
|
-
markdownRef.current?.start();
|
|
318
|
-
setHasStarted(true);
|
|
319
|
-
}
|
|
320
|
-
setIsPlaying(true);
|
|
321
|
-
};
|
|
171
|
+
### MarkdownTyper Methods
|
|
322
172
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
173
|
+
| 方法 | 说明 |
|
|
174
|
+
|------|------|
|
|
175
|
+
| `start()` | 开始打字动画 |
|
|
176
|
+
| `stop()` | 暂停打字动画 |
|
|
177
|
+
| `resume()` | 恢复打字动画 |
|
|
178
|
+
| `restart()` | 重新开始 |
|
|
327
179
|
|
|
328
|
-
|
|
329
|
-
markdownRef.current?.resume();
|
|
330
|
-
setIsPlaying(true);
|
|
331
|
-
};
|
|
180
|
+
### MarkdownTyperCMD Methods
|
|
332
181
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
182
|
+
| 方法 | 参数 | 说明 |
|
|
183
|
+
|------|------|------|
|
|
184
|
+
| `push(content)` | `string` | 添加内容并开始打字 |
|
|
185
|
+
| `clear()` | - | 清空所有内容和状态 |
|
|
186
|
+
| `start()` | - | 开始打字动画 |
|
|
187
|
+
| `stop()` | - | 暂停打字动画 |
|
|
188
|
+
| `resume()` | - | 恢复打字动画 |
|
|
189
|
+
| `restart()` | - | 重新开始 |
|
|
337
190
|
|
|
338
|
-
|
|
339
|
-
setIsPlaying(false);
|
|
340
|
-
};
|
|
191
|
+
### IntervalType
|
|
341
192
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
⏸️ 暂停
|
|
351
|
-
</button>
|
|
352
|
-
<button onClick={handleResume} disabled={isPlaying}>
|
|
353
|
-
▶️ 恢复
|
|
354
|
-
</button>
|
|
355
|
-
<button onClick={handleRestart}>🔄 重新开始</button>
|
|
356
|
-
</div>
|
|
357
|
-
|
|
358
|
-
<div style={{ margin: '10px 0', padding: '10px', background: '#f5f5f5', borderRadius: '4px' }}>
|
|
359
|
-
<strong>动画状态:</strong> {isPlaying ? '🟢 播放中' : '🔴 已暂停'}
|
|
360
|
-
</div>
|
|
361
|
-
|
|
362
|
-
<MarkdownCMD ref={markdownRef} interval={25} onEnd={handleEnd} />
|
|
363
|
-
</div>
|
|
364
|
-
);
|
|
193
|
+
支持动态打字速度:
|
|
194
|
+
|
|
195
|
+
```typescript
|
|
196
|
+
type IntervalType = number | {
|
|
197
|
+
max: number; // 最大间隔
|
|
198
|
+
min: number; // 最小间隔
|
|
199
|
+
curve?: 'ease' | 'ease-in' | 'ease-out' | 'ease-in-out' | 'linear';
|
|
200
|
+
curveFn?: (x: number) => number; // 自定义曲线函数
|
|
365
201
|
}
|
|
366
202
|
```
|
|
367
203
|
|
|
368
|
-
|
|
369
|
-
|
|
204
|
+
**示例**:
|
|
370
205
|
```tsx
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
markdownRef.current?.clear();
|
|
381
|
-
markdownRef.current?.push(
|
|
382
|
-
'# 手动开始动画演示\n\n' +
|
|
383
|
-
'这个示例展示了如何使用 `start()` 方法:\n\n' +
|
|
384
|
-
'- 🎯 **手动控制**:当 `autoStartTyping=false` 时,需要手动调用 `start()`\n' +
|
|
385
|
-
'- ⏱️ **延迟开始**:可以在用户交互后开始动画\n' +
|
|
386
|
-
'- 🎮 **游戏化**:适合需要用户主动触发的场景\n\n' +
|
|
387
|
-
'点击"开始动画"按钮来手动启动打字效果!',
|
|
388
|
-
'answer',
|
|
389
|
-
);
|
|
390
|
-
setIsPlaying(false);
|
|
391
|
-
};
|
|
392
|
-
|
|
393
|
-
const handleStart = () => {
|
|
394
|
-
if (hasStarted) {
|
|
395
|
-
// 如果已经开始过,则重新开始
|
|
396
|
-
markdownRef.current?.restart();
|
|
397
|
-
} else {
|
|
398
|
-
// 第一次开始
|
|
399
|
-
markdownRef.current?.start();
|
|
400
|
-
setHasStarted(true);
|
|
401
|
-
}
|
|
402
|
-
setIsPlaying(true);
|
|
403
|
-
};
|
|
404
|
-
|
|
405
|
-
const handleEnd = () => {
|
|
406
|
-
setIsPlaying(false);
|
|
407
|
-
};
|
|
408
|
-
|
|
409
|
-
return (
|
|
410
|
-
<div>
|
|
411
|
-
<div style={{ marginBottom: '10px', display: 'flex', gap: '10px', flexWrap: 'wrap' }}>
|
|
412
|
-
<button onClick={loadContent}>📝 加载内容</button>
|
|
413
|
-
<button onClick={handleStart} disabled={isPlaying}>
|
|
414
|
-
{hasStarted ? '🔄 重新开始' : '▶️ 开始动画'}
|
|
415
|
-
</button>
|
|
416
|
-
</div>
|
|
417
|
-
|
|
418
|
-
<div style={{ margin: '10px 0', padding: '10px', background: '#f5f5f5', borderRadius: '4px' }}>
|
|
419
|
-
<strong>状态:</strong> {isPlaying ? '🟢 动画播放中' : '🔴 等待开始'}
|
|
420
|
-
</div>
|
|
421
|
-
|
|
422
|
-
<MarkdownCMD ref={markdownRef} interval={30} autoStartTyping={false} onEnd={handleEnd} />
|
|
423
|
-
</div>
|
|
424
|
-
);
|
|
425
|
-
}
|
|
206
|
+
<MarkdownTyper
|
|
207
|
+
interval={{
|
|
208
|
+
min: 10,
|
|
209
|
+
max: 100,
|
|
210
|
+
curve: 'ease-out' // 开始快,结束慢
|
|
211
|
+
}}
|
|
212
|
+
>
|
|
213
|
+
内容...
|
|
214
|
+
</MarkdownTyper>
|
|
426
215
|
```
|
|
427
216
|
|
|
428
217
|
---
|
|
429
218
|
|
|
430
|
-
##
|
|
219
|
+
## 🧮 数学公式
|
|
431
220
|
|
|
432
|
-
|
|
221
|
+
安装 KaTeX 插件:
|
|
433
222
|
|
|
434
|
-
```
|
|
435
|
-
|
|
223
|
+
```bash
|
|
224
|
+
npm install remark-math rehype-katex katex
|
|
436
225
|
```
|
|
437
226
|
|
|
438
|
-
|
|
439
|
-
| ----------------------------- | ------------------------------------------- | ------------------------------------------------------------- | ----------------------------------------------------------- |
|
|
440
|
-
| `interval` | `number` | 打字间隔 (毫秒) | `30` |
|
|
441
|
-
| `timerType` | `'setTimeout'` \| `'requestAnimationFrame'` | 定时器类型,不支持动态修改 | 当前默认值是`setTimeout`,后期会改为`requestAnimationFrame` |
|
|
442
|
-
| `theme` | `'light'` \| `'dark'` | 主题类型 | `'light'` |
|
|
443
|
-
| `customConvertMarkdownString` | `(markdownString: string) => string` | 自定义 markdown 字符串转换函数 | - |
|
|
444
|
-
| `onEnd` | `(data: EndData) => void` | 打字结束回调 | - |
|
|
445
|
-
| `onStart` | `(data: StartData) => void` | 打字开始回调 | - |
|
|
446
|
-
| `onBeforeTypedChar` | `(data: IBeforeTypedChar) => Promise<void>` | 字符打字前的回调,支持异步操作,会阻塞之后的打字 | - |
|
|
447
|
-
| `onTypedChar` | `(data: ITypedChar) => void` | 每字符打字后的回调 | - |
|
|
448
|
-
| `disableTyping` | `boolean` | 禁用打字动画效果 | `false` |
|
|
449
|
-
| `autoStartTyping` | `boolean` | 是否自动开始打字动画,设为 false 时需手动触发,不支持动态修改 | `true` |
|
|
450
|
-
|
|
451
|
-
> 注意: 如果当在打字中 `disableTyping`从 `true` 变为 `false`,则在下一个打字触发时,会把剩下的所有字一次性显示
|
|
227
|
+
使用:
|
|
452
228
|
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
229
|
+
```tsx
|
|
230
|
+
import remarkMath from 'remark-math';
|
|
231
|
+
import rehypeKatex from 'rehype-katex';
|
|
232
|
+
import 'katex/dist/katex.min.css';
|
|
233
|
+
|
|
234
|
+
<MarkdownTyper
|
|
235
|
+
interval={20}
|
|
236
|
+
reactMarkdownProps={{
|
|
237
|
+
remarkPlugins: [remarkMath],
|
|
238
|
+
rehypePlugins: [rehypeKatex]
|
|
239
|
+
}}
|
|
240
|
+
>
|
|
241
|
+
行内公式:$E = mc^2$
|
|
242
|
+
|
|
243
|
+
块级公式:
|
|
244
|
+
$$
|
|
245
|
+
\int_0^\infty e^{-x^2} dx = \frac{\sqrt{\pi}}{2}
|
|
246
|
+
$$
|
|
247
|
+
</MarkdownTyper>
|
|
248
|
+
```
|
|
473
249
|
|
|
474
|
-
|
|
250
|
+
---
|
|
475
251
|
|
|
476
|
-
|
|
477
|
-
- 内容过滤或清理
|
|
478
|
-
- 与外部 markdown 处理器集成
|
|
479
|
-
- 自定义链接处理或格式化
|
|
252
|
+
## 🔌 插件系统
|
|
480
253
|
|
|
481
|
-
|
|
254
|
+
完全兼容 [react-markdown](https://github.com/remarkjs/react-markdown) 的插件生态:
|
|
482
255
|
|
|
483
256
|
```tsx
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
257
|
+
import rehypeHighlight from 'rehype-highlight';
|
|
258
|
+
import remarkGfm from 'remark-gfm';
|
|
259
|
+
import 'highlight.js/styles/github.css';
|
|
260
|
+
|
|
261
|
+
<MarkdownTyper
|
|
262
|
+
reactMarkdownProps={{
|
|
263
|
+
remarkPlugins: [remarkGfm],
|
|
264
|
+
rehypePlugins: [rehypeHighlight]
|
|
265
|
+
}}
|
|
266
|
+
>
|
|
267
|
+
```javascript
|
|
268
|
+
console.log('代码高亮');
|
|
269
|
+
```
|
|
270
|
+
</MarkdownTyper>
|
|
488
271
|
```
|
|
489
272
|
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
可以通过 `reactMarkdownProps` 传入 react-markdown 所有的属性来支持
|
|
493
|
-
|
|
494
|
-
### 组件暴露的方法
|
|
273
|
+
---
|
|
495
274
|
|
|
496
|
-
|
|
275
|
+
## 🎛️ 定时器模式
|
|
497
276
|
|
|
498
|
-
|
|
499
|
-
| --------- | ---- | -------------------------------------- |
|
|
500
|
-
| `start` | - | 开始打字动画 |
|
|
501
|
-
| `stop` | - | 暂停打字动画 |
|
|
502
|
-
| `resume` | - | 恢复打字动画 |
|
|
503
|
-
| `restart` | - | 重新开始打字动画,从头开始播放当前内容 |
|
|
277
|
+
### `requestAnimationFrame` 模式(推荐)
|
|
504
278
|
|
|
505
|
-
|
|
279
|
+
- 时间驱动,批量处理字符
|
|
280
|
+
- 与浏览器 60fps 刷新率同步
|
|
281
|
+
- 适合高频打字(interval < 16ms)
|
|
506
282
|
|
|
507
|
-
|
|
508
|
-
| ----------------- | ------------------------------------------- | -------------------------------------- |
|
|
509
|
-
| `push` | `(content: string, answerType: AnswerType)` | 添加内容并开始打字 |
|
|
510
|
-
| `clear` | - | 清空所有内容和状态 |
|
|
511
|
-
| `triggerWholeEnd` | - | 手动触发完成回调 |
|
|
512
|
-
| `start` | - | 开始打字动画 |
|
|
513
|
-
| `stop` | - | 暂停打字动画 |
|
|
514
|
-
| `resume` | - | 恢复打字动画 |
|
|
515
|
-
| `restart` | - | 重新开始打字动画,从头开始播放当前内容 |
|
|
283
|
+
### `setTimeout` 模式
|
|
516
284
|
|
|
517
|
-
|
|
285
|
+
- 单字符处理,固定间隔
|
|
286
|
+
- 精确时间控制
|
|
287
|
+
- 适合低频打字或需要精确节奏的场景
|
|
518
288
|
|
|
519
289
|
```tsx
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
290
|
+
// 高频推荐 requestAnimationFrame
|
|
291
|
+
<MarkdownTyper interval={5} timerType="requestAnimationFrame">
|
|
292
|
+
快速打字
|
|
293
|
+
</MarkdownTyper>
|
|
294
|
+
|
|
295
|
+
// 低频推荐 setTimeout
|
|
296
|
+
<MarkdownTyper interval={100} timerType="setTimeout">
|
|
297
|
+
慢速打字
|
|
298
|
+
</MarkdownTyper>
|
|
524
299
|
```
|
|
525
300
|
|
|
526
301
|
---
|
|
527
302
|
|
|
528
|
-
##
|
|
303
|
+
## 💡 高级功能
|
|
529
304
|
|
|
530
|
-
###
|
|
305
|
+
### 自定义 Markdown 转换
|
|
531
306
|
|
|
532
307
|
```tsx
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
};
|
|
543
|
-
|
|
544
|
-
return (
|
|
545
|
-
<MarkdownTyper interval={20} customConvertMarkdownString={customConvertMarkdownString}>
|
|
546
|
-
# 自定义 Markdown 处理 这是**粗体文字**和*斜体文字*。查看[我们的网站](https://example.com)了解更多信息!
|
|
547
|
-
</MarkdownTyper>
|
|
548
|
-
);
|
|
549
|
-
}
|
|
308
|
+
<MarkdownTyper
|
|
309
|
+
customConvertMarkdownString={(str) => {
|
|
310
|
+
// 自定义处理逻辑
|
|
311
|
+
return str.replace(/\[([^\]]+)\]\(([^)]+)\)/g,
|
|
312
|
+
'<a href="$2" target="_blank">$1</a>');
|
|
313
|
+
}}
|
|
314
|
+
>
|
|
315
|
+
[链接](https://example.com)
|
|
316
|
+
</MarkdownTyper>
|
|
550
317
|
```
|
|
551
318
|
|
|
552
|
-
###
|
|
553
|
-
|
|
554
|
-
````tsx
|
|
555
|
-
// 复杂的自定义处理示例
|
|
556
|
-
const customConvertMarkdownString = (markdownString) => {
|
|
557
|
-
return (
|
|
558
|
-
markdownString
|
|
559
|
-
// 自定义表情符号处理
|
|
560
|
-
.replace(/:([a-zA-Z0-9_]+):/g, '<span class="emoji">:$1:</span>')
|
|
561
|
-
// 自定义提及处理
|
|
562
|
-
.replace(/@([a-zA-Z0-9_]+)/g, '<span class="mention">@$1</span>')
|
|
563
|
-
// 自定义代码块处理
|
|
564
|
-
.replace(/```(\w+)\n([\s\S]*?)```/g, '<pre class="code-block"><code class="language-$1">$2</code></pre>')
|
|
565
|
-
// 自定义链接处理(带安全性)
|
|
566
|
-
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (match, text, url) => {
|
|
567
|
-
if (url.startsWith('http')) {
|
|
568
|
-
return `<a href="${url}" target="_blank" rel="noopener noreferrer">${text}</a>`;
|
|
569
|
-
}
|
|
570
|
-
return match;
|
|
571
|
-
})
|
|
572
|
-
);
|
|
573
|
-
};
|
|
574
|
-
````
|
|
575
|
-
|
|
576
|
-
### 与外部处理器集成
|
|
319
|
+
### 回调函数
|
|
577
320
|
|
|
578
321
|
```tsx
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
322
|
+
<MarkdownTyper
|
|
323
|
+
onStart={(data) => console.log('开始打字', data)}
|
|
324
|
+
onEnd={(data) => console.log('打字结束', data)}
|
|
325
|
+
onTypedChar={(data) => {
|
|
326
|
+
console.log('进度:', data.percent + '%');
|
|
327
|
+
}}
|
|
328
|
+
>
|
|
329
|
+
内容...
|
|
330
|
+
</MarkdownTyper>
|
|
588
331
|
```
|
|
589
332
|
|
|
590
|
-
###
|
|
333
|
+
### 禁用打字动画
|
|
591
334
|
|
|
592
335
|
```tsx
|
|
593
|
-
const
|
|
594
|
-
// 过滤敏感内容
|
|
595
|
-
const filteredContent = markdownString.replace(/password[:\s]*[^\s]+/gi, 'password: [已过滤]').replace(/token[:\s]*[^\s]+/gi, 'token: [已过滤]');
|
|
336
|
+
const [disable, setDisable] = useState(false);
|
|
596
337
|
|
|
597
|
-
|
|
598
|
-
|
|
338
|
+
<MarkdownTyper disableTyping={disable}>
|
|
339
|
+
内容会立即显示,无动画
|
|
340
|
+
</MarkdownTyper>
|
|
599
341
|
```
|
|
600
342
|
|
|
601
343
|
---
|
|
602
344
|
|
|
603
|
-
##
|
|
604
|
-
|
|
605
|
-
可查看 [react-markdown](https://github.com/remarkjs/react-markdown)
|
|
606
|
-
|
|
607
|
-
---
|
|
608
|
-
|
|
609
|
-
## 🎛️ 定时器模式详解
|
|
610
|
-
|
|
611
|
-
### `requestAnimationFrame` 模式 🌟 (推荐)
|
|
612
|
-
|
|
613
|
-
```typescript
|
|
614
|
-
// 🎯 特性
|
|
615
|
-
- 时间驱动:基于真实经过时间计算字符数量
|
|
616
|
-
- 批量处理:单帧内可处理多个字符
|
|
617
|
-
- 帧同步:与浏览器 60fps 刷新率同步
|
|
618
|
-
- 高频优化:完美支持 interval < 16ms 的高速打字
|
|
619
|
-
|
|
620
|
-
// 🎯 适用场景
|
|
621
|
-
- 现代 Web 应用的默认选择
|
|
622
|
-
- 追求流畅动画效果
|
|
623
|
-
- 高频打字 (interval > 0 即可)
|
|
624
|
-
- AI 实时对话场景
|
|
625
|
-
```
|
|
345
|
+
## 📖 示例项目
|
|
626
346
|
|
|
627
|
-
|
|
347
|
+
克隆仓库查看完整示例:
|
|
628
348
|
|
|
629
|
-
```
|
|
630
|
-
|
|
631
|
-
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
- 精确控制:适合特定时序要求
|
|
635
|
-
|
|
636
|
-
// 🎯 适用场景
|
|
637
|
-
- 需要精确时间控制
|
|
638
|
-
- 营造复古打字机效果
|
|
639
|
-
- 兼容性要求较高的场景
|
|
349
|
+
```bash
|
|
350
|
+
git clone https://github.com/onshinpei/react-markdown-typer.git
|
|
351
|
+
cd react-markdown-typer
|
|
352
|
+
npm install
|
|
353
|
+
npm run dev
|
|
640
354
|
```
|
|
641
355
|
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
| **高频间隔** | ✅ 优秀 (5ms → 每帧3字符) | ❌ 可能卡顿 |
|
|
648
|
-
| **低频间隔** | ✅ 正常 (100ms → 6帧后1字符) | ✅ 精确 |
|
|
649
|
-
| **视觉效果** | 🎬 流畅动画感 | ⚡ 精确节拍感 |
|
|
650
|
-
| **性能开销** | 🟢 低 (帧同步) | 🟡 中等 (定时器) |
|
|
651
|
-
|
|
652
|
-
高频推荐`requestAnimationFrame`,低频推荐 `setTimeout`
|
|
356
|
+
示例位置:
|
|
357
|
+
- `example/basic/` - 基础用法
|
|
358
|
+
- `example/cmd/` - 命令式 API
|
|
359
|
+
- `example/cursor/` - 光标效果
|
|
360
|
+
- `example/katex/` - 数学公式
|
|
653
361
|
|
|
654
362
|
---
|
|
655
363
|
|
|
656
|
-
##
|
|
657
|
-
|
|
658
|
-
### 📝 AI 流式对话
|
|
659
|
-
|
|
660
|
-
<!-- [DEMO: 🔧 StackBlitz 体验](https://stackblitz.com/edit/vitejs-vite-2ri8kex3?file=src%2FApp.tsx) -->
|
|
661
|
-
|
|
662
|
-
````tsx
|
|
663
|
-
import { useRef } from 'react';
|
|
664
|
-
import { MarkdownCMD, MarkdownTyperCMDRef } from 'react-markdown-typer';
|
|
665
|
-
|
|
666
|
-
function StreamingChat() {
|
|
667
|
-
const markdownRef = useRef<MarkdownTyperCMDRef>(null);
|
|
668
|
-
|
|
669
|
-
// 模拟 AI 流式响应
|
|
670
|
-
const simulateAIResponse = async () => {
|
|
671
|
-
markdownRef.current?.clear();
|
|
672
|
-
|
|
673
|
-
// 思考阶段
|
|
674
|
-
markdownRef.current?.push('🤔 正在分析您的问题...', 'thinking');
|
|
675
|
-
await delay(1000);
|
|
676
|
-
markdownRef.current?.push('\n\n✅ 分析完成,开始回答', 'thinking');
|
|
677
|
-
|
|
678
|
-
// 流式回答
|
|
679
|
-
const chunks = [
|
|
680
|
-
'# React 19 新特性解析\n\n',
|
|
681
|
-
'## 🚀 React Compiler\n',
|
|
682
|
-
'React 19 最大的亮点是引入了 **React Compiler**:\n\n',
|
|
683
|
-
'- 🎯 **自动优化**:无需手动 memo 和 useMemo\n',
|
|
684
|
-
'- ⚡ **性能提升**:编译时优化,运行时零开销\n',
|
|
685
|
-
'- 🔧 **向后兼容**:现有代码无需修改\n\n',
|
|
686
|
-
'## 📝 Actions 简化表单\n',
|
|
687
|
-
'新的 Actions API 让表单处理变得更简单:\n\n',
|
|
688
|
-
'```tsx\n',
|
|
689
|
-
'function ContactForm({ action }) {\n',
|
|
690
|
-
' const [state, formAction] = useActionState(action, null);\n',
|
|
691
|
-
' return (\n',
|
|
692
|
-
' <form action={formAction}>\n',
|
|
693
|
-
' <input name="email" type="email" />\n',
|
|
694
|
-
' <button>提交</button>\n',
|
|
695
|
-
' </form>\n',
|
|
696
|
-
' );\n',
|
|
697
|
-
'}\n',
|
|
698
|
-
'```\n\n',
|
|
699
|
-
'希望这个解答对您有帮助!🎉',
|
|
700
|
-
];
|
|
701
|
-
|
|
702
|
-
for (const chunk of chunks) {
|
|
703
|
-
await delay(100);
|
|
704
|
-
markdownRef.current?.push(chunk, 'answer');
|
|
705
|
-
}
|
|
706
|
-
};
|
|
707
|
-
|
|
708
|
-
return (
|
|
709
|
-
<div className="chat-container">
|
|
710
|
-
<button onClick={simulateAIResponse}>🤖 询问 React 19 新特性</button>
|
|
711
|
-
|
|
712
|
-
<MarkdownCMD ref={markdownRef} interval={10} timerType="requestAnimationFrame" onEnd={(data) => console.log('段落完成:', data)} />
|
|
713
|
-
</div>
|
|
714
|
-
);
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
718
|
-
````
|
|
719
|
-
|
|
720
|
-
### 🔧 自定义 Markdown 处理演示
|
|
721
|
-
|
|
722
|
-
```tsx
|
|
723
|
-
function CustomMarkdownStreamingDemo() {
|
|
724
|
-
const markdownRef = useRef<MarkdownTyperCMDRef>(null);
|
|
725
|
-
|
|
726
|
-
const customConvertMarkdownString = (markdownString) => {
|
|
727
|
-
return markdownString
|
|
728
|
-
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>')
|
|
729
|
-
.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>')
|
|
730
|
-
.replace(/\*([^*]+)\*/g, '<em>$1</em>')
|
|
731
|
-
.replace(/`([^`]+)`/g, '<code>$1</code>');
|
|
732
|
-
};
|
|
733
|
-
|
|
734
|
-
const simulateCustomResponse = async () => {
|
|
735
|
-
markdownRef.current?.clear();
|
|
736
|
-
|
|
737
|
-
const customChunks = [
|
|
738
|
-
'# 自定义 Markdown 处理\n\n',
|
|
739
|
-
'这个演示展示了如何在流式内容中使用**自定义 markdown 处理**:\n\n',
|
|
740
|
-
'## 功能特性\n',
|
|
741
|
-
'- *自定义链接处理*\n',
|
|
742
|
-
'- **粗体和斜体**文字处理\n',
|
|
743
|
-
'- `行内代码`格式化\n',
|
|
744
|
-
'- [外部链接](https://example.com) 带安全属性\n\n',
|
|
745
|
-
'`customConvertMarkdownString` 函数允许您在渲染前预处理内容!',
|
|
746
|
-
];
|
|
747
|
-
|
|
748
|
-
for (const chunk of customChunks) {
|
|
749
|
-
await delay(150);
|
|
750
|
-
markdownRef.current?.push(chunk, 'answer');
|
|
751
|
-
}
|
|
752
|
-
};
|
|
753
|
-
|
|
754
|
-
return (
|
|
755
|
-
<div>
|
|
756
|
-
<button onClick={simulateCustomResponse}>🔧 自定义 Markdown 演示</button>
|
|
757
|
-
|
|
758
|
-
<MarkdownCMD ref={markdownRef} interval={20} timerType="requestAnimationFrame" customConvertMarkdownString={customConvertMarkdownString} />
|
|
759
|
-
</div>
|
|
760
|
-
);
|
|
761
|
-
}
|
|
762
|
-
```
|
|
763
|
-
|
|
764
|
-
### 🎯 高级回调控制
|
|
765
|
-
|
|
766
|
-
```tsx
|
|
767
|
-
import { useRef, useState } from 'react';
|
|
768
|
-
import { MarkdownCMD, MarkdownTyperCMDRef } from 'react-markdown-typer';
|
|
364
|
+
## 🤝 贡献
|
|
769
365
|
|
|
770
|
-
|
|
771
|
-
const markdownRef = useRef<MarkdownTyperCMDRef>(null);
|
|
772
|
-
const [typingStats, setTypingStats] = useState({ progress: 0, currentChar: '', totalChars: 0 });
|
|
366
|
+
欢迎提交 Issue 和 Pull Request!
|
|
773
367
|
|
|
774
|
-
|
|
775
|
-
// 在字符打字前进行异步操作
|
|
776
|
-
console.log('即将打字:', data.currentChar);
|
|
368
|
+
## 📄 License
|
|
777
369
|
|
|
778
|
-
|
|
779
|
-
if (data.currentChar === '!') {
|
|
780
|
-
await new Promise((resolve) => setTimeout(resolve, 500)); // 模拟延迟
|
|
781
|
-
}
|
|
782
|
-
};
|
|
783
|
-
|
|
784
|
-
const handleTypedChar = (data) => {
|
|
785
|
-
// 更新打字统计信息
|
|
786
|
-
setTypingStats({
|
|
787
|
-
progress: Math.round(data.percent),
|
|
788
|
-
currentChar: data.currentChar,
|
|
789
|
-
totalChars: data.currentIndex + 1,
|
|
790
|
-
});
|
|
791
|
-
|
|
792
|
-
// 可以在这里添加音效、动画等效果
|
|
793
|
-
if (data.currentChar === '.') {
|
|
794
|
-
// 播放句号音效
|
|
795
|
-
console.log('播放句号音效');
|
|
796
|
-
}
|
|
797
|
-
};
|
|
798
|
-
|
|
799
|
-
const handleStart = (data) => {
|
|
800
|
-
console.log('开始打字:', data.currentChar);
|
|
801
|
-
};
|
|
802
|
-
|
|
803
|
-
const handleEnd = (data) => {
|
|
804
|
-
console.log('打字完成:', data.str);
|
|
805
|
-
};
|
|
806
|
-
|
|
807
|
-
const startDemo = () => {
|
|
808
|
-
markdownRef.current?.clear();
|
|
809
|
-
markdownRef.current?.push(
|
|
810
|
-
'# 高级回调演示\n\n' +
|
|
811
|
-
'这个示例展示了如何使用 `onBeforeTypedChar` 和 `onTypedChar` 回调:\n\n' +
|
|
812
|
-
'- 🎯 **打字前回调**:可以在字符显示前进行异步操作\n' +
|
|
813
|
-
'- 📊 **打字后回调**:可以实时更新进度和添加特效\n' +
|
|
814
|
-
'- ⚡ **性能优化**:支持异步操作,不影响打字流畅度\n\n' +
|
|
815
|
-
'当前进度:' +
|
|
816
|
-
typingStats.progress +
|
|
817
|
-
'%\n' +
|
|
818
|
-
'已打字数:' +
|
|
819
|
-
typingStats.totalChars +
|
|
820
|
-
'\n\n' +
|
|
821
|
-
'这是一个非常强大的功能!',
|
|
822
|
-
'answer',
|
|
823
|
-
);
|
|
824
|
-
};
|
|
370
|
+
MIT © [onshinpei](https://github.com/onshinpei)
|
|
825
371
|
|
|
826
|
-
|
|
827
|
-
<div>
|
|
828
|
-
<button onClick={startDemo}>🚀 开始高级演示</button>
|
|
372
|
+
---
|
|
829
373
|
|
|
830
|
-
|
|
831
|
-
<strong>打字统计:</strong> 进度 {typingStats.progress}% | 当前字符: "{typingStats.currentChar}" | 总字符数: {typingStats.totalChars}
|
|
832
|
-
</div>
|
|
374
|
+
## 🔗 相关项目
|
|
833
375
|
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
);
|
|
837
|
-
}
|
|
838
|
-
```
|
|
376
|
+
- [react-markdown](https://github.com/remarkjs/react-markdown) - Markdown 渲染核心
|
|
377
|
+
- [ds-markdown](https://github.com/onshinpei/ds-markdown) - 带样式的增强版(支持 mermaid 图表)
|