neweditor 1.2.0 → 1.3.1
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 +693 -693
- package/dist/components/Editor.vue.d.ts +6 -3
- package/dist/extensions/index.d.ts +2 -1
- package/dist/rich-text-editor.es.js +12885 -12508
- package/dist/style.css +2 -2
- package/package.json +114 -114
package/README.md
CHANGED
|
@@ -1,693 +1,693 @@
|
|
|
1
|
-
# TestFox RichText Editor
|
|
2
|
-
|
|
3
|
-
基于 TipTap 的富文本编辑器,专为 TestFox 项目设计,支持完整的主题系统和亮色/暗色模式切换。
|
|
4
|
-
|
|
5
|
-
## 特性
|
|
6
|
-
|
|
7
|
-
- ✅ 完整的富文本编辑功能
|
|
8
|
-
- ✅ 支持 40+ 种编程语言语法高亮
|
|
9
|
-
- ✅ Slash Command(斜杠命令)快捷操作
|
|
10
|
-
- ✅ 图片粘贴上传(Ctrl+V)
|
|
11
|
-
- ✅ 文件拖拽上传(图片、视频、音频)
|
|
12
|
-
- ✅ 自动上传到服务器(支持自定义上传处理器)
|
|
13
|
-
- ✅ 拖拽排序功能
|
|
14
|
-
- ✅ 表格编辑(合并单元格、调整列宽等)
|
|
15
|
-
- ✅ 多语言支持(中文/英文)
|
|
16
|
-
- ✅ 主题系统(亮色/暗色模式)
|
|
17
|
-
- ✅ 响应式设计
|
|
18
|
-
|
|
19
|
-
## 安装
|
|
20
|
-
|
|
21
|
-
```bash
|
|
22
|
-
npm install neweditor
|
|
23
|
-
# 或
|
|
24
|
-
pnpm install neweditor
|
|
25
|
-
# 或
|
|
26
|
-
yarn add neweditor
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
## 使用
|
|
30
|
-
|
|
31
|
-
### 基础使用
|
|
32
|
-
|
|
33
|
-
```vue
|
|
34
|
-
<template>
|
|
35
|
-
<RichTextEditor :editor="editor" locale="zh-CN" />
|
|
36
|
-
</template>
|
|
37
|
-
|
|
38
|
-
<script setup>
|
|
39
|
-
import { useEditor, RichTextEditor, allExtensions } from 'neweditor'
|
|
40
|
-
import 'neweditor/dist/style.css'
|
|
41
|
-
|
|
42
|
-
const editor = useEditor({
|
|
43
|
-
extensions: allExtensions,
|
|
44
|
-
content: '<p>Hello World!</p>',
|
|
45
|
-
})
|
|
46
|
-
</script>
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
### 完整示例
|
|
50
|
-
|
|
51
|
-
```vue
|
|
52
|
-
<template>
|
|
53
|
-
<div class="editor-wrapper">
|
|
54
|
-
<RichTextEditor
|
|
55
|
-
:editor="editor"
|
|
56
|
-
locale="zh-CN"
|
|
57
|
-
class="my-editor"
|
|
58
|
-
/>
|
|
59
|
-
</div>
|
|
60
|
-
</template>
|
|
61
|
-
|
|
62
|
-
<script setup>
|
|
63
|
-
import { ref } from 'vue'
|
|
64
|
-
import { useEditor, RichTextEditor, allExtensions } from 'neweditor'
|
|
65
|
-
import 'neweditor/dist/style.css'
|
|
66
|
-
|
|
67
|
-
const content = ref('<p>初始内容</p>')
|
|
68
|
-
|
|
69
|
-
const editor = useEditor({
|
|
70
|
-
extensions: allExtensions,
|
|
71
|
-
content: content.value,
|
|
72
|
-
editable: true,
|
|
73
|
-
autofocus: false,
|
|
74
|
-
editorProps: {
|
|
75
|
-
attributes: {
|
|
76
|
-
'data-placeholder': '请输入内容...',
|
|
77
|
-
},
|
|
78
|
-
},
|
|
79
|
-
onUpdate: ({ editor }) => {
|
|
80
|
-
content.value = editor.getHTML()
|
|
81
|
-
},
|
|
82
|
-
onFocus: ({ editor }) => {
|
|
83
|
-
console.log('编辑器获得焦点')
|
|
84
|
-
},
|
|
85
|
-
onBlur: ({ editor }) => {
|
|
86
|
-
console.log('编辑器失去焦点')
|
|
87
|
-
},
|
|
88
|
-
})
|
|
89
|
-
</script>
|
|
90
|
-
|
|
91
|
-
<style>
|
|
92
|
-
.editor-wrapper {
|
|
93
|
-
border: 1px solid #d9d9d9;
|
|
94
|
-
border-radius: 4px;
|
|
95
|
-
}
|
|
96
|
-
</style>
|
|
97
|
-
```
|
|
98
|
-
|
|
99
|
-
### 带文件上传功能的示例
|
|
100
|
-
|
|
101
|
-
```vue
|
|
102
|
-
<template>
|
|
103
|
-
<div class="editor-wrapper">
|
|
104
|
-
<RichTextEditor
|
|
105
|
-
:editor="editor"
|
|
106
|
-
locale="zh-CN"
|
|
107
|
-
class="my-editor"
|
|
108
|
-
/>
|
|
109
|
-
</div>
|
|
110
|
-
</template>
|
|
111
|
-
|
|
112
|
-
<script setup>
|
|
113
|
-
import { ref } from 'vue'
|
|
114
|
-
import { useEditor, RichTextEditor, allExtensions } from 'testfox-richtext-editor'
|
|
115
|
-
import 'testfox-richtext-editor/dist/style.css'
|
|
116
|
-
|
|
117
|
-
const content = ref('<p>初始内容</p>')
|
|
118
|
-
|
|
119
|
-
// 自定义文件上传处理器
|
|
120
|
-
const handleFileUpload = async (file) => {
|
|
121
|
-
try {
|
|
122
|
-
// 1. 验证文件大小(例如限制 100MB)
|
|
123
|
-
const maxSize = 100 * 1024 * 1024
|
|
124
|
-
if (file.size > maxSize) {
|
|
125
|
-
console.error('文件大小不能超过100MB')
|
|
126
|
-
return null
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
// 2. 创建 FormData
|
|
130
|
-
const formData = new FormData()
|
|
131
|
-
formData.append('file', file)
|
|
132
|
-
|
|
133
|
-
// 3. 调用你的上传 API
|
|
134
|
-
const response = await fetch('/api/upload', {
|
|
135
|
-
method: 'POST',
|
|
136
|
-
body: formData,
|
|
137
|
-
})
|
|
138
|
-
|
|
139
|
-
const result = await response.json()
|
|
140
|
-
|
|
141
|
-
// 4. 返回文件 URL(成功)或 null(失败)
|
|
142
|
-
if (result.success) {
|
|
143
|
-
return result.fileUrl // 返回文件的访问 URL
|
|
144
|
-
} else {
|
|
145
|
-
console.error('上传失败:', result.message)
|
|
146
|
-
return null
|
|
147
|
-
}
|
|
148
|
-
} catch (error) {
|
|
149
|
-
console.error('上传异常:', error)
|
|
150
|
-
return null
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// 配置扩展并传递上传处理器
|
|
155
|
-
const configuredExtensions = allExtensions.map(ext => {
|
|
156
|
-
// 为图片、视频、音频扩展配置上传处理器
|
|
157
|
-
if (ext.name === 'image' || ext.name === 'video' || ext.name === 'audio') {
|
|
158
|
-
return ext.configure({
|
|
159
|
-
uploadHandler: handleFileUpload
|
|
160
|
-
})
|
|
161
|
-
}
|
|
162
|
-
return ext
|
|
163
|
-
})
|
|
164
|
-
|
|
165
|
-
const editor = useEditor({
|
|
166
|
-
extensions: configuredExtensions, // 使用配置好的扩展
|
|
167
|
-
content: content.value,
|
|
168
|
-
editable: true,
|
|
169
|
-
onUpdate: ({ editor }) => {
|
|
170
|
-
content.value = editor.getHTML()
|
|
171
|
-
},
|
|
172
|
-
})
|
|
173
|
-
</script>
|
|
174
|
-
|
|
175
|
-
<style>
|
|
176
|
-
.editor-wrapper {
|
|
177
|
-
border: 1px solid #d9d9d9;
|
|
178
|
-
border-radius: 4px;
|
|
179
|
-
}
|
|
180
|
-
</style>
|
|
181
|
-
```
|
|
182
|
-
|
|
183
|
-
## API
|
|
184
|
-
|
|
185
|
-
### useEditor(options)
|
|
186
|
-
|
|
187
|
-
创建编辑器实例的 Hook。
|
|
188
|
-
|
|
189
|
-
**参数:**
|
|
190
|
-
|
|
191
|
-
```typescript
|
|
192
|
-
interface EditorOptions {
|
|
193
|
-
extensions: Extension[] // 扩展列表
|
|
194
|
-
content?: string // 初始内容(HTML)
|
|
195
|
-
editable?: boolean // 是否可编辑
|
|
196
|
-
autofocus?: boolean // 是否自动聚焦
|
|
197
|
-
editorProps?: EditorProps // 编辑器属性
|
|
198
|
-
onUpdate?: (props) => void // 内容更新回调
|
|
199
|
-
onFocus?: (props) => void // 获得焦点回调
|
|
200
|
-
onBlur?: (props) => void // 失去焦点回调
|
|
201
|
-
onCreate?: (props) => void // 创建完成回调
|
|
202
|
-
}
|
|
203
|
-
```
|
|
204
|
-
|
|
205
|
-
**返回值:**
|
|
206
|
-
|
|
207
|
-
返回一个响应式的编辑器实例。
|
|
208
|
-
|
|
209
|
-
### 文件上传配置
|
|
210
|
-
|
|
211
|
-
#### uploadHandler
|
|
212
|
-
|
|
213
|
-
文件上传处理器函数,用于处理图片、视频、音频的上传。
|
|
214
|
-
|
|
215
|
-
**类型签名:**
|
|
216
|
-
|
|
217
|
-
```typescript
|
|
218
|
-
type UploadHandler = (file: File) => Promise<string | null>
|
|
219
|
-
```
|
|
220
|
-
|
|
221
|
-
**参数:**
|
|
222
|
-
- `file`: File 对象,包含要上传的文件
|
|
223
|
-
|
|
224
|
-
**返回值:**
|
|
225
|
-
- 成功:返回文件的访问 URL(字符串)
|
|
226
|
-
- 失败:返回 `null`
|
|
227
|
-
|
|
228
|
-
**配置方式:**
|
|
229
|
-
|
|
230
|
-
```javascript
|
|
231
|
-
// 为需要上传功能的扩展配置 uploadHandler
|
|
232
|
-
const configuredExtensions = allExtensions.map(ext => {
|
|
233
|
-
if (ext.name === 'image' || ext.name === 'video' || ext.name === 'audio') {
|
|
234
|
-
return ext.configure({
|
|
235
|
-
uploadHandler: async (file) => {
|
|
236
|
-
// 你的上传逻辑
|
|
237
|
-
const url = await uploadToServer(file)
|
|
238
|
-
return url // 返回 URL 或 null
|
|
239
|
-
}
|
|
240
|
-
})
|
|
241
|
-
}
|
|
242
|
-
return ext
|
|
243
|
-
})
|
|
244
|
-
```
|
|
245
|
-
|
|
246
|
-
**工作流程:**
|
|
247
|
-
|
|
248
|
-
1. 用户粘贴图片或拖拽文件到编辑器
|
|
249
|
-
2. 编辑器创建临时预览(Blob URL)
|
|
250
|
-
3. 立即显示预览,用户无需等待
|
|
251
|
-
4. 后台调用 `uploadHandler` 上传文件
|
|
252
|
-
5. 上传成功后,自动替换为真实 URL
|
|
253
|
-
6. 上传失败则删除临时预览
|
|
254
|
-
|
|
255
|
-
**示例实现:**
|
|
256
|
-
|
|
257
|
-
```javascript
|
|
258
|
-
const handleFileUpload = async (file) => {
|
|
259
|
-
try {
|
|
260
|
-
// 验证文件大小
|
|
261
|
-
const maxSize = 100 * 1024 * 1024 // 100MB
|
|
262
|
-
if (file.size > maxSize) {
|
|
263
|
-
console.error('文件过大')
|
|
264
|
-
return null
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
// 上传到服务器
|
|
268
|
-
const formData = new FormData()
|
|
269
|
-
formData.append('file', file)
|
|
270
|
-
|
|
271
|
-
const response = await fetch('/api/upload', {
|
|
272
|
-
method: 'POST',
|
|
273
|
-
body: formData,
|
|
274
|
-
})
|
|
275
|
-
|
|
276
|
-
const result = await response.json()
|
|
277
|
-
|
|
278
|
-
// 返回文件 URL
|
|
279
|
-
return result.success ? result.fileUrl : null
|
|
280
|
-
} catch (error) {
|
|
281
|
-
console.error('上传失败:', error)
|
|
282
|
-
return null
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
```
|
|
286
|
-
|
|
287
|
-
### RichTextEditor 组件
|
|
288
|
-
|
|
289
|
-
编辑器 UI 组件。
|
|
290
|
-
|
|
291
|
-
**Props:**
|
|
292
|
-
|
|
293
|
-
| 属性 | 类型 | 默认值 | 说明 |
|
|
294
|
-
|------|------|--------|------|
|
|
295
|
-
| `editor` | Editor | - | 编辑器实例(必需) |
|
|
296
|
-
| `locale` | String | `'zh-CN'` | 语言设置(zh-CN/en) |
|
|
297
|
-
|
|
298
|
-
### allExtensions
|
|
299
|
-
|
|
300
|
-
包含所有内置扩展的数组,包括:
|
|
301
|
-
|
|
302
|
-
- 文本格式化(粗体、斜体、下划线、删除线等)
|
|
303
|
-
- 标题(H1-H6)
|
|
304
|
-
- 列表(有序、无序、任务列表)
|
|
305
|
-
- 代码(行内代码、代码块)
|
|
306
|
-
- 表格
|
|
307
|
-
- 图片、视频、音频
|
|
308
|
-
- 链接
|
|
309
|
-
- 引用块
|
|
310
|
-
- 分栏布局
|
|
311
|
-
- 折叠面板
|
|
312
|
-
- Slash Command
|
|
313
|
-
- 拖拽排序
|
|
314
|
-
- 搜索和替换
|
|
315
|
-
- 等等...
|
|
316
|
-
|
|
317
|
-
## 编辑器功能
|
|
318
|
-
|
|
319
|
-
### 文本格式化
|
|
320
|
-
- 粗体、斜体、下划线、删除线、上标、下标
|
|
321
|
-
- 标题(H1-H6)
|
|
322
|
-
- 文本颜色和背景色
|
|
323
|
-
- 字体大小
|
|
324
|
-
- 文本对齐
|
|
325
|
-
- 清除格式
|
|
326
|
-
- 格式刷
|
|
327
|
-
|
|
328
|
-
### 列表和引用
|
|
329
|
-
- 无序列表
|
|
330
|
-
- 有序列表
|
|
331
|
-
- 任务列表
|
|
332
|
-
- 引用块
|
|
333
|
-
- 缩进
|
|
334
|
-
|
|
335
|
-
### 代码
|
|
336
|
-
- 行内代码
|
|
337
|
-
- 代码块(支持 40+ 种语言语法高亮)
|
|
338
|
-
|
|
339
|
-
### 媒体内容
|
|
340
|
-
- 图片(支持粘贴、拖拽上传、调整大小)
|
|
341
|
-
- 视频(支持拖拽上传)
|
|
342
|
-
- 音频(支持拖拽上传)
|
|
343
|
-
- iframe
|
|
344
|
-
|
|
345
|
-
### 表格
|
|
346
|
-
- 创建表格
|
|
347
|
-
- 添加/删除行和列
|
|
348
|
-
- 合并/拆分单元格
|
|
349
|
-
- 调整列宽
|
|
350
|
-
- 表格拖拽排序
|
|
351
|
-
|
|
352
|
-
### 快捷功能
|
|
353
|
-
- Slash Command(输入 `/` 或 `、` 触发)
|
|
354
|
-
- 拖拽排序
|
|
355
|
-
- 搜索和替换
|
|
356
|
-
- 撤销/重做
|
|
357
|
-
- 字符统计
|
|
358
|
-
|
|
359
|
-
## 快捷键
|
|
360
|
-
|
|
361
|
-
| 快捷键 | 功能 |
|
|
362
|
-
|--------|------|
|
|
363
|
-
| `Ctrl/Cmd + B` | 粗体 |
|
|
364
|
-
| `Ctrl/Cmd + I` | 斜体 |
|
|
365
|
-
| `Ctrl/Cmd + U` | 下划线 |
|
|
366
|
-
| `Ctrl/Cmd + Shift + X` | 删除线 |
|
|
367
|
-
| `Ctrl/Cmd + E` | 行内代码 |
|
|
368
|
-
| `Ctrl/Cmd + Z` | 撤销 |
|
|
369
|
-
| `Ctrl/Cmd + Shift + Z` | 重做 |
|
|
370
|
-
| `Ctrl/Cmd + K` | 插入链接 |
|
|
371
|
-
| `Ctrl/Cmd + V` | 粘贴图片 |
|
|
372
|
-
| `/` 或 `、` | Slash Command |
|
|
373
|
-
| `Tab` | 增加缩进 |
|
|
374
|
-
| `Shift + Tab` | 减少缩进 |
|
|
375
|
-
|
|
376
|
-
## 主题系统
|
|
377
|
-
|
|
378
|
-
编辑器支持亮色和暗色两种主题模式,通过 CSS 变量控制。
|
|
379
|
-
|
|
380
|
-
### 主题变量
|
|
381
|
-
|
|
382
|
-
```css
|
|
383
|
-
/* 亮色主题 */
|
|
384
|
-
.theme-light .testfox-rich-text-editor {
|
|
385
|
-
--editor-primary-color: #9373ee;
|
|
386
|
-
--editor-text-color: rgba(0, 0, 0, 0.85);
|
|
387
|
-
--editor-background-color: #ffffff;
|
|
388
|
-
--editor-border-color: #f0f0f0;
|
|
389
|
-
/* ... 更多变量 */
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
/* 暗色主题 */
|
|
393
|
-
.theme-dark .testfox-rich-text-editor {
|
|
394
|
-
--editor-primary-color: #9373ee;
|
|
395
|
-
--editor-text-color: rgba(255, 255, 255, 0.85);
|
|
396
|
-
--editor-background-color: #1f1f1f;
|
|
397
|
-
--editor-border-color: #303030;
|
|
398
|
-
/* ... 更多变量 */
|
|
399
|
-
}
|
|
400
|
-
```
|
|
401
|
-
|
|
402
|
-
### 自定义主题
|
|
403
|
-
|
|
404
|
-
可以通过覆盖 CSS 变量来自定义主题:
|
|
405
|
-
|
|
406
|
-
```css
|
|
407
|
-
.my-editor {
|
|
408
|
-
--editor-primary-color: #your-color;
|
|
409
|
-
--editor-background-color: #your-bg;
|
|
410
|
-
}
|
|
411
|
-
```
|
|
412
|
-
|
|
413
|
-
## 开发
|
|
414
|
-
|
|
415
|
-
### 安装依赖
|
|
416
|
-
|
|
417
|
-
```bash
|
|
418
|
-
npm install
|
|
419
|
-
```
|
|
420
|
-
|
|
421
|
-
### 开发模式
|
|
422
|
-
|
|
423
|
-
```bash
|
|
424
|
-
npm run dev
|
|
425
|
-
```
|
|
426
|
-
|
|
427
|
-
### 构建
|
|
428
|
-
|
|
429
|
-
```bash
|
|
430
|
-
npm run build
|
|
431
|
-
```
|
|
432
|
-
|
|
433
|
-
### 类型检查
|
|
434
|
-
|
|
435
|
-
```bash
|
|
436
|
-
npm run typecheck
|
|
437
|
-
```
|
|
438
|
-
|
|
439
|
-
## 扩展开发
|
|
440
|
-
|
|
441
|
-
如需为编辑器添加新功能,可以创建自定义扩展。详细的扩展开发指南请参考:
|
|
442
|
-
|
|
443
|
-
- [扩展开发文档](./docs/extension.md)
|
|
444
|
-
- [Slash Command 指南](./docs/slash-command-guide.md)
|
|
445
|
-
- [Slash Command 示例](./docs/slash-command-examples.md)
|
|
446
|
-
|
|
447
|
-
## 文件上传功能
|
|
448
|
-
|
|
449
|
-
### 功能特性
|
|
450
|
-
|
|
451
|
-
编辑器支持以下文件上传方式:
|
|
452
|
-
|
|
453
|
-
- ✅ **图片粘贴上传**:按 `Ctrl+V` 粘贴图片,自动上传
|
|
454
|
-
- ✅ **图片拖拽上传**:拖拽图片文件到编辑器
|
|
455
|
-
- ✅ **视频拖拽上传**:拖拽视频文件到编辑器
|
|
456
|
-
- ✅ **音频拖拽上传**:拖拽音频文件到编辑器
|
|
457
|
-
- ✅ **即时预览**:上传前立即显示预览
|
|
458
|
-
- ✅ **自动替换**:上传成功后自动替换为真实 URL
|
|
459
|
-
- ✅ **失败处理**:上传失败自动删除预览
|
|
460
|
-
|
|
461
|
-
### 支持的文件类型
|
|
462
|
-
|
|
463
|
-
| 类型 | 扩展名 | 操作方式 |
|
|
464
|
-
|------|--------|---------|
|
|
465
|
-
| 图片 | jpg, jpeg, png, gif, webp, svg, bmp | 粘贴、拖拽 |
|
|
466
|
-
| 视频 | mp4, webm, ogg | 拖拽 |
|
|
467
|
-
| 音频 | mp3, wav, ogg | 拖拽 |
|
|
468
|
-
|
|
469
|
-
### 实现步骤
|
|
470
|
-
|
|
471
|
-
#### 1. 定义上传处理器
|
|
472
|
-
|
|
473
|
-
```javascript
|
|
474
|
-
const handleFileUpload = async (file) => {
|
|
475
|
-
try {
|
|
476
|
-
// 验证文件大小
|
|
477
|
-
const maxSize = 100 * 1024 * 1024 // 100MB
|
|
478
|
-
if (file.size > maxSize) {
|
|
479
|
-
console.error('文件大小不能超过100MB')
|
|
480
|
-
return null
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
// 创建 FormData
|
|
484
|
-
const formData = new FormData()
|
|
485
|
-
formData.append('file', file)
|
|
486
|
-
|
|
487
|
-
// 调用上传 API
|
|
488
|
-
const response = await fetch('/api/upload', {
|
|
489
|
-
method: 'POST',
|
|
490
|
-
body: formData,
|
|
491
|
-
})
|
|
492
|
-
|
|
493
|
-
const result = await response.json()
|
|
494
|
-
|
|
495
|
-
// 返回文件 URL(成功)或 null(失败)
|
|
496
|
-
if (result.success) {
|
|
497
|
-
return result.fileUrl
|
|
498
|
-
} else {
|
|
499
|
-
console.error('上传失败:', result.message)
|
|
500
|
-
return null
|
|
501
|
-
}
|
|
502
|
-
} catch (error) {
|
|
503
|
-
console.error('上传异常:', error)
|
|
504
|
-
return null
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
```
|
|
508
|
-
|
|
509
|
-
#### 2. 配置扩展
|
|
510
|
-
|
|
511
|
-
```javascript
|
|
512
|
-
import { allExtensions } from 'testfox-richtext-editor'
|
|
513
|
-
|
|
514
|
-
// 为需要上传功能的扩展配置 uploadHandler
|
|
515
|
-
const configuredExtensions = allExtensions.map(ext => {
|
|
516
|
-
if (ext.name === 'image' || ext.name === 'video' || ext.name === 'audio') {
|
|
517
|
-
return ext.configure({
|
|
518
|
-
uploadHandler: handleFileUpload
|
|
519
|
-
})
|
|
520
|
-
}
|
|
521
|
-
return ext
|
|
522
|
-
})
|
|
523
|
-
```
|
|
524
|
-
|
|
525
|
-
#### 3. 创建编辑器
|
|
526
|
-
|
|
527
|
-
```javascript
|
|
528
|
-
const editor = useEditor({
|
|
529
|
-
extensions: configuredExtensions, // 使用配置好的扩展
|
|
530
|
-
content: '<p>Hello World!</p>',
|
|
531
|
-
})
|
|
532
|
-
```
|
|
533
|
-
|
|
534
|
-
### 完整示例
|
|
535
|
-
|
|
536
|
-
```vue
|
|
537
|
-
<template>
|
|
538
|
-
<RichTextEditor :editor="editor" locale="zh-CN" />
|
|
539
|
-
</template>
|
|
540
|
-
|
|
541
|
-
<script setup>
|
|
542
|
-
import { useEditor, RichTextEditor, allExtensions } from 'testfox-richtext-editor'
|
|
543
|
-
import 'testfox-richtext-editor/dist/style.css'
|
|
544
|
-
|
|
545
|
-
// 上传处理器
|
|
546
|
-
const handleFileUpload = async (file) => {
|
|
547
|
-
try {
|
|
548
|
-
const maxSize = 100 * 1024 * 1024
|
|
549
|
-
if (file.size > maxSize) {
|
|
550
|
-
alert('文件大小不能超过100MB')
|
|
551
|
-
return null
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
const formData = new FormData()
|
|
555
|
-
formData.append('file', file)
|
|
556
|
-
|
|
557
|
-
const response = await fetch('/api/upload', {
|
|
558
|
-
method: 'POST',
|
|
559
|
-
body: formData,
|
|
560
|
-
})
|
|
561
|
-
|
|
562
|
-
const result = await response.json()
|
|
563
|
-
return result.success ? result.fileUrl : null
|
|
564
|
-
} catch (error) {
|
|
565
|
-
console.error('上传失败:', error)
|
|
566
|
-
return null
|
|
567
|
-
}
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
// 配置扩展
|
|
571
|
-
const configuredExtensions = allExtensions.map(ext => {
|
|
572
|
-
if (ext.name === 'image' || ext.name === 'video' || ext.name === 'audio') {
|
|
573
|
-
return ext.configure({ uploadHandler: handleFileUpload })
|
|
574
|
-
}
|
|
575
|
-
return ext
|
|
576
|
-
})
|
|
577
|
-
|
|
578
|
-
// 创建编辑器
|
|
579
|
-
const editor = useEditor({
|
|
580
|
-
extensions: configuredExtensions,
|
|
581
|
-
content: '<p>试试粘贴图片或拖拽文件到这里...</p>',
|
|
582
|
-
})
|
|
583
|
-
</script>
|
|
584
|
-
```
|
|
585
|
-
|
|
586
|
-
### 上传流程
|
|
587
|
-
|
|
588
|
-
```
|
|
589
|
-
1. 用户粘贴图片或拖拽文件
|
|
590
|
-
↓
|
|
591
|
-
2. 创建临时预览(Blob URL)
|
|
592
|
-
↓
|
|
593
|
-
3. 立即显示预览(用户无需等待)
|
|
594
|
-
↓
|
|
595
|
-
4. 后台调用 uploadHandler 上传
|
|
596
|
-
↓
|
|
597
|
-
5. 上传成功
|
|
598
|
-
├─ 替换为真实 URL
|
|
599
|
-
└─ 释放临时 Blob URL
|
|
600
|
-
↓
|
|
601
|
-
6. 上传失败
|
|
602
|
-
├─ 删除临时预览
|
|
603
|
-
└─ 释放临时 Blob URL
|
|
604
|
-
```
|
|
605
|
-
|
|
606
|
-
### 注意事项
|
|
607
|
-
|
|
608
|
-
1. **返回值约定**
|
|
609
|
-
- 成功:必须返回文件的完整访问 URL(字符串)
|
|
610
|
-
- 失败:必须返回 `null`
|
|
611
|
-
|
|
612
|
-
2. **文件大小限制**
|
|
613
|
-
- 建议在前端验证文件大小
|
|
614
|
-
- 避免上传超大文件导致失败
|
|
615
|
-
|
|
616
|
-
3. **错误处理**
|
|
617
|
-
- 捕获所有异常并返回 `null`
|
|
618
|
-
- 提供友好的错误提示
|
|
619
|
-
|
|
620
|
-
4. **URL 格式**
|
|
621
|
-
- 返回的 URL 必须是可访问的完整 URL
|
|
622
|
-
- 例如:`https://example.com/uploads/image.jpg`
|
|
623
|
-
|
|
624
|
-
5. **异步处理**
|
|
625
|
-
- uploadHandler 必须是异步函数
|
|
626
|
-
- 使用 `async/await` 或返回 Promise
|
|
627
|
-
|
|
628
|
-
### 常见问题
|
|
629
|
-
|
|
630
|
-
**Q: 粘贴后图片消失了?**
|
|
631
|
-
|
|
632
|
-
A: 检查 uploadHandler 是否返回了正确的 URL。如果返回 `null`,图片会被删除。
|
|
633
|
-
|
|
634
|
-
**Q: 如何禁用上传功能?**
|
|
635
|
-
|
|
636
|
-
A: 不配置 uploadHandler 即可。图片会保持为 Blob URL(仅本地预览)。
|
|
637
|
-
|
|
638
|
-
**Q: 支持哪些文件类型?**
|
|
639
|
-
|
|
640
|
-
A: 图片(粘贴+拖拽)、视频(拖拽)、音频(拖拽)。可以通过扩展支持更多类型。
|
|
641
|
-
|
|
642
|
-
**Q: 如何自定义文件大小限制?**
|
|
643
|
-
|
|
644
|
-
A: 在 uploadHandler 中添加验证逻辑:
|
|
645
|
-
|
|
646
|
-
```javascript
|
|
647
|
-
const maxSize = 50 * 1024 * 1024 // 50MB
|
|
648
|
-
if (file.size > maxSize) {
|
|
649
|
-
alert('文件过大')
|
|
650
|
-
return null
|
|
651
|
-
}
|
|
652
|
-
```
|
|
653
|
-
|
|
654
|
-
**Q: 如何添加上传进度提示?**
|
|
655
|
-
|
|
656
|
-
A: 在 uploadHandler 中使用 UI 库显示进度:
|
|
657
|
-
|
|
658
|
-
```javascript
|
|
659
|
-
const handleFileUpload = async (file) => {
|
|
660
|
-
// 显示加载提示
|
|
661
|
-
showLoading('正在上传...')
|
|
662
|
-
|
|
663
|
-
try {
|
|
664
|
-
const url = await uploadToServer(file)
|
|
665
|
-
hideLoading()
|
|
666
|
-
return url
|
|
667
|
-
} catch (error) {
|
|
668
|
-
hideLoading()
|
|
669
|
-
showError('上传失败')
|
|
670
|
-
return null
|
|
671
|
-
}
|
|
672
|
-
}
|
|
673
|
-
```
|
|
674
|
-
|
|
675
|
-
## 浏览器兼容性
|
|
676
|
-
|
|
677
|
-
- Chrome/Edge: ✅ 完全支持
|
|
678
|
-
- Firefox: ✅ 完全支持
|
|
679
|
-
- Safari: ✅ 完全支持
|
|
680
|
-
- IE: ❌ 不支持
|
|
681
|
-
|
|
682
|
-
## 相关链接
|
|
683
|
-
|
|
684
|
-
- [Gitee 仓库](https://gitee.com/xzq_95/testfox-richtext-editor)
|
|
685
|
-
- [TipTap 官方文档](https://tiptap.dev/)
|
|
686
|
-
|
|
687
|
-
## 许可证
|
|
688
|
-
|
|
689
|
-
GPL-3.0
|
|
690
|
-
|
|
691
|
-
## 贡献
|
|
692
|
-
|
|
693
|
-
欢迎提交 Issue 和 Pull Request!
|
|
1
|
+
# TestFox RichText Editor
|
|
2
|
+
|
|
3
|
+
基于 TipTap 的富文本编辑器,专为 TestFox 项目设计,支持完整的主题系统和亮色/暗色模式切换。
|
|
4
|
+
|
|
5
|
+
## 特性
|
|
6
|
+
|
|
7
|
+
- ✅ 完整的富文本编辑功能
|
|
8
|
+
- ✅ 支持 40+ 种编程语言语法高亮
|
|
9
|
+
- ✅ Slash Command(斜杠命令)快捷操作
|
|
10
|
+
- ✅ 图片粘贴上传(Ctrl+V)
|
|
11
|
+
- ✅ 文件拖拽上传(图片、视频、音频)
|
|
12
|
+
- ✅ 自动上传到服务器(支持自定义上传处理器)
|
|
13
|
+
- ✅ 拖拽排序功能
|
|
14
|
+
- ✅ 表格编辑(合并单元格、调整列宽等)
|
|
15
|
+
- ✅ 多语言支持(中文/英文)
|
|
16
|
+
- ✅ 主题系统(亮色/暗色模式)
|
|
17
|
+
- ✅ 响应式设计
|
|
18
|
+
|
|
19
|
+
## 安装
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install neweditor
|
|
23
|
+
# 或
|
|
24
|
+
pnpm install neweditor
|
|
25
|
+
# 或
|
|
26
|
+
yarn add neweditor
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## 使用
|
|
30
|
+
|
|
31
|
+
### 基础使用
|
|
32
|
+
|
|
33
|
+
```vue
|
|
34
|
+
<template>
|
|
35
|
+
<RichTextEditor :editor="editor" locale="zh-CN" />
|
|
36
|
+
</template>
|
|
37
|
+
|
|
38
|
+
<script setup>
|
|
39
|
+
import { useEditor, RichTextEditor, allExtensions } from 'neweditor'
|
|
40
|
+
import 'neweditor/dist/style.css'
|
|
41
|
+
|
|
42
|
+
const editor = useEditor({
|
|
43
|
+
extensions: allExtensions,
|
|
44
|
+
content: '<p>Hello World!</p>',
|
|
45
|
+
})
|
|
46
|
+
</script>
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### 完整示例
|
|
50
|
+
|
|
51
|
+
```vue
|
|
52
|
+
<template>
|
|
53
|
+
<div class="editor-wrapper">
|
|
54
|
+
<RichTextEditor
|
|
55
|
+
:editor="editor"
|
|
56
|
+
locale="zh-CN"
|
|
57
|
+
class="my-editor"
|
|
58
|
+
/>
|
|
59
|
+
</div>
|
|
60
|
+
</template>
|
|
61
|
+
|
|
62
|
+
<script setup>
|
|
63
|
+
import { ref } from 'vue'
|
|
64
|
+
import { useEditor, RichTextEditor, allExtensions } from 'neweditor'
|
|
65
|
+
import 'neweditor/dist/style.css'
|
|
66
|
+
|
|
67
|
+
const content = ref('<p>初始内容</p>')
|
|
68
|
+
|
|
69
|
+
const editor = useEditor({
|
|
70
|
+
extensions: allExtensions,
|
|
71
|
+
content: content.value,
|
|
72
|
+
editable: true,
|
|
73
|
+
autofocus: false,
|
|
74
|
+
editorProps: {
|
|
75
|
+
attributes: {
|
|
76
|
+
'data-placeholder': '请输入内容...',
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
onUpdate: ({ editor }) => {
|
|
80
|
+
content.value = editor.getHTML()
|
|
81
|
+
},
|
|
82
|
+
onFocus: ({ editor }) => {
|
|
83
|
+
console.log('编辑器获得焦点')
|
|
84
|
+
},
|
|
85
|
+
onBlur: ({ editor }) => {
|
|
86
|
+
console.log('编辑器失去焦点')
|
|
87
|
+
},
|
|
88
|
+
})
|
|
89
|
+
</script>
|
|
90
|
+
|
|
91
|
+
<style>
|
|
92
|
+
.editor-wrapper {
|
|
93
|
+
border: 1px solid #d9d9d9;
|
|
94
|
+
border-radius: 4px;
|
|
95
|
+
}
|
|
96
|
+
</style>
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### 带文件上传功能的示例
|
|
100
|
+
|
|
101
|
+
```vue
|
|
102
|
+
<template>
|
|
103
|
+
<div class="editor-wrapper">
|
|
104
|
+
<RichTextEditor
|
|
105
|
+
:editor="editor"
|
|
106
|
+
locale="zh-CN"
|
|
107
|
+
class="my-editor"
|
|
108
|
+
/>
|
|
109
|
+
</div>
|
|
110
|
+
</template>
|
|
111
|
+
|
|
112
|
+
<script setup>
|
|
113
|
+
import { ref } from 'vue'
|
|
114
|
+
import { useEditor, RichTextEditor, allExtensions } from 'testfox-richtext-editor'
|
|
115
|
+
import 'testfox-richtext-editor/dist/style.css'
|
|
116
|
+
|
|
117
|
+
const content = ref('<p>初始内容</p>')
|
|
118
|
+
|
|
119
|
+
// 自定义文件上传处理器
|
|
120
|
+
const handleFileUpload = async (file) => {
|
|
121
|
+
try {
|
|
122
|
+
// 1. 验证文件大小(例如限制 100MB)
|
|
123
|
+
const maxSize = 100 * 1024 * 1024
|
|
124
|
+
if (file.size > maxSize) {
|
|
125
|
+
console.error('文件大小不能超过100MB')
|
|
126
|
+
return null
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// 2. 创建 FormData
|
|
130
|
+
const formData = new FormData()
|
|
131
|
+
formData.append('file', file)
|
|
132
|
+
|
|
133
|
+
// 3. 调用你的上传 API
|
|
134
|
+
const response = await fetch('/api/upload', {
|
|
135
|
+
method: 'POST',
|
|
136
|
+
body: formData,
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
const result = await response.json()
|
|
140
|
+
|
|
141
|
+
// 4. 返回文件 URL(成功)或 null(失败)
|
|
142
|
+
if (result.success) {
|
|
143
|
+
return result.fileUrl // 返回文件的访问 URL
|
|
144
|
+
} else {
|
|
145
|
+
console.error('上传失败:', result.message)
|
|
146
|
+
return null
|
|
147
|
+
}
|
|
148
|
+
} catch (error) {
|
|
149
|
+
console.error('上传异常:', error)
|
|
150
|
+
return null
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// 配置扩展并传递上传处理器
|
|
155
|
+
const configuredExtensions = allExtensions.map(ext => {
|
|
156
|
+
// 为图片、视频、音频扩展配置上传处理器
|
|
157
|
+
if (ext.name === 'image' || ext.name === 'video' || ext.name === 'audio') {
|
|
158
|
+
return ext.configure({
|
|
159
|
+
uploadHandler: handleFileUpload
|
|
160
|
+
})
|
|
161
|
+
}
|
|
162
|
+
return ext
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
const editor = useEditor({
|
|
166
|
+
extensions: configuredExtensions, // 使用配置好的扩展
|
|
167
|
+
content: content.value,
|
|
168
|
+
editable: true,
|
|
169
|
+
onUpdate: ({ editor }) => {
|
|
170
|
+
content.value = editor.getHTML()
|
|
171
|
+
},
|
|
172
|
+
})
|
|
173
|
+
</script>
|
|
174
|
+
|
|
175
|
+
<style>
|
|
176
|
+
.editor-wrapper {
|
|
177
|
+
border: 1px solid #d9d9d9;
|
|
178
|
+
border-radius: 4px;
|
|
179
|
+
}
|
|
180
|
+
</style>
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
## API
|
|
184
|
+
|
|
185
|
+
### useEditor(options)
|
|
186
|
+
|
|
187
|
+
创建编辑器实例的 Hook。
|
|
188
|
+
|
|
189
|
+
**参数:**
|
|
190
|
+
|
|
191
|
+
```typescript
|
|
192
|
+
interface EditorOptions {
|
|
193
|
+
extensions: Extension[] // 扩展列表
|
|
194
|
+
content?: string // 初始内容(HTML)
|
|
195
|
+
editable?: boolean // 是否可编辑
|
|
196
|
+
autofocus?: boolean // 是否自动聚焦
|
|
197
|
+
editorProps?: EditorProps // 编辑器属性
|
|
198
|
+
onUpdate?: (props) => void // 内容更新回调
|
|
199
|
+
onFocus?: (props) => void // 获得焦点回调
|
|
200
|
+
onBlur?: (props) => void // 失去焦点回调
|
|
201
|
+
onCreate?: (props) => void // 创建完成回调
|
|
202
|
+
}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
**返回值:**
|
|
206
|
+
|
|
207
|
+
返回一个响应式的编辑器实例。
|
|
208
|
+
|
|
209
|
+
### 文件上传配置
|
|
210
|
+
|
|
211
|
+
#### uploadHandler
|
|
212
|
+
|
|
213
|
+
文件上传处理器函数,用于处理图片、视频、音频的上传。
|
|
214
|
+
|
|
215
|
+
**类型签名:**
|
|
216
|
+
|
|
217
|
+
```typescript
|
|
218
|
+
type UploadHandler = (file: File) => Promise<string | null>
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
**参数:**
|
|
222
|
+
- `file`: File 对象,包含要上传的文件
|
|
223
|
+
|
|
224
|
+
**返回值:**
|
|
225
|
+
- 成功:返回文件的访问 URL(字符串)
|
|
226
|
+
- 失败:返回 `null`
|
|
227
|
+
|
|
228
|
+
**配置方式:**
|
|
229
|
+
|
|
230
|
+
```javascript
|
|
231
|
+
// 为需要上传功能的扩展配置 uploadHandler
|
|
232
|
+
const configuredExtensions = allExtensions.map(ext => {
|
|
233
|
+
if (ext.name === 'image' || ext.name === 'video' || ext.name === 'audio') {
|
|
234
|
+
return ext.configure({
|
|
235
|
+
uploadHandler: async (file) => {
|
|
236
|
+
// 你的上传逻辑
|
|
237
|
+
const url = await uploadToServer(file)
|
|
238
|
+
return url // 返回 URL 或 null
|
|
239
|
+
}
|
|
240
|
+
})
|
|
241
|
+
}
|
|
242
|
+
return ext
|
|
243
|
+
})
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
**工作流程:**
|
|
247
|
+
|
|
248
|
+
1. 用户粘贴图片或拖拽文件到编辑器
|
|
249
|
+
2. 编辑器创建临时预览(Blob URL)
|
|
250
|
+
3. 立即显示预览,用户无需等待
|
|
251
|
+
4. 后台调用 `uploadHandler` 上传文件
|
|
252
|
+
5. 上传成功后,自动替换为真实 URL
|
|
253
|
+
6. 上传失败则删除临时预览
|
|
254
|
+
|
|
255
|
+
**示例实现:**
|
|
256
|
+
|
|
257
|
+
```javascript
|
|
258
|
+
const handleFileUpload = async (file) => {
|
|
259
|
+
try {
|
|
260
|
+
// 验证文件大小
|
|
261
|
+
const maxSize = 100 * 1024 * 1024 // 100MB
|
|
262
|
+
if (file.size > maxSize) {
|
|
263
|
+
console.error('文件过大')
|
|
264
|
+
return null
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// 上传到服务器
|
|
268
|
+
const formData = new FormData()
|
|
269
|
+
formData.append('file', file)
|
|
270
|
+
|
|
271
|
+
const response = await fetch('/api/upload', {
|
|
272
|
+
method: 'POST',
|
|
273
|
+
body: formData,
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
const result = await response.json()
|
|
277
|
+
|
|
278
|
+
// 返回文件 URL
|
|
279
|
+
return result.success ? result.fileUrl : null
|
|
280
|
+
} catch (error) {
|
|
281
|
+
console.error('上传失败:', error)
|
|
282
|
+
return null
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
### RichTextEditor 组件
|
|
288
|
+
|
|
289
|
+
编辑器 UI 组件。
|
|
290
|
+
|
|
291
|
+
**Props:**
|
|
292
|
+
|
|
293
|
+
| 属性 | 类型 | 默认值 | 说明 |
|
|
294
|
+
|------|------|--------|------|
|
|
295
|
+
| `editor` | Editor | - | 编辑器实例(必需) |
|
|
296
|
+
| `locale` | String | `'zh-CN'` | 语言设置(zh-CN/en) |
|
|
297
|
+
|
|
298
|
+
### allExtensions
|
|
299
|
+
|
|
300
|
+
包含所有内置扩展的数组,包括:
|
|
301
|
+
|
|
302
|
+
- 文本格式化(粗体、斜体、下划线、删除线等)
|
|
303
|
+
- 标题(H1-H6)
|
|
304
|
+
- 列表(有序、无序、任务列表)
|
|
305
|
+
- 代码(行内代码、代码块)
|
|
306
|
+
- 表格
|
|
307
|
+
- 图片、视频、音频
|
|
308
|
+
- 链接
|
|
309
|
+
- 引用块
|
|
310
|
+
- 分栏布局
|
|
311
|
+
- 折叠面板
|
|
312
|
+
- Slash Command
|
|
313
|
+
- 拖拽排序
|
|
314
|
+
- 搜索和替换
|
|
315
|
+
- 等等...
|
|
316
|
+
|
|
317
|
+
## 编辑器功能
|
|
318
|
+
|
|
319
|
+
### 文本格式化
|
|
320
|
+
- 粗体、斜体、下划线、删除线、上标、下标
|
|
321
|
+
- 标题(H1-H6)
|
|
322
|
+
- 文本颜色和背景色
|
|
323
|
+
- 字体大小
|
|
324
|
+
- 文本对齐
|
|
325
|
+
- 清除格式
|
|
326
|
+
- 格式刷
|
|
327
|
+
|
|
328
|
+
### 列表和引用
|
|
329
|
+
- 无序列表
|
|
330
|
+
- 有序列表
|
|
331
|
+
- 任务列表
|
|
332
|
+
- 引用块
|
|
333
|
+
- 缩进
|
|
334
|
+
|
|
335
|
+
### 代码
|
|
336
|
+
- 行内代码
|
|
337
|
+
- 代码块(支持 40+ 种语言语法高亮)
|
|
338
|
+
|
|
339
|
+
### 媒体内容
|
|
340
|
+
- 图片(支持粘贴、拖拽上传、调整大小)
|
|
341
|
+
- 视频(支持拖拽上传)
|
|
342
|
+
- 音频(支持拖拽上传)
|
|
343
|
+
- iframe
|
|
344
|
+
|
|
345
|
+
### 表格
|
|
346
|
+
- 创建表格
|
|
347
|
+
- 添加/删除行和列
|
|
348
|
+
- 合并/拆分单元格
|
|
349
|
+
- 调整列宽
|
|
350
|
+
- 表格拖拽排序
|
|
351
|
+
|
|
352
|
+
### 快捷功能
|
|
353
|
+
- Slash Command(输入 `/` 或 `、` 触发)
|
|
354
|
+
- 拖拽排序
|
|
355
|
+
- 搜索和替换
|
|
356
|
+
- 撤销/重做
|
|
357
|
+
- 字符统计
|
|
358
|
+
|
|
359
|
+
## 快捷键
|
|
360
|
+
|
|
361
|
+
| 快捷键 | 功能 |
|
|
362
|
+
|--------|------|
|
|
363
|
+
| `Ctrl/Cmd + B` | 粗体 |
|
|
364
|
+
| `Ctrl/Cmd + I` | 斜体 |
|
|
365
|
+
| `Ctrl/Cmd + U` | 下划线 |
|
|
366
|
+
| `Ctrl/Cmd + Shift + X` | 删除线 |
|
|
367
|
+
| `Ctrl/Cmd + E` | 行内代码 |
|
|
368
|
+
| `Ctrl/Cmd + Z` | 撤销 |
|
|
369
|
+
| `Ctrl/Cmd + Shift + Z` | 重做 |
|
|
370
|
+
| `Ctrl/Cmd + K` | 插入链接 |
|
|
371
|
+
| `Ctrl/Cmd + V` | 粘贴图片 |
|
|
372
|
+
| `/` 或 `、` | Slash Command |
|
|
373
|
+
| `Tab` | 增加缩进 |
|
|
374
|
+
| `Shift + Tab` | 减少缩进 |
|
|
375
|
+
|
|
376
|
+
## 主题系统
|
|
377
|
+
|
|
378
|
+
编辑器支持亮色和暗色两种主题模式,通过 CSS 变量控制。
|
|
379
|
+
|
|
380
|
+
### 主题变量
|
|
381
|
+
|
|
382
|
+
```css
|
|
383
|
+
/* 亮色主题 */
|
|
384
|
+
.theme-light .testfox-rich-text-editor {
|
|
385
|
+
--editor-primary-color: #9373ee;
|
|
386
|
+
--editor-text-color: rgba(0, 0, 0, 0.85);
|
|
387
|
+
--editor-background-color: #ffffff;
|
|
388
|
+
--editor-border-color: #f0f0f0;
|
|
389
|
+
/* ... 更多变量 */
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/* 暗色主题 */
|
|
393
|
+
.theme-dark .testfox-rich-text-editor {
|
|
394
|
+
--editor-primary-color: #9373ee;
|
|
395
|
+
--editor-text-color: rgba(255, 255, 255, 0.85);
|
|
396
|
+
--editor-background-color: #1f1f1f;
|
|
397
|
+
--editor-border-color: #303030;
|
|
398
|
+
/* ... 更多变量 */
|
|
399
|
+
}
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
### 自定义主题
|
|
403
|
+
|
|
404
|
+
可以通过覆盖 CSS 变量来自定义主题:
|
|
405
|
+
|
|
406
|
+
```css
|
|
407
|
+
.my-editor {
|
|
408
|
+
--editor-primary-color: #your-color;
|
|
409
|
+
--editor-background-color: #your-bg;
|
|
410
|
+
}
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
## 开发
|
|
414
|
+
|
|
415
|
+
### 安装依赖
|
|
416
|
+
|
|
417
|
+
```bash
|
|
418
|
+
npm install
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
### 开发模式
|
|
422
|
+
|
|
423
|
+
```bash
|
|
424
|
+
npm run dev
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
### 构建
|
|
428
|
+
|
|
429
|
+
```bash
|
|
430
|
+
npm run build
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
### 类型检查
|
|
434
|
+
|
|
435
|
+
```bash
|
|
436
|
+
npm run typecheck
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
## 扩展开发
|
|
440
|
+
|
|
441
|
+
如需为编辑器添加新功能,可以创建自定义扩展。详细的扩展开发指南请参考:
|
|
442
|
+
|
|
443
|
+
- [扩展开发文档](./docs/extension.md)
|
|
444
|
+
- [Slash Command 指南](./docs/slash-command-guide.md)
|
|
445
|
+
- [Slash Command 示例](./docs/slash-command-examples.md)
|
|
446
|
+
|
|
447
|
+
## 文件上传功能
|
|
448
|
+
|
|
449
|
+
### 功能特性
|
|
450
|
+
|
|
451
|
+
编辑器支持以下文件上传方式:
|
|
452
|
+
|
|
453
|
+
- ✅ **图片粘贴上传**:按 `Ctrl+V` 粘贴图片,自动上传
|
|
454
|
+
- ✅ **图片拖拽上传**:拖拽图片文件到编辑器
|
|
455
|
+
- ✅ **视频拖拽上传**:拖拽视频文件到编辑器
|
|
456
|
+
- ✅ **音频拖拽上传**:拖拽音频文件到编辑器
|
|
457
|
+
- ✅ **即时预览**:上传前立即显示预览
|
|
458
|
+
- ✅ **自动替换**:上传成功后自动替换为真实 URL
|
|
459
|
+
- ✅ **失败处理**:上传失败自动删除预览
|
|
460
|
+
|
|
461
|
+
### 支持的文件类型
|
|
462
|
+
|
|
463
|
+
| 类型 | 扩展名 | 操作方式 |
|
|
464
|
+
|------|--------|---------|
|
|
465
|
+
| 图片 | jpg, jpeg, png, gif, webp, svg, bmp | 粘贴、拖拽 |
|
|
466
|
+
| 视频 | mp4, webm, ogg | 拖拽 |
|
|
467
|
+
| 音频 | mp3, wav, ogg | 拖拽 |
|
|
468
|
+
|
|
469
|
+
### 实现步骤
|
|
470
|
+
|
|
471
|
+
#### 1. 定义上传处理器
|
|
472
|
+
|
|
473
|
+
```javascript
|
|
474
|
+
const handleFileUpload = async (file) => {
|
|
475
|
+
try {
|
|
476
|
+
// 验证文件大小
|
|
477
|
+
const maxSize = 100 * 1024 * 1024 // 100MB
|
|
478
|
+
if (file.size > maxSize) {
|
|
479
|
+
console.error('文件大小不能超过100MB')
|
|
480
|
+
return null
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// 创建 FormData
|
|
484
|
+
const formData = new FormData()
|
|
485
|
+
formData.append('file', file)
|
|
486
|
+
|
|
487
|
+
// 调用上传 API
|
|
488
|
+
const response = await fetch('/api/upload', {
|
|
489
|
+
method: 'POST',
|
|
490
|
+
body: formData,
|
|
491
|
+
})
|
|
492
|
+
|
|
493
|
+
const result = await response.json()
|
|
494
|
+
|
|
495
|
+
// 返回文件 URL(成功)或 null(失败)
|
|
496
|
+
if (result.success) {
|
|
497
|
+
return result.fileUrl
|
|
498
|
+
} else {
|
|
499
|
+
console.error('上传失败:', result.message)
|
|
500
|
+
return null
|
|
501
|
+
}
|
|
502
|
+
} catch (error) {
|
|
503
|
+
console.error('上传异常:', error)
|
|
504
|
+
return null
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
#### 2. 配置扩展
|
|
510
|
+
|
|
511
|
+
```javascript
|
|
512
|
+
import { allExtensions } from 'testfox-richtext-editor'
|
|
513
|
+
|
|
514
|
+
// 为需要上传功能的扩展配置 uploadHandler
|
|
515
|
+
const configuredExtensions = allExtensions.map(ext => {
|
|
516
|
+
if (ext.name === 'image' || ext.name === 'video' || ext.name === 'audio') {
|
|
517
|
+
return ext.configure({
|
|
518
|
+
uploadHandler: handleFileUpload
|
|
519
|
+
})
|
|
520
|
+
}
|
|
521
|
+
return ext
|
|
522
|
+
})
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
#### 3. 创建编辑器
|
|
526
|
+
|
|
527
|
+
```javascript
|
|
528
|
+
const editor = useEditor({
|
|
529
|
+
extensions: configuredExtensions, // 使用配置好的扩展
|
|
530
|
+
content: '<p>Hello World!</p>',
|
|
531
|
+
})
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
### 完整示例
|
|
535
|
+
|
|
536
|
+
```vue
|
|
537
|
+
<template>
|
|
538
|
+
<RichTextEditor :editor="editor" locale="zh-CN" />
|
|
539
|
+
</template>
|
|
540
|
+
|
|
541
|
+
<script setup>
|
|
542
|
+
import { useEditor, RichTextEditor, allExtensions } from 'testfox-richtext-editor'
|
|
543
|
+
import 'testfox-richtext-editor/dist/style.css'
|
|
544
|
+
|
|
545
|
+
// 上传处理器
|
|
546
|
+
const handleFileUpload = async (file) => {
|
|
547
|
+
try {
|
|
548
|
+
const maxSize = 100 * 1024 * 1024
|
|
549
|
+
if (file.size > maxSize) {
|
|
550
|
+
alert('文件大小不能超过100MB')
|
|
551
|
+
return null
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
const formData = new FormData()
|
|
555
|
+
formData.append('file', file)
|
|
556
|
+
|
|
557
|
+
const response = await fetch('/api/upload', {
|
|
558
|
+
method: 'POST',
|
|
559
|
+
body: formData,
|
|
560
|
+
})
|
|
561
|
+
|
|
562
|
+
const result = await response.json()
|
|
563
|
+
return result.success ? result.fileUrl : null
|
|
564
|
+
} catch (error) {
|
|
565
|
+
console.error('上传失败:', error)
|
|
566
|
+
return null
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// 配置扩展
|
|
571
|
+
const configuredExtensions = allExtensions.map(ext => {
|
|
572
|
+
if (ext.name === 'image' || ext.name === 'video' || ext.name === 'audio') {
|
|
573
|
+
return ext.configure({ uploadHandler: handleFileUpload })
|
|
574
|
+
}
|
|
575
|
+
return ext
|
|
576
|
+
})
|
|
577
|
+
|
|
578
|
+
// 创建编辑器
|
|
579
|
+
const editor = useEditor({
|
|
580
|
+
extensions: configuredExtensions,
|
|
581
|
+
content: '<p>试试粘贴图片或拖拽文件到这里...</p>',
|
|
582
|
+
})
|
|
583
|
+
</script>
|
|
584
|
+
```
|
|
585
|
+
|
|
586
|
+
### 上传流程
|
|
587
|
+
|
|
588
|
+
```
|
|
589
|
+
1. 用户粘贴图片或拖拽文件
|
|
590
|
+
↓
|
|
591
|
+
2. 创建临时预览(Blob URL)
|
|
592
|
+
↓
|
|
593
|
+
3. 立即显示预览(用户无需等待)
|
|
594
|
+
↓
|
|
595
|
+
4. 后台调用 uploadHandler 上传
|
|
596
|
+
↓
|
|
597
|
+
5. 上传成功
|
|
598
|
+
├─ 替换为真实 URL
|
|
599
|
+
└─ 释放临时 Blob URL
|
|
600
|
+
↓
|
|
601
|
+
6. 上传失败
|
|
602
|
+
├─ 删除临时预览
|
|
603
|
+
└─ 释放临时 Blob URL
|
|
604
|
+
```
|
|
605
|
+
|
|
606
|
+
### 注意事项
|
|
607
|
+
|
|
608
|
+
1. **返回值约定**
|
|
609
|
+
- 成功:必须返回文件的完整访问 URL(字符串)
|
|
610
|
+
- 失败:必须返回 `null`
|
|
611
|
+
|
|
612
|
+
2. **文件大小限制**
|
|
613
|
+
- 建议在前端验证文件大小
|
|
614
|
+
- 避免上传超大文件导致失败
|
|
615
|
+
|
|
616
|
+
3. **错误处理**
|
|
617
|
+
- 捕获所有异常并返回 `null`
|
|
618
|
+
- 提供友好的错误提示
|
|
619
|
+
|
|
620
|
+
4. **URL 格式**
|
|
621
|
+
- 返回的 URL 必须是可访问的完整 URL
|
|
622
|
+
- 例如:`https://example.com/uploads/image.jpg`
|
|
623
|
+
|
|
624
|
+
5. **异步处理**
|
|
625
|
+
- uploadHandler 必须是异步函数
|
|
626
|
+
- 使用 `async/await` 或返回 Promise
|
|
627
|
+
|
|
628
|
+
### 常见问题
|
|
629
|
+
|
|
630
|
+
**Q: 粘贴后图片消失了?**
|
|
631
|
+
|
|
632
|
+
A: 检查 uploadHandler 是否返回了正确的 URL。如果返回 `null`,图片会被删除。
|
|
633
|
+
|
|
634
|
+
**Q: 如何禁用上传功能?**
|
|
635
|
+
|
|
636
|
+
A: 不配置 uploadHandler 即可。图片会保持为 Blob URL(仅本地预览)。
|
|
637
|
+
|
|
638
|
+
**Q: 支持哪些文件类型?**
|
|
639
|
+
|
|
640
|
+
A: 图片(粘贴+拖拽)、视频(拖拽)、音频(拖拽)。可以通过扩展支持更多类型。
|
|
641
|
+
|
|
642
|
+
**Q: 如何自定义文件大小限制?**
|
|
643
|
+
|
|
644
|
+
A: 在 uploadHandler 中添加验证逻辑:
|
|
645
|
+
|
|
646
|
+
```javascript
|
|
647
|
+
const maxSize = 50 * 1024 * 1024 // 50MB
|
|
648
|
+
if (file.size > maxSize) {
|
|
649
|
+
alert('文件过大')
|
|
650
|
+
return null
|
|
651
|
+
}
|
|
652
|
+
```
|
|
653
|
+
|
|
654
|
+
**Q: 如何添加上传进度提示?**
|
|
655
|
+
|
|
656
|
+
A: 在 uploadHandler 中使用 UI 库显示进度:
|
|
657
|
+
|
|
658
|
+
```javascript
|
|
659
|
+
const handleFileUpload = async (file) => {
|
|
660
|
+
// 显示加载提示
|
|
661
|
+
showLoading('正在上传...')
|
|
662
|
+
|
|
663
|
+
try {
|
|
664
|
+
const url = await uploadToServer(file)
|
|
665
|
+
hideLoading()
|
|
666
|
+
return url
|
|
667
|
+
} catch (error) {
|
|
668
|
+
hideLoading()
|
|
669
|
+
showError('上传失败')
|
|
670
|
+
return null
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
```
|
|
674
|
+
|
|
675
|
+
## 浏览器兼容性
|
|
676
|
+
|
|
677
|
+
- Chrome/Edge: ✅ 完全支持
|
|
678
|
+
- Firefox: ✅ 完全支持
|
|
679
|
+
- Safari: ✅ 完全支持
|
|
680
|
+
- IE: ❌ 不支持
|
|
681
|
+
|
|
682
|
+
## 相关链接
|
|
683
|
+
|
|
684
|
+
- [Gitee 仓库](https://gitee.com/xzq_95/testfox-richtext-editor)
|
|
685
|
+
- [TipTap 官方文档](https://tiptap.dev/)
|
|
686
|
+
|
|
687
|
+
## 许可证
|
|
688
|
+
|
|
689
|
+
GPL-3.0
|
|
690
|
+
|
|
691
|
+
## 贡献
|
|
692
|
+
|
|
693
|
+
欢迎提交 Issue 和 Pull Request!
|