Typewriter 打字器 ✍
📌 注意
v1.2.0 版本
提供解决 样式覆盖 、渲染图表 以及 自定义代码高亮样式、自定义插件 简单方案
一、我们在组件库新增了 prismjs
官方的 css 样式文件,可以在项目直接引入,解决 md 代码块高亮问题。
二、我们在组件库新增了 Mermaid.js
。用于解决 mermaid 格式
简单的图表渲染问题。
三、我们把 markdown-it
内置的 代码高亮方法 和 插件 暴露出来。方便开发者更好的集成第三方生态的 样式 和 插件
💩 md 渲染这一块,还是有较多的 问题
,比如增量更新,代码块高亮,以及复杂图表的渲染等实际项目需求。目前也在尝试更多的 解决方案
。💩
💩 打字器组件 请酌情使用,需求比较复杂,组件库确实不能满足还请自行处理渲染逻辑。💩
🐵 此温馨提示更新时间:2025-05-06
💌 消息
v1.1.6 版本
支持雾化效果。请及时更新尝试
此消息更新时间:2025-04-13
💔 危险
v1.0.81 版本
,以及更早版本。在流式输出的情况下,存在一定的性能问题。
在新版本中已修复,请及时升级至最新版本,以获得更好的体验。
此警告更新时间:2025-04-06
介绍
Typewriter
是一个可高度定制化开发的 打字器组件
,灵感来自 ant-design-x
官方 气泡组件
案例,将打字方法剥离出来。支持 Markdown 渲染 和 动态打字效果。
💌 消息
🐱 打字器组件会在组件的生命周期中会自动销毁,不用担心内存泄漏,请放心使用。
代码演示
基本使用
基础用法。
<template>
<ClientOnly>
<Typewriter content="content 属性设置打字器内容" />
</ClientOnly>
</template>
2
3
4
5
Markdown 渲染
通过 isMarkdown
属性控制是否启用 Markdown 渲染模式。
<script setup lang="ts">
const markdownText = ref(`#### 标题 \n 这是一个 Markdown 示例。\n - 列表项 1 \n - 列表项 2 **粗体文本** 和 *斜体文本* \n \`\`\`javascript \n console.log('Hello, world!'); \n \`\`\``)
</script>
<template>
<ClientOnly>
<Typewriter :content="markdownText" :is-markdown="true" />
</ClientOnly>
</template>
2
3
4
5
6
7
8
9
MD-代码块高亮(v1.2.0 新增)
提供一个内置的样式
// 导入 Prism 语法高亮的不同主题样式(基于 vue-element-plus-x 插件提供的样式文件)
// 每个文件对应一种独立的代码高亮主题风格,可根据项目需求选择启用
// 1. Coy 主题(简约浅色风格,适合日常阅读)
import 'vue-element-plus-x/styles/prism-coy.min.css'
// 2. Dark 主题(深色背景主题,适合夜间模式或低光环境)
import 'vue-element-plus-x/styles/prism-dark.min.css'
// 3. Funky 主题(鲜艳色彩风格,代码语法高亮对比强烈)
import 'vue-element-plus-x/styles/prism-funky.min.css'
// 4. Okaidia 主题(深色高对比度主题,注重代码结构区分)
import 'vue-element-plus-x/styles/prism-okaidia.min.css'
// 5. Solarized Light 主题(柔和浅色主题,基于 Solarized 配色方案)
import 'vue-element-plus-x/styles/prism-solarizedlight.min.css'
// 6. Tomorrow 主题(现代简约风格,适合宽屏和大字体显示)
import 'vue-element-plus-x/styles/prism-tomorrow.min.css'
// 7. Twilight 主题(黄昏色调主题,介于明暗之间的平衡风格)
import 'vue-element-plus-x/styles/prism-twilight.min.css'
// 8. Prism 核心基础样式(必须导入,包含语法高亮的基础样式和结构)
import 'vue-element-plus-x/styles/prism.min.css'
/* 使用说明:
1. prism.min.css 是 Prism 的核心样式,包含基本的代码块布局和通用样式,必须保留
2. 其他以 prism-开头的文件是不同的主题样式,可根据项目视觉设计选择 1 个或多个导入
3. 若同时导入多个主题,后导入的样式会覆盖先导入的(可通过切换类名动态切换主题)
4. 主题名称对应 Prism 官方预设主题(如 Coy、Okaidia 等),样式细节可参考 Prism 主题文档
*/
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<script setup lang="ts">
import { ref } from 'vue'
import Typewriter from 'vue-element-plus-x/src/components/Typewriter/index.vue'
// import { usePrism } from 'vue-element-plus-x/src/hooks/usePrism.js'
// import AppConfig from 'vue-element-plus-x/src/components/AppConfig/index.vue'
// 这里可以引入 Prism 的核心样式,也可以自己引入其他第三方主题样式
import 'vue-element-plus-x/styles/prism.min.css'
const markdownText = ref(`#### 标题 \n 这是一个 Markdown 示例。\n - 列表项 1 \n - 列表项 2 **粗体文本** 和 *斜体文本* \n \`\`\`js \n console.log('Hello, world!'); \n \`\`\``)
</script>
<template>
<ClientOnly>
<div>
<Typewriter :content="markdownText" :is-markdown="true" />
</div>
</ClientOnly>
</template>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
MD-插件模式(v1.2.0 新增)
如果你觉得内置的样式不好看或者内置的插件不能满足你的需求,可以通过插件模式自定定义 样式 和 插件。
你也可以自定在 markdown-it
社区中寻找自定义插件,以实现更多自定义功能。
通过 md-plugins
属性,传入 markdown-it
插件数组,即可在 markdown-it
中使用自定义插件。
通过 highlight
函数,传入 Prism 的高亮函数,或者其他高亮库,作用在 markdown-it
中使用 Prism 的高亮功能。
详细 Mermaid 格式 参见:Mermaid.js
📌 注意
md 渲染这一块暂时这样处理,后续有计划做成 豆包那种,目前没有内置样式。请耐心等待。。。🐒预计5月底至6月初上
<script setup lang="ts">
import markdownItMermaid from '@jsonlee_12138/markdown-it-mermaid'
import { ref } from 'vue'
// 这里是组件库内置的一个 代码高亮库 Prismjs,自定义的 hooks 例子。(仅供集成参考)代码地址:https://github.com/HeJiaYue520/Element-Plus-X/blob/main/packages/components/src/hooks/usePrism.ts
import { usePrism } from 'vue-element-plus-x'
// 这里可以引入 Prism 的核心样式,也可以自己引入其他第三方主题样式
import 'vue-element-plus-x/styles/prism.min.css'
const mdPlugins = [markdownItMermaid({ delay: 100, forceLegacyMathML: true })]
const highlight = usePrism()
const markdownText = ref(`#### 标题 \n 这是一个 Markdown 示例。\n - 列表项 1 \n - 列表项 2 **粗体文本** 和 *斜体文本* \n \`\`\`javascript \n console.log('Hello, world!'); \n \`\`\` \n \`\`\`mermaid
pie title Pets adopted by volunteers
"Dogs" : 386
"Cats" : 85
"Rats" : 15
\n
\`\`\`
\`\`\`mermaid
xychart-beta
title "Sales Revenue"
x-axis [jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec]
y-axis "Revenue (in $)" 4000 --> 11000
bar [5000, 6000, 7500, 8200, 9500, 10500, 11000, 10200, 9200, 8500, 7000, 6000]
line [5000, 6000, 7500, 8200, 9500, 10500, 11000, 10200, 9200, 8500, 7000, 6000]
\n
\`\`\`
`)
</script>
<template>
<ClientOnly>
<div>
<Typewriter :content="markdownText" :is-markdown="true" :md-plugins="mdPlugins" :highlight="highlight" />
</div>
</ClientOnly>
</template>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
开启打字效果
通过 typing
属性控制是否启用 打字渲染模式。typing
也可以是一个对象,设置 step 属性,控制打字每次吐字,interval 属性控制打字间隔,suffix 属性控制打字添加的后缀。
📌 注意
suffix
属性只能设置字符串,且在 isMarkdown
为 true
时失效,因为后缀会受 markdown
渲染影响,始终会另起一行进行展示,这一点在 ant-design-x
中也会出现。所以我们先暂时决定在 isMarkdown
为 true
时,不展示后缀,让打字器尽可能美观。
<script setup lang="ts">
onMounted(() => {
setContents('text')
setContents('markdown')
})
const isTyping = ref(true)
const content = ref('')
const content1 = ref('')
const markdownText = ref('')
function setContents(type: string) {
if (type === 'text') {
content.value = ''
content1.value = ''
setTimeout(() => {
content.value = 'typing 属性开启打字效果'
content1.value = 'typing 属性也可以是对象,来控制打每次打字吐字、每次打字间隔、和打字器后缀'
}, 800)
}
else if (type === 'markdown') {
markdownText.value = ''
setTimeout(() => {
markdownText.value = ` ### 🐒 is-markdown 和 typing 结合使用 \n 这是一个 Markdown 示例。\n - 列表项 1 \n - 列表项 2 **粗体文本** 和 *斜体文本* \n \`\`\`javascript \n console.log('Hello, world!'); \n \`\`\` `
}, 800)
}
}
</script>
<template>
<ClientOnly>
<div style="display: flex; flex-direction: column; gap: 8px;">
<div>
<el-button style="width: fit-content;" @click="setContents('text')">
重置文本
</el-button>
<el-button style="width: fit-content;" type="primary" @click="setContents('markdown')">
重置 markdown
</el-button>
</div>
<div style="display: flex; gap: 8px; flex-direction: column;">
<Typewriter :content="content" :typing="isTyping" />
<Typewriter :content="content1" :typing="{ step: 2, interval: 100, suffix: '💩' }" />
<Typewriter :content="markdownText" :typing="isTyping" :is-markdown="true" />
</div>
</div>
</ClientOnly>
</template>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
打字器雾化效果
通过 isFog
属性控制是否启用雾化效果。注意,该属性在 isTyping
为 true
时才生效。切回覆盖默认的 typing
后缀属性。
<script setup lang="ts">
const content = ref(`#### 标题 \n 这是一个 Markdown 示例。\n - 列表项 1 \n - 列表项 2 **粗体文本** 和 *斜体文本* \n \`\`\`javascript \n console.log('Hello, world!'); \n \`\`\``)
function setContent(type: number) {
content.value = ''
setTimeout(() => {
content.value = type === 1 ? `#### 标题 \n 这是一个 Markdown 示例。\n - 列表项 1 \n - 列表项 2 **粗体文本** 和 *斜体文本* \n \`\`\`javascript \n console.log('Hello, world!'); \n \`\`\`` : '欢迎使用 Element-Plus-X 💖'.repeat(10)
}, 800)
}
</script>
<template>
<ClientOnly>
<div style="display: flex; flex-direction: column; gap: 10px;">
<div style="display: flex; gap: 10px;">
<el-button @click="setContent(1)">
雾化 Markdown
</el-button>
<el-button @click="setContent(2)">
雾化 文本
</el-button>
</div>
<Typewriter :content="content" :is-markdown="true" is-fog typing />
</div>
</ClientOnly>
</template>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
动态更新内容
🐒 当使用 typing
属性时,更新 content
如果是之前的子集,则会继续输出,否则会重新输出。
<script setup lang="ts">
const content = ref('🥰 感谢使用 Element-Plus-X ! 你的支持,是我们开源的最强动力 ~ ')
const num = ref(1)
function setContents() {
num.value++
content.value = content.value.repeat(num.value)
if (num.value > 3) {
num.value = 1
content.value = '🥰 感谢使用 Element-Plus-X ! 你的支持,是我们开源的最强动力 ~ '
}
}
</script>
<template>
<ClientOnly>
<div style="display: flex; flex-direction: column; gap: 10px;">
<el-button style="width: fit-content;" @click="setContents">
设置 content
</el-button>
<Typewriter typing :content="content" />
</div>
</ClientOnly>
</template>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
控制打字
💩 更好的控制中断输出、继续打字和销毁等操作
你可以通过组件的 ref
实例获取以下方法和属性:
interrupt
中断打字过程typerRef.interrupt()
continue
继续未完成的打字typerRef.continue()
restart
重新开始打字typerRef.restart()
destroy
销毁组件(清理资源)typerRef.destroy()
renderedContent
获取当前渲染的内容。typerRef.renderedContent.value
isTyping
获取当前是否正在打字。typerRef.isTyping.value
progress
获取当前进度百分比。typerRef.progress.value
💡 提示
你还可以设置组件的监听事件,获取组件的状态。
@start
打字开始时触发@finish
打字结束时触发@writing
打字时触发
三个方法,默认参数返回组件实例。
<script setup lang="ts">
import type { TypewriterInstance } from 'vue-element-plus-x/types/typewriter'
import { Delete, RefreshLeft, VideoPause, VideoPlay } from '@element-plus/icons-vue'
const markdownContent = ref(`# 🔥 Typewriter 实例方法-事件 \n 😄 使你的打字器可高度定制化。\n - 更方便的控制打字器的状态 \n - 列表项 **粗体文本** 和 *斜体文本* \n \`\`\`javascript \n // 🙉 控制台可以查看相关打日志\n console.log('Hello, world!'); \n \`\`\``)
const isTypingValue = ref(false)
const progressValue = ref(0)
const typerRef = ref()
// 开始打字的监听方法
function onStart(instance: TypewriterInstance) {
console.log('开始打字:组件 ref 实例', unref(instance))
isTypingValue.value = true
}
// 打字中,进度监听方法
function onWriting(instance: TypewriterInstance) {
const progress: number = instance.progress.value
// 避免打印打多次 onWriting 事件 😂
if (progress > 90 && progress < 100) {
// 可以直接获取打字进度,可以根据打字进度,设置更炫酷的样式
// console.log('Writing', `${progress}%`)
console.log('打字中 isTyping:', instance.isTyping.value, 'progress:', progress)
}
if (~~progress === 80) {
console.log('打字中 progress 为 80% 时候的内容', instance.renderedContent.value)
}
isTypingValue.value = true
progressValue.value = ~~progress // 通过运算符~~取整 💩
}
// 监听打字结束事件
function onFinish(instance: TypewriterInstance) {
isTypingValue.value = false
console.log('打字结束 isTyping', instance.isTyping.value, 'progress:', instance.progress.value)
}
// 组件实例方法,控制 暂停打字
function onInterrupt() {
typerRef.value.interrupt()
isTypingValue.value = false
}
function onDestroy() {
typerRef.value.destroy()
isTypingValue.value = false
progressValue.value = 0
}
</script>
<template>
<ClientOnly>
<div style="display: flex; flex-direction: column; gap: 12px;">
<div style="display: flex;">
<el-button v-if="isTypingValue" type="warning" style="width: fit-content;" @click="onInterrupt">
<el-icon :size="18">
<VideoPause />
</el-icon>
<span>暂停</span>
</el-button>
<el-button v-if="!isTypingValue && (progressValue !== 0 && progressValue !== 100)" type="success" style="width: fit-content;" @click="typerRef?.continue()">
<el-icon :size="18">
<VideoPlay />
</el-icon>
<span>继续</span>
</el-button>
<el-button v-if="!isTypingValue && (progressValue === 0 || progressValue === 100)" type="primary" style="width: fit-content;" @click="typerRef?.restart()">
<el-icon :size="18">
<RefreshLeft />
</el-icon>
<span>重播</span>
</el-button>
<el-button type="danger" style="width: fit-content;" @click="onDestroy">
<el-icon><Delete /></el-icon>
<span>销毁</span>
</el-button>
</div>
<el-progress v-if="progressValue > 0 && progressValue !== 100" :duration="0" :percentage="progressValue" />
<el-progress v-if=" progressValue === 100" :percentage="100" status="success" />
<!-- 这里展示了如果是 markdown 的话,typing.suffix 会被忽略 -->
<Typewriter
ref="typerRef" :content="markdownContent" :typing="{ suffix: '💩', interval: 40 }" :is-markdown="true"
@start="onStart" @writing="onWriting" @finish="onFinish"
/>
</div>
</ClientOnly>
</template>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
属性
属性名 | 类型 | 是否必填 | 默认值 | 描述 |
---|---|---|---|---|
content | String | 否 | '' | 要展示的文本内容,支持纯文本或 Markdown 格式。 |
isMarkdown | Boolean | 否 | false | 是否启用 Markdown 渲染模式。 |
typing | Boolean | { step?: number, interval?: number, suffix?: string } | 否 | false | 是否启用打字机效果。 |
typing.step | Number | 否 | 2 | 每次打字吐多少字符。 |
typing.interval | Number | 否 | 50 | 每次打字的间隔时间 单位( ms )。 |
typing.suffix | String | 否 | '|' | 打字器后缀光标字符(仅在非 Markdown 模式下生效)。 |
isFog | Boolean | { bgColor?: string, width?: string } | 否 | false | 是否启用雾化效果,可以设置背景色和宽度。 |
事件
事件名 | 参数 | 类型 | 描述 |
---|---|---|---|
@start | ref 实例 | Function | 当打字效果开始时触发 |
@finish | ref 实例 | Function | 当打字效果完成时触发 |
@writing | ref 实例 | Function | 当打字效果进行中不断触发 |
Ref 实例方法
属性名 | 类型 | 描述 |
---|---|---|
interrupt | Function | 中断打字。 |
continue | Function | 继续未完成的打字。 |
restart | Function | 重新开始打字。 |
destroy | Function | 主动销毁打字组件。 |
renderedContent | String | 获取打字组件渲染的内容。 |
isTyping | Boolean | 是否正在打字。 |
progress | Number | 打字进度,取值范围 0 - 100。 |
功能特性
- Markdown 支持:支持渲染 Markdown 格式的文本,并应用 GitHub 风格的样式。
- 动态打字效果:可以模拟打字机的效果,逐步显示文本内容。
- 代码高亮:内置 Prism.js,支持代码块的语法高亮。
- XSS 安全:使用 DOMPurify 对 HTML 内容进行过滤,防止 XSS 攻击。
- 灵活配置:支持自定义打字速度、光标字符、后缀等参数。
- 定制化开发:支持更据组件打字的状态做定制化开发。