SwEditor
SwEditor 是基于 Tiptap(底层 ProseMirror)的富文本编辑器,面向评论 / 发帖 / 聊天等 compose 场景:v-model 双向绑定、@ 提及用户、# 话题、内置 emoji 面板、图 / 视频上传、配菜式工具栏,且SSR 安全——可直接放进 SSR / ISR 页面(编辑器本体只在客户端挂载,不拖累整页渲染、不强迫宿主页 ssr: false)。
视觉走「最小必要基础样式」——编辑框 / 工具栏 / 下拉 / 表情面板默认都肉眼可辨、体面,颜色 / 圆角 / 内距等通过 --sw-editor-* token 或项目 CSS 覆盖。
能力边界:当前实现
compose形态(文本 + 提及 + 表情 + 上传按钮)。长文 WYSIWYG(标题 / 表格 / 代码块工具栏)属document形态,已预留modeprop、按需再扩展。
基础用法
v-model 直接拿到 HTML 字符串,v-model:json 同步拿到 Tiptap JSON 结构(demo 下方双输出对照);submit-on-enter 让回车提交(Shift / Ctrl / Meta + 回车换行)。
基础用法(v-model / v-model:json + 回车提交)
<script setup lang="ts">
import { ref } from 'vue'
const content = ref('<p>你好</p>')
function onSubmit() {
console.log(content.value) // 提交时拿 HTML
}
</script>
<template>
<SwEditor v-model="content" placeholder="说点什么…" submit-on-enter @submit="onSubmit" />
</template>
也可用
v-model:json拿到 Tiptap 的 JSON 结构(与 HTML 同步),或通过ref调getHTML()/getText()/getJSON()。
@ 提及与 # 话题
传 :mention 选项对象注入搜索回调(解耦 API):输入 @ 触发用户搜索、# 触发话题搜索。customTopic: true 时,话题搜索无结果会用输入词兜底为自定义话题。点击已插入的提及触发 @mention-click(导航由业务决定,framework 不耦合路由)。
@ 提及 + # 话题(含自定义话题)
<script setup lang="ts">
import { ref } from 'vue'
const content = ref('')
// 返回 { id, label }[](可带 avatar 等额外字段)
async function fetchUsers(q: string) {
return await request('/user/search', { body: { q } })
}
async function fetchTags(q: string) {
return await request('/tag/search', { body: { q } })
}
function onMentionClick(m: { id: string; type: string; label: string }) {
// type 为 '@'(用户)或 '#'(话题),自行决定跳转
navigateTo(m.type === '@' ? `/u/${m.id}` : `/topic/${m.label}`)
}
</script>
<template>
<SwEditor
v-model="content"
:mention="{ fetchUsers, fetchTags, customTopic: true }"
placeholder="输入 @ 或 #"
@mention-click="onMentionClick"
/>
</template>
提交时用 getByChar(json, char) 从 JSON 提取提及列表:
const json = editorRef.value!.getJSON()
const atUsers = editorRef.value!.getByChar(json, '@') // [{ id, label, char }]
const hashTags = editorRef.value!.getByChar(json, '#')
内置工具栏 + 表情面板
:tools 配菜式渲染内置通用按钮(mention / topic / emoji / submit,以及上传的 image / video);#toolbar 作用域插槽放业务按钮(如打赏),夹在工具与发送之间。表情面板内置一套精选 native-unicode 表情(零图片资产、随编辑器核心懒加载);要换风格 / 图包用 #emoji 插槽整体替换。
内置工具栏 + 表情 + 业务按钮
<template>
<SwEditor
v-model="content"
:mention="{ fetchUsers }"
:tools="['mention', 'topic', 'emoji', 'submit']"
@submit="onSend"
>
<!-- 业务按钮:拿 editor 实例 + insert 命令 -->
<template #toolbar="{ insert }">
<button type="button" @click="insert('🎁')">打赏</button>
</template>
</SwEditor>
</template>
图 / 视频上传
image / video 工具按钮内部包 SwUploadImage / SwUploadVideo,业务经 :upload 注入上传 / 查询函数。上传成功 emit upload({ type, result }) 交业务自行处置(当独立消息发、或自行插入),framework 不替你把媒体插进编辑器内容(与社交站「图 / 视频作独立消息」用法一致)。
<template>
<SwEditor
v-model="content"
:tools="['image', 'video', 'submit']"
:upload="{
image: (file, thumbnail) => uploadImage(file, thumbnail),
video: { upload: chunkUpload, query: queryVideo },
}"
@upload="(p) => sendMedia(p.type, p.result)"
@upload-error="(e) => toast.error(e.message)"
/>
</template>
字数上限与只读
limit 是 CharacterCount 硬上限——超出的输入直接被拦,不是仅提示;editable 切只读(阅读态 / 详情展示复用同一渲染)。autofocus 挂载即聚焦,单编辑器页可用,多编辑器 / 文档页勿开(抢焦点)。
字数硬上限(limit)+ 只读切换(editable)
薄壳扩展(extensions)
extensions 从业务侧注入额外 Tiptap 扩展(薄壳扩展,不改 framework 源码;内置已含 StarterKit / Placeholder / CharacterCount / Mention,别重复注入)。扩展包属新增依赖,按依赖管控先报批再装:
<script setup lang="ts">
import Highlight from '@tiptap/extension-highlight'
const extensions = [Highlight.configure({ multicolor: true })]
</script>
<template>
<SwEditor v-model="content" :extensions="extensions" />
</template>
样式定制 Token
视觉品牌经 --sw-editor-* token 覆盖(fallback 即默认视觉),无需 :deep 穿透:
| Token | 默认值 | 作用点 |
|---|---|---|
--sw-editor-bg | var(--background) | 编辑区背景 |
--sw-editor-border | var(--input) | 外框边框色(1px 宽度常驻;focus 只变色、零 reflow) |
--sw-editor-radius | var(--radius) | 外框圆角 |
--sw-editor-padding | 0.625rem 0.75rem | 编辑区内边距 |
--sw-editor-min-height | 4rem | 编辑区最小高度(SSR 占位壳同高,防 hydration 跳动 / CLS) |
--sw-editor-mention-color | var(--primary) | @提及 / #话题 的文字色 |
/* 深色卡片里的编辑器:整体压暗 + 提及用品牌辅色 */
.comment-card .sw-editor {
--sw-editor-bg: var(--card);
--sw-editor-border: var(--border);
--sw-editor-min-height: 6rem;
--sw-editor-mention-color: var(--brand-gold);
}
Props
| 名称 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
modelValue | string | '' | v-model,编辑器内容 HTML | |
json(v-model:json) | object | null | null | 编辑器内容的 Tiptap JSON | |
placeholder | string | '' | 空状态占位文本 | |
limit | number | — | 最大字符数(CharacterCount 硬限制,不破坏已有格式) | |
submitOnEnter | boolean | false | 回车触发 submit(Shift/Ctrl/Meta+回车换行) | |
editable | boolean | true | 是否可编辑(false 为只读) | |
autofocus | boolean | false | 挂载后自动聚焦 | |
mode | 'compose' | 'document' | 'compose' | 形态档位(当前实现 compose;document 预留) | |
mention | { fetchUsers?, fetchTags?, customTopic? } | — | 提及配置;缺省不启用 @ / # | |
tools | EditorTool[] | — | 内置工具栏按钮:'mention'|'topic'|'emoji'|'image'|'video'|'submit';缺省不显示工具栏 | |
upload | EditorUploadConfig | — | image / video 工具的上传函数注入;缺对应配置则不渲染该按钮 | |
styles | Record<string, string> | {} | 应用到编辑区的内联样式(如 minHeight) | |
extensions | unknown[] | — | 额外 Tiptap 扩展(薄壳扩展,不改源码) |
Events
| 事件 | 载荷 | 说明 |
|---|---|---|
update:modelValue | string | 内容变化(HTML) |
update:json | object | 内容变化(Tiptap JSON) |
focus / blur | — | 聚焦 / 失焦 |
submit | — | 按回车(submitOnEnter 开启时)或点击内置工具栏发送按钮(:tools 含 'submit')时触发 |
ready | Editor | 编辑器实例就绪 |
mention-click | { id, type, label } | 点击已插入的提及(type 为 '@' / '#') |
upload | { type, result } | 图 / 视频上传成功(type 为 'image' / 'video') |
upload-error | Error | 上传失败 |
Slots
| 插槽 | 作用域 | 说明 |
|---|---|---|
toolbar | { editor, insert } | 业务自渲染工具栏 / 业务按钮(与 :tools 内置按钮共存) |
emoji | { insert } | 整体替换内置表情面板(默认内置 native-unicode 面板) |
Expose(ref 方法)
| 方法 | 返回值 | 说明 |
|---|---|---|
getHTML() | string | 获取内容 HTML |
getText() | string | 获取纯文本 |
getJSON() | object | 获取 Tiptap JSON |
getByChar(json, char) | { id, label, char }[] | 从 JSON 提取指定触发符('@' / '#')的提及列表 |
setContent(html) | void | 设置内容(不触发 update) |
clear() | void | 清空内容 |
focus() / blur() | void | 聚焦 / 失焦 |
insertContent(content) | void | 在光标处插入内容并聚焦 |
getEditor() | Editor | null | 取底层 Tiptap 实例 |
注意事项
- SSR 安全:编辑器本体仅客户端挂载(内部 client-only island + 占位壳),可直接放进 SSR / ISR 页面,不会崩 SSR、不强迫宿主页
ssr: false。 - v-model:内容用
v-model(HTML)或v-model:json(JSON)双向绑定;也可ref取值。 - XSS:输出的 HTML 在展示前需消毒(服务端过滤或客户端 DOMPurify),禁止直接
v-html渲染未过滤内容。 - 上传不内嵌:
upload事件把上传结果交业务处置,framework 不替你插入编辑器内容(compose 场景图 / 视频通常作独立消息发送)。 - 表情:内置精选 native-unicode 表情(跨平台外观随系统 emoji 字体);要统一外观 / 自定义图包用
#emoji插槽。 - 图标:framework 不依赖图标库,工具栏图标均内联 SVG。